/*
This file is part of Ext JS 7.0.0.168

Copyright (c) 2011-2020 Sencha Inc

Contact:  http://www.sencha.com/contact

GNU General Public License Usage
This file may be used under the terms of the GNU General Public License version 3.0 as
published by the Free Software Foundation and appearing in the file LICENSE included in the
packaging of this file.

Please review the following information to ensure the GNU General Public License version 3.0
requirements will be met: http://www.gnu.org/copyleft/gpl.html.

If you are unsure which license is appropriate for your use, please contact the sales department
at http://www.sencha.com/contact.

Version: 7.0.0.168 Build date: 2020-01-14 07:07:49 (513838d69a96c96beec9d0851e67aa660b7f288d)

*/
// @tag core
// @define Ext.Boot
var Ext = Ext || {};
//<editor-fold desc="Boot">
/**
 * @class Ext.Boot
 * @singleton
 * @private
 */
Ext.Boot = Ext.Boot || (function(emptyFn) {
    var doc = document,
        _emptyArray = [],
        _config = {
            /**
             * @cfg {Boolean} [disableCaching=true]
             * If `true` current timestamp is added to script URL's to prevent caching.
             * In debug builds, adding a "cache" or "disableCacheBuster" query parameter
             * to the page's URL will set this to `false`.
             */
            disableCaching: (/[?&](?:cache|disableCacheBuster)\b/i.test(location.search) || !(/http[s]?\:/i.test(location.href)) || /(^|[ ;])ext-cache=1/.test(doc.cookie)) ? false : true,
            /**
             * @cfg {String} [disableCachingParam="_dc"]
             * The query parameter name for the cache buster's timestamp.
             */
            disableCachingParam: '_dc',
            /**
             * @cfg {Boolean} loadDelay
             * Millisecond delay between asynchronous script injection (prevents stack
             * overflow on some user agents) 'false' disables delay but potentially
             * increases stack load.
             */
            loadDelay: false,
            /**
             * @cfg {Boolean} preserveScripts
             * `false` to remove asynchronously loaded scripts, `true` to retain script
             * element for browser debugger compatibility and improved load performance.
             */
            preserveScripts: true,
            /**
             * @cfg {String} [charset=UTF-8]
             * Optional charset to specify encoding of dynamic content.
             */
            charset: 'UTF-8'
        },
        _assetConfig = {},
        cssRe = /\.css(?:\?|$)/i,
        resolverEl = doc.createElement('a'),
        isBrowser = typeof window !== 'undefined',
        _environment = {
            browser: isBrowser,
            node: !isBrowser && (typeof require === 'function'),
            phantom: (window && (window._phantom || window.callPhantom)) || /PhantomJS/.test(window.navigator.userAgent)
        },
        _tags = (Ext.platformTags = {}),
        // All calls to _debug are commented out to speed up old browsers a bit;
        // yes that makes a difference because the cost of concatenating strings
        // and passing them into _debug() adds up pretty quickly.
        _debug = function(message) {},
        //console.log(message);
        _apply = function(object, config, defaults) {
            if (defaults) {
                _apply(object, defaults);
            }
            if (object && config && typeof config === 'object') {
                for (var i in config) {
                    object[i] = config[i];
                }
            }
            return object;
        },
        _merge = function() {
            var lowerCase = false,
                obj = Array.prototype.shift.call(arguments),
                index, i, len, value;
            if (typeof arguments[arguments.length - 1] === 'boolean') {
                lowerCase = Array.prototype.pop.call(arguments);
            }
            len = arguments.length;
            for (index = 0; index < len; index++) {
                value = arguments[index];
                if (typeof value === 'object') {
                    for (i in value) {
                        obj[lowerCase ? i.toLowerCase() : i] = value[i];
                    }
                }
            }
            return obj;
        },
        _getKeys = (typeof Object.keys == 'function') ? function(object) {
            if (!object) {
                return [];
            }
            return Object.keys(object);
        } : function(object) {
            var keys = [],
                property;
            for (property in object) {
                if (object.hasOwnProperty(property)) {
                    keys.push(property);
                }
            }
            return keys;
        },
        /*
     * The Boot loader class manages Request objects that contain one or
     * more individual urls that need to be loaded.  Requests can be performed
     * synchronously or asynchronously, but will always evaluate urls in the
     * order specified on the request object.
     */
        Boot = {
            loading: 0,
            loaded: 0,
            apply: _apply,
            env: _environment,
            config: _config,
            /**
             * @cfg {Object} assetConfig
             * A map (url->assetConfig) that contains information about assets loaded by the Microlaoder.
             */
            assetConfig: _assetConfig,
            // Keyed by absolute URL this object holds "true" if that URL is already loaded
            // or an array of callbacks to call once it loads.
            scripts: {},
            /*
                 Entry objects

                 'http://foo.com/bar/baz/Thing.js': {
                 done: true,
                 el: scriptEl || linkEl,
                 preserve: true,
                 requests: [ request1, ... ]
                 }
                 */
            /**
             * contains the current script name being loaded
             * (loadSync or sequential load only)
             */
            currentFile: null,
            suspendedQueue: [],
            currentRequest: null,
            // when loadSync is called, need to cause subsequent load requests to also be loadSync,
            // eg, when Ext.require(...) is called
            syncMode: false,
            /*
             * simple helper method for debugging
             */
            debug: _debug,
            /**
             * enables / disables loading scripts via script / link elements rather
             * than using ajax / eval
             */
            useElements: true,
            listeners: [],
            Request: Request,
            Entry: Entry,
            allowMultipleBrowsers: false,
            browserNames: {
                ie: 'IE',
                firefox: 'Firefox',
                safari: 'Safari',
                chrome: 'Chrome',
                opera: 'Opera',
                dolfin: 'Dolfin',
                edge: 'Edge',
                webosbrowser: 'webOSBrowser',
                chromeMobile: 'ChromeMobile',
                chromeiOS: 'ChromeiOS',
                silk: 'Silk',
                other: 'Other'
            },
            osNames: {
                ios: 'iOS',
                android: 'Android',
                windowsPhone: 'WindowsPhone',
                webos: 'webOS',
                blackberry: 'BlackBerry',
                rimTablet: 'RIMTablet',
                mac: 'MacOS',
                win: 'Windows',
                tizen: 'Tizen',
                linux: 'Linux',
                bada: 'Bada',
                chromeOS: 'ChromeOS',
                other: 'Other'
            },
            browserPrefixes: {
                ie: 'MSIE ',
                edge: 'Edge/',
                firefox: 'Firefox/',
                chrome: 'Chrome/',
                safari: 'Version/',
                opera: 'OPR/',
                dolfin: 'Dolfin/',
                webosbrowser: 'wOSBrowser/',
                chromeMobile: 'CrMo/',
                chromeiOS: 'CriOS/',
                silk: 'Silk/'
            },
            // When a UA reports multiple browsers this list is used to prioritize the 'real' browser
            // lower index number will win
            browserPriority: [
                'edge',
                'opera',
                'dolfin',
                'webosbrowser',
                'silk',
                'chromeiOS',
                'chromeMobile',
                'ie',
                'firefox',
                'safari',
                'chrome'
            ],
            osPrefixes: {
                tizen: '(Tizen )',
                ios: 'i(?:Pad|Phone|Pod)(?:.*)CPU(?: iPhone)? OS ',
                android: '(Android |HTC_|Silk/)',
                // Some HTC devices ship with an OSX userAgent by default,
                // so we need to add a direct check for HTC_
                windowsPhone: 'Windows Phone ',
                blackberry: '(?:BlackBerry|BB)(?:.*)Version/',
                rimTablet: 'RIM Tablet OS ',
                webos: '(?:webOS|hpwOS)/',
                bada: 'Bada/',
                chromeOS: 'CrOS '
            },
            fallbackOSPrefixes: {
                windows: 'win',
                mac: 'mac',
                linux: 'linux'
            },
            devicePrefixes: {
                iPhone: 'iPhone',
                iPod: 'iPod',
                iPad: 'iPad'
            },
            maxIEVersion: 12,
            /**
             * The default function that detects various platforms and sets tags
             * in the platform map accordingly.  Examples are iOS, android, tablet, etc.
             * @param tags the set of tags to populate
             */
            detectPlatformTags: function() {
                var me = this,
                    ua = navigator.userAgent,
                    isMobile = /Mobile(\/|\s)/.test(ua),
                    element = document.createElement('div'),
                    isEventSupported = function(name, tag) {
                        if (tag === undefined) {
                            tag = window;
                        }
                        var eventName = 'on' + name.toLowerCase(),
                            isSupported = (eventName in element);
                        if (!isSupported) {
                            if (element.setAttribute && element.removeAttribute) {
                                element.setAttribute(eventName, '');
                                isSupported = typeof element[eventName] === 'function';
                                if (typeof element[eventName] !== 'undefined') {
                                    element[eventName] = undefined;
                                }
                                element.removeAttribute(eventName);
                            }
                        }
                        return isSupported;
                    },
                    // Browser Detection
                    getBrowsers = function() {
                        var browsers = {},
                            maxIEVersion, prefix, value, key, index, len, match, version, matched;
                        // MS Edge browser (and possibly others) can report multiple browsers in the UserAgent
                        // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240"
                        // we use this to prioritize the actual browser in this situation
                        len = me.browserPriority.length;
                        for (index = 0; index < len; index++) {
                            key = me.browserPriority[index];
                            if (!matched) {
                                value = me.browserPrefixes[key];
                                match = ua.match(new RegExp('(' + value + ')([\\w\\._]+)'));
                                version = match && match.length > 1 ? parseInt(match[2]) : 0;
                                if (version) {
                                    matched = true;
                                }
                            } else {
                                version = 0;
                            }
                            browsers[key] = version;
                        }
                        //Deal with IE document mode
                        if (browsers.ie) {
                            var mode = document.documentMode;
                            if (mode >= 8) {
                                browsers.ie = mode;
                            }
                        }
                        // Fancy IE greater than and less then quick tags
                        version = browsers.ie || false;
                        maxIEVersion = Math.max(version, me.maxIEVersion);
                        for (index = 8; index <= maxIEVersion; ++index) {
                            prefix = 'ie' + index;
                            browsers[prefix + 'm'] = version ? version <= index : 0;
                            browsers[prefix] = version ? version === index : 0;
                            browsers[prefix + 'p'] = version ? version >= index : 0;
                        }
                        return browsers;
                    },
                    //OS Detection
                    getOperatingSystems = function() {
                        var systems = {},
                            value, key, keys, index, len, match, matched, version, activeCount;
                        keys = _getKeys(me.osPrefixes);
                        len = keys.length;
                        for (index = 0 , activeCount = 0; index < len; index++) {
                            key = keys[index];
                            value = me.osPrefixes[key];
                            match = ua.match(new RegExp('(' + value + ')([^\\s;]+)'));
                            matched = match ? match[1] : null;
                            // This is here because some HTC android devices show an OSX Snow Leopard userAgent by default.
                            // And the Kindle Fire doesn't have any indicator of Android as the OS in its User Agent
                            if (matched && (matched === 'HTC_' || matched === 'Silk/')) {
                                version = 2.3;
                            } else {
                                version = match && match.length > 1 ? parseFloat(match[match.length - 1]) : 0;
                            }
                            if (version) {
                                activeCount++;
                            }
                            systems[key] = version;
                        }
                        keys = _getKeys(me.fallbackOSPrefixes);
                        // If no OS could be found we resort to the fallbacks, otherwise we just
                        // falsify the fallbacks
                        len = keys.length;
                        for (index = 0; index < len; index++) {
                            key = keys[index];
                            // No OS was detected from osPrefixes
                            if (activeCount === 0) {
                                value = me.fallbackOSPrefixes[key];
                                match = ua.toLowerCase().match(new RegExp(value));
                                systems[key] = match ? true : 0;
                            } else {
                                systems[key] = 0;
                            }
                        }
                        return systems;
                    },
                    // Device Detection
                    getDevices = function() {
                        var devices = {},
                            value, key, keys, index, len, match;
                        keys = _getKeys(me.devicePrefixes);
                        len = keys.length;
                        for (index = 0; index < len; index++) {
                            key = keys[index];
                            value = me.devicePrefixes[key];
                            match = ua.match(new RegExp(value));
                            devices[key] = match ? true : 0;
                        }
                        return devices;
                    },
                    browsers = getBrowsers(),
                    systems = getOperatingSystems(),
                    devices = getDevices(),
                    platformParams = Boot.loadPlatformsParam();
                // We apply platformParams from the query here first to allow for forced user valued
                // to be used in calculation of generated tags
                _merge(_tags, browsers, systems, devices, platformParams, true);
                _tags.phone = !!((_tags.iphone || _tags.ipod) || (!_tags.silk && (_tags.android && (_tags.android < 3 || isMobile))) || (_tags.blackberry && isMobile) || (_tags.windowsphone));
                _tags.tablet = !!(!_tags.phone && (_tags.ipad || _tags.android || _tags.silk || _tags.rimtablet || (_tags.ie10 && /; Touch/.test(ua))));
                _tags.touch = // if the browser has touch events we can be reasonably sure the device has
                // a touch screen
                isEventSupported('touchend') || // browsers that use pointer event have maxTouchPoints > 0 if the
                // device supports touch input
                // http://www.w3.org/TR/pointerevents/#widl-Navigator-maxTouchPoints
                navigator.maxTouchPoints || // IE10 uses a vendor-prefixed maxTouchPoints property
                navigator.msMaxTouchPoints;
                _tags.desktop = !_tags.phone && !_tags.tablet;
                _tags.cordova = _tags.phonegap = !!(window.PhoneGap || window.Cordova || window.cordova);
                _tags.webview = /(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)(?!.*FBAN)/i.test(ua);
                _tags.androidstock = (_tags.android <= 4.3) && (_tags.safari || _tags.silk);
                // Re-apply any query params here to allow for user override of generated tags (desktop, touch, tablet, etc)
                _merge(_tags, platformParams, true);
            },
            /**
             * Extracts user supplied platform tags from the "platformTags" query parameter
             * of the form:
             *
             *      ?platformTags=name:state,name:state,...
             *
             * (each tag defaults to true when state is unspecified)
             *
             * Example:
             *
             *      ?platformTags=isTablet,isPhone:false,isDesktop:0,iOS:1,Safari:true, ...
             *
             * @returns {Object} the platform tags supplied by the query string
             */
            loadPlatformsParam: function() {
                // Check if the ?platform parameter is set in the URL
                var paramsString = window.location.search.substr(1),
                    paramsArray = paramsString.split("&"),
                    params = {},
                    i,
                    platforms = {},
                    tmpArray, tmplen, platform, name, enabled;
                for (i = 0; i < paramsArray.length; i++) {
                    tmpArray = paramsArray[i].split("=");
                    params[tmpArray[0]] = tmpArray[1];
                }
                if (params.platformTags) {
                    tmpArray = params.platformTags.split(",");
                    for (tmplen = tmpArray.length , i = 0; i < tmplen; i++) {
                        platform = tmpArray[i].split(":");
                        name = platform[0];
                        enabled = true;
                        if (platform.length > 1) {
                            enabled = platform[1];
                            if (enabled === 'false' || enabled === '0') {
                                enabled = false;
                            }
                        }
                        platforms[name] = enabled;
                    }
                }
                return platforms;
            },
            filterPlatform: function(platform, excludes) {
                platform = _emptyArray.concat(platform || _emptyArray);
                excludes = _emptyArray.concat(excludes || _emptyArray);
                var plen = platform.length,
                    elen = excludes.length,
                    include = (!plen && elen),
                    // default true if only excludes specified
                    i, tag;
                for (i = 0; i < plen && !include; i++) {
                    tag = platform[i];
                    include = !!_tags[tag];
                }
                for (i = 0; i < elen && include; i++) {
                    tag = excludes[i];
                    include = !_tags[tag];
                }
                return include;
            },
            init: function() {
                var scriptEls = doc.getElementsByTagName('script'),
                    script = scriptEls[0],
                    len = scriptEls.length,
                    re = /\/ext(\-[a-z\-]+)?\.js$/,
                    entry, src, state, baseUrl, key, n, origin;
                // No check for script definedness because there always should be at least one
                Boot.hasReadyState = ("readyState" in script);
                Boot.hasAsync = ("async" in script);
                Boot.hasDefer = ("defer" in script);
                Boot.hasOnLoad = ("onload" in script);
                // Feature detecting IE
                Boot.isIE8 = Boot.hasReadyState && !Boot.hasAsync && Boot.hasDefer && !Boot.hasOnLoad;
                Boot.isIE9 = Boot.hasReadyState && !Boot.hasAsync && Boot.hasDefer && Boot.hasOnLoad;
                Boot.isIE10p = Boot.hasReadyState && Boot.hasAsync && Boot.hasDefer && Boot.hasOnLoad;
                if (Boot.isIE8) {
                    Boot.isIE10 = false;
                    Boot.isIE10m = true;
                } else {
                    Boot.isIE10 = navigator.appVersion.indexOf('MSIE 10') !== -1;
                    Boot.isIE10m = Boot.isIE10 || Boot.isIE9 || Boot.isIE8;
                }
                // IE11 does not support conditional compilation so we detect it by exclusion
                Boot.isIE11 = Boot.isIE10p && !Boot.isIE10;
                // Since we are loading after other scripts, and we needed to gather them
                // anyway, we track them in _scripts so we don't have to ask for them all
                // repeatedly.
                for (n = 0; n < len; n++) {
                    src = (script = scriptEls[n]).src;
                    if (!src) {
                        
                        continue;
                    }
                    state = script.readyState || null;
                    // If we find a script file called "ext-*.js", then the base path is that file's base path.
                    if (!baseUrl && re.test(src)) {
                        baseUrl = src;
                    }
                    if (!Boot.scripts[key = Boot.canonicalUrl(src)]) {
                        //                         _debug("creating entry " + key + " in Boot.init");
                        entry = new Entry({
                            key: key,
                            url: src,
                            done: state === null || // non-IE
                            state === 'loaded' || state === 'complete',
                            // IE only
                            el: script,
                            prop: 'src'
                        });
                    }
                }
                if (!baseUrl) {
                    script = scriptEls[scriptEls.length - 1];
                    baseUrl = script.src;
                }
                Boot.baseUrl = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1);
                origin = window.location.origin || window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
                Boot.origin = origin;
                Boot.detectPlatformTags();
                Ext.filterPlatform = Boot.filterPlatform;
            },
            /**
             * This method returns a canonical URL for the given URL.
             *
             * For example, the following all produce the same canonical URL (which is the
             * last one):
             *
             *      http://foo.com/bar/baz/zoo/derp/../../goo/Thing.js?_dc=12345
             *      http://foo.com/bar/baz/zoo/derp/../../goo/Thing.js
             *      http://foo.com/bar/baz/zoo/derp/../jazz/../../goo/Thing.js
             *      http://foo.com/bar/baz/zoo/../goo/Thing.js
             *      http://foo.com/bar/baz/goo/Thing.js
             *
             * @private
             */
            canonicalUrl: function(url) {
                // *WARNING WARNING WARNING*
                // This method yields the most correct result we can get but it is EXPENSIVE!
                // In ALL browsers! When called multiple times in a sequence, as if when
                // we resolve dependencies for entries, it will cause garbage collection events
                // and overall painful slowness. This is why we try to avoid it as much as we can.
                // 
                // @TODO - see if we need this fallback logic
                // http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
                resolverEl.href = url;
                var ret = resolverEl.href,
                    dc = _config.disableCachingParam,
                    pos = dc ? ret.indexOf(dc + '=') : -1,
                    c, end;
                // If we have a _dc query parameter we need to remove it from the canonical
                // URL.
                if (pos > 0 && ((c = ret.charAt(pos - 1)) === '?' || c === '&')) {
                    end = ret.indexOf('&', pos);
                    end = (end < 0) ? '' : ret.substring(end);
                    if (end && c === '?') {
                        ++pos;
                        // keep the '?'
                        end = end.substring(1);
                    }
                    // remove the '&'
                    ret = ret.substring(0, pos - 1) + end;
                }
                return ret;
            },
            /**
             * Get the config value corresponding to the specified name. If no name is given, will return the config object
             * @param {String} name The config property name
             * @return {Object}
             */
            getConfig: function(name) {
                return name ? Boot.config[name] : Boot.config;
            },
            /**
             * Set the configuration.
             * @param {Object} config The config object to override the default values.
             * @return {Ext.Boot} this
             */
            setConfig: function(name, value) {
                if (typeof name === 'string') {
                    Boot.config[name] = value;
                } else {
                    for (var s in name) {
                        Boot.setConfig(s, name[s]);
                    }
                }
                return Boot;
            },
            getHead: function() {
                return Boot.docHead || (Boot.docHead = doc.head || doc.getElementsByTagName('head')[0]);
            },
            create: function(url, key, cfg) {
                var config = cfg || {};
                config.url = url;
                config.key = key;
                return Boot.scripts[key] = new Entry(config);
            },
            getEntry: function(url, cfg, canonicalPath) {
                var key, entry;
                // Canonicalizing URLs via anchor element href yields the most correct result
                // but is *extremely* resource heavy so we need to avoid it whenever possible
                key = canonicalPath ? url : Boot.canonicalUrl(url);
                entry = Boot.scripts[key];
                if (!entry) {
                    entry = Boot.create(url, key, cfg);
                    if (canonicalPath) {
                        entry.canonicalPath = true;
                    }
                }
                return entry;
            },
            registerContent: function(url, type, content) {
                var cfg = {
                        content: content,
                        loaded: true,
                        css: type === 'css'
                    };
                return Boot.getEntry(url, cfg);
            },
            processRequest: function(request, sync) {
                request.loadEntries(sync);
            },
            load: function(request) {
                //                 _debug("Boot.load called");
                var request = new Request(request);
                if (request.sync || Boot.syncMode) {
                    return Boot.loadSync(request);
                }
                // If there is a request in progress, we must
                // queue this new request to be fired  when the current request completes.
                if (Boot.currentRequest) {
                    //                     _debug("current active request, suspending this request");
                    // trigger assignment of entries now to ensure that overlapping
                    // entries with currently running requests will synchronize state
                    // with this pending one as they complete
                    request.getEntries();
                    Boot.suspendedQueue.push(request);
                } else {
                    Boot.currentRequest = request;
                    Boot.processRequest(request, false);
                }
                return Boot;
            },
            loadSync: function(request) {
                //                 _debug("Boot.loadSync called");
                var request = new Request(request);
                Boot.syncMode++;
                Boot.processRequest(request, true);
                Boot.syncMode--;
                return Boot;
            },
            loadBasePrefix: function(request) {
                request = new Request(request);
                request.prependBaseUrl = true;
                return Boot.load(request);
            },
            loadSyncBasePrefix: function(request) {
                request = new Request(request);
                request.prependBaseUrl = true;
                return Boot.loadSync(request);
            },
            requestComplete: function(request) {
                var next;
                if (Boot.currentRequest === request) {
                    Boot.currentRequest = null;
                    while (Boot.suspendedQueue.length > 0) {
                        next = Boot.suspendedQueue.shift();
                        if (!next.done) {
                            //                             _debug("resuming suspended request");
                            Boot.load(next);
                            break;
                        }
                    }
                }
                if (!Boot.currentRequest && Boot.suspendedQueue.length == 0) {
                    Boot.fireListeners();
                }
            },
            isLoading: function() {
                return !Boot.currentRequest && Boot.suspendedQueue.length == 0;
            },
            fireListeners: function() {
                var listener;
                while (Boot.isLoading() && (listener = Boot.listeners.shift())) {
                    listener();
                }
            },
            onBootReady: function(listener) {
                if (!Boot.isLoading()) {
                    listener();
                } else {
                    Boot.listeners.push(listener);
                }
            },
            /**
             * this is a helper function used by Ext.Loader to flush out
             * 'uses' arrays for classes in some Ext versions
             */
            getPathsFromIndexes: function(indexMap, loadOrder) {
                // In older versions indexMap was an object instead of a sparse array
                if (!('length' in indexMap)) {
                    var indexArray = [],
                        index;
                    for (index in indexMap) {
                        if (!isNaN(+index)) {
                            indexArray[+index] = indexMap[index];
                        }
                    }
                    indexMap = indexArray;
                }
                return Request.prototype.getPathsFromIndexes(indexMap, loadOrder);
            },
            createLoadOrderMap: function(loadOrder) {
                return Request.prototype.createLoadOrderMap(loadOrder);
            },
            fetch: function(url, complete, scope, async) {
                async = (async === undefined) ? !!complete : async;
                var xhr = new XMLHttpRequest(),
                    result, status, content,
                    exception = false,
                    readyStateChange = function() {
                        if (xhr && xhr.readyState == 4) {
                            status = (xhr.status === 1223) ? 204 : (xhr.status === 0 && ((self.location || {}).protocol === 'file:' || (self.location || {}).protocol === 'ionp:')) ? 200 : xhr.status;
                            content = xhr.responseText;
                            result = {
                                content: content,
                                status: status,
                                exception: exception
                            };
                            if (complete) {
                                complete.call(scope, result);
                            }
                            xhr.onreadystatechange = emptyFn;
                            xhr = null;
                        }
                    };
                if (async) {
                    xhr.onreadystatechange = readyStateChange;
                }
                try {
                    //                     _debug("fetching " + url + " " + (async ? "async" : "sync"));
                    xhr.open('GET', url, async);
                    xhr.send(null);
                } catch (err) {
                    exception = err;
                    readyStateChange();
                    return result;
                }
                if (!async) {
                    readyStateChange();
                }
                return result;
            },
            notifyAll: function(entry) {
                entry.notifyRequests();
            }
        };
    function Request(cfg) {
        //The request class encapsulates a series of Entry objects
        //and provides notification around the completion of all Entries
        //in this request.
        if (cfg.$isRequest) {
            return cfg;
        }
        var cfg = cfg.url ? cfg : {
                url: cfg
            },
            url = cfg.url,
            urls = url.charAt ? [
                url
            ] : url,
            charset = cfg.charset || Boot.config.charset;
        _apply(this, cfg);
        delete this.url;
        this.urls = urls;
        this.charset = charset;
    }
    
    Request.prototype = {
        $isRequest: true,
        createLoadOrderMap: function(loadOrder) {
            var len = loadOrder.length,
                loadOrderMap = {},
                i, element;
            for (i = 0; i < len; i++) {
                element = loadOrder[i];
                loadOrderMap[element.path] = element;
            }
            return loadOrderMap;
        },
        getLoadIndexes: function(item, indexMap, loadOrder, includeUses, skipLoaded) {
            var resolved = [],
                queue = [
                    item
                ],
                itemIndex = item.idx,
                queue, entry, dependencies, depIndex, i, len;
            if (indexMap[itemIndex]) {
                // prevent cycles
                return resolved;
            }
            // Both indexMap and resolved are sparse arrays keyed by indexes.
            // This gives us a naturally sorted sequence of indexes later on
            // when we need to convert them to paths.
            // indexMap is the map of all indexes we have visited at least once
            // per the current expandUrls() invocation, and resolved is the map
            // of all dependencies for the current item that are not included
            // in indexMap.
            indexMap[itemIndex] = resolved[itemIndex] = true;
            while (item = queue.shift()) {
                // Canonicalizing URLs is expensive, we try to avoid it
                if (item.canonicalPath) {
                    entry = Boot.getEntry(item.path, null, true);
                } else {
                    entry = Boot.getEntry(this.prepareUrl(item.path));
                }
                if (!(skipLoaded && entry.done)) {
                    if (includeUses && item.uses && item.uses.length) {
                        dependencies = item.requires.concat(item.uses);
                    } else {
                        dependencies = item.requires;
                    }
                    for (i = 0 , len = dependencies.length; i < len; i++) {
                        depIndex = dependencies[i];
                        if (!indexMap[depIndex]) {
                            indexMap[depIndex] = resolved[depIndex] = true;
                            queue.push(loadOrder[depIndex]);
                        }
                    }
                }
            }
            return resolved;
        },
        getPathsFromIndexes: function(indexes, loadOrder) {
            var paths = [],
                index, len;
            // indexes is a sparse array with values being true for defined indexes
            for (index = 0 , len = indexes.length; index < len; index++) {
                if (indexes[index]) {
                    paths.push(loadOrder[index].path);
                }
            }
            return paths;
        },
        expandUrl: function(url, loadOrder, loadOrderMap, indexMap, includeUses, skipLoaded) {
            var item, resolved;
            if (loadOrder) {
                item = loadOrderMap[url];
                if (item) {
                    resolved = this.getLoadIndexes(item, indexMap, loadOrder, includeUses, skipLoaded);
                    if (resolved.length) {
                        return this.getPathsFromIndexes(resolved, loadOrder);
                    }
                }
            }
            return [
                url
            ];
        },
        expandUrls: function(urls, includeUses) {
            var me = this,
                loadOrder = me.loadOrder,
                expanded = [],
                expandMap = {},
                indexMap = [],
                loadOrderMap, tmpExpanded, i, len, t, tlen, tUrl;
            if (typeof urls === "string") {
                urls = [
                    urls
                ];
            }
            if (loadOrder) {
                loadOrderMap = me.loadOrderMap;
                if (!loadOrderMap) {
                    loadOrderMap = me.loadOrderMap = me.createLoadOrderMap(loadOrder);
                }
            }
            for (i = 0 , len = urls.length; i < len; i++) {
                // We don't want to skip loaded entries (last argument === false).
                // There are some overrides that get loaded before their respective classes,
                // and when the class dependencies are processed we don't want to skip over
                // the overrides' dependencies just because they were loaded first.
                tmpExpanded = this.expandUrl(urls[i], loadOrder, loadOrderMap, indexMap, includeUses, false);
                for (t = 0 , tlen = tmpExpanded.length; t < tlen; t++) {
                    tUrl = tmpExpanded[t];
                    if (!expandMap[tUrl]) {
                        expandMap[tUrl] = true;
                        expanded.push(tUrl);
                    }
                }
            }
            if (expanded.length === 0) {
                expanded = urls;
            }
            return expanded;
        },
        expandLoadOrder: function() {
            var me = this,
                urls = me.urls,
                expanded;
            if (!me.expanded) {
                expanded = this.expandUrls(urls, true);
                me.expanded = true;
            } else {
                expanded = urls;
            }
            me.urls = expanded;
            // if we added some urls to the request to honor the indicated
            // load order, the request needs to be sequential
            if (urls.length != expanded.length) {
                me.sequential = true;
            }
            return me;
        },
        getUrls: function() {
            this.expandLoadOrder();
            return this.urls;
        },
        prepareUrl: function(url) {
            if (this.prependBaseUrl) {
                return Boot.baseUrl + url;
            }
            return url;
        },
        getEntries: function() {
            var me = this,
                entries = me.entries,
                loadOrderMap, item, i, entry, urls, url;
            if (!entries) {
                entries = [];
                urls = me.getUrls();
                // If we have loadOrder array then the map will be expanded by now
                if (me.loadOrder) {
                    loadOrderMap = me.loadOrderMap;
                }
                for (i = 0; i < urls.length; i++) {
                    url = me.prepareUrl(urls[i]);
                    if (loadOrderMap) {
                        item = loadOrderMap[url];
                    }
                    entry = Boot.getEntry(url, {
                        buster: me.buster,
                        charset: me.charset
                    }, item && item.canonicalPath);
                    entry.requests.push(me);
                    entries.push(entry);
                }
                me.entries = entries;
            }
            return entries;
        },
        loadEntries: function(sync) {
            var me = this,
                entries = me.getEntries(),
                len = entries.length,
                start = me.loadStart || 0,
                continueLoad, entries, entry, i;
            if (sync !== undefined) {
                me.sync = sync;
            }
            me.loaded = me.loaded || 0;
            me.loading = me.loading || len;
            for (i = start; i < len; i++) {
                entry = entries[i];
                if (!entry.loaded) {
                    continueLoad = entries[i].load(me.sync);
                } else {
                    continueLoad = true;
                }
                if (!continueLoad) {
                    me.loadStart = i;
                    entry.onDone(function() {
                        me.loadEntries(sync);
                    });
                    break;
                }
            }
            me.processLoadedEntries();
        },
        processLoadedEntries: function() {
            var me = this,
                entries = me.getEntries(),
                len = entries.length,
                start = me.startIndex || 0,
                i, entry;
            if (!me.done) {
                for (i = start; i < len; i++) {
                    entry = entries[i];
                    if (!entry.loaded) {
                        me.startIndex = i;
                        return;
                    }
                    if (!entry.evaluated) {
                        entry.evaluate();
                    }
                    if (entry.error) {
                        me.error = true;
                    }
                }
                me.notify();
            }
        },
        notify: function() {
            var me = this;
            if (!me.done) {
                var error = me.error,
                    fn = me[error ? 'failure' : 'success'],
                    delay = ('delay' in me) ? me.delay : (error ? 1 : Boot.config.chainDelay),
                    scope = me.scope || me;
                me.done = true;
                if (fn) {
                    if (delay === 0 || delay > 0) {
                        // Free the stack (and defer the next script)
                        setTimeout(function() {
                            fn.call(scope, me);
                        }, delay);
                    } else {
                        fn.call(scope, me);
                    }
                }
                me.fireListeners();
                Boot.requestComplete(me);
            }
        },
        onDone: function(listener) {
            var me = this,
                listeners = me.listeners || (me.listeners = []);
            if (me.done) {
                listener(me);
            } else {
                listeners.push(listener);
            }
        },
        fireListeners: function() {
            var listeners = this.listeners,
                listener;
            if (listeners) {
                //                 _debug("firing request listeners");
                while ((listener = listeners.shift())) {
                    listener(this);
                }
            }
        }
    };
    function Entry(cfg) {
        //The Entry class is a token to manage the load and evaluation
        //state of a particular url.  It is used to notify all Requests
        //interested in this url that the content is available.
        if (cfg.$isEntry) {
            return cfg;
        }
        //         _debug("creating entry for " + cfg.url);
        var charset = cfg.charset || Boot.config.charset,
            manifest = Ext.manifest,
            loader = manifest && manifest.loader,
            cache = (cfg.cache !== undefined) ? cfg.cache : (loader && loader.cache),
            buster, busterParam;
        if (Boot.config.disableCaching) {
            if (cache === undefined) {
                cache = !Boot.config.disableCaching;
            }
            if (cache === false) {
                buster = +new Date();
            } else if (cache !== true) {
                buster = cache;
            }
            if (buster) {
                busterParam = (loader && loader.cacheParam) || Boot.config.disableCachingParam;
                buster = busterParam + "=" + buster;
            }
        }
        _apply(this, cfg);
        this.charset = charset;
        this.buster = buster;
        this.requests = [];
    }
    
    Entry.prototype = {
        $isEntry: true,
        done: false,
        evaluated: false,
        loaded: false,
        isCrossDomain: function() {
            var me = this;
            if (me.crossDomain === undefined) {
                //                 _debug("checking " + me.getLoadUrl() + " for prefix " + Boot.origin);
                me.crossDomain = (me.getLoadUrl().indexOf(Boot.origin) !== 0);
            }
            return me.crossDomain;
        },
        isCss: function() {
            var me = this;
            if (me.css === undefined) {
                if (me.url) {
                    var assetConfig = Boot.assetConfig[me.url];
                    me.css = assetConfig ? assetConfig.type === "css" : cssRe.test(me.url);
                } else {
                    me.css = false;
                }
            }
            return this.css;
        },
        getElement: function(tag) {
            var me = this,
                el = me.el;
            if (!el) {
                //                 _debug("creating element for " + me.url);
                if (me.isCss()) {
                    tag = tag || "link";
                    el = doc.createElement(tag);
                    if (tag == "link") {
                        el.rel = 'stylesheet';
                        me.prop = 'href';
                    } else {
                        me.prop = "textContent";
                    }
                    el.type = "text/css";
                } else {
                    tag = tag || "script";
                    el = doc.createElement(tag);
                    el.type = 'text/javascript';
                    me.prop = 'src';
                    if (me.charset) {
                        el.charset = me.charset;
                    }
                    if (Boot.hasAsync) {
                        el.async = false;
                    }
                }
                me.el = el;
            }
            return el;
        },
        getLoadUrl: function() {
            var me = this,
                url;
            url = me.canonicalPath ? me.url : Boot.canonicalUrl(me.url);
            if (!me.loadUrl) {
                me.loadUrl = !!me.buster ? (url + (url.indexOf('?') === -1 ? '?' : '&') + me.buster) : url;
            }
            return me.loadUrl;
        },
        fetch: function(req) {
            var url = this.getLoadUrl(),
                async = !!req.async,
                complete = req.complete;
            Boot.fetch(url, complete, this, async);
        },
        onContentLoaded: function(response) {
            var me = this,
                status = response.status,
                content = response.content,
                exception = response.exception,
                url = this.getLoadUrl();
            me.loaded = true;
            if ((exception || status === 0) && !_environment.phantom) {
                me.error = ("Failed loading synchronously via XHR: '" + url + "'. It's likely that the file is either being loaded from a " + "different domain or from the local file system where cross " + "origin requests are not allowed for security reasons. Try " + "asynchronous loading instead.") || true;
                me.evaluated = true;
            } else if ((status >= 200 && status < 300) || status === 304 || _environment.phantom || (status === 0 && content.length > 0)) {
                me.content = content;
            } else {
                me.error = ("Failed loading synchronously via XHR: '" + url + "'. Please verify that the file exists. XHR status code: " + status) || true;
                me.evaluated = true;
            }
        },
        createLoadElement: function(callback) {
            var me = this,
                el = me.getElement();
            me.preserve = true;
            el.onerror = function() {
                me.error = true;
                if (callback) {
                    callback();
                    callback = null;
                }
            };
            if (Boot.isIE10m) {
                el.onreadystatechange = function() {
                    if (this.readyState === 'loaded' || this.readyState === 'complete') {
                        if (callback) {
                            callback();
                            callback = this.onreadystatechange = this.onerror = null;
                        }
                    }
                };
            } else {
                el.onload = function() {
                    callback();
                    callback = this.onload = this.onerror = null;
                };
            }
            // IE starts loading here
            el[me.prop] = me.getLoadUrl();
        },
        onLoadElementReady: function() {
            Boot.getHead().appendChild(this.getElement());
            this.evaluated = true;
        },
        inject: function(content, asset) {
            //             _debug("injecting content for " + this.url);
            var me = this,
                head = Boot.getHead(),
                url = me.url,
                key = me.key,
                base, el, ieMode, basePath;
            if (me.isCss()) {
                me.preserve = true;
                basePath = key.substring(0, key.lastIndexOf("/") + 1);
                base = doc.createElement('base');
                base.href = basePath;
                if (head.firstChild) {
                    head.insertBefore(base, head.firstChild);
                } else {
                    head.appendChild(base);
                }
                // reset the href attribute to cuase IE to pick up the change
                base.href = base.href;
                if (url) {
                    content += "\n/*# sourceURL=" + key + " */";
                }
                // create element after setting base
                el = me.getElement("style");
                ieMode = ('styleSheet' in el);
                head.appendChild(base);
                if (ieMode) {
                    head.appendChild(el);
                    el.styleSheet.cssText = content;
                } else {
                    el.textContent = content;
                    head.appendChild(el);
                }
                head.removeChild(base);
            } else {
                // Debugger friendly, file names are still shown even though they're
                // eval'ed code. Breakpoints work on both Firebug and Chrome's Web
                // Inspector.
                if (url) {
                    content += "\n//# sourceURL=" + key;
                }
                Ext.globalEval(content);
            }
            return me;
        },
        loadCrossDomain: function() {
            var me = this,
                complete = function() {
                    me.el.onerror = me.el.onload = emptyFn;
                    me.el = null;
                    me.loaded = me.evaluated = me.done = true;
                    me.notifyRequests();
                };
            me.createLoadElement(function() {
                complete();
            });
            me.evaluateLoadElement();
            // at this point, we need sequential evaluation,
            // which means we can't advance the load until
            // this entry has fully completed
            return false;
        },
        loadElement: function() {
            var me = this,
                complete = function() {
                    me.el.onerror = me.el.onload = emptyFn;
                    me.el = null;
                    me.loaded = me.evaluated = me.done = true;
                    me.notifyRequests();
                };
            me.createLoadElement(function() {
                complete();
            });
            me.evaluateLoadElement();
            return true;
        },
        loadSync: function() {
            var me = this;
            me.fetch({
                async: false,
                complete: function(response) {
                    me.onContentLoaded(response);
                }
            });
            me.evaluate();
            me.notifyRequests();
        },
        load: function(sync) {
            var me = this;
            if (!me.loaded) {
                if (me.loading) {
                    // if we're calling back through load and we're loading but haven't
                    // yet loaded, then we should be in a sequential, cross domain
                    // load scenario which means we can't continue the load on the
                    // request until this entry has fully evaluated, which will mean
                    // loaded = evaluated = done = true in one step.  For css files, this
                    // will happen immediately upon <link> element creation / insertion,
                    // but <script> elements will set this upon load notification
                    return false;
                }
                me.loading = true;
                // for async modes, we have some options
                if (!sync) {
                    // if cross domain, just inject the script tag and let the onload
                    // events drive the progression.
                    // IE10 also needs sequential loading because of a bug that makes it
                    // fire readystate event prematurely:
                    // https://connect.microsoft.com/IE/feedback/details/729164/ie10-dynamic-script-element-fires-loaded-readystate-prematurely
                    if (Boot.isIE10 || me.isCrossDomain()) {
                        return me.loadCrossDomain();
                    }
                    // for IE, use the readyStateChange allows us to load scripts in parallel
                    // but serialize the evaluation by appending the script node to the
                    // document
                    else if (!me.isCss() && Boot.hasReadyState) {
                        me.createLoadElement(function() {
                            me.loaded = true;
                            me.notifyRequests();
                        });
                    } else if (Boot.useElements && // older webkit, phantomjs included, won't fire load for link elements
                    !(me.isCss() && _environment.phantom)) {
                        return me.loadElement();
                    } else // for other browsers, just ajax the content down in parallel, and use
                    // globalEval to serialize evaluation
                    {
                        me.fetch({
                            async: !sync,
                            complete: function(response) {
                                me.onContentLoaded(response);
                                me.notifyRequests();
                            }
                        });
                    }
                } else // for sync mode in js, global eval FTW.  IE won't honor the comment
                // paths in the debugger, so eventually we need a sync mode for IE that
                // uses the readyStateChange mechanism
                {
                    me.loadSync();
                }
            }
            // signal that the load process can continue
            return true;
        },
        evaluateContent: function() {
            this.inject(this.content);
            this.content = null;
        },
        evaluateLoadElement: function() {
            Boot.getHead().appendChild(this.getElement());
        },
        evaluate: function() {
            var me = this;
            if (!me.evaluated) {
                if (me.evaluating) {
                    return;
                }
                me.evaluating = true;
                if (me.content !== undefined) {
                    me.evaluateContent();
                } else if (!me.error) {
                    me.evaluateLoadElement();
                }
                me.evaluated = me.done = true;
                me.cleanup();
            }
        },
        cleanup: function() {
            var me = this,
                el = me.el,
                prop;
            if (!el) {
                return;
            }
            if (!me.preserve) {
                me.el = null;
                el.parentNode.removeChild(el);
                // Remove, since its useless now
                for (prop in el) {
                    try {
                        if (prop !== me.prop) {
                            // If we set the src property to null IE
                            // will try and request a script at './null'
                            el[prop] = null;
                        }
                        delete el[prop];
                    } // and prepare for GC
                    catch (cleanEx) {}
                }
            }
            //ignore
            // Setting to null can cause exceptions if IE ever needs to call these
            // again (like onreadystatechange). This emptyFn has nothing locked in
            // closure scope so it is about as safe as null for memory leaks.
            el.onload = el.onerror = el.onreadystatechange = emptyFn;
        },
        notifyRequests: function() {
            var requests = this.requests,
                len = requests.length,
                i, request;
            for (i = 0; i < len; i++) {
                request = requests[i];
                request.processLoadedEntries();
            }
            if (this.done) {
                this.fireListeners();
            }
        },
        onDone: function(listener) {
            var me = this,
                listeners = me.listeners || (me.listeners = []);
            if (me.done) {
                listener(me);
            } else {
                listeners.push(listener);
            }
        },
        fireListeners: function() {
            var listeners = this.listeners,
                listener;
            if (listeners && listeners.length > 0) {
                //                 _debug("firing event listeners for url " + this.url);
                while ((listener = listeners.shift())) {
                    listener(this);
                }
            }
        }
    };
    /**
     * Turns on or off the "cache buster" applied to dynamically loaded scripts. Normally
     * dynamically loaded scripts have an extra query parameter appended to avoid stale
     * cached scripts. This method can be used to disable this mechanism, and is primarily
     * useful for testing. This is done using a cookie.
     * @param {Boolean} disable True to disable the cache buster.
     * @param {String} [path="/"] An optional path to scope the cookie.
     */
    Ext.disableCacheBuster = function(disable, path) {
        var date = new Date();
        date.setTime(date.getTime() + (disable ? 10 * 365 : -1) * 24 * 60 * 60 * 1000);
        date = date.toGMTString();
        doc.cookie = 'ext-cache=1; expires=' + date + '; path=' + (path || '/');
    };
    Boot.init();
    return Boot;
}(// NOTE: We run the eval at global scope to protect the body of the function and allow
// compressors to still process it.
function() {}));
//(eval("/*@cc_on!@*/!1"));
/**
 * This method evaluates the given code free of any local variable. This
 * will be at global scope, in others it will be in a function.
 * @param {String} code The code to evaluate.
 * @private
 * @method
 * @member Ext
 */
Ext.globalEval = Ext.globalEval || (this.execScript ? function(code) {
    execScript(code);
} : function($$code) {
    eval.call(window, $$code);
});
/*
 * Only IE8 & IE/Quirks lack Function.prototype.bind so we polyfill that here.
 */
if (!Function.prototype.bind) {
    (function() {
        var slice = Array.prototype.slice,
            // To reduce overhead on call of the bound fn we have two flavors based on
            // whether we have args to prepend or not:
            bind = function(me) {
                var args = slice.call(arguments, 1),
                    method = this;
                if (args.length) {
                    return function() {
                        var t = arguments;
                        // avoid the slice/concat if the caller does not supply args
                        return method.apply(me, t.length ? args.concat(slice.call(t)) : args);
                    };
                }
                // this is the majority use case - just fn.bind(this) and no args
                args = null;
                return function() {
                    return method.apply(me, arguments);
                };
            };
        Function.prototype.bind = bind;
        bind.$extjs = true;
    }());
}
// to detect this polyfill if one want to improve it
//</editor-fold>
Ext.setResourcePath = function(poolName, path) {
    var manifest = Ext.manifest || (Ext.manifest = {}),
        paths = manifest.resources || (manifest.resources = {});
    if (manifest) {
        if (typeof poolName !== 'string') {
            Ext.apply(paths, poolName);
        } else {
            paths[poolName] = path;
        }
        manifest.resources = paths;
    }
};
Ext.getResourcePath = function(path, poolName, packageName) {
    if (typeof path !== 'string') {
        poolName = path.pool;
        packageName = path.packageName;
        path = path.path;
    }
    var manifest = Ext.manifest,
        paths = manifest && manifest.resources,
        poolPath = paths[poolName],
        output = [];
    if (poolPath == null) {
        poolPath = paths.path;
        if (poolPath == null) {
            poolPath = 'resources';
        }
    }
    if (poolPath) {
        output.push(poolPath);
    }
    if (packageName) {
        output.push(packageName);
    }
    output.push(path);
    return output.join('/');
};

// @tag core
/**
 * @class Ext
 *
 * The Ext namespace (global object) encapsulates all classes, singletons, and
 * utility methods provided by Sencha's libraries.
 *
 * Most user interface Components are at a lower level of nesting in the namespace,
 * but many common utility functions are provided as direct properties of the Ext namespace.
 *
 * Also many frequently used methods from other classes are provided as shortcuts
 * within the Ext namespace. For example {@link Ext#getCmp Ext.getCmp} aliases
 * {@link Ext.ComponentManager#get Ext.ComponentManager.get}.
 *
 * Many applications are initiated with {@link Ext#application Ext.application} which is
 * called once the DOM is ready. This ensures all scripts have been loaded, preventing
 * dependency issues. For example:
 *
 *      Ext.application({
 *          name: 'MyApp',
 *
 *          launch: function () {
 *              Ext.Msg.alert(this.getName(), 'Ready to go!');
 *          }
 *      });
 *
 * <b><a href="http://www.sencha.com/products/sencha-cmd/">Sencha Cmd</a></b> is a free tool
 * for helping you generate and build Ext JS (and Sencha Touch) applications. See
 * `{@link Ext.app.Application Application}` for more information about creating an app.
 *
 * A lower-level technique that does not use the `Ext.app.Application` architecture is
 * {@link Ext#onReady Ext.onReady}.
 *
 * You can also discuss concepts and issues with others on the
 * <a href="http://www.sencha.com/forum/">Sencha Forums</a>.
 *
 * @singleton
 */
var Ext = Ext || {};
// @define Ext
/* eslint indent: "off" */
(function() {
    var global = this,
        objectPrototype = Object.prototype,
        toString = objectPrototype.toString,
        enumerables = [
            // 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable',
            'valueOf',
            'toLocaleString',
            'toString',
            'constructor'
        ],
        emptyFn = Ext.fireIdle = function() {},
        // see GlobalEvents for true fireIdle
        privateFn = function() {},
        identityFn = function(o) {
            return o;
        },
        // eslint-disable-line
        // This is the "$previous" method of a hook function on an instance. When called, it
        // calls through the class prototype by the name of the called method.
        callOverrideParent = function() {
            var method = callOverrideParent.caller.caller;
            // skip callParent (our caller)
            return method.$owner.prototype[method.$name].apply(this, arguments);
        },
        manifest = Ext.manifest || {},
        iterableRe = /\[object\s*(?:Array|Arguments|\w*Collection|\w*List|HTML\s+document\.all\s+class)\]/,
        MSDateRe = /^\\?\/Date\(([-+])?(\d+)(?:[+-]\d{4})?\)\\?\/$/,
        /* eslint-disable-next-line no-unused-vars */
        elevateArgs, elevateFn, elevateRet, elevateScope, i;
    Ext.global = global;
    Ext.$nextIid = 0;
    /**
     * Returns the current timestamp.
     * @return {Number} Milliseconds since UNIX epoch.
     * @method now
     * @member Ext
     */
    Ext.now = Date.now || (Date.now = function() {
        return +new Date();
    });
    /**
     * Returns the current high-resolution timestamp.
     * @return {Number} Milliseconds ellapsed since arbitrary epoch.
     * @method ticks
     * @member Ext
     * @since 6.0.1
     */
    Ext.ticks = (global.performance && global.performance.now) ? function() {
        return performance.now();
    } : Ext.now;
    Ext._startTime = Ext.ticks();
    // Mark these special fn's for easy identification:
    emptyFn.$nullFn = identityFn.$nullFn = emptyFn.$emptyFn = identityFn.$identityFn = privateFn.$nullFn = true;
    privateFn.$privacy = 'framework';
    // We also want to prevent these functions from being cleaned up on destroy
    emptyFn.$noClearOnDestroy = identityFn.$noClearOnDestroy = true;
    privateFn.$noClearOnDestroy = true;
    // These are emptyFn's in core and are redefined only in Ext JS (we use this syntax
    // so Cmd does not detect them):
    /* eslint-disable-next-line dot-notation */
    Ext['suspendLayouts'] = Ext['resumeLayouts'] = emptyFn;
    for (i in {
        toString: 1
    }) {
        enumerables = null;
    }
    /**
     * An array containing extra enumerables for old browsers
     * @property {String[]}
     */
    Ext.enumerables = enumerables;
    /**
     * Copies all the properties of `config` to the specified `object`. There are two levels
     * of defaulting supported:
     * 
     *      Ext.apply(obj, { a: 1 }, { a: 2 });
     *      //obj.a === 1
     * 
     *      Ext.apply(obj, {  }, { a: 2 });
     *      //obj.a === 2
     * 
     * Note that if recursive merging and cloning without referencing the original objects
     * or arrays is needed, use {@link Ext.Object#merge} instead.
     * 
     * @param {Object} object The receiver of the properties.
     * @param {Object} config The primary source of the properties.
     * @param {Object} [defaults] An object that will also be applied for default values.
     * @return {Object} returns `object`.
     */
    Ext.apply = function(object, config, defaults) {
        var i, j, k;
        if (object) {
            if (defaults) {
                Ext.apply(object, defaults);
            }
            if (config && typeof config === 'object') {
                for (i in config) {
                    object[i] = config[i];
                }
                if (enumerables) {
                    for (j = enumerables.length; j--; ) {
                        k = enumerables[j];
                        if (config.hasOwnProperty(k)) {
                            object[k] = config[k];
                        }
                    }
                }
            }
        }
        return object;
    };
    // Used by Ext.override
    function addInstanceOverrides(target, owner, overrides) {
        var name, value;
        for (name in overrides) {
            if (overrides.hasOwnProperty(name)) {
                value = overrides[name];
                if (typeof value === 'function') {
                    if (owner.$className) {
                        value.name = owner.$className + '#' + name;
                    }
                    value.$name = name;
                    value.$owner = owner;
                    value.$previous = target.hasOwnProperty(name) ? target[name] : // already hooked, so call previous hook
                    callOverrideParent;
                }
                // calls by name on prototype
                target[name] = value;
            }
        }
    }
    Ext.buildSettings = Ext.apply({
        baseCSSPrefix: 'x-'
    }, Ext.buildSettings || {});
    Ext.apply(Ext, {
        /**
         * @private
         */
        idSeed: 0,
        /**
         * @private
         */
        idPrefix: 'ext-',
        /**
         * @private
         */
        isRobot: false,
        /**
         * @property {Boolean} isSecure
         * True if the page is running over SSL
         * @readonly
         */
        isSecure: /^https/i.test(window.location.protocol),
        /**
         * `true` to automatically uncache orphaned Ext.Elements periodically. If set to
         * `false`, the application will be required to clean up orphaned Ext.Elements and
         * it's listeners as to not cause memory leakage.
         */
        enableGarbageCollector: false,
        /**
         * True to automatically purge event listeners during garbageCollection.
         */
        enableListenerCollection: true,
        /**
         * @property {String} [name='Ext']
         * The name of the property in the global namespace (The `window` in browser
         * environments) which refers to the current instance of Ext.
         * This is usually `"Ext"`, but if a sandboxed build of ExtJS is being used, this will be
         * an alternative name.
         * If code is being generated for use by `eval` or to create a `new Function`, and the
         * global instance of Ext must be referenced, this is the name that should be built
         * into the code.
         */
        name: Ext.sandboxName || 'Ext',
        /**
         * @property {Function}
         * A reusable empty function for use as `privates` members.
         *
         *      Ext.define('MyClass', {
         *          nothing: Ext.emptyFn,
         *
         *          privates: {
         *              privateNothing: Ext.privateFn
         *          }
         *      });
         *
         */
        privateFn: privateFn,
        /**
         * @property {Function}
         * A reusable empty function.
         */
        emptyFn: emptyFn,
        /**
         * @property {Function}
         * A reusable identity function that simply returns its first argument.
         */
        identityFn: identityFn,
        /**
         * This indicate the start timestamp of current cycle.
         * It is only reliable during dom-event-initiated cycles and
         * {@link Ext.draw.Animator} initiated cycles.
         */
        frameStartTime: Ext.now(),
        /**
         * This object is initialized prior to loading the framework
         * and contains settings and other information describing the application.
         *
         * For applications built using Sencha Cmd, this is produced from the `"app.json"`
         * file with information extracted from all of the required packages' `"package.json"`
         * files. This can be set to a string when your application is using the
         * (microloader)[#/guide/microloader]. In this case, the string of "foo" will be
         * requested as `"foo.json"` and the object in that JSON file will parsed and set
         * as this object.
         *
         * @cfg {String/Ext.Manifest} manifest
         * @since 5.0.0
         */
        manifest: manifest,
        /**
         * @cfg {Object} [debugConfig]
         * This object is used to enable or disable debugging for classes or namespaces. The
         * default instance looks like this:
         *
         *      Ext.debugConfig = {
         *          hooks: {
         *              '*': true
         *          }
         *      };
         *
         * Typically applications will set this in their `"app.json"` like so:
         *
         *      {
         *          "debug": {
         *              "hooks": {
         *                  // Default for all namespaces:
         *                  '*': true,
         *
         *                  // Except for Ext namespace which is disabled
         *                  'Ext': false,
         *
         *                  // Except for Ext.layout namespace which is enabled
         *                  'Ext.layout': true
         *              }
         *          }
         *      }
         *
         * Alternatively, because this property is consumed very early in the load process of
         * the framework, this can be set in a `script` tag that is defined prior to loading
         * the framework itself.
         *
         * For example, to enable debugging for the `Ext.layout` namespace only:
         *
         *      var Ext = Ext || {};
         *      Ext.debugConfig = {
         *          hooks: {
         *              //...
         *          }
         *      };
         *
         * For any class declared, the longest matching namespace specified determines if its
         * `debugHooks` will be enabled. The default setting is specified by the '*' property.
         *
         * **NOTE:** This option only applies to debug builds. All debugging is disabled in
         * production builds.
         */
        debugConfig: Ext.debugConfig || manifest.debug || {
            hooks: {
                '*': true
            }
        },
        /**
         * @property {Boolean} [enableAria=true] This property is provided for backward
         * compatibility with previous versions of Ext JS. Accessibility is always enabled
         * in Ext JS 6.0+.
         *
         * This property is deprecated. To disable WAI-ARIA compatibility warnings,
         * override `Ext.ariaWarn` function in your application startup code:
         *
         *      Ext.application({
         *          launch: function() {
         *              Ext.ariaWarn = Ext.emptyFn;
         *          }
         *      });
         *
         * For stricter compatibility with WAI-ARIA requirements, replace `Ext.ariaWarn`
         * with a function that will raise an error instead:
         *
         *      Ext.application({
         *          launch: function() {
         *              Ext.ariaWarn = function(target, msg) {
         *                  Ext.raise({
         *                      msg: msg,
         *                      component: target
         *                  });
         *              };
         *          }
         *      });
         *
         * @since 6.0.0
         * @deprecated 6.0.2 This property is no longer necessary, so no replacement is required.
         */
        enableAria: true,
        startsWithHashRe: /^#/,
        /**
         * @property {RegExp}
         * @private
         * Regular expression used for validating identifiers.
         */
        validIdRe: /^[a-z_][a-z0-9\-_]*$/i,
        /**
         * @property {String} BLANK_IMAGE_URL
         * URL to a 1x1 transparent gif image used by Ext to create inline icons with
         * CSS background images.
         */
        /* eslint-disable-next-line max-len */
        BLANK_IMAGE_URL: 'data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==',
        /**
         * Converts an id (`'foo'`) into an id selector (`'#foo'`).  This method is used
         * internally by the framework whenever an id needs to be converted into a selector
         * and is provided as a hook for those that need to escape IDs selectors since,
         * as of Ext 5.0, the framework no longer escapes IDs by default.
         * @private
         * @param {String} id
         * @return {String}
         */
        makeIdSelector: function(id) {
            if (!Ext.validIdRe.test(id)) {
                Ext.raise('Invalid id selector: "' + id + '"');
            }
            return '#' + id;
        },
        /**
         * Generates unique ids. If the object/element is passes and it already has an `id`, it is
         * unchanged.
         * @param {Object} [o] The object to generate an id for.
         * @param {String} [prefix=ext-gen] (optional) The `id` prefix.
         * @return {String} The generated `id`.
         */
        id: function(o, prefix) {
            if (o && o.id) {
                return o.id;
            }
            /* eslint-disable-next-line vars-on-top */
            var id = (prefix || Ext.idPrefix) + (++Ext.idSeed);
            if (o) {
                o.id = id;
            }
            return id;
        },
        /**
         * A reusable function which returns the value of `getId()` called upon a single passed
         * parameter. Useful when creating a {@link Ext.util.MixedCollection} of objects keyed
         * by an identifier returned from a `getId` method.
         */
        returnId: function(o) {
            return o.getId();
        },
        /**
         * A reusable function which returns `true`.
         */
        returnTrue: function() {
            return true;
        },
        /**
         * A zero length string which will pass a truth test. Useful for passing to methods
         * which use a truth test to reject <i>falsy</i> values where a string value must be
         * cleared.
         */
        emptyString: new String(),
        /**
         * An immutable empty array if Object.freeze is supported by the browser
         * @since 6.5.0
         * @private
         */
        emptyArray: Object.freeze ? Object.freeze([]) : [],
        /**
         * @property {String} [baseCSSPrefix='x-']
         * The base prefix to use for all `Ext` components. To configure this property, you should
         * use the Ext.buildSettings object before the framework is loaded:
         *
         *     Ext.buildSettings = {
         *         baseCSSPrefix : 'abc-'
         *     };
         *
         * or you can change it before any components are rendered:
         *
         *     Ext.baseCSSPrefix = Ext.buildSettings.baseCSSPrefix = 'abc-';
         *
         * This will change what CSS classes components will use and you should
         * then recompile the SASS changing the `$prefix` SASS variable to match.
         */
        baseCSSPrefix: Ext.buildSettings.baseCSSPrefix,
        /**
         * @property {Object} $eventNameMap
         * A map of event names which contained the lower-cased versions of any mixed
         * case event names.
         * @private
         */
        $eventNameMap: {},
        // Vendor-specific events do not work if lower-cased.  This regex specifies event
        // prefixes for names that should NOT be lower-cased by Ext.canonicalEventName()
        $vendorEventRe: /^(DOMMouse|Moz.+|MS.+|webkit.+)/,
        // TODO: inlinable function - SDKTOOLS-686
        /**
         * @private
         */
        canonicalEventName: function(name) {
            return Ext.$eventNameMap[name] || (Ext.$eventNameMap[name] = (Ext.$vendorEventRe.test(name) ? name : name.toLowerCase()));
        },
        /**
         * Copies all the properties of config to object if they don't already exist.
         * @param {Object} object The receiver of the properties
         * @param {Object} config The source of the properties
         * @return {Object} returns obj
         */
        applyIf: function(object, config) {
            var property;
            if (object && config && typeof config === 'object') {
                for (property in config) {
                    if (object[property] === undefined) {
                        object[property] = config[property];
                    }
                }
            }
            return object;
        },
        /**
         * Destroys all of the given objects. If arrays are passed, the elements of these
         * are destroyed recursively.
         *
         * What it means to "destroy" an object depends on the type of object.
         *
         *  * `Array`: Each element of the array is destroyed recursively.
         *  * `Object`: Any object with a `destroy` method will have that method called.
         *
         * @param {Mixed...} args Any number of objects or arrays.
         */
        destroy: function() {
            var ln = arguments.length,
                i, arg;
            for (i = 0; i < ln; i++) {
                arg = arguments[i];
                if (arg) {
                    if (Ext.isArray(arg)) {
                        this.destroy.apply(this, arg);
                    } else if (Ext.isFunction(arg.destroy) && !arg.destroyed) {
                        arg.destroy();
                    }
                }
            }
            return null;
        },
        /**
         * Destroys the specified named members of the given object using `Ext.destroy`. These
         * properties will be set to `null`.
         * @param {Object} object The object who's properties you wish to destroy.
         * @param {String...} args One or more names of the properties to destroy and remove from
         * the object.
         */
        destroyMembers: function(object) {
            /* eslint-disable-next-line vars-on-top */
            for (var ref, name,
                i = 1,
                a = arguments,
                len = a.length; i < len; i++) {
                ref = object[name = a[i]];
                // Avoid adding the property if it does not already exist
                if (ref != null) {
                    object[name] = Ext.destroy(ref);
                }
            }
        },
        /**
         * Overrides members of the specified `target` with the given values.
         *
         * If the `target` is a class declared using {@link Ext#define Ext.define}, the
         * `override` method of that class is called (see {@link Ext.Base#override}) given
         * the `overrides`.
         *
         * If the `target` is a function, it is assumed to be a constructor and the contents
         * of `overrides` are applied to its `prototype` using {@link Ext#apply Ext.apply}.
         *
         * If the `target` is an instance of a class declared using {@link Ext#define Ext.define},
         * the `overrides` are applied to only that instance. In this case, methods are
         * specially processed to allow them to use {@link Ext.Base#method!callParent}.
         *
         *      var panel = new Ext.Panel({ ... });
         *
         *      Ext.override(panel, {
         *          initComponent: function () {
         *              // extra processing...
         *
         *              this.callParent();
         *          }
         *      });
         *
         * If the `target` is none of these, the `overrides` are applied to the `target`
         * using {@link Ext#apply Ext.apply}.
         *
         * Please refer to {@link Ext#define Ext.define} and {@link Ext.Base#override} for
         * further details.
         *
         * @param {Object} target The target to override.
         * @param {Object} overrides The properties to add or replace on `target`.
         * @method override
         */
        override: function(target, overrides) {
            if (target.$isClass) {
                target.override(overrides);
            } else if (typeof target === 'function') {
                Ext.apply(target.prototype, overrides);
            } else {
                /* eslint-disable-next-line vars-on-top */
                var owner = target.self,
                    privates;
                if (owner && owner.$isClass) {
                    // if (instance of Ext.define'd class)
                    privates = overrides.privates;
                    if (privates) {
                        overrides = Ext.apply({}, overrides);
                        delete overrides.privates;
                        addInstanceOverrides(target, owner, privates);
                    }
                    addInstanceOverrides(target, owner, overrides);
                } else {
                    Ext.apply(target, overrides);
                }
            }
            return target;
        },
        /**
         * Returns the given value itself if it's not empty, as described in {@link Ext#isEmpty};
         * returns the default value (second argument) otherwise.
         *
         * @param {Object} value The value to test.
         * @param {Object} defaultValue The value to return if the original value is empty.
         * @param {Boolean} [allowBlank=false] `true` to allow zero length strings to qualify
         * as non-empty.
         * @return {Object} value, if non-empty, else defaultValue.
         */
        valueFrom: function(value, defaultValue, allowBlank) {
            return Ext.isEmpty(value, allowBlank) ? defaultValue : value;
        },
        /**
         * Returns true if the passed value is empty, false otherwise. The value is deemed to be
         * empty if it is either:
         *
         * - `null`
         * - `undefined`
         * - a zero-length array
         * - a zero-length string (Unless the `allowEmptyString` parameter is set to `true`)
         *
         * @param {Object} value The value to test.
         * @param {Boolean} [allowEmptyString=false] `true` to allow empty strings.
         * @return {Boolean}
         */
        isEmpty: function(value, allowEmptyString) {
            return (value == null) || (!allowEmptyString ? value === '' : false) || (Ext.isArray(value) && value.length === 0);
        },
        /**
         * Returns `true` if the passed value is a JavaScript Array, `false` otherwise.
         *
         * @param {Object} target The target to test.
         * @return {Boolean}
         * @method
         */
        isArray: ('isArray' in Array) ? Array.isArray : function(value) {
            return toString.call(value) === '[object Array]';
        },
        /**
         * Returns `true` if the passed value is a JavaScript Date object, `false` otherwise.
         * @param {Object} obj The object to test.
         * @return {Boolean}
         */
        isDate: function(obj) {
            return toString.call(obj) === '[object Date]';
        },
        /**
         * Returns 'true' if the passed value is a String that matches the MS Date JSON
         * encoding format.
         * @param {String} value The string to test.
         * @return {Boolean}
         */
        isMSDate: function(value) {
            if (!Ext.isString(value)) {
                return false;
            }
            return MSDateRe.test(value);
        },
        /**
         * Returns `true` if the passed value is a JavaScript Object, `false` otherwise.
         * @param {Object} value The value to test.
         * @return {Boolean}
         * @method
         */
        isObject: (toString.call(null) === '[object Object]') ? function(value) {
            // check ownerDocument here as well to exclude DOM nodes
            return value != null && toString.call(value) === '[object Object]' && value.ownerDocument === undefined;
        } : function(value) {
            return toString.call(value) === '[object Object]';
        },
        /**
         * @private
         */
        isSimpleObject: function(value) {
            return value instanceof Object && value.constructor === Object;
        },
        /**
         * Returns `true` if the passed value is a JavaScript 'primitive', a string, number
         * or boolean.
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isPrimitive: function(value) {
            var type = typeof value;
            return type === 'string' || type === 'number' || type === 'boolean';
        },
        /**
         * Returns `true` if the passed value is a JavaScript Function, `false` otherwise.
         * @param {Object} value The value to test.
         * @return {Boolean}
         * @method
         */
        isFunction: // Safari 3.x and 4.x returns 'function' for typeof <NodeList>, hence we need to fall back
        // to using Object.prototype.toString (slower)
        (typeof document !== 'undefined' && typeof document.getElementsByTagName('body') === 'function') ? function(value) {
            return !!value && toString.call(value) === '[object Function]';
        } : function(value) {
            return !!value && typeof value === 'function';
        },
        /**
         * Returns `true` if the passed value is a number. Returns `false` for non-finite numbers.
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isNumber: function(value) {
            return typeof value === 'number' && isFinite(value);
        },
        /**
         * Validates that a value is numeric.
         * @param {Object} value Examples: 1, '1', '2.34'
         * @return {Boolean} True if numeric, false otherwise
         */
        isNumeric: function(value) {
            return !isNaN(parseFloat(value)) && isFinite(value);
        },
        /**
         * Returns `true `if the passed value is a string.
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isString: function(value) {
            return typeof value === 'string';
        },
        /**
         * Returns `true` if the passed value is a boolean.
         *
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isBoolean: function(value) {
            return typeof value === 'boolean';
        },
        /**
         * Returns `true` if the passed value is an HTMLElement
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isElement: function(value) {
            return value ? value.nodeType === 1 : false;
        },
        /**
         * Returns `true` if the passed value is a TextNode
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isTextNode: function(value) {
            return value ? value.nodeName === "#text" : false;
        },
        /**
         * Returns `true` if the passed value is defined.
         * @param {Object} value The value to test.
         * @return {Boolean}
         */
        isDefined: function(value) {
            return typeof value !== 'undefined';
        },
        /**
         * Returns `true` if the passed value is iterable, that is, if elements of it are
         * addressable using array notation with numeric indices, `false` otherwise.
         *
         * Arrays and function `arguments` objects are iterable. Also HTML collections such as
         * `NodeList` and `HTMLCollection' are iterable.
         *
         * @param {Object} value The value to test
         * @return {Boolean}
         */
        isIterable: function(value) {
            // To be iterable, the object must have a numeric length property and must not be
            // a string or function.
            if (!value || typeof value.length !== 'number' || typeof value === 'string' || Ext.isFunction(value)) {
                return false;
            }
            // Certain "standard" collections in IE (such as document.images) do not offer
            // the correct Javascript Object interface; specifically, they lack the
            // propertyIsEnumerable method.
            // And the item property while it does exist is not typeof "function"
            if (!value.propertyIsEnumerable) {
                return !!value.item;
            }
            // If it is a regular, interrogatable JS object (not an IE ActiveX object), then...
            // If it has its own property called "length", but not enumerable, it's iterable
            if (value.hasOwnProperty('length') && !value.propertyIsEnumerable('length')) {
                return true;
            }
            // Test against whitelist which includes known iterable collection types
            return iterableRe.test(toString.call(value));
        },
        /**
         * This method returns `true` if debug is enabled for the specified class. This is
         * done by checking the `Ext.debugConfig.hooks` config for the closest match to the
         * given `className`.
         * @param {String} className The name of the class.
         * @return {Boolean} `true` if debug is enabled for the specified class.
         * @method
         */
        isDebugEnabled: function(className, defaultEnabled) {
            var debugConfig = Ext.debugConfig.hooks;
            if (debugConfig.hasOwnProperty(className)) {
                return debugConfig[className];
            }
            /* eslint-disable-next-line vars-on-top */
            var enabled = debugConfig['*'],
                prefixLength = 0;
            if (defaultEnabled !== undefined) {
                enabled = defaultEnabled;
            }
            if (!className) {
                return enabled;
            }
            /* eslint-disable-next-line vars-on-top */
            for (var prefix in debugConfig) {
                var value = debugConfig[prefix];
                // eslint-disable-line vars-on-top
                // if prefix=='Ext' match 'Ext.foo.Bar' but not 'Ext4.foo.Bar'
                if (className.charAt(prefix.length) === '.') {
                    if (className.substring(0, prefix.length) === prefix) {
                        if (prefixLength < prefix.length) {
                            prefixLength = prefix.length;
                            enabled = value;
                        }
                    }
                }
            }
            return enabled;
        } || emptyFn,
        /**
         * Clone simple variables including array, {}-like objects, DOM nodes and Date without
         * keeping the old reference. A reference for the object itself is returned if it's not
         * a direct descendant of Object. For model cloning, see
         * {@link Ext.data.Model#copy Model.copy}.
         *
         * @param {Object} item The variable to clone
         * @param {Boolean} [cloneDom=true] `true` to clone DOM nodes.
         * @return {Object} clone
         */
        clone: function(item, cloneDom) {
            if (item == null) {
                return item;
            }
            // DOM nodes
            // TODO proxy this to Ext.Element.clone to handle automatic id attribute changing
            // recursively
            if (cloneDom !== false && item.nodeType && item.cloneNode) {
                return item.cloneNode(true);
            }
            /* eslint-disable-next-line vars-on-top */
            var type = toString.call(item),
                i, j, k, clone, key;
            // Date
            if (type === '[object Date]') {
                return new Date(item.getTime());
            }
            // Array
            if (type === '[object Array]') {
                i = item.length;
                clone = [];
                while (i--) {
                    clone[i] = Ext.clone(item[i], cloneDom);
                }
            }
            // Object
            else if (type === '[object Object]' && item.constructor === Object) {
                clone = {};
                for (key in item) {
                    clone[key] = Ext.clone(item[key], cloneDom);
                }
                if (enumerables) {
                    for (j = enumerables.length; j--; ) {
                        k = enumerables[j];
                        if (item.hasOwnProperty(k)) {
                            clone[k] = item[k];
                        }
                    }
                }
            }
            return clone || item;
        },
        /**
         * @private
         * Generate a unique reference of Ext in the global scope, useful for sandboxing
         */
        getUniqueGlobalNamespace: function() {
            var uniqueGlobalNamespace = this.uniqueGlobalNamespace,
                i;
            if (uniqueGlobalNamespace === undefined) {
                i = 0;
                do {
                    uniqueGlobalNamespace = 'ExtBox' + (++i);
                } while (global[uniqueGlobalNamespace] !== undefined);
                global[uniqueGlobalNamespace] = Ext;
                this.uniqueGlobalNamespace = uniqueGlobalNamespace;
            }
            return uniqueGlobalNamespace;
        },
        /**
         * @private
         */
        functionFactoryCache: {},
        cacheableFunctionFactory: function() {
            var me = this,
                args = Array.prototype.slice.call(arguments),
                cache = me.functionFactoryCache,
                idx, fn, ln;
            if (Ext.isSandboxed) {
                ln = args.length;
                if (ln > 0) {
                    ln--;
                    args[ln] = 'var Ext=window.' + Ext.name + ';' + args[ln];
                }
            }
            idx = args.join('');
            fn = cache[idx];
            if (!fn) {
                fn = Function.prototype.constructor.apply(Function.prototype, args);
                cache[idx] = fn;
            }
            return fn;
        },
        functionFactory: function() {
            var args = Array.prototype.slice.call(arguments),
                ln;
            if (Ext.isSandboxed) {
                ln = args.length;
                if (ln > 0) {
                    ln--;
                    args[ln] = 'var Ext=window.' + Ext.name + ';' + args[ln];
                }
            }
            return Function.prototype.constructor.apply(Function.prototype, args);
        },
        /**
         * @private
         */
        Logger: {
            log: function(message, priority) {
                if (message && global.console) {
                    if (!priority || !(priority in global.console)) {
                        priority = 'log';
                    }
                    message = '[' + priority.toUpperCase() + '] ' + message;
                    global.console[priority](message);
                }
            },
            verbose: function(message) {
                this.log(message, 'verbose');
            },
            info: function(message) {
                this.log(message, 'info');
            },
            warn: function(message) {
                this.log(message, 'warn');
            },
            error: function(message) {
                throw new Error(message);
            },
            deprecate: function(message) {
                this.log(message, 'warn');
            }
        } || {
            verbose: emptyFn,
            log: emptyFn,
            info: emptyFn,
            warn: emptyFn,
            error: function(message) {
                throw new Error(message);
            },
            deprecate: emptyFn
        },
        ariaWarn: function(target, msg) {
            // The checks still can be disabled by setting Ext.enableAria to false;
            // this is for backwards compatibility. Also make sure we're not running
            // under the slicer, warnings are pointless in that case.
            if (Ext.enableAria && !Ext.slicer) {
                if (!Ext.ariaWarn.first) {
                    Ext.ariaWarn.first = true;
                    Ext.log.warn("WAI-ARIA compatibility warnings can be suppressed " + "by adding the following to application startup code:");
                    Ext.log.warn("    Ext.ariaWarn = Ext.emptyFn;");
                }
                Ext.log.warn({
                    msg: msg,
                    dump: target
                });
            }
        },
        /**
         * @private
         */
        getElementById: function(id) {
            return document.getElementById(id);
        },
        /**
         * @member Ext
         * @private
         */
        splitAndUnescape: (function() {
            var cache = {};
            return function(origin, delimiter) {
                if (!origin) {
                    return [];
                } else if (!delimiter) {
                    return [
                        origin
                    ];
                }
                /* eslint-disable-next-line vars-on-top, max-len */
                var replaceRe = cache[delimiter] || (cache[delimiter] = new RegExp('\\\\' + delimiter, 'g')),
                    result = [],
                    parts, part;
                parts = origin.split(delimiter);
                while ((part = parts.shift()) !== undefined) {
                    // If any of the parts ends with the delimiter that means
                    // the delimiter was escaped and the split was invalid. Roll back.
                    while (part.charAt(part.length - 1) === '\\' && parts.length > 0) {
                        part = part + delimiter + parts.shift();
                    }
                    // Now that we have split the parts, unescape the delimiter char
                    part = part.replace(replaceRe, delimiter);
                    result.push(part);
                }
                return result;
            };
        })(),
        /**
         * This is the target of the user-supplied `Ext.elevateFunction`. It wraps the
         * call to a function and concludes by calling {@link Ext#fireIdle}.
         * @since 6.5.1
         * @private
         */
        doElevate: function() {
            var fn = elevateFn,
                args = elevateArgs,
                scope = elevateScope;
            // We really should never re-enter here, but we'll latch these vars just
            // in case.
            elevateFn = elevateArgs = elevateScope = null;
            elevateRet = args ? fn.apply(scope, args) : fn.call(scope);
            // Be sure to fire the idle event while elevated or its handlers will
            // be running in an unprivileged context.
            Ext.fireIdle();
        },
        /**
         * Runs the given `fn` directly or using the user-provided `Ext.elevateFunction`
         * (if present). After calling the `fn` the global `idle` event is fired using
         * the {@link Ext#fireIdle} method.
         *
         * @param {Function} fn
         * @param {Object} [scope]
         * @param {Array} [args]
         * @param {Object} [timer]
         * @return {Mixed}
         * @since 6.5.1
         * @private
         */
        elevate: function(fn, scope, args, timer) // eslint-disable-line comma-style
        {
            var ret;
            if (args && !args.length) {
                args = null;
            }
            Ext._suppressIdle = false;
            if (timer) {
                timer.tick();
            }
            if (Ext.elevateFunction) {
                elevateFn = fn;
                elevateScope = scope;
                elevateArgs = args;
                // We reuse the same fn here to avoid GC pressure.
                Ext.elevateFunction(Ext.doElevate);
                ret = elevateRet;
                elevateRet = null;
            } else {
                ret = args ? fn.apply(scope, args) : fn.call(scope);
                Ext.fireIdle();
            }
            if (timer) {
                timer.tock();
            }
            return ret;
        },
        Timer: {
            all: {},
            track: false,
            captureStack: true,
            created: function(kind, id, info) {
                if (!Ext.Timer.track) {
                    return null;
                }
                /* eslint-disable-next-line vars-on-top */
                var timer = Ext.apply({
                        kind: kind,
                        id: id,
                        done: false,
                        firing: false,
                        creator: Ext.Timer.captureStack ? new Error().stack : null,
                        tick: Ext.Timer.tick,
                        tock: Ext.Timer.tock
                    }, info);
                /* eslint-disable-next-line vars-on-top, one-var */
                var timers = Ext.Timer.all[kind] || (Ext.Timer.all[kind] = {});
                timers[timer.id] = timer;
                if (Ext.Timer.hook) {
                    Ext.Timer.hook(timer);
                }
                return timer;
            },
            get: function(id, kind) {
                kind = kind || 'timeout';
                /* eslint-disable-next-line vars-on-top */
                var timers = Ext.Timer.all[kind];
                return timers && timers[id] || null;
            },
            cancel: function(kind, id) {
                var timers = Ext.Timer.all[kind],
                    timer = timers && timers[id];
                if (timer) {
                    timer.cancelled = true;
                    timers[id] = null;
                    delete timers[id];
                }
            },
            tick: function() {
                if (Ext.Timer.firing) {
                    // One reason for Ext.Timer.firing to get stuck is exception thrown
                    // in timer handler. In that case the timer is never tock()ed
                    // and will be left hanging. Just clean it up.
                    Ext.log.error('Previous timer state not cleaned up properly: ' + Ext.Timer.firing.creator);
                }
                if (this.kind !== 'interval') {
                    this.done = true;
                    Ext.Timer.all[this.kind][this.id] = null;
                    delete Ext.Timer.all[this.kind][this.id];
                }
                this.firing = true;
                Ext.Timer.firing = this;
            },
            tock: function() {
                this.firing = false;
                if (Ext.Timer.firing === this) {
                    Ext.Timer.firing = null;
                }
            }
        },
        /**
         * @private
         */
        getExpando: function(target, id) {
            var expandos = target.$expandos;
            return expandos && expandos[id] || null;
        },
        /**
         * @private
         */
        setExpando: function(target, id, value) {
            var expandos = target.$expandos;
            if (value !== undefined) {
                (expandos || (target.$expandos = {}))[id] = value;
            } else if (expandos) {
                delete expandos[id];
            }
            return value;
        }
    });
    Ext.returnTrue.$nullFn = Ext.returnId.$nullFn = true;
}());

// @override Ext
// This file is order extremely early (typically right after Ext.js) due to the
// above Cmd directive. This ensures that the "modern" and "classic" platform tags
// are properly set up as soon as possible.
Ext.platformTags.modern = !(Ext.platformTags.classic = Ext.isClassic = true);

/* eslint-disable max-len */
/**
 * A helper class for the native JavaScript Error object that adds a few useful capabilities for handling
 * errors in an application. When you use Ext.Error to {@link #raise} an error from within any class that
 * uses the Class System, the Error class can automatically add the source class and method from which
 * the error was raised. It also includes logic to automatically log the error to the console, if available,
 * with additional metadata about the error. In all cases, the error will always be thrown at the end so that
 * execution will halt.
 *
 * Ext.Error also offers a global error {@link #handle handling} method that can be overridden in order to
 * handle application-wide errors in a single spot. You can optionally {@link #ignore} errors altogether,
 * although in a real application it's usually a better idea to override the handling function and perform
 * logging or some other method of reporting the errors in a way that is meaningful to the application.
 *
 * At its simplest you can simply raise an error as a simple string from within any code:
 *
 * Example usage:
 *
 *     Ext.raise('Something bad happened!');
 *
 * If raised from plain JavaScript code, the error will be logged to the console (if available) and the message
 * displayed. In most cases however you'll be raising errors from within a class, and it may often be useful to add
 * additional metadata about the error being raised.  The {@link #raise} method can also take a config object.
 * In this form the `msg` attribute becomes the error description, and any other data added to the config gets
 * added to the error object and, if the console is available, logged to the console for inspection.
 *
 * Example usage:
 *
 *     Ext.define('Ext.Foo', {
 *         doSomething: function(option){
 *             if (someCondition === false) {
 *                 Ext.raise({
 *                     msg: 'You cannot do that!',
 *                     option: option,   // whatever was passed into the method
 *                     'error code': 100 // other arbitrary info
 *                 });
 *             }
 *         }
 *     });
 *
 * If a console is available (that supports the `console.dir` function) you'll see console output like:
 *
 *     An error was raised with the following data:
 *     option:         Object { foo: "bar"}
 *         foo:        "bar"
 *     error code:     100
 *     msg:            "You cannot do that!"
 *     sourceClass:   "Ext.Foo"
 *     sourceMethod:  "doSomething"
 *
 *     uncaught exception: You cannot do that!
 *
 * As you can see, the error will report exactly where it was raised and will include as much information as the
 * raising code can usefully provide.
 *
 * If you want to handle all application errors globally you can simply override the static {@link #handle} method
 * and provide whatever handling logic you need. If the method returns true then the error is considered handled
 * and will not be thrown to the browser. If anything but true is returned then the error will be thrown normally.
 *
 * Example usage:
 *
 *     Ext.Error.handle = function(err) {
 *         if (err.someProperty == 'NotReallyAnError') {
 *             // maybe log something to the application here if applicable
 *             return true;
 *         }
 *         // any non-true return value (including none) will cause the error to be thrown
 *     }
 *
 * @class Ext.Error
 */
/* eslint-enable max-len */
/* eslint-disable indent */
(function() {
    // @define Ext.lang.Error
    // @define Ext.Error
    // @require Ext
    function toString() {
        var me = this,
            cls = me.sourceClass,
            method = me.sourceMethod,
            msg = me.msg;
        if (method) {
            if (msg) {
                method += '(): ';
                method += msg;
            } else {
                method += '()';
            }
        }
        if (cls) {
            method = method ? (cls + '.' + method) : cls;
        }
        return method || msg || '';
    }
    Ext.Error = function(config) {
        var error = new Error();
        if (Ext.isString(config)) {
            config = {
                msg: config
            };
        }
        Ext.apply(error, config);
        error.message = error.message || error.msg;
        // 'message' is standard ('msg' is non-standard)
        // note: the above does not work in old WebKit (me.message is readonly) (Safari 4)
        error.toString = toString;
        return error;
    };
    Ext.apply(Ext.Error, {
        /**
         * @property {Boolean} ignore
         * Static flag that can be used to globally disable error reporting to the browser if set
         * to true (defaults to false). Note that if you ignore Ext errors it's likely that some
         * other code may fail and throw a native JavaScript error thereafter, so use with caution.
         * In most cases it will probably be preferable to supply a custom error
         * {@link #handle handling} function instead.
         *
         * Example usage:
         *
         *     Ext.Error.ignore = true;
         *
         * @static
         */
        ignore: false,
        /**
         * This method is called internally by {@link Ext#raise}. Application code should
         * call {@link Ext#raise} instead of calling this method directly.
         *
         * @static
         * @deprecated 6.0.0 Use {@link Ext#raise} instead.
         */
        raise: function(err) {
            var me = this,
                method = me.raise.caller,
                msg, name;
            err = err || {};
            if (Ext.isString(err)) {
                err = {
                    msg: err
                };
            }
            if (method === Ext.raise) {
                method = method.caller;
            }
            if (method) {
                if (!err.sourceMethod && (name = method.$name)) {
                    err.sourceMethod = name;
                }
                if (!err.sourceClass && (name = method.$owner) && (name = name.$className)) {
                    err.sourceClass = name;
                }
            }
            if (me.handle(err) !== true) {
                msg = toString.call(err);
                Ext.log({
                    msg: msg,
                    level: 'error',
                    dump: err,
                    stack: true
                });
                throw new Ext.Error(err);
            }
        },
        /**
         * Globally handle any Ext errors that may be raised, optionally providing custom logic
         * to handle different errors individually. Return true from the function to bypass 
         * throwing the error to the browser, otherwise the error will be thrown and execution
         * will halt.
         *
         * Example usage:
         *
         *     Ext.Error.handle = function(err) {
         *         if (err.someProperty == 'NotReallyAnError') {
         *             // maybe log something to the application here if applicable
         *             return true;
         *         }
         *         // any non-true return value (including none) will cause the error to be thrown
         *     }
         *
         * @param {Object} err The error being raised. It will contain any attributes that were
         * originally raised with it, plus properties about the method and class from which
         * the error originated (if raised from a class that uses the Class System).
         * @static
         */
        handle: function() {
            return this.ignore;
        }
    });
})();
/**
 * Create a function that will throw an error if called (in debug mode) with a message that
 * indicates the method has been removed.
 * @param {String} suggestion Optional text to include in the message (a workaround perhaps).
 * @return {Function} The generated function.
 * @private
 */
Ext.deprecated = function(suggestion) {
    if (!suggestion) {
        suggestion = '';
    }
    function fail() {
        Ext.raise('The method "' + fail.$owner.$className + '.' + fail.$name + '" has been removed. ' + suggestion);
    }
    return fail;
    return Ext.emptyFn;
};
// eslint-disable-line no-unreachable
/**
 * Raise an error that can include additional data and supports automatic console logging
 * if available. You can pass a string error message or an object with the `msg` attribute
 * which will be used as the error message. The object can contain any other name-value
 * attributes (or objects) to be logged along with the error.
 *
 * Note that after displaying the error message a JavaScript error will ultimately be
 * thrown so that execution will halt.
 *
 * Example usage:
 *
 *     Ext.raise('A simple string error message');
 *
 *     // or...
 *
 *     Ext.define('Ext.Foo', {
 *         doSomething: function(option){
 *             if (someCondition === false) {
 *                 Ext.raise({
 *                     msg: 'You cannot do that!',
 *                     option: option,   // whatever was passed into the method
 *                     code: 100 // other arbitrary info
 *                 });
 *             }
 *         }
 *     });
 *
 * @param {String/Object} err The error message string, or an object containing the
 * attribute "msg" that will be used as the error message. Any other data included in the
 * object will also be logged to the browser console, if available.
 * @method raise
 * @member Ext
 */
Ext.raise = function() {
    Ext.Error.raise.apply(Ext.Error, arguments);
};
/*
 * This mechanism is used to notify the user of the first error encountered on the page. In
 * most cases errors go unobserved especially on IE. This mechanism pushes this information
 * to the status bar so that users don't miss it.
 */
(function(skipNotify) {
    if (skipNotify || typeof window === 'undefined') {
        return;
    }
    // build system or some such environment...
    // eslint-disable-next-line vars-on-top
    var last = 0,
        // This method is called to notify the user of the current error status.
        notify = function() {
            var cnt = Ext.log && Ext.log.counters,
                n = cnt && (cnt.error + cnt.warn + cnt.info + cnt.log),
                msg;
            // Put log counters to the status bar (for most browsers):
            if (n && last !== n) {
                msg = [];
                if (cnt.error) {
                    msg.push('Errors: ' + cnt.error);
                }
                if (cnt.warn) {
                    msg.push('Warnings: ' + cnt.warn);
                }
                if (cnt.info) {
                    msg.push('Info: ' + cnt.info);
                }
                if (cnt.log) {
                    msg.push('Log: ' + cnt.log);
                }
                window.status = '*** ' + msg.join(' -- ');
                last = n;
            }
        };
    // Allow unit tests to skip this when checking for dangling timers
    notify.$skipTimerCheck = true;
    // window.onerror sounds ideal but it prevents the built-in error dialog from doing
    // its (better) thing. We deliberately use setInterval() here instead of going with
    // Ext.interval() to keep it basic and simple.
    setInterval(notify, 1000);
}(!!window.__UNIT_TESTING__));

/**
 * @class Ext.Array
 * @singleton
 *
 * A set of useful static methods to deal with arrays; provide missing methods for
 * older browsers.
 */
Ext.Array = (function() {
    /* eslint-disable indent */
    // @define Ext.lang.Array
    // @define Ext.Array
    // @require Ext
    // @require Ext.lang.Error
    var arrayPrototype = Array.prototype,
        slice = arrayPrototype.slice,
        supportsSplice = (function() {
            var array = [],
                lengthBefore,
                j = 20;
            if (!array.splice) {
                return false;
            }
            // This detects a bug in IE8 splice method:
            // see http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/
            // 6e946d03-e09f-4b22-a4dd-cd5e276bf05a/
            while (j--) {
                array.push("A");
            }
            array.splice(15, 0, "F", "F", "F", "F", "F", "F", "F", "F", "F", "F", "F", "F", "F", "F", "F", "F", "F", "F", "F", "F", "F");
            lengthBefore = array.length;
            // 41
            array.splice(13, 0, "XXX");
            // add one element
            if (lengthBefore + 1 !== array.length) {
                return false;
            }
            // end IE8 bug
            return true;
        }()),
        supportsIndexOf = 'indexOf' in arrayPrototype,
        supportsSliceOnNodeList = true;
    // Sort an array using the comparator, but if the comparator returns zero, use the objects'
    // original indices to tiebreak This results in a stable sort.
    function stableSort(array, userComparator) {
        var len = array.length,
            indices = new Array(len),
            i;
        // generate 0-n index map from original array
        for (i = 0; i < len; i++) {
            indices[i] = i;
        }
        // Sort indices array using a comparator which compares the original values at the two
        // indices, and uses those indices as a tiebreaker
        indices.sort(function(index1, index2) {
            return userComparator(array[index1], array[index2]) || (index1 - index2);
        });
        // Reconsitute a sorted array using the array that the indices have been sorted into
        for (i = 0; i < len; i++) {
            indices[i] = array[indices[i]];
        }
        // Rebuild the original array
        for (i = 0; i < len; i++) {
            array[i] = indices[i];
        }
        return array;
    }
    try {
        // IE 6 - 8 will throw an error when using Array.prototype.slice on NodeList
        if (typeof document !== 'undefined') {
            slice.call(document.getElementsByTagName('body'));
        }
    } catch (e) {
        supportsSliceOnNodeList = false;
    }
    /* eslint-disable-next-line vars-on-top */
    var fixArrayIndex = function(array, index) {
            return (index < 0) ? Math.max(0, array.length + index) : Math.min(array.length, index);
        },
        /*
    Does the same work as splice, but with a slightly more convenient signature. The splice
    method has bugs in IE8, so this is the implementation we use on that platform.

    The rippling of items in the array can be tricky. Consider two use cases:

                  index=2
                  removeCount=2
                 /=====\
        +---+---+---+---+---+---+---+---+
        | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
        +---+---+---+---+---+---+---+---+
                         /  \/  \/  \/  \
                        /   /\  /\  /\   \
                       /   /  \/  \/  \   +--------------------------+
                      /   /   /\  /\   +--------------------------+   \
                     /   /   /  \/  +--------------------------+   \   \
                    /   /   /   /+--------------------------+   \   \   \
                   /   /   /   /                             \   \   \   \
                  v   v   v   v                               v   v   v   v
        +---+---+---+---+---+---+       +---+---+---+---+---+---+---+---+---+
        | 0 | 1 | 4 | 5 | 6 | 7 |       | 0 | 1 | a | b | c | 4 | 5 | 6 | 7 |
        +---+---+---+---+---+---+       +---+---+---+---+---+---+---+---+---+
        A                               B        \=========/
                                                 insert=[a,b,c]

    In case A, it is obvious that copying of [4,5,6,7] must be left-to-right so
    that we don't end up with [0,1,6,7,6,7]. In case B, we have the opposite; we
    must go right-to-left or else we would end up with [0,1,a,b,c,4,4,4,4].
    */
        replaceSim = function(array, index, removeCount, insert) {
            var add = insert ? insert.length : 0,
                length = array.length,
                pos = fixArrayIndex(array, index);
            // we try to use Array.push when we can for efficiency...
            if (pos === length) {
                if (add) {
                    array.push.apply(array, insert);
                }
            } else {
                /* eslint-disable-next-line vars-on-top */
                var remove = Math.min(removeCount, length - pos),
                    tailOldPos = pos + remove,
                    tailNewPos = tailOldPos + add - remove,
                    tailCount = length - tailOldPos,
                    lengthAfterRemove = length - remove,
                    i;
                if (tailNewPos < tailOldPos) {
                    // case A
                    for (i = 0; i < tailCount; ++i) {
                        array[tailNewPos + i] = array[tailOldPos + i];
                    }
                } else if (tailNewPos > tailOldPos) {
                    // case B
                    for (i = tailCount; i--; ) {
                        array[tailNewPos + i] = array[tailOldPos + i];
                    }
                }
                // else, add == remove (nothing to do)
                if (add && pos === lengthAfterRemove) {
                    array.length = lengthAfterRemove;
                    // truncate array
                    array.push.apply(array, insert);
                } else {
                    array.length = lengthAfterRemove + add;
                    // reserves space
                    for (i = 0; i < add; ++i) {
                        array[pos + i] = insert[i];
                    }
                }
            }
            return array;
        },
        replaceNative = function(array, index, removeCount, insert) {
            if (insert && insert.length) {
                // Inserting at index zero with no removing: use unshift
                if (index === 0 && !removeCount) {
                    array.unshift.apply(array, insert);
                }
                // Inserting/replacing in middle of array
                else if (index < array.length) {
                    array.splice.apply(array, [
                        index,
                        removeCount
                    ].concat(insert));
                } else // Appending to array
                {
                    array.push.apply(array, insert);
                }
            } else {
                array.splice(index, removeCount);
            }
            return array;
        },
        eraseSim = function(array, index, removeCount) {
            return replaceSim(array, index, removeCount);
        },
        eraseNative = function(array, index, removeCount) {
            array.splice(index, removeCount);
            return array;
        },
        spliceSim = function(array, index, removeCount) {
            var len = arguments.length,
                pos = fixArrayIndex(array, index),
                removed;
            if (len < 3) {
                removeCount = array.length - pos;
            }
            removed = array.slice(index, fixArrayIndex(array, pos + removeCount));
            if (len < 4) {
                replaceSim(array, pos, removeCount);
            } else {
                replaceSim(array, pos, removeCount, slice.call(arguments, 3));
            }
            return removed;
        },
        spliceNative = function(array) {
            return array.splice.apply(array, slice.call(arguments, 1));
        },
        erase = supportsSplice ? eraseNative : eraseSim,
        replace = supportsSplice ? replaceNative : replaceSim,
        splice = supportsSplice ? spliceNative : spliceSim,
        // NOTE: from here on, use erase, replace or splice (not native methods)...
        ExtArray = {
            /**
         * This method returns the index that a given item would be inserted into the
         * given (sorted) `array`. Note that the given `item` may or may not be in the
         * array. This method will return the index of where the item *should* be.
         *
         * For example:
         *
         *      var array = [ 'A', 'D', 'G', 'K', 'O', 'R', 'X' ];
         *      var index = Ext.Array.binarySearch(array, 'E');
         *
         *      console.log('index: ' + index);
         *      // logs "index: 2"
         *
         *      array.splice(index, 0, 'E');
         *
         *      console.log('array : ' + array.join(''));
         *      // logs "array: ADEGKORX"
         *
         * @param {Object[]} array The array to search.
         * @param {Object} item The item that you want to insert into the `array`.
         * @param {Number} [begin=0] The first index in the `array` to consider.
         * @param {Number} [end=array.length] The index that marks the end of the range
         * to consider. The item at this index is *not* considered.
         * @param {Function} [compareFn] The comparison function that matches the sort
         * order of the `array`. The default `compareFn` compares items using less-than
         * and greater-than operators.
         * @return {Number} The index for the given item in the given array based on
         * the current sorters.
         */
            binarySearch: function(array, item, begin, end, compareFn) {
                var length = array.length,
                    middle, comparison;
                if (begin instanceof Function) {
                    compareFn = begin;
                    begin = 0;
                    end = length;
                } else if (end instanceof Function) {
                    compareFn = end;
                    end = length;
                } else {
                    if (begin === undefined) {
                        begin = 0;
                    }
                    if (end === undefined) {
                        end = length;
                    }
                    compareFn = compareFn || ExtArray.lexicalCompare;
                }
                --end;
                while (begin <= end) {
                    middle = (begin + end) >> 1;
                    comparison = compareFn(item, array[middle]);
                    if (comparison >= 0) {
                        begin = middle + 1;
                    } else if (comparison < 0) {
                        end = middle - 1;
                    }
                }
                return begin;
            },
            defaultCompare: function(lhs, rhs) {
                return (lhs < rhs) ? -1 : ((lhs > rhs) ? 1 : 0);
            },
            // Default comparator to use when no comparator is specified for the sort method.
            // Javascript sort does LEXICAL comparison.
            lexicalCompare: function(lhs, rhs) {
                lhs = String(lhs);
                rhs = String(rhs);
                return (lhs < rhs) ? -1 : ((lhs > rhs) ? 1 : 0);
            },
            /**
         * Iterates an array or an iterable value and invoke the given callback function for each
         * item.
         *
         *     var countries = ['Vietnam', 'Singapore', 'United States', 'Russia'];
         *
         *     Ext.Array.each(countries, function(name, index, countriesItSelf) {
         *         console.log(name);
         *     });
         *
         *     var sum = function() {
         *         var sum = 0;
         *
         *         Ext.Array.each(arguments, function(value) {
         *             sum += value;
         *         });
         *
         *         return sum;
         *     };
         *
         *     sum(1, 2, 3); // returns 6
         *
         * The iteration can be stopped by returning `false` from the callback function.  
         * Returning `undefined` (i.e `return;`) will only exit the callback function and 
         * proceed with the next iteration of the loop.
         *
         *     Ext.Array.each(countries, function(name, index, countriesItSelf) {
         *         if (name === 'Singapore') {
         *             return false; // break here
         *         }
         *     });
         *
         * {@link Ext#each Ext.each} is alias for {@link Ext.Array#each Ext.Array.each}
         *
         * @param {Array/NodeList/Object} array The value to be iterated. If this
         * argument is not iterable, the callback function is called once.
         * @param {Function} fn The callback function. If it returns `false`, the iteration
         * stops and this method returns the current `index`. Returning `undefined` (i.e 
         * `return;`) will only exit the callback function and proceed with the next iteration 
         * in the loop.
         * @param {Object} fn.item The item at the current `index` in the passed `array`
         * @param {Number} fn.index The current `index` within the `array`
         * @param {Array} fn.allItems The `array` itself which was passed as the first argument
         * @param {Boolean} fn.return Return `false` to stop iteration.
         * @param {Object} [scope] The scope (`this` reference) in which the specified function is
         * executed.
         * @param {Boolean} [reverse=false] Reverse the iteration order (loop from the end to the
         * beginning).
         * @return {Boolean/Number} If all array entries were iterated, this will be `true. If
         * iteration was halted early because the passed fuction returned `false`, this will
         * be the index at which iteration was halted.
         */
            each: function(array, fn, scope, reverse) {
                var i, ln;
                array = ExtArray.from(array);
                ln = array.length;
                if (reverse !== true) {
                    for (i = 0; i < ln; i++) {
                        if (fn.call(scope || array[i], array[i], i, array) === false) {
                            return i;
                        }
                    }
                } else {
                    for (i = ln - 1; i > -1; i--) {
                        if (fn.call(scope || array[i], array[i], i, array) === false) {
                            return i;
                        }
                    }
                }
                return true;
            },
            /*
         * Calculates the the insertion index of a passed object into the passed Array according
         * to the passed comparator function. Note that the passed Array *MUST* already be ordered.
         * @param {Object} item The item to calculate the insertion index for.
         * @param {Array} The array into which the item is to be inserted.
         * @param {Function} comparatorFn The comparison function. Must return -1 or 0 or 1.
         * @param {Object} comparatorFn.lhs The left object to compare.
         * @param {Object} comparatorFn.rhs The right object to compare.
         * @param {Number} index The possible correct index to try first before a binary
         * search is instigated.
         */
            findInsertionIndex: function(item, items, comparatorFn, index) {
                var len = items.length,
                    beforeCheck, afterCheck;
                comparatorFn = comparatorFn || ExtArray.lexicalCompare;
                if (0 <= index && index < len) {
                    beforeCheck = index > 0 ? comparatorFn(item, items[index - 1]) : 0;
                    afterCheck = (index < len) ? comparatorFn(item, items[index]) : 0;
                    if (0 <= beforeCheck && afterCheck < 1) {
                        return index;
                    }
                }
                return ExtArray.binarySearch(items, item, comparatorFn);
            },
            /**
         * @method
         * Iterates an array and invoke the given callback function for each item. Note that this
         * will simply delegate to the native `Array.prototype.forEach` method if supported. It
         * doesn't support stopping the iteration by returning `false` in the callback function
         * like {@link Ext.Array#each}. However, performance could be much better in modern
         * browsers comparing with {@link Ext.Array#each}
         *
         * @param {Array} array The array to iterate.
         * @param {Function} fn The callback function.
         * @param {Object} fn.item The item at the current `index` in the passed `array`.
         * @param {Number} fn.index The current `index` within the `array`.
         * @param {Array}  fn.allItems The `array` itself which was passed as the first argument.
         * @param {Object} scope (Optional) The execution scope (`this`) in which the
         * specified function is executed.
         */
            forEach: ('forEach' in arrayPrototype) ? function(array, fn, scope) {
                array.forEach(fn, scope);
            } : function(array, fn, scope) {
                var i, ln;
                for (i = 0 , ln = array.length; i < ln; i++) {
                    fn.call(scope, array[i], i, array);
                }
            },
            /**
         * @method
         * Get the index of the provided `item` in the given `array`, a supplement for the
         * missing arrayPrototype.indexOf in Internet Explorer.
         *
         * @param {Array} array The array to check.
         * @param {Object} item The item to find.
         * @param {Number} from (Optional) The index at which to begin the search.
         * @return {Number} The index of item in the array (or -1 if it is not found).
         */
            indexOf: supportsIndexOf ? function(array, item, from) {
                // May be called with no array which causes an error.
                return array ? arrayPrototype.indexOf.call(array, item, from) : -1;
            } : function(array, item, from) {
                var i,
                    length = array ? array.length : 0;
                for (i = (from < 0) ? Math.max(0, length + from) : from || 0; i < length; i++) {
                    if (array[i] === item) {
                        return i;
                    }
                }
                return -1;
            },
            /**
         * @method
         * Checks whether or not the given `array` contains the specified `item`.
         *
         * @param {Array} array The array to check.
         * @param {Object} item The item to find.
         * @return {Boolean} `true` if the array contains the item, `false` otherwise.
         */
            contains: supportsIndexOf ? function(array, item) {
                return arrayPrototype.indexOf.call(array, item) !== -1;
            } : function(array, item) {
                var i, ln;
                for (i = 0 , ln = array.length; i < ln; i++) {
                    if (array[i] === item) {
                        return true;
                    }
                }
                return false;
            },
            /**
         * Converts any iterable (numeric indices and a length property) into a true array.
         *
         *     function test() {
         *         var args = Ext.Array.toArray(arguments),
         *             fromSecondToLastArgs = Ext.Array.toArray(arguments, 1);
         *
         *         alert(args.join(' '));
         *         alert(fromSecondToLastArgs.join(' '));
         *     }
         *
         *     test('just', 'testing', 'here'); // alerts 'just testing here';
         *                                      // alerts 'testing here';
         *
         *     // will convert the NodeList into an array
         *     Ext.Array.toArray(document.getElementsByTagName('div'));
         *     Ext.Array.toArray('splitted'); // returns ['s', 'p', 'l', 'i', 't', 't', 'e', 'd']
         *     Ext.Array.toArray('splitted', 0, 3); // returns ['s', 'p', 'l']
         *
         * {@link Ext#toArray Ext.toArray} is alias for {@link Ext.Array#toArray Ext.Array.toArray}
         *
         * @param {Object} iterable the iterable object to be turned into a true Array.
         * @param {Number} [start=0] a zero-based index that specifies the start of extraction.
         * @param {Number} [end=-1] a 1-based index that specifies the end of extraction.
         * @return {Array}
         */
            toArray: function(iterable, start, end) {
                var array = [],
                    i;
                if (!iterable || !iterable.length) {
                    return array;
                }
                if (typeof iterable === 'string') {
                    iterable = iterable.split('');
                }
                if (supportsSliceOnNodeList) {
                    return slice.call(iterable, start || 0, end || iterable.length);
                }
                start = start || 0;
                end = end ? ((end < 0) ? iterable.length + end : end) : iterable.length;
                for (i = start; i < end; i++) {
                    array.push(iterable[i]);
                }
                return array;
            },
            /**
         * Plucks the value of a property from each item in the Array. Example:
         *
         *     // [el1.className, el2.className, ..., elN.className]
         *     Ext.Array.pluck(Ext.query("p"), "className");
         *
         * @param {Array/NodeList} array The Array of items to pluck the value from.
         * @param {String} propertyName The property name to pluck from each element.
         * @return {Array} The value from each item in the Array.
         */
            pluck: function(array, propertyName) {
                var ret = [],
                    i, ln, item;
                for (i = 0 , ln = array.length; i < ln; i++) {
                    item = array[i];
                    ret.push(item[propertyName]);
                }
                return ret;
            },
            /**
         * @method
         * Creates a new array with the results of calling a provided function on every element
         * in this array.
         *
         * @param {Array} array
         * @param {Function} fn Callback function for each item.
         * @param {Mixed} fn.item Current item.
         * @param {Number} fn.index Index of the item.
         * @param {Array} fn.array The whole array that's being iterated.
         * @param {Object} [scope] Callback function scope
         * @return {Array} results
         */
            map: ('map' in arrayPrototype) ? function(array, fn, scope) {
                Ext.Assert.isFunction(fn, 'Ext.Array.map must have a callback function passed as second argument.');
                return array.map(fn, scope);
            } : function(array, fn, scope) {
                Ext.Assert.isFunction(fn, 'Ext.Array.map must have a callback function passed as second argument.');
                /* eslint-disable-next-line vars-on-top */
                var len = array.length,
                    results = new Array(len),
                    i;
                for (i = 0; i < len; i++) {
                    results[i] = fn.call(scope, array[i], i, array);
                }
                return results;
            },
            /**
         * @method
         * Executes the specified function for each array element until the function returns
         * a falsy value. If such an item is found, the function will return `false` immediately.
         * Otherwise, it will return `true`.
         *
         * @param {Array} array
         * @param {Function} fn Callback function for each item.
         * @param {Mixed} fn.item Current item.
         * @param {Number} fn.index Index of the item.
         * @param {Array} fn.array The whole array that's being iterated.
         * @param {Object} scope Callback function scope.
         * @return {Boolean} `true` if no false value is returned by the callback function.
         */
            every: ('every' in arrayPrototype) ? function(array, fn, scope) {
                Ext.Assert.isFunction(fn, 'Ext.Array.every must have a callback function passed as second argument.');
                return array.every(fn, scope);
            } : function(array, fn, scope) {
                var i, ln;
                Ext.Assert.isFunction(fn, 'Ext.Array.every must have a callback function passed as second argument.');
                for (i = 0 , ln = array.length; i < ln; ++i) {
                    if (!fn.call(scope, array[i], i, array)) {
                        return false;
                    }
                }
                return true;
            },
            /**
         * @method
         * Executes the specified function for each array element until the function returns
         * a truthy value. If such an item is found, the function will return `true` immediately.
         * Otherwise, it will return `false`.
         *
         * @param {Array} array
         * @param {Function} fn Callback function for each item.
         * @param {Mixed} fn.item Current item.
         * @param {Number} fn.index Index of the item.
         * @param {Array} fn.array The whole array that's being iterated.
         * @param {Object} scope Callback function scope.
         * @return {Boolean} `true` if the callback function returns a truthy value.
         */
            some: ('some' in arrayPrototype) ? function(array, fn, scope) {
                Ext.Assert.isFunction(fn, 'Ext.Array.some must have a callback function passed as second argument.');
                return array.some(fn, scope);
            } : function(array, fn, scope) {
                var i, ln;
                Ext.Assert.isFunction(fn, 'Ext.Array.some must have a callback function passed as second argument.');
                for (i = 0 , ln = array.length; i < ln; ++i) {
                    if (fn.call(scope, array[i], i, array)) {
                        return true;
                    }
                }
                return false;
            },
            /**
         * Shallow compares the contents of 2 arrays using strict equality.
         * @param {Array} array1
         * @param {Array} array2
         * @return {Boolean} `true` if the arrays are equal.
         */
            equals: function(array1, array2) {
                var len1 = array1.length,
                    len2 = array2.length,
                    i;
                // Short circuit if the same array is passed twice
                if (array1 === array2) {
                    return true;
                }
                if (len1 !== len2) {
                    return false;
                }
                for (i = 0; i < len1; ++i) {
                    if (array1[i] !== array2[i]) {
                        return false;
                    }
                }
                return true;
            },
            /**
         * Filter through an array and remove empty item as defined in
         * {@link Ext#isEmpty Ext.isEmpty}.
         *
         * See {@link Ext.Array#filter}
         *
         * @param {Array} array
         * @return {Array} results
         */
            clean: function(array) {
                var results = [],
                    i, ln, item;
                for (i = 0 , ln = array.length; i < ln; i++) {
                    item = array[i];
                    if (!Ext.isEmpty(item)) {
                        results.push(item);
                    }
                }
                return results;
            },
            /**
         * Returns a new array with unique items.
         *
         * @param {Array} array
         * @return {Array} results
         */
            unique: function(array) {
                var clone = [],
                    i, ln, item;
                for (i = 0 , ln = array.length; i < ln; i++) {
                    item = array[i];
                    if (ExtArray.indexOf(clone, item) === -1) {
                        clone.push(item);
                    }
                }
                return clone;
            },
            /**
         * @method
         * Creates a new array with all of the elements of this array for which
         * the provided filtering function returns a truthy value.
         *
         * @param {Array} array
         * @param {Function} fn Callback function for each item.
         * @param {Mixed} fn.item Current item.
         * @param {Number} fn.index Index of the item.
         * @param {Array} fn.array The whole array that's being iterated.
         * @param {Object} scope Callback function scope.
         * @return {Array} results
         */
            filter: ('filter' in arrayPrototype) ? function(array, fn, scope) {
                Ext.Assert.isFunction(fn, 'Ext.Array.filter must have a filter function passed as second argument.');
                return array.filter(fn, scope);
            } : function(array, fn, scope) {
                var results = [],
                    i, ln;
                Ext.Assert.isFunction(fn, 'Ext.Array.filter must have a filter function passed as second argument.');
                for (i = 0 , ln = array.length; i < ln; i++) {
                    if (fn.call(scope, array[i], i, array)) {
                        results.push(array[i]);
                    }
                }
                return results;
            },
            /**
         * Returns the first item in the array which elicits a truthy return value from the
         * passed selection function.
         * @param {Array} array The array to search
         * @param {Function} fn The selection function to execute for each item.
         * @param {Mixed} fn.item The array item.
         * @param {Number} fn.index The index of the array item.
         * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the
         * function is executed. Defaults to the array
         * @return {Object} The first item in the array which returned true from the selection
         * function, or null if none was found.
         */
            findBy: function(array, fn, scope) {
                var i, len;
                for (i = 0 , len = array.length; i < len; i++) {
                    if (fn.call(scope || array, array[i], i)) {
                        return array[i];
                    }
                }
                return null;
            },
            /**
         * Converts a value to an array if it's not already an array; returns:
         *
         * - An empty array if given value is `undefined` or `null`
         * - Itself if given value is already an array
         * - An array copy if given value is {@link Ext#isIterable iterable} (arguments, NodeList
         * and alike)
         * - An array with one item which is the given value, otherwise
         *
         * @param {Object} value The value to convert to an array if it's not already is an array.
         * @param {Boolean} [newReference] `true` to clone the given array and return a new
         * reference if necessary.
         * @return {Array} array
         */
            from: function(value, newReference) {
                var type;
                if (value === undefined || value === null) {
                    return [];
                }
                if (Ext.isArray(value)) {
                    return (newReference) ? slice.call(value) : value;
                }
                type = typeof value;
                // Both strings and functions will have a length property. In phantomJS, NodeList
                // instances report typeof=='function' but don't have an apply method...
                if (value && value.length !== undefined && type !== 'string' && (type !== 'function' || !value.apply)) {
                    return ExtArray.toArray(value);
                }
                return [
                    value
                ];
            },
            /**
         * Removes the specified item from the array if it exists.
         *
         * @param {Array} array The array.
         * @param {Object} item The item to remove.
         * @return {Array} The passed array.
         */
            remove: function(array, item) {
                var index = ExtArray.indexOf(array, item);
                if (index !== -1) {
                    erase(array, index, 1);
                }
                return array;
            },
            /**
         * Removes item/s at the specified index.
         * 
         * @param {Array} array The array.
         * @param {Number} index The index of the item to be removed.
         * @param {Number} [count=1] The number of items to be removed.
         * @return {Array} The passed array.
         */
            removeAt: function(array, index, count) {
                var len = array.length;
                if (index >= 0 && index < len) {
                    count = count || 1;
                    count = Math.min(count, len - index);
                    erase(array, index, count);
                }
                return array;
            },
            /**
         * Push an item into the array only if the array doesn't contain it yet.
         *
         * @param {Array} array The array.
         * @param {Object} item The item to include.
         */
            include: function(array, item) {
                if (!ExtArray.contains(array, item)) {
                    array.push(item);
                }
            },
            /**
         * Clone a flat array without referencing the previous one. Note that this is different
         * from `Ext.clone` since it doesn't handle recursive cloning. It's simply a convenient,
         * easy-to-remember method for `Array.prototype.slice.call(array)`.
         *
         * @param {Array} array The array.
         * @return {Array} The clone array.
         */
            clone: function(array) {
                return slice.call(array);
            },
            /**
         * Merge multiple arrays into one with unique items.
         *
         * {@link Ext.Array#union} is alias for {@link Ext.Array#merge}
         *
         * @param {Array} array1
         * @param {Array} array2
         * @param {Array} etc
         * @return {Array} merged
         */
            merge: function() {
                var args = slice.call(arguments),
                    array = [],
                    i, ln;
                for (i = 0 , ln = args.length; i < ln; i++) {
                    array = array.concat(args[i]);
                }
                return ExtArray.unique(array);
            },
            /**
         * Merge multiple arrays into one with unique items that exist in all of the arrays.
         *
         * @param {Array} array1
         * @param {Array} array2
         * @param {Array} etc
         * @return {Array} intersect
         */
            intersect: function() {
                var intersection = [],
                    arrays = slice.call(arguments),
                    arraysLength, array, arrayLength, minArray, minArrayIndex, minArrayCandidate, minArrayLength, element, elementCandidate, elementCount, i, j, k;
                if (!arrays.length) {
                    return intersection;
                }
                // Find the smallest array
                arraysLength = arrays.length;
                for (i = minArrayIndex = 0; i < arraysLength; i++) {
                    minArrayCandidate = arrays[i];
                    if (!minArray || minArrayCandidate.length < minArray.length) {
                        minArray = minArrayCandidate;
                        minArrayIndex = i;
                    }
                }
                minArray = ExtArray.unique(minArray);
                erase(arrays, minArrayIndex, 1);
                // Use the smallest unique'd array as the anchor loop. If the other array(s) do contain
                // an item in the small array, we're likely to find it before reaching the end
                // of the inner loop and can terminate the search early.
                minArrayLength = minArray.length;
                arraysLength = arrays.length;
                for (i = 0; i < minArrayLength; i++) {
                    element = minArray[i];
                    elementCount = 0;
                    for (j = 0; j < arraysLength; j++) {
                        array = arrays[j];
                        arrayLength = array.length;
                        for (k = 0; k < arrayLength; k++) {
                            elementCandidate = array[k];
                            if (element === elementCandidate) {
                                elementCount++;
                                break;
                            }
                        }
                    }
                    if (elementCount === arraysLength) {
                        intersection.push(element);
                    }
                }
                return intersection;
            },
            /**
         * Perform a set difference A-B by subtracting all items in array B from array A.
         *
         * @param {Array} arrayA
         * @param {Array} arrayB
         * @return {Array} difference
         */
            difference: function(arrayA, arrayB) {
                var clone = slice.call(arrayA),
                    ln = clone.length,
                    i, j, lnB;
                for (i = 0 , lnB = arrayB.length; i < lnB; i++) {
                    for (j = 0; j < ln; j++) {
                        if (clone[j] === arrayB[i]) {
                            erase(clone, j, 1);
                            j--;
                            ln--;
                        }
                    }
                }
                return clone;
            },
            /**
         * This method applies the `reduceFn` function against an accumulator and each
         * value of the `array` (from left-to-right) to reduce it to a single value.
         *
         * If no `initialValue` is specified, the first element of the array is used as
         * the initial value. For example:
         *
         *      function reducer (previous, value, index) {
         *          console.log('[' + index + ']: (' + previous + ',' + value + '}');
         *          return previous * 10 + value;
         *      }
         *
         *      v = Ext.Array.reduce([2, 3, 4], reducer);
         *      console.log('v = ' + v);
         *
         *      > [1]: (2, 3)
         *      > [2]: (23, 4)
         *      > v = 234
         *
         *      v = Ext.Array.reduce([2, 3, 4], reducer, 1);
         *      console.log('v = ' + v);
         *
         *      > [0]: (1, 2)
         *      > [1]: (12, 3)
         *      > [2]: (123, 4)
         *      > v = 1234
         *
         * @param {Array} array The array to process.
         * @param {Function} reduceFn The reducing callback function.
         * @param {Mixed} reduceFn.previous The previous value.
         * @param {Mixed} reduceFn.value The current value.
         * @param {Number} reduceFn.index The index in the array of the current `value`.
         * @param {Array} reduceFn.array The array to being processed.
         * @param {Mixed} [initialValue] The starting value.
         * @return {Mixed} The reduced value.
         * @method reduce
         * @since 6.0.0
         */
            reduce: Array.prototype.reduce ? function(array, reduceFn, initialValue) {
                if (arguments.length === 3) {
                    return Array.prototype.reduce.call(array, reduceFn, initialValue);
                }
                return Array.prototype.reduce.call(array, reduceFn);
            } : function(array, reduceFn, initialValue) {
                array = Object(array);
                if (!Ext.isFunction(reduceFn)) {
                    Ext.raise('Invalid parameter: expected a function.');
                }
                /* eslint-disable-next-line vars-on-top */
                var index = 0,
                    length = array.length >>> 0,
                    reduced = initialValue;
                if (arguments.length < 3) {
                    while (true) {
                        // eslint-disable-line no-constant-condition
                        if (index in array) {
                            reduced = array[index++];
                            break;
                        }
                        if (++index >= length) {
                            throw new TypeError('Reduce of empty array with no initial value');
                        }
                    }
                }
                for (; index < length; ++index) {
                    if (index in array) {
                        reduced = reduceFn(reduced, array[index], index, array);
                    }
                }
                return reduced;
            },
            /**
         * Returns a shallow copy of a part of an array. This is equivalent to the native
         * call `Array.prototype.slice.call(array, begin, end)`. This is often used when "array"
         * is "arguments" since the arguments object does not supply a slice method but can
         * be the context object to `Array.prototype.slice`.
         *
         * @param {Array} array The array (or arguments object).
         * @param {Number} begin The index at which to begin. Negative values are offsets from
         * the end of the array.
         * @param {Number} end The index at which to end. The copied items do not include
         * end. Negative values are offsets from the end of the array. If end is omitted,
         * all items up to the end of the array are copied.
         * @return {Array} The copied piece of the array.
         * @method slice
         */
            // Note: IE8 will return [] on slice.call(x, undefined).
            slice: ([
                1,
                2
            ].slice(1, undefined).length ? function(array, begin, end) {
                return slice.call(array, begin, end);
            } : function(array, begin, end) {
                // see http://jsperf.com/slice-fix
                if (typeof begin === 'undefined') {
                    return slice.call(array);
                }
                if (typeof end === 'undefined') {
                    return slice.call(array, begin);
                }
                return slice.call(array, begin, end);
            }),
            /**
         * Sorts the elements of an Array in a stable manner (equivalently keyed values do not move
         * relative to each other). By default, this method sorts the elements alphabetically and
         * ascending.
         * **Note:** This method modifies the passed array, in the same manner as the
         * native javascript Array.sort. 
         *
         * @param {Array} array The array to sort.
         * @param {Function} [sortFn] The comparison function.
         * @param {Mixed} sortFn.a The first item to compare.
         * @param {Mixed} sortFn.b The second item to compare.
         * @param {Number} sortFn.return `-1` if a < b, `1` if a > b, otherwise `0`.
         * @return {Array} The sorted array.
         */
            sort: function(array, sortFn) {
                return stableSort(array, sortFn || ExtArray.lexicalCompare);
            },
            /**
         * Recursively flattens into 1-d Array. Injects Arrays inline.
         *
         * @param {Array} array The array to flatten
         * @return {Array} The 1-d array.
         */
            flatten: function(array) {
                var worker = [];
                function rFlatten(a) {
                    var i, ln, v;
                    for (i = 0 , ln = a.length; i < ln; i++) {
                        v = a[i];
                        if (Ext.isArray(v)) {
                            rFlatten(v);
                        } else {
                            worker.push(v);
                        }
                    }
                    return worker;
                }
                return rFlatten(array);
            },
            /**
         * Returns the minimum value in the Array.
         *
         * @param {Array/NodeList} array The Array from which to select the minimum value.
         * @param {Function} comparisonFn (optional) a function to perform the comparison which
         * determines minimization.
         * If omitted the "<" operator will be used.
         * __Note:__ gt = 1; eq = 0; lt = -1
         * @param {Mixed} comparisonFn.min Current minimum value.
         * @param {Mixed} comparisonFn.item The value to compare with the current minimum.
         * @return {Object} minValue The minimum value.
         */
            min: function(array, comparisonFn) {
                var min = array[0],
                    i, ln, item;
                for (i = 0 , ln = array.length; i < ln; i++) {
                    item = array[i];
                    if (comparisonFn) {
                        if (comparisonFn(min, item) === 1) {
                            min = item;
                        }
                    } else {
                        if (item < min) {
                            min = item;
                        }
                    }
                }
                return min;
            },
            /**
         * Returns the maximum value in the Array.
         *
         * @param {Array/NodeList} array The Array from which to select the maximum value.
         * @param {Function} comparisonFn (optional) a function to perform the comparison which
         * determines maximization.
         * If omitted the ">" operator will be used.
         * __Note:__ gt = 1; eq = 0; lt = -1
         * @param {Mixed} comparisonFn.max Current maximum value.
         * @param {Mixed} comparisonFn.item The value to compare with the current maximum.
         * @return {Object} maxValue The maximum value.
         */
            max: function(array, comparisonFn) {
                var max = array[0],
                    i, ln, item;
                for (i = 0 , ln = array.length; i < ln; i++) {
                    item = array[i];
                    if (comparisonFn) {
                        if (comparisonFn(max, item) === -1) {
                            max = item;
                        }
                    } else {
                        if (item > max) {
                            max = item;
                        }
                    }
                }
                return max;
            },
            /**
         * Calculates the mean of all items in the array.
         *
         * @param {Array} array The Array to calculate the mean value of.
         * @return {Number} The mean.
         */
            mean: function(array) {
                return array.length > 0 ? ExtArray.sum(array) / array.length : undefined;
            },
            /**
         * Calculates the sum of all items in the given array.
         *
         * @param {Array} array The Array to calculate the sum value of.
         * @return {Number} The sum.
         */
            sum: function(array) {
                var sum = 0,
                    i, ln, item;
                for (i = 0 , ln = array.length; i < ln; i++) {
                    item = array[i];
                    sum += item;
                }
                return sum;
            },
            /**
         * Creates a map (object) keyed by the elements of the given array. The values in
         * the map are the index+1 of the array element. For example:
         * 
         *      var map = Ext.Array.toMap(['a','b','c']);
         *
         *      // map = { a: 1, b: 2, c: 3 };
         * 
         * Or a key property can be specified:
         * 
         *      var map = Ext.Array.toMap([
         *              { name: 'a' },
         *              { name: 'b' },
         *              { name: 'c' }
         *          ], 'name');
         *
         *      // map = { a: 1, b: 2, c: 3 };
         * 
         * Lastly, a key extractor can be provided:
         * 
         *      var map = Ext.Array.toMap([
         *              { name: 'a' },
         *              { name: 'b' },
         *              { name: 'c' }
         *          ], function(obj) { return obj.name.toUpperCase(); });
         *
         *      // map = { A: 1, B: 2, C: 3 };
         * 
         * @param {String/String[]} strings The strings from which to create the map.
         * @param {String/Function} [getKey] Name of the object property to use
         * as a key or a function to extract the key.
         * @param {Object} [scope] Value of `this` inside callback specified for `getKey`.
         * @return {Object} The resulting map.
         */
            toMap: function(strings, getKey, scope) {
                var map, i;
                if (!strings) {
                    return null;
                }
                map = {};
                i = strings.length;
                if (typeof strings === 'string') {
                    map[strings] = 1;
                } else if (!getKey) {
                    while (i--) {
                        map[strings[i]] = i + 1;
                    }
                } else if (typeof getKey === 'string') {
                    while (i--) {
                        map[strings[i][getKey]] = i + 1;
                    }
                } else {
                    while (i--) {
                        map[getKey.call(scope, strings[i])] = i + 1;
                    }
                }
                return map;
            },
            /**
         * Creates a map (object) keyed by a property of elements of the given array. The values in
         * the map are the array element. For example:
         * 
         *      var map = Ext.Array.toValueMap(['a','b','c']);
         *
         *      // map = { a: 'a', b: 'b', c: 'c' };
         * 
         * Or a key property can be specified:
         * 
         *      var map = Ext.Array.toValueMap([
         *              { name: 'a' },
         *              { name: 'b' },
         *              { name: 'c' }
         *          ], 'name');
         *
         *      // map = { a: {name: 'a'}, b: {name: 'b'}, c: {name: 'c'} };
         * 
         * Lastly, a key extractor can be provided:
         * 
         *      var map = Ext.Array.toValueMap([
         *              { name: 'a' },
         *              { name: 'b' },
         *              { name: 'c' }
         *          ], function(obj) { return obj.name.toUpperCase(); });
         *
         *      // map = { A: {name: 'a'}, B: {name: 'b'}, C: {name: 'c'} };
         *
         * @param {Array} array The Array to create the map from.
         * @param {String/Function} [getKey] Name of the object property to use
         * as a key or a function to extract the key.
         * @param {Object} [scope] Value of this inside callback. This parameter is only
         * passed when `getKey` is a function. If `getKey` is not a function, the 3rd
         * argument is `arrayify`.
         * @param {Number} [arrayify] Pass `1` to create arrays for all map entries
         * or `2` to create arrays for map entries that have 2 or more items with the
         * same key. This only applies when `getKey` is specified. By default the map will
         * hold the last entry with a given key.
         * @return {Object} The resulting map.
         */
            toValueMap: function(array, getKey, scope, arrayify) {
                var map = {},
                    i = array.length,
                    autoArray, alwaysArray, entry, fn, key, value;
                if (!getKey) {
                    while (i--) {
                        value = array[i];
                        map[value] = value;
                    }
                } else {
                    if (!(fn = (typeof getKey !== 'string'))) {
                        arrayify = scope;
                    }
                    alwaysArray = arrayify === 1;
                    autoArray = arrayify === 2;
                    while (i--) {
                        value = array[i];
                        key = fn ? getKey.call(scope, value) : value[getKey];
                        if (alwaysArray) {
                            if (key in map) {
                                map[key].push(value);
                            } else {
                                map[key] = [
                                    value
                                ];
                            }
                        } else if (autoArray && (key in map)) {
                            if ((entry = map[key]) instanceof Array) {
                                entry.push(value);
                            } else {
                                map[key] = [
                                    entry,
                                    value
                                ];
                            }
                        } else {
                            map[key] = value;
                        }
                    }
                }
                return map;
            },
            _replaceSim: replaceSim,
            // for unit testing
            _spliceSim: spliceSim,
            /**
         * Removes items from an array. This is functionally equivalent to the splice method
         * of Array, but works around bugs in IE8's splice method and does not copy the
         * removed elements in order to return them (because very often they are ignored).
         *
         * @param {Array} array The Array on which to replace.
         * @param {Number} index The index in the array at which to operate.
         * @param {Number} removeCount The number of items to remove at index.
         * @return {Array} The array passed.
         * @method
         */
            erase: erase,
            /**
         * Inserts items in to an array.
         *
         * @param {Array} array The Array in which to insert.
         * @param {Number} index The index in the array at which to operate.
         * @param {Array} items The array of items to insert at index.
         * @return {Array} The array passed.
         */
            insert: function(array, index, items) {
                return replace(array, index, 0, items);
            },
            move: function(array, fromIdx, toIdx) {
                if (toIdx === fromIdx) {
                    return;
                }
                /* eslint-disable-next-line vars-on-top */
                var item = array[fromIdx],
                    incr = toIdx > fromIdx ? 1 : -1,
                    i;
                for (i = fromIdx; i !== toIdx; i += incr) {
                    array[i] = array[i + incr];
                }
                array[toIdx] = item;
            },
            /**
         * Replaces items in an array. This is functionally equivalent to the splice method
         * of Array, but works around bugs in IE8's splice method and is often more convenient
         * to call because it accepts an array of items to insert rather than use a variadic
         * argument list.
         *
         * @param {Array} array The Array on which to replace.
         * @param {Number} index The index in the array at which to operate.
         * @param {Number} removeCount The number of items to remove at index (can be 0).
         * @param {Array} insert (optional) An array of items to insert at index.
         * @return {Array} The array passed.
         * @method
         */
            replace: replace,
            /**
         * Replaces items in an array. This is equivalent to the splice method of Array, but
         * works around bugs in IE8's splice method. The signature is exactly the same as the
         * splice method except that the array is the first argument. All arguments following
         * removeCount are inserted in the array at index.
         *
         * @param {Array} array The Array on which to replace.
         * @param {Number} index The index in the array at which to operate.
         * @param {Number} removeCount The number of items to remove at index (can be 0).
         * @param {Object...} elements The elements to add to the array. If you don't specify
         * any elements, splice simply removes elements from the array.
         * @return {Array} An array containing the removed items.
         * @method
         */
            splice: splice,
            /**
         * Pushes new items onto the end of an Array.
         *
         * Passed parameters may be single items, or arrays of items. If an Array is found in the
         * argument list, all its elements are pushed into the end of the target Array.
         *
         * @param {Array} target The Array onto which to push new items
         * @param {Object...} elements The elements to add to the array. Each parameter may
         * be an Array, in which case all the elements of that Array will be pushed into the end
         * of the destination Array.
         * @return {Array} An array containing all the new items push onto the end.
         */
            push: function(target) {
                var args = arguments,
                    len = args.length,
                    i, newItem;
                if (target === undefined) {
                    target = [];
                } else if (!Ext.isArray(target)) {
                    target = [
                        target
                    ];
                }
                for (i = 1; i < len; i++) {
                    newItem = args[i];
                    Array.prototype.push[Ext.isIterable(newItem) ? 'apply' : 'call'](target, newItem);
                }
                return target;
            },
            /**
         * A function used to sort an array by numeric value. By default, javascript array values
         * are coerced to strings when sorting, which can be problematic when using numeric values.
         * To ensure that the values are sorted numerically, this method can be passed to the sort
         * method:
         * 
         *     Ext.Array.sort(myArray, Ext.Array.numericSortFn);
         */
            numericSortFn: function(a, b) {
                return a - b;
            }
        };
    /**
     * @method each
     * @member Ext
     * @inheritdoc Ext.Array#each
     */
    Ext.each = ExtArray.each;
    /**
     * @method union
     * @member Ext.Array
     * @inheritdoc Ext.Array#merge
     */
    ExtArray.union = ExtArray.merge;
    /**
     * Old alias to {@link Ext.Array#min}
     * @deprecated 4.0.0 Use {@link Ext.Array#min} instead
     * @method min
     * @member Ext
     * @inheritdoc Ext.Array#min
     */
    Ext.min = ExtArray.min;
    /**
     * Old alias to {@link Ext.Array#max}
     * @deprecated 4.0.0 Use {@link Ext.Array#max} instead
     * @method max
     * @member Ext
     * @inheritdoc Ext.Array#max
     */
    Ext.max = ExtArray.max;
    /**
     * Old alias to {@link Ext.Array#sum}
     * @deprecated 4.0.0 Use {@link Ext.Array#sum} instead
     * @method sum
     * @member Ext
     * @inheritdoc Ext.Array#sum
     */
    Ext.sum = ExtArray.sum;
    /**
     * Old alias to {@link Ext.Array#mean}
     * @deprecated 4.0.0 Use {@link Ext.Array#mean} instead
     * @method mean
     * @member Ext
     * @inheritdoc Ext.Array#mean
     */
    Ext.mean = ExtArray.mean;
    /**
     * Old alias to {@link Ext.Array#flatten}
     * @deprecated 4.0.0 Use {@link Ext.Array#flatten} instead
     * @method flatten
     * @member Ext
     * @inheritdoc Ext.Array#flatten
     */
    Ext.flatten = ExtArray.flatten;
    /**
     * Old alias to {@link Ext.Array#clean}
     * @deprecated 4.0.0 Use {@link Ext.Array#clean} instead
     * @method clean
     * @member Ext
     * @inheritdoc Ext.Array#clean
     */
    Ext.clean = ExtArray.clean;
    /**
     * Old alias to {@link Ext.Array#unique}
     * @deprecated 4.0.0 Use {@link Ext.Array#unique} instead
     * @method unique
     * @member Ext
     * @inheritdoc Ext.Array#unique
     */
    Ext.unique = ExtArray.unique;
    /**
     * Old alias to {@link Ext.Array#pluck Ext.Array.pluck}
     * @deprecated 4.0.0 Use {@link Ext.Array#pluck Ext.Array.pluck} instead
     * @method pluck
     * @member Ext
     * @inheritdoc Ext.Array#pluck
     */
    Ext.pluck = ExtArray.pluck;
    /**
     * @method toArray
     * @member Ext
     * @inheritdoc Ext.Array#toArray
     */
    Ext.toArray = function() {
        return ExtArray.toArray.apply(ExtArray, arguments);
    };
    return ExtArray;
}());

// @define Ext.lang.Assert
// @define Ext.Assert
// @require Ext.lang.Error
/**
 * @class Ext.Assert
 * This class provides help value testing methods useful for diagnostics. These are often
 * used in `debugHooks`:
 * 
 *      Ext.define('Foo.bar.Class', {
 *
 *          debugHooks: {
 *              method: function (a) {
 *                  Ext.Assert.truthy(a, 'Expected "a" to be truthy');
 *              },
 *
 *              foo: function (object) {
 *                  Ext.Assert.isFunctionProp(object, 'doSomething');
 *              }
 *          }
 *      });
 * 
 * **NOTE:** This class is entirely removed in production builds so all uses of it should
 * be either in `debug` conditional comments or `debugHooks`.
 * 
 * The following type detection methods from the `Ext` object are wrapped as assertions
 * by this class:
 * 
 *  * `isEmpty`
 *  * `isArray`
 *  * `isDate`
 *  * `isObject`
 *  * `isSimpleObject`
 *  * `isPrimitive`
 *  * `isFunction`
 *  * `isNumber`
 *  * `isNumeric`
 *  * `isString`
 *  * `isBoolean`
 *  * `isElement`
 *  * `isTextNode`
 *  * `isDefined`
 *  * `isIterable`
 * 
 * These appear both their exact name and with a "Prop" suffix for checking a property on
 * an object. For example, these are almost identical:
 * 
 *      Ext.Assert.isFunction(object.foo);
 *
 *      Ext.Assert.isFunctionProp(object, 'foo');
 *
 * The difference is the default error message generated is better in the second use case
 * than the first.
 * 
 * The above list are also expanded for "Not" flavors (and "Not...Prop"):
 * 
 *  * `isNotEmpty`
 *  * `isNotArray`
 *  * `isNotDate`
 *  * `isNotObject`
 *  * `isNotSimpleObject`
 *  * `isNotPrimitive`
 *  * `isNotFunction`
 *  * `isNotNumber`
 *  * `isNotNumeric`
 *  * `isNotString`
 *  * `isNotBoolean`
 *  * `isNotElement`
 *  * `isNotTextNode`
 *  * `isNotDefined`
 *  * `isNotIterable`
 */
Ext.Assert = {
    /**
     * Checks that the first argument is falsey and throws an `Error` if it is not.
     */
    falsey: function(b, msg) {
        if (b) {
            Ext.raise(msg || ('Expected a falsey value but was ' + b));
        }
    },
    /**
     * Checks that the first argument is falsey and throws an `Error` if it is not.
     */
    falseyProp: function(object, property) {
        var b;
        Ext.Assert.truthy(object);
        b = object[property];
        if (b) {
            if (object.$className) {
                property = object.$className + '#' + property;
            }
            Ext.raise('Expected a falsey value for ' + property + ' but was ' + b);
        }
    },
    /**
     * Checks that the first argument is truthy and throws an `Error` if it is not.
     */
    truthy: function(b, msg) {
        if (!b) {
            Ext.raise(msg || ('Expected a truthy value but was ' + typeof b));
        }
    },
    /**
     * Checks that the first argument is truthy and throws an `Error` if it is not.
     */
    truthyProp: function(object, property) {
        var b;
        Ext.Assert.truthy(object);
        b = object[property];
        if (!b) {
            if (object.$className) {
                property = object.$className + '#' + property;
            }
            Ext.raise('Expected a truthy value for ' + property + ' but was ' + typeof b);
        }
    }
};
/* eslint-disable indent */
(function() {
    var name, kind;
    function makeAssert(name, kind) {
        var testFn = Ext[name],
            def;
        return function(value, msg) {
            if (!testFn(value)) {
                Ext.raise(msg || def || (def = 'Expected value to be ' + kind));
            }
        };
    }
    function makeAssertProp(name, kind) {
        var testFn = Ext[name],
            def;
        return function(object, prop) {
            Ext.Assert.truthy(object);
            if (!testFn(object[prop])) {
                Ext.raise(def || (def = 'Expected ' + (object.$className ? object.$className + '#' : '') + prop + ' to be ' + kind));
            }
        };
    }
    function makeNotAssert(name, kind) {
        var testFn = Ext[name],
            def;
        return function(value, msg) {
            if (testFn(value)) {
                Ext.raise(msg || def || (def = 'Expected value to NOT be ' + kind));
            }
        };
    }
    function makeNotAssertProp(name, kind) {
        var testFn = Ext[name],
            def;
        return function(object, prop) {
            Ext.Assert.truthy(object);
            if (testFn(object[prop])) {
                Ext.raise(def || (def = 'Expected ' + (object.$className ? object.$className + '#' : '') + prop + ' to NOT be ' + kind));
            }
        };
    }
    for (name in Ext) {
        if (name.substring(0, 2) === "is" && Ext.isFunction(Ext[name])) {
            kind = name.substring(2);
            Ext.Assert[name] = makeAssert(name, kind);
            Ext.Assert[name + 'Prop'] = makeAssertProp(name, kind);
            Ext.Assert['isNot' + kind] = makeNotAssert(name, kind);
            Ext.Assert['isNot' + kind + 'Prop'] = makeNotAssertProp(name, kind);
        }
    }
}());

/**
 * @class Ext.String
 *
 * A collection of useful static methods to deal with strings.
 * @singleton
 */
/* eslint-disable indent */
Ext.String = (function() {
    // @define Ext.lang.String
    // @define Ext.String
    // @require Ext
    // @require Ext.lang.Array
    // eslint-disable-next-line no-control-regex
    var trimRegex = /^[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000]+|[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000]+$/g,
        escapeRe = /('|\\)/g,
        // eslint-disable-next-line no-useless-escape
        escapeRegexRe = /([-.*+?\^${}()|\[\]\/\\])/g,
        basicTrimRe = /^\s+|\s+$/g,
        whitespaceRe = /\s+/,
        varReplace = /(^[^a-z]*|[^\w])/gi,
        charToEntity, entityToChar, charToEntityRegex, entityToCharRegex,
        htmlEncodeReplaceFn = function(match, capture) {
            return charToEntity[capture];
        },
        htmlDecodeReplaceFn = function(match, capture) {
            return (capture in entityToChar) ? entityToChar[capture] : String.fromCharCode(parseInt(capture.substr(2), 10));
        },
        boundsCheck = function(s, other) {
            if (s === null || s === undefined || other === null || other === undefined) {
                return false;
            }
            return other.length <= s.length;
        },
        fromCharCode = String.fromCharCode,
        ExtString;
    return ExtString = {
        /**
         * @method
         * Creates a string created by using the specified sequence of code points.
         * @param {Number...} codePoint Codepoints from which to build the string.
         * @return {String} A string built from the sequence of code points passed.
         */
        fromCodePoint: String.fromCodePoint || function() {
            var codePoint,
                result = '',
                codeUnits = [],
                index = -1,
                length = arguments.length;
            while (++index < length) {
                codePoint = Number(arguments[index]);
                if (!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
                codePoint < 0 || // not a valid Unicode code point
                codePoint > 1114111 || // not a valid Unicode code point
                Math.floor(codePoint) !== codePoint) // not an integer
                {
                    Ext.raise('Invalid code point: ' + codePoint);
                }
                if (codePoint <= 65535) {
                    // BMP code point
                    codeUnits.push(codePoint);
                } else {
                    // Astral code point; split in surrogate halves
                    // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
                    codePoint -= 65536;
                    codeUnits.push((codePoint >> 10) + 55296, (codePoint % 1024) + 56320);
                }
                if (index + 1 === length) {
                    result += fromCharCode(codeUnits);
                    codeUnits.length = 0;
                }
            }
            return result;
        },
        /**
         * Inserts a substring into a string.
         * @param {String} s The original string.
         * @param {String} value The substring to insert.
         * @param {Number} index The index to insert the substring. Negative indexes will insert
         * from the end of the string. Example: 
         *
         *     Ext.String.insert("abcdefg", "h", -1); // abcdefhg
         *
         * @return {String} The value with the inserted substring
         */
        insert: function(s, value, index) {
            var len;
            if (!s) {
                return value;
            }
            if (!value) {
                return s;
            }
            len = s.length;
            if (!index && index !== 0) {
                index = len;
            }
            if (index < 0) {
                index *= -1;
                if (index >= len) {
                    // negative overflow, insert at start
                    index = 0;
                } else {
                    index = len - index;
                }
            }
            if (index === 0) {
                s = value + s;
            } else if (index >= s.length) {
                s += value;
            } else {
                s = s.substr(0, index) + value + s.substr(index);
            }
            return s;
        },
        /**
         * Checks if a string starts with a substring
         * @param {String} s The original string
         * @param {String} start The substring to check
         * @param {Boolean} [ignoreCase=false] True to ignore the case in the comparison
         */
        startsWith: function(s, start, ignoreCase) {
            var result = boundsCheck(s, start);
            if (result) {
                if (ignoreCase) {
                    s = s.toLowerCase();
                    start = start.toLowerCase();
                }
                result = s.lastIndexOf(start, 0) === 0;
            }
            return result;
        },
        /**
         * Checks if a string ends with a substring
         * @param {String} s The original string
         * @param {String} end The substring to check
         * @param {Boolean} [ignoreCase=false] True to ignore the case in the comparison
         */
        endsWith: function(s, end, ignoreCase) {
            var result = boundsCheck(s, end);
            if (result) {
                if (ignoreCase) {
                    s = s.toLowerCase();
                    end = end.toLowerCase();
                }
                result = s.indexOf(end, s.length - end.length) !== -1;
            }
            return result;
        },
        /**
         * Converts a string of characters into a legal, parse-able JavaScript `var` name
         * as long as the passed string contains at least one alphabetic character.
         * Non alphanumeric characters, and *leading* non alphabetic characters will be removed.
         * @param {String} s A string to be converted into a `var` name.
         * @return {String} A legal JavaScript `var` name.
         */
        createVarName: function(s) {
            return s.replace(varReplace, '');
        },
        /**
         * Convert certain characters (&, <, >, ', and ") to their HTML character equivalents
         * for literal display in web pages.
         * @param {String} value The string to encode.
         * @return {String} The encoded text.
         * @method
         */
        htmlEncode: function(value) {
            return (!value) ? value : String(value).replace(charToEntityRegex, htmlEncodeReplaceFn);
        },
        /**
         * Convert certain characters (&, <, >, ', and ") from their HTML character equivalents.
         * @param {String} value The string to decode.
         * @return {String} The decoded text.
         * @method
         */
        htmlDecode: function(value) {
            return (!value) ? value : String(value).replace(entityToCharRegex, htmlDecodeReplaceFn);
        },
        /**
         * Checks if a string has values needing to be html encoded.
         * @private
         * @param {String} s The string to test
         * @return {Boolean} `true` if the string contains HTML characters
         */
        hasHtmlCharacters: function(s) {
            return charToEntityRegex.test(s);
        },
        /**
         * Adds a set of character entity definitions to the set used by
         * {@link Ext.String#htmlEncode} and {@link Ext.String#htmlDecode}.
         *
         * This object should be keyed by the entity name sequence,
         * with the value being the textual representation of the entity.
         *
         *      Ext.String.addCharacterEntities({
         *          '&amp;Uuml;':'Ü',
         *          '&amp;ccedil;':'ç',
         *          '&amp;ntilde;':'ñ',
         *          '&amp;egrave;':'è'
         *      });
         *      var s = Ext.String.htmlEncode("A string with entities: èÜçñ");
         *
         * __Note:__ the values of the character entities defined on this object are expected
         * to be single character values.  As such, the actual values represented by the
         * characters are sensitive to the character encoding of the JavaScript source
         * file when defined in string literal form. Script tags referencing server
         * resources with character entities must ensure that the 'charset' attribute
         * of the script node is consistent with the actual character encoding of the
         * server resource.
         *
         * The set of character entities may be reset back to the default state by using
         * the {@link Ext.String#resetCharacterEntities} method
         *
         * @param {Object} newEntities The set of character entities to add to the current
         * definitions.
         */
        addCharacterEntities: function(newEntities) {
            var charKeys = [],
                entityKeys = [],
                key, echar;
            for (key in newEntities) {
                echar = newEntities[key];
                entityToChar[key] = echar;
                charToEntity[echar] = key;
                charKeys.push(echar);
                entityKeys.push(key);
            }
            charToEntityRegex = new RegExp('(' + charKeys.join('|') + ')', 'g');
            entityToCharRegex = new RegExp('(' + entityKeys.join('|') + '|&#[0-9]{1,5};' + ')', 'g');
        },
        /**
         * Resets the set of character entity definitions used by
         * {@link Ext.String#htmlEncode} and {@link Ext.String#htmlDecode} back to the
         * default state.
         */
        resetCharacterEntities: function() {
            charToEntity = {};
            entityToChar = {};
            // add the default set
            this.addCharacterEntities({
                '&amp;': '&',
                '&gt;': '>',
                '&lt;': '<',
                '&quot;': '"',
                '&#39;': "'"
            });
        },
        /**
         * Appends content to the query string of a URL, handling logic for whether to place
         * a question mark or ampersand.
         * @param {String} url The URL to append to.
         * @param {String} string The content to append to the URL.
         * @return {String} The resulting URL
         */
        urlAppend: function(url, string) {
            if (!Ext.isEmpty(string)) {
                return url + (url.indexOf('?') === -1 ? '?' : '&') + string;
            }
            return url;
        },
        /**
         * Trims whitespace from either end of a string, leaving spaces within the string intact.
         * Example:
         *
         *     var s = '  foo bar  ';
         *     alert('-' + s + '-');                   //alerts "- foo bar -"
         *     alert('-' + Ext.String.trim(s) + '-');  //alerts "-foo bar-"
         *
         * @param {String} string The string to trim.
         * @return {String} The trimmed string.
         */
        trim: function(string) {
            if (string) {
                string = string.replace(trimRegex, "");
            }
            return string || '';
        },
        /**
         * Capitalize the first letter of the given string.
         * @param {String} string
         * @return {String}
         */
        capitalize: function(string) {
            if (string) {
                string = string.charAt(0).toUpperCase() + string.substr(1);
            }
            return string || '';
        },
        /**
         * Uncapitalize the first letter of a given string.
         * @param {String} string
         * @return {String}
         */
        uncapitalize: function(string) {
            if (string) {
                string = string.charAt(0).toLowerCase() + string.substr(1);
            }
            return string || '';
        },
        /**
         * Truncate a string and add an ellipsis ('...') to the end if it exceeds
         * the specified length.
         * @param {String} value The string to truncate.
         * @param {Number} length The maximum length to allow before truncating.
         * @param {Boolean} [word=false] `true` to try to find a common word break.
         * @return {String} The converted text.
         */
        ellipsis: function(value, length, word) {
            var vs, index;
            if (value && value.length > length) {
                if (word) {
                    vs = value.substr(0, length - 2);
                    index = Math.max(vs.lastIndexOf(' '), vs.lastIndexOf('.'), vs.lastIndexOf('!'), vs.lastIndexOf('?'));
                    if (index !== -1 && index >= (length - 15)) {
                        return vs.substr(0, index) + "...";
                    }
                }
                return value.substr(0, length - 3) + "...";
            }
            return value;
        },
        /**
         * Escapes the passed string for use in a regular expression.
         * @param {String} string The string to escape.
         * @return {String} The escaped string.
         */
        escapeRegex: function(string) {
            return string.replace(escapeRegexRe, "\\$1");
        },
        /**
         * Creates a `RegExp` given a string `value` and optional flags. For example, the
         * following two regular expressions are equivalent.
         *
         *      var regex1 = Ext.String.createRegex('hello');
         *
         *      var regex2 = /^hello$/i;
         *
         * The following two regular expressions are also equivalent:
         *
         *      var regex1 = Ext.String.createRegex('world', false, false, false);
         *
         *      var regex2 = /world/;
         *
         * @param {String/RegExp} value The String to convert to a `RegExp`.
         * @param {Boolean} [startsWith=true] Pass `false` to allow a match to start 
         * anywhere in the string. By default the `value` will match only at the start 
         * of the string.
         * @param {Boolean} [endsWith=true] Pass `false` to allow the match to end before
         * the end of the string. By default the `value` will match only at the end of the
         * string.
         * @param {Boolean} [ignoreCase=true] Pass `false` to make the `RegExp` case
         * sensitive (removes the 'i' flag).
         * @since 5.0.0
         * @return {RegExp}
         */
        createRegex: function(value, startsWith, endsWith, ignoreCase) {
            var ret = value;
            if (value != null && !value.exec) {
                // not a regex
                ret = ExtString.escapeRegex(String(value));
                if (startsWith !== false) {
                    ret = '^' + ret;
                }
                if (endsWith !== false) {
                    ret += '$';
                }
                ret = new RegExp(ret, (ignoreCase !== false) ? 'i' : '');
            }
            return ret;
        },
        /**
         * Escapes the passed string for ' and \.
         * @param {String} string The string to escape.
         * @return {String} The escaped string.
         */
        escape: function(string) {
            return string.replace(escapeRe, "\\$1");
        },
        /**
         * Utility function that allows you to easily switch a string between two alternating
         * values. The passed value is compared to the current string, and if they are equal,
         * the other value that was passed in is returned. If they are already different,
         * the first value passed in is returned.  Note that this method returns the new value
         * but does not change the current string.
         *
         *     // alternate sort directions
         *     sort = Ext.String.toggle(sort, 'ASC', 'DESC');
         *
         *     // instead of conditional logic:
         *     sort = (sort === 'ASC' ? 'DESC' : 'ASC');
         *
         * @param {String} string The current string.
         * @param {String} value The value to compare to the current string.
         * @param {String} other The new value to use if the string already equals the first value
         * passed in.
         * @return {String} The new value.
         */
        toggle: function(string, value, other) {
            return string === value ? other : value;
        },
        /**
         * Pads the left side of a string with a specified character.  This is especially useful
         * for normalizing number and date strings.  Example usage:
         *
         *     var s = Ext.String.leftPad('123', 5, '0');
         *     // s now contains the string: '00123'
         *
         * @param {String} string The original string.
         * @param {Number} size The total length of the output string.
         * @param {String} [character=' '] (optional) The character with which to pad the original
         * string.
         * @return {String} The padded string.
         */
        leftPad: function(string, size, character) {
            var result = String(string);
            character = character || " ";
            while (result.length < size) {
                result = character + result;
            }
            return result;
        },
        /**
         * Returns a string with a specified number of repetitions a given string pattern.
         * The pattern be separated by a different string.
         *
         *      var s = Ext.String.repeat('---', 4); // = '------------'
         *      var t = Ext.String.repeat('--', 3, '/'); // = '--/--/--'
         *
         * @param {String} pattern The pattern to repeat.
         * @param {Number} count The number of times to repeat the pattern (may be 0).
         * @param {String} sep An option string to separate each pattern.
         */
        repeat: function(pattern, count, sep) {
            var buf = [],
                i;
            if (count < 1) {
                count = 0;
            }
            for (i = count; i--; ) {
                buf.push(pattern);
            }
            return buf.join(sep || '');
        },
        /**
         * Splits a string of space separated words into an array, trimming as needed. If the
         * words are already an array, it is returned.
         *
         * @param {String/Array} words
         */
        splitWords: function(words) {
            if (words && typeof words === 'string') {
                return words.replace(basicTrimRe, '').split(whitespaceRe);
            }
            return words || [];
        }
    };
}());
// initialize the default encode / decode entities
Ext.String.resetCharacterEntities();
/**
 * @method htmlEncode
 * @member Ext
 * @inheritdoc Ext.String#htmlEncode
 */
Ext.htmlEncode = Ext.String.htmlEncode;
/**
 * @method htmlDecode
 * @member Ext
 * @inheritdoc Ext.String#htmlDecode
 */
Ext.htmlDecode = Ext.String.htmlDecode;
/**
 * @method urlAppend
 * @member Ext
 * @inheritdoc Ext.String#urlAppend
 */
Ext.urlAppend = Ext.String.urlAppend;

/* eslint-disable max-len */
/**
 * @class Ext.Date
 * This class defines some basic methods for handling dates.
 *
 * The date parsing and formatting syntax contains a subset of
 * [PHP's `date()` function](http://www.php.net/date), and the formats that are
 * supported will provide results equivalent to their PHP versions.
 *
 * The following is a list of all currently supported formats:
 *
 *      Format      Description                                                               Example returned values
 *      ------      -----------------------------------------------------------------------   -----------------------
 *        d         Day of the month, 2 digits with leading zeros                             01 to 31
 *        D         A short textual representation of the day of the week                     Mon to Sun
 *        j         Day of the month without leading zeros                                    1 to 31
 *        l         A full textual representation of the day of the week                      Sunday to Saturday
 *        N         ISO-8601 numeric representation of the day of the week                    1 (for Monday) through 7 (for Sunday)
 *        S         English ordinal suffix for the day of the month, 2 characters             st, nd, rd or th. Works well with j
 *        w         Numeric representation of the day of the week                             0 (for Sunday) to 6 (for Saturday)
 *        z         The day of the year (starting from 0)                                     0 to 364 (365 in leap years)
 *        W         ISO-8601 week number of year, weeks starting on Monday                    01 to 53
 *        F         A full textual representation of a month, such as January or March        January to December
 *        m         Numeric representation of a month, with leading zeros                     01 to 12
 *        M         A short textual representation of a month                                 Jan to Dec
 *        n         Numeric representation of a month, without leading zeros                  1 to 12
 *        t         Number of days in the given month                                         28 to 31
 *        L         Whether it&#39;s a leap year                                                  1 if it is a leap year, 0 otherwise.
 *        o         ISO-8601 year number (identical to (Y), but if the ISO week number (W)    Examples: 1998 or 2004
 *                  belongs to the previous or next year, that year is used instead)
 *        Y         A full numeric representation of a year, 4 digits                         Examples: 1999 or 2003
 *        y         A two digit representation of a year                                      Examples: 99 or 03
 *        a         Lowercase Ante meridiem and Post meridiem                                 am or pm
 *        A         Uppercase Ante meridiem and Post meridiem                                 AM or PM
 *        g         12-hour format of an hour without leading zeros                           1 to 12
 *        G         24-hour format of an hour without leading zeros                           0 to 23
 *        h         12-hour format of an hour with leading zeros                              01 to 12
 *        H         24-hour format of an hour with leading zeros                              00 to 23
 *        i         Minutes, with leading zeros                                               00 to 59
 *        s         Seconds, with leading zeros                                               00 to 59
 *        u         Decimal fraction of a second                                              Examples:
 *                  (minimum 1 digit, arbitrary number of digits allowed)                     001 (i.e. 0.001s) or
 *                                                                                            100 (i.e. 0.100s) or
 *                                                                                            999 (i.e. 0.999s) or
 *                                                                                            999876543210 (i.e. 0.999876543210s)
 *        O         Difference to Greenwich time (GMT) in hours and minutes                   Example: +1030
 *        P         Difference to Greenwich time (GMT) with colon between hours and minutes   Example: -08:00
 *        T         Timezone abbreviation of the machine running the code                     Examples: EST, MDT, PDT ...
 *        Z         Timezone offset in seconds (negative if west of UTC, positive if east)    -43200 to 50400
 *        c         ISO 8601 date represented as the local time with an offset to UTC appended.
 *                  Notes:                                                                    Examples:
 *                  1) If unspecified, the month / day defaults to the current month / day,   1991 or
 *                     the time defaults to midnight, while the timezone defaults to the      1992-10 or
 *                     browser's timezone. If a time is specified, it must include both hours 1993-09-20 or
 *                     and minutes. The "T" delimiter, seconds, milliseconds and timezone     1994-08-19T16:20+01:00 or
 *                     are optional.                                                          1995-07-18T17:21:28-02:00 or
 *                  2) The decimal fraction of a second, if specified, must contain at        1996-06-17T18:22:29.98765+03:00 or
 *                     least 1 digit (there is no limit to the maximum number                 1997-05-16T19:23:30,12345-0400 or
 *                     of digits allowed), and may be delimited by either a '.' or a ','      1998-04-15T20:24:31.2468Z or
 *                  Refer to the examples on the right for the various levels of              1999-03-14T20:24:32Z or
 *                  date-time granularity which are supported, or see                         2000-02-13T21:25:33
 *                  http://www.w3.org/TR/NOTE-datetime for more info.                         2001-01-12 22:26:34
 *        C         An ISO date string as implemented by the native Date object's             1962-06-17T09:21:34.125Z
 *                  [Date.toISOString](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
 *                  method. This outputs the numeric part with *UTC* hour and minute
 *                  values, and indicates this by appending the `'Z'` timezone
 *                  identifier.
 *        U         Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT)                1193432466 or -2138434463
 *        MS        Microsoft AJAX serialized dates                                           \/Date(1238606590509)\/ (i.e. UTC milliseconds since epoch) or
 *                                                                                            \/Date(1238606590509+0800)\/
 *        time      A javascript millisecond timestamp                                        1350024476440
 *        timestamp A UNIX timestamp (same as U)                                              1350024866
 *
 * Example usage (note that you must escape format specifiers with '\\' to render them as character literals):
 *
 *     // Sample date:
 *     // 'Wed Jan 10 2007 15:05:01 GMT-0600 (Central Standard Time)'
 *
 *     var dt = new Date('1/10/2007 03:05:01 PM GMT-0600');
 *     console.log(Ext.Date.format(dt, 'Y-m-d'));                          // 2007-01-10
 *     console.log(Ext.Date.format(dt, 'F j, Y, g:i a'));                  // January 10, 2007, 3:05 pm
 *     console.log(Ext.Date.format(dt, 'l, \\t\\he jS \\of F Y h:i:s A')); // Wednesday, the 10th of January 2007 03:05:01 PM
 *
 * Here are some standard date/time patterns that you might find helpful.  They
 * are not part of the source of Ext.Date, but to use them you can simply copy this
 * block of code into any script that is included after Ext.Date and they will also become
 * globally available on the Date object.  Feel free to add or remove patterns as needed in your code.
 *
 *     Ext.Date.patterns = {
 *         ISO8601Long:"Y-m-d H:i:s",
 *         ISO8601Short:"Y-m-d",
 *         ShortDate: "n/j/Y",
 *         LongDate: "l, F d, Y",
 *         FullDateTime: "l, F d, Y g:i:s A",
 *         MonthDay: "F d",
 *         ShortTime: "g:i A",
 *         LongTime: "g:i:s A",
 *         SortableDateTime: "Y-m-d\\TH:i:s",
 *         UniversalSortableDateTime: "Y-m-d H:i:sO",
 *         YearMonth: "F, Y"
 *     };
 *
 * Example usage:
 *
 *     var dt = new Date();
 *     console.log(Ext.Date.format(dt, Ext.Date.patterns.ShortDate));
 *
 * Developer-written, custom formats may be used by supplying both a formatting and a parsing function
 * which perform to specialized requirements. The functions are stored in {@link #parseFunctions} and {@link #formatFunctions}.
 * @singleton
 */
Ext.Date = (function() {
    /* eslint-disable indent */
    // @define Ext.lang.Date
    // @define Ext.Date
    // @require Ext
    // @require Ext.lang.String
    var utilDate,
        nativeDate = Date,
        stripEscapeRe = /(\\.)/g,
        hourInfoRe = /([gGhHisucUOPZ]|MS)/,
        dateInfoRe = /([djzmnYycU]|MS)/,
        slashRe = /\\/gi,
        numberTokenRe = /\{(\d+)\}/g,
        MSFormatRe = new RegExp('\\/Date\\(([-+])?(\\d+)(?:[+-]\\d{4})?\\)\\/'),
        datePartsRe = /^(?:(\d{1,4})|(\w{3,}))[/\-.\\\s](?:(\d{1,2})|(\w{3,}))[/\-.\\\s](\d{1,4})$/,
        pad = Ext.String.leftPad,
        dayInfo = {
            d: true,
            j: true
        },
        monthInfo = {
            F: true,
            m: true,
            M: true,
            n: true
        },
        yearInfo = {
            o: true,
            Y: true,
            y: true
        },
        // Most of the date-formatting functions below are the excellent work of Baron Schwartz.
        // (see http://www.xaprb.com/blog/2005/12/12/javascript-closures-for-runtime-efficiency/)
        // They generate precompiled functions from format patterns instead of parsing and
        // processing each pattern every time a date is formatted.
        code = [
            // date calculations (note: the code below creates a dependency on Ext.Number.from())
            "var me = this, dt, y, m, d, h, i, s, ms, o, O, z, zz, u, v, W, year, jan4, week1monday, daysInMonth, dayMatched,",
            "def = me.defaults,",
            "from = Ext.Number.from,",
            "results = String(input).match(me.parseRegexes[{0}]);",
            // either null, or an array of matched strings
            "if(results){",
            "{1}",
            "if(u != null){",
            // i.e. unix time is defined
            "v = new Date(u * 1000);",
            // give top priority to UNIX time
            "}else{",
            // create Date object representing midnight of the current day;
            // this will provide us with our date defaults
            // (note: clearTime() handles Daylight Saving Time automatically)
            "dt = me.clearTime(new Date);",
            "y = from(y, from(def.y, dt.getFullYear()));",
            "m = from(m, from(def.m - 1, dt.getMonth()));",
            "dayMatched = d !== undefined;",
            "d = from(d, from(def.d, dt.getDate()));",
            // Attempt to validate the day. Since it defaults to today, it may go out
            // of range, for example parsing m/Y where the value is 02/2000 on the 31st of May.
            // It will attempt to parse 2000/02/31, which will overflow to March and end up
            // returning 03/2000. We only do this when we default the day. If an invalid day value
            // was set to be parsed by the user, continue on and either let it overflow or return null
            // depending on the strict value. This will be in line with the normal Date behaviour.
            "if (!dayMatched) {",
            "dt.setDate(1);",
            "dt.setMonth(m);",
            "dt.setFullYear(y);",
            "daysInMonth = me.getDaysInMonth(dt);",
            "if (d > daysInMonth) {",
            "d = daysInMonth;",
            "}",
            "}",
            "h  = from(h, from(def.h, dt.getHours()));",
            "i  = from(i, from(def.i, dt.getMinutes()));",
            "s  = from(s, from(def.s, dt.getSeconds()));",
            "ms = from(ms, from(def.ms, dt.getMilliseconds()));",
            "if(z >= 0 && y >= 0){",
            // both the year and zero-based day of year are defined and >= 0.
            // these 2 values alone provide sufficient info to create a full date object
            // create Date object representing January 1st for the given year
            // handle years < 100 appropriately
            "v = me.add(new Date(y < 100 ? 100 : y, 0, 1, h, i, s, ms), me.YEAR, y < 100 ? y - 100 : 0);",
            // then add day of year, checking for Date "rollover" if necessary
            "v = !strict? v : (strict === true && (z <= 364 || (me.isLeapYear(v) && z <= 365))? me.add(v, me.DAY, z) : null);",
            "}else if(strict === true && !me.isValid(y, m + 1, d, h, i, s, ms)){",
            // check for Date "rollover"
            "v = null;",
            // invalid date, so return null
            "}else{",
            "if (W) {",
            // support ISO-8601
            // http://en.wikipedia.org/wiki/ISO_week_date
            //
            // Mutually equivalent definitions for week 01 are:
            // a. the week starting with the Monday which is nearest in time to 1 January
            // b. the week with 4 January in it
            // ... there are many others ...
            //
            // We'll use letter b above to determine the first week of the year.
            //
            // So, first get a Date object for January 4th of whatever calendar year is desired.
            //
            // Then, the first Monday of the year can easily be determined by (operating on this Date):
            // 1. Getting the day of the week.
            // 2. Subtracting that by one.
            // 3. Multiplying that by 86400000 (one day in ms).
            // 4. Subtracting this number of days (in ms) from the January 4 date (represented in ms).
            //
            // Example #1 ...
            //
            //       January 2012
            //   Su Mo Tu We Th Fr Sa
            //    1  2  3  4  5  6  7
            //    8  9 10 11 12 13 14
            //   15 16 17 18 19 20 21
            //   22 23 24 25 26 27 28
            //   29 30 31
            //
            // 1. January 4th is a Wednesday.
            // 2. Its day number is 3.
            // 3. Simply substract 2 days from Wednesday.
            // 4. The first week of the year begins on Monday, January 2. Simple!
            //
            // Example #2 ...
            //       January 1992
            //   Su Mo Tu We Th Fr Sa
            //             1  2  3  4
            //    5  6  7  8  9 10 11
            //   12 13 14 15 16 17 18
            //   19 20 21 22 23 24 25
            //   26 27 28 29 30 31
            //
            // 1. January 4th is a Saturday.
            // 2. Its day number is 6.
            // 3. Simply subtract 5 days from Saturday.
            // 4. The first week of the year begins on Monday, December 30. Simple!
            //
            // v = Ext.Date.clearTime(new Date(week1monday.getTime() + ((W - 1) * 604800000 + 43200000)));
            // (This is essentially doing the same thing as above but for the week rather than the day)
            "year = y || (new Date()).getFullYear();",
            "jan4 = new Date(year, 0, 4, 0, 0, 0);",
            "d = jan4.getDay();",
            // If the 1st is a Thursday, then the 4th will be a Sunday, so we need the appropriate
            // day number here, which is why we use the day === checks.
            "week1monday = new Date(jan4.getTime() - ((d === 0 ? 6 : d - 1) * 86400000));",
            // The reason for adding 43200000 (12 hours) is to avoid any complication with daylight saving
            // switch overs. For example,  if the clock is rolled back, an hour will repeat, so adding 7 days
            // will leave us 1 hour short (Sun <date> 23:00:00). By setting is to 12:00, subtraction
            // or addition of an hour won't make any difference.
            "v = Ext.Date.clearTime(new Date(week1monday.getTime() + ((W - 1) * 604800000 + 43200000)));",
            "} else {",
            // plain old Date object
            // handle years < 100 properly
            "v = me.add(new Date(y < 100 ? 100 : y, m, d, h, i, s, ms), me.YEAR, y < 100 ? y - 100 : 0);",
            "}",
            "}",
            "}",
            "}",
            "if(v){",
            // favor UTC offset over GMT offset
            "if(zz != null){",
            // reset to UTC, then add offset
            "v = me.add(v, me.SECOND, -v.getTimezoneOffset() * 60 - zz);",
            "}else if(o){",
            // reset to GMT, then add offset
            "v = me.add(v, me.MINUTE, -v.getTimezoneOffset() + (sn == '+'? -1 : 1) * (hr * 60 + mn));",
            "}",
            "}",
            "return (v != null) ? v : null;"
        ].join('\n');
    // Polyfill Date's toISOString instance method where not implemented.
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
    // TODO: Remove this when IE8 retires.
    if (!Date.prototype.toISOString) {
        Date.prototype.toISOString = function() {
            var me = this;
            return pad(me.getUTCFullYear(), 4, '0') + '-' + pad(me.getUTCMonth() + 1, 2, '0') + '-' + pad(me.getUTCDate(), 2, '0') + 'T' + pad(me.getUTCHours(), 2, '0') + ':' + pad(me.getUTCMinutes(), 2, '0') + ':' + pad(me.getUTCSeconds(), 2, '0') + '.' + pad(me.getUTCMilliseconds(), 3, '0') + 'Z';
        };
    }
    /**
     * @method xf
     * @private
     * @param format
     * Create private copy of Ext JS's `Ext.util.Format.format()` method
     * + to remove unnecessary dependency
     * + to resolve namespace conflict with MS-Ajax's implementation
     */
    function xf(format) {
        var args = Array.prototype.slice.call(arguments, 1);
        return format.replace(numberTokenRe, function(m, i) {
            return args[i];
        });
    }
    /* eslint-enable indent, max-len */
    utilDate = {
        /** @ignore */
        now: nativeDate.now,
        // always available due to polyfill in Ext.js
        /**
     * @private
     */
        toString: function(date) {
            if (!date) {
                date = new nativeDate();
            }
            return date.getFullYear() + "-" + pad(date.getMonth() + 1, 2, '0') + "-" + pad(date.getDate(), 2, '0') + "T" + pad(date.getHours(), 2, '0') + ":" + pad(date.getMinutes(), 2, '0') + ":" + pad(date.getSeconds(), 2, '0');
        },
        /**
     * Returns the number of milliseconds between two dates.
     * @param {Date} dateA The first date.
     * @param {Date} [dateB=new Date()] (optional) The second date.
     * @return {Number} The difference in milliseconds
     */
        getElapsed: function(dateA, dateB) {
            return Math.abs(dateA - (dateB || utilDate.now()));
        },
        /**
     * Global flag which determines if strict date parsing should be used.
     * Strict date parsing will not roll-over invalid dates, which is the
     * default behavior of JavaScript Date objects.
     * (see {@link #parse} for more information)
     * @type Boolean
    */
        useStrict: false,
        /**
     * @private
     */
        formatCodeToRegex: function(character, currentGroup) {
            // Note: currentGroup - position in regex result array (see notes for 
            // Ext.Date.parseCodes below)
            var p = utilDate.parseCodes[character];
            if (p) {
                p = typeof p === 'function' ? p() : p;
                // reassign function result to prevent repeated execution
                utilDate.parseCodes[character] = p;
            }
            return p ? Ext.applyIf({
                c: p.c ? xf(p.c, currentGroup || "{0}") : p.c
            }, p) : {
                g: 0,
                c: null,
                s: Ext.String.escapeRegex(character)
            };
        },
        // treat unrecognized characters as literals
        /**
     * An object hash in which each property is a date parsing function. The property name is the
     * format string which that function parses.
     *
     * This object is automatically populated with date parsing functions as
     * date formats are requested for Ext standard formatting strings.
     *
     * Custom parsing functions may be inserted into this object, keyed by a name which from then on
     * may be used as a format string to {@link #parse}.
     *
     * Example:
     *
     *     Ext.Date.parseFunctions['x-date-format'] = myDateParser;
     *
     *  A parsing function should return a Date object, and is passed the following parameters:
     *
     * - `date`: {@link String} - The date string to parse.
     * - `strict`: {@link Boolean} - `true` to validate date strings while parsing
     * (i.e. prevent JavaScript Date "rollover"). __The default must be `false`.__
     * Invalid date strings should return `null` when parsed.
     *
     * To enable Dates to also be _formatted_ according to that format, a corresponding
     * formatting function must be placed into the {@link #formatFunctions} property.
     * @property parseFunctions
     * @type Object
     */
        parseFunctions: {
            "MS": function(input, strict) {
                // note: the timezone offset is ignored since the MS Ajax server sends
                // a UTC milliseconds-since-Unix-epoch value (negative values are allowed)
                var r = (input || '').match(MSFormatRe);
                return r ? new nativeDate(((r[1] || '') + r[2]) * 1) : null;
            },
            "time": function(input, strict) {
                var num = parseInt(input, 10);
                if (num || num === 0) {
                    return new nativeDate(num);
                }
                return null;
            },
            "timestamp": function(input, strict) {
                var num = parseInt(input, 10);
                if (num || num === 0) {
                    return new nativeDate(num * 1000);
                }
                return null;
            }
        },
        parseRegexes: [],
        /**
     * An object hash in which each property is a date formatting function. The property name is the
     * format string which corresponds to the produced formatted date string.
     *
     * This object is automatically populated with date formatting functions as
     * date formats are requested for Ext standard formatting strings.
     *
     * Custom formatting functions may be inserted into this object, keyed by a name which
     * from then on may be used as a format string to {@link #format}.
     *
     * Example:
     *
     *     Ext.Date.formatFunctions['x-date-format'] = myDateFormatter;
     *
     * A formatting function should return a string representation of the Date object which
     * is the scope (this) of the function.
     *
     * To enable date strings to also be _parsed_ according to that format, a corresponding
     * parsing function must be placed into the {@link #parseFunctions} property.
     * @property formatFunctions
     * @type Object
     */
        formatFunctions: {
            "MS": function() {
                // UTC milliseconds since Unix epoch (MS-AJAX serialized date format (MRSF))
                return '\\/Date(' + this.getTime() + ')\\/';
            },
            "time": function() {
                return this.getTime().toString();
            },
            "timestamp": function() {
                return utilDate.format(this, 'U');
            }
        },
        y2kYear: 50,
        /**
     * Date interval constant.
     * @type String
     */
        MILLI: "ms",
        /**
     * Date interval constant.
     * @type String
     */
        SECOND: "s",
        /**
     * Date interval constant.
     * @type String
     */
        MINUTE: "mi",
        /**
     * Date interval constant.
     * @type String
     */
        HOUR: "h",
        /**
     * Date interval constant.
     * @type String
     */
        DAY: "d",
        /**
     * Date interval constant.
     * @type String
     */
        MONTH: "mo",
        /**
     * Date interval constant.
     * @type String
     */
        YEAR: "y",
        /**
     * The number of days in a week.
     * @type Number
     */
        DAYS_IN_WEEK: 7,
        /**
     * The number of months in a year.
     * @type Number
     */
        MONTHS_IN_YEAR: 12,
        /**
     * The maximum number of days in a month.
     * @type {Number}
     */
        MAX_DAYS_IN_MONTH: 31,
        SUNDAY: 0,
        MONDAY: 1,
        TUESDAY: 2,
        WEDNESDAY: 3,
        THURSDAY: 4,
        FRIDAY: 5,
        SATURDAY: 6,
        /**
     * An object hash containing default date values used during date parsing.
     *
     * The following properties are available:
     *
     * - `y`: {@link Number} - The default year value. Defaults to `undefined`.
     * - `m`: {@link Number} - The default 1-based month value. Defaults to `undefined`.
     * - `d`: {@link Number} - The default day value. Defaults to `undefined`.
     * - `h`: {@link Number} - The default hour value. Defaults to `undefined`.
     * - `i`: {@link Number} - The default minute value. Defaults to `undefined`.
     * - `s`: {@link Number} - The default second value. Defaults to `undefined`.
     * - `ms`: {@link Number} - The default millisecond value. Defaults to `undefined`.
     * 
     * Override these properties to customize the default date values used by the {@link #parse}
     * method.
     * 
     * __Note:__ In countries which experience Daylight Saving Time (i.e. DST), the `h`, `i`, `s`
     * and `ms` properties may coincide with the exact time in which DST takes effect.
     * It is the responsibility of the developer to account for this.
     *
     * Example Usage:
     *
     *     // set default day value to the first day of the month
     *     Ext.Date.defaults.d = 1;
     *
     *     // parse a February date string containing only year and month values.
     *     // setting the default day value to 1 prevents weird date rollover issues
     *     // when attempting to parse the following date string on, for example, March 31st 2009
     *     Ext.Date.parse('2009-02', 'Y-m'); // returns a Date object representing February 1st 2009
     *
     * @property defaults
     * @type Object
     */
        defaults: {},
        /**
     * @property {String[]} dayNames
     * An array of textual day names.
     * Override these values for international dates.
     *
     * Example:
     *
     *     Ext.Date.dayNames = [
     *         'SundayInYourLang',
     *         'MondayInYourLang'
     *         // ...
     *     ];
     * @locale
     */
        dayNames: [
            "Sunday",
            "Monday",
            "Tuesday",
            "Wednesday",
            "Thursday",
            "Friday",
            "Saturday"
        ],
        /**
     * @property {String[]} monthNames
     * An array of textual month names.
     * Override these values for international dates.
     *
     * Example:
     *
     *     Ext.Date.monthNames = [
     *         'JanInYourLang',
     *         'FebInYourLang'
     *         // ...
     *     ];
     * @locale
     */
        monthNames: [
            "January",
            "February",
            "March",
            "April",
            "May",
            "June",
            "July",
            "August",
            "September",
            "October",
            "November",
            "December"
        ],
        /**
     * @property {Object} monthNumbers
     * An object hash of zero-based JavaScript month numbers (with short month names as keys).
     *
     * __Note:__ keys are case-sensitive.
     *
     * Override these values for international dates.
     *
     * Example:
     *
     *     Ext.Date.monthNumbers = {
     *         'LongJanNameInYourLang': 0,
     *         'ShortJanNameInYourLang':0,
     *         'LongFebNameInYourLang':1,
     *         'ShortFebNameInYourLang':1
     *         // ...
     *     };
     * @locale
     */
        monthNumbers: {
            January: 0,
            Jan: 0,
            February: 1,
            Feb: 1,
            March: 2,
            Mar: 2,
            April: 3,
            Apr: 3,
            May: 4,
            June: 5,
            Jun: 5,
            July: 6,
            Jul: 6,
            August: 7,
            Aug: 7,
            September: 8,
            Sep: 8,
            October: 9,
            Oct: 9,
            November: 10,
            Nov: 10,
            December: 11,
            Dec: 11
        },
        /**
     * @property {String} defaultFormat
     * The date format string that the {@link Ext.util.Format#dateRenderer}
     * and {@link Ext.util.Format#date} functions use.  See {@link Ext.Date} for details.
     *
     * This is the format that {@link #method!flexParse} uses to disambiguate all-numeric
     * input dates.
     *
     * This may be overridden in a locale file.
     * @locale
     */
        defaultFormat: 'm/d/Y',
        /**
     * @property {String} defaultTimeFormat
     * The default time format.
     *
     * This may be overridden in a locale file.
     * @locale
     */
        defaultTimeFormat: 'h:i A',
        /**
     * @property {Number} firstDayOfWeek
     * The day on which the week starts. `0` being Sunday, through `6` being Saturday.
     *
     * This may be overridden in a locale file.
     * @locale
     */
        firstDayOfWeek: 0,
        /**
     * @property {Number[]} weekendDays
     * The days on which weekend falls. `0` being Sunday, through `6` being Saturday.
     *
     * This may be overridden in a locale file.
     * @locale
     */
        weekendDays: [
            0,
            6
        ],
        /**
     * Get the short month name for the given month number.
     * Override this function for international dates.
     * @param {Number} month A zero-based JavaScript month number.
     * @return {String} The short month name.
     * @locale
     */
        getShortMonthName: function(month) {
            return utilDate.monthNames[month].substring(0, 3);
        },
        /**
     * Get the short day name for the given day number.
     * Override this function for international dates.
     * @param {Number} day A zero-based JavaScript day number.
     * @return {String} The short day name.
     * @locale
     */
        getShortDayName: function(day) {
            return utilDate.dayNames[day].substring(0, 3);
        },
        /**
     * Get the zero-based JavaScript month number for the given short/full month name.
     * Override this function for international dates.
     * @param {String} name The short/full month name.
     * @return {Number} The zero-based JavaScript month number.
     * @locale
     */
        getMonthNumber: function(name) {
            // handle camel casing for English month names (since the keys for 
            // the Ext.Date.monthNumbers hash are case sensitive)
            return utilDate.monthNumbers[name.substring(0, 1).toUpperCase() + name.substring(1, 3).toLowerCase()];
        },
        /**
     * Checks if the specified format contains hour information
     * @param {String} format The format to check
     * @return {Boolean} True if the format contains hour information
     * @method
     */
        formatContainsHourInfo: function(format) {
            return hourInfoRe.test(format.replace(stripEscapeRe, ''));
        },
        /**
     * Checks if the specified format contains information about
     * anything other than the time.
     * @param {String} format The format to check
     * @return {Boolean} True if the format contains information about
     * date/day information.
     * @method
     */
        formatContainsDateInfo: function(format) {
            return dateInfoRe.test(format.replace(stripEscapeRe, ''));
        },
        /**
     * @private
     * Checks if the specified format contains only month information.
     *
     * @param {String} format Format to check
     *
     * @return {Boolean}
     */
        isMonthFormat: function(format) {
            return !!monthInfo[format];
        },
        /**
     * @private
     * Checks if the specified format contains only year information.
     *
     * @param {String} format Format to check.
     *
     * @return {Boolean}
     */
        isYearFormat: function(format) {
            return !!yearInfo[format];
        },
        /**
     * Removes all escaping for a date format string. In date formats,
     * using a '\' can be used to escape special characters.
     * @param {String} format The format to unescape
     * @return {String} The unescaped format
     * @method
     */
        unescapeFormat: function(format) {
            // Escape the format, since \ can be used to escape special
            // characters in a date format. For example, in a Spanish
            // locale the format may be: 'd \\de F \\de Y'
            return format.replace(slashRe, '');
        },
        /**
     * The base format-code to formatting-function hashmap used by the {@link #format} method.
     * Formatting functions are strings (or functions which return strings) which
     * will return the appropriate value when evaluated in the context of the Date object
     * from which the {@link #format} method is called.
     * Add to / override these mappings for custom date formatting.
     *
     * __Note:__ `Ext.Date.format()` treats characters as literals if an appropriate mapping
     * cannot be found.
     *
     * Example:
     *
     *     Ext.Date.formatCodes.x = "Ext.util.Format.leftPad(this.getDate(), 2, '0')";
     *     console.log(Ext.Date.format(new Date(), 'X'); // returns the current day of the month
     * @type Object
     */
        formatCodes: {
            /* eslint-disable max-len */
            d: "Ext.String.leftPad(m.getDate(), 2, '0')",
            D: "Ext.Date.getShortDayName(m.getDay())",
            // get localized short day name
            j: "m.getDate()",
            l: "Ext.Date.dayNames[m.getDay()]",
            N: "(m.getDay() ? m.getDay() : 7)",
            S: "Ext.Date.getSuffix(m)",
            w: "m.getDay()",
            z: "Ext.Date.getDayOfYear(m)",
            W: "Ext.String.leftPad(Ext.Date.getWeekOfYear(m), 2, '0')",
            F: "Ext.Date.monthNames[m.getMonth()]",
            m: "Ext.String.leftPad(m.getMonth() + 1, 2, '0')",
            M: "Ext.Date.getShortMonthName(m.getMonth())",
            // get localized short month name
            n: "(m.getMonth() + 1)",
            t: "Ext.Date.getDaysInMonth(m)",
            L: "(Ext.Date.isLeapYear(m) ? 1 : 0)",
            o: "(m.getFullYear() + (Ext.Date.getWeekOfYear(m) == 1 && m.getMonth() > 0 ? +1 : (Ext.Date.getWeekOfYear(m) >= 52 && m.getMonth() < 11 ? -1 : 0)))",
            Y: "Ext.String.leftPad(m.getFullYear(), 4, '0')",
            y: "('' + m.getFullYear()).substring(2, 4)",
            a: "(m.getHours() < 12 ? 'am' : 'pm')",
            A: "(m.getHours() < 12 ? 'AM' : 'PM')",
            g: "((m.getHours() % 12) ? m.getHours() % 12 : 12)",
            G: "m.getHours()",
            h: "Ext.String.leftPad((m.getHours() % 12) ? m.getHours() % 12 : 12, 2, '0')",
            H: "Ext.String.leftPad(m.getHours(), 2, '0')",
            i: "Ext.String.leftPad(m.getMinutes(), 2, '0')",
            s: "Ext.String.leftPad(m.getSeconds(), 2, '0')",
            u: "Ext.String.leftPad(m.getMilliseconds(), 3, '0')",
            O: "Ext.Date.getGMTOffset(m)",
            P: "Ext.Date.getGMTOffset(m, true)",
            T: "Ext.Date.getTimezone(m)",
            Z: "(m.getTimezoneOffset() * -60)",
            /* eslint-enable max-len */
            c: function() {
                // ISO-8601 -- GMT format
                var c = "Y-m-dTH:i:sP",
                    code = [],
                    l = c.length,
                    i, e;
                for (i = 0; i < l; ++i) {
                    e = c.charAt(i);
                    // treat T as a character literal
                    code.push(e === "T" ? "'T'" : utilDate.getFormatCode(e));
                }
                return code.join(" + ");
            },
            C: function() {
                // ISO-1601 -- browser format. UTC numerics with the 'Z' TZ id.
                return 'm.toISOString()';
            },
            U: "Math.round(m.getTime() / 1000)"
        },
        /**
     * Checks if the passed Date parameters will cause a JavaScript Date "rollover".
     * @param {Number} year 4-digit year.
     * @param {Number} month 1-based month-of-year.
     * @param {Number} day Day of month.
     * @param {Number} hour (optional) Hour.
     * @param {Number} minute (optional) Minute.
     * @param {Number} second (optional) Second.
     * @param {Number} millisecond (optional) Millisecond.
     * @return {Boolean} `true` if the passed parameters do not cause a Date "rollover",
     * `false` otherwise.
     */
        isValid: function(year, month, day, hour, minute, second, millisecond) {
            var dt;
            // setup defaults
            hour = hour || 0;
            minute = minute || 0;
            second = second || 0;
            millisecond = millisecond || 0;
            // Special handling for year < 100
            /* eslint-disable-next-line max-len */
            dt = utilDate.add(new nativeDate(year < 100 ? 100 : year, month - 1, day, hour, minute, second, millisecond), utilDate.YEAR, year < 100 ? year - 100 : 0);
            return year === dt.getFullYear() && month === dt.getMonth() + 1 && day === dt.getDate() && hour === dt.getHours() && minute === dt.getMinutes() && second === dt.getSeconds() && millisecond === dt.getMilliseconds();
        },
        /**
     * Parses the passed string using the specified date format.
     * Note that this function expects normal calendar dates, meaning that months are 1-based
     * (i.e. 1 = January). The {@link #defaults} hash will be used for any date value (i.e. year,
     * month, day, hour, minute, second or millisecond) which cannot be found in the passed string.
     * If a corresponding default date value has not been specified in the {@link #defaults} hash,
     * the current date's year, month, day or DST-adjusted zero-hour time value will be used
     * instead. Keep in mind that the input date string must precisely match the specified format
     * string in order for the parse operation to be successful (failed parse operations return a 
     * `null` value).
     *
     * Example:
     *
     *     //dt = Fri May 25 2007 (current date)
     *     var dt = new Date();
     *
     *     //dt = Thu May 25 2006 (today&#39;s month/day in 2006)
     *     dt = Ext.Date.parse("2006", "Y");
     *
     *     //dt = Sun Jan 15 2006 (all date parts specified)
     *     dt = Ext.Date.parse("2006-01-15", "Y-m-d");
     *
     *     //dt = Sun Jan 15 2006 15:20:01
     *     dt = Ext.Date.parse("2006-01-15 3:20:01 PM", "Y-m-d g:i:s A");
     *
     *     // attempt to parse Sun Feb 29 2006 03:20:01 in strict mode
     *     dt = Ext.Date.parse("2006-02-29 03:20:01", "Y-m-d H:i:s", true); // returns null
     *
     * ## Heuristic Parsing
     * When no `format` is specified, this method parses the date in a flexible way allowing
     * for different delimiters and textual month names to infer the position of the other
     * parts.
     *
     * Supported inferred date orders when alphabetic month names are used are:
     *
     *     - `D,M,Y`
     *     - `M,D,Y`
     *     - `Y,M,D`
     *
     * If the passed in date consists of all numeric tokens then the relative magnitude of
     * the first two tokens is used to make an inference about the user's intention.
     * If one token is less than 13 and the other is greater than 12, then the user's
     * intention is known.
     *
     * Failing this, the {@link #defaultFormat} is used to determine the input order for
     * the current locale.
     *
     * Part delimiters may be any of these:
     *
     *     - `'/'`
     *     - `'-'`
     *     - `'.'`
     *     - `'\'`
     *     - `' '` (space)
     *
     * For example, the inputs `"Jun 1 62"` and `"1 Jun 62"` would be understood as the
     * first of June, 1962 in all English locales regardless of the locale's default date
     * ordering.
     *
     * If `"25/1/62"` was passed in, it's obvious that the user means the twenty fifth
     * of January.
     *
     * If, however, `"1/6/62"` was passed in, the {@link #defaultFormat} would be consulted
     * to disambiguate the meaning of those first two tokens.
     *
     * @param {String} input The date string to parse.
     * @param {String} [format] The expected date string format. If not passed, the date
     * string will be parsed heuristically as described above.
     * @param {Boolean} [strict=false] Pass `true` to validate date strings while parsing
     * (i.e. prevents JavaScript Date "rollover"). Invalid date strings will return `null`
     * when parsed.
     * @return {Date} The parsed Date, or `null` if an invalid date string.
     */
        parse: function(input, format, strict) {
            var p;
            if (!format) {
                return utilDate.flexParse(input);
            }
            p = utilDate.parseFunctions;
            if (p[format] == null) {
                utilDate.createParser(format);
            }
            return p[format].call(utilDate, input, Ext.isDefined(strict) ? strict : utilDate.useStrict);
        },
        // Backwards compat
        parseDate: function(input, format, strict) {
            return utilDate.parse(input, format, strict);
        },
        /**
     * @private
     */
        getFormatCode: function(character) {
            var f = utilDate.formatCodes[character];
            if (f) {
                f = typeof f === 'function' ? f() : f;
                // reassign function result to prevent repeated execution
                utilDate.formatCodes[character] = f;
            }
            // note: unknown characters are treated as literals
            return f || ("'" + Ext.String.escape(character) + "'");
        },
        /**
     * @private
     */
        createFormat: function(format) {
            var code = [],
                special = false,
                ch = '',
                i;
            for (i = 0; i < format.length; ++i) {
                ch = format.charAt(i);
                if (!special && ch === "\\") {
                    special = true;
                } else if (special) {
                    special = false;
                    code.push("'" + Ext.String.escape(ch) + "'");
                } else {
                    if (ch === '\n') {
                        code.push("'\\n'");
                    } else {
                        code.push(utilDate.getFormatCode(ch));
                    }
                }
            }
            utilDate.formatFunctions[format] = Ext.functionFactory("var m = this; return " + code.join('+'));
        },
        /**
     * @private
     */
        createParser: function(format) {
            var regexNum = utilDate.parseRegexes.length,
                currentGroup = 1,
                calc = [],
                regex = [],
                special = false,
                ch = "",
                i = 0,
                len = format.length,
                atEnd = [],
                obj;
            for (; i < len; ++i) {
                ch = format.charAt(i);
                if (!special && ch === "\\") {
                    special = true;
                } else if (special) {
                    special = false;
                    regex.push(Ext.String.escape(ch));
                } else {
                    obj = utilDate.formatCodeToRegex(ch, currentGroup);
                    currentGroup += obj.g;
                    regex.push(obj.s);
                    if (obj.g && obj.c) {
                        if (obj.calcAtEnd) {
                            atEnd.push(obj.c);
                        } else {
                            calc.push(obj.c);
                        }
                    }
                }
            }
            calc = calc.concat(atEnd);
            utilDate.parseRegexes[regexNum] = new RegExp("^" + regex.join('') + "$", 'i');
            utilDate.parseFunctions[format] = Ext.functionFactory("input", "strict", xf(code, regexNum, calc.join('')));
        },
        /**
     * @private
     */
        parseCodes: {
            // Notes:
            // g = {Number} calculation group (0 or 1. only group 1 contributes to
            // date calculations.)
            // c = {String} calculation method (required for group 1. null for group 0.
            // {0} = currentGroup - position in regex result array)
            // s = {String} regex pattern. all matches are stored in results[], and are
            // accessible by the calculation mapped to 'c'
            d: {
                g: 1,
                c: "d = parseInt(results[{0}], 10);\n",
                s: "(3[0-1]|[1-2][0-9]|0[1-9])"
            },
            // day of month with leading zeroes (01 - 31)
            j: {
                g: 1,
                c: "d = parseInt(results[{0}], 10);\n",
                s: "(3[0-1]|[1-2][0-9]|[1-9])"
            },
            // day of month without leading zeroes (1 - 31)
            D: function() {
                var a = [],
                    i;
                // get localised short day names
                for (i = 0; i < 7; i++) {
                    a.push(utilDate.getShortDayName(i));
                }
                return {
                    g: 0,
                    c: null,
                    s: "(?:" + a.join("|") + ")"
                };
            },
            l: function() {
                return {
                    g: 0,
                    c: null,
                    s: "(?:" + utilDate.dayNames.join("|") + ")"
                };
            },
            N: {
                g: 0,
                c: null,
                s: "[1-7]"
            },
            // ISO-8601 day number (1 (monday) - 7 (sunday))
            //<locale type="object" property="parseCodes">
            S: {
                g: 0,
                c: null,
                s: "(?:st|nd|rd|th)"
            },
            //</locale>
            w: {
                g: 0,
                c: null,
                s: "[0-6]"
            },
            // JavaScript day number (0 (sunday) - 6 (saturday))
            z: {
                g: 1,
                c: "z = parseInt(results[{0}], 10);\n",
                s: "(\\d{1,3})"
            },
            // day of the year (0 - 364 (365 in leap years))
            W: {
                g: 1,
                c: "W = parseInt(results[{0}], 10);\n",
                s: "(\\d{2})"
            },
            // ISO-8601 week number (with leading zero)
            F: function() {
                return {
                    g: 1,
                    c: "m = parseInt(me.getMonthNumber(results[{0}]), 10);\n",
                    s: "(" + utilDate.monthNames.join("|") + ")"
                };
            },
            M: function() {
                var a = [],
                    i;
                // get localised short month names
                for (i = 0; i < 12; i++) {
                    a.push(utilDate.getShortMonthName(i));
                }
                return Ext.applyIf({
                    s: "(" + a.join("|") + ")"
                }, utilDate.formatCodeToRegex("F"));
            },
            m: {
                g: 1,
                c: "m = parseInt(results[{0}], 10) - 1;\n",
                s: "(1[0-2]|0[1-9])"
            },
            // month number with leading zeros (01 - 12)
            n: {
                g: 1,
                c: "m = parseInt(results[{0}], 10) - 1;\n",
                s: "(1[0-2]|[1-9])"
            },
            // month number without leading zeros (1 - 12)
            t: {
                g: 0,
                c: null,
                s: "(?:\\d{2})"
            },
            // no. of days in the month (28 - 31)
            L: {
                g: 0,
                c: null,
                s: "(?:1|0)"
            },
            o: {
                g: 1,
                c: "y = parseInt(results[{0}], 10);\n",
                s: "(\\d{4})"
            },
            // ISO-8601 year number (with leading zero)
            Y: {
                g: 1,
                c: "y = parseInt(results[{0}], 10);\n",
                s: "(\\d{4})"
            },
            // 4-digit year
            y: {
                g: 1,
                c: "var ty = parseInt(results[{0}], 10);\n" + "y = ty > me.y2kYear ? 1900 + ty : 2000 + ty;\n",
                // 2-digit year
                s: "(\\d{2})"
            },
            // In the am/pm parsing routines, we allow both upper and lower case
            // even though it doesn't exactly match the spec. It gives much more flexibility
            // in being able to specify case insensitive regexes.
            /* eslint-disable indent */
            //<locale type="object" property="parseCodes">
            a: {
                g: 1,
                c: "if (/(am)/i.test(results[{0}])) {\n" + "if (!h || h == 12) { h = 0; }\n" + "} else { if (!h || h < 12) { h = (h || 0) + 12; }}",
                s: "(am|pm|AM|PM)",
                calcAtEnd: true
            },
            //</locale>
            //<locale type="object" property="parseCodes">
            A: {
                g: 1,
                c: "if (/(am)/i.test(results[{0}])) {\n" + "if (!h || h == 12) { h = 0; }\n" + "} else { if (!h || h < 12) { h = (h || 0) + 12; }}",
                s: "(AM|PM|am|pm)",
                calcAtEnd: true
            },
            //</locale>
            g: {
                g: 1,
                c: "h = parseInt(results[{0}], 10);\n",
                s: "(1[0-2]|[1-9])"
            },
            //  12-hr format of an hour without leading zeroes (1 - 12)
            G: {
                g: 1,
                c: "h = parseInt(results[{0}], 10);\n",
                s: "(2[0-3]|1[0-9]|[0-9])"
            },
            // 24-hr format of an hour without leading zeroes (0 - 23)
            h: {
                g: 1,
                c: "h = parseInt(results[{0}], 10);\n",
                s: "(1[0-2]|0[1-9])"
            },
            //  12-hr format of an hour with leading zeroes (01 - 12)
            H: {
                g: 1,
                c: "h = parseInt(results[{0}], 10);\n",
                s: "(2[0-3]|[0-1][0-9])"
            },
            //  24-hr format of an hour with leading zeroes (00 - 23)
            i: {
                g: 1,
                c: "i = parseInt(results[{0}], 10);\n",
                s: "([0-5][0-9])"
            },
            // minutes with leading zeros (00 - 59)
            s: {
                g: 1,
                c: "s = parseInt(results[{0}], 10);\n",
                s: "([0-5][0-9])"
            },
            // seconds with leading zeros (00 - 59)
            u: {
                g: 1,
                c: "ms = results[{0}]; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n",
                s: "(\\d+)"
            },
            // decimal fraction of a second (minimum = 1 digit, maximum = unlimited)
            /* eslint-disable max-len */
            O: {
                g: 1,
                c: [
                    "o = results[{0}];",
                    "var sn = o.substring(0,1),",
                    // get + / - sign
                    "hr = o.substring(1,3)*1 + Math.floor(o.substring(3,5) / 60),",
                    // get hours (performs minutes-to-hour conversion also, just in case)
                    "mn = o.substring(3,5) % 60;",
                    // get minutes
                    "o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + Ext.String.leftPad(hr, 2, '0') + Ext.String.leftPad(mn, 2, '0')) : null;\n"
                ].// -12hrs <= GMT offset <= 14hrs
                join("\n"),
                s: "([+-]\\d{4})"
            },
            // GMT offset in hrs and mins
            P: {
                g: 1,
                c: [
                    "o = results[{0}];",
                    "var sn = o.substring(0,1),",
                    // get + / - sign
                    "hr = o.substring(1,3)*1 + Math.floor(o.substring(4,6) / 60),",
                    // get hours (performs minutes-to-hour conversion also, just in case)
                    "mn = o.substring(4,6) % 60;",
                    // get minutes
                    "o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + Ext.String.leftPad(hr, 2, '0') + Ext.String.leftPad(mn, 2, '0')) : null;\n"
                ].// -12hrs <= GMT offset <= 14hrs
                join("\n"),
                s: "([+-]\\d{2}:\\d{2})"
            },
            // GMT offset in hrs and mins (with colon separator)
            T: {
                g: 0,
                c: null,
                s: "[A-Z]{1,5}"
            },
            // timezone abbrev. may be between 1 - 5 chars
            Z: {
                g: 1,
                c: "zz = results[{0}] * 1;\n" + // -43200 <= UTC offset <= 50400
                "zz = (-43200 <= zz && zz <= 50400)? zz : null;\n",
                s: "([+-]?\\d{1,5})"
            },
            // leading '+' sign is optional for UTC offset
            c: function() {
                var calc = [],
                    arr = [
                        utilDate.formatCodeToRegex("Y", 1),
                        // year
                        utilDate.formatCodeToRegex("m", 2),
                        // month
                        utilDate.formatCodeToRegex("d", 3),
                        // day
                        utilDate.formatCodeToRegex("H", 4),
                        // hour
                        utilDate.formatCodeToRegex("i", 5),
                        // minute
                        utilDate.formatCodeToRegex("s", 6),
                        // second
                        {
                            c: "ms = results[7] || '0'; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n"
                        },
                        // decimal fraction of a second (minimum = 1 digit, maximum = unlimited)
                        {
                            c: [
                                // allow either "Z" (i.e. UTC) or "-0530" or "+08:00" (i.e. UTC offset) timezone delimiters. assumes local timezone if no timezone is specified
                                "if (results[8]) {",
                                // timezone specified
                                "if (results[8] == 'Z') {",
                                "zz = 0;",
                                // UTC
                                "}",
                                "else if (results[8].indexOf(':') > -1) {",
                                utilDate.formatCodeToRegex("P", 8).c,
                                // timezone offset with colon separator
                                "}",
                                "else {",
                                utilDate.formatCodeToRegex("O", 8).c,
                                // timezone offset without colon separator
                                "}",
                                "}"
                            ].join('\n')
                        }
                    ],
                    i, l;
                for (i = 0 , l = arr.length; i < l; ++i) {
                    calc.push(arr[i].c);
                }
                return {
                    g: 1,
                    c: calc.join(""),
                    s: [
                        arr[0].s,
                        // year (required)
                        "(?:",
                        "-",
                        arr[1].s,
                        // month (optional)
                        "(?:",
                        "-",
                        arr[2].s,
                        // day (optional)
                        "(?:",
                        "(?:T| )?",
                        // time delimiter -- either a "T" or a single blank space
                        arr[3].s,
                        ":",
                        arr[4].s,
                        // hour AND minute, delimited by a single colon (optional). MUST be preceded by either a "T" or a single blank space
                        "(?::",
                        arr[5].s,
                        ")?",
                        // seconds (optional)
                        "(?:(?:\\.|,)(\\d+))?",
                        // decimal fraction of a second (e.g. ",12345" or ".98765") (optional)
                        "(Z|(?:[-+]\\d{2}(?::)?\\d{2}))?",
                        // "Z" (UTC) or "-0530" (UTC offset without colon delimiter) or "+08:00" (UTC offset with colon delimiter) (optional)
                        ")?",
                        ")?",
                        ")?"
                    ].join("")
                };
            },
            U: {
                g: 1,
                c: "u = parseInt(results[{0}], 10);\n",
                s: "(-?\\d+)"
            }
        },
        // leading minus sign indicates seconds before UNIX epoch
        /* eslint-enable indent, max-len */
        compare: function(d1, d2, includeTime) {
            var s1, s2;
            if (typeof d1 === 'string') {
                d1 = Ext.Date.parse(d1);
            }
            if (typeof d2 === 'string') {
                d2 = Ext.Date.parse(d2);
            }
            s1 = Ext.Date.format(d1, 'C');
            s2 = Ext.Date.format(d2, 'C');
            if (!includeTime) {
                s1 = s1.substr(0, 10);
                // "YYYY-MM-DD".length === 10
                s2 = s2.substr(0, 10);
            }
            return (s1 < s2) ? -1 : ((s2 < s1) ? 1 : 0);
        },
        // Old Ext.Date prototype methods.
        /**
     * @private
     */
        dateFormat: function(date, format) {
            return utilDate.format(date, format);
        },
        /**
     * Compares if two dates are equal by comparing their values.
     * @param {Date} date1
     * @param {Date} date2
     * @return {Boolean} `true` if the date values are equal
     */
        isEqual: function(date1, date2) {
            // check we have 2 date objects
            if (date1 && date2) {
                return +date1 === +date2;
            }
            // one or both isn't a date, only equal if both are falsy
            return !(date1 || date2);
        },
        /**
     * Formats a date given the supplied format string.
     * @param {Date} date The date to format
     * @param {String} format The format string
     * @return {String} The formatted date or an empty string if date parameter is not
     * a JavaScript Date object
     */
        format: function(date, format) {
            var formatFunctions = utilDate.formatFunctions;
            if (!Ext.isDate(date)) {
                return '';
            }
            if (formatFunctions[format] == null) {
                utilDate.createFormat(format);
            }
            return formatFunctions[format].call(date) + '';
        },
        /**
     * Get the timezone abbreviation of the current date (equivalent to the format specifier 'T').
     *
     * __Note:__ The date string returned by the JavaScript Date object's `toString()` method varies
     * between browsers (e.g. FF vs IE) and system region settings (e.g. IE in Asia vs IE in
     * America). For a given date string e.g. "Thu Oct 25 2007 22:55:35 GMT+0800 (Malay Peninsula
     * Standard Time)", `getTimezone()` first tries to get the timezone abbreviation from between
     * a pair of parentheses (which may or may not be present), failing which it proceeds to get
     * the timezone abbreviation from the GMT offset portion of the date string.
     * 
     *     var dt = new Date('9/17/2011');
     *     console.log(Ext.Date.getTimezone(dt));
     *
     * @param {Date} date The date
     * @return {String} The abbreviated timezone name (e.g. 'CST', 'PDT', 'EDT', 'MPST' ...).
     */
        getTimezone: function(date) {
            /* eslint-disable max-len, no-useless-escape, newline-per-chained-call */
            // the following list shows the differences between date strings from different browsers on a WinXP SP2 machine from an Asian locale:
            //
            // Opera  : "Thu, 25 Oct 2007 22:53:45 GMT+0800" -- shortest (weirdest) date string of the lot
            // Safari : "Thu Oct 25 2007 22:55:35 GMT+0800 (Malay Peninsula Standard Time)" -- value in parentheses always gives the correct timezone (same as FF)
            // FF     : "Thu Oct 25 2007 22:55:35 GMT+0800 (Malay Peninsula Standard Time)" -- value in parentheses always gives the correct timezone
            // IE     : "Thu Oct 25 22:54:35 UTC+0800 2007" -- (Asian system setting) look for 3-4 letter timezone abbrev
            // IE     : "Thu Oct 25 17:06:37 PDT 2007" -- (American system setting) look for 3-4 letter timezone abbrev
            //
            // this crazy regex attempts to guess the correct timezone abbreviation despite these differences.
            // step 1: (?:\((.*)\) -- find timezone in parentheses
            // step 2: ([A-Z]{1,4})(?:[\-+][0-9]{4})?(?: -?\d+)?) -- if nothing was found in step 1, find timezone from timezone offset portion of date string
            // step 3: remove all non uppercase characters found in step 1 and 2
            return date.toString().replace(/^.* (?:\((.*)\)|([A-Z]{1,5})(?:[\-+][0-9]{4})?(?: -?\d+)?)$/, "$1$2").replace(/[^A-Z]/g, "");
        },
        /* eslint-enable max-len, no-useless-escape, newline-per-chained-call */
        /**
     * Get the offset from GMT of the current date (equivalent to the format specifier 'O').
     *
     *     var dt = new Date('9/17/2011');
     *     console.log(Ext.Date.getGMTOffset(dt));
     *
     * @param {Date} date The date
     * @param {Boolean} [colon=false] `true` to separate the hours and minutes with a colon.
     * @return {String} The 4-character offset string prefixed with + or - (e.g. '-0600').
     */
        getGMTOffset: function(date, colon) {
            var offset = date.getTimezoneOffset();
            return (offset > 0 ? "-" : "+") + Ext.String.leftPad(Math.floor(Math.abs(offset) / 60), 2, "0") + (colon ? ":" : "") + Ext.String.leftPad(Math.abs(offset % 60), 2, "0");
        },
        /**
     * Get the numeric day number of the year, adjusted for leap year.
     *
     *     var dt = new Date('9/17/2011');
     *     console.log(Ext.Date.getDayOfYear(dt)); // 259
     *
     * @param {Date} date The date
     * @return {Number} 0 to 364 (365 in leap years).
     */
        getDayOfYear: function(date) {
            var num = 0,
                d = utilDate.clone(date),
                m = date.getMonth(),
                i;
            for (i = 0 , d.setDate(1) , d.setMonth(0); i < m; d.setMonth(++i)) {
                num += utilDate.getDaysInMonth(d);
            }
            return num + date.getDate() - 1;
        },
        /**
     * Get the numeric ISO-8601 week number of the year.
     * (equivalent to the format specifier 'W', but without a leading zero).
     *
     *     var dt = new Date('9/17/2011');
     *     console.log(Ext.Date.getWeekOfYear(dt)); // 37
     *
     * @param {Date} date The date.
     * @return {Number} 1 to 53.
     * @method
     */
        getWeekOfYear: (function() {
            // adapted from http://www.merlyn.demon.co.uk/weekcalc.htm
            var ms1d = 86400000,
                // milliseconds in a day
                ms7d = 7 * ms1d;
            // milliseconds in a week
            return function(date) {
                // return a closure so constants get calculated only once
                /* eslint-disable-next-line max-len */
                var DC3 = nativeDate.UTC(date.getFullYear(), date.getMonth(), date.getDate() + 3) / ms1d,
                    // an Absolute Day Number
                    AWN = Math.floor(DC3 / 7),
                    // an Absolute Week Number
                    Wyr = new nativeDate(AWN * ms7d).getUTCFullYear();
                return AWN - Math.floor(nativeDate.UTC(Wyr, 0, 7) / ms7d) + 1;
            };
        }()),
        /**
     * Checks if the current date falls within a leap year.
     *
     *     var dt = new Date('1/10/2011');
     *     console.log(Ext.Date.isLeapYear(dt)); // false
     *
     * @param {Date} date The date
     * @return {Boolean} `true` if the current date falls within a leap year, `false` otherwise.
     */
        isLeapYear: function(date) {
            var year = date.getFullYear();
            return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year)));
        },
        /**
     * Get the first day of the current month, adjusted for leap year.  The returned value
     * is the numeric day index within the week (0-6) which can be used in conjunction with
     * the {@link #monthNames} array to retrieve the textual day name.
     *
     *     var dt = new Date('1/10/2007'),
     *         firstDay = Ext.Date.getFirstDayOfMonth(dt);
     *
     *     console.log(Ext.Date.dayNames[firstDay]); // output: 'Monday'
     *
     * @param {Date} date The date
     * @return {Number} The day number (0-6).
     */
        getFirstDayOfMonth: function(date) {
            var day = (date.getDay() - (date.getDate() - 1)) % 7;
            return (day < 0) ? (day + 7) : day;
        },
        /**
     * Get the last day of the current month, adjusted for leap year.  The returned value
     * is the numeric day index within the week (0-6) which can be used in conjunction with
     * the {@link #monthNames} array to retrieve the textual day name.
     *
     *     var dt = new Date('1/10/2007'),
     *         lastDay = Ext.Date.getLastDayOfMonth(dt);
     *
     *     console.log(Ext.Date.dayNames[lastDay]); // output: 'Wednesday'
     *
     * @param {Date} date The date
     * @return {Number} The day number (0-6).
     */
        getLastDayOfMonth: function(date) {
            return utilDate.getLastDateOfMonth(date).getDay();
        },
        /**
     * Get the date of the first day of the month in which this date resides.
     * @param {Date} date The date
     * @return {Date}
     */
        getFirstDateOfMonth: function(date) {
            return new nativeDate(date.getFullYear(), date.getMonth(), 1);
        },
        /**
     * Get the date of the last day of the month in which this date resides.
     * @param {Date} date The date
     * @return {Date}
     */
        getLastDateOfMonth: function(date) {
            return new nativeDate(date.getFullYear(), date.getMonth(), utilDate.getDaysInMonth(date));
        },
        /**
     * Get the number of days in the current month, adjusted for leap year.
     * @param {Date} date The date
     * @return {Number} The number of days in the month.
     * @method
     */
        getDaysInMonth: (function() {
            var daysInMonth = [
                    31,
                    28,
                    31,
                    30,
                    31,
                    30,
                    31,
                    31,
                    30,
                    31,
                    30,
                    31
                ];
            return function(date) {
                // return a closure for efficiency
                var m = date.getMonth();
                return m === 1 && utilDate.isLeapYear(date) ? 29 : daysInMonth[m];
            };
        }()),
        /**
     * Get the English ordinal suffix of the current day (equivalent to the format specifier 'S').
     * @param {Date} date The date
     * @return {String} 'st, 'nd', 'rd' or 'th'.
     * @locale
     */
        getSuffix: function(date) {
            switch (date.getDate()) {
                case 1:
                case 21:
                case 31:
                    return "st";
                case 2:
                case 22:
                    return "nd";
                case 3:
                case 23:
                    return "rd";
                default:
                    return "th";
            }
        },
        /**
     * Creates and returns a new Date instance with the exact same date value as the called
     * instance. Dates are copied and passed by reference, so if a copied date variable is modified
     * later, the original variable will also be changed.  When the intention is to create a new
     * variable that will not modify the original instance, you should create a clone.
     *
     * Example of correctly cloning a date:
     *
     *     //wrong way:
     *     var orig = new Date('10/1/2006');
     *     var copy = orig;
     *     copy.setDate(5);
     *     console.log(orig);  // returns 'Thu Oct 05 2006'!
     *
     *     //correct way:
     *     var orig = new Date('10/1/2006'),
     *         copy = Ext.Date.clone(orig);
     *     copy.setDate(5);
     *     console.log(orig);  // returns 'Thu Oct 01 2006'
     *
     * @param {Date} date The date.
     * @return {Date} The new Date instance.
     */
        clone: function(date) {
            return new nativeDate(date.getTime());
        },
        /**
     * Checks if the current date is affected by Daylight Saving Time (DST).
     * @param {Date} date The date
     * @return {Boolean} `true` if the current date is affected by DST.
     */
        isDST: function(date) {
            // adapted from http://sencha.com/forum/showthread.php?p=247172#post247172
            // courtesy of @geoffrey.mcgill
            /* eslint-disable-next-line max-len */
            return new nativeDate(date.getFullYear(), 0, 1).getTimezoneOffset() !== date.getTimezoneOffset();
        },
        /**
     * Attempts to clear all time information from this Date by setting the time to midnight
     * of the same day, automatically adjusting for Daylight Saving Time (DST) where applicable.
     *
     * __Note:__ DST timezone information for the browser's host operating system is assumed to be
     * up-to-date.
     * @param {Date} date The date
     * @param {Boolean} [clone=false] `true` to create a clone of this date, clear the time and
     * return it.
     * @return {Date} this or the clone.
     */
        clearTime: function(date, clone) {
            var d, hr, c;
            // handles invalid dates preventing the browser from crashing.
            if (isNaN(date.getTime())) {
                return date;
            }
            if (clone) {
                return utilDate.clearTime(utilDate.clone(date));
            }
            // get current date before clearing time
            d = date.getDate();
            // clear time
            date.setHours(0);
            date.setMinutes(0);
            date.setSeconds(0);
            date.setMilliseconds(0);
            // account for DST (i.e. day of month changed when setting hour = 0)
            if (date.getDate() !== d) {
                // note: DST adjustments are assumed to occur in multiples of 1 hour
                // (this is almost always the case)
                // refer to http://www.timeanddate.com/time/aboutdst.html for the (rare) exceptions
                // to this rule
                // increment hour until cloned date == current date
                /* eslint-disable-next-line max-len, curly, nonblock-statement-body-position */
                for (hr = 1 , c = utilDate.add(date, utilDate.HOUR, hr); c.getDate() !== d; hr++ , c = utilDate.add(date, utilDate.HOUR, hr)){}
                date.setDate(d);
                date.setHours(c.getHours());
            }
            return date;
        },
        /**
     * Provides a convenient method for performing basic date arithmetic. This method
     * does not modify the Date instance being called - it creates and returns
     * a new Date instance containing the resulting date value.
     *
     * Examples:
     *
     *     // Basic usage:
     *     var dt = Ext.Date.add(new Date('10/29/2006'), Ext.Date.DAY, 5);
     *     console.log(dt); // returns 'Fri Nov 03 2006 00:00:00'
     *
     *     // Negative values will be subtracted:
     *     var dt2 = Ext.Date.add(new Date('10/1/2006'), Ext.Date.DAY, -5);
     *     console.log(dt2); // returns 'Tue Sep 26 2006 00:00:00'
     *
     *      // Decimal values can be used:
     *     var dt3 = Ext.Date.add(new Date('10/1/2006'), Ext.Date.DAY, 1.25);
     *     console.log(dt3); // returns 'Mon Oct 02 2006 06:00:00'
     *
     * @param {Date} date The date to modify
     * @param {String} interval A valid date interval enum value.
     * @param {Number} value The amount to add to the current date.
     * @param {Boolean} [preventDstAdjust=false] `true` to prevent adjustments when crossing
     * daylight savings boundaries.
     * @return {Date} The new Date instance.
     */
        add: function(date, interval, value, preventDstAdjust) {
            var d = utilDate.clone(date),
                base = 0,
                day, decimalValue;
            if (!interval || value === 0) {
                return d;
            }
            decimalValue = value - parseInt(value, 10);
            value = parseInt(value, 10);
            if (value) {
                switch (interval.toLowerCase()) {
                    // See EXTJSIV-7418. We use setTime() here to deal with issues related to
                    // the switchover that occurs when changing to daylight savings and vice
                    // versa. setTime() handles this correctly where setHour/Minute/Second/Millisecond
                    // do not. Let's assume the DST change occurs at 2am and we're incrementing using
                    // add for 15 minutes at time. When entering DST, we should see:
                    // 01:30am
                    // 01:45am
                    // 03:00am // skip 2am because the hour does not exist
                    // ...
                    // Similarly, leaving DST, we should see:
                    // 01:30am
                    // 01:45am
                    // 01:00am // repeat 1am because that's the change over
                    // 01:30am
                    // 01:45am
                    // 02:00am
                    // ....
                    //
                    case utilDate.MILLI:
                        if (preventDstAdjust) {
                            d.setMilliseconds(d.getMilliseconds() + value);
                        } else {
                            d.setTime(d.getTime() + value);
                        };
                        break;
                    case utilDate.SECOND:
                        if (preventDstAdjust) {
                            d.setSeconds(d.getSeconds() + value);
                        } else {
                            d.setTime(d.getTime() + value * 1000);
                        };
                        break;
                    case utilDate.MINUTE:
                        if (preventDstAdjust) {
                            d.setMinutes(d.getMinutes() + value);
                        } else {
                            d.setTime(d.getTime() + value * 60 * 1000);
                        };
                        break;
                    case utilDate.HOUR:
                        if (preventDstAdjust) {
                            d.setHours(d.getHours() + value);
                        } else {
                            d.setTime(d.getTime() + value * 60 * 60 * 1000);
                        };
                        break;
                    case utilDate.DAY:
                        if (preventDstAdjust === false) {
                            d.setTime(d.getTime() + value * 24 * 60 * 60 * 1000);
                        } else {
                            d.setDate(d.getDate() + value);
                        };
                        break;
                    case utilDate.MONTH:
                        day = date.getDate();
                        if (day > 28) {
                            /* eslint-disable-next-line max-len */
                            day = Math.min(day, utilDate.getLastDateOfMonth(utilDate.add(utilDate.getFirstDateOfMonth(date), utilDate.MONTH, value)).getDate());
                        };
                        d.setDate(day);
                        d.setMonth(date.getMonth() + value);
                        break;
                    case utilDate.YEAR:
                        day = date.getDate();
                        if (day > 28) {
                            /* eslint-disable-next-line max-len */
                            day = Math.min(day, utilDate.getLastDateOfMonth(utilDate.add(utilDate.getFirstDateOfMonth(date), utilDate.YEAR, value)).getDate());
                        };
                        d.setDate(day);
                        d.setFullYear(date.getFullYear() + value);
                        break;
                }
            }
            if (decimalValue) {
                switch (interval.toLowerCase()) {
                    /* eslint-disable no-multi-spaces */
                    case utilDate.MILLI:
                        base = 1;
                        break;
                    case utilDate.SECOND:
                        base = 1000;
                        break;
                    case utilDate.MINUTE:
                        base = 1000 * 60;
                        break;
                    case utilDate.HOUR:
                        base = 1000 * 60 * 60;
                        break;
                    case utilDate.DAY:
                        base = 1000 * 60 * 60 * 24;
                        break;
                    /* eslint-enable no-multi-spaces */
                    case utilDate.MONTH:
                        day = utilDate.getDaysInMonth(d);
                        base = 1000 * 60 * 60 * 24 * day;
                        break;
                    case utilDate.YEAR:
                        day = (utilDate.isLeapYear(d) ? 366 : 365);
                        base = 1000 * 60 * 60 * 24 * day;
                        break;
                }
                if (base) {
                    d.setTime(d.getTime() + base * decimalValue);
                }
            }
            return d;
        },
        /**
     * Provides a convenient method for performing basic date arithmetic. This method
     * does not modify the Date instance being called - it creates and returns
     * a new Date instance containing the resulting date value.
     *
     * Examples:
     *
     *     // Basic usage:
     *     var dt = Ext.Date.subtract(new Date('10/29/2006'), Ext.Date.DAY, 5);
     *     console.log(dt); // returns 'Tue Oct 24 2006 00:00:00'
     *
     *     // Negative values will be added:
     *     var dt2 = Ext.Date.subtract(new Date('10/1/2006'), Ext.Date.DAY, -5);
     *     console.log(dt2); // returns 'Fri Oct 6 2006 00:00:00'
     *
     *      // Decimal values can be used:
     *     var dt3 = Ext.Date.subtract(new Date('10/1/2006'), Ext.Date.DAY, 1.25);
     *     console.log(dt3); // returns 'Fri Sep 29 2006 06:00:00'
     *
     * @param {Date} date The date to modify
     * @param {String} interval A valid date interval enum value.
     * @param {Number} value The amount to subtract from the current date.
     * @param {Boolean} [preventDstAdjust=false] `true` to prevent adjustments when crossing
     * daylight savings boundaries.
     * @return {Date} The new Date instance.
     */
        subtract: function(date, interval, value, preventDstAdjust) {
            return utilDate.add(date, interval, -value, preventDstAdjust);
        },
        /**
     * Checks if a date falls on or between the given start and end dates.
     * @param {Date} date The date to check
     * @param {Date} start Start date
     * @param {Date} end End date
     * @return {Boolean} `true` if this date falls on or between the given start and end dates.
     */
        between: function(date, start, end) {
            var t = date.getTime();
            return start.getTime() <= t && t <= end.getTime();
        },
        /**
     * Checks if the date is a weekend day. Uses {@link #weekendDays}.
     * @param {Date} date The date.
     * @return {Boolean} `true` if the day falls on a weekend.
     *
     * @since 6.2.0
     */
        isWeekend: function(date) {
            return Ext.Array.indexOf(this.weekendDays, date.getDay()) > -1;
        },
        /**
     * Converts the passed UTC date into a local date.
     * For example, if the passed date is:
     * `Wed Jun 01 2016 00:10:00 GMT+1000 (AUS Eastern Standard Time)`, then
     * the returned date will be `Wed Jun 01 2016 00:00:00 GMT+1000 (AUS Eastern Standard Time)`.
     * @param {Date} d The date to convert.
     * @return {Date} The date as a local. Does not modify the passed date.
     *
     * @since 6.2.0
     */
        utcToLocal: function(d) {
            return new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
        },
        /**
     * Converts the passed local date into a UTC date.
     * For example, if the passed date is:
     * `Wed Jun 01 2016 00:00:00 GMT+1000 (AUS Eastern Standard Time)`, then
     * the returned date will be `Wed Jun 01 2016 10:00:00 GMT+1000 (AUS Eastern Standard Time)`.
     * @param {Date} d The date to convert.
     * @return {Date} The date as UTC. Does not modify the passed date.
     *
     * @since 6.2.0
     */
        localToUtc: function(d) {
            return utilDate.utc(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds());
        },
        /**
     * Create a UTC date.
     * @param {Number} year The year.
     * @param {Number} month The month.
     * @param {Number} day The day.
     * @param {Number} [hour=0] The hour.
     * @param {Number} [min=0] The minutes.
     * @param {Number} [s=0] The seconds.
     * @param {Number} [ms=0] The milliseconds.
     * @return {Date} The UTC date.
     *
     * @since 6.2.0
     */
        utc: function(year, month, day, hour, min, s, ms) {
            return new Date(Date.UTC(year, month, day, hour || 0, min || 0, s || 0, ms || 0));
        },
        // Maintains compatibility with old static and prototype window.Date methods.
        compat: function() {
            var p,
                /* eslint-disable max-len */
                statics = [
                    'useStrict',
                    'formatCodeToRegex',
                    'parseFunctions',
                    'parseRegexes',
                    'formatFunctions',
                    'y2kYear',
                    'MILLI',
                    'SECOND',
                    'MINUTE',
                    'HOUR',
                    'DAY',
                    'MONTH',
                    'YEAR',
                    'defaults',
                    'dayNames',
                    'monthNames',
                    'monthNumbers',
                    'getShortMonthName',
                    'getShortDayName',
                    'getMonthNumber',
                    'formatCodes',
                    'isValid',
                    'parseDate',
                    'getFormatCode',
                    'createFormat',
                    'createParser',
                    'parseCodes'
                ],
                proto = [
                    'dateFormat',
                    'format',
                    'getTimezone',
                    'getGMTOffset',
                    'getDayOfYear',
                    'getWeekOfYear',
                    'isLeapYear',
                    'getFirstDayOfMonth',
                    'getLastDayOfMonth',
                    'getDaysInMonth',
                    'getSuffix',
                    'clone',
                    'isDST',
                    'clearTime',
                    'add',
                    'between'
                ],
                /* eslint-enable max-len */
                sLen = statics.length,
                pLen = proto.length,
                stat, prot, s;
            // Append statics
            for (s = 0; s < sLen; s++) {
                stat = statics[s];
                nativeDate[stat] = utilDate[stat];
            }
            // Append to prototype
            for (p = 0; p < pLen; p++) {
                prot = proto[p];
                nativeDate.prototype[prot] = function() {
                    var args = Array.prototype.slice.call(arguments);
                    args.unshift(this);
                    return utilDate[prot].apply(utilDate, args);
                };
            }
        },
        /**
     * Calculate how many units are there between two time.
     * @param {Date} min The first time.
     * @param {Date} max The second time.
     * @param {String} unit The unit. This unit is compatible with the date interval constants.
     * @return {Number} The maximum number n of units that min + n * unit <= max.
     */
        diff: function(min, max, unit) {
            var diff = +max - min,
                est;
            switch (unit) {
                case utilDate.MILLI:
                    return diff;
                case utilDate.SECOND:
                    return Math.floor(diff / 1000);
                case utilDate.MINUTE:
                    return Math.floor(diff / 60000);
                case utilDate.HOUR:
                    return Math.floor(diff / 3600000);
                case utilDate.DAY:
                    return Math.floor(diff / 86400000);
                case 'w':
                    return Math.floor(diff / 604800000);
                case utilDate.MONTH:
                    est = (max.getFullYear() * 12 + max.getMonth()) - (min.getFullYear() * 12 + min.getMonth());
                    if (utilDate.add(min, unit, est) > max) {
                        return est - 1;
                    };
                    return est;
                case utilDate.YEAR:
                    est = max.getFullYear() - min.getFullYear();
                    if (utilDate.add(min, unit, est) > max) {
                        return est - 1;
                    } else {
                        return est;
                    };
            }
        },
        /**
     * Align the date to `unit`.
     * @param {Date} date The date to be aligned.
     * @param {String} unit The unit. This unit is compatible with the date interval constants.
     * @param {Number} step
     * @return {Date} The aligned date.
     */
        align: function(date, unit, step) {
            var num = new nativeDate(+date);
            switch (unit.toLowerCase()) {
                case utilDate.MILLI:
                    return num;
                case utilDate.SECOND:
                    num.setUTCSeconds(num.getUTCSeconds() - num.getUTCSeconds() % step);
                    num.setUTCMilliseconds(0);
                    return num;
                case utilDate.MINUTE:
                    num.setUTCMinutes(num.getUTCMinutes() - num.getUTCMinutes() % step);
                    num.setUTCSeconds(0);
                    num.setUTCMilliseconds(0);
                    return num;
                case utilDate.HOUR:
                    num.setUTCHours(num.getUTCHours() - num.getUTCHours() % step);
                    num.setUTCMinutes(0);
                    num.setUTCSeconds(0);
                    num.setUTCMilliseconds(0);
                    return num;
                case utilDate.DAY:
                    if (step === 7 || step === 14) {
                        num.setUTCDate(num.getUTCDate() - num.getUTCDay() + 1);
                    };
                    num.setUTCHours(0);
                    num.setUTCMinutes(0);
                    num.setUTCSeconds(0);
                    num.setUTCMilliseconds(0);
                    return num;
                case utilDate.MONTH:
                    num.setUTCMonth(num.getUTCMonth() - (num.getUTCMonth() - 1) % step, 1);
                    num.setUTCHours(0);
                    num.setUTCMinutes(0);
                    num.setUTCSeconds(0);
                    num.setUTCMilliseconds(0);
                    return num;
                case utilDate.YEAR:
                    num.setUTCFullYear(num.getUTCFullYear() - num.getUTCFullYear() % step, 1, 1);
                    num.setUTCHours(0);
                    num.setUTCMinutes(0);
                    num.setUTCSeconds(0);
                    num.setUTCMilliseconds(0);
                    return date;
            }
        },
        flexParse: function(inDate, defaultFormat) {
            var parts = datePartsRe.exec(inDate),
                firstFormatToken, day, month, year, result;
            // Regex couldn't parse; invalid date.
            if (!parts) {
                return Ext.Date.parse(inDate, 'C');
            }
            // handle "YYYY-MM-DDThh:mm:ssZ"
            // Use this format string to work out what is the desired date order, d/m/y|m/d/y|y/m/d
            if (!defaultFormat) {
                defaultFormat = Ext.Date.defaultFormat;
            }
            // Now the parts array will be:
            // [0] - the full string
            // [1] - The first token if numeric
            // [2] - The first token if alphabetic
            // [3] - The second token if numeric
            // [4] - The second token if alphabetic
            // [5] - The third token.
            // If they've used all numeric parts, we have to use locale order
            // to decide what the parts are. We have three valid choices:
            // d/m/y
            // m/d/y
            // y/m/d
            if (!(parts[2] || parts[4])) {
                firstFormatToken = defaultFormat[0];
                // Not in a y/m/d locale and (first character is a day token, or first
                // token is definitely a day) - it's d/m/y
                // eslint-disable-next-line no-undef
                if (!yearInfo[firstFormatToken] && (dayInfo[firstFormatToken] || (parts[1] > 12 && parts[3] < 13))) {
                    day = parseInt(parts[1]);
                    month = parseInt(parts[3]) - 1;
                    year = parseInt(parts[5]);
                } else if (!yearInfo[firstFormatToken] && (monthInfo[firstFormatToken] || (parts[3] > 12 && parts[1] < 13))) {
                    // Not in a y/m/d locale and (first charecter is a month token, or
                    // first token is definitely a month) - it's m/d/y
                    month = parseInt(parts[1]) - 1;
                    day = parseInt(parts[3]);
                    year = parseInt(parts[5]);
                } else {
                    // y/m/d is the only other valid format
                    year = parseInt(parts[1]);
                    month = parseInt(parts[3]) - 1;
                    day = parseInt(parts[5]);
                }
            } else {
                // They've used an alphabetic month
                // Two alphabetic tokens - not valid.
                if (parts[2] && parts[4]) {
                    return null;
                }
                // m/d/y
                if (parts[2]) {
                    month = utilDate.monthNumbers[Ext.String.capitalize(parts[2].substr(0, 3))];
                    day = parseInt(parts[3]);
                    year = parseInt(parts[5]);
                } else {
                    // d/m/y
                    day = parseInt(parts[1]);
                    month = utilDate.monthNumbers[Ext.String.capitalize(parts[4].substr(0, 3))];
                    year = parseInt(parts[5]);
                }
            }
            // Alphabetic month couldn't be found, or numeric one out of range.
            if (isNaN(month) || (month < 0 || month > 11)) {
                return null;
            }
            // Short years must be upgraded according to the y2kYear setting
            if (year < utilDate.y2kYear) {
                year += 2000;
            }
            // Create the first of the month so that we can check the validity of the day
            result = new Date(year, month, 1, 0, 0, 0);
            // Validate the day of month.
            if (day < 1 || day > Ext.Date.getDaysInMonth(result)) {
                return null;
            }
            result.setDate(day);
            return result;
        }
    };
    utilDate.parseCodes.C = utilDate.parseCodes.c;
    return utilDate;
}());

/**
 * @class Ext.Function
 *
 * A collection of useful static methods to deal with function callbacks.
 * @singleton
 */
/* eslint-disable indent */
Ext.Function = (function() {
    // @define Ext.lang.Function
    // @define Ext.Function
    // @require Ext
    // @require Ext.lang.Array
    var lastTime = 0,
        animFrameId,
        animFrameHandlers = [],
        animFrameNoArgs = [],
        idSource = 0,
        animFrameMap = {},
        slice = Array.prototype.slice,
        win = window,
        global = Ext.global,
        // We disable setImmediate in unit tests because it derails internal Jasmine queue
        hasImmediate = !Ext.disableImmediate && !!(global.setImmediate && global.clearImmediate),
        requestAnimFrame = win.requestAnimationFrame || win.webkitRequestAnimationFrame || win.mozRequestAnimationFrame || win.oRequestAnimationFrame || function(callback) {
            var currTime = Ext.now(),
                timeToCall = Math.max(0, 16 - (currTime - lastTime)),
                timerFn = function() {
                    callback(currTime + timeToCall);
                },
                id;
            timerFn.$origFn = callback.$origFn || callback;
            timerFn.$skipTimerCheck = timerFn.$origFn.$skipTimerCheck;
            id = win.setTimeout(timerFn, timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        },
        fireHandlers = function() {
            var len = animFrameHandlers.length,
                id, i, handler;
            animFrameId = null;
            var timer;
            // eslint-disable-line vars-on-top
            // Fire all animation frame handlers in one go
            for (i = 0; i < len; i++) {
                handler = animFrameHandlers[i];
                id = handler[3];
                // Check if this timer has been canceled; its map entry is going to be removed
                if (animFrameMap[id]) {
                    delete animFrameMap[id];
                    timer = Ext.Timer.get(id, 'raf');
                    if (timer) {
                        timer.tick();
                    }
                    handler[0].apply(handler[1] || global, handler[2] || animFrameNoArgs);
                    if (timer) {
                        timer.tock();
                    }
                }
            }
            // Clear all fired animation frame handlers, don't forget that new handlers
            // could have been created in user handler functions called in the loop above
            animFrameHandlers = animFrameHandlers.slice(len);
        },
        fireElevatedHandlers = function() {
            Ext.elevate(fireHandlers);
        },
        ExtFunction = {
            /**
         * A very commonly used method throughout the framework. It acts as a wrapper around
         * another method which originally accepts 2 arguments for `name` and `value`.
         * The wrapped function then allows "flexible" value setting of either:
         *
         * - `name` and `value` as 2 arguments
         * - one single object argument with multiple key - value pairs
         *
         * For example:
         *
         *     var setValue = Ext.Function.flexSetter(function(name, value) {
         *         this[name] = value;
         *     });
         *
         *     // Afterwards
         *     // Setting a single name - value
         *     setValue('name1', 'value1');
         *
         *     // Settings multiple name - value pairs
         *     setValue({
         *         name1: 'value1',
         *         name2: 'value2',
         *         name3: 'value3'
         *     });
         *
         * @param {Function} setter The single value setter method.
         * @param {String} setter.name The name of the value being set.
         * @param {Object} setter.value The value being set.
         * @return {Function}
         */
            flexSetter: function(setter) {
                return function(name, value) {
                    var k, i;
                    if (name !== null) {
                        if (typeof name !== 'string') {
                            for (k in name) {
                                if (name.hasOwnProperty(k)) {
                                    setter.call(this, k, name[k]);
                                }
                            }
                            if (Ext.enumerables) {
                                for (i = Ext.enumerables.length; i--; ) {
                                    k = Ext.enumerables[i];
                                    if (name.hasOwnProperty(k)) {
                                        setter.call(this, k, name[k]);
                                    }
                                }
                            }
                        } else {
                            setter.call(this, name, value);
                        }
                    }
                    return this;
                };
            },
            /**
         * Create a new function from the provided `fn`, change `this` to the provided scope,
         * optionally overrides arguments for the call. Defaults to the arguments passed by
         * the caller.
         *
         * {@link Ext#bind Ext.bind} is alias for {@link Ext.Function#bind Ext.Function.bind}
         * 
         * **NOTE:** This method is similar to the native `bind()` method. The major difference
         * is in the way the parameters are passed. This method expects an array of parameters,
         * and if supplied, it does not automatically pass forward parameters from the bound
         * function:
         * 
         *      function foo (a, b, c) {
         *          console.log(a, b, c);
         *      }
         *      
         *      var nativeFn = foo.bind(this, 1, 2);
         *      var extFn = Ext.Function.bind(foo, this, [1, 2]);
         *
         *      nativeFn(3); // 1, 2, 3
         *      extFn(3); // 1, 2, undefined
         *
         * This method is unavailable natively on IE8 and IE/Quirks but Ext JS provides a
         * "polyfill" to emulate the important features of the standard `bind` method. In
         * particular, the polyfill only provides binding of "this" and optional arguments.
         * 
         * @param {Function} fn The function to delegate.
         * @param {Object} [scope] The scope (`this` reference) in which the function
         * is executed.
         * **If omitted, defaults to the global environment object (usually the browser `window`).**
         * @param {Array} [args] Overrides arguments for the call. (Defaults to
         * the arguments passed by the caller).
         * @param {Boolean/Number} [appendArgs] if `true` the `args` are appended to the
         * arguments passed to the returned wrapper (by default these arguments are ignored).
         * If a number then the `args` are inserted at the specified position.
         * @return {Function} The bound wrapper function.
         */
            bind: function(fn, scope, args, appendArgs) {
                // Function.prototype.bind is polyfilled in IE8, otherwise native
                if (arguments.length <= 2) {
                    return fn.bind(scope);
                }
                var method = fn;
                // eslint-disable-line vars-on-top
                return function() {
                    var callArgs = args || arguments;
                    if (appendArgs === true) {
                        callArgs = slice.call(arguments, 0);
                        callArgs = callArgs.concat(args);
                    } else if (typeof appendArgs === 'number') {
                        callArgs = slice.call(arguments, 0);
                        // copy arguments first
                        Ext.Array.insert(callArgs, appendArgs, args);
                    }
                    return method.apply(scope || global, callArgs);
                };
            },
            /**
         * Captures the given parameters for a later call to `Ext.callback`. This binding is
         * most useful for resolving scopes for example to an `Ext.app.ViewController`.
         *
         * The arguments match that of `Ext.callback` except for the `args` which, if provided
         * to this method, are prepended to any arguments supplied by the eventual caller of
         * the returned function.
         *
         * @return {Function} A function that, when called, uses `Ext.callback` to call the
         * captured `callback`.
         * @since 5.0.0
         */
            bindCallback: function(callback, scope, args, delay, caller) {
                return function() {
                    var a = slice.call(arguments);
                    return Ext.callback(callback, scope, args ? args.concat(a) : a, delay, caller);
                };
            },
            /**
         * Create a new function from the provided `fn`, the arguments of which are pre-set
         * to `args`. New arguments passed to the newly created callback when it's invoked
         * are appended after the pre-set ones.
         * This is especially useful when creating callbacks.
         *
         * For example:
         *
         *     var originalFunction = function(){
         *         alert(Ext.Array.from(arguments).join(' '));
         *     };
         *
         *     var callback = Ext.Function.pass(originalFunction, ['Hello', 'World']);
         *
         *     callback(); // alerts 'Hello World'
         *     callback('by Me'); // alerts 'Hello World by Me'
         *
         * {@link Ext#pass Ext.pass} is alias for {@link Ext.Function#pass Ext.Function.pass}
         *
         * @param {Function} fn The original function.
         * @param {Array} args The arguments to pass to new callback.
         * @param {Object} scope (optional) The scope (`this` reference) in which the function
         * is executed.
         * @return {Function} The new callback function.
         */
            pass: function(fn, args, scope) {
                if (!Ext.isArray(args)) {
                    if (Ext.isIterable(args)) {
                        args = Ext.Array.clone(args);
                    } else {
                        args = args !== undefined ? [
                            args
                        ] : [];
                    }
                }
                return function() {
                    var fnArgs = args.slice();
                    fnArgs.push.apply(fnArgs, arguments);
                    return fn.apply(scope || this, fnArgs);
                };
            },
            /**
         * Create an alias to the provided method property with name `methodName` of `object`.
         * Note that the execution scope will still be bound to the provided `object` itself.
         *
         * @param {Object/Function} object
         * @param {String} methodName
         * @return {Function} aliasFn
         */
            alias: function(object, methodName) {
                return function() {
                    return object[methodName].apply(object, arguments);
                };
            },
            /**
         * Create a "clone" of the provided method. The returned method will call the given
         * method passing along all arguments and the "this" pointer and return its result.
         *
         * @param {Function} method
         * @return {Function} cloneFn
         */
            clone: function(method) {
                var newMethod, prop;
                newMethod = function() {
                    return method.apply(this, arguments);
                };
                for (prop in method) {
                    if (method.hasOwnProperty(prop)) {
                        newMethod[prop] = method[prop];
                    }
                }
                return newMethod;
            },
            /**
         * Creates an interceptor function. The passed function is called before the original one.
         * If it returns false, the original one is not called. The resulting function returns
         * the results of the original function. The passed function is called with the parameters
         * of the original function. Example usage:
         *
         *     var sayHi = function(name){
         *         alert('Hi, ' + name);
         *     };
         *
         *     sayHi('Fred'); // alerts "Hi, Fred"
         *
         *     // create a new function that validates input without
         *     // directly modifying the original function:
         *     var sayHiToFriend = Ext.Function.createInterceptor(sayHi, function(name){
         *         return name === 'Brian';
         *     });
         *
         *     sayHiToFriend('Fred');  // no alert
         *     sayHiToFriend('Brian'); // alerts "Hi, Brian"
         *
         * @param {Function} origFn The original function.
         * @param {Function} newFn The function to call before the original.
         * @param {Object} [scope] The scope (`this` reference) in which the passed function
         * is executed. **If omitted, defaults to the scope in which the original function
         * is called or the browser window.**
         * @param {Object} [returnValue=null] The value to return if the passed function return
         * `false`.
         * @return {Function} The new function.
         */
            createInterceptor: function(origFn, newFn, scope, returnValue) {
                if (!Ext.isFunction(newFn)) {
                    return origFn;
                } else {
                    returnValue = Ext.isDefined(returnValue) ? returnValue : null;
                    return function() {
                        var me = this,
                            args = arguments;
                        return (newFn.apply(scope || me || global, args) !== false) ? origFn.apply(me || global, args) : returnValue;
                    };
                }
            },
            /**
         * Creates a delegate (callback) which, when called, executes after a specific delay.
         *
         * @param {Function} fn The function which will be called on a delay when the returned
         * function is called. Optionally, a replacement (or additional) argument list
         * may be specified.
         * @param {Number} delay The number of milliseconds to defer execution by whenever called.
         * @param {Object} scope (optional) The scope (`this` reference) used by the function
         * at execution time.
         * @param {Array} args (optional) Override arguments for the call.
         * (Defaults to the arguments passed by the caller)
         * @param {Boolean/Number} appendArgs (optional) if True args are appended to call args
         * instead of overriding, if a number the args are inserted at the specified position.
         * @return {Function} A function which, when called, executes the original function
         * after the specified delay.
         */
            createDelayed: function(fn, delay, scope, args, appendArgs) {
                var boundFn = fn;
                if (scope || args) {
                    boundFn = Ext.Function.bind(fn, scope, args, appendArgs);
                }
                return function() {
                    var me = this,
                        args = slice.call(arguments),
                        timerFn, timerId;
                    var timer;
                    // eslint-disable-line vars-on-top, one-var
                    timerFn = function() {
                        Ext.elevate(boundFn, me, args, timer);
                    };
                    // eslint-disable-line comma-style
                    timerId = setTimeout(timerFn, delay);
                    timerFn.$origFn = fn.$origFn || fn;
                    timerFn.$skipTimerCheck = timerFn.$origFn.$skipTimerCheck;
                    timer = Ext.Timer.created('timeout', timerId, {
                        type: 'createDelayed',
                        fn: fn,
                        timerFn: timerFn
                    });
                };
            },
            /**
         * Calls function `fn` after the number of milliseconds specified, optionally with
         * a specific `scope` (`this` pointer).
         *
         * Example usage:
         *
         *     var sayHi = function(name) {
         *         alert('Hi, ' + name);
         *     }
         *
         *     // executes immediately:
         *     sayHi('Fred');
         *
         *     // executes after 2 seconds:
         *     Ext.defer(sayHi, 2000, this, ['Fred']);
         *
         * The following syntax is useful for scheduling anonymous functions:
         *
         *     Ext.defer(function() {
         *         alert('Anonymous');
         *     }, 100);
         *
         * NOTE: The `Ext.Function.defer()` method is an alias for `Ext.defer()`.
         *
         * @param {Function} fn The function to defer.
         * @param {Number} millis The number of milliseconds for the `setTimeout` call
         * (if less than or equal to 0 the function is executed immediately).
         * @param {Object} scope (optional) The scope (`this` reference) in which the function
         * is executed. **If omitted, defaults to the browser window.**
         * @param {Array} [args] Overrides arguments for the call. Defaults to the arguments passed
         * by the caller.
         * @param {Boolean/Number} [appendArgs=false] If `true` args are appended to call args
         * instead of overriding, or, if a number, then the args are inserted at the specified
         * position.
         * @return {Number} The timeout id that can be used with `Ext.undefer`.
         */
            defer: function(fn, millis, scope, args, appendArgs) {
                var timerId = 0,
                    timerFn, boundFn;
                var timer;
                // eslint-disable-line vars-on-top, one-var
                if (!scope && !args && !appendArgs) {
                    boundFn = fn;
                } else {
                    boundFn = Ext.Function.bind(fn, scope, args, appendArgs);
                }
                if (millis > 0) {
                    timerFn = function() {
                        Ext.elevate(boundFn, null, null, timer);
                    };
                    // eslint-disable-line comma-style
                    timerId = setTimeout(timerFn, millis);
                    timerFn.$origFn = fn.$origFn || fn;
                    timerFn.$skipTimerCheck = timerFn.$origFn.$skipTimerCheck;
                    timer = Ext.Timer.created('timeout', timerId, {
                        type: 'defer',
                        fn: fn,
                        timerFn: timerFn
                    });
                } else {
                    boundFn();
                }
                return timerId;
            },
            /**
         * Calls the function `fn` repeatedly at a given interval, optionally with a
         * specific `scope` (`this` pointer).
         *
         *     var sayHi = function(name) {
         *         console.log('Hi, ' + name);
         *     }
         *
         *     // executes every 2 seconds:
         *     var timerId = Ext.interval(sayHi, 2000, this, ['Fred']);
         *
         * The timer is stopped by:
         *
         *     Ext.uninterval(timerId);
         *
         * NOTE: The `Ext.Function.interval()` method is an alias for `Ext.interval()`.
         *
         * @param {Function} fn The function to defer.
         * @param {Number} millis The number of milliseconds for the `setInterval` call
         * @param {Object} scope (optional) The scope (`this` reference) in which the function
         * is executed. **If omitted, defaults to the browser window.**
         * @param {Array} [args] Overrides arguments for the call. Defaults to the arguments
         * passed by the caller.
         * @param {Boolean/Number} [appendArgs=false] If `true` args are appended to call args
         * instead of overriding, or, if a number, then the args are inserted at the specified
         * position.
         * @return {Number} The interval id that can be used with `Ext.uninterval`.
         */
            interval: function(fn, millis, scope, args, appendArgs) {
                var timerFn, timerId, boundFn;
                var timer;
                // eslint-disable-line vars-on-top, one-var
                boundFn = Ext.Function.bind(fn, scope, args, appendArgs);
                timerFn = function() {
                    Ext.elevate(boundFn, null, null, timer);
                };
                // eslint-disable-line comma-style
                timerId = setInterval(timerFn, millis);
                timerFn.$origFn = boundFn.$origFn || fn;
                timerFn.$skipTimerCheck = timerFn.$origFn.$skipTimerCheck;
                timer = Ext.Timer.created('interval', timerId, {
                    type: 'interval',
                    fn: fn,
                    timerFn: timerFn
                });
                return timerId;
            },
            /**
         * Create a combined function call sequence of the original function + the passed function.
         * The resulting function returns the results of the original function.
         * The passed function is called with the parameters of the original function.
         * Example usage:
         *
         *     var sayHi = function(name){
         *         alert('Hi, ' + name);
         *     };
         *
         *     sayHi('Fred'); // alerts "Hi, Fred"
         *
         *     var sayGoodbye = Ext.Function.createSequence(sayHi, function(name){
         *         alert('Bye, ' + name);
         *     });
         *
         *     sayGoodbye('Fred'); // both alerts show
         *
         * @param {Function} originalFn The original function.
         * @param {Function} newFn The function to sequence.
         * @param {Object} [scope] The scope (`this` reference) in which the passed function
         * is executed. If omitted, defaults to the scope in which the original function is called
         * or the default global environment object (usually the browser window).
         * @return {Function} The new function.
         */
            createSequence: function(originalFn, newFn, scope) {
                if (!newFn) {
                    return originalFn;
                } else {
                    return function() {
                        var result = originalFn.apply(this, arguments);
                        newFn.apply(scope || this, arguments);
                        return result;
                    };
                }
            },
            /**
         * Creates a delegate function, optionally with a bound scope which, when called, buffers
         * the execution of the passed function for the configured number of milliseconds.
         * If called again within that period, the impending invocation will be canceled, and the
         * timeout period will begin again.
         *
         * @param {Function} fn The function to invoke on a buffered timer.
         * @param {Number} buffer The number of milliseconds by which to buffer the invocation
         * of the function.
         * @param {Object} [scope] The scope (`this` reference) in which.
         * the passed function is executed. If omitted, defaults to the scope specified
         * by the caller.
         * @param {Array} [args] Override arguments for the call. Defaults to the arguments
         * passed by the caller.
         * @return {Function} A function which invokes the passed function after buffering
         * for the specified time.
         */
            createBuffered: function(fn, buffer, scope, args) {
                var timerId,
                    result = function() {
                        var callArgs = args || slice.call(arguments, 0),
                            me = scope || this,
                            timerFn;
                        var timer;
                        // eslint-disable-line vars-on-top, one-var
                        if (timerId) {
                            Ext.undefer(timerId);
                        }
                        timerFn = function() {
                            Ext.elevate(fn, me, callArgs, timer);
                        };
                        // eslint-disable-line comma-style
                        result.timer = timerId = setTimeout(timerFn, buffer);
                        timerFn.$origFn = fn.$origFn || fn;
                        timerFn.$skipTimerCheck = timerFn.$origFn.$skipTimerCheck;
                        timer = Ext.Timer.created('timeout', timerId, {
                            type: 'createBuffered',
                            fn: fn,
                            timerFn: timerFn
                        });
                    };
                return result;
            },
            /**
        * Creates a wrapped function that, when invoked, defers execution until the next
        * animation frame
         * @private
         * @param {Function} fn The function to call.
         * @param {Object} [scope] The scope (`this` reference) in which the function is executed.
         * Defaults to the window object.
         * @param {Array} [args] The argument list to pass to the function.
         * @param {Number} [queueStrategy=3] A bit flag that indicates how multiple calls to
         * the returned function within the same animation frame should be handled.
         *
         * - 1: All calls will be queued - FIFO order
         * - 2: Only the first call will be queued
         * - 3: The last call will replace all previous calls
         *
         * @return {Function}
         */
            createAnimationFrame: function(fn, scope, args, queueStrategy) {
                var boundFn, timerId;
                queueStrategy = queueStrategy || 3;
                boundFn = function() {
                    var timerFn,
                        callArgs = args || slice.call(arguments, 0);
                    scope = scope || this;
                    if (queueStrategy === 3 && timerId) {
                        ExtFunction.cancelAnimationFrame(timerId);
                    }
                    if ((queueStrategy & 1) || !timerId) {
                        timerFn = function() {
                            timerId = boundFn.timerId = null;
                            fn.apply(scope, callArgs);
                        };
                        timerFn.$origFn = fn.$origFn || fn;
                        timerFn.$skipTimerCheck = timerFn.$origFn.$skipTimerCheck;
                        timerId = boundFn.timerId = ExtFunction.requestAnimationFrame(timerFn);
                    }
                };
                return boundFn;
            },
            /**
         * @private
         * Schedules the passed function to be called on the next animation frame.
         * @param {Function} fn The function to call.
         * @param {Object} [scope] The scope (`this` reference) in which the function is executed.
         * Defaults to the window object.
         * @param {Mixed[]} [args] The argument list to pass to the function.
         *
         * @return {Number} Timer id for the new animation frame to use when canceling it.
         */
            requestAnimationFrame: function(fn, scope, args) {
                var id = ++idSource,
                    // Ids start at 1
                    handler = slice.call(arguments, 0);
                handler[3] = id;
                animFrameMap[id] = 1;
                // A flag to indicate that the timer exists
                Ext.Timer.created('raf', id, {
                    type: 'raf',
                    fn: fn
                });
                // We might be in fireHandlers at this moment but this new entry will not
                // be executed until the next frame
                animFrameHandlers.push(handler);
                if (!animFrameId) {
                    animFrameId = requestAnimFrame(fireElevatedHandlers);
                }
                return id;
            },
            cancelAnimationFrame: function(id) {
                // Don't remove any handlers from animFrameHandlers array, because
                // the might be in use at the moment (when cancelAnimationFrame is called).
                // Just remove the handler id from the map so it will not be executed
                delete animFrameMap[id];
                Ext.Timer.cancel('raf', id);
            },
            /**
         * Creates a throttled version of the passed function which, when called repeatedly and
         * rapidly, invokes the passed function only after a certain interval has elapsed since the
         * previous invocation.
         *
         * This is useful for wrapping functions which may be called repeatedly, such as
         * a handler of a mouse move event when the processing is expensive.
         *
         * @param {Function} fn The function to execute at a regular time interval.
         * @param {Number} interval The interval in milliseconds on which the passed function
         * is executed.
         * @param {Object} [scope] The scope (`this` reference) in which
         * the passed function is executed. If omitted, defaults to the scope specified
         * by the caller.
         * @return {Function} A function which invokes the passed function at the specified
         * interval.
         */
            createThrottled: function(fn, interval, scope) {
                var lastCallTime = 0,
                    elapsed, lastArgs, timerId,
                    execute = function() {
                        fn.apply(scope, lastArgs);
                        lastCallTime = Ext.now();
                        lastArgs = timerId = null;
                    };
                execute.$origFn = fn.$origFn || fn;
                execute.$skipTimerCheck = execute.$origFn.$skipTimerCheck;
                return function() {
                    // Use scope of last call unless the creator specified a scope
                    if (!scope) {
                        scope = this;
                    }
                    elapsed = Ext.now() - lastCallTime;
                    lastArgs = Ext.Array.slice(arguments);
                    // If this is the first invocation, or the throttle interval has been reached,
                    // clear any pending invocation, and call the target function now.
                    if (elapsed >= interval) {
                        Ext.undefer(timerId);
                        execute();
                    }
                    // Throttle interval has not yet been reached. Only set the timer to fire
                    // if not already set.
                    else if (!timerId) {
                        timerId = Ext.defer(execute, interval - elapsed);
                    }
                };
            },
            /**
         * Wraps the passed function in a barrier function which will call the passed function
         * after the passed number of invocations.
         * @param {Number} count The number of invocations which will result in the calling
         * of the passed function.
         * @param {Function} fn The function to call after the required number of invocations.
         * @param {Object} scope The scope (`this` reference) in which the function will be called.
         */
            createBarrier: function(count, fn, scope) {
                var barrierFn = function() {
                        if (!--count) {
                            fn.apply(scope, arguments);
                        }
                    };
                barrierFn.$origFn = fn.$origFn || fn;
                barrierFn.$skipTimerCheck = barrierFn.$origFn.$skipTimerCheck;
                return barrierFn;
            },
            /**
         * Adds behavior to an existing method that is executed before the
         * original behavior of the function.  For example:
         * 
         *     var soup = {
         *         contents: [],
         *         add: function(ingredient) {
         *             this.contents.push(ingredient);
         *         }
         *     };
         *     Ext.Function.interceptBefore(soup, "add", function(ingredient){
         *         if (!this.contents.length && ingredient !== "water") {
         *             // Always add water to start with
         *             this.contents.push("water");
         *         }
         *     });
         *     soup.add("onions");
         *     soup.add("salt");
         *     soup.contents; // will contain: water, onions, salt
         * 
         * @param {Object} object The target object
         * @param {String} methodName Name of the method to override
         * @param {Function} fn Function with the new behavior.  It will
         * be called with the same arguments as the original method.  The
         * return value of this function will be the return value of the
         * new method.
         * @param {Object} [scope] The scope to execute the interceptor function.
         * Defaults to the object.
         * @return {Function} The new function just created.
         */
            interceptBefore: function(object, methodName, fn, scope) {
                var method = object[methodName] || Ext.emptyFn;
                return (object[methodName] = function() {
                    var ret = fn.apply(scope || this, arguments);
                    method.apply(this, arguments);
                    return ret;
                });
            },
            /**
         * Adds behavior to an existing method that is executed after the
         * original behavior of the function.  For example:
         * 
         *     var soup = {
         *         contents: [],
         *         add: function(ingredient) {
         *             this.contents.push(ingredient);
         *         }
         *     };
         *     Ext.Function.interceptAfter(soup, "add", function(ingredient){
         *         // Always add a bit of extra salt
         *         this.contents.push("salt");
         *     });
         *     soup.add("water");
         *     soup.add("onions");
         *     soup.contents; // will contain: water, salt, onions, salt
         * 
         * @param {Object} object The target object
         * @param {String} methodName Name of the method to override
         * @param {Function} fn Function with the new behavior.  It will
         * be called with the same arguments as the original method.  The
         * return value of this function will be the return value of the
         * new method.
         * @param {Object} [scope] The scope to execute the interceptor function.
         * Defaults to the object.
         * @return {Function} The new function just created.
         */
            interceptAfter: function(object, methodName, fn, scope) {
                var method = object[methodName] || Ext.emptyFn;
                return (object[methodName] = function() {
                    method.apply(this, arguments);
                    return fn.apply(scope || this, arguments);
                });
            },
            interceptAfterOnce: function(object, methodName, fn, scope) {
                var origMethod = object[methodName],
                    newMethod;
                newMethod = function() {
                    var ret;
                    if (origMethod) {
                        origMethod.apply(this, arguments);
                    }
                    ret = fn.apply(scope || this, arguments);
                    object[methodName] = origMethod;
                    object = methodName = fn = scope = origMethod = newMethod = null;
                    return ret;
                };
                object[methodName] = newMethod;
                return newMethod;
            },
            makeCallback: function(callback, scope) {
                if (!scope[callback]) {
                    if (scope.$className) {
                        Ext.raise('No method "' + callback + '" on ' + scope.$className);
                    }
                    Ext.raise('No method "' + callback + '"');
                }
                return function() {
                    return scope[callback].apply(scope, arguments);
                };
            },
            /**
         * Returns a wrapper function that caches the return value for previously
         * processed function argument(s).
         *
         * For example:
         *
         *      function factorial (value) {
         *          var ret = value;
         *
         *          while (--value > 1) {
         *              ret *= value;
         *          }
         *
         *          return ret;
         *      }
         *
         * Each call to `factorial` will loop and multiply to produce the answer. Using
         * this function we can wrap the above and cache its answers:
         *
         *      factorial = Ext.Function.memoize(factorial);
         *
         * The returned function operates in the same manner as before, but results are
         * stored in a cache to avoid calling the wrapped function when given the same
         * arguments.
         *
         *      var x = factorial(20);  // first time; call real factorial()
         *      var y = factorial(20);  // second time; return value from first call
         *
         * To support multi-argument methods, you will need to provide a `hashFn`.
         *
         *      function permutation (n, k) {
         *          return factorial(n) / factorial(n - k);
         *      }
         *
         *      permutation = Ext.Function.memoize(permutation, null, function(n, k) {
         *          n + '-' + k;
         *      });
         *
         * In this case, the `memoize` of `factorial` is sufficient optimization, but the
         * example is simply to illustrate how to generate a unique key for an expensive,
         * multi-argument method.
         *
         * **IMPORTANT**: This cache is unbounded so be cautious of memory leaks if the
         * `memoize`d function is kept indefinitely or is given an unbounded set of
         * possible arguments.
         *
         * @param {Function} fn Function to wrap.
         * @param {Object} scope Optional scope in which to execute the wrapped function.
         * @param {Function} hashFn Optional function used to compute a hash key for
         * storing the result, based on the arguments to the original function.
         * @return {Function} The caching wrapper function.
         * @since 6.0.0
         */
            memoize: function(fn, scope, hashFn) {
                var memo = {},
                    isFunc = hashFn && Ext.isFunction(hashFn);
                return function(value) {
                    var key = isFunc ? hashFn.apply(scope, arguments) : value;
                    if (!(key in memo)) {
                        memo[key] = fn.apply(scope, arguments);
                    }
                    return memo[key];
                };
            },
            _stripCommentRe: /(\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(\/\/.*)/g,
            toCode: function(fn) {
                var s = fn ? fn.toString() : '';
                s = s.replace(ExtFunction._stripCommentRe, '');
                return s;
            },
            // This is useful for unit testing so we can force handlers which have been deferred
            // to the next animation frame to run immediately
            fireElevatedHandlers: function() {
                // eslint-disable-line comma-style
                fireElevatedHandlers();
            }
        };
    // ExtFunction
    /**
     * @member Ext
     * @method asap
     * Schedules the specified callback function to be executed on the next turn of the
     * event loop. Where available, this method uses the browser's `setImmediate` API. If
     * not available, this method substitutes `setTimeout(0)`. Though not a perfect
     * replacement for `setImmediate` it is sufficient for many use cases.
     *
     * For more details see [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate).
     *
     * @param {Function} fn Callback function.
     * @param {Object} [scope] The scope for the callback (`this` pointer).
     * @param {Mixed[]} [parameters] Additional parameters to pass to `fn`.
     * @return {Number} A cancellation id for `{@link Ext#unasap}`.
     */
    Ext.asap = hasImmediate ? function(fn, scope, parameters) {
        var boundFn = fn,
            timerFn, timerId;
        var timer;
        // eslint-disable-line vars-on-top, one-var
        if (scope != null || parameters != null) {
            boundFn = ExtFunction.bind(fn, scope, parameters);
        }
        timerFn = function() {
            Ext.elevate(boundFn, null, null, timer);
        };
        // eslint-disable-line comma-style
        timerId = setImmediate(timerFn);
        timerFn.$origFn = fn.$origFn || fn;
        timerFn.$skipTimerCheck = timerFn.$origFn.$skipTimerCheck;
        timer = Ext.Timer.created('asap', timerId, {
            type: 'asap',
            fn: fn,
            timerFn: timerFn
        });
        return timerId;
    } : function(fn, scope, parameters) {
        var boundFn = fn,
            timerFn, timerId;
        var timer;
        // eslint-disable-line vars-on-top, one-var
        if (scope != null || parameters != null) {
            boundFn = ExtFunction.bind(fn, scope, parameters);
        }
        timerFn = function() {
            Ext.elevate(boundFn, null, null, timer);
        };
        // eslint-disable-line comma-style
        timerId = setTimeout(timerFn, 0, true);
        timerFn.$origFn = fn.$origFn || fn;
        timerFn.$skipTimerCheck = timerFn.$origFn.$skipTimerCheck;
        timer = Ext.Timer.created('timeout', timerId, {
            type: 'asap',
            fn: fn,
            timerFn: timerFn
        });
        return timerId;
    };
    /**
     * @member Ext
     * @method unasap
     * Cancels a previously scheduled call to `{@link Ext#asap}`.
     *
     *      var timerId = Ext.asap(me.method, me);
     *      ...
     *
     *      if (nevermind) {
     *          Ext.unasap(timerId);
     *      }
     *
     * This method always returns `null` to enable simple cleanup:
     *
     *      timerId = Ext.unasap(timerId);  // safe even if !timerId
     *
     * @param {Number} id The id returned by `{@link Ext#asap}`.
     * @return {Object} Always returns `null`.
     */
    Ext.unasap = hasImmediate ? function(id) {
        if (id) {
            clearImmediate(id);
            Ext.Timer.cancel('asap', id);
        }
        return null;
    } : function(id) {
        return Ext.undefer(id);
    };
    /**
     * @member Ext
     * @method asapCancel
     * Cancels a previously scheduled call to `{@link Ext#asap}`.
     * @param {Number} id The id returned by `{@link Ext#asap}`.
     * @deprecated 6.5.1 Use `Ext.unasap` instead.
     */
    Ext.asapCancel = function(id) {
        return Ext.unasap(id);
    };
    /**
     * @method defer
     * @member Ext
     * @inheritdoc Ext.Function#defer
     */
    Ext.defer = ExtFunction.defer;
    /**
     * @member Ext
     * @method undefer
     * Cancels a previously scheduled call to `{@link Ext#defer}`.
     *
     *      var timerId = Ext.defer(me.method, me);
     *      ...
     *
     *      if (nevermind) {
     *          Ext.undefer(timerId);
     *      }
     *
     * This method always returns `null` to enable simple cleanup:
     *
     *      timerId = Ext.undefer(timerId);  // safe even if !timerId
     *
     * @param {Number} id The id returned by `{@link Ext#defer}`.
     */
    Ext.undefer = function(id) {
        if (id) {
            clearTimeout(id);
            Ext.Timer.cancel('timeout', id);
        }
        return null;
    };
    /**
     * @method interval
     * @member Ext
     * @inheritdoc Ext.Function#interval
     */
    Ext.interval = ExtFunction.interval;
    /**
     * @member Ext
     * @method uninterval
     * Cancels a previously scheduled call to `{@link Ext#interval}`.
     *
     *      var timerId = Ext.interval(me.method, me);
     *      ...
     *
     *      if (nevermind) {
     *          Ext.uninterval(timerId);
     *      }
     *
     * This method always returns `null` to enable simple cleanup:
     *
     *      timerId = Ext.uninterval(timerId);  // safe even if !timerId
     *
     * @param {Number} id The id returned by `{@link Ext#interval}`.
     */
    Ext.uninterval = function(id) {
        if (id) {
            clearInterval(id);
            Ext.Timer.cancel('interval', id);
        }
        return null;
    };
    /**
     * @method pass
     * @member Ext
     * @inheritdoc Ext.Function#pass
     */
    Ext.pass = ExtFunction.pass;
    /**
     * @method bind
     * @member Ext
     * @inheritdoc Ext.Function#bind
     */
    Ext.bind = ExtFunction.bind;
    Ext.raf = function() {
        return ExtFunction.requestAnimationFrame.apply(ExtFunction, arguments);
    };
    Ext.unraf = function(id) {
        ExtFunction.cancelAnimationFrame(id);
    };
    return ExtFunction;
})();

/**
 * @class Ext.Number
 *
 * A collection of useful static methods to deal with numbers
 * @singleton
 */
Ext.Number = (new function() {
    // jshint ignore:line
    // @define Ext.lang.Number
    // @define Ext.Number
    // @require Ext
    var ExtNumber = this,
        isToFixedBroken = (0.9).toFixed() !== '1',
        math = Math,
        ClipDefault = {
            count: false,
            inclusive: false,
            wrap: true
        };
    // polyfill
    Number.MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER || -(math.pow(2, 53) - 1);
    Number.MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || math.pow(2, 53) - 1;
    Ext.apply(ExtNumber, {
        MIN_SAFE_INTEGER: Number.MIN_SAFE_INTEGER,
        MAX_SAFE_INTEGER: Number.MAX_SAFE_INTEGER,
        // No good way to allow "9." w/o allowing "." alone but we use isNaN to reject that
        floatRe: /^[-+]?(?:\d+|\d*\.\d*)(?:[Ee][+-]?\d+)?$/,
        intRe: /^[-+]?\d+(?:[Ee]\+?\d+)?$/,
        Clip: {
            DEFAULT: ClipDefault,
            COUNT: Ext.applyIf({
                count: true
            }, ClipDefault),
            INCLUSIVE: Ext.applyIf({
                inclusive: true
            }, ClipDefault),
            NOWRAP: Ext.applyIf({
                wrap: false
            }, ClipDefault)
        },
        /**
         * Strictly parses the given value and returns the value as a number or `null` if
         * the value is not a number or contains non-numeric pieces.
         * @param {String} value
         * @return {Number}
         * @since 6.5.1
         */
        parseFloat: function(value) {
            if (value === undefined) {
                value = null;
            }
            if (value !== null && typeof value !== 'number') {
                value = String(value);
                value = ExtNumber.floatRe.test(value) ? +value : null;
                if (isNaN(value)) {
                    value = null;
                }
            }
            return value;
        },
        /**
         * Strictly parses the given value and returns the value as a number or `null` if
         * the value is not an integer number or contains non-integer pieces.
         * @param {String} value
         * @return {Number}
         * @since 6.5.1
         */
        parseInt: function(value) {
            if (value === undefined) {
                value = null;
            }
            if (typeof value === 'number') {
                value = Math.floor(value);
            } else if (value !== null) {
                value = String(value);
                value = ExtNumber.intRe.test(value) ? +value : null;
            }
            return value;
        },
        binarySearch: function(array, value, begin, end) {
            var middle, midVal;
            if (begin === undefined) {
                begin = 0;
            }
            if (end === undefined) {
                end = array.length;
            }
            --end;
            while (begin <= end) {
                middle = (begin + end) >>> 1;
                // unsigned right shift = Math.floor(x/2)
                midVal = array[middle];
                if (value === midVal) {
                    return middle;
                }
                if (midVal < value) {
                    begin = middle + 1;
                } else {
                    end = middle - 1;
                }
            }
            return begin;
        },
        bisectTuples: function(array, value, index, begin, end) {
            var middle, midVal;
            if (begin === undefined) {
                begin = 0;
            }
            if (end === undefined) {
                end = array.length;
            }
            --end;
            while (begin <= end) {
                middle = (begin + end) >>> 1;
                // unsigned right shift = Math.floor(x/2)
                midVal = array[middle][index];
                if (value === midVal) {
                    return middle;
                }
                if (midVal < value) {
                    begin = middle + 1;
                } else {
                    end = middle - 1;
                }
            }
            return begin;
        },
        /**
         * Coerces a given index into a valid index given a `length`.
         *
         * Negative indexes are interpreted starting at the end of the collection. That is,
         * a value of -1 indicates the last item, or equivalent to `length - 1`.
         *
         * When handling methods that take "begin" and "end" arguments like most array or
         * string methods, this method can be used like so:
         *
         *      function foo (array, begin, end) {
         *          var range = Ext.Number.clipIndices(array.length, [begin, end]);
         *
         *          begin = range[0];
         *          end   = range[1];
         *
         *          // 0 <= begin <= end <= array.length
         *
         *          var length = end - begin;
         *      }
         *
         * For example:
         *
         *      +---+---+---+---+---+---+---+---+
         *      |   |   |   |   |   |   |   |   |  length = 8
         *      +---+---+---+---+---+---+---+---+
         *        0   1   2   3   4   5   6   7
         *       -8  -7  -6  -5  -4  -3  -2  -1
         *
         *      console.log(Ext.Number.clipIndices(8, [3, 10]); // logs "[3, 8]"
         *      console.log(Ext.Number.clipIndices(8, [-5]);    // logs "[3, 8]"
         *      console.log(Ext.Number.clipIndices(8, []);
         *      console.log(Ext.Number.clipIndices(8, []);
         *
         * @param {Number} length
         * @param {Number[]} indices
         * @param {Object} [options] An object with different option flags.
         * @param {Boolean} [options.count=false] The second number in `indices` is the
         * count not and an index.
         * @param {Boolean} [options.inclusive=false] The second number in `indices` is
         * "inclusive" meaning that the item should be considered in the range. Normally,
         * the second number is considered the first item outside the range or as an
         * "exclusive" bound.
         * @param {Boolean} [options.wrap=true] Wraps negative numbers backwards from the
         * end of the array. Passing `false` simply clips negative index values at 0.
         * @return {Number[]} The normalized `[begin, end]` array where `end` is now
         * exclusive such that `length = end - begin`. Both values are between 0 and the
         * given `length` and `end` will not be less-than `begin`.
         */
        clipIndices: function(length, indices, options) {
            var defaultValue = 0,
                // default value for "begin"
                wrap, begin, end, i;
            options = options || ClipDefault;
            wrap = options.wrap;
            indices = indices || [];
            for (i = 0; i < 2; ++i) {
                // names are off on first pass but used this way so things make sense
                // following the loop..
                begin = end;
                // pick up and keep the result from the first loop
                end = indices[i];
                if (end == null) {
                    end = defaultValue;
                } else if (i && options.count) {
                    end += begin;
                    // this is the length not "end" so convert to "end"
                    end = (end > length) ? length : end;
                } else {
                    if (wrap) {
                        end = (end < 0) ? (length + end) : end;
                    }
                    if (i && options.inclusive) {
                        ++end;
                    }
                    end = (end < 0) ? 0 : ((end > length) ? length : end);
                }
                defaultValue = length;
            }
            // default value for "end"
            // after loop:
            // 0 <= begin <= length  (calculated from indices[0])
            // 0 <= end <= length    (calculated from indices[1])
            indices[0] = begin;
            indices[1] = (end < begin) ? begin : end;
            return indices;
        },
        /**
         * Checks whether or not the passed number is within a desired range. If the number is
         * already within the range it is returned, otherwise the min or max value is returned
         * depending on which side of the range is exceeded. Note that this method returns the
         * constrained value but does not change the current number.
         * @param {Number} number The number to check
         * @param {Number} min The minimum number in the range
         * @param {Number} max The maximum number in the range
         * @return {Number} The constrained value if outside the range, otherwise the current value
         */
        constrain: function(number, min, max) {
            var x = parseFloat(number);
            // (x < Nan) || (x < undefined) == false
            // same for (x > NaN) || (x > undefined)
            // sadly this is not true of null - (1 > null)
            if (min === null) {
                min = number;
            }
            if (max === null) {
                max = number;
            }
            // Watch out for NaN in Chrome 18
            // V8bug: http://code.google.com/p/v8/issues/detail?id=2056
            // Operators are faster than Math.min/max. See http://jsperf.com/number-constrain
            return (x < min) ? min : ((x > max) ? max : x);
        },
        /**
         * Snaps the passed number between stopping points based upon a passed increment value.
         *
         * The difference between this and {@link #snapInRange} is that {@link #snapInRange} uses
         * the minValue when calculating snap points:
         *
         *     // Returns 56 - snap points are zero based
         *     r = Ext.Number.snap(56, 2, 55, 65);
         *
         *     // Returns 57 - snap points are based from minValue
         *     r = Ext.Number.snapInRange(56, 2, 55, 65);
         *
         * @param {Number} value The unsnapped value.
         * @param {Number} increment The increment by which the value must move.
         * @param {Number} minValue The minimum value to which the returned value must be
         * constrained. Overrides the increment.
         * @param {Number} maxValue The maximum value to which the returned value must be
         * constrained. Overrides the increment.
         * @return {Number} The value of the nearest snap target.
         */
        snap: function(value, increment, minValue, maxValue) {
            var m;
            // If no value passed, or minValue was passed and value is less than minValue
            // (anything < undefined is false)
            // Then use the minValue (or zero if the value was undefined)
            if (value === undefined || value < minValue) {
                return minValue || 0;
            }
            if (increment) {
                m = value % increment;
                if (m !== 0) {
                    value -= m;
                    if (m * 2 >= increment) {
                        value += increment;
                    } else if (m * 2 < -increment) {
                        value -= increment;
                    }
                }
            }
            return ExtNumber.constrain(value, minValue, maxValue);
        },
        /**
         * Snaps the passed number between stopping points based upon a passed increment value.
         *
         * The difference between this and {@link #snap} is that {@link #snap} does not use
         * the minValue when calculating snap points:
         *
         *     // Returns 56 - snap points are zero based
         *     r = Ext.Number.snap(56, 2, 55, 65);
         *
         *     // Returns 57 - snap points are based from minValue
         *     r = Ext.Number.snapInRange(56, 2, 55, 65);
         *
         * @param {Number} value The unsnapped value.
         * @param {Number} increment The increment by which the value must move.
         * @param {Number} [minValue=0] The minimum value to which the returned value must be
         * constrained.
         * @param {Number} [maxValue=Infinity] The maximum value to which the returned value
         * must be constrained.
         * @return {Number} The value of the nearest snap target.
         */
        snapInRange: function(value, increment, minValue, maxValue) {
            var tween;
            // default minValue to zero
            minValue = (minValue || 0);
            // If value is undefined, or less than minValue, use minValue
            if (value === undefined || value < minValue) {
                return minValue;
            }
            // Calculate how many snap points from the minValue the passed value is.
            if (increment && (tween = ((value - minValue) % increment))) {
                value -= tween;
                tween *= 2;
                if (tween >= increment) {
                    value += increment;
                }
            }
            // If constraining within a maximum, ensure the maximum is on a snap point
            if (maxValue !== undefined) {
                if (value > (maxValue = ExtNumber.snapInRange(maxValue, increment, minValue))) {
                    value = maxValue;
                }
            }
            return value;
        },
        /**
         * Round a number to the nearest interval.
         * @param {Number} value The value to round.
         * @param {Number} interval The interval to round to.
         * @return {Number} The rounded value.
         *
         * @since 6.2.0
         */
        roundToNearest: function(value, interval) {
            interval = interval || 1;
            return interval * math.round(value / interval);
        },
        /**
         * Rounds a number to the specified precision.
         * @param value
         * @param precision
         * @return {number}
         */
        roundToPrecision: function(value, precision) {
            var factor = math.pow(10, precision || 1);
            return math.round(value * factor) / factor;
        },
        /**
         * Truncates a number to the specified precision,
         * without rounding.
         * @param value
         * @param precision
         * @return {number}
         * @since 6.5.1
         */
        truncateToPrecision: function(value, precision) {
            var factor = math.pow(10, precision || 1);
            return parseInt(value * factor, 10) / factor;
        },
        /**
         * Returns the sign of the given number. See also MDN for Math.sign documentation
         * for the standard method this method emulates.
         * @param {Number} x The number.
         * @return {Number} The sign of the number `x`, indicating whether the number is
         * positive (1), negative (-1) or zero (0).
         * @method sign
         */
        sign: math.sign || function(x) {
            x = +x;
            // force to a Number
            if (x === 0 || isNaN(x)) {
                return x;
            }
            return (x > 0) ? 1 : -1;
        },
        /**
         * Returns the base 10 logarithm of a number.
         * This will use Math.log10, if available (ES6).
         * @param {Number} x The number.
         * @return {Number} Base 10 logarithm of the number 'x'.
         * @method log10
         */
        log10: math.log10 || function(x) {
            return math.log(x) * math.LOG10E;
        },
        /**
         * Determines if two numbers `n1` and `n2` are equal within a given
         * margin of precision `epsilon`.
         * @param {Number} n1 First number.
         * @param {Number} n2 Second number.
         * @param {Number} epsilon Margin of precision.
         * @return {Boolean} `true`, if numbers are equal. `false` otherwise.
         */
        isEqual: function(n1, n2, epsilon) {
            /* eslint-disable-next-line max-len */
            if (!(typeof n1 === 'number' && typeof n2 === 'number' && typeof epsilon === 'number')) {
                Ext.raise("All parameters should be valid numbers.");
            }
            return math.abs(n1 - n2) < epsilon;
        },
        /**
         * Determines if the value passed is a number and also finite.
         * This a Polyfill version of Number.isFinite(),differently than
         * the isFinite() function, this method doesn't convert the parameter to a number.
         * @param {Number} value Number to be tested.
         * @return {Boolean} `true`, if the parameter is a number and finite, `false` otherwise.
         * @since 6.2
         */
        isFinite: Number.isFinite || function(value) {
            return typeof value === 'number' && isFinite(value);
        },
        isInteger: Number.isInteger || function(value) {
            // Add zero get a valid result in a special case where the value is a number string.
            // E.g. '10' + 0 is '100'.
            return ~~(value + 0) === value;
        },
        /**
         * @method
         * Formats a number using fixed-point notation
         * @param {Number} value The number to format
         * @param {Number} precision The number of digits to show after the decimal point
         */
        toFixed: isToFixedBroken ? function(value, precision) {
            var pow;
            precision = precision || 0;
            pow = math.pow(10, precision);
            return (math.round(value * pow) / pow).toFixed(precision);
        } : function(value, precision) {
            return value.toFixed(precision);
        },
        /**
         * Validate that a value is numeric and convert it to a number if necessary.
         * Returns the specified default value if it is not.
         *
         *      Ext.Number.from('1.23', 1); // returns 1.23
         *      Ext.Number.from('abc', 1); // returns 1
         *
         * @param {Object} value
         * @param {Number} defaultValue The value to return if the original value is non-numeric
         * @return {Number} value, if numeric, defaultValue otherwise
         */
        from: function(value, defaultValue) {
            if (isFinite(value)) {
                value = parseFloat(value);
            }
            return !isNaN(value) ? value : defaultValue;
        },
        /**
         * Returns a random integer between the specified range (inclusive)
         * @param {Number} from Lowest value to return.
         * @param {Number} to Highest value to return.
         * @return {Number} A random integer within the specified range.
         */
        randomInt: function(from, to) {
            return math.floor(math.random() * (to - from + 1) + from);
        },
        /**
         * Corrects floating point numbers that overflow to a non-precise
         * value because of their floating nature, for example `0.1 + 0.2`
         * @param {Number} n The number
         * @return {Number} The correctly rounded number
         */
        correctFloat: function(n) {
            // This is to correct the type of errors where 2 floats end with
            // a long string of decimals, eg 0.1 + 0.2. When they overflow in this
            // manner, they usually go to 15-16 decimals, so we cut it off at 14.
            return parseFloat(n.toPrecision(14));
        }
    });
    /**
     * @deprecated 4.0.0 Please use {@link Ext.Number#from} instead.
     * @member Ext
     * @method num
     * @inheritdoc Ext.Number#from
     */
    Ext.num = function() {
        return ExtNumber.from.apply(this, arguments);
    };
}());

/**
 * @class Ext.Object
 *
 * A collection of useful static methods to deal with objects.
 *
 * @singleton
 */
(function() {
    // The "constructor" for chain:
    var TemplateClass = function() {},
        queryRe = /^\?/,
        keyRe = /(\[):?([^\]]*)\]/g,
        nameRe = /^([^\[]+)/,
        // eslint-disable-line no-useless-escape
        plusRe = /\+/g,
        ExtObject;
    // @define Ext.lang.Object
    // @define Ext.Object
    // @require Ext
    // @require Ext.lang.Date
    ExtObject = Ext.Object = {
        /**
     * @method
     * Returns a new object with the given object as the prototype chain. This method is
     * designed to mimic the ECMA standard `Object.create` method and is assigned to that
     * function when it is available.
     * 
     * **NOTE** This method does not support the property definitions capability of the
     * `Object.create` method. Only the first argument is supported.
     * 
     * @param {Object} object The prototype chain for the new object.
     */
        chain: Object.create || function(object) {
            var result;
            TemplateClass.prototype = object;
            result = new TemplateClass();
            TemplateClass.prototype = null;
            return result;
        },
        /**
     * This method removes all keys from the given object.
     * @param {Object} object The object from which to remove all keys.
     * @return {Object} The given object.
     */
        clear: function(object) {
            var key;
            // Safe to delete during iteration
            for (key in object) {
                delete object[key];
            }
            return object;
        },
        /**
     * Freezes the given object making it immutable. This operation is by default shallow
     * and does not effect objects referenced by the given object.
     * 
     * @method
     * @param {Object} obj The object to freeze.
     * @param {Boolean} [deep=false] Pass `true` to freeze sub-objects recursively.
     * @return {Object} The given object `obj`.
     */
        freeze: Object.freeze ? function(obj, deep) {
            var name;
            if (obj && typeof obj === 'object' && !Object.isFrozen(obj)) {
                Object.freeze(obj);
                if (deep) {
                    for (name in obj) {
                        ExtObject.freeze(obj[name], deep);
                    }
                }
            }
            return obj;
        } : Ext.identityFn,
        /**
     * Converts a `name` - `value` pair to an array of objects with support for nested structures.
     * Useful to construct query strings. For example:
     *
     *     var objects = Ext.Object.toQueryObjects('hobbies', ['reading', 'cooking', 'swimming']);
     *
     *     // objects then equals:
     *     [
     *         { name: 'hobbies', value: 'reading' },
     *         { name: 'hobbies', value: 'cooking' },
     *         { name: 'hobbies', value: 'swimming' },
     *     ];
     *
     *     var objects = Ext.Object.toQueryObjects('dateOfBirth', {
     *         day: 3,
     *         month: 8,
     *         year: 1987,
     *         extra: {
     *             hour: 4
     *             minute: 30
     *         }
     *     }, true); // Recursive
     *
     *     // objects then equals:
     *     [
     *         { name: 'dateOfBirth[day]', value: 3 },
     *         { name: 'dateOfBirth[month]', value: 8 },
     *         { name: 'dateOfBirth[year]', value: 1987 },
     *         { name: 'dateOfBirth[extra][hour]', value: 4 },
     *         { name: 'dateOfBirth[extra][minute]', value: 30 },
     *     ];
     *
     * @param {String} name
     * @param {Object/Array} value
     * @param {Boolean} [recursive=false] True to traverse object recursively
     * @return {Object[]}
     */
        toQueryObjects: function(name, value, recursive) {
            var self = ExtObject.toQueryObjects,
                objects = [],
                i, ln;
            if (Ext.isArray(value)) {
                for (i = 0 , ln = value.length; i < ln; i++) {
                    if (recursive) {
                        objects = objects.concat(self(name + '[' + i + ']', value[i], true));
                    } else {
                        objects.push({
                            name: name,
                            value: value[i]
                        });
                    }
                }
            } else if (Ext.isObject(value)) {
                for (i in value) {
                    if (value.hasOwnProperty(i)) {
                        if (recursive) {
                            objects = objects.concat(self(name + '[' + i + ']', value[i], true));
                        } else {
                            objects.push({
                                name: name,
                                value: value[i]
                            });
                        }
                    }
                }
            } else {
                objects.push({
                    name: name,
                    value: value
                });
            }
            return objects;
        },
        /* eslint-disable max-len */
        /**
     * Takes an object and converts it to an encoded query string.
     *
     * Non-recursive:
     *
     *     Ext.Object.toQueryString({foo: 1, bar: 2}); // returns "foo=1&bar=2"
     *     Ext.Object.toQueryString({foo: null, bar: 2}); // returns "foo=&bar=2"
     *     Ext.Object.toQueryString({'some price': '$300'}); // returns "some%20price=%24300"
     *     Ext.Object.toQueryString({date: new Date(2011, 0, 1)}); // returns "date=%222011-01-01T00%3A00%3A00%22"
     *     Ext.Object.toQueryString({colors: ['red', 'green', 'blue']}); // returns "colors=red&colors=green&colors=blue"
     *
     * Recursive:
     *
     *     Ext.Object.toQueryString({
     *         username: 'Jacky',
     *         dateOfBirth: {
     *             day: 1,
     *             month: 2,
     *             year: 1911
     *         },
     *         hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
     *     }, true); // returns the following string (broken down and url-decoded for ease of reading purpose):
     *     // username=Jacky
     *     //    &dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911
     *     //    &hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&hobbies[3][0]=nested&hobbies[3][1]=stuff
     *
     * @param {Object} object The object to encode
     * @param {Boolean} [recursive=false] Whether or not to interpret the object in recursive format.
     * (PHP / Ruby on Rails servers and similar).
     * @return {String} queryString
     */
        toQueryString: function(object, recursive) {
            var paramObjects = [],
                params = [],
                i, j, ln, paramObject, value;
            for (i in object) {
                if (object.hasOwnProperty(i)) {
                    paramObjects = paramObjects.concat(ExtObject.toQueryObjects(i, object[i], recursive));
                }
            }
            for (j = 0 , ln = paramObjects.length; j < ln; j++) {
                paramObject = paramObjects[j];
                value = paramObject.value;
                if (Ext.isEmpty(value)) {
                    value = '';
                } else if (Ext.isDate(value)) {
                    value = Ext.Date.toString(value);
                }
                params.push(encodeURIComponent(paramObject.name) + '=' + encodeURIComponent(String(value)));
            }
            return params.join('&');
        },
        /**
     * Converts a query string back into an object.
     *
     * Non-recursive:
     *
     *     Ext.Object.fromQueryString("foo=1&bar=2"); // returns {foo: '1', bar: '2'}
     *     Ext.Object.fromQueryString("foo=&bar=2"); // returns {foo: '', bar: '2'}
     *     Ext.Object.fromQueryString("some%20price=%24300"); // returns {'some price': '$300'}
     *     Ext.Object.fromQueryString("colors=red&colors=green&colors=blue"); // returns {colors: ['red', 'green', 'blue']}
     *
     * Recursive:
     *
     *     Ext.Object.fromQueryString(
     *         "username=Jacky&"+
     *         "dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911&"+
     *         "hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&"+
     *         "hobbies[3][0]=nested&hobbies[3][1]=stuff", true);
     *
     *     // returns
     *     {
     *         username: 'Jacky',
     *         dateOfBirth: {
     *             day: '1',
     *             month: '2',
     *             year: '1911'
     *         },
     *         hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
     *     }
     *
     * @param {String} queryString The query string to decode
     * @param {Boolean} [recursive=false] Whether or not to recursively decode the string. This format is supported by
     * PHP / Ruby on Rails servers and similar.
     * @return {Object}
     */
        fromQueryString: function(queryString, recursive) {
            var parts = queryString.replace(queryRe, '').split('&'),
                object = {},
                temp, components, name, value, i, ln, part, j, subLn, matchedKeys, matchedName, keys, key, nextKey;
            for (i = 0 , ln = parts.length; i < ln; i++) {
                part = parts[i];
                if (part.length > 0) {
                    components = part.split('=');
                    name = components[0];
                    name = name.replace(plusRe, '%20');
                    name = decodeURIComponent(name);
                    value = components[1];
                    if (value !== undefined) {
                        value = value.replace(plusRe, '%20');
                        value = decodeURIComponent(value);
                    } else {
                        value = '';
                    }
                    if (!recursive) {
                        if (object.hasOwnProperty(name)) {
                            if (!Ext.isArray(object[name])) {
                                object[name] = [
                                    object[name]
                                ];
                            }
                            object[name].push(value);
                        } else {
                            object[name] = value;
                        }
                    } else {
                        matchedKeys = name.match(keyRe);
                        matchedName = name.match(nameRe);
                        if (!matchedName) {
                            throw new Error('[Ext.Object.fromQueryString] Malformed query string given, failed parsing name from "' + part + '"');
                        }
                        name = matchedName[0];
                        keys = [];
                        if (matchedKeys === null) {
                            object[name] = value;
                            
                            continue;
                        }
                        for (j = 0 , subLn = matchedKeys.length; j < subLn; j++) {
                            key = matchedKeys[j];
                            key = (key.length === 2) ? '' : key.substring(1, key.length - 1);
                            keys.push(key);
                        }
                        keys.unshift(name);
                        temp = object;
                        for (j = 0 , subLn = keys.length; j < subLn; j++) {
                            key = keys[j];
                            if (j === subLn - 1) {
                                if (Ext.isArray(temp) && key === '') {
                                    temp.push(value);
                                } else {
                                    temp[key] = value;
                                }
                            } else {
                                if (temp[key] === undefined || typeof temp[key] === 'string') {
                                    nextKey = keys[j + 1];
                                    temp[key] = (Ext.isNumeric(nextKey) || nextKey === '') ? [] : {};
                                }
                                temp = temp[key];
                            }
                        }
                    }
                }
            }
            return object;
        },
        /* eslint-enable max-len */
        /**
     * Iterates through an object and invokes the given callback function for each iteration.
     * The iteration can be stopped by returning `false` in the callback function. For example:
     *
     *     var person = {
     *         name: 'Jacky'
     *         hairColor: 'black'
     *         loves: ['food', 'sleeping', 'wife']
     *     };
     *
     *     Ext.Object.each(person, function(key, value, myself) {
     *         console.log(key + ":" + value);
     *
     *         if (key === 'hairColor') {
     *             return false; // stop the iteration
     *         }
     *     });
     *
     * @param {Object} object The object to iterate
     * @param {Function} fn The callback function.
     * @param {String} fn.key
     * @param {Object} fn.value
     * @param {Object} fn.object The object itself
     * @param {Object} [scope] The execution scope (`this`) of the callback function
     */
        each: function(object, fn, scope) {
            var enumerables = Ext.enumerables,
                i, property;
            if (object) {
                scope = scope || object;
                for (property in object) {
                    if (object.hasOwnProperty(property)) {
                        if (fn.call(scope, property, object[property], object) === false) {
                            return;
                        }
                    }
                }
                if (enumerables) {
                    for (i = enumerables.length; i--; ) {
                        if (object.hasOwnProperty(property = enumerables[i])) {
                            if (fn.call(scope, property, object[property], object) === false) {
                                return;
                            }
                        }
                    }
                }
            }
        },
        /**
     * Iterates through an object and invokes the given callback function for each iteration.
     * The iteration can be stopped by returning `false` in the callback function. For example:
     *
     *     var items = {
     *         1: 'Hello',
     *         2: 'World'
     *     };
     *
     *     Ext.Object.eachValue(items, function(value) {
     *         console.log("Value: " + value);
     *     });
     *
     * This will log 'Hello' and 'World' in no particular order. This method is useful
     * in cases where the keys are not important to the processing, just the values.
     *
     * @param {Object} object The object to iterate
     * @param {Function} fn The callback function.
     * @param {Object} fn.value The value of
     * @param {Object} [scope] The execution scope (`this`) of the callback function
     */
        eachValue: function(object, fn, scope) {
            var enumerables = Ext.enumerables,
                i, property;
            scope = scope || object;
            for (property in object) {
                if (object.hasOwnProperty(property)) {
                    if (fn.call(scope, object[property]) === false) {
                        return;
                    }
                }
            }
            if (enumerables) {
                for (i = enumerables.length; i--; ) {
                    if (object.hasOwnProperty(property = enumerables[i])) {
                        if (fn.call(scope, object[property]) === false) {
                            return;
                        }
                    }
                }
            }
        },
        /**
     * Merges any number of objects recursively without referencing them or their children.
     * Note: It will reference arrays if they are only present in one of the objects being merged.
     *
     *     var extjs = {
     *         companyName: 'Ext JS',
     *         products: ['Ext JS', 'Ext GWT', 'Ext Designer'],
     *         isSuperCool: true,
     *         office: {
     *             size: 2000,
     *             location: 'Palo Alto',
     *             isFun: true
     *         }
     *     };
     *
     *     var newStuff = {
     *         companyName: 'Sencha Inc.',
     *         products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
     *         office: {
     *             size: 40000,
     *             location: 'Redwood City'
     *         }
     *     };
     *
     *     var sencha = Ext.Object.merge(extjs, newStuff);
     *
     *     // extjs and sencha then equals to
     *     {
     *         companyName: 'Sencha Inc.',
     *         products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
     *         isSuperCool: true,
     *         office: {
     *             size: 40000,
     *             location: 'Redwood City',
     *             isFun: true
     *         }
     *     }
     *
     * @param {Object} destination The object into which all subsequent objects are merged.
     * @param {Object...} object Any number of objects to merge into the destination.
     * @return {Object} merged The destination object with all passed objects merged in.
     */
        merge: function(destination) {
            var i = 1,
                args = arguments,
                ln = args.length,
                mergeFn = ExtObject.merge,
                cloneFn = Ext.clone,
                object, key, value, sourceKey;
            for (; i < ln; i++) {
                object = args[i];
                for (key in object) {
                    value = object[key];
                    if (value && value.constructor === Object) {
                        sourceKey = destination[key];
                        if (sourceKey && sourceKey.constructor === Object) {
                            mergeFn(sourceKey, value);
                        } else {
                            destination[key] = cloneFn(value);
                        }
                    } else {
                        destination[key] = value;
                    }
                }
            }
            return destination;
        },
        /**
     * @private
     * @param destination
     */
        mergeIf: function(destination) {
            var i = 1,
                ln = arguments.length,
                cloneFn = Ext.clone,
                object, key, value;
            for (; i < ln; i++) {
                object = arguments[i];
                for (key in object) {
                    if (!(key in destination)) {
                        value = object[key];
                        if (value && value.constructor === Object) {
                            destination[key] = cloneFn(value);
                        } else {
                            destination[key] = value;
                        }
                    }
                }
            }
            return destination;
        },
        /**
     * Returns all keys of the given object as an array.
     *
     * @param {Object} object
     * @return {String[]} An array of keys from the object or any of its prototypes.
     * @method
     */
        getAllKeys: function(object) {
            var keys = [],
                property;
            for (property in object) {
                keys.push(property);
            }
            return keys;
        },
        /**
     * Returns the first matching key corresponding to the given value.
     * If no matching value is found, null is returned.
     *
     *     var person = {
     *         name: 'Jacky',
     *         loves: 'food'
     *     };
     *
     *     alert(Ext.Object.getKey(person, 'food')); // alerts 'loves'
     *
     * @param {Object} object
     * @param {Object} value The value to find
     */
        getKey: function(object, value) {
            var property;
            for (property in object) {
                if (object.hasOwnProperty(property) && object[property] === value) {
                    return property;
                }
            }
            return null;
        },
        /**
     * Gets all values of the given object as an array.
     *
     *     var values = Ext.Object.getValues({
     *         name: 'Jacky',
     *         loves: 'food'
     *     }); // ['Jacky', 'food']
     *
     * @param {Object} object
     * @return {Array} An array of values from the object
     */
        getValues: function(object) {
            var values = [],
                property;
            for (property in object) {
                if (object.hasOwnProperty(property)) {
                    values.push(object[property]);
                }
            }
            return values;
        },
        /**
     * Returns the `hasOwnProperty` keys of the given object as an array.
     *
     *     var values = Ext.Object.getKeys({
     *         name: 'Jacky',
     *         loves: 'food'
     *     }); // ['name', 'loves']
     *
     * @param {Object} object
     * @return {String[]} An array of keys from the object
     * @method
     */
        getKeys: (typeof Object.keys === 'function') ? function(object) {
            if (!object) {
                return [];
            }
            return Object.keys(object);
        } : function(object) {
            var keys = [],
                property;
            for (property in object) {
                if (object.hasOwnProperty(property)) {
                    keys.push(property);
                }
            }
            return keys;
        },
        /**
     * Gets the total number of this object's own properties
     *
     *     var size = Ext.Object.getSize({
     *         name: 'Jacky',
     *         loves: 'food'
     *     }); // size equals 2
     *
     * @param {Object} object
     * @return {Number} size
     */
        getSize: function(object) {
            var size = 0,
                property;
            for (property in object) {
                if (object.hasOwnProperty(property)) {
                    size++;
                }
            }
            return size;
        },
        /**
     * Checks if there are any properties on this object.
     * @param {Object} object
     * @return {Boolean} `true` if there no properties on the object.
     */
        isEmpty: function(object) {
            var key;
            for (key in object) {
                if (object.hasOwnProperty(key)) {
                    return false;
                }
            }
            return true;
        },
        /**
     * @method
     * Shallow compares the contents of 2 objects using strict equality. Objects are
     * considered equal if they both have the same set of properties and the
     * value for those properties equals the other in the corresponding object.
     * 
     *     // Returns true
     *     Ext.Object.equals({
     *         foo: 1,
     *         bar: 2
     *     }, {
     *         foo: 1,
     *         bar: 2
     *     });
     * 
     * @param {Object} object1
     * @param {Object} object2
     * @return {Boolean} `true` if the objects are equal.
     */
        equals: (function() {
            var check = function(o1, o2) {
                    var key;
                    for (key in o1) {
                        if (o1.hasOwnProperty(key)) {
                            if (o1[key] !== o2[key]) {
                                return false;
                            }
                        }
                    }
                    return true;
                };
            return function(object1, object2) {
                // Short circuit if the same object is passed twice
                if (object1 === object2) {
                    return true;
                }
                if (object1 && object2) {
                    // Do the second check because we could have extra keys in
                    // object2 that don't exist in object1.
                    return check(object1, object2) && check(object2, object1);
                } else if (!object1 && !object2) {
                    return object1 === object2;
                } else {
                    return false;
                }
            };
        })(),
        /**
     * @private
     */
        fork: function(obj) {
            var ret, key, value;
            if (obj && obj.constructor === Object) {
                ret = ExtObject.chain(obj);
                for (key in obj) {
                    value = obj[key];
                    if (value) {
                        if (value.constructor === Object) {
                            ret[key] = ExtObject.fork(value);
                        } else if (value instanceof Array) {
                            ret[key] = Ext.Array.clone(value);
                        }
                    }
                }
            } else {
                ret = obj;
            }
            return ret;
        },
        defineProperty: ('defineProperty' in Object) ? Object.defineProperty : function(object, name, descriptor) {
            if (!Object.prototype.__defineGetter__) {
                return;
            }
            if (descriptor.get) {
                object.__defineGetter__(name, descriptor.get);
            }
            if (descriptor.set) {
                object.__defineSetter__(name, descriptor.set);
            }
        },
        /**
     * @private
     */
        classify: function(object) {
            var prototype = object,
                objectProperties = [],
                propertyClassesMap = {},
                objectClass, key, value;
            objectClass = function() {
                var property, i, ln;
                for (i = 0 , ln = objectProperties.length; i < ln; i++) {
                    property = objectProperties[i];
                    this[property] = new propertyClassesMap[property]();
                }
            };
            for (key in object) {
                if (object.hasOwnProperty(key)) {
                    value = object[key];
                    if (value && value.constructor === Object) {
                        objectProperties.push(key);
                        propertyClassesMap[key] = ExtObject.classify(value);
                    }
                }
            }
            objectClass.prototype = prototype;
            return objectClass;
        }
    };
    /**
 * A convenient alias method for {@link Ext.Object#merge}.
 *
 * @member Ext
 * @method merge
 * @inheritdoc Ext.Object#merge
 */
    Ext.merge = Ext.Object.merge;
    /**
 * @private
 * @member Ext
 */
    Ext.mergeIf = Ext.Object.mergeIf;
}());

/*
 * This file contains miscellaneous utility methods that depends on various helper classes
 * like `Ext.Array` and `Ext.Date`. Historically these methods were defined in Ext.js or
 * Ext-more.js but that creates circular dependencies so they were consolidated here.
 */
Ext.apply(Ext, {
    // @define Ext.Util
    // @require Ext
    // @require Ext.lang.*
    // shortcut for the special named scopes for listener scope resolution
    _namedScopes: {
        'this': {
            isThis: 1
        },
        controller: {
            isController: 1
        },
        owner: {
            isOwner: 1
        },
        // Uses helper Ext.lookUpFn to find the scope where fn is defined (skips the
        // component at which things originate):
        up: {
            isUp: 1
        },
        // These two are private, used to indicate that listeners were declared on the
        // class body with either an unspecified scope, or scope:'controller'
        self: {
            isSelf: 1
        },
        'self.controller': {
            isSelf: 1,
            isController: 1
        }
    },
    scrollbar: {
        _size: null,
        /**
         * @member Ext.scrollbar
         * Returns the size of the browser scrollbars. This can differ depending on
         * operating system settings, such as the theme or font size.
         * @param {Boolean} [force] Pass `true` to force a recalculation of scrollbar size.
         * @return {Object} An object containing scrollbar sizes.
         * @return {Number} return.width The width of the vertical scrollbar.
         * @return {Number} return.height The height of the horizontal scrollbar.
         */
        size: function(force) {
            var scrollbar = Ext.scrollbar,
                size = scrollbar._size;
            if (!Ext.isDomReady) {
                Ext.raise("Ext.scrollbar.size() called before DomReady");
            }
            if (force || !size) {
                /* eslint-disable-next-line vars-on-top */
                var db = document.body,
                    div = document.createElement('div'),
                    h, w;
                div.style.width = div.style.height = '100px';
                div.style.overflow = 'scroll';
                div.style.position = 'absolute';
                db.appendChild(div);
                // now we can measure the div...
                // at least in iE9 the div is not 100px - the scrollbar size is removed!
                scrollbar._size = size = {
                    width: w = div.offsetWidth - div.clientWidth,
                    height: h = div.offsetHeight - div.clientHeight
                };
                size.reservedWidth = w ? 'calc(100% - ' + w + 'px)' : '';
                size.reservedHeight = h ? 'calc(100% - ' + h + 'px)' : '';
                db.removeChild(div);
            }
            return size;
        },
        height: function(force) {
            return Ext.scrollbar.size(force).height;
        },
        width: function(force) {
            return Ext.scrollbar.size(force).width;
        }
    },
    escapeId: (function() {
        /* eslint-disable-next-line no-useless-escape */
        var validIdRe = /^[a-zA-Z_][a-zA-Z0-9_\-]*$/i,
            escapeRx = /([\W]{1})/g,
            leadingNumRx = /^(\d)/g,
            escapeFn = function(match, capture) {
                return "\\" + capture;
            },
            numEscapeFn = function(match, capture) {
                return '\\00' + capture.charCodeAt(0).toString(16) + ' ';
            };
        return function(id) {
            return validIdRe.test(id) ? id : // replace the number portion last to keep the trailing ' '
            // from being escaped
            id.replace(escapeRx, escapeFn).replace(leadingNumRx, numEscapeFn);
        };
    }()),
    lookUpFn: function(from, fn) {
        if (!from || !Ext.isFunction(from.up)) {
            Ext.raise('Callback "up" syntax requires a caller with "up" method');
        }
        // eslint-disable-next-line vars-on-top
        var controller, scope;
        for (scope = from.up(); scope && !scope[fn]; scope = scope.up()) {
            controller = scope.controller;
            if (controller && controller[fn]) {
                scope = controller;
                break;
            }
        }
        if (!scope || !Ext.isFunction(scope[fn])) {
            Ext.raise('No such method "' + fn + '" found up() from ' + (from.getId ? from.getId() : from.id));
        }
        return scope;
    },
    /**
     * @method callback
     * @member Ext
     * Execute a callback function in a particular scope. If `callback` argument is a
     * function reference, that is called. If it is a string, the string is assumed to
     * be the name of a method on the given `scope`. If no function is passed the call
     * is ignored.
     *
     * For example, these calls are equivalent:
     *
     *      var myFunc = this.myFunc;
     *
     *      Ext.callback('myFunc', this, [arg1, arg2]);
     *      Ext.callback(myFunc, this, [arg1, arg2]);
     *
     *      Ext.isFunction(myFunc) && this.myFunc(arg1, arg2);
     *
     * @param {Function/String} callback The callback function to execute or the name of
     * the callback method on the provided `scope`.
     * @param {Object} [scope] The scope in which `callback` should be invoked. If `callback`
     * is a string this object provides the method by that name. If this is `null` then
     * the `caller` is used to resolve the scope to a `ViewController` or the proper
     * `defaultListenerScope`.
     * @param {Array} [args] The arguments to pass to the function.
     * @param {Number} [delay] Pass a number to delay the call by a number of milliseconds.
     * @param {Object} [caller] The object calling the callback. This is used to resolve
     * named methods when no explicit `scope` is provided.
     * @param {Object} [defaultScope=caller] The default scope to return if none is found.
     * @return The value returned by the callback or `undefined` (if there is a `delay`
     * or if the `callback` is not a function).
     */
    callback: function(callback, scope, args, delay, caller, defaultScope) {
        if (!callback) {
            return;
        }
        /* eslint-disable-next-line vars-on-top */
        var namedScope = (scope in Ext._namedScopes),
            ret;
        if (callback.charAt) {
            // if (isString(fn))
            // Custom components cannot often use declarative method resolution when
            // they need to allow the user to supply declarative method names that can
            // reach the user's controller. The "up" callback syntax can help with that:
            //
            //      xtype: 'button',
            //      handler: 'up.onFoo',
            //
            // When Ext.callback('up.onFoo',..., button) is called, we can perform a
            // "button.up('[onFoo]')" search for the handler. Thus we have a declarative
            // way to dispatch such handlers that will work even if the user can supply
            // such handlers.
            //
            if (callback[2] === '.') {
                // callback = 'up.foo'
                if (callback.substr(0, 2) !== 'up') {
                    Ext.raise('Invalid callback method name "' + callback + '"');
                }
                if (scope) {
                    Ext.raise('Callback "up" syntax is incompatible with scopes');
                }
                scope = Ext.lookUpFn(caller, callback = callback.substr(3));
            } else if (caller) {
                if (namedScope && namedScope.isUp) {
                    scope = Ext.lookUpFn(caller, callback);
                } else if (!scope || namedScope) {
                    scope = caller.resolveListenerScope(namedScope ? scope : defaultScope);
                }
            }
            if (!scope || !Ext.isObject(scope)) {
                Ext.raise('Named method "' + callback + '" requires a scope object');
            }
            if (!Ext.isFunction(scope[callback])) {
                Ext.raise('No method named "' + callback + '" on ' + (scope.$className || 'scope object'));
            }
            callback = scope[callback];
        } else if (namedScope) {
            scope = defaultScope || caller;
        } else if (!scope) {
            scope = caller;
        }
        if (callback && Ext.isFunction(callback)) {
            scope = scope || Ext.global;
            if (delay) {
                Ext.defer(callback, delay, scope, args);
            } else {
                ret = args ? callback.apply(scope, args) : callback.call(scope);
            }
        }
        return ret;
    },
    /**
     * @method coerce
     * @member Ext
     * Coerces the first value if possible so that it is comparable to the second value.
     *
     * Coercion only works between the basic atomic data types String, Boolean, Number, Date, null
     * and undefined. Numbers and numeric strings are coerced to Dates using the value
     * as the millisecond era value.
     *
     * Strings are coerced to Dates by parsing using the
     * {@link Ext.Date#defaultFormat defaultFormat}.
     *
     * For example
     *
     *     Ext.coerce('false', true);
     *
     * returns the boolean value `false` because the second parameter is of type `Boolean`.
     *
     * @param {Mixed} from The value to coerce
     * @param {Mixed} to The value it must be compared against
     * @return The coerced value.
     */
    coerce: function(from, to) {
        var fromType = Ext.typeOf(from),
            toType = Ext.typeOf(to),
            isString = typeof from === 'string';
        if (fromType !== toType) {
            switch (toType) {
                case 'string':
                    return String(from);
                case 'number':
                    return Number(from);
                case 'boolean':
                    // See http://ecma262-5.com/ELS5_HTML.htm#Section_11.9.3 as to why '0'.
                    // TL;DR => ('0' == 0), so if given string '0', we must return boolean false.
                    return isString && (!from || from === 'false' || from === '0') ? false : Boolean(from);
                case 'null':
                    return isString && (!from || from === 'null') ? null : false;
                case 'undefined':
                    return isString && (!from || from === 'undefined') ? undefined : false;
                case 'date':
                    return isString && isNaN(from) ? Ext.Date.parse(from, Ext.Date.defaultFormat) : Date(Number(from));
            }
        }
        return from;
    },
    /**
     * @method copyTo
     * @member Ext
     * Copies a set of named properties fom the source object to the destination object.
     *
     * Example:
     *
     *     var foo = { a: 1, b: 2, c: 3 };
     *
     *     var bar = Ext.copyTo({}, foo, 'a,c');
     *     // bar = { a: 1, c: 3 };
     *
     * Important note: To borrow class prototype methods, use {@link Ext.Base#borrow} instead.
     *
     * @param {Object} dest The destination object.
     * @param {Object} source The source object.
     * @param {String/String[]} names Either an Array of property names, or a comma-delimited list
     * of property names to copy.
     * @param {Boolean} [usePrototypeKeys=false] Pass `true` to copy keys off of the
     * prototype as well as the instance.
     * @return {Object} The `dest` object.
     * @deprecated 6.0.1 Use {@link Ext#copy Ext.copy} instead. This old method
     * would copy the named preoperties even if they did not exist in the source which
     * could produce `undefined` values in the destination.
     */
    copyTo: function(dest, source, names, usePrototypeKeys) {
        var name, i, n;
        if (typeof names === 'string') {
            names = names.split(Ext.propertyNameSplitRe);
        }
        for (i = 0 , n = names ? names.length : 0; i < n; i++) {
            name = names[i];
            if (usePrototypeKeys || source.hasOwnProperty(name)) {
                dest[name] = source[name];
            }
        }
        return dest;
    },
    /**
     * @method copy
     * @member Ext
     * Copies a set of named properties fom the source object to the destination object.
     *
     * Example:
     *
     *     var foo = { a: 1, b: 2, c: 3 };
     *
     *     var bar = Ext.copy({}, foo, 'a,c');
     *     // bar = { a: 1, c: 3 };
     *
     * Important note: To borrow class prototype methods, use {@link Ext.Base#borrow} instead.
     *
     * @param {Object} dest The destination object.
     * @param {Object} source The source object.
     * @param {String/String[]} names Either an Array of property names, or a comma-delimited list
     * of property names to copy.
     * @param {Boolean} [usePrototypeKeys=false] Pass `true` to copy keys off of the
     * prototype as well as the instance.
     * @return {Object} The `dest` object.
     */
    copy: function(dest, source, names, usePrototypeKeys) {
        var name, i, n;
        if (typeof names === 'string') {
            names = names.split(Ext.propertyNameSplitRe);
        }
        for (i = 0 , n = names ? names.length : 0; i < n; i++) {
            name = names[i];
            // Only copy a property if the source actually *has* that property.
            // If we are including prototype properties, then ensure that a property of
            // that name can be found *somewhere* in the prototype chain (otherwise we'd be
            // copying undefined in which may break things)
            if (source.hasOwnProperty(name) || (usePrototypeKeys && name in source)) {
                dest[name] = source[name];
            }
        }
        return dest;
    },
    propertyNameSplitRe: /[,;\s]+/,
    /**
     * @method copyToIf
     * @member Ext
     * Copies a set of named properties fom the source object to the destination object
     * if the destination object does not already have them.
     *
     * Example:
     *
     *     var foo = { a: 1, b: 2, c: 3 };
     *
     *     var bar = Ext.copyToIf({ a:42 }, foo, 'a,c');
     *     // bar = { a: 42, c: 3 };
     *
     * @param {Object} destination The destination object.
     * @param {Object} source The source object.
     * @param {String/String[]} names Either an Array of property names, or a single string
     * with a list of property names separated by ",", ";" or spaces.
     * @return {Object} The `dest` object.
     * @deprecated 6.0.1 Use {@link Ext#copyIf Ext.copyIf} instead. This old method
     * would copy the named preoperties even if they did not exist in the source which
     * could produce `undefined` values in the destination.
     */
    copyToIf: function(destination, source, names) {
        var name, i, n;
        if (typeof names === 'string') {
            names = names.split(Ext.propertyNameSplitRe);
        }
        for (i = 0 , n = names ? names.length : 0; i < n; i++) {
            name = names[i];
            if (destination[name] === undefined) {
                destination[name] = source[name];
            }
        }
        return destination;
    },
    /**
     * @method copyIf
     * @member Ext
     * Copies a set of named properties fom the source object to the destination object
     * if the destination object does not already have them.
     *
     * Example:
     *
     *     var foo = { a: 1, b: 2, c: 3 };
     *
     *     var bar = Ext.copyIf({ a:42 }, foo, 'a,c');
     *     // bar = { a: 42, c: 3 };
     *
     * @param {Object} destination The destination object.
     * @param {Object} source The source object.
     * @param {String/String[]} names Either an Array of property names, or a single string
     * with a list of property names separated by ",", ";" or spaces.
     * @return {Object} The `dest` object.
     */
    copyIf: function(destination, source, names) {
        var name, i, n;
        if (typeof names === 'string') {
            names = names.split(Ext.propertyNameSplitRe);
        }
        for (i = 0 , n = names ? names.length : 0; i < n; i++) {
            name = names[i];
            // Only copy a property if the destination has no property by that name
            if (!(name in destination) && (name in source)) {
                destination[name] = source[name];
            }
        }
        return destination;
    },
    /**
     * @method extend
     * @member Ext
     * This method deprecated. Use {@link Ext#define Ext.define} instead.
     * @param {Function} superclass
     * @param {Object} overrides
     * @return {Function} The subclass constructor from the <tt>overrides</tt> parameter,
     * or a generated one if not provided.
     * @deprecated 4.0.0 Use {@link Ext#define Ext.define} instead
     */
    extend: (function() {
        // inline overrides
        var objectConstructor = Object.prototype.constructor,
            inlineOverrides = function(o) {
                var m;
                for (m in o) {
                    if (!o.hasOwnProperty(m)) {
                        
                        continue;
                    }
                    this[m] = o[m];
                }
            };
        return function(subclass, superclass, overrides) {
            // First we check if the user passed in just the superClass with overrides
            if (Ext.isObject(superclass)) {
                overrides = superclass;
                superclass = subclass;
                subclass = overrides.constructor !== objectConstructor ? overrides.constructor : function() {
                    superclass.apply(this, arguments);
                };
            }
            if (!superclass) {
                Ext.raise({
                    sourceClass: 'Ext',
                    sourceMethod: 'extend',
                    msg: 'Attempting to extend from a class which has not been loaded on the page.'
                });
            }
            // We create a new temporary class
            /* eslint-disable-next-line vars-on-top */
            var F = function() {},
                superclassProto = superclass.prototype,
                subclassProto;
            F.prototype = superclassProto;
            subclassProto = subclass.prototype = new F();
            subclassProto.constructor = subclass;
            subclass.superclass = superclassProto;
            if (superclassProto.constructor === objectConstructor) {
                superclassProto.constructor = superclass;
            }
            subclass.override = function(overrides) {
                Ext.override(subclass, overrides);
            };
            subclassProto.override = inlineOverrides;
            subclassProto.proto = subclassProto;
            subclass.override(overrides);
            subclass.extend = function(o) {
                return Ext.extend(subclass, o);
            };
            return subclass;
        };
    }()),
    /**
     * Indicates if the page is currently running in online or offline mode, according
     * to the `navigator.onLine` property.
     * @return {Boolean} `true` if the page is currently running in an online mode.
     *
     * @since 6.2.1
     */
    isOnline: function() {
        return Ext.global.navigator.onLine;
    },
    /**
     * @method iterate
     * @member Ext
     * Iterates either an array or an object. This method delegates to
     * {@link Ext.Array#each Ext.Array.each} if the given value is iterable, and
     * {@link Ext.Object#each Ext.Object.each} otherwise.
     *
     * @param {Object/Array} object The object or array to be iterated.
     * @param {Function} fn The function to be called for each iteration. See and
     * {@link Ext.Array#each Ext.Array.each} and {@link Ext.Object#each Ext.Object.each}
     * for detailed lists of arguments passed to this function depending on the given object
     * type that is being iterated.
     * @param {Object} [scope] The scope (`this` reference) in which the specified function
     * is executed. Defaults to the object being iterated itself.
     */
    iterate: function(object, fn, scope) {
        if (Ext.isEmpty(object)) {
            return;
        }
        if (scope === undefined) {
            scope = object;
        }
        if (Ext.isIterable(object)) {
            Ext.Array.each.call(Ext.Array, object, fn, scope);
        } else {
            Ext.Object.each.call(Ext.Object, object, fn, scope);
        }
    },
    _resourcePoolRe: /^[<]([^<>@:]*)(?:[@]([^<>@:]+))?[>](.+)$/,
    /**
     * Resolves a resource URL that may contain a resource pool identifier token at the
     * front. The tokens are formatted as HTML tags "&lt;poolName@packageName&gt;" followed
     * by a normal relative path. This token is only processed if present at the first
     * character of the given string.
     *
     * These tokens are parsed and the pieces are then passed to the
     * {@link Ext#getResourcePath} method.
     *
     * For example:
     *
     *      [{
     *          xtype: 'image',
     *          src: '<shared>images/foo.png'
     *      },{
     *          xtype: 'image',
     *          src: '<@package>images/foo.png'
     *      },{
     *          xtype: 'image',
     *          src: '<shared@package>images/foo.png'
     *      }]
     *
     * In the above example, "shared" is the name of a Sencha Cmd resource pool and
     * "package" is the name of a Sencha Cmd package.
     * @member Ext
     * @param {String} url The URL that may contain a resource pool token at the front.
     * @return {String}
     * @since 6.0.1
     */
    resolveResource: function(url) {
        var ret = url,
            m;
        if (url && url.charAt(0) === '<') {
            m = Ext._resourcePoolRe.exec(url);
            if (m) {
                ret = Ext.getResourcePath(m[3], m[1], m[2]);
            }
        }
        return ret;
    },
    /**
     *
     * @member Ext
     * @method urlEncode
     * @inheritdoc Ext.Object#toQueryString
     * @deprecated 4.0.0 Use {@link Ext.Object#toQueryString} instead
     */
    urlEncode: function() {
        var args = Ext.Array.from(arguments),
            prefix = '';
        // Support for the old `pre` argument
        if (Ext.isString(args[1])) {
            prefix = args[1] + '&';
            args[1] = false;
        }
        return prefix + Ext.Object.toQueryString.apply(Ext.Object, args);
    },
    /**
     * Alias for {@link Ext.Object#fromQueryString}.
     *
     * @member Ext
     * @method urlDecode
     * @inheritdoc Ext.Object#fromQueryString
     * @deprecated 4.0.0 Use {@link Ext.Object#fromQueryString} instead
     */
    urlDecode: function() {
        return Ext.Object.fromQueryString.apply(Ext.Object, arguments);
    },
    /**
     * @method getScrollbarSize
     * @member Ext
     * Returns the size of the browser scrollbars. This can differ depending on
     * operating system settings, such as the theme or font size.
     * @param {Boolean} [force] true to force a recalculation of the value.
     * @return {Object} An object containing scrollbar sizes.
     * @return {Number} return.width The width of the vertical scrollbar.
     * @return {Number} return.height The height of the horizontal scrollbar.
     * @deprecated 7.0 Use `Ext.scrollbar.size` instead.
     */
    getScrollbarSize: function(force) {
        return Ext.scrollbar.size(force);
    },
    /**
     * @method typeOf
     * @member Ext
     * Returns the type of the given variable in string format. List of possible values are:
     *
     * - `undefined`: If the given value is `undefined`
     * - `null`: If the given value is `null`
     * - `string`: If the given value is a string
     * - `number`: If the given value is a number
     * - `boolean`: If the given value is a boolean value
     * - `date`: If the given value is a `Date` object
     * - `function`: If the given value is a function reference
     * - `object`: If the given value is an object
     * - `array`: If the given value is an array
     * - `regexp`: If the given value is a regular expression
     * - `element`: If the given value is a DOM Element
     * - `textnode`: If the given value is a DOM text node and contains something other than
     * whitespace
     * - `whitespace`: If the given value is a DOM text node and contains only whitespace
     *
     * @param {Object} value
     * @return {String}
     */
    typeOf: (function() {
        var nonWhitespaceRe = /\S/,
            toString = Object.prototype.toString,
            typeofTypes = {
                number: 1,
                string: 1,
                'boolean': 1,
                'undefined': 1
            },
            toStringTypes = {
                '[object Array]': 'array',
                '[object Date]': 'date',
                '[object Boolean]': 'boolean',
                '[object Number]': 'number',
                '[object RegExp]': 'regexp'
            };
        return function(value) {
            if (value === null) {
                return 'null';
            }
            /* eslint-disable-next-line vars-on-top */
            var type = typeof value,
                ret, typeToString;
            if (typeofTypes[type]) {
                return type;
            }
            ret = toStringTypes[typeToString = toString.call(value)];
            if (ret) {
                return ret;
            }
            if (type === 'function') {
                return 'function';
            }
            if (type === 'object') {
                if (value.nodeType !== undefined) {
                    if (value.nodeType === 3) {
                        return nonWhitespaceRe.test(value.nodeValue) ? 'textnode' : 'whitespace';
                    } else {
                        return 'element';
                    }
                }
                return 'object';
            }
            Ext.raise({
                sourceClass: 'Ext',
                sourceMethod: 'typeOf',
                msg: 'Failed to determine the type of "' + value + '".'
            });
            return typeToString;
        };
    }()),
    /**
     * A global factory method to instantiate a class from a config object. For example,
     * these two calls are equivalent:
     *
     *     Ext.factory({ text: 'My Button' }, 'Ext.Button');
     *     Ext.create('Ext.Button', { text: 'My Button' });
     *
     * If an existing instance is also specified, it will be updated with the supplied config
     * object. This is useful if you need to either create or update an object, depending on
     * if an instance already exists. For example:
     *
     *     var button;
     *     button = Ext.factory({ text: 'New Button' }, 'Ext.Button', button);     // Button created
     *     button = Ext.factory({ text: 'Updated Button' }, 'Ext.Button', button); // Button updated
     *
     * @param {Object} config  The config object to instantiate or update an instance with.
     * @param {String} [classReference]  The class to instantiate from (if there is a default).
     * @param {Object} [instance]  The instance to update.
     * @param [aliasNamespace]
     * @member Ext
     * @deprecated 6.5.0 Use the {@link Ext.Factory#method!update update} method of the
     * associated factory instead.
     */
    factory: function(config, classReference, instance, aliasNamespace) {
        var manager = Ext.ClassManager,
            newInstance;
        // If config is falsy or a valid instance, destroy the current instance
        // (if it exists) and replace with the new one
        if (!config || config.isInstance) {
            if (instance && instance !== config) {
                instance.destroy();
            }
            return config;
        }
        if (aliasNamespace) {
            // If config is a string value, treat it as an alias
            if (typeof config === 'string') {
                return manager.instantiateByAlias(aliasNamespace + '.' + config);
            }
            // Same if 'type' is given in config
            else if (Ext.isObject(config) && 'type' in config) {
                return manager.instantiateByAlias(aliasNamespace + '.' + config.type, config);
            }
        }
        if (config === true) {
            if (!instance && !classReference) {
                Ext.raise('[Ext.factory] Cannot determine type of class to create');
            }
            return instance || Ext.create(classReference);
        }
        if (!Ext.isObject(config)) {
            Ext.raise("Invalid config, must be a valid config object");
        }
        if ('xtype' in config) {
            newInstance = manager.instantiateByAlias('widget.' + config.xtype, config);
        } else if ('xclass' in config) {
            newInstance = Ext.create(config.xclass, config);
        }
        if (newInstance) {
            if (instance) {
                instance.destroy();
            }
            return newInstance;
        }
        if (instance) {
            return instance.setConfig(config);
        }
        return Ext.create(classReference, config);
    },
    /**
     * This method converts an object containing config objects keyed by `itemId` into
     * an array of config objects.
     *
     * @param {Object} items An object containing config objects keyed by `itemId`.
     * @param {String} [defaultProperty="xtype"] The property to set for string items.
     * @param functionProperty
     * @return {Object[]}
     * @member Ext
     * @since 6.5.0
     * @private
     */
    convertKeyedItems: function(items, defaultProperty, functionProperty) {
        if (items && !items.isInstance && Ext.isObject(items)) {
            /* eslint-disable-next-line vars-on-top */
            var obj = items,
                item, itemId, value;
            items = [];
            // See if obj looks like a single thing
            if (obj.xtype || obj.xclass || obj.itemId || obj.id) {
                items.push(obj);
            } else {
                for (itemId in obj) {
                    item = obj[itemId];
                    if (item) {
                        if (item === true) {
                            item = {};
                        } else if (typeof item === 'function') {
                            if (!functionProperty) {
                                Ext.raise('Function not expected here');
                            }
                            value = item;
                            item = {};
                            item[functionProperty] = value;
                        } else if (typeof item === 'string') {
                            value = item;
                            item = {};
                            item[defaultProperty || 'xtype'] = value;
                        } else {
                            item = Ext.apply({}, item);
                        }
                        item.itemId = itemId;
                        items.push(item);
                    }
                }
            }
        }
        return items;
    },
    sortByWeight: function(items) {
        if (items) {
            Ext.Array.sort(items, Ext.weightSortFn);
        }
    },
    /**
     * Comparison function for sorting an array of objects in ascending order of `weight`.
     * @param {Object} lhs
     * @param {Object} rhs
     * @return {Number}
     * @member Ext
     * @private
     * @since 6.5.0
     */
    weightSortFn: function(lhs, rhs) {
        return (lhs.weight || 0) - (rhs.weight || 0);
    },
    /**
     * Concatenate 2 arrays. If either argument is `null` or `undefined` then it's not
     * concatenated.
     *
     * @param {Object/Object[]} a
     * @param {Object/Object[]} b
     * @return {Object[]}
     * @member Ext
     * @private
     * @since 6.5.1
     */
    concat: function(a, b) {
        var noB = b == null,
            E = Ext.emptyArray;
        return (a == null) ? (noB ? a : E.concat(b)) : (noB ? E.concat(a) : E.concat(a, b));
    },
    /**
     * @method log
     * @member Ext
     * Logs a message. If a console is present it will be used. On Opera, the method
     * "opera.postError" is called. In other cases, the message is logged to an array
     * "Ext.log.out". An attached debugger can watch this array and view the log. The
     * log buffer is limited to a maximum of "Ext.log.max" entries (defaults to 250).
     *
     * If additional parameters are passed, they are joined and appended to the message.
     * A technique for tracing entry and exit of a function is this:
     *
     *     function foo () {
     *         Ext.log({ indent: 1 }, '>> foo');
     *
     *         // log statements in here or methods called from here will be indented
     *         // by one step
     *
     *         Ext.log({ outdent: 1 }, '<< foo');
     *     }
     *
     * This method does nothing in a release build.
     *
     * @param {String/Object} [options] The message to log or an options object with any
     * of the following properties:
     *
     *  - `msg`: The message to log (required).
     *  - `level`: One of: "error", "warn", "info" or "log" (the default is "log").
     *  - `dump`: An object to dump to the log as part of the message.
     *  - `stack`: True to include a stack trace in the log.
     *  - `indent`: Cause subsequent log statements to be indented one step.
     *  - `outdent`: Cause this and following statements to be one step less indented.
     *
     * @param {String...} [message] The message to log (required unless specified in
     * options object).
     */
    log: (function() {
        /*
             * Iterate through an object to dump its content into a string.
             * For example:
             *     {
             *         style: {
             *             lineWidth: 1
             *         },
             *         label: {},
             *         marker: {
             *             strokeStyle: "#555",
             *             radius: 3,
             *             size: 3
             *         },
             *         subStyle: {
             *             fillStyle: [
             *                 0: "#133987",
             *                 1: "#1c55ca",
             *                 2: "#4d7fe6"
             *             ]
             *         },
             *         markerSubStyle: {}
             *     } 
             *
             * @param {Object} object The object to iterate
             * @param {Number} [level] Current level of identation (and recursion). Default is 0.
             * @param {Number} [maxLevel] Maximum level of recursion. Default is 3.
             * @param {Boolean} [withFunctions] Include functions in the output.
             * @return {String} The string with the contents of the object
             */
        var primitiveRe = /string|number|boolean/;
        function dumpObject(object, level, maxLevel, withFunctions) {
            var member, type, value, name, prefix, suffix,
                members = [];
            if (Ext.isArray(object)) {
                prefix = '[';
                suffix = ']';
            } else if (Ext.isObject(object)) {
                prefix = '{';
                suffix = '}';
            }
            if (!maxLevel) {
                maxLevel = 3;
            }
            if (level > maxLevel) {
                return prefix + '...' + suffix;
            }
            level = level || 1;
            /* eslint-disable-next-line vars-on-top */
            var spacer = (new Array(level)).join('    ');
            // Cannot use Ext.encode since it can recurse endlessly
            for (name in object) {
                if (object.hasOwnProperty(name)) {
                    value = object[name];
                    type = typeof value;
                    if (type === 'function') {
                        if (!withFunctions) {
                            
                            continue;
                        }
                        member = type;
                    } else if (type === 'undefined') {
                        member = type;
                    } else if (value === null || primitiveRe.test(type) || Ext.isDate(value)) {
                        member = Ext.encode(value);
                    } else if (Ext.isArray(value)) {
                        member = dumpObject(value, level + 1, maxLevel, withFunctions);
                    } else if (Ext.isObject(value)) {
                        member = dumpObject(value, level + 1, maxLevel, withFunctions);
                    } else {
                        member = type;
                    }
                    members.push(spacer + name + ': ' + member);
                }
            }
            // or Ext.encode(name)
            if (members.length) {
                return prefix + '\n    ' + members.join(',\n    ') + '\n' + spacer + suffix;
            }
            return prefix + suffix;
        }
        function log(message) {
            var options, dump,
                con = Ext.global.console,
                level = 'log',
                indent = log.indent || 0,
                prefix, stack, fn, out, max;
            log.indent = indent;
            if (typeof message !== 'string') {
                options = message;
                message = options.msg || '';
                level = options.level || level;
                dump = options.dump;
                stack = options.stack;
                prefix = options.prefix;
                fn = options.fn;
                if (options.indent) {
                    ++log.indent;
                } else if (options.outdent) {
                    log.indent = indent = Math.max(indent - 1, 0);
                }
                if (dump && !(con && con.dir)) {
                    message += dumpObject(dump);
                    dump = null;
                }
            }
            if (arguments.length > 1) {
                message += Array.prototype.slice.call(arguments, 1).join('');
            }
            if (prefix) {
                message = prefix + ' - ' + message;
            }
            message = indent ? Ext.String.repeat(' ', log.indentSize * indent) + message : message;
            // w/o console, all messages are equal, so munge the level into the message:
            if (level !== 'log') {
                message = '[' + level.charAt(0).toUpperCase() + '] ' + message;
            }
            if (fn) {
                message += '\nCaller: ' + fn.toString();
            }
            // Not obvious, but 'console' comes and goes when Firebug is turned on/off, so
            // an early test may fail either direction if Firebug is toggled.
            //
            if (con) {
                // if (Firebug-like console)
                if (con[level]) {
                    con[level](message);
                } else {
                    con.log(message);
                }
                if (dump) {
                    con.dir(dump);
                }
                if (stack && con.trace) {
                    // Firebug's console.error() includes a trace already...
                    if (!con.firebug || level !== 'error') {
                        con.trace();
                    }
                }
            } else if (Ext.isOpera) {
                /* eslint-disable-next-line no-undef */
                opera.postError(message);
            } else {
                out = log.out;
                max = log.max;
                if (out.length >= max) {
                    // this formula allows out.max to change (via debugger), where the
                    // more obvious "max/4" would not quite be the same
                    // keep newest 75%
                    Ext.Array.erase(out, 0, out.length - 3 * Math.floor(max / 4));
                }
                out.push(message);
            }
            // Mostly informational, but the Ext.Error notifier uses them:
            ++log.count;
            ++log.counters[level];
        }
        function logx(level, args) {
            if (typeof args[0] === 'string') {
                args.unshift({});
            }
            args[0].level = level;
            log.apply(this, args);
        }
        log.error = function() {
            logx('error', Array.prototype.slice.call(arguments));
        };
        log.info = function() {
            logx('info', Array.prototype.slice.call(arguments));
        };
        log.warn = function() {
            logx('warn', Array.prototype.slice.call(arguments));
        };
        log.count = 0;
        log.counters = {
            error: 0,
            warn: 0,
            info: 0,
            log: 0
        };
        log.indentSize = 2;
        log.out = [];
        log.max = 750;
        return log;
    }()) || (function() {
        var nullLog = function() {};
        nullLog.info = nullLog.warn = nullLog.error = Ext.emptyFn;
        return nullLog;
    }())
});

/**
 * @class Ext.Version
 *
 * A utility class that wraps around a version number string and provides convenient methods
 * to perform comparisons. A version number is expressed in the following general format:
 *
 *     major[.minor[.patch[.build[release]]]]
 * 
 * The `Version` instance holds various readonly properties that contain the digested form
 * of the version string. The numeric componnets of `major`, `minor`, `patch` and `build`
 * as well as the textual suffix called `release`.
 * 
 * Not depicted in the above syntax are three possible prefixes used to control partial
 * matching. These are '^' (the default), '>' and '~'. These are discussed below.
 *
 * Examples:
 *
 *     var version = new Ext.Version('1.0.2beta'); // or maybe "1.0" or "1.2.3.4RC"
 *     console.log("Version is " + version); // Version is 1.0.2beta
 *
 *     console.log(version.getMajor()); // 1
 *     console.log(version.getMinor()); // 0
 *     console.log(version.getPatch()); // 2
 *     console.log(version.getBuild()); // 0
 *     console.log(version.getRelease()); // beta
 *
 * The understood values of `release` are assigned numberic equivalents for the sake of
 * comparsion. The order of these from smallest to largest is as follows:
 *
 *  * `"dev"`
 *  * `"alpha"` or `"a"`
 *  * `"beta"` or `"b"`
 *  * `"RC"` or `"rc"`
 *  * `"#"`
 *  * `"pl"` or `"p"`
 *
 * Any other (unrecognized) suffix is consider greater than any of these.
 * 
 * ## Comparisons
 * There are two forms of comparison that are commonly needed: full and partial. Full
 * comparison is simpler and is also the default.
 * 
 * Example:
 *
 *     var version = new Ext.Version('1.0.2beta');
 *
 *     console.log(version.isGreaterThan('1.0.1')); // True
 *     console.log(version.isGreaterThan('1.0.2alpha')); // True
 *     console.log(version.isGreaterThan('1.0.2RC')); // False
 *     console.log(version.isGreaterThan('1.0.2')); // False
 *     console.log(version.isLessThan('1.0.2')); // True
 *
 *     console.log(version.match(1.0)); // True (using a Number)
 *     console.log(version.match('1.0.2')); // True (using a String)
 * 
 * These comparisons are ultimately implemented by {@link Ext.Version#compareTo compareTo}
 * which returns -1, 0 or 1 depending on whether the `Version' instance is less than, equal
 * to, or greater than the given "other" version.
 * 
 * For example:
 * 
 *      var n = version.compareTo('1.0.1');  // == 1  (because 1.0.2beta > 1.0.1)
 *      
 *      n = version.compareTo('1.1');  // == -1
 *      n = version.compareTo(version); // == 0
 * 
 * ### Partial Comparisons
 * By default, unspecified version number fields are filled with 0. In other words, the
 * version number fields are 0-padded on the right or a "lower bound". This produces the
 * most commonly used forms of comparsion:
 * 
 *      var ver = new Version('4.2');
 *
 *      n = ver.compareTo('4.2.1'); // == -1  (4.2 promotes to 4.2.0 and is less than 4.2.1)
 * 
 * There are two other ways to interpret comparisons of versions of different length. The
 * first of these is to change the padding on the right to be a large number (scuh as
 * Infinity) instead of 0. This has the effect of making the version an upper bound. For
 * example:
 * 
 *      var ver = new Version('^4.2'); // NOTE: the '^' prefix used
 *
 *      n = ver.compareTo('4.3'); // == -1  (less than 4.3)
 *      
 *      n = ver.compareTo('4.2'); // == 1   (greater than all 4.2's)
 *      n = ver.compareTo('4.2.1'); // == 1
 *      n = ver.compareTo('4.2.9'); // == 1
 * 
 * The second way to interpret this comparison is to ignore the extra digits, making the
 * match a prefix match. For example:
 * 
 *      var ver = new Version('~4.2'); // NOTE: the '~' prefix used
 *
 *      n = ver.compareTo('4.3'); // == -1
 *      
 *      n = ver.compareTo('4.2'); // == 0
 *      n = ver.compareTo('4.2.1'); // == 0
 * 
 * This final form can be useful when version numbers contain more components than are
 * important for certain comparisons. For example, the full version of Ext JS 4.2.1 is
 * "4.2.1.883" where 883 is the `build` number.
 * 
 * This is how to create a "partial" `Version` and compare versions to it:
 * 
 *      var version421ish = new Version('~4.2.1');
 *      
 *      n = version421ish.compareTo('4.2.1.883'); // == 0
 *      n = version421ish.compareTo('4.2.1.2'); // == 0
 *      n = version421ish.compareTo('4.2.1'); // == 0
 *
 *      n = version421ish.compareTo('4.2'); // == 1
 *
 * In the above example, '4.2.1.2' compares as equal to '4.2.1' because digits beyond the
 * given "4.2.1" are ignored. However, '4.2' is less than the '4.2.1' prefix; its missing
 * digit is filled with 0.
 */
/* eslint-disable indent */
(function() {
    // @define Ext.Version
    // @require Ext.String
    var // used by checkVersion to avoid temp arrays:
        checkVerTemp = [
            ''
        ],
        endOfVersionRe = /([^\d.])/,
        notDigitsRe = /[^\d]/g,
        plusMinusRe = /[-+]/g,
        stripRe = /\s/g,
        underscoreRe = /_/g,
        toolkitNames = {
            classic: 1,
            modern: 1
        },
        Version;
    Ext.Version = Version = function(version, defaultMode) {
        var me = this,
            padModes = me.padModes,
            ch, i, pad, parts, release, releaseStartIndex, ver;
        if (version.isVersion) {
            version = version.version;
        }
        me.version = ver = String(version).toLowerCase().replace(underscoreRe, '.').replace(plusMinusRe, '');
        ch = ver.charAt(0);
        if (ch in padModes) {
            ver = ver.substring(1);
            pad = padModes[ch];
        } else {
            pad = defaultMode ? padModes[defaultMode] : 0;
        }
        // careful - NaN is falsey!
        me.pad = pad;
        releaseStartIndex = ver.search(endOfVersionRe);
        me.shortVersion = ver;
        if (releaseStartIndex !== -1) {
            me.release = release = ver.substr(releaseStartIndex, version.length);
            me.shortVersion = ver.substr(0, releaseStartIndex);
            release = Version.releaseValueMap[release] || release;
        }
        me.releaseValue = release || pad;
        me.shortVersion = me.shortVersion.replace(notDigitsRe, '');
        /**
         * @property {Number[]} parts
         * The split array of version number components found in the version string.
         * For example, for "1.2.3", this would be `[1, 2, 3]`.
         * @readonly
         * @private
         */
        me.parts = parts = ver.split('.');
        for (i = parts.length; i--; ) {
            parts[i] = parseInt(parts[i], 10);
        }
        if (pad === Infinity) {
            // have to add this to the end to create an upper bound:
            parts.push(pad);
        }
        /**
         * @property {Number} major
         * The first numeric part of the version number string.
         * @readonly
         */
        me.major = parts[0] || pad;
        /**
         * @property {Number} [minor]
         * The second numeric part of the version number string.
         * @readonly
         */
        me.minor = parts[1] || pad;
        /**
         * @property {Number} [patch]
         * The third numeric part of the version number string.
         * @readonly
         */
        me.patch = parts[2] || pad;
        /**
         * @property {Number} [build]
         * The fourth numeric part of the version number string.
         * @readonly
         */
        me.build = parts[3] || pad;
        return me;
    };
    Version.prototype = {
        isVersion: true,
        padModes: {
            '~': NaN,
            '^': Infinity
        },
        /**
         * @property {String} [release=""]
         * The release level. The following values are understood:
         * 
         *  * `"dev"`
         *  * `"alpha"` or `"a"`
         *  * `"beta"` or `"b"`
         *  * `"RC"` or `"rc"`
         *  * `"#"`
         *  * `"pl"` or `"p"`
         * @readonly
         */
        release: '',
        /**
         * Compares this version instance to the specified `other` version.
         *
         * @param {String/Number/Ext.Version} other The other version to which to compare.
         * @return {Number} -1 if this version is less than the target version, 1 if this
         * version is greater, and 0 if they are equal.
         */
        compareTo: function(other) {
            // "lhs" == "left-hand-side"
            // "rhs" == "right-hand-side"
            var me = this,
                lhsPad = me.pad,
                lhsParts = me.parts,
                lhsLength = lhsParts.length,
                rhsVersion = other.isVersion ? other : new Version(other),
                rhsPad = rhsVersion.pad,
                rhsParts = rhsVersion.parts,
                rhsLength = rhsParts.length,
                length = Math.max(lhsLength, rhsLength),
                i, lhs, rhs;
            for (i = 0; i < length; i++) {
                lhs = (i < lhsLength) ? lhsParts[i] : lhsPad;
                rhs = (i < rhsLength) ? rhsParts[i] : rhsPad;
                // When one or both of the values are NaN these tests produce false
                // and we end up treating NaN as equal to anything.
                if (lhs < rhs) {
                    return -1;
                }
                if (lhs > rhs) {
                    return 1;
                }
            }
            // same comments about NaN apply here...
            lhs = me.releaseValue;
            rhs = rhsVersion.releaseValue;
            if (lhs < rhs) {
                return -1;
            }
            if (lhs > rhs) {
                return 1;
            }
            return 0;
        },
        /**
         * Override the native `toString` method
         * @private
         * @return {String} version
         */
        toString: function() {
            return this.version;
        },
        /**
         * Override the native `valueOf` method
         * @private
         * @return {String} version
         */
        valueOf: function() {
            return this.version;
        },
        /**
         * Returns the major component value.
         * @return {Number}
         */
        getMajor: function() {
            return this.major;
        },
        /**
         * Returns the minor component value.
         * @return {Number}
         */
        getMinor: function() {
            return this.minor;
        },
        /**
         * Returns the patch component value.
         * @return {Number}
         */
        getPatch: function() {
            return this.patch;
        },
        /**
         * Returns the build component value.
         * @return {Number}
         */
        getBuild: function() {
            return this.build;
        },
        /**
         * Returns the release component text (e.g., "beta").
         * @return {String}
         */
        getRelease: function() {
            return this.release;
        },
        /**
         * Returns the release component value for comparison purposes.
         * @return {Number/String}
         */
        getReleaseValue: function() {
            return this.releaseValue;
        },
        /**
         * Returns whether this version if greater than the supplied argument
         * @param {String/Number} target The version to compare with
         * @return {Boolean} `true` if this version if greater than the target, `false` otherwise
         */
        isGreaterThan: function(target) {
            return this.compareTo(target) > 0;
        },
        /**
         * Returns whether this version if greater than or equal to the supplied argument
         * @param {String/Number} target The version to compare with
         * @return {Boolean} `true` if this version if greater than or equal to the target,
         * `false` otherwise
         */
        isGreaterThanOrEqual: function(target) {
            return this.compareTo(target) >= 0;
        },
        /**
         * Returns whether this version if smaller than the supplied argument
         * @param {String/Number} target The version to compare with
         * @return {Boolean} `true` if this version if smaller than the target, `false` otherwise
         */
        isLessThan: function(target) {
            return this.compareTo(target) < 0;
        },
        /**
         * Returns whether this version if less than or equal to the supplied argument
         * @param {String/Number} target The version to compare with
         * @return {Boolean} `true` if this version if less than or equal to the target,
         * `false` otherwise
         */
        isLessThanOrEqual: function(target) {
            return this.compareTo(target) <= 0;
        },
        /**
         * Returns whether this version equals to the supplied argument
         * @param {String/Number} target The version to compare with
         * @return {Boolean} `true` if this version equals to the target, `false` otherwise
         */
        equals: function(target) {
            return this.compareTo(target) === 0;
        },
        /**
         * Returns whether this version matches the supplied argument. Example:
         *
         *     var version = new Ext.Version('1.0.2beta');
         *     console.log(version.match(1)); // true
         *     console.log(version.match(1.0)); // true
         *     console.log(version.match('1.0.2')); // true
         *     console.log(version.match('1.0.2RC')); // false
         *
         * @param {String/Number} target The version to compare with
         * @return {Boolean} `true` if this version matches the target, `false` otherwise
         */
        match: function(target) {
            target = String(target);
            return this.version.substr(0, target.length) === target;
        },
        /**
         * Returns this format: [major, minor, patch, build, release]. Useful for comparison.
         * @return {Number[]}
         */
        toArray: function() {
            var me = this;
            return [
                me.getMajor(),
                me.getMinor(),
                me.getPatch(),
                me.getBuild(),
                me.getRelease()
            ];
        },
        /**
         * Returns shortVersion version without dots and release
         * @return {String}
         */
        getShortVersion: function() {
            return this.shortVersion;
        },
        /**
         * Convenient alias to {@link Ext.Version#isGreaterThan isGreaterThan}
         * @param {String/Number/Ext.Version} target
         * @return {Boolean}
         */
        gt: function(target) {
            return this.compareTo(target) > 0;
        },
        /**
         * Convenient alias to {@link Ext.Version#isLessThan isLessThan}
         * @param {String/Number/Ext.Version} target
         * @return {Boolean}
         */
        lt: function(target) {
            return this.compareTo(target) < 0;
        },
        /**
         * Convenient alias to {@link Ext.Version#isGreaterThanOrEqual isGreaterThanOrEqual}
         * @param {String/Number/Ext.Version} target
         * @return {Boolean}
         */
        gtEq: function(target) {
            return this.compareTo(target) >= 0;
        },
        /**
         * Convenient alias to {@link Ext.Version#isLessThanOrEqual isLessThanOrEqual}
         * @param {String/Number/Ext.Version} target
         * @return {Boolean}
         */
        ltEq: function(target) {
            return this.compareTo(target) <= 0;
        }
    };
    Ext.apply(Version, {
        aliases: {
            from: {
                extjs: 'ext',
                core: 'core',
                touch: 'modern'
            },
            to: {
                ext: [
                    'extjs'
                ],
                'core': [
                    'core'
                ],
                modern: [
                    'touch'
                ]
            }
        },
        /**
         * @private
         */
        releaseValueMap: {
            dev: -6,
            alpha: -5,
            a: -5,
            beta: -4,
            b: -4,
            rc: -3,
            '#': -2,
            p: -1,
            pl: -1
        },
        /**
         * Converts a version component to a comparable value
         *
         * @static
         * @param {Object} value The value to convert
         * @return {Object}
         */
        getComponentValue: function(value) {
            // eslint-disable-next-line max-len
            return !value ? 0 : (isNaN(value) ? this.releaseValueMap[value] || value : parseInt(value, 10));
        },
        /**
         * Compare 2 specified versions by ensuring the first parameter is a `Version`
         * instance and then calling the `compareTo` method.
         *
         * @static
         * @param {String} current The current version to compare to
         * @param {String} target The target version to compare to
         * @return {Number} Returns -1 if the current version is smaller than the target version,
         * 1 if greater, and 0 if they're equivalent
         */
        compare: function(current, target) {
            var ver = current.isVersion ? current : new Version(current);
            return ver.compareTo(target);
        },
        set: function(collection, packageName, version) {
            var aliases = Version.aliases.to[packageName],
                ver = version.isVersion ? version : new Version(version),
                i;
            collection[packageName] = ver;
            if (aliases) {
                for (i = aliases.length; i-- > 0; ) {
                    collection[aliases[i]] = ver;
                }
            }
            return ver;
        }
    });
    /**
     * @class Ext
     */
    Ext.apply(Ext, {
        /**
         * @private
         */
        compatVersions: {},
        /**
         * @private
         * 
         * Object containing version information for all packages utilized by your 
         * application. 
         * 
         * For a public getter, please see `Ext.getVersion()`.
         */
        versions: {},
        /**
         * @private
         */
        lastRegisteredVersion: null,
        /**
         * Get the compatibility level (a version number) for the given package name. If
         * none has been registered with `Ext.setCompatVersion` then `Ext.getVersion` is
         * used to get the current version.
         *
         * @param {String} packageName The package name, e.g. 'core', 'touch', 'ext'.
         * @since 5.0.0
         * @private
         */
        getCompatVersion: function(packageName) {
            var versions = Ext.compatVersions,
                compat;
            if (!packageName) {
                compat = versions.ext || versions.touch || versions.core;
            } else {
                compat = versions[Version.aliases.from[packageName] || packageName];
            }
            return compat || Ext.getVersion(packageName);
        },
        /**
         * Set the compatibility level (a version number) for the given package name.
         *
         * @param {String} packageName The package name, e.g. 'core', 'touch', 'ext'.
         * @param {String/Ext.Version} version The version, e.g. '4.2'.
         * @since 5.0.0
         * @private
         */
        setCompatVersion: function(packageName, version) {
            Version.set(Ext.compatVersions, packageName, version);
        },
        /**
         * Set version number for the given package name.
         *
         * @param {String} packageName The package name, e.g. 'core', 'touch', 'ext'.
         * @param {String/Ext.Version} version The version, e.g. '1.2.3alpha', '2.4.0-dev'.
         * @return {Ext}
         */
        setVersion: function(packageName, version) {
            if (packageName in toolkitNames) {
                Ext.toolkit = packageName;
            }
            Ext.lastRegisteredVersion = Version.set(Ext.versions, packageName, version);
            return this;
        },
        /**
         * Get the version number of the supplied package name; will return the version of
         * the framework.
         *
         * @param {String} [packageName] The package name, e.g., 'core', 'touch', 'ext'.
         * @return {Ext.Version} The version.
         */
        getVersion: function(packageName) {
            var versions = Ext.versions;
            if (!packageName) {
                return versions.ext || versions.touch || versions.core;
            }
            return versions[Version.aliases.from[packageName] || packageName];
        },
        /**
         * This method checks the registered package versions against the provided version
         * `specs`. A `spec` is either a string or an object indicating a boolean operator.
         * This method accepts either form or an array of these as the first argument. The
         * second argument applies only when the first is an array and indicates whether
         * all `specs` must match or just one.
         * 
         * ## Package Version Specifications
         * The string form of a `spec` is used to indicate a version or range of versions
         * for a particular package. This form of `spec` consists of three (3) parts:
         * 
         *  * Package name followed by "@". If not provided, the framework is assumed.
         *  * Minimum version.
         *  * Maximum version.
         * 
         * At least one version number must be provided. If both minimum and maximum are
         * provided, these must be separated by a "-".
         * 
         * Some examples of package version specifications:
         * 
         *      4.2.2           (exactly version 4.2.2 of the framework)
         *      4.2.2+          (version 4.2.2 or higher of the framework)
         *      4.2.2-          (version 4.2.2 or higher of the framework)
         *      4.2.1 - 4.2.3   (versions from 4.2.1 up to 4.2.3 of the framework)
         *      - 4.2.2         (any version up to version 4.2.1 of the framework)
         *      
         *      foo@1.0         (exactly version 1.0 of package "foo")
         *      foo@1.0-1.3     (versions 1.0 up to 1.3 of package "foo")
         * 
         * **NOTE:** This syntax is the same as that used in Sencha Cmd's package
         * requirements declarations.
         * 
         * ## Boolean Operator Specifications
         * Instead of a string, an object can be used to describe a boolean operation to
         * perform on one or more `specs`. The operator is either **`and`** or **`or`**
         * and can contain an optional **`not`**.
         * 
         * For example:
         * 
         *      {
         *          not: true,  // negates boolean result
         *          and: [
         *              '4.2.2',
         *              'foo@1.0.1 - 2.0.1'
         *          ]
         *      }
         * 
         * Each element of the array can in turn be a string or object spec. In other
         * words, the value is passed to this method (recursively) as the first argument
         * so these two calls are equivalent:
         * 
         *      Ext.checkVersion({
         *          not: true,  // negates boolean result
         *          and: [
         *              '4.2.2',
         *              'foo@1.0.1 - 2.0.1'
         *          ]
         *      });
         *
         *      !Ext.checkVersion([
         *              '4.2.2',
         *              'foo@1.0.1 - 2.0.1'
         *          ], true);
         * 
         * ## Examples
         * 
         *      // A specific framework version
         *      Ext.checkVersion('4.2.2');
         * 
         *      // A range of framework versions:
         *      Ext.checkVersion('4.2.1-4.2.3');
         * 
         *      // A specific version of a package:
         *      Ext.checkVersion('foo@1.0.1');
         * 
         *      // A single spec that requires both a framework version and package
         *      // version range to match:
         *      Ext.checkVersion({
         *          and: [
         *              '4.2.2',
         *              'foo@1.0.1-1.0.2'
         *          ]
         *      });
         * 
         *      // These checks can be nested:
         *      Ext.checkVersion({
         *          and: [
         *              '4.2.2',  // exactly version 4.2.2 of the framework *AND*
         *              {
         *                  // either (or both) of these package specs:
         *                  or: [
         *                      'foo@1.0.1-1.0.2',
         *                      'bar@3.0+'
         *                  ]
         *              }
         *          ]
         *      });
         * 
         * ## Version Comparisons
         * Version comparsions are assumed to be "prefix" based. That is to say, `"foo@1.2"`
         * matches any version of "foo" that has a major version 1 and a minor version of 2.
         * 
         * This also applies to ranges. For example `"foo@1.2-2.2"` matches all versions
         * of "foo" from 1.2 up to 2.2 regardless of the specific patch and build.
         * 
         * ## Use in Overrides
         * This methods primary use is in support of conditional overrides on an
         * `Ext.define` declaration.
         * 
         * @param {String/Array/Object} specs A version specification string, an object
         * containing `or` or `and` with a value that is equivalent to `specs` or an array
         * of either of these.
         * @param {Boolean} [matchAll=false] Pass `true` to require all specs to match.
         * @return {Boolean} True if `specs` matches the registered package versions.
         */
        checkVersion: function(specs, matchAll) {
            var isArray = Ext.isArray(specs),
                aliases = Version.aliases.from,
                compat = isArray ? specs : checkVerTemp,
                length = compat.length,
                versions = Ext.versions,
                frameworkVer = versions.ext || versions.touch,
                i, index, matches, minVer, maxVer, packageName, spec, range, ver;
            if (!isArray) {
                checkVerTemp[0] = specs;
            }
            for (i = 0; i < length; ++i) {
                if (!Ext.isString(spec = compat[i])) {
                    matches = Ext.checkVersion(spec.and || spec.or, !spec.or);
                    if (spec.not) {
                        matches = !matches;
                    }
                } else {
                    if (spec.indexOf(' ') >= 0) {
                        spec = spec.replace(stripRe, '');
                    }
                    // For "name@..." syntax, we need to find the package by the given name
                    // as a registered package.
                    index = spec.indexOf('@');
                    if (index < 0) {
                        range = spec;
                        ver = frameworkVer;
                    } else {
                        packageName = spec.substring(0, index);
                        if (!(ver = versions[aliases[packageName] || packageName])) {
                            // The package is not registered, so if we must matchAll then
                            // we are done - FAIL:
                            if (matchAll) {
                                return false;
                            }
                            // Otherwise this spec is not a match so we can move on to the
                            // next...
                            
                            continue;
                        }
                        range = spec.substring(index + 1);
                    }
                    // Now look for a version, version range or partial range:
                    index = range.indexOf('-');
                    if (index < 0) {
                        // just a version or "1.0+"
                        if (range.charAt(index = range.length - 1) === '+') {
                            minVer = range.substring(0, index);
                            maxVer = null;
                        } else {
                            minVer = maxVer = range;
                        }
                    } else if (index > 0) {
                        // a range like "1.0-1.5" or "1.0-"
                        minVer = range.substring(0, index);
                        maxVer = range.substring(index + 1);
                    } else // may be empty
                    {
                        // an upper limit like "-1.5"
                        minVer = null;
                        maxVer = range.substring(index + 1);
                    }
                    matches = true;
                    if (minVer) {
                        minVer = new Version(minVer, '~');
                        // prefix matching
                        matches = minVer.ltEq(ver);
                    }
                    if (matches && maxVer) {
                        maxVer = new Version(maxVer, '~');
                        // prefix matching
                        matches = maxVer.gtEq(ver);
                    }
                }
                // string spec
                if (matches) {
                    // spec matched and we are looking for any match, so we are GO!
                    if (!matchAll) {
                        return true;
                    }
                } else if (matchAll) {
                    // spec does not match the registered package version
                    return false;
                }
            }
            // In the loop above, for matchAll we return FALSE on mismatch, so getting
            // here with matchAll means we had no mismatches. On the other hand, if we
            // are !matchAll, we return TRUE on match and so we get here only if we found
            // no matches.
            return !!matchAll;
        },
        /**
         * Create a closure for deprecated code.
         *
         *     // This means Ext.oldMethod is only supported in 4.0.0beta and older.
         *     // If Ext.getVersion('extjs') returns a version that is later than '4.0.0beta',
         *     // for example '4.0.0RC', the closure will not be invoked
         *     Ext.deprecate('extjs', '4.0.0beta', function() {
         *         Ext.oldMethod = Ext.newMethod;
         *
         *         ...
         *     });
         *
         * @param {String} packageName The package name
         * @param {String} since The last version before it's deprecated
         * @param {Function} closure The callback function to be executed with the specified
         * version is less than the current version
         * @param {Object} scope The execution scope (`this`) if the closure
         * @private
         */
        deprecate: function(packageName, since, closure, scope) {
            if (Version.compare(Ext.getVersion(packageName), since) < 1) {
                closure.call(scope);
            }
        }
    });
}());
// End Versioning
// load the cmd-5 style app manifest metadata now, if available...
(function(manifest) {
    var packages = (manifest && manifest.packages) || {},
        compat = manifest && manifest.compatibility,
        name, pkg;
    for (name in packages) {
        pkg = packages[name];
        if (pkg && pkg.version) {
            Ext.setVersion(name, pkg.version);
        }
    }
    if (compat) {
        if (Ext.isString(compat)) {
            Ext.setCompatVersion('core', compat);
        } else {
            for (name in compat) {
                Ext.setCompatVersion(name, compat[name]);
            }
        }
    }
    if (!packages.ext && !packages.touch) {
        Ext.setVersion('ext', '7.0.0.168');
        Ext.setVersion('core', '7.0.0.168');
    }
})(Ext.manifest);

/**
 * @class Ext.Config
 * This class manages a config property. Instances of this type are created and cached as
 * classes declare their config properties. One instance of this class is created per
 * config property name.
 *
 *      Ext.define('MyClass', {
 *          config: {
 *              foo: 42
 *          }
 *      });
 *
 * This uses the cached `Ext.Config` instance for the "foo" property.
 *
 * When config properties apply options to config properties a prototype chained object is
 * created from the cached instance. For example:
 *
 *      Ext.define('MyClass', {
 *          config: {
 *              foo: {
 *                  $value: 42,
 *                  lazy: true
 *              }
 *          }
 *      });
 *
 * This creates a prototype chain to the cached "foo" instance of `Ext.Config` and applies
 * the `lazy` option to that new instance. This chained instance is then kept by the
 * `Ext.Configurator` for that class.
 * @private
 */
Ext.Config = function(name) {
    // @define Ext.class.Config
    // @define Ext.Config
    var me = this,
        capitalizedName = name.charAt(0).toUpperCase() + name.substr(1);
    /**
     * @property {String} name
     * The name of this config property.
     * @readonly
     * @private
     * @since 5.0.0
     */
    me.name = name;
    /**
     * @property {Object} names
     * This object holds the cached names used to lookup properties or methods for this
     * config property. The properties of this object are explained in the context of an
     * example property named "foo".
     *
     * @property {String} names.internal The default backing property ("_foo").
     *
     * @property {String} names.initializing The property that is `true` when the config
     * is being initialized ("isFooInitializing").
     *
     * @property {String} names.apply The name of the applier method ("applyFoo").
     *
     * @property {String} names.update  The name of the updater method ("updateFoo").
     *
     * @property {String} names.get The name of the getter method ("getFoo").
     *
     * @property {String} names.set The name of the setter method ("setFoo").
     *
     * @property {String} names.initGet The name of the initializing getter ("initGetFoo").
     *
     * @property {String} names.changeEvent The name of the change event ("foochange").
     *
     * @readonly
     * @private
     * @since 5.0.0
     */
    me.names = {
        internal: '_' + name,
        initializing: 'is' + capitalizedName + 'Initializing',
        apply: 'apply' + capitalizedName,
        update: 'update' + capitalizedName,
        get: 'get' + capitalizedName,
        set: 'set' + capitalizedName,
        initGet: 'initGet' + capitalizedName,
        changeEvent: name.toLowerCase() + 'change'
    };
    // This allows folks to prototype chain on top of these objects and yet still cache
    // generated methods at the bottom of the chain.
    me.root = me;
};
Ext.Config.map = {};
Ext.Config.get = function(name) {
    var map = Ext.Config.map,
        ret = map[name] || (map[name] = new Ext.Config(name));
    return ret;
};
Ext.Config.prototype = {
    self: Ext.Config,
    isConfig: true,
    /**
     * @cfg {Boolean} [cached=false]
     * When set as `true` the config property will be stored on the class prototype once
     * the first instance has had a chance to process the default value.
     * @private
     * @since 5.0.0
     */
    /**
     * @cfg {Boolean} [lazy=false]
     * When set as `true` the config property will not be immediately initialized during
     * the `initConfig` call.
     * @private
     * @since 5.0.0
     */
    /**
     * @cfg {Boolean} [evented=false]
     * When set as `true` the config property will be treated as a
     * {@link Ext.Evented Evented Config}.
     * @private
     * @since 6.0.0
     */
    /**
     * @cfg {Function} [merge]
     * This function if supplied will be called as classes or instances provide values
     * that need to be combined with inherited values. The function should return the
     * value that will be the config value. Further calls may receive such returned
     * values as `oldValue`.
     *
     * @cfg {Mixed} merge.newValue The new value to merge with the old.
     *
     * @cfg {Mixed} merge.oldValue The current value prior to `newValue` being merged.
     *
     * @cfg {Mixed} merge.target The class or instance to which the merged config value
     * will be applied.
     *
     * @cfg {Ext.Class} merge.mixinClass The mixin providing the `newValue` or `null` if
     * the `newValue` is not being provided by a mixin.
     */
    combine: function(value, baseValue, instance, clone) {
        var cfg = this;
        if (cfg.merge) {
            value = cfg.merge(clone ? Ext.clone(value) : value, baseValue, instance);
        } else if (value && value.constructor === Object && baseValue && baseValue.constructor === Object) {
            value = Ext.merge({}, baseValue, value);
        } else if (clone && value) {
            value = Ext.clone(value);
        }
        return value;
    },
    equals: function(value1, value2) {
        return value1 === value2;
    },
    getGetter: function() {
        return this.getter || (this.root.getter = this.makeGetter());
    },
    getInitGetter: function() {
        return this.initGetter || (this.root.initGetter = this.makeInitGetter());
    },
    getSetter: function() {
        return this.setter || (this.root.setter = this.makeSetter());
    },
    getEventedSetter: function() {
        return this.eventedSetter || (this.root.eventedSetter = this.makeEventedSetter());
    },
    /**
     * Returns the name of the property that stores this config on the given instance or
     * class prototype.
     * @param {Object} target
     * @return {String}
     */
    getInternalName: function(target) {
        return target.$configPrefixed ? this.names.internal : this.name;
    },
    mergeNew: function(newValue, oldValue, target, mixinClass) {
        var ret, key;
        if (!oldValue) {
            ret = newValue;
        } else if (!newValue) {
            ret = oldValue;
        } else {
            ret = Ext.Object.chain(oldValue);
            for (key in newValue) {
                if (!mixinClass || !(key in ret)) {
                    ret[key] = newValue[key];
                }
            }
        }
        return ret;
    },
    /**
     * Merges the `newValue` and the `oldValue` assuming that these are basically objects
     * the represent sets. For example something like:
     *
     *      {
     *          foo: true,
     *          bar: true
     *      }
     *
     * The merge process converts arrays like the following into the above:
     *
     *      [ 'foo', 'bar' ]
     *
     * @param {String/String[]/Object} newValue
     * @param {Object} oldValue
     * @param {Boolean} [preserveExisting=false]
     * @return {Object}
     * @private
     * @since 5.0.0
     */
    mergeSets: function(newValue, oldValue, preserveExisting) {
        var ret = oldValue ? Ext.Object.chain(oldValue) : {},
            i, val;
        if (newValue instanceof Array) {
            for (i = newValue.length; i--; ) {
                val = newValue[i];
                if (!preserveExisting || !(val in ret)) {
                    ret[val] = true;
                }
            }
        } else if (newValue) {
            if (newValue.constructor === Object) {
                for (i in newValue) {
                    val = newValue[i];
                    if (!preserveExisting || !(i in ret)) {
                        ret[i] = val;
                    }
                }
            } else if (!preserveExisting || !(newValue in ret)) {
                ret[newValue] = true;
            }
        }
        return ret;
    },
    //--------------------------------------------------
    // Factories
    makeGetter: function() {
        var name = this.name,
            prefixedName = this.names.internal;
        return function() {
            var internalName = this.$configPrefixed ? prefixedName : name;
            return this[internalName];
        };
    },
    makeInitGetter: function() {
        var name = this.name,
            names = this.names,
            setName = names.set,
            getName = names.get,
            initializingName = names.initializing;
        return function() {
            var me = this;
            me[initializingName] = true;
            // Remove the initGetter from the instance now that the value has been set.
            delete me[getName];
            me[setName](me.config[name]);
            delete me[initializingName];
            return me[getName].apply(me, arguments);
        };
    },
    makeSetter: function() {
        var name = this.name,
            names = this.names,
            prefixedName = names.internal,
            getName = names.get,
            applyName = names.apply,
            updateName = names.update,
            setter;
        // http://jsperf.com/method-call-apply-or-direct
        // http://jsperf.com/method-detect-invoke
        setter = function(value) {
            var me = this,
                internalName = me.$configPrefixed ? prefixedName : name,
                oldValue = me[internalName],
                watch;
            // Remove the initGetter from the instance now that the value has been set.
            delete me[getName];
            if (!me[applyName] || (value = me[applyName](value, oldValue)) !== undefined) {
                // The old value might have been changed at this point
                // (after the apply call chain) so it should be read again
                if (value !== (oldValue = me[internalName])) {
                    me[internalName] = value;
                    if (me[updateName]) {
                        me[updateName](value, oldValue);
                    }
                    watch = me.$configWatch;
                    if (watch && !me.isConfiguring) {
                        // Since the updater could have modified things (tho unlikely), just
                        // re-read the stored value:
                        watch.fire(name, [
                            me,
                            name,
                            me[internalName],
                            oldValue
                        ]);
                    }
                }
            }
            return me;
        };
        setter.$isDefault = true;
        return setter;
    },
    makeEventedSetter: function() {
        var name = this.name,
            names = this.names,
            prefixedName = names.internal,
            getName = names.get,
            applyName = names.apply,
            updateName = names.update,
            changeEventName = names.changeEvent,
            updateFn = function(me, value, oldValue, internalName) {
                me[internalName] = value;
                if (me[updateName]) {
                    me[updateName](value, oldValue);
                }
                // eslint-disable-next-line vars-on-top
                var watch = me.$configWatch;
                if (watch) {
                    // !me.isConfiguring is assured
                    watch.fire(name, [
                        me,
                        name,
                        value,
                        oldValue
                    ]);
                }
            },
            setter;
        // http://jsperf.com/method-call-apply-or-direct
        // http://jsperf.com/method-detect-invoke
        setter = function(value) {
            var me = this,
                internalName = me.$configPrefixed ? prefixedName : name,
                oldValue = me[internalName];
            // Remove the initGetter from the instance now that the value has been set.
            delete me[getName];
            if (!me[applyName] || (value = me[applyName](value, oldValue)) !== undefined) {
                // The old value might have been changed at this point
                // (after the apply call chain) so it should be read again
                if (value !== (oldValue = me[internalName])) {
                    if (me.isConfiguring) {
                        me[internalName] = value;
                        if (me[updateName]) {
                            me[updateName](value, oldValue);
                        }
                    } else {
                        me.fireEventedAction(changeEventName, [
                            me,
                            value,
                            oldValue
                        ], updateFn, me, [
                            me,
                            value,
                            oldValue,
                            internalName
                        ]);
                    }
                }
            }
            return me;
        };
        setter.$isDefault = true;
        return setter;
    }
};

/**
 * @class Ext.Configurator
 * This class manages the config properties for a class.
 * @private
 */
(function() {
    // see end of file (and please don't indent the whole file)
    var ExtConfig = Ext.Config,
        configPropMap = ExtConfig.map,
        ExtObject = Ext.Object;
    Ext.Configurator = function(cls) {
        // @define Ext.class.Configurator
        // @define Ext.Configurator
        // @require Ext.Config
        var me = this,
            prototype = cls.prototype,
            superCfg = cls.superclass ? cls.superclass.self.$config : null;
        /**
     * @property {Ext.Class} cls The class to which this instance is associated.
     * @private
     * @readonly
     */
        me.cls = cls;
        /**
     * The super class `Configurator` instance or `null` if there is no super class.
     *
     * @property {Ext.Configurator} superCfg
     * @private
     * @readonly
     */
        me.superCfg = superCfg;
        if (superCfg) {
            /**
         * This object holds an `Ext.Config` value for each config property keyed by name.
         * This object has as its prototype object the `configs` of its super class.
         * 
         * This map is maintained as each property is added via the `add` method.
         * 
         * @property {Object} configs
         * @private
         * @readonly
         */
            me.configs = ExtObject.chain(superCfg.configs);
            /**
         * This object holds a bool value for each cachedConfig property keyed by name.
         * 
         * This map is maintained as each property is added via the `add` method.
         * 
         * @property {Object} cachedConfigs
         * @private
         * @readonly
         */
            me.cachedConfigs = ExtObject.chain(superCfg.cachedConfigs);
            /**
         * This object holds a `Number` for each config property keyed by name. This object has
         * as its prototype object the `initMap` of its super class. The value of each property
         * has the following meaning:
         * 
         *   * `0` - initial value is `null` and requires no processing.
         *   * `1` - initial value must be set on each instance.
         *   * `2` - initial value can be cached on the prototype by the first instance.
         *
         * Any `null` values will either never be added to this map or (if added by a base
         * class and set to `null` by a derived class) will cause the entry to be 0.
         * 
         * This map is maintained as each property is added via the `add` method.
         * 
         * @property {Object} initMap
         * @private
         * @readonly
         */
            me.initMap = ExtObject.chain(superCfg.initMap);
            /**
         * This object holds the default value for each config property keyed by name. This
         * object has as its prototype object the `values` of its super class.
         * 
         * This map is maintained as each property is added via the `add` method.
         * 
         * @property {Object} values
         * @private
         * @readonly
         */
            me.values = ExtObject.chain(superCfg.values);
            me.needsFork = superCfg.needsFork;
            // The reason this feature is debug only is that we would have to create this
            // map for all classes because deprecations could be added to bases after the
            // derived class had created its Configurator.
            me.deprecations = ExtObject.chain(superCfg.deprecations);
        } else {
            me.configs = {};
            me.cachedConfigs = {};
            me.initMap = {};
            me.values = {};
            me.deprecations = {};
        }
        prototype.config = prototype.defaultConfig = me.values;
        cls.$config = me;
    };
    Ext.Configurator.prototype = {
        self: Ext.Configurator,
        needsFork: false,
        /**
     * This array holds the properties that need to be set on new instances.
     * 
     * This array is populated when the first instance is passed to `configure` (basically
     * when the first instance is created). The entries in `initMap` are iterated to find
     * those configs needing per-instance processing.
     * 
     * @property {Ext.Config[]} initList
     * @private
     */
        initList: null,
        /**
     * This method adds new config properties. This is called for classes when they are
     * declared, then for any mixins that class may define and finally for any overrides
     * defined that target the class.
     * 
     * @param {Object} config The config object containing the new config properties.
     * @param {Ext.Class} [mixinClass] The mixin class if the configs are from a mixin.
     * @private
     */
        add: function(config, mixinClass) {
            var me = this,
                Cls = me.cls,
                configs = me.configs,
                cachedConfigs = me.cachedConfigs,
                initMap = me.initMap,
                prototype = Cls.prototype,
                mixinConfigs = mixinClass && mixinClass.$config.configs,
                values = me.values,
                isObject, meta, isCached, merge, cfg, currentValue, name, names, s, value;
            for (name in config) {
                value = config[name];
                isObject = value && value.constructor === Object;
                meta = isObject && '$value' in value ? value : null;
                isCached = false;
                if (meta) {
                    isCached = !!meta.cached;
                    value = meta.$value;
                    isObject = value && value.constructor === Object;
                }
                merge = meta && meta.merge;
                cfg = configs[name];
                if (cfg) {
                    // Only proceed with a mixin if we have a custom merge.
                    if (mixinClass) {
                        merge = cfg.merge;
                        if (!merge) {
                            
                            continue;
                        }
                        // Don't want the mixin meta modifying our own
                        meta = null;
                    } else {
                        merge = merge || cfg.merge;
                    }
                    // This means that we've already declared this as a config in a superclass
                    // Let's not allow us to change it here.
                    if (!mixinClass && isCached && !cachedConfigs[name]) {
                        Ext.raise('Redefining config as cached: ' + name + ' in class: ' + Cls.$className);
                    }
                    // There is already a value for this config and we are not allowed to
                    // modify it. So, if it is an object and the new value is also an object,
                    // the result is a merge so we have to merge both on to a new object.
                    currentValue = values[name];
                    if (merge) {
                        value = merge.call(cfg, value, currentValue, Cls, mixinClass);
                    } else if (isObject) {
                        if (currentValue && currentValue.constructor === Object) {
                            // We favor moving the cost of an "extra" copy here because this
                            // is likely to be a rare thing two object values for the same
                            // property. The alternative would be to clone the initial value
                            // to make it safely modifiable even though it is likely to never
                            // need to be modified.
                            value = Ext.merge({}, currentValue, value);
                        }
                    }
                } else // else "currentValue" is a primitive so "value" can just replace it
                // else "value" is a primitive and it can just replace currentValue
                {
                    // This is a new property value, so add it to the various maps "as is".
                    // In the majority of cases this value will not be overridden or need to
                    // be forked.
                    if (mixinConfigs) {
                        // Since this is a config from a mixin, we don't want to apply its
                        // meta-ness because it already has. Instead we want to use its cfg
                        // instance:
                        cfg = mixinConfigs[name];
                        meta = null;
                    } else {
                        cfg = ExtConfig.get(name);
                    }
                    configs[name] = cfg;
                    if (cfg.cached || isCached) {
                        cachedConfigs[name] = true;
                    }
                    // Ensure that the new config has a getter and setter. Because this method
                    // is called during class creation as the "config" (or "cachedConfig") is
                    // being processed, the user's methods will not be on the prototype yet.
                    // 
                    // This has the following trade-offs:
                    // 
                    // - Custom getters are rare so there is minimal waste generated by them.
                    // 
                    // - Custom setters are more common but, by putting the default setter on
                    //   the prototype prior to addMembers, when the user methods are added
                    //   callParent can be used to call the generated setter. This is almost
                    //   certainly desirable as the setter has some very important semantics
                    //   that a custom setter would probably want to preserve by just adding
                    //   logic before and/or after the callParent.
                    //   
                    // - By not adding these to the class body we avoid all the "is function"
                    //   tests that get applied to each class member thereby streamlining the
                    //   downstream class creation process.
                    //
                    // We still check for getter and/or setter but primarily for reasons of
                    // backwards compatibility and "just in case" someone relied on inherited
                    // getter/setter even though the base did not have the property listed as
                    // a "config" (obscure case certainly).
                    //
                    names = cfg.names;
                    if (!prototype[s = names.get]) {
                        prototype[s] = cfg.getter || cfg.getGetter();
                    }
                    if (!prototype[s = names.set]) {
                        prototype[s] = (meta && meta.evented) ? (cfg.eventedSetter || cfg.getEventedSetter()) : (cfg.setter || cfg.getSetter());
                    }
                }
                if (meta) {
                    if (cfg.owner !== Cls) {
                        configs[name] = cfg = Ext.Object.chain(cfg);
                        cfg.owner = Cls;
                    }
                    Ext.apply(cfg, meta);
                    delete cfg.$value;
                }
                // Fork checks all the default values to see if they are arrays or objects
                // Do this to save us from doing it on each run
                if (!me.needsFork && value && (value.constructor === Object || value instanceof Array)) {
                    me.needsFork = true;
                }
                // If the value is non-null, we need to initialize it.
                if (value !== null) {
                    initMap[name] = true;
                } else {
                    if (prototype.$configPrefixed) {
                        prototype[configs[name].names.internal] = null;
                    } else {
                        prototype[configs[name].name] = null;
                    }
                    if (name in initMap) {
                        // Only set this to false if we already have it in the map, otherwise,
                        // just leave it out!
                        initMap[name] = false;
                    }
                }
                values[name] = value;
            }
        },
        addDeprecations: function(configs) {
            var me = this,
                deprecations = me.deprecations,
                className = (me.cls.$className || '') + '#',
                message, newName, oldName;
            for (oldName in configs) {
                newName = configs[oldName];
                //      configs: {
                //          dead: null,
                //
                //          renamed: 'newName',
                //
                //          removed: {
                //              message: 'This config was replaced by pixie dust'
                //          }
                //      }
                if (!newName) {
                    message = 'This config has been removed.';
                } else if (!(message = newName.message)) {
                    message = 'This config has been renamed to "' + newName + '"';
                }
                deprecations[oldName] = className + oldName + ': ' + message;
            }
        },
        /**
     * This method configures the given `instance` using the specified `instanceConfig`.
     * The given `instance` should have been created by this object's `cls`.
     * 
     * @param {Object} instance The instance to configure.
     * @param {Object} instanceConfig The configuration properties to apply to `instance`.
     * @private
     */
        configure: function(instance, instanceConfig) {
            var me = this,
                configs = me.configs,
                deprecations = me.deprecations,
                initMap = me.initMap,
                initListMap = me.initListMap,
                initList = me.initList,
                prototype = me.cls.prototype,
                values = me.values,
                remaining = 0,
                firstInstance = !initList,
                cachedInitList, cfg, getter, i, internalName, ln, names, name, value, isCached, valuesKey, field, transforms;
            values = me.needsFork ? ExtObject.fork(values) : ExtObject.chain(values);
            // Let apply/update methods know that the initConfig is currently running.
            instance.isConfiguring = true;
            if (firstInstance) {
                // When called to configure the first instance of the class to which we are
                // bound we take a bit to plan for instance 2+.
                me.initList = initList = [];
                me.initListMap = initListMap = {};
                instance.isFirstInstance = true;
                for (name in initMap) {
                    cfg = configs[name];
                    isCached = cfg.cached;
                    if (initMap[name]) {
                        names = cfg.names;
                        value = values[name];
                        if (!prototype[names.set].$isDefault || prototype[names.apply] || prototype[names.update] || typeof value === 'object') {
                            if (isCached) {
                                // This is a cachedConfig, so it needs to be initialized with
                                // the default value and placed on the prototype... but the
                                // instanceConfig may have a different value so the value may
                                // need resetting. We have to defer the call to the setter so
                                // that all of the initGetters are set up first.
                                (cachedInitList || (cachedInitList = [])).push(cfg);
                            } else {
                                // Remember this config so that all instances (including this
                                // one) can invoke the setter to properly initialize it.
                                initList.push(cfg);
                                initListMap[name] = true;
                            }
                            // Point all getters to the initGetters. By doing this here we
                            // avoid creating initGetters for configs that don't need them
                            // and we can easily pick up the cached fn to save the call.
                            instance[names.get] = cfg.initGetter || cfg.getInitGetter();
                        } else {
                            // Non-object configs w/o custom setter, applier or updater can
                            // be simply stored on the prototype.
                            prototype[cfg.getInternalName(prototype)] = value;
                        }
                    } else if (isCached) {
                        prototype[cfg.getInternalName(prototype)] = undefined;
                    }
                }
            }
            // TODO - we need to combine the cached loop with the instanceConfig loop to
            // avoid duplication of init getter setups (for correctness if a cached cfg
            // calls on a non-cached cfg)
            ln = cachedInitList && cachedInitList.length;
            if (ln) {
                // This is only ever done on the first instance we configure. Any config in
                // cachedInitList has to be set to the default value to allow any side-effects
                // or transformations to occur. The resulting values can then be elevated to
                // the prototype and this property need not be initialized on each instance.
                for (i = 0; i < ln; ++i) {
                    internalName = cachedInitList[i].getInternalName(prototype);
                    // Since these are cached configs the base class will potentially have put
                    // its cached values on the prototype so we need to hide these while we
                    // run the inits for our cached configs.
                    instance[internalName] = null;
                }
                for (i = 0; i < ln; ++i) {
                    names = (cfg = cachedInitList[i]).names;
                    getter = names.get;
                    if (instance.hasOwnProperty(getter)) {
                        instance[names.set](values[cfg.name]);
                        delete instance[getter];
                    }
                }
                for (i = 0; i < ln; ++i) {
                    internalName = cachedInitList[i].getInternalName(prototype);
                    prototype[internalName] = instance[internalName];
                    delete instance[internalName];
                }
            }
            // The cachedConfigs have all been set to the default values including any of
            // those that may have been triggered by their getter.
            // If the instanceConfig has a platformConfig in it, we need to merge the active
            // rules of that object to make the actual instanceConfig.
            if (instanceConfig && instanceConfig.platformConfig) {
                instanceConfig = me.resolvePlatformConfig(instance, instanceConfig);
            }
            if (firstInstance) {
                // Allow the class to do things once the cachedConfig has been processed.
                // We need to call this method always when the first instance is configured
                // whether or not it actually has cached configs
                if (instance.afterCachedConfig && !instance.afterCachedConfig.$nullFn) {
                    instance.afterCachedConfig(instanceConfig);
                }
            }
            // Now that the cachedConfigs have been processed we can apply the instanceConfig
            // and hide the "configs" on the prototype. This will serve as the source for any
            // configs that need to initialize from their initial getter call.
            // IMPORTANT: "this.hasOwnProperty('config')" is how a config applier/updater can
            // tell it is processing the cached config value vs an instance config value.
            instance.config = values;
            // There are 2 possibilities here:
            // 1) If it's the first time in this function, we may have had cachedConfigs running.
            //    these configs may have called the getters for any of the normal getters, which
            //    means the initial getters have been clobbered on the instance and won't be able
            //    to be called below when we iterate over the initList. As such, we need to
            //    reinitialize them here, even though we've done it up above.
            //
            // 2) If this the second time in this function, the cachedConfigs won't be processed,
            //    so we don't need to worry about them clobbering config values. However, since
            //    we've already done all our setup, we won't enter into the block that sets the
            //    initGetter, so we need to do it here anyway.
            //
            // Also note, that lazy configs will appear in the initList because we need
            // to spin up the initGetter.
            for (i = 0 , ln = initList.length; i < ln; ++i) {
                cfg = initList[i];
                instance[cfg.names.get] = cfg.initGetter || cfg.getInitGetter();
            }
            // Give the class a chance to transform the configs. These are stored on the class
            // as a sorted array after the first instance is created. Prior to that, these are
            // stored as a prototype chained object on the class prototype. This allows the
            // transforms to be registered at any time during class load so long as they are
            // all loaded before instances are created.
            if (!(transforms = instance.self.$configTransforms)) {
                instance.self.$configTransforms = transforms = [];
                ln = instance.$configTransforms;
                for (name in ln) {
                    transforms.push([
                        name,
                        ln[name]
                    ]);
                }
                ln = transforms.length;
                if (ln > 1) {
                    transforms.sort(me.transformSorter);
                    for (i = 0; i < ln; ++i) {
                        transforms[i] = transforms[i][0];
                    }
                } else if (ln) {
                    transforms[0] = transforms[0][0];
                }
            }
            for (i = 0; i < transforms.length; ++i) {
                name = transforms[i];
                if (instance[name]) {
                    instanceConfig = instance[name](instanceConfig, me);
                }
            }
            // Important: We are looping here twice on purpose. This first loop serves 2 purposes:
            //
            // 1) Ensure the values collection is fully populated before we call any setters. Since
            // a setter may have an updater/applier, it could potentially call another getter() to grab
            // the value for some other property, so this ensures they are all set on the config object.
            //
            // 2) Ensure that the initGetter is set as the getter for any config that doesn't appear in
            // the initList. We need to ensure that the initGetter is pushed on for everything that
            // we will be setting during init time.
            //
            // The merging in this loop cannot be completed by Ext.merge(), since we do NOT want
            // to merge non-strict values, they should always just be assigned across without
            // modification.
            if (instanceConfig) {
                for (name in instanceConfig) {
                    value = instanceConfig[name];
                    cfg = configs[name];
                    if (deprecations[name]) {
                        Ext.log.warn(deprecations[name]);
                        if (!cfg) {
                            // If there is a Config for this, perhaps the class is emulating
                            // the old config... If there is not a Config we don't want to
                            // proceed and put the property on the instance. That will likely
                            // hide the bug during development.
                            
                            continue;
                        }
                    }
                    if (!cfg) {
                        field = instance.self.prototype[name];
                        if (instance.$configStrict && (typeof field === 'function') && !field.$nullFn) {
                            // In strict mode you cannot override functions
                            Ext.raise('Cannot override method ' + name + ' on ' + instance.$className + ' instance.');
                        }
                        // Not all "configs" use the config system so in this case simply put
                        // the value on the instance:
                        instance[name] = value;
                    } else {
                        // However we still need to create the initial value that needs
                        // to be used. We also need to spin up the initGetter.
                        if (!cfg.lazy) {
                            ++remaining;
                        }
                        if (!initListMap[name]) {
                            instance[cfg.names.get] = cfg.initGetter || cfg.getInitGetter();
                        }
                        valuesKey = values[name];
                        if (cfg.merge) {
                            value = cfg.merge(value, valuesKey, instance);
                        } else if (value && value.constructor === Object) {
                            if (valuesKey && valuesKey.constructor === Object) {
                                value = Ext.merge(values[name], value);
                            } else {
                                value = Ext.clone(value, false);
                            }
                        }
                    }
                    values[name] = value;
                }
            }
            // Give the class a chance to hook in prior to initializing the configs.
            if (instance.beforeInitConfig && !instance.beforeInitConfig.$nullFn) {
                if (instance.beforeInitConfig(instanceConfig) === false) {
                    return;
                }
            }
            if (instanceConfig) {
                for (name in instanceConfig) {
                    if (!remaining) {
                        // For classes that have few proper Config properties, this saves us
                        // from making the full 2 passes over the instanceConfig.
                        break;
                    }
                    // We can ignore deprecated configs here because we warned about them
                    // above. Further, since we only process proper Config's here we would
                    // not be skipping them anyway.
                    cfg = configs[name];
                    if (cfg && !cfg.lazy) {
                        --remaining;
                        // A proper "config" property so call the setter to set the value.
                        names = cfg.names;
                        getter = names.get;
                        // At this point the initGetter may have already been called and
                        // cleared if the getter was called from the applier or updater of a
                        // previously processed instance config. checking if the instance has
                        // its own getter ensures the setter does not get called twice.
                        if (instance.hasOwnProperty(getter)) {
                            instance[names.set](values[name]);
                            // The generated setter will remove the initGetter from the instance
                            // but the user may have provided their own setter so we have to do
                            // this here as well:
                            delete instance[names.get];
                        }
                    }
                }
            }
            // Process configs declared on the class that need per-instance initialization.
            for (i = 0 , ln = initList.length; i < ln; ++i) {
                cfg = initList[i];
                names = cfg.names;
                getter = names.get;
                if (!cfg.lazy && instance.hasOwnProperty(getter)) {
                    // Since the instance still hasOwn the getter, that means we've set an initGetter
                    // and it hasn't been cleared by calling any setter. Since we've never set the value
                    // because it wasn't passed in the instance, we go and set it here, taking the value
                    // from our definition config and passing it through finally clear off the getter.
                    instance[names.set](values[cfg.name]);
                    delete instance[getter];
                }
            }
            // Expose the value from the prototype chain (false):
            delete instance.isConfiguring;
        },
        getCurrentConfig: function(instance) {
            var defaultConfig = instance.defaultConfig,
                config = {},
                name;
            for (name in defaultConfig) {
                config[name] = instance[configPropMap[name].names.get]();
            }
            return config;
        },
        /**
     * This method is called to update the internal state of a given config when that
     * config is needed in a config transform (such as responsive or stateful mixins).
     *
     * @param {Ext.Base} instance The instance to configure.
     * @param {Object} instanceConfig The config for the instance.
     * @param {String[]} names The name(s) of the config(s) to process.
     * @private
     * @since 6.7.0
     */
        hoistConfigs: function(instance, instanceConfig, names) {
            var config = instance.config,
                configs = this.configs,
                initListMap = this.initListMap,
                ret = false,
                cfg, i, name;
            for (i = 0; i < names.length; ++i) {
                name = names[i];
                if (instanceConfig && name in instanceConfig) {
                    cfg = configs[name];
                    // the Ext.Config instance
                    config[name] = cfg.combine(instanceConfig[name], config[name], instance);
                    if (!initListMap[name]) {
                        instance[cfg.names.get] = cfg.initGetter || cfg.getInitGetter();
                    }
                }
                // The config could be defined on the class, so may be present even if
                // not in instance config.
                if (config[name] != null) {
                    ret = true;
                }
            }
            return ret;
        },
        /**
     * Merges the values of a config object onto a base config.
     * @param {Ext.Base} instance
     * @param {Object} baseConfig
     * @param {Object} config
     * @param {Boolean} [clone=false]
     * @return {Object} the merged config
     * @private
     */
        merge: function(instance, baseConfig, config, clone) {
            // Although this is a "private" method.  It is used by Sencha Architect and so
            // its api should remain stable.
            var configs = this.configs,
                name, value, baseValue, cfg;
            if (clone) {
                baseConfig = Ext.clone(baseConfig, /* cloneDom= */
                false);
            }
            for (name in config) {
                value = config[name];
                cfg = configs[name];
                if (cfg) {
                    baseValue = baseConfig[name];
                    if (cfg.merge) {
                        value = cfg.merge(value, baseValue, instance);
                    } else if (value && value.constructor === Object) {
                        if (baseValue && baseValue.constructor === Object) {
                            value = Ext.merge(baseValue, value);
                        } else {
                            value = Ext.clone(value, false);
                        }
                    }
                }
                baseConfig[name] = value;
            }
            return baseConfig;
        },
        /**
     * @private
     */
        reconfigure: function(instance, instanceConfig, options) {
            var currentConfig = instance.config,
                configList = [],
                strict = instance.$configStrict && !(options && options.strict === false),
                configs = this.configs,
                defaults = options && options.defaults,
                cfg, getter, i, len, name, names, prop;
            for (name in instanceConfig) {
                cfg = configs[name];
                /* eslint-disable-next-line max-len */
                if (defaults && instance.hasOwnProperty(cfg && instance.$configPrefixed ? cfg.names.internal : name)) {
                    
                    continue;
                }
                currentConfig[name] = instanceConfig[name];
                if (this.deprecations[name]) {
                    // See similar logic doc in configure() method.
                    Ext.log.warn(this.deprecations[name]);
                    if (!cfg) {
                        
                        continue;
                    }
                }
                if (cfg) {
                    // To ensure that configs being set here get processed in the proper order
                    // we must give them init getters just in case they depend upon each other
                    instance[cfg.names.get] = cfg.initGetter || cfg.getInitGetter();
                } else {
                    // Check for existence of the property on the prototype before proceeding.
                    // If present on the prototype, and if the property is a function we
                    // do not allow it to be overridden by a property in the config object
                    // in strict mode (unless the function on the prototype is a emptyFn or
                    // identityFn).  Note that we always check the prototype, not the instance
                    // because calling setConfig a second time should have the same results -
                    // the first call may have set a function on the instance.
                    prop = instance.self.prototype[name];
                    if (strict) {
                        if ((typeof prop === 'function') && !prop.$nullFn) {
                            Ext.Error.raise("Cannot override method " + name + " on " + instance.$className + " instance.");
                            
                            continue;
                        } else {
                            if (name !== 'type') {
                                Ext.log.warn('No such config "' + name + '" for class ' + instance.$className);
                            }
                        }
                    }
                }
                configList.push(name);
            }
            for (i = 0 , len = configList.length; i < len; i++) {
                name = configList[i];
                cfg = configs[name];
                if (cfg) {
                    names = cfg.names;
                    getter = names.get;
                    if (instance.hasOwnProperty(getter)) {
                        // Since the instance still hasOwn the getter, that means we've set
                        // an initGetter and it hasn't been cleared by calling any setter.
                        // Since we've never set the value because it wasn't passed in the instance,
                        // we go and set it here, taking the value from our definition config
                        // and passing it through finally clear off the getter.
                        instance[names.set](instanceConfig[name]);
                        delete instance[getter];
                    }
                } else {
                    cfg = configPropMap[name] || Ext.Config.get(name);
                    names = cfg.names;
                    if (instance[names.set]) {
                        instance[names.set](instanceConfig[name]);
                    } else {
                        // apply non-config props directly to the instance
                        instance[name] = instanceConfig[name];
                    }
                }
            }
        },
        /**
     * This method accepts an instance config object containing a `platformConfig`
     * property and merges the appropriate rules from that sub-object with the root object
     * to create the final config object that should be used. This is method called by
     * `{@link #configure}` when it receives an `instanceConfig` containing a
     * `platformConfig` property.
     *
     * @param {Ext.Base} instance
     * @param {Object} instanceConfig The instance config parameter.
     * @return {Object} The new instance config object with platformConfig results applied.
     * @private
     * @since 5.1.0
     */
        resolvePlatformConfig: function(instance, instanceConfig) {
            var platformConfig = instanceConfig && instanceConfig.platformConfig,
                ret = instanceConfig,
                i, keys, n;
            if (platformConfig) {
                keys = Ext.getPlatformConfigKeys(platformConfig);
                n = keys.length;
                if (n) {
                    ret = Ext.merge({}, ret);
                    // this deep copies sub-objects
                    for (i = 0 , n = keys.length; i < n; ++i) {
                        this.merge(instance, ret, platformConfig[keys[i]]);
                    }
                }
            }
            return ret;
        },
        transformSorter: function(a, b) {
            return a[1] - b[1];
        }
    };
}());
// prototype
// closure on whole file

// @tag class
/**
 * @class Ext.Base
 *
 * The root of all classes created with {@link Ext#define}.
 *
 * Ext.Base is the building block of all Ext classes. All classes in Ext inherit from Ext.Base.
 * All prototype and static members of this class are inherited by all other classes.
 */
Ext.Base = (function(flexSetter) {
    // @define Ext.Base
    // @require Ext.Util
    // @require Ext.Version
    // @require Ext.Configurator
    // @uses Ext.ClassManager
    // @uses Ext.mixin.Watchable
    /* eslint-disable indent */
    var noArgs = [],
        baseStaticMember,
        baseStaticMembers = [],
        makeDeprecatedMethod = function(oldName, newName, msg) {
            var message = '"' + oldName + '" is deprecated.';
            if (msg) {
                message += ' ' + msg;
            } else if (newName) {
                message += ' Please use "' + newName + '" instead.';
            }
            return function() {
                Ext.raise(message);
            };
        },
        addDeprecatedProperty = function(object, oldName, newName, message) {
            if (!message) {
                message = '"' + oldName + '" is deprecated.';
            }
            if (newName) {
                message += ' Please use "' + newName + '" instead.';
            }
            if (message) {
                Ext.Object.defineProperty(object, oldName, {
                    get: function() {
                        // eslint-disable-line getter-return
                        Ext.raise(message);
                    },
                    set: function(value) {
                        Ext.raise(message);
                    },
                    configurable: true
                });
            }
        },
        getOwnObject = function(proto, name) {
            if (!proto.hasOwnProperty(name)) {
                proto[name] = Ext.Object.chain(getOwnObject(proto.superclass, name));
            }
            return proto[name];
        },
        makeAliasFn = function(name) {
            return function() {
                return this[name].apply(this, arguments);
            };
        },
        Version = Ext.Version,
        leadingDigitRe = /^\d/,
        oneMember = {},
        aliasOneMember = {},
        Base = function() {},
        BasePrototype = Base.prototype,
        Reaper;
    Ext.Reaper = Reaper = {
        delay: 100,
        queue: [],
        timer: null,
        add: function(obj) {
            if (!Reaper.timer) {
                Reaper.timer = Ext.defer(Reaper.tick, Reaper.delay);
            }
            Reaper.queue.push(obj);
        },
        flush: function() {
            if (Reaper.timer) {
                Ext.undefer(Reaper.timer);
                Reaper.timer = null;
            }
            /* eslint-disable-next-line vars-on-top */
            var queue = Reaper.queue,
                n = queue.length,
                i, obj;
            Reaper.queue = [];
            for (i = 0; i < n; ++i) {
                obj = queue[i];
                if (obj && obj.$reap) {
                    obj.$reap();
                }
            }
        },
        tick: function() {
            Reaper.timer = null;
            Reaper.flush();
        }
    };
    // These static properties will be copied to every newly created class with {@link Ext#define}
    Ext.apply(Base, {
        $className: 'Ext.Base',
        $isClass: true,
        /**
         * Create a new instance of this Class.
         *
         *     Ext.define('My.cool.Class', {
         *         ...
         *     });
         *
         *     My.cool.Class.create({
         *         someConfig: true
         *     });
         *
         * All parameters are passed to the constructor of the class.
         *
         * @return {Object} the created instance.
         * @static
         * @inheritable
         */
        create: function() {
            return Ext.create.apply(Ext, [
                this
            ].concat(Array.prototype.slice.call(arguments, 0)));
        },
        addConfigTransform: function(methodName, priority) {
            var transforms = getOwnObject(this.prototype, '$configTransforms');
            if (this.$configTransforms) {
                Ext.raise('Config transforms cannot be added after instances are created');
            }
            transforms[methodName] = priority;
        },
        /**
         * This method applies a versioned, deprecation declaration to this class. This
         * is typically called by the `deprecated` config.
         * @private
         */
        addDeprecations: function(deprecations) {
            var me = this,
                all = [],
                compatVersion = Ext.getCompatVersion(deprecations.name),
                configurator = me.getConfigurator(),
                displayName = (me.$className || '') + '#',
                deprecate, versionSpec, index, message, target, enabled, existing, fn, names, oldName, newName, member, statics, version;
            for (versionSpec in deprecations) {
                if (leadingDigitRe.test(versionSpec)) {
                    version = new Ext.Version(versionSpec);
                    version.deprecations = deprecations[versionSpec];
                    all.push(version);
                }
            }
            all.sort(Version.compare);
            for (index = all.length; index--; ) {
                deprecate = (version = all[index]).deprecations;
                target = me.prototype;
                statics = deprecate.statics;
                // If user specifies, say 4.2 compatibility and we have a 5.0 deprecation
                // then that block needs to be "enabled" to "revert" to behaviors prior
                // to 5.0. By default, compatVersion === currentVersion, so there are no
                // enabled blocks. In dev mode we still want to visit all the blocks and
                // possibly add shims to detect use of deprecated methods, but in a build
                // (if the deprecated block remains somehow) we just break the loop.
                enabled = compatVersion && compatVersion.lt(version);
                if (!enabled) {}
                // eslint-disable-line no-empty, brace-style
                else if (!enabled) {
                    // we won't get here in dev mode when !enabled
                    break;
                }
                while (deprecate) {
                    names = deprecate.methods;
                    if (names) {
                        for (oldName in names) {
                            member = names[oldName];
                            fn = null;
                            if (!member) {
                                /*
                                 * Something like:
                                 *
                                 *      '5.1': {
                                 *          methods: {
                                 *              removedMethod: null
                                 *          }
                                 *      }
                                 *
                                 * Since there is no recovering the method, we always put
                                 * on a shim to catch abuse.
                                 */
                                // The class should not already have a method by the oldName
                                Ext.Assert.isNotDefinedProp(target, oldName);
                                fn = makeDeprecatedMethod(displayName + oldName);
                            } else if (Ext.isString(member)) {
                                /*
                                 * Something like:
                                 *
                                 *      '5.1': {
                                 *          methods: {
                                 *              oldName: 'newName'
                                 *          }
                                 *      }
                                 *
                                 * If this block is enabled, we just put an alias in place.
                                 * Otherwise we need to inject a
                                 */
                                // The class should not already have a method by the oldName
                                Ext.Assert.isNotDefinedProp(target, oldName);
                                Ext.Assert.isDefinedProp(target, member);
                                if (enabled) {
                                    // This call to the real method name must be late
                                    // bound if it is to pick up overrides and such.
                                    fn = makeAliasFn(member);
                                } else {
                                    fn = makeDeprecatedMethod(displayName + oldName, member);
                                }
                            } else {
                                /*
                                 * Something like:
                                 *
                                 *      '5.1': {
                                 *          methods: {
                                 *              foo: function() { ... }
                                 *          }
                                 *      }
                                 *
                                 * Or this:
                                 *
                                 *      '5.1': {
                                 *          methods: {
                                 *              foo: {
                                 *                  fn: function() { ... },
                                 *                  message: 'Please use "bar" instead.'
                                 *              }
                                 *          }
                                 *      }
                                 *
                                 * Or just this:
                                 *
                                 *      '5.1': {
                                 *          methods: {
                                 *              foo: {
                                 *                  message: 'Use something else instead.'
                                 *              }
                                 *          }
                                 *      }
                                 *
                                 * If this block is enabled, and "foo" is an existing
                                 * method, than we apply the given method as an override.
                                 * If "foo" is not existing, we simply add the method.
                                 *
                                 * If the block is not enabled and there is no existing
                                 * method by that name, than we add a shim to prevent
                                 * abuse.
                                 */
                                message = '';
                                if (member.message || member.fn) {
                                    message = member.message;
                                    member = member.fn;
                                }
                                existing = target.hasOwnProperty(oldName) && target[oldName];
                                if (enabled && member) {
                                    member.$owner = me;
                                    member.$name = oldName;
                                    member.name = displayName + oldName;
                                    if (existing) {
                                        member.$previous = existing;
                                    }
                                    fn = member;
                                } else if (!existing) {
                                    fn = makeDeprecatedMethod(displayName + oldName, null, message);
                                }
                            }
                            if (fn) {
                                target[oldName] = fn;
                            }
                        }
                    }
                    // for oldName
                    //-------------------------------------
                    // Debug only
                    names = deprecate.configs;
                    if (names) {
                        //
                        //  '6.0': {
                        //      configs: {
                        //          dead: null,
                        //
                        //          renamed: 'newName',
                        //
                        //          removed: {
                        //              message: 'This config was replaced by pixie dust'
                        //          }
                        //      }
                        //  }
                        //
                        configurator.addDeprecations(names);
                    }
                    names = deprecate.properties;
                    if (names && !enabled) {
                        // For properties about the only thing we can do is (on Good
                        // Browsers), add warning shims for accessing them. So if the
                        // block is enabled, we don't want those.
                        for (oldName in names) {
                            newName = names[oldName];
                            if (Ext.isString(newName)) {
                                addDeprecatedProperty(target, displayName + oldName, newName);
                            } else if (newName && newName.message) {
                                addDeprecatedProperty(target, displayName + oldName, null, newName.message);
                            } else {
                                addDeprecatedProperty(target, displayName + oldName);
                            }
                        }
                    }
                    //-------------------------------------
                    // reset to handle statics and apply them to the class
                    deprecate = statics;
                    statics = null;
                    target = me;
                }
            }
        },
        /**
         * @private
         * @static
         * @inheritable
         * @param parentClass
         */
        extend: function(parentClass) {
            var me = this,
                parentPrototype = parentClass.prototype,
                prototype, name, statics;
            prototype = me.prototype = Ext.Object.chain(parentPrototype);
            prototype.self = me;
            me.superclass = prototype.superclass = parentPrototype;
            if (!parentClass.$isClass) {
                for (name in BasePrototype) {
                    if (name in prototype) {
                        prototype[name] = BasePrototype[name];
                    }
                }
            }
            // Statics inheritance
            statics = parentPrototype.$inheritableStatics;
            if (statics) {
                for (name in statics) {
                    if (!me.hasOwnProperty(name)) {
                        me[name] = parentClass[name];
                    }
                }
            }
            if (parentClass.$onExtended) {
                me.$onExtended = parentClass.$onExtended.slice();
            }
            me.getConfigurator();
        },
        /**
         * @private
         * @static
         * @inheritable
         */
        $onExtended: [],
        /**
         * @private
         * @static
         * @inheritable
         */
        triggerExtended: function() {
            if (Ext.classSystemMonitor) {
                Ext.classSystemMonitor(this, 'Ext.Base#triggerExtended', arguments);
            }
            /* eslint-disable-next-line vars-on-top */
            var callbacks = this.$onExtended,
                ln = callbacks.length,
                i, callback;
            if (ln > 0) {
                for (i = 0; i < ln; i++) {
                    callback = callbacks[i];
                    callback.fn.apply(callback.scope || this, arguments);
                }
            }
        },
        /**
         * @private
         * @static
         * @inheritable
         */
        onExtended: function(fn, scope) {
            this.$onExtended.push({
                fn: fn,
                scope: scope
            });
            return this;
        },
        /**
         * Add / override static properties of this class.
         *
         *     Ext.define('My.cool.Class', {
         *         ...
         *     });
         *
         *     My.cool.Class.addStatics({
         *         someProperty: 'someValue',      // My.cool.Class.someProperty = 'someValue'
         *         method1: function() { ... },    // My.cool.Class.method1 = function() { ... };
         *         method2: function() { ... }     // My.cool.Class.method2 = function() { ... };
         *     });
         *
         * @param {Object} members
         * @return {Ext.Base} this
         * @static
         * @inheritable
         */
        addStatics: function(members) {
            this.addMembers(members, true);
            return this;
        },
        /**
         * @private
         * @static
         * @inheritable
         * @param {Object} members
         */
        addInheritableStatics: function(members) {
            var me = this,
                proto = me.prototype,
                inheritableStatics = me.$inheritableStatics,
                name, member, current;
            if (!inheritableStatics) {
                inheritableStatics = Ext.apply({}, proto.$inheritableStatics);
                me.$inheritableStatics = proto.$inheritableStatics = inheritableStatics;
            }
            /* eslint-disable-next-line vars-on-top */
            var className = Ext.getClassName(me) + '.';
            for (name in members) {
                if (members.hasOwnProperty(name)) {
                    member = members[name];
                    current = me[name];
                    if (typeof member === 'function') {
                        member.name = className + name;
                    }
                    if (typeof current === 'function' && !current.$isClass && !current.$nullFn) {
                        member.$previous = current;
                    }
                    me[name] = member;
                    inheritableStatics[name] = true;
                }
            }
            return me;
        },
        /**
         * Add methods / properties to the prototype of this class.
         *
         *     Ext.define('My.awesome.Cat', {
         *         constructor: function() {
         *             ...
         *         }
         *     });
         *
         *      My.awesome.Cat.addMembers({
         *          meow: function() {
         *             alert('Meowww...');
         *          }
         *      });
         *
         *      var kitty = new My.awesome.Cat();
         *      kitty.meow();
         *
         * @param {Object} members The members to add to this class.
         * @param {Boolean} [isStatic=false] Pass `true` if the members are static.
         * @param {Boolean} [privacy=false] Pass `true` if the members are private. This
         * only has meaning in debug mode and only for methods.
         * @static
         * @inheritable
         */
        addMembers: function(members, isStatic, privacy) {
            var me = this,
                // this class
                cloneFunction = Ext.Function.clone,
                target = isStatic ? me : me.prototype,
                defaultConfig = !isStatic && target.defaultConfig,
                enumerables = Ext.enumerables,
                privates = members.privates,
                configs, i, ln, member, name, subPrivacy, privateStatics;
            /* eslint-disable-next-line vars-on-top, one-var */
            var displayName = (me.$className || '') + '#';
            if (privates) {
                // This won't run for normal class private members but will pick up all
                // others (statics, overrides, etc).
                delete members.privates;
                if (!isStatic) {
                    privateStatics = privates.statics;
                    delete privates.statics;
                }
                subPrivacy = privates.privacy || privacy || 'framework';
                me.addMembers(privates, isStatic, subPrivacy);
                if (privateStatics) {
                    me.addMembers(privateStatics, true, subPrivacy);
                }
            }
            for (name in members) {
                if (members.hasOwnProperty(name)) {
                    member = members[name];
                    if (privacy === true) {
                        privacy = 'framework';
                    }
                    if (member && member.$nullFn && privacy !== member.$privacy) {
                        Ext.raise('Cannot use stock function for private method ' + (me.$className ? me.$className + '#' : '') + name);
                    }
                    if (typeof member === 'function' && !member.$isClass && !member.$nullFn) {
                        if (member.$owner) {
                            member = cloneFunction(member);
                        }
                        if (target.hasOwnProperty(name)) {
                            member.$previous = target[name];
                        }
                        // This information is needed by callParent() and callSuper() as
                        // well as statics() and even Ext.fly().
                        member.$owner = me;
                        member.$name = name;
                        member.name = displayName + name;
                        /* eslint-disable-next-line vars-on-top */
                        var existing = target[name];
                        if (privacy) {
                            member.$privacy = privacy;
                            // The general idea here is that an existing, non-private
                            // method can be marked private. This is because the other
                            // way is strictly forbidden (private method going public)
                            // so if a method is in that gray area it can only be made
                            // private in doc form which allows a derived class to make
                            // it public.
                            if (existing && existing.$privacy && existing.$privacy !== privacy) {
                                Ext.privacyViolation(me, existing, member, isStatic);
                            }
                        } else if (existing && existing.$privacy) {
                            Ext.privacyViolation(me, existing, member, isStatic);
                        }
                    }
                    // The last part of the check here resolves a conflict if we have the same
                    // property declared as both a config and a member on the class so that
                    // the config wins.
                    else if (defaultConfig && (name in defaultConfig) && !target.config.hasOwnProperty(name)) {
                        // This is a config property so it must be added to the configs
                        // collection not just smashed on the prototype...
                        (configs || (configs = {}))[name] = member;
                        
                        continue;
                    }
                    target[name] = member;
                }
            }
            if (configs) {
                // Add any configs found in the normal members arena:
                me.addConfig(configs);
            }
            if (enumerables) {
                for (i = 0 , ln = enumerables.length; i < ln; ++i) {
                    if (members.hasOwnProperty(name = enumerables[i])) {
                        member = members[name];
                        // The enumerables are all functions...
                        if (member && !member.$nullFn) {
                            if (member.$owner) {
                                member = cloneFunction(member);
                            }
                            member.$owner = me;
                            member.$name = name;
                            member.name = displayName + name;
                            if (target.hasOwnProperty(name)) {
                                member.$previous = target[name];
                            }
                        }
                        target[name] = member;
                    }
                }
            }
            return this;
        },
        /**
         * @private
         * @static
         * @inheritable
         * @param name
         * @param member
         * @param privacy
         */
        addMember: function(name, member, privacy) {
            oneMember[name] = member;
            this.addMembers(oneMember, false, privacy);
            delete oneMember[name];
            return this;
        },
        hookMember: function(name, member) {
            var existing = this.prototype[name];
            return this.addMember(name, member, existing && existing.$privacy);
        },
        /**
         * Borrow another class' members to the prototype of this class.
         *
         *     Ext.define('Bank', {
         *         money: '$$$',
         *         printMoney: function() {
         *             alert('$$$$$$$');
         *         }
         *     });
         *
         *     Ext.define('Thief', {
         *         ...
         *     });
         *
         *     Thief.borrow(Bank, ['money', 'printMoney']);
         *
         *     var steve = new Thief();
         *
         *     alert(steve.money); // alerts '$$$'
         *     steve.printMoney(); // alerts '$$$$$$$'
         *
         * @param {Ext.Base} fromClass The class to borrow members from
         * @param {Array/String} members The names of the members to borrow
         * @return {Ext.Base} this
         * @static
         * @inheritable
         * @private
         */
        borrow: function(fromClass, members) {
            if (Ext.classSystemMonitor) {
                Ext.classSystemMonitor(this, 'Ext.Base#borrow', arguments);
            }
            /* eslint-disable-next-line vars-on-top */
            var prototype = fromClass.prototype,
                membersObj = {},
                i, ln, name;
            members = Ext.Array.from(members);
            for (i = 0 , ln = members.length; i < ln; i++) {
                name = members[i];
                membersObj[name] = prototype[name];
            }
            return this.addMembers(membersObj);
        },
        /**
         * Override members of this class. Overridden methods can be invoked via
         * {@link Ext.Base#method!callParent}.
         *
         *     Ext.define('My.Cat', {
         *         constructor: function() {
         *             alert("I'm a cat!");
         *         }
         *     });
         *
         *     My.Cat.override({
         *         constructor: function() {
         *             alert("I'm going to be a cat!");
         *
         *             this.callParent(arguments);
         *
         *             alert("Meeeeoooowwww");
         *         }
         *     });
         *
         *     var kitty = new My.Cat(); // alerts "I'm going to be a cat!"
         *                               // alerts "I'm a cat!"
         *                               // alerts "Meeeeoooowwww"
         *
         * Direct use of this method should be rare. Use {@link Ext#define Ext.define}
         * instead:
         *
         *     Ext.define('My.CatOverride', {
         *         override: 'My.Cat',
         *         constructor: function() {
         *             alert("I'm going to be a cat!");
         *
         *             this.callParent(arguments);
         *
         *             alert("Meeeeoooowwww");
         *         }
         *     });
         *
         * The above accomplishes the same result but can be managed by the {@link Ext.Loader}
         * which can properly order the override and its target class and the build process
         * can determine whether the override is needed based on the required state of the
         * target class (My.Cat).
         *
         * @param {Object} members The properties to add to this class. This should be
         * specified as an object literal containing one or more properties.
         * @return {Ext.Base} this class
         * @static
         * @inheritable
         */
        override: function(members) {
            var me = this,
                statics = members.statics,
                inheritableStatics = members.inheritableStatics,
                config = members.config,
                mixins = members.mixins,
                cachedConfig = members.cachedConfig;
            if (statics || inheritableStatics || config) {
                members = Ext.apply({}, members);
            }
            if (statics) {
                me.addMembers(statics, true);
                delete members.statics;
            }
            if (inheritableStatics) {
                me.addInheritableStatics(inheritableStatics);
                delete members.inheritableStatics;
            }
            if (members.platformConfig) {
                me.addPlatformConfig(members);
            }
            if (config) {
                me.addConfig(config);
                delete members.config;
            }
            if (cachedConfig) {
                me.addCachedConfig(cachedConfig);
                delete members.cachedConfig;
            }
            delete members.mixins;
            me.addMembers(members);
            if (mixins) {
                me.mixin(mixins);
            }
            return me;
        },
        addPlatformConfig: function(data) {
            var me = this,
                prototype = me.prototype,
                platformConfigs = data.platformConfig,
                added, classConfigs, configs, configurator, keys, name, value, i, ln;
            delete prototype.platformConfig;
            if (platformConfigs instanceof Array) {
                throw new Error('platformConfigs must be specified as an object.');
            }
            configurator = me.getConfigurator();
            classConfigs = configurator.configs;
            // Get the keys shortest to longest (ish).
            keys = Ext.getPlatformConfigKeys(platformConfigs);
            // To leverage the Configurator#add method, we want to generate potentially
            // two objects to pass in: "added" and "hoisted". For any properties in an
            // active platformConfig rule that set proper Configs in the base class, we
            // need to put them in "added". If instead of the proper Config coming from
            // a base class, it comes from this class's config block, we still need to
            // put that config in "added" but we also need move the class-level config
            // out of "config" and into "hoisted".
            //
            // This will ensure that the config defined at the class level is added to
            // the Configurator first.
            for (i = 0 , ln = keys.length; i < ln; ++i) {
                configs = platformConfigs[keys[i]];
                added = null;
                for (name in configs) {
                    value = configs[name];
                    // We have a few possibilities for each config name:
                    if (name in classConfigs) {
                        //  It is a proper Config defined by a base class.
                        (added || (added = {}))[name] = value;
                    } else {
                        //  It is just a property to put on the prototype.
                        prototype[name] = value;
                    }
                }
                if (added) {
                    configurator.add(added);
                }
            }
        },
        /**
         * @protected
         * @static
         * @inheritable
         */
        callParent: function(args) {
            var method;
            // This code is intentionally inlined for the least amount of debugger stepping
            return (method = this.callParent.caller) && (method.$previous || ((method = method.$owner ? method : method.caller) && method.$owner.superclass.self[method.$name])).apply(this, args || noArgs);
        },
        /**
         * @protected
         * @static
         * @inheritable
         */
        callSuper: function(args) {
            var method;
            // This code is intentionally inlined for the least amount of debugger stepping
            return (method = this.callSuper.caller) && ((method = method.$owner ? method : method.caller) && method.$owner.superclass.self[method.$name]).apply(this, args || noArgs);
        },
        /**
         * Used internally by the mixins pre-processor
         * @private
         * @static
         * @inheritable
         */
        mixin: function(name, mixinClass) {
            var me = this,
                mixin, prototype, key, statics, i, ln, mixinName, mixinValue, mixins, mixinStatics, staticName;
            if (typeof name !== 'string') {
                mixins = name;
                if (mixins instanceof Array) {
                    for (i = 0 , ln = mixins.length; i < ln; i++) {
                        mixin = mixins[i];
                        me.mixin(mixin.prototype.mixinId || mixin.$className, mixin);
                    }
                } else {
                    // Not a string or array - process the object form:
                    // mixins: {
                    //     foo: ...
                    // }
                    for (mixinName in mixins) {
                        me.mixin(mixinName, mixins[mixinName]);
                    }
                }
                return;
            }
            mixin = mixinClass.prototype;
            prototype = me.prototype;
            if (mixin.onClassMixedIn) {
                mixin.onClassMixedIn.call(mixinClass, me);
            }
            if (!prototype.hasOwnProperty('mixins')) {
                if ('mixins' in prototype) {
                    prototype.mixins = Ext.Object.chain(prototype.mixins);
                } else {
                    prototype.mixins = {};
                }
            }
            for (key in mixin) {
                mixinValue = mixin[key];
                if (key === 'mixins') {
                    // if 2 superclasses (e.g. a base class and a mixin) of this class both
                    // have a mixin with the same id, the first one wins, that is to say,
                    // the first mixin's methods to be applied to the prototype will not
                    // be overwritten by the second one.  Since this is the case we also
                    // want to make sure we use the first mixin's prototype as the mixin
                    // reference, hence the "applyIf" below.  A real world example of this
                    // is Ext.Widget which mixes in Ext.mixin.Observable.  Ext.Widget can
                    // be mixed into subclasses of Ext.Component, which mixes in
                    // Ext.util.Observable.  In this example, since the first "observable"
                    // mixin's methods win, we also want its reference to be preserved.
                    Ext.applyIf(prototype.mixins, mixinValue);
                }
                /* eslint-disable-next-line max-len */
                else if (!(key === 'mixinId' || key === 'config' || key === '$inheritableStatics') && (prototype[key] === undefined)) {
                    prototype[key] = mixinValue;
                }
            }
            // Mixin statics inheritance
            statics = mixin.$inheritableStatics;
            if (statics) {
                mixinStatics = {};
                for (staticName in statics) {
                    if (!me.hasOwnProperty(staticName)) {
                        mixinStatics[staticName] = mixinClass[staticName];
                    }
                }
                me.addInheritableStatics(mixinStatics);
            }
            if ('config' in mixin) {
                me.addConfig(mixin.config, mixinClass);
            }
            prototype.mixins[name] = mixin;
            if (mixin.afterClassMixedIn) {
                mixin.afterClassMixedIn.call(mixinClass, me);
            }
            return me;
        },
        /**
         * Adds new config properties to this class. This is called for classes when they
         * are declared, then for any mixins that class may define and finally for any
         * overrides defined that target the class.
         *
         * @param {Object} config
         * @param {Ext.Class} [mixinClass] The mixin class if the configs are from a mixin.
         * @private
         * @static
         * @inheritable
         */
        addConfig: function(config, mixinClass) {
            var cfg = this.$config || this.getConfigurator();
            cfg.add(config, mixinClass);
        },
        addCachedConfig: function(config, isMixin) {
            var cached = {},
                key;
            for (key in config) {
                cached[key] = {
                    cached: true,
                    $value: config[key]
                };
            }
            this.addConfig(cached, isMixin);
        },
        /**
         * Returns the `Ext.Configurator` for this class.
         *
         * @return {Ext.Configurator}
         * @private
         * @static
         * @inheritable
         */
        getConfigurator: function() {
            // the Ext.Configurator ctor will set $config so micro-opt out fn call:
            return this.$config || new Ext.Configurator(this);
        },
        /**
         * Get the current class' name in string format.
         *
         *     Ext.define('My.cool.Class', {
         *         constructor: function() {
         *             alert(this.self.getName()); // alerts 'My.cool.Class'
         *         }
         *     });
         *
         *     My.cool.Class.getName(); // 'My.cool.Class'
         *
         * @return {String} className
         * @static
         * @inheritable
         */
        getName: function() {
            return Ext.getClassName(this);
        },
        /**
         * Create aliases for existing prototype methods. Example:
         *
         *     Ext.define('My.cool.Class', {
         *         method1: function() { ... },
         *         method2: function() { ... }
         *     });
         *
         *     var test = new My.cool.Class();
         *
         *     My.cool.Class.createAlias({
         *         method3: 'method1',
         *         method4: 'method2'
         *     });
         *
         *     test.method3(); // test.method1()
         *
         *     My.cool.Class.createAlias('method5', 'method3');
         *
         *     test.method5(); // test.method3() -> test.method1()
         *
         * @param {String/Object} alias The new method name, or an object to set multiple aliases.
         * See {@link Ext.Function#flexSetter flexSetter}
         * @param {String/Object} origin The original method name
         * @static
         * @inheritable
         * @method
         */
        createAlias: flexSetter(function(alias, origin) {
            aliasOneMember[alias] = function() {
                return this[origin].apply(this, arguments);
            };
            this.override(aliasOneMember);
            delete aliasOneMember[alias];
        })
    });
    // Capture the set of static members on Ext.Base that we want to copy to all
    // derived classes. This array is used by Ext.Class as well as the optimizer.
    for (baseStaticMember in Base) {
        if (Base.hasOwnProperty(baseStaticMember)) {
            baseStaticMembers.push(baseStaticMember);
        }
    }
    Base.$staticMembers = baseStaticMembers;
    Base.getConfigurator();
    // lazily create now so as not capture in $staticMembers
    Base.addMembers({
        /** @private */
        $className: 'Ext.Base',
        /**
         * @property {Object/Array} $configTransforms
         * A prototype-chained object storing transform method names and priorities stored
         * on the class prototype. On first instantiation, this object is converted into
         * an array that is sorted by priority and stored on the constructor.
         * @private
         */
        $configTransforms: {},
        /**
         * @property {Boolean} isInstance
         * This value is `true` and is used to identify plain objects from instances of
         * a defined class.
         * @protected
         * @readonly
         */
        isInstance: true,
        /**
         * @property {Boolean} $configPrefixed
         * The value `true` causes `config` values to be stored on instances using a
         * property name prefixed with an underscore ("_") character. A value of `false`
         * stores `config` values as properties using their exact name (no prefix).
         * @private
         * @since 5.0.0
         */
        $configPrefixed: true,
        /**
         * @property {Boolean} $configStrict
         * The value `true` instructs the `initConfig` method to only honor values for
         * properties declared in the `config` block of a class. When `false`, properties
         * that are not declared in a `config` block will be placed on the instance.
         * @private
         * @since 5.0.0
         */
        $configStrict: true,
        /**
         * @property {Boolean} isConfiguring
         * This property is set to `true` during the call to `initConfig`.
         * @protected
         * @readonly
         * @since 5.0.0
         */
        isConfiguring: false,
        /**
         * @property {Boolean} isFirstInstance
         * This property is set to `true` if this instance is the first of its class.
         * @protected
         * @readonly
         * @since 5.0.0
         */
        isFirstInstance: false,
        /**
         * @property {Boolean} destroyed
         * This property is set to `true` after the `destroy` method is called.
         */
        destroyed: false,
        /**
         * @property {Boolean/"async"} [clearPropertiesOnDestroy=true]
         * Setting this property to `false` will prevent nulling object references
         * on a Class instance after destruction. Setting this to `"async"` will delay
         * the clearing for approx 50ms.
         * @protected
         * @since 6.2.0
         */
        clearPropertiesOnDestroy: true,
        /**
         * @property {Boolean} [clearPrototypeOnDestroy=false]
         * Setting this property to `true` will result in setting the object's
         * prototype to `null` after the destruction sequence is fully completed.
         * After that, most attempts at calling methods on the object instance
         * will result in "method not defined" exception. This can be very helpful
         * with tracking down otherwise hard to find bugs like runaway Ajax requests,
         * timed functions not cleared on destruction, etc.
         *
         * Note that this option can only work in browsers that support `Object.setPrototypeOf`
         * method, and is only available in debugging mode.
         * @private
         * @since 6.2.0
         */
        clearPrototypeOnDestroy: false,
        /**
         * Get the reference to the class from which this object was instantiated. Note that unlike
         * {@link Ext.Base#self}, `this.statics()` is scope-independent and it always returns
         * the class from which it was called, regardless of what `this` points to during run-time
         *
         *     Ext.define('My.Cat', {
         *         statics: {
         *             totalCreated: 0,
         *             speciesName: 'Cat' // My.Cat.speciesName = 'Cat'
         *         },
         *
         *         constructor: function() {
         *             var statics = this.statics();
         *
         *             // always equals to 'Cat' no matter what 'this' refers to
         *             // equivalent to: My.Cat.speciesName
         *             alert(statics.speciesName);
         * 
         *
         *             alert(this.self.speciesName);   // dependent on 'this'
         *
         *             statics.totalCreated++;
         *         },
         *
         *         clone: function() {
         *             var cloned = new this.self();   // dependent on 'this'
         *
         *             // equivalent to: My.Cat.speciesName
         *             cloned.groupName = this.statics().speciesName;
         *
         *             return cloned;
         *         }
         *     });
         *
         *
         *     Ext.define('My.SnowLeopard', {
         *         extend: 'My.Cat',
         *
         *         statics: {
         *             speciesName: 'Snow Leopard' // My.SnowLeopard.speciesName = 'Snow Leopard'
         *         },
         *
         *         constructor: function() {
         *             this.callParent();
         *         }
         *     });
         *
         *     var cat = new My.Cat();                 // alerts 'Cat', then alerts 'Cat'
         *
         *     var snowLeopard = new My.SnowLeopard(); // alerts 'Cat', then alerts 'Snow Leopard'
         *
         *     var clone = snowLeopard.clone();
         *     alert(Ext.getClassName(clone));         // alerts 'My.SnowLeopard'
         *     alert(clone.groupName);                 // alerts 'Cat'
         *
         *     alert(My.Cat.totalCreated);             // alerts 3
         *
         * @protected
         * @return {Ext.Class}
         */
        statics: function() {
            var method = this.statics.caller,
                self = this.self;
            if (!method) {
                return self;
            }
            return method.$owner;
        },
        /**
         * Call the "parent" method of the current method. That is the method previously
         * overridden by derivation or by an override (see {@link Ext#define}).
         *
         *      Ext.define('My.Base', {
         *          constructor: function(x) {
         *              this.x = x;
         *          },
         *
         *          statics: {
         *              method: function(x) {
         *                  return x;
         *              }
         *          }
         *      });
         *
         *      Ext.define('My.Derived', {
         *          extend: 'My.Base',
         *
         *          constructor: function() {
         *              this.callParent([21]);
         *          }
         *      });
         *
         *      var obj = new My.Derived();
         *
         *      alert(obj.x);  // alerts 21
         *
         * This can be used with an override as follows:
         *
         *      Ext.define('My.DerivedOverride', {
         *          override: 'My.Derived',
         *
         *          constructor: function(x) {
         *              this.callParent([x*2]); // calls original My.Derived constructor
         *          }
         *      });
         *
         *      var obj = new My.Derived();
         *
         *      alert(obj.x);  // now alerts 42
         *
         * This also works with static and private methods.
         *
         *      Ext.define('My.Derived2', {
         *          extend: 'My.Base',
         *
         *          // privates: {
         *          statics: {
         *              method: function(x) {
         *                  return this.callParent([x*2]); // calls My.Base.method
         *              }
         *          }
         *      });
         *
         *      alert(My.Base.method(10));     // alerts 10
         *      alert(My.Derived2.method(10)); // alerts 20
         *
         * Lastly, it also works with overridden static methods.
         *
         *      Ext.define('My.Derived2Override', {
         *          override: 'My.Derived2',
         *
         *          // privates: {
         *          statics: {
         *              method: function(x) {
         *                  return this.callParent([x*2]); // calls My.Derived2.method
         *              }
         *          }
         *      });
         *
         *      alert(My.Derived2.method(10); // now alerts 40
         *
         * To override a method and replace it and also call the superclass method, use
         * {@link #method-callSuper}. This is often done to patch a method to fix a bug.
         *
         * @protected
         * @param {Array/Arguments} args The arguments, either an array or the `arguments` object
         * from the current method, for example: `this.callParent(arguments)`
         * @return {Object} Returns the result of calling the parent method
         */
        callParent: function(args) {
            // NOTE: this code is deliberately as few expressions (and no function calls)
            // as possible so that a debugger can skip over this noise with the minimum number
            // of steps. Basically, just hit Step Into until you are where you really wanted
            // to be.
            var method,
                superMethod = (method = this.callParent.caller) && (method.$previous || ((method = method.$owner ? method : method.caller) && method.$owner.superclass[method.$name]));
            if (!superMethod) {
                method = this.callParent.caller;
                /* eslint-disable-next-line vars-on-top */
                var parentClass, methodName;
                if (!method.$owner) {
                    if (!method.caller) {
                        throw new Error("Attempting to call a protected method from the " + "public scope, which is not allowed");
                    }
                    method = method.caller;
                }
                parentClass = method.$owner.superclass;
                methodName = method.$name;
                if (!(methodName in parentClass)) {
                    throw new Error("this.callParent() was called but there's no such method (" + methodName + ") found in the parent class (" + (Ext.getClassName(parentClass) || 'Object') + ")");
                }
            }
            return superMethod.apply(this, args || noArgs);
        },
        /**
         * This method is used by an **override** to call the superclass method but
         * bypass any overridden method. This is often done to "patch" a method that
         * contains a bug but for whatever reason cannot be fixed directly.
         *
         * Consider:
         *
         *      Ext.define('Ext.some.Class', {
         *          method: function() {
         *              console.log('Good');
         *          }
         *      });
         *
         *      Ext.define('Ext.some.DerivedClass', {
         *          extend: 'Ext.some.Class',
         *          
         *          method: function() {
         *              console.log('Bad');
         * 
         *              // ... logic but with a bug ...
         *              
         *              this.callParent();
         *          }
         *      });
         *
         * To patch the bug in `Ext.some.DerivedClass.method`, the typical solution is to create an
         * override:
         *
         *      Ext.define('App.patches.DerivedClass', {
         *          override: 'Ext.some.DerivedClass',
         *          
         *          method: function() {
         *              console.log('Fixed');
         * 
         *              // ... logic but with bug fixed ...
         *
         *              this.callSuper();
         *          }
         *      });
         *
         * The patch method cannot use {@link #method-callParent} to call the superclass
         * `method` since that would call the overridden method containing the bug. In
         * other words, the above patch would only produce "Fixed" then "Good" in the
         * console log, whereas, using `callParent` would produce "Fixed" then "Bad"
         * then "Good".
         *
         * @protected
         * @param {Array/Arguments} args The arguments, either an array or the `arguments` object
         * from the current method, for example: `this.callSuper(arguments)`
         * @return {Object} Returns the result of calling the superclass method
         */
        callSuper: function(args) {
            // NOTE: this code is deliberately as few expressions (and no function calls)
            // as possible so that a debugger can skip over this noise with the minimum number
            // of steps. Basically, just hit Step Into until you are where you really wanted
            // to be.
            var method,
                superMethod = (method = this.callSuper.caller) && ((method = method.$owner ? method : method.caller) && method.$owner.superclass[method.$name]);
            if (!superMethod) {
                method = this.callSuper.caller;
                /* eslint-disable-next-line vars-on-top */
                var parentClass, methodName;
                if (!method.$owner) {
                    if (!method.caller) {
                        throw new Error("Attempting to call a protected method from the " + "public scope, which is not allowed");
                    }
                    method = method.caller;
                }
                parentClass = method.$owner.superclass;
                methodName = method.$name;
                if (!(methodName in parentClass)) {
                    throw new Error("this.callSuper() was called but there's no such method (" + methodName + ") found in the parent class (" + (Ext.getClassName(parentClass) || 'Object') + ")");
                }
            }
            return superMethod.apply(this, args || noArgs);
        },
        /**
         * @property {Ext.Class} self
         *
         * Get the reference to the current class from which this object was instantiated. Unlike
         * {@link Ext.Base#statics}, `this.self` is scope-dependent and it's meant to be used
         * for dynamic inheritance. See {@link Ext.Base#statics} for a detailed comparison
         *
         *     Ext.define('My.Cat', {
         *         statics: {
         *             speciesName: 'Cat' // My.Cat.speciesName = 'Cat'
         *         },
         *
         *         constructor: function() {
         *             alert(this.self.speciesName); // dependent on 'this'
         *         },
         *
         *         clone: function() {
         *             return new this.self();
         *         }
         *     });
         *
         *
         *     Ext.define('My.SnowLeopard', {
         *         extend: 'My.Cat',
         *         statics: {
         *             speciesName: 'Snow Leopard' // My.SnowLeopard.speciesName = 'Snow Leopard'
         *         }
         *     });
         *
         *     var cat = new My.Cat();                     // alerts 'Cat'
         *     var snowLeopard = new My.SnowLeopard();     // alerts 'Snow Leopard'
         *
         *     var clone = snowLeopard.clone();
         *     alert(Ext.getClassName(clone));             // alerts 'My.SnowLeopard'
         *
         * @protected
         */
        self: Base,
        // Default constructor, simply returns `this`
        constructor: function() {
            return this;
        },
        /**
         * Initialize configuration for this class. a typical example:
         *
         *     Ext.define('My.awesome.Class', {
         *         // The default config
         *         config: {
         *             name: 'Awesome',
         *             isAwesome: true
         *         },
         *
         *         constructor: function(config) {
         *             this.initConfig(config);
         *         }
         *     });
         *
         *     var awesome = new My.awesome.Class({
         *         name: 'Super Awesome'
         *     });
         *
         *     alert(awesome.getName()); // 'Super Awesome'
         *
         * @protected
         * @param {Object} instanceConfig
         * @return {Ext.Base} this
         * @chainable
         */
        initConfig: function(instanceConfig) {
            var me = this,
                cfg = me.self.getConfigurator();
            me.initConfig = Ext.emptyFn;
            // ignore subsequent calls to initConfig
            me.initialConfig = instanceConfig || {};
            cfg.configure(me, instanceConfig);
            return me;
        },
        beforeInitConfig: Ext.emptyFn,
        /**
         * Returns a specified config property value. If the name parameter is not passed,
         * all current configuration options will be returned as key value pairs.
         * @param {String} [name] The name of the config property to get.
         * @param {Boolean} [peek=false] `true` to peek at the raw value without calling the getter.
         * @param {Boolean} [ifInitialized=false] `true` to only return the initialized property
         * value, not the raw config value, and *not* to trigger initialization. Returns
         * `undefined` if the property has not yet been initialized.
         * @return {Object} The config property value.
         */
        getConfig: function(name, peek, ifInitialized) {
            var me = this,
                ret, cfg, propName;
            if (name) {
                cfg = me.self.$config.configs[name];
                if (cfg) {
                    propName = me.$configPrefixed ? cfg.names.internal : name;
                    // They only want the fully initialized value, not the initial config,
                    //  but only if it's already present on this instance.
                    // They don't want to trigger the initGetter.
                    // This form is used by Bindable#updatePublishes to initially publish
                    // the properties it's being asked make publishable.
                    if (ifInitialized) {
                        ret = me.hasOwnProperty(propName) ? me[propName] : null;
                    } else if (peek) {
                        // Attempt to return the instantiated property on this instance first.
                        // Only return the config object if it has not yet been pulled through
                        // the applier into the instance.
                        ret = me.hasOwnProperty(propName) ? me[propName] : me.config[name];
                    } else {
                        ret = me[cfg.names.get]();
                    }
                } else {
                    ret = me[name];
                }
            } else {
                ret = me.getCurrentConfig();
            }
            return ret;
        },
        /**
         * Destroys member properties by name.
         *
         * If a property name is the name of a *config*, the getter is *not* invoked, so
         * if the config has not been initialized, nothing will be done.
         *
         * The property will be destroyed, and the corrected name (if the property is a *config*
         * and config names are prefixed) will set to `null` in this object's dictionary.
         *
         * @param {String...} args One or more names of the properties to destroy and remove from
         * the object.
         */
        destroyMembers: function() {
            var me = this,
                configs = me.self.$config.configs,
                len = arguments.length,
                cfg, name, value, i;
            for (i = 0; i < len; i++) {
                name = arguments[i];
                cfg = configs[name];
                name = cfg && me.$configPrefixed ? cfg.names.internal : name;
                value = me.hasOwnProperty(name) && me[name];
                if (value) {
                    Ext.destroy(value);
                    me[name] = null;
                }
            }
        },
        freezeConfig: function(name) {
            var me = this,
                config = Ext.Config.get(name),
                names = config.names,
                value = me[names.get]();
            me[names.set] = function(v) {
                if (v !== value) {
                    Ext.raise('Cannot change frozen config "' + name + '"');
                }
                return me;
            };
            if (!Ext.isIE8) {
                Object.defineProperty(me, me.$configPrefixed ? names.internal : name, {
                    get: function() {
                        return value;
                    },
                    set: function(v) {
                        if (v !== value) {
                            Ext.raise('Cannot change frozen config "' + name + '"');
                        }
                    }
                });
            }
        },
        /**
         * Sets a single/multiple configuration options.
         * @param {String/Object} name The name of the property to set, or a set of key value
         * pairs to set.
         * @param {Object} [value] The value to set for the name parameter.
         * @param {Object} [options] (private)
         * @return {Ext.Base} this
         */
        setConfig: function(name, value, options) {
            // options can have the following properties:
            // - defaults `true` to only set the config(s) that have not been already set on
            // this instance.
            // - strict `false` to apply properties to the instance that are not configs,
            // and do not have setters.
            var me = this,
                configurator, config, prop;
            if (name) {
                configurator = me.self.getConfigurator();
                if (typeof name === 'string') {
                    config = configurator.configs[name];
                    if (!config) {
                        if (me.$configStrict) {
                            prop = me.self.prototype[name];
                            if ((typeof prop === 'function') && !prop.$nullFn) {
                                Ext.Error.raise("Cannot override method " + name + " on " + me.$className + " instance.");
                                return me;
                            } else {
                                if (name !== 'type') {
                                    Ext.log.warn('No such config "' + name + '" for class ' + me.$className);
                                }
                            }
                        }
                        config = Ext.Config.map[name] || Ext.Config.get(name);
                    }
                    if (me[config.names.set]) {
                        me[config.names.set](value);
                    } else {
                        // apply non-config props directly to the instance
                        me[name] = value;
                    }
                } else {
                    // This should not have "options ||" except that it shipped in that
                    // broken state, so we use it if present for compat.
                    configurator.reconfigure(me, name, options || value);
                }
            }
            return me;
        },
        getConfigWatcher: function() {
            return this.$configWatch || (this.$configWatch = new Ext.mixin.Watchable());
        },
        /**
         * Watches config properties.
         *
         *      instance.watchConfig({
         *          title: 'onTitleChange',
         *          scope: me
         *      });
         *
         * @private
         * @since 6.7.0
         */
        watchConfig: function(name, fn, scope) {
            var watcher = this.getConfigWatcher();
            return watcher.on.apply(watcher, arguments);
        },
        $configWatch: null,
        /**
         * @private
         */
        getCurrentConfig: function() {
            var cfg = this.self.getConfigurator();
            return cfg.getCurrentConfig(this);
        },
        /**
         * @param {String} name
         * @private
         */
        hasConfig: function(name) {
            return name in this.defaultConfig;
        },
        /**
         * Returns the initial configuration passed to the constructor when
         * instantiating this class.
         *
         * Given this example Ext.button.Button definition and instance:
         *
         *     Ext.define('MyApp.view.Button', {
         *         extend: 'Ext.button.Button',
         *         xtype: 'mybutton',
         *     
         *         scale: 'large',
         *         enableToggle: true
         *     });
         *
         *     var btn = Ext.create({
         *         xtype: 'mybutton',
         *         renderTo: Ext.getBody(),
         *         text: 'Test Button'
         *     });
         *
         * Calling `btn.getInitialConfig()` would return an object including the config
         * options passed to the `create` method:
         *
         *     xtype: 'mybutton',
         *     renderTo: // The document body itself
         *     text: 'Test Button'
         *
         * Calling `btn.getInitialConfig('text')`returns **'Test Button'**.
         *
         * @param {String} [name] Name of the config option to return.
         * @return {Object/Mixed} The full config object or a single config value
         * when `name` parameter specified.
         */
        getInitialConfig: function(name) {
            var config = this.config;
            if (!name) {
                return config;
            }
            return config[name];
        },
        $links: null,
        /**
         * Adds a "destroyable" object to an internal list of objects that will be destroyed
         * when this instance is destroyed (via `{@link #method!destroy}`).
         * @param {String} name
         * @param {Object} value
         * @return {Object} The `value` passed.
         * @private
         */
        link: function(name, value) {
            var me = this,
                links = me.$links || (me.$links = {});
            links[name] = true;
            me[name] = value;
            return value;
        },
        /**
         * Destroys a given set of `{@link #link linked}` objects. This is only needed if
         * the linked object is being destroyed before this instance.
         * @param {String[]} names The names of the linked objects to destroy.
         * @return {Ext.Base} this
         * @private
         */
        unlink: function(names) {
            var me = this,
                i, ln, link, value;
            if (!Ext.isArray(names)) {
                Ext.raise('Invalid argument - expected array of strings');
            }
            for (i = 0 , ln = names.length; i < ln; i++) {
                link = names[i];
                value = me[link];
                if (value) {
                    if (value.isInstance && !value.destroyed) {
                        value.destroy();
                    } else if (value.parentNode && 'nodeType' in value) {
                        value.parentNode.removeChild(value);
                    }
                }
                me[link] = null;
            }
            return me;
        },
        $reap: function() {
            var me = this,
                keepers = me.$noClearOnDestroy,
                props, prop, val, t, i, len;
            // This only returns own keys which is *much* faster than iterating
            // over the whole prototype chain and calling hasOwnProperty()
            props = Ext.Object.getKeys(me);
            for (i = 0 , len = props.length; i < len; i++) {
                prop = props[i];
                val = me[prop];
                // typeof null === 'object' :(
                if (val && !(keepers && keepers[prop])) {
                    t = typeof val;
                    // Object may retain references to other objects. Functions can do too
                    // if they are closures, and most of the *own* function properties
                    // are closures indeed. We skip Ext.emptyFn and the like though,
                    // they're mostly harmless.
                    if (t === 'object' || (t === 'function' && !val.$noClearOnDestroy)) {
                        me[prop] = null;
                    }
                }
            }
            me.$nulled = true;
            // We also want to make sure no methods are called on the destroyed object,
            // because that may lead to accessing nulled properties and resulting exceptions.
            if (Object.setPrototypeOf) {
                if (me.clearPrototypeOnDestroy && !me.$vetoClearingPrototypeOnDestroy) {
                    props = me.$preservePrototypeProperties;
                    if (props) {
                        for (i = 0 , len = props.length; i < len; i++) {
                            prop = props[i];
                            if (!me.hasOwnProperty(prop)) {
                                /* eslint-disable-next-line no-self-assign */
                                me[prop] = me[prop];
                            }
                        }
                    }
                    Object.setPrototypeOf(me, null);
                }
            }
        },
        /**
         * This method is called to cleanup an object and its resources. After calling
         * this method, the object should not be used any further in any way, including
         * access to its methods and properties.
         *
         * To prevent potential memory leaks, all object references will be nulled
         * at the end of destruction sequence, unless {@link #clearPropertiesOnDestroy}
         * is set to `false`.
         */
        destroy: function() {
            var me = this,
                links = me.$links,
                clearPropertiesOnDestroy = me.clearPropertiesOnDestroy;
            if (links) {
                me.$links = null;
                me.unlink(Ext.Object.getKeys(links));
            }
            me.destroy = Ext.emptyFn;
            // isDestroyed added for compat reasons
            me.isDestroyed = me.destroyed = true;
            // By this time the destruction is complete. Now we can make sure
            // no objects are retained by the husk of this ex-Instance.
            if (clearPropertiesOnDestroy === true) {
                // Observable mixin will call destroyObservable that will reap the properties.
                if (!me.isObservable) {
                    me.$reap();
                }
            } else if (clearPropertiesOnDestroy) {
                if (clearPropertiesOnDestroy !== 'async') {
                    Ext.raise('Invalid value for clearPropertiesOnDestroy');
                }
                Reaper.add(me);
            }
        }
    });
    /**
     * @method callOverridden
     * Call the original method that was previously overridden with {@link Ext.Base#override}
     *
     *     Ext.define('My.Cat', {
     *         constructor: function() {
     *             alert("I'm a cat!");
     *         }
     *     });
     *
     *     My.Cat.override({
     *         constructor: function() {
     *             alert("I'm going to be a cat!");
     *
     *             this.callOverridden();
     *
     *             alert("Meeeeoooowwww");
     *         }
     *     });
     *
     *     var kitty = new My.Cat(); // alerts "I'm going to be a cat!"
     *                               // alerts "I'm a cat!"
     *                               // alerts "Meeeeoooowwww"
     *
     * @param {Array/Arguments} args The arguments, either an array or the `arguments` object
     * from the current method, for example: `this.callOverridden(arguments)`
     * @return {Object} Returns the result of calling the overridden method
     * @deprecated 4.1.0 Use {@link #method-callParent} instead.
     * @protected
     */
    BasePrototype.callOverridden = BasePrototype.callParent;
    Ext.privacyViolation = function(cls, existing, member, isStatic) {
        var name = member.$name,
            conflictCls = existing.$owner && existing.$owner.$className,
            s = isStatic ? 'static ' : '',
            msg = member.$privacy ? 'Private ' + s + member.$privacy + ' method "' + name + '"' : 'Public ' + s + 'method "' + name + '"';
        if (cls.$className) {
            msg = cls.$className + ': ' + msg;
        }
        if (!existing.$privacy) {
            msg += conflictCls ? ' hides public method inherited from ' + conflictCls : ' hides inherited public method.';
        } else {
            msg += conflictCls ? ' conflicts with private ' + existing.$privacy + ' method declared by ' + conflictCls : ' conflicts with inherited private ' + existing.$privacy + ' method.';
        }
        /* eslint-disable-next-line vars-on-top */
        var compat = Ext.getCompatVersion(),
            ver = Ext.getVersion();
        // When compatibility is enabled, log problems instead of throwing errors.
        if (ver && compat && compat.lt(ver)) {
            Ext.log.error(msg);
        } else {
            Ext.raise(msg);
        }
    };
    Ext.Reaper.tick.$skipTimerCheck = true;
    return Base;
}(Ext.Function.flexSetter));

/**
 * This class manages a double-linked list. It provides an absolutely minimal container
 * interface.
 *
 * @class Ext.util.LRU
 * @private
 * @since 6.5.0
 */
/* eslint-disable indent */
(function(LRU, prototype) {
    // @define Ext.util.LRU
    // NOTE: We have to implement this class old-school because it is used by the
    // platformConfig class processor (so Ext.define is not yet ready for action).
    (Ext.util || (Ext.util = {})).LRU = LRU = function(config) {
        var me = this,
            head;
        if (config) {
            Ext.apply(me, config);
        }
        // Give all entries the same object shape.
        me.head = head = {
            id: (me.seed = 0),
            key: null,
            value: null
        };
        /**
         * @property {Object} map
         * The items in the list indexed by their `key`.
         * @readonly
         */
        me.map = {};
        head.next = head.prev = head;
    };
    LRU.prototype = prototype = {
        /**
         * @property {Number} count
         * The number of items in this list.
         * @readonly
         */
        count: 0,
        /**
         * Adds an item to the list with the specified `key`. Items are added at the
         * front (MRU) of the list.
         * @param {String} key
         * @param {Object} value
         */
        add: function(key, value) {
            var me = this,
                map = me.map,
                entry = map[key];
            if (entry) {
                me.unlink(entry);
                --me.count;
            }
            map[key] = entry = {
                id: ++me.seed,
                key: key,
                value: value
            };
            me.link(entry);
            ++me.count;
            return entry;
        },
        /**
         * Removes all items from this list optionally calling a function for each
         * remove item.
         * @param {Function} [fn] A function to call for each removed item.
         * @param {Object} fn.key The key of the removed item.
         * @param {Object} fn.value The removed item.
         * @param {Object} [scope] The `this` pointer for `fn`.
         */
        clear: function(fn, scope) {
            var me = this,
                head = me.head,
                entry = head.next;
            head.next = head.prev = head;
            me.count = 0;
            if (fn && !fn.$nullFn) {
                for (; entry !== head; entry = entry.next) {
                    fn.call(scope || me, entry.key, entry.value);
                }
            }
        },
        /**
         * Calls the given function `fn` for each item in the list. The items will be
         * passed to `fn` from most-to-least recently added or touched.
         * @param {Function} fn The function to call for each cache item.
         * @param {String} fn.key The key for the item.
         * @param {Object} fn.value The item.
         * @param {Object} [scope] The `this` pointer to use for `fn`.
         */
        each: function(fn, scope) {
            var head, ent;
            scope = scope || this;
            for (head = this.head , ent = head.next; ent !== head; ent = ent.next) {
                if (fn.call(scope, ent.key, ent.value)) {
                    break;
                }
            }
        },
        /**
         * Removes the item at the end (LRU) of the list. Optionally the item can be passed
         * to a callback function. If the list is empty, no callback is made and this
         * method will return `undefined`.
         * @param {Function} fn The function to call for the removed item.
         * @param {Object} fn.key The key of the removed item.
         * @param {Object} fn.value The removed item.
         * @param {Object} [scope] The `this` pointer to use for `fn`.
         * @return {Object} The removed item.
         */
        prune: function(fn, scope) {
            var me = this,
                entry = me.head.prev,
                ret;
            if (me.count) {
                ret = entry.value;
                me.unlink(entry);
                --me.count;
                if (fn) {
                    fn.call(scope || me, entry.key, ret);
                }
            }
            return ret;
        },
        /**
         * Removes an item from the list given its key.
         * @param {String} key The key of the item to remove.
         * @return {Object} The removed item or `undefined` if the key was not found.
         */
        remove: function(key) {
            var me = this,
                map = me.map,
                entry = map[key],
                value;
            if (entry) {
                me.unlink(entry);
                value = entry.value;
                delete map[key];
                --me.count;
            }
            return value;
        },
        /**
         * Moves the item with the given key to the front (MRU) of the list.
         * @param {String} key The key of the item to move to the front.
         */
        touch: function(key) {
            var me = this,
                head = me.head,
                entry = me.map[key];
            if (entry && entry.prev !== head) {
                // The entry is not at the front, so remove it and insert it at the front
                // (to make it the MRU - Most Recently Used).
                me.unlink(entry);
                me.link(entry);
            }
        },
        /**
         * Reduces the length of the list to be no more than the specified `size`, removing
         * items from the end of the list as necessary. Optionally each removed item can
         * be passed to a callback `fn`.
         * @param {Number} size The number of items in the list
         * @param {Function} [fn] A function to call for each removed item.
         * @param {Object} fn.key The key of the removed item.
         * @param {Object} fn.value The removed item.
         * @param {Object} [scope] The `this` pointer for `fn`.
         */
        trim: function(size, fn, scope) {
            while (this.count > size) {
                this.prune(fn, scope);
            }
        },
        //-------------------------------------------------------------------------
        // Internals
        /**
         * Inserts the given entry at the front (MRU) end of the entry list.
         * @param {Object} entry The cache item entry.
         * @private
         */
        link: function(entry) {
            var head = this.head,
                first = head.next;
            entry.next = first;
            entry.prev = head;
            head.next = entry;
            first.prev = entry;
        },
        /**
         * Removes the given entry from the entry list.
         * @param {Object} entry The cache item entry.
         * @private
         */
        unlink: function(entry) {
            var next = entry.next,
                prev = entry.prev;
            prev.next = next;
            next.prev = prev;
        }
    };
    prototype.destroy = function() {
        this.clear.apply(this, arguments);
    };
}());

/**
 * This class is used to manage simple, LRU caches. It provides an absolutely minimal
 * container interface. It is created like this:
 *
 *      this.itemCache = new Ext.util.Cache({
 *          miss: function (key) {
 *              return new CacheItem(key);
 *          }
 *      });
 *
 * The `{@link #miss}` abstract method must be implemented by either a derived class or
 * at the instance level as shown above.
 *
 * Once the cache exists and it can handle cache misses, the cache is used like so:
 *
 *      var item = this.itemCache.get(key);
 *
 * The `key` is some value that uniquely identifies the cached item.
 *
 * In some cases, creating the cache item may require more than just the lookup key. In
 * that case, any extra arguments passed to `get` will be passed to `miss`.
 *
 *      this.otherCache = new Ext.util.Cache({
 *          miss: function (key, extra) {
 *              return new CacheItem(key, extra);
 *          }
 *      });
 *
 *      var item = this.otherCache.get(key, extra);
 *
 * To process items as they are removed, you can provide an `{@link #evict}` method. The
 * stock method is `Ext.emptyFn` and so does nothing.
 *
 * For example:
 *
 *      this.itemCache = new Ext.util.Cache({
 *          miss: function (key) {
 *              return new CacheItem(key);
 *          },
 *
 *          evict: function (key, cacheItem) {
 *              cacheItem.destroy();
 *          }
 *      });
 *
 * @class Ext.util.Cache
 * @private
 * @since 5.1.0
 */
/* eslint-disable indent */
(function(LRU, fn, Cache) {
    // @require Ext.util.LRU
    // @define Ext.util.Cache
    // NOTE: We have to implement this class old-school because it is used by the
    // platformConfig class processor (so Ext.define is not yet ready for action).
    Ext.util.Cache = Cache = function(config) {
        LRU.call(this, config);
    };
    fn.prototype = LRU.prototype;
    Cache.prototype = Ext.apply(new fn(), {
        /**
         * @cfg {Number} maxSize The maximum size the cache is allowed to grow to before
         * further additions cause removal of the least recently used entry.
         */
        maxSize: 100,
        /**
         * This method is called by `{@link #get}` when the key is not found in the cache.
         * The implementation of this method should create the (expensive) value and return
         * it. Whatever arguments were passed to `{@link #get}` will be passed on to this
         * method.
         *
         * @param {String} key The cache lookup key for the item.
         * @param {Object...} args Any other arguments originally passed to `{@link #get}`.
         * @method miss
         * @abstract
         * @protected
         */
        /**
         * Removes all items from this cache.
         */
        clear: function() {
            LRU.prototype.clear.call(this, this.evict);
        },
        /**
         * Finds an item in this cache and returns its value. If the item is present, it is
         * shuffled into the MRU (most-recently-used) position as necessary. If the item is
         * missing, the `{@link #miss}` method is called to create the item.
         *
         * @param {String} key The cache key of the item.
         * @param {Object...} args Arguments for the `miss` method should it be needed.
         * @return {Object} The cached object.
         */
        get: function(key) {
            var me = this,
                entry = me.map[key],
                value;
            if (entry) {
                value = entry.value;
                me.touch(key);
            } else {
                value = me.miss.apply(me, arguments);
                me.add(key, value);
                me.trim(me.maxSize, me.evict);
            }
            return value;
        },
        //-------------------------------------------------------------------------
        // Internals
        /**
         * This method is called internally from `{@link #get}` when the cache is full and
         * the least-recently-used (LRU) item has been removed.
         *
         * @param {String} key The cache lookup key for the item being removed.
         * @param {Object} value The cache value (returned by `{@link #miss}`) for the item
         * being removed.
         * @method evict
         * @template
         * @protected
         */
        evict: Ext.emptyFn
    });
}(Ext.util.LRU, function() {}));

/**
 * @class Ext.Class
 *
 * This is a low level factory that is used by {@link Ext#define Ext.define} and should not be used
 * directly in application code.
 * 
 * The configs of this class are intended to be used in `Ext.define` calls to describe the class you
 * are declaring. For example:
 * 
 *     Ext.define('App.util.Thing', {
 *         extend: 'App.util.Other',
 * 
 *         alias: 'util.thing',
 * 
 *         config: {
 *             foo: 42
 *         }
 *     });
 *
 * Ext.Class is the factory and **not** the superclass of everything. For the base class
 * that **all** classes inherit from, see {@link Ext.Base}.
 */
/* eslint-disable indent */
(function() {
    // @tag class
    // @define Ext.Class
    // @require Ext.Base
    // @require Ext.Util
    // @require Ext.util.Cache
    var ExtClass,
        Base = Ext.Base,
        baseStaticMembers = Base.$staticMembers,
        ruleKeySortFn = function(a, b) {
            // longest to shortest, by text if names are equal
            return (a.length - b.length) || ((a < b) ? -1 : ((a > b) ? 1 : 0));
        };
    // Creates a constructor that has nothing extra in its scope chain.
    function makeCtor(className) {
        function constructor() {
            // Opera has some problems returning from a constructor when Dragonfly isn't running.
            // The || null seems to be sufficient to stop it misbehaving. Known to be required
            // against 10.53, 11.51 and 11.61.
            return this.constructor.apply(this, arguments) || null;
        }
        if (className) {
            constructor.name = className;
        }
        return constructor;
    }
    /**
     * @method constructor
     * Create a new anonymous class.
     *
     * @param Class
     * @param {Object} data An object represent the properties of this class
     * @param {Function} onCreated Optional, the callback function to be executed when this class
     * is fully created. Note that the creation process can be asynchronous depending
     * on the pre-processors used.
     *
     * @return {Ext.Base} The newly created class
     */
    Ext.Class = ExtClass = function(Class, data, onCreated) {
        if (typeof Class !== 'function') {
            onCreated = data;
            data = Class;
            Class = null;
        }
        if (!data) {
            data = {};
        }
        Class = ExtClass.create(Class, data);
        ExtClass.process(Class, data, onCreated);
        return Class;
    };
    Ext.apply(ExtClass, {
        makeCtor: makeCtor,
        /**
         * @private
         */
        onBeforeCreated: function(Class, data, hooks) {
            if (Ext.classSystemMonitor) {
                Ext.classSystemMonitor(Class, '>> Ext.Class#onBeforeCreated', arguments);
            }
            Class.addMembers(data);
            hooks.onCreated.call(Class, Class);
            if (Ext.classSystemMonitor) {
                Ext.classSystemMonitor(Class, '<< Ext.Class#onBeforeCreated', arguments);
            }
        },
        /**
         * @private
         */
        create: function(Class, data) {
            var i = baseStaticMembers.length,
                name;
            if (!Class) {
                Class = makeCtor(data.$className);
            }
            while (i--) {
                name = baseStaticMembers[i];
                Class[name] = Base[name];
            }
            return Class;
        },
        /**
         * @private
         */
        process: function(Class, data, onCreated) {
            var preprocessorStack = data.preprocessors || ExtClass.defaultPreprocessors,
                registeredPreprocessors = this.preprocessors,
                hooks = {
                    onBeforeCreated: this.onBeforeCreated
                },
                preprocessors = [],
                preprocessor, preprocessorsProperties, i, ln, j, subLn, preprocessorProperty;
            delete data.preprocessors;
            Class._classHooks = hooks;
            for (i = 0 , ln = preprocessorStack.length; i < ln; i++) {
                preprocessor = preprocessorStack[i];
                if (typeof preprocessor === 'string') {
                    preprocessor = registeredPreprocessors[preprocessor];
                    preprocessorsProperties = preprocessor.properties;
                    if (preprocessorsProperties === true) {
                        preprocessors.push(preprocessor.fn);
                    } else if (preprocessorsProperties) {
                        for (j = 0 , subLn = preprocessorsProperties.length; j < subLn; j++) {
                            preprocessorProperty = preprocessorsProperties[j];
                            if (data.hasOwnProperty(preprocessorProperty)) {
                                preprocessors.push(preprocessor.fn);
                                break;
                            }
                        }
                    }
                } else {
                    preprocessors.push(preprocessor);
                }
            }
            hooks.onCreated = onCreated ? onCreated : Ext.emptyFn;
            hooks.preprocessors = preprocessors;
            this.doProcess(Class, data, hooks);
        },
        doProcess: function(Class, data, hooks) {
            var me = this,
                preprocessors = hooks.preprocessors,
                preprocessor = preprocessors.shift(),
                doProcess = me.doProcess;
            for (; preprocessor; preprocessor = preprocessors.shift()) {
                // Returning false signifies an asynchronous preprocessor - it will call doProcess
                // when we can continue
                if (preprocessor.call(me, Class, data, hooks, doProcess) === false) {
                    return;
                }
            }
            hooks.onBeforeCreated.apply(me, arguments);
        },
        /**
         * @private
         * */
        preprocessors: {},
        /**
         * Register a new pre-processor to be used during the class creation process
         *
         * @param {String} name The pre-processor's name
         * @param {Function} fn The callback function to be executed. Typical format:
         *
         *     function(cls, data, fn) {
         *         // Your code here
         *
         *         // Execute this when the processing is finished.
         *         // Asynchronous processing is perfectly ok
         *         if (fn) {
         *             fn.call(this, cls, data);
         *         }
         *     });
         *
         * @param {Function} fn.cls The created class
         * @param {Object} fn.data The set of properties passed in {@link Ext.Class} constructor
         * @param {Function} fn.fn The callback function that **must** to be executed when this
         * pre-processor finishes, regardless of whether the processing is synchronous or
         * asynchronous.
         * @param properties
         * @param position
         * @param relativeTo
         * @return {Ext.Class} this
         * @private
         * @static
         */
        registerPreprocessor: function(name, fn, properties, position, relativeTo) {
            if (!position) {
                position = 'last';
            }
            if (!properties) {
                properties = [
                    name
                ];
            }
            this.preprocessors[name] = {
                name: name,
                properties: properties || false,
                fn: fn
            };
            this.setDefaultPreprocessorPosition(name, position, relativeTo);
            return this;
        },
        /**
         * Retrieve a pre-processor callback function by its name, which has been registered before
         *
         * @param {String} name
         * @return {Function} preprocessor
         * @private
         * @static
         */
        getPreprocessor: function(name) {
            return this.preprocessors[name];
        },
        /**
         * @private
         */
        getPreprocessors: function() {
            return this.preprocessors;
        },
        /**
         * @private
         */
        defaultPreprocessors: [],
        /**
         * Retrieve the array stack of default pre-processors
         * @return {Function[]} defaultPreprocessors
         * @private
         * @static
         */
        getDefaultPreprocessors: function() {
            return this.defaultPreprocessors;
        },
        /**
         * Set the default array stack of default pre-processors
         *
         * @private
         * @param {Array} preprocessors
         * @return {Ext.Class} this
         * @static
         */
        setDefaultPreprocessors: function(preprocessors) {
            this.defaultPreprocessors = Ext.Array.from(preprocessors);
            return this;
        },
        /**
         * Insert this pre-processor at a specific position in the stack, optionally relative to
         * any existing pre-processor. For example:
         *
         *     Ext.Class.registerPreprocessor('debug', function(cls, data, fn) {
         *         // Your code here
         *
         *         if (fn) {
         *             fn.call(this, cls, data);
         *         }
         *     }).setDefaultPreprocessorPosition('debug', 'last');
         *
         * @private
         * @param {String} name The pre-processor name. Note that it needs to be registered with
         * {@link Ext.Class#registerPreprocessor registerPreprocessor} before this
         * @param {String} offset The insertion position. Four possible values are:
         * 'first', 'last', or: 'before', 'after' (relative to the name provided in the third
         * argument)
         * @param {String} relativeName
         * @return {Ext.Class} this
         * @static
         */
        setDefaultPreprocessorPosition: function(name, offset, relativeName) {
            var defaultPreprocessors = this.defaultPreprocessors,
                index;
            if (typeof offset === 'string') {
                if (offset === 'first') {
                    defaultPreprocessors.unshift(name);
                    return this;
                } else if (offset === 'last') {
                    defaultPreprocessors.push(name);
                    return this;
                }
                offset = (offset === 'after') ? 1 : -1;
            }
            index = Ext.Array.indexOf(defaultPreprocessors, relativeName);
            if (index !== -1) {
                Ext.Array.splice(defaultPreprocessors, Math.max(0, index + offset), 0, name);
            }
            return this;
        }
    });
    /**
     * @cfg {String} extend
     * The parent class that this class extends. For example:
     *
     *     Ext.define('Person', {
     *         say: function(text) { alert(text); }
     *     });
     *
     *     Ext.define('Developer', {
     *         extend: 'Person',
     *         say: function(text) { this.callParent(["print "+text]); }
     *     });
     */
    ExtClass.registerPreprocessor('extend', function(Class, data, hooks) {
        var Base = Ext.Base,
            basePrototype = Base.prototype,
            extend = data.extend,
            Parent, parentPrototype, i;
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(Class, 'Ext.Class#extendPreProcessor', arguments);
        }
        delete data.extend;
        if (extend && extend !== Object) {
            Parent = extend;
        } else {
            Parent = Base;
        }
        parentPrototype = Parent.prototype;
        if (!Parent.$isClass) {
            for (i in basePrototype) {
                if (!parentPrototype[i]) {
                    parentPrototype[i] = basePrototype[i];
                }
            }
        }
        Class.extend(Parent);
        Class.triggerExtended.apply(Class, arguments);
        /**
         * @cfg {Object} eventedConfig
         * Config options defined within `eventedConfig` will auto-generate the setter /
         * getter methods (see {@link #cfg-config config} for more information on
         * auto-generated getter / setter methods).  Additionally, when an
         * `eventedConfig` is set it will also fire a before{cfg}change and {cfg}change
         * event when the value of the eventedConfig is changed from its originally
         * defined value.
         *
         * **Note:** When creating a custom class you'll need to extend Ext.Evented
         *
         * Example custom class:
         *
         *     Ext.define('MyApp.util.Test', {
         *         extend: 'Ext.Evented',
         *
         *         eventedConfig: {
         *             foo: null
         *         }
         *     });
         *
         * In this example, the `foo` config will initially be null.  Changing it via
         * `setFoo` will fire the `beforefoochange` event.  The call to the setter can be
         * halted by returning `false` from a listener on the **before** event.
         *
         *     var test = Ext.create('MyApp.util.Test', {
         *         listeners: {
         *             beforefoochange: function (instance, newValue, oldValue) {
         *                 return newValue !== 'bar';
         *             },
         *             foochange: function (instance, newValue, oldValue) {
         *                console.log('foo changed to:', newValue);
         *             }
         *         }
         *     });
         *
         *     test.setFoo('bar');
         *
         * The `before` event handler can be used to validate changes to `foo`.
         * Returning `false` will prevent the setter from changing the value of the
         * config.  In the previous example the `beforefoochange` handler returns false
         * so `foo` will not be updated and `foochange` will not be fired.
         *
         *     test.setFoo('baz');
         *
         * Setting `foo` to 'baz' will not be prevented by the `before` handler.  Foo
         * will be set to the value: 'baz' and the `foochange` event will be fired.
         */
        if (data.onClassExtended) {
            Class.onExtended(data.onClassExtended, Class);
            delete data.onClassExtended;
        }
    }, true);
    // true to always run this preprocessor even w/o "extend" keyword
    /**
     * @cfg {Object} privates
     * The `privates` config is a list of methods intended to be used internally by the 
     * framework.  Methods are placed in a `privates` block to prevent developers from 
     * accidentally overriding framework methods in custom classes.
     *
     *     Ext.define('Computer', {
     *         privates: {
     *             runFactory: function(brand) {
     *                 // internal only processing of brand passed to factory
     *                 this.factory(brand);
     *             }
     *         },
     *     
     *         factory: function (brand) {}
     *     });
     * 
     * In order to override a method from a `privates` block, the overridden method must 
     * also be placed in a `privates` block within the override class.
     * 
     *     Ext.define('Override.Computer', {
     *         override: 'Computer',
     *         privates: {
     *             runFactory: function() {
     *                 // overriding logic
     *             }
     *         }
     *     });
     */
    ExtClass.registerPreprocessor('privates', function(Class, data) {
        var privates = data.privates,
            statics = privates.statics,
            privacy = privates.privacy || true;
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(Class, 'Ext.Class#privatePreprocessor', arguments);
        }
        delete data.privates;
        delete privates.statics;
        // We have to add this preprocessor so that private getters/setters are picked up
        // by the config system. This also catches duplication in the public part of the
        // class since it is an error to override a private method with a public one.
        Class.addMembers(privates, false, privacy);
        if (statics) {
            Class.addMembers(statics, true, privacy);
        }
    });
    /**
     * @cfg {Object} statics
     * List of static methods for this class. For example:
     *
     *     Ext.define('Computer', {
     *          statics: {
     *              factory: function(brand) {
     *                  // 'this' in static methods refer to the class itself
     *                  return new this(brand);
     *              }
     *          },
     *
     *          constructor: function() { ... }
     *     });
     *
     *     var dellComputer = Computer.factory('Dell');
     */
    ExtClass.registerPreprocessor('statics', function(Class, data) {
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(Class, 'Ext.Class#staticsPreprocessor', arguments);
        }
        Class.addStatics(data.statics);
        delete data.statics;
    });
    /**
     * @cfg {Object} inheritableStatics
     * List of inheritable static methods for this class.
     * Otherwise just like {@link #statics} but subclasses inherit these methods.
     */
    ExtClass.registerPreprocessor('inheritableStatics', function(Class, data) {
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(Class, 'Ext.Class#inheritableStaticsPreprocessor', arguments);
        }
        Class.addInheritableStatics(data.inheritableStatics);
        delete data.inheritableStatics;
    });
    Ext.createRuleFn = function(code) {
        return new Function('$c', 'with($c) { try { return (' + code + '); } catch(e) { return false;}}');
    };
    Ext.expressionCache = new Ext.util.Cache({
        miss: Ext.createRuleFn
    });
    Ext.ruleKeySortFn = ruleKeySortFn;
    Ext.getPlatformConfigKeys = function(platformConfig) {
        var ret = [],
            platform, rule;
        for (platform in platformConfig) {
            rule = Ext.expressionCache.get(platform);
            if (rule(Ext.platformTags)) {
                ret.push(platform);
            }
        }
        ret.sort(ruleKeySortFn);
        return ret;
    };
    /**
     * @cfg {Object} config
     *
     * List of configuration options with their default values.
     *
     * __Note:__ You need to make sure {@link Ext.Base#initConfig} is called from your constructor
     * if you are defining your own class or singleton, unless you are extending a Component.
     * Otherwise the generated getter and setter methods will not be initialized.
     *
     * Each config item will have its own setter and getter method automatically generated inside
     * the class prototype during class creation time, if the class does not have those methods
     * explicitly defined.
     *
     * As an example, let's convert the name property of a Person class to be a config item, then
     * add extra age and gender items.
     *
     *     Ext.define('My.sample.Person', {
     *         config: {
     *             name: 'Mr. Unknown',
     *             age: 0,
     *             gender: 'Male'
     *         },
     *
     *         constructor: function(config) {
     *             this.initConfig(config);
     *
     *             return this;
     *         }
     *
     *         // ...
     *     });
     *
     * Within the class, this.name still has the default value of "Mr. Unknown". However, it's now
     * publicly accessible without sacrificing encapsulation, via setter and getter methods.
     *
     *     var jacky = new My.sample.Person({
     *         name: "Jacky",
     *         age: 35
     *     });
     *
     *     alert(jacky.getAge());      // alerts 35
     *     alert(jacky.getGender());   // alerts "Male"
     *
     *     jacky.setName("Mr. Nguyen");
     *     alert(jacky.getName());     // alerts "Mr. Nguyen"
     *
     * Notice that we changed the class constructor to invoke this.initConfig() and pass in the
     * provided config object. Two key things happened:
     *
     *  - The provided config object when the class is instantiated is recursively merged with
     * the default config object.
     *  - All corresponding setter methods are called with the merged values.
     *
     * Beside storing the given values, throughout the frameworks, setters generally have two key
     * responsibilities:
     *
     *  - Filtering / validation / transformation of the given value before it's actually stored
     * within the instance.
     *  - Notification (such as firing events) / post-processing after the value has been set,
     * or changed from a previous value.
     *
     * By standardize this common pattern, the default generated setters provide two extra template
     * methods that you can put your own custom logic into, i.e: an "applyFoo" and "updateFoo"
     * method for a "foo" config item, which are executed before and after the value is actually
     * set, respectively. Back to the example class, let's validate that age must be a valid
     * positive number, and fire an 'agechange' if the value is modified.
     *
     *     Ext.define('My.sample.Person', {
     *         config: {
     *             // ...
     *         },
     *
     *         constructor: {
     *             // ...
     *         },
     *
     *         applyAge: function(age) {
     *             if (typeof age !== 'number' || age < 0) {
     *                 console.warn("Invalid age, must be a positive number");
     *                 return;
     *             }
     *
     *             return age;
     *         },
     *
     *         updateAge: function(newAge, oldAge) {
     *             // age has changed from "oldAge" to "newAge"
     *             this.fireEvent('agechange', this, newAge, oldAge);
     *         }
     *
     *         // ...
     *     });
     *
     *     var jacky = new My.sample.Person({
     *         name: "Jacky",
     *         age: 'invalid'
     *     });
     *
     *     alert(jacky.getAge());      // alerts 0
     *
     *     alert(jacky.setAge(-100));  // alerts 0
     *     alert(jacky.getAge());      // alerts 0
     *
     *     alert(jacky.setAge(35));    // alerts 0
     *     alert(jacky.getAge());      // alerts 35
     *
     * In other words, when leveraging the config feature, you mostly never need to define setter
     * and getter methods explicitly. Instead, "apply*" and "update*" methods should be implemented
     * where necessary. Your code will be consistent throughout and only contain the minimal logic
     * that you actually care about.
     *
     * When it comes to inheritance, the default config of the parent class is automatically,
     * recursively merged with the child's default config. The same applies for mixins.
     */
    ExtClass.registerPreprocessor('config', function(Class, data) {
        // Need to copy to the prototype here because that happens after preprocessors
        if (data.hasOwnProperty('$configPrefixed')) {
            Class.prototype.$configPrefixed = data.$configPrefixed;
        }
        Class.addConfig(data.config);
        // We need to remove this or it will be applied by addMembers and smash the
        // "config" placed on the prototype by Configurator (which includes *all* configs
        // such as cachedConfigs).
        delete data.config;
    });
    /**
     * @cfg {Object} cachedConfig
     * 
     * This configuration works in a very similar manner to the {@link #config} option.
     * The difference is that the configurations are only ever processed when the first instance
     * of that class is created. The processed value is then stored on the class prototype and
     * will not be processed on subsequent instances of the class. Getters/setters will be generated
     * in exactly the same way as {@link #config}.
     * 
     * This option is useful for expensive objects that can be shared across class instances. 
     * The class itself ensures that the creation only occurs once.
     */
    ExtClass.registerPreprocessor('cachedConfig', function(Class, data) {
        // Need to copy to the prototype here because that happens after preprocessors
        if (data.hasOwnProperty('$configPrefixed')) {
            Class.prototype.$configPrefixed = data.$configPrefixed;
        }
        Class.addCachedConfig(data.cachedConfig);
        // Remove this so it won't be placed on the prototype.
        delete data.cachedConfig;
    });
    /**
     * @cfg {String[]/Object} mixins
     * List of classes to mix into this class. For example:
     *
     *     Ext.define('CanSing', {
     *          sing: function() {
     *              alert("For he's a jolly good fellow...")
     *          }
     *     });
     *
     *     Ext.define('Musician', {
     *          mixins: ['CanSing']
     *     })
     *
     * In this case the Musician class will get a `sing` method from CanSing mixin.
     *
     * But what if the Musician already has a `sing` method? Or you want to mix
     * in two classes, both of which define `sing`?  In such a cases it's good
     * to define mixins as an object, where you assign a name to each mixin:
     *
     *     Ext.define('Musician', {
     *          mixins: {
     *              canSing: 'CanSing'
     *          },
     * 
     *          sing: function() {
     *              // delegate singing operation to mixin
     *              this.mixins.canSing.sing.call(this);
     *          }
     *     })
     *
     * In this case the `sing` method of Musician will overwrite the
     * mixed in `sing` method. But you can access the original mixed in method
     * through special `mixins` property.
     */
    ExtClass.registerPreprocessor('mixins', function(Class, data, hooks) {
        var mixins = data.mixins,
            onCreated = hooks.onCreated;
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(Class, 'Ext.Class#mixinsPreprocessor', arguments);
        }
        delete data.mixins;
        hooks.onCreated = function() {
            /* eslint-disable-next-line max-len */
            if (Ext.classSystemMonitor) {
                Ext.classSystemMonitor(Class, 'Ext.Class#mixinsPreprocessor#beforeCreated', arguments);
            }
            // Put back the original onCreated before processing mixins. This allows a
            // mixin to hook onCreated by access Class._classHooks.
            hooks.onCreated = onCreated;
            Class.mixin(mixins);
            // We must go back to hooks.onCreated here because it may have changed during
            // calls to onClassMixedIn.
            return hooks.onCreated.apply(this, arguments);
        };
    });
    // Backwards compatible
    Ext.extend = function(Class, Parent, members) {
        var cls, m;
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(Class, 'Ext.Class#extend-backwards-compatible', arguments);
        }
        if (arguments.length === 2 && Ext.isObject(Parent)) {
            members = Parent;
            Parent = Class;
            Class = null;
        }
        if (!Parent) {
            throw new Error("[Ext.extend] Attempting to extend from a class which has not " + "been loaded on the page.");
        }
        members.extend = Parent;
        /* eslint-disable comma-style */
        members.preprocessors = [
            'extend',
            'statics',
            'inheritableStatics',
            'mixins',
            'config'
        ];
        /* eslint-enable comma-style */
        if (Class) {
            cls = new ExtClass(Class, members);
            // The 'constructor' is given as 'Class' but also needs to be on prototype
            cls.prototype.constructor = Class;
        } else {
            cls = new ExtClass(members);
        }
        cls.prototype.override = function(o) {
            for (m in o) {
                if (o.hasOwnProperty(m)) {
                    this[m] = o[m];
                }
            }
        };
        return cls;
    };
}());
/**
     * This object contains properties that describe the current device or platform. These
     * values can be used in `{@link Ext.Class#platformConfig platformConfig}` as well as
     * `{@link Ext.mixin.Responsive#responsiveConfig responsiveConfig}` statements.
     *
     * This object can be modified to include tags that are useful for the application. To
     * add custom properties, it is advisable to use a sub-object. For example:
     *
     *      Ext.platformTags.app = {
     *          mobile: true
     *      };
     *
     * @property {Object} platformTags
     * @property {Boolean} platformTags.phone
     * @property {Boolean} platformTags.tablet
     * @property {Boolean} platformTags.desktop
     * @property {Boolean} platformTags.touch Indicates touch inputs are available.
     * @property {Boolean} platformTags.safari
     * @property {Boolean} platformTags.chrome
     * @property {Boolean} platformTags.windows
     * @property {Boolean} platformTags.firefox
     * @property {Boolean} platformTags.ios True for iPad, iPhone and iPod.
     * @property {Boolean} platformTags.android
     * @property {Boolean} platformTags.blackberry
     * @property {Boolean} platformTags.tizen
     * @member Ext
     */

// @tag class
/**
 * @class Ext.Inventory
 * @private
 */
Ext.Inventory = function() {
    // @define Ext.Script
    // @define Ext.Inventory
    // @require Ext.Function
    var me = this;
    me.names = [];
    me.paths = {};
    me.alternateToName = {};
    me.aliasToName = {};
    me.nameToAliases = {};
    me.nameToAlternates = {};
    me.nameToPrefix = {};
};
Ext.Inventory.prototype = {
    _array1: [
        0
    ],
    prefixes: null,
    dotRe: /\./g,
    wildcardRe: /\*/g,
    addAlias: function(className, alias, update) {
        return this.addMapping(className, alias, this.aliasToName, this.nameToAliases, update);
    },
    addAlternate: function(className, alternate) {
        return this.addMapping(className, alternate, this.alternateToName, this.nameToAlternates);
    },
    addMapping: function(className, alternate, toName, nameTo, update) {
        var name = className.$className || className,
            mappings = name,
            array = this._array1,
            a, aliases, cls, i, length, nameMapping;
        if (Ext.isString(name)) {
            mappings = {};
            mappings[name] = alternate;
        }
        for (cls in mappings) {
            aliases = mappings[cls];
            if (Ext.isString(aliases)) {
                array[0] = aliases;
                aliases = array;
            }
            length = aliases.length;
            nameMapping = nameTo[cls] || (nameTo[cls] = []);
            for (i = 0; i < length; ++i) {
                if (!(a = aliases[i])) {
                    
                    continue;
                }
                if (toName[a] !== cls) {
                    if (!update && toName[a] && ('Ext.Gadget' !== a)) {
                        Ext.log.warn("Overriding existing mapping: '" + a + "' From '" + toName[a] + "' to '" + cls + "'. Is this intentional?");
                    }
                    toName[a] = cls;
                    nameMapping.push(a);
                }
            }
        }
    },
    /**
     * Get the aliases of a class by the class name
     *
     * @param {String} name
     * @return {Array} aliases
     */
    getAliasesByName: function(name) {
        return this.nameToAliases[name] || null;
    },
    getAlternatesByName: function(name) {
        return this.nameToAlternates[name] || null;
    },
    /**
     * Get the name of a class by its alias.
     *
     * @param {String} alias
     * @return {String} className
     */
    getNameByAlias: function(alias) {
        return this.aliasToName[alias] || '';
    },
    /**
     * Get the name of a class by its alternate name.
     *
     * @param {String} alternate
     * @return {String} className
     */
    getNameByAlternate: function(alternate) {
        return this.alternateToName[alternate] || '';
    },
    /**
     * Converts a string expression to an array of matching class names. An expression can
     * either refers to class aliases or class names. Expressions support wildcards:
     *
     *      // returns ['Ext.window.Window']
     *     var window = Ext.ClassManager.getNamesByExpression('widget.window');
     *
     *     // returns ['widget.panel', 'widget.window', ...]
     *     var allWidgets = Ext.ClassManager.getNamesByExpression('widget.*');
     *
     *     // returns ['Ext.data.Store', 'Ext.data.ArrayProxy', ...]
     *     var allData = Ext.ClassManager.getNamesByExpression('Ext.data.*');
     *
     * @param {String/String[]} expression
     * @param {Object} [exclude=null] An object keyed by class name containing classes to
     * exclude from the returned classes. This must be provided if `accumulate` is set to
     * `true`.
     * @param {Boolean} [accumulate=false] Pass `true` to add matching classes to the
     * specified `exclude` object.
     * @return {String[]} An array of class names.
     */
    getNamesByExpression: function(expression, exclude, accumulate) {
        var me = this,
            aliasToName = me.aliasToName,
            alternateToName = me.alternateToName,
            nameToAliases = me.nameToAliases,
            nameToAlternates = me.nameToAlternates,
            map = accumulate ? exclude : {},
            names = [],
            expressions = Ext.isString(expression) ? [
                expression
            ] : expression,
            length = expressions.length,
            wildcardRe = me.wildcardRe,
            expr, i, list, match, n, name, regex;
        for (i = 0; i < length; ++i) {
            if ((expr = expressions[i]).indexOf('*') < 0) {
                // No wildcard
                if (!(name = aliasToName[expr])) {
                    if (!(name = alternateToName[expr])) {
                        name = expr;
                    }
                }
                if (!(name in map) && !(exclude && (name in exclude))) {
                    map[name] = 1;
                    names.push(name);
                }
            } else {
                regex = new RegExp('^' + expr.replace(wildcardRe, '(.*?)') + '$');
                for (name in nameToAliases) {
                    if (!(name in map) && !(exclude && (name in exclude))) {
                        if (!(match = regex.test(name))) {
                            n = (list = nameToAliases[name]).length;
                            while (!match && n-- > 0) {
                                match = regex.test(list[n]);
                            }
                            list = nameToAlternates[name];
                            if (list && !match) {
                                n = list.length;
                                while (!match && n-- > 0) {
                                    match = regex.test(list[n]);
                                }
                            }
                        }
                        if (match) {
                            map[name] = 1;
                            names.push(name);
                        }
                    }
                }
            }
        }
        return names;
    },
    getPath: function(className) {
        var me = this,
            paths = me.paths,
            ret = '',
            prefix;
        if (className in paths) {
            ret = paths[className];
        } else {
            prefix = me.nameToPrefix[className] || (me.nameToPrefix[className] = me.getPrefix(className));
            if (prefix) {
                className = className.substring(prefix.length + 1);
                ret = paths[prefix];
                if (ret) {
                    ret += '/';
                }
            }
            ret += className.replace(me.dotRe, '/') + '.js';
        }
        return ret;
    },
    getPrefix: function(className) {
        if (className in this.paths) {
            return className;
        } else if (className in this.nameToPrefix) {
            return this.nameToPrefix[className];
        }
        /* eslint-disable-next-line vars-on-top */
        var prefixes = this.getPrefixes(),
            length = className.length,
            items, currChar, prefix, j, jlen;
        // Walk the prefixes backwards so we consider the longest ones first.
        // Prefixes are kept in a sparse array grouped by length so we don't have to
        // iterate over all of them, just the ones we need.
        while (length-- > 0) {
            items = prefixes[length];
            if (items) {
                currChar = className.charAt(length);
                if (currChar !== '.') {
                    
                    continue;
                }
                for (j = 0 , jlen = items.length; j < jlen; j++) {
                    prefix = items[j];
                    if (prefix === className.substring(0, length)) {
                        return prefix;
                    }
                }
            }
        }
        return '';
    },
    getPrefixes: function() {
        var me = this,
            prefixes = me.prefixes,
            names, name, nameLength, items, i, len;
        if (!prefixes) {
            names = me.names.slice(0);
            me.prefixes = prefixes = [];
            for (i = 0 , len = names.length; i < len; i++) {
                name = names[i];
                nameLength = name.length;
                items = prefixes[nameLength] || (prefixes[nameLength] = []);
                items.push(name);
            }
        }
        return prefixes;
    },
    removeName: function(name) {
        var me = this,
            aliasToName = me.aliasToName,
            alternateToName = me.alternateToName,
            nameToAliases = me.nameToAliases,
            nameToAlternates = me.nameToAlternates,
            aliases = nameToAliases[name],
            alternates = nameToAlternates[name],
            i, a;
        delete nameToAliases[name];
        delete nameToAlternates[name];
        delete me.nameToPrefix[name];
        if (aliases) {
            for (i = aliases.length; i--; ) {
                // Aliases can be reassigned so if this class is the current mapping of
                // the alias, remove it. Since there is no chain to restore what was
                // removed this is not perfect.
                if (name === aliasToName[a = aliases[i]]) {
                    delete aliasToName[a];
                }
            }
        }
        if (alternates) {
            for (i = alternates.length; i--; ) {
                // Like aliases, alternate class names can also be remapped.
                if (name === alternateToName[a = alternates[i]]) {
                    delete alternateToName[a];
                }
            }
        }
    },
    resolveName: function(name) {
        var me = this,
            trueName;
        // If the name has a registered alias, it is a true className (not an alternate)
        // so we can stop now.
        if (!(name in me.nameToAliases)) {
            // The name is not a known class name, so check to see if it is a known alias:
            if (!(trueName = me.aliasToName[name])) {
                // The name does not correspond to a known alias, so check if it is a known
                // alternateClassName:
                trueName = me.alternateToName[name];
            }
        }
        return trueName || name;
    },
    /**
     * This method returns a selector object that produces a selection of classes and
     * delivers them to the desired `receiver`.
     * 
     * The returned selector object has the same methods as the given `receiver` object
     * but these methods on the selector accept a first argument that expects a pattern
     * or array of patterns. The actual method on the `receiver` will be called with an
     * array of classes that match these patterns but with any patterns passed to an
     * `exclude` call removed.
     * 
     * For example:
     * 
     *      var sel = inventory.select({
     *              require: function (classes) {
     *                  console.log('Classes: ' + classes.join(','));
     *              }
     *          });
     * 
     *      sel.exclude('Ext.chart.*').exclude('Ext.draw.*').require('*');
     *      
     *      // Logs all classes except those in the Ext.chart and Ext.draw namespaces.
     * 
     * @param {Object} receiver
     * @param {Object} [scope] Optional scope to use when calling `receiver` methods.
     * @return {Object} An object with the same methods as `receiver` plus `exclude`.
     */
    select: function(receiver, scope) {
        var me = this,
            excludes = {},
            ret = {
                excludes: excludes,
                exclude: function() {
                    me.getNamesByExpression(arguments[0], excludes, true);
                    return this;
                }
            },
            name;
        for (name in receiver) {
            ret[name] = me.selectMethod(excludes, receiver[name], scope || receiver);
        }
        return ret;
    },
    selectMethod: function(excludes, fn, scope) {
        var me = this;
        return function(include) {
            var args = Ext.Array.slice(arguments, 1);
            args.unshift(me.getNamesByExpression(include, excludes));
            return fn.apply(scope, args);
        };
    },
    /**
     * Sets the path of a namespace.
     * For Example:
     *
     *      inventory.setPath('Ext', '.');
     *      inventory.setPath({
     *          Ext: '.'
     *      });
     *
     * @param {String/Object} name The name of a single mapping or an object of mappings.
     * @param {String} [path] If `name` is a String, then this is the path for that name.
     * Otherwise this parameter is ignored.
     * @return {Ext.Inventory} this
     * @method
     */
    setPath: Ext.Function.flexSetter(function(name, path) {
        var me = this;
        me.paths[name] = path;
        me.names.push(name);
        me.prefixes = null;
        me.nameToPrefix = {};
        return me;
    })
};

// @tag class
/**
 * @class Ext.ClassManager
 *
 * Ext.ClassManager manages all classes and handles mapping from string class name to
 * actual class objects throughout the whole framework. It is not generally accessed directly,
 * rather through these convenient shorthands:
 *
 * - {@link Ext#define Ext.define}
 * - {@link Ext#method!create Ext.create}
 * - {@link Ext#widget Ext.widget}
 * - {@link Ext#getClass Ext.getClass}
 * - {@link Ext#getClassName Ext.getClassName}
 *
 * # Basic syntax:
 *
 *     Ext.define(className, properties);
 *
 * in which `properties` is an object represent a collection of properties that apply to the class.
 * See {@link Ext.ClassManager#method!create} for more detailed instructions.
 *
 *     Ext.define('Person', {
 *          name: 'Unknown',
 *
 *          constructor: function(name) {
 *              if (name) {
 *                  this.name = name;
 *              }
 *          },
 *
 *          eat: function(foodType) {
 *              alert("I'm eating: " + foodType);
 *
 *              return this;
 *          }
 *     });
 *
 *     var aaron = new Person("Aaron");
 *     aaron.eat("Sandwich"); // alert("I'm eating: Sandwich");
 *
 * Ext.Class has a powerful set of extensible {@link Ext.Class#registerPreprocessor pre-processors}
 * which takes care of everything related to class creation, including but not limited to
 * inheritance, mixins, configuration, statics, etc.
 *
 * # Inheritance:
 *
 *     Ext.define('Developer', {
 *          extend: 'Person',
 *
 *          constructor: function(name, isGeek) {
 *              this.isGeek = isGeek;
 *
 *              // Apply a method from the parent class' prototype
 *              this.callParent([name]);
 *          },
 *
 *          code: function(language) {
 *              alert("I'm coding in: " + language);
 *
 *              this.eat("Bugs");
 *
 *              return this;
 *          }
 *     });
 *
 *     var jacky = new Developer("Jacky", true);
 *     jacky.code("JavaScript"); // alert("I'm coding in: JavaScript");
 *                               // alert("I'm eating: Bugs");
 *
 * See {@link Ext.Base#method!callParent} for more details on calling superclass' methods
 *
 * # Mixins:
 *
 *     Ext.define('CanPlayGuitar', {
 *          playGuitar: function() {
 *             alert("F#...G...D...A");
 *          }
 *     });
 *
 *     Ext.define('CanComposeSongs', {
 *          composeSongs: function() { ... }
 *     });
 *
 *     Ext.define('CanSing', {
 *          sing: function() {
 *              alert("For he's a jolly good fellow...")
 *          }
 *     });
 *
 *     Ext.define('Musician', {
 *          extend: 'Person',
 *
 *          mixins: {
 *              canPlayGuitar: 'CanPlayGuitar',
 *              canComposeSongs: 'CanComposeSongs',
 *              canSing: 'CanSing'
 *          }
 *     })
 *
 *     Ext.define('CoolPerson', {
 *          extend: 'Person',
 *
 *          mixins: {
 *              canPlayGuitar: 'CanPlayGuitar',
 *              canSing: 'CanSing'
 *          },
 *
 *          sing: function() {
 *              alert("Ahem....");
 *
 *              this.mixins.canSing.sing.call(this);
 *
 *              alert("[Playing guitar at the same time...]");
 *
 *              this.playGuitar();
 *          }
 *     });
 *
 *     var me = new CoolPerson("Jacky");
 *
 *     me.sing(); // alert("Ahem...");
 *                // alert("For he's a jolly good fellow...");
 *                // alert("[Playing guitar at the same time...]");
 *                // alert("F#...G...D...A");
 *
 * # Config:
 *
 *     Ext.define('SmartPhone', {
 *          config: {
 *              hasTouchScreen: false,
 *              operatingSystem: 'Other',
 *              price: 500
 *          },
 *
 *          isExpensive: false,
 *
 *          constructor: function(config) {
 *              this.initConfig(config);
 *          },
 *
 *          applyPrice: function(price) {
 *              this.isExpensive = (price > 500);
 *
 *              return price;
 *          },
 *
 *          applyOperatingSystem: function(operatingSystem) {
 *              if (!(/^(iOS|Android|BlackBerry)$/i).test(operatingSystem)) {
 *                  return 'Other';
 *              }
 *
 *              return operatingSystem;
 *          }
 *     });
 *
 *     var iPhone = new SmartPhone({
 *          hasTouchScreen: true,
 *          operatingSystem: 'iOS'
 *     });
 *
 *     iPhone.getPrice(); // 500;
 *     iPhone.getOperatingSystem(); // 'iOS'
 *     iPhone.getHasTouchScreen(); // true;
 *
 *     iPhone.isExpensive; // false;
 *     iPhone.setPrice(600);
 *     iPhone.getPrice(); // 600
 *     iPhone.isExpensive; // true;
 *
 *     iPhone.setOperatingSystem('AlienOS');
 *     iPhone.getOperatingSystem(); // 'Other'
 *
 * # Statics:
 *
 *     Ext.define('Computer', {
 *          statics: {
 *              factory: function(brand) {
 *                 // 'this' in static methods refer to the class itself
 *                  return new this(brand);
 *              }
 *          },
 *
 *          constructor: function() { ... }
 *     });
 *
 *     var dellComputer = Computer.factory('Dell');
 *
 * Also see {@link Ext.Base#statics} and {@link Ext.Base#self} for more details on accessing
 * static properties within class methods
 *
 * @singleton
 */
Ext.ClassManager = (function(Class, alias, arraySlice, arrayFrom, global) {
    // @define Ext.ClassManager
    // @require Ext.Inventory
    // @require Ext.Class
    // @require Ext.Function
    // @require Ext.Array
    var makeCtor = Ext.Class.makeCtor,
        nameLookupStack = [],
        namespaceCache = {
            Ext: {
                name: 'Ext',
                value: Ext
            }
        },
        // specially added for sandbox (Ext === global.Ext6)
        /*
        'Ext.grid': {
            name: 'grid',
            parent: namespaceCache['Ext']
        },
        'Ext.grid.Panel': {
            name: 'Panel',
            parent: namespaceCache['Ext.grid']
        },
        ...

        Also,
        'MyApp': {
            name: 'MyApp',
            value: MyApp
        }
        */
        Manager = Ext.apply(new Ext.Inventory(), {
            /**
         * @property {Object} classes
         * All classes which were defined through the ClassManager. Keys are the
         * name of the classes and the values are references to the classes.
         * @private
         */
            classes: {},
            classCount: 0,
            classState: {},
            /*
             * 'Ext.foo.Bar': <state enum>
             *
             *  10 = Ext.define called
             *  20 = Ext.define/override called
             *  30 = Manager.existCache[<name>] == true for define
             *  40 = Manager.existCache[<name>] == true for define/override
             *  50 = Manager.isCreated(<name>) == true for define
             *  60 = Manager.isCreated(<name>) == true for define/override
             *
             */
            /**
         * @private
         */
            existCache: {},
            /** @private */
            instantiators: [],
            /**
         * Checks if a class has already been created.
         *
         * @param {String} className
         * @return {Boolean} exist
         */
            isCreated: function(className) {
                if (typeof className !== 'string' || className.length < 1) {
                    throw new Error("[Ext.ClassManager] Invalid classname, must be a string and " + "must not be empty");
                }
                if (Manager.classes[className] || Manager.existCache[className]) {
                    return true;
                }
                if (!Manager.lookupName(className, false)) {
                    return false;
                }
                Manager.triggerCreated(className);
                return true;
            },
            /**
         * @private
         */
            createdListeners: [],
            /**
         * @private
         */
            nameCreatedListeners: {},
            /**
         * @private
         */
            existsListeners: [],
            /**
         * @private
         */
            nameExistsListeners: {},
            /**
         * @private
         */
            overrideMap: {},
            /**
         * @private
         */
            triggerCreated: function(className, state) {
                Manager.existCache[className] = state || 1;
                Manager.classState[className] += 40;
                Manager.notify(className, Manager.createdListeners, Manager.nameCreatedListeners);
            },
            /**
         * @private
         */
            onCreated: function(fn, scope, className) {
                Manager.addListener(fn, scope, className, Manager.createdListeners, Manager.nameCreatedListeners);
            },
            /**
         * @private
         */
            notify: function(className, listeners, nameListeners) {
                var alternateNames = Manager.getAlternatesByName(className),
                    names = [
                        className
                    ],
                    i, ln, j, subLn, listener, name;
                for (i = 0 , ln = listeners.length; i < ln; i++) {
                    listener = listeners[i];
                    listener.fn.call(listener.scope, className);
                }
                while (names) {
                    for (i = 0 , ln = names.length; i < ln; i++) {
                        name = names[i];
                        listeners = nameListeners[name];
                        if (listeners) {
                            for (j = 0 , subLn = listeners.length; j < subLn; j++) {
                                listener = listeners[j];
                                listener.fn.call(listener.scope, name);
                            }
                            delete nameListeners[name];
                        }
                    }
                    names = alternateNames;
                    // for 2nd pass (if needed)
                    alternateNames = null;
                }
            },
            // no 3rd pass
            /**
         * @private
         */
            addListener: function(fn, scope, className, listeners, nameListeners) {
                var i;
                if (Ext.isArray(className)) {
                    fn = Ext.Function.createBarrier(className.length, fn, scope);
                    for (i = 0; i < className.length; i++) {
                        this.addListener(fn, null, className[i], listeners, nameListeners);
                    }
                    return;
                }
                /* eslint-disable-next-line vars-on-top */
                var listener = {
                        fn: fn,
                        scope: scope
                    };
                if (className) {
                    if (this.isCreated(className)) {
                        fn.call(scope, className);
                        return;
                    }
                    if (!nameListeners[className]) {
                        nameListeners[className] = [];
                    }
                    nameListeners[className].push(listener);
                } else {
                    listeners.push(listener);
                }
            },
            /**
         * Supports namespace rewriting.
         * @private
         */
            $namespaceCache: namespaceCache,
            /**
         * See `{@link Ext#addRootNamespaces Ext.addRootNamespaces}`.
         * @since 6.0.0
         * @private
         */
            addRootNamespaces: function(namespaces) {
                var name;
                for (name in namespaces) {
                    namespaceCache[name] = {
                        name: name,
                        value: namespaces[name]
                    };
                }
            },
            /**
         * Clears the namespace lookup cache. After application launch, this cache can
         * often contain several hundred entries that are unlikely to be needed again.
         * These will be rebuilt as needed, so it is harmless to clear this cache even
         * if its results will be used again.
         * @since 6.0.0
         * @private
         */
            clearNamespaceCache: function() {
                var name;
                nameLookupStack.length = 0;
                for (name in namespaceCache) {
                    if (!namespaceCache[name].value) {
                        delete namespaceCache[name];
                    }
                }
            },
            /**
         * Return the namespace cache entry for the given a class name or namespace (e.g.,
         * "Ext.grid.Panel").
         *
         * @param {String} namespace The namespace or class name to lookup.
         * @return {Object} The cache entry.
         * @return {String} return.name The leaf name ("Panel" for "Ext.grid.Panel").
         * @return {Object} return.parent The entry of the parent namespace (i.e., "Ext.grid").
         * @return {Object} return.value The namespace object. This is only set for
         * top-level namespace entries to support renaming them for sandboxing ("Ext6" vs
         * "Ext").
         * @since 6.0.0
         * @private
         */
            getNamespaceEntry: function(namespace) {
                var entry, i;
                if (typeof namespace !== 'string') {
                    return namespace;
                }
                // assume we've been given an entry object
                entry = namespaceCache[namespace];
                if (!entry) {
                    i = namespace.lastIndexOf('.');
                    if (i < 0) {
                        entry = {
                            name: namespace
                        };
                    } else {
                        entry = {
                            name: namespace.substring(i + 1),
                            parent: Manager.getNamespaceEntry(namespace.substring(0, i))
                        };
                    }
                    namespaceCache[namespace] = entry;
                }
                return entry;
            },
            /**
         * Return the value of the given "dot path" name. This supports remapping (for use
         * in sandbox builds) as well as auto-creating of namespaces.
         *
         * @param {String} namespace The name of the namespace or class.
         * @param {Boolean} [autoCreate] Pass `true` to create objects for undefined names.
         * @return {Object} The object that is the namespace or class name.
         * @since 6.0.0
         * @private
         */
            lookupName: function(namespace, autoCreate) {
                var entry = Manager.getNamespaceEntry(namespace),
                    scope = Ext.global,
                    i = 0,
                    e, parent;
                // Put entries on the stack in reverse order: [ 'Panel', 'grid', 'Ext' ]
                for (e = entry; e; e = e.parent) {
                    // since we process only what we add to the array, and that always
                    // starts at index=0, we don't need to clean up the array (that would
                    // just encourage the GC to do something pointless).
                    nameLookupStack[i++] = e;
                }
                while (scope && i-- > 0) {
                    // We'll process entries in top-down order ('Ext', 'grid' then 'Panel').
                    e = nameLookupStack[i];
                    parent = scope;
                    scope = e.value || scope[e.name];
                    if (!scope && autoCreate) {
                        parent[e.name] = scope = {};
                    }
                }
                return scope;
            },
            /**
         * Creates a namespace and assign the `value` to the created object.
         *
         *     Ext.ClassManager.setNamespace('MyCompany.pkg.Example', someObject);
         *
         *     alert(MyCompany.pkg.Example === someObject); // alerts true
         *
         * @param {String} namespace
         * @param {Object} value
         */
            setNamespace: function(namespace, value) {
                var entry = Manager.getNamespaceEntry(namespace),
                    scope = Ext.global;
                if (entry.parent) {
                    scope = Manager.lookupName(entry.parent, true);
                }
                scope[entry.name] = value;
                return value;
            },
            /**
         * Changes the mapping of an `xtype` to map to the specified component class.
         * @param {String/Ext.Class} cls The class or class name to which `xtype` is mapped.
         * @param {String} xtype The `xtype` to map or redefine as `cls`.
         * @since 6.0.1
         * @private
         */
            setXType: function(cls, xtype) {
                var className = cls.$className,
                    C = className ? cls : Manager.get(className = cls),
                    proto = C.prototype,
                    xtypes = proto.xtypes,
                    xtypesChain = proto.xtypesChain,
                    xtypesMap = proto.xtypesMap;
                if (!proto.hasOwnProperty('xtypes')) {
                    proto.xtypes = xtypes = [];
                    proto.xtypesChain = xtypesChain = xtypesChain ? xtypesChain.slice(0) : [];
                    proto.xtypesMap = xtypesMap = Ext.apply({}, xtypesMap);
                }
                Manager.addAlias(className, 'widget.' + xtype, true);
                xtypes.push(xtype);
                xtypesChain.push(xtype);
                xtypesMap[xtype] = true;
            },
            // TODO consider updating derived class xtypesChain / xtypesMap
            /**
         * Sets a name reference to a class.
         *
         * @param {String} name
         * @param {Object} value
         * @return {Ext.ClassManager} this
         */
            set: function(name, value) {
                var targetName = Manager.getName(value);
                Manager.classes[name] = Manager.setNamespace(name, value);
                Manager.classCount++;
                if (targetName && targetName !== name) {
                    Manager.addAlternate(targetName, name);
                }
                return Manager;
            },
            /**
         * Retrieve a class by its name.
         *
         * @param {String} name
         * @return {Ext.Class} class
         */
            get: function(name) {
                return Manager.classes[name] || Manager.lookupName(name, false);
            },
            /**
         * Adds a batch of class name to alias mappings.
         * @param {Object} aliases The set of mappings of the form.
         * className : [values...]
         */
            addNameAliasMappings: function(aliases) {
                Manager.addAlias(aliases);
            },
            /**
         *
         * @param {Object} alternates The set of mappings of the form
         * className : [values...]
         */
            addNameAlternateMappings: function(alternates) {
                Manager.addAlternate(alternates);
            },
            /**
         * Get a reference to the class by its alias.
         *
         * @param {String} alias
         * @return {Ext.Class} class
         */
            getByAlias: function(alias) {
                return Manager.get(Manager.getNameByAlias(alias));
            },
            /**
         * Get a component class name from a config object.
         * @param {Object} config The config object.
         * @param {String} [aliasPrefix] A prefix to use when getting
         * a class name by alias.
         * @return {Ext.Class} The class.
         *
         * @private
         */
            getByConfig: function(config, aliasPrefix) {
                var xclass = config.xclass,
                    name;
                if (xclass) {
                    name = xclass;
                } else {
                    name = config.xtype;
                    if (name) {
                        aliasPrefix = 'widget.';
                    } else {
                        name = config.type;
                    }
                    name = Manager.getNameByAlias(aliasPrefix + name);
                }
                return Manager.get(name);
            },
            /**
         * Get the name of the class by its reference or its instance. This is
         * usually invoked by the shorthand {@link Ext#getClassName}.
         *
         *     Ext.ClassManager.getName(Ext.Action); // returns "Ext.Action"
         *
         * @param {Ext.Class/Object} object
         * @return {String} className
         */
            getName: function(object) {
                return object && object.$className || '';
            },
            /**
         * Get the class of the provided object; returns null if it's not an instance
         * of any class created with Ext.define. This is usually invoked by the
         * shorthand {@link Ext#getClass}.
         *
         *     var component = new Ext.Component();
         *
         *     Ext.getClass(component); // returns Ext.Component
         *
         * @param {Object} object
         * @return {Ext.Class} class
         */
            getClass: function(object) {
                return object && object.self || null;
            },
            /**
         * Defines a class.
         * @deprecated 4.1 Use {@link Ext#define} instead.
         * @private
         */
            create: function(className, data, createdFn) {
                var ctor;
                if (className != null && typeof className !== 'string') {
                    throw new Error("[Ext.define] Invalid class name '" + className + "' specified, must be a non-empty string");
                }
                ctor = makeCtor(className);
                if (typeof data === 'function') {
                    data = data(ctor);
                }
                if (className) {
                    if (Manager.classes[className]) {
                        Ext.log.warn("[Ext.define] Duplicate class name '" + className + "' specified, must be a non-empty string");
                    }
                    ctor.name = className;
                }
                data.$className = className;
                return new Class(ctor, data, function() {
                    var postprocessorStack = data.postprocessors || Manager.defaultPostprocessors,
                        registeredPostprocessors = Manager.postprocessors,
                        postprocessors = [],
                        postprocessor, i, ln, j, subLn, postprocessorProperties, postprocessorProperty;
                    delete data.postprocessors;
                    for (i = 0 , ln = postprocessorStack.length; i < ln; i++) {
                        postprocessor = postprocessorStack[i];
                        if (typeof postprocessor === 'string') {
                            postprocessor = registeredPostprocessors[postprocessor];
                            postprocessorProperties = postprocessor.properties;
                            if (postprocessorProperties === true) {
                                postprocessors.push(postprocessor.fn);
                            } else if (postprocessorProperties) {
                                for (j = 0 , subLn = postprocessorProperties.length; j < subLn; j++) {
                                    postprocessorProperty = postprocessorProperties[j];
                                    if (data.hasOwnProperty(postprocessorProperty)) {
                                        postprocessors.push(postprocessor.fn);
                                        break;
                                    }
                                }
                            }
                        } else {
                            postprocessors.push(postprocessor);
                        }
                    }
                    data.postprocessors = postprocessors;
                    data.createdFn = createdFn;
                    Manager.processCreate(className, this, data);
                });
            },
            processCreate: function(className, cls, clsData) {
                var me = this,
                    postprocessor = clsData.postprocessors.shift(),
                    createdFn = clsData.createdFn;
                if (!postprocessor) {
                    if (Ext.classSystemMonitor) {
                        Ext.classSystemMonitor(className, 'Ext.ClassManager#classCreated', arguments);
                    }
                    if (className) {
                        me.set(className, cls);
                    }
                    delete cls._classHooks;
                    if (createdFn) {
                        createdFn.call(cls, cls);
                    }
                    if (className) {
                        me.triggerCreated(className);
                    }
                    return;
                }
                if (postprocessor.call(me, className, cls, clsData, me.processCreate) !== false) {
                    me.processCreate(className, cls, clsData);
                }
            },
            createOverride: function(className, data, createdFn) {
                var me = this,
                    overriddenClassName = data.override,
                    requires = data.requires,
                    uses = data.uses,
                    mixins = data.mixins,
                    mixinsIsArray,
                    compat = 1,
                    // default if 'compatibility' is not specified
                    dependenciesLoaded, classReady;
                classReady = function() {
                    var cls, dependencies, i, key, temp;
                    if (!dependenciesLoaded) {
                        dependencies = requires ? requires.slice(0) : [];
                        if (mixins) {
                            if (!(mixinsIsArray = mixins instanceof Array)) {
                                for (key in mixins) {
                                    if (Ext.isString(cls = mixins[key])) {
                                        dependencies.push(cls);
                                    }
                                }
                            } else {
                                for (i = 0 , temp = mixins.length; i < temp; ++i) {
                                    if (Ext.isString(cls = mixins[i])) {
                                        dependencies.push(cls);
                                    }
                                }
                            }
                        }
                        dependenciesLoaded = true;
                        if (dependencies.length) {
                            // Since the override is going to be used (its target class is
                            // now created), we need to fetch the required classes for the
                            // override and call us back once they are loaded:
                            Ext.require(dependencies, classReady);
                            return;
                        }
                    }
                    // else we have no dependencies, so proceed
                    // transform mixin class names into class references, This
                    // loop can handle both the array and object forms of
                    // mixin definitions
                    if (mixinsIsArray) {
                        for (i = 0 , temp = mixins.length; i < temp; ++i) {
                            if (Ext.isString(cls = mixins[i])) {
                                mixins[i] = Ext.ClassManager.get(cls);
                            }
                        }
                    } else if (mixins) {
                        for (key in mixins) {
                            if (Ext.isString(cls = mixins[key])) {
                                mixins[key] = Ext.ClassManager.get(cls);
                            }
                        }
                    }
                    // The target class and the required classes for this override are
                    // ready, so we can apply the override now:
                    cls = overriddenClassName.$isClass ? overriddenClassName : me.get(overriddenClassName);
                    // We don't want to apply these:
                    delete data.override;
                    delete data.compatibility;
                    delete data.requires;
                    delete data.uses;
                    Ext.override(cls, data);
                    // This pushes the overriding file itself into Ext.Loader.history
                    // Hence if the target class never exists, the overriding file will
                    // never be included in the build.
                    Ext.Loader.history.push(className);
                    if (uses) {
                        // This "hides" from the Cmd auto-dependency scanner since
                        // the reference is circular (Loader requires us).
                        /* eslint-disable-next-line dot-notation */
                        Ext['Loader'].addUsedClasses(uses);
                    }
                    // get these classes too!
                    if (createdFn) {
                        createdFn.call(cls, cls);
                    }
                };
                // last but not least!
                if (className) {
                    Manager.overrideMap[className] = true;
                }
                // If specified, parse strings as versions, but otherwise treat as a
                // boolean (maybe "compatibility: Ext.isIE8" or something).
                //
                if ('compatibility' in data) {
                    compat = data.compatibility;
                    if (!compat) {
                        // Cast '', null, undefined, 0 to false.
                        compat = false;
                    } else if (typeof compat === 'number') {
                        // By virtue of the condition above we must be a nonzero number.
                        compat = true;
                    } else if (typeof compat !== 'boolean') {
                        compat = Ext.checkVersion(compat);
                    }
                }
                if (compat) {
                    // override the target class right after it's created
                    if (overriddenClassName.$isClass) {
                        classReady();
                    } else {
                        me.onCreated(classReady, me, overriddenClassName);
                    }
                }
                me.triggerCreated(className, 2);
                return me;
            },
            /**
         * Instantiate a class by its alias. This is usually invoked by the
         * shorthand {@link Ext#createByAlias}.
         *
         * If {@link Ext.Loader} is {@link Ext.Loader#setConfig enabled} and the class
         * has not been defined yet, it will attempt to load the class via synchronous
         * loading.
         *
         *     var window = Ext.createByAlias('widget.window', { width: 600, height: 800 });
         *
         * @param {String} alias
         * @param {Object...} args Additional arguments after the alias will be passed to the
         * class constructor.
         * @return {Object} instance
         */
            instantiateByAlias: function() {
                var alias = arguments[0],
                    args = arraySlice.call(arguments),
                    className = this.getNameByAlias(alias);
                if (!className) {
                    throw new Error("[Ext.createByAlias] Unrecognized alias: " + alias);
                }
                args[0] = className;
                return Ext.create.apply(Ext, args);
            },
            /**
         * Instantiate a class by either full name, alias or alternate name
         * @param {String} name
         * @param {Mixed} args Additional arguments after the name will be passed to the class'
         * constructor.
         * @return {Object} instance
         * @deprecated 5.0 Use Ext.create() instead.
         */
            instantiate: function() {
                Ext.log.warn('Ext.ClassManager.instantiate() is deprecated. Use Ext.create() instead.');
                return Ext.create.apply(Ext, arguments);
            },
            /**
         * @private
         * @param name
         * @param args
         */
            dynInstantiate: function(name, args) {
                args = arrayFrom(args, true);
                args.unshift(name);
                return Ext.create.apply(Ext, args);
            },
            /**
         * @private
         * @param length
         */
            getInstantiator: function(length) {
                var instantiators = this.instantiators,
                    instantiator, args, i;
                instantiator = instantiators[length];
                if (!instantiator) {
                    i = length;
                    args = [];
                    for (i = 0; i < length; i++) {
                        args.push('a[' + i + ']');
                    }
                    instantiator = instantiators[length] = new Function('c', 'a', 'return new c(' + args.join(',') + ')');
                    instantiator.name = "Ext.create" + length;
                }
                return instantiator;
            },
            /**
         * @private
         */
            postprocessors: {},
            /**
         * @private
         */
            defaultPostprocessors: [],
            /**
         * Register a post-processor function.
         *
         * @param {String} name
         * @param {Function} fn
         * @param {String/String[]} properties
         * @param {String} position
         * @param {String} relativeTo
         * @private
         */
            registerPostprocessor: function(name, fn, properties, position, relativeTo) {
                if (!position) {
                    position = 'last';
                }
                if (!properties) {
                    properties = [
                        name
                    ];
                }
                this.postprocessors[name] = {
                    name: name,
                    properties: properties || false,
                    fn: fn
                };
                this.setDefaultPostprocessorPosition(name, position, relativeTo);
                return this;
            },
            /**
         * Set the default post processors array stack which are applied to every class.
         *
         * @private
         * @param {String/Array} postprocessors The name of a registered post processor or an array
         * of registered names.
         * @return {Ext.ClassManager} this
         */
            setDefaultPostprocessors: function(postprocessors) {
                this.defaultPostprocessors = arrayFrom(postprocessors);
                return this;
            },
            /**
         * Insert this post-processor at a specific position in the stack, optionally relative to
         * any existing post-processor
         *
         * @private
         * @param {String} name The post-processor name. Note that it needs to be registered with
         * {@link Ext.ClassManager#registerPostprocessor} before this
         * @param {String} offset The insertion position. Four possible values are:
         * 'first', 'last', or: 'before', 'after' (relative to the name provided in the third
         * argument)
         * @param {String} relativeName
         * @return {Ext.ClassManager} this
         */
            setDefaultPostprocessorPosition: function(name, offset, relativeName) {
                var defaultPostprocessors = this.defaultPostprocessors,
                    index;
                if (typeof offset === 'string') {
                    if (offset === 'first') {
                        defaultPostprocessors.unshift(name);
                        return this;
                    } else if (offset === 'last') {
                        defaultPostprocessors.push(name);
                        return this;
                    }
                    offset = (offset === 'after') ? 1 : -1;
                }
                index = Ext.Array.indexOf(defaultPostprocessors, relativeName);
                if (index !== -1) {
                    Ext.Array.splice(defaultPostprocessors, Math.max(0, index + offset), 0, name);
                }
                return this;
            }
        });
    /* eslint-disable indent */
    /**
     * @cfg xtype
     * @member Ext.Class
     * @inheritdoc Ext.Component#cfg-xtype
     */
    /**
     * @cfg {String} override
     * @member Ext.Class
     * Overrides members of the specified `target` class.
     * 
     * **NOTE:** the overridden class must have been defined using 
     * {@link Ext#define Ext.define} in order to use the `override` config.
     * 
     * Methods defined on the overriding class will not automatically call the methods of 
     * the same name in the ancestor class chain.  To call the parent's method of the 
     * same name you must call {@link Ext.Base#method!callParent callParent}.  To skip the
     * method of the overridden class and call its parent you will instead call 
     * {@link Ext.Base#method!callSuper callSuper}.
     *
     * See {@link Ext#define Ext.define} for additional usage examples.
     */
    /**
     * @cfg {Object} platformConfig
     * Allows setting config values for a class based on specific platforms. The value
     * of this config is an object whose properties are "rules" and whose values are
     * objects containing config values.
     *
     * For example:
     *
     *      Ext.define('App.view.Foo', {
     *          extend: 'Ext.panel.Panel',
     *
     *          platformConfig: {
     *              desktop: {
     *                  title: 'Some Rather Descriptive Title'
     *              },
     *
     *              '!desktop': {
     *                  title: 'Short Title'
     *              }
     *          }
     *      });
     *
     * In the above, "desktop" and "!desktop" are (mutually exclusive) rules. Whichever
     * evaluates to `true` will have its configs applied to the class. In this case, only
     * the "title" property, but the object can contain any number of config properties.
     * In this case, the `platformConfig` is evaluated as part of the class and there is
     * no cost for each instance created.
     *
     * The rules are evaluated expressions in the context of the platform tags contained
     * in `{@link Ext#platformTags Ext.platformTags}`. Any properties of that object are
     * implicitly usable (as shown above).
     *
     * If a `platformConfig` specifies a config value, it will replace any values declared
     * on the class itself.
     *
     * Use of `platformConfig` on instances is handled by the config system when classes
     * call `{@link Ext.Base#initConfig initConfig}`. For example:
     *
     *      Ext.create({
     *          xtype: 'panel',
     *
     *          platformConfig: {
     *              desktop: {
     *                  title: 'Some Rather Descriptive Title'
     *              },
     *
     *              '!desktop': {
     *                  title: 'Short Title'
     *              }
     *          }
     *      });
     *
     * The following is equivalent to the above:
     *
     *      if (Ext.platformTags.desktop) {
     *          Ext.create({
     *              xtype: 'panel',
     *              title: 'Some Rather Descriptive Title'
     *          });
     *      } else {
     *          Ext.create({
     *              xtype: 'panel',
     *              title: 'Short Title'
     *          });
     *      }
     *
     * To adjust configs based on dynamic conditions, see `{@link Ext.mixin.Responsive}`.
     */
    Manager.registerPostprocessor('platformConfig', function(name, Class, data) {
        Class.addPlatformConfig(data);
    });
    /**
     * @cfg {String/String[]} alias
     * @member Ext.Class
     * List of short aliases for class names. An alias consists of a namespace and a name
     * concatenated by a period as &#60;namespace&#62;.&#60;name&#62;
     *
     *  - **namespace** - The namespace describes what kind of alias this is and must be
     *  all lowercase.
     *  - **name** - The name of the alias which allows the lazy-instantiation via the
     *  alias. The name shouldn't contain any periods.
     *
     * A list of namespaces and the usages are:
     *
     *  - **feature** - {@link Ext.grid.Panel Grid} features
     *  - **plugin** - Plugins
     *  - **store** - {@link Ext.data.Store}
     *  - **widget** - Components
     *
     * Most useful for defining xtypes for widgets:
     *
     *     Ext.define('MyApp.CoolPanel', {
     *         extend: 'Ext.panel.Panel',
     *         alias: ['widget.coolpanel'],
     *         title: 'Yeah!'
     *     });
     *
     *     // Using Ext.create
     *     Ext.create('widget.coolpanel');
     *
     *     // Using the shorthand for defining widgets by xtype
     *     Ext.widget('panel', {
     *         items: [
     *             {xtype: 'coolpanel', html: 'Foo'},
     *             {xtype: 'coolpanel', html: 'Bar'}
     *         ]
     *     });
     */
    Manager.registerPostprocessor('alias', function(name, cls, data) {
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(name, 'Ext.ClassManager#aliasPostProcessor', arguments);
        }
        /* eslint-disable-next-line vars-on-top */
        var aliases = Ext.Array.from(data.alias),
            i, ln;
        for (i = 0 , ln = aliases.length; i < ln; i++) {
            alias = aliases[i];
            this.addAlias(cls, alias);
        }
    }, [
        'xtype',
        'alias'
    ]);
    /**
     * @cfg {Boolean} singleton
     * @member Ext.Class
     * When set to true, the class will be instantiated as singleton.  For example:
     *
     *     Ext.define('Logger', {
     *         singleton: true,
     *         log: function(msg) {
     *             console.log(msg);
     *         }
     *     });
     *
     *     Logger.log('Hello');
     */
    Manager.registerPostprocessor('singleton', function(name, cls, data, fn) {
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(name, 'Ext.ClassManager#singletonPostProcessor', arguments);
        }
        if (data.singleton) {
            fn.call(this, name, new cls(), data);
        } else {
            return true;
        }
        return false;
    });
    /**
     * @cfg {String/String[]} alternateClassName
     * @member Ext.Class
     * Defines alternate names for this class.  For example:
     *
     *     Ext.define('Developer', {
     *         alternateClassName: ['Coder', 'Hacker'],
     *         code: function(msg) {
     *             alert('Typing... ' + msg);
     *         }
     *     });
     *
     *     var joe = Ext.create('Developer');
     *     joe.code('stackoverflow');
     *
     *     var rms = Ext.create('Hacker');
     *     rms.code('hack hack');
     */
    Manager.registerPostprocessor('alternateClassName', function(name, cls, data) {
        var alternates = data.alternateClassName,
            i, ln, alternate;
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(name, 'Ext.ClassManager#alternateClassNamePostprocessor', arguments);
        }
        if (!(alternates instanceof Array)) {
            alternates = [
                alternates
            ];
        }
        for (i = 0 , ln = alternates.length; i < ln; i++) {
            alternate = alternates[i];
            if (typeof alternate !== 'string') {
                throw new Error("[Ext.define] Invalid alternate of: '" + alternate + "' for class: '" + name + "'; must be a valid string");
            }
            this.set(alternate, cls);
        }
    });
    /**
     * @cfg {Object} debugHooks
     * A collection of diagnostic methods to decorate the real methods of the class. These
     * methods are applied as an `override` if this class has debug enabled as defined by
     * `Ext.isDebugEnabled`.
     *
     * These will be automatically removed by the Sencha Cmd compiler for production builds.
     *
     * Example usage:
     *
     *      Ext.define('Foo.bar.Class', {
     *          foo: function(a, b, c) {
     *              ...
     *          },
     *
     *          bar: function(a, b) {
     *              ...
     *              return 42;
     *          },
     *
     *          debugHooks: {
     *              foo: function(a, b, c) {
     *                  // check arguments...
     *                  return this.callParent(arguments);
     *              }
     *          }
     *      });
     *
     * If you specify a `$enabled` property in the `debugHooks` object that will be used
     * as the default enabled state for the hooks. If the `{@link Ext#manifest}` contains
     * a `debug` object of if `{@link Ext#debugConfig}` is specified, the `$enabled` flag
     * will override its "*" value.
     */
    Manager.registerPostprocessor('debugHooks', function(name, Class, data) {
        var target;
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(Class, 'Ext.Class#debugHooks', arguments);
        }
        if (Ext.isDebugEnabled(Class.$className, data.debugHooks.$enabled)) {
            delete data.debugHooks.$enabled;
            Ext.override(Class, data.debugHooks);
        }
        // may already have an instance here in the case of singleton
        target = Class.isInstance ? Class.self : Class;
        delete target.prototype.debugHooks;
    });
    /**
     * @cfg {Object} deprecated
     * The object given has properties that describe the versions at which the deprecations
     * apply.
     *
     * The purpose of the `deprecated` declaration is to enable development mode to give
     * suitable error messages when deprecated methods or properties are used. Methods can
     * always be injected to provide this feedback, but properties can only be handled on
     * some browsers (those that support `Object.defineProperty`).
     *
     * In some cases, deprecated methods can be restored to their previous behavior or
     * added back if they have been removed.
     *
     * The structure of a `deprecated` declaration is this:
     *
     *      Ext.define('Foo.bar.Class', {
     *          ...
     *
     *          deprecated: {
     *              // Optional package name - default is the framework (ext or touch)
     *              name: 'foobar',
     *
     *              '5.0': {
     *                  methods: {
     *                      // Throws: '"removedMethod" is deprecated.'
     *                      removedMethod: null,
     *
     *                      // Throws: '"oldMethod" is deprecated. Please use "newMethod" instead.'
     *                      oldMethod: 'newMethod',
     *
     *                      // When this block is enabled, this method is applied as an
     *                      // override. Otherwise you get same as "removeMethod".
     *                      method: function() {
     *                          // Do what v5 "method" did. If "method" exists in newer
     *                          // versions callParent can call it. If 5.1 has "method"
     *                          // then it would be next in line, otherwise 5.2 and last
     *                          // would be the current class.
     *                      },
     *
     *                      moreHelpful: {
     *                          message: 'Something helpful to do instead.',
     *                          fn: function() {
     *                              // The v5 "moreHelpful" method to use when enabled.
     *                          }
     *                      }
     *                  },
     *                  properties: {
     *                      // Throws: '"removedProp" is deprecated.'
     *                      removedProp: null,
     *
     *                      // Throws: '"oldProp" is deprecated. Please use "newProp" instead.'
     *                      oldProp: 'newProp',
     *
     *                      helpful: {
     *                          message: 'Something helpful message about what to do.'
     *                      }
     *                      ...
     *                  },
     *                  statics: {
     *                      methods: {
     *                          ...
     *                      },
     *                      properties: {
     *                          ...
     *                      },
     *                  }
     *              },
     *
     *              '5.1': {
     *                  ...
     *              },
     *
     *              '5.2': {
     *                  ...
     *              }
     *          }
     *      });
     *
     * The primary content of `deprecated` are the version number keys. These indicate
     * a version number where methods or properties were deprecated. These versions are
     * compared to the version reported by `Ext.getCompatVersion` to determine the action
     * to take for each "block".
     *
     * When the compatibility version is set to a value less than a version number key,
     * that block is said to be "enabled". For example, if a method was deprecated in
     * version 5.0 but the desired compatibility level is 4.2 then the block is used to
     * patch methods and (to some degree) restore pre-5.0 compatibility.
     *
     * When multiple active blocks have the same method name, each method is applied as
     * an override in reverse order of version. In the above example, if a method appears
     * in the "5.0", "5.1" and "5.2" blocks then the "5.2" method is applied as an override
     * first, followed by the "5.1" method and finally the "5.0" method. This means that
     * the `callParent` from the "5.0" method calls the "5.1" method which calls the
     * "5.2" method which can (if applicable) call the current version.
     */
    Manager.registerPostprocessor('deprecated', function(name, Class, data) {
        var target;
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(Class, 'Ext.Class#deprecated', arguments);
        }
        // may already have an instance here in the case of singleton
        target = Class.isInstance ? Class.self : Class;
        target.addDeprecations(data.deprecated);
        delete target.prototype.deprecated;
    });
    Ext.apply(Ext, {
        /**
         * Instantiate a class by either full name, alias or alternate name.
         *
         * If {@link Ext.Loader} is {@link Ext.Loader#setConfig enabled} and the class has
         * not been defined yet, it will attempt to load the class via synchronous loading.
         *
         * For example, all these three lines return the same result:
         *
         *      // xtype
         *      var window = Ext.create({
         *          xtype: 'window',
         *          width: 600,
         *          height: 800,
         *          ...
         *      });
         *
         *      // alias
         *      var window = Ext.create('widget.window', {
         *          width: 600,
         *          height: 800,
         *          ...
         *      });
         *
         *      // alternate name
         *      var window = Ext.create('Ext.Window', {
         *          width: 600,
         *          height: 800,
         *          ...
         *      });
         *
         *      // full class name
         *      var window = Ext.create('Ext.window.Window', {
         *          width: 600,
         *          height: 800,
         *          ...
         *      });
         *
         *      // single object with xclass property:
         *      var window = Ext.create({
         *          xclass: 'Ext.window.Window', // any valid value for 'name' (above)
         *          width: 600,
         *          height: 800,
         *          ...
         *      });
         *
         * @param {String} [name] The class name or alias. Can be specified as `xclass`
         * property if only one object parameter is specified.
         * @param {Object...} [args] Additional arguments after the name will be passed to
         * the class' constructor.
         * @return {Object} instance
         * @member Ext
         * @method create
         */
        create: function() {
            var name = arguments[0],
                nameType = typeof name,
                args = arraySlice.call(arguments, 1),
                cls;
            if (nameType === 'function') {
                cls = name;
            } else {
                if (nameType !== 'string' && args.length === 0) {
                    args = [
                        name
                    ];
                    if (!(name = name.xclass)) {
                        name = args[0].xtype;
                        if (name) {
                            name = 'widget.' + name;
                        }
                    }
                }
                if (typeof name !== 'string' || name.length < 1) {
                    throw new Error("[Ext.create] Invalid class name or alias '" + name + "' specified, must be a non-empty string");
                }
                name = Manager.resolveName(name);
                cls = Manager.get(name);
            }
            // Still not existing at this point, try to load it via synchronous mode as the last
            // resort
            if (!cls) {
                Ext.syncRequire(name);
                cls = Manager.get(name);
            }
            if (!cls) {
                throw new Error("[Ext.create] Unrecognized class name / alias: " + name);
            }
            if (typeof cls !== 'function') {
                throw new Error("[Ext.create] Singleton '" + name + "' cannot be instantiated.");
            }
            return Manager.getInstantiator(args.length)(cls, args);
        },
        /**
         * Convenient shorthand to create a widget by its xtype or a config object.
         *
         *      var button = Ext.widget('button'); // Equivalent to Ext.create('widget.button');
         *
         *      var panel = Ext.widget('panel', { // Equivalent to Ext.create('widget.panel')
         *          title: 'Panel'
         *      });
         *
         *      var grid = Ext.widget({
         *          xtype: 'grid',
         *          ...
         *      });
         *
         * If a {@link Ext.Component component} instance is passed, it is simply returned.
         *
         * @member Ext
         * @param {String} [name] The xtype of the widget to create.
         * @param {Object} [config] The configuration object for the widget constructor.
         * @return {Object} The widget instance
         */
        widget: function(name, config) {
            // forms:
            //      1: (xtype)
            //      2: (xtype, config)
            //      3: (config)
            //      4: (xtype, component)
            //      5: (component)
            //      
            var xtype = name,
                alias, className, T;
            if (typeof xtype !== 'string') {
                // if (form 3 or 5)
                // first arg is config or component
                config = name;
                // arguments[0]
                xtype = config.xtype;
                className = config.xclass;
            } else {
                config = config || {};
            }
            if (config.isComponent) {
                return config;
            }
            if (!className) {
                alias = 'widget.' + xtype;
                className = Manager.getNameByAlias(alias);
            }
            // this is needed to support demand loading of the class
            if (className) {
                T = Manager.get(className);
            }
            if (!T) {
                return Ext.create(className || alias, config);
            }
            return new T(config);
        },
        /**
         * @method createByAlias
         * @inheritdoc Ext.ClassManager#method-instantiateByAlias
         * @member Ext
         */
        createByAlias: alias(Manager, 'instantiateByAlias'),
        /**
         * Defines a class or override. A basic class is defined like this:
         *
         *      Ext.define('My.awesome.Class', {
         *          someProperty: 'something',
         *
         *          someMethod: function(s) {
         *              alert(s + this.someProperty);
         *          }
         *
         *          ...
         *      });
         *
         *      var obj = new My.awesome.Class();
         *
         *      obj.someMethod('Say '); // alerts 'Say something'
         *
         * To create an anonymous class, pass `null` for the `className`:
         *
         *      Ext.define(null, {
         *          constructor: function() {
         *              // ...
         *          }
         *      });
         *
         * In some cases, it is helpful to create a nested scope to contain some private
         * properties. The best way to do this is to pass a function instead of an object
         * as the second parameter. This function will be called to produce the class
         * body:
         *
         *      Ext.define('MyApp.foo.Bar', function() {
         *          var id = 0;
         *
         *          return {
         *              nextId: function() {
         *                  return ++id;
         *              }
         *          };
         *      });
         * 
         * _Note_ that when using override, the above syntax will not override successfully,
         * because the passed function would need to be executed first to determine whether or not
         * the result  is an override or defining a new object. As such, an alternative syntax that
         * immediately  invokes the function can be used:
         * 
         *      Ext.define('MyApp.override.BaseOverride', function() {
         *          var counter = 0;
         *
         *          return {
         *              override: 'Ext.Component',
         *              logId: function() {
         *                  console.log(++counter, this.id);
         *              }
         *          };
         *      }());
         * 
         *
         * When using this form of `Ext.define`, the function is passed a reference to its
         * class. This can be used as an efficient way to access any static properties you
         * may have:
         *
         *      Ext.define('MyApp.foo.Bar', function(Bar) {
         *          return {
         *              statics: {
         *                  staticMethod: function() {
         *                      // ...
         *                  }
         *              },
         *
         *              method: function() {
         *                  return Bar.staticMethod();
         *              }
         *          };
         *      });
         *
         * To define an override, include the `override` property. The content of an
         * override is aggregated with the specified class in order to extend or modify
         * that class. This can be as simple as setting default property values or it can
         * extend and/or replace methods. This can also extend the statics of the class.
         *
         * One use for an override is to break a large class into manageable pieces.
         *
         *      // File: /src/app/Panel.js
         *
         *      Ext.define('My.app.Panel', {
         *          extend: 'Ext.panel.Panel',
         *          requires: [
         *              'My.app.PanelPart2',
         *              'My.app.PanelPart3'
         *          ]
         *
         *          constructor: function(config) {
         *              this.callParent(arguments); // calls Ext.panel.Panel's constructor
         *              //...
         *          },
         *
         *          statics: {
         *              method: function() {
         *                  return 'abc';
         *              }
         *          }
         *      });
         *
         *      // File: /src/app/PanelPart2.js
         *      Ext.define('My.app.PanelPart2', {
         *          override: 'My.app.Panel',
         *
         *          constructor: function(config) {
         *              this.callParent(arguments); // calls My.app.Panel's constructor
         *              //...
         *          }
         *      });
         *
         * Another use of overrides is to provide optional parts of classes that can be
         * independently required. In this case, the class may even be unaware of the
         * override altogether.
         *
         *      Ext.define('My.ux.CoolTip', {
         *          override: 'Ext.tip.ToolTip',
         *
         *          constructor: function(config) {
         *              this.callParent(arguments); // calls Ext.tip.ToolTip's constructor
         *              //...
         *          }
         *      });
         *
         * The above override can now be required as normal.
         *
         *      Ext.define('My.app.App', {
         *          requires: [
         *              'My.ux.CoolTip'
         *          ]
         *      });
         *
         * Overrides can also contain statics, inheritableStatics, or privates:
         *
         *      Ext.define('My.app.BarMod', {
         *          override: 'Ext.foo.Bar',
         *
         *          statics: {
         *              method: function(x) {
         *                  return this.callParent([x * 2]); // call Ext.foo.Bar.method
         *              }
         *          }
         *      });
         * 
         * Starting in version 4.2.2, overrides can declare their `compatibility` based
         * on the framework version or on versions of other packages. For details on the
         * syntax and options for these checks, see `Ext.checkVersion`.
         * 
         * The simplest use case is to test framework version for compatibility:
         * 
         *      Ext.define('App.overrides.grid.Panel', {
         *          override: 'Ext.grid.Panel',
         *
         *          compatibility: '4.2.2', // only if framework version is 4.2.2
         *
         *          //...
         *      });
         * 
         * An array is treated as an OR, so if any specs match, the override is
         * compatible.
         * 
         *      Ext.define('App.overrides.some.Thing', {
         *          override: 'Foo.some.Thing',
         *
         *          compatibility: [
         *              '4.2.2',
         *              'foo@1.0.1-1.0.2'
         *          ],
         *
         *          //...
         *      });
         * 
         * To require that all specifications match, an object can be provided:
         * 
         *      Ext.define('App.overrides.some.Thing', {
         *          override: 'Foo.some.Thing',
         *
         *          compatibility: {
         *              and: [
         *                  '4.2.2',
         *                  'foo@1.0.1-1.0.2'
         *              ]
         *          },
         *
         *          //...
         *      });
         * 
         * Because the object form is just a recursive check, these can be nested:
         * 
         *      Ext.define('App.overrides.some.Thing', {
         *          override: 'Foo.some.Thing',
         *
         *          compatibility: {
         *              and: [
         *                  '4.2.2',  // exactly version 4.2.2 of the framework *AND*
         *                  {
         *                      // either (or both) of these package specs:
         *                      or: [
         *                          'foo@1.0.1-1.0.2',
         *                          'bar@3.0+'
         *                      ]
         *                  }
         *              ]
         *          },
         *
         *          //...
         *      });
         *
         * IMPORTANT: An override is only included in a build if the class it overrides is
         * required. Otherwise, the override, like the target class, is not included. In
         * Sencha Cmd v4, the `compatibility` declaration can likewise be used to remove
         * incompatible overrides from a build.
         *
         * @param {String} className The class name to create in string dot-namespaced format,
         * for example: 'My.very.awesome.Class', 'FeedViewer.plugin.CoolPager'
         * 
         * It is highly recommended to follow this simple convention:
         *  - The root and the class name are 'CamelCased'
         *  - Everything else is lower-cased
         * Pass `null` to create an anonymous class.
         * @param {Object} data The key - value pairs of properties to apply to this class.
         * Property names can be of any valid strings, except those in the reserved listed below:
         *  
         *  - {@link Ext.Class#cfg-alias alias}
         *  - {@link Ext.Class#cfg-alternateClassName alternateClassName}
         *  - {@link Ext.Class#cfg-cachedConfig cachedConfig}
         *  - {@link Ext.Class#cfg-config config}
         *  - {@link Ext.Class#cfg-extend extend}
         *  - {@link Ext.Class#cfg-inheritableStatics inheritableStatics}
         *  - {@link Ext.Class#cfg-mixins mixins}
         *  - {@link Ext.Class#cfg-override override}
         *  - {@link Ext.Class#cfg-platformConfig platformConfig}
         *  - {@link Ext.Class#cfg-privates privates}
         *  - {@link Ext.Class#cfg-requires requires}
         *  - `self`
         *  - {@link Ext.Class#cfg-singleton singleton}
         *  - {@link Ext.Class#cfg-statics statics}
         *  - {@link Ext.Class#cfg-uses uses}
         *  - {@link Ext.Class#cfg-xtype xtype} (for {@link Ext.Component Components} only)
         *
         * @param {Function} [createdFn] Callback to execute after the class is created,
         * the execution scope of which (`this`) will be the newly created class itself.
         * @return {Ext.Base}
         * @member Ext
         */
        define: function(className, data, createdFn) {
            if (Ext.classSystemMonitor) {
                Ext.classSystemMonitor(className, 'ClassManager#define', arguments);
            }
            if (data.override) {
                Manager.classState[className] = 20;
                return Manager.createOverride.apply(Manager, arguments);
            }
            Manager.classState[className] = 10;
            return Manager.create.apply(Manager, arguments);
        },
        /**
         * Undefines a class defined using the #define method. Typically used
         * for unit testing where setting up and tearing down a class multiple
         * times is required.  For example:
         * 
         *     // define a class
         *     Ext.define('Foo', {
         *        ...
         *     });
         *     
         *     // run test
         *     
         *     // undefine the class
         *     Ext.undefine('Foo');
         * @param {String} className The class name to undefine in string dot-namespaced format.
         * @private
         */
        undefine: function(className) {
            var classes = Manager.classes;
            if (Ext.classSystemMonitor) {
                Ext.classSystemMonitor(className, 'Ext.ClassManager#undefine', arguments);
            }
            if (classes[className]) {
                Manager.classCount--;
            }
            delete classes[className];
            delete Manager.existCache[className];
            delete Manager.classState[className];
            Manager.removeName(className);
            // Indiscriminately clear all factory caches here. It might be slightly inefficient
            // however undefine is typically only called during unit testing.
            Ext.Factory.clearCaches();
            /* eslint-disable-next-line vars-on-top */
            var entry = Manager.getNamespaceEntry(className),
                scope = entry.parent ? Manager.lookupName(entry.parent, false) : Ext.global,
                entryName;
            if (scope) {
                entryName = entry.name;
                // Old IE blows up on attempt to delete window property
                try {
                    delete scope[entryName];
                } catch (e) {
                    scope[entryName] = undefined;
                }
            }
            // We need to know entry name in unit tests
            return entryName;
        },
        /**
         * @method getClassName
         * @inheritdoc Ext.ClassManager#method-getName
         * @member Ext
         */
        getClassName: alias(Manager, 'getName'),
        /**
         * Returns the displayName property or className or object. When all else fails,
         * returns "Anonymous".
         * @param {Object} object
         * @return {String}
         */
        getDisplayName: function(object) {
            if (object) {
                if (object.displayName) {
                    return object.displayName;
                }
                if (object.$name && object.$class) {
                    return Ext.getClassName(object.$class) + '#' + object.$name;
                }
                if (object.$className) {
                    return object.$className;
                }
            }
            return 'Anonymous';
        },
        /**
         * @method getClass
         * @inheritdoc Ext.ClassManager#method-getClass
         * @member Ext
         */
        getClass: alias(Manager, 'getClass'),
        /**
         * Creates namespaces to be used for scoping variables and classes so that they are not
         * global. Specifying the last node of a namespace implicitly creates all other nodes.
         * Usage:
         *
         *     Ext.namespace('Company', 'Company.data');
         *
         *     // equivalent and preferable to the above syntax
         *     Ext.ns('Company.data');
         *
         *     Company.Widget = function() { ... };
         *
         *     Company.data.CustomStore = function(config) { ... };
         *
         * @param {String...} namespaces
         * @return {Object} The (last) namespace object created.
         * @member Ext
         * @method namespace
         */
        namespace: function() {
            var root = global,
                i;
            for (i = arguments.length; i-- > 0; ) {
                root = Manager.lookupName(arguments[i], true);
            }
            return root;
        }
    });
    /**
     * This function registers top-level (root) namespaces. This is needed for "sandbox"
     * builds.
     *
     *      Ext.addRootNamespaces({
     *          MyApp: MyApp,
     *          Common: Common
     *      });
     *
     * In the above example, `MyApp` and `Common` are top-level namespaces that happen
     * to also be included in the sandbox closure. Something like this:
     *
     *      (function(Ext) {
     *
     *      Ext.sandboxName = 'Ext6';
     *      Ext.isSandboxed = true;
     *      Ext.buildSettings = { baseCSSPrefix: "x6-", scopeResetCSS: true };
     *
     *      var MyApp = MyApp || {};
     *      Ext.addRootNamespaces({ MyApp: MyApp );
     *
     *      ... normal app.js goes here ...
     *
     *      })(this.Ext6 || (this.Ext6 = {}));
     *
     * The sandbox wrapper around the normally built `app.js` content has to take care
     * of introducing top-level namespaces as well as call this method.
     *
     * @param {Object} namespaces
     * @method addRootNamespaces
     * @member Ext
     * @since 6.0.0
     * @private
     */
    Ext.addRootNamespaces = Manager.addRootNamespaces;
    /**
     * Old name for {@link Ext#widget}.
     * @deprecated 5.0 Use {@link Ext#widget} instead.
     * @method createWidget
     * @member Ext
     * @private
     */
    Ext.createWidget = Ext.widget;
    /**
     * @method ns
     * Convenient alias for {@link Ext#namespace Ext.namespace}.
     * @inheritdoc Ext#method-namespace
     * @member Ext
     */
    Ext.ns = Ext.namespace;
    Class.registerPreprocessor('className', function(cls, data) {
        if ('$className' in data) {
            cls.$className = data.$className;
            cls.displayName = cls.$className;
        }
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(cls, 'Ext.ClassManager#classNamePreprocessor', arguments);
        }
    }, true, 'first');
    Class.registerPreprocessor('alias', function(cls, data) {
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(cls, 'Ext.ClassManager#aliasPreprocessor', arguments);
        }
        /* eslint-disable-next-line vars-on-top */
        var prototype = cls.prototype,
            xtypes = arrayFrom(data.xtype),
            aliases = arrayFrom(data.alias),
            widgetPrefix = 'widget.',
            widgetPrefixLength = widgetPrefix.length,
            xtypesChain = Array.prototype.slice.call(prototype.xtypesChain || []),
            xtypesMap = Ext.merge({}, prototype.xtypesMap || {}),
            i, ln, alias, xtype;
        for (i = 0 , ln = aliases.length; i < ln; i++) {
            alias = aliases[i];
            if (typeof alias !== 'string' || alias.length < 1) {
                throw new Error("[Ext.define] Invalid alias of: '" + alias + "' for class: '" + name + "'; must be a valid string");
            }
            if (alias.substring(0, widgetPrefixLength) === widgetPrefix) {
                xtype = alias.substring(widgetPrefixLength);
                Ext.Array.include(xtypes, xtype);
            }
        }
        cls.xtype = data.xtype = xtypes[0];
        data.xtypes = xtypes;
        for (i = 0 , ln = xtypes.length; i < ln; i++) {
            xtype = xtypes[i];
            if (!xtypesMap[xtype]) {
                xtypesMap[xtype] = true;
                xtypesChain.push(xtype);
            }
        }
        data.xtypesChain = xtypesChain;
        data.xtypesMap = xtypesMap;
        // Class is already extended at this point
        Ext.Function.interceptAfterOnce(cls, 'onClassCreated', function() {
            var cls = this,
                prototype = cls.prototype,
                mixins = prototype.mixins,
                key, mixin;
            if (Ext.classSystemMonitor) {
                Ext.classSystemMonitor(cls, 'Ext.ClassManager#aliasPreprocessor#afterClassCreated', arguments);
            }
            for (key in mixins) {
                if (mixins.hasOwnProperty(key)) {
                    mixin = mixins[key];
                    xtypes = mixin.xtypes;
                    if (xtypes) {
                        for (i = 0 , ln = xtypes.length; i < ln; i++) {
                            xtype = xtypes[i];
                            if (!xtypesMap[xtype]) {
                                xtypesMap[xtype] = true;
                                xtypesChain.push(xtype);
                            }
                        }
                    }
                }
            }
        });
        for (i = 0 , ln = xtypes.length; i < ln; i++) {
            xtype = xtypes[i];
            if (typeof xtype !== 'string' || xtype.length < 1) {
                throw new Error("[Ext.define] Invalid xtype of: '" + xtype + "' for class: '" + name + "'; must be a valid non-empty string");
            }
            Ext.Array.include(aliases, widgetPrefix + xtype);
        }
        data.alias = aliases;
    }, [
        'xtype',
        'alias'
    ]);
    // load the cmd-5 style app manifest metadata now, if available...
    if (Ext.manifest) {
        /* eslint-disable-next-line vars-on-top */
        var manifest = Ext.manifest,
            classes = manifest.classes,
            paths = manifest.paths,
            aliases = {},
            alternates = {},
            className, obj, name, path, baseUrl;
        if (paths) {
            // if the manifest paths were calculated as relative to the
            // bootstrap file, then we need to prepend Boot.baseUrl to the
            // paths before processing
            if (manifest.bootRelative) {
                baseUrl = Ext.Boot.baseUrl;
                for (path in paths) {
                    if (paths.hasOwnProperty(path)) {
                        paths[path] = baseUrl + paths[path];
                    }
                }
            }
            Manager.setPath(paths);
        }
        if (classes) {
            for (className in classes) {
                alternates[className] = [];
                aliases[className] = [];
                obj = classes[className];
                if (obj.alias) {
                    aliases[className] = obj.alias;
                }
                if (obj.alternates) {
                    alternates[className] = obj.alternates;
                }
            }
        }
        Manager.addAlias(aliases);
        Manager.addAlternate(alternates);
    }
    return Manager;
}(Ext.Class, Ext.Function.alias, Array.prototype.slice, Ext.Array.from, Ext.global));

// @tag class
/**
 *
 * @since 6.7.0
 * @private
 */
Ext.define('Ext.mixin.Watchable', {
    on: function(name, fn, scope) {
        return this._watchUpdate(false, '_watchAdd', name, fn, scope);
    },
    fire: function(event, args) {
        var me = this,
            watching = me.watching,
            watchers = watching && watching[event],
            fn, i, r, scope;
        if (watchers) {
            ++watchers.$firing;
            for (i = 0; i < watchers.length; ++i) {
                scope = watchers[i][0];
                fn = watchers[i][1];
                if (fn.charAt) {
                    r = args ? scope[fn].apply(scope, args) : scope[fn]();
                } else {
                    r = args ? fn.apply(scope, args) : fn.call(scope);
                }
                if (r === false) {
                    return r;
                }
            }
            --watchers.$firing;
        }
    },
    fireEvent: function() {
        var args = Ext.Array.slice(arguments),
            event = args.shift();
        return this.fire(event, args);
    },
    un: function(name, fn, scope) {
        return this._watchUpdate(true, '_watchRemove', name, fn, scope);
    },
    privates: {
        watching: null,
        $watchOptions: {
            destroyable: 1,
            scope: 1
        },
        _watchAdd: function(watching, name, fn, scope, destroyable) {
            if (typeof fn === 'string' && !scope[fn]) {
                Ext.raise('No such method "' + fn + '" on ' + scope.$className);
            }
            // eslint-disable-next-line vars-on-top
            var watchers = watching[name],
                entry = [
                    scope,
                    fn
                ],
                i, ent;
            if (!watchers) {
                watching[name] = watchers = [];
                watchers.$firing = 0;
            } else {
                // If the scope/fn pair is already registered, don't duplicate it.
                for (i = watchers.length; i-- > 0; ) /* empty */
                {
                    ent = watchers[i];
                    if (fn === ent[1]) {
                        if (scope ? ent[0] === scope : !ent[0]) {
                            return;
                        }
                    }
                }
                if (watchers.$firing) {
                    watching[name] = watchers = watchers.slice();
                    watchers.$firing = 0;
                }
            }
            watchers.push(entry);
            if (destroyable) {
                entry.push(name);
                destroyable.items.push(entry);
            }
        },
        _watchRemove: function(watching, name, fn, scope) {
            var watchers = watching[name],
                i;
            if (watchers) {
                if (watchers.$firing) {
                    watching[name] = watchers = watchers.slice();
                    watchers.$firing = 0;
                }
                for (i = watchers.length; i-- > 0; ) /* empty */
                {
                    if (watchers[i][0] === scope && watchers[i][1] === fn) {
                        watchers.splice(i, 1);
                    }
                }
            }
        },
        _watchUpdate: function(remove, process, name, fn, scope) {
            var me = this,
                watch = name,
                watching = me.watching,
                destroyable;
            if (!watching) {
                if (remove) {
                    return;
                }
                me.watching = watching = {};
            }
            if (typeof name === 'string') {
                me[process](watching, name, fn, scope);
            } else {
                destroyable = watch.destroyable ? {
                    owner: me,
                    items: [],
                    destroy: me._watcherDestroyer
                } : null;
                scope = watch.scope;
                for (name in watch) {
                    if (!me.$watchOptions[name]) {
                        me[process](watching, name, watch[name], scope, destroyable);
                    }
                }
            }
            return destroyable;
        },
        _watcherDestroyer: function() {
            var me = this.owner,
                watching = me.watching,
                items = this.items,
                entry, i;
            for (i = 0; i < items.length; ++i) {
                entry = items[i];
                me._watchRemove(watching, entry[2], entry[1], entry[0]);
            }
        }
    }
});

/**
 * @class Ext.env.Browser
 * Provides information about browser.
 *
 * Should not be manually instantiated unless for unit-testing.
 * Access the global instance stored in {@link Ext.browser} instead.
 * @private
 */
(Ext.env || (Ext.env = {})).Browser = function(userAgent, publish) {
    // @define Ext.env.Browser
    // @define Ext.browser
    // @require Ext.Object
    // @require Ext.Version
    var me = this,
        browserPrefixes = Ext.Boot.browserPrefixes,
        browserNames = Ext.Boot.browserNames,
        enginePrefixes = me.enginePrefixes,
        engineNames = me.engineNames,
        browserMatch = userAgent.match(new RegExp('((?:' + Ext.Object.getValues(browserPrefixes).join(')|(?:') + '))([\\w\\._]+)')),
        engineMatch = userAgent.match(new RegExp('((?:' + Ext.Object.getValues(enginePrefixes).join(')|(?:') + '))([\\w\\._]+)')),
        browserName = browserNames.other,
        engineName = engineNames.other,
        browserVersion = '',
        engineVersion = '',
        majorVer = '',
        isWebView = false,
        edgeRE = /(Edge\/)([\w.]+)/,
        ripple = '',
        i, prefix, name;
    /**
     * @property {String}
     * Browser User Agent string.
     */
    me.userAgent = userAgent;
    /**
     * A "hybrid" property, can be either accessed as a method call, for example:
     *
     *     if (Ext.browser.is('IE')) {
     *         // ...
     *     }
     *
     * Or as an object with Boolean properties, for example:
     *
     *     if (Ext.browser.is.IE) {
     *         // ...
     *     }
     *
     * Versions can be conveniently checked as well. For example:
     *
     *     if (Ext.browser.is.IE10) {
     *         // Equivalent to (Ext.browser.is.IE && Ext.browser.version.equals(10))
     *     }
     *
     * __Note:__ Only {@link Ext.Version#getMajor major component} and
     * {@link Ext.Version#getShortVersion simplified} value of the version are available via
     * direct property checking.
     *
     * Supported values are:
     *
     * - IE
     * - Firefox
     * - Safari
     * - Chrome
     * - Opera
     * - WebKit
     * - Gecko
     * - Presto
     * - Trident
     * - WebView
     * - Other
     *
     * @param {String} name The OS name to check.
     * @return {Boolean}
     */
    this.is = function(name) {
        // Since this function reference also acts as a map, we do not want it to be
        // shared between instances, so it is defined here, not on the prototype.
        return !!this.is[name];
    };
    // Edge has a userAgent with All browsers so we manage it separately
    // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)
    // Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240"
    if (/Edge\//.test(userAgent)) {
        browserMatch = userAgent.match(edgeRE);
        engineMatch = userAgent.match(edgeRE);
    }
    if (browserMatch) {
        browserName = browserNames[Ext.Object.getKey(browserPrefixes, browserMatch[1])];
        if (browserName === 'Safari' && /^Opera/.test(userAgent)) {
            // Prevent Opera 12 and earlier from being incorrectly reported as Safari
            browserName = 'Opera';
        }
        browserVersion = new Ext.Version(browserMatch[2]);
    }
    if (engineMatch) {
        engineName = engineNames[Ext.Object.getKey(enginePrefixes, engineMatch[1])];
        engineVersion = new Ext.Version(engineMatch[2]);
    }
    if (engineName === 'Trident' && browserName !== 'IE') {
        browserName = 'IE';
        var version = userAgent.match(/.*rv:(\d+.\d+)/);
        // eslint-disable-line vars-on-top
        if (version && version.length) {
            version = version[1];
            browserVersion = new Ext.Version(version);
        }
    }
    if (browserName && browserVersion) {
        Ext.setVersion(browserName, browserVersion);
    }
    /**
     * @property chromeVersion
     * The current version of Chrome (0 if the browser is not Chrome).
     * @readonly
     * @type Number
     * @member Ext
     */
    /**
     * @property firefoxVersion
     * The current version of Firefox (0 if the browser is not Firefox).
     * @readonly
     * @type Number
     * @member Ext
     */
    /**
     * @property ieVersion
     * The current version of IE (0 if the browser is not IE). This does not account
     * for the documentMode of the current page, which is factored into {@link #isIE8},
     * and {@link #isIE9}. Thus this is not always true:
     *
     *     Ext.isIE8 == (Ext.ieVersion == 8)
     *
     * @readonly
     * @type Number
     * @member Ext
     */
    /**
     * @property isChrome
     * True if the detected browser is Chrome.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isGecko
     * True if the detected browser uses the Gecko layout engine (e.g. Mozilla, Firefox).
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isIE
     * True if the detected browser is Internet Explorer.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isIE8
     * True if the detected browser is Internet Explorer 8.x.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isIE8m
     * True if the detected browser is Internet Explorer 8.x or lower.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isIE8p
     * True if the detected browser is Internet Explorer 8.x or higher.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isIE9
     * True if the detected browser is Internet Explorer 9.x.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isIE9m
     * True if the detected browser is Internet Explorer 9.x or lower.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isIE9p
     * True if the detected browser is Internet Explorer 9.x or higher.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isIE10
     * True if the detected browser is Internet Explorer 10.x.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isIE10m
     * True if the detected browser is Internet Explorer 10.x or lower.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isIE10p
     * True if the detected browser is Internet Explorer 10.x or higher.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isIE11
     * True if the detected browser is Internet Explorer 11.x.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isIE11m
     * True if the detected browser is Internet Explorer 11.x or lower.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isIE11p
     * True if the detected browser is Internet Explorer 11.x or higher.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isEdge
     * True if the detected browser is Edge.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isLinux
     * True if the detected platform is Linux.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isMac
     * True if the detected platform is Mac OS.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isOpera
     * True if the detected browser is Opera.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isSafari
     * True if the detected browser is Safari.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isWebKit
     * True if the detected browser uses WebKit.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property isWindows
     * True if the detected platform is Windows.
     * @readonly
     * @type Boolean
     * @member Ext
     */
    /**
     * @property operaVersion
     * The current version of Opera (0 if the browser is not Opera).
     * @readonly
     * @type Number
     * @member Ext
     */
    /**
     * @property safariVersion
     * The current version of Safari (0 if the browser is not Safari).
     * @readonly
     * @type Number
     * @member Ext
     */
    /**
     * @property webKitVersion
     * The current version of WebKit (0 if the browser does not use WebKit).
     * @readonly
     * @type Number
     * @member Ext
     */
    // Facebook changes the userAgent when you view a website within their iOS app.
    // For some reason, the strip out information about the browser, so we have to detect
    // that and fake it...
    if (userAgent.match(/FB/) && browserName === 'Other') {
        browserName = browserNames.safari;
        engineName = engineNames.webkit;
    }
    // Detect chrome first as Chrome in Android 8.0 introduced OPR in the user agent
    else if (userAgent.match(/Android.*Chrome/g)) {
        browserName = 'ChromeMobile';
    } else {
        browserMatch = userAgent.match(/OPR\/(\d+.\d+)/);
        if (browserMatch) {
            browserName = 'Opera';
            browserVersion = new Ext.Version(browserMatch[1]);
        }
    }
    Ext.apply(this, {
        engineName: engineName,
        engineVersion: engineVersion,
        name: browserName,
        version: browserVersion
    });
    this.setFlag(browserName, true, publish);
    // e.g., Ext.isIE
    if (browserVersion) {
        majorVer = browserVersion.getMajor() || '';
        if (me.is.IE) {
            majorVer = document.documentMode || parseInt(majorVer, 10);
            for (i = 7; i <= 11; ++i) {
                prefix = 'isIE' + i;
                Ext[prefix] = majorVer === i;
                Ext[prefix + 'm'] = majorVer <= i;
                Ext[prefix + 'p'] = majorVer >= i;
            }
        }
        if (me.is.Opera && parseInt(majorVer, 10) <= 12) {
            Ext.isOpera12m = true;
        }
        Ext.chromeVersion = Ext.isChrome ? majorVer : 0;
        Ext.firefoxVersion = Ext.isFirefox ? majorVer : 0;
        Ext.ieVersion = Ext.isIE ? majorVer : 0;
        Ext.operaVersion = Ext.isOpera ? majorVer : 0;
        Ext.safariVersion = Ext.isSafari ? majorVer : 0;
        Ext.webKitVersion = Ext.isWebKit ? majorVer : 0;
        this.setFlag(browserName + majorVer, true, publish);
        // Ext.isIE10
        this.setFlag(browserName + browserVersion.getShortVersion());
    }
    for (i in browserNames) {
        if (browserNames.hasOwnProperty(i)) {
            name = browserNames[i];
            this.setFlag(name, browserName === name);
        }
    }
    this.setFlag(name);
    if (engineVersion) {
        this.setFlag(engineName + (engineVersion.getMajor() || ''));
        this.setFlag(engineName + engineVersion.getShortVersion());
    }
    for (i in engineNames) {
        if (engineNames.hasOwnProperty(i)) {
            name = engineNames[i];
            this.setFlag(name, engineName === name, publish);
        }
    }
    this.setFlag('Standalone', !!navigator.standalone);
    // Cross domain access could throw an error
    try {
        ripple = window.top.ripple;
    } catch (e) {}
    // Do nothing, can't access cross frame so leave it empty
    /* eslint-disable-next-line max-len */
    this.setFlag('Ripple', !!document.getElementById("tinyhippos-injected") && !Ext.isEmpty(ripple));
    this.setFlag('WebWorks', !!window.blackberry);
    if (window.PhoneGap !== undefined || window.Cordova !== undefined || window.cordova !== undefined) {
        isWebView = true;
        this.setFlag('PhoneGap');
        this.setFlag('Cordova');
    }
    // Check if running in UIWebView
    if (/(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)(?!.*FBAN)/i.test(userAgent)) {
        isWebView = true;
    }
    // Flag to check if it we are in the WebView
    this.setFlag('WebView', isWebView);
    /**
     * @property {Boolean}
     * `true` if browser is using strict mode.
     */
    this.isStrict = Ext.isStrict = document.compatMode === "CSS1Compat";
    /**
     * @property {Boolean}
     * `true` if page is running over SSL.
     */
    this.isSecure = Ext.isSecure;
    // IE10Quirks, Chrome26Strict, etc.
    this.identity = browserName + majorVer + (this.isStrict ? 'Strict' : 'Quirks');
};
Ext.env.Browser.prototype = {
    constructor: Ext.env.Browser,
    engineNames: {
        edge: 'Edge',
        webkit: 'WebKit',
        gecko: 'Gecko',
        presto: 'Presto',
        trident: 'Trident',
        other: 'Other'
    },
    enginePrefixes: {
        edge: 'Edge/',
        webkit: 'AppleWebKit/',
        gecko: 'Gecko/',
        presto: 'Presto/',
        trident: 'Trident/'
    },
    styleDashPrefixes: {
        WebKit: '-webkit-',
        Gecko: '-moz-',
        Trident: '-ms-',
        Presto: '-o-',
        Other: ''
    },
    stylePrefixes: {
        WebKit: 'Webkit',
        Gecko: 'Moz',
        Trident: 'ms',
        Presto: 'O',
        Other: ''
    },
    propertyPrefixes: {
        WebKit: 'webkit',
        Gecko: 'moz',
        Trident: 'ms',
        Presto: 'o',
        Other: ''
    },
    // scope: Ext.env.Browser.prototype
    /**
     * The full name of the current browser.
     * Possible values are:
     *
     * - IE
     * - Firefox
     * - Safari
     * - Chrome
     * - Opera
     * - Other
     * @type String
     * @readonly
     */
    name: null,
    /**
     * Refer to {@link Ext.Version}.
     * @type Ext.Version
     * @readonly
     */
    version: null,
    /**
     * The full name of the current browser's engine.
     * Possible values are:
     *
     * - WebKit
     * - Gecko
     * - Presto
     * - Trident
     * - Other
     * @type String
     * @readonly
     */
    engineName: null,
    /**
     * Refer to {@link Ext.Version}.
     * @type Ext.Version
     * @readonly
     */
    engineVersion: null,
    setFlag: function(name, value, publish) {
        if (value === undefined) {
            value = true;
        }
        this.is[name] = value;
        this.is[name.toLowerCase()] = value;
        if (publish) {
            Ext['is' + name] = value;
        }
        return this;
    },
    getStyleDashPrefix: function() {
        return this.styleDashPrefixes[this.engineName];
    },
    getStylePrefix: function() {
        return this.stylePrefixes[this.engineName];
    },
    getVendorProperyName: function(name) {
        var prefix = this.propertyPrefixes[this.engineName];
        if (prefix.length > 0) {
            return prefix + Ext.String.capitalize(name);
        }
        return name;
    }
};
/**
 * @class Ext.browser
 * @extends Ext.env.Browser
 * @singleton
 * Provides useful information about the current browser.
 *
 * Example:
 *
 *     if (Ext.browser.is.IE) {
 *         // IE specific code here
 *     }
 *
 *     if (Ext.browser.is.WebKit) {
 *         // WebKit specific code here
 *     }
 *
 *     console.log("Version " + Ext.browser.version);
 *
 * For a full list of supported values, refer to {@link #is} property/method.
 *
 */
(function(userAgent) {
    Ext.browser = new Ext.env.Browser(userAgent, true);
    Ext.userAgent = userAgent.toLowerCase();
    /**
 * @property {String} SSL_SECURE_URL
 * URL to a blank file used by Ext when in secure mode for iframe src and onReady src
 * to prevent the IE insecure content warning (`'about:blank'`, except for IE
 * in secure mode, which is `'javascript:""'`).
 * @member Ext
 */
    Ext.SSL_SECURE_URL = Ext.isSecure && Ext.isIE ? 'javascript:\'\'' : 'about:blank';
}(Ext.global.navigator.userAgent));

/**
 * @class Ext.env.OS
 *
 * Provides information about operating system environment.
 *
 * Should not be manually instantiated unless for unit-testing.
 * Access the global instance stored in {@link Ext.os} instead.
 * @private
 */
Ext.env.OS = function(userAgent, platform, browserScope) {
    // @define Ext.env.OS
    // @define Ext.os
    // @require Ext.Version
    // @require Ext.env.Browser
    var me = this,
        names = Ext.Boot.osNames,
        prefixes = Ext.Boot.osPrefixes,
        name,
        version = '',
        is = me.is,
        i, prefix, match, item, match1;
    browserScope = browserScope || Ext.browser;
    for (i in prefixes) {
        if (prefixes.hasOwnProperty(i)) {
            prefix = prefixes[i];
            match = userAgent.match(new RegExp('(?:' + prefix + ')([^\\s;]+)'));
            if (match) {
                name = names[i];
                match1 = match[1];
                // This is here because some HTC android devices show an OSX Snow Leopard
                // userAgent by default.
                // And the Kindle Fire doesn't have any indicator of Android as the OS in its
                // User Agent
                if (match1 && match1 === "HTC_") {
                    version = new Ext.Version("2.3");
                } else if (match1 && match1 === "Silk/") {
                    version = new Ext.Version("2.3");
                } else {
                    version = new Ext.Version(match[match.length - 1]);
                }
                break;
            }
        }
    }
    if (!name) {
        name = names[(userAgent.toLowerCase().match(/mac|win|linux/) || [
            'other'
        ])[0]];
        version = new Ext.Version('');
    }
    this.name = name;
    this.version = version;
    // This is added as a workaround for Chrome iPad emulation mode
    // it will report the platform of the machine (MacIntel, Win32, etc) instead
    // of an emulated platform
    if (userAgent.match(/ipad/i)) {
        platform = 'iPad';
    }
    if (platform) {
        this.setFlag(platform.replace(/ simulator$/i, ''));
    }
    this.setFlag(name);
    if (version) {
        this.setFlag(name + (version.getMajor() || ''));
        this.setFlag(name + version.getShortVersion());
    }
    for (i in names) {
        if (names.hasOwnProperty(i)) {
            item = names[i];
            if (!is.hasOwnProperty(name)) {
                this.setFlag(item, (name === item));
            }
        }
    }
    // Detect if the device is the iPhone 5.
    if (this.name === "iOS" && window.screen.height === 568) {
        this.setFlag('iPhone5');
    }
    if (browserScope.is.Safari || browserScope.is.Silk) {
        // Ext.browser.version.shortVersion == 501 is for debugging off device
        if (this.is.Android2 || this.is.Android3 || browserScope.version.shortVersion === 501) {
            browserScope.setFlag("AndroidStock");
        }
        if (this.is.Android4) {
            browserScope.setFlag("AndroidStock");
            browserScope.setFlag("AndroidStock4");
        }
    }
};
Ext.env.OS.prototype = {
    constructor: Ext.env.OS,
    /**
     * A "hybrid" property, can be either accessed as a method call, i.e:
     *
     *     if (Ext.os.is('Android')) {
     *         // ...
     *     }
     *
     * or as an object with boolean properties, i.e:
     *
     *     if (Ext.os.is.Android) {
     *         // ...
     *     }
     *
     * Versions can be conveniently checked as well. For example:
     *
     *     if (Ext.os.is.Android2) {
     *         // Equivalent to (Ext.os.is.Android && Ext.os.version.equals(2))
     *     }
     *
     *     if (Ext.os.is.iOS32) {
     *         // Equivalent to (Ext.os.is.iOS && Ext.os.version.equals(3.2))
     *     }
     *
     * Note that only {@link Ext.Version#getMajor major component} and
     * {@link Ext.Version#getShortVersion simplified} value of the version are available
     * via direct property checking. Supported values are:
     *
     * - iOS
     * - iPad
     * - iPhone
     * - iPhone5 (also true for 4in iPods).
     * - iPod
     * - Android
     * - WebOS
     * - BlackBerry
     * - Bada
     * - MacOS
     * - Windows
     * - Linux
     * - Other
     * @member Ext.os
     * @param {String} name The OS name to check.
     * @return {Boolean}
     */
    is: function(name) {
        return !!this[name];
    },
    /**
     * @property {String} [name=null]
     * @readonly
     * @member Ext.os
     * The full name of the current operating system. Possible values are:
     *
     * - iOS
     * - Android
     * - WebOS
     * - BlackBerry,
     * - MacOS
     * - Windows
     * - Linux
     * - Other
     */
    name: null,
    /**
     * @property {Ext.Version} [version=null]
     * Refer to {@link Ext.Version}
     * @member Ext.os
     * @readonly
     */
    version: null,
    setFlag: function(name, value) {
        if (value === undefined) {
            value = true;
        }
        if (this.flags) {
            this.flags[name] = value;
        }
        this.is[name] = value;
        this.is[name.toLowerCase()] = value;
        return this;
    }
};
(function() {
    var navigation = Ext.global.navigator,
        userAgent = navigation.userAgent,
        OS = Ext.env.OS,
        is = (Ext.is || (Ext.is = {})),
        osEnv, osName, deviceType;
    OS.prototype.flags = is;
    /**
 * @class Ext.os
 * @extends Ext.env.OS
 * @singleton
 * Provides useful information about the current operating system environment.
 *
 * Example:
 *
 *     if (Ext.os.is.Windows) {
 *         // Windows specific code here
 *     }
 *
 *     if (Ext.os.is.iOS) {
 *         // iPad, iPod, iPhone, etc.
 *     }
 *
 *     console.log("Version " + Ext.os.version);
 *
 * For a full list of supported values, refer to the {@link #is} property/method.
 *
 */
    Ext.os = osEnv = new OS(userAgent, navigation.platform);
    osName = osEnv.name;
    // A couple compatible flavors:
    Ext['is' + osName] = true;
    // e.g., Ext.isWindows
    Ext.isMac = is.Mac = is.MacOS;
    Ext.isApple = Ext.isMac || Ext.isiOS;
    // eslint-disable-next-line vars-on-top
    var search = window.location.search.match(/deviceType=(Tablet|Phone)/),
        nativeDeviceType = window.deviceType;
    // Override deviceType by adding a get variable of deviceType. NEEDED FOR DOCS APP.
    // E.g: example/kitchen-sink.html?deviceType=Phone
    if (search && search[1]) {
        deviceType = search[1];
    } else if (nativeDeviceType === 'iPhone') {
        deviceType = 'Phone';
    } else if (nativeDeviceType === 'iPad') {
        deviceType = 'Tablet';
    } else {
        if (!osEnv.is.Android && !osEnv.is.iOS && !osEnv.is.WindowsPhone && /Windows|Linux|MacOS|ChromeOS/.test(osName)) {
            deviceType = 'Desktop';
            // always set it to false when you are on a desktop not using Ripple Emulation
            Ext.browser.is.WebView = !!Ext.browser.is.Ripple;
        } else if (osEnv.is.iPad || osEnv.is.RIMTablet || osEnv.is.Android3 || Ext.browser.is.Silk || (osEnv.is.Android && userAgent.search(/mobile/i) === -1)) {
            deviceType = 'Tablet';
        } else {
            deviceType = 'Phone';
        }
    }
    /**
 * @property {String} deviceType
 * The generic type of the current device.
 *
 * Possible values:
 *
 * - Phone
 * - Tablet
 * - Desktop
 *
 * For testing purposes the deviceType can be overridden by adding
 * a deviceType parameter to the URL of the page, like so:
 *
 *     http://localhost/mypage.html?deviceType=Tablet
 *
 * @member Ext.os
 */
    osEnv.setFlag(deviceType, true);
    osEnv.deviceType = deviceType;
    delete OS.prototype.flags;
}());

/**
 * @class Ext.feature
 * @singleton
 *
 * A simple class to verify if a browser feature exists or not on the current device.
 *
 *     if (Ext.feature.has.Canvas) {
 *         // do some cool things with canvas here
 *     }
 *
 * See the {@link #has} property/method for details of the features that can be detected.
 *
 */
/* eslint-disable vars-on-top */
Ext.feature = {
    // @define Ext.env.Feature
    // @define Ext.feature
    // @define Ext.supports
    // @require Ext.String
    // @require Ext.env.Browser
    // @require Ext.env.OS
    /**
     * @method has
     * @member Ext.feature
     * Verifies if a browser feature exists or not on the current device.
     *
     * A "hybrid" property, can be either accessed as a method call, i.e:
     *
     *     if (Ext.feature.has('Canvas')) {
     *         // ...
     *     }
     *
     * or as an object with boolean properties, i.e:
     *
     *     if (Ext.feature.has.Canvas) {
     *         // ...
     *     }
     *
     * For possible properties/parameter values see `Ext.supports`.
     *
     * @param {String} name The feature name to check.
     * @return {Boolean}
     */
    has: function(name) {
        return !!this.has[name];
    },
    testElements: {},
    getTestElement: function(tag, createNew) {
        if (tag === undefined) {
            tag = 'div';
        } else if (typeof tag !== 'string') {
            return tag;
        }
        if (createNew) {
            return document.createElement(tag);
        }
        if (!this.testElements[tag]) {
            this.testElements[tag] = document.createElement(tag);
        }
        return this.testElements[tag];
    },
    isStyleSupported: function(name, tag) {
        var elementStyle = this.getTestElement(tag).style,
            cName = Ext.String.capitalize(name);
        if (typeof elementStyle[name] !== 'undefined' || typeof elementStyle[Ext.browser.getStylePrefix(name) + cName] !== 'undefined') {
            return true;
        }
        return false;
    },
    isStyleSupportedWithoutPrefix: function(name, tag) {
        var elementStyle = this.getTestElement(tag).style;
        if (typeof elementStyle[name] !== 'undefined') {
            return true;
        }
        return false;
    },
    isEventSupported: function(name, tag) {
        if (tag === undefined) {
            tag = window;
        }
        var element = this.getTestElement(tag),
            eventName = 'on' + name.toLowerCase(),
            isSupported = (eventName in element);
        if (!isSupported) {
            if (element.setAttribute && element.removeAttribute) {
                element.setAttribute(eventName, '');
                isSupported = typeof element[eventName] === 'function';
                if (typeof element[eventName] !== 'undefined') {
                    element[eventName] = undefined;
                }
                element.removeAttribute(eventName);
            }
        }
        return isSupported;
    },
    // This is a local copy of certain logic from Element.getStyle
    // to break a dependancy between the supports mechanism and Element
    // use this instead of element references to check for styling info
    getStyle: function(element, styleName) {
        var view = element.ownerDocument.defaultView,
            style = (view ? view.getComputedStyle(element, null) : element.currentStyle);
        return (style || element.style)[styleName];
    },
    getSupportedPropertyName: function(object, name) {
        var vendorName = Ext.browser.getVendorProperyName(name);
        if (vendorName in object) {
            return vendorName;
        } else if (name in object) {
            return name;
        }
        return null;
    },
    /**
     * Runs feature detection routines and sets the various flags. This is called when
     * the scripts loads (very early) and again at {@link Ext#onReady}. Some detections
     * can be run immediately. Others that require the document body will not run until
     * domready (these have the `ready` flag set).
     *
     * Each test is run only once, so calling this method from an onReady function is safe
     * and ensures that all flags have been set.
     * @private
     */
    detect: function(isReady) {
        var me = this,
            doc = document,
            toRun = me.toRun || me.tests,
            n = toRun.length,
            div = doc.createElement('div'),
            notRun = [],
            supports = Ext.supports,
            has = me.has,
            name, names, test, vector, value;
        // Only the legacy browser tests use this div so clip this out if we don't need
        // to use it.
        div.innerHTML = '<div style="height:30px;width:50px;">' + '<div style="height:20px;width:20px;"></div>' + '</div>' + '<div style="width: 200px; height: 200px; position: relative; padding: 5px;">' + '<div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></div>' + // eslint-disable-line max-len
        '</div>' + '<div style="position: absolute; left: 10%; top: 10%;"></div>' + '<div style="float:left; background-color:transparent;"></div>';
        if (isReady) {
            doc.body.appendChild(div);
        }
        vector = me.preDetected[Ext.browser.identity] || [];
        while (n--) {
            test = toRun[n];
            value = vector[n];
            name = test.name;
            names = test.names;
            if (value === undefined) {
                if (!isReady && test.ready) {
                    // test requires domready state
                    notRun.push(test);
                    
                    continue;
                }
                value = test.fn.call(me, doc, div);
            }
            // Store test results on Ext.supports and Ext.feature.has
            if (name) {
                supports[name] = has[name] = value;
            } else if (names) {
                while (names.length) {
                    name = names.pop();
                    supports[name] = has[name] = value;
                }
            }
        }
        if (isReady) {
            doc.body.removeChild(div);
        }
        me.toRun = notRun;
    },
    //</debug>
    report: function() {
        var values = [],
            len = this.tests.length,
            i;
        for (i = 0; i < len; ++i) {
            values.push(this.has[this.tests[i].name] ? 1 : 0);
        }
        Ext.log(Ext.browser.identity + ': [' + values.join(',') + ']');
    },
    //</debug>
    preDetected: {},
    // TODO
    /**
     * @class Ext.supports
     *
     * Contains information about features supported in the current environment as well
     * as bugs detected.
     *
     * @singleton
     */
    tests: [
        {
            /**
         * @property CloneNodeCopiesExpando `true` if the native DOM cloneNode method copies
         * expando properties to the newly cloned node.
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'CloneNodeCopiesExpando',
            fn: function() {
                var el = document.createElement('div');
                el.expandoProp = {};
                return el.cloneNode().expandoProp === el.expandoProp;
            }
        },
        {
            /**
         * @property CSSPointerEvents `true` if document environment supports the CSS3
         * pointer-events style.
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'CSSPointerEvents',
            fn: function(doc) {
                return 'pointerEvents' in doc.documentElement.style;
            }
        },
        {
            /**
         * @property CSS3BoxShadow `true` if document environment supports the CSS3
         * box-shadow style.
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'CSS3BoxShadow',
            fn: function(doc) {
                return 'boxShadow' in doc.documentElement.style || 'WebkitBoxShadow' in doc.documentElement.style || 'MozBoxShadow' in doc.documentElement.style;
            }
        },
        {
            name: 'CSS3NegationSelector',
            fn: function(doc) {
                try {
                    doc.querySelectorAll("foo:not(bar)");
                } catch (e) {
                    return false;
                }
                return true;
            }
        },
        {
            /**
         * @property ClassList `true` if document environment supports the HTML5
         * classList API.
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'ClassList',
            fn: function(doc) {
                return !!doc.documentElement.classList;
            }
        },
        {
            /**
         * @property Canvas `true` if the device supports Canvas.
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'Canvas',
            fn: function() {
                var element = this.getTestElement('canvas');
                return !!(element && element.getContext && element.getContext('2d'));
            }
        },
        {
            /**
         * @property Svg `true` if the device supports SVG.
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'Svg',
            fn: function(doc) {
                /* eslint-disable-next-line max-len */
                return !!(doc.createElementNS && !!doc.createElementNS("http:/" + "/www.w3.org/2000/svg", "svg").createSVGRect);
            }
        },
        {
            /**
         * @property Vml `true` if the device supports VML.
         * @type {Boolean}
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'Vml',
            fn: function() {
                var element = this.getTestElement(),
                    ret = false;
                element.innerHTML = "<!--[if vml]><br><![endif]-->";
                ret = (element.childNodes.length === 1);
                element.innerHTML = "";
                return ret;
            }
        },
        {
            /**
         * @property {Boolean} Touch `true` if the browser supports touch input.
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'Touch',
            fn: function() {
                // IE10 uses a vendor-prefixed maxTouchPoints property
                var maxTouchPoints = navigator.msMaxTouchPoints || navigator.maxTouchPoints;
                // if the browser has touch events we can be reasonably sure the device has
                // a touch screen
                // browsers that use pointer event have maxTouchPoints > 1 if the
                // device supports touch input
                // Chrome Desktop < 39 reports maxTouchPoints === 1 even if there is no
                // touch support on the device
                // http://www.w3.org/TR/pointerevents/#widl-Navigator-maxTouchPoints
                // Chrome Desktop > 39 properly reports maxTouchPoints === 0 and
                // Chrome Desktop Device Emulation mode reports maxTouchPoints === 1
                if (Ext.browser.is.Chrome && Ext.browser.version.isLessThanOrEqual(39)) {
                    return (Ext.supports.TouchEvents && maxTouchPoints !== 1) || maxTouchPoints > 1;
                } else {
                    return Ext.supports.TouchEvents || maxTouchPoints > 0;
                }
            }
        },
        {
            /**
         * @property {Boolean} PointerEvents
         * @type {Boolean}
         * @private
         *
         * `true` If the event system should use [pointer events](https://www.w3.org/TR/pointerevents/).
         * Currently only set to true if the browser supports pointer events and does not
         * also support touch events.  Touch events are preferred since they allow run-time
         * cancellation of browser default behavior such as scrolling by invoking
         * `e.preventDefault()` whereas pointer events require such intentions to be declared
         * in advance via CSS [touch-action](https://www.w3.org/TR/pointerevents/#h3_the-touch-action-css-property).
         * This means that when pointer events are used, certain interactions are not possible
         * such as long-press to drag within a scrollable element.
         */
            name: 'PointerEvents',
            fn: function() {
                var pointerEvent = window.PointerEvent,
                    nav = window.navigator,
                    pointerEnabled = !!(pointerEvent && (nav.pointerEnabled || !Ext.isIE));
                return pointerEnabled && !Ext.supports.TouchEvents;
            }
        },
        {
            /**
         * @property {Boolean} MSPointerEvents
         * @private
         */
            name: 'MSPointerEvents',
            fn: function() {
                return Ext.isIE10;
            }
        },
        {
            /**
         * @property {Boolean} TouchEvents
         *
         * `true` if the device supports touch events (`touchstart`, `touchmove`, `touchend`).
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'TouchEvents',
            fn: function() {
                return this.isEventSupported('touchend');
            }
        },
        {
            /**
         * @property {Boolean} TouchAction
         * @private
         *
         * A bit flag representing which property values the browser recognizes as valid
         * values of the CSS `touch-action` property.
         *
         *     panX            1  "00000001"
         *     panY            2  "00000010"
         *     pinchZoom       4  "00000100"
         *     doubleTapZoom   8  "00001000"
         */
            name: 'TouchAction',
            ready: true,
            fn: function(doc, div) {
                if (!window.getComputedStyle) {
                    return 0;
                }
                var values = [
                        'pan-x',
                        'pan-y',
                        'pinch-zoom',
                        'double-tap-zoom'
                    ],
                    flags = [
                        1,
                        2,
                        4,
                        8
                    ],
                    ln = values.length,
                    flag = 0,
                    i, value;
                for (i = 0; i < ln; i++) {
                    value = values[i];
                    div.style.touchAction = value;
                    if (getComputedStyle(div).touchAction === value) {
                        flag |= flags[i];
                    }
                }
                return flag;
            }
        },
        {
            /**
         * @property Orientation `true` if the device supports different orientations.
         * @type {Boolean}
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'Orientation',
            fn: function() {
                return ('orientation' in window) && this.isEventSupported('orientationchange');
            }
        },
        {
            /**
         * @property OrientationChange `true` if the device supports the `orientationchange`
         * event.
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'OrientationChange',
            fn: function() {
                return this.isEventSupported('orientationchange');
            }
        },
        {
            /**
         * @property DeviceMotion `true` if the device supports device motion (acceleration
         * and rotation rate).
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'DeviceMotion',
            fn: function() {
                return this.isEventSupported('devicemotion');
            }
        },
        {
            /**
         * @property Geolocation `true` if the device supports GeoLocation.
         * @type {Boolean}
         *
         * This property is available at application boot time, before document ready.
         */
            /**
         * @property GeoLocation `true` if the device supports Geo-location.
         * @type {Boolean}
         * @deprecated 5.0.0 Use `Geolocation` instead (notice the lower-casing of 'L').
         */
            names: [
                'Geolocation',
                'GeoLocation'
            ],
            fn: function() {
                return 'geolocation' in window.navigator;
            }
        },
        {
            name: 'SqlDatabase',
            fn: function() {
                return 'openDatabase' in window;
            }
        },
        {
            name: 'WebSockets',
            fn: function() {
                return 'WebSocket' in window;
            }
        },
        {
            /**
         * @property Range `true` if browser support document.createRange native method.
         * See https://developer.mozilla.org/en/DOM/range.
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'Range',
            fn: function() {
                return !!document.createRange;
            }
        },
        {
            /**
         * @property CreateContextualFragment `true` if browser support CreateContextualFragment
         * range native methods.
         * See https://developer.mozilla.org/en/DOM/range.createContextualFragment
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'CreateContextualFragment',
            fn: function() {
                var range = !!document.createRange ? document.createRange() : false;
                return range && !!range.createContextualFragment;
            }
        },
        {
            /**
         * @property History `true` if the device supports HTML5 history. See
         * https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'History',
            fn: function() {
                return ('history' in window && 'pushState' in window.history);
            }
        },
        {
            /**
         * @property Css3DTransforms `true` if the device supports CSS3DTransform.
         * @type {Boolean}
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'Css3dTransforms',
            fn: function() {
                // See https://sencha.jira.com/browse/TOUCH-1544
                return this.has('CssTransforms') && this.isStyleSupported('perspective');
            }
        },
        // TODO - double check vs Ext JS flavor:
        /* eslint-disable-next-line max-len */
        // return (typeof WebKitCSSMatrix != 'undefined' && new WebKitCSSMatrix().hasOwnProperty('m41'));
        {
            // Important that this goes after Css3dTransforms, since tests are run in reverse order
            name: 'CssTransforms',
            fn: function() {
                return this.isStyleSupported('transform');
            }
        },
        {
            name: 'CssTransformNoPrefix',
            fn: function() {
                return this.isStyleSupportedWithoutPrefix('transform');
            }
        },
        {
            name: 'CssAnimations',
            fn: function() {
                return this.isStyleSupported('animationName');
            }
        },
        {
            /**
         * @property Transitions `true` if the device supports CSS3 Transitions.
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            names: [
                'CssTransitions',
                'Transitions'
            ],
            fn: function() {
                return this.isStyleSupported('transitionProperty');
            }
        },
        {
            /**
         * @property Audio `true` if the device supports the HTML5 `audio` tag.
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            /**
         * @property AudioTag `true` if the device supports the HTML5 `audio` tag.
         * @type {Boolean}
         * @deprecated 5.0.0 Use `Audio` instead.
         */
            names: [
                'Audio',
                'AudioTag'
            ],
            fn: function() {
                return !!this.getTestElement('audio').canPlayType;
            }
        },
        {
            /**
         * @property Video `true` if the device supports the HTML5 `video` tag.
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'Video',
            fn: function() {
                return !!this.getTestElement('video').canPlayType;
            }
        },
        {
            /**
         * @property LocalStorage `true` if localStorage is supported.
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'LocalStorage',
            fn: function() {
                try {
                    // IE10/Win8 throws "Access Denied" accessing window.localStorage, so
                    // this test needs to have a try/catch
                    /* eslint-disable-next-line dot-notation */
                    if ('localStorage' in window && window['localStorage'] !== null) {
                        // this should throw an error in private browsing mode in iOS as well
                        localStorage.setItem('sencha-localstorage-test', 'test success');
                        // clean up if setItem worked
                        localStorage.removeItem('sencha-localstorage-test');
                        return true;
                    }
                } catch (e) {}
                // ignore
                return false;
            }
        },
        {
            /**
         * @property {Boolean} XmlQuerySelector `true` if the browsers supports querySelector
         * and querySelectorAll methods on XML nodes.
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'XmlQuerySelector',
            fn: function() {
                var xmlString = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?><root></root>',
                    xmlDoc;
                // IE10 doesn't create IXMLDOMDocument via DOMParser
                if (window.ActiveXObject) {
                    xmlDoc = new ActiveXObject("Microsoft.xmlDOM");
                    // eslint-disable-line no-undef
                    xmlDoc.async = false;
                    // eslint-disable-line id-blacklist
                    xmlDoc.loadXML(xmlString);
                } else if (window.DOMParser) {
                    var parser = new DOMParser();
                    xmlDoc = parser.parseFromString(xmlString, 'text/xml');
                }
                return xmlDoc ? !!xmlDoc.lastChild.querySelector : false;
            }
        },
        {
            /**
         * @property XHR2 `true` if the browser supports XMLHttpRequest
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'XHR2',
            fn: function() {
                return window.ProgressEvent && window.FormData && window.XMLHttpRequest && ('withCredentials' in new XMLHttpRequest());
            }
        },
        {
            /**
         * @property XHRUploadProgress `true` if the browser supports XMLHttpRequest
         * upload progress info
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'XHRUploadProgress',
            fn: function() {
                var xhr;
                if (window.XMLHttpRequest && !Ext.browser.is.AndroidStock) {
                    xhr = new XMLHttpRequest();
                    return xhr && ('upload' in xhr) && ('onprogress' in xhr.upload);
                }
                return false;
            }
        },
        {
            /**
         * @property NumericInputPlaceHolder `true` if the browser supports placeholders
         * on numeric input fields
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'NumericInputPlaceHolder',
            fn: function() {
                return !(Ext.browser.is.AndroidStock4 && Ext.os.version.getMinor() < 2);
            }
        },
        {
            /**
         * @property {String} matchesSelector
         * The method name which matches an element against a selector if implemented in this
         * environment.
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'matchesSelector',
            fn: function() {
                var el = document.documentElement,
                    w3 = 'matches',
                    wk = 'webkitMatchesSelector',
                    ms = 'msMatchesSelector',
                    mz = 'mozMatchesSelector';
                return el[w3] ? w3 : el[wk] ? wk : el[ms] ? ms : el[mz] ? mz : null;
            }
        },
        {
            /**
         * @property RightMargin `true` if the device supports right margin.
         * See https://bugs.webkit.org/show_bug.cgi?id=13343 for why this is needed.
         *
         * This property is *NOT* available at application boot time. Only after the document
         * ready event.
         * @type {Boolean}
         */
            name: 'RightMargin',
            ready: true,
            fn: function(doc, div) {
                var view = doc.defaultView;
                /* eslint-disable-next-line max-len */
                return !(view && view.getComputedStyle(div.firstChild.firstChild, null).marginRight !== '0px');
            }
        },
        {
            /**
         * @property DisplayChangeInputSelectionBug `true` if INPUT elements lose their
         * selection when their display style is changed. Essentially, if a text input
         * has focus and its display style is changed, the I-beam disappears.
         *
         * This bug is encountered due to the work around in place for the {@link #RightMargin}
         * bug. This has been observed in Safari 4.0.4 and older, and appears to be fixed
         * in Safari 5. It's not clear if Safari 4.1 has the bug, but it has the same WebKit
         * version number as Safari 5 (according to http://unixpapa.com/js/gecko.html).
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'DisplayChangeInputSelectionBug',
            fn: function() {
                var webKitVersion = Ext.webKitVersion;
                // WebKit but older than Safari 5 or Chrome 6:
                return 0 < webKitVersion && webKitVersion < 533;
            }
        },
        {
            /**
         * @property DisplayChangeTextAreaSelectionBug `true` if TEXTAREA elements lose their
         * selection when their display style is changed. Essentially, if a text area has
         * focus and its display style is changed, the I-beam disappears.
         *
         * This bug is encountered due to the work around in place for the {@link #RightMargin}
         * bug. This has been observed in Chrome 10 and Safari 5 and older, and appears to
         * be fixed in Chrome 11.
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'DisplayChangeTextAreaSelectionBug',
            fn: function() {
                var webKitVersion = Ext.webKitVersion;
                /*
            Has bug w/textarea:

            (Chrome) Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US)
                        AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.127
                        Safari/534.16
            (Safari) Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-us)
                        AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5
                        Safari/533.21.1

            No bug:

            (Chrome) Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7)
                        AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.57
                        Safari/534.24
            */
                return 0 < webKitVersion && webKitVersion < 534.24;
            }
        },
        {
            /**
         * @property TransparentColor `true` if the device supports transparent color.
         * @type {Boolean}
         *
         * This property is *NOT* available at application boot time. Only after the document
         * ready event.
         */
            name: 'TransparentColor',
            ready: true,
            fn: function(doc, div, view) {
                view = doc.defaultView;
                /* eslint-disable-next-line max-len */
                return !(view && view.getComputedStyle(div.lastChild, null).backgroundColor !== 'transparent');
            }
        },
        {
            /**
         * @property ComputedStyle `true` if the browser supports
         * document.defaultView.getComputedStyle().
         * @type {Boolean}
         *
         * This property is *NOT* available at application boot time. Only after the document
         * ready event.
         */
            name: 'ComputedStyle',
            ready: true,
            fn: function(doc, div, view) {
                view = doc.defaultView;
                return !!(view && view.getComputedStyle);
            }
        },
        {
            /**
         * @property Float `true` if the device supports CSS float.
         * @type {Boolean}
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'Float',
            fn: function(doc) {
                return 'cssFloat' in doc.documentElement.style;
            }
        },
        {
            /**
         * @property CSS3BorderRadius `true` if the device supports CSS3 border radius.
         * @type {Boolean}
         *
         * This property is *NOT* available at application boot time. Only after the document
         * ready event.
         */
            name: 'CSS3BorderRadius',
            ready: true,
            fn: function(doc) {
                var domPrefixes = [
                        'borderRadius',
                        'BorderRadius',
                        'MozBorderRadius',
                        'WebkitBorderRadius',
                        'OBorderRadius',
                        'KhtmlBorderRadius'
                    ],
                    pass = false,
                    i;
                for (i = 0; i < domPrefixes.length; i++) {
                    if (doc.documentElement.style[domPrefixes[i]] !== undefined) {
                        pass = true;
                    }
                }
                return pass && !Ext.isIE9;
            }
        },
        {
            /**
         * @property CSS3LinearGradient `true` if the device supports CSS3 linear gradients.
         * @type {Boolean}
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'CSS3LinearGradient',
            fn: function(doc, div) {
                var property = 'background-image:',
                    webkit = '-webkit-gradient(linear, left top, right bottom, from(black), to(white))',
                    w3c = 'linear-gradient(left top, black, white)',
                    moz = '-moz-' + w3c,
                    ms = '-ms-' + w3c,
                    opera = '-o-' + w3c,
                    options = [
                        property + webkit,
                        property + w3c,
                        property + moz,
                        property + ms,
                        property + opera
                    ];
                div.style.cssText = options.join(';');
                return (("" + div.style.backgroundImage).indexOf('gradient') !== -1) && !Ext.isIE9;
            }
        },
        {
            /**
         * @property MouseEnterLeave `true` if the browser supports mouseenter and mouseleave events
         * @type {Boolean}
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'MouseEnterLeave',
            fn: function(doc) {
                return ('onmouseenter' in doc.documentElement && 'onmouseleave' in doc.documentElement);
            }
        },
        {
            /**
         * @property MouseWheel `true` if the browser supports the mousewheel event
         * @type {Boolean}
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'MouseWheel',
            fn: function(doc) {
                return ('onmousewheel' in doc.documentElement);
            }
        },
        {
            /**
         * @property Opacity `true` if the browser supports normal css opacity
         * @type {Boolean}
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'Opacity',
            fn: function(doc, div) {
                // Not a strict equal comparison in case opacity can be converted to a number.
                if (Ext.isIE8) {
                    return false;
                }
                div.firstChild.style.cssText = 'opacity:0.73';
                return div.firstChild.style.opacity == '0.73';
            }
        },
        // eslint-disable-line eqeqeq
        {
            /**
         * @property Placeholder `true` if the browser supports the HTML5 placeholder attribute
         * on inputs
         * @type {Boolean}
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'Placeholder',
            fn: function(doc) {
                return 'placeholder' in doc.createElement('input');
            }
        },
        {
            /**
         * @property Direct2DBug `true` if when asking for an element's dimension via offsetWidth
         * or offsetHeight, getBoundingClientRect, etc. the browser returns the subpixel width
         * rounded to the nearest pixel.
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            name: 'Direct2DBug',
            fn: function(doc) {
                return Ext.isString(doc.documentElement.style.msTransformOrigin) && Ext.isIE9m;
            }
        },
        {
            /**
         * @property BoundingClientRect `true` if the browser supports the getBoundingClientRect
         * method on elements
         * @type {Boolean}
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'BoundingClientRect',
            fn: function(doc) {
                return 'getBoundingClientRect' in doc.documentElement;
            }
        },
        {
            /**
         * @property RotatedBoundingClientRect `true` if the BoundingClientRect is
         * rotated when the element is rotated using a CSS transform.
         * @type {Boolean}
         *
         * This property is *NOT* available at application boot time. Only after the document
         * ready event.
         */
            name: 'RotatedBoundingClientRect',
            ready: true,
            fn: function(doc) {
                var body = doc.body,
                    supports = false,
                    el = doc.createElement('div'),
                    style = el.style;
                if (el.getBoundingClientRect) {
                    // If the document body already has child nodes (text nodes etc) we can end
                    // up with subpixel rounding errors in IE11 when measuring the height.
                    // Absolute positioning prevents this.
                    style.position = 'absolute';
                    style.top = "0";
                    style.WebkitTransform = style.MozTransform = style.msTransform = style.OTransform = style.transform = 'rotate(90deg)';
                    style.width = '100px';
                    style.height = '30px';
                    body.appendChild(el);
                    supports = el.getBoundingClientRect().height !== 100;
                    body.removeChild(el);
                }
                return supports;
            }
        },
        {
            /**
         * @property ChildContentClearedWhenSettingInnerHTML `true` if created child elements
         * lose their innerHTML when modifying the innerHTML of the parent element.
         * @type {Boolean}
         *
         * This property is *NOT* available at application boot time. Only after the document
         * ready event.
         */
            name: 'ChildContentClearedWhenSettingInnerHTML',
            ready: true,
            fn: function() {
                var el = this.getTestElement(),
                    child;
                el.innerHTML = '<div>a</div>';
                child = el.firstChild;
                el.innerHTML = '<div>b</div>';
                return child.innerHTML !== 'a';
            }
        },
        {
            name: 'IncludePaddingInWidthCalculation',
            ready: true,
            fn: function(doc, div) {
                return div.childNodes[1].firstChild.offsetWidth === 210;
            }
        },
        {
            name: 'IncludePaddingInHeightCalculation',
            ready: true,
            fn: function(doc, div) {
                return div.childNodes[1].firstChild.offsetHeight === 210;
            }
        },
        {
            /**
         * @property TextAreaMaxLength `true` if the browser supports maxlength on textareas.
         * @type {Boolean}
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'TextAreaMaxLength',
            fn: function(doc) {
                return ('maxlength' in doc.createElement('textarea'));
            }
        },
        {
            /**
         * @property GetPositionPercentage `true` if the browser will return the
         * left/top/right/bottom position as a percentage when explicitly set as a percentage value.
         *
         * This property is *NOT* available at application boot time. Only after the document
         * ready event.
         * @type {Boolean}
         */
            // Related bug: https://bugzilla.mozilla.org/show_bug.cgi?id=707691#c7
            name: 'GetPositionPercentage',
            ready: true,
            fn: function(doc, div) {
                return Ext.feature.getStyle(div.childNodes[2], 'left') === '10%';
            }
        },
        {
            /**
         * @property {Boolean} PercentageHeightOverflowBug
         * In some browsers (IE quirks, IE6, IE7, IE9, chrome, safari and opera at the time
         * of this writing) a percentage-height element ignores the horizontal scrollbar
         * of its parent element.  This method returns true if the browser is affected
         * by this bug.
         *
         * This property is *NOT* available at application boot time. Only after the document
         * ready event.
         * @private
         */
            name: 'PercentageHeightOverflowBug',
            ready: true,
            fn: function(doc) {
                var hasBug = false,
                    style, el;
                if (Ext.getScrollbarSize().height) {
                    // must have space-consuming scrollbars for bug to be possible
                    el = this.getTestElement('div', true);
                    style = el.style;
                    style.height = '50px';
                    style.width = '50px';
                    style.overflow = 'auto';
                    style.position = 'absolute';
                    /* eslint-disable indent */
                    el.innerHTML = [
                        '<div style="display:table;height:100%;">',
                        // The element that causes the horizontal overflow must be 
                        // a child of the element with the 100% height, otherwise
                        // horizontal overflow is not triggered in webkit quirks mode
                        '<div style="width:51px;"></div>',
                        '</div>'
                    ].join('');
                    /* eslint-enable indent */
                    doc.body.appendChild(el);
                    if (el.firstChild.offsetHeight === 50) {
                        hasBug = true;
                    }
                    doc.body.removeChild(el);
                }
                return hasBug;
            }
        },
        {
            /**
         * @property {Boolean} xOriginBug
         * In Chrome 24.0, an RTL element which has vertical overflow positions its right X origin
         * incorrectly. It skips a non-existent scrollbar which has been moved to the left edge
         * due to the RTL setting.
         *
         * http://code.google.com/p/chromium/issues/detail?id=174656
         *
         * This method returns true if the browser is affected by this bug.
         *
         * This property is *NOT* available at application boot time. Only after the document
         * ready event.
         * @private
         */
            name: 'xOriginBug',
            ready: true,
            fn: function(doc, div) {
                /* eslint-disable max-len */
                div.innerHTML = '<div id="b1" style="height:100px;width:100px;direction:rtl;position:relative;overflow:scroll">' + '<div id="b2" style="position:relative;width:100%;height:20px;"></div>' + '<div id="b3" style="position:absolute;width:20px;height:20px;top:0px;right:0px"></div>' + '</div>';
                /* eslint-enable max-len */
                var outerBox = document.getElementById('b1').getBoundingClientRect(),
                    b2 = document.getElementById('b2').getBoundingClientRect(),
                    b3 = document.getElementById('b3').getBoundingClientRect();
                return (b2.left !== outerBox.left && b3.right !== outerBox.right);
            }
        },
        {
            /**
         * @property {Boolean} ScrollWidthInlinePaddingBug
         * In some browsers the right padding of an overflowing element is not accounted
         * for in its scrollWidth.  The result can vary depending on whether or not
         * The element contains block-level children.  This method tests the effect
         * of padding on scrollWidth when there are no block-level children inside the
         * overflowing element.
         *
         * This method returns true if the browser is affected by this bug.
         *
         * This property is *NOT* available at application boot time. Only after the document
         * ready event.
         */
            name: 'ScrollWidthInlinePaddingBug',
            ready: true,
            fn: function(doc) {
                var hasBug = false,
                    style, el;
                el = doc.createElement('div');
                style = el.style;
                style.height = '50px';
                style.width = '50px';
                style.padding = '10px';
                style.overflow = 'hidden';
                style.position = 'absolute';
                el.innerHTML = '<span style="display:inline-block;zoom:1;height:60px;width:60px;"></span>';
                doc.body.appendChild(el);
                if (el.scrollWidth === 70) {
                    hasBug = true;
                }
                doc.body.removeChild(el);
                return hasBug;
            }
        },
        {
            /**
         * @property {Boolean} rtlVertScrollbarOnRight
         * Safari, in RTL mode keeps the scrollbar at the right side.
         * This means that when two elements must keep their left/right positions synched, if one
         * has no vert scrollbar, it must have some extra padding.
         * See https://sencha.jira.com/browse/EXTJSIV-11245
         *
         * This property is *NOT* available at application boot time. Only after the document
         * ready event.
         * @private
         */
            name: 'rtlVertScrollbarOnRight',
            ready: true,
            fn: function(doc, div) {
                div.innerHTML = '<div style="height:100px;width:100px;direction:rtl;overflow:scroll">' + '<div style="width:20px;height:200px;"></div>' + '</div>';
                var outerBox = div.firstChild,
                    innerBox = outerBox.firstChild;
                /* eslint-disable-next-line max-len */
                return (innerBox.offsetLeft + innerBox.offsetWidth !== outerBox.offsetLeft + outerBox.offsetWidth);
            }
        },
        {
            /**
         * @property {Boolean} rtlVertScrollbarOverflowBug
         * In Chrome, in RTL mode, horizontal overflow only into the vertical scrollbar does NOT
         * trigger horizontal scrollability.
         * See https://code.google.com/p/chromium/issues/detail?id=179332
         * We need to detect this for when a grid header needs to have exactly the same horizontal
         * scrolling range as its table view. See {@link Ext.grid.ColumnLayout#publishInnerCtSize}
         * TODO: Remove this when all supported Chrome versions are fixed.
         *
         * This property is *NOT* available at application boot time. Only after the document
         * ready event.
         * @private
         */
            name: 'rtlVertScrollbarOverflowBug',
            ready: true,
            fn: function(doc, div) {
                div.innerHTML = '<div style="height:100px;width:100px;direction:rtl;overflow:auto">' + '<div style="width:95px;height:200px;"></div>' + '</div>';
                // If the bug is present, the 95 pixel wide inner div, encroaches into the
                // vertical scrollbar, but does NOT trigger horizontal overflow, so the clientHeight
                // remains equal to the offset height.
                var outerBox = div.firstChild,
                    style = div.style,
                    pos = style.position;
                // This issue seems to require a repaint to measure correctly
                style.position = 'absolute';
                // eslint-disable-next-line no-unused-expressions
                outerBox.offsetHeight;
                style.position = pos;
                return outerBox.clientHeight === outerBox.offsetHeight;
            }
        },
        {
            identity: 'defineProperty',
            fn: function() {
                if (Ext.isIE8m) {
                    Ext.Object.defineProperty = Ext.emptyFn;
                    return false;
                }
                return true;
            }
        },
        {
            identify: 'nativeXhr',
            fn: function() {
                if (typeof XMLHttpRequest !== 'undefined') {
                    return true;
                }
                // Apply a polyfill:
                XMLHttpRequest = function() {
                    // eslint-disable-line no-global-assign
                    try {
                        // eslint-disable-next-line no-undef
                        return new ActiveXObject('MSXML2.XMLHTTP.3.0');
                    } catch (ex) {
                        return null;
                    }
                };
                return false;
            }
        },
        {
            /**
         * @property {Boolean} SpecialKeyDownRepeat
         * True if the browser fires the keydown event on specialkey autorepeat
         *
         * note 1: IE fires ONLY the keydown event on specialkey autorepeat
         * note 2: Safari < 3.1, Gecko (Mac/Linux) & Opera fire only the keypress event on
         * specialkey autorepeat (research done by Jan Wolter at
         * http://unixpapa.com/js/key.html)
         * note 3: Opera 12 behaves like other modern browsers so this workaround does not
         * work anymore
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'SpecialKeyDownRepeat',
            fn: function() {
                return Ext.isWebKit ? parseInt(navigator.userAgent.match(/AppleWebKit\/(\d+)/)[1], 10) >= 525 : !(!(Ext.isGecko || Ext.isIE || Ext.isEdge) || (Ext.isOpera && Ext.operaVersion < 12));
            }
        },
        // eslint-disable-line max-len
        {
            /**
         * @property {Boolean} EmulatedMouseOver
         * True if the browser emulates a mouseover event on tap (mobile safari)
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'EmulatedMouseOver',
            fn: function() {
                // TODO: is it possible to feature detect this?
                return Ext.os.is.iOS;
            }
        },
        {
            /**
         * @property Hashchange True if the user agent supports the hashchange event
         *
         * This property is available at application boot time, before document ready.
         * @type {Boolean}
         */
            // support Vector 12
            name: 'Hashchange',
            fn: function() {
                // Note that IE8 in IE7 compatibility mode reports true for 'onhashchange' in window,
                // so also test documentMode
                var docMode = document.documentMode;
                return 'onhashchange' in window && (docMode === undefined || docMode > 7);
            }
        },
        {
            /**
         * @property FixedTableWidthBug
         * @private
         * @type {Boolean}
         * `true` if the browser has this bug: https://bugs.webkit.org/show_bug.cgi?id=130239
         *
         * This property is *NOT* available at application boot time. Only after the document
         * ready event.
         */
            name: 'FixedTableWidthBug',
            ready: true,
            fn: function() {
                if (Ext.isIE8) {
                    // IE8 incorrectly detects that we have this bug.
                    return false;
                }
                var outer = document.createElement('div'),
                    inner = document.createElement('div'),
                    width;
                outer.setAttribute('style', 'display:table;table-layout:fixed;');
                inner.setAttribute('style', 'display:table-cell;min-width:50px;');
                outer.appendChild(inner);
                document.body.appendChild(outer);
                // must poke offsetWidth to trigger a reflow before setting width
                // eslint-disable-next-line no-unused-expressions
                outer.offsetWidth;
                outer.style.width = '25px';
                width = outer.offsetWidth;
                document.body.removeChild(outer);
                return width === 50;
            }
        },
        {
            /**
         * @property FocusinFocusoutEvents
         * @private
         * @type {Boolean}
         * `true` if the browser supports focusin and focusout events:
         * https://developer.mozilla.org/en-US/docs/Web/Events/focusin
         * At this point, only Firefox does not, see this bug:
         * https://bugzilla.mozilla.org/show_bug.cgi?id=687787
         *
         * This property is available at application boot time, before document ready.
         */
            name: 'FocusinFocusoutEvents',
            fn: function() {
                // There is no reliable way to feature detect focusin/focusout event support.
                // window.onfocusin will return undefined both in Chrome (where it is supported)
                // and in Firefox (where it is not supported); adding an element and trying to
                // focus it will fail when the browser window itself is not focused.
                return !(Ext.isGecko && Ext.firefoxVersion < 52);
            }
        },
        {
            /**
         * @property {Boolean} AsyncFocusEvents
         * `true` if the browser fires focus events (focus, blur, focusin, focusout)
         * asynchronously, i.e. in a separate event loop invocation. This is only true
         * for all versions Internet Explorer; Microsoft Edge and other browsers fire
         * focus events synchronously.
         */
            name: 'AsyncFocusEvents',
            fn: function() {
                // The sad part is that we can't feature detect this because the focus
                // event won't be fired when the browser window itself is not focused.
                // Private shortcut for brevity
                return Ext.asyncFocus = !!Ext.isIE;
            }
        },
        {
            /**
         * @property {Object} accessibility Accessibility features.
         *
         * @property {Boolean} accessibility.Images `true` if the browser is configured
         * to display images.
         *
         * @property {Boolean} accessibility.BackgroundImages `true` if the browser
         * is configured to display background images.
         *
         * @property {Boolean} accessibility.BorderColors `true` if the browser
         * is configured to honor CSS styling for border colors.
         *
         * @property {Boolean} accessibility.LightOnDark `true` if the browser
         * is currently using reverse colors in light-on-dark accessibility mode.
         */
            name: 'accessibility',
            ready: true,
            fn: function(doc) {
                var body = doc.body,
                    div, img, style, supports, bgImg;
                function getColor(colorTxt) {
                    var values = [],
                        colorValue = 0,
                        regex, match;
                    if (colorTxt.indexOf('rgb(') !== -1) {
                        values = colorTxt.replace('rgb(', '').replace(')', '').split(', ');
                    } else if (colorTxt.indexOf('#') !== -1) {
                        regex = colorTxt.length === 7 ? /^#(\S\S)(\S\S)(\S\S)$/ : /^#(\S)(\S)(\S)$/;
                        match = colorTxt.match(regex);
                        if (match) {
                            values = [
                                '0x' + match[1],
                                '0x' + match[2],
                                '0x' + match[3]
                            ];
                        }
                    }
                    for (var i = 0; i < values.length; i++) {
                        colorValue += parseInt(values[i]);
                    }
                    return colorValue;
                }
                div = doc.createElement('div');
                img = doc.createElement('img');
                style = div.style;
                Ext.apply(style, {
                    width: '2px',
                    position: 'absolute',
                    clip: 'rect(1px,1px,1px,1px)',
                    borderWidth: '1px',
                    borderStyle: 'solid',
                    borderTopTolor: '#f00',
                    borderRightColor: '#ff0',
                    backgroundColor: '#fff',
                    backgroundImage: 'url(' + Ext.BLANK_IMAGE_URL + ')'
                });
                img.alt = '';
                img.src = Ext.BLANK_IMAGE_URL;
                div.appendChild(img);
                body.appendChild(div);
                // Now check if the styles were indeed honored
                style = div.currentStyle || div.style;
                bgImg = style.backgroundImage;
                supports = {
                    // In IE it is possible to untick "Show pictures" option in Advanced
                    // settings; this will result in img element reporting its readyState
                    // as 'uninitialized'.
                    // See http://www.paciellogroup.com/blog/2011/10/detecting-if-images-are-disabled-in-browsers/
                    Images: img.offsetWidth === 1 && img.readyState !== 'uninitialized',
                    BackgroundImages: !(bgImg !== null && (bgImg === "none" || bgImg === "url(invalid-url:)")),
                    BorderColors: style.borderTopColor !== style.borderRightColor,
                    LightOnDark: getColor(style.color) - getColor(style.backgroundColor) > 0
                };
                Ext.supports.HighContrastMode = !supports.BackgroundImages;
                body.removeChild(div);
                div = img = null;
                return supports;
            }
        },
        {
            /**
         * @property ViewportUnits `true` if the device supports ViewportUnits.
         * @type {Boolean}
         *
         */
            name: 'ViewportUnits',
            ready: true,
            fn: function(doc) {
                // Even attempting to detect the feature throws a fatal error on IE8
                if (Ext.isIE8) {
                    return false;
                }
                var body = doc.body,
                    div = document.createElement('div'),
                    style = div.currentStyle || div.style,
                    width, divWidth;
                body.appendChild(div);
                Ext.apply(style, {
                    width: '50vw'
                });
                width = parseInt(window.innerWidth / 2, 10);
                // eslint-disable-next-line max-len
                divWidth = parseInt((window.getComputedStyle ? getComputedStyle(div, null) : div.currentStyle).width, 10);
                body.removeChild(div);
                div = null;
                return width === divWidth;
            }
        },
        {
            name: 'CSSVariables',
            ready: false,
            fn: function() {
                // Legacy browsers do not have this method.
                if (!window.getComputedStyle) {
                    return false;
                }
                return window.CSS && window.CSS.supports && window.CSS.supports('--test-var', 0);
            }
        },
        {
            /**
         * @property Selectors2 `true` if the browser supports the CSS selector API level 2.
         * https://dev.w3.org/2006/webapi/selectors-api2/
         * @type {Boolean}
         *
         */
            name: 'Selectors2',
            ready: false,
            fn: function(doc) {
                try {
                    return !!doc.querySelectorAll(':scope');
                } catch (e) {
                    return false;
                }
            }
        },
        {
            /**
         * @property CSSScrollSnap
         * @private
         * @type {Boolean}
         */
            name: 'CSSScrollSnap',
            ready: false,
            fn: function(doc) {
                var style = doc.documentElement.style;
                return 'scrollSnapType' in style || 'webkitScrollSnapType' in style || 'msScrollSnapType' in style;
            }
        },
        {
            /**
         * @property TranslateYCausesHorizontalScroll
         * @private
         * @type {Boolean}
         *
         * Bug for Edge logged here: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/9743268/
         */
            name: 'TranslateYCausesHorizontalScroll',
            ready: true,
            fn: function(doc, div) {
                div.innerHTML = '<div style="position: relative; overflow: auto; height: 200px; width: 200px;">' + // eslint-disable-line max-len
                '<div>' + '<div style="transform: translateY(260px); width: 50px;">a</div>' + '</div>' + '</div>';
                return div.firstChild.scrollWidth > div.firstChild.clientWidth;
            }
        },
        {
            /**
         * @property FlexBoxBasisBug
         * @private
         * @type {Boolean}
         * Allows align: stretch to align items to the height of the tallest item
         * in an auto-heighted hbox layout.
         * can't use flex-basis: auto everywhere because it breaks percentage-sized children
         * https://bugs.chromium.org/p/chromium/issues/detail?id=680484
         */
            name: 'FlexBoxBasisBug',
            ready: true,
            fn: function() {
                if (Ext.isIE11 || (Ext.os.is.iOS && Ext.os.version.major <= 10) || (Ext.isSafari && Ext.browser.version.isLessThan(11)) || (Ext.os.is.Android && Ext.os.version.isLessThan(6))) {
                    return true;
                }
                return false;
            }
        },
        {
            /**
         * @property PercentageSizeFlexBug
         * @private
         * @type {Boolean}
         *
         * Detects https://bugs.webkit.org/show_bug.cgi?id=137730
         */
            name: 'PercentageSizeFlexBug',
            ready: true,
            fn: function(doc, div) {
                if (Ext.isIE9m) {
                    return false;
                }
                var style = div.style;
                style.display = 'flex';
                style.flexDirection = 'column';
                style.height = style.width = '100px';
                div.innerHTML = '<div style="flex: 1 1;"><div style="height:50%"></div></div>';
                return div.firstChild.firstChild.offsetHeight !== 50;
            }
        },
        {
            /**
         * @property CannotScrollExactHeight
         * @type {Boolean}
         *
         * Feature detect the support of browsers that are unable to scroll elements that are
         * the same height as the native scrollbar height.
         */
            name: 'CannotScrollExactHeight',
            fn: function() {
                return Ext.isIE10p;
            }
        },
        {
            /**
         * @property WebKitTextInputMarginBug
         * @private
         * @type {Boolean}
         *
         * Detects the following bug:
         * https://bugs.webkit.org/show_bug.cgi?id=137693
         *
         * Feb 22, 2017: This bug used to affect chrome as well, but appears to be fixed in
         * Chrome 56. The issue still exists in safari 10
         */
            name: 'WebKitInputTableBoxModelBug',
            ready: true,
            fn: function(doc, div) {
                var table = document.createElement('div'),
                    cell = document.createElement('div'),
                    input = document.createElement('input'),
                    tableStyle = table.style,
                    cellStyle = cell.style,
                    inputStyle = input.style,
                    body = doc.body,
                    hasBug;
                input.type = 'text';
                tableStyle.display = 'table';
                tableStyle.height = '100px';
                cellStyle.display = 'table-cell';
                inputStyle.border = '0';
                inputStyle.padding = '10px';
                inputStyle.boxSizing = 'border-box';
                inputStyle.height = '100%';
                cell.appendChild(input);
                table.appendChild(cell);
                body.appendChild(table);
                hasBug = input.offsetHeight === 80;
                body.removeChild(table);
                return hasBug;
            }
        },
        {
            /**
         * @property PassiveEventListener
         * @private
         * @type {Boolean}
         *
         * Detects support for the "passive" event listener option
         */
            name: 'PassiveEventListener',
            fn: function(doc, div) {
                var supportsPassive = false,
                    options;
                try {
                    options = Object.defineProperty({}, 'passive', {
                        get: function() {
                            // eslint-disable-line getter-return
                            supportsPassive = true;
                        }
                    });
                    window.addEventListener('e', null, options);
                    window.removeEventListener('e', null, options);
                } catch (e) {}
                // ignore
                return supportsPassive;
            }
        },
        {
            /**
         * @property MinContent
         * @private
         * @type {Boolean}
         *
         * Detects support for CSS "min-content"
         */
            name: 'CSSMinContent',
            ready: true,
            fn: function(doc, div) {
                // As of 3/24/2017 IE/Edge have no min-content support, and firefox has
                // partial/buggy support: https://bugzilla.mozilla.org/show_bug.cgi?id=135015
                // This feature detector is designed to return false if there is not "full" support.
                // eslint-disable-next-line max-len
                div.innerHTML = '<div style="height:4px;width:4px;min-height:-webkit-min-content;min-height:-moz-min-content;min-height:min-content"><div style="height:8px;width:8px"></div></div>';
                return div.firstChild.offsetHeight === 8;
            }
        },
        {
            name: 'ComputedSizeIncludesPadding',
            ready: true,
            fn: function(doc, div) {
                var ret = false,
                    bd = document.body,
                    el, w;
                if (window.getComputedStyle) {
                    el = document.createElement('div');
                    el.style.cssText = 'width:10px;padding:2px;' + '-webkit-box-sizing:border-box;box-sizing:border-box;';
                    bd.appendChild(el);
                    w = window.getComputedStyle(el, null).width;
                    ret = w === '10px';
                    bd.removeChild(el);
                }
                return ret;
            }
        },
        {
            name: 'inputEventData',
            ready: false,
            fn: function() {
                return !!(window.InputEvent && 'data' in new InputEvent('input'));
            }
        },
        /* eslint-disable indent */
        // placeholder so legacy browser detectors can come/go cleanly
        0
    ]
};
/* eslint-enable indent */
Ext.feature.tests.pop();
// remove the placeholder
Ext.supports = {};
Ext.feature.detect();

/**
 * This class manages ready detection and handling. Direct use of this class is not
 * recommended. Instead use `Ext.onReady`:
 * 
 *      Ext.onReady(function () {
 *          // DOM and Framework are ready...
 *      });
 *
 * ## DOM Ready
 *
 * The lowest-level of readiness is DOM readiness. This level implies only that the document
 * body exists. Many things require the DOM to be ready for manipulation. If that is all
 * that is required, the `Ext.onDocumentReady` method can be called to register a callback
 * to be called as soon as the DOM is ready:
 *
 *      Ext.onDocumentReady(function () {
 *          // the document body is ready
 *      });
 *
 * ## Framework Ready
 *
 * In production builds of applications it is common to have all of the code loaded before
 * DOM ready, so the need to wait for "onReady" is often confused with only that concern.
 * This is easy to understand, at least in part because historically `Ext.onReady` only
 * waited for DOM ready.
 *
 * With the introduction of `Ext.Loader`, however, it became common for DOM ready to occur
 * in the middle of dynamically loading code. If application code were executed at that
 * time, any use of the yet-to-be-loaded classes would throw errors. As a consequence of
 * this, the `Ext.onReady` mechanism was extended to wait for both DOM ready *and* all of
 * the required classes to be loaded.
 *
 * When the framework enters or leaves a state where it is not ready (for example, the
 * first dynamic load is requested or last load completes), `Ext.env.Ready` is informed.
 * For example:
 *
 *      Ext.env.Ready.block();
 *
 *      //...
 *
 *      Ext.env.Ready.unblock();
 *
 * When there are no blocks and the DOM is ready, the Framework is ready and the "onReady"
 * callbacks are called.
 *
 * Priority can be used to control the ordering of onReady listeners, for example:
 *
 *     Ext.onReady(function() {
 *
 *     }, null, {
 *         priority: 100
 *     });
 *
 * Ready listeners with higher priorities will run sooner than those with lower priorities,
 * the default priority being `0`.  Internally the framework reserves priorities of 1000
 * or greater, and -1000 or lesser for onReady handlers that must run before or after
 * any application code.  Applications should stick to using priorities in the -999 - 999
 * range. The following priorities are currently in use by the framework:
 *
 * - Element_scroll rtl override: `1001`
 * - Event system initialization: `2000`
 * - Ext.dom.Element: `1500`
 *
 * @class Ext.env.Ready
 * @singleton
 * @private
 * @since 5.0.0
 */
Ext.env.Ready = {
    // @define Ext.env.Ready
    // @require Ext.env.Browser
    // @require Ext.env.OS
    // @require Ext.env.Feature
    /**
     * @property {Number} blocks The number of Framework readiness blocks.
     * @private
     */
    blocks: (location.search || '').indexOf('ext-pauseReadyFire') > 0 ? 1 : 0,
    /**
     * @property {Number} bound This property stores the state of event listeners bound
     * to the document or window to detect ready state.
     * @private
     */
    bound: 0,
    /**
     * @property {Number} [delay=1]
     * This allows the DOM listener thread to complete (usually desirable with mobWebkit,
     * Gecko) before firing the entire onReady chain (high stack load on Loader). For mobile
     * devices when running from Home Screen, the splash screen will not disappear until
     * all external resource requests finish. This delay clears the splash screen.
     * @private
     */
    delay: 1,
    /**
     * @property {Event[]} events An array of events that have triggered ready state. This
     * is for diagnostic purposes only and is only available in debug builds.
     * An array
     * @private
     */
    events: [],
    /**
     * @property {Boolean} firing This property is `true` when we currently calling the
     * listeners.
     * @private
     */
    firing: false,
    /**
     * @property {Number} generation A counter of the number of mutations of `listeners`.
     * @private
     */
    generation: 0,
    /**
     * @property {Object[]} listeners The set of listeners waiting for ready.
     * @private
     */
    listeners: [],
    /**
     * @property {Number} nextId A counter so we can assign listeners an `id` to keep
     * them in FIFO order.
     * @private
     */
    nextId: 0,
    /**
     * @property {Number} sortGeneration A captured value of `generation` that indicates
     * when the `listeners` were last sorted.
     * @private
     */
    sortGeneration: 0,
    /**
     * @property {Number} state
     * Holds the current ready state as managed by this class. The values possible are:
     * 
     *   * 0 - Not ready.
     *   * 1 - Ready detected but listeners are not yet notified.
     *   * 2 - Ready detected and listeners are notified. See also `firing`.
     *   
     * @private
     */
    state: 0,
    /**
     * @property {Object} timer The handle from `setTimeout` for the delayed notification
     * of ready.
     * @private
     */
    timer: null,
    /**
     * Binds the appropriate browser event for checking if the DOM has loaded.
     * @private
     */
    bind: function() {
        var me = Ext.env.Ready,
            doc = document;
        if (!me.bound) {
            // Test scenario where load is dynamic AFTER window.load
            if (doc.readyState === 'complete') {
                // Firefox4+ got support for this state, others already do.
                me.onReadyEvent({
                    type: doc.readyState || 'body'
                });
            } else {
                me.bound = 1;
                if (Ext.browser.is.PhoneGap && !Ext.os.is.Desktop) {
                    me.bound = 2;
                    doc.addEventListener('deviceready', me.onReadyEvent, false);
                }
                doc.addEventListener('DOMContentLoaded', me.onReadyEvent, false);
                window.addEventListener('load', me.onReadyEvent, false);
            }
        }
    },
    block: function() {
        ++this.blocks;
        Ext.isReady = false;
    },
    /**
     * This method starts the process of firing the ready event. This may be delayed based
     * on the `delay` property.
     * @private
     */
    fireReady: function() {
        var me = Ext.env.Ready;
        if (!me.state) {
            Ext._readyTime = Ext.ticks();
            Ext.isDomReady = true;
            me.state = 1;
            // As soon as we transition to domready, complete the feature detection:
            Ext.feature.detect(true);
            if (!me.delay) {
                me.handleReady();
            } else if (navigator.standalone) {
                // When running from Home Screen, the splash screen will not disappear
                // until all external resource requests finish.
                // The first timeout clears the splash screen
                // The second timeout allows inital HTML content to be displayed
                me.timer = Ext.defer(function() {
                    me.timer = null;
                    me.handleReadySoon();
                }, 1);
            } else {
                me.handleReadySoon();
            }
        }
    },
    /**
     * This method iterates over the `listeners` and invokes them. This advances the
     * `state` from 1 to 2 and ensure the proper subset of `listeners` are invoked.
     * @private
     */
    handleReady: function() {
        var me = this;
        if (me.state === 1) {
            me.state = 2;
            Ext._beforeReadyTime = Ext.ticks();
            me.invokeAll();
            Ext._afterReadyTime = Ext.ticks();
        }
    },
    /**
     * This method is called to schedule a call to `handleReady` using a `setTimeout`. It
     * ensures that only one timer is pending.
     * @param {Number} [delay] If passed, this overrides the `delay` property.
     * @private
     */
    handleReadySoon: function(delay) {
        var me = this;
        if (!me.timer) {
            me.timer = Ext.defer(function() {
                me.timer = null;
                me.handleReady();
            }, delay || me.delay);
        }
    },
    /**
     * This method invokes the given `listener` instance based on its options.
     * @param {Object} listener
     */
    invoke: function(listener) {
        var delay = listener.delay;
        if (delay) {
            Ext.defer(listener.fn, delay, listener.scope);
        } else {
            if (Ext.elevateFunction) {
                Ext.elevateFunction(listener.fn, listener.scope);
            } else {
                listener.fn.call(listener.scope);
            }
        }
    },
    /**
     * Invokes as many listeners as are appropriate given the current state. This should
     * only be called when DOM ready is achieved. The remaining business of `blocks` is
     * handled here.
     */
    invokeAll: function() {
        if (Ext.elevateFunction) {
            Ext.elevateFunction(this.doInvokeAll, this);
        } else {
            this.doInvokeAll();
        }
    },
    doInvokeAll: function() {
        var me = this,
            listeners = me.listeners,
            listener;
        if (!me.blocks) {
            // Since DOM is ready and we have no blocks, we mark the framework as ready.
            Ext.isReady = true;
        }
        me.firing = true;
        // NOTE: We cannot cache this length because each time through the loop a callback
        // may have added new callbacks.
        while (listeners.length) {
            if (me.sortGeneration !== me.generation) {
                me.sortGeneration = me.generation;
                // This will happen just once on the first pass... if nothing is being
                // added as we call the callbacks. This sorts the listeners such that the
                // highest priority listener is at the *end* of the array ... so we can
                // use pop (as opposed to shift) to extract it.
                listeners.sort(me.sortFn);
            }
            listener = listeners.pop();
            if (me.blocks && !listener.dom) {
                // If we are blocked (i.e., only DOM ready) and this listener is not a
                // DOM-ready listener we have reached the end of the line. The remaining
                // listeners are Framework ready listeners.
                listeners.push(listener);
                break;
            }
            me.invoke(listener);
        }
        me.firing = false;
    },
    /**
     * This method wraps the given listener pieces in a proper object for the `listeners`
     * array and `invoke` methods.
     * @param {Function} fn The method to call.
     * @param {Object} [scope] The scope (`this` reference) in which the `fn` executes.
     * Defaults to the browser window.
     * @param {Object} [options] An object with extra options.
     * @param {Number} [options.delay=0] A number of milliseconds to delay.
     * @param {Number} [options.priority=0] Relative priority of this callback. A larger
     * number will result in the callback being sorted before the others.  Priorities
     * 1000 or greater and -1000 or lesser are reserved for internal framework use only.
     * @param {Boolean} [options.dom=false] Pass `true` to only wait for DOM ready, `false`
     * means full Framework and DOM readiness.
     * @return {Object} The listener instance.
     * @private
     */
    makeListener: function(fn, scope, options) {
        var ret = {
                fn: fn,
                id: ++this.nextId,
                // so sortFn can respect FIFO
                scope: scope,
                dom: false,
                priority: 0
            };
        if (options) {
            Ext.apply(ret, options);
        }
        ret.phase = ret.dom ? 0 : 1;
        // to simplify the sortFn
        return ret;
    },
    /**
     * Adds a listener to be notified when the document is ready (before onload and before
     * images are loaded).
     *
     * @param {Function} fn The method to call.
     * @param {Object} [scope] The scope (`this` reference) in which the `fn` executes.
     * Defaults to the browser window.
     * @param {Object} [options] An object with extra options.
     * @param {Number} [options.delay=0] A number of milliseconds to delay.
     * @param {Number} [options.priority=0] Relative priority of this callback. A larger
     * number will result in the callback being sorted before the others.  Priorities
     * 1000 or greater and -1000 or lesser are reserved for internal framework use only.
     * @param {Boolean} [options.dom=false] Pass `true` to only wait for DOM ready, `false`
     * means full Framework and DOM readiness.
     * @private
     */
    on: function(fn, scope, options) {
        var me = Ext.env.Ready,
            listener = me.makeListener(fn, scope, options);
        if (me.state === 2 && !me.firing && (listener.dom || !me.blocks)) {
            // If we are DOM ready (state === 2) and not currently in invokeAll (!firing)
            // and this listener is ready to call (either a DOM ready listener or there
            // are no blocks), then we need to invoke the listener now.
            //
            // Otherwise we can fall to the else block and push the listener. The eventual
            // (or currently executing) call to handleReady or unblock will trigger its
            // delivery in proper priority order.
            me.invoke(listener);
        } else {
            me.listeners.push(listener);
            ++me.generation;
            if (!me.bound) {
                // If we have never bound then bind the ready event now. If we have unbound
                // the event then me.bound == -1 and we don't want to rebind it as the DOM
                // is ready.
                me.bind();
            }
        }
    },
    /**
     * This is a generic event handler method attached to all of the various events that
     * may indicate ready state. The first call to this method indicates ready state has
     * been achieved.
     * @param {Event} [ev] The event instance.
     * @private
     */
    onReadyEvent: function(ev) {
        var me = Ext.env.Ready;
        if (Ext.elevateFunction) {
            Ext.elevateFunction(me.doReadyEvent, me, arguments);
        } else {
            me.doReadyEvent(ev);
        }
    },
    doReadyEvent: function(ev) {
        var me = this;
        if (ev && ev.type) {
            me.events.push(ev);
        }
        if (me.bound > 0) {
            me.unbind();
            me.bound = -1;
        }
        // NOTE: *not* 0 or false - we never want to rebind!
        if (!me.state) {
            me.fireReady();
        }
    },
    /**
     * Sorts the `listeners` array by `phase` and `priority` such that the first listener
     * to fire can be determined using `pop` on the `listeners` array.
     * @private
     */
    sortFn: function(a, b) {
        return -((a.phase - b.phase) || (b.priority - a.priority) || (a.id - b.id));
    },
    unblock: function() {
        var me = this;
        if (me.blocks) {
            if (!--me.blocks) {
                if (me.state === 2 && !me.firing) {
                    // We have already finished handleReady (the DOM ready handler) so
                    // this trigger just needs to dispatch all the remaining listeners.
                    me.invokeAll();
                }
            }
        }
    },
    // if we are currently firing then invokeAll will pick up the Framework
    // ready listeners automatically.
    //
    // If me.state < 2 then we are waiting for DOM ready so it will eventually
    // call handleReady and invokeAll when everything is ready.
    /**
     * This method is called to remove all event listeners that may have been set up to
     * detect ready state.
     * @private
     */
    unbind: function() {
        var me = this,
            doc = document;
        if (me.bound > 1) {
            doc.removeEventListener('deviceready', me.onReadyEvent, false);
        }
        doc.removeEventListener('DOMContentLoaded', me.onReadyEvent, false);
        window.removeEventListener('load', me.onReadyEvent, false);
    }
};
(function() {
    var Ready = Ext.env.Ready;
    /*
 *  EXTJS-13522
 *  Although IE 9 has the DOMContentLoaded event available, usage of that causes
 *  timing issues when attempting to access document.namespaces (VmlCanvas.js).
 *  Consequently, even in IE 9 we need to use the legacy bind override for ready
 *  detection.  This defers ready firing enough to allow access to the
 *  document.namespaces property.
 *
 *  NOTE: this issue is very timing sensitive, and typically only displays itself
 *  when there is a large amount of latency between the browser and the server, and
 *  when testing against a built page (ext-all.js) and not a dev mode page.
 */
    if (Ext.isIE9m) {
        /* Customized implementation for Legacy IE. The default implementation is 
     * configured for use with all other 'standards compliant' agents.
     * References: http://javascript.nwbox.com/IEContentLoaded/
     * licensed courtesy of http://developer.yahoo.com/yui/license.html
     */
        Ext.apply(Ready, {
            /**
         * Timer for doScroll polling
         * @private
         */
            scrollTimer: null,
            /**
         * @private
         */
            readyStatesRe: /complete/i,
            /**
         * This strategy has minimal benefits for Sencha solutions that build
         * themselves (ie. minimal initial page markup). However, progressively-enhanced
         * pages (with image content and/or embedded frames) will benefit the most
         * from it. Browser timer resolution is too poor to ensure a doScroll check
         * more than once on a page loaded with minimal assets (the readystatechange
         * event 'complete' usually beats the doScroll timer on a 'lightly-loaded'
         * initial document).
         * @private
         */
            pollScroll: function() {
                var scrollable = true;
                try {
                    document.documentElement.doScroll('left');
                } catch (e) {
                    scrollable = false;
                }
                // on IE8, when running within an iFrame, document.body is not immediately
                // available
                if (scrollable && document.body) {
                    Ready.onReadyEvent({
                        type: 'doScroll'
                    });
                } else {
                    // Minimize thrashing --
                    // adjusted for setTimeout's close-to-minimums (not too low),
                    // as this method SHOULD always be called once initially
                    Ready.scrollTimer = Ext.defer(Ready.pollScroll, 20);
                }
                return scrollable;
            },
            bind: function() {
                var doc = document,
                    topContext;
                if (Ready.bound) {
                    return;
                }
                // See if we are in an IFRAME? (doScroll ineffective here)
                try {
                    topContext = window.frameElement === undefined;
                } catch (e) {}
                // If we throw an exception, it means we're probably getting access
                // denied, which means we're in an iframe cross domain.
                if (!topContext || !doc.documentElement.doScroll) {
                    Ready.pollScroll = Ext.emptyFn;
                }
                // then noop this test altogether
                else if (Ready.pollScroll()) {
                    // starts scroll polling if necessary
                    return;
                }
                if (doc.readyState === 'complete') {
                    // Loaded AFTER initial document write/load...
                    Ready.onReadyEvent({
                        type: 'already ' + (doc.readyState || 'body')
                    });
                } else {
                    doc.attachEvent('onreadystatechange', Ready.onReadyStateChange);
                    window.attachEvent('onload', Ready.onReadyEvent);
                    Ready.bound = 1;
                }
            },
            unbind: function() {
                document.detachEvent('onreadystatechange', Ready.onReadyStateChange);
                window.detachEvent('onload', Ready.onReadyEvent);
                if (Ext.isNumber(Ready.scrollTimer)) {
                    Ext.undefer(Ready.scrollTimer);
                    Ready.scrollTimer = null;
                }
            },
            /**
         * This event handler is called when the readyState changes.
         * @private
         */
            onReadyStateChange: function() {
                var state = document.readyState;
                if (Ready.readyStatesRe.test(state)) {
                    Ready.onReadyEvent({
                        type: state
                    });
                }
            }
        });
    }
    /**
 * @property {Boolean} isDomReady
 * `true` when the document body is ready for use.
 * @member Ext
 * @readonly
 */
    /**
 * @property {Boolean} isReady
 * `true` when `isDomReady` is true and the Framework is ready for use.
 * @member Ext
 * @readonly
 */
    /**
 * @method onDocumentReady
 * @member Ext
 * Adds a listener to be notified when the document is ready (before onload and before
 * images are loaded).
 *
 * @param {Function} fn The method to call.
 * @param {Object} [scope] The scope (`this` reference) in which the handler function
 * executes. Defaults to the browser window.
 * @param {Object} [options] An object with extra options.
 * @param {Number} [options.delay=0] A number of milliseconds to delay.
 * @param {Number} [options.priority=0] Relative priority of this callback. A larger
 * number will result in the callback being sorted before the others.  Priorities
 * 1000 or greater and -1000 or lesser are reserved for internal framework use only.
 * @private
 */
    Ext.onDocumentReady = function(fn, scope, options) {
        var opt = {
                dom: true
            };
        if (options) {
            Ext.apply(opt, options);
        }
        Ready.on(fn, scope, opt);
    };
    /**
 * @method onReady
 * @member Ext
 * Adds a listener to be notified when the document is ready (before onload and before
 * images are loaded).
 *
 * @param {Function} fn The method to call.
 * @param {Object} [scope] The scope (`this` reference) in which the handler function
 * executes. Defaults to the browser window.
 * @param {Object} [options] An object with extra options.
 * @param {Number} [options.delay=0] A number of milliseconds to delay.
 * @param {Number} [options.priority=0] Relative priority of this callback. A larger
 * number will result in the callback being sorted before the others.  Priorities
 * 1000 or greater and -1000 or lesser are reserved for internal framework use only.
 * @param {Boolean} [options.dom=false] Pass `true` to only wait for DOM ready, `false`
 * means full Framework and DOM readiness.
 * numbers are reserved.
 */
    Ext.onReady = function(fn, scope, options) {
        Ready.on(fn, scope, options);
    };
    // A shortcut method for onReady with a high priority
    Ext.onInternalReady = function(fn, scope, options) {
        Ready.on(fn, scope, Ext.apply({
            priority: 1000
        }, options));
    };
    Ready.bind();
}());

// @tag class
/**
 * This class provides dynamic loading support for JavaScript classes. Application code
 * does not typically need to call `Ext.Loader` except perhaps to configure path mappings
 * when not using [Sencha Cmd](http://www.sencha.com/products/sencha-cmd/).
 *
 *      Ext.Loader.setPath('MyApp', 'app');
 *
 * When using Sencha Cmd, this is handled by the "bootstrap" provided by the application
 * build script and such configuration is not necessary.
 *
 * # Typical Usage
 *
 * The `Ext.Loader` is most often used behind the scenes to satisfy class references in
 * class declarations. Like so:
 *
 *      Ext.define('MyApp.view.Main', {
 *          extend: 'Ext.panel.Panel',
 *
 *          mixins: [
 *              'MyApp.util.Mixin'
 *          ],
 *
 *          requires: [
 *              'Ext.grid.Panel'
 *          ],
 *
 *          uses: [
 *              'MyApp.util.Stuff'
 *          ]
 *      });
 *
 * In all of these cases, `Ext.Loader` is used internally to resolve these class names
 * and ensure that the necessary class files are loaded.
 *
 * During development, these files are loaded individually for optimal debugging. For a
 * production use, [Sencha Cmd](http://www.sencha.com/products/sencha-cmd/) will replace
 * all of these strings with the actual resolved class references because it ensures that
 * the classes are all contained in the build in the correct order. In development, these
 * files will not be loaded until the `MyApp.view.Main` class indicates they are needed
 * as shown above.
 *
 * # Loading Classes
 *
 * You can also use `Ext.Loader` directly to load classes or files. The simplest form of
 * use is `{@link Ext#require}`.
 *
 * For example:
 *
 *      Ext.require('MyApp.view.Main', function () {
 *          // On callback, the MyApp.view.Main class is now loaded
 *
 *          var view = new MyApp.view.Main();
 *      });
 *
 * You can alternatively require classes by alias or wildcard.
 *
 *     Ext.require('widget.window');
 *
 *     Ext.require(['widget.window', 'layout.border', 'Ext.data.Connection']);
 *
 *     Ext.require(['widget.*', 'layout.*', 'Ext.data.*']);
 *
 * The callback function is optional.
 *
 * **Note** Using `Ext.require` at global scope will cause `{@link Ext#onReady}` and
 * `{@link Ext.app.Application#launch}` methods to be deferred until the required classes
 * are loaded. It is these cases where the callback function is most often unnecessary.
 *
 * ## Using Excludes
 *
 * Alternatively, you can exclude what you don't need:
 *
 *     // Include everything except Ext.tree.*
 *     Ext.exclude('Ext.tree.*').require('*');
 *
 *     // Include all widgets except widget.checkbox* (this will exclude
 *     // widget.checkbox, widget.checkboxfield, widget.checkboxgroup, etc.)
 *     Ext.exclude('widget.checkbox*').require('widget.*');
 *
 * # Dynamic Instantiation
 *
 * Another feature enabled by `Ext.Loader` is instantiation using class names or aliases.
 *
 * For example:
 *
 *      var win = Ext.create({
 *          xtype: 'window',
 *
 *          // or
 *          // xclass: 'Ext.window.Window'
 *
 *          title: 'Hello'
 *      });
 *
 * This form of creation can be useful if the type to create (`window` in the above) is
 * not known statically. Internally, `{@link Ext#method!create}` may need to *synchronously*
 * load the desired class and its requirements. Doing this will generate a warning in
 * the console:
 * 
 *      [Ext.Loader] Synchronously loading 'Ext.window.Window'...
 *
 * If you see these in your debug console, you should add the indicated class(es) to the
 * appropriate `requires` array (as above) or make an `{@link Ext#require}` call.
 * 
 * 
 * **Note** Using `{@link Ext#method!create}` has some performance overhead and is best reserved
 * for cases where the target class is not known until run-time.
 * 
 * @class Ext.Loader
 * @singleton
 */
Ext.Loader = (new function() {
    // @define Ext.Loader
    // @require Ext.Base
    // @require Ext.Class
    // @require Ext.ClassManager
    // @require Ext.mixin.Watchable
    // @require Ext.Function
    // @require Ext.Array
    // @require Ext.env.Ready
    var Loader = this,
        Manager = Ext.ClassManager,
        // this is an instance of Ext.Inventory
        Boot = Ext.Boot,
        Class = Ext.Class,
        Ready = Ext.env.Ready,
        alias = Ext.Function.alias,
        dependencyProperties = [
            'extend',
            'mixins',
            'requires'
        ],
        isInHistory = {},
        history = [],
        readyListeners = [],
        usedClasses = [],
        _requiresMap = {},
        _config = {
            /**
             * @cfg {Boolean} [enabled=true]
             * Whether or not to enable the dynamic dependency loading feature.
             */
            enabled: true,
            /**
             * @cfg {Boolean} [scriptChainDelay=false]
             * millisecond delay between asynchronous script injection (prevents stack
             * overflow on some user agents) 'false' disables delay but potentially
             * increases stack load.
             */
            scriptChainDelay: false,
            /**
             * @cfg {Boolean} [disableCaching=true]
             * Appends current timestamp to script files to prevent caching.
             */
            disableCaching: true,
            /**
             * @cfg {String} [disableCachingParam="_dc"]
             * The get parameter name for the cache buster's timestamp.
             */
            disableCachingParam: '_dc',
            /**
             * @cfg {Object} paths
             * The mapping from namespaces to file paths
             *
             *     {
             *         'Ext': '.', // This is set by default, Ext.layout.container.Container will be
             *                     // loaded from ./layout/Container.js
             *
             *         'My': './src/my_own_folder' // My.layout.Container will be loaded from
             *                                     // ./src/my_own_folder/layout/Container.js
             *     }
             *
             * Note that all relative paths are relative to the current HTML document.
             * If not being specified, for example, `Other.awesome.Class` will simply be
             * loaded from `"./Other/awesome/Class.js"`.
             */
            paths: Manager.paths,
            /**
             * @cfg {Boolean} preserveScripts
             * `false` to remove asynchronously loaded scripts, `true` to retain script
             * element for browser debugger compatibility and improved load performance.
             */
            preserveScripts: true,
            /**
             * @cfg {String} scriptCharset
             * Optional charset to specify encoding of dynamic script content.
             */
            scriptCharset: undefined
        },
        // These configs are delegated to Ext.Script and may need different names:
        delegatedConfigs = {
            disableCaching: true,
            disableCachingParam: true,
            preserveScripts: true,
            scriptChainDelay: 'loadDelay'
        };
    Ext.apply(Loader, {
        /**
         * @private
         */
        isInHistory: isInHistory,
        /**
         * Flag indicating whether there are still files being loaded
         * @private
         */
        isLoading: false,
        /**
         * An array of class names to keep track of the dependency loading order.
         * This is not guaranteed to be the same everytime due to the asynchronous
         * nature of the Loader.
         *
         * @property {Array} history
         */
        history: history,
        /**
         * Configuration
         * @private
         */
        config: _config,
        /**
         * Maintain the list of listeners to execute when all required scripts are fully loaded
         * @private
         */
        readyListeners: readyListeners,
        /**
         * Contains classes referenced in `uses` properties.
         * @private
         */
        optionalRequires: usedClasses,
        /**
         * Map of fully qualified class names to an array of dependent classes.
         * @private
         */
        requiresMap: _requiresMap,
        /** @private */
        hasFileLoadError: false,
        /**
         * The number of scripts loading via loadScript.
         * @private
         */
        scriptsLoading: 0,
        /**
         * @private
         */
        classesLoading: {},
        missingCount: 0,
        missingQueue: {},
        /**
         * @private
         */
        syncModeEnabled: false,
        init: function() {
            // initalize the default path of the framework
            var scripts = document.getElementsByTagName('script'),
                src = scripts[scripts.length - 1].src,
                path = src.substring(0, src.lastIndexOf('/') + 1),
                meta = Ext._classPathMetadata,
                microloader = Ext.Microloader,
                manifest = Ext.manifest,
                loadOrder, baseUrl, loadlen, l, loadItem;
            if (src.indexOf("packages/core/src/") !== -1) {
                path = path + "../../";
            } else if (src.indexOf("/core/src/class/") !== -1) {
                path = path + "../../../";
            }
            if (!Manager.getPath("Ext")) {
                Manager.setPath('Ext', path + 'src');
            }
            // Pull in Cmd generated metadata if available.
            if (meta) {
                Ext._classPathMetadata = null;
                Loader.addClassPathMappings(meta);
            }
            if (manifest) {
                loadOrder = manifest.loadOrder;
                // if the manifest paths were calculated as relative to the 
                // bootstrap file, then we need to prepend Boot.baseUrl to the
                // paths before processing
                baseUrl = Ext.Boot.baseUrl;
                if (loadOrder && manifest.bootRelative) {
                    for (loadlen = loadOrder.length , l = 0; l < loadlen; l++) {
                        loadItem = loadOrder[l];
                        loadItem.path = baseUrl + loadItem.path;
                        loadItem.canonicalPath = true;
                    }
                }
            }
            if (microloader) {
                Ready.block();
                microloader.onMicroloaderReady(function() {
                    Ready.unblock();
                });
            }
        },
        /**
         * @method setConfig
         * Set the configuration for the loader. This should be called right after ext-(debug).js
         * is included in the page, and before Ext.onReady. i.e:
         *
         *     <script type="text/javascript" src="ext-core-debug.js"></script>
         *     <script type="text/javascript">
         *         Ext.Loader.setConfig({
         *           enabled: true,
         *           paths: {
         *               'My': 'my_own_path'
         *           }
         *         });
         *     </script>
         *     <script type="text/javascript">
         *         Ext.require(...);
         *
         *         Ext.onReady(function() {
         *           // application code here
         *         });
         *     </script>
         *
         * Refer to config options of {@link Ext.Loader} for the list of possible properties
         *
         * @param {Object} config The config object to override the default values
         * @return {Ext.Loader} this
         */
        setConfig: Ext.Function.flexSetter(function(name, value) {
            var delegated = delegatedConfigs[name];
            if (name === 'paths') {
                Loader.setPath(value);
            } else {
                _config[name] = value;
                if (delegated) {
                    Boot.setConfig((delegated === true) ? name : delegated, value);
                }
            }
            return Loader;
        }),
        /**
         * Get the config value corresponding to the specified name. If no name is given,
         * will return the config object
         *
         * @param {String} name The config property name
         * @return {Object}
         */
        getConfig: function(name) {
            return name ? _config[name] : _config;
        },
        /**
         * Sets the path of a namespace.
         * For Example:
         *
         *     Ext.Loader.setPath('Ext', '.');
         *
         * @param {String/Object} name See {@link Ext.Function#flexSetter flexSetter}
         * @param {String} [path] See {@link Ext.Function#flexSetter flexSetter}
         * @return {Ext.Loader} this
         * @method
         */
        setPath: function() {
            // Paths are an Ext.Inventory thing and ClassManager is an instance of that:
            Manager.setPath.apply(Manager, arguments);
            return Loader;
        },
        /**
         * Sets a batch of path entries
         *
         * @param {Object} paths a set of className: path mappings
         * @return {Ext.Loader} this
         */
        addClassPathMappings: function(paths) {
            // Paths are an Ext.Inventory thing and ClassManager is an instance of that:
            Manager.setPath(paths);
            return Loader;
        },
        /**
         * fixes up loader path configs by prepending Ext.Boot#baseUrl to the beginning
         * of the path, then delegates to Ext.Loader#addClassPathMappings
         * @param pathConfig
         */
        addBaseUrlClassPathMappings: function(pathConfig) {
            var name;
            for (name in pathConfig) {
                pathConfig[name] = Boot.baseUrl + pathConfig[name];
            }
            Ext.Loader.addClassPathMappings(pathConfig);
        },
        /**
         * Translates a className to a file path by adding the
         * the proper prefix and converting the .'s to /'s. For example:
         *
         *     Ext.Loader.setPath('My', '/path/to/My');
         *
         *     // alerts '/path/to/My/awesome/Class.js'
         *     alert(Ext.Loader.getPath('My.awesome.Class'));
         *
         * Note that the deeper namespace levels, if explicitly set, are always resolved first.
         * For example:
         *
         *     Ext.Loader.setPath({
         *         'My': '/path/to/lib',
         *         'My.awesome': '/other/path/for/awesome/stuff',
         *         'My.awesome.more': '/more/awesome/path'
         *     });
         *
         *     // alerts '/other/path/for/awesome/stuff/Class.js'
         *     alert(Ext.Loader.getPath('My.awesome.Class'));
         *
         *     // alerts '/more/awesome/path/Class.js'
         *     alert(Ext.Loader.getPath('My.awesome.more.Class'));
         *
         *     // alerts '/path/to/lib/cool/Class.js'
         *     alert(Ext.Loader.getPath('My.cool.Class'));
         *
         *     // alerts 'Unknown/strange/Stuff.js'
         *     alert(Ext.Loader.getPath('Unknown.strange.Stuff'));
         *
         * @param {String} className
         * @return {String} path
         */
        getPath: function(className) {
            // Paths are an Ext.Inventory thing and ClassManager is an instance of that:
            return Manager.getPath(className);
        },
        require: function(expressions, fn, scope, excludes) {
            var classNames;
            if (excludes) {
                return Loader.exclude(excludes).require(expressions, fn, scope);
            }
            classNames = Manager.getNamesByExpression(expressions);
            return Loader.load(classNames, fn, scope);
        },
        syncRequire: function() {
            var wasEnabled = Loader.syncModeEnabled,
                ret;
            Loader.syncModeEnabled = true;
            ret = Loader.require.apply(Loader, arguments);
            Loader.syncModeEnabled = wasEnabled;
            return ret;
        },
        exclude: function(excludes) {
            var selector = Manager.select({
                    require: function(classNames, fn, scope) {
                        return Loader.load(classNames, fn, scope);
                    },
                    syncRequire: function(classNames, fn, scope) {
                        var wasEnabled = Loader.syncModeEnabled,
                            ret;
                        Loader.syncModeEnabled = true;
                        ret = Loader.load(classNames, fn, scope);
                        Loader.syncModeEnabled = wasEnabled;
                        return ret;
                    }
                });
            selector.exclude(excludes);
            return selector;
        },
        load: function(classNames, callback, scope) {
            if (callback) {
                if (callback.length) {
                    // If callback expects arguments, shim it with a function that will map
                    // the requires class(es) from the names we are given.
                    callback = Loader.makeLoadCallback(classNames, callback);
                }
                callback = callback.bind(scope || Ext.global);
            }
            /* eslint-disable-next-line vars-on-top */
            var state = Manager.classState,
                missingClassNames = [],
                urls = [],
                urlByClass = {},
                numClasses = classNames.length,
                className, i, numMissing;
            for (i = 0; i < numClasses; ++i) {
                className = Manager.resolveName(classNames[i]);
                if (!Manager.isCreated(className)) {
                    missingClassNames.push(className);
                    if (!state[className]) {
                        urlByClass[className] = Loader.getPath(className);
                        urls.push(urlByClass[className]);
                    }
                }
            }
            // If the dynamic dependency feature is not being used, throw an error
            // if the dependencies are not defined
            numMissing = missingClassNames.length;
            if (numMissing) {
                Loader.missingCount += numMissing;
                Manager.onCreated(function() {
                    if (callback) {
                        Ext.callback(callback, scope, arguments);
                    }
                    Loader.checkReady();
                }, Loader, missingClassNames);
                if (!_config.enabled) {
                    Ext.raise("Ext.Loader is not enabled, so dependencies cannot be resolved " + "dynamically. Missing required class" + ((missingClassNames.length > 1) ? "es" : "") + ": " + missingClassNames.join(', '));
                }
                if (urls.length) {
                    Loader.loadScripts({
                        url: urls,
                        // scope will be this options object so we can pass these along:
                        _classNames: missingClassNames,
                        _urlByClass: urlByClass
                    });
                } else {
                    // need to call checkReady here, as the _missingCoun
                    // may have transitioned from 0 to > 0, meaning we
                    // need to block ready
                    Loader.checkReady();
                }
            } else {
                if (callback) {
                    callback.call(scope);
                }
                // need to call checkReady here, as the _missingCoun
                // may have transitioned from 0 to > 0, meaning we
                // need to block ready
                Loader.checkReady();
            }
            if (Loader.syncModeEnabled) {
                // Class may have been just loaded or was already loaded
                if (numClasses === 1) {
                    return Manager.get(classNames[0]);
                }
            }
            return Loader;
        },
        makeLoadCallback: function(classNames, callback) {
            return function() {
                var classes = [],
                    i = classNames.length;
                while (i-- > 0) {
                    classes[i] = Manager.get(classNames[i]);
                }
                return callback.apply(this, classes);
            };
        },
        onLoadFailure: function(request) {
            var options = this,
                entries = request.entries || [],
                onError = options.onError,
                error, entry, i;
            Loader.hasFileLoadError = true;
            --Loader.scriptsLoading;
            if (onError) {
                for (i = 0; i < entries.length; i++) {
                    entry = entries[i];
                    if (entry.error) {
                        error = new Error('Failed to load: ' + entry.url);
                        break;
                    }
                }
                error = error || new Error('Failed to load');
                onError.call(options.userScope, options, error, request);
            } else {
                Ext.log.error("[Ext.Loader] Some requested files failed to load.");
            }
            Loader.checkReady();
        },
        onLoadSuccess: function() {
            var options = this,
                onLoad = options.onLoad,
                classNames = options._classNames,
                urlByClass = options._urlByClass,
                state = Manager.classState,
                missingQueue = Loader.missingQueue,
                className, i, len;
            --Loader.scriptsLoading;
            if (onLoad) {
                // TODO: need an adapter to convert to v4 onLoad signatures
                onLoad.call(options.userScope, options);
            }
            // onLoad can cause more loads to start, so it must run first
            // classNames is the array of *all* classes that load() was asked to load,
            // including those that might have been already loaded but not yet created.
            // urlByClass is a map of only those classes that we asked Boot to load.
            for (i = 0 , len = classNames.length; i < len; i++) {
                className = classNames[i];
                // When a script is loaded and executed, we should have Ext.define() called
                // for at least one of the classes in the list, which will set the state
                // for that class. That by itself does not mean that the class is available
                // *now* but it means that ClassManager is tracking it and will fire the
                // onCreated callback that we set back in load().
                // However if there is no state for the class, that may mean two things:
                // either it is not a Ext class, or it is truly missing. In any case we need
                // to watch for that thing ourselves, which we will do every checkReady().
                if (!state[className]) {
                    missingQueue[className] = urlByClass[className];
                }
            }
            Loader.checkReady();
        },
        // TODO: this timing of this needs to be deferred until all classes have had
        // a chance to be created
        reportMissingClasses: function() {
            var missingQueue = Loader.missingQueue,
                missingClasses = [],
                missingPaths = [],
                missingClassName;
            if (!Loader.syncModeEnabled && !Loader.scriptsLoading && Loader.isLoading && !Loader.hasFileLoadError) {
                for (missingClassName in missingQueue) {
                    missingClasses.push(missingClassName);
                    missingPaths.push(missingQueue[missingClassName]);
                }
                if (missingClasses.length) {
                    throw new Error("The following classes are not declared even if their files " + "have been loaded: '" + missingClasses.join("', '") + "'. Please check the source code of their " + "corresponding files for possible typos: '" + missingPaths.join("', '"));
                }
            }
        },
        /**
         * Add a new listener to be executed when all required scripts are fully loaded
         *
         * @param {Function} fn The function callback to be executed
         * @param {Object} scope The execution scope (`this`) of the callback function.
         * @param {Boolean} [withDomReady=true] Pass `false` to not also wait for document
         * dom ready.
         * @param {Object} [options] Additional callback options.
         * @param {Number} [options.delay=0] A number of milliseconds to delay.
         * @param {Number} [options.priority=0] Relative priority of this callback. Negative
         * numbers are reserved.
         */
        onReady: function(fn, scope, withDomReady, options) {
            var listener;
            if (withDomReady) {
                Ready.on(fn, scope, options);
            } else {
                listener = Ready.makeListener(fn, scope, options);
                if (Loader.isLoading) {
                    readyListeners.push(listener);
                } else {
                    Ready.invoke(listener);
                }
            }
        },
        /**
         * @private
         * Ensure that any classes referenced in the `uses` property are loaded.
         */
        addUsedClasses: function(classes) {
            var cls, i, ln;
            if (classes) {
                classes = (typeof classes === 'string') ? [
                    classes
                ] : classes;
                for (i = 0 , ln = classes.length; i < ln; i++) {
                    cls = classes[i];
                    if (typeof cls === 'string' && !Ext.Array.contains(usedClasses, cls)) {
                        usedClasses.push(cls);
                    }
                }
            }
            return Loader;
        },
        /**
         * @private
         */
        triggerReady: function() {
            var listener,
                refClasses = usedClasses;
            if (Loader.isLoading && refClasses.length) {
                // Empty the array to eliminate potential recursive loop issue
                usedClasses = [];
                // this may immediately call us back if all 'uses' classes
                // have been loaded
                Loader.require(refClasses);
            } else {
                // Must clear this before calling callbacks. This will cause any new loads
                // to call Ready.block() again. See below for more on this.
                Loader.isLoading = false;
                // These listeners are just those attached directly to Loader to wait for
                // class loading only.
                readyListeners.sort(Ready.sortFn);
                // this method can be called with Loader.isLoading either true or false
                // (can be called with false when all 'uses' classes are already loaded)
                // this may bypass the above if condition
                while (readyListeners.length && !Loader.isLoading) {
                    // we may re-enter triggerReady so we cannot necessarily iterate the
                    // readyListeners array
                    listener = readyListeners.pop();
                    Ready.invoke(listener);
                }
                // If the DOM is also ready, this will fire the normal onReady listeners.
                // An astute observer would note that we may now be back to isLoading and
                // so ask "Why you call unblock?". The reason is that we must match the
                // calls to block and since we transitioned from isLoading to !isLoading
                // here we must call unblock. If we have transitioned back to isLoading in
                // the above loop it will have called block again so the counter will be
                // increased and this call will not reduce the block count to 0. This is
                // done by loadScripts.
                Ready.unblock();
            }
        },
        /**
         * @private
         * @param {String} className
         */
        historyPush: function(className) {
            if (className && !isInHistory[className] && !Manager.overrideMap[className]) {
                isInHistory[className] = true;
                history.push(className);
            }
            return Loader;
        },
        /**
         * This is an internal method that delegate content loading to the 
         * bootstrap layer.
         * @private
         * @param params
         */
        loadScripts: function(params) {
            var manifest = Ext.manifest,
                loadOrder = manifest && manifest.loadOrder,
                loadOrderMap = manifest && manifest.loadOrderMap,
                options;
            ++Loader.scriptsLoading;
            // if the load order map hasn't been created, create it now 
            // and cache on the manifest
            if (loadOrder && !loadOrderMap) {
                manifest.loadOrderMap = loadOrderMap = Boot.createLoadOrderMap(loadOrder);
            }
            // verify the loading state, as this may have transitioned us from
            // not loading to loading
            Loader.checkReady();
            options = Ext.apply({
                loadOrder: loadOrder,
                loadOrderMap: loadOrderMap,
                charset: _config.scriptCharset,
                success: Loader.onLoadSuccess,
                failure: Loader.onLoadFailure,
                sync: Loader.syncModeEnabled,
                _classNames: []
            }, params);
            options.userScope = options.scope;
            options.scope = options;
            Boot.load(options);
        },
        /**
         * This method is provide for use by the bootstrap layer.
         * @private
         * @param {String[]} urls
         */
        loadScriptsSync: function(urls) {
            var syncwas = Loader.syncModeEnabled;
            Loader.syncModeEnabled = true;
            Loader.loadScripts({
                url: urls
            });
            Loader.syncModeEnabled = syncwas;
        },
        /**
         * This method is provide for use by the bootstrap layer.
         * @private
         * @param {String[]} urls
         */
        loadScriptsSyncBasePrefix: function(urls) {
            var syncwas = Loader.syncModeEnabled;
            Loader.syncModeEnabled = true;
            Loader.loadScripts({
                url: urls,
                prependBaseUrl: true
            });
            Loader.syncModeEnabled = syncwas;
        },
        /**
         * Loads the specified script URL and calls the supplied callbacks. If this method
         * is called before {@link Ext#isReady}, the script's load will delay the transition
         * to ready. This can be used to load arbitrary scripts that may contain further
         * {@link Ext#require Ext.require} calls.
         *
         * @param {Object/String/String[]} options The options object or simply the URL(s) to load.
         * @param {String} options.url The URL from which to load the script.
         * @param {Function} [options.onLoad] The callback to call on successful load.
         * @param {Function} [options.onError] The callback to call on failure to load.
         * @param {Object} [options.scope] The scope (`this`) for the supplied callbacks.
         */
        loadScript: function(options) {
            var isString = typeof options === 'string',
                isArray = options instanceof Array,
                isObject = !isArray && !isString,
                url = isObject ? options.url : options,
                onError = isObject && options.onError,
                onLoad = isObject && options.onLoad,
                scope = isObject && options.scope,
                request = {
                    url: url,
                    scope: scope,
                    onLoad: onLoad,
                    onError: onError,
                    _classNames: []
                };
            Loader.loadScripts(request);
        },
        /**
         * @private
         */
        checkMissingQueue: function() {
            var missingQueue = Loader.missingQueue,
                newQueue = {},
                missing = 0,
                name;
            for (name in missingQueue) {
                // If class state is available for the name, that means ClassManager
                // is tracking it and will fire callback when it is created.
                // We only need to track non-class things in the Loader.
                if (!(Manager.classState[name] || Manager.isCreated(name))) {
                    newQueue[name] = missingQueue[name];
                    missing++;
                }
            }
            Loader.missingCount = missing;
            Loader.missingQueue = newQueue;
        },
        /**
         * @private
         */
        checkReady: function() {
            var wasLoading = Loader.isLoading,
                isLoading;
            Loader.checkMissingQueue();
            isLoading = Loader.missingCount + Loader.scriptsLoading;
            if (isLoading && !wasLoading) {
                Ready.block();
                Loader.isLoading = !!isLoading;
            } else if (!isLoading && wasLoading) {
                Loader.triggerReady();
            }
            if (!Loader.scriptsLoading && Loader.missingCount) {
                // Things look bad, but since load requests may come later, defer this
                // for a bit then check if things are still stuck.
                Ext.defer(function() {
                    var name;
                    if (!Loader.scriptsLoading && Loader.missingCount) {
                        Ext.log.error('[Loader] The following classes failed to load:');
                        for (name in Loader.missingQueue) {
                            Ext.log.error('[Loader] ' + name + ' from ' + Loader.missingQueue[name]);
                        }
                    }
                }, 1000);
            }
        }
    });
    /**
     * Loads all classes by the given names and all their direct dependencies; optionally
     * executes the given callback function when finishes, within the optional scope.
     *
     * @param {String/String[]} expressions The class, classes or wildcards to load.
     * @param {Function} [fn] The callback function.
     * @param {Object} [scope] The execution scope (`this`) of the callback function.
     * @member Ext
     * @method require
     */
    Ext.require = alias(Loader, 'require');
    /**
     * Synchronously loads all classes by the given names and all their direct dependencies;
     * optionally executes the given callback function when finishes, within the optional scope.
     *
     * @param {String/String[]} expressions The class, classes or wildcards to load.
     * @param {Function} [fn] The callback function.
     * @param {Object} [scope] The execution scope (`this`) of the callback function.
     * @member Ext
     * @method syncRequire
     */
    Ext.syncRequire = alias(Loader, 'syncRequire');
    /**
     * Explicitly exclude files from being loaded. Useful when used in conjunction with a
     * broad include expression. Can be chained with more `require` and `exclude` methods,
     * for example:
     *
     *     Ext.exclude('Ext.data.*').require('*');
     *
     *     Ext.exclude('widget.button*').require('widget.*');
     *
     * @param {String/String[]} excludes
     * @return {Object} Contains `exclude`, `require` and `syncRequire` methods for chaining.
     * @member Ext
     * @method exclude
     */
    Ext.exclude = alias(Loader, 'exclude');
    /**
     * @cfg {String[]} requires
     * @member Ext.Class
     * List of classes that have to be loaded before instantiating this class.
     * For example:
     *
     *     Ext.define('Mother', {
     *         requires: ['Child'],
     *         giveBirth: function() {
     *             // we can be sure that child class is available.
     *             return new Child();
     *         }
     *     });
     */
    Class.registerPreprocessor('loader', function(cls, data, hooks, continueFn) {
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(cls, 'Ext.Loader#loaderPreprocessor', arguments);
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            dependencies = [],
            dependency,
            className = Manager.getName(cls),
            i, j, ln, subLn, value, propertyName, propertyValue, requiredMap;
        /*
        Loop through the dependencyProperties, look for string class names and push
        them into a stack, regardless of whether the property's value is a string, array or object.
        For example:
        {
              extend: 'Ext.MyClass',
              requires: ['Ext.some.OtherClass'],
              mixins: {
                  thing: 'Foo.bar.Thing';
              }
        }
        which will later be transformed into:
        {
              extend: Ext.MyClass,
              requires: [Ext.some.OtherClass],
              mixins: {
                  thing: Foo.bar.Thing;
              }
        }
        */
        for (i = 0 , ln = dependencyProperties.length; i < ln; i++) {
            propertyName = dependencyProperties[i];
            if (data.hasOwnProperty(propertyName)) {
                propertyValue = data[propertyName];
                if (typeof propertyValue === 'string') {
                    dependencies.push(propertyValue);
                } else if (propertyValue instanceof Array) {
                    for (j = 0 , subLn = propertyValue.length; j < subLn; j++) {
                        value = propertyValue[j];
                        if (typeof value === 'string') {
                            dependencies.push(value);
                        }
                    }
                } else if (typeof propertyValue !== 'function') {
                    for (j in propertyValue) {
                        if (propertyValue.hasOwnProperty(j)) {
                            value = propertyValue[j];
                            if (typeof value === 'string') {
                                dependencies.push(value);
                            }
                        }
                    }
                }
            }
        }
        if (dependencies.length === 0) {
            return;
        }
        if (className) {
            _requiresMap[className] = dependencies;
        }
        /* eslint-disable-next-line vars-on-top */
        var manifestClasses = Ext.manifest && Ext.manifest.classes,
            deadlockPath = [],
            detectDeadlock;
        /*
         * Automatically detect deadlocks before-hand,
         * will throw an error with detailed path for ease of debugging. Examples
         * of deadlock cases:
         *
         *  - A extends B, then B extends A
         *  - A requires B, B requires C, then C requires A
         *
         * The detectDeadlock function will recursively transverse till the leaf, hence
         * it can detect deadlocks no matter how deep the path is. However we don't need
         * to run this check if the class name is in the manifest: that means Cmd has
         * already resolved all dependencies for this class with no deadlocks.
         */
        if (className && (!manifestClasses || !manifestClasses[className])) {
            requiredMap = Loader.requiredByMap || (Loader.requiredByMap = {});
            for (i = 0 , ln = dependencies.length; i < ln; i++) {
                dependency = dependencies[i];
                (requiredMap[dependency] || (requiredMap[dependency] = [])).push(className);
            }
            detectDeadlock = function(cls) {
                var requires = _requiresMap[cls],
                    dep, i, ln;
                deadlockPath.push(cls);
                if (requires) {
                    if (Ext.Array.contains(requires, className)) {
                        Ext.Error.raise("Circular requirement detected! '" + className + "' and '" + deadlockPath[1] + "' mutually require each other. " + "Path: " + deadlockPath.join(' -> ') + " -> " + deadlockPath[0]);
                    }
                    for (i = 0 , ln = requires.length; i < ln; i++) {
                        dep = requires[i];
                        if (!isInHistory[dep]) {
                            detectDeadlock(requires[i]);
                        }
                    }
                }
            };
            detectDeadlock(className);
        }
        (className ? Loader.exclude(className) : Loader).require(dependencies, function() {
            var i, ln, j, subLn, k;
            for (i = 0 , ln = dependencyProperties.length; i < ln; i++) {
                propertyName = dependencyProperties[i];
                if (data.hasOwnProperty(propertyName)) {
                    propertyValue = data[propertyName];
                    if (typeof propertyValue === 'string') {
                        data[propertyName] = Manager.get(propertyValue);
                    } else if (propertyValue instanceof Array) {
                        for (j = 0 , subLn = propertyValue.length; j < subLn; j++) {
                            value = propertyValue[j];
                            if (typeof value === 'string') {
                                data[propertyName][j] = Manager.get(value);
                            }
                        }
                    } else if (typeof propertyValue !== 'function') {
                        for (k in propertyValue) {
                            if (propertyValue.hasOwnProperty(k)) {
                                value = propertyValue[k];
                                if (typeof value === 'string') {
                                    data[propertyName][k] = Manager.get(value);
                                }
                            }
                        }
                    }
                }
            }
            continueFn.call(me, cls, data, hooks);
        });
        return false;
    }, true, 'after', 'className');
    /**
     * @cfg {String[]} uses
     * @member Ext.Class
     * List of optional classes to load together with this class. These aren't neccessarily loaded
     * before this class is created, but are guaranteed to be available before Ext.onReady
     * listeners are invoked. For example:
     *
     *     Ext.define('Mother', {
     *         uses: ['Child'],
     *         giveBirth: function() {
     *             // This code might, or might not work:
     *             // return new Child();
     *
     *             // Instead use Ext.create() to load the class at the spot if not loaded already:
     *             return Ext.create('Child');
     *         }
     *     });
     */
    Manager.registerPostprocessor('uses', function(name, cls, data) {
        var uses = data.uses,
            classNames;
        if (Ext.classSystemMonitor) {
            Ext.classSystemMonitor(cls, 'Ext.Loader#usesPostprocessor', arguments);
        }
        if (uses) {
            classNames = Manager.getNamesByExpression(data.uses);
            Loader.addUsedClasses(classNames);
        }
    });
    Manager.onCreated(Loader.historyPush);
    Loader.init();
}());
//-----------------------------------------------------------------------------
// Use performance.now when available to keep timestamps consistent.
Ext._endTime = Ext.ticks();
// This hook is to allow tools like DynaTrace to deterministically detect the availability
// of Ext.onReady. Since Loader takes over Ext.onReady this must be done here and not in
// Ext.env.Ready.
if (Ext._beforereadyhandler) {
    Ext._beforereadyhandler();
}

/**
 * This class is a base class for mixins. These are classes that extend this class and are
 * designed to be used as a `mixin` by user code.
 *
 * It provides mixins with the ability to "hook" class methods of the classes in to which
 * they are mixed. For example, consider the `destroy` method pattern. If a mixin class
 * had cleanup requirements, it would need to be called as part of `destroy`.
 * 
 * Starting with a basic class we might have:
 * 
 *      Ext.define('Foo.bar.Base', {
 *          destroy: function() {
 *              console.log('B');
 *              // cleanup
 *          }
 *      });
 *
 * A derived class would look like this:
 *
 *      Ext.define('Foo.bar.Derived', {
 *          extend: 'Foo.bar.Base',
 *
 *          destroy: function() {
 *              console.log('D');
 *              // more cleanup
 *
 *              this.callParent(); // let Foo.bar.Base cleanup as well
 *          }
 *      });
 *
 * To see how using this class help, start with a "normal" mixin class that also needs to
 * cleanup its resources. These mixins must be called explicitly by the classes that use
 * them. For example:
 * 
 *      Ext.define('Foo.bar.Util', {
 *          destroy: function() {
 *              console.log('U');
 *          }
 *      });
 * 
 *      Ext.define('Foo.bar.Derived', {
 *          extend: 'Foo.bar.Base',
 *
 *          mixins: {
 *              util: 'Foo.bar.Util'
 *          },
 *
 *          destroy: function() {
 *              console.log('D');
 *              // more cleanup
 *
 *              this.mixins.util.destroy.call(this);
 *
 *              this.callParent(); // let Foo.bar.Base cleanup as well
 *          }
 *      });
 *
 *      var obj = new Foo.bar.Derived();
 *
 *      obj.destroy();
 *      // logs D then U then B
 *
 * This class is designed to solve the above in simpler and more reliable way.
 *
 * ## mixinConfig
 * 
 * Using `mixinConfig` the mixin class can provide "before" or "after" hooks that do not
 * involve the derived class implementation. This also means the derived class cannot
 * adjust parameters to the hook methods.
 * 
 *      Ext.define('Foo.bar.Util', {
 *          extend: 'Ext.Mixin',
 *
 *          mixinConfig: {
 *              after: {
 *                  destroy: 'destroyUtil'
 *              }
 *          },
 *          
 *          destroyUtil: function() {
 *              console.log('U');
 *          }
 *      });
 * 
 *      Ext.define('Foo.bar.Class', {
 *          mixins: {
 *              util: 'Foo.bar.Util'
 *          },
 *
 *          destroy: function() {
 *              console.log('D');
 *          }
 *      });
 *
 *      var obj = new Foo.bar.Derived();
 *
 *      obj.destroy();
 *      // logs D then U
 * 
 *  If the destruction should occur in the other order, you can use `before`:
 * 
 *      Ext.define('Foo.bar.Util', {
 *          extend: 'Ext.Mixin',
 *
 *          mixinConfig: {
 *              before: {
 *                  destroy: 'destroyUtil'
 *              }
 *          },
 *          
 *          destroyUtil: function() {
 *              console.log('U');
 *          }
 *      });
 * 
 *      Ext.define('Foo.bar.Class', {
 *          mixins: {
 *              util: 'Foo.bar.Util'
 *          },
 *
 *          destroy: function() {
 *              console.log('D');
 *          }
 *      });
 *
 *      var obj = new Foo.bar.Derived();
 *
 *      obj.destroy();
 *      // logs U then D
 *
 * ### Configs
 *
 * Normally when a mixin defines `config` properties and the class into which the mixin is
 * initially mixed needs to specify values for those configs, the class processor does not
 * yet recognize these config and instead retains the properties on te target class
 * prototype.
 *
 * Changing the above behavior would potentially break application code, so this class
 * provides a way to remedy this:
 *
 *      Ext.define('Foo.bar.Util', {
 *          extend: 'Ext.Mixin',
 *
 *          mixinConfig: {
 *              configs: true
 *          },
 *
 *          config: {
 *              foo: null
 *          }
 *      });
 *
 * Now the direct user class can correctly specify `foo` config properties:
 *
 *      Ext.define('Some.other.Class', {
 *          mixins: [
 *              'Foo.bar.Util'
 *          ],
 *
 *          foo: 'bar'  // recognized as the foo config form Foo.bar.Util
 *      });
 *
 * ### Chaining
 *
 * One way for a mixin to provide methods that act more like normal inherited methods is
 * to use an `on` declaration. These methods will be injected into the `callParent` chain
 * between the derived and superclass. For example:
 *
 *      Ext.define('Foo.bar.Util', {
 *          extend: 'Ext.Mixin',
 *
 *          mixinConfig: {
 *              on: {
 *                  destroy: function() {
 *                      console.log('M');
 *                  }
 *              }
 *          }
 *      });
 *
 *      Ext.define('Foo.bar.Base', {
 *          destroy: function() {
 *              console.log('B');
 *          }
 *      });
 *
 *      Ext.define('Foo.bar.Derived', {
 *          extend: 'Foo.bar.Base',
 *
 *          mixins: {
 *              util: 'Foo.bar.Util'
 *          },
 *
 *          destroy: function() {
 *              this.callParent();
 *              console.log('D');
 *          }
 *      });
 *
 *      var obj = new Foo.bar.Derived();
 *
 *      obj.destroy();
 *      // logs M then B then D
 *
 * As with `before` and `after`, the value of `on` can be a method name.
 *
 *      Ext.define('Foo.bar.Util', {
 *          extend: 'Ext.Mixin',
 *
 *          mixinConfig: {
 *              on: {
 *                  destroy: 'onDestroy'
 *              }
 *          }
 *
 *          onDestroy: function() {
 *              console.log('M');
 *          }
 *      });
 *
 * Because this technique leverages `callParent`, the derived class controls the time and
 * parameters for the call to all of its bases (be they `extend` or `mixin` flavor).
 *
 * ### Derivations
 *
 * Some mixins need to process class extensions of their target class. To do this you can
 * define an `extended` method like so:
 *
 *      Ext.define('Foo.bar.Util', {
 *          extend: 'Ext.Mixin',
 *
 *          mixinConfig: {
 *              extended: function(baseClass, derivedClass, classBody) {
 *                  // This function is called whenever a new "derivedClass" is created
 *                  // that extends a "baseClass" in to which this mixin was mixed.
 *              }
 *          }
 *      });
 *
 * @protected
 */
Ext.define('Ext.Mixin', function(Mixin) {
    return {
        // eslint-disable-line brace-style
        statics: {
            addHook: function(hookFn, targetClass, methodName, mixinClassPrototype) {
                var isFunc = Ext.isFunction(hookFn),
                    hook = function() {
                        var a = arguments,
                            fn = isFunc ? hookFn : mixinClassPrototype[hookFn],
                            result = this.callParent(a);
                        fn.apply(this, a);
                        return result;
                    },
                    existingFn = targetClass.hasOwnProperty(methodName) && targetClass[methodName];
                if (isFunc) {
                    hookFn.$previous = Ext.emptyFn;
                }
                // no callParent for these guys
                hook.$name = methodName;
                hook.$owner = targetClass.self;
                if (existingFn) {
                    hook.$previous = existingFn.$previous;
                    existingFn.$previous = hook;
                } else {
                    targetClass[methodName] = hook;
                }
            }
        },
        onClassExtended: function(cls, data) {
            var mixinConfig = data.mixinConfig,
                hooks = data.xhooks,
                superclass = cls.superclass,
                onClassMixedIn = data.onClassMixedIn,
                afterClassMixedIn = data.afterClassMixedIn,
                afters, befores, configs, extended, mixed, parentMixinConfig;
            if (hooks) {
                // Legacy way
                delete data.xhooks;
                (mixinConfig || (data.mixinConfig = mixinConfig = {})).on = hooks;
            }
            if (mixinConfig) {
                parentMixinConfig = superclass.mixinConfig;
                if (parentMixinConfig) {
                    data.mixinConfig = mixinConfig = Ext.merge({}, parentMixinConfig, mixinConfig);
                }
                data.mixinId = mixinConfig.id;
                if (mixinConfig.beforeHooks) {
                    Ext.raise('Use of "beforeHooks" is deprecated - use "before" instead');
                }
                if (mixinConfig.hooks) {
                    Ext.raise('Use of "hooks" is deprecated - use "after" instead');
                }
                if (mixinConfig.afterHooks) {
                    Ext.raise('Use of "afterHooks" is deprecated - use "after" instead');
                }
                afters = mixinConfig.after;
                befores = mixinConfig.before;
                configs = mixinConfig.configs;
                extended = mixinConfig.extended;
                hooks = mixinConfig.on;
                mixed = mixinConfig.mixed;
            }
            if (afters || befores || hooks || extended) {
                // Note: tests are with Ext.Class
                data.onClassMixedIn = function(targetClass) {
                    var mixin = this.prototype,
                        targetProto = targetClass.prototype,
                        key;
                    if (befores) {
                        Ext.Object.each(befores, function(key, value) {
                            targetClass.hookMember(key, function() {
                                if (mixin[value].apply(this, arguments) !== false) {
                                    return this.callParent(arguments);
                                }
                            });
                        });
                    }
                    if (afters) {
                        Ext.Object.each(afters, function(key, value) {
                            targetClass.hookMember(key, function() {
                                var ret = this.callParent(arguments);
                                mixin[value].apply(this, arguments);
                                return ret;
                            });
                        });
                    }
                    if (hooks) {
                        for (key in hooks) {
                            Mixin.addHook(hooks[key], targetProto, key, mixin);
                        }
                    }
                    if (extended) {
                        targetClass.onExtended(function() {
                            var args = Ext.Array.slice(arguments, 0);
                            args.unshift(targetClass);
                            return extended.apply(this, args);
                        }, this);
                    }
                    if (onClassMixedIn) {
                        onClassMixedIn.apply(this, arguments);
                    }
                };
            }
            if (configs || mixed) {
                data.afterClassMixedIn = function(targetClass) {
                    if (configs) {
                        // eslint-disable-next-line vars-on-top
                        var proto = targetClass.prototype,
                            hoistable = this.$config.configs,
                            cfg, name, hoist;
                        for (name in proto) {
                            cfg = hoistable[name];
                            if (cfg && cfg.isConfig && proto.hasOwnProperty(name)) {
                                (hoist || (hoist = {}))[name] = proto[name];
                                delete proto[name];
                            }
                        }
                        if (hoist) {
                            targetClass.$config.add(hoist);
                        }
                    }
                    if (afterClassMixedIn) {
                        afterClassMixedIn.apply(this, arguments);
                    }
                    if (mixed) {
                        mixed.apply(this, arguments);
                    }
                };
            }
        }
    };
});
// eslint-disable-line block-spacing, brace-style

// @tag core
/**
 * @class Ext.util.DelayedTask
 * 
 * The DelayedTask class provides a convenient way to "buffer" the execution of a method,
 * performing setTimeout where a new timeout cancels the old timeout. When called, the
 * task will wait the specified time period before executing. If durng that time period,
 * the task is called again, the original call will be cancelled. This continues so that
 * the function is only called a single time for each iteration.
 * 
 * This method is especially useful for things like detecting whether a user has finished
 * typing in a text field. An example would be performing validation on a keypress. You can
 * use this class to buffer the keypress events for a certain number of milliseconds, and
 * perform only if they stop for that amount of time.  
 * 
 * ## Usage
 * 
 *     var task = new Ext.util.DelayedTask(function(){
 *         alert(Ext.getDom('myInputField').value.length);
 *     });
 *     
 *     // Wait 500ms before calling our function. If the user presses another key
 *     // during that 500ms, it will be cancelled and we'll wait another 500ms.
 *     Ext.get('myInputField').on('keypress', function() {
 *         task.delay(500);
 *     });
 * 
 * Note that we are using a DelayedTask here to illustrate a point. The configuration
 * option `buffer` for {@link Ext.util.Observable#addListener addListener/on} will
 * also setup a delayed task for you to buffer events.
 * 
 * @constructor The parameters to this constructor serve as defaults and are not required.
 * @param {Function} [fn] The default function to call. If not specified here, it must be
 * specified during the {@link #delay} call.
 * @param {Object} [scope] The default scope (The **`this`** reference) in which the
 * function is called. If not specified, `this` will refer to the browser window.
 * @param {Array} [args] The default Array of arguments.
 * @param {Boolean} [cancelOnDelay=true] By default, each call to {@link #delay} cancels
 * any pending invocation and reschedules a new invocation. Specifying this as `false` means
 * that calls to {@link #delay} when an invocation is pending just update the call settings,
 * `newDelay`, `newFn`, `newScope` or `newArgs`, whichever are passed.
 */
Ext.util = Ext.util || {};
Ext.util.DelayedTask = function(fn, scope, args, cancelOnDelay, fireIdleEvent) {
    // @define Ext.util.DelayedTask
    // @uses Ext.GlobalEvents
    var me = this,
        delay,
        call = function() {
            me.id = null;
            if (!(scope && scope.destroyed)) {
                if (args) {
                    fn.apply(scope, args);
                } else {
                    fn.call(scope);
                }
            }
            if (fireIdleEvent === false) {
                Ext._suppressIdle = true;
            }
        };
    // DelayedTask can be called with no function upfront
    if (fn) {
        call.$origFn = fn.$origFn || fn;
        call.$skipTimerCheck = call.$origFn.$skipTimerCheck;
    }
    cancelOnDelay = typeof cancelOnDelay === 'boolean' ? cancelOnDelay : true;
    /**
     * @property {Number} id
     * The id of the currently pending invocation.  Will be set to `null` if there is no
     * invocation pending.
     */
    me.id = null;
    /**
     * @method delay
     * By default, cancels any pending timeout and queues a new one.
     *
     * If the `cancelOnDelay` parameter was specified as `false` in the constructor, this does not
     * cancel and reschedule, but just updates the call settings, `newDelay`, `newFn`, `newScope` 
     * or `newArgs`, whichever are passed.
     *
     * @param {Number} newDelay The milliseconds to delay. `-1` means schedule for the next
     * animation frame if supported.
     * @param {Function} [newFn] Overrides function passed to constructor
     * @param {Object} [newScope] Overrides scope passed to constructor. Remember that if no scope
     * is specified, `this` will refer to the browser window.
     * @param {Array} [newArgs] Overrides args passed to constructor
     * @return {Number} The timer id being used.
     */
    me.delay = function(newDelay, newFn, newScope, newArgs) {
        if (cancelOnDelay) {
            me.cancel();
        }
        if (typeof newDelay === 'number') {
            delay = newDelay;
        }
        fn = newFn || fn;
        scope = newScope || scope;
        args = newArgs || args;
        me.delayTime = delay;
        if (fn) {
            call.$origFn = fn.$origFn || fn;
            call.$skipTimerCheck = call.$origFn.$skipTimerCheck;
        }
        if (!me.id) {
            if (delay === -1) {
                me.id = Ext.raf(call);
            } else {
                me.id = Ext.defer(call, delay || 1);
            }
        }
        // 0 == immediate call
        return me.id;
    };
    /**
     * Cancel the last queued timeout
     */
    me.cancel = function() {
        if (me.id) {
            if (me.delayTime === -1) {
                Ext.unraf(me.id);
            } else {
                Ext.undefer(me.id);
            }
            me.id = null;
        }
    };
    me.flush = function() {
        var was;
        if (me.id) {
            me.cancel();
            // we're not running on our own timer so don't mess with whatever thread
            // is calling us...
            was = fireIdleEvent;
            fireIdleEvent = true;
            call();
            fireIdleEvent = was;
        }
    };
    /**
     * @private
     * Cancel the timeout if it was set for the specified fn and scope.
     */
    me.stop = function(stopFn, stopScope) {
        // This kludginess is here because Classic components use shared focus task
        // and we need to be sure the task's current timeout was set for that
        // particular component before we can safely cancel it.
        if (stopFn && stopFn === fn && (!stopScope || stopScope === scope)) {
            me.cancel();
        }
    };
};

// @tag core
/**
 * Represents single event type that an Observable object listens to.
 * All actual listeners are tracked inside here.  When the event fires,
 * it calls all the registered listener functions.
 *
 * @private
 */
Ext.define('Ext.util.Event', function() {
    var arraySlice = Array.prototype.slice,
        arrayInsert = Ext.Array.insert,
        toArray = Ext.Array.toArray,
        fireArgs = {};
    return {
        requires: 'Ext.util.DelayedTask',
        /**
         * @property {Boolean} isEvent
         * `true` in this class to identify an object as an instantiated Event, or subclass thereof.
         */
        isEvent: true,
        // Private. Event suspend count
        suspended: 0,
        noOptions: {},
        constructor: function(observable, name) {
            this.name = name;
            this.observable = observable;
            this.listeners = [];
        },
        addListener: function(fn, scope, options, caller, manager) {
            var me = this,
                added = false,
                observable = me.observable,
                eventName = me.name,
                listeners, listener, priority, isNegativePriority, highestNegativePriorityIndex, hasNegativePriorityIndex, length, index, i, listenerPriority, managedListeners;
            if (scope && !Ext._namedScopes[scope] && (typeof fn === 'string') && (typeof scope[fn] !== 'function')) {
                Ext.raise("No method named '" + fn + "' found on scope object");
            }
            if (me.findListener(fn, scope) === -1) {
                listener = me.createListener(fn, scope, options, caller, manager);
                if (me.firing) {
                    // if we are currently firing this event, don't disturb the listener loop
                    me.listeners = me.listeners.slice(0);
                }
                listeners = me.listeners;
                index = length = listeners.length;
                priority = options && options.priority;
                highestNegativePriorityIndex = me._highestNegativePriorityIndex;
                hasNegativePriorityIndex = highestNegativePriorityIndex !== undefined;
                if (priority) {
                    // Find the index at which to insert the listener into the listeners array,
                    // sorted by priority highest to lowest.
                    isNegativePriority = (priority < 0);
                    if (!isNegativePriority || hasNegativePriorityIndex) {
                        // If the priority is a positive number, or if it is a negative number
                        // and there are other existing negative priority listenrs, then we
                        // need to calcuate the listeners priority-order index.
                        // If the priority is a negative number, begin the search for priority
                        // order index at the index of the highest existing negative priority
                        // listener, otherwise begin at 0
                        // eslint-disable-next-line max-len
                        for (i = (isNegativePriority ? highestNegativePriorityIndex : 0); i < length; i++) {
                            // Listeners created without options will have no "o" property
                            listenerPriority = listeners[i].o ? listeners[i].o.priority || 0 : 0;
                            if (listenerPriority < priority) {
                                index = i;
                                break;
                            }
                        }
                    } else {
                        // if the priority is a negative number, and there are no other negative
                        // priority listeners, then no calculation is needed - the negative
                        // priority listener gets appended to the end of the listeners array.
                        me._highestNegativePriorityIndex = index;
                    }
                } else if (hasNegativePriorityIndex) {
                    // listeners with a priority of 0 or undefined are appended to the end of
                    // the listeners array unless there are negative priority listeners in the
                    // listeners array, then they are inserted before the highest negative
                    // priority listener.
                    index = highestNegativePriorityIndex;
                }
                if (!isNegativePriority && index <= highestNegativePriorityIndex) {
                    me._highestNegativePriorityIndex++;
                }
                if (index === length) {
                    listeners[length] = listener;
                } else {
                    arrayInsert(listeners, index, [
                        listener
                    ]);
                }
                if (observable.isElement) {
                    // It is the role of Ext.util.Event (vs Ext.Element) to handle subscribe/
                    // unsubscribe because it is the lowest level place to intercept the
                    // listener before it is added/removed.  For addListener this could easily
                    // be done in Ext.Element's doAddListener override, but since there are
                    // multiple paths for listener removal (un, clearListeners), it is best
                    // to keep all subscribe/unsubscribe logic here.
                    observable._getPublisher(eventName, options.translate === false).subscribe(observable, eventName, options.delegated !== false, options.capture);
                }
                // If the listener was passed with a manager, add it to the manager's list.
                if (manager) {
                    // if scope is an observable, the listener will be automatically managed
                    // this eliminates the need to call mon() in a majority of cases
                    managedListeners = manager.managedListeners || (manager.managedListeners = []);
                    managedListeners.push({
                        item: me.observable,
                        ename: (options && options.managedName) || me.name,
                        fn: fn,
                        scope: scope,
                        options: options
                    });
                }
                added = true;
            }
            return added;
        },
        createListener: function(fn, scope, o, caller, manager) {
            var me = this,
                namedScopes = Ext._namedScopes,
                namedScope = namedScopes[scope],
                listener = {
                    fn: fn,
                    scope: scope,
                    ev: me,
                    caller: caller,
                    manager: manager,
                    namedScope: namedScope,
                    defaultScope: namedScope ? (scope || me.observable) : undefined,
                    lateBound: typeof fn === 'string'
                },
                handler = fn,
                wrapped = false,
                type;
            if (listener.lateBound && fn[2] === '.') {
                if (fn.substr(0, 2) !== 'up') {
                    Ext.raise('Invalid listener method: ' + fn);
                }
                listener.defaultScope = null;
                listener.namedScope = namedScopes[listener.scope = scope = 'up'];
                listener.fn = handler = fn.substr(3);
            }
            // The order is important. The 'single' wrapper must be wrapped by the 'buffer'
            // and 'delayed' wrapper because the event removal that the single listener
            // does destroys the listener's DelayedTask(s)
            if (o) {
                listener.o = o;
                if (o.single) {
                    handler = me.createSingle(handler, listener, o, scope);
                    wrapped = true;
                }
                if (o.target) {
                    handler = me.createTargeted(handler, listener, o, scope, wrapped);
                    wrapped = true;
                }
                if (o.onFrame) {
                    handler = me.createAnimFrame(handler, listener, o, scope, wrapped);
                    wrapped = true;
                }
                if (o.delay) {
                    handler = me.createDelayed(handler, listener, o, scope, wrapped);
                    wrapped = true;
                }
                if (o.buffer) {
                    handler = me.createBuffered(handler, listener, o, scope, wrapped);
                    wrapped = true;
                }
                if (me.observable.isElement) {
                    // If the event type was translated, e.g. mousedown -> touchstart, we need
                    // to save the original type in the listener object so that the Ext.event.Event
                    // object can reflect the correct type at firing time
                    type = o.type;
                    if (type) {
                        listener.type = type;
                    }
                }
            }
            listener.fireFn = handler;
            listener.wrapped = wrapped;
            return listener;
        },
        findListener: function(fn, scope) {
            var listeners = this.listeners,
                i = listeners.length,
                listener;
            while (i--) {
                listener = listeners[i];
                if (listener) {
                    // use ==, not === for scope comparison, so that undefined and null are equal
                    // eslint-disable-next-line eqeqeq
                    if (listener.fn === fn && listener.scope == scope) {
                        return i;
                    }
                }
            }
            return -1;
        },
        removeListener: function(fn, scope, index) {
            var me = this,
                removed = false,
                observable = me.observable,
                eventName = me.name,
                listener, options, manager, managedListeners, managedListener, i;
            index = index != null ? index : me.findListener(fn, scope);
            if (index !== -1) {
                listener = me.listeners[index];
                if (me.firing) {
                    me.listeners = me.listeners.slice(0);
                }
                // Remove this listener from the listeners array. We can use splice directly here.
                // The IE8 bug which Ext.Array works around only affects *insertion*
                // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/
                me.listeners.splice(index, 1);
                // if the listeners array contains negative priority listeners, adjust the
                // internal index if needed.
                if (me._highestNegativePriorityIndex) {
                    if (index < me._highestNegativePriorityIndex) {
                        me._highestNegativePriorityIndex--;
                    } else if (index === me._highestNegativePriorityIndex && index === me.listeners.length) {
                        delete me._highestNegativePriorityIndex;
                    }
                }
                if (listener) {
                    options = listener.o;
                    // cancel and remove a buffered handler that hasn't fired yet.
                    // When the buffered listener is invoked, it must check whether
                    // it still has a task.
                    if (listener.task) {
                        listener.task.cancel();
                        delete listener.task;
                    }
                    // cancel and remove all delayed handlers that haven't fired yet
                    i = listener.tasks && listener.tasks.length;
                    if (i) {
                        while (i--) {
                            listener.tasks[i].cancel();
                        }
                        delete listener.tasks;
                    }
                    // Cancel the timer that could have been set if the event has already fired
                    listener.fireFn.timerId = Ext.undefer(listener.fireFn.timerId);
                    manager = listener.manager;
                    if (manager) {
                        // If this is a managed listener we need to remove it from the manager's
                        // managedListeners array.  This ensures that if we listen using mon
                        // and then remove without using mun, the managedListeners array is updated
                        // accordingly, for example
                        //
                        //     manager.on(target, 'foo', fn);
                        //
                        //     target.un('foo', fn);
                        managedListeners = manager.managedListeners;
                        if (managedListeners) {
                            for (i = managedListeners.length; i--; ) {
                                managedListener = managedListeners[i];
                                if (managedListener.item === me.observable && managedListener.ename === eventName && managedListener.fn === fn && managedListener.scope === scope) {
                                    managedListeners.splice(i, 1);
                                }
                            }
                        }
                    }
                    if (observable.isElement) {
                        // eslint-disable-next-line max-len
                        observable._getPublisher(eventName, options.translate === false).unsubscribe(observable, eventName, options.delegated !== false, options.capture);
                    }
                }
                removed = true;
            }
            return removed;
        },
        // Iterate to stop any buffered/delayed events
        clearListeners: function() {
            var listeners = this.listeners,
                i = listeners.length,
                listener;
            while (i--) {
                listener = listeners[i];
                this.removeListener(listener.fn, listener.scope);
            }
        },
        suspend: function() {
            ++this.suspended;
        },
        resume: function() {
            if (this.suspended) {
                --this.suspended;
            }
        },
        isSuspended: function() {
            return this.suspended > 0;
        },
        fireDelegated: function(firingObservable, args) {
            this.firingObservable = firingObservable;
            return this.fire.apply(this, args);
        },
        fire: function() {
            var me = this,
                CQ = Ext.ComponentQuery,
                listeners = me.listeners,
                count = listeners.length,
                observable = me.observable,
                isElement = observable.isElement,
                isComponent = observable.isComponent,
                firingObservable = me.firingObservable,
                options, delegate, fireInfo, i, args, listener, len, delegateEl, currentTarget, type, chained, firingArgs, e, fireFn, fireScope;
            if (!me.suspended && count > 0) {
                me.firing = true;
                args = arguments.length ? arraySlice.call(arguments, 0) : [];
                len = args.length;
                if (isElement) {
                    e = args[0];
                }
                for (i = 0; i < count; i++) {
                    listener = listeners[i];
                    // Listener may be undefined if one of the previous listeners
                    // destroyed the observable that was listening to these events.
                    // We'd be still in the middle of the loop here, unawares.
                    if (!listener) {
                        
                        continue;
                    }
                    options = listener.o;
                    if (isElement) {
                        if (currentTarget) {
                            // restore the previous currentTarget if we changed it last time
                            // around the loop while processing the delegate option.
                            e.setCurrentTarget(currentTarget);
                        }
                        // For events that have been translated to provide device compatibility,
                        // e.g. mousedown -> touchstart, we want the event object to reflect the
                        // type that was originally listened for, not the type of the actual event
                        // that fired. The listener's "type" property reflects the original type.
                        type = listener.type;
                        if (type) {
                            // chain a new object to the event object before changing the type.
                            // This is more efficient than creating a new event object, and we
                            // don't want to change the type of the original event because it may
                            // be used asynchronously by other handlers
                            // Translated events are not gestures. They must appear to be
                            // atomic events, so that they can be stopped.
                            chained = e;
                            e = args[0] = chained.chain({
                                type: type,
                                isGesture: false
                            });
                        }
                        // In Ext4 Ext.EventObject was a singleton event object that was reused
                        // as events were fired. Set Ext.EventObject to the last fired event
                        // for compatibility.
                        Ext.EventObject = e;
                    }
                    firingArgs = args;
                    if (options) {
                        delegate = options.delegate;
                        if (delegate) {
                            if (isElement) {
                                // prepending the currentTarget.id to the delegate selector
                                // allows us to match selectors such as "> div"
                                // eslint-disable-next-line max-len
                                delegateEl = e.getTarget(typeof delegate === 'function' ? delegate : '#' + e.currentTarget.id + ' ' + delegate);
                                if (delegateEl) {
                                    args[1] = delegateEl;
                                    // save the current target before changing it to the delegateEl
                                    // so that we can restore it next time around
                                    currentTarget = e.currentTarget;
                                    e.setCurrentTarget(delegateEl);
                                } else {
                                    
                                    continue;
                                }
                            }
                            // eslint-disable-next-line max-len
                            else if (isComponent && !CQ.is(firingObservable, delegate, observable)) {
                                
                                continue;
                            }
                        }
                        if (isElement) {
                            if (options.preventDefault) {
                                e.preventDefault();
                            }
                            if (options.stopPropagation) {
                                e.stopPropagation();
                            }
                            if (options.stopEvent) {
                                e.stopEvent();
                            }
                        }
                        args[len] = options;
                        if (options.args) {
                            firingArgs = options.args.concat(args);
                        }
                    }
                    fireInfo = me.getFireInfo(listener);
                    fireFn = fireInfo.fn;
                    fireScope = fireInfo.scope;
                    // We don't want to keep closure and scope on the Event prototype!
                    fireInfo.fn = fireInfo.scope = null;
                    // If the scope is already destroyed, we absolutely cannot deliver events to it.
                    // We also need to clean up the listener to avoid it hanging around forever
                    // like a zombie. Scope can be null/undefined, that's normal.
                    if (fireScope && fireScope.destroyed) {
                        me.removeListener(fireFn, fireScope, i);
                        fireFn = null;
                        // Skip warnings for Ext.container.Monitor
                        // It is to be deprecated and removed shortly.
                        if (fireScope.$className !== 'Ext.container.Monitor') {
                            (Ext.raiseOnDestroyed ? Ext.raise : Ext.log.warn)({
                                msg: 'Attempting to fire "' + me.name + '" event on destroyed ' + (fireScope.$className || 'object') + ' instance with id: ' + (fireScope.id || 'unknown'),
                                instance: fireScope
                            });
                        }
                    }
                    // N.B. This is where actual listener code is called. Step boldly into!
                    if (fireFn && fireFn.apply(fireScope, firingArgs) === false) {
                        Ext.EventObject = null;
                        return (me.firing = false);
                    }
                    // We should remove the last item here to avoid future listeners
                    // in the Array to inherit these options by mistake
                    if (options) {
                        args.length--;
                    }
                    if (chained) {
                        // if we chained the event object for type translation we need to
                        // un-chain it before proceeding to process the next listener, which
                        // may not be a translated event.
                        e = args[0] = chained;
                        chained = null;
                    }
                    // We don't guarantee Ext.EventObject existence outside of the immediate
                    // event propagation scope
                    Ext.EventObject = null;
                }
            }
            me.firing = false;
            return true;
        },
        getFireInfo: function(listener, fromWrapped) {
            var observable = this.observable,
                fireFn = listener.fireFn,
                scope = listener.scope,
                namedScope = listener.namedScope,
                fn, origin;
            // If we are called with a wrapped listener, only attempt to do scope
            // resolution if we are explicitly called by the last wrapped function
            if (!fromWrapped && listener.wrapped) {
                fireArgs.fn = fireFn;
                return fireArgs;
            }
            fn = fromWrapped ? listener.fn : fireFn;
            var name = fn;
            // eslint-disable-line vars-on-top
            if (listener.lateBound) {
                // handler is a function name - need to resolve it to a function reference
                origin = listener.caller || observable;
                if (namedScope && namedScope.isUp) {
                    scope = Ext.lookUpFn(origin, fn);
                } else if (!scope || namedScope) {
                    // Only invoke resolveListenerScope if the user did not specify a scope,
                    // or if the user specified a named scope.  Named function handlers that
                    // use an arbitrary object as the scope just skip this part, and just
                    // use the given scope object to resolve the method.
                    scope = origin.resolveListenerScope(listener.defaultScope);
                }
                if (!scope) {
                    Ext.raise('Unable to dynamically resolve scope for "' + listener.ev.name + '" listener on ' + this.observable.id);
                }
                if (!Ext.isFunction(scope[fn])) {
                    Ext.raise('No method named "' + fn + '" on ' + (scope.$className || 'scope object.'));
                }
                fn = scope[fn];
            } else if (namedScope && namedScope.isController) {
                // If handler is a function reference and scope:'controller' was requested
                // we'll do our best to look up a controller.
                scope = (listener.caller || observable).resolveListenerScope(listener.defaultScope);
                if (!scope) {
                    Ext.raise('Unable to dynamically resolve scope for "' + listener.ev.name + '" listener on ' + this.observable.id);
                }
            } else if (!scope || namedScope) {
                // If handler is a function reference we use the observable instance as
                // the default scope
                scope = observable;
            }
            // We can only ever be firing one event at a time, so just keep
            // overwriting the object we've got in our closure, otherwise we'll be
            // creating a whole bunch of garbage objects
            fireArgs.fn = fn;
            fireArgs.scope = scope;
            if (!fn) {
                Ext.raise('Unable to dynamically resolve method "' + name + '" on ' + this.observable.$className);
            }
            return fireArgs;
        },
        createAnimFrame: function(handler, listener, o, scope, wrapped) {
            var fireInfo;
            if (!wrapped) {
                fireInfo = listener.ev.getFireInfo(listener, true);
                handler = fireInfo.fn;
                scope = fireInfo.scope;
                // We don't want to keep closure and scope references on the Event prototype!
                fireInfo.fn = fireInfo.scope = null;
            }
            return Ext.Function.createAnimationFrame(handler, scope, o.args);
        },
        createTargeted: function(handler, listener, o, scope, wrapped) {
            return function() {
                var fireInfo;
                if (o.target === arguments[0]) {
                    if (!wrapped) {
                        fireInfo = listener.ev.getFireInfo(listener, true);
                        handler = fireInfo.fn;
                        scope = fireInfo.scope;
                        // We don't want to keep closure and scope references
                        // on the Event prototype!
                        fireInfo.fn = fireInfo.scope = null;
                    }
                    return handler.apply(scope, arguments);
                }
            };
        },
        createBuffered: function(handler, listener, o, scope, wrapped) {
            listener.task = new Ext.util.DelayedTask();
            return function() {
                var fireInfo;
                // If the listener is removed during the event call, the listener stays in the
                // list of listeners to be invoked in the fire method, but the task is deleted
                // So if we get here with no task, it's because the listener has been removed.
                if (listener.task) {
                    if (Ext._unitTesting) {
                        o.$delayedTask = listener.task;
                    }
                    // for unit test access
                    if (!wrapped) {
                        fireInfo = listener.ev.getFireInfo(listener, true);
                        handler = fireInfo.fn;
                        scope = fireInfo.scope;
                        // We don't want to keep closure and scope references
                        // on the Event prototype!
                        fireInfo.fn = fireInfo.scope = null;
                    }
                    listener.task.delay(o.buffer, handler, scope, toArray(arguments));
                }
            };
        },
        createDelayed: function(handler, listener, o, scope, wrapped) {
            return function() {
                var task = new Ext.util.DelayedTask(),
                    fireInfo;
                if (!wrapped) {
                    fireInfo = listener.ev.getFireInfo(listener, true);
                    handler = fireInfo.fn;
                    scope = fireInfo.scope;
                    // We don't want to keep closure and scope references on the Event prototype!
                    fireInfo.fn = fireInfo.scope = null;
                }
                if (!listener.tasks) {
                    listener.tasks = [];
                }
                listener.tasks.push(task);
                if (Ext._unitTesting) {
                    o.$delayedTask = task;
                }
                // for unit test access
                task.delay(o.delay || 10, handler, scope, toArray(arguments));
            };
        },
        createSingle: function(handler, listener, o, scope, wrapped) {
            return function() {
                var event = listener.ev,
                    observable = event.observable,
                    fn = listener.fn,
                    fireInfo;
                // If we have an observable, use that to clean up because there
                // can be special cases that need handling. For example element
                // listeners may bind multiple events (mousemove+touchmove) and they
                // need to act in tandem.
                if (observable) {
                    if (!observable.destroyed) {
                        observable.removeListener(event.name, fn, scope);
                    }
                } else {
                    event.removeListener(fn, scope);
                }
                if (!wrapped) {
                    fireInfo = event.getFireInfo(listener, true);
                    handler = fireInfo.fn;
                    scope = fireInfo.scope;
                    // We don't want to keep closure and scope references on the Event prototype!
                    fireInfo.fn = fireInfo.scope = null;
                }
                return handler.apply(scope, arguments);
            };
        }
    };
});

// @tag dom,core
/* eslint-disable max-len */
/**
 * An Identifiable mixin.
 * @private
 */
Ext.define('Ext.mixin.Identifiable', function(Identifiable) {
    return {
        // eslint-disable-line brace-style
        /* eslint-enable max-len */
        isIdentifiable: true,
        mixinId: 'identifiable',
        /**
     * Retrieves the `id`. This method Will auto-generate an id if one has not already
     * been configured.
     * @return {String} id
     */
        getId: function() {
            var me = this,
                id = me.id,
                cfg;
            if (!(id || id === 0)) {
                cfg = me.initialConfig;
                // The id config can be requested so early (e.g., by stateful) that the
                // property has not been put on the instance yet.
                if (cfg && cfg.id) {
                    id = cfg.id;
                } else {
                    id = me.generateAutoId();
                    me.autoGenId = true;
                }
                me.setId(id);
            }
            me.getId = Identifiable._getId;
            return id;
        },
        setId: function(id) {
            // The double assignment here and in setId is intentional to workaround a JIT
            // issue that prevents me.id from being assigned in random scenarios. The issue
            // occurs on 4th gen iPads and lower, possibly other older iOS devices.
            // See EXTJS-16494.
            this.id = this.id = id;
        },
        privates: {
            statics: {
                _idCleanRe: /\.|[^\w-]/g,
                uniqueIds: {},
                _getId: function() {
                    return this.id;
                }
            },
            defaultIdPrefix: 'ext-',
            defaultIdSeparator: '-',
            id: null,
            /**
         * @property {Boolean} autoGenId
         * `true` indicates an `id` was auto-generated rather than provided by configuration.
         * @private
         * @since 6.7.0
         */
            autoGenId: false,
            generateAutoId: function() {
                var me = this,
                    prototype = me.self.prototype,
                    sep = me.defaultIdSeparator,
                    uniqueIds = Identifiable.uniqueIds,
                    cleanRe, defaultIdPrefix, prefix, xtype;
                if (!prototype.hasOwnProperty('identifiablePrefix')) {
                    cleanRe = Identifiable._idCleanRe;
                    defaultIdPrefix = me.defaultIdPrefix;
                    xtype = me.xtype;
                    if (xtype) {
                        prefix = defaultIdPrefix + xtype.replace(cleanRe, sep) + sep;
                    } else if (!(prefix = prototype.$className)) {
                        prefix = defaultIdPrefix + 'anonymous' + sep;
                    } else {
                        prefix = prefix.replace(cleanRe, sep).toLowerCase() + sep;
                    }
                    prototype.identifiablePrefix = prefix;
                }
                prefix = me.identifiablePrefix;
                if (!uniqueIds.hasOwnProperty(prefix)) {
                    uniqueIds[prefix] = 0;
                }
                return prefix + (++uniqueIds[prefix]);
            }
        }
    };
});
// privates

// @tag core
/**
 * Base class that provides a common interface for publishing events. Subclasses are
 * expected to have a property "events" which is populated as event listeners register,
 * and, optionally, a property "listeners" with configured listeners defined.
 *
 * *Note*: This mixin requires the constructor to be called, which is typically done
 * during the construction of your object. The Observable constructor will call 
 * {@link #initConfig}, so it does not need to be called a second time.
 *
 * For example:
 *
 *     Ext.define('Employee', {
 *         mixins: ['Ext.mixin.Observable'],
 *
 *         config: {
 *             name: ''
 *         },
 *
 *         constructor: function (config) {
 *             // The `listeners` property is processed to add listeners and the config
 *             // is applied to the object.
 *             this.mixins.observable.constructor.call(this, config);
 *             // Config has been initialized
 *             console.log(this.getEmployeeName());
 *         }
 *     });
 *
 * This could then be used like this:
 *
 *     var newEmployee = new Employee({
 *         name: employeeName,
 *         listeners: {
 *             quit: function() {
 *                 // By default, "this" will be the object that fired the event.
 *                 alert(this.getName() + " has quit!");
 *             }
 *         }
 *     });
 */
Ext.define('Ext.mixin.Observable', function(Observable) {
    var emptyFn = Ext.emptyFn,
        emptyArray = [],
        arrayProto = Array.prototype,
        arraySlice = arrayProto.slice,
        // Destroyable class which removes listeners
        ListenerRemover = function(observable) {
            // Passed a ListenerRemover: return it
            if (observable instanceof ListenerRemover) {
                return observable;
            }
            this.observable = observable;
            // Called when addManagedListener is used with the event source as the second arg:
            // (owner, eventSource, args...)
            if (arguments[1].isObservable) {
                this.managedListeners = true;
            }
            this.args = arraySlice.call(arguments, 1);
        },
        // These properties should not be nulled during Base destroy(),
        // we will take care of them in destroyObservable()
        protectedProps = [
            'events',
            'hasListeners',
            'managedListeners',
            'eventedBeforeEventNames'
        ];
    ListenerRemover.prototype.destroy = function() {
        var me = this,
            args = me.args,
            observable = me.observable,
            // Extract the element reference from the options object.
            // Single arg form first, options is args[3] in multi arg form.
            elementName = args[0].element || (args[3] && args[3].element);
        // If it was an element listener, then the listener is added to that element reference.
        if (elementName) {
            if (Ext.Array.indexOf(observable.referenceList, elementName) === -1) {
                Ext.Logger.error("Destroying event listener with an invalid element reference " + "of '" + elementName + "' for this component. Available values are: '" + observable.referenceList.join("', '") + "'", observable);
            }
            observable = observable[elementName];
        }
        // If that observable is already destroyed, all its listeners were cleared
        if (!observable.destroyed) {
            observable[me.managedListeners ? 'mun' : 'un'].apply(observable, me.args);
        }
        me.destroy = Ext.emptyFn;
    };
    return {
        extend: 'Ext.Mixin',
        mixinConfig: {
            id: 'observable',
            after: {
                destroy: 'destroyObservable'
            }
        },
        requires: [
            'Ext.util.Event'
        ],
        mixins: [
            'Ext.mixin.Identifiable'
        ],
        statics: {
            /**
            * Removes **all** added captures from the Observable.
            *
            * @param {Ext.util.Observable} o The Observable to release
            * @static
            */
            releaseCapture: function(o) {
                o.fireEventArgs = this.prototype.fireEventArgs;
            },
            /**
             * Starts capture on the specified Observable. All events will be passed to the supplied
             * function with the event name + standard signature of the event **before** the event
             * is fired. If the supplied function returns false, the event will not fire.
             *
             * @param {Ext.util.Observable} o The Observable to capture events from.
             * @param {Function} fn The function to call when an event is fired.
             * @param {Object} scope (optional) The scope (`this` reference) in which the function
             * is executed. Defaults to the Observable firing the event.
             * @static
             */
            capture: function(o, fn, scope) {
                // We're capturing calls to fireEventArgs to avoid duplication of events;
                // however fn expects fireEvent's signature so we have to convert it here.
                // To avoid unnecessary conversions, observe() below is aware of the changes
                // and will capture fireEventArgs instead.
                var newFn = function(eventName, args) {
                        return fn.apply(scope, [
                            eventName
                        ].concat(args));
                    };
                this.captureArgs(o, newFn, scope);
            },
            /**
             * @method
             * @private
             */
            captureArgs: function(o, fn, scope) {
                o.fireEventArgs = Ext.Function.createInterceptor(o.fireEventArgs, fn, scope);
            },
            /**
            * Sets observability on the passed class constructor.
            *
            * This makes any event fired on any instance of the passed class also fire a single
            * event through the **class** allowing for central handling of events on many instances
            * at once.
            *
            * Usage:
            *
            *     Ext.util.Observable.observe(Ext.data.Connection);
            *     Ext.data.Connection.on('beforerequest', function(con, options) {
            *         console.log('Ajax request made to ' + options.url);
            *     });
            *
            * @param {Function} cls The class constructor to make observable.
            * @param {Object} listeners An object containing a series of listeners to 
            * add. See {@link Ext.util.Observable#addListener addListener}.
            * @static
            */
            observe: function(cls, listeners) {
                if (cls) {
                    if (!cls.isObservable) {
                        Ext.applyIf(cls, new this());
                        this.captureArgs(cls.prototype, cls.fireEventArgs, cls);
                    }
                    if (Ext.isObject(listeners)) {
                        cls.on(listeners);
                    }
                }
                return cls;
            },
            /**
            * @method prepareClass
            * Prepares a given class for observable instances. This method is called when a
            * class derives from this class or uses this class as a mixin.
            * @param {Function} T The class constructor to prepare.
            * @param {Ext.util.Observable} mixin The mixin if being used as a mixin.
            * @param {Object} data The raw class creation data if this is an extend.
            * @private
            */
            prepareClass: function(T, mixin, data) {
                // T.hasListeners is the object to track listeners on class T. This object's
                // prototype (__proto__) is the "hasListeners" of T.superclass.
                // Instances of T will create "hasListeners" that have T.hasListeners as their
                // immediate prototype (__proto__).
                var listeners = T.listeners = [],
                    // If this function was called as a result of an "onExtended", it will
                    // receive the class as "T", but the members will not yet have been
                    // applied to the prototype. If this is the case, just grab listeners
                    // off of the raw data object.
                    target = data || T.prototype,
                    targetListeners = target.listeners,
                    superListeners = mixin ? mixin.listeners : T.superclass.self.listeners,
                    scope, namedScope, i, len;
                // Process listeners that have been declared on the class body. These
                // listeners must not override each other, but each must be added
                // separately. This is accomplished by maintaining a nested array
                // of listeners for the class and it's superclasses/mixins
                if (superListeners) {
                    listeners.push(superListeners);
                }
                if (targetListeners) {
                    // Allow listener scope resolution mechanism to know if the listeners
                    // were declared on the class.  This is only necessary when scope
                    // is unspecified, or when scope is 'controller'.  We use special private
                    // named scopes of "self" and "self.controller" to indicate either
                    // unspecified scope, or scope declared as controller on the class
                    // body.  To avoid iterating the listeners object multiple times, we
                    // only put this special scope on the outermost object at this point
                    // and allow addListener to handle scope:'controller' declared on
                    // inner objects of the listeners config.
                    scope = targetListeners.scope;
                    if (!scope) {
                        targetListeners.scope = 'self';
                    } else {
                        namedScope = Ext._namedScopes[scope];
                        if (namedScope && namedScope.isController) {
                            targetListeners.scope = 'self.controller';
                        }
                    }
                    listeners.push(targetListeners);
                    // After adding the target listeners to the declared listeners array
                    // we can delete it off of the prototype (or data object).  This ensures
                    // that we don't attempt to add the listeners twice, once during
                    // addDeclaredListeners, and again when we add this.listeners in the
                    // constructor.
                    target.listeners = null;
                }
                if (!T.HasListeners) {
                    // We create a HasListeners "class" for this class. The "prototype" of the
                    // HasListeners class is an instance of the HasListeners class associated
                    // with this class's super class (or with Observable).
                    // eslint-disable-next-line vars-on-top
                    var HasListeners = function() {},
                        SuperHL = T.superclass.HasListeners || (mixin && mixin.HasListeners) || Observable.HasListeners;
                    // Make the HasListener class available on the class and its prototype:
                    T.prototype.HasListeners = T.HasListeners = HasListeners;
                    // And connect its "prototype" to the new HasListeners of our super class
                    // (which is also the class-level "hasListeners" instance).
                    HasListeners.prototype = T.hasListeners = new SuperHL();
                }
                // Reusing a variable here
                scope = T.prototype.$noClearOnDestroy || {};
                for (i = 0 , len = protectedProps.length; i < len; i++) {
                    scope[protectedProps[i]] = true;
                }
                T.prototype.$noClearOnDestroy = scope;
            }
        },
        /* End Definitions */
        /**
         * @cfg {Object} listeners
         *
         * A config object containing one or more event handlers to be added to this object during
         * initialization. This should be a valid listeners config object as specified in the 
         * {@link Ext.util.Observable#addListener addListener} example for attaching 
         * multiple handlers at once.
         *
         * **DOM events from Ext JS {@link Ext.Component Components}**
         *
         * While _some_ Ext JS Component classes export selected DOM events (e.g. "click",
         * "mouseover" etc), this is usually only done when extra value can be added. For example
         * the {@link Ext.view.View DataView}'s **`{@link Ext.view.View#itemclick itemclick}`**
         * event passing the node clicked on. To access DOM events directly from a child element
         * of a Component, we need to specify the `element` option to identify the Component
         * property to add a DOM listener to:
         *
         *     new Ext.panel.Panel({
         *         width: 400,
         *         height: 200,
         *         dockedItems: [{
         *             xtype: 'toolbar'
         *         }],
         *         listeners: {
         *             click: {
         *                 element: 'el', //bind to the underlying el property on the panel
         *                 fn: function(){ console.log('click el'); }
         *             },
         *             dblclick: {
         *                 element: 'body', //bind to the underlying body property on the panel
         *                 fn: function(){ console.log('dblclick body'); }
         *             }
         *         }
         *     });
         */
        /**
         * @property {Boolean} isObservable
         * `true` in this class to identify an object as an instantiated Observable, or subclass
         * thereof.
         */
        isObservable: true,
        /**
         * @private
         * We don't want the base destructor to clear the prototype because
         * our destroyObservable handler must be called the very last. It will take care
         * of the prototype after completing Observable destruction sequence.
         */
        $vetoClearingPrototypeOnDestroy: true,
        /**
         * @private
         * Initial suspended call count. Incremented when {@link #suspendEvents} is called,
         * decremented when {@link #resumeEvents} is called.
         */
        eventsSuspended: 0,
        /**
         * @property {Object} hasListeners
         * @readonly
         * This object holds a key for any event that has a listener. The listener may be set
         * directly on the instance, or on its class or a super class (via {@link #observe}) or
         * on the {@link Ext.app.EventBus MVC EventBus}. The values of this object are truthy
         * (a non-zero number) and falsy (0 or undefined). They do not represent an exact count
         * of listeners. The value for an event is truthy if the event must be fired and is
         * falsy if there is no need to fire the event.
         * 
         * The intended use of this property is to avoid the expense of fireEvent calls when
         * there are no listeners. This can be particularly helpful when one would otherwise
         * have to call fireEvent hundreds or thousands of times. It is used like this:
         * 
         *      if (this.hasListeners.foo) {
         *          this.fireEvent('foo', this, arg1);
         *      }
         */
        constructor: function(config) {
            var me = this,
                self = me.self,
                declaredListeners, listeners, bubbleEvents, len, i;
            // Observable can be extended and/or mixed in at multiple levels in a Class
            // hierarchy, and may have its constructor invoked multiple times for a given
            // instance.  The following ensures we only perform initialization the first
            // time the constructor is called.
            if (me.$observableInitialized) {
                return;
            }
            me.$observableInitialized = true;
            // This double assignment is intentional - it works around a strange JIT
            // bug that prevents this.hasListeners from being assigned in some cases on
            // some versions of iOS and iOS simulator.
            // (This bug manifests itself in the unit tests for Ext.data.NodeInterface
            // where we repeatedly create tree nodes in each spec.  Sometimes node.hasListeners
            // is undefined immediately after node construction).
            // A similar issue occurs with the data property of Ext.data.Model (see
            // constructor)
            me.hasListeners = me.hasListeners = new me.HasListeners();
            me.eventedBeforeEventNames = {};
            me.events = me.events || {};
            declaredListeners = self.listeners;
            if (declaredListeners && !me._addDeclaredListeners(declaredListeners)) {
                // Nulling out declared listeners allows future instances to avoid
                // recursing into the declared listeners arrays if the first instance
                // discovers that there are no declarative listeners in its hierarchy
                self.listeners = null;
            }
            listeners = (config && config.listeners) || me.listeners;
            if (listeners) {
                if (listeners instanceof Array) {
                    // Support for listeners declared as an array:
                    //
                    //     listeners: [
                    //         { foo: fooHandler },
                    //         { bar: barHandler }
                    //     ]
                    for (i = 0 , len = listeners.length; i < len; ++i) {
                        me.addListener(listeners[i]);
                    }
                } else {
                    me.addListener(listeners);
                }
            }
            bubbleEvents = (config && config.bubbleEvents) || me.bubbleEvents;
            if (bubbleEvents) {
                me.enableBubble(bubbleEvents);
            }
            if (me.$applyConfigs) {
                // Ext.util.Observable applies config properties directly to the instance
                if (config) {
                    Ext.apply(me, config);
                }
            } else {
                // Ext.mixin.Observable uses the config system
                me.initConfig(config);
            }
            if (listeners) {
                // Set as an instance property to preempt the prototype in case any are set there.
                // Prevents listeners from being added multiple times if this constructor
                // is called more than once by multiple parties in the inheritance hierarchy
                me.listeners = null;
            }
        },
        onClassExtended: function(T, data) {
            if (!T.HasListeners) {
                // Some classes derive from us and some others derive from those classes. All
                // of these are passed to this method.
                Observable.prepareClass(T, T.prototype.$observableMixedIn ? undefined : data);
            }
        },
        /**
         * @private
         * Matches options property names within a listeners specification object - property names
         * which are never used as event names.
         */
        $eventOptions: {
            scope: 1,
            delay: 1,
            buffer: 1,
            onFrame: 1,
            single: 1,
            args: 1,
            destroyable: 1,
            priority: 1,
            order: 1
        },
        $orderToPriority: {
            before: 100,
            current: 0,
            after: -100
        },
        /**
         * Adds declarative listeners as nested arrays of listener objects.
         * @private
         * @param {Array} listeners
         * @return {Boolean} `true` if any listeners were added
         */
        _addDeclaredListeners: function(listeners) {
            var me = this;
            if (listeners instanceof Array) {
                Ext.each(listeners, me._addDeclaredListeners, me);
            } else {
                me._addedDeclaredListeners = true;
                me.addListener(listeners);
            }
            return me._addedDeclaredListeners;
        },
        /**
         * The addManagedListener method is used when some object (call it "A") is listening 
         * to an event on another observable object ("B") and you want to remove that listener 
         * from "B" when "A" is destroyed. This is not an issue when "B" is destroyed because
         * all of its listeners will be removed at that time.
         *
         * Example:
         *
         *     Ext.define('Foo', {
         *         extend: 'Ext.Component',
         *     
         *         initComponent: function () {
         *             this.addManagedListener(MyApp.SomeSharedMenu, 'show', this.doSomething);
         *             this.callParent();
         *         }
         *     });
         *
         * As you can see, when an instance of Foo is destroyed, it ensures that the 'show' 
         * listener on the menu (`MyApp.SomeGlobalSharedMenu`) is also removed.
         *
         * As of version 5.1 it is no longer necessary to use this method in most cases because
         * listeners are automatically managed if the scope object provided to 
         * {@link Ext.util.Observable#addListener addListener} is an Observable instance. 
         * However, if the observable instance and scope are not the same object you 
         * still need to use `mon` or `addManagedListener` if you want the listener to be 
         * managed.
         *
         * @param {Ext.util.Observable/Ext.dom.Element} item The item to which to add
         * a listener/listeners.
         * @param {Object/String} ename The event name, or an object containing event name
         * properties.
         * @param {Function/String} fn (optional) If the `ename` parameter was an event 
         * name, this is the handler function or the name of a method on the specified 
         * `scope`.
         * @param {Object} scope (optional) If the `ename` parameter was an event name, this is
         * the scope (`this` reference) in which the handler function is executed.
         * @param {Object} options (optional) If the `ename` parameter was an event name, this is
         * the {@link Ext.util.Observable#addListener addListener} options.
         * @param {Boolean} noDestroy (private)
         * @return {Object} **Only when the `destroyable` option is specified.**
         *
         * A `Destroyable` object. An object which implements the `destroy` method which removes
         * all listeners added in this call. For example:
         *
         *     this.btnListeners = myButton.mon({
         *         destroyable: true
         *         mouseover:   function() { console.log('mouseover'); },
         *         mouseout:    function() { console.log('mouseout'); },
         *         click:       function() { console.log('click'); }
         *     });
         *
         * And when those listeners need to be removed:
         *
         *     Ext.destroy(this.btnListeners);
         *
         * or
         *
         *     this.btnListeners.destroy();
         */
        addManagedListener: function(item, ename, fn, scope, options, noDestroy) {
            var me = this,
                config, passedOptions;
            me.managedListeners = me.managedListeners || [];
            if (typeof ename !== 'string') {
                // When creating listeners using the object form, allow caller to override
                // the default of using the listeners object as options.
                // This is used by relayEvents, when adding its relayer so that it does not
                // contribute a spurious options param to the end of the arg list.
                passedOptions = arguments.length > 4 ? options : ename;
                options = ename;
                for (ename in options) {
                    if (options.hasOwnProperty(ename)) {
                        config = options[ename];
                        if (!item.$eventOptions[ename]) {
                            // recurse, but pass the noDestroy parameter as true so that lots
                            // of individual Destroyables are not created.
                            // We create a single one at the end if necessary.
                            me.addManagedListener(item, ename, config.fn || config, config.scope || options.scope || scope, config.fn ? config : passedOptions, true);
                        }
                    }
                }
                if (options && options.destroyable) {
                    return new ListenerRemover(me, item, options);
                }
            } else {
                if (fn !== emptyFn) {
                    item.doAddListener(ename, fn, scope, options, null, me, me);
                    // The 'noDestroy' flag is sent if we're looping through a hash
                    // of listeners passing each one to addManagedListener separately
                    if (!noDestroy && options && options.destroyable) {
                        return new ListenerRemover(me, item, ename, fn, scope);
                    }
                }
            }
        },
        /**
         * Removes listeners that were added by the {@link #mon} method.
         *
         * @param {Ext.util.Observable/Ext.dom.Element} item The item from which to remove
         * a listener/listeners.
         * @param {Object/String} ename The event name, or an object containing event name
         * properties.
         * @param {Function} fn (optional) If the `ename` parameter was an event name, this is
         * the handler function.
         * @param {Object} scope (optional) If the `ename` parameter was an event name, this is
         * the scope (`this` reference) in which the handler function is executed.
         */
        removeManagedListener: function(item, ename, fn, scope) {
            var me = this,
                options, config, managedListeners, length, i;
            if (item.$observableDestroyed) {
                return;
            }
            if (typeof ename !== 'string') {
                options = ename;
                for (ename in options) {
                    if (options.hasOwnProperty(ename)) {
                        config = options[ename];
                        if (!item.$eventOptions[ename]) {
                            me.removeManagedListener(item, ename, config.fn || config, config.scope || options.scope || scope);
                        }
                    }
                }
            } else {
                managedListeners = me.managedListeners ? me.managedListeners.slice() : [];
                ename = Ext.canonicalEventName(ename);
                for (i = 0 , length = managedListeners.length; i < length; i++) {
                    me.removeManagedListenerItem(false, managedListeners[i], item, ename, fn, scope);
                }
            }
        },
        /**
         * Fires the specified event with the passed parameters (minus the event name, plus
         * the `options` object passed to {@link Ext.util.Observable#addListener addListener}).
         *
         * An event may be set to bubble up an Observable parent hierarchy (See
         * {@link Ext.Component#getBubbleTarget}) by calling {@link #enableBubble}.
         *
         * @param {String} eventName The name of the event to fire.
         * @param {Object...} args Variable number of parameters are passed to handlers.
         * @return {Boolean} returns false if any of the handlers return false otherwise it
         * returns true.
         */
        fireEvent: function(eventName) {
            return this.fireEventArgs(eventName, arraySlice.call(arguments, 1));
        },
        /**
         * Gets the default scope for firing late bound events (string names with
         * no scope attached) at runtime.
         * @param {Object} [defaultScope=this] The default scope to return if none is found.
         * @return {Object} The default event scope
         * @protected
         */
        resolveListenerScope: function(defaultScope) {
            var namedScope = Ext._namedScopes[defaultScope];
            if (namedScope) {
                if (namedScope.isController) {
                    Ext.raise('scope: "controller" can only be specified on classes ' + 'that derive from Ext.Component or Ext.Widget');
                }
                if (namedScope.isSelf || namedScope.isThis) {
                    defaultScope = null;
                }
            }
            return defaultScope || this;
        },
        /**
         * Fires the specified event with the passed parameter list.
         *
         * An event may be set to bubble up an Observable parent hierarchy
         * (See {@link Ext.Component#getBubbleTarget}) by calling {@link #enableBubble}.
         *
         * @param {String} eventName The name of the event to fire.
         * @param {Object[]} args An array of parameters which are passed to handlers.
         * @return {Boolean} returns false if any of the handlers return false otherwise
         * it returns true.
         */
        fireEventArgs: function(eventName, args) {
            var me = this,
                // no need to make events since we need an Event with listeners
                events = me.events,
                ret = true,
                event;
            eventName = Ext.canonicalEventName(eventName);
            event = events && events[eventName];
            // Only continue firing the event if there are listeners to be informed.
            // Bubbled events will always have a listener count, so will be fired.
            if (me.hasListeners[eventName]) {
                ret = me.doFireEvent(eventName, args || emptyArray, event ? event.bubble : false);
            }
            return ret;
        },
        /**
         * Fires the specified event with the passed parameters and executes a function (action).
         * By default, the action function will be executed after any "before" event handlers
         * (as specified using the `order` option of 
         * `{@link Ext.util.Observable#addListener addListener}`), but before any other 
         * handlers are fired.  This gives the "before" handlers an opportunity to 
         * cancel the event by returning `false`, and prevent the action function from 
         * being called.
         *
         * The action can also be configured to run after normal handlers, but before any "after"
         * handlers (as specified using the `order` event option) by passing `'after'`
         * as the `order` parameter.  This configuration gives any event handlers except
         * for "after" handlers the opportunity to cancel the event and prevent the action
         * function from being called.
         *
         * @param {String} eventName The name of the event to fire.
         * @param {Array} args Arguments to pass to handlers and to the action function.
         * @param {Function} fn The action function.
         * @param {Object} [scope] The scope (`this` reference) in which the handler function is
         * executed. **If omitted, defaults to the object which fired the event.**
         * @param {Object} [options] Event options for the action function.  Accepts any
         * of the options of `{@link Ext.util.Observable#addListener addListener}`
         * @param {String} [order='before'] The order to call the action function relative
         * too the event handlers (`'before'` or `'after'`).  Note that this option is
         * simply used to sort the action function relative to the event handlers by "priority".
         * An order of `'before'` is equivalent to a priority of `99.5`, while an order of
         * `'after'` is equivalent to a priority of `-99.5`.  See the `priority` option
         * of `{@link Ext.util.Observable#addListener addListener}` for more details.
         * @deprecated 5.5 Use {@link #fireEventedAction} instead.
         */
        fireAction: function(eventName, args, fn, scope, options, order) {
            // The historical behaviour has been to default the scope to `this`.
            if (typeof fn === 'string' && !scope) {
                fn = this[fn];
            }
            // chain options to avoid mutating the user's options object
            options = options ? Ext.Object.chain(options) : {};
            options.single = true;
            options.priority = ((order === 'after') ? -99.5 : 99.5);
            this.doAddListener(eventName, fn, scope, options);
            this.fireEventArgs(eventName, args);
        },
        $eventedController: {
            _paused: 1,
            pause: function() {
                ++this._paused;
            },
            resume: function() {
                var me = this,
                    fn = me.fn,
                    scope = me.scope,
                    fnArgs = me.fnArgs,
                    args, ret;
                if (!--me._paused) {
                    if (fn) {
                        args = Ext.Array.slice(fnArgs || me.args);
                        if (fnArgs === false) {
                            // Passing false will remove the first item (typically the owner)
                            args.shift();
                        }
                        me.fn = null;
                        // only call fn once
                        args.push(me);
                        if (Ext.isFunction(fn)) {
                            ret = fn.apply(scope, args);
                        } else if (scope && Ext.isString(fn) && Ext.isFunction(scope[fn])) {
                            ret = scope[fn].apply(scope, args);
                        }
                        if (ret === false) {
                            return false;
                        }
                    }
                    if (!me._paused) {
                        // fn could have paused us
                        return me.owner.fireEventArgs(me.eventName, me.args);
                    }
                }
            }
        },
        /**
         * Fires the specified event with the passed parameters and executes a function (action).
         * Evented Actions will automatically dispatch a 'before' event passing. This event will
         * be given a special controller that allows for pausing/resuming of the event flow.
         *
         * By pausing the controller the updater and events will not run until resumed. Pausing,
         * however, will not stop the processing of any other before events.
         *
         * @param {String} eventName The name of the event to fire.
         * @param {Array} args Arguments to pass to handlers and to the action function.
         * @param {Function/String} fn The action function.
         * @param {Object} [scope] The scope (`this` reference) in which the handler function is
         * executed. **If omitted, defaults to the object which fired the event.**
         * @param {Array/Boolean} [fnArgs] Optional arguments for the action `fn`. If not
         * given, the normal `args` will be used to call `fn`. If `false` is passed, the
         * `args` are used but if the first argument is this instance it will be removed
         * from the args passed to the action function.
         */
        fireEventedAction: function(eventName, args, fn, scope, fnArgs) {
            var me = this,
                eventedBeforeEventNames = me.eventedBeforeEventNames,
                beforeEventName = eventedBeforeEventNames[eventName] || (eventedBeforeEventNames[eventName] = 'before' + eventName),
                controller = Ext.apply({
                    owner: me,
                    eventName: eventName,
                    fn: fn,
                    scope: scope,
                    fnArgs: fnArgs,
                    args: args
                }, me.$eventedController),
                value;
            args.push(controller);
            value = me.fireEventArgs(beforeEventName, args);
            args.pop();
            if (value === false) {
                return false;
            }
            return controller.resume();
        },
        /**
         * Continue to fire event.
         * @private
         *
         * @param {String} eventName
         * @param {Array} args
         * @param {Boolean} bubbles
         */
        doFireEvent: function(eventName, args, bubbles) {
            var target = this,
                queue, event,
                ret = true;
            do {
                if (target.eventsSuspended) {
                    if ((queue = target.eventQueue)) {
                        queue.push([
                            eventName,
                            args
                        ]);
                    }
                    return ret;
                } else {
                    event = target.events && target.events[eventName];
                    if (event && event !== true) {
                        if ((ret = event.fire.apply(event, args)) === false) {
                            break;
                        }
                    }
                }
            } while (// Continue bubbling if event exists and it is `true` or the handler
            // didn't returns false and it configure to bubble.
            bubbles && (target = target.getBubbleParent()));
            return ret;
        },
        /**
         * Gets the bubbling parent for an Observable
         * @private
         * @return {Ext.util.Observable} The bubble parent. null is returned if
         * no bubble target exists
         */
        getBubbleParent: function() {
            var me = this,
                parent = me.getBubbleTarget && me.getBubbleTarget();
            if (parent && parent.isObservable) {
                return parent;
            }
            return null;
        },
        /**
         * The {@link #on} method is shorthand for 
         * {@link Ext.util.Observable#addListener addListener}.
         *
         * Appends an event handler to this object.  For example:
         *
         *     myGridPanel.on("itemclick", this.onItemClick, this);
         *
         * The method also allows for a single argument to be passed which is a config object
         * containing properties which specify multiple events. For example:
         *
         *     myGridPanel.on({
         *         cellclick: this.onCellClick,
         *         select: this.onSelect,
         *         viewready: this.onViewReady,
         *         scope: this // Important. Ensure "this" is correct during handler execution
         *     });
         *
         * One can also specify options for each event handler separately:
         *
         *     myGridPanel.on({
         *         cellclick: {fn: this.onCellClick, scope: this, single: true},
         *         viewready: {fn: panel.onViewReady, scope: panel}
         *     });
         *
         * *Names* of methods in a specified scope may also be used:
         *
         *     myGridPanel.on({
         *         cellclick: {fn: 'onCellClick', scope: this, single: true},
         *         viewready: {fn: 'onViewReady', scope: panel}
         *     });
         *
         * @param {String/Object} eventName The name of the event to listen for.
         * May also be an object who's property names are event names.
         *
         * @param {Function/String} [fn] The method the event invokes or the *name* of 
         * the method within the specified `scope`.  Will be called with arguments
         * given to {@link Ext.util.Observable#fireEvent} plus the `options` parameter described 
         * below.
         *
         * @param {Object} [scope] The scope (`this` reference) in which the handler function is
         * executed. **If omitted, defaults to the object which fired the event.**
         *
         * @param {Object} [options] An object containing handler configuration.
         *
         * **Note:** The options object will also be passed as the last argument to every 
         * event handler.
         *
         * This object may contain any of the following properties:
         *
         * @param {Object} options.scope
         *   The scope (`this` reference) in which the handler function is executed. **If omitted,
         *   defaults to the object which fired the event.**
         *
         * @param {Number} options.delay
         *   The number of milliseconds to delay the invocation of the handler after the event 
         *   fires.
         *
         * @param {Boolean} options.single
         *   True to add a handler to handle just the next firing of the event, and then remove 
         *   itself.
         *
         * @param {Number} options.buffer
         *   Causes the handler to be scheduled to run in an {@link Ext.util.DelayedTask} delayed
         *   by the specified number of milliseconds. If the event fires again within that time,
         *   the original handler is _not_ invoked, but the new handler is scheduled in its place.
         *
         * @param {Number} options.onFrame
         *   Causes the handler to be scheduled to run at the next 
         *   {@link Ext.Function#requestAnimationFrame animation frame event}. If the
         *   event fires again before that time, the handler is not rescheduled - the handler
         *   will only be called once when the next animation frame is fired, with the last set
         *   of arguments passed.
         *
         * @param {Ext.util.Observable} options.target
         *   Only call the handler if the event was fired on the target Observable, _not_ if the 
         *   event was bubbled up from a child Observable.
         *
         * @param {String} options.element
         *   **This option is only valid for listeners bound to {@link Ext.Component Components}.**
         *   The name of a Component property which references an {@link Ext.dom.Element element} 
         *   to add a listener to.
         *
         *   This option is useful during Component construction to add DOM event listeners to 
         *   elements of {@link Ext.Component Components} which will exist only after the 
         *   Component is rendered.
         *   
         *   For example, to add a click listener to a Panel's body:
         *
         *       var panel = new Ext.panel.Panel({
         *           title: 'The title',
         *           listeners: {
         *               click: this.handlePanelClick,
         *               element: 'body'
         *           }
         *       });
         *       
         * In order to remove listeners attached using the element, you'll need to reference
         * the element itself as seen below.  
         *
         *      panel.body.un(...)
         * 
         * @param {String} [options.delegate]
         *   A simple selector to filter the event target or look for a descendant of the target.  
         *   
         *   The "delegate" option is only available on Ext.dom.Element instances (or 
         *   when attaching a listener to a Ext.dom.Element via a Component using the 
         *   element option). 
         *   
         *   See the *delegate* example below.
         *
         * @param {Boolean} [options.capture]
         *  When set to `true`, the listener is fired in the capture phase of the event propagation
         *  sequence, instead of the default bubble phase.
         *   
         *   The `capture` option is only available on Ext.dom.Element instances (or 
         *   when attaching a listener to a Ext.dom.Element via a Component using the 
         *   element option). 
         *
         * @param {Boolean} [options.stopPropagation]
         *   **This option is only valid for listeners bound to {@link Ext.dom.Element Elements}.**
         *   `true` to call {@link Ext.event.Event#stopPropagation stopPropagation} on the event
         *   object before firing the handler.
         *
         * @param {Boolean} [options.preventDefault]
         *   **This option is only valid for listeners bound to {@link Ext.dom.Element Elements}.**
         *   `true` to call {@link Ext.event.Event#preventDefault preventDefault} on the event
         *   object before firing the handler.
         *
         * @param {Boolean} [options.stopEvent]
         *   **This option is only valid for listeners bound to {@link Ext.dom.Element Elements}.**
         *   `true` to call {@link Ext.event.Event#stopEvent stopEvent} on the event object
         *   before firing the handler.
         *
         * @param {Array} [options.args]
         *
         * Optional set of arguments to pass to the handler function before the actual
         * fired event arguments. For example, if `args` is set to `['foo', 42]`,
         * the event handler function will be called with an arguments list like this:
         *
         *      handler('foo', 42, <actual event arguments>...);
         *
         * @param {Boolean} [options.destroyable=false]
         *   When specified as `true`, the function returns a `destroyable` object. An object 
         *   which implements the `destroy` method which removes all listeners added in this call.
         *   This syntax can be a helpful shortcut to using {@link #un}; particularly when 
         *   removing multiple listeners.  *NOTE* - not compatible when using the _element_
         *   option.  See {@link #un} for the proper syntax for removing listeners added using the
         *   _element_ config.
         *   
         * @param {Number} [options.priority]
         *   An optional numeric priority that determines the order in which event handlers
         *   are run. Event handlers with no priority will be run as if they had a priority
         *   of 0. Handlers with a higher priority will be prioritized to run sooner than
         *   those with a lower priority.  Negative numbers can be used to set a priority
         *   lower than the default. Internally, the framework uses a range of 1000 or
         *   greater, and -1000 or lesser for handlers that are intended to run before or
         *   after all others, so it is recommended to stay within the range of -999 to 999
         *   when setting the priority of event handlers in application-level code.
         *   A priority must be an integer to be valid.  Fractional values are reserved for
         *   internal framework use.
         *
         * @param {String} [options.order='current']
         *   A legacy option that is provided for backward compatibility.
         *   It is recommended to use the `priority` option instead.  Available options are:
         *
         *   - `'before'`: equal to a priority of `100`
         *   - `'current'`: equal to a priority of `0` or default priority
         *   - `'after'`: equal to a priority of `-100`
         *
         * @param {String} [order='current']
         *   A shortcut for the `order` event option.  Provided for backward compatibility.
         *   Please use the `priority` event option instead.
         *
         * @param caller (private)
         *
         * **Combining Options**
         *
         * Using the options argument, it is possible to combine different types of listeners:
         *
         * A delayed, one-time listener.
         *
         *     myPanel.on('hide', this.handleClick, this, {
         *         single: true,
         *         delay: 100
         *     });
         *
         * **Attaching multiple handlers in 1 call**
         *
         * The method also allows for a single argument to be passed which is a config object 
         * containing properties which specify multiple handlers and handler configs.
         *
         *     grid.on({
         *         itemclick: 'onItemClick',
         *         itemcontextmenu: grid.onItemContextmenu,
         *         destroy: {
         *             fn: function () {
         *                 // function called within the 'altCmp' scope instead of grid
         *             },
         *             scope: altCmp // unique scope for the destroy handler
         *         },
         *         scope: grid       // default scope - provided for example clarity
         *     });
         *
         * **Delegate**
         *
         * This is a configuration option that you can pass along when registering a handler for 
         * an event to assist with event delegation. By setting this configuration option 
         * to a simple selector, the target element will be filtered to look for a 
         * descendant of the target. For example:
         *
         *     var panel = Ext.create({
         *         xtype: 'panel',
         *         renderTo: document.body,
         *         title: 'Delegate Handler Example',
         *         frame: true,
         *         height: 220,
         *         width: 220,
         *         html: '<h1 class="myTitle">BODY TITLE</h1>Body content'
         *     });
         *
         *     // The click handler will only be called when the click occurs on the
         *     // delegate: h1.myTitle ("h1" tag with class "myTitle")
         *     panel.on({
         *         click: function (e) {
         *             console.log(e.getTarget().innerHTML);
         *         },
         *         element: 'body',
         *         delegate: 'h1.myTitle'
         *      });
         *
         * @return {Object} **Only when the `destroyable` option is specified.**
         *
         *  A `Destroyable` object. An object which implements the `destroy` method which removes 
         *  all listeners added in this call. For example:
         *
         *     this.btnListeners =  = myButton.on({
         *         destroyable: true
         *         mouseover:   function() { console.log('mouseover'); },
         *         mouseout:    function() { console.log('mouseout'); },
         *         click:       function() { console.log('click'); }
         *     });
         *
         * And when those listeners need to be removed:
         *
         *     Ext.destroy(this.btnListeners);
         *
         * or
         *
         *     this.btnListeners.destroy();
         */
        addListener: function(eventName, fn, scope, options, order, caller) {
            var me = this,
                namedScopes = Ext._namedScopes,
                config, namedScope, isClassListener, innerScope, eventOptions;
            // Object listener hash passed
            if (typeof eventName !== 'string') {
                options = eventName;
                scope = options.scope;
                namedScope = scope && namedScopes[scope];
                isClassListener = namedScope && namedScope.isSelf;
                // give subclasses the opportunity to switch the valid eventOptions
                // (Ext.Component uses this when the "element" option is used)
                eventOptions = ((me.isComponent || me.isWidget) && options.element) ? me.$elementEventOptions : me.$eventOptions;
                for (eventName in options) {
                    config = options[eventName];
                    if (!eventOptions[eventName]) {
                        /* This would be an API change so check removed until https://sencha.jira.com/browse/EXTJSIV-7183 is fully implemented in 4.2
                        // Test must go here as well as in the simple form because of
                        // the attempted property access here on the config object.
                        if (!config || (typeof config !== 'function' && !config.fn)) {
                            Ext.raise('No function passed for event ' + me.$className + '.' +
                                      ename);
                        }
                        */
                        innerScope = config.scope;
                        // for proper scope resolution, scope:'controller' specified on an
                        // inner object, must be translated to 'self.controller' if the
                        // listeners object was declared on the class body.
                        // see also Ext.util.Observable#prepareClass and
                        // Ext.mixin.Inheritable#resolveListenerScope
                        if (innerScope && isClassListener) {
                            namedScope = namedScopes[innerScope];
                            if (namedScope && namedScope.isController) {
                                innerScope = 'self.controller';
                            }
                        }
                        me.doAddListener(eventName, config.fn || config, innerScope || scope, config.fn ? config : options, order, caller);
                    }
                }
                if (options && options.destroyable) {
                    return new ListenerRemover(me, options);
                }
            } else {
                me.doAddListener(eventName, fn, scope, options, order, caller);
                if (options && options.destroyable) {
                    return new ListenerRemover(me, eventName, fn, scope, options);
                }
            }
            return me;
        },
        /**
         * Removes an event handler.
         *
         * @param {String} eventName The type of event the handler was associated with.
         * @param {Function} fn The handler to remove. **This must be a reference to the function 
         * passed into the
         * {@link Ext.util.Observable#addListener addListener} call.**
         * @param {Object} scope (optional) The scope originally specified for the handler. It 
         * must be the same as the scope argument specified in the original call to 
         * {@link Ext.util.Observable#addListener} or the listener will not be removed.
         * @param eventOptions (private)
         *
         * **Convenience Syntax**
         *
         * You can use the {@link Ext.util.Observable#addListener addListener} 
         * `destroyable: true` config option in place of calling un().  For example:
         *
         *     var listeners = cmp.on({
         *         scope: cmp,
         *         afterrender: cmp.onAfterrender,
         *         beforehide: cmp.onBeforeHide,
         *         destroyable: true
         *     });
         *
         *     // Remove listeners
         *     listeners.destroy();
         *     // or
         *     cmp.un(
         *         scope: cmp,
         *         afterrender: cmp.onAfterrender,
         *         beforehide: cmp.onBeforeHide
         *     );
         *
         * **Exception - DOM event handlers using the element config option**
         *
         * You must go directly through the element to detach an event handler attached using
         * the {@link Ext.util.Observable#addListener addListener} _element_ option.
         *
         *     panel.on({
         *         element: 'body',
         *         click: 'onBodyCLick'
         *     });
         *
         *     panel.body.un({
         *         click: 'onBodyCLick'
         *     });
         */
        removeListener: function(eventName, fn, scope, eventOptions) {
            var me = this,
                config, options;
            if (typeof eventName !== 'string') {
                options = eventName;
                // give subclasses the opportunity to switch the valid eventOptions
                // (Ext.Component uses this when the "element" option is used)
                eventOptions = eventOptions || me.$eventOptions;
                for (eventName in options) {
                    if (options.hasOwnProperty(eventName)) {
                        config = options[eventName];
                        if (!me.$eventOptions[eventName]) {
                            me.doRemoveListener(eventName, config.fn || config, config.scope || options.scope);
                        }
                    }
                }
            } else {
                me.doRemoveListener(eventName, fn, scope);
            }
            return me;
        },
        /**
         * Appends a before-event handler.  Returning `false` from the handler will stop the event.
         *
         * Same as {@link Ext.util.Observable#addListener addListener} with `order` set 
         * to `'before'`.
         *
         * @param {String/String[]/Object} eventName The name of the event to listen for.
         * @param {Function/String} fn The method the event invokes.
         * @param {Object} [scope] The scope for `fn`.
         * @param {Object} [options] An object containing handler configuration.
         */
        onBefore: function(eventName, fn, scope, options) {
            return this.addListener(eventName, fn, scope, options, 'before');
        },
        /**
         * Appends an after-event handler.
         *
         * Same as {@link Ext.util.Observable#addListener addListener} with `order` set 
         * to `'after'`.
         *
         * @param {String/String[]/Object} eventName The name of the event to listen for.
         * @param {Function/String} fn The method the event invokes.
         * @param {Object} [scope] The scope for `fn`.
         * @param {Object} [options] An object containing handler configuration.
         */
        onAfter: function(eventName, fn, scope, options) {
            return this.addListener(eventName, fn, scope, options, 'after');
        },
        /**
         * Removes a before-event handler.
         *
         * Same as {@link #removeListener} with `order` set to `'before'`.
         *
         * @param {String/String[]/Object} eventName The name of the event the handler
         * was associated with.
         * @param {Function/String} fn The handler to remove.
         * @param {Object} [scope] The scope originally specified for `fn`.
         * @param {Object} [options] Extra options object.
         */
        unBefore: function(eventName, fn, scope, options) {
            return this.removeListener(eventName, fn, scope, options, 'before');
        },
        /**
         * Removes a before-event handler.
         *
         * Same as {@link #removeListener} with `order` set to `'after'`.
         *
         * @param {String/String[]/Object} eventName The name of the event the handler
         * was associated with.
         * @param {Function/String} fn The handler to remove.
         * @param {Object} [scope] The scope originally specified for `fn`.
         * @param {Object} [options] Extra options object.
         */
        unAfter: function(eventName, fn, scope, options) {
            return this.removeListener(eventName, fn, scope, options, 'after');
        },
        /**
         * Alias for {@link #onBefore}.
         */
        addBeforeListener: function() {
            return this.onBefore.apply(this, arguments);
        },
        /**
         * Alias for {@link #onAfter}.
         */
        addAfterListener: function() {
            return this.onAfter.apply(this, arguments);
        },
        /**
         * Alias for {@link #unBefore}.
         */
        removeBeforeListener: function() {
            return this.unBefore.apply(this, arguments);
        },
        /**
         * Alias for {@link #unAfter}.
         */
        removeAfterListener: function() {
            return this.unAfter.apply(this, arguments);
        },
        /**
         * Removes all listeners for this object including the managed listeners
         */
        clearListeners: function() {
            var me = this,
                events = me.events,
                hasListeners = me.hasListeners,
                event, key;
            if (events) {
                for (key in events) {
                    if (events.hasOwnProperty(key)) {
                        event = events[key];
                        if (event.isEvent) {
                            delete hasListeners[key];
                            event.clearListeners();
                        }
                    }
                }
                me.events = null;
            }
            me.clearManagedListeners();
        },
        purgeListeners: function() {
            if (Ext.global.console) {
                Ext.global.console.warn('Observable: purgeListeners has been deprecated. ' + 'Please use clearListeners.');
            }
            return this.clearListeners.apply(this, arguments);
        },
        /**
         * Removes all managed listeners for this object.
         */
        clearManagedListeners: function() {
            var me = this,
                managedListeners = me.managedListeners,
                i, len;
            if (managedListeners) {
                // So that Event#removeListener doesn't find a managedListeners array from which
                // to remove the listener it is removing. It iterates the array to find a match,
                // and splices it.
                me.managedListeners = null;
                for (i = 0 , len = managedListeners.length; i < len; i++) {
                    me.removeManagedListenerItem(true, managedListeners[i]);
                }
                managedListeners.length = 0;
            }
            me.managedListeners = managedListeners;
        },
        /**
         * Remove a single managed listener item
         * @private
         * @param {Boolean} isClear True if this is being called during a clear
         * @param {Object} managedListener The managed listener item
         * @param {Object} item
         * @param {String} ename
         * @param {Function} fn
         * @param {Object} scope
         * See removeManagedListener for other args
         */
        removeManagedListenerItem: function(isClear, managedListener, item, ename, fn, scope) {
            if (isClear || (managedListener.item === item && managedListener.ename === ename && (!fn || managedListener.fn === fn) && (!scope || managedListener.scope === scope))) {
                // Pass along the options for mixin.Observable, for example if using delegate.
                // If the item has already been destroyed, its listeners were already cleared.
                if (!managedListener.item.destroyed) {
                    managedListener.item.doRemoveListener(managedListener.ename, managedListener.fn, managedListener.scope, managedListener.options);
                }
                if (!isClear) {
                    Ext.Array.remove(this.managedListeners, managedListener);
                }
            }
        },
        purgeManagedListeners: function() {
            if (Ext.global.console) {
                Ext.global.console.warn('Observable: purgeManagedListeners has been deprecated. ' + 'Please use clearManagedListeners.');
            }
            return this.clearManagedListeners.apply(this, arguments);
        },
        /**
         * Checks to see if this object has any listeners for a specified event, or whether
         * the event bubbles. The answer indicates whether the event needs firing or not.
         *
         * @param {String} eventName The name of the event to check for
         * @return {Boolean} `true` if the event is being listened for or bubbles, else `false`
         */
        hasListener: function(eventName) {
            eventName = Ext.canonicalEventName(eventName);
            return !!this.hasListeners[eventName];
        },
        /**
         * Checks if all events, or a specific event, is suspended.
         * @param {String} [event] The name of the specific event to check
         * @return {Boolean} `true` if events are suspended
         */
        isSuspended: function(event) {
            var suspended = this.eventsSuspended > 0,
                events = this.events;
            if (!suspended && event && events) {
                event = events[event];
                if (event && event.isEvent) {
                    return event.isSuspended();
                }
            }
            return suspended;
        },
        /**
         * Suspends the firing of all events. (see {@link #resumeEvents})
         *
         * @param {Boolean} queueSuspended `true` to queue up suspended events to be fired
         * after the {@link #resumeEvents} call instead of discarding all suspended events.
         */
        suspendEvents: function(queueSuspended) {
            ++this.eventsSuspended;
            if (queueSuspended && !this.eventQueue) {
                this.eventQueue = [];
            }
        },
        /**
         * Suspends firing of the named event(s).
         *
         * After calling this method to suspend events, the events will no longer fire when
         * requested to fire.
         *
         * **Note that if this is called multiple times for a certain event, the converse method
         * {@link #resumeEvent} will have to be called the same number of times for it to resume
         * firing.**
         *
         * @param  {String...} eventName Multiple event names to suspend.
         */
        suspendEvent: function() {
            var me = this,
                events = me.events,
                len = arguments.length,
                i, event, ename;
            for (i = 0; i < len; i++) {
                ename = arguments[i];
                ename = Ext.canonicalEventName(ename);
                event = events[ename];
                // we need to spin up the Event instance so it can hold the suspend count
                if (!event || !event.isEvent) {
                    event = me._initEvent(ename);
                }
                event.suspend();
            }
        },
        /**
         * Resumes firing of the named event(s).
         *
         * After calling this method to resume events, the events will fire when requested to fire.
         *
         * **Note that if the {@link #suspendEvent} method is called multiple times for a certain
         * event, this converse method will have to be called the same number of times for it
         * to resume firing.**
         *
         * @param {String...} eventName Multiple event names to resume.
         */
        resumeEvent: function() {
            var events = this.events || 0,
                len = events && arguments.length,
                i, event, ename;
            for (i = 0; i < len; i++) {
                ename = Ext.canonicalEventName(arguments[i]);
                event = events[ename];
                // If it exists, and is an Event object (not still a boolean placeholder), resume it
                if (event && event.resume) {
                    event.resume();
                }
            }
        },
        /**
        * Resumes firing events (see {@link #suspendEvents}).
        *
        * If events were suspended using the `queueSuspended` parameter, then all events fired
        * during event suspension will be sent to any listeners now.
        * 
        * @param {Boolean} [discardQueue] `true` to prevent any previously queued events from firing
        * while we were suspended. See {@link #suspendEvents}.
        */
        resumeEvents: function(discardQueue) {
            var me = this,
                queued = me.eventQueue,
                qLen, q;
            if (me.eventsSuspended && !--me.eventsSuspended) {
                delete me.eventQueue;
                if (!discardQueue && queued) {
                    qLen = queued.length;
                    for (q = 0; q < qLen; q++) {
                        // Important to call fireEventArgs here so MVC can hook in
                        me.fireEventArgs.apply(me, queued[q]);
                    }
                }
            }
        },
        /**
         * Relays selected events from the specified Observable as if the events were fired
         * by `this`.
         *
         * For example if you are extending Grid, you might decide to forward some events from
         * store. So you can do this inside your initComponent:
         *
         *     this.relayEvents(this.getStore(), ['load']);
         *
         * The grid instance will then have an observable 'load' event which will be passed 
         * the parameters of the store's load event and any function fired with the grid's 
         * load event would have access to the grid using the this keyword (unless the event 
         * is handled by a controller's control/listen event listener in which case 'this' 
         * will be the controller rather than the grid).
         *
         * @param {Object} origin The Observable whose events this object is to relay.
         * @param {String[]/Object} events Array of event names to relay or an Object with key/value
         * pairs translating to ActualEventName/NewEventName respectively. For example:
         *     this.relayEvents(this, {add:'push', remove:'pop'});
         *
         * Would now redispatch the add event of this as a push event and the remove event
         * as a pop event.
         *
         * @param {String} [prefix] A common prefix to prepend to the event names. For example:
         *
         *     this.relayEvents(this.getStore(), ['load', 'clear'], 'store');
         *
         * Now the grid will forward 'load' and 'clear' events of store as 'storeload' and
         * 'storeclear'.
         *
         * @return {Object} A `Destroyable` object. An object which implements the `destroy` method
         * which, when destroyed, removes all relayers. For example:
         *
         *     this.storeRelayers = this.relayEvents(this.getStore(), ['load', 'clear'], 'store');
         *
         * Can be undone by calling
         *
         *     Ext.destroy(this.storeRelayers);
         *
         * or
         *     this.store.relayers.destroy();
         */
        relayEvents: function(origin, events, prefix) {
            var me = this,
                len = events.length,
                i = 0,
                oldName, newName,
                relayers = {};
            if (Ext.isObject(events)) {
                for (i in events) {
                    newName = events[i];
                    relayers[i] = me.createRelayer(newName);
                }
            } else {
                for (; i < len; i++) {
                    oldName = events[i];
                    // Build up the listener hash.
                    relayers[oldName] = me.createRelayer(prefix ? prefix + oldName : oldName);
                }
            }
            // Add the relaying listeners as ManagedListeners so that they are removed when
            // this.clearListeners is called (usually when _this_ is destroyed)
            // Explicitly pass options as undefined so that the listener does not get
            // an extra options param which then has to be sliced off in the relayer.
            me.mon(origin, relayers, null, null, undefined);
            // relayed events are always destroyable.
            return new ListenerRemover(me, origin, relayers);
        },
        /**
         * @private
         * Creates an event handling function which re-fires the event from this object
         * as the passed event name.
         * @param {String} newName The name under which to re-fire the passed parameters.
         * @param {Array} beginEnd (optional) The caller can specify on which indices to slice.
         * @return {Function}
         */
        createRelayer: function(newName, beginEnd) {
            var me = this;
            return function() {
                // eslint-disable-next-line max-len
                return me.fireEventArgs.call(me, newName, beginEnd ? arraySlice.apply(arguments, beginEnd) : arguments);
            };
        },
        /**
         * Enables events fired by this Observable to bubble up an owner hierarchy by calling
         * `this.getBubbleTarget()` if present. There is no implementation in the Observable
         * base class.
         *
         * This is commonly used by Ext.Components to bubble events to owner Containers.
         * See {@link Ext.Component#getBubbleTarget}. The default implementation in Ext.Component
         * returns the Component's immediate owner. But if a known target is required, this can be
         * overridden to access the required target more quickly.
         *
         * Example:
         *
         *     Ext.define('Ext.overrides.form.field.Base', {
         *         override: 'Ext.form.field.Base',
         *
         *         //  Add functionality to Field's initComponent to enable
         *         // the change event to bubble
         *         initComponent: function () {
         *             this.callParent();
         *             this.enableBubble('change');
         *         }
         *     });
         *
         *     var myForm = Ext.create('Ext.form.Panel', {
         *         title: 'User Details',
         *         items: [{
         *             ...
         *         }],
         *         listeners: {
         *             change: function() {
         *                 // Title goes red if form has been modified.
         *                 myForm.header.setStyle('color', 'red');
         *             }
         *         }
         *     });
         *
         * @param {String/String[]} eventNames The event name to bubble, or an Array of event names.
         */
        enableBubble: function(eventNames) {
            if (eventNames) {
                // eslint-disable-next-line vars-on-top
                var me = this,
                    names = (typeof eventNames === 'string') ? arguments : eventNames,
                    // we must create events now if we have not yet
                    events = me.events,
                    length = events && names.length,
                    ename, event, i;
                for (i = 0; i < length; ++i) {
                    ename = names[i];
                    ename = Ext.canonicalEventName(ename);
                    event = events[ename];
                    if (!event || !event.isEvent) {
                        event = me._initEvent(ename);
                    }
                    // Event must fire if it bubbles (We don't know if anyone up the
                    // bubble hierarchy has listeners added)
                    me.hasListeners._incr_(ename);
                    event.bubble = true;
                }
            }
        },
        /**
         * @private
         * Destructor for classes that extend Observable.
         */
        destroy: function() {
            this.clearListeners();
            this.callParent();
            this.destroyObservable(true);
        },
        destroyObservable: function(skipClearListeners) {
            var me = this,
                clearPropertiesOnDestroy = me.clearPropertiesOnDestroy;
            if (me.$observableDestroyed) {
                return;
            }
            if (!skipClearListeners) {
                me.clearListeners();
            }
            // This method is called after the Base destructor, and most of the instances
            // should be already destroyed at this point. However Classic Components are
            // conditionally destructible and so can possibly *not* be destroyed before
            // our mixed-in destructor is called. Component's destructor will take care
            // of that by calling this method explicitly.
            if (me.destroyed) {
                if (clearPropertiesOnDestroy) {
                    if (clearPropertiesOnDestroy === true && !me.$nulled) {
                        me.$reap();
                    }
                    // At this point we can safely assume that the instance is completely
                    // destroyed and should not be able to fire events anymore. We don't
                    // want to do this when the prototype is going to be cleared below,
                    // because having these emptyFns on the object instance will defy
                    // the purpose of prototype clearing.
                    if (!me.clearPrototypeOnDestroy) {
                        me.fireEvent = me.fireEventArgs = me.fireAction = me.fireEventedAction = Ext.emptyFn;
                    }
                    // We do not null hasListeners reference since it's a) very special,
                    // and b) can't possibly lead to significant leaks. (In theory, right).
                    me.events = me.managedListeners = me.eventedBeforeEventNames = null;
                    me.$observableDestroyed = true;
                }
                // Due to the way Observable mixin installs the after handler,
                // this can be called twice in a row. Doing that the second time
                // will most probably blow up on some method call -- and that is
                // totally what we are about, except in this particular case.
                if (me.clearPrototypeOnDestroy && Object.setPrototypeOf && !me.$alreadyNulled) {
                    Object.setPrototypeOf(me, null);
                    me.$alreadyNulled = true;
                }
            }
        },
        privates: {
            doAddListener: function(ename, fn, scope, options, order, caller, manager) {
                var me = this,
                    ret = false,
                    event, priority;
                order = order || (options && options.order);
                if (order) {
                    priority = (options && options.priority);
                    if (!priority) {
                        // priority option takes precedence over order
                        // do not mutate the user's options
                        options = options ? Ext.Object.chain(options) : {};
                        options.priority = me.$orderToPriority[order];
                    }
                }
                ename = Ext.canonicalEventName(ename);
                if (!fn) {
                    Ext.raise("Cannot add '" + ename + "' listener to " + me.$className + " instance.  No function specified.");
                }
                event = (me.events || (me.events = {}))[ename];
                if (!event || !event.isEvent) {
                    event = me._initEvent(ename);
                }
                if (fn !== emptyFn) {
                    // Check whether the listener should be managed.
                    // Event#addListener will add it to the manager's managedListeners stack 
                    // upon successful add of the listener to the event.
                    if (!manager && (scope && scope.isObservable && (scope !== me))) {
                        manager = scope;
                    }
                    if (event.addListener(fn, scope, options, caller, manager)) {
                        // If a new listener has been added (Event.addListener rejects
                        // duplicates of the same fn+scope)
                        // then increment the hasListeners counter
                        me.hasListeners._incr_(ename);
                        ret = true;
                    }
                }
                return ret;
            },
            doRemoveListener: function(ename, fn, scope) {
                var me = this,
                    ret = false,
                    events = me.events,
                    event;
                ename = Ext.canonicalEventName(ename);
                event = events && events[ename];
                if (!fn) {
                    Ext.raise("Cannot remove '" + ename + "' listener to " + me.$className + " instance.  No function specified.");
                }
                if (event && event.isEvent) {
                    if (event.removeListener(fn, scope)) {
                        me.hasListeners._decr_(ename);
                        ret = true;
                    }
                }
                return ret;
            },
            _initEvent: function(eventName) {
                return (this.events[eventName] = new Ext.util.Event(this, eventName));
            }
        },
        deprecated: {
            '5.0': {
                methods: {
                    addEvents: null
                }
            }
        }
    };
}, function() {
    var Observable = this,
        proto = Observable.prototype,
        HasListeners = function() {},
        prepareMixin = function(T) {
            var proto = T.prototype;
            if (!T.HasListeners) {
                // Keep track of whether we were added via a mixin or not, this becomes
                // important later when discovering merged listeners on the class.
                proto.$observableMixedIn = 1;
                // Classes that use us as a mixin (best practice) need to be prepared.
                Observable.prepareClass(T, this);
                // Now that we are mixed in to class T, we need to watch T for derivations
                // and prepare them also.
                T.onExtended(function(U, data) {
                    if (Ext.classSystemMonitor) {
                        Ext.classSystemMonitor('extend mixin', arguments);
                    }
                    Observable.prepareClass(U, null, data);
                });
                // Also, if a class uses us as a mixin and that class is then used as
                // a mixin, we need to be notified of that as well.
                if (proto.onClassMixedIn) {
                    // play nice with other potential overrides...
                    Ext.override(T, {
                        onClassMixedIn: function(U) {
                            prepareMixin.call(this, U);
                            this.callParent(arguments);
                        }
                    });
                } else {
                    // just us chickens, so add the method...
                    proto.onClassMixedIn = function(U) {
                        prepareMixin.call(this, U);
                    };
                }
            }
            superOnClassMixedIn.call(this, T);
        },
        // We are overriding the onClassMixedIn of Ext.Mixin. Save a reference to it
        // so we can call it after our onClassMixedIn.
        superOnClassMixedIn = proto.onClassMixedIn;
    HasListeners.prototype = {
        // $$: 42  // to make sure we have a proper prototype
        _decr_: function(ev, count) {
            // count is optionally passed when clearing listeners in bulk
            // e.g. when clearListeners is called on a component that has listeners that
            // were attached using the "delegate" option
            if (count == null) {
                count = 1;
            }
            if (!(this[ev] -= count)) {
                // Delete this entry, since 0 does not mean no one is listening, just
                // that no one is *directly* listening. This allows the eventBus or
                // class observers to "poke" through and expose their presence.
                delete this[ev];
            }
        },
        _incr_: function(ev) {
            if (this.hasOwnProperty(ev)) {
                // if we already have listeners at this level, just increment the count...
                ++this[ev];
            } else {
                // otherwise, start the count at 1 (which hides whatever is in our prototype
                // chain)...
                this[ev] = 1;
            }
        }
    };
    proto.HasListeners = Observable.HasListeners = HasListeners;
    Observable.createAlias({
        /**
         * @method on
         * @inheritdoc Ext.util.Observable#method-addListener
         */
        on: 'addListener',
        /**
         * @method un
         * Shorthand for {@link #removeListener}.
         * @inheritdoc Ext.util.Observable#method-removeListener
         */
        un: 'removeListener',
        /**
         * @method mon
         * Shorthand for {@link #addManagedListener}.
         * @inheritdoc Ext.util.Observable#method-addManagedListener
         */
        mon: 'addManagedListener',
        /**
         * @method mun
         * Shorthand for {@link #removeManagedListener}.
         * @inheritdoc Ext.util.Observable#method-removeManagedListener
         */
        mun: 'removeManagedListener',
        /**
         * @method
         * An alias for {@link Ext.util.Observable#addListener addListener}.  In 
         * versions prior to 5.1, {@link #listeners} had a generated setter which could 
         * be called to add listeners.  In 5.1 the listeners config is not processed 
         * using the config system and has no generated setter, so this method is 
         * provided for backward compatibility.  The preferred way of adding listeners 
         * is to use the {@link #on} method.
         * @param {Object} listeners The listeners
         */
        setListeners: 'addListener'
    });
    // deprecated, will be removed in 5.0
    Observable.observeClass = Observable.observe;
    // Used by Ext.mixin.Hookable to create sequences.
    function getMethodEvent(method) {
        var event = (this.methodEvents = this.methodEvents || {})[method],
            returnValue, v, cancel,
            me = this,
            makeCall;
        if (!event) {
            me.methodEvents[method] = event = {};
            event.originalFn = me[method];
            event.methodName = method;
            event.before = [];
            event.after = [];
            makeCall = function(fn, scope, args) {
                scope = scope || me;
                if (typeof fn === 'string') {
                    fn = scope[fn];
                }
                if ((v = fn.apply(scope, args)) !== undefined) {
                    if (typeof v === 'object') {
                        if (v.returnValue !== undefined) {
                            returnValue = v.returnValue;
                        } else {
                            returnValue = v;
                        }
                        cancel = !!v.cancel;
                    } else if (v === false) {
                        cancel = true;
                    } else {
                        returnValue = v;
                    }
                }
            };
            me[method] = function() {
                var args = Array.prototype.slice.call(arguments, 0),
                    argsLen = args.length,
                    b, i, len;
                returnValue = v = undefined;
                cancel = false;
                for (i = 0 , len = event.before.length; i < len; i++) {
                    b = event.before[i];
                    if (b.extraArgs) {
                        args.push.apply(args, b.extraArgs);
                    }
                    makeCall(b.fn, b.scope, args);
                    args.length = argsLen;
                    if (cancel || b.preventDefault) {
                        return returnValue;
                    }
                }
                if ((v = event.originalFn.apply(me, args)) !== undefined) {
                    returnValue = v;
                }
                for (i = 0 , len = event.after.length; i < len; i++) {
                    b = event.after[i];
                    if (b.extraArgs) {
                        args.push.apply(args, b.extraArgs);
                    }
                    makeCall(b.fn, b.scope, args);
                    args.length = argsLen;
                    if (cancel || b.preventDefault) {
                        return returnValue;
                    }
                }
                return returnValue;
            };
        }
        return event;
    }
    Ext.apply(proto, {
        onClassMixedIn: prepareMixin,
        // adds an 'interceptor' called before the original method
        beforeMethod: function(method, fn, scope, preventDefault, extraArgs) {
            getMethodEvent.call(this, method).before.push({
                fn: fn,
                scope: scope,
                extraArgs: extraArgs,
                preventDefault: preventDefault
            });
        },
        // adds a 'sequence' called after the original method
        afterMethod: function(method, fn, scope, preventDefault, extraArgs) {
            getMethodEvent.call(this, method).after.push({
                fn: fn,
                scope: scope,
                extraArgs: extraArgs,
                preventDefault: preventDefault
            });
        },
        removeMethodListener: function(method, fn, scope) {
            var e = getMethodEvent.call(this, method),
                i, len;
            for (i = 0 , len = e.before.length; i < len; i++) {
                // eslint-disable-next-line eqeqeq
                if (e.before[i].fn == fn && e.before[i].scope == scope) {
                    Ext.Array.erase(e.before, i, 1);
                    return;
                }
            }
            for (i = 0 , len = e.after.length; i < len; i++) {
                // eslint-disable-next-line eqeqeq
                if (e.after[i].fn == fn && e.after[i].scope == scope) {
                    Ext.Array.erase(e.after, i, 1);
                    return;
                }
            }
        },
        toggleEventLogging: function(toggle) {
            Ext.util.Observable[toggle ? 'capture' : 'releaseCapture'](this, function(en) {
                if (Ext.isDefined(Ext.global.console)) {
                    Ext.global.console.log(en, arguments);
                }
            });
        }
    });
});

/**
 * Represents a collection of a set of key and value pairs. Each key in the HashMap
 * must be unique, the same key cannot exist twice. Access to items is provided via
 * the key only. Sample usage:
 *
 *     var map = new Ext.util.HashMap();
 *     map.add('key1', 1);
 *     map.add('key2', 2);
 *     map.add('key3', 3);
 *
 *     map.each(function(key, value, length){
 *         console.log(key, value, length);
 *     });
 *
 * The HashMap is an unordered class,
 * there is no guarantee when iterating over the items that they will be in any particular
 * order. If this is required, then use a {@link Ext.util.MixedCollection}.
 */
Ext.define('Ext.util.HashMap', {
    mixins: [
        'Ext.mixin.Observable'
    ],
    /**
     * Mutation counter which is incremented upon add and remove.
     * @readonly
     */
    generation: 0,
    config: {
        /**
        * @cfg {Function} keyFn A function that is used to retrieve a default key for a passed
        * object. A default is provided that returns the `id` property on the object. This function
        * is only used if the `add` method is called with a single argument.
        */
        keyFn: null
    },
    /**
     * @event add
     * Fires when a new item is added to the hash.
     * @param {Ext.util.HashMap} this
     * @param {String} key The key of the added item.
     * @param {Object} value The value of the added item.
     */
    /**
     * @event clear
     * Fires when the hash is cleared.
     * @param {Ext.util.HashMap} this
     */
    /**
     * @event remove
     * Fires when an item is removed from the hash.
     * @param {Ext.util.HashMap} this
     * @param {String} key The key of the removed item.
     * @param {Object} value The value of the removed item.
     */
    /**
     * @event replace
     * Fires when an item is replaced in the hash.
     * @param {Ext.util.HashMap} this
     * @param {String} key The key of the replaced item.
     * @param {Object} value The new value for the item.
     * @param {Object} old The old value for the item.
     */
    /**
     * Creates new HashMap.
     * @param {Object} config (optional) Config object.
     */
    constructor: function(config) {
        var me = this,
            fn;
        // Will call initConfig
        me.mixins.observable.constructor.call(me, config);
        me.clear(true);
        fn = me.getKeyFn();
        if (fn) {
            me.getKey = fn;
        }
    },
    /**
     * Gets the number of items in the hash.
     * @return {Number} The number of items in the hash.
     */
    getCount: function() {
        return this.length;
    },
    /**
     * Implementation for being able to extract the key from an object if only
     * a single argument is passed.
     * @private
     * @param {String} key The key
     * @param {Object} value The value
     * @return {Array} [key, value]
     */
    getData: function(key, value) {
        // if we have no value, it means we need to get the key from the object
        if (value === undefined) {
            value = key;
            key = this.getKey(value);
        }
        return [
            key,
            value
        ];
    },
    /**
     * Extracts the key from an object. This is a default implementation, it may be overridden
     * @param {Object} o The object to get the key from
     * @return {String} The key to use.
     */
    getKey: function(o) {
        return o.id;
    },
    /**
     * Adds an item to the collection. Fires the {@link #event-add} event when complete.
     *
     * @param {String/Object} key The key to associate with the item, or the new item.
     *
     * If a {@link #getKey} implementation was specified for this HashMap,
     * or if the key of the stored items is in a property called `id`,
     * the HashMap will be able to *derive* the key for the new item.
     * In this case just pass the new item in this parameter.
     *
     * @param {Object} [value] The item to add.
     * @return {Object} The item added.
     */
    add: function(key, value) {
        var me = this;
        // Need to check arguments length here, since we could have called:
        // map.add('foo', undefined);
        if (arguments.length === 1) {
            value = key;
            key = me.getKey(value);
        }
        if (me.containsKey(key)) {
            return me.replace(key, value);
        }
        me.map[key] = value;
        ++me.length;
        me.generation++;
        if (me.hasListeners.add) {
            me.fireEvent('add', me, key, value);
        }
        return value;
    },
    /**
     * Replaces an item in the hash. If the key doesn't exist, the
     * {@link #method-add} method will be used.
     * @param {String} key The key of the item.
     * @param {Object} value The new value for the item.
     * @return {Object} The new value of the item.
     */
    replace: function(key, value) {
        var me = this,
            map = me.map,
            old;
        // Need to check arguments length here, since we could have called:
        // map.replace('foo', undefined);
        if (arguments.length === 1) {
            value = key;
            key = me.getKey(value);
        }
        if (!me.containsKey(key)) {
            me.add(key, value);
        }
        old = map[key];
        map[key] = value;
        me.generation++;
        if (me.hasListeners.replace) {
            me.fireEvent('replace', me, key, value, old);
        }
        return value;
    },
    /**
     * Remove an item from the hash.
     * @param {Object} o The value of the item to remove.
     * @return {Boolean} True if the item was successfully removed.
     */
    remove: function(o) {
        var key = this.findKey(o);
        if (key !== undefined) {
            return this.removeAtKey(key);
        }
        return false;
    },
    /**
     * Remove an item from the hash.
     * @param {String} key The key to remove.
     * @return {Boolean} True if the item was successfully removed.
     */
    removeAtKey: function(key) {
        var me = this,
            value;
        if (me.containsKey(key)) {
            value = me.map[key];
            delete me.map[key];
            --me.length;
            me.generation++;
            if (me.hasListeners.remove) {
                me.fireEvent('remove', me, key, value);
            }
            return true;
        }
        return false;
    },
    /**
     * Retrieves an item with a particular key.
     * @param {String} key The key to lookup.
     * @return {Object} The value at that key. If it doesn't exist, `undefined` is returned.
     */
    get: function(key) {
        var map = this.map;
        return map.hasOwnProperty(key) ? map[key] : undefined;
    },
    /**
     * @ignore
     */
    clear: function(initial) {
        // We use the above syntax because we don't want the initial param to be part
        // of the public API
        var me = this;
        // Only clear if it has ever had any content
        if (initial || me.generation) {
            me.map = {};
            me.length = 0;
            me.generation = initial ? 0 : me.generation + 1;
        }
        if (initial !== true && me.hasListeners.clear) {
            me.fireEvent('clear', me);
        }
        return me;
    },
    /**
     * Checks whether a key exists in the hash.
     * @param {String} key The key to check for.
     * @return {Boolean} True if they key exists in the hash.
     */
    containsKey: function(key) {
        var map = this.map;
        return map.hasOwnProperty(key) && map[key] !== undefined;
    },
    /**
     * Checks whether a value exists in the hash.
     * @param {Object} value The value to check for.
     * @return {Boolean} True if the value exists in the dictionary.
     */
    contains: function(value) {
        return this.containsKey(this.findKey(value));
    },
    /**
     * Return all of the keys in the hash.
     * @return {Array} An array of keys.
     */
    getKeys: function() {
        return this.getArray(true);
    },
    /**
     * Return all of the values in the hash.
     * @return {Array} An array of values.
     */
    getValues: function() {
        return this.getArray(false);
    },
    /**
     * Gets either the keys/values in an array from the hash.
     * @private
     * @param {Boolean} isKey True to extract the keys, otherwise, the value
     * @return {Array} An array of either keys/values from the hash.
     */
    getArray: function(isKey) {
        var arr = [],
            key,
            map = this.map;
        for (key in map) {
            if (map.hasOwnProperty(key)) {
                arr.push(isKey ? key : map[key]);
            }
        }
        return arr;
    },
    /**
     * Executes the specified function once for each item in the hash.
     * Returning false from the function will cease iteration.
     *
     * @param {Function} fn The function to execute.
     * @param {String} fn.key The key of the item.
     * @param {Number} fn.value The value of the item.
     * @param {Number} fn.length The total number of items in the hash.
     * @param {Object} [scope] The scope to execute in. Defaults to <tt>this</tt>.
     * @return {Ext.util.HashMap} this
     */
    each: function(fn, scope) {
        // copy items so they may be removed during iteration.
        var items = Ext.apply({}, this.map),
            key,
            length = this.length;
        scope = scope || this;
        for (key in items) {
            if (items.hasOwnProperty(key)) {
                if (fn.call(scope, key, items[key], length) === false) {
                    break;
                }
            }
        }
        return this;
    },
    /**
     * Performs a shallow copy on this hash.
     * @return {Ext.util.HashMap} The new hash object.
     */
    clone: function() {
        var hash = new this.self(this.initialConfig),
            map = this.map,
            key;
        hash.suspendEvents();
        for (key in map) {
            if (map.hasOwnProperty(key)) {
                hash.add(key, map[key]);
            }
        }
        hash.resumeEvents();
        return hash;
    },
    /**
     * @private
     * Find the key for a value.
     * @param {Object} value The value to find.
     * @return {Object} The value of the item. Returns <tt>undefined</tt> if not found.
     */
    findKey: function(value) {
        var key,
            map = this.map;
        for (key in map) {
            if (map.hasOwnProperty(key) && map[key] === value) {
                return key;
            }
        }
        return undefined;
    }
}, function(HashMap) {
    var prototype = HashMap.prototype;
    /**
     * @method removeByKey
     * An alias for {@link #removeAtKey}
     * @inheritdoc Ext.util.HashMap#removeAtKey
     */
    prototype.removeByKey = prototype.removeAtKey;
});

/**
 * Base Manager class
 * @private
 * @deprecated 5.0.0 Will be removed in 7.0.
 */
Ext.define('Ext.AbstractManager', {
    requires: [
        'Ext.util.HashMap'
    ],
    typeName: 'type',
    constructor: function(config) {
        Ext.apply(this, config || {});
        /**
         * @property {Ext.util.HashMap} all
         * Contains all of the items currently managed
         */
        this.all = new Ext.util.HashMap();
        this.types = {};
    },
    /**
     * Returns an item by id.
     * For additional details see {@link Ext.util.HashMap#get}.
     * @param {String} id The id of the item
     * @return {Object} The item, undefined if not found.
     */
    get: function(id) {
        return this.all.get(id);
    },
    /**
     * Registers an item to be managed
     * @param {Object} item The item to register
     */
    register: function(item) {
        var key = this.all.getKey(item);
        if (key === undefined) {
            Ext.raise('Key is undefined. Please ensure the item has a key ' + 'before registering the item.');
        }
        if (this.all.containsKey(key)) {
            Ext.raise('Registering duplicate id "' + key + '" with ' + this.$className);
        }
        this.all.add(item);
    },
    /**
     * Unregisters an item by removing it from this manager
     * @param {Object} item The item to unregister
     */
    unregister: function(item) {
        this.all.remove(item);
    },
    /**
     * Registers a new item constructor, keyed by a type key.
     * @param {String} type The mnemonic string by which the class may be looked up.
     * @param {Function} cls The new instance class.
     */
    registerType: function(type, cls) {
        this.types[type] = cls;
        cls[this.typeName] = type;
    },
    /**
     * Checks if an item type is registered.
     * @param {String} type The mnemonic string by which the class may be looked up
     * @return {Boolean} Whether the type is registered.
     */
    isRegistered: function(type) {
        return this.types[type] !== undefined;
    },
    /**
     * Creates and returns an instance of whatever this manager manages, based on
     * the supplied type and config object.
     * @param {Object} config The config object
     * @param {String} defaultType If no type is discovered in the config object, we
     * fall back to this type
     * @return {Object} The instance of whatever this manager is managing
     */
    create: function(config, defaultType) {
        var type = config[this.typeName] || config.type || defaultType,
            Constructor = this.types[type];
        if (Constructor === undefined) {
            Ext.raise("The '" + type + "' type has not been registered with this manager");
        }
        return new Constructor(config);
    },
    /**
     * Registers a function that will be called when an item with the specified id is added
     * to the manager. This will happen on instantiation.
     * @param {String} id The item id
     * @param {Function} fn The callback function. Called with a single parameter, the item.
     * @param {Object} scope The scope (this reference) in which the callback is executed.
     * Defaults to the item.
     */
    onAvailable: function(id, fn, scope) {
        var all = this.all,
            item, callback;
        if (all.containsKey(id)) {
            item = all.get(id);
            fn.call(scope || item, item);
        } else {
            callback = function(map, key, item) {
                if (key === id) {
                    fn.call(scope || item, item);
                    all.un('add', callback);
                }
            };
            all.on('add', callback);
        }
    },
    /**
     * Executes the specified function once for each item in the collection.
     * @param {Function} fn The function to execute.
     * @param {String} fn.key The key of the item
     * @param {Number} fn.value The value of the item
     * @param {Number} fn.length The total number of items in the collection
     * @param {Boolean} fn.return False to cease iteration.
     * @param {Object} scope The scope to execute in. Defaults to `this`.
     */
    each: function(fn, scope) {
        this.all.each(fn, scope || this);
    },
    /**
     * Gets the number of items in the collection.
     * @return {Number} The number of items in the collection.
     */
    getCount: function() {
        return this.all.getCount();
    }
});

//
// Ext.promise.Consequence adapted from:
// [DeftJS](https://github.com/deftjs/deftjs5)
// Copyright (c) 2012-2013 [DeftJS Framework Contributors](http://deftjs.org)
// Open source under the [MIT License](http://en.wikipedia.org/wiki/MIT_License).
//
/**
 * Consequences are used internally by a Deferred to capture and notify callbacks, and
 * propagate their transformed results as fulfillment or rejection.
 *
 * Developers never directly interact with a Consequence.
 *
 * A Consequence forms a chain between two Deferreds, where the result of the first
 * Deferred is transformed by the corresponding callback before being applied to the
 * second Deferred.
 *
 * Each time a Deferred's `then` method is called, it creates a new Consequence that will
 * be triggered once its originating Deferred has been fulfilled or rejected. A Consequence
 * captures a pair of optional onFulfilled and onRejected callbacks.
 *
 * Each Consequence has its own Deferred (which in turn has a Promise) that is resolved or
 * rejected when the Consequence is triggered. When a Consequence is triggered by its
 * originating Deferred, it calls the corresponding callback and propagates the transformed
 * result to its own Deferred; resolved with the callback return value or rejected with any
 * error thrown by the callback.
 *
 * @since 6.0.0
 * @private
 */
Ext.define('Ext.promise.Consequence', function(Consequence) {
    return {
        // eslint-disable-line brace-style, max-len
        /**
     * @property {Ext.promise.Promise}
     * Promise of the future value of this Consequence.
     */
        promise: null,
        /**
     * @property {Ext.promise.Deferred} deferred Internal Deferred for this Consequence.
     *
     * @private
     */
        deferred: null,
        /**
     * @property {Function} onFulfilled Callback to execute when this Consequence is triggered
     * with a fulfillment value.
     *
     * @private
     */
        onFulfilled: null,
        /**
     * @property {Function} onRejected Callback to execute when this Consequence is triggered
     * with a rejection reason.
     *
     * @private
     */
        onRejected: null,
        /**
     * @property {Function} onProgress Callback to execute when this Consequence is updated
     * with a progress value.
     *
     * @private
     */
        onProgress: null,
        /**
     * @param {Function} onFulfilled Callback to execute to transform a fulfillment value.
     * @param {Function} onRejected Callback to execute to transform a rejection reason.
     * @param {Function} onProgress Callback to execute to transform a progress value.
     */
        constructor: function(onFulfilled, onRejected, onProgress) {
            var me = this;
            me.onFulfilled = onFulfilled;
            me.onRejected = onRejected;
            me.onProgress = onProgress;
            me.deferred = new Ext.promise.Deferred();
            me.promise = me.deferred.promise;
        },
        /**
     * Trigger this Consequence with the specified action and value.
     *
     * @param {String} action Completion action (i.e. fulfill or reject).
     * @param {Mixed} value Fulfillment value or rejection reason.
     */
        trigger: function(action, value) {
            var me = this,
                deferred = me.deferred;
            switch (action) {
                case 'fulfill':
                    me.propagate(value, me.onFulfilled, deferred, deferred.resolve);
                    break;
                case 'reject':
                    me.propagate(value, me.onRejected, deferred, deferred.reject);
                    break;
            }
        },
        /**
     * Update this Consequence with the specified progress value.
     *
     * @param {Mixed} progress Progress value.
     */
        update: function(progress) {
            if (Ext.isFunction(this.onProgress)) {
                progress = this.onProgress(progress);
            }
            this.deferred.update(progress);
        },
        /**
     * Transform and propagate the specified value using the
     * optional callback and propagate the transformed result.
     *
     * @param {Mixed} value Value to transform and/or propagate.
     * @param {Function} [callback] Callback to use to transform the value.
     * @param {Function} deferred Deferred to use to propagate the value, if no callback
     * was specified.
     * @param {Function} deferredMethod Deferred method to call to propagate the value,
     * if no callback was specified.
     *
     * @private
     */
        propagate: function(value, callback, deferred, deferredMethod) {
            if (Ext.isFunction(callback)) {
                this.schedule(function() {
                    try {
                        deferred.resolve(callback(value));
                    } catch (e) {
                        deferred.reject(e);
                    }
                });
            } else {
                deferredMethod.call(this.deferred, value);
            }
        },
        /**
     * Schedules the specified callback function to be executed on the next turn of the
     * event loop.
     *
     * @param {Function} callback Callback function.
     *
     * @private
     */
        schedule: function(callback) {
            var n = Consequence.queueSize++;
            Consequence.queue[n] = callback;
            if (!n) {
                // if (queue was empty)
                Ext.asap(Consequence.dispatch);
            }
        },
        statics: {
            /**
         * @property {Function[]} queue The queue of callbacks pending. This array is never
         * shrunk to reduce GC thrash but instead its elements will be set to `null`.
         *
         * @private
         */
            queue: new Array(10000),
            /**
         * @property {Number} queueSize The number of callbacks in the `queue`.
         *
         * @private
         */
            queueSize: 0,
            /**
         * This method drains the callback queue and calls each callback in order.
         *
         * @private
         */
            dispatch: function() {
                var queue = Consequence.queue,
                    fn, i;
                // The queue could grow on each call, so we cannot cache queueSize here.
                for (i = 0; i < Consequence.queueSize; ++i) {
                    fn = queue[i];
                    queue[i] = null;
                    // release our reference on the callback
                    fn();
                }
                Consequence.queueSize = 0;
            }
        }
    };
}, function(Consequence) {
    // eslint-disable-line comma-style
    Consequence.dispatch.$skipTimerCheck = true;
});

/*
 Ext.promise.Deferred adapted from:
 [DeftJS](https://github.com/deftjs/deftjs5)
 Copyright (c) 2012-2013 [DeftJS Framework Contributors](http://deftjs.org)
 Open source under the [MIT License](http://en.wikipedia.org/wiki/MIT_License).
 */
/**
 * Deferreds are the mechanism used to create new Promises. A Deferred has a single
 * associated Promise that can be safely returned to external consumers to ensure they do
 * not interfere with the resolution or rejection of the deferred operation.
 *
 * A Deferred is typically used within the body of a function that performs an asynchronous
 * operation. When that operation succeeds, the Deferred should be resolved; if that
 * operation fails, the Deferred should be rejected.
 *
 * Each Deferred has an associated Promise. A Promise delegates `then` calls to its
 * Deferred's `then` method. In this way, access to Deferred operations are divided between
 * producer (Deferred) and consumer (Promise) roles.
 *
 * When a Deferred's `resolve` method is called, it fulfills with the optionally specified
 * value. If `resolve` is called with a then-able (i.e.a Function or Object with a `then`
 * function, such as another Promise) it assimilates the then-able's result; the Deferred
 * provides its own `resolve` and `reject` methods as the onFulfilled or onRejected
 * arguments in a call to that then-able's `then` function. If an error is thrown while
 * calling the then-able's `then` function (prior to any call back to the specified
 * `resolve` or `reject` methods), the Deferred rejects with that error. If a Deferred's
 * `resolve` method is called with its own Promise, it rejects with a TypeError.
 *
 * When a Deferred's `reject` method is called, it rejects with the optionally specified
 * reason.
 *
 * Each time a Deferred's `then` method is called, it captures a pair of optional
 * onFulfilled and onRejected callbacks and returns a Promise of the Deferred's future
 * value as transformed by those callbacks.
 *
 * @private
 * @since 6.0.0
 */
Ext.define('Ext.promise.Deferred', {
    requires: [
        'Ext.promise.Consequence'
    ],
    /**
     * @property {Ext.promise.Promise} promise Promise of the future value of this Deferred.
     */
    promise: null,
    /**
     * @property {Ext.promise.Consequence[]} consequences Pending Consequences chained
     * to this Deferred.
     *
     * @private
     */
    consequences: [],
    /**
     * @property {Boolean} completed Indicates whether this Deferred has been completed.
     *
     * @private
     */
    completed: false,
    /**
     * @property {String} completeAction The completion action (i.e. 'fulfill' or 'reject').
     *
     * @private
     */
    completionAction: null,
    /**
     * @property {Mixed} completionValue The completion value (i.e. resolution value
     * or rejection error).
     *
     * @private
     */
    completionValue: null,
    constructor: function() {
        var me = this;
        me.promise = new Ext.promise.Promise(me);
        me.consequences = [];
        me.completed = false;
        me.completionAction = null;
        me.completionValue = null;
    },
    /**
     * Used to specify onFulfilled and onRejected callbacks that will be
     * notified when the future value becomes available.
     *
     * Those callbacks can subsequently transform the value that was
     * fulfilled or the error that was rejected. Each call to `then`
     * returns a new Promise of that transformed value; i.e., a Promise
     * that is fulfilled with the callback return value or rejected with
     * any error thrown by the callback.
     *
     * @param {Function} [onFulfilled] Callback to execute to transform a fulfillment value.
     * @param {Function} [onRejected] Callback to execute to transform a rejection reason.
     * @param {Function} [onProgress] Callback to execute to transform a progress value.
     *
     * @return Promise that is fulfilled with the callback return value or rejected with
     * any error thrown by the callback.
     */
    then: function(onFulfilled, onRejected, onProgress) {
        var me = this,
            consequence = new Ext.promise.Consequence(onFulfilled, onRejected, onProgress);
        if (me.completed) {
            consequence.trigger(me.completionAction, me.completionValue);
        } else {
            me.consequences.push(consequence);
        }
        return consequence.promise;
    },
    /**
     * Resolve this Deferred with the (optional) specified value.
     *
     * If called with a then-able (i.e.a Function or Object with a `then`
     * function, such as another Promise) it assimilates the then-able's
     * result; the Deferred provides its own `resolve` and `reject` methods
     * as the onFulfilled or onRejected arguments in a call to that
     * then-able's `then` function.  If an error is thrown while calling
     * the then-able's `then` function (prior to any call back to the
     * specified `resolve` or `reject` methods), the Deferred rejects with
     * that error. If a Deferred's `resolve` method is called with its own
     * Promise, it rejects with a TypeError.
     *
     * Once a Deferred has been fulfilled or rejected, it is considered to be complete
     * and subsequent calls to `resolve` or `reject` are ignored.
     *
     * @param {Mixed} value Value to resolve as either a fulfillment value or rejection
     * reason.
     */
    resolve: function(value) {
        var me = this,
            isHandled, thenFn;
        if (me.completed) {
            return;
        }
        try {
            if (value === me.promise) {
                throw new TypeError('A Promise cannot be resolved with itself.');
            }
            if (value != null && (typeof value === 'object' || Ext.isFunction(value)) && Ext.isFunction(thenFn = value.then)) {
                isHandled = false;
                try {
                    thenFn.call(value, function(value) {
                        if (!isHandled) {
                            isHandled = true;
                            me.resolve(value);
                        }
                    }, function(error) {
                        if (!isHandled) {
                            isHandled = true;
                            me.reject(error);
                        }
                    });
                } catch (e1) {
                    if (!isHandled) {
                        me.reject(e1);
                    }
                }
            } else {
                me.complete('fulfill', value);
            }
        } catch (e2) {
            me.reject(e2);
        }
    },
    /**
     * Reject this Deferred with the specified reason.
     *
     * Once a Deferred has been rejected, it is considered to be complete
     * and subsequent calls to `resolve` or `reject` are ignored.
     *
     * @param {Error} reason Rejection reason.
     */
    reject: function(reason) {
        if (this.completed) {
            return;
        }
        this.complete('reject', reason);
    },
    /**
     * Updates progress for this Deferred, if it is still pending, triggering it to
     * execute the `onProgress` callback and propagate the resulting transformed progress
     * value to Deferreds that originate from this Deferred.
     *
     * @param {Mixed} progress The progress value.
     */
    update: function(progress) {
        var consequences = this.consequences,
            consequence, i, len;
        if (this.completed) {
            return;
        }
        for (i = 0 , len = consequences.length; i < len; i++) {
            consequence = consequences[i];
            consequence.update(progress);
        }
    },
    /**
     * Complete this Deferred with the specified action and value.
     *
     * @param {String} action Completion action (i.e. 'fufill' or 'reject').
     * @param {Mixed} value Fulfillment value or rejection reason.
     *
     * @private
     */
    complete: function(action, value) {
        var me = this,
            consequences = me.consequences,
            consequence, i, len;
        me.completionAction = action;
        me.completionValue = value;
        me.completed = true;
        for (i = 0 , len = consequences.length; i < len; i++) {
            consequence = consequences[i];
            consequence.trigger(me.completionAction, me.completionValue);
        }
        me.consequences = null;
    }
});

/*
 Ext.promise.Deferred adapted from:
 [DeftJS](https://github.com/deftjs/deftjs5)
 Copyright (c) 2012-2014 [DeftJS Framework Contributors](http://deftjs.org)
 Open source under the [MIT License](http://en.wikipedia.org/wiki/MIT_License).
 */
/**
 * Promises represent a future value; i.e., a value that may not yet be available.
 *
 * Users should **not** create instances of this class directly. Instead user code should
 * use `new {@link Ext.Promise}()` or `new {@link Ext.Deferred}()` to create and manage
 * promises. If the browser supports the standard `Promise` constructor, this class will
 * not be used by `Ext.Promise`. This class will always be used by `Ext.Deferred` in order
 * to provide enhanced capabilities beyond standard promises.
 *
 * A Promise's `{@link #then then()}` method is used to specify onFulfilled and onRejected
 * callbacks that will be notified when the future value becomes available. Those callbacks
 * can subsequently transform the value that was resolved or the reason that was rejected.
 * Each call to `then` returns a new Promise of that transformed value; i.e., a Promise
 * that is resolved with the callback return value or rejected with any error thrown by
 * the callback.
 *
 * ## Basic Usage
 *
 *      this.companyService.loadCompanies().then(
 *          function(records) {
 *              // Do something with result.
 *          },
 *          function(error) {
 *              // Do something on failure.
 *          }).
 *      always(function() {
 *          // Do something whether call succeeded or failed
 *      });
 *
 * The above code uses the `Promise` returned from the `companyService.loadCompanies()`
 * method and uses `then()` to attach success and failure handlers. Finally, an `always()`
 * method call is chained onto the returned promise. This specifies a callback function
 * that will run whether the underlying call succeeded or failed.
 *
 * See `{@link Ext.Deferred}` for an example of using the returned Promise.
 *
 * [1]: http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts#april_14_2015_rev_38_final_draft
 *
 * @since 6.0.0
 */
Ext.define('Ext.promise.Promise', function(ExtPromise) {
    var Deferred;
    /* eslint-disable indent */
    return {
        requires: [
            'Ext.promise.Deferred'
        ],
        statics: {
            /**
         * @property CancellationError
         * @static
         * The type of `Error` propagated by the `{@link #method-cancel}` method. If
         * the browser provides a native `CancellationError` then that type is used. If
         * not, a basic `Error` type is used.
         */
            CancellationError: Ext.global.CancellationError || Error,
            _ready: function() {
                // Our requires are met, so we can cache Ext.promise.Deferred
                Deferred = Ext.promise.Deferred;
            },
            /**
         * Returns a new Promise that will only resolve once all the specified
         * `promisesOrValues` have resolved.
         *
         * The resolution value will be an Array containing the resolution value of each
         * of the `promisesOrValues`.
         *
         * The public API's to use instead of this method are `{@link Ext.Promise#all}`
         * and `{@link Ext.Deferred#all}`.
         *
         * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An
         * Array of values or Promises, or a Promise of an Array of values or Promises.
         * @return {Ext.promise.Promise} A Promise of an Array of the resolved values.
         *
         * @static
         * @private
         */
            all: function(promisesOrValues) {
                if (!(Ext.isArray(promisesOrValues) || ExtPromise.is(promisesOrValues))) {
                    Ext.raise('Invalid parameter: expected an Array or Promise of an Array.');
                }
                return ExtPromise.when(promisesOrValues).then(function(promisesOrValues) {
                    var deferred = new Deferred(),
                        remainingToResolve = promisesOrValues.length,
                        results = new Array(remainingToResolve),
                        index, promiseOrValue, resolve, i, len;
                    if (!remainingToResolve) {
                        deferred.resolve(results);
                    } else {
                        resolve = function(item, index) {
                            return ExtPromise.when(item).then(function(value) {
                                results[index] = value;
                                if (!--remainingToResolve) {
                                    deferred.resolve(results);
                                }
                                return value;
                            }, function(reason) {
                                return deferred.reject(reason);
                            });
                        };
                        for (index = i = 0 , len = promisesOrValues.length; i < len; index = ++i) {
                            promiseOrValue = promisesOrValues[index];
                            if (index in promisesOrValues) {
                                resolve(promiseOrValue, index);
                            } else {
                                remainingToResolve--;
                            }
                        }
                    }
                    return deferred.promise;
                });
            },
            /**
         * Determines whether the specified value is a Promise (including third-party
         * untrusted Promises or then()-ables), based on the Promises/A specification
         * feature test.
         *
         * @param {Mixed} value A potential Promise.
         * @return {Boolean} `true` if the given value is a Promise, otherwise `false`.
         * @static
         * @private
         */
            is: function(value) {
                return value != null && (typeof value === 'object' || Ext.isFunction(value)) && Ext.isFunction(value.then);
            },
            /**
         * Returns a promise that resolves or rejects as soon as one of the promises in the array
         * resolves or rejects, with the value or reason from that promise.
         * @param {Ext.promise.Promise[]} promises The promises.
         * @return {Ext.promise.Promise} The promise to be resolved when the race completes.
         *
         * @private
         * @static
         * @since 6.5.0
         */
            race: function(promises) {
                var deferred = new Deferred(),
                    len = promises.length,
                    i;
                if (!Ext.isArray(promises)) {
                    Ext.raise('Invalid parameter: expected an Array.');
                }
                for (i = 0; i < len; ++i) {
                    deferred.resolve(promises[i]);
                }
                return deferred.promise;
            },
            /**
         * Rethrows the specified Error on the next turn of the event loop.
         * @static
         * @private
         */
            rethrowError: function(error) {
                Ext.asap(function() {
                    throw error;
                });
            },
            /**
         * Returns a new Promise that either
         *
         *  * Resolves immediately for the specified value, or
         *  * Resolves or rejects when the specified promise (or third-party Promise or
         *    then()-able) is resolved or rejected.
         *
         * The public API's to use instead of this method are `{@link Ext.Promise#resolve}`
         * and `{@link Ext.Deferred#resolved}`.
         *
         * @param {Mixed} value A Promise (or third-party Promise or then()-able)
         * or value.
         * @return {Ext.Promise} A Promise of the specified Promise or value.
         *
         * @static
         * @private
         */
            when: function(value) {
                var deferred = new Deferred();
                deferred.resolve(value);
                return deferred.promise;
            }
        },
        /**
     * @property {Ext.promise.Deferred} Reference to this promise's
     * `{@link Ext.promise.Deferred Deferred}` instance.
     *
     * @readonly
     * @private
     */
        owner: null,
        /**
     * NOTE: {@link Ext.promise.Deferred Deferreds} are the mechanism used to create new
     * Promises.
     * @param {Ext.promise.Deferred} owner The owning `Deferred` instance.
     *
     * @private
     */
        constructor: function(owner) {
            this.owner = owner;
        },
        /**
     * Attaches onFulfilled and onRejected callbacks that will be notified when the future
     * value becomes available.
     *
     * Those callbacks can subsequently transform the value that was fulfilled or the error
     * that was rejected. Each call to `then` returns a new Promise of that transformed
     * value; i.e., a Promise that is fulfilled with the callback return value or rejected
     * with any error thrown by the callback.
     *
     * @param {Function} onFulfilled Optional callback to execute to transform a
     * fulfillment value.
     * @param {Function} onRejected Optional callback to execute to transform a rejection
     * reason.
     * @param {Function} onProgress Optional callback function to be called with progress
     * updates.
     * @param {Object} scope Optional scope for the callback(s).
     * @return {Ext.promise.Promise} Promise that is fulfilled with the callback return
     * value or rejected with any error thrown by the callback.
     */
        then: function(onFulfilled, onRejected, onProgress, scope) {
            var ref;
            if (arguments.length === 1 && Ext.isObject(arguments[0])) {
                ref = arguments[0];
                onFulfilled = ref.success;
                onRejected = ref.failure;
                onProgress = ref.progress;
                scope = ref.scope;
            }
            if (scope) {
                if (onFulfilled) {
                    onFulfilled = onFulfilled.bind(scope);
                }
                if (onRejected) {
                    onRejected = onRejected.bind(scope);
                }
                if (onProgress) {
                    onProgress = onProgress.bind(scope);
                }
            }
            return this.owner.then(onFulfilled, onRejected, onProgress);
        },
        /**
     * Attaches an onRejected callback that will be notified if this Promise is rejected.
     *
     * The callback can subsequently transform the reason that was rejected. Each call to
     * `otherwise` returns a new Promise of that transformed value; i.e., a Promise that
     * is resolved with the original resolved value, or resolved with the callback return
     * value or rejected with any error thrown by the callback.
     *
     * @param {Function} onRejected Callback to execute to transform a rejection reason.
     * @param {Object} scope Optional scope for the callback.
     * @return {Ext.promise.Promise} Promise of the transformed future value.
     *
     * @since 6.5.0
     */
        'catch': function(onRejected, scope) {
            var ref;
            if (arguments.length === 1 && Ext.isObject(arguments[0])) {
                ref = arguments[0];
                onRejected = ref.fn;
                scope = ref.scope;
            }
            if (scope != null) {
                onRejected = onRejected.bind(scope);
            }
            return this.owner.then(null, onRejected);
        },
        /**
     * An alias for the {@link #catch} method. To be used for browsers
     * where catch cannot be used as a method name.
     */
        otherwise: function(onRejected, scope) {
            return this['catch'].apply(this, arguments);
        },
        // eslint-disable-line dot-notation
        /**
     * Attaches an onCompleted callback that will be notified when this Promise is completed.
     *
     * Similar to `finally` in `try... catch... finally`.
     *
     * NOTE: The specified callback does not affect the resulting Promise's outcome; any
     * return value is ignored and any Error is rethrown.
     *
     * @param {Function} onCompleted Callback to execute when the Promise is resolved or
     * rejected.
     * @param {Object} scope Optional scope for the callback.
     * @return {Ext.promise.Promise} A new "pass-through" Promise that is resolved with
     * the original value or rejected with the original reason.
     */
        always: function(onCompleted, scope) {
            var ref;
            if (arguments.length === 1 && Ext.isObject(arguments[0])) {
                ref = arguments[0];
                onCompleted = ref.fn;
                scope = ref.scope;
            }
            if (scope != null) {
                onCompleted = onCompleted.bind(scope);
            }
            return this.owner.then(function(value) {
                try {
                    onCompleted();
                } catch (e) {
                    ExtPromise.rethrowError(e);
                }
                return value;
            }, function(reason) {
                try {
                    onCompleted();
                } catch (e) {
                    ExtPromise.rethrowError(e);
                }
                throw reason;
            });
        },
        /**
     * Terminates a Promise chain, ensuring that unhandled rejections will be rethrown as
     * Errors.
     *
     * One of the pitfalls of interacting with Promise-based APIs is the tendency for
     * important errors to be silently swallowed unless an explicit rejection handler is
     * specified.
     *
     * For example:
     *
     *      promise.then(function() {
     *          // logic in your callback throws an error and it is interpreted as a
     *          // rejection. throw new Error("Boom!");
     *      });
     *
     *      // The Error was not handled by the Promise chain and is silently swallowed.
     *
     * This problem can be addressed by terminating the Promise chain with the done()
     * method:
     *
     *      promise.then(function() {
     *          // logic in your callback throws an error and it is interpreted as a
     *          // rejection. throw new Error("Boom!");
     *      }).done();
     *
     *     // The Error was not handled by the Promise chain and is rethrown by done() on
     *     // the next tick.
     *
     * The `done()` method ensures that any unhandled rejections are rethrown as Errors.
     */
        done: function() {
            this.owner.then(null, ExtPromise.rethrowError);
        },
        /**
     * Cancels this Promise if it is still pending, triggering a rejection with a
     * `{@link #CancellationError}` that will propagate to any Promises originating from
     * this Promise.
     *
     * NOTE: Cancellation only propagates to Promises that branch from the target Promise.
     * It does not traverse back up to parent branches, as this would reject nodes from
     * which other Promises may have branched, causing unintended side-effects.
     *
     * @param {Error} reason Cancellation reason.
     */
        cancel: function(reason) {
            if (reason == null) {
                reason = null;
            }
            this.owner.reject(new this.self.CancellationError(reason));
        },
        /**
     * Logs the resolution or rejection of this Promise with the specified category and
     * optional identifier. Messages are logged via all registered custom logger functions.
     *
     * @param {String} identifier An optional identifier to incorporate into the
     * resulting log entry.
     *
     * @return {Ext.promise.Promise} A new "pass-through" Promise that is resolved with
     * the original value or rejected with the original reason.
     */
        log: function(identifier) {
            if (identifier == null) {
                identifier = '';
            }
            return this.owner.then(function(value) {
                Ext.log("" + (identifier || 'Promise') + " resolved with value: " + value);
                return value;
            }, function(reason) {
                Ext.log("" + (identifier || 'Promise') + " rejected with reason: " + reason);
                throw reason;
            });
        }
    };
}, function(ExtPromise) {
    ExtPromise._ready();
});

/**
 * This class provides an API compatible implementation of the ECMAScript 6 Promises API
 * (providing an implementation as necessary for browsers that do not natively support the
 * `Promise` class).
 *
 * This class will use the native `Promise` implementation if one is available. The
 * native implementation, while standard, does not provide all of the features of the
 * Ext JS Promises implementation.
 *
 * To use the Ext JS enhanced Promises implementation, see `{@link Ext.Deferred}` for
 * creating enhanced promises and additional static utility methods.
 *
 * Typical usage:
 *
 *      function getAjax (url) {
 *          // The function passed to Ext.Promise() is called immediately to start
 *          // the asynchronous action.
 *          //
 *          return new Ext.Promise(function (resolve, reject) {
 *              Ext.Ajax.request({
 *                  url: url,
 *
 *                  success: function (response) {
 *                      // Use the provided "resolve" method to deliver the result.
 *                      //
 *                      resolve(response.responseText);
 *                  },
 *
 *                  failure: function (response) {
 *                      // Use the provided "reject" method to deliver error message.
 *                      //
 *                      reject(response.status);
 *                  }
 *              });
 *          });
 *      }
 *
 *      getAjax('http://stuff').then(function (content) {
 *          // content is responseText of ajax response
 *      });
 *
 * To adapt the Ext JS `{@link Ext.data.Store store}` to use a Promise, you might do
 * something like this:
 *
 *      loadCompanies: function() {
 *          var companyStore = this.companyStore;
 *
 *          return new Ext.Promise(function (resolve, reject) {
 *              companyStore.load({
 *                  callback: function(records, operation, success) {
 *                      if (success) {
 *                          // Use the provided "resolve" method  to drive the promise:
 *                          resolve(records);
 *                      }
 *                      else {
 *                          // Use the provided "reject" method  to drive the promise:
 *                          reject("Error loading Companies.");
 *                      }
 *                  }
 *              });
 *          });
 *      }
 *
 * @since 6.0.0
 */
Ext.define('Ext.Promise', function() {
    /* eslint-disable indent */
    var Polyfiller;
    return {
        requires: [
            'Ext.promise.Promise'
        ],
        statics: {
            _ready: function() {
                // We can cache this now that our requires are met
                Polyfiller = Ext.promise.Promise;
            },
            /**
         * Returns a new Promise that will only resolve once all the specified
         * `promisesOrValues` have resolved.
         *
         * The resolution value will be an Array containing the resolution value of each
         * of the `promisesOrValues`.
         *
         * @param {Mixed[]/Ext.Promise[]/Ext.Promise} promisesOrValues An Array of values
         * or Promises, or a Promise of an Array of values or Promises.
         *
         * @return {Ext.Promise} A Promise of an Array of the resolved values.
         * @static
         */
            all: function() {
                return Polyfiller.all.apply(Polyfiller, arguments);
            },
            /**
         * Returns a promise that resolves or rejects as soon as one of the promises in the
         * array resolves or rejects, with the value or reason from that promise.
         * @param {Ext.promise.Promise[]} promises The promises.
         * @return {Ext.promise.Promise} The promise to be resolved when the race completes.
         *
         * @static
         * @since 6.5.0
         */
            race: function() {
                return Polyfiller.race.apply(Polyfiller, arguments);
            },
            /**
         * Convenience method that returns a new Promise rejected with the specified
         * reason.
         *
         * @param {Error} reason Rejection reason.
         * @return {Ext.Promise} The rejected Promise.
         * @static
         */
            reject: function(reason) {
                var deferred = new Ext.promise.Deferred();
                deferred.reject(reason);
                return deferred.promise;
            },
            /**
         * Returns a new Promise that either
         *
         *  * Resolves immediately for the specified value, or
         *  * Resolves or rejects when the specified promise (or third-party Promise or
         *    then()-able) is resolved or rejected.
         *
         * @param {Mixed} value A Promise (or third-party Promise or then()-able)
         * or value.
         * @return {Ext.Promise} A Promise of the specified Promise or value.
         * @static
         */
            resolve: function(value) {
                var deferred = new Ext.promise.Deferred();
                deferred.resolve(value);
                return deferred.promise;
            }
        },
        constructor: function(action) {
            var deferred = new Ext.promise.Deferred();
            action(deferred.resolve.bind(deferred), deferred.reject.bind(deferred));
            return deferred.promise;
        }
    };
}, function(ExtPromise) {
    var P = Ext.global.Promise;
    if (P && P.resolve && !Ext.useExtPromises) {
        Ext.Promise = P;
    } else {
        ExtPromise._ready();
    }
});

//
// Ext.Deferred adapted from:
// [DeftJS](https://github.com/deftjs/deftjs5)
// Copyright (c) 2012-2013 [DeftJS Framework Contributors](http://deftjs.org)
// Open source under the [MIT License](http://en.wikipedia.org/wiki/MIT_License).
//
// when(), all(), any(), some(), map(), reduce(), delay() and timeout()
// sequence(), parallel(), pipeline()
// methods adapted from: [when.js](https://github.com/cujojs/when)
// Copyright (c) B Cavalier & J Hann
// Open source under the [MIT License](http://en.wikipedia.org/wiki/MIT_License).
//
/**
 * Deferreds are the mechanism used to create new Promises. A Deferred has a single
 * associated Promise that can be safely returned to external consumers to ensure they do
 * not interfere with the resolution or rejection of the deferred operation.
 *
 * This implementation of Promises is an extension of the ECMAScript 6 Promises API as
 * detailed [here][1]. For a compatible, though less full featured, API see `{@link Ext.Promise}`.
 *
 * A Deferred is typically used within the body of a function that performs an asynchronous
 * operation. When that operation succeeds, the Deferred should be resolved; if that
 * operation fails, the Deferred should be rejected.
 *
 * Each Deferred has an associated Promise. A Promise delegates `then` calls to its
 * Deferred's `then` method. In this way, access to Deferred operations are divided between
 * producer (Deferred) and consumer (Promise) roles.
 *
 * ## Basic Usage
 *
 * In it's most common form, a method will create and return a Promise like this:
 *
 *      // A method in a service class which uses a Store and returns a Promise
 *      //
 *      loadCompanies: function () {
 *          var deferred = new Ext.Deferred(); // create the Ext.Deferred object
 *
 *          this.companyStore.load({
 *              callback: function (records, operation, success) {
 *                  if (success) {
 *                      // Use "deferred" to drive the promise:
 *                      deferred.resolve(records);
 *                  }
 *                  else {
 *                      // Use "deferred" to drive the promise:
 *                      deferred.reject("Error loading Companies.");
 *                  }
 *              }
 *          });
 *
 *          return deferred.promise;  // return the Promise to the caller
 *      }
 *
 * You can see this method first creates a `{@link Ext.Deferred Deferred}` object. It then
 * returns its `Promise` object for use by the caller. Finally, in the asynchronous
 * callback, it resolves the `deferred` object if the call was successful, and rejects the
 * `deferred` if the call failed.
 *
 * When a Deferred's `resolve` method is called, it fulfills with the optionally specified
 * value. If `resolve` is called with a then-able (i.e.a Function or Object with a `then`
 * function, such as another Promise) it assimilates the then-able's result; the Deferred
 * provides its own `resolve` and `reject` methods as the onFulfilled or onRejected
 * arguments in a call to that then-able's `then` function. If an error is thrown while
 * calling the then-able's `then` function (prior to any call back to the specified
 * `resolve` or `reject` methods), the Deferred rejects with that error. If a Deferred's
 * `resolve` method is called with its own Promise, it rejects with a TypeError.
 *
 * When a Deferred's `reject` method is called, it rejects with the optionally specified
 * reason.
 *
 * Each time a Deferred's `then` method is called, it captures a pair of optional
 * onFulfilled and onRejected callbacks and returns a Promise of the Deferred's future
 * value as transformed by those callbacks.
 *
 * See `{@link Ext.promise.Promise}` for an example of using the returned Promise.
 *
 * @since 6.0.0
 */
Ext.define('Ext.Deferred', function(Deferred) {
    /* eslint indent: "off" */
    var ExtPromise, rejected, resolved, when;
    // eslint-disable-line
    return {
        extend: 'Ext.promise.Deferred',
        requires: [
            'Ext.Promise'
        ],
        statics: {
            _ready: function() {
                // Our requires are met, so we can cache Ext.promise.Deferred
                ExtPromise = Ext.promise.Promise;
                when = Ext.Promise.resolve;
            },
            /**
         * Returns a new Promise that will only resolve once all the specified
         * `promisesOrValues` have resolved.
         *
         * The resolution value will be an Array containing the resolution value of each
         * of the `promisesOrValues`.
         *
         * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An
         * Array of values or Promises, or a Promise of an Array of values or Promises.
         * @return {Ext.promise.Promise} A Promise of an Array of the resolved values.
         * @static
         */
            all: function() {
                return ExtPromise.all.apply(ExtPromise, arguments);
            },
            /**
         * Initiates a competitive race, returning a new Promise that will resolve when
         * any one of the specified `promisesOrValues` have resolved, or will reject when
         * all `promisesOrValues` have rejected or cancelled.
         *
         * The resolution value will the first value of `promisesOrValues` to resolve.
         *
         * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An
         * Array of values or Promises, or a Promise of an Array of values or Promises.
         * @return {Ext.promise.Promise} A Promise of the first resolved value.
         * @static
         */
            any: function(promisesOrValues) {
                if (!(Ext.isArray(promisesOrValues) || ExtPromise.is(promisesOrValues))) {
                    Ext.raise('Invalid parameter: expected an Array or Promise of an Array.');
                }
                return Deferred.some(promisesOrValues, 1).then(function(array) {
                    return array[0];
                }, function(error) {
                    if (error instanceof Error && error.message === 'Too few Promises were resolved.') {
                        Ext.raise('No Promises were resolved.');
                    } else {
                        throw error;
                    }
                });
            },
            /**
         * Returns a new Promise that will automatically resolve with the specified
         * Promise or value after the specified delay (in milliseconds).
         *
         * @param {Mixed} promiseOrValue A Promise or value.
         * @param {Number} milliseconds A delay duration (in milliseconds).
         * @return {Ext.promise.Promise} A Promise of the specified Promise or value that
         * will resolve after the specified delay.
         * @static
         */
            delay: function(promiseOrValue, milliseconds) {
                var deferred;
                if (arguments.length === 1) {
                    milliseconds = promiseOrValue;
                    promiseOrValue = undefined;
                }
                milliseconds = Math.max(milliseconds, 1);
                deferred = new Deferred();
                deferred.timeoutId = Ext.defer(function() {
                    delete deferred.timeoutId;
                    deferred.resolve(promiseOrValue);
                }, milliseconds);
                return deferred.promise;
            },
            /**
         * Get a shared cached rejected promise. Assumes Promises
         * have been required.
         * @return {Ext.Promise}
         *
         * @private
         * @since 6.5.0
         */
            getCachedRejected: function() {
                if (!rejected) {
                    // Prevent Cmd from requiring
                    rejected = Ext.Promise.reject();
                }
                return rejected;
            },
            /**
         * Get a shared cached resolved promise. Assumes Promises
         * have been required.
         * @return {Ext.Promise}
         *
         * @private
         * @since 6.5.0
         */
            getCachedResolved: function() {
                if (!resolved) {
                    // Prevent Cmd from requiring
                    resolved = Ext.Promise.resolve();
                }
                return resolved;
            },
            /**
         * Traditional map function, similar to `Array.prototype.map()`, that allows
         * input to contain promises and/or values.
         *
         * The specified map function may return either a value or a promise.
         *
         * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An
         * Array of values or Promises, or a Promise of an Array of values or Promises.
         * @param {Function} mapFn A Function to call to transform each resolved value in
         * the Array.
         * @return {Ext.promise.Promise} A Promise of an Array of the mapped resolved
         * values.
         * @static
         */
            map: function(promisesOrValues, mapFn) {
                if (!(Ext.isArray(promisesOrValues) || ExtPromise.is(promisesOrValues))) {
                    Ext.raise('Invalid parameter: expected an Array or Promise of an Array.');
                }
                if (!Ext.isFunction(mapFn)) {
                    Ext.raise('Invalid parameter: expected a function.');
                }
                return Deferred.resolved(promisesOrValues).then(function(promisesOrValues) {
                    var deferred, index, promiseOrValue, remainingToResolve, resolve, results, i, len;
                    remainingToResolve = promisesOrValues.length;
                    results = new Array(promisesOrValues.length);
                    deferred = new Deferred();
                    if (!remainingToResolve) {
                        deferred.resolve(results);
                    } else {
                        resolve = function(item, index) {
                            return Deferred.resolved(item).then(function(value) {
                                return mapFn(value, index, results);
                            }).then(function(value) {
                                results[index] = value;
                                if (!--remainingToResolve) {
                                    deferred.resolve(results);
                                }
                                return value;
                            }, function(reason) {
                                return deferred.reject(reason);
                            });
                        };
                        for (index = i = 0 , len = promisesOrValues.length; i < len; index = ++i) {
                            promiseOrValue = promisesOrValues[index];
                            if (index in promisesOrValues) {
                                resolve(promiseOrValue, index);
                            } else {
                                remainingToResolve--;
                            }
                        }
                    }
                    return deferred.promise;
                });
            },
            /**
         * Returns a new function that wraps the specified function and caches the
         * results for previously processed inputs.
         *
         * Similar to {@link Ext.Function#memoize Ext.Function.memoize()}, except it
         * allows for parameters that are Promises and/or values.
         *
         * @param {Function} fn A Function to wrap.
         * @param {Object} scope An optional scope in which to execute the wrapped function.
         * @param {Function} hashFn An optional function used to compute a hash key for
         * storing the result, based on the arguments to the original function.
         * @return {Function} The new wrapper function.
         * @static
         */
            memoize: function(fn, scope, hashFn) {
                var memoizedFn = Ext.Function.memoize(fn, scope, hashFn);
                return function() {
                    return Deferred.all(Ext.Array.slice(arguments)).then(function(values) {
                        return memoizedFn.apply(scope, values);
                    });
                };
            },
            /**
         * Execute an Array (or {@link Ext.promise.Promise Promise} of an Array) of
         * functions in parallel.
         *
         * The specified functions may optionally return their results as
         * {@link Ext.promise.Promise Promises}.
         *
         * @param {Function[]/Ext.promise.Promise} fns The Array (or Promise of an Array)
         * of functions to execute.
         * @param {Object} scope Optional scope in which to execute the specified functions.
         * @return {Ext.promise.Promise} Promise of an Array of results for each function
         * call (in the same order).
         * @static
         */
            parallel: function(fns, scope) {
                var args;
                if (scope == null) {
                    scope = null;
                }
                args = Ext.Array.slice(arguments, 2);
                return Deferred.map(fns, function(fn) {
                    if (!Ext.isFunction(fn)) {
                        throw new Error('Invalid parameter: expected a function.');
                    }
                    return fn.apply(scope, args);
                });
            },
            /**
         * Execute an Array (or {@link Ext.promise.Promise Promise} of an Array) of
         * functions as a pipeline, where each function's result is passed to the
         * subsequent function as input.
         *
         * The specified functions may optionally return their results as
         * {@link Ext.promise.Promise Promises}.
         *
         * @param {Function[]/Ext.promise.Promise} fns The Array (or Promise of an Array)
         * of functions to execute.
         * @param {Object} initialValue Initial value to be passed to the first function
         * in the pipeline.
         * @param {Object} scope Optional scope in which to execute the specified functions.
         * @return {Ext.promise.Promise} Promise of the result value for the final
         * function in the pipeline.
         * @static
         */
            pipeline: function(fns, initialValue, scope) {
                if (scope == null) {
                    scope = null;
                }
                return Deferred.reduce(fns, function(value, fn) {
                    if (!Ext.isFunction(fn)) {
                        throw new Error('Invalid parameter: expected a function.');
                    }
                    return fn.call(scope, value);
                }, initialValue);
            },
            /**
         * Returns a promise that resolves or rejects as soon as one of the promises
         * in the array resolves or rejects, with the value or reason from that promise.
         * @param {Ext.promise.Promise[]} promises The promises.
         * @return {Ext.promise.Promise} The promise to be resolved when the race completes.
         *
         * @static
         * @since 6.5.0
         */
            race: function() {
                return ExtPromise.race.apply(ExtPromise, arguments);
            },
            /**
         * Traditional reduce function, similar to `Array.reduce()`, that allows input to
         * contain promises and/or values.
         *
         * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} values An
         * Array of values or Promises, or a Promise of an Array of values or Promises.
         * @param {Function} reduceFn A Function to call to transform each successive
         * item in the Array into the final reduced value.
         * @param {Mixed} initialValue An initial Promise or value.
         * @return {Ext.promise.Promise} A Promise of the reduced value.
         * @static
         */
            reduce: function(values, reduceFn, initialValue) {
                var initialValueSpecified;
                if (!(Ext.isArray(values) || ExtPromise.is(values))) {
                    Ext.raise('Invalid parameter: expected an Array or Promise of an Array.');
                }
                if (!Ext.isFunction(reduceFn)) {
                    Ext.raise('Invalid parameter: expected a function.');
                }
                initialValueSpecified = arguments.length === 3;
                return Deferred.resolved(values).then(function(promisesOrValues) {
                    var reduceArguments = [
                            promisesOrValues,
                            function(previousValueOrPromise, currentValueOrPromise, currentIndex) {
                                return Deferred.resolved(previousValueOrPromise).then(function(previousValue) {
                                    return Deferred.resolved(currentValueOrPromise).then(function(currentValue) {
                                        return reduceFn(previousValue, currentValue, currentIndex, promisesOrValues);
                                    });
                                });
                            }
                        ];
                    if (initialValueSpecified) {
                        reduceArguments.push(initialValue);
                    }
                    return Ext.Array.reduce.apply(Ext.Array, reduceArguments);
                });
            },
            /**
         * Convenience method that returns a new Promise rejected with the specified
         * reason.
         *
         * @param {Error} reason Rejection reason.
         * @return {Ext.promise.Promise} The rejected Promise.
         * @static
         */
            rejected: function(reason) {
                var deferred = new Ext.Deferred();
                deferred.reject(reason);
                return deferred.promise;
            },
            /**
         * Returns a new Promise that either
         *
         *  * Resolves immediately for the specified value, or
         *  * Resolves or rejects when the specified promise (or third-party Promise or
         *    then()-able) is resolved or rejected.
         *
         * @param {Mixed} promiseOrValue A Promise (or third-party Promise or then()-able)
         * or value.
         * @return {Ext.promise.Promise} A Promise of the specified Promise or value.
         * @static
         */
            resolved: function(promiseOrValue) {
                var deferred = new Ext.Deferred();
                deferred.resolve(promiseOrValue);
                return deferred.promise;
            },
            /**
         * Execute an Array (or {@link Ext.promise.Promise Promise} of an Array) of
         * functions sequentially.
         *
         * The specified functions may optionally return their results as {@link
         * Ext.promise.Promise Promises}.
         *
         * @param {Function[]/Ext.promise.Promise} fns The Array (or Promise of an Array)
         * of functions to execute.
         * @param {Object} scope Optional scope in which to execute the specified functions.
         * @return {Ext.promise.Promise} Promise of an Array of results for each function
         * call (in the same order).
         * @static
         */
            sequence: function(fns, scope) {
                var args;
                if (scope == null) {
                    scope = null;
                }
                args = Ext.Array.slice(arguments, 2);
                return Deferred.reduce(fns, function(results, fn) {
                    if (!Ext.isFunction(fn)) {
                        throw new Error('Invalid parameter: expected a function.');
                    }
                    return Deferred.resolved(fn.apply(scope, args)).then(function(result) {
                        results.push(result);
                        return results;
                    });
                }, []);
            },
            /**
         * Initiates a competitive race, returning a new Promise that will resolve when
         * `howMany` of the specified `promisesOrValues` have resolved, or will reject
         * when it becomes impossible for `howMany` to resolve.
         *
         * The resolution value will be an Array of the first `howMany` values of
         * `promisesOrValues` to resolve.
         *
         * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An
         * Array of values or Promises, or a Promise of an Array of values or Promises.
         * @param {Number} howMany The expected number of resolved values.
         * @return {Ext.promise.Promise} A Promise of the expected number of resolved
         * values.
         * @static
         */
            some: function(promisesOrValues, howMany) {
                if (!(Ext.isArray(promisesOrValues) || ExtPromise.is(promisesOrValues))) {
                    Ext.raise('Invalid parameter: expected an Array or Promise of an Array.');
                }
                if (!Ext.isNumeric(howMany) || howMany <= 0) {
                    Ext.raise('Invalid parameter: expected a positive integer.');
                }
                return Deferred.resolved(promisesOrValues).then(function(promisesOrValues) {
                    var deferred, index, onReject, onResolve, promiseOrValue, remainingToReject, remainingToResolve, values, i, len;
                    values = [];
                    remainingToResolve = howMany;
                    remainingToReject = (promisesOrValues.length - remainingToResolve) + 1;
                    deferred = new Deferred();
                    if (promisesOrValues.length < howMany) {
                        deferred.reject(new Error('Too few Promises were resolved.'));
                    } else {
                        onResolve = function(value) {
                            if (remainingToResolve > 0) {
                                values.push(value);
                            }
                            remainingToResolve--;
                            if (remainingToResolve === 0) {
                                deferred.resolve(values);
                            }
                            return value;
                        };
                        onReject = function(reason) {
                            remainingToReject--;
                            if (remainingToReject === 0) {
                                deferred.reject(new Error('Too few Promises were resolved.'));
                            }
                            return reason;
                        };
                        for (index = i = 0 , len = promisesOrValues.length; i < len; index = ++i) {
                            promiseOrValue = promisesOrValues[index];
                            if (index in promisesOrValues) {
                                Deferred.resolved(promiseOrValue).then(onResolve, onReject);
                            }
                        }
                    }
                    return deferred.promise;
                });
            },
            /**
         * Returns a new Promise that will automatically reject after the specified
         * timeout (in milliseconds) if the specified promise has not resolved or
         * rejected.
         *
         * @param {Mixed} promiseOrValue A Promise or value.
         * @param {Number} milliseconds A timeout duration (in milliseconds).
         * @return {Ext.promise.Promise} A Promise of the specified Promise or value that
         * enforces the specified timeout.
         * @static
         */
            timeout: function(promiseOrValue, milliseconds) {
                var deferred = new Deferred(),
                    timeoutId;
                timeoutId = Ext.defer(function() {
                    if (timeoutId) {
                        deferred.reject(new Error('Promise timed out.'));
                    }
                }, milliseconds);
                Deferred.resolved(promiseOrValue).then(function(value) {
                    Ext.undefer(timeoutId);
                    timeoutId = null;
                    deferred.resolve(value);
                }, function(reason) {
                    Ext.undefer(timeoutId);
                    timeoutId = null;
                    deferred.reject(reason);
                });
                return deferred.promise;
            }
        }
    };
}, function(Deferred) {
    Deferred._ready();
});

// @define Ext.Factory
/**
 * @class Ext.Factory
 * Manages factories for families of classes (classes with a common `alias` prefix). The
 * factory for a class family is a function stored as a `static` on `Ext.Factory`. These
 * are created either by directly calling `Ext.Factory.define` or by using the
 * `Ext.mixin.Factoryable` interface.
 *
 * To illustrate, consider the layout system's use of aliases. The `hbox` layout maps to
 * the `"layout.hbox"` alias that one typically provides via the `layout` config on a
 * Container.
 *
 * Under the covers this maps to a call like this:
 *
 *      Ext.Factory.layout('hbox');
 *
 * Or possibly:
 *
 *      Ext.Factory.layout({
 *          type: 'hbox'
 *      });
 *
 * The value of the `layout` config is passed to the `Ext.Factory.layout` function. The
 * exact signature of a factory method matches `{@link Ext.Factory#method!create}`.
 *
 * To define this factory directly, one could call `Ext.Factory.define` like so:
 *
 *      Ext.Factory.define('layout', 'auto');  // "layout.auto" is the default type
 *
 * @since 5.0.0
 */
Ext.Factory = function(type) {
    var me = this;
    me.aliasPrefix = type + '.';
    me.cache = {};
    me.name = type.replace(me.fixNameRe, me.fixNameFn);
    me.type = type;
    /**
     * @cfg {String} [creator]
     * The name of the method used to prepare config objects for creation. This defaults
     * to `'create'` plus the capitalized name (e.g., `'createLayout'` for the 'laoyut'
     * alias family).
     */
    me.creator = 'create' + Ext.String.capitalize(me.name);
};
Ext.Factory.prototype = {
    /**
     * @cfg {String} [aliasPrefix]
     * The prefix to apply to `type` values to form a complete alias. This defaults to the
     * proper value in most all cases and should not need to be specified.
     *
     * @since 5.0.0
     */
    /**
     * @cfg {String} [defaultProperty="type"]
     * The config property to set when the factory is given a config that is a string.
     *
     * @since 5.0.0
     */
    defaultProperty: 'type',
    /**
     * @cfg {String} [defaultType=null]
     * An optional type to use if none is given to the factory at invocation. This is a
     * suffix added to the `aliasPrefix`. For example, if `aliasPrefix="layout."` and
     * `defaultType="hbox"` the default alias is `"layout.hbox"`. This is an alternative
     * to `xclass` so only one should be provided.
     *
     * @since 5.0.0
     */
    /**
     * @cfg {String} [instanceProp="isInstance"]
     * The property that identifies an object as instance vs a config.
     *
     * @since 5.0.0
     */
    instanceProp: 'isInstance',
    /**
     * @cfg {String} [xclass=null]
     * The full classname of the type of instance to create when none is provided to the
     * factory. This is an alternative to `defaultType` so only one should be specified.
     *
     * @since 5.0.0
     */
    /**
     * @property {Ext.Class} [defaultClass=null]
     * The Class reference of the type of instance to create when none is provided to the
     * factory. This property is set from `xclass` when the factory instance is created.
     * @private
     * @readonly
     *
     * @since 5.0.0
     */
    /**
     * @cfg {String} [typeProperty="type"]
     * The property from which to read the type alias suffix.
     * @since 6.5.0
     */
    typeProperty: 'type',
    /**
     * Creates an instance of this class family given configuration options.
     *
     * @param {Object/String} [config] The configuration or instance (if an Object) or
     * just the type (if a String) describing the instance to create.
     * @param {String} [config.xclass] The full class name of the class to create.
     * @param {String} [config.type] The type string to add to the alias prefix for this
     * factory.
     * @param {String/Object} [defaultType] The type to create if no type is contained in the
     * `config`, or an object containing a default set of configs.
     * @return {Object} The newly created instance.
     *
     * @since 5.0.0
     */
    create: function(config, defaultType) {
        var me = this,
            Manager = Ext.ClassManager,
            cache = me.cache,
            typeProperty = me.typeProperty,
            alias, className, klass, suffix;
        if (config) {
            if (config[me.instanceProp]) {
                return config;
            }
            if (typeof config === 'string') {
                suffix = config;
                config = {};
                config[me.defaultProperty] = suffix;
            }
            className = config.xclass;
            suffix = config[typeProperty];
        }
        if (defaultType && defaultType.constructor === Object) {
            config = Ext.apply({}, config, defaultType);
            defaultType = defaultType[typeProperty];
        }
        if (className) {
            if (!(klass = Manager.get(className))) {
                return Manager.instantiate(className, config);
            }
        } else {
            if (!(suffix = suffix || defaultType || me.defaultType)) {
                klass = me.defaultClass;
            }
            if (!suffix && !klass) {
                Ext.raise('No type specified for ' + me.type + '.create');
            }
            if (!klass && !(klass = cache[suffix])) {
                alias = me.aliasPrefix + suffix;
                className = Manager.getNameByAlias(alias);
                // this is needed to support demand loading of the class
                if (!(klass = className && Manager.get(className))) {
                    return Manager.instantiateByAlias(alias, config);
                }
                cache[suffix] = klass;
            }
        }
        return klass.isInstance ? klass : new klass(config);
    },
    fixNameRe: /\.[a-z]/ig,
    fixNameFn: function(match) {
        return match.substring(1).toUpperCase();
    },
    clearCache: function() {
        this.cache = {};
        this.instanceCache = {};
    },
    /**
     * Sets a hook on the creation process. If the hook `fn` returns `undefined` then
     * the original `create` method is called.
     *
     * @param {Function} fn The hook function to call when `create` is invoked.
     * @param {Function} fn.original The original `create` method.
     * @param {String/Object} fn.config See {@link #method!create create}.
     * @param {String/Object} fn.defaultType See {@link #method!create create}.
     * @private
     * @since 6.5.0
     */
    hook: function(fn) {
        var me = this,
            original = me.create;
        me.create = function(config, defaultType) {
            var ret = fn.call(me, original, config, defaultType);
            if (ret === undefined) {
                ret = original.call(me, config, defaultType);
            }
            return ret;
        };
    },
    /**
     * This method accepts a `config` object and an existing `instance` if one exists
     * (can be `null`).
     *
     * The details are best explained by example:
     *
     *      config: {
     *          header: {
     *              xtype: 'itemheader'
     *          }
     *      },
     *
     *      applyHeader: function (header, oldHeader) {
     *          return Ext.Factory.widget.update(oldHeader, header,
     *              this, 'createHeader');
     *      },
     *
     *      createHeader: function (header) {
     *          return Ext.apply({
     *              xtype: 'itemheader',
     *              ownerCmp: this
     *          }, header);
     *      }
     *
     * Normally the `applyHeader` method would have to coordinate potential reuse of
     * the `oldHeader` and perhaps call `setConfig` on it with the new `header` config
     * options. If there was no `oldHeader`, of course, a new instance must be created
     * instead. These details are handled by this method. If the `oldHeader` is not
     * reused, it will be {@link Ext.Base#method!destroy destroyed}.
     *
     * For derived class flexibility, the pattern of calling out to a "creator" method
     * that only returns the config object has become widely used in many components.
     * This pattern is also covered in this method. The goal is to allow the derived
     * class to `callParent` and yet not end up with an instantiated component (since
     * the type may not yet be known).
     *
     * This mechanism should be used in favor of `Ext.factory()`.
     *
     * @param {Ext.Base} instance
     * @param {Object/String} config The configuration (see {@link #method!create}).
     * @param {Object} [creator] If passed, this object must provide the `creator`
     * method or the `creatorMethod` parameter.
     * @param {String} [creatorMethod] The name of a creation wrapper method on the
     * given `creator` instance that "upgrades" the raw `config` object into a final
     * form for creation.
     * @param {String} [defaultsConfig] The name of a config property (on the provided
     * `creator` instance) that contains defaults to be used to create instances. These
     * defaults are present in the config object passed to the `creatorMethod`.
     * @return {Object} The reconfigured `instance` or a newly created one.
     * @since 6.5.0
     */
    update: function(instance, config, creator, creatorMethod, defaultsConfig) {
        var me = this,
            aliases, defaults, reuse, type;
        // If config is falsy or a valid instance, destroy the current instance
        // (if it exists) and replace with the new one
        if (!config || config.isInstance) {
            if (config && !config[me.instanceProp]) {
                Ext.raise('Config instance failed ' + me.instanceProp + ' requirement');
            }
            if (instance && instance !== config) {
                instance.destroy();
            }
            return config;
        }
        if (typeof config === 'string') {
            type = config;
            config = {};
            config[me.defaultProperty] = type;
        }
        // See if the existing instance can just be reconfigured:
        if (instance) {
            if (config === true) {
                return instance;
            }
            if (!(type = config.xclass)) {
                if (!(type = config.xtype)) {
                    type = config[me.typeProperty];
                    if (type) {
                        // instance must have the right alias...
                        type = me.aliasPrefix + type;
                        aliases = instance.self.prototype;
                        // The alias for the class is on the prototype (derived
                        // classes do not really own their inherited aliases since
                        // they won't be created when using them):
                        if (aliases.hasOwnProperty('alias')) {
                            aliases = aliases.alias;
                            if (aliases) {
                                reuse = aliases === type || aliases.indexOf(type) > -1;
                            }
                        }
                    }
                } else {
                    // config = { xtype: ... }
                    reuse = instance.isXType(type, /* shallow= */
                    true);
                }
            } else {
                // config = { xclass: ... } so we're good if they match
                reuse = instance.$className === type;
            }
            if (reuse) {
                instance.setConfig(config);
                return instance;
            }
            instance.destroy();
        }
        if (config === true) {
            config = {};
        }
        if (creator) {
            if (defaultsConfig) {
                defaults = Ext.Config.map[defaultsConfig];
                defaults = creator[defaults.names.get]();
                if (defaults) {
                    config = Ext.merge(Ext.clone(defaults), config);
                }
            }
            creatorMethod = creatorMethod || me.creator;
            if (creator[creatorMethod]) {
                config = creator[creatorMethod](config);
                if (!config) {
                    Ext.raise('Missing return value from ' + creatorMethod + ' on class ' + creator.$className);
                }
            }
        }
        return me.create(config);
    }
};
/**
 * For example, the layout alias family could be defined like this:
 *
 *      Ext.Factory.define('layout', {
 *          defaultType: 'auto'
 *      });
 *
 * To define multiple families at once:
 *
 *      Ext.Factory.define({
 *          layout: {
 *              defaultType: 'auto'
 *          }
 *      });
 *
 * @param {String} type The alias family (e.g., "layout").
 * @param {Object/String} [config] An object specifying the config for the `Ext.Factory`
 * to be created. If a string is passed it is treated as the `defaultType`.
 * @return {Function}
 * @static
 * @since 5.0.0
 */
Ext.Factory.define = function(type, config) {
    var Factory = Ext.Factory,
        cacheable = config && config.cacheable,
        defaultClass, factory, fn;
    if (type.constructor === Object) {
        Ext.Object.each(type, Factory.define, Factory);
    } else {
        factory = new Ext.Factory(type);
        if (config) {
            if (config.constructor === Object) {
                Ext.apply(factory, config);
                if (typeof (defaultClass = factory.xclass) === 'string') {
                    factory.defaultClass = Ext.ClassManager.get(defaultClass);
                }
            } else {
                factory.defaultType = config;
            }
        }
        /*
         *  layout = Ext.Factory.layout('hbox');
         */
        Factory[factory.name] = fn = function(config, defaultType) {
            // maintain indirection through "create" name on instance to allow
            // the hook() mechanism to replace it.
            return factory.create(config, defaultType);
        };
        if (cacheable) {
            factory.instanceCache = {};
            factory.hook(function(original, config, defaultType) {
                var cache = this.instanceCache,
                    v;
                if (typeof config === 'string' && !(v = cache[config])) {
                    v = original.call(this, config, defaultType);
                    // Validator may have cacheable:false to force new instances each time,
                    // avoiding the cache
                    if (v.cacheable !== false) {
                        cache[config] = v;
                        // this should catch some improper modifications to the shared
                        // cached instance, during development but not in production.
                        Ext.Object.freeze(v);
                    }
                }
                return v;
            });
        }
        fn.instance = factory;
        /*
         * Typically called by an applier:
         *
         *      applyLayout: function (layout, oldLayout) {
         *          return Ext.Factory.layout.update(oldLayout, layout, this);
         *      },
         *
         *      createLayout: function (config) {
         *          return Ext.apply({
         *              //.. stuff
         *          }, config);
         *      }
         */
        fn.update = function(instance, config, creator, creatorMethod, defaultsConfig) {
            return factory.update(instance, config, creator, creatorMethod, defaultsConfig);
        };
    }
    return fn;
};
Ext.Factory.clearCaches = function() {
    var Factory = Ext.Factory,
        key, item;
    for (key in Factory) {
        item = Factory[key];
        item = item.instance;
        if (item) {
            item.clearCache();
        }
    }
};
Ext.Factory.on = function(name, fn) {
    Ext.Factory[name].instance.hook(fn);
};
/**
 * This mixin automates use of `Ext.Factory`. When mixed in to a class, the `alias` of the
 * class is retrieved and combined with an optional `factoryConfig` property on that class
 * to produce the configuration to pass to `Ext.Factory`.
 *
 * The factory method created by `Ext.Factory` is also added as a static method to the
 * target class.
 *
 * Given a class declared like so:
 *
 *      Ext.define('App.bar.Thing', {
 *          mixins: [
 *              'Ext.mixin.Factoryable'
 *          ],
 *
 *          alias: 'bar.thing',  // this is detected by Factoryable
 *
 *          factoryConfig: {
 *              defaultType: 'thing',  // this is the default deduced from the alias
 *              // other configs
 *          },
 *
 *          ...
 *      });
 *
 * The produced factory function can be used to create instances using the following
 * forms:
 *
 *      var obj;
 *
 *      obj = App.bar.Thing.create('thing'); // same as "new App.bar.Thing()"
 *
 *      obj = App.bar.Thing.create({
 *          type: 'thing'       // same as above
 *      });
 *
 *      obj = App.bar.Thing.create({
 *          xclass: 'App.bar.Thing'  // same as above
 *      });
 *
 *      var obj2 = App.bar.Thing.create(obj);
 *      // obj === obj2  (passing an instance returns the instance)
 *
 * Alternatively the produced factory is available as a static method of `Ext.Factory`.
 *
 * @since 5.0.0
 */
Ext.define('Ext.mixin.Factoryable', {
    mixinId: 'factoryable',
    onClassMixedIn: function(targetClass) {
        var proto = targetClass.prototype,
            factoryConfig = proto.factoryConfig,
            alias = proto.alias,
            config = {},
            dot, createFn;
        alias = alias && alias.length && alias[0];
        if (alias && (dot = alias.lastIndexOf('.')) > 0) {
            config.type = alias.substring(0, dot);
            config.defaultType = alias.substring(dot + 1);
        }
        if (factoryConfig) {
            delete proto.factoryConfig;
            Ext.apply(config, factoryConfig);
        }
        createFn = Ext.Factory.define(config.type, config);
        if (targetClass.create === Ext.Base.create) {
            // allow targetClass to override the create method
            targetClass.create = createFn;
        }
    }
});
/**
     * @property {Object} [factoryConfig]
     * If this property is specified by the target class of this mixin its properties are
     * used to configure the created `Ext.Factory`.
     */

/**
 * This class manages a pending Ajax request. Instances of this type are created by the
 * `{@link Ext.data.Connection#request}` method.
 * @since 6.0.0
 */
Ext.define('Ext.data.request.Base', {
    requires: [
        'Ext.Deferred'
    ],
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    // Since this class is abstract, we don't have an alias of our own for Factoryable
    // to use.
    factoryConfig: {
        type: 'request',
        defaultType: 'ajax'
    },
    // this is the default deduced from the alias
    result: null,
    success: null,
    timer: null,
    constructor: function(config) {
        var me = this;
        // ownerConfig contains default values for config options
        // applicable to every Request spawned by that owner;
        // however the values can be overridden in the options
        // object passed to owner's request() method.
        Ext.apply(me, config.options || {}, config.ownerConfig);
        me.id = ++Ext.data.Connection.requestId;
        me.owner = config.owner;
        me.options = config.options;
        me.requestOptions = config.requestOptions;
    },
    /**
     * Start the request.
     */
    start: function() {
        var me = this,
            timeout = me.getTimeout();
        if (timeout && me.async) {
            me.timer = Ext.defer(me.onTimeout, timeout, me);
        }
    },
    abort: function() {
        var me = this;
        me.clearTimer();
        if (!me.timedout) {
            me.aborted = true;
        }
        me.abort = Ext.emptyFn;
    },
    createDeferred: function() {
        var me = this,
            result = me.result,
            d = new Ext.Deferred();
        if (me.completed) {
            if (me.success) {
                d.resolve(result);
            } else {
                d.reject(result);
            }
        }
        me.deferred = d;
        return d;
    },
    getDeferred: function() {
        return this.deferred || this.createDeferred();
    },
    getPromise: function() {
        return this.getDeferred().promise;
    },
    /**
     * @method then
     * Returns a new promise resolving to the value of the called method.
     * @param {Function} success Called when the Promise is fulfilled.
     * @param {Function} failure Called when the Promise is rejected.
     * @returns {Ext.promise.Promise}
     */
    then: function() {
        var promise = this.getPromise();
        return promise.then.apply(promise, arguments);
    },
    /**
     * @method isLoading
     * Determines whether this request is in progress.
     *
     * @return {Boolean} `true` if this request is in progress, `false` if complete.
     */
    onComplete: function() {
        var me = this,
            deferred = me.deferred,
            result = me.result;
        me.clearTimer();
        if (deferred) {
            if (me.success) {
                deferred.resolve(result);
            } else {
                deferred.reject(result);
            }
        }
        me.completed = true;
    },
    onTimeout: function() {
        var me = this;
        me.timedout = true;
        me.timer = null;
        me.abort(true);
    },
    getTimeout: function() {
        return this.timeout;
    },
    clearTimer: function() {
        this.timer = Ext.undefer(this.timer);
    },
    destroy: function() {
        var me = this;
        me.abort();
        me.owner = me.options = me.requestOptions = me.result = null;
        me.callParent();
    },
    privates: {
        /**
         * Creates the exception object
         * @param {Object} request
         * @private
         */
        createException: function() {
            var me = this,
                result;
            result = {
                request: me,
                requestId: me.id,
                status: me.aborted ? -1 : 0,
                statusText: me.aborted ? 'transaction aborted' : 'communication failure',
                getResponseHeader: me._getHeader,
                getAllResponseHeaders: me._getHeaders
            };
            if (me.aborted) {
                result.aborted = true;
            }
            if (me.timedout) {
                result.timedout = true;
            }
            return result;
        },
        _getHeader: function(name) {
            var headers = this.headers;
            return headers && headers[name.toLowerCase()];
        },
        _getHeaders: function() {
            return this.headers;
        }
    }
});

/**
 *
 * Simulates an XMLHttpRequest object's methods and properties as returned
 * form the flash polyfill plugin. Used in submitting binary data in browsers that do 
 * not support doing so from JavaScript.
 * NOTE: By default this will look for the flash object in the ext directory. When packaging and
 * deploying the app, copy the `ext/plugins` directory and its contents to your root
 * directory. For custom deployments where just the `FlashPlugin.swf` file gets copied
 * (e.g. to `/resources/FlashPlugin.swf`), make sure to notify the framework of the location
 * of the plugin before making the first attempt to post binary data, e.g. in the `launch`
 * method of your app do:
 *
 *      Ext.flashPluginPath="/resources/FlashPlugin.swf";
 *
 * @private
 */
Ext.define('Ext.data.flash.BinaryXhr', {
    statics: {
        /**
         * Called by the flash plugin once it's installed and open for business.
         * @private
         */
        flashPluginActivated: function() {
            Ext.data.flash.BinaryXhr.flashPluginActive = true;
            Ext.data.flash.BinaryXhr.flashPlugin = document.getElementById("ext-flash-polyfill");
            Ext.GlobalEvents.fireEvent("flashready");
        },
        // let all pending connections know
        /**
         * Set to `trut` once the plugin registers and is active.
         * @private
         */
        flashPluginActive: false,
        /**
         * Flag to avoid installing the plugin twice.
         * @private
         */
        flashPluginInjected: false,
        /**
         * Counts IDs for new connections.
         * @private
         */
        connectionIndex: 1,
        /**
         * Placeholder for active connections.
         * @private
         */
        liveConnections: {},
        /**
         * Reference to the actual plugin, once activated.
         * @private
         */
        flashPlugin: null,
        /**
         * Called by the flash plugin once the state of one of the active connections changes.
         * @param {Number/number} javascriptId the ID of the connection.
         * @param {number} state the state of the connection. Equivalent to readyState numbers in
         * XHR.
         * @param {Object} data optional object containing the returned data, error and status
         * codes.
         * @private
         */
        onFlashStateChange: function(javascriptId, state, data) {
            var connection;
            // Identify the request this is for. Make sure its a native number
            connection = this.liveConnections[Number(javascriptId)];
            if (connection) {
                connection.onFlashStateChange(state, data);
            } else {
                Ext.warn.log("onFlashStateChange for unknown connection ID: " + javascriptId);
            }
        },
        /**
         * Adds the BinaryXhr object to the tracked connection list and assigns it an ID
         * @param {Ext.data.flash.BinaryXhr} conn the connection to register
         * @return {Number} id
         * @private
         */
        registerConnection: function(conn) {
            var i = this.connectionIndex;
            this.conectionIndex = this.connectionIndex + 1;
            this.liveConnections[i] = conn;
            return i;
        },
        /**
         * Injects the flash polyfill plugin to allow posting binary data.
         * This is done in two steps: First we load the javascript loader for flash objects,
         * then we call it to inject the flash object.
         * @private
         */
        injectFlashPlugin: function() {
            var me = this,
                flashLoaderPath, flashObjectPath;
            /* eslint-disable max-len */
            // Generate the following HTML set of tags:
            // + '<div id="ext-flash-polyfill">'
            // + '<p>To view this page ensure that Adobe Flash Player version 11.1.0 or greater is installed, and that the FlashPlugin.swf file was correctly placed in the /resources directory.</p>'
            // + '<a href="http://www.adobe.com/go/getflashplayer"><img src="' + window.location.protocol + '//www.adobe.com/images/shared/download_buttons/get_flash_player.gif" alt="Get Adobe Flash player" /></a>'
            // + '</div>'
            me.flashPolyfillEl = Ext.getBody().appendChild({
                id: 'ext-flash-polyfill',
                cn: [
                    {
                        tag: 'p',
                        html: 'To view this page ensure that Adobe Flash Player version 11.1.0 or greater is installed.'
                    },
                    {
                        tag: 'a',
                        href: 'http://www.adobe.com/go/getflashplayer',
                        cn: [
                            {
                                tag: 'img',
                                src: window.location.protocol + '//www.adobe.com/images/shared/download_buttons/get_flash_player.gif',
                                alt: 'Get Adobe Flash player'
                            }
                        ]
                    }
                ]
            });
            // Now load the flash-loading script
            flashLoaderPath = [
                Ext.Loader.getPath('Ext.data.Connection'),
                '../../../plugins/flash/swfobject.js'
            ].join('/');
            flashObjectPath = "/plugins/flash/FlashPlugin.swf";
            flashObjectPath = [
                Ext.Loader.getPath('Ext.data.Connection'),
                '../../plugins/flash/FlashPlugin.swf'
            ].join('/');
            /* eslint-enable max-len */
            if (Ext.flashPluginPath) {
                flashObjectPath = Ext.flashPluginPath;
            }
            Ext.Loader.loadScript({
                url: flashLoaderPath,
                onLoad: function() {
                    // For version detection, set to min. required Flash Player version,
                    // or 0 (or 0.0.0), for no version detection. 
                    // To use express install, set to playerProductInstall.swf, otherwise
                    // the empty string. 
                    var swfVersionStr = "11.4.0",
                        xiSwfUrlStr = "playerProductInstall.swf",
                        flashvars = {},
                        params = {},
                        attributes = {};
                    params.quality = "high";
                    params.bgcolor = "#ffffff";
                    params.allowscriptaccess = "sameDomain";
                    params.allowfullscreen = "true";
                    attributes.id = "ext-flash-polyfill";
                    attributes.name = "polyfill";
                    attributes.align = "middle";
                    /* eslint-disable-next-line no-undef */
                    swfobject.embedSWF(flashObjectPath, "ext-flash-polyfill", "0", "0", // no size so it's not visible.
                    swfVersionStr, xiSwfUrlStr, flashvars, params, attributes);
                },
                onError: function() {
                    /* eslint-disable-next-line no-undef */
                    Ext.raise("Could not load flash-loader file swfobject.js from " + flashLoader);
                },
                scope: me
            });
            Ext.data.flash.BinaryXhr.flashPluginInjected = true;
        }
    },
    /**
     * @property {number} readyState The connection's simulated readyState. Note that the only
     * supported values are 0, 1 and 4. States 2 and 3 will never be reported.
     */
    readyState: 0,
    /**
     * @property {number} status Connection status code returned by flash or the server.
     */
    status: 0,
    /**
     * Status text (if any) returned by flash or the server.
     */
    statusText: "",
    /**
     * @property {Array} responseBytes The binary bytes returned.
     */
    responseBytes: null,
    /**
     * An ID representing this connection with flash.
     * @private
     */
    javascriptId: null,
    /**
     * Creates a new instance of BinaryXhr.
     */
    constructor: function(config) {
        var me = this;
        // first, make sure flash is loading if needed
        if (!Ext.data.flash.BinaryXhr.flashPluginInjected) {
            Ext.data.flash.BinaryXhr.injectFlashPlugin();
        }
        Ext.apply(me, config);
        me.requestHeaders = {};
    },
    /**
     * Abort this connection. Sets its readyState to 4.
     */
    abort: function() {
        var me = this;
        // if complete, nothing to abort 
        if (me.readyState === 4) {
            Ext.warn.log("Aborting a connection that's completed its transfer: " + this.url);
            return;
        }
        // Mark as aborted
        me.aborted = true;
        // Remove ourselves from the listeners if flash isn't active yet
        if (!Ext.data.flash.BinaryXhr.flashPluginActive) {
            Ext.GlobalEvents.removeListener("flashready", me.onFlashReady, me);
            return;
        }
        // Flash is already live, so we should have a javascriptID and should have called flash
        // to get the request going. Cancel:
        Ext.data.flash.BinaryXhr.flashPlugin.abortRequest(me.javascriptId);
        // remove from list
        delete Ext.data.flash.BinaryXhr.liveConnections[me.javascriptId];
    },
    /**
     * As in XMLHttpRequest.
     */
    getAllResponseHeaders: function() {
        var headers = [];
        Ext.Object.each(this.responseHeaders, function(name, value) {
            headers.push(name + ': ' + value);
        });
        return headers.join('\r\n');
    },
    /**
     * As in XMLHttpRequest.
     */
    getResponseHeader: function(header) {
        var headers = this.responseHeaders;
        return (headers && headers[header]) || null;
    },
    /**
     * As in XMLHttpRequest.
     */
    open: function(method, url, isAsync, user, password) {
        var me = this;
        me.method = method;
        me.url = url;
        me.async = isAsync !== false;
        // eslint-disable-line id-blacklist
        me.user = user;
        me.password = password;
        if (!me.async) {
            Ext.raise("Binary posts are only supported in async mode: " + url);
        }
        if (me.method !== "POST") {
            Ext.log.warn("Binary data can only be sent as a POST request: " + url);
        }
    },
    /**
     * As in XMLHttpRequest.
     */
    overrideMimeType: function(mimeType) {
        this.mimeType = mimeType;
    },
    /**
     * Initiate the request.
     * @param {Array} body an array of byte values to send.
     */
    send: function(body) {
        var me = this;
        me.body = body;
        if (!Ext.data.flash.BinaryXhr.flashPluginActive) {
            Ext.GlobalEvents.addListener("flashready", me.onFlashReady, me);
        } else {
            this.onFlashReady();
        }
    },
    /**
     * Called by send, or once flash is loaded, to actually send the bytes.
     * @private
     */
    onFlashReady: function() {
        var me = this,
            req;
        me.javascriptId = Ext.data.flash.BinaryXhr.registerConnection(me);
        // Create the request object we're sending to flash
        req = {
            method: me.method,
            // ignored since we always POST binary data
            url: me.url,
            user: me.user,
            password: me.password,
            mimeType: me.mimeType,
            requestHeaders: me.requestHeaders,
            body: me.body,
            javascriptId: me.javascriptId
        };
        Ext.data.flash.BinaryXhr.flashPlugin.postBinary(req);
    },
    /**
     * Updates readyState and notifies listeners.
     * @private
     */
    setReadyState: function(state) {
        var me = this;
        if (me.readyState !== state) {
            me.readyState = state;
            me.onreadystatechange();
        }
    },
    /**
     * As in XMLHttpRequest.
     */
    setRequestHeader: function(header, value) {
        this.requestHeaders[header] = value;
    },
    /**
     * @method
     * As in XMLHttpRequest.
     */
    onreadystatechange: Ext.emptyFn,
    /**
     * Parses data returned from flash once a connection is done.
     * @param {Object} data the data object send from Flash.
     * @private
     */
    parseData: function(data) {
        var me = this;
        // parse data and set up variables so that listeners can use this XHR
        this.status = data.status || 0;
        // we get back no response headers, so fake what we know:
        me.responseHeaders = {};
        if (me.mimeType) {
            me.responseHeaders["content-type"] = me.mimeType;
        }
        if (data.reason === "complete") {
            // Transfer complete and data received
            this.responseBytes = data.data;
            me.responseHeaders["content-length"] = data.data.length;
        } else if (data.reason === "error" || data.reason === "securityError") {
            this.statusText = data.text;
            me.responseHeaders["content-length"] = 0;
        } else // we don't get the error response data
        {
            Ext.raise("Unkown reason code in data: " + data.reason);
        }
    },
    /**
     * Called once flash calls back with updates about the connection
     * @param {Number} state the readyState of the connection.
     * @param {Object} data optional data object.
     * @private
     */
    onFlashStateChange: function(state, data) {
        var me = this;
        if (state === 4) {
            // parse data and prepare for handing back to initiator
            me.parseData(data);
            // remove from list
            delete Ext.data.flash.BinaryXhr.liveConnections[me.javascriptId];
        }
        me.setReadyState(state);
    }
});
// notify all listeners

/**
 * This class manages a pending Ajax request. Instances of this type are created by the
 * `{@link Ext.data.Connection#request}` method.
 * @since 6.0.0
 */
Ext.define('Ext.data.request.Ajax', {
    extend: 'Ext.data.request.Base',
    alias: 'request.ajax',
    requires: [
        'Ext.data.flash.BinaryXhr'
    ],
    statics: {
        /**
         * Checks if the response status was successful
         * @param {Number} status The status code
         * @param {Object} response The Response object
         * @return {Object} An object containing success/status state
         * @private
         */
        parseStatus: function(status, response) {
            var type, len, success, isException;
            if (response) {
                // We have to account for binary and other response types
                type = response.responseType;
                if (type === 'arraybuffer') {
                    len = response.byteLength;
                } else if (type === 'blob') {
                    len = response.response.size;
                } else if ((type === 'json' || type === 'document') && response.response) {
                    len = 0;
                } else if ((type === 'text' || type === '' || !type) && response.responseText) {
                    len = response.responseText.length;
                }
            }
            // see: https://prototype.lighthouseapp.com/projects/8886/tickets/129-ie-mangles-http-response-status-code-204-to-1223
            status = status === 1223 ? 204 : status;
            isException = false;
            // Status can be 0 for file:/// requests
            success = (status >= 200 && status < 300) || status === 304 || (status === 0 && Ext.isNumber(len));
            if (!success) {
                switch (status) {
                    case 12002:
                    case 12029:
                    case 12030:
                    case 12031:
                    case 12152:
                    case 13030:
                        isException = true;
                        break;
                }
            }
            return {
                success: success,
                isException: isException
            };
        }
    },
    start: function(data) {
        var me = this,
            options = me.options,
            requestOptions = me.requestOptions,
            isXdr = me.isXdr,
            xhr;
        xhr = me.xhr = me.openRequest(options, requestOptions, me.async, me.username, me.password);
        // XDR doesn't support setting any headers
        if (!isXdr) {
            me.setupHeaders(xhr, options, requestOptions.data, requestOptions.params);
        }
        if (me.async) {
            if (!isXdr) {
                xhr.onreadystatechange = me.bindStateChange();
            }
        }
        if (isXdr) {
            me.processXdrRequest(me, xhr);
        }
        // Parent will set the timeout if needed
        me.callParent([
            data
        ]);
        // start the request!
        xhr.send(data);
        if (!me.async) {
            return me.onComplete();
        }
        return me;
    },
    /**
     * Aborts an active request.
     */
    abort: function(force) {
        var me = this,
            xhr = me.xhr;
        if (force || me.isLoading()) {
            /*
             * Clear out the onreadystatechange here, this allows us
             * greater control, the browser may/may not fire the function
             * depending on a series of conditions.
             */
            try {
                xhr.onreadystatechange = null;
            } catch (e) {
                // Setting onreadystatechange to null can cause problems in IE, see
                // http://www.quirksmode.org/blog/archives/2005/09/xmlhttp_notes_a_1.html
                xhr.onreadystatechange = Ext.emptyFn;
            }
            xhr.abort();
            me.callParent([
                force
            ]);
            me.onComplete();
            me.cleanup();
        }
    },
    /**
     * Cleans up any left over information from the request
     */
    cleanup: function() {
        this.xhr = null;
        delete this.xhr;
    },
    isLoading: function() {
        var me = this,
            xhr = me.xhr,
            state = xhr && xhr.readyState,
            C = Ext.data.flash && Ext.data.flash.BinaryXhr;
        if (!xhr || me.aborted || me.timedout) {
            return false;
        }
        // if there is a connection and readyState is not 0 or 4, or in case of
        // BinaryXHR, not 4
        if (C && xhr instanceof C) {
            return state !== 4;
        }
        return state !== 0 && state !== 4;
    },
    /**
     * Creates and opens an appropriate XHR transport for a given request on this browser.
     * This logic is contained in an individual method to allow for overrides to process all
     * of the parameters and options and return a suitable, open connection.
     * @private
     */
    openRequest: function(options, requestOptions, isAsync, username, password) {
        var me = this,
            xhr = me.newRequest(options);
        if (username) {
            xhr.open(requestOptions.method, requestOptions.url, isAsync, username, password);
        } else {
            if (me.isXdr) {
                xhr.open(requestOptions.method, requestOptions.url);
            } else {
                xhr.open(requestOptions.method, requestOptions.url, isAsync);
            }
        }
        if (options.binary || me.binary) {
            if (window.Uint8Array) {
                xhr.responseType = 'arraybuffer';
            } else if (xhr.overrideMimeType) {
                // In some older non-IE browsers, e.g. ff 3.6, that do not
                // support Uint8Array, a mime type override is required so that
                // the unprocessed binary data can be read from the responseText
                // (see createResponse())
                xhr.overrideMimeType('text/plain; charset=x-user-defined');
            } else if (!Ext.isIE) {
                Ext.log.warn("Your browser does not support loading binary data using Ajax.");
            }
        }
        if (options.responseType) {
            xhr.responseType = options.responseType;
        }
        if (options.withCredentials || me.withCredentials) {
            xhr.withCredentials = true;
        }
        return xhr;
    },
    /**
     * Creates the appropriate XHR transport for a given request on this browser. On IE
     * this may be an `XDomainRequest` rather than an `XMLHttpRequest`.
     * @private
     */
    newRequest: function(options) {
        var me = this,
            xhr;
        if (options.binaryData) {
            // This is a binary data request. Handle submission differently for differnet browsers
            if (window.Uint8Array) {
                xhr = me.getXhrInstance();
            } else {
                // catch all for all other browser types
                xhr = new Ext.data.flash.BinaryXhr();
            }
        } else if (me.cors && Ext.isIE9m) {
            xhr = me.getXdrInstance();
            me.isXdr = true;
        } else {
            xhr = me.getXhrInstance();
            me.isXdr = false;
        }
        return xhr;
    },
    /**
     * Setup all the headers for the request
     * @private
     * @param {Object} xhr The xhr object
     * @param {Object} options The options for the request
     * @param {Object} data The data for the request
     * @param {Object} params The params for the request
     */
    setupHeaders: function(xhr, options, data, params) {
        var me = this,
            headers = Ext.apply({}, options.headers || {}, me.defaultHeaders),
            contentType = me.defaultPostHeader,
            jsonData = options.jsonData,
            xmlData = options.xmlData,
            type = 'Content-Type',
            useHeader = me.useDefaultXhrHeader,
            key, header;
        if (!headers.hasOwnProperty(type) && (data || params)) {
            if (data) {
                if (options.rawData) {
                    contentType = 'text/plain';
                } else {
                    if (xmlData && Ext.isDefined(xmlData)) {
                        contentType = 'text/xml';
                    } else if (jsonData && Ext.isDefined(jsonData)) {
                        contentType = 'application/json';
                    }
                }
            }
            headers[type] = contentType;
        }
        if (useHeader && !headers['X-Requested-With']) {
            headers['X-Requested-With'] = me.defaultXhrHeader;
        }
        // If undefined/null, remove it and don't set the header.
        // Allow the browser to do so.
        if (headers[type] === undefined || headers[type] === null) {
            delete headers[type];
        }
        // set up all the request headers on the xhr object
        try {
            for (key in headers) {
                if (headers.hasOwnProperty(key)) {
                    header = headers[key];
                    xhr.setRequestHeader(key, header);
                }
            }
        } catch (e) {
            // TODO Request shouldn't fire events from its owner
            me.owner.fireEvent('exception', key, header);
        }
        return headers;
    },
    /**
     * Creates the appropriate XDR transport for this browser.
     * - IE 7 and below don't support CORS
     * - IE 8 and 9 support CORS with native XDomainRequest object
     * - IE 10 (and above?) supports CORS with native XMLHttpRequest object
     * @private
     */
    getXdrInstance: function() {
        var xdr;
        if (Ext.ieVersion >= 8) {
            xdr = new XDomainRequest();
        } else // eslint-disable-line no-undef
        {
            Ext.raise({
                msg: 'Your browser does not support CORS'
            });
        }
        return xdr;
    },
    /**
     * @private
     * Do not remove this method. This is where Ajax simulator injects request stubs.
     */
    getXhrInstance: function() {
        return new XMLHttpRequest();
    },
    processXdrRequest: function(request, xhr) {
        var me = this;
        // Mutate the request object as per XDR spec.
        delete request.headers;
        request.contentType = request.options.contentType || me.defaultXdrContentType;
        xhr.onload = me.bindStateChange(true);
        xhr.onerror = xhr.ontimeout = me.bindStateChange(false);
    },
    processXdrResponse: function(response, xhr) {
        // Mutate the response object as per XDR spec.
        response.getAllResponseHeaders = function() {
            return [];
        };
        response.getResponseHeader = function() {
            return '';
        };
        response.contentType = xhr.contentType || this.defaultXdrContentType;
    },
    bindStateChange: function(xdrResult) {
        var me = this;
        return function() {
            Ext.elevate(function() {
                me.onStateChange(xdrResult);
            });
        };
    },
    onStateChange: function(xdrResult) {
        var me = this,
            xhr = me.xhr;
        // Using CORS with IE doesn't support readyState so we fake it.
        if ((xhr && xhr.readyState === 4) || me.isXdr) {
            me.clearTimer();
            me.onComplete(xdrResult);
            me.cleanup();
        }
    },
    /**
     * To be called when the request has come back from the server
     * @param {Object} xdrResult
     * @return {Object} The response
     * @private
     */
    onComplete: function(xdrResult) {
        var me = this,
            owner = me.owner,
            options = me.options,
            xhr = me.xhr,
            failure = {
                success: false,
                isException: false
            },
            result, success, response;
        if (!xhr || me.destroyed) {
            return me.result = failure;
        }
        try {
            result = Ext.data.request.Ajax.parseStatus(xhr.status, xhr);
            if (result.success) {
                // This is quite difficult to reproduce, however if we abort a request
                // just before it returns from the server, occasionally the status will be
                // returned correctly but the request is still yet to be complete.
                result.success = xhr.readyState === 4;
            }
        } catch (e) {
            // In some browsers we can't access the status if the readyState is not 4,
            // so the request has failed
            result = failure;
        }
        success = me.success = me.isXdr ? xdrResult : result.success;
        if (success) {
            response = me.createResponse(xhr);
            if (owner.hasListeners.requestcomplete) {
                owner.fireEvent('requestcomplete', owner, response, options);
            }
            if (options.success) {
                Ext.callback(options.success, options.scope, [
                    response,
                    options
                ]);
            }
        } else {
            if (result.isException || me.aborted || me.timedout) {
                response = me.createException(xhr);
            } else {
                response = me.createResponse(xhr);
            }
            if (owner.hasListeners.requestexception) {
                owner.fireEvent('requestexception', owner, response, options);
            }
            if (options.failure) {
                Ext.callback(options.failure, options.scope, [
                    response,
                    options
                ]);
            }
        }
        me.result = response;
        if (options.callback) {
            Ext.callback(options.callback, options.scope, [
                options,
                success,
                response
            ]);
        }
        owner.onRequestComplete(me);
        me.callParent([
            xdrResult
        ]);
        return response;
    },
    /**
     * Creates the response object
     * @param {Object} xhr
     * @private
     */
    createResponse: function(xhr) {
        var me = this,
            isXdr = me.isXdr,
            headers = {},
            lines = isXdr ? [] : xhr.getAllResponseHeaders().replace(/\r\n/g, '\n').split('\n'),
            count = lines.length,
            line, index, key, response;
        while (count--) {
            line = lines[count];
            index = line.indexOf(':');
            if (index >= 0) {
                key = line.substr(0, index).toLowerCase();
                if (line.charAt(index + 1) === ' ') {
                    ++index;
                }
                headers[key] = line.substr(index + 1);
            }
        }
        response = {
            request: me,
            requestId: me.id,
            status: xhr.status,
            statusText: xhr.statusText,
            getResponseHeader: function(header) {
                return headers[header.toLowerCase()];
            },
            getAllResponseHeaders: function() {
                return headers;
            }
        };
        if (isXdr) {
            me.processXdrResponse(response, xhr);
        }
        if (me.binary) {
            response.responseBytes = me.getByteArray(xhr);
        } else {
            if (xhr.responseType) {
                response.responseType = xhr.responseType;
            }
            if (xhr.responseType === 'blob') {
                response.responseBlob = xhr.response;
            } else if (xhr.responseType === 'json') {
                response.responseJson = xhr.response;
            } else if (xhr.responseType === 'document') {
                response.responseXML = xhr.response;
            } else {
                // an error is thrown when trying to access responseText or responseXML
                // on an xhr object with responseType with any value but "text" or "",
                // so only attempt to set these properties in the response if we're not
                // dealing with other specified response types
                response.responseText = xhr.responseText;
                response.responseXML = xhr.responseXML;
            }
        }
        return response;
    },
    destroy: function() {
        this.xhr = null;
        this.callParent();
    },
    privates: {
        /**
         * Gets binary data from the xhr response object and returns it as a byte array
         * @param {Object} xhr the xhr response object
         * @return {Uint8Array/Array}
         * @private
         */
        getByteArray: function(xhr) {
            var response = xhr.response,
                responseBody = xhr.responseBody,
                Cls = Ext.data.flash && Ext.data.flash.BinaryXhr,
                byteArray, responseText, len, i;
            if (xhr instanceof Cls) {
                // If this was a BinaryXHR request via flash, we already have the bytes ready
                byteArray = xhr.responseBytes;
            } else if (window.Uint8Array) {
                // Modern browsers (including IE10) have a native byte array
                // which can be created by passing the ArrayBuffer (returned as
                // the xhr.response property) to the Uint8Array constructor.
                /* eslint-disable-next-line no-undef */
                byteArray = response ? new Uint8Array(response) : [];
            } else if (Ext.isIE9p) {
                // In IE9 and below the responseBody property contains a byte array
                // but it is not directly accessible using javascript.
                // In IE9p we can get the bytes by constructing a VBArray
                // using the responseBody and then converting it to an Array.
                try {
                    /* eslint-disable-next-line no-undef */
                    byteArray = new VBArray(responseBody).toArray();
                } catch (e) {
                    // If the binary response is empty, the VBArray constructor will
                    // choke on the responseBody.  We can't simply do a null check
                    // on responseBody because responseBody is always falsy when it
                    // contains binary data.
                    byteArray = [];
                }
            } else if (Ext.isIE) {
                // IE8 and below also have a VBArray constructor, but throw a
                // "VBArray Expected" error if you try to pass the responseBody to
                // the VBArray constructor.
                // http://msdn.microsoft.com/en-us/library/ye3x9by3%28v=vs.71%29.aspx
                // so we have to use vbscript injection to access the bytes
                if (!this.self.vbScriptInjected) {
                    this.injectVBScript();
                }
                /* eslint-disable-next-line no-undef */
                getIEByteArray(xhr.responseBody, byteArray = []);
            } else {
                // in other older browsers make a best-effort attempt to read the
                // bytes from responseText
                byteArray = [];
                responseText = xhr.responseText;
                len = responseText.length;
                for (i = 0; i < len; i++) {
                    // Some characters have an extra byte 0xF7 in the high order
                    // position. Throw away the high order byte and then push the
                    // result onto the byteArray.
                    byteArray.push(responseText.charCodeAt(i) & 255);
                }
            }
            return byteArray;
        },
        /**
         * Injects a vbscript tag containing a 'getIEByteArray' method for reading
         * binary data from an xhr response in IE8 and below.
         * @private
         */
        injectVBScript: function() {
            var scriptTag = document.createElement('script');
            scriptTag.type = 'text/vbscript';
            /* eslint-disable indent */
            scriptTag.text = [
                'Function getIEByteArray(byteArray, out)',
                'Dim len, i',
                'len = LenB(byteArray)',
                'For i = 1 to len',
                'out.push(AscB(MidB(byteArray, i, 1)))',
                'Next',
                'End Function'
            ].join('\n');
            /* eslint-enable indent */
            Ext.getHead().dom.appendChild(scriptTag);
            this.self.vbScriptInjected = true;
        }
    }
});

/**
 * This class manages a pending form submit. Instances of this type are created by the
 * `{@link Ext.data.Connection#request}` method.
 * @since 6.0.0
 */
Ext.define('Ext.data.request.Form', {
    extend: 'Ext.data.request.Base',
    alias: 'request.form',
    start: function(data) {
        var me = this,
            options = me.options,
            requestOptions = me.requestOptions;
        // Parent will set the timeout
        me.callParent([
            data
        ]);
        me.form = me.upload(options.form, requestOptions.url, requestOptions.data, options);
        return me;
    },
    abort: function(force) {
        var me = this,
            frame;
        if (me.isLoading()) {
            try {
                frame = me.frame.dom;
                if (frame.stop) {
                    frame.stop();
                } else {
                    frame.document.execCommand('Stop');
                }
            } catch (e) {}
        }
        // ignore
        me.callParent([
            force
        ]);
        me.onComplete();
        me.cleanup();
    },
    /*
     * Clean up any left over information from the form submission.
     */
    cleanup: function() {
        var me = this,
            frame = me.frame;
        if (frame) {
            // onComplete hasn't fired yet if frame != null so need to clean up
            frame.un('load', me.onComplete, me);
            Ext.removeNode(frame);
        }
        me.frame = me.form = null;
    },
    isLoading: function() {
        return !!this.frame;
    },
    /**
     * Uploads a form using a hidden iframe.
     * @param {String/HTMLElement/Ext.dom.Element} form The form to upload
     * @param {String} url The url to post to
     * @param {String} params Any extra parameters to pass
     * @param {Object} options The initial options
     * @private
     */
    upload: function(form, url, params, options) {
        form = Ext.getDom(form);
        options = options || {};
        /* eslint-disable-next-line vars-on-top */
        var frameDom = document.createElement('iframe'),
            frame = Ext.get(frameDom),
            id = frame.id,
            hiddens = [],
            encoding = 'multipart/form-data',
            buf = {
                target: form.target,
                method: form.method,
                encoding: form.encoding,
                enctype: form.enctype,
                action: form.action
            },
            addField = function(name, value) {
                hiddenItem = document.createElement('input');
                Ext.fly(hiddenItem).set({
                    type: 'hidden',
                    value: value,
                    name: name
                });
                form.appendChild(hiddenItem);
                hiddens.push(hiddenItem);
            },
            hiddenItem, obj, value, name, vLen, v, hLen, h;
        /*
         * Originally this behaviour was modified for Opera 10 to apply the secure URL after
         * the frame had been added to the document. It seems this has since been corrected in
         * Opera so the behaviour has been reverted, the URL will be set before being added.
         */
        frame.set({
            name: id,
            cls: Ext.baseCSSPrefix + 'hidden-display',
            src: Ext.SSL_SECURE_URL,
            tabIndex: -1
        });
        document.body.appendChild(frameDom);
        document.body.appendChild(form);
        // This is required so that IE doesn't pop the response up in a new window.
        if (document.frames) {
            document.frames[id].name = id;
        }
        Ext.fly(form).set({
            target: id,
            method: 'POST',
            enctype: encoding,
            encoding: encoding,
            action: url || buf.action
        });
        // add dynamic params
        if (params) {
            obj = Ext.Object.fromQueryString(params) || {};
            for (name in obj) {
                if (obj.hasOwnProperty(name)) {
                    value = obj[name];
                    if (Ext.isArray(value)) {
                        vLen = value.length;
                        for (v = 0; v < vLen; v++) {
                            addField(name, value[v]);
                        }
                    } else {
                        addField(name, value);
                    }
                }
            }
        }
        this.frame = frame;
        frame.on({
            load: this.onComplete,
            scope: this,
            // Opera introduces multiple 'load' events, so account for extras as well
            single: !Ext.isOpera
        });
        form.submit();
        document.body.removeChild(form);
        // Restore form to previous settings
        Ext.fly(form).set(buf);
        for (hLen = hiddens.length , h = 0; h < hLen; h++) {
            Ext.removeNode(hiddens[h]);
        }
        return form;
    },
    getDoc: function() {
        var frame = this.frame.dom;
        return (frame && (frame.contentWindow.document || frame.contentDocument)) || (window.frames[frame.id] || {}).document;
    },
    getTimeout: function() {
        // For a form post, since it can include large file uploads, we do not use the
        // default timeout from the owner. Only explicit timeouts passed in the options
        // are meaningful here.
        return this.options.timeout;
    },
    /**
     * Callback handler for the upload function. After we've submitted the form via the
     * iframe this creates a bogus response object to simulate an XHR and populates its
     * responseText from the now-loaded iframe's document body (or a textarea inside the
     * body). We then clean up by removing the iframe.
     * @private
     */
    onComplete: function() {
        var me = this,
            frame = me.frame,
            owner = me.owner,
            options = me.options,
            callback, doc, success, contentNode, response;
        // Nulled out frame means onComplete was fired already
        if (!frame) {
            return;
        }
        if (me.aborted || me.timedout) {
            me.result = response = me.createException();
            response.responseXML = null;
            response.responseText = Ext.encode({
                success: false,
                message: Ext.String.trim(response.statusText)
            });
            response.request = me;
            callback = options.failure;
            success = false;
        } else {
            try {
                doc = me.getDoc();
                // bogus response object
                me.result = response = {
                    responseText: '',
                    responseXML: null,
                    request: me
                };
                // Opera will fire an extraneous load event on about:blank
                // We want to ignore this since the load event will be fired twice
                if (doc) {
                    // TODO: See if this still applies vs Current opera-webkit releases
                    if (Ext.isOpera && doc.location === Ext.SSL_SECURE_URL) {
                        return;
                    }
                    if (doc.body) {
                        // Response sent as Content-Type: text/json or text/plain.
                        // Browser will embed it in a <pre> element.
                        // Note: The statement below tests the result of an assignment.
                        if ((contentNode = doc.body.firstChild) && /pre/i.test(contentNode.tagName)) {
                            response.responseText = contentNode.textContent || contentNode.innerText;
                        }
                        // Response sent as Content-Type: text/html. We must still support
                        // JSON response wrapped in textarea.
                        // Note: The statement below tests the result of an assignment.
                        else if ((contentNode = doc.getElementsByTagName('textarea')[0])) {
                            response.responseText = contentNode.value;
                        } else // Response sent as Content-Type: text/html with no wrapping. Scrape
                        // JSON response out of text
                        {
                            response.responseText = doc.body.textContent || doc.body.innerText;
                        }
                    }
                    // in IE the document may still have a body even if returns XML.
                    // TODO What is this about?
                    response.responseXML = doc.XMLDocument || doc;
                    callback = options.success;
                    success = true;
                    response.status = 200;
                } else {
                    Ext.raise("Could not acquire a suitable connection for the " + "file upload service.");
                }
            } catch (e) {
                me.result = response = me.createException();
                // Report any error in the message property
                response.status = 400;
                response.statusText = (e.message || e.description) + '';
                response.responseText = Ext.encode({
                    success: false,
                    message: Ext.String.trim(response.statusText)
                });
                response.responseXML = null;
                callback = options.failure;
                success = false;
            }
        }
        me.frame = null;
        me.success = success;
        owner.fireEvent(success ? 'requestcomplete' : 'requestexception', owner, response, options);
        Ext.callback(callback, options.scope, [
            response,
            options
        ]);
        Ext.callback(options.callback, options.scope, [
            options,
            success,
            response
        ]);
        owner.onRequestComplete(me);
        // Must defer slightly to permit full exit from load event before destruction
        Ext.asap(frame.destroy, frame);
        me.callParent();
    },
    destroy: function() {
        this.cleanup();
        this.callParent();
    }
});

/**
 * The Connection class encapsulates a connection to the page's originating domain, allowing
 * requests to be made either to a configured URL, or to a URL specified at request time.
 *
 * Requests made by this class are asynchronous, and will return immediately. No data from the
 * server will be available to the statement immediately following the {@link #request} call.
 * To process returned data, use a success callback in the request options object, or an
 * {@link #requestcomplete event listener}.
 *
 * # File Uploads
 *
 * File uploads are not performed using normal "Ajax" techniques, that is they are not performed
 * using XMLHttpRequests. Instead the form is submitted in the standard manner with the DOM
 * &lt;form&gt; element temporarily modified to have its target set to refer to a dynamically
 * generated, hidden &lt;iframe&gt; which is inserted into the document but removed after the
 * return data has been gathered.
 *
 * The server response is parsed by the browser to create the document for the IFRAME. If the
 * server is using JSON to send the return object, then the Content-Type header must be set to
 * "text/html" in order to tell the browser to insert the text unchanged into the document body.
 *
 * Characters which are significant to an HTML parser must be sent as HTML entities, so encode
 * `<` as `&lt;`, `&` as `&amp;` etc.
 *
 * The response text is retrieved from the document, and a fake XMLHttpRequest object is created
 * containing a responseText property in order to conform to the requirements of event handlers and
 * callbacks.
 *
 * Be aware that file upload packets are sent with the content type multipart/form and some server
 * technologies (notably JEE) may require some custom processing in order to retrieve parameter
 * names and parameter values from the packet content.
 *
 * Also note that it's not possible to check the response code of the hidden iframe, so the success
 * handler will ALWAYS fire.
 *
 * # Binary Posts
 *
 * The class supports posting binary data to the server by using native browser capabilities,
 * or a flash polyfill plugin in browsers that do not support native binary posting (e.g.
 * Internet Explorer version 9 or less). A number of limitations exist when the polyfill is used:
 *
 * - Only asynchronous connections are supported.
 * - Only the POST method can be used.
 * - The return data can only be binary for now. Set the {@link Ext.data.Connection#binary binary}
 * parameter to `true`.
 * - Only the 0, 1 and 4 (complete) readyState values will be reported to listeners.
 * - The flash object will be injected at the bottom of the document and should be invisible.
 * - Important: See note about packaing the flash plugin with the app in the documenetation of
 * {@link Ext.data.flash.BinaryXhr BinaryXhr}.
 *
 */
Ext.define('Ext.data.Connection', {
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
    requires: [
        'Ext.data.request.Ajax',
        'Ext.data.request.Form',
        'Ext.data.flash.BinaryXhr',
        'Ext.Deferred'
    ],
    statics: {
        requestId: 0
    },
    enctypeRe: /multipart\/form-data/i,
    config: {
        /**
         * @cfg {String} url
         * The URL for this connection.
         */
        url: null,
        /**
         * @cfg {Boolean} async
         * `true` if this request should run asynchronously. Setting this to `false` should
         * generally be avoided, since it will cause the UI to be blocked, the user won't be able
         * to interact with the browser until the request completes.
         */
        async: true,
        // eslint-disable-line id-blacklist
        /**
         * @cfg {String} username
         * The username to pass when using {@link #withCredentials}.
         */
        username: '',
        /**
         * @cfg {String} password
         * The password to pass when using {@link #withCredentials}.
         */
        password: '',
        /**
         * @cfg {Boolean} disableCaching
         * True to add a unique cache-buster param to GET requests.
         */
        disableCaching: true,
        /**
         * @cfg {Boolean} withCredentials
         * True to set `withCredentials = true` on the XHR object
         */
        withCredentials: false,
        /**
         * @cfg {Boolean} binary
         * True if the response should be treated as binary data.  If true, the binary
         * data will be accessible as a "responseBytes" property on the response object.
         */
        binary: false,
        /**
         * @cfg {Boolean} cors
         * True to enable CORS support on the XHR object. Currently the only effect of this option
         * is to use the XDomainRequest object instead of XMLHttpRequest if the browser is IE8
         * or above.
         */
        cors: false,
        isXdr: false,
        defaultXdrContentType: 'text/plain',
        /**
         * @cfg {String} disableCachingParam
         * Change the parameter which is sent went disabling caching through a cache buster.
         */
        disableCachingParam: '_dc',
        /**
         * @cfg {Number} [timeout=30000] The timeout in milliseconds to be used for
         * requests.
         * Defaults to 30000 milliseconds (30 seconds).
         *
         * When a request fails due to timeout the XMLHttpRequest response object will
         * contain:
         *
         *     timedout: true
         */
        timeout: 30000,
        /**
         * @cfg {Object} [extraParams] Any parameters to be appended to the request.
         */
        extraParams: null,
        /**
         * @cfg {Boolean} [autoAbort=false]
         * Whether this request should abort any pending requests.
         */
        autoAbort: false,
        /**
         * @cfg {String} method
         * The default HTTP method to be used for requests.
         *
         * If not set, but {@link #request} params are present, POST will be used;
         * otherwise, GET will be used.
         */
        method: null,
        /**
         * @cfg {Object} defaultHeaders
         * An object containing request headers which are added to each request made by this object.
         */
        defaultHeaders: null,
        /**
         * @cfg {String} defaultPostHeader
         * The default header to be sent out with any post request.
         */
        defaultPostHeader: 'application/x-www-form-urlencoded; charset=UTF-8',
        /**
         * @cfg {Boolean} useDefaultXhrHeader
         * `true` to send the {@link #defaultXhrHeader} along with any request.
         */
        useDefaultXhrHeader: true,
        /**
         * @cfg {String}
         * The header to send with Ajax requests. Also see {@link #useDefaultXhrHeader}.
         */
        defaultXhrHeader: 'XMLHttpRequest'
    },
    /**
     * @event beforerequest
     * @preventable
     * Fires before a network request is made to retrieve a data object.
     * @param {Ext.data.Connection} conn This Connection object.
     * @param {Object} options The options config object passed to the {@link #request} method.
     */
    /**
     * @event requestcomplete
     * Fires if the request was successfully completed.
     * @param {Ext.data.Connection} conn This Connection object.
     * @param {Object} response The XHR object containing the response data.
     * See [The XMLHttpRequest Object](http://www.w3.org/TR/XMLHttpRequest/) for details.
     * @param {Object} options The options config object passed to the {@link #request} method.
     */
    /**
     * @event requestexception
     * Fires if an error HTTP status was returned from the server. This event may also
     * be listened to in the event that a request has timed out or has been aborted.
     * See [HTTP Status Code Definitions](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
     * for details of HTTP status codes.
     * @param {Ext.data.Connection} conn This Connection object.
     * @param {Object} response The XHR object containing the response data.
     * See [The XMLHttpRequest Object](http://www.w3.org/TR/XMLHttpRequest/) for details.
     * @param {Object} options The options config object passed to the {@link #request} method.
     */
    constructor: function(config) {
        // Will call initConfig
        this.mixins.observable.constructor.call(this, config);
        this.requests = {};
    },
    /**
     * @method request
     * Sends an HTTP (Ajax) request to a remote server.
     *
     * **Important:** Ajax server requests are asynchronous, and this call will
     * return before the response has been received.
     *
     * Instead, process any returned data using a promise:
     *
     *      Ext.Ajax.request({
     *          url: 'ajax_demo/sample.json'
     *      }).then(function(response, opts) {
     *          var obj = Ext.decode(response.responseText);
     *          console.dir(obj);
     *      },
     *      function(response, opts) {
     *          console.log('server-side failure with status code ' + response.status);
     *      });
     *
     * Or in callback functions:
     *
     *      Ext.Ajax.request({
     *          url: 'ajax_demo/sample.json',
     *
     *          success: function(response, opts) {
     *              var obj = Ext.decode(response.responseText);
     *              console.dir(obj);
     *          },
     *
     *          failure: function(response, opts) {
     *              console.log('server-side failure with status code ' + response.status);
     *          }
     *      });
     *
     * To execute a callback function in the correct scope, use the `scope` option.
     *
     * @param {Object} options An object which may contain the following properties:
     *
     * (The options object may also contain any other property which might be needed to perform
     * postprocessing in a callback because it is passed to callback functions.)
     *
     * @param {String/Function} options.url The URL to which to send the request, or a function
     * to call which returns a URL string. The scope of the function is specified by the `scope`
     * option. Defaults to the configured `url`.
     *
     * @param {Boolean} options.async `true` if this request should run asynchronously.
     * Setting this to `false` should generally be avoided, since it will cause the UI to be
     * blocked, the user won't be able to interact with the browser until the request completes.
     * Defaults to `true`.
     *
     * @param {Object/String/Function} options.params An object containing properties which are
     * used as parameters to the request, a url encoded string or a function to call to get either.
     * The scope of the function is specified by the `scope` option.
     *
     * @param {String} options.method The HTTP method to use
     * for the request. Defaults to the configured method, or if no method was configured,
     * "GET" if no parameters are being sent, and "POST" if parameters are being sent.  Note that
     * the method name is case-sensitive and should be all caps.
     *
     * @param {Function} options.callback The function to be called upon receipt of the HTTP
     * response. The callback is called regardless of success or failure and is passed the
     * following parameters:
     * @param {Object} options.callback.options The parameter to the request call.
     * @param {Boolean} options.callback.success True if the request succeeded.
     * @param {Object} options.callback.response The XMLHttpRequest object containing the response
     * data. See [www.w3.org/TR/XMLHttpRequest/](http://www.w3.org/TR/XMLHttpRequest/) for details
     * about accessing elements of the response.
     *
     * @param {Function} options.success The function to be called upon success of the request.
     * The callback is passed the following parameters:
     * @param {Object} options.success.response The XMLHttpRequest object containing the response
     * data.
     * @param {Object} options.success.options The parameter to the request call.
     *
     * @param {Function} options.failure The function to be called upon failure of the request.
     * The callback is passed the following parameters:
     * @param {Object} options.failure.response The XMLHttpRequest object containing the response
     * data.
     * @param {Object} options.failure.options The parameter to the request call.
     *
     * @param {Object} options.scope The scope in which to execute the callbacks: The "this" object
     * for the callback function. If the `url`, or `params` options were specified as functions
     * from which to draw values, then this also serves as the scope for those function calls.
     * Defaults to the browser window.
     *
     * @param {Number} options.timeout The timeout in milliseconds to be used for this
     * request.
     * Defaults to 30000 milliseconds (30 seconds).
     *
     * When a request fails due to timeout the XMLHttpRequest response object will
     * contain:
     *
     *     timedout: true
     *
     * @param {Ext.Element/HTMLElement/String} options.form The `<form>` Element or the id of the
     * `<form>` to pull parameters from.
     *
     * @param {Boolean} options.isUpload **Only meaningful when used with the `form` option.**
     *
     * True if the form object is a file upload (will be set automatically if the form was
     * configured with **`enctype`** `"multipart/form-data"`).
     *
     * File uploads are not performed using normal "Ajax" techniques, that is they are **not**
     * performed using XMLHttpRequests. Instead the form is submitted in the standard manner with
     * the DOM `&lt;form&gt;` element temporarily modified to have its
     * [target](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/target)
     * set to refer to a dynamically generated, hidden `&lt;iframe&gt;` which is inserted
     * into the document but removed after the return data has been gathered.
     *
     * The server response is parsed by the browser to create the document for the IFRAME. If the
     * server is using JSON to send the return object, then the
     * [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type)
     * header must be set to "text/html" in order to tell the browser to insert the text
     * unchanged into the document body.
     *
     * The response text is retrieved from the document, and a fake XMLHttpRequest object is created
     * containing a `responseText` property in order to conform to the requirements of event
     * handlers and callbacks.
     *
     * Be aware that file upload packets are sent with the content type
     * [multipart/form](https://tools.ietf.org/html/rfc7233#section-4.1) and some server
     * technologies (notably JEE) may require some custom processing in order to retrieve parameter
     * names and parameter values from the packet content.
     *
     *  - [target](http://www.w3.org/TR/REC-html40/present/frames.html#adef-target)
     *  - [Content-Type](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17)
     *  - [multipart/form](http://www.faqs.org/rfcs/rfc2388.html)
     *
     * @param {Object} options.headers Request headers to set for the request.
     * The XHR will attempt to set an appropriate Content-Type based on the params/data passed
     * to the request. To prevent this, setting the Content-Type header to `null` or `undefined`
     * will not attempt to set any Content-Type and it will be left to the browser.
     *
     * @param {Object} options.xmlData XML document to use for the post. Note: This will be used
     * instead of params for the post data. Any params will be appended to the URL.
     *
     * @param {Object/String} options.jsonData JSON data to use as the post. Note: This will be used
     * instead of params for the post data. Any params will be appended to the URL.
     *
     * @param {String} options.rawData A raw string to use as the post. Note: This will be used
     * instead of params for the post data. Any params will be appended to the URL.
     *
     * @param {Array} options.binaryData An array of bytes to submit in binary form. Any params
     * will be appended to the URL. If binaryData is present, you must set
     * {@link Ext.data.Connection#binary binary} to `true` and options.method to `POST`.
     *
     * @param {Boolean} options.disableCaching True to add a unique cache-buster param to GET
     * requests.
     *
     * @param {Boolean} options.withCredentials True to add the withCredentials property to the
     * XHR object
     *
     * @param {String} options.username The username to pass when using `withCredentials`.
     *
     * @param {String} options.password The password to pass when using `withCredentials`.
     *
     * @param {Boolean} options.binary True if the response should be treated as binary data.
     * If true, the binary data will be accessible as a "responseBytes" property on the response
     * object.
     *
     * @return {Ext.data.request.Base} The request object. This may be used to abort the
     * request.
     */
    request: function(options) {
        var me = this,
            requestOptions, request;
        options = options || {};
        if (me.fireEvent('beforerequest', me, options) !== false) {
            requestOptions = me.setOptions(options, options.scope || Ext.global);
            request = me.createRequest(options, requestOptions);
            return request.start(requestOptions.data);
        }
        // Reusing for response
        request = {
            status: -1,
            statusText: 'Request cancelled in beforerequest event handler'
        };
        Ext.callback(options.callback, options.scope, [
            options,
            false,
            request
        ]);
        return Ext.Deferred.rejected([
            options,
            false,
            request
        ]);
    },
    createRequest: function(options, requestOptions) {
        var me = this,
            type = options.type || requestOptions.type,
            request;
        // If request type is not specified we have to deduce it
        if (!type) {
            type = me.isFormUpload(options) ? 'form' : 'ajax';
        }
        // if autoabort is set, cancel the current transactions
        if (options.autoAbort || me.getAutoAbort()) {
            me.abort();
        }
        // It is possible for the original options object to be mutated if somebody
        // had overridden Connection.setOptions method; it is also possible that such
        // override would do a sensible thing and mutate outgoing requestOptions instead.
        // So we have to pass *both* to the Request constructor, along with the set
        // of defaults potentially set on the Connection instance.
        // If it looks ridiculous, that's because it is; things we have to do for
        // backward compatibility...
        request = Ext.Factory.request({
            type: type,
            owner: me,
            options: options,
            requestOptions: requestOptions,
            ownerConfig: me.getConfig()
        });
        me.requests[request.id] = request;
        me.latestId = request.id;
        return request;
    },
    /**
     * Detects whether the form is intended to be used for an upload.
     * @private
     */
    isFormUpload: function(options) {
        var form = this.getForm(options);
        if (form) {
            return options.isUpload || this.enctypeRe.test(form.getAttribute('enctype'));
        }
        return false;
    },
    /**
     * Gets the form object from options.
     * @private
     * @param {Object} options The request options
     * @return {HTMLElement} The form, null if not passed
     */
    getForm: function(options) {
        return Ext.getDom(options.form);
    },
    /**
     * Sets various options such as the url, params for the request
     * @param {Object} options The initial options
     * @param {Object} scope The scope to execute in
     * @return {Object} The params for the request
     */
    setOptions: function(options, scope) {
        var me = this,
            params = options.params || {},
            extraParams = me.getExtraParams(),
            urlParams = options.urlParams,
            url = options.url || me.getUrl(),
            cors = options.cors,
            jsonData = options.jsonData,
            method, disableCache, data;
        if (cors !== undefined) {
            me.setCors(cors);
        }
        // allow params to be a method that returns the params object
        if (Ext.isFunction(params)) {
            params = params.call(scope, options);
        }
        // allow url to be a method that returns the actual url
        if (Ext.isFunction(url)) {
            url = url.call(scope, options);
        }
        url = this.setupUrl(options, url);
        if (!url) {
            Ext.raise({
                options: options,
                msg: 'No URL specified'
            });
        }
        // check for xml or json data, and make sure json data is encoded
        data = options.rawData || options.binaryData || options.xmlData || jsonData || null;
        if (jsonData && !Ext.isPrimitive(jsonData)) {
            data = Ext.encode(data);
        }
        // Check for binary data. Transform if needed
        if (options.binaryData) {
            if (!Ext.isArray(options.binaryData)) {
                Ext.log.warn("Binary submission data must be an array of byte values! " + "Instead got " + typeof (options.binaryData));
            }
            if (me.nativeBinaryPostSupport()) {
                data = (new Uint8Array(options.binaryData));
                // eslint-disable-line no-undef
                if ((Ext.isChrome && Ext.chromeVersion < 22) || Ext.isSafari || Ext.isGecko) {
                    // send the underlying buffer, not the view, since that's not supported
                    // on versions of chrome older than 22
                    data = data.buffer;
                }
            }
        }
        // make sure params are a url encoded string and include any extraParams if specified
        if (Ext.isObject(params)) {
            params = Ext.Object.toQueryString(params);
        }
        if (Ext.isObject(extraParams)) {
            extraParams = Ext.Object.toQueryString(extraParams);
        }
        params = params + ((extraParams) ? ((params) ? '&' : '') + extraParams : '');
        urlParams = Ext.isObject(urlParams) ? Ext.Object.toQueryString(urlParams) : urlParams;
        params = this.setupParams(options, params);
        // decide the proper method for this request
        method = (options.method || me.getMethod() || ((params || data) ? 'POST' : 'GET')).toUpperCase();
        this.setupMethod(options, method);
        disableCache = options.disableCaching !== false ? (options.disableCaching || me.getDisableCaching()) : false;
        // if the method is get append date to prevent caching
        if (method === 'GET' && disableCache) {
            url = Ext.urlAppend(url, (options.disableCachingParam || me.getDisableCachingParam()) + '=' + (new Date().getTime()));
        }
        // if the method is get or there is json/xml data append the params to the url
        if ((method === 'GET' || data) && params) {
            url = Ext.urlAppend(url, params);
            params = null;
        }
        // allow params to be forced into the url
        if (urlParams) {
            url = Ext.urlAppend(url, urlParams);
        }
        return {
            url: url,
            method: method,
            data: data || params || null
        };
    },
    /**
     * Template method for overriding url
     * @private
     * @param {Object} options
     * @param {String} url
     * @return {String} The modified url
     */
    setupUrl: function(options, url) {
        var form = this.getForm(options);
        if (form) {
            url = url || form.action;
        }
        return url;
    },
    /**
     * Template method for overriding params
     * @private
     * @param {Object} options
     * @param {String} params
     * @return {String} The modified params
     */
    setupParams: function(options, params) {
        var form = this.getForm(options),
            serializedForm;
        if (form && !this.isFormUpload(options)) {
            serializedForm = Ext.Element.serializeForm(form);
            params = params ? (params + '&' + serializedForm) : serializedForm;
        }
        return params;
    },
    /**
     * Template method for overriding method
     * @private
     * @param {Object} options
     * @param {String} method
     * @return {String} The modified method
     */
    setupMethod: function(options, method) {
        if (this.isFormUpload(options)) {
            return 'POST';
        }
        return method;
    },
    /**
     * Determines whether this object has a request outstanding.
     *
     * @param {Object} [request] Defaults to the last transaction
     *
     * @return {Boolean} True if there is an outstanding request.
     */
    isLoading: function(request) {
        if (!request) {
            request = this.getLatest();
        }
        return request ? request.isLoading() : false;
    },
    /**
     * Aborts an active request.
     * @param {Ext.ajax.Request} [request] Defaults to the last request
     */
    abort: function(request) {
        if (!request) {
            request = this.getLatest();
        }
        if (request && request.isLoading()) {
            request.abort();
        }
    },
    /**
     * Aborts all active requests
     */
    abortAll: function() {
        var requests = this.requests,
            id;
        for (id in requests) {
            this.abort(requests[id]);
        }
    },
    /**
     * Gets the most recent request
     * @return {Object} The request. Null if there is no recent request
     * @private
     */
    getLatest: function() {
        var id = this.latestId,
            request;
        if (id) {
            request = this.requests[id];
        }
        return request || null;
    },
    /**
     * Clears the timeout on the request
     * @param {Object} request The request
     * @private
     */
    clearTimeout: function(request) {
        if (!request) {
            request = this.getLatest();
        }
        if (request) {
            request.clearTimer();
        }
    },
    onRequestComplete: function(request) {
        delete this.requests[request.id];
    },
    /**
     * @return {Boolean} `true` if the browser can natively post binary data.
     * @private
     */
    nativeBinaryPostSupport: function() {
        return Ext.isChrome || (Ext.isSafari && Ext.isDefined(window.Uint8Array)) || (Ext.isGecko && Ext.isDefined(window.Uint8Array));
    }
});

/**
 * A singleton instance of an `{@link Ext.data.Connection}`. This class is used to
 * communicate with your server side code. It can be used as follows:
 *
 *      Ext.Ajax.request({
 *          url: 'ajax_demo/sample.json',
 *
 *          success: function(response, opts) {
 *              var obj = Ext.decode(response.responseText);
 *              console.dir(obj);
 *          },
 *
 *          failure: function(response, opts) {
 *              console.log('server-side failure with status code ' + response.status);
 *          }
 *      });
 *
 * Default options for all requests can be set by changing a property on the Ext.Ajax class:
 *
 *      Ext.Ajax.setTimeout(60000); // 60 seconds
 *
 * Any options specified in the request method for the Ajax request will override any
 * defaults set on the `Ext.Ajax` singleton. In the code sample below, the timeout for the
 * request will be 60 seconds.
 *
 *      Ext.Ajax.setTimeout(120000); // 120 seconds
 *
 *      Ext.Ajax.request({
 *          url: 'page.aspx',
 *          timeout: 60000
 *      });
 *
 * In general, this class will be used for all Ajax requests in your application. The main
 * reason for creating a separate `{@link Ext.data.Connection}` is for a series of
 * requests that share common settings that are different to all other requests in the
 * application.
 */
Ext.define('Ext.Ajax', {
    extend: 'Ext.data.Connection',
    singleton: true,
    /**
     * @cfg {Object} extraParams
     * @hide
     */
    /**
     * @cfg {Object} defaultHeaders
     * @hide
     */
    /**
     * @cfg {String} method
     * @hide
     */
    /**
     * @cfg {Number} timeout
     * @hide
     */
    /**
     * @cfg {Boolean} autoAbort
     * @hide
     */
    /**
     * @cfg {Boolean} disableCaching
     * @hide
     */
    /**
     * @property {Boolean} disableCaching
     * True to add a unique cache-buster param to GET requests. Defaults to true.
     */
    /**
     * @property {String} url
     * The default URL to be used for requests to the server.
     * If the server receives all requests through one URL, setting this once is easier than
     * entering it on every request.
     */
    /**
     * @property {Object} extraParams
     * An object containing properties which are used as extra parameters to each request made
     * by this object. Session information and other data that you need
     * to pass with each request are commonly put here.
     */
    /**
     * @property {Object} defaultHeaders
     * An object containing request headers which are added to each request made by this object.
     */
    /**
     * @property {String} method
     * The default HTTP method to be used for requests. Note that this is case-sensitive and
     * should be all caps (if not set but params are present will use `POST`, otherwise will
     * use `GET`.)
     */
    /**
     * @property {Number} timeout
     * The timeout in milliseconds to be used for requests. Defaults to 30000.
     * 
     * When a request fails due to timeout the XMLHttpRequest response object will 
     * contain:
     * 
     *     timedout: true
     */
    /**
     * @property {Boolean} autoAbort
     * Whether a new request should abort any pending requests.
     */
    autoAbort: false
});

/**
 * @private
 */
Ext.define('Ext.AnimationQueue', {
    singleton: true,
    constructor: function() {
        var me = this;
        me.queue = [];
        me.taskQueue = [];
        me.runningQueue = [];
        me.idleQueue = [];
        me.isRunning = false;
        me.isIdle = true;
        me.run = me.run.bind(me);
        // iOS has a nasty bug which causes pending requestAnimationFrame to not release
        // the callback when the WebView is switched back and forth from / to being background
        // process. We use a watchdog timer to workaround this, and restore the pending state
        // correctly if this happens.
        // This timer has to be set as an interval from the very beginning and we have to keep
        // it running for as long as the app lives, setting it later doesn't seem to work.
        // The watchdog timer must be accessible for environments to cancel.
        if (Ext.os.is.iOS) {
            me.watch.$skipTimerCheck = true;
            me.watchdogTimer = Ext.interval(me.watch, 500, me);
        }
    },
    /**
     *
     * @param {Function} fn
     * @param {Object} [scope]
     * @param {Object} [args]
     */
    start: function(fn, scope, args) {
        var me = this;
        me.queue.push(arguments);
        if (!me.isRunning) {
            if (me.hasOwnProperty('idleTimer')) {
                Ext.undefer(me.idleTimer);
                delete me.idleTimer;
            }
            if (me.hasOwnProperty('idleQueueTimer')) {
                Ext.undefer(me.idleQueueTimer);
                delete me.idleQueueTimer;
            }
            me.isIdle = false;
            me.isRunning = true;
            me.startCountTime = Ext.now();
            me.count = 0;
            me.doStart();
        }
    },
    clear: function() {
        var me = this;
        Ext.undefer(me.idleTimer);
        Ext.undefer(me.idleQueueTimer);
        Ext.unraf(me.animationFrameId);
        me.idleTimer = me.idleQueueTimer = me.animationFrameId = null;
        me.queue.length = me.taskQueue.length = me.runningQueue.length = me.idleQueue.length = 0;
        me.isRunning = false;
        me.isIdle = true;
        me.startCountTime = Ext.now();
        me.count = 0;
    },
    watch: function() {
        if (this.isRunning && Ext.now() - this.lastRunTime >= 500) {
            this.run();
        }
    },
    run: function() {
        var me = this,
            queue = me.runningQueue,
            now, item, element, i, ln;
        // When asked to start or iterate, it will now create a new one
        me.animationFrameId = null;
        if (!me.isRunning) {
            return;
        }
        now = Ext.now();
        me.lastRunTime = now;
        me.frameStartTime = now;
        // We are doing cleanup here for any destroyed elements
        // this is temporary until we fix CssTransition to properly
        // inform an element that it is being animated
        // then the element, during destruction, will need to cleanup
        // the animation (see Ext.fx.runner.CssTransition#run)
        i = me.queue.length;
        while (i--) {
            item = me.queue[i];
            element = item[1] && item[1].getElement && item[1].getElement();
            if (element && element.destroyed) {
                me.queue.splice(i, 1);
            }
        }
        queue.push.apply(queue, me.queue);
        // take a snapshot of the current queue and run it
        for (i = 0 , ln = queue.length; i < ln; i++) {
            me.invoke(queue[i]);
        }
        queue.length = 0;
        /* eslint-disable-next-line vars-on-top */
        var elapse = me.frameStartTime - me.startCountTime,
            count = ++me.count;
        if (elapse >= 200) {
            me.onFpsChanged(count * 1000 / elapse, count, elapse);
            me.startCountTime = me.frameStartTime;
            me.count = 0;
        }
        if (!me.queue.length) {
            me.stop();
        }
        // Could have been stopped while invoking handlers
        if (me.isRunning) {
            me.doIterate();
        }
    },
    onFpsChanged: Ext.emptyFn,
    onStop: Ext.emptyFn,
    doStart: function() {
        if (!this.animationFrameId) {
            this.animationFrameId = Ext.raf(this.run);
        }
        this.lastRunTime = Ext.now();
    },
    doIterate: function() {
        if (!this.animationFrameId) {
            this.animationFrameId = Ext.raf(this.run);
        }
    },
    doStop: function() {
        if (this.animationFrameId) {
            Ext.unraf(this.animationFrameId);
        }
        this.animationFrameId = null;
    },
    /**
     *
     * @param {Function} fn
     * @param {Object} [scope]
     * @param {Object} [args]
     */
    stop: function(fn, scope, args) {
        var me = this,
            queue = me.queue,
            ln = queue.length,
            i, item;
        if (!me.isRunning) {
            return;
        }
        for (i = 0; i < ln; i++) {
            item = queue[i];
            if (item[0] === fn && item[1] === scope && item[2] === args) {
                queue.splice(i, 1);
                i--;
                ln--;
            }
        }
        if (ln === 0) {
            me.doStop();
            me.onStop();
            me.isRunning = false;
            if (me.idleQueue.length && !me.idleTimer) {
                me.idleTimer = Ext.defer(me.whenIdle, 100, me);
            }
        }
    },
    onIdle: function(fn, scope, args) {
        var me = this,
            listeners = me.idleQueue,
            i, ln, listener;
        for (i = 0 , ln = listeners.length; i < ln; i++) {
            listener = listeners[i];
            if (fn === listener[0] && scope === listener[1] && args === listener[2]) {
                return;
            }
        }
        listeners.push(arguments);
        if (me.isIdle) {
            me.processIdleQueue();
        } else if (!me.idleTimer) {
            me.idleTimer = Ext.defer(me.whenIdle, 100, me);
        }
    },
    unIdle: function(fn, scope, args) {
        var me = this,
            listeners = me.idleQueue,
            i, ln, listener;
        for (i = 0 , ln = listeners.length; i < ln; i++) {
            listener = listeners[i];
            if (fn === listener[0] && scope === listener[1] && args === listener[2]) {
                listeners.splice(i, 1);
                return true;
            }
        }
        if (!listeners.length && me.idleTimer) {
            Ext.undefer(me.idleTimer);
            delete me.idleTimer;
        }
        if (!listeners.length && me.idleQueueTimer) {
            Ext.undefer(me.idleQueueTimer);
            delete me.idleQueueTimer;
        }
        return false;
    },
    queueTask: function(fn, scope, args) {
        this.taskQueue.push(arguments);
        this.processTaskQueue();
    },
    dequeueTask: function(fn, scope, args) {
        var listeners = this.taskQueue,
            i, ln, listener;
        for (i = 0 , ln = listeners.length; i < ln; i++) {
            listener = listeners[i];
            if (fn === listener[0] && scope === listener[1] && args === listener[2]) {
                listeners.splice(i, 1);
                i--;
                ln--;
            }
        }
    },
    invoke: function(listener) {
        var fn = listener[0],
            scope = listener[1],
            args = listener[2];
        fn = (typeof fn === 'string' ? scope[fn] : fn);
        if (Ext.isArray(args)) {
            fn.apply(scope, args);
        } else {
            fn.call(scope, args);
        }
    },
    whenIdle: function() {
        delete this.idleTimer;
        this.isIdle = true;
        this.processIdleQueue();
    },
    processIdleQueue: function() {
        if (!this.hasOwnProperty('idleQueueTimer')) {
            this.idleQueueTimer = Ext.defer(this.processIdleQueueItem, 1, this);
        }
    },
    processIdleQueueItem: function() {
        var listeners = this.idleQueue,
            listener;
        delete this.idleQueueTimer;
        if (!this.isIdle) {
            return;
        }
        if (listeners.length > 0) {
            listener = listeners.shift();
            this.invoke(listener);
            this.processIdleQueue();
        }
    },
    processTaskQueue: function() {
        if (!this.hasOwnProperty('taskQueueTimer')) {
            this.taskQueueTimer = Ext.defer(this.processTaskQueueItem, 15, this);
        }
    },
    processTaskQueueItem: function() {
        var listeners = this.taskQueue,
            listener;
        delete this.taskQueueTimer;
        if (listeners.length > 0) {
            listener = listeners.shift();
            this.invoke(listener);
            this.processTaskQueue();
        }
    },
    /* eslint-disable-next-line comma-style */
    /**
     *
     * @param {Number} fps Frames per second.
     * @param {Number} count Actual number of frames rendered during interval.
     * @param {Number} interval Interval duration.
     */
    showFps: function() {
        var styleTpl = {
                color: 'white',
                'background-color': 'black',
                'text-align': 'center',
                'font-family': 'sans-serif',
                'font-size': '8px',
                'font-weight': 'normal',
                'font-style': 'normal',
                'line-height': '20px',
                '-webkit-font-smoothing': 'antialiased',
                'zIndex': 100000,
                position: 'absolute'
            };
        Ext.getBody().append([
            // --- Average ---
            {
                style: Ext.applyIf({
                    bottom: '50px',
                    left: 0,
                    width: '50px',
                    height: '20px'
                }, styleTpl),
                html: 'Average'
            },
            {
                style: Ext.applyIf({
                    'background-color': 'red',
                    'font-size': '18px',
                    'line-height': '50px',
                    bottom: 0,
                    left: 0,
                    width: '50px',
                    height: '50px'
                }, styleTpl),
                id: '__averageFps',
                html: '0'
            },
            // --- Min ---
            {
                style: Ext.applyIf({
                    bottom: '50px',
                    left: '50px',
                    width: '50px',
                    height: '20px'
                }, styleTpl),
                html: 'Min (Last 1k)'
            },
            {
                style: Ext.applyIf({
                    'background-color': 'orange',
                    'font-size': '18px',
                    'line-height': '50px',
                    bottom: 0,
                    left: '50px',
                    width: '50px',
                    height: '50px'
                }, styleTpl),
                id: '__minFps',
                html: '0'
            },
            // --- Max ---
            {
                style: Ext.applyIf({
                    bottom: '50px',
                    left: '100px',
                    width: '50px',
                    height: '20px'
                }, styleTpl),
                html: 'Max (Last 1k)'
            },
            {
                style: Ext.applyIf({
                    'background-color': 'maroon',
                    'font-size': '18px',
                    'line-height': '50px',
                    bottom: 0,
                    left: '100px',
                    width: '50px',
                    height: '50px'
                }, styleTpl),
                id: '__maxFps',
                html: '0'
            },
            // --- Current ---
            {
                style: Ext.applyIf({
                    bottom: '50px',
                    left: '150px',
                    width: '50px',
                    height: '20px'
                }, styleTpl),
                html: 'Current'
            },
            {
                style: Ext.applyIf({
                    'background-color': 'green',
                    'font-size': '18px',
                    'line-height': '50px',
                    bottom: 0,
                    left: '150px',
                    width: '50px',
                    height: '50px'
                }, styleTpl),
                id: '__currentFps',
                html: '0'
            }
        ]);
        Ext.AnimationQueue.resetFps();
    },
    resetFps: function() {
        var currentFps = Ext.get('__currentFps'),
            averageFps = Ext.get('__averageFps'),
            minFps = Ext.get('__minFps'),
            maxFps = Ext.get('__maxFps'),
            min = 1000,
            max = 0,
            count = 0,
            sum = 0;
        if (!currentFps) {
            return;
        }
        Ext.AnimationQueue.onFpsChanged = function(fps) {
            count++;
            if (!(count % 10)) {
                min = 1000;
                max = 0;
            }
            sum += fps;
            min = Math.min(min, fps);
            max = Math.max(max, fps);
            currentFps.setHtml(Math.round(fps));
            // All-time average since last reset.
            averageFps.setHtml(Math.round(sum / count));
            minFps.setHtml(Math.round(min));
            maxFps.setHtml(Math.round(max));
        };
    }
}, function() {
    /*
        Global FPS indicator. Add ?showfps to use in any application. Note that this REQUIRES
        true requestAnimationFrame to be accurate.
     */
    var paramsString = window.location.search.substr(1),
        paramsArray = paramsString.split("&");
    if (Ext.Array.contains(paramsArray, "showfps")) {
        Ext.onReady(this.showFps.bind(this));
    }
});

/**
 * This class makes buffered methods simple and also handles cleanup on `destroy`.
 *
 *      Ext.define('Foo', {
 *          mixins: [
 *              'Ext.mixin.Bufferable'
 *          ],
 *
 *          bufferableMethods: {
 *              // Provides a "foobar" method that calls "doFoobar" with the
 *              // most recent arguments but delayed by 50ms from the last
 *              // call. Calls to "foobar" made during the 50ms wait restart
 *              // the timer and replace the arguments.
 *
 *              foobar: 50
 *          },
 *
 *          method: function() {
 *              this.foobar(42);  // call doFoobar in 50ms
 *
 *              if (this.isFoobarPending) {
 *                  // test if "foobar" is pending
 *              }
 *
 *              this.flushFoobar();  // actually, call it now
 *
 *              this.cancelFoobar(); // or never mind
 *          },
 *
 *          doFoobar: function() {
 *              // time to do the "foobar" thing
 *          }
 *      });
 *
 * @since 6.5.0
 * @private
 */
Ext.define('Ext.mixin.Bufferable', function(Bufferable) {
    return {
        // eslint-disable-line brace-style, max-len
        extend: 'Ext.Mixin',
        mixinConfig: {
            id: 'bufferable',
            after: {
                destroy: 'cancelAllCalls'
            },
            before: {
                // The bufferables need to be destroyed before they get nulled
                $reap: 'cancelAllCalls'
            },
            extended: function(baseClass, derivedClass, classBody) {
                var bufferableMethods = classBody.bufferableMethods;
                if (bufferableMethods) {
                    delete classBody.bufferableMethods;
                    Bufferable.processClass(derivedClass, bufferableMethods);
                }
            }
        },
        afterClassMixedIn: function(targetClass) {
            Bufferable.processClass(targetClass);
        },
        privates: {
            /**
         * Cancel all pending `bufferableMethod` calls on this object.
         * @since 6.5.0
         * @private
         */
            cancelAllCalls: function() {
                var bufferables = this.bufferables,
                    name;
                if (bufferables) {
                    for (name in bufferables) {
                        bufferables[name].cancel();
                        delete bufferables[name];
                    }
                }
            },
            /**
         * Cancel a specific pending `bufferableMethod` call on this object.
         * @param {String} name The name of the buffered method to cancel.
         * @param {Boolean} invoke (private)
         * @return {Boolean} Returns `true` if a cancellation occurred.
         * @since 6.5.0
         * @private
         */
            cancelBufferedCall: function(name, invoke) {
                var bufferables = this.bufferables,
                    timer = bufferables && bufferables[name];
                if (timer) {
                    timer[invoke ? 'invoke' : 'cancel']();
                }
                return !!timer;
            },
            /**
         * Flushes a specific pending `bufferableMethod` call on this object if one is
         * pending.
         * @param {String} name The name of the buffered method to cancel.
         * @return {Boolean} Returns `true` if a flush occurred.
         * @since 6.5.0
         * @private
         */
            flushBufferedCall: function(name) {
                return this.cancelBufferedCall(name, true);
            },
            /**
         * This method initializes an instance when the first bufferable method is called.
         * It merges an instance-level `bufferableMethods` config if present. This allows
         * an instance to change the buffer timeouts, even to 0 to disable buffering.
         *
         *      Ext.create({
         *          ...
         *          bufferableMethods: {
         *              foobar: 0
         *          }
         *      });
         *
         * Note, this method cannot effect unbuffered methods. The `bufferableMethods`
         * config only instruments buffered methods when used on a class declaration.
         *
         * @return {Object}
         * @since 6.5.0
         * @private
         */
            initBufferables: function() {
                var me = this,
                    methods = me.hasOwnProperty('bufferableMethods') && me.bufferableMethods,
                    classMethods;
                if (methods) {
                    Bufferable._canonicalize(methods);
                    classMethods = me.self.prototype.bufferableMethods;
                    me.bufferableMethods = Ext.merge(Ext.clone(classMethods), methods);
                }
                return (me.bufferables = {});
            },
            /**
         * Returns `true` if a specific `bufferableMethod` is pending.
         * @param {String} name The name of the buffered method to cancel.
         * @return {Boolean}
         * @since 6.5.0
         * @private
         */
            isCallPending: function(name) {
                var bufferables = this.bufferables,
                    timer = bufferables && bufferables[name];
                return !!timer;
            },
            statics: {
                SINGLE: {
                    single: true
                },
                _canonicalize: function(methods) {
                    var t, def, s, name;
                    for (name in methods) {
                        s = Ext.String.capitalize(name);
                        def = methods[name];
                        t = typeof def;
                        if (t === 'number' || t === 'string') {
                            // method: 50
                            // method: 'asap'
                            // method: 'idle'
                            // method: 'raf'
                            methods[name] = def = {
                                delay: def
                            };
                        }
                        if (typeof (t = def.delay) === 'string') {
                            // method: {
                            //     delay: 'asap'
                            // }
                            def[t] = true;
                            delete def.delay;
                        }
                        def.capitalized = s;
                        def.name = name;
                        if (!def.fn) {
                            def.fn = 'do' + s;
                        }
                        if (!def.flag) {
                            def.flag = 'is' + s + 'Pending';
                        }
                    }
                },
                _canceller: function() {
                    var timer = this,
                        // this fn is "cancel()" on timer instances
                        id = timer.id;
                    if (id) {
                        if (timer.delay) {
                            Ext.undefer(id);
                        } else if (timer.asap) {
                            Ext.unasap(id);
                        } else if (timer.idle) {
                            Ext.un('idle', id, null, Bufferable.SINGLE);
                        } else if (timer.raf) {
                            Ext.unraf(id);
                        }
                        timer.id = null;
                    }
                    timer.args = null;
                    timer.target[timer.flag] = false;
                },
                _invoker: function() {
                    var timer = this,
                        // this fn is "invoke()" on timer instances
                        args = timer.args || Ext.emptyArray,
                        target = timer.target;
                    ++timer.invokes;
                    timer.cancel();
                    target[timer.fn].apply(target, args);
                },
                delayCall: function(target, def, args) {
                    if (target.destroying) {
                        return;
                    }
                    // eslint-disable-next-line vars-on-top
                    var bufferables = target.bufferables || target.initBufferables(),
                        name = def.name,
                        timer = bufferables[name] || (bufferables[name] = Ext.apply({
                            calls: 0,
                            invokes: 0,
                            args: null,
                            cancel: Bufferable._canceller,
                            id: null,
                            target: target,
                            invoke: Bufferable._invoker
                        }, def)),
                        delay = def.delay,
                        exec = function() {
                            if (timer.id) {
                                timer.id = null;
                                timer.invoke();
                            }
                        };
                    if (timer.id) {
                        timer.cancel();
                    }
                    timer.args = args;
                    ++timer.calls;
                    target[timer.flag] = true;
                    if (delay) {
                        timer.id = Ext.defer(exec, delay);
                    } else if (def.asap) {
                        timer.id = Ext.asap(exec);
                    } else if (def.idle) {
                        timer.id = exec;
                        Ext.on('idle', exec, null, Bufferable.SINGLE);
                    } else if (def.raf) {
                        timer.id = Ext.raf(exec);
                    } else {
                        // allow bufferableMethods: { foo: 0 } to force immediate call
                        timer.invoke();
                    }
                },
                processClass: function(cls, bufferableMethods) {
                    var proto = cls.prototype,
                        inherited = proto.bufferableMethods,
                        def, name;
                    if (bufferableMethods) {
                        // if (derived class)
                        Bufferable._canonicalize(bufferableMethods);
                        if (inherited) {
                            // If we have a derived class, it could be just adjusting the
                            // configuration, not introducing new properties, so clone the
                            // inherited config and merge on the one from the classBody.
                            inherited = Ext.merge(Ext.clone(inherited), bufferableMethods);
                        }
                        proto.bufferableMethods = inherited || bufferableMethods;
                    } else {
                        // else we are being mixed in, so the bufferableMethods on the
                        // prototype almost certainly belong to the immediate user class
                        // that is mixing us in... (leave the config on the prototype)
                        bufferableMethods = inherited;
                        Bufferable._canonicalize(bufferableMethods);
                        // prevent shape change
                        proto.bufferables = null;
                    }
                    if (bufferableMethods) {
                        for (name in bufferableMethods) {
                            if (!proto[name]) {
                                def = bufferableMethods[name];
                                Bufferable.processMethod(proto, def, Array.prototype.slice);
                            }
                        }
                    }
                },
                processMethod: function(proto, def, slice) {
                    var name = def.name,
                        cap = def.capitalized;
                    proto[name] = function() {
                        return Bufferable.delayCall(this, def, slice.call(arguments));
                    };
                    proto['cancel' + cap] = function() {
                        return this.cancelBufferedCall(name);
                    };
                    proto['flush' + cap] = function() {
                        return this.flushBufferedCall(name);
                    };
                }
            }
        }
    };
});
// statics
// privates

/**
 * Provides a registry of all Components (instances of {@link Ext.Component} or any subclass
 * thereof) on a page so that they can be easily accessed by {@link Ext.Component component}
 * {@link Ext.Component#id id} (see {@link #get}, or the convenience method
 * {@link Ext#getCmp Ext.getCmp}).
 *
 * This object also provides a registry of available Component *classes* indexed by a
 * mnemonic code known as the Component's {@link Ext.Component#xtype xtype}. The `xtype`
 * provides a way to avoid instantiating child Components when creating a full, nested
 * config object for a complete Ext page.
 *
 * A child Component may be specified simply as a *config object* as long as the correct
 * `{@link Ext.Component#xtype xtype}` is specified so that if and when the Component
 * needs rendering, the correct type can be looked up for lazy instantiation.
 * 
 * @singleton
 */
Ext.define('Ext.ComponentManager', {
    alternateClassName: 'Ext.ComponentMgr',
    singleton: true,
    mixins: [
        'Ext.mixin.Bufferable'
    ],
    count: 0,
    fixReferencesTimer: null,
    referenceRepairs: 0,
    typeName: 'xtype',
    bufferableMethods: {
        handleDocumentMouseDown: 'asap'
    },
    /**
     * @private
     */
    constructor: function(config) {
        var me = this;
        Ext.apply(me, config);
        me.all = {};
        me.byInstanceId = {};
        me.holders = {};
        me.onAvailableCallbacks = {};
    },
    /**
     * Creates a new Component from the specified config object using the config object's
     * `xtype` to determine the class to instantiate.
     *
     * @param {Object} config A configuration object for the Component you wish to create.
     * @param {String} [defaultType] The `xtype` to use if the config object does not
     * contain a `xtype`. (Optional if the config contains a `xtype`).
     * @return {Ext.Component} The newly instantiated Component.
     */
    create: function(config, defaultType) {
        if (typeof config === 'string') {
            return Ext.widget(config);
        }
        if (config.isComponent) {
            return config;
        }
        if ('xclass' in config) {
            return Ext.create(config.xclass, config);
        }
        return Ext.widget(config.xtype || defaultType, config);
    },
    /**
     * Returns an item by id.
     * @param {String} id The id of the item
     * @return {Object} The item, undefined if not found.
     */
    get: function(id) {
        return this.all[id];
    },
    register: function(component) {
        var me = this,
            id = component.getId(),
            onAvailableCallbacks = me.onAvailableCallbacks;
        if (id === undefined) {
            Ext.raise('Component id is undefined. Please ensure the component has an id.');
        }
        if (id in me.all) {
            Ext.raise('Duplicate component id "' + id + '"');
        }
        if (component.$iid in me.byInstanceId) {
            Ext.raise('Duplicate component instance id "' + component.$iid + '"');
        }
        me.all[id] = component;
        me.byInstanceId[component.$iid] = component;
        if (component.nameHolder || component.referenceHolder) {
            me.holders[id] = component;
        }
        ++me.count;
        if (!me.hasFocusListener) {
            me.installFocusListener();
        }
        onAvailableCallbacks = onAvailableCallbacks && onAvailableCallbacks[id];
        if (onAvailableCallbacks && onAvailableCallbacks.length) {
            me.notifyAvailable(component);
        }
    },
    unregister: function(component) {
        var me = this,
            all = me.all,
            byInstanceId = me.byInstanceId,
            holders = me.holders,
            id = component.getId();
        if (id in holders) {
            // Helps IE since delete may just mark the entry as "free" and not
            // release the object by clearing the entry value.
            // TODO find out when IE fixed this
            holders[id] = null;
            delete holders[id];
        }
        all[id] = null;
        delete all[id];
        id = component.$iid;
        byInstanceId[id] = null;
        delete byInstanceId[id];
        --me.count;
    },
    markReferencesDirty: function() {
        var me = this,
            holders = me.holders,
            holder, id;
        if (!Ext.referencesDirty) {
            // Clear all collections (no stale entries)
            for (id in holders) {
                holder = holders[id];
                holder.refs = holder.nameRefs = null;
                if (holder.invalidateChildDirty) {
                    holder.invalidateChildDirty();
                }
            }
            Ext.referencesDirty = true;
            me.fixReferencesTimer = Ext.asap(function() {
                me.fixReferencesTimer = null;
                me.fixReferences();
            });
        }
    },
    fixReferences: function() {
        var me = this,
            all = me.all,
            holders = me.holders,
            holder, id;
        if (Ext.referencesDirty) {
            me.fixReferencesTimer = Ext.unasap(me.fixReferencesTimer);
            // Falsy value but also !== false so we can tell we're fixing the refs
            Ext.referencesDirty = 0;
            ++me.referenceRepairs;
            for (id in holders) {
                holder = holders[id];
                if (holder.beginSyncChildDirty) {
                    holder.beginSyncChildDirty();
                }
            }
            for (id in all) {
                all[id]._fixReference();
            }
            for (id in holders) {
                holder = holders[id];
                if (holder.finishSyncChildDirty) {
                    holder.finishSyncChildDirty();
                }
            }
            Ext.referencesDirty = false;
        }
    },
    /**
     * Registers a function that will be called (a single time) when an item with the specified
     * id is added to the manager. This will happen on instantiation.
     * @param {String} id The item id
     * @param {Function} fn The callback function. Called with a single parameter, the item.
     * @param {Object} scope The scope ('this' reference) in which the callback is executed.
     * Defaults to the item.
     */
    onAvailable: function(id, fn, scope) {
        var me = this,
            callbacks = me.onAvailableCallbacks,
            all = me.all,
            item;
        if (id in all) {
            // if already an instance, callback immediately
            item = all[id];
            fn.call(scope || item, item);
        } else if (id) {
            // otherwise, queue for dispatch
            if (!Ext.isArray(callbacks[id])) {
                callbacks[id] = [];
            }
            callbacks[id].push(function(item) {
                fn.call(scope || item, item);
            });
        }
    },
    /**
    * @private
    */
    notifyAvailable: function(item) {
        var callbacks = this.onAvailableCallbacks[item && item.getId()] || [];
        while (callbacks.length) {
            (callbacks.shift())(item);
        }
    },
    /**
     * Executes the specified function once for each item in the collection.
     * @param {Function} fn The function to execute.
     * @param {String} fn.key The key of the item
     * @param {Number} fn.value The value of the item
     * @param {Number} fn.length The total number of items in the collection ** Removed
     * in 5.0 **
     * @param {Boolean} fn.return False to cease iteration.
     * @param {Object} scope The scope to execute in. Defaults to `this`.
     */
    each: function(fn, scope) {
        Ext.Object.each(this.all, fn, scope);
    },
    /**
     * Gets the number of items in the collection.
     * @return {Number} The number of items in the collection.
     */
    getCount: function() {
        return this.count;
    },
    /**
     * Returns an array of all components
     * @return {Array}
     */
    getAll: function() {
        return Ext.Object.getValues(this.all);
    },
    /**
     * Return the currently active (focused) Component
     *
     * @return {Ext.Component/null} Active Component, or null
     * @private
     */
    getActiveComponent: function() {
        return Ext.Component.from(Ext.dom.Element.getActiveElement());
    },
    // Deliver focus events to Component
    onGlobalFocus: function(info) {
        var me = this,
            event = info.event.chain(),
            infoCopy = Ext.applyIf({
                event: event
            }, info),
            to, from, ancestor, target;
        to = event.toComponent = infoCopy.toComponent = Ext.Component.from(info.toElement);
        from = event.fromComponent = infoCopy.fromComponent = Ext.Component.from(info.fromElement);
        ancestor = me.getCommonAncestor(from, to);
        // Focus moves *within* a component should not cause component focus leave/enter
        if (to !== from) {
            if (from && !from.destroyed && !from.isDestructing()) {
                if (from.handleBlurEvent) {
                    from.handleBlurEvent(infoCopy);
                }
                // Call onFocusLeave on the component axis from which focus is exiting
                for (target = from; target && target !== ancestor; target = target.getRefOwner()) {
                    if (!(target.destroyed || target.destroying)) {
                        event.type = 'focusleave';
                        target.onFocusLeave(event);
                    }
                }
            }
            if (to && !to.destroyed && !to.isDestructing()) {
                if (to.handleFocusEvent) {
                    to.handleFocusEvent(infoCopy);
                }
                // Call onFocusEnter on the component axis to which focus is entering
                for (target = to; target && target !== ancestor; target = target.getRefOwner()) {
                    event.type = 'focusenter';
                    target.onFocusEnter(event);
                }
            }
        }
        for (target = ancestor; target; target = target.getRefOwner()) {
            if (!(target.destroying || target.destroyed)) {
                target.onFocusMove(infoCopy);
            }
        }
    },
    getCommonAncestor: function(compA, compB) {
        if (compA === compB) {
            return compA;
        }
        while (compA && !(compA.isAncestor(compB) || compA === compB)) {
            compA = compA.getRefOwner();
        }
        return compA;
    },
    privates: {
        /**
         * This method reorders the DOM structure of floated components to arrange that the
         * clicked element is last of its siblings, and therefore on the visual "top" of
         * the floated component stack.
         *
         * This is a Bufferable ASAP method invoked directly from Ext.GlobalEvents.
         *
         * We need to use ASAP, not a low priority listener because we need it
         * to run *after* the browser's default response to the event has been
         * processed, ie focus consequences.
         * For example, a Dialog contains a picker field, and the picker field has
         * its floated picker open and focused.
         * The user mousedowns on another field in the dialog. The resulting
         * immediate DOM shuffle to bring the dialog above the picker results
         * in focus being silently lost.
         * @param {type} e The mousedown event
         * @private
         */
        doHandleDocumentMouseDown: function(e) {
            var floatedSelector = Ext.Widget.prototype.floatedSelector,
                targetFloated;
            // If mousedown/pointerdown/touchstart is on a floated Component, ask it to sync
            // its place in the hierarchy.
            if (floatedSelector) {
                targetFloated = Ext.Component.from(e.getTarget(floatedSelector, Ext.getBody()));
                // If the mousedown is in a floated, move it to top.
                if (targetFloated) {
                    targetFloated.toFront(true);
                }
            }
        },
        installFocusListener: function() {
            var me = this;
            Ext.on('focus', me.onGlobalFocus, me);
            me.hasFocusListener = true;
        },
        clearAll: function() {
            var me = this;
            me.all = {};
            me.byInstanceId = {};
            me.holders = {};
            me.onAvailableCallbacks = {};
        },
        /**
         * Find the Widget or Component to which the given Element belongs.
         *
         * @param {Ext.dom.Element/HTMLElement} el The element from which to start to find
         * an owning Component.
         * @param {Ext.dom.Element/HTMLElement} [limit] The element at which to stop upward
         * searching for an owning Component, or the number of Components to traverse before
         * giving up. Defaults to the document's HTML element.
         * @param {String} [selector] An optional {@link Ext.ComponentQuery} selector to
         * filter the target.
         * @return {Ext.Widget/Ext.Component} The widget, component or `null`.
         *
         * @private
         * @since 6.5.0
         */
        from: function(el, limit, selector) {
            var cache = this.all,
                depth = 0,
                target, topmost, cmpId, cmp;
            if (el && el.isEvent) {
                el = el.target;
            }
            target = Ext.getDom(el);
            if (typeof limit !== 'number') {
                topmost = Ext.getDom(limit);
                limit = Number.MAX_VALUE;
            }
            while (target && target.nodeType === 1 && depth < limit && target !== topmost) {
                cmpId = target.getAttribute('data-componentid') || target.id;
                if (cmpId) {
                    cmp = cache[cmpId];
                    if (cmp && (!selector || Ext.ComponentQuery.is(cmp, selector))) {
                        return cmp;
                    }
                    // Increment depth on every *Component* found, not Element
                    depth++;
                }
                target = target.parentNode;
            }
            return null;
        }
    }
}, function(ComponentManager) {
    // Backwards compat:
    ComponentManager.fromElement = ComponentManager.from;
    // No components yet, so nothing is dirty. We need this to be false when the first
    // component is created so that it sets our fixup timer.
    Ext.referencesDirty = false;
    Ext.fixReferences = function() {
        ComponentManager.fixReferences();
    };
    Ext.markReferencesDirty = function() {
        ComponentManager.markReferencesDirty();
    };
    /**
     * This is shorthand reference to {@link Ext.ComponentManager#get}.
     * Looks up an existing {@link Ext.Component Component} by {@link Ext.Component#id id}
     *
     * @method getCmp
     * @param {String} id The component {@link Ext.Component#id id}
     * @return {Ext.Component} The Component, `undefined` if not found, or `null` if a
     * Class was found.
     * @member Ext
     */
    Ext.getCmp = function(id) {
        return ComponentManager.get(id);
    };
    Ext.iidToCmp = function(iid) {
        return ComponentManager.byInstanceId[iid] || null;
    };
    /**
     * @private
     * @deprecated 6.6.0 Inline event handlers are deprecated
     */
    Ext.doEv = function(node, e) {
        var cmp, method, event;
        // The event here is a raw browser event, so don't pass the event directly
        // since from expects an Ext.event.Event
        cmp = Ext.Component.from(e.target);
        if (cmp && !cmp.destroying && !cmp.destroyed && cmp.getEventHandlers) {
            method = cmp.getEventHandlers()[e.type];
            if (method && cmp[method]) {
                event = new Ext.event.Event(e);
                return cmp[method](event);
            }
        }
        return true;
    };
});

/**
 * This class defines the operators that are shared by DomQuery and ComponentQuery
 * @class Ext.util.Operators
 * @private
 */
/* eslint-disable eqeqeq */
Ext.ns('Ext.util').Operators = {
    // @define Ext.util.Operators
    "=": function(a, v) {
        return a == v;
    },
    "!=": function(a, v) {
        return a != v;
    },
    "^=": function(a, v) {
        return a && a.substr(0, v.length) == v;
    },
    "$=": function(a, v) {
        return a && a.substr(a.length - v.length) == v;
    },
    "*=": function(a, v) {
        return a && a.indexOf(v) !== -1;
    },
    "%=": function(a, v) {
        return (a % v) === 0;
    },
    "|=": function(a, v) {
        return a && (a == v || a.substr(0, v.length + 1) == v + '-');
    },
    "~=": function(a, v) {
        return a && (' ' + a + ' ').indexOf(' ' + v + ' ') != -1;
    }
};

/**
 * @private
 * @class Ext.util.LruCache
 * @extend Ext.util.HashMap
 * A linked {@link Ext.util.HashMap HashMap} implementation which maintains most recently accessed
 * items at the end of the list, and purges the cache down to the most recently accessed
 * {@link #maxSize} items upon add.
 */
Ext.define('Ext.util.LruCache', {
    extend: 'Ext.util.HashMap',
    config: {
        /** 
         * @cfg {Number} maxSize
         * The maximum size the cache is allowed to grow to before further additions
         * cause removal of the least recently used entry.
         */
        maxSize: null
    },
    /**
     * @method add
     * @inheritdoc
     */
    add: function(key, newValue) {
        var me = this,
            entry, last;
        me.removeAtKey(key);
        last = me.last;
        entry = {
            prev: last,
            next: null,
            key: key,
            value: newValue
        };
        if (last) {
            // If the list is not empty, update the last entry
            last.next = entry;
        } else {
            // List is empty
            me.first = entry;
        }
        me.last = entry;
        me.callParent([
            key,
            entry
        ]);
        me.prune();
        return newValue;
    },
    /**
     * @private
     */
    insertBefore: function(key, newValue, sibling) {
        var me = this,
            existingKey, entry;
        // NOT an assignment.
        // If there is a following sibling
        // eslint-disable-next-line no-cond-assign
        if (sibling = this.map[this.findKey(sibling)]) {
            existingKey = me.findKey(newValue);
            // "new" value is in the list.
            if (existingKey) {
                me.unlinkEntry(entry = me.map[existingKey]);
            } else // Genuinely new: create an entry for it.
            {
                entry = {
                    prev: sibling.prev,
                    next: sibling,
                    key: key,
                    value: newValue
                };
            }
            if (sibling.prev) {
                entry.prev.next = entry;
            } else {
                me.first = entry;
            }
            entry.next = sibling;
            sibling.prev = entry;
            me.prune();
            return newValue;
        } else // No following sibling, it's just an add.
        {
            return me.add(key, newValue);
        }
    },
    /**
     * @method get
     * @inheritdoc
     */
    get: function(key) {
        var entry = this.map[key];
        if (entry) {
            // If it's not the end, move to end of list on get
            if (entry.next) {
                this.moveToEnd(entry);
            }
            return entry.value;
        }
    },
    /**
     * @private
     */
    removeAtKey: function(key) {
        this.unlinkEntry(this.map[key]);
        return this.callParent(arguments);
    },
    /**
     * @method clear
     * @inheritdoc
     * @param initial (private)
     */
    clear: function(initial) {
        this.first = this.last = null;
        return this.callParent([
            initial
        ]);
    },
    /**
     * @private
     */
    unlinkEntry: function(entry) {
        // Stitch the list back up.
        if (entry) {
            if (entry.next) {
                entry.next.prev = entry.prev;
            } else {
                this.last = entry.prev;
            }
            if (entry.prev) {
                entry.prev.next = entry.next;
            } else {
                this.first = entry.next;
            }
            entry.prev = entry.next = null;
        }
    },
    /**
     * @private
     */
    moveToEnd: function(entry) {
        this.unlinkEntry(entry);
        // NOT an assignment.
        // If the list is not empty, update the last entry
        // eslint-disable-next-line no-cond-assign
        if (entry.prev = this.last) {
            this.last.next = entry;
        } else // List is empty
        {
            this.first = entry;
        }
        this.last = entry;
    },
    /**
     * @private
     */
    getArray: function(isKey) {
        var arr = [],
            entry = this.first;
        while (entry) {
            arr.push(isKey ? entry.key : entry.value);
            entry = entry.next;
        }
        return arr;
    },
    /**
     * Executes the specified function once for each item in the cache.
     * Returning false from the function will cease iteration.
     *
     * By default, iteration is from least recently used to most recent.
     *
     * The paramaters passed to the function are:
     * <div class="mdetail-params"><ul>
     * <li><b>key</b> : String<p class="sub-desc">The key of the item</p></li>
     * <li><b>value</b> : Number<p class="sub-desc">The value of the item</p></li>
     * <li><b>length</b> : Number<p class="sub-desc">The total number of items in the hash</p></li>
     * </ul></div>
     * @param {Function} fn The function to execute.
     * @param {Object} scope The scope (<code>this</code> reference) to execute in. Defaults
     * to this LruCache.
     * @param {Boolean} [reverse=false] Pass <code>true</code> to iterate the list in reverse
     * (most recent first) order.
     * @return {Ext.util.LruCache} this
     */
    each: function(fn, scope, reverse) {
        var me = this,
            entry = reverse ? me.last : me.first,
            length = me.length;
        scope = scope || me;
        while (entry) {
            if (fn.call(scope, entry.key, entry.value, length) === false) {
                break;
            }
            entry = reverse ? entry.prev : entry.next;
        }
        return me;
    },
    /**
     * @private
     */
    findKey: function(value) {
        var key,
            map = this.map;
        for (key in map) {
            // Attention. Differs from subclass in that this compares the value property
            // of the entry.
            if (map.hasOwnProperty(key) && map[key].value === value) {
                return key;
            }
        }
        return undefined;
    },
    /**
     * Performs a shallow copy on this haLruCachesh.
     * @return {Ext.util.HashMap} The new hash object.
     */
    clone: function() {
        var newCache = new this.self(this.initialConfig),
            map = this.map,
            key;
        newCache.suspendEvents();
        for (key in map) {
            if (map.hasOwnProperty(key)) {
                newCache.add(key, map[key].value);
            }
        }
        newCache.resumeEvents();
        return newCache;
    },
    /**
     * Purge the least recently used entries if the maxSize has been exceeded.
     */
    prune: function() {
        var me = this,
            max = me.getMaxSize(),
            purgeCount = max ? (me.length - max) : 0;
        if (purgeCount > 0) {
            for (; me.first && purgeCount; purgeCount--) {
                me.removeAtKey(me.first.key);
            }
        }
    }
});
/**
     * @method containsKey
     * @private
     */
/**
     * @method contains
     * @private
     */
/**
     * @method getKeys
     * @private
     */
/**
     * @method getValues
     * @private
     */

/**
 * Provides searching of Components within Ext.ComponentManager (globally) or a specific
 * Ext.container.Container on the document with a similar syntax to a CSS selector.
 * Returns Array of matching Components, or empty Array.
 *
 * ## Basic Component lookup
 *
 * Components can be retrieved by using their {@link Ext.Component xtype}:
 *
 * - `component`
 * - `gridpanel`
 *
 * Matching by `xtype` matches inherited types, so in the following code, the previous field
 * *of any type which inherits from `TextField`* will be found:
 *
 *     prevField = myField.previousNode('textfield');
 *
 * To match only the exact type, pass the "shallow" flag by adding `(true)` to xtype
 * (See Component's {@link Ext.Component#isXType isXType} method):
 *
 *     prevTextField = myField.previousNode('textfield(true)');
 *
 * You can search Components by their `id` or `itemId` property, prefixed with a #:
 *
 *     #myContainer
 *
 * Component `xtype` and `id` or `itemId` can be used together to avoid possible
 * id collisions between Components of different types:
 *
 *     panel#myPanel
 *
 * When Component's `id` or `xtype` contains dots, you can escape them in your selector:
 *
 *     my\.panel#myPanel
 *
 * Keep in mind that JavaScript treats the backslash character in a special way, so you
 * need to escape it, too, in the actual code:
 *
 *     var myPanel = Ext.ComponentQuery.query('my\\.panel#myPanel');
 *
 * ## Traversing Component tree
 *
 * Components can be found by their relation to other Components. There are several
 * relationship operators, mostly taken from CSS selectors:
 *
 * - **`E F`** All descendant Components of E that match F
 * - **`E > F`** All direct children Components of E that match F
 * - **`E ^ F`** All parent Components of E that match F
 *
 * Expressions between relationship operators are matched left to right, i.e. leftmost
 * selector is applied first, then if one or more matches are found, relationship operator
 * itself is applied, then next selector expression, etc. It is possible to combine
 * relationship operators in complex selectors:
 *
 *     window[title="Input form"] textfield[name=login] ^ form > button[action=submit]
 *
 * That selector can be read this way: Find a window with title "Input form", in that
 * window find a TextField with name "login" at any depth (including subpanels and/or
 * FieldSets), then find an `Ext.form.Panel` that is a parent of the TextField, and in
 * that form find a direct child that is a button with custom property `action` set to
 * value "submit".
 *
 * Whitespace on both sides of `^` and `>` operators is non-significant, i.e. can be
 * omitted, but usually is used for clarity.
 *
 * ## Searching by Component attributes
 *
 * Components can be searched by their object property values (attributes). To do that,
 * use attribute matching expression in square brackets:
 *
 * - `component[disabled]` - matches any Component that has `disabled` property with
 * any truthy (non-empty, not `false`) value.
 * - `panel[title="Test"]` - matches any Component that has `title` property set to
 * "Test". Note that if the value does not contain spaces, the quotes are optional.
 *
 * Attributes can use any of the following operators to compare values:
 * `=`, `!=`, `^=`, `$=`, `*=`, `%=`, `|=` and `~=`.
 *
 * Prefixing the attribute name with an at sign `@` means that the property must be
 * the object's `ownProperty`, not a property from the prototype chain.
 *
 * Specifications like `[propName]` check that the property is a truthy value. To check
 * that the object has an `ownProperty` of a certain name, regardless of the value use
 * the form `[?propName]`.
 *
 * The specified value is coerced to match the type of the property found in the
 * candidate Component using {@link Ext#coerce}.
 *
 * If you need to find Components by their `itemId` property, use the `#id` form; it will
 * do the same as `[itemId=id]` but is easier to read.
 *
 * If you need to include a metacharacter like (, ), [, ], etc., in the query, escape it
 * by prefixing it with a backslash:
 *
 *      var component = Ext.ComponentQuery.query('[myProperty=\\[foo\\]]');
 *
 * ## Attribute matching operators
 *
 * The '=' operator will return the results that **exactly** match the
 * specified object property (attribute):
 *
 *     Ext.ComponentQuery.query('panel[cls=my-cls]');
 *
 * Will match the following Component:
 *
 *     Ext.create('Ext.window.Window', {
 *         cls: 'my-cls'
 *     });
 *
 * But will not match the following Component, because 'my-cls' is one value
 * among others:
 *
 *      Ext.create('Ext.panel.Panel', {
 *          cls: 'foo-cls my-cls bar-cls'
 *      });
 *
 * You can use the '~=' operator instead, it will return Components with
 * the property that **exactly** matches one of the whitespace-separated
 * values. This is also true for properties that only have *one* value:
 *
 *     Ext.ComponentQuery.query('panel[cls~=my-cls]');
 *
 * Will match both Components:
 *
 *     Ext.create('Ext.panel.Panel', {
 *         cls: 'foo-cls my-cls bar-cls'
 *     });
 *     
 *     Ext.create('Ext.window.Window', {
 *         cls: 'my-cls'
 *     });
 *
 * Generally, '=' operator is more suited for object properties other than
 * CSS classes, while '~=' operator will work best with properties that
 * hold lists of whitespace-separated CSS classes.
 *
 * The '^=' operator will return Components with specified attribute that
 * start with the passed value:
 *
 *     Ext.ComponentQuery.query('panel[title^=Sales]');
 *
 * Will match the following Component:
 *
 *     Ext.create('Ext.panel.Panel', {
 *         title: 'Sales estimate for Q4'
 *     });
 *
 * The '$=' operator will return Components with specified properties that
 * end with the passed value:
 *
 *     Ext.ComponentQuery.query('field[fieldLabel$=name]');
 *
 * Will match the following Component:
 *
 *     Ext.create('Ext.form.field.Text', {
 *         fieldLabel: 'Enter your name'
 *     });
 *
 * The '/=' operator will return Components with specified properties that
 * match the passed regular expression:
 *
 *     Ext.ComponentQuery.query('button[action/="edit|save"]');
 *
 * Will match the following Components with a custom `action` property:
 *
 *     Ext.create('Ext.button.Button', {
 *          action: 'edit'
 *     });
 *
 *     Ext.create('Ext.button.Button', {
 *          action: 'save'
 *     });
 *
 * When you need to use meta characters like [], (), etc. in your query, make sure
 * to escape them with back slashes:
 *
 *     Ext.ComponentQuery.query('panel[title="^Sales for Q\\[1-4\\]"]');
 *
 * The following test will find panels with their `ownProperty` collapsed being equal to
 * `false`. It will **not** match a collapsed property from the prototype chain.
 *
 *     Ext.ComponentQuery.query('panel[@collapsed=false]');
 *
 * Member expressions from candidate Components may be tested. If the expression returns
 * a *truthy* value, the candidate Component will be included in the query:
 *
 *     var disabledFields = myFormPanel.query("{isDisabled()}");
 *
 * Such expressions are executed in Component's context, and the above expression is
 * similar to running this snippet for every Component in your application:
 *
 *      if (component.isDisabled()) {
 *          matches.push(component);
 *      }
 *
 * It is important to use only methods that are available in **every** Component instance
 * to avoid run time exceptions. If you need to match your Components with a custom
 * condition formula, you can augment `Ext.Component` to provide custom matcher that
 * will return `false` by default, and override it in your custom classes:
 * 
 *      Ext.define('My.Component', {
 *          override: 'Ext.Component',
 *          myMatcher: function() { return false; }
 *      });
 *
 *      Ext.define('My.Panel', {
 *          extend: 'Ext.panel.Panel',
 *          requires: ['My.Component'],     // Ensure that Component override is applied
 *          myMatcher: function(selector) {
 *              return selector === 'myPanel';
 *          }
 *      });
 *
 * After that you can use a selector with your custom matcher to find all instances
 * of `My.Panel`:
 *
 *      Ext.ComponentQuery.query("{myMatcher('myPanel')}");
 *
 * However if you really need to use a custom matcher, you may find it easier to implement
 * a custom Pseudo class instead (see below).
 *
 * ## Conditional matching
 *
 * Attribute matchers can be combined to select only Components that match **all**
 * conditions (logical AND operator):
 *
 *     Ext.ComponentQuery.query('panel[cls~=my-cls][floating=true][title$="sales data"]');
 *
 * E.g., the query above will match only a Panel-descended Component that has 'my-cls'
 * CSS class *and* is floating *and* with a title that ends with "sales data".
 *
 * Expressions separated with commas will match any Component that satisfies
 * *either* expression (logical OR operator):
 *
 *     Ext.ComponentQuery.query('field[fieldLabel^=User], field[fieldLabel*=password]');
 *
 * E.g., the query above will match any field with field label starting with "User",
 * *or* any field that has "password" in its label.
 *
 * If you need to include a comma in an attribute matching expression, escape it with a
 * backslash:
 *
 *     Ext.ComponentQuery.query('field[fieldLabel^="User\\, foo"], field[fieldLabel*=password]');
 *
 * ## Pseudo classes
 *
 * Pseudo classes may be used to filter results in the same way as in
 * {@link Ext.dom.Query}. There are five default pseudo classes:
 *
 * * `not` Negates a selector.
 * * `first` Filters out all except the first matching item for a selector.
 * * `last` Filters out all except the last matching item for a selector.
 * * `focusable` Filters out all except Components which by definition and configuration are
 *      potentially able to receieve focus, and can be focused at this time. Component can be
 *      focused when it is rendered, visible, and not disabled. Some Components can be focusable
 *      even when disabled (e.g. Menu items) via their parent Container configuration.
 *      Containers such as Panels generally are not focusable by themselves but can use
 *      focus delegation (`defaultFocus` config). Some Containers such as Menus and Windows
 *      are focusable by default.
 * * `canfocus` Filters out all except Components which are curently able to receieve focus.
 *      That is, they are defined and configured focusable, and they are also visible and enabled.
 *      Note that this selector intentionally bypasses some checks done by `focusable` selector
 *      and works in a subtly different way. It is used internally by the framework and is not
 *      a replacement for `:focusable` selector.
 * * `nth-child` Filters Components by ordinal position in the selection.
 * * `scrollable` Filters out all except Components which are scrollable.
 * * `visible` Filters out hidden Components. May test deep visibility using `':visible(true)'`
 *
 * These pseudo classes can be used with other matchers or without them:
 *
 *      // Select first direct child button in any panel
 *      Ext.ComponentQuery.query('panel > button:first');
 *
 *      // Select last field in Profile form
 *      Ext.ComponentQuery.query('form[title=Profile] field:last');
 *
 *      // Find first focusable Component in a panel and focus it
 *      panel.down(':canfocus').focus();
 *
 *      // Select any field that is not hidden in a form
 *      form.query('field:not(hiddenfield)');
 *
 *      // Find last scrollable Component and reset its scroll positions.
 *      tabpanel.down(':scrollable[hideMode=display]:last').getScrollable().scrollTo(0, 0);
 *
 * Pseudo class `nth-child` can be used to find any child Component by its
 * position relative to its siblings. This class' handler takes one argument
 * that specifies the selection formula as `Xn` or `Xn+Y`:
 *
 *      // Find every odd field in a form
 *      form.query('field:nth-child(2n+1)'); // or use shortcut: :nth-child(odd)
 *
 *      // Find every even field in a form
 *      form.query('field:nth-child(2n)');   // or use shortcut: :nth-child(even)
 *
 *      // Find every 3rd field in a form
 *      form.query('field:nth-child(3n)');
 *
 * **Note:** The `nth-child` selector returns 1-based result sets.
 *
 * Pseudo classes can be combined to further filter the results, e.g., in the
 * form example above we can modify the query to exclude hidden fields:
 *
 *      // Find every 3rd non-hidden field in a form
 *      form.query('field:not(hiddenfield):nth-child(3n)');
 *
 * Note that when combining pseudo classes, whitespace is significant, i.e.
 * there should be no spaces between pseudo classes. This is a common mistake;
 * if you accidentally type a space between `field` and `:not`, the query
 * will not return any result because it will mean "find *field's children
 * Components* that are not hidden fields...".
 *
 * ## Custom pseudo classes
 *
 * It is possible to define your own custom pseudo classes. In fact, a
 * pseudo class is just a property in `Ext.ComponentQuery.pseudos` object
 * that defines pseudo class name (property name) and pseudo class handler
 * (property value):
 *
 *     // Function receives array and returns a filtered array.
 *     Ext.ComponentQuery.pseudos.invalid = function(items) {
 *         var i = 0, l = items.length, c, result = [];
 *         for (; i < l; i++) {
 *             if (!(c = items[i]).isValid()) {
 *                 result.push(c);
 *             }
 *         }
 *         return result;
 *     };
 * 
 *     var invalidFields = myFormPanel.query('field:invalid');
 *     if (invalidFields.length) {
 *         invalidFields[0].getEl().scrollIntoView(myFormPanel.body);
 *         for (var i = 0, l = invalidFields.length; i < l; i++) {
 *             invalidFields[i].getEl().frame("red");
 *         }
 *     }
 *
 * Pseudo class handlers can be even more flexible, with a selector
 * argument used to define the logic:
 *
 *      // Handler receives array of itmes and selector in parentheses
 *      Ext.ComponentQuery.pseudos.titleRegex = function(components, selector) {
 *          var i = 0, l = components.length, c, result = [], regex = new RegExp(selector);
 *          for (; i < l; i++) {
 *              c = components[i];
 *              if (c.title && regex.test(c.title)) {
 *                  result.push(c);
 *              }
 *          }
 *          return result;
 *      }
 *
 *      var salesTabs = tabPanel.query('panel:titleRegex("sales\\s+for\\s+201[123]")');
 *
 * Be careful when using custom pseudo classes with MVC Controllers: when
 * you use a pseudo class in Controller's `control` or `listen` component
 * selectors, the pseudo class' handler function will be called very often
 * and may slow down your application significantly. A good rule of thumb
 * is to always specify Component xtype with the pseudo class so that the
 * handlers are only called on Components that you need, and try to make
 * the condition checks as cheap in terms of execution time as possible.
 * Note how in the example above, handler function checks that Component
 * *has* a title first, before running regex test on it.
 *
 * ## Query examples
 *
 * Queries return an array of Components. Here are some example queries:
 *
 *     // retrieve all Ext.Panels in the document by xtype
 *     var panelsArray = Ext.ComponentQuery.query('panel');
 *
 *     // retrieve all Ext.Panels within the container with an id myCt
 *     var panelsWithinmyCt = Ext.ComponentQuery.query('#myCt panel');
 *
 *     // retrieve all direct children which are Ext.Panels within myCt
 *     var directChildPanel = Ext.ComponentQuery.query('#myCt > panel');
 *
 *     // retrieve all grids or trees
 *     var gridsAndTrees = Ext.ComponentQuery.query('gridpanel, treepanel');
 *
 *     // Focus first Component
 *     myFormPanel.child(':canfocus').focus();
 *
 *     // Retrieve every odd text field in a form
 *     myFormPanel.query('textfield:nth-child(odd)');
 *
 *     // Retrieve every even field in a form, excluding hidden fields
 *     myFormPanel.query('field:not(hiddenfield):nth-child(even)');
 *
 *     // Retrieve every scrollable in a tabpanel
 *     tabpanel.query(':scrollable');
 *
 * For easy access to queries based from a particular Container see the
 * {@link Ext.container.Container#query}, {@link Ext.container.Container#down} and
 * {@link Ext.container.Container#child} methods. Also see
 * {@link Ext.Component#up}.
 */
Ext.define('Ext.ComponentQuery', {
    singleton: true,
    requires: [
        'Ext.ComponentManager',
        'Ext.util.Operators',
        'Ext.util.LruCache'
    ]
}, function() {
    var cq = this,
        queryOperators = Ext.util.Operators,
        nthRe = /(\d*)n\+?(\d*)/,
        nthRe2 = /\D/,
        stripLeadingSpaceRe = /^(\s)+/,
        unescapeRe = /\\(.)/g,
        regexCache = new Ext.util.LruCache({
            maxSize: 100
        }),
        // A function source code pattern with a placeholder which accepts an expression which
        // yields a truth value when applied as a member on each item in the passed array.
        /* eslint-disable indent */
        filterFnPattern = [
            'var r = [],',
            'i = 0,',
            'it = items,',
            'l = it.length,',
            'c;',
            'for (; i < l; i++) {',
            'c = it[i];',
            'if (c.{0}) {',
            'r.push(c);',
            '}',
            '}',
            'return r;'
        ].join(''),
        /* eslint-enable indent */
        filterItems = function(items, operation) {
            // Argument list for the operation is [ itemsArray, operationArg1, operationArg2...]
            // The operation's method loops over each item in the candidate array and
            // returns an array of items which match its criteria
            return operation.method.apply(this, [
                items
            ].concat(operation.args));
        },
        getItems = function(items, mode) {
            var result = [],
                i = 0,
                length = items.length,
                candidate,
                deep = mode !== '>';
            for (; i < length; i++) {
                candidate = items[i];
                if (candidate.getRefItems) {
                    result = result.concat(candidate.getRefItems(deep));
                }
            }
            return result;
        },
        getAncestors = function(items) {
            var result = [],
                i = 0,
                length = items.length,
                candidate;
            for (; i < length; i++) {
                candidate = items[i];
                while (!!(candidate = candidate.getRefOwner())) {
                    result.push(candidate);
                }
            }
            return result;
        },
        // Filters the passed candidate array and returns only items which match the passed xtype
        filterByXType = function(items, xtype, shallow) {
            if (xtype === '*') {
                return items.slice();
            } else {
                /* eslint-disable-next-line vars-on-top */
                var result = [],
                    i = 0,
                    length = items.length,
                    candidate;
                for (; i < length; i++) {
                    candidate = items[i];
                    if (!candidate.destroyed && candidate.isXType(xtype, shallow)) {
                        result.push(candidate);
                    }
                }
                return result;
            }
        },
        // Filters the passed candidate array and returns only items which have the specified
        // property match
        filterByAttribute = function(items, property, operator, compareTo) {
            var result = [],
                length = items.length,
                mustBeOwnProperty, presenceOnly, candidate, propValue, config, i, j, propLen;
            // Prefixing property name with an @ means that the property must be in the candidate,
            // not in its prototype
            if (property.charAt(0) === '@') {
                mustBeOwnProperty = true;
                property = property.substr(1);
            }
            if (property.charAt(0) === '?') {
                mustBeOwnProperty = true;
                presenceOnly = true;
                property = property.substr(1);
            }
            for (i = 0; i < length; i++) {
                candidate = items[i];
                // If the candidate is a product of the Ext class system, then
                // use the configurator to call getters to access the property.
                // CQ can be used to filter raw Objects.
                config = candidate.self && candidate.self.getConfigurator && candidate.self.$config.configs[property];
                if (config) {
                    propValue = candidate[config.names.get]();
                } else if (mustBeOwnProperty && !candidate.hasOwnProperty(property)) {
                    
                    continue;
                } else {
                    propValue = candidate[property];
                }
                if (presenceOnly) {
                    result.push(candidate);
                }
                // implies property is an array, and we must compare value against each element.
                else if (operator === '~=') {
                    if (propValue) {
                        // We need an array
                        if (!Ext.isArray(propValue)) {
                            propValue = propValue.split(' ');
                        }
                        for (j = 0 , propLen = propValue.length; j < propLen; j++) {
                            /* eslint-disable-next-line max-len */
                            if (queryOperators[operator](Ext.coerce(propValue[j], compareTo), compareTo)) {
                                result.push(candidate);
                                break;
                            }
                        }
                    }
                } else if (operator === '/=') {
                    if (propValue != null && compareTo.test(propValue)) {
                        result.push(candidate);
                    }
                }
                /* eslint-disable-next-line max-len */
                else if (!compareTo ? !!propValue : queryOperators[operator](Ext.coerce(propValue, compareTo), compareTo)) {
                    result.push(candidate);
                }
            }
            return result;
        },
        // Filters the passed candidate array and returns only items which have the specified
        // itemId or id
        filterById = function(items, id, idOnly) {
            var result = [],
                i = 0,
                length = items.length,
                candidate, check;
            for (; i < length; i++) {
                candidate = items[i];
                check = idOnly ? candidate.id : candidate.getItemId();
                if (check === id) {
                    result.push(candidate);
                }
            }
            return result;
        },
        // Filters the passed candidate array and returns only items which the named pseudo class
        // matcher filters in
        filterByPseudo = function(items, name, value) {
            return cq.pseudos[name](items, value);
        },
        // Determines leading mode
        // > for direct child, and ^ to switch to ownerCt axis
        /* eslint-disable-next-line no-useless-escape */
        modeRe = /^(\s?([>\^])\s?|\s|$)/,
        // Matches a token with possibly (true|false) appended for the "shallow" parameter
        /* eslint-disable-next-line no-useless-escape */
        tokenRe = /^(#)?((?:\\\.|[\w\-])+|\*)(?:\((true|false)\))?/,
        matchers = [
            {
                // Checks for .xtype with possibly (true|false) appended for the "shallow" parameter
                /* eslint-disable-next-line no-useless-escape */
                re: /^\.((?:\\\.|[\w\-])+)(?:\((true|false)\))?/,
                method: filterByXType,
                argTransform: function(args) {
                    var selector = args[0];
                    Ext.log.warn('"' + selector + '" ComponentQuery selector style is deprecated,' + ' use "' + selector.replace(/^\./, '') + '" without the leading dot instead');
                    if (args[1] !== undefined) {
                        args[1] = args[1].replace(unescapeRe, '$1');
                    }
                    return args.slice(1);
                }
            },
            {
                // Allow [@attribute] to check truthy ownProperty
                // Allow [?attribute] to check for presence of ownProperty
                // Allow [$attribute]
                // Checks for @|?|$ -> word/hyphen chars -> any special attribute selector characters
                // before the '=', etc. It strips out whitespace.
                // For example:
                //     [attribute=value], [attribute^=value], [attribute$=value], [attribute*=value],
                //     [attribute~=value], [attribute%=value], [attribute!=value]
                /* eslint-disable-next-line max-len, no-useless-escape */
                re: /^(?:\[((?:[@?$])?[\w\-]*)\s*(?:([\^$*~%!\/]?=)\s*(['"])?((?:\\\]|.)*?)\3)?(?!\\)\])/,
                method: filterByAttribute,
                argTransform: function(args) {
                    var selector = args[0],
                        property = args[1],
                        operator = args[2],
                        compareTo = args[4],
                        compareRe;
                    // Unescape the attribute value matcher first
                    if (compareTo !== undefined) {
                        compareTo = compareTo.replace(unescapeRe, '$1');
                        /* eslint-disable-next-line vars-on-top */
                        var format = Ext.String.format,
                            msg = "ComponentQuery selector '{0}' has an unescaped ({1}) character " + "at the {2} of the attribute value pattern. Usually that indicates " + "an error where the opening quote is not followed by the closing " + "quote. If you need to match a ({1}) character at the {2} of the " + "attribute value, escape the quote character in your pattern: " + "(\\{1})",
                            match;
                        /* eslint-disable-next-line no-cond-assign */
                        if (match = /^(['"]).*?[^'"]$/.exec(compareTo)) {
                            Ext.log.warn(format(msg, selector, match[1], 'beginning'));
                        }
                        /* eslint-disable-next-line no-cond-assign */
                        else if (match = /^[^'"].*?(['"])$/.exec(compareTo)) {
                            Ext.log.warn(format(msg, selector, match[1], 'end'));
                        }
                    }
                    if (operator === '/=') {
                        compareRe = regexCache.get(compareTo);
                        if (compareRe) {
                            compareTo = compareRe;
                        } else {
                            compareTo = regexCache.add(compareTo, new RegExp(compareTo));
                        }
                    }
                    return [
                        property,
                        operator,
                        compareTo
                    ];
                }
            },
            {
                // checks for #cmpItemId
                /* eslint-disable-next-line no-useless-escape */
                re: /^#((?:\\\.|[\w\-])+)/,
                method: filterById
            },
            {
                // checks for :<pseudo_class>(<selector>)
                /* eslint-disable-next-line no-useless-escape */
                re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/,
                method: filterByPseudo,
                argTransform: function(args) {
                    if (args[2] !== undefined) {
                        args[2] = args[2].replace(unescapeRe, '$1');
                    }
                    return args.slice(1);
                }
            },
            {
                // checks for {<member_expression>}
                /* eslint-disable-next-line no-useless-escape */
                re: /^(?:\{([^\}]+)\})/,
                method: filterFnPattern
            }
        ];
    // Internal class Ext.ComponentQuery.Query
    cq.Query = Ext.extend(Object, {
        constructor: function(cfg) {
            cfg = cfg || {};
            Ext.apply(this, cfg);
        },
        // Executes this Query upon the selected root.
        // The root provides the initial source of candidate Component matches which are
        // progressively filtered by iterating through this Query's operations cache.
        // If no root is provided, all registered Components are searched via the ComponentManager.
        // root may be a Container who's descendant Components are filtered
        // root may be a Component with an implementation of getRefItems which provides some nested
        // Components such as the docked items within a Panel.
        // root may be an array of candidate Components to filter using this Query.
        execute: function(root) {
            var operations = this.operations,
                result = [],
                op, i, len;
            for (i = 0 , len = operations.length; i < len; i++) {
                op = operations[i];
                result = result.concat(this._execute(root, op));
            }
            return result;
        },
        _execute: function(root, operations) {
            var i = 0,
                length = operations.length,
                operation, workingItems;
            // no root, use all Components in the document
            if (!root) {
                workingItems = Ext.ComponentManager.getAll();
            }
            // Root is an iterable object like an Array, or system Collection, eg HtmlCollection
            else if (Ext.isIterable(root)) {
                workingItems = root;
            }
            // Root is a MixedCollection
            else if (root.isMixedCollection) {
                workingItems = root.items;
            }
            // We are going to loop over our operations and take care of them
            // one by one.
            for (; i < length; i++) {
                operation = operations[i];
                // The mode operation requires some custom handling.
                // All other operations essentially filter down our current
                // working items, while mode replaces our current working
                // items by getting children from each one of our current
                // working items. The type of mode determines the type of
                // children we get. (e.g. > only gets direct children)
                if (operation.mode === '^') {
                    workingItems = getAncestors(workingItems || [
                        root
                    ]);
                } else if (operation.mode) {
                    workingItems = getItems(workingItems || [
                        root
                    ], operation.mode);
                } else {
                    workingItems = filterItems(workingItems || getItems([
                        root
                    ]), operation);
                }
                // If this is the last operation, it means our current working
                // items are the final matched items. Thus return them!
                if (i === length - 1) {
                    return workingItems;
                }
            }
            return [];
        },
        is: function(component, root) {
            var operations = this.operations,
                result = false,
                len = operations.length,
                op, i;
            if (len === 0) {
                return true;
            }
            for (i = 0; i < len; i++) {
                op = operations[i];
                result = this._is(component, root, op);
                if (result) {
                    return result;
                }
            }
            return false;
        },
        _is: function(component, root, operations) {
            var len = operations.length,
                active = [
                    component
                ],
                operation, i, j, mode, items, item;
            // Loop backwards, since we're going up the hierarchy
            for (i = len - 1; i >= 0; --i) {
                operation = operations[i];
                mode = operation.mode;
                // Traversing hierarchy
                if (mode) {
                    if (mode === '^') {
                        active = getItems(active, ' ');
                    } else if (mode === '>') {
                        items = [];
                        for (j = 0 , len = active.length; j < len; ++j) {
                            item = active[j].getRefOwner();
                            if (item) {
                                items.push(item);
                            }
                        }
                        active = items;
                    } else {
                        active = getAncestors(active);
                    }
                } else {
                    active = filterItems(active, operation);
                }
                // After traversing the hierarchy, if we have no items, jump out
                if (active.length === 0) {
                    return false;
                }
            }
            // We don't push these on as operations because we don't want to mutate the
            // array, but this is essentially a continuation of the loop above.
            if (root) {
                if (!mode) {
                    // Last operation wasn't a mode operation, so navigate up to find
                    // ancestors
                    active = getAncestors(active);
                }
                if (active.length > 0) {
                    // If we have active items, check the root exists there to ensure we're
                    // part of the tree
                    active = filterItems(active, {
                        method: filterById,
                        args: [
                            root.id,
                            true
                        ]
                    });
                }
                if (active.length === 0) {
                    return false;
                }
            }
            return true;
        },
        getMatches: function(components, operations) {
            var len = operations.length,
                i;
            for (i = 0; i < len; ++i) {
                components = filterItems(components, operations[i]);
                // Performance enhancement, if we have nothing, we can
                // never add anything new, so jump out
                if (components.length === 0) {
                    break;
                }
            }
            return components;
        },
        isMultiMatch: function() {
            return this.operations.length > 1;
        }
    });
    Ext.apply(cq, {
        /**
         * @private
         * Cache of selectors and matching ComponentQuery.Query objects
         */
        cache: new Ext.util.LruCache({
            maxSize: 100
        }),
        /**
         * @private
         * Cache of pseudo class filter functions
         */
        pseudos: {
            not: function(components, selector) {
                var i = 0,
                    length = components.length,
                    results = [],
                    index = -1,
                    component;
                for (; i < length; ++i) {
                    component = components[i];
                    if (!cq.is(component, selector)) {
                        results[++index] = component;
                    }
                }
                return results;
            },
            first: function(components) {
                var ret = [];
                if (components.length > 0) {
                    ret.push(components[0]);
                }
                return ret;
            },
            last: function(components) {
                var len = components.length,
                    ret = [];
                if (len > 0) {
                    ret.push(components[len - 1]);
                }
                return ret;
            },
            // This filters for components which by definition and configuration are
            // theoretically focusable. It does not take into account the current app state.
            focusable: function(cmps) {
                var len = cmps.length,
                    results = [],
                    i = 0,
                    c;
                for (; i < len; i++) {
                    c = cmps[i];
                    if (c.isFocusable && c.isFocusable()) {
                        results.push(c);
                    }
                }
                return results;
            },
            // This filters for components which are currently able to recieve focus.
            canfocus: function(cmps, value) {
                var len = cmps.length,
                    results = [],
                    i = 0,
                    c;
                for (; i < len; i++) {
                    c = cmps[i];
                    if (c.canFocus && c.canFocus(false, value)) {
                        results.push(c);
                    }
                }
                return results;
            },
            "nth-child": function(c, a) {
                var result = [],
                    m, f, i, len, n, nodeIndex;
                m = nthRe.exec(a === "even" && "2n" || a === "odd" && "2n+1" || !nthRe2.test(a) && "n+" + a || a);
                f = (m[1] || 1) - 0;
                len = m[2] - 0;
                /* eslint-disable-next-line no-cond-assign */
                for (i = 0; n = c[i]; i++) {
                    nodeIndex = i + 1;
                    if (f === 1) {
                        if (len === 0 || nodeIndex === len) {
                            result.push(n);
                        }
                    } else if ((nodeIndex + len) % f === 0) {
                        result.push(n);
                    }
                }
                return result;
            },
            scrollable: function(cmps) {
                var len = cmps.length,
                    results = [],
                    i = 0,
                    c;
                for (; i < len; i++) {
                    c = cmps[i];
                    // Note that modern toolkit prefixes with an underscore.
                    if (c.scrollable || c._scrollable) {
                        results.push(c);
                    }
                }
                return results;
            },
            visible: function(cmps, deep) {
                var len = cmps.length,
                    results = [],
                    i = 0,
                    c;
                deep = deep === 'true';
                for (; i < len; i++) {
                    c = cmps[i];
                    // Note that modern toolkit prefixes with an underscore.
                    if (c.isVisible(deep)) {
                        results.push(c);
                    }
                }
                return results;
            }
        },
        /**
         * Returns an array of matched Components from within the passed root object.
         *
         * This method filters returned Components in a similar way to how CSS selector based DOM
         * queries work using a textual selector string.
         *
         * See class summary for details.
         *
         * @param {String} selector The selector string to filter returned Components.
         * @param {Ext.container.Container} [root] The Container within which to perform the query.
         * If omitted, all Components within the document are included in the search.
         *
         * This parameter may also be an array of Components to filter according to the selector.
         * @return {Ext.Component[]} The matched Components.
         *
         * @member Ext.ComponentQuery
         */
        query: function(selector, root) {
            // An empty query will match every Component
            if (!selector) {
                return Ext.ComponentManager.getAll();
            }
            /* eslint-disable-next-line vars-on-top */
            var results = [],
                noDupResults = [],
                dupMatcher = {},
                query = cq.cache.get(selector),
                resultsLn, cmp, i;
            if (!query) {
                query = cq.cache.add(selector, cq.parse(selector));
            }
            results = query.execute(root);
            // multiple selectors, potential to find duplicates
            // lets filter them out.
            if (query.isMultiMatch()) {
                resultsLn = results.length;
                for (i = 0; i < resultsLn; i++) {
                    cmp = results[i];
                    if (!dupMatcher[cmp.id]) {
                        noDupResults.push(cmp);
                        dupMatcher[cmp.id] = true;
                    }
                }
                results = noDupResults;
            }
            return results;
        },
        /**
         * Traverses the tree rooted at the passed root in pre-order mode, calling the passed
         * function on the nodes at each level. That is the function is called upon each node
         * **before** being called on its children).
         *
         * For an object to be queryable, it must implement the `getRefItems` method which returns
         * all immediate child items.
         *
         * This method is used at each level down the cascade. Currently
         * {@link Ext.Component Component}s and {@link Ext.data.TreeModel TreeModel}s are queryable.
         *
         * If you have tree-structured data, you can make your nodes queryable, and use
         * ComponentQuery on them.
         *
         * @param {Object} selector A ComponentQuery selector used to filter candidate nodes before
         * calling the function. An empty string matches any node.
         * @param {String} root The root queryable object to start from.
         * @param {Function} fn The function to call. Return `false` to abort the traverse.
         * @param {Object} fn.node The node being visited.
         * @param {Object} [scope] The context (`this` reference) in which the function is executed.
         * @param {Array} [extraArgs] A set of arguments to be appended to the function's argument
         * list to pass down extra data known to the caller **after** the node being visited.
         */
        visitPreOrder: function(selector, root, fn, scope, extraArgs) {
            cq._visit(true, selector, root, fn, scope, extraArgs);
        },
        /**
         * Traverses the tree rooted at the passed root in post-order mode, calling the passed
         * function on the nodes at each level. That is the function is called upon each node
         * **after** being called on its children).
         *
         * For an object to be queryable, it must implement the `getRefItems` method which returns
         * all immediate child items.
         *
         * This method is used at each level down the cascade. Currently
         * {@link Ext.Component Component}s and {@link Ext.data.TreeModel TreeModel}s are queryable.
         *
         * If you have tree-structured data, you can make your nodes queryable, and use
         * ComponentQuery on them.
         *
         * @param {Object} selector A ComponentQuery selector used to filter candidate nodes
         * before calling the function. An empty string matches any node.
         * @param {String} root The root queryable object to start from.
         * @param {Function} fn The function to call. Return `false` to abort the traverse.
         * @param {Object} fn.node The node being visited.
         * @param {Object} [scope] The context (`this` reference) in which the function is executed.
         * @param {Array} [extraArgs] A set of arguments to be appended to the function's argument
         * list to pass down extra data known to the caller **after** the node being visited.
         */
        visitPostOrder: function(selector, root, fn, scope, extraArgs) {
            cq._visit(false, selector, root, fn, scope, extraArgs);
        },
        /**
         * @private
         * Visit implementation which handles both preOrder and postOrder modes.
         */
        _visit: function(preOrder, selector, root, fn, scope, extraArgs) {
            var query = cq.cache.get(selector),
                callArgs = [
                    root
                ],
                children,
                len = 0,
                i, rootMatch;
            if (!query) {
                query = cq.cache.add(selector, cq.parse(selector));
            }
            rootMatch = query.is(root);
            if (root.getRefItems) {
                children = root.getRefItems();
                len = children.length;
            }
            // append optional extraArgs
            if (extraArgs) {
                Ext.Array.push(callArgs, extraArgs);
            }
            if (preOrder) {
                if (rootMatch) {
                    if (fn.apply(scope || root, callArgs) === false) {
                        return false;
                    }
                }
            }
            for (i = 0; i < len; i++) {
                /* eslint-disable-next-line max-len */
                if (cq._visit.call(cq, preOrder, selector, children[i], fn, scope, extraArgs) === false) {
                    return false;
                }
            }
            if (!preOrder) {
                if (rootMatch) {
                    if (fn.apply(scope || root, callArgs) === false) {
                        return false;
                    }
                }
            }
        },
        /**
         * Tests whether the passed Component matches the selector string.
         * An empty selector will always match.
         *
         * @param {Ext.Component} component The Component to test
         * @param {String/Function} selector The selector string to test against.
         * Or a filter function which returns `true` if the component matches.
         * @param {Ext.Component} [root=null] The root component.
         * @return {Boolean} True if the Component matches the selector.
         * @member Ext.ComponentQuery
         */
        is: function(component, selector, root) {
            var query;
            if (!selector) {
                return true;
            }
            if (typeof selector === 'function') {
                return selector(component);
            } else {
                query = cq.cache.get(selector);
                if (!query) {
                    query = cq.cache.add(selector, cq.parse(selector));
                }
                return query.is(component, root);
            }
        },
        parse: function(selector) {
            var operations = [],
                selectors, sel, i, len;
            selectors = Ext.splitAndUnescape(selector, ',');
            for (i = 0 , len = selectors.length; i < len; i++) {
                // Trim the whitespace as the parser expects it.
                sel = Ext.String.trim(selectors[i]);
                // Usually, a dangling comma at the end of a selector means a typo.
                // In that case, the last sel value will be an empty string; the parser
                // will silently ignore it which is not good, so we throw an error here.
                if (sel === '') {
                    Ext.raise('Invalid ComponentQuery selector: ""');
                }
                operations.push(cq._parse(sel));
            }
            //  Now that we have all our operations in an array, we are going
            // to create a new Query using these operations.
            return new cq.Query({
                operations: operations
            });
        },
        _parse: function(selector) {
            var operations = [],
                trim = Ext.String.trim,
                length = matchers.length,
                lastSelector, tokenMatch, token, matchedChar, modeMatch, selectorMatch, transform, i, matcher, method, args;
            // We are going to parse the beginning of the selector over and
            // over again, slicing off the selector any portions we converted into an
            // operation, until it is an empty string.
            while (selector && lastSelector !== selector) {
                lastSelector = selector;
                // First we check if we are dealing with a token like #, * or an xtype
                tokenMatch = selector.match(tokenRe);
                if (tokenMatch) {
                    matchedChar = tokenMatch[1];
                    token = trim(tokenMatch[2]).replace(unescapeRe, '$1');
                    // If the token is prefixed with a # we push a filterById operation to our stack
                    if (matchedChar === '#') {
                        operations.push({
                            method: filterById,
                            args: [
                                token
                            ]
                        });
                    } else // If the token is a * or an xtype string, we push a filterByXType
                    // operation to the stack.
                    {
                        operations.push({
                            method: filterByXType,
                            args: [
                                token,
                                Boolean(tokenMatch[3])
                            ]
                        });
                    }
                    // Now we slice of the part we just converted into an operation
                    selector = selector.replace(tokenMatch[0], '').replace(stripLeadingSpaceRe, '$1');
                }
                // If the next part of the query is not a space or > or ^, it means we
                // are going to check for more things that our current selection
                // has to comply to.
                while (!(modeMatch = selector.match(modeRe))) {
                    // Lets loop over each type of matcher and execute it
                    // on our current selector.
                    for (i = 0; selector && i < length; i++) {
                        matcher = matchers[i];
                        selectorMatch = selector.match(matcher.re);
                        method = matcher.method;
                        transform = matcher.argTransform;
                        // If we have a match, add an operation with the method
                        // associated with this matcher, and pass the regular
                        // expression matches are arguments to the operation.
                        if (selectorMatch) {
                            // Transform function will do unescaping and additional checks
                            if (transform) {
                                args = transform(selectorMatch);
                            } else {
                                args = selectorMatch.slice(1);
                            }
                            operations.push({
                                method: Ext.isString(matcher.method) ? // Turn a string method into a function by formatting the
                                // string with our selector matche expression
                                // A new method is created for different match expressions,
                                // eg {id=='textfield-1024'}
                                // Every expression may be different in different selectors.
                                /* eslint-disable-next-line max-len */
                                Ext.functionFactory('items', Ext.String.format.apply(Ext.String, [
                                    method
                                ].concat(selectorMatch.slice(1)))) : matcher.method,
                                args: args
                            });
                            selector = selector.replace(selectorMatch[0], '').replace(stripLeadingSpaceRe, '$1');
                            break;
                        }
                        // Break on match
                        // Exhausted all matches: It's an error
                        if (i === (length - 1)) {
                            Ext.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"');
                        }
                    }
                }
                // Now we are going to check for a mode change. This means a space
                // or a > to determine if we are going to select all the children
                // of the currently matched items, or a ^ if we are going to use the
                // ownerCt axis as the candidate source.
                if (modeMatch[1]) {
                    // Assignment, and test for truthiness!
                    operations.push({
                        mode: modeMatch[2] || modeMatch[1]
                    });
                    // When we have consumed the mode character, clean up leading spaces
                    selector = selector.replace(modeMatch[0], '').replace(stripLeadingSpaceRe, '');
                }
            }
            return operations;
        }
    });
    /**
     * Same as {@link Ext.ComponentQuery#query}.
     * @param {String} selector The selector string to filter returned Components.
     * @param {Ext.container.Container} [root] The Container within which to perform the query.
     * If omitted, all Components within the document are included in the search.
     *
     * This parameter may also be an array of Components to filter according to the selector.
     * @return {Ext.Component[]} The matched Components.
     * @method all
     * @member Ext
     */
    Ext.all = function() {
        return cq.query.apply(cq, arguments);
    };
    /**
     * Returns the first match to the given component query.
     * See {@link Ext.ComponentQuery#query}.
     * @param {String} selector The selector string to filter returned Component.
     * @param {Ext.container.Container} [root] The Container within which to perform the query.
     * If omitted, all Components within the document are included in the search.
     *
     * This parameter may also be an array of Components to filter according to the selector.
     * @return {Ext.Component} The first matched Component or `null`.
     * @method first
     * @member Ext
     */
    Ext.first = function() {
        var matches = cq.query.apply(cq, arguments);
        return (matches && matches[0]) || null;
    };
});

/**
 * @private
 */
Ext.define('Ext.Evented', {
    alternateClassName: 'Ext.EventedBase',
    mixins: [
        'Ext.mixin.Observable'
    ],
    initialized: false,
    constructor: function(config) {
        // Base constructor is overriden for testing
        this.callParent();
        this.mixins.observable.constructor.call(this, config);
        this.initialized = true;
    },
    onClassExtended: function(cls, data) {
        if (!data.hasOwnProperty('eventedConfig')) {
            return;
        }
        /* eslint-disable-next-line vars-on-top */
        var config = data.config,
            eventedConfig = data.eventedConfig,
            name, cfg;
        if (config) {
            Ext.applyIf(config, eventedConfig);
        } else {
            cls.addConfig(eventedConfig);
        }
        /*
         * These are generated setters for eventedConfig
         *
         * If the component is initialized, it invokes fireAction to fire the event as well,
         * which indicate something has changed. Otherwise, it just executes the action
         * (happens during initialization)
         *
         * This is helpful when we only want the event to be fired for subsequent changes.
         * Also it's a major performance improvement for instantiation when fired events
         * are mostly useless since there's no listeners
         */
        // TODO: Move this into Observable
        for (name in eventedConfig) {
            if (eventedConfig.hasOwnProperty(name)) {
                cfg = Ext.Config.get(name);
                data[cfg.names.set] = cfg.eventedSetter || cfg.getEventedSetter();
            }
        }
    }
});

/**
 * This mixin provides a common interface for objects that can be positioned, e.g.
 * {@link Ext.Component Components} and {@link Ext.dom.Element Elements}
 * @private
 */
Ext.define('Ext.util.Positionable', {
    mixinId: 'positionable',
    _positionTopLeft: [
        'position',
        'top',
        'left'
    ],
    // Stub implementation called after positioning.
    // May be implemented in subclasses. Component has an implementation.
    // Hardware acceleration due to the transform:translateZ(0) flickering
    // when painting clipped elements. This class allows that to be turned off
    // while elements are in a clipped state.
    clippedCls: Ext.baseCSSPrefix + 'clipped',
    afterSetPosition: Ext.emptyFn,
    // ***********************
    // Begin Abstract Methods
    // ***********************
    /**
     * Gets the x,y coordinates of an element specified by the anchor position on the
     * element.
     * @param {Ext.dom.Element} el The element
     * @param {String} [anchor='tl'] The specified anchor position.
     * See {@link #alignTo} for details on supported anchor positions.
     * @param {Boolean} [local] True to get the local (element top/left-relative) anchor
     * position instead of page coordinates
     * @param {Object} [size] An object containing the size to use for calculating anchor
     * position {width: (target width), height: (target height)} (defaults to the
     * element's current size)
     * @return {Number[]} [x, y] An array containing the element's x and y coordinates
     * @private
     */
    getAnchorToXY: function() {
        Ext.raise("getAnchorToXY is not implemented in " + this.$className);
    },
    /**
     * Returns the size of the element's borders and padding.
     * @return {Object} an object with the following numeric properties
     * - beforeX
     * - afterX
     * - beforeY
     * - afterY
     * @private
     */
    getBorderPadding: function() {
        Ext.raise("getBorderPadding is not implemented in " + this.$className);
    },
    /**
     * Returns the x coordinate of this element reletive to its `offsetParent`.
     * @return {Number} The local x coordinate
     */
    getLocalX: function() {
        Ext.raise("getLocalX is not implemented in " + this.$className);
    },
    /**
     * Returns the x and y coordinates of this element relative to its `offsetParent`.
     * @return {Number[]} The local XY position of the element
     */
    getLocalXY: function() {
        Ext.raise("getLocalXY is not implemented in " + this.$className);
    },
    /**
     * Returns the y coordinate of this element reletive to its `offsetParent`.
     * @return {Number} The local y coordinate
     */
    getLocalY: function() {
        Ext.raise("getLocalY is not implemented in " + this.$className);
    },
    /**
     * Gets the current X position of the DOM element based on page coordinates.
     * @return {Number} The X position of the element
     */
    getX: function() {
        Ext.raise("getX is not implemented in " + this.$className);
    },
    /**
     * Gets the current position of the DOM element based on page coordinates.
     * @return {Number[]} The XY position of the element
     */
    getXY: function() {
        Ext.raise("getXY is not implemented in " + this.$className);
    },
    /**
     * Gets the current Y position of the DOM element based on page coordinates.
     * @return {Number} The Y position of the element
     */
    getY: function() {
        Ext.raise("getY is not implemented in " + this.$className);
    },
    /**
     * Sets the local x coordinate of this element using CSS style. When used on an
     * absolute positioned element this method is symmetrical with {@link #getLocalX}, but
     * may not be symmetrical when used on a relatively positioned element.
     * @param {Number} x The x coordinate. A value of `null` sets the left style to 'auto'.
     * @return {Ext.util.Positionable} this
     */
    setLocalX: function() {
        Ext.raise("setLocalX is not implemented in " + this.$className);
    },
    /**
     * Sets the local x and y coordinates of this element using CSS style. When used on an
     * absolute positioned element this method is symmetrical with {@link #getLocalXY}, but
     * may not be symmetrical when used on a relatively positioned element.
     * @param {Number/Array} x The x coordinate or an array containing [x, y]. A value of
     * `null` sets the left style to 'auto'
     * @param {Number} [y] The y coordinate, required if x is not an array. A value of
     * `null` sets the top style to 'auto'
     * @return {Ext.util.Positionable} this
     */
    setLocalXY: function() {
        Ext.raise("setLocalXY is not implemented in " + this.$className);
    },
    /**
     * Sets the local y coordinate of this element using CSS style. When used on an
     * absolute positioned element this method is symmetrical with {@link #getLocalY}, but
     * may not be symmetrical when used on a relatively positioned element.
     * @param {Number} y The y coordinate. A value of `null` sets the top style to 'auto'.
     * @return {Ext.util.Positionable} this
     */
    setLocalY: function() {
        Ext.raise("setLocalY is not implemented in " + this.$className);
    },
    /**
     * Sets the X position of the DOM element based on page coordinates.
     * @param {Number} x The X position
     * @return {Ext.util.Positionable} this
     */
    setX: function() {
        Ext.raise("setX is not implemented in " + this.$className);
    },
    /**
     * Sets the position of the DOM element in page coordinates.
     * @param {Number[]} pos Contains X & Y [x, y] values for new position (coordinates
     * are page-based)
     * @return {Ext.util.Positionable} this
     */
    setXY: function() {
        Ext.raise("setXY is not implemented in " + this.$className);
    },
    /**
     * Sets the Y position of the DOM element based on page coordinates.
     * @param {Number} y The Y position
     * @return {Ext.util.Positionable} this
     */
    setY: function() {
        Ext.raise("setY is not implemented in " + this.$className);
    },
    // ***********************
    // End Abstract Methods
    // ***********************
    // TODO: currently only used by ToolTip. does this method belong here?
    /**
     * @private
     */
    adjustForConstraints: function(xy, parent) {
        var vector = this.getConstrainVector(parent, xy);
        if (vector) {
            xy[0] += vector[0];
            xy[1] += vector[1];
        }
        return xy;
    },
    /**
     * Aligns the element with another element relative to the specified anchor points. If
     * the other element is the document it aligns it to the viewport. The position
     * parameter is optional, and can be specified in any one of the following formats:
     *
     * - **Blank**: Defaults to aligning the element's top-left corner to the target's
     *   bottom-left corner ("tl-bl").
     * - **Two anchors**: If two values from the table below are passed separated by a dash,
     *   the first value is used as the element's anchor point, and the second value is
     *   used as the target's anchor point.
     * - **Two edge/offset descriptors:** An edge/offset descriptor is an edge initial
     *   (`t`/`r`/`b`/`l`) followed by a percentage along that side. This describes a
     *   point to align with a similar point in the target. So `'t0-b0'` would be
     *   the same as `'tl-bl'`, `'l0-r50'` would place the top left corner of this item
     *   halfway down the right edge of the target item. This allows more flexibility
     *   and also describes which two edges are considered adjacent when positioning a tip pointer. 
     *
     * Following are all of the supported predefined anchor positions:
     *
     *      Value  Description
     *      -----  -----------------------------
     *      tl     The top left corner
     *      t      The center of the top edge
     *      tr     The top right corner
     *      l      The center of the left edge
     *      c      The center
     *      r      The center of the right edge
     *      bl     The bottom left corner
     *      b      The center of the bottom edge
     *      br     The bottom right corner
     *
     * You can put a '?' at the end of the alignment string to constrain the positioned element
     * to the {@link Ext.Viewport Viewport}. The element will attempt to align as specified, but
     * the position will be adjusted to constrain to the viewport if necessary. Note that
     * the element being aligned might be swapped to align to a different position than that
     * specified in order to enforce the viewport constraints.
     *
     * Example Usage:
     *
     *     // align el to other-el using the default positioning
     *     // ("tl-bl", non-constrained)
     *     el.alignTo("other-el");
     *
     *     // align the top left corner of el with the top right corner of other-el
     *     // (constrained to viewport)
     *     el.alignTo("other-el", "tl-tr?");
     *
     *     // align the bottom right corner of el with the center left edge of other-el
     *     el.alignTo("other-el", "br-l?");
     *
     *     // align the center of el with the bottom left corner of other-el and
     *     // adjust the x position by -6 pixels (and the y position by 0)
     *     el.alignTo("other-el", "c-bl", [-6, 0]);
     *
     *     // align the 25% point on the bottom edge of this el
     *     // with the 75% point on the top edge of other-el.
     *     el.alignTo("other-el", 'b25-t75');
     *
     * @param {Ext.util.Positionable/HTMLElement/String} element The Positionable,
     * HTMLElement, or id of the element to align to.
     * @param {String} [position="tl-bl?"] The position to align to
     * @param {Number[]} [offsets] Offset the positioning by [x, y]
     * Element animation config object
     * @param {Boolean} animate (private)
     * @return {Ext.util.Positionable} this
     */
    alignTo: function(element, position, offsets, animate) {
        var me = this,
            el = me.el;
        return me.setXY(me.getAlignToXY(element, position, offsets), el.anim && !!animate ? el.anim(animate) : false);
    },
    /**
     * Calculates x,y coordinates specified by the anchor position on the element, adding
     * extraX and extraY values.
     * @param {String} [anchor='tl'] The specified anchor position.
     * See {@link #alignTo} for details on supported anchor positions.
     * @param {Number} [extraX] value to be added to the x coordinate
     * @param {Number} [extraY] value to be added to the y coordinate
     * @param {Object} [size] An object containing the size to use for calculating anchor
     * position {width: (target width), height: (target height)} (defaults to the
     * element's current size) 
     * @return {Number[]} [x, y] An array containing the element's x and y coordinates
     * @private
     */
    calculateAnchorXY: function(anchor, extraX, extraY, size) {
        var region = this.getRegion();
        region.setPosition(0, 0);
        region.translateBy(extraX || 0, extraY || 0);
        if (size) {
            region.setWidth(size.width);
            region.setHeight(size.height);
        }
        return region.getAnchorPoint(anchor);
    },
    /**
     * This function converts a legacy alignment string such as 't-b' into a
     * pair of edge, offset objects which describe the alignment points of
     * the two regions.
     *
     * So tl-br becomes {myEdge:'t', offset:0}, {otherEdge:'b', offset:100}
     *
     * This not only allows more flexibility in the alignment possibilities,
     * but it also resolves any ambiguity as to chich two edges are desired
     * to be adjacent if an anchor pointer is required.
     * @private
     */
    convertPositionSpec: function(posSpec) {
        return Ext.util.Region.getAlignInfo(posSpec);
    },
    /**
     * Gets the x,y coordinates to align this element with another element. See
     * {@link #alignTo} for more info on the supported position values.
     * @param {Ext.util.Positionable/HTMLElement/String} alignToEl The Positionable,
     * HTMLElement, or id of the element to align to.
     * @param {String} [position="tl-bl?"] The position to align to
     * @param {Number[]} [offsets] Offset the positioning by [x, y]
     * @return {Number[]} [x, y]
     */
    getAlignToXY: function(alignToEl, position, offsets) {
        var newRegion = this.getAlignToRegion(alignToEl, position, offsets);
        return [
            newRegion.x,
            newRegion.y
        ];
    },
    getAlignToRegion: function(alignToEl, posSpec, offset, minHeight) {
        var me = this,
            inside, newRegion, bodyScroll;
        alignToEl = Ext.fly(alignToEl.el || alignToEl);
        if (!alignToEl || !alignToEl.dom) {
            Ext.raise({
                sourceClass: 'Ext.util.Positionable',
                sourceMethod: 'getAlignToXY',
                msg: 'Attempted to align an element that doesn\'t exist'
            });
        }
        posSpec = me.convertPositionSpec(posSpec);
        // If position spec ended with a "?" or "!", then constraining is necessary
        if (posSpec.constrain) {
            // Constrain to the correct enclosing object:
            // If the assertive form was used (like "tl-bl!"), constrain to the alignToEl.
            if (posSpec.constrain === '!') {
                inside = alignToEl;
            } else {
                // Otherwise, attempt to use the constrainTo property.
                // Otherwise, if we are a Component, there will be a container property.
                // Otherwise, use this Positionable's element's parent node.
                inside = me.constrainTo || me.container || me.el.parent();
            }
            inside = Ext.fly(inside.el || inside).getConstrainRegion();
        }
        if (alignToEl === Ext.getBody()) {
            bodyScroll = alignToEl.getScroll();
            offset = [
                bodyScroll.left,
                bodyScroll.top
            ];
        }
        newRegion = me.getRegion().alignTo({
            target: alignToEl.getRegion(),
            inside: inside,
            minHeight: minHeight,
            offset: offset,
            align: posSpec,
            axisLock: true
        });
        return newRegion;
    },
    /**
     * Gets the x,y coordinates specified by the anchor position on the element.
     * @param {String} [anchor='tl'] The specified anchor position.
     * See {@link #alignTo} for details on supported anchor positions.
     * @param {Boolean} [local] True to get the local (element top/left-relative) anchor
     * position instead of page coordinates
     * @param {Object} [size] An object containing the size to use for calculating anchor
     * position {width: (target width), height: (target height)} (defaults to the
     * element's current size)
     * @return {Number[]} [x, y] An array containing the element's x and y coordinates
     */
    getAnchorXY: function(anchor, local, size) {
        var me = this,
            region = me.getRegion(),
            el = me.el,
            isViewport = el.dom.nodeName === 'BODY' || el.dom.nodeType === 9,
            scroll = el.getScroll();
        if (local) {
            region.setPosition(0, 0);
        } else if (isViewport) {
            region.setPosition(scroll.left, scroll.top);
        }
        if (size) {
            region.setWidth(size.width);
            region.setHeight(size.height);
        }
        return region.getAnchorPoint(anchor);
    },
    /**
     * Return an object defining the area of this Element which can be passed to
     * {@link #setBox} to set another Element's size/location to match this element.
     *
     * @param {Boolean} [contentBox] If true a box for the content of the element is
     * returned.
     * @param {Boolean} [local] If true the element's left and top relative to its
     * `offsetParent` are returned instead of page x/y.
     * @return {Object} An object in the format
     * @return {Number} return.x The element's X position.
     * @return {Number} return.y The element's Y position.
     * @return {Number} return.width The element's width.
     * @return {Number} return.height The element's height.
     * @return {Number} return.bottom The element's lower bound.
     * @return {Number} return.right The element's rightmost bound.
     *
     * The returned object may also be addressed as an Array where index 0 contains the X
     * position and index 1 contains the Y position. The result may also be used for
     * {@link #setXY}
     */
    getBox: function(contentBox, local) {
        var me = this,
            xy = local ? me.getLocalXY() : me.getXY(),
            x = xy[0],
            y = xy[1],
            w, h, borderPadding, beforeX, beforeY;
        // Document body or document is special case
        if (me.el.dom.nodeName === 'BODY' || me.el.dom.nodeType === 9) {
            w = Ext.Element.getViewportWidth();
            h = Ext.Element.getViewportHeight();
        } else {
            w = me.getWidth();
            h = me.getHeight();
        }
        if (contentBox) {
            borderPadding = me.getBorderPadding();
            beforeX = borderPadding.beforeX;
            beforeY = borderPadding.beforeY;
            x += beforeX;
            y += beforeY;
            w -= (beforeX + borderPadding.afterX);
            h -= (beforeY + borderPadding.afterY);
        }
        return {
            x: x,
            left: x,
            0: x,
            y: y,
            top: y,
            1: y,
            width: w,
            height: h,
            right: x + w,
            bottom: y + h
        };
    },
    /**
     * Calculates the new [x,y] position to move this Positionable into a constrain region.
     *
     * By default, this Positionable is constrained to be within the container it was added to,
     * or the element it was rendered to.
     *
     * Priority is given to constraining the top and left within the constraint.
     *
     * An alternative constraint may be passed.
     * @param {String/HTMLElement/Ext.dom.Element/Ext.util.Region} [constrainTo] The Element
     * or {@link Ext.util.Region Region} into which this Component is to be constrained.
     * Defaults to the element into which this Positionable was rendered, or this Component's
     * {@link Ext.Component#constrainTo}.
     * @param {Number[]} [proposedPosition] A proposed `[X, Y]` position to test for validity
     * and to coerce into constraints instead of using this Positionable's current position.
     * @param {Boolean} [local] The proposedPosition is local *(relative to floatParent
     * if a floating Component)*
     * @param {Number[]} [proposedSize] A proposed `[width, height]` size to use when calculating
     * constraints instead of using this Positionable's current size.
     * @return {Number[]} **If** the element *needs* to be translated, the new `[X, Y]` position
     * within constraints if possible, giving priority to keeping the top and left edge
     * in the constrain region. Otherwise, `false`.
     * @private
     */
    calculateConstrainedPosition: function(constrainTo, proposedPosition, local, proposedSize) {
        var me = this,
            vector,
            fp = me.floatParent,
            parentNode = fp ? fp.getTargetEl() : null,
            parentOffset, borderPadding, proposedConstrainPosition,
            xy = false;
        if (local && fp) {
            parentOffset = parentNode.getXY();
            borderPadding = parentNode.getBorderPadding();
            parentOffset[0] += borderPadding.beforeX;
            parentOffset[1] += borderPadding.beforeY;
            if (proposedPosition) {
                proposedConstrainPosition = [
                    proposedPosition[0] + parentOffset[0],
                    proposedPosition[1] + parentOffset[1]
                ];
            }
        } else {
            proposedConstrainPosition = proposedPosition;
        }
        // Calculate the constrain vector to coerce our position to within our
        // constrainTo setting. getConstrainVector will provide a default constraint
        // region if there is no explicit constrainTo, *and* there is no floatParent
        // owner Component.
        constrainTo = constrainTo || me.constrainTo || parentNode || me.container || me.el.parent();
        if (local && proposedConstrainPosition) {
            proposedConstrainPosition = me.reverseTranslateXY(proposedConstrainPosition);
        }
        vector = ((me.constrainHeader && me.header.rendered) ? me.header : me).getConstrainVector(constrainTo, proposedConstrainPosition, proposedSize);
        // false is returned if no movement is needed
        if (vector) {
            xy = proposedPosition || me.getPosition(local);
            xy[0] += vector[0];
            xy[1] += vector[1];
        }
        return xy;
    },
    /**
     * Returns the content region of this element for purposes of constraining or clipping floating
     * children.  That is the region within the borders and scrollbars, but not within the padding.
     *
     * @return {Ext.util.Region} A Region containing "top, left, bottom, right" properties.
     */
    getConstrainRegion: function() {
        var me = this,
            el = me.el,
            isBody = el.dom.nodeName === 'BODY',
            dom = el.dom,
            borders = el.getBorders(),
            pos = el.getXY(),
            left = pos[0] + borders.beforeX,
            top = pos[1] + borders.beforeY,
            scroll, width, height;
        // For the body we want to do some special logic.
        if (isBody) {
            scroll = el.getScroll();
            left = scroll.left;
            top = scroll.top;
            width = Ext.Element.getViewportWidth();
            height = Ext.Element.getViewportHeight();
        } else {
            width = dom.clientWidth;
            height = dom.clientHeight;
        }
        return new Ext.util.Region(top, left + width, top + height, left);
    },
    /**
     * Returns the `[X, Y]` vector by which this Positionable's element must be translated to make
     * a best attempt to constrain within the passed constraint. Returns `false` if the element
     * does not need to be moved.
     *
     * Priority is given to constraining the top and left within the constraint.
     *
     * The constraint may either be an existing element into which the element is to be
     * constrained, or a {@link Ext.util.Region Region} into which this element is to be
     * constrained.
     *
     * By default, any extra shadow around the element is **not** included in the constrain
     * calculations - the edges of the element are used as the element bounds. To constrain
     * the shadow within the constrain region, set the `constrainShadow` property on this element
     * to `true`.
     *
     * @param {Ext.util.Positionable/HTMLElement/String/Ext.util.Region} [constrainTo] The
     * Positionable, HTMLElement, element id, or Region into which the element is to be
     * constrained.
     * @param {Number[]} [proposedPosition] A proposed `[X, Y]` position to test for validity
     * and to produce a vector for instead of using the element's current position
     * @param {Number[]} [proposedSize] A proposed `[width, height]` size to constrain
     * instead of using the element's current size
     * @return {Number[]/Boolean} **If** the element *needs* to be translated, an `[X, Y]`
     * vector by which this element must be translated. Otherwise, `false`.
     */
    getConstrainVector: function(constrainTo, proposedPosition, proposedSize) {
        var me = this,
            thisRegion = me.getRegion(),
            vector = [
                0,
                0
            ],
            shadowSize = (me.shadow && me.constrainShadow && !me.shadowDisabled) ? me.el.shadow.getShadowSize() : undefined,
            overflowed = false,
            constraintInsets = me.constraintInsets;
        if (!(constrainTo instanceof Ext.util.Region)) {
            constrainTo = Ext.get(constrainTo.el || constrainTo);
            // getConstrainRegion uses clientWidth and clientHeight.
            // so it will clear any scrollbars.
            constrainTo = constrainTo.getConstrainRegion();
        }
        // Apply constraintInsets
        if (constraintInsets) {
            constraintInsets = Ext.isObject(constraintInsets) ? constraintInsets : Ext.Element.parseBox(constraintInsets);
            constrainTo.adjust(constraintInsets.top, constraintInsets.right, constraintInsets.bottom, constraintInsets.left);
        }
        // Shift this region to occupy the proposed position
        if (proposedPosition) {
            thisRegion.translateBy(proposedPosition[0] - thisRegion.x, proposedPosition[1] - thisRegion.y);
        }
        // Set the size of this region to the proposed size
        if (proposedSize) {
            thisRegion.right = thisRegion.left + proposedSize[0];
            thisRegion.bottom = thisRegion.top + proposedSize[1];
        }
        // Reduce the constrain region to allow for shadow
        if (shadowSize) {
            constrainTo.adjust(shadowSize[0], -shadowSize[1], -shadowSize[2], shadowSize[3]);
        }
        // Constrain the X coordinate by however much this Element overflows
        if (thisRegion.right > constrainTo.right) {
            overflowed = true;
            vector[0] = (constrainTo.right - thisRegion.right);
        }
        // overflowed the right
        if (thisRegion.left + vector[0] < constrainTo.left) {
            overflowed = true;
            vector[0] = (constrainTo.left - thisRegion.left);
        }
        // overflowed the left
        // Constrain the Y coordinate by however much this Element overflows
        if (thisRegion.bottom > constrainTo.bottom) {
            overflowed = true;
            vector[1] = (constrainTo.bottom - thisRegion.bottom);
        }
        // overflowed the bottom
        if (thisRegion.top + vector[1] < constrainTo.top) {
            overflowed = true;
            vector[1] = (constrainTo.top - thisRegion.top);
        }
        // overflowed the top
        return overflowed ? vector : false;
    },
    /**
      * Returns the offsets of this element from the passed element. The element must both
      * be part of the DOM tree and not have display:none to have page coordinates.
      * @param {Ext.util.Positionable/HTMLElement/String} offsetsTo The Positionable,
      * HTMLElement, or element id to get get the offsets from.
      * @return {Number[]} The XY page offsets (e.g. `[100, -200]`)
      */
    getOffsetsTo: function(offsetsTo) {
        var o = this.getXY(),
            e = offsetsTo.isRegion ? [
                offsetsTo.x,
                offsetsTo.y
            ] : Ext.fly(offsetsTo.el || offsetsTo).getXY();
        return [
            o[0] - e[0],
            o[1] - e[1]
        ];
    },
    /**
     * Returns a region object that defines the area of this element.
     * @param {Boolean} [contentBox] If true a box for the content of the element is
     * returned.
     * @param {Boolean} [local] If true the element's left and top relative to its
     * `offsetParent` are returned instead of page x/y.
     * @return {Ext.util.Region} A Region containing "top, left, bottom, right" properties.
     */
    getRegion: function(contentBox, local) {
        var box = this.getBox(contentBox, local);
        return new Ext.util.Region(box.top, box.right, box.bottom, box.left);
    },
    /**
     * Returns a region object that defines the client area of this element.
     *
     * That is, the area *within* any scrollbars.
     * @return {Ext.util.Region} A Region containing "top, left, bottom, right" properties.
     */
    getClientRegion: function() {
        var me = this,
            el = me.el,
            dom = el.dom,
            viewContentBox = me.getBox(true),
            scrollbarHeight = dom.offsetHeight > dom.clientHeight,
            scrollbarWidth = dom.offsetWidth > dom.clientWidth,
            padding, scrollSize, isRTL;
        if (scrollbarHeight || scrollbarWidth) {
            scrollSize = Ext.getScrollbarSize();
            // Capture width taken by any vertical scrollbar.
            // If there is a vertical scrollbar, shrink the box.
            if (scrollbarWidth) {
                scrollbarWidth = scrollSize.width;
                isRTL = el.getStyle('direction') === 'rtl' && !Ext.supports.rtlVertScrollbarOnRight;
                if (isRTL) {
                    padding = el.getPadding('l');
                    viewContentBox.left -= padding + Math.max(padding, scrollbarWidth);
                } else {
                    padding = el.getPadding('r');
                    viewContentBox.right += padding - Math.max(padding, scrollbarWidth);
                }
            }
            // Capture height taken by any horizontal scrollbar.
            // If there is a horizontal scrollbar, shrink the box.
            if (scrollbarHeight) {
                scrollbarHeight = scrollSize.height;
                padding = el.getPadding('b');
                viewContentBox.bottom += padding - Math.max(padding, scrollbarHeight);
            }
        }
        // The client region excluding any scrollbars.
        return new Ext.util.Region(viewContentBox.top, viewContentBox.right, viewContentBox.bottom, viewContentBox.left);
    },
    /**
     * Returns the **content** region of this element. That is the region within the borders
     * and padding.
     * @return {Ext.util.Region} A Region containing "top, left, bottom, right" member data.
     */
    getViewRegion: function() {
        var me = this,
            el = me.el,
            isBody = el.dom.nodeName === 'BODY',
            borderPadding, scroll, pos, top, left, width, height;
        // For the body we want to do some special logic
        if (isBody) {
            scroll = el.getScroll();
            left = scroll.left;
            top = scroll.top;
            width = Ext.Element.getViewportWidth();
            height = Ext.Element.getViewportHeight();
        } else {
            borderPadding = me.getBorderPadding();
            pos = me.getXY();
            left = pos[0] + borderPadding.beforeX;
            top = pos[1] + borderPadding.beforeY;
            width = me.getWidth(true);
            height = me.getHeight(true);
        }
        return new Ext.util.Region(top, left + width, top + height, left);
    },
    /**
     * Move the element relative to its current position.
     * @param {String} direction Possible values are:
     *
     * - `"l"` (or `"left"`)
     * - `"r"` (or `"right"`)
     * - `"t"` (or `"top"`, or `"up"`)
     * - `"b"` (or `"bottom"`, or `"down"`)
     *
     * @param {Number} distance How far to move the element in pixels
     * @param {Boolean} animate (private)
     */
    move: function(direction, distance, animate) {
        var me = this,
            xy = me.getXY(),
            x = xy[0],
            y = xy[1],
            left = [
                x - distance,
                y
            ],
            right = [
                x + distance,
                y
            ],
            top = [
                x,
                y - distance
            ],
            bottom = [
                x,
                y + distance
            ],
            hash = {
                l: left,
                left: left,
                r: right,
                right: right,
                t: top,
                top: top,
                up: top,
                b: bottom,
                bottom: bottom,
                down: bottom
            };
        direction = direction.toLowerCase();
        me.setXY([
            hash[direction][0],
            hash[direction][1]
        ], animate);
    },
    /**
     * Sets the element's box.
     * @param {Object} box The box to fill {x, y, width, height}
     * @return {Ext.util.Positionable} this
     */
    setBox: function(box) {
        var me = this,
            x, y;
        if (box.isRegion) {
            box = {
                x: box.left,
                y: box.top,
                width: box.right - box.left,
                height: box.bottom - box.top
            };
        }
        me.constrainBox(box);
        x = box.x;
        y = box.y;
        // Position to the contrained position
        // Call setSize *last* so that any possible layout has the last word on position.
        me.setXY([
            x,
            y
        ]);
        me.setSize(box.width, box.height);
        me.afterSetPosition(x, y);
        return me;
    },
    /**
     * @private
     */
    constrainBox: function(box) {
        var me = this,
            constrainedPos, x, y;
        if (me.constrain || me.constrainHeader) {
            x = ('x' in box) ? box.x : box.left;
            y = ('y' in box) ? box.y : box.top;
            constrainedPos = me.calculateConstrainedPosition(null, [
                x,
                y
            ], false, [
                box.width,
                box.height
            ]);
            // If it *needs* constraining, change the position
            if (constrainedPos) {
                box.x = constrainedPos[0];
                box.y = constrainedPos[1];
            }
        }
    },
    /**
     * Translates the passed page coordinates into left/top css values for the element
     * @param {Number/Array} x The page x or an array containing [x, y]
     * @param {Number} [y] The page y, required if x is not an array
     * @return {Object} An object with left and top properties. e.g.
     * {left: (value), top: (value)}
     */
    translatePoints: function(x, y) {
        var pos = this.translateXY(x, y);
        return {
            left: pos.x,
            top: pos.y
        };
    },
    /**
     * Translates the passed page coordinates into x and y css values for the element
     * @param {Number/Array} x The page x or an array containing [x, y]
     * @param {Number} [y] The page y, required if x is not an array
     * @return {Object} An object with x and y properties. e.g.
     * {x: (value), y: (value)}
     * @private
     */
    translateXY: function(x, y) {
        var me = this,
            el = me.el,
            styles = el.getStyle(me._positionTopLeft),
            relative = styles.position === 'relative',
            left = parseFloat(styles.left),
            top = parseFloat(styles.top),
            xy = me.getXY();
        if (Ext.isArray(x)) {
            y = x[1];
            x = x[0];
        }
        if (isNaN(left)) {
            left = relative ? 0 : el.dom.offsetLeft;
        }
        if (isNaN(top)) {
            top = relative ? 0 : el.dom.offsetTop;
        }
        left = (typeof x === 'number') ? x - xy[0] + left : undefined;
        top = (typeof y === 'number') ? y - xy[1] + top : undefined;
        return {
            x: left,
            y: top
        };
    },
    /**
     * Converts local coordinates into page-level coordinates
     * @param {Number[]} xy The local x and y coordinates
     * @return {Number[]} The translated coordinates
     * @private
     */
    reverseTranslateXY: function(xy) {
        var coords = xy,
            el = this.el,
            dom = el.dom,
            offsetParent = dom.offsetParent,
            relative, offsetParentXY, x, y;
        if (offsetParent) {
            relative = el.isStyle('position', 'relative');
            offsetParentXY = Ext.fly(offsetParent).getXY();
            x = xy[0] + offsetParentXY[0] + offsetParent.clientLeft;
            y = xy[1] + offsetParentXY[1] + offsetParent.clientTop;
            if (relative) {
                // relative positioned elements sit inside the offsetParent's padding,
                // while absolute positioned element sit just inside the border
                x += el.getPadding('l');
                y += el.getPadding('t');
            }
            coords = [
                x,
                y
            ];
        }
        return coords;
    },
    privates: {
        /**
         * Clips this Component/Element to fit within the passed element's or component's view area
         * @param {Ext.Component/Ext.Element/Ext.util.Region} clippingEl The Component or element
         * or Region which should clip this element even if this element is outside the bounds
         * of that region.
         * @param {Number} sides The sides to clip 1=top, 2=right, 4=bottom, 8=left.
         *
         * This is to support components being clipped to their logical owner, such as a grid row
         * editor when the row being edited scrolls out of sight. The editor should be clipped
         * at the edge of the scrolling element.
         * @private
         */
        clipTo: function(clippingEl, sides) {
            var clippingRegion,
                el = this.el,
                floaterRegion = el.getRegion(),
                overflow, i,
                clipValues = [],
                clippedCls = this.clippedCls,
                clipStyle, clipped, shadow;
            // Allow a Region to be passed
            if (clippingEl.isRegion) {
                clippingRegion = clippingEl;
            } else {
                // eslint-disable-next-line max-len
                clippingRegion = (clippingEl.isComponent ? clippingEl.el : Ext.fly(clippingEl)).getConstrainRegion();
            }
            // Default to clipping all round.
            if (!sides) {
                sides = 15;
            }
            // Calculate how much all sides exceed the clipping region
            if (sides & 1 && (overflow = clippingRegion.top - floaterRegion.top) > 0) {
                clipValues[0] = overflow;
                clipped = true;
            } else {
                clipValues[0] = -10000;
            }
            if (sides & 2 && (overflow = floaterRegion.right - clippingRegion.right) > 0) {
                clipValues[1] = Math.max(0, el.getWidth() - overflow);
                clipped = true;
            } else {
                clipValues[1] = 10000;
            }
            if (sides & 4 && (overflow = floaterRegion.bottom - clippingRegion.bottom) > 0) {
                clipValues[2] = Math.max(0, el.getHeight() - overflow);
                clipped = true;
            } else {
                clipValues[2] = 10000;
            }
            if (sides & 8 && (overflow = clippingRegion.left - floaterRegion.left) > 0) {
                clipValues[3] = overflow;
                clipped = true;
            } else {
                clipValues[3] = -10000;
            }
            clipStyle = 'rect(';
            for (i = 0; i < 4; ++i) {
                // Use the clipValue if there is one calculated.
                // If not, top and left must be 0px, right and bottom must be 'auto'.
                clipStyle += Ext.Element.addUnits(clipValues[i], 'px');
                clipStyle += (i === 3) ? ')' : ',';
            }
            el.dom.style.clip = clipStyle;
            // hardware acceleration causes flickering problems on clipped elements.
            // disable it while an element is clipped.
            el.addCls(clippedCls);
            // Clip/unclip shadow too.
            // TODO: As SOON as IE8 retires, refactor Ext.dom.Shadow to use CSS3BoxShadow directly
            // on its el Then we won't have to bother clipping the shadow as well. We'll just
            // have to adjust the clipping on the element outwards in the unclipped dimensions
            // to keep the shadow visible.
            if ((shadow = el.shadow) && (el = shadow.el) && el.dom) {
                clipValues[2] -= shadow.offsets.y;
                clipValues[3] -= shadow.offsets.x;
                clipStyle = 'rect(';
                for (i = 0; i < 4; ++i) {
                    // Use the clipValue if there is one calculated.
                    // If not, clear the edges by 10px to allow the shadow's spread to be visible.
                    clipStyle += Ext.Element.addUnits(clipValues[i], 'px');
                    clipStyle += (i === 3) ? ')' : ',';
                }
                el.dom.style.clip = clipStyle;
                // Clip does not work on IE8 shadows
                // TODO: As SOON as IE8 retires, refactor Ext.dom.Shadow to use CSS3BoxShadow
                // directly on its el
                if (clipped && !Ext.supports.CSS3BoxShadow) {
                    el.dom.style.display = 'none';
                } else {
                    el.dom.style.display = '';
                    // hardware acceleration causes flickering problems on clipped elements.
                    // disable it while an element is clipped.
                    el.addCls(clippedCls);
                }
            }
        },
        /**
         * Clears any clipping applied to this component by {@link #method-clipTo}.
         * @private
         */
        clearClip: function() {
            var el = this.el,
                clippedCls = this.clippedCls;
            el.dom.style.clip = Ext.isIE8 ? 'auto' : '';
            // hardware acceleration causes flickering problems on clipped elements.
            // re-enable it when an element is unclipped.
            el.removeCls(clippedCls);
            // unclip shadow too.
            if (el.shadow && el.shadow.el && el.shadow.el.dom) {
                el.shadow.el.dom.style.clip = Ext.isIE8 ? 'auto' : '';
                // Clip does not work on IE8 shadows
                // TODO: As SOON as IE8 retires, refactor Ext.dom.Shadow to use CSS3BoxShadow
                // directly on its el
                if (!Ext.supports.CSS3BoxShadow) {
                    el.dom.style.display = '';
                    // hardware acceleration causes flickering problems on clipped elements.
                    // re-enable it when an element is unclipped.
                    el.removeCls(clippedCls);
                }
            }
        }
    }
});

/**
 * @class Ext.util.Positionable
 */
Ext.define('Ext.overrides.util.Positionable', {
    override: 'Ext.util.Positionable',
    /**
     * @method alignTo
     * @param {Ext.util.Positionable/HTMLElement/String} anchorToEl The Positionable,
     * HTMLElement, or id of the element to align to.
     * @param {String} [alignment="tl-bl?"] The position to align to
     * @param {Number[]} [offsets] Offset the positioning by [x, y]
     * @param {Boolean/Object} [animate] true for the default animation or a standard
     * Element animation config object
     * @return {Ext.util.Positionable} this
     */
    /**
     * @method anchorTo
     * Anchors an element to another element and realigns it when the window is resized.
     * @param {Ext.util.Positionable/HTMLElement/String} anchorToEl The Positionable,
     * HTMLElement, or id of the element to align to.
     * @param {String} [alignment="tl-bl?"] The position to align to
     * @param {Number[]} [offsets] Offset the positioning by [x, y]
     * @param {Boolean/Object} [animate] true for the default animation or a standard
     * Element animation config object
     * @param {Boolean/Number} [monitorScroll=50] True to monitor body scroll and
     * reposition. If this parameter is a number, it is used as the buffer delay in
     * milliseconds.
     * @param {Function} [callback] The function to call after the animation finishes
     * @return {Ext.util.Positionable} this
     */
    anchorTo: function(anchorToEl, alignment, offsets, animate, monitorScroll, callback) {
        var me = this,
            scroll = !Ext.isEmpty(monitorScroll),
            action = function() {
                me.mixins.positionable.alignTo.call(me, anchorToEl, alignment, offsets, animate);
                Ext.callback(callback, me);
            },
            anchor = me.getAnchor();
        // previous listener anchor, remove it
        me.removeAnchor();
        Ext.apply(anchor, {
            fn: action,
            scroll: scroll
        });
        Ext.on('resize', action, null);
        if (scroll) {
            Ext.getWin().on('scroll', action, null, {
                buffer: !isNaN(monitorScroll) ? monitorScroll : 50
            });
        }
        action();
        // align immediately
        return me;
    },
    getAnchor: function() {
        var el = this.el,
            data, anchor;
        if (!el || !el.dom) {
            return;
        }
        data = el.getData();
        anchor = data._anchor;
        if (!anchor) {
            anchor = data._anchor = {};
        }
        return anchor;
    },
    alignTo: function(element, position, offsets, /* private (documented in ext) */
    animate) {
        var me = this,
            el = me.el,
            newMaxHeight, newRegion;
        // Release any height constraint prior to aligning if we are shrinkwrap height.
        if (me.isComponent && me.getSizeModel().height.shrinkWrap) {
            if (me.maxHeight) {
                me.setMaxHeight(null);
            }
            newMaxHeight = true;
        }
        newRegion = me.getAlignToRegion(element, position, offsets, me.minHeight || 150);
        me.setXY([
            newRegion.x,
            newRegion.y
        ], el.anim && !!animate ? el.anim(animate) : false);
        // Impose calculated height constraint.
        if (newMaxHeight && (newMaxHeight = newRegion.getHeight()) !== me.getHeight()) {
            me.setMaxHeight(newMaxHeight);
        }
        return me;
    },
    /**
     * @method move
     * Move the element relative to its current position.
     * @param {String} direction Possible values are:
     *
     * - `"l"` (or `"left"`)
     * - `"r"` (or `"right"`)
     * - `"t"` (or `"top"`, or `"up"`)
     * - `"b"` (or `"bottom"`, or `"down"`)
     *
     * @param {Number} distance How far to move the element in pixels
     * @param {Boolean/Object} [animate] true for the default animation or a standard
     * Element animation config object
     */
    /**
     * Remove any anchor to this element. See {@link #anchorTo}.
     * @return {Ext.util.Positionable} this
     */
    removeAnchor: function() {
        var anchor = this.getAnchor();
        if (anchor && anchor.fn) {
            Ext.un('resize', anchor.fn);
            if (anchor.scroll) {
                Ext.getWin().on('scroll', anchor.fn);
            }
            delete anchor.fn;
        }
        return this;
    },
    /**
     * @method setBox
     * Sets the element's box. If animate is true then x, y, width, and height will be
     * animated concurrently.
     * @param {Object} box The box to fill {x, y, width, height}
     * @param {Boolean/Object} [animate] true for the default animation or a standard
     * Element animation config object
     * @return {Ext.util.Positionable} this
     */
    setBox: function(box, animate) {
        var me = this;
        if (box.isRegion) {
            box = {
                x: box.left,
                y: box.top,
                width: box.right - box.left,
                height: box.bottom - box.top
            };
        }
        if (animate) {
            me.constrainBox(box);
            me.animate(Ext.applyIf({
                to: box,
                listeners: {
                    afteranimate: Ext.Function.bind(me.afterSetPosition, me, [
                        box.x,
                        box.y
                    ])
                }
            }, animate));
        } else {
            me.callParent([
                box
            ]);
        }
        return me;
    }
});
/**
     * @method setX
     * Sets the X position of the DOM element based on page coordinates.
     * @param {Number} x The X position
     * @param {Boolean/Object} [animate] True for the default animation, or a standard
     * Element animation config object
     * @return {Ext.util.Positionable} this
     */
/**
     * @method setXY
     * Sets the position of the DOM element in page coordinates.
     * @param {Number[]} pos Contains X & Y [x, y] values for new position (coordinates
     * are page-based)
     * @param {Boolean/Object} [animate] True for the default animation, or a standard
     * Element animation config object
     * @return {Ext.util.Positionable} this
     */
/**
     * @method setY
     * Sets the Y position of the DOM element based on page coordinates.
     * @param {Number} y The Y position
     * @param {Boolean/Object} [animate] True for the default animation, or a standard
     * Element animation config object
     * @return {Ext.util.Positionable} this
     */

/**
 * Private utility class that manages the internal cache for {@link Ext.dom.Shadow Underlays}
 * and {@link Ext.dom.Shim Shims}.
 * @private
 */
Ext.define('Ext.dom.UnderlayPool', {
    /**
     * @constructor
     * @param {Object} elementConfig A {@link Ext.dom.Helper DomHelper} config object to
     * use for generating elements in the pool.
     */
    constructor: function(elementConfig) {
        this.elementConfig = elementConfig;
        this.cache = [];
    },
    /**
     * Checks an element out of the pool.
     * @return {Ext.dom.Element}
     */
    checkOut: function() {
        var el = this.cache.shift();
        if (!el) {
            el = Ext.Element.create(this.elementConfig);
            el.setVisibilityMode(2);
            // tell the spec runner to ignore this element when checking if the dom is clean
            el.dom.setAttribute('data-sticky', true);
        }
        return el;
    },
    /**
     * Checks an element back into the pool for future reuse
     * @param {Ext.dom.Element} el
     */
    checkIn: function(el) {
        this.cache.push(el);
        Ext.getDetachedBody().dom.appendChild(el.dom);
    },
    /**
     * Reset the pool by emptying the cache and destroying all its elements
     */
    reset: function() {
        var cache = this.cache,
            i = cache.length;
        while (i--) {
            cache[i].destroy();
        }
        this.cache = [];
    }
});

/**
 * A class that provides an underlay element which displays behind an absolutely positioned
 * target element and tracks its size and position. Abstract base class for
 * {@link Ext.dom.Shadow} and {@link Ext.dom.Shim}
 *  
 * 
 * @private
 * @abstract
 */
Ext.define('Ext.dom.Underlay', {
    requires: [
        'Ext.dom.UnderlayPool'
    ],
    /**
     * @cfg {Ext.dom.Element} target
     * The target element
     */
    /**
     * @cfg {Number} zIndex
     * The CSS z-index to use for this underlay.  Defaults to the z-index of {@link #target}.
     */
    constructor: function(config) {
        Ext.apply(this, config);
    },
    /**
     * @method
     * @protected
     * Called before the underlay is shown, immediately after its element is retrieved
     * from the pool
     */
    beforeShow: Ext.emptyFn,
    /**
     * @protected
     * Returns the dom element that this underlay should be inserted before.
     * Defaults to the target element
     * @return {Ext.dom.Element}
     */
    getInsertionTarget: function() {
        return this.target;
    },
    /**
     * @protected
     * @return {Ext.dom.UnderlayPool}
     */
    getPool: function() {
        return this.pool || (this.self.prototype.pool = new Ext.dom.UnderlayPool(this.elementConfig));
    },
    /**
     * Hides the underlay
     */
    hide: function() {
        var me = this,
            el = me.el;
        if (el) {
            if (el.dom) {
                el.hide();
                me.getPool().checkIn(el);
            }
            me.el = null;
        }
        me.hidden = true;
    },
    /**
     * Aligns the underlay to its target element
     * @param {Number} [x] The x position of the target element.  If not provided, the 
     * x position will be read from the DOM.
     * @param {Number} [y] The y position of the target element.  If not provided, the
     * y position will be read from the DOM.
     * @param {Number} [width] The width of the target element.  If not provided, the
     * width will be read from the DOM.
     * @param {Number} [height] The height of the target element.  If not provided, the
     * height will be read from the DOM.
     */
    realign: function(x, y, width, height) {
        var me = this,
            el = me.el,
            target = me.target,
            offsets = me.offsets,
            max = Math.max;
        if (el) {
            if (x == null) {
                x = target.getX();
            }
            if (y == null) {
                y = target.getY();
            }
            if (width == null) {
                width = target.getWidth();
            }
            if (height == null) {
                height = target.getHeight();
            }
            if (offsets) {
                x = x + offsets.x;
                y = y + offsets.y;
                width = max(width + offsets.w, 0);
                height = max(height + offsets.h, 0);
            }
            el.setXY([
                x,
                y
            ]);
            el.setSize(width, height);
        }
    },
    /**
     * Adjust the z-index of this underlay
     * @param {Number} zIndex The new z-index
     */
    setZIndex: function(zIndex) {
        this.zIndex = zIndex;
        if (this.el) {
            this.el.setStyle("z-index", zIndex);
        }
    },
    /**
     * Shows the underlay
     */
    show: function() {
        var me = this,
            target = me.target,
            zIndex = me.zIndex,
            el = me.el,
            insertionTarget = me.getInsertionTarget().dom,
            dom;
        if (!el) {
            el = me.el = me.getPool().checkOut();
        }
        me.beforeShow();
        if (zIndex == null) {
            // For best results, we need the underlay to be as close as possible to its
            // target element in the z-index stacking order without overlaying the target
            // element.  Since the UnderlayPool inserted the underlay as high as possible
            // in the dom tree when we checked the underlay out of the pool, we can assume
            // that it comes before the target element in the dom tree, and therefore can
            // give it the exact same index as the target element.
            zIndex = (parseInt(target.getStyle("z-index"), 10));
        }
        if (zIndex) {
            el.setStyle("z-index", zIndex);
        }
        // Overlay elements are shared, so fix position to match current owner
        el.setStyle('position', me.fixed ? 'fixed' : '');
        dom = el.dom;
        if (dom.nextSibling !== insertionTarget) {
            // inserting the underlay as the previous sibling of the target ensures that
            // it will show behind the target, as long as its z-index is less than or equal
            // to the z-index of the target element.
            target.dom.parentNode.insertBefore(dom, insertionTarget);
        }
        el.show();
        me.realign();
        me.hidden = false;
    }
});

/**
 * Simple class that can provide a shadow effect for any absolutely positioned {@link
 * Ext.dom.Element Element}.
 * 
 * Not meant to be used directly. To apply a shadow to an Element use the 
 * {@link Ext.dom.Element#enableShadow enableShadow} method.
 * 
 * @private
 */
Ext.define('Ext.dom.Shadow', {
    extend: 'Ext.dom.Underlay',
    alternateClassName: 'Ext.Shadow',
    /**
     * @cfg {String} mode
     * The shadow display mode.  Supports the following options:
     *
     * - sides : Shadow displays on both sides and bottom only
     * - frame : Shadow displays equally on all four sides
     * - drop : Traditional bottom-right drop shadow
     */
    mode: 'drop',
    /**
     * @cfg {Number} offset
     * The number of pixels to offset the shadow from the element
     */
    offset: 4,
    cls: Ext.baseCSSPrefix + (!Ext.supports.CSS3BoxShadow ? 'ie' : 'css') + '-shadow',
    /**
     * Creates new Shadow.
     * @param {Object} config (optional) Config object.
     */
    constructor: function(config) {
        var me = this,
            outerOffsets, offsets, offset, rad;
        me.callParent([
            config
        ]);
        me.elementConfig = {
            cls: me.cls,
            role: 'presentation'
        };
        offset = me.offset;
        rad = Math.floor(offset / 2);
        me.opacity = 50;
        switch (me.mode.toLowerCase()) {
            case "drop":
                outerOffsets = {
                    x: 0,
                    y: 0,
                    w: offset,
                    h: offset
                };
                if (Ext.supports.CSS3BoxShadow) {
                    offsets = {
                        x: offset,
                        y: offset,
                        h: -offset,
                        w: -offset
                    };
                } else {
                    offsets = {
                        x: -rad,
                        y: -rad,
                        h: -rad,
                        w: -rad
                    };
                };
                break;
            case "sides":
                outerOffsets = {
                    x: -offset,
                    y: 0,
                    w: offset * 2,
                    h: offset
                };
                if (Ext.supports.CSS3BoxShadow) {
                    offsets = {
                        x: 0,
                        y: offset,
                        h: -offset,
                        w: 0
                    };
                } else {
                    offsets = {
                        x: 1 + rad - 2 * offset,
                        y: -(1 + rad),
                        h: -1,
                        w: rad - 1
                    };
                };
                break;
            case "frame":
                outerOffsets = {
                    x: -offset,
                    y: -offset,
                    w: offset * 2,
                    h: offset * 2
                };
                if (Ext.supports.CSS3BoxShadow) {
                    offsets = {
                        x: 0,
                        y: 0,
                        h: 0,
                        w: 0
                    };
                } else {
                    offsets = {
                        x: 1 + rad - 2 * offset,
                        y: 1 + rad - 2 * offset,
                        h: offset - rad - 1,
                        w: offset - rad - 1
                    };
                };
                break;
            case "bottom":
                outerOffsets = {
                    x: -offset,
                    y: 0,
                    w: offset * 2,
                    h: offset
                };
                if (Ext.supports.CSS3BoxShadow) {
                    offsets = {
                        x: 0,
                        y: offset,
                        h: -offset,
                        w: 0
                    };
                } else {
                    offsets = {
                        x: 0,
                        y: offset,
                        h: 0,
                        w: 0
                    };
                };
                break;
        }
        /**
         * @property {Object} offsets The offsets used for positioning the shadow element
         * relative to its target element
         */
        me.offsets = offsets;
        /**
         * @property {Object} outerOffsets Offsets that represent the union of the areas
         * of the target element and the shadow combined.  Used by Ext.dom.Element for
         * ensuring that the shim (if present) extends under the full area of both elements.
         */
        me.outerOffsets = outerOffsets;
    },
    /**
     * @private
     * Returns the shadow size on each side of the element in standard CSS order: top, right,
     * bottom, left;
     * @return {Number[]} Top, right, bottom and left shadow size.
     */
    getShadowSize: function() {
        var me = this,
            offset = me.el ? me.offset : 0,
            result = [
                offset,
                offset,
                offset,
                offset
            ],
            mode = me.mode.toLowerCase();
        // There are only offsets if the shadow element is present.
        if (me.el && mode !== 'frame') {
            result[0] = 0;
            if (mode === 'drop') {
                result[3] = 0;
            }
        }
        return result;
    },
    /**
     * @private
     * CSS property used to set the box shadow.
     */
    boxShadowProperty: (function() {
        var property = 'boxShadow',
            style = document.documentElement.style;
        if (!('boxShadow' in style)) {
            if ('WebkitBoxShadow' in style) {
                // Safari prior to version 5.1 and Chrome prior to version 10
                property = 'WebkitBoxShadow';
            } else if ('MozBoxShadow' in style) {
                // FF 3.5 & 3.6
                property = 'MozBoxShadow';
            }
        }
        return property;
    }()),
    beforeShow: function() {
        var me = this,
            style = me.el.dom.style,
            shim = me.shim;
        if (Ext.supports.CSS3BoxShadow) {
            style[me.boxShadowProperty] = '0 0 ' + (me.offset + 2) + 'px #888';
        } else {
            style.filter = "progid:DXImageTransform.Microsoft.alpha(opacity=" + me.opacity + ") progid:DXImageTransform.Microsoft.Blur(pixelradius=" + (me.offset) + ")";
        }
        // if we are showing a shadow, and we already have a visible shim, we need to
        // realign the shim to ensure that it includes the size of target and shadow els
        if (shim) {
            shim.realign();
        }
    },
    /**
     * Sets the opacity of the shadow
     * @param {Number} opacity The opacity
     */
    setOpacity: function(opacity) {
        var el = this.el;
        if (el) {
            if (Ext.isIE && !Ext.supports.CSS3BoxShadow) {
                opacity = Math.floor(opacity * 100 / 2) / 100;
            }
            this.opacity = opacity;
            el.setOpacity(opacity);
        }
    }
});

/**
 * Simple class that provides an iframe shim for any absolutely positioned {@link
 * Ext.dom.Element Element} to prevent windowed objects from showing through.
 * 
 * Not meant to be used directly. Internally shims are applied to Elements using
 * {@link Ext.dom.Element#enableShim enableShim}.  Developers should use the
 * {@link Ext.util.Floating#shim shim} config to add shims to their
 * {@link Ext.Component Components} or set {@link Ext#useShims Ext.useShims}=true.
 * @private
 */
Ext.define('Ext.dom.Shim', {
    extend: 'Ext.dom.Underlay',
    cls: Ext.baseCSSPrefix + 'shim',
    constructor: function(config) {
        this.callParent([
            config
        ]);
        this.elementConfig = {
            tag: 'iframe',
            cls: this.cls,
            role: 'presentation',
            frameBorder: '0',
            src: Ext.SSL_SECURE_URL,
            // tabIndex of -1 ensures that the iframe is not focusable by the user
            tabindex: '-1'
        };
    },
    getInsertionTarget: function() {
        // ensure that the shim is inserted before the shadow in the dom, so that the
        // shadow will be stacked on top of it.
        var shadow = this.shadow;
        return (shadow && shadow.el) || this.target;
    }
});

/**
 * A special Ext.util.Event subclass that adds support for capture (top-down propagation)
 * listeners, and non-delegated (directly attached to the dom) listeners.
 *
 * An Ext.Element will have one instance of this class per event type that is being listened
 * for.  The ElementEvent instance provides a single point for attaching event listeners
 * and abstracts away important details on the timing and ordering of event firing.
 * Internally this class manages up to 3 separate Ext.util.Event instances.  These represent
 * separate stacks of listeners that may be invoked during different phases of event propagation.
 *
 * - `captures` - tracks listeners that should fire during the "capture" phase of the
 * standard delegated model (listeners attached using capture:true)
 * - `direct` - tracks directly attached listeners, that is listeners that should fire
 * immediately when the event is dispatched to the dom element, before the event bubbles
 * upward and delegated listener processing begins
 * (listeners attached using delegated:false)
 * - `directCaptures` - tracks directly attached capture listeners (only works in IE10+)
 *
 * For more detail on the timing of when these event stacks are dispatched please see
 * Ext.event.publisher.Dom
 *
 * @private
 */
Ext.define('Ext.dom.ElementEvent', {
    extend: 'Ext.util.Event',
    addListener: function(fn, scope, options, caller, manager) {
        var me = this,
            added = false,
            name = me.name,
            isDirectEvent = Ext.event.publisher.Dom.instance.directEvents[name],
            captures, directs, directCaptures;
        options = options || {};
        if (options.delegated === false || isDirectEvent) {
            if (isDirectEvent && options.delegate) {
                options.capture = true;
            }
            if (options.capture) {
                directCaptures = me.directCaptures || (me.directCaptures = new Ext.util.Event(me.observable, name));
                added = directCaptures.addListener(fn, scope, options, caller, manager);
            } else {
                directs = me.directs || (me.directs = new Ext.util.Event(me.observable, name));
                added = directs.addListener(fn, scope, options, caller, manager);
            }
        } else if (options.capture) {
            captures = me.captures || (me.captures = new Ext.util.Event(me.observable, name));
            added = captures.addListener(fn, scope, options, caller, manager);
        } else {
            added = me.callParent([
                fn,
                scope,
                options,
                caller,
                manager
            ]);
        }
        return added;
    },
    removeListener: function(fn, scope) {
        var me = this,
            captures = me.captures,
            directs = me.directs,
            directCaptures = me.directCaptures,
            removed = false,
            index = me.findListener(fn, scope);
        if (index !== -1) {
            removed = me.callParent([
                fn,
                scope,
                index
            ]);
        } else {
            if (directs) {
                index = directs.findListener(fn, scope);
            }
            if (index !== -1) {
                removed = directs.removeListener(fn, scope, index);
            } else {
                if (captures) {
                    index = captures.findListener(fn, scope);
                }
                if (index !== -1) {
                    removed = captures.removeListener(fn, scope, index);
                } else if (directCaptures) {
                    index = directCaptures.findListener(fn, scope);
                    if (index !== -1) {
                        removed = directCaptures.removeListener(fn, scope, index);
                    }
                }
            }
        }
        return removed;
    },
    clearListeners: function() {
        var me = this,
            directCaptures = me.directCaptures,
            directs = me.directs,
            captures = me.captures;
        if (directCaptures) {
            directCaptures.clearListeners();
        }
        if (directs) {
            directs.clearListeners();
        }
        if (captures) {
            captures.clearListeners();
        }
        me.callParent();
    },
    suspend: function() {
        var me = this,
            directCaptures = me.directCaptures,
            directs = me.directs,
            captures = me.captures;
        if (directCaptures) {
            directCaptures.suspend();
        }
        if (directs) {
            directs.suspend();
        }
        if (captures) {
            captures.suspend();
        }
        me.callParent();
    },
    resume: function() {
        var me = this,
            directCaptures = me.directCaptures,
            directs = me.directs,
            captures = me.captures;
        if (directCaptures) {
            directCaptures.resume();
        }
        if (directs) {
            directs.resume();
        }
        if (captures) {
            captures.resume();
        }
        me.callParent();
    }
});

/**
 * Abstract base class for event publishers
 * @private
 */
Ext.define('Ext.event.publisher.Publisher', {
    isEventPublisher: true,
    $vetoClearingPrototypeOnDestroy: true,
    /**
     * @property {Array} handledEvents
     * An array of events that this publisher handles.
     */
    handledEvents: [],
    statics: {
        /**
         * @property {Object} publishers
         * A map of all publisher singleton instances.  Publishers register themselves
         * in this map as soon as they are constructed.
         */
        publishers: {},
        /**
         * @property publishersByEvent
         * A map of handled event names to the publisher that handles each event.
         * Provides a convenient way for looking up the publisher that handles any given
         * event, for example:
         *
         *     // get the publisher that  handles click:
         *     var publisher = Ext.event.publisher.Publisher.publishersByEvent.click;
         */
        publishersByEvent: {}
    },
    constructor: function() {
        var me = this,
            type = me.type;
        /**
         * @property {Object} handles
         * @private
         * A map for conveniently checking if this publisher handles a given event
         */
        me.handles = {};
        if (!type) {
            Ext.raise("Event publisher '" + me.$className + "' defined without a 'type' property.");
        }
        if (me.self.instance) {
            Ext.raise("Cannot create multiple instances of '" + me.$className + "'. " + "Use '" + me.$className + ".instance' to retrieve the singleton instance.");
        }
        me.registerEvents();
        Ext.event.publisher.Publisher.publishers[type] = me;
    },
    /**
     * Registers all {@link #handledEvents} in the
     * {@link Ext.event.publisher.Publisher#publishersByEvent} map.
     * @param {String[]} [events] optional events to register instead of handledEvents.
     * @protected
     */
    registerEvents: function(events) {
        var me = this,
            publishersByEvent = Ext.event.publisher.Publisher.publishersByEvent,
            handledEvents = events || me.handledEvents,
            ln = handledEvents.length,
            eventName, i;
        for (i = 0; i < ln; i++) {
            eventName = handledEvents[i];
            me.handles[eventName] = 1;
            publishersByEvent[eventName] = me;
        }
    },
    subscribe: function() {
        Ext.raise("Ext.event.publisher.Publisher subclass '" + this.$className + '" has no subscribe method.');
    },
    unsubscribe: function() {
        Ext.raise("Ext.event.publisher.Publisher subclass '" + this.$className + '" has no unsubscribe method.');
    },
    fire: function(element, eventName, args) {
        var event;
        if (element.hasListeners[eventName]) {
            event = element.events[eventName];
            if (event) {
                event.fire.apply(event, args);
            }
        }
    }
});

/**
 * @private
 */
Ext.define('Ext.util.Offset', {
    /* Begin Definitions */
    statics: {
        fromObject: function(obj) {
            if (obj instanceof this) {
                return obj;
            }
            if (typeof obj === 'number') {
                return new this(obj, obj);
            }
            if (obj.length) {
                return new this(obj[0], obj[1]);
            }
            return new this(obj.x, obj.y);
        }
    },
    /* End Definitions */
    constructor: function(x, y) {
        this.x = (x != null && !isNaN(x)) ? x : 0;
        this.y = (y != null && !isNaN(y)) ? y : 0;
        return this;
    },
    copy: function() {
        return new Ext.util.Offset(this.x, this.y);
    },
    copyFrom: function(p) {
        this.x = p.x;
        this.y = p.y;
    },
    toString: function() {
        return "Offset[" + this.x + "," + this.y + "]";
    },
    equals: function(offset) {
        if (!(offset instanceof this.statics())) {
            Ext.raise('Offset must be an instance of Ext.util.Offset');
        }
        return (this.x === offset.x && this.y === offset.y);
    },
    add: function(offset) {
        if (!(offset instanceof this.statics())) {
            Ext.raise('Offset must be an instance of Ext.util.Offset');
        }
        this.x += offset.x;
        this.y += offset.y;
    },
    round: function(to) {
        var factor;
        if (!isNaN(to)) {
            factor = Math.pow(10, to);
            this.x = Math.round(this.x * factor) / factor;
            this.y = Math.round(this.y * factor) / factor;
        } else {
            this.x = Math.round(this.x);
            this.y = Math.round(this.y);
        }
    },
    isZero: function() {
        return this.x === 0 && this.y === 0;
    }
});

/**
 * This class represents a rectangular region in X,Y space, and performs geometric
 * transformations or tests upon the region.
 *
 * This class may be used to compare the document regions occupied by elements.
 */
Ext.define('Ext.util.Region', function() {
    var ExtUtil = Ext.util,
        // eslint-disable-next-line no-useless-escape
        constrainRe = /([^\?!]*)(!|\?)?$/,
        alignRe = /^(?:(?:([trbl])(\d+))|(tl|t|tc|tr|l|c|r|bl|b|bc|br))(?:-(?:(?:([trbl])(\d+))|(tl|t|tc|tr|l|c|r|bl|b|bc|br)))?$/i,
        // Each side has the first letter as the main align side, so [tlbr]
        // The next optional component is a offset factor, so [tb] may be followed by [lr]
        // and vice versa
        // The offset factor may also be a number along that edge from 0 to 100.
        // So 'tl-br' is equal to 't0-b100'.
        // The offset factor defaults to 'c' or 50 meaning the 't-b' is equivalent to
        // 't50-b50' or 'tc-bc'
        LTROffsetFactors = {
            l: 0,
            r: 100,
            t: 0,
            b: 100,
            c: 50
        },
        RTLOffsetFactors = {
            l: 100,
            r: 0,
            t: 0,
            b: 100,
            c: 50
        },
        relativePositions = {
            b: 0,
            l: 1,
            t: 2,
            r: 3
        },
        alignMap = {
            "tl-tr": "l0-r0",
            "tl-r": "l0-r50",
            "bl-r": "l100-r50",
            "bl-br": "l100-r100",
            "tr-tl": "r0-l0",
            "tr-l": "r0-l50",
            "br-l": "r100-l50",
            "br-bl": "r100-l100"
        },
        rtlAlignMap = {
            "tl-tr": "r0-l0",
            "tl-r": "r0-l50",
            "bl-r": "r100-l50",
            "bl-br": "r100-l100",
            "tr-tl": "l0-r0",
            "tr-l": "l0-r50",
            "br-l": "l100-r50",
            "br-bl": "l100-r100"
        },
        adjustParams = [],
        zeroOffset = new ExtUtil.Offset(0, 0),
        parseRegion = function(box) {
            var Region = ExtUtil.Region,
                type = typeof box,
                top, right, bottom, left;
            if (box == null) {
                return Region.EMPTY;
            }
            if (box.isRegion) {
                return box;
            }
            if (box.isElement || box.nodeType === 1) {
                return this.getRegion(box);
            }
            if (type === 'string') {
                box = box.split(' ');
                switch (box.length) {
                    case 1:
                        box[1] = box[2] = box[3] = box[0];
                        break;
                    case 2:
                        box[2] = box[0];
                        box[3] = box[1];
                        break;
                    case 3:
                        box[3] = box[1];
                }
                top = parseInt(box[0], 10) || 0;
                right = parseInt(box[1], 10) || 0;
                bottom = parseInt(box[2], 10) || 0;
                left = parseInt(box[3], 10) || 0;
            } else if (type === 'number') {
                top = right = bottom = left = box;
            } else if (typeof box.x === 'number') {
                top = box.y;
                left = box.x;
                if (typeof box.right === 'number') {
                    right = box.right;
                    bottom = box.bottom;
                } else {
                    right = left + box.width;
                    bottom = top + box.height;
                }
            } else {
                Ext.raise('Not convertible to a Region: ' + box);
            }
            return new Region(top, right, bottom, left);
        },
        magnitude = [
            -1,
            1,
            1,
            -1
        ],
        // Depending on the "relativePosition" which will be 0,1,2 or 3 for T,R,B,L
        // extend the adjacent edge of the target to account for the offset.
        // Also, shrink the adjacent edge to create overlap for the anchor to center in.
        addAnchorOffset = function(target, anchorSize, relativePosition) {
            // Expand the adjacent edge by the anchor HEIGHT.
            if (relativePosition != null && anchorSize) {
                adjustParams[0] = adjustParams[1] = adjustParams[2] = adjustParams[3] = 0;
                adjustParams[relativePosition] = anchorSize.y * magnitude[relativePosition];
                target = ExtUtil.Region.from(target);
                target.adjust.apply(target, adjustParams);
            }
            return target;
        },
        // Shrink the adjacent edge to create overlap for the anchor to center in.
        calculateAnchorPosition = function(target, result, relativePosition, anchorSize, inside) {
            var anchorWidth = Math.ceil(anchorSize.x),
                minOverlap = Math.ceil(anchorWidth / 2) + 3,
                min, max, anchorPos, isBefore, overlapLine, x, y;
            // target is out of bounds. We can't show an anchor
            if (inside && !inside.intersect(target)) {
                return;
            }
            if (relativePosition != null) {
                // The result is to the left or right of the target
                if (relativePosition & 1) {
                    // Not enough height to support a side anchor
                    if (result.getHeight() < anchorWidth + 4) {
                        return;
                    }
                    //
                    //               +------+ <--- min
                    //               |      |
                    //               |      |
                    //  +---------+ <       | <-anchorMax
                    //  |         |  +------+
                    //  |         |
                    //  |         |
                    //  |         |
                    //  |         |
                    //  |         | +-------+ <--- max
                    //  +---------+ <       | <-anchorMin
                    //              |       |
                    //              |       |
                    //              +-------+
                    //
                    // Coerce the result's top to create enough overlap with target.
                    // Needs at least anchorWidth / 2 + 2 to look right.
                    min = target.top + minOverlap - result.height;
                    max = target.bottom - minOverlap - 1;
                    result.setPosition(result.x, Math.min(Math.max(result.y, min), max));
                    // Now calculate the min & max permissible anchor top so that the
                    // anchor baseline clears the result's corner by ar least 2px.
                    min = result.top + 2;
                    max = result.bottom - (anchorWidth + 2);
                    isBefore = relativePosition === 3;
                    x = isBefore ? result.right : result.left;
                    // eslint-disable-next-line max-len
                    overlapLine = new ExtUtil.Region(Math.max(result.top, target.top), x, Math.min(result.bottom, target.bottom), x);
                    // Align to the centre of the overlap line, wherever that may be
                    anchorPos = new ExtUtil.Region(0, 0, 0, 0).setWidth(anchorSize.y).setHeight(anchorWidth).alignTo({
                        target: overlapLine,
                        align: isBefore ? 'l-r' : 'r-l',
                        overlap: true
                    });
                    // Coerce the anchor into the bounds of the result.
                    anchorPos.setPosition(anchorPos.x, Math.min(Math.max(anchorPos.y, min), max));
                    anchorPos.position = isBefore ? 'right' : 'left';
                } else // The result is above or below the target.
                {
                    // Not enough width to support a top/bottom anchor
                    if (result.getWidth() < anchorWidth + 4) {
                        return;
                    }
                    // Coerce the result's left to create enough overlap with target.
                    // Needs at least anchorWidth / 2 + 2 to look right.
                    min = target.left + minOverlap - result.width;
                    max = target.right - minOverlap - 1;
                    result.setPosition(Math.min(Math.max(result.x, min), max), result.y);
                    // Now calculate the min & max permissible anchor left so that the
                    // anchor baseline clears the result's corner by ar least 2px.
                    min = result.left + 2;
                    max = result.right - (anchorWidth + 2);
                    // If there is not enough overlap. coerce the result to create enough overlap
                    isBefore = relativePosition === 0;
                    y = isBefore ? result.bottom : result.top;
                    // eslint-disable-next-line max-len
                    overlapLine = new ExtUtil.Region(y, Math.min(result.right, target.right), y, Math.max(result.left, target.left));
                    // Align to the centre of the overlap line, wherever that may be
                    anchorPos = new ExtUtil.Region(0, 0, 0, 0).setWidth(anchorWidth).setHeight(anchorSize.y).alignTo({
                        target: overlapLine,
                        align: isBefore ? 't-b' : 'b-t',
                        overlap: true
                    });
                    // Coerce the anchor into the bounds of the result.
                    anchorPos.setPosition(Math.min(Math.max(anchorPos.x, min), max), anchorPos.y);
                    anchorPos.position = isBefore ? 'bottom' : 'top';
                }
                // If anchor is outside constrain region it cannot be shown.
                if (inside && !inside.contains(anchorPos)) {
                    return;
                }
                result.anchor = anchorPos;
                result.anchor.align = relativePosition;
            }
        },
        checkMinHeight = function(minHeight, result, target, inside) {
            var newHeight;
            if (minHeight && inside) {
                // Overflows the bottom of the target
                if (result.top >= target.bottom && result.bottom > inside.bottom) {
                    result.setHeight(Math.max(result.getHeight() + inside.bottom - result.bottom, minHeight));
                    result.constrainHeight = true;
                }
                // Overflows the top of the target
                else if (result.bottom <= target.top && result.top < inside.top) {
                    newHeight = Math.max(result.getHeight() + result.top - inside.top, minHeight);
                    result.adjust(result.getHeight() - newHeight);
                    result.constrainHeight = true;
                }
                // Just too high
                else if (result.getHeight() > inside.getHeight()) {
                    result.setHeight(Math.max(minHeight, inside.getHeight()));
                    result.setPosition(result.x, 0);
                    result.constrainHeight = true;
                }
            }
        },
        checkMinWidth = function(minWidth, result, target, inside) {
            var newWidth;
            if (minWidth && inside) {
                // Overflows the right of the target
                if (result.left >= target.right && result.right > inside.right) {
                    result.setWidth(Math.max(result.getWidth() + inside.right - result.right, minWidth));
                    result.constrainWidth = true;
                }
                // Overflows the left of the target
                else if (result.right <= target.left && result.left < inside.left) {
                    newWidth = Math.max(result.getWidth() + result.left - inside.left, minWidth);
                    result.adjust(0, 0, 0, result.getWidth() - newWidth);
                    result.constrainWidth = true;
                }
                // Just too wide
                else if (result.getWidth() > inside.getWidth()) {
                    result.setWidth(Math.max(minWidth, inside.getWidth()));
                    result.setPosition(0, result.y);
                    result.constrainWidth = true;
                }
            }
        };
    /* eslint-disable indent */
    return {
        requires: [
            'Ext.util.Offset'
        ],
        isRegion: true,
        statics: {
            /**
         * @static
         * Retrieves an Ext.util.Region for a particular element.
         * @param {String/HTMLElement/Ext.dom.Element} el An element ID, htmlElement or Ext.Element
         * representing an element in the document.
         * @return {Ext.util.Region} region
         */
            getRegion: function(el) {
                return Ext.fly(el).getRegion();
            },
            /**
         * @static
         * Creates a Region from a "box" Object which contains four numeric properties `top`,
         * `right`, `bottom` and `left`.
         * @param {Object} o An object with `top`, `right`, `bottom` and `left` properties.
         * @return {Ext.util.Region} region The Region constructed based on the passed object
         */
            from: function(o) {
                return new this(o.top, o.right, o.bottom, o.left);
            },
            /**
         * This function converts a legacy alignment string such as 't-b' into a
         * pair of edge, offset objects which describe the alignment points of
         * the two regions.
         *
         * So tl-br becomes {myEdge:'t', offset:0}, {otherEdge:'b', offset:100}
         *
         * This not only allows more flexibility in the alignment possibilities,
         * but it also resolves any ambiguity as to which two edges are desired
         * to be adjacent if an anchor pointer is required.
         * 
         * @param {String} align The align spec, eg `"tl-br"`
         * @param {Boolean} [rtl] Pass `true` to use RTL calculations.
         */
            getAlignInfo: function(align, rtl) {
                if (typeof align === 'object') {
                    return align;
                }
                align = align ? ((align.indexOf('-') < 0) ? 'tl-' + align : align) : 'tl-bl';
                // Snip any constraint modifier off so that we can match the alignMaps
                constrain = constrainRe.exec(align);
                align = constrain[1];
                // Convert left to right alignments which are specified using top/bottom
                // corner definitions.
                align = (rtl ? rtlAlignMap : alignMap)[align] || align;
                // eslint-disable-next-line vars-on-top
                var offsetFactors = rtl ? RTLOffsetFactors : LTROffsetFactors,
                    constrain,
                    parts = alignRe.exec(align),
                    result;
                if (!parts) {
                    Ext.raise({
                        sourceClass: 'Ext.util.Region',
                        sourceMethod: 'getAlignInfo',
                        position: align,
                        msg: 'Attempted to align an element with an invalid position: "' + align + '"'
                    });
                }
                result = {
                    myEdge: parts[1],
                    myOffset: parts[2],
                    otherEdge: parts[4],
                    otherOffset: parts[5],
                    constrain: constrain[2]
                };
                // t-l, b-r etc.
                // Convert points to edge and offset.
                if (parts[3]) {
                    result.myEdge = parts[3][0];
                    result.myOffset = offsetFactors[parts[3][1]];
                    if (result.myOffset == null) {
                        result.myOffset = 50;
                    }
                }
                if (parts[6]) {
                    result.otherEdge = parts[6][0];
                    result.otherOffset = offsetFactors[parts[6][1]];
                    if (result.otherOffset == null) {
                        result.otherOffset = 50;
                    }
                }
                // TOP=0, RIGHT=1, BOTTOM=2, LEFT=3, INSIDE=undefined
                result.position = relativePositions[result.myEdge];
                return result;
            }
        },
        /* End Definitions */
        /**
     * Creates a region from the bounding sides.
     * @param {Number} top The topmost pixel of the Region.
     * @param {Number} right The rightmost pixel of the Region.
     * @param {Number} bottom The bottom pixel of the Region.
     * @param {Number} left The leftmost pixel of the Region.
     */
        constructor: function(top, right, bottom, left) {
            var me = this;
            me.y = me.top = me[1] = top;
            me.right = right;
            me.bottom = bottom;
            me.x = me.left = me[0] = left;
            me.height = me.bottom - me.top;
            me.width = me.right - me.left;
        },
        /**
     * Translates this Region to the specified position
     * @param {Number} x The new X position.
     * @param {Number} y The new Y position.
     * @returns {Ext.util.Region} This region after translation.
     */
        setPosition: function(x, y) {
            // Allow [x, y]
            if (arguments.length === 1) {
                y = x[1];
                x = x[0];
            }
            return this.translateBy(x - this.x, y - this.y);
        },
        /**
     * Checks if this region completely contains the region or point that is passed in.
     * @param {Ext.util.Region/Ext.util.Point} region
     * @return {Boolean}
     */
        contains: function(region) {
            var me = this;
            return (region.x >= me.x && (region.right || region.x) <= me.right && region.y >= me.y && (region.bottom || region.y) <= me.bottom);
        },
        /**
     * Checks if this region intersects the region passed in.
     * @param {Ext.util.Region} region
     * @return {Ext.util.Region/Boolean} Returns the intersected region or false
     * if there is no intersection.
     */
        intersect: function(region) {
            var me = this,
                t = Math.max(me.y, region.y),
                r = Math.min(me.right, region.right),
                b = Math.min(me.bottom, region.bottom),
                l = Math.max(me.x, region.x);
            if (b > t && r > l) {
                return new this.self(t, r, b, l);
            } else {
                return false;
            }
        },
        /**
     * Returns the smallest region that contains the current AND targetRegion.
     * @param {Ext.util.Region} region
     * @return {Ext.util.Region} a new region
     */
        union: function(region) {
            var me = this,
                t = Math.min(me.y, region.y),
                r = Math.max(me.right, region.right),
                b = Math.max(me.bottom, region.bottom),
                l = Math.min(me.x, region.x);
            return new this.self(t, r, b, l);
        },
        /**
     * Modifies the current region to be constrained to the targetRegion.
     * @param {Ext.util.Region} targetRegion
     * @return {Ext.util.Region} this
     */
        constrainTo: function(targetRegion) {
            var me = this,
                constrain = Ext.Number.constrain;
            me.top = me.y = constrain(me.top, targetRegion.y, targetRegion.bottom);
            me.bottom = constrain(me.bottom, targetRegion.y, targetRegion.bottom);
            me.left = me.x = constrain(me.left, targetRegion.x, targetRegion.right);
            me.right = constrain(me.right, targetRegion.x, targetRegion.right);
            me.height = me.bottom - me.top;
            me.width = me.right - me.left;
            return me;
        },
        /**
     * Modifies the current region to be adjusted by offsets.
     * @param {Number} top Top offset
     * @param {Number} right Right offset
     * @param {Number} bottom Bottom offset
     * @param {Number} left Left offset
     * @return {Ext.util.Region} this
     */
        adjust: function(top, right, bottom, left) {
            var me = this;
            me.top = me.y += top || 0;
            me.left = me.x += left || 0;
            me.right += right || 0;
            me.bottom += bottom || 0;
            me.height = me.bottom - me.top;
            me.width = me.right - me.left;
            return me;
        },
        /**
     * Get the offset amount of a point outside the region
     * @param {String} [axis]
     * @param {Ext.util.Point} [p] the point
     * @return {Ext.util.Offset}
     */
        getOutOfBoundOffset: function(axis, p) {
            var d;
            if (!Ext.isObject(axis)) {
                if (axis === 'x') {
                    return this.getOutOfBoundOffsetX(p);
                } else {
                    return this.getOutOfBoundOffsetY(p);
                }
            } else {
                p = axis;
                d = new ExtUtil.Offset();
                d.x = this.getOutOfBoundOffsetX(p.x);
                d.y = this.getOutOfBoundOffsetY(p.y);
                return d;
            }
        },
        /**
     * Get the offset amount on the x-axis
     * @param {Number} p the offset
     * @return {Number}
     */
        getOutOfBoundOffsetX: function(p) {
            if (p <= this.x) {
                return this.x - p;
            } else if (p >= this.right) {
                return this.right - p;
            }
            return 0;
        },
        /**
     * Get the offset amount on the y-axis
     * @param {Number} p the offset
     * @return {Number}
     */
        getOutOfBoundOffsetY: function(p) {
            if (p <= this.y) {
                return this.y - p;
            } else if (p >= this.bottom) {
                return this.bottom - p;
            }
            return 0;
        },
        /**
     * Check whether the point / offset is out of bound
     * @param {String} [axis]
     * @param {Ext.util.Point/Number} [p] the point / offset
     * @return {Boolean}
     */
        isOutOfBound: function(axis, p) {
            if (!Ext.isObject(axis)) {
                if (axis === 'x') {
                    return this.isOutOfBoundX(p);
                } else {
                    return this.isOutOfBoundY(p);
                }
            } else {
                p = axis;
                return (this.isOutOfBoundX(p.x) || this.isOutOfBoundY(p.y));
            }
        },
        /**
     * Check whether the offset is out of bound in the x-axis
     * @param {Number} p the offset
     * @return {Boolean}
     */
        isOutOfBoundX: function(p) {
            return (p < this.x || p > this.right);
        },
        /**
     * Check whether the offset is out of bound in the y-axis
     * @param {Number} p the offset
     * @return {Boolean}
     */
        isOutOfBoundY: function(p) {
            return (p < this.y || p > this.bottom);
        },
        /**
     * Restrict a point within the region by a certain factor.
     * @param {String} [axis]
     * @param {Ext.util.Point/Ext.util.Offset/Object} [p]
     * @param {Number} [factor]
     * @return {Ext.util.Point/Ext.util.Offset/Object/Number}
     * @private
     */
        restrict: function(axis, p, factor) {
            var newP;
            if (Ext.isObject(axis)) {
                factor = p;
                p = axis;
                if (p.copy) {
                    newP = p.copy();
                } else {
                    newP = {
                        x: p.x,
                        y: p.y
                    };
                }
                newP.x = this.restrictX(p.x, factor);
                newP.y = this.restrictY(p.y, factor);
                return newP;
            } else {
                if (axis === 'x') {
                    return this.restrictX(p, factor);
                } else {
                    return this.restrictY(p, factor);
                }
            }
        },
        /**
     * Restrict an offset within the region by a certain factor, on the x-axis
     * @param {Number} p
     * @param {Number} [factor=1] The factor.
     * @return {Number}
     * @private
     */
        restrictX: function(p, factor) {
            if (!factor) {
                factor = 1;
            }
            if (p <= this.x) {
                p -= (p - this.x) * factor;
            } else if (p >= this.right) {
                p -= (p - this.right) * factor;
            }
            return p;
        },
        /**
     * Restrict an offset within the region by a certain factor, on the y-axis
     * @param {Number} p
     * @param {Number} [factor] The factor, defaults to 1
     * @return {Number}
     * @private
     */
        restrictY: function(p, factor) {
            if (!factor) {
                factor = 1;
            }
            if (p <= this.y) {
                p -= (p - this.y) * factor;
            } else if (p >= this.bottom) {
                p -= (p - this.bottom) * factor;
            }
            return p;
        },
        /**
     * Returns the Region to which this rectangle should be moved in order to
     * have the desired alignment with the specified target while remaining within the
     * constraint.
     *
     * The `align` option can be one of these forms:
     *
     * - **Blank**: Defaults to aligning the region's top-left corner to the target's
     *   bottom-left corner ("tl-bl").
     * - **Two anchors**: If two values from the table below are passed separated by a dash,
     *   the first value is used as this region's anchor point, and the second value is
     *   used as the target's anchor point.
     * - **One anchor**: The passed anchor position is used as the target's anchor point.
     *   This region will position its top-left corner (tl) to that point.
     * - **Two edge/offset descriptors:** An edge/offset descriptor is an edge initial
     *   (`t`/`r`/`b`/`l`) followed by a percentage along that side. This describes a
     *   point to align with a similar point in the target. So `'t0-b0'` would be
     *   the same as `'tl-bl'`, `'l0-r50'` would place the top left corner of this item
     *   halfway down the right edge of the target item. This allows more flexibility
     *   and also describes which two edges are considered adjacent when positioning an anchor. 
     *
     * If the `inside` option is passed, the Region will attempt to align as specified,
     * but the position will be adjusted to constrain to the `inside` Region if necessary.
     * Note that the Region being aligned might be swapped to align to a different position
     * than that specified in order to enforce the constraints. Following are all of the
     * supported anchor positions:
     *
     *      Value  Description
     *      -----  -----------------------------
     *      tl     The top left corner
     *      t      The center of the top edge
     *      tr     The top right corner
     *      l      The center of the left edge
     *      c      The center
     *      r      The center of the right edge
     *      bl     The bottom left corner
     *      b      The center of the bottom edge
     *      br     The bottom right corner
     *
     * Example Usage:
     *
     *      var xy = comp.getRegion().alignTo({
     *          align: 't-b',  // align comp's top/center to el's bottom/center
     *          target: el.getRegion(),
     *          anchorSize: new Ext.util.Point(10, 10),
     *          inside: new Ext.util.Region(0, Ext.Element.getViewportWidth(),
     *                                      Ext.Element.getViewportHeight(), 0)
     *      });
     *
     * @param {Object} options The alignment options.
     * @param {Ext.util.Region} options.target The rectangle to which this rectangle
     * should align.
     * @param {String} [options.align=tl-bl] The alignment descriptor for positioning this
     * rectangle with respect to the `target`. See {@link Ext.util.Positionable#alignTo}.
     * Note that if the requested alignment results in violation of the `inside` constraint,
     * the result will be flipped align to the closest edge which conforms to the constraint.
     * 
     * @param {Array/Ext.util.Position} [options.position] The position at which to place the
     * resulting region before being excluded from the target area and aligned to the closest
     * edge which allows conformity with any passed `inside` option. Used instead of the `align`
     * option.
     * @param {Ext.util.Offset/Number[]} [options.offset] An offset by which to adjust the result.
     * @param {Ext.util.Offset/Number[]} [options.anchorSize] The width and height of any external
     * anchor
     * element. This is used to calculate the true bounds of the Region inclusive of the anchor.
     * The `x` dimension is the height of the arrow in all orientations, and the `y` dimension
     * is the width of the baseline of the arrow in all dimensions.
     * If this option is used, and the returned region successfully clears the 
     * bounds of the target, then the anchor region will be returned in the return value
     * as the `anchor` property. This will in turn have a `position` property which will
     * be `'top'`, `'left`, `'right'`, or `'bottom'`.
     * @param {Boolean} [options.overlap] Pass `true` to allow this rectangle to overlap
     * the target.
     * @param {Boolean} [options.rtl] Pass `true` to swap left/right alignment.
     * @param {Ext.util.Region/Ext.dom.Element} [options.inside] The rectangle to
     * which this rectangle is constrained.
     * @param {Number} [options.minHeight] Used when this Region is to be aligned directly
     * below or above  the target. Gives the option to reduce the height to fit in the
     * available space.
     * @param {Boolean} [options.axisLock] If `true`, then fallback on constraint violation will
     * only take place along the major align axis. That is, if `align: "l-r"` is being used, and
     * `axisLock: true` is used, then if constraints fail, only fallback to `r-l` is considered.
     * @return {Ext.util.Region} The Region that will align this rectangle. Note that if
     * a `minHeight` option was passed, and alignment is either above or below the target,
     * the Region might be reduced to fit within the space.
     */
        alignTo: function(options) {
            var me = this,
                Region = me.self,
                Offset = ExtUtil.Offset,
                Element = Ext.Element,
                target = parseRegion(options.target),
                targetPlusAnchorOffset,
                rtl = options.rtl,
                overlap = options.overlap,
                align = options.align,
                anchorSize = options.anchorSize,
                offset = options.offset,
                inside = options.inside,
                position = options.position,
                allowXTranslate = options.allowXTranslate,
                allowYTranslate = options.allowYTranslate,
                wasConstrained, result, initialPosition, constrainedPosition;
            if (offset) {
                offset = Offset.fromObject(offset);
                if (!(offset instanceof Offset)) {
                    Ext.raise('offset option must be an Ext.util.Offset');
                }
            }
            if (anchorSize) {
                anchorSize = Offset.fromObject(anchorSize);
                if (!(anchorSize instanceof Offset)) {
                    Ext.raise('anchorSize option must be an Ext.util.Offset');
                }
            }
            if (inside && !inside.isRegion) {
                if (Ext.getDom(inside) === document.body) {
                    inside = new Region(0, Element.getDocumentWidth(), Element.getDocumentHeight(), 0);
                } else {
                    inside = Ext.fly(inside).getRegion();
                }
            }
            // Position the region using an exact position.
            // Our purpose is then to constrain within the inside
            // Region, while probably not occluding the target.
            if (position) {
                if (position.length === 2) {
                    position = new ExtUtil.Point(position[0], position[1]);
                }
                // Calculate the unconstrained position.
                result = new Region().copyFrom(me).setPosition(position.x, position.y);
            } else {
                // Convert string align spec to informational object
                align = me.getAlignInfo(align, rtl);
                // target is out of bounds.
                // Move it so that it's 1px inside to that the alignment points
                if (inside) {
                    if (target.x >= inside.right) {
                        target.setPosition(inside.right - 1, target.y);
                        if (align.position !== 3) {
                            align = me.getAlignInfo('r-l', rtl);
                        }
                    } else if (target.right < inside.x) {
                        target.setPosition(inside.x - target.getWidth() + 1, target.y);
                        if (align.position !== 1) {
                            align = me.getAlignInfo('l-r', rtl);
                        }
                    }
                    if (target.y >= inside.bottom) {
                        target.setPosition(target.x, inside.bottom - 1);
                        if (align.position !== 0) {
                            align = me.getAlignInfo('b-t', rtl);
                        }
                    } else if (target.bottom < inside.y) {
                        target.setPosition(target.x, inside.y - target.getHeight() + 1);
                        if (align.position !== 2) {
                            align = me.getAlignInfo('t-b', rtl);
                        }
                    }
                }
                // Adjust the adjacent edge to account for the anchor height.
                targetPlusAnchorOffset = anchorSize ? addAnchorOffset(target, anchorSize, align.position) : target;
                // Start with requested position.
                result = Region.from(me).translateBy(me.getAlignToVector(targetPlusAnchorOffset, align));
                // If they ASKED for it to intersect (eg: c-c, tl-c). we must honour that,
                // and not exclude it.
                overlap = !!result.intersect(targetPlusAnchorOffset);
                if (offset && (overlap || !anchorSize)) {
                    result.translateBy(offset);
                }
                // Calculate the anchor position.
                // This also forces the adjacent edges to overlap enough to create space
                // for the anchor arrow.
                if (anchorSize) {
                    calculateAnchorPosition(target, result, align.position, anchorSize, inside);
                }
            }
            // If we are constraining Region...
            if (inside) {
                initialPosition = result.copy();
                // Constrain to within left boundary
                if (result.left < inside.left) {
                    result.translateBy(inside.left - result.left, 0);
                    wasConstrained = true;
                }
                // If it overflows right, and there is space to move it left, then do so.
                if (result.right > inside.right && result.left > inside.left) {
                    result.translateBy(inside.right - result.right, 0);
                    wasConstrained = true;
                }
                // Constrain to within top boundary
                if (result.top < inside.top) {
                    result.translateBy(0, inside.top - result.top);
                    wasConstrained = true;
                }
                // If it overflows bottom, and there is space to move it up, then do so.
                if (result.bottom > inside.bottom && result.top > inside.top) {
                    result.translateBy(0, inside.bottom - result.bottom);
                    wasConstrained = true;
                }
                // If we've budged the result to within the constrain bounds,
                // ensure the result region does not overlay the target
                if (wasConstrained && !overlap) {
                    // Recalculate it. We must return null if anchoring is not possible.
                    result.anchor = null;
                    // axisLock means that only flipping in the align axis is allowed, not fallback
                    // to all other sides.
                    //
                    // That is, if align is l-r, and the result won't fit, it only
                    // falls back to r-l.
                    //
                    // This will be used for BoundLists which must only flip from t0-b0 to b0-t0
                    if (options.axisLock) {
                        if (align.position & 1) {
                            allowYTranslate = false;
                        } else {
                            allowXTranslate = false;
                        }
                    }
                    // If using an [X,Y] position, then only total occlusion causes exclusion
                    if (position) {
                        if (result.contains(position)) {
                            position.exclude(result, {
                                inside: inside,
                                centerOnSideChange: false
                            });
                        }
                    } else // If edge aligning, we must completely exclude the region
                    {
                        constrainedPosition = result.copy();
                        if (result.intersect(targetPlusAnchorOffset)) {
                            // This will also exclude any additional anchor even if the region itself
                            // does not intersect.
                            align.position = target.exclude(result, {
                                initialPosition: initialPosition,
                                defaultPosition: align.position,
                                inside: inside,
                                minHeight: options.minHeight,
                                minWidth: options.minWidth,
                                allowX: allowXTranslate,
                                allowY: allowYTranslate,
                                offset: offset,
                                anchorHeight: anchorSize ? anchorSize.y : 0,
                                centerOnSideChange: !!anchorSize
                            });
                        } else if (options.minWidth && result.getWidth() > inside.getWidth()) {
                            result.setPosition(0, result.y);
                            result.setWidth(Math.max(inside.getWidth(), options.minWidth));
                            result.constrainWidth = true;
                        } else if (options.minHeight && result.getHeight() > inside.getHeight()) {
                            result.setPosition(result.x, 0);
                            result.setHeight(Math.max(inside.getHeight(), options.minHeight));
                            result.constrainHeight = true;
                        }
                        result.align = align;
                        if (inside.contains(result)) {
                            // Calculate the anchor position.
                            // This also forces the adjacent edges to overlap enough to create space
                            // for the anchor arrow.
                            if (anchorSize) {
                                calculateAnchorPosition(target, result, align.position, anchorSize, inside);
                            }
                        } else // We tried everything, but couldn't fit in the "inside" region.
                        // Fall back to the constrained position overlapping the target.
                        // Usually happens on a phone where there's not enough space to edge-align
                        // and insist on no overlapping of align target    .
                        {
                            result = constrainedPosition;
                        }
                    }
                }
            }
            return result;
        },
        /**
     * This method pushes the "other" Region out of this region via the shortest
     * translation. If an "inside" Region is passed, the exclusion also honours
     * that constraint.
     * @param {Region} other The Region to move so that it does not intersect this Region.
     * @param {Object} options Object of options passed to exclude.
     * @param {Region} options.inside A Region into which the other Region must be constrained.
     * @param {Number} [options.minHeight] If passed, indicates that the height may be reduced up
     * to a point to fit the "other" region below or above the target but within the "inside"
     * Region.
     * @param {Boolean} [options.allowX=true] Pass `false` to disallow translation along the X axis.
     * @param {Boolean} [options.allowY=true] Pass `false` to disallow translation along the Y axis.
     * @return {Number} The edge it is now aligned to, 0=top, 1=right, 2=bottom, 3=left.
     */
        exclude: function(other, options) {
            options = options || {};
            // eslint-disable-next-line vars-on-top
            var me = this,
                initialPosition = options.initialPosition || other,
                inside = options.inside,
                defaultPosition = options.defaultPosition,
                centerOnSideChange = options.centerOnSideChange,
                minHeight = options.minHeight,
                minWidth = options.minWidth,
                allowX = options.allowX !== false,
                allowY = options.allowY !== false,
                anchorHeight = options.anchorHeight,
                offset = options.offset,
                translations = [],
                testRegion, t, i, sizeConstrainedSolution, leastBadSolution, intersection, result;
            // Create adjustments for each dimension so we can also exclude any anchor
            if (!offset) {
                offset = zeroOffset;
            }
            // Calculate vectors to move the "other" region by to fully clear this region.
            // Store the total moved distance, (element [4]) as the distance from the initially
            // desired position, not the constrained, overlapped position.
            /* eslint-disable max-len */
            if (allowY) {
                translations.push([
                    0,
                    me.top - other.bottom - anchorHeight + offset.y,
                    'b-t',
                    0,
                    Math.abs(me.top - initialPosition.bottom - anchorHeight + offset.y)
                ]);
                translations.push([
                    0,
                    me.bottom - other.top + anchorHeight + offset.y,
                    't-b',
                    2,
                    Math.abs(me.bottom - initialPosition.top + anchorHeight + offset.y)
                ]);
            } else {
                centerOnSideChange = false;
            }
            if (allowX) {
                translations.push([
                    me.left - other.right - anchorHeight + offset.x,
                    0,
                    'r-l',
                    3,
                    Math.abs(me.left - initialPosition.right - anchorHeight + offset.x)
                ]);
                translations.push([
                    me.right - other.left + anchorHeight + offset.x,
                    0,
                    'l-r',
                    1,
                    Math.abs(me.right - initialPosition.left + anchorHeight + offset.x)
                ]);
            } else {
                centerOnSideChange = false;
            }
            /* eslint-enable max-len */
            // Sort the exclusion vectors into order, shortest first
            Ext.Array.sort(translations, function(l, r) {
                var result = l[4] - r[4];
                // If equidistant, prefer the translation which moves to the defaultPosition
                if (!result) {
                    if (l[3] === defaultPosition) {
                        return -1;
                    }
                    if (r[3] === defaultPosition) {
                        return 1;
                    }
                }
                return result;
            });
            // We might have to fall back through the choices of direction
            // until we find one which doesn't violate the constraints.
            if (inside) {
                for (i = 0; i < translations.length; i++) {
                    t = translations[i];
                    testRegion = ExtUtil.Region.from(other);
                    testRegion.translateBy.apply(testRegion, t);
                    // When we find a translation that satisfies the constraint, we're done
                    if (inside.contains(testRegion)) {
                        other.copyFrom(testRegion);
                        result = {
                            align: t[2],
                            position: t[3],
                            distance: t[4]
                        };
                        break;
                    }
                    // If we are directly above or below and we are allowed to shrink the
                    // height, and it's too high, then calculate a height constrained solution
                    // to which we can fall back if no translations are fully successful.
                    if (minHeight) {
                        checkMinHeight(minHeight, testRegion, me, inside);
                        if (inside.contains(testRegion)) {
                            // eslint-disable-next-line max-len
                            if (!sizeConstrainedSolution || testRegion.getArea() > sizeConstrainedSolution.region.getArea()) {
                                sizeConstrainedSolution = {
                                    region: testRegion,
                                    align: t[2],
                                    position: t[3],
                                    distance: t[4]
                                };
                            }
                        }
                    }
                    if (minWidth) {
                        checkMinWidth(minWidth, testRegion, me, inside);
                        if (inside.contains(testRegion)) {
                            // eslint-disable-next-line max-len
                            if (!sizeConstrainedSolution || testRegion.getArea() > sizeConstrainedSolution.region.getArea()) {
                                sizeConstrainedSolution = {
                                    region: testRegion,
                                    align: t[2],
                                    position: t[3],
                                    distance: t[4]
                                };
                            }
                        }
                    }
                    // If all else fails, keep track of the translation which yields the largest
                    // intersection with the "inside" region. If there's no translation which satisfies
                    // the constraint,  use this least bad one.
                    intersection = inside.intersect(testRegion);
                    if (intersection) {
                        intersection = intersection.getArea();
                        // eslint-disable-next-line max-len
                        if (!leastBadSolution || (intersection && leastBadSolution.area < intersection)) {
                            leastBadSolution = {
                                region: testRegion,
                                align: t[2],
                                position: t[3],
                                distance: t[4],
                                area: intersection
                            };
                        }
                    }
                }
                if (!result) {
                    // Only constrain height if other translations fail.
                    if (sizeConstrainedSolution) {
                        other.copyFrom(sizeConstrainedSolution.region);
                        result = sizeConstrainedSolution;
                        other.constrainWidth = sizeConstrainedSolution.region.constrainWidth;
                        other.constrainHeight = sizeConstrainedSolution.region.constrainHeight;
                    }
                    // Only use the least bad failed solution as a last resort.
                    else if (leastBadSolution) {
                        other.copyFrom(leastBadSolution.region);
                        result = leastBadSolution;
                    }
                }
                if (result) {
                    // The exclude switched align axis (t/b to l/r), flip it to a center align on
                    // the new side.
                    if ((result.position & 1) !== (defaultPosition & 1)) {
                        if (result.distance && centerOnSideChange) {
                            t = other.alignTo({
                                align: result.align,
                                target: me,
                                anchorSize: anchorHeight,
                                offset: offset,
                                axisLock: true,
                                inside: inside,
                                minHeight: options.minHeight,
                                minWidth: options.minWidth
                            });
                            if (inside.contains(t)) {
                                other.setPosition(t.x, t.y);
                            }
                        }
                    }
                    return result.position;
                }
            } else // No external constraint
            {
                // Move by the shortest path
                other.translateBy.apply(other, translations[0]);
                return translations[0][3];
            }
            return defaultPosition;
        },
        getAlignToXY: function(target, align, rtl) {
            var alignVector = this.getAlignToVector(target, align, rtl);
            return [
                this.x + alignVector[0],
                this.y + alignVector[1]
            ];
        },
        getAnchorPoint: function(align, rtl) {
            align = (typeof align === 'string') ? this.getAlignInfo(align + '-tl', rtl) : align;
            return this['getAnchorPoint_' + align.myEdge](align.myOffset);
        },
        getAlignToVector: function(target, align, rtl) {
            align = (typeof align === 'string') ? this.getAlignInfo(align, rtl) : align;
            // eslint-disable-next-line vars-on-top
            var myAnchorPoint = this['getAnchorPoint_' + align.myEdge](align.myOffset),
                targetAnchorPoint = target['getAnchorPoint_' + align.otherEdge](align.otherOffset);
            return [
                targetAnchorPoint[0] - myAnchorPoint[0],
                targetAnchorPoint[1] - myAnchorPoint[1]
            ];
        },
        getAnchorPoint_t: function(offset) {
            return [
                this.x + Math.round(this.getWidth() * (offset / 100)),
                this.y
            ];
        },
        getAnchorPoint_b: function(offset) {
            return [
                this.x + Math.round(this.getWidth() * (offset / 100)),
                this.bottom
            ];
        },
        getAnchorPoint_l: function(offset) {
            return [
                this.x,
                this.y + Math.round(this.getHeight() * (offset / 100))
            ];
        },
        getAnchorPoint_r: function(offset) {
            return [
                this.right,
                this.y + Math.round(this.getHeight() * (offset / 100))
            ];
        },
        getAnchorPoint_c: function() {
            return [
                this.x + Math.round(this.getWidth() / 2),
                this.y + Math.round(this.getHeight() / 2)
            ];
        },
        getCenter: function() {
            return [
                this.x + this.width / 2,
                this.y + this.height / 2
            ];
        },
        getHeight: function() {
            return this.bottom - this.y;
        },
        getWidth: function() {
            return this.right - this.x;
        },
        getArea: function() {
            return this.getHeight() * this.getWidth();
        },
        setHeight: function(h) {
            this.height = h;
            this.bottom = this.top + h;
            return this;
        },
        setWidth: function(w) {
            this.width = w;
            this.right = this.left + w;
            return this;
        },
        /**
     * Get the width / height of this region
     * @return {Object} an object with width and height properties
     * @private
     */
        getSize: function() {
            return {
                width: this.right - this.x,
                height: this.bottom - this.y
            };
        },
        setSize: function(w, h) {
            if (h === undefined) {
                h = w;
            }
            this.setWidth(w);
            return this.setHeight(h);
        },
        /**
     * Create a copy of this Region.
     * @return {Ext.util.Region}
     */
        copy: function() {
            return new this.self(this.y, this.right, this.bottom, this.x);
        },
        /**
     * Copy the values of another Region to this Region
     * @param {Ext.util.Region} p The region to copy from.
     * @return {Ext.util.Region} This Region
     */
        copyFrom: function(p) {
            var me = this;
            me.top = me.y = me[1] = p.y;
            me.right = p.right;
            me.bottom = p.bottom;
            me.left = me.x = me[0] = p.x;
            return me;
        },
        /*
     * Dump this to an eye-friendly string, great for debugging
     * @return {String}
     */
        toString: function() {
            return "Region[" + this.top + "," + this.right + "," + this.bottom + "," + this.left + "]";
        },
        /**
     * Translate this Region by the given offset amount
     * @param {Ext.util.Offset/Object} x Object containing the `x` and `y` properties.
     * Or the x value is using the two argument form.
     * @param {Number} y The y value unless using an Offset object.
     * @return {Ext.util.Region} this This Region
     */
        translateBy: function(x, y) {
            var me = this;
            if (x.length) {
                y = x[1];
                x = x[0];
            } else if (arguments.length === 1) {
                y = x.y;
                x = x.x;
            }
            me.top = me.y += y;
            me.right += x;
            me.bottom += y;
            me.left = me.x += x;
            return me;
        },
        /**
     * Round all the properties of this region
     * @return {Ext.util.Region} this This Region
     */
        round: function() {
            var me = this;
            me.top = me.y = Math.round(me.y);
            me.right = Math.round(me.right);
            me.bottom = Math.round(me.bottom);
            me.left = me.x = Math.round(me.x);
            return me;
        },
        /**
     * Check whether this region is equivalent to the given region
     * @param {Ext.util.Region} region The region to compare with
     * @return {Boolean}
     */
        equals: function(region) {
            return (this.top === region.top && this.right === region.right && this.bottom === region.bottom && this.left === region.left);
        },
        /**
     * Returns the offsets of this region from the passed region or point.
     * @param {Ext.util.Region/Ext.util.Point} offsetsTo The region or point to get get
     * the offsets from.
     * @return {Object} The XY page offsets
     * @return {Number} return.x The x offset
     * @return {Number} return.y The y offset
     */
        getOffsetsTo: function(offsetsTo) {
            return {
                x: this.x - offsetsTo.x,
                y: this.y - offsetsTo.y
            };
        },
        highlight: function() {
            // eslint-disable-line comma-style
            var highlightEl = Ext.getBody().createChild({
                    style: 'background-color:#52a0db;opacity:0.4;position:absolute;z-index:9999999'
                });
            highlightEl.setBox(this);
            Ext.defer(function() {
                highlightEl.destroy();
            }, 5000);
            return highlightEl;
        }
    };
}, function(Region) {
    Region.prototype.getAlignInfo = Region.getAlignInfo;
    Region.EMPTY = new Region(0, 0, 0, 0);
    if (Object.freeze) {
        Object.freeze(Region.EMPTY);
    }
});

/**
 * Represents a 2D point with x and y properties, useful for comparison and instantiation
 * from an event:
 *
 *     var point = Ext.util.Point.fromEvent(e);
 */
Ext.define('Ext.util.Point', {
    extend: 'Ext.util.Region',
    isPoint: true,
    radianToDegreeConstant: 180 / Math.PI,
    origin: {
        x: 0,
        y: 0
    },
    statics: {
        /**
         * Returns a new instance of {@link Ext.util.Point} based on the `pageX` / `pageY` values
         * of the given event.
         * @static
         * @param {Event} e The event.
         * @return {Ext.util.Point}
         */
        fromEvent: function(e) {
            var changedTouches = e.changedTouches,
                touch = (changedTouches && changedTouches.length > 0) ? changedTouches[0] : e;
            return this.fromTouch(touch);
        },
        /**
         * Returns a new instance of {@link Ext.util.Point} based on the `pageX` / `pageY` values
         * of the given touch.
         * @static
         * @param {Event} touch
         * @return {Ext.util.Point}
         */
        fromTouch: function(touch) {
            return new this(touch.pageX, touch.pageY);
        },
        /**
         * Returns a new point from an object that has `x` and `y` properties, if that object
         * is not an instance of {@link Ext.util.Point}. Otherwise, returns the given point itself.
         * @param {Object} object
         * @return {Ext.util.Point}
         */
        from: function(object) {
            if (!object) {
                return new this(0, 0);
            }
            if (!(object instanceof this)) {
                return new this(object.x, object.y);
            }
            return object;
        }
    },
    /**
     * Creates point on 2D plane.
     * @param {Number} [x=0] X coordinate.
     * @param {Number} [y=0] Y coordinate.
     */
    constructor: function(x, y) {
        if (x == null) {
            x = 0;
        }
        if (y == null) {
            y = 0;
        }
        this.callParent([
            y,
            x,
            y,
            x
        ]);
    },
    /**
     * Copy a new instance of this point.
     * @return {Ext.util.Point} The new point.
     */
    clone: function() {
        return new this.self(this.x, this.y);
    },
    /**
     * Clones this Point.
     * @deprecated 2.0.0 Please use {@link #clone} instead.
     * @return {Ext.util.Point} The new point.
     */
    copy: function() {
        return this.clone.apply(this, arguments);
    },
    /**
     * Copy the `x` and `y` values of another point / object to this point itself.
     * @param {Ext.util.Point/Object} point.
     * @return {Ext.util.Point} This point.
     */
    copyFrom: function(point) {
        this.x = point.x;
        this.y = point.y;
        return this;
    },
    /**
     * Returns a human-eye-friendly string that represents this point,
     * useful for debugging.
     * @return {String} For example `Point[12,8]`.
     */
    toString: function() {
        return "Point[" + this.x + "," + this.y + "]";
    },
    /**
     * Compare this point and another point.
     * @param {Ext.util.Point/Object} point The point to compare with, either an instance
     * of {@link Ext.util.Point} or an object with `x` and `y` properties.
     * @return {Boolean} Returns whether they are equivalent.
     */
    equals: function(point) {
        return (this.x === point.x && this.y === point.y);
    },
    /**
     * Returns `true` if the passed point is within a certain distance of this point.
     * @param {Ext.util.Point/Object} point The point to check with, either an instance
     * of {@link Ext.util.Point} or an object with `x` and `y` properties.
     * @param {Object/Number} threshold Can be either an object with `x` and `y` properties
     * or a number.
     * @return {Boolean}
     */
    isCloseTo: function(point, threshold) {
        if (typeof threshold === 'number') {
            return this.getDistanceTo(point) <= threshold;
        }
        // eslint-disable-next-line vars-on-top
        var x = point.x,
            y = point.y,
            thresholdX = threshold.x,
            thresholdY = threshold.y;
        return (this.x <= x + thresholdX && this.x >= x - thresholdX && this.y <= y + thresholdY && this.y >= y - thresholdY);
    },
    /**
     * Returns `true` if this point is close to another one.
     * @deprecated 2.0.0 Please use {@link #isCloseTo} instead.
     * @return {Boolean}
     */
    isWithin: function() {
        return this.isCloseTo.apply(this, arguments);
    },
    /**
     * Determins whether this Point contained by the passed Region, Component or element.
     * @param {Ext.util.Region/Ext.Component/Ext.dom.Element/HTMLElement} region
     * The rectangle to check that this Point is within.
     * @return {Boolean}
     */
    isContainedBy: function(region) {
        if (!(region instanceof Ext.util.Region)) {
            region = Ext.get(region.el || region).getRegion();
        }
        return region.contains(this);
    },
    /**
     * Compare this point with another point when the `x` and `y` values of both points are rounded.
     * For example: [100.3,199.8] will equals to [100, 200].
     * @param {Ext.util.Point/Object} point The point to compare with, either an instance
     * of Ext.util.Point or an object with `x` and `y` properties.
     * @return {Boolean}
     */
    roundedEquals: function(point) {
        if (!point || typeof point !== 'object') {
            point = this.origin;
        }
        return (Math.round(this.x) === Math.round(point.x) && Math.round(this.y) === Math.round(point.y));
    },
    getDistanceTo: function(point) {
        if (!point || typeof point !== 'object') {
            point = this.origin;
        }
        // eslint-disable-next-line vars-on-top
        var deltaX = this.x - point.x,
            deltaY = this.y - point.y;
        return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
    },
    getAngleTo: function(point) {
        if (!point || typeof point !== 'object') {
            point = this.origin;
        }
        // eslint-disable-next-line vars-on-top
        var deltaX = this.x - point.x,
            deltaY = this.y - point.y;
        return Math.atan2(deltaY, deltaX) * this.radianToDegreeConstant;
    }
}, function() {
    /**
     * @method translate
     * @member Ext.util.Point
     * Alias for {@link #translateBy}
     * @inheritdoc Ext.util.Region#method-translateBy
     */
    this.prototype.translate = this.prototype.translateBy;
});

/**
 * Just as {@link Ext.dom.Element} wraps around a native DOM node, {@link Ext.event.Event} wraps
 * the browser's native event-object normalizing cross-browser differences such as mechanisms
 * to stop event-propagation along with a method to prevent default actions from taking place.
 *
 * Here is a simple example of how you use it:
 *
 *     @example
 *     var container = Ext.create('Ext.Container', {
 *         layout: 'fit',
 *         renderTo: Ext.getBody(),
 *         items: [{
 *             id: 'logger',
 *             html: 'Click somewhere!',
 *             padding: 5
 *         }]
 *     });
 *
 *     container.getEl().on({
 *         click: function(e, node) {
 *             var string = '';
 *
 *             string += 'You clicked at: { x: ' + e.pageX + ', y: ' + e.pageY + ' } ' +
                         '<i>(e.pageX & e.pageY)</i>';
 *             string += '<hr />';
 *             string += 'The HTMLElement you clicked has the className of: <strong>' +
                         e.target.className + '</strong> <i>(e.target)</i>';
 *             string += '<hr />';
 *             string += 'The HTMLElement which has the listener has a className of: <strong>' +
                          e.currentTarget.className + '</strong> <i>(e.currentTarget)</i>';
 *
 *             Ext.getCmp('logger').setHtml(string);
 *         }
 *     });
 *
 * ## Recognizers
 *
 * Ext JS includes many default event recognizers to know when a user interacts with
 * the application.
 *
 * For a full list of default recognizers, and more information, please view the
 * {@link Ext.event.gesture.Recognizer} documentation.
 * 
 * This class also provides a set of constants for use with key events.  These are useful
 * for determining if a specific key was pressed, and are available both on instances,
 * and as static properties of the class.  The following two statements are equivalent:
 * 
 *     if (e.getKey() === Ext.event.Event.TAB) {
 *         // tab key was pressed
 *     }
 * 
 *     if (e.getKey() === e.TAB) {
 *         // tab key was pressed
 *     }
 */
Ext.define('Ext.event.Event', {
    alternateClassName: 'Ext.EventObjectImpl',
    requires: [
        'Ext.util.Point'
    ],
    /**
     * @property {Number} distance
     * The distance of the event.
     *
     * **This is only available when the event type is `swipe` and `pinch`.**
     */
    /**
     * @property {HTMLElement} target
     * The element that fired this event.  For the element whose handlers are currently
     * being processed, i.e. the element that the event handler was attached to, use
     * `currentTarget`
     */
    /**
     * @property {HTMLElement} currentTarget
     * Refers to the element the event handler was attached to, vs the `target`, which is
     * the actual element that fired the event.  For example, if the event bubbles, the
     * `target` element may be a descendant of the `currentTarget`, as the event may
     * have been triggered on the `target` and then bubbled up to the `currentTarget`
     * where it was handled.
     */
    /**
     * @property {HTMLElement} delegatedTarget
     * Same as `currentTarget`
     * @deprecated 5.0.0 use {@link #currentTarget} instead.
     */
    /**
     * @property {Number} button
     * Indicates which mouse button caused the event for mouse events, for example
     * `mousedown`, `click`, `mouseup`:
     * - `0` for left button.
     * - `1` for middle button.
     * - `2` for right button.
     *
     * *Note*: In IE8 & IE9, the `click` event does not provide the button.
     */
    /**
     * @property {Number} pageX The browsers x coordinate of the event.
     * Note: this only works in browsers that support pageX on the native browser event
     * object (pageX is not natively supported in IE9 and earlier).  In Ext JS, for a
     * cross browser normalized x-coordinate use {@link #getX}
     */
    /**
     * @property {Number} pageY The browsers y coordinate of the event.
     * Note: this only works in browsers that support pageY on the native browser event
     * object (pageY is not natively supported in IE9 and earlier).  In Ext JS, for a
     * cross browser normalized y-coordinate use {@link #getY}
     */
    /**
     * @property {Boolean} ctrlKey
     * True if the control key was down during the event.
     * In Mac this will also be true when meta key was down.
     */
    /**
     * @property {Boolean} altKey
     * True if the alt key was down during the event.
     */
    /**
     * @property {Boolean} shiftKey
     * True if the shift key was down during the event.
     */
    /**
     * @property {Event} browserEvent
     * The raw browser event which this object wraps.
     */
    /**
     * @property {String} pointerType
     * The pointer type for this event. May be empty if the event was
     * not triggered by a pointer. Current available types are:
     * - `mouse`
     * - `touch`
     * - `pen`
     */
    /**
     * @property {Boolean}
     * `true` if {@link #stopPropagation} has been called on this instance
     * @private
     */
    stopped: false,
    /**
     * @property {Boolean}
     * `true` if {@link #claimGesture} has been called on this instance
     * @private
     */
    claimed: false,
    /**
     * @property {Boolean}
     * Indicates whether or not {@link #preventDefault preventDefault()} was called on the event.
     */
    defaultPrevented: false,
    isEvent: true,
    // With these events, the relatedTarget can sometimes be an Object literal in FF.
    geckoRelatedTargetEvents: {
        blur: 1,
        dragenter: 1,
        dragleave: 1,
        focus: 1
    },
    statics: {
        resolveTextNode: function(node) {
            return (node && node.nodeType === 3) ? node.parentNode : node;
        },
        /**
         * @private
         * An amalgamation of pointerEvents/mouseEvents/touchEvents.
         * Will be populated in class callback.
         */
        gestureEvents: {},
        /**
         * @private
         */
        pointerEvents: {
            pointerdown: 1,
            pointermove: 1,
            pointerup: 1,
            pointercancel: 1,
            pointerover: 1,
            pointerout: 1,
            pointerenter: 1,
            pointerleave: 1,
            MSPointerDown: 1,
            MSPointerMove: 1,
            MSPointerUp: 1,
            MSPointerOver: 1,
            MSPointerOut: 1,
            MSPointerCancel: 1,
            MSPointerEnter: 1,
            MSPointerLeave: 1
        },
        /**
         * @private
         */
        mouseEvents: {
            mousedown: 1,
            mousemove: 1,
            mouseup: 1,
            mouseover: 1,
            mouseout: 1,
            mouseenter: 1,
            mouseleave: 1
        },
        /**
         * @private
         * These are tracked separately from mouseEvents because the mouseEvents map
         * is used by Dom publisher to eliminate duplicate events on devices that fire
         * multiple kinds of events (mouse, touch, pointer).  Adding click events to the
         * mouse events map can cause click events to be blocked from firing in some cases.
         */
        clickEvents: {
            click: 1,
            dblclick: 1
        },
        /**
         * @private
         */
        touchEvents: {
            touchstart: 1,
            touchmove: 1,
            touchend: 1,
            touchcancel: 1
        },
        /**
         * @private
         */
        focusEvents: {
            focus: 1,
            focusin: 1,
            focusenter: 1
        },
        /**
         * @private
         */
        blurEvents: {
            blur: 1,
            focusout: 1,
            focusleave: 1
        },
        /**
         * @private
         */
        wheelEvents: {
            wheel: 1,
            mousewheel: 1
        },
        // msPointerTypes in IE10 are numbers, in the w3c spec they are strings.
        // this map allows us to normalize the pointerType for an event
        // http://www.w3.org/TR/pointerevents/#widl-PointerEvent-pointerType
        // http://msdn.microsoft.com/en-us/library/ie/hh772359(v=vs.85).aspx
        pointerTypeMap: {
            2: 'touch',
            3: 'pen',
            4: 'mouse',
            touch: 'touch',
            pen: 'pen',
            mouse: 'mouse'
        },
        keyEventRe: /^key/,
        keyFlags: {
            CTRL: 'ctrlKey',
            CONTROL: 'ctrlKey',
            ALT: 'altKey',
            SHIFT: 'shiftKey',
            CMD: 'metaKey',
            COMMAND: 'metaKey',
            CMDORCTRL: Ext.isMac ? 'metaKey' : 'ctrlKey',
            COMMANDORCONTROL: Ext.isMac ? 'metaKey' : 'ctrlKey',
            META: 'metaKey'
        },
        modifierGlyphs: {
            ctrlKey: '⌃',
            altKey: '⌥',
            metaKey: Ext.isMac ? '⌘' : '⊞',
            shiftKey: '⇧'
        },
        specialKeyGlyphs: {
            BACKSPACE: '⌫',
            TAB: '⇥',
            ENTER: '⏎',
            RETURN: '⏎',
            SPACE: '␣',
            PAGE_UP: '⇞',
            PAGE_DOWN: '⇟',
            END: '⇲',
            HOME: '⌂',
            LEFT: '←',
            UP: '↑',
            RIGHT: '→',
            DOWN: '↓',
            PRINT_SCREEN: '⎙',
            INSERT: '⎀',
            DELETE: '⌦',
            CONTEXT_MENU: '☰'
        },
        // eslint-disable-next-line no-useless-escape
        _hyphenRe: /^[a-z]+\-/i,
        // eg "Ctrl-" (instead of "Ctrl+")
        /**
         * Convert a key specification in the form eg: "CTRL+ALT+DELETE" to the glyph sequence
         * for use in menu items, eg "⌃⌥⌦".
         * @private
         */
        getKeyId: function(keyName) {
            // Translate eg: 27 to "ESC"
            if (typeof keyName === 'number') {
                keyName = this.keyCodes[keyName];
            } else {
                keyName = keyName.toUpperCase();
            }
            // eslint-disable-next-line vars-on-top
            var me = this,
                delim = me._hyphenRe.test(keyName) ? '-' : '+',
                parts = (keyName === delim) ? [
                    delim
                ] : keyName.split(delim),
                numModifiers = parts.length - 1,
                rawKey = parts[numModifiers],
                result = [],
                eventFlag, i;
            if (!Ext.event.Event[rawKey]) {
                Ext.raise('Invalid key name: "' + rawKey + '"');
            }
            for (i = 0; i < numModifiers; i++) {
                eventFlag = me.keyFlags[parts[i]];
                if (!eventFlag) {
                    Ext.raise('Invalid key modifier: "' + parts[i] + '"');
                }
                result[eventFlag] = true;
            }
            if (result.ctrlKey) {
                result.push(me.modifierGlyphs.ctrlKey);
            }
            if (result.altKey) {
                result.push(me.modifierGlyphs.altKey);
            }
            if (result.shiftKey) {
                result.push(me.modifierGlyphs.shiftKey);
            }
            if (result.metaKey) {
                result.push(me.modifierGlyphs.metaKey);
            }
            result.push(this.specialKeyGlyphs[rawKey] || rawKey);
            return result.join('');
        },
        /**
         * @private
         */
        globalTabKeyDown: function(e) {
            if (e.keyCode === 9) {
                Ext.event.Event.forwardTab = !e.shiftKey;
            }
        },
        /**
         * @private
         */
        globalTabKeyUp: function(e) {
            if (e.keyCode === 9) {
                delete Ext.event.Event.forwardTab;
            }
        }
    },
    constructor: function(event) {
        var me = this,
            self = me.self,
            resolveTextNode = me.self.resolveTextNode,
            changedTouches = event.changedTouches,
            // The target object from which to obtain the coordinates (pageX, pageY). For
            // mouse and pointer events this is simply the event object itself, but touch
            // events have their coordinates on the "Touch" object(s) instead.
            coordinateOwner = changedTouches ? changedTouches[0] : event,
            type = event.type,
            pointerType, relatedTarget;
        // This is a milliseconds value with decimal precision.
        me.timeStamp = me.time = Ext.ticks();
        me.pageX = coordinateOwner.pageX;
        me.pageY = coordinateOwner.pageY;
        me.clientX = coordinateOwner.clientX;
        me.clientY = coordinateOwner.clientY;
        me.target = me.delegatedTarget = resolveTextNode(event.target);
        me.currentTarget = resolveTextNode(event.currentTarget);
        relatedTarget = event.relatedTarget;
        if (relatedTarget) {
            if (Ext.isGecko && me.geckoRelatedTargetEvents[type]) {
                try {
                    me.relatedTarget = resolveTextNode(relatedTarget);
                } catch (e) {
                    me.relatedTarget = null;
                }
            } else {
                me.relatedTarget = resolveTextNode(relatedTarget);
            }
        }
        me.browserEvent = me.event = event;
        me.type = type;
        // set button to 0 if undefined so that touchstart, touchend, and tap will quack
        // like left mouse button mousedown mouseup, and click
        me.button = event.button || 0;
        me.shiftKey = event.shiftKey;
        // mac metaKey behaves like ctrlKey
        me.ctrlKey = event.ctrlKey || event.metaKey || false;
        me.altKey = event.altKey;
        me.charCode = event.charCode;
        me.keyCode = event.keyCode;
        me.buttons = event.buttons;
        // When invoking synthetic events, current APIs do not
        // have the ability to specify the buttons config, which
        // defaults to button. For buttons, 0 means no button
        // is pressed, whereas for button, 0 means left click.
        // Normalize that here
        if (me.button === 0 && me.buttons === 0) {
            me.buttons = 1;
        }
        if (self.focusEvents[type] || self.blurEvents[type]) {
            if (self.forwardTab !== undefined) {
                me.forwardTab = self.forwardTab;
            }
            if (self.focusEvents[type]) {
                me.fromElement = event.relatedTarget;
                me.toElement = event.target;
            } else {
                me.fromElement = event.target;
                me.toElement = event.relatedTarget;
            }
        } else if (type !== 'keydown') {
            // Normally this property should be cleaned up in keyup handler;
            // however that one might never come if something prevented default
            // on the keydown. Make sure the property won't get stuck.
            delete self.forwardTab;
        }
        if (self.mouseEvents[type]) {
            pointerType = 'mouse';
        } else if (self.clickEvents[type]) {
            // On pointer events browsers like IE11 and Edge click events have a pointerType
            // property.  On touch events devices we check if the last touchend event
            // happened less than a second ago - if so we can reasonably assume that
            // the click event happened because of a tap on the screen.
            pointerType = self.pointerTypeMap[event.pointerType] || (((Ext.now() - Ext.event.publisher.Dom.lastTouchEndTime) < 1000) ? 'touch' : 'mouse');
        }
        // eslint-disable-line max-len
        else if (self.pointerEvents[type]) {
            // In electron the mousemove event when the mouse first enters the document
            // can have a pointerType of "", so default it to mouse if not found in the map.
            pointerType = self.pointerTypeMap[event.pointerType] || 'mouse';
        } else if (self.touchEvents[type]) {
            pointerType = 'touch';
        }
        if (pointerType) {
            me.pointerType = pointerType;
        }
        // Is this is not the primary touch for PointerEvents (first touch)
        // or there are multiples touches for Touch Events
        me.isMultitouch = event.isPrimary === false || (event.touches && event.touches.length > 1);
        if (self.wheelEvents[type]) {
            me.getWheelDeltas();
        }
    },
    // call deprecated method to initialize deltaX and deltaY props
    /**
     * Creates a new Event object that is prototype-chained to this event.  Useful for
     * creating identical events so that certain properties can be changed without
     * affecting the original event.  For example, translated events have their "type"
     * corrected in this manner.
     * @param {Object} props properties to set on the chained event
     * @private
     */
    chain: function(props) {
        var e = Ext.Object.chain(this);
        e.parentEvent = this;
        // needed for stopPropagation
        return Ext.apply(e, props);
    },
    /**
     * Correctly scales a given wheel delta.
     * @param {Number} delta The delta value.
     * @private
     */
    correctWheelDelta: function(delta) {
        var me = this,
            // 0 = pixel
            // 1 = line
            // 2 = page
            deltaMode = me.browserEvent.deltaMode,
            correctedDelta = delta;
        if (deltaMode === 0) {
            correctedDelta = delta * me.WHEEL_PIXEL_SIZE;
        } else if (deltaMode === 1) {
            correctedDelta = delta * me.WHEEL_LINE_SIZE;
        } else if (deltaMode === 2) {
            correctedDelta = delta * me.WHEEL_PAGE_SIZE;
        }
        return Math.round(correctedDelta);
    },
    getChar: function() {
        var r = this.which();
        return String.fromCharCode(r);
    },
    /**
     * Gets the character code for the event.
     * @return {Number}
     */
    getCharCode: function() {
        return this.charCode || this.keyCode;
    },
    /**
     * Returns a normalized keyCode for the event.
     * @return {Number} The key code
     */
    getKey: function() {
        return this.keyCode || this.charCode;
    },
    /**
     * Returns the name of the keyCode for the event.
     * @return {String} The key name
     */
    getKeyName: function() {
        return this.type === 'keypress' ? String.fromCharCode(this.getCharCode()) : this.keyCodes[this.keyCode];
    },
    /**
     * Returns the `key` property of a keyboard event.
     * @return {String}
     */
    key: function() {
        return this.browserEvent.key;
    },
    which: function() {
        var me = this,
            e = me.browserEvent,
            r = e.which;
        if (r == null) {
            if (me.self.keyEventRe.test(e.type)) {
                r = e.charCode || e.keyCode;
            } else if ((r = e.button) !== undefined) {
                // L M R button codes (1, 4, 2):
                r = (r & 1) ? 1 : ((r & 4) ? 2 : ((r & 2) ? 3 : 0));
            }
        }
        return r;
    },
    /**
     * If this is an event of {@link #type} `paste`, this returns the clipboard data
     * of the pasesd mime type.
     *
     * @param {String} [type='text/plain'] The mime type of the data to extract from the
     * clipabord.
     *
     * Note that this uses non-standard browaer APIs and may not work reliably on all
     * platforms.
     * @return {Mixed} The clipboard data.
     * @since 6.5.1
     */
    getClipboardData: function(type) {
        var clipboardData = this.browserEvent.clipboardData,
            clipIE = Ext.global.clipboardData,
            // IE
            result = null,
            typeIE;
        type = type || 'text/plain';
        if (clipboardData && clipboardData.getData) {
            result = clipboardData.getData(type);
        } else if (clipIE && clipIE.getData) {
            typeIE = this.ieMimeType[type];
            if (typeIE) {
                result = clipIE.getData(typeIE);
            }
        }
        return result;
    },
    /**
     * Returns a point object that consists of the object coordinates.
     * @return {Ext.util.Point} point
     */
    getPoint: function() {
        var me = this,
            point = me.point,
            xy;
        if (!point) {
            xy = me.getXY();
            point = me.point = new Ext.util.Point(xy[0], xy[1]);
        }
        return point;
    },
    /**
     * Gets the related target.
     * @param {String} [selector] A simple selector to filter the target or look for an
     * ancestor of the target. See {@link Ext.dom.Query} for information about simple
     * selectors.
     * @param {Number/HTMLElement} [maxDepth] The max depth to search as a number or
     * element (defaults to 10 || document.body).
     * @param {Boolean} [returnEl] `true` to return a Ext.Element object instead of DOM
     * node.
     * @return {HTMLElement}
     */
    getRelatedTarget: function(selector, maxDepth, returnEl) {
        var relatedTarget = this.relatedTarget,
            target = null;
        // In some cases in IE10/11, when the mouse is leaving the document over a scrollbar
        // the relatedTarget will be an empty object literal. So just check we have an element
        // looking object here before we proceed.
        if (relatedTarget && relatedTarget.nodeType) {
            if (selector) {
                target = Ext.fly(relatedTarget).findParent(selector, maxDepth, returnEl);
            } else {
                target = returnEl ? Ext.get(relatedTarget) : relatedTarget;
            }
        }
        return target;
    },
    /**
     * Gets the target for the event.
     * @param {String} selector (optional) A simple selector to filter the target or look
     * for an ancestor of the target
     * @param {Number/Mixed} [maxDepth=10||document.body] (optional) The max depth to
     * search as a number or element (defaults to 10 || document.body)
     * @param {Boolean} returnEl (optional) `true` to return a Ext.Element object instead
     * of DOM node.
     * @return {HTMLElement}
     */
    getTarget: function(selector, maxDepth, returnEl) {
        return selector ? Ext.fly(this.target).findParent(selector, maxDepth, returnEl) : (returnEl ? Ext.get(this.target) : this.target);
    },
    /**
     * Returns the time of the event.
     * @return {Date}
     */
    getTime: function() {
        return this.time;
    },
    /**
     * Normalizes mouse wheel y-delta across browsers. To get x-delta information, use
     * {@link #getWheelDeltas} instead.
     * @return {Number} The mouse wheel y-delta
     * @deprecated 6.6.0 Use {@link #deltaY} instead
     */
    getWheelDelta: function() {
        var deltas = this.getWheelDeltas();
        return deltas.y;
    },
    /**
     * Returns the mouse wheel deltas for this event.
     * @return {Object} An object with "x" and "y" properties holding the mouse wheel deltas.
     * @deprecated 6.7.0 Use {@link #deltaX} and {@link #deltaY} instead
     */
    getWheelDeltas: function() {
        var me = this,
            wheelDeltas = me.wheelDeltas,
            browserEvent, deltaX, deltaY;
        if (!wheelDeltas) {
            browserEvent = me.browserEvent;
            deltaX = me.correctWheelDelta(browserEvent.deltaX || 0);
            deltaY = browserEvent.deltaY;
            deltaY = me.correctWheelDelta(deltaY == null ? -browserEvent.wheelDelta : deltaY);
            /**
             * @property {Number} deltaX
             */
            me.deltaX = deltaX;
            /**
             * @property {Number} deltaY
             */
            me.deltaY = deltaY;
            me.wheelDeltas = wheelDeltas = {
                x: deltaX,
                y: deltaY
            };
        }
        return wheelDeltas;
    },
    /**
     * Gets the x coordinate of the event.
     * @return {Number}
     */
    getX: function() {
        return this.getXY()[0];
    },
    /**
     * Gets the X and Y coordinates of the event.
     * @return {Number[]} The xy values like [x, y]
     */
    getXY: function() {
        var me = this,
            xy = me.xy;
        if (!xy) {
            xy = me.xy = [
                me.pageX,
                me.pageY
            ];
            // eslint-disable-next-line vars-on-top, one-var
            var x = xy[0],
                browserEvent, doc, docEl, body;
            // pageX/pageY not available (undefined, not null), use clientX/clientY instead
            if (!x && x !== 0) {
                browserEvent = me.browserEvent;
                doc = document;
                docEl = doc.documentElement;
                body = doc.body;
                xy[0] = browserEvent.clientX + (docEl && docEl.scrollLeft || body && body.scrollLeft || 0) - (docEl && docEl.clientLeft || body && body.clientLeft || 0);
                xy[1] = browserEvent.clientY + (docEl && docEl.scrollTop || body && body.scrollTop || 0) - (docEl && docEl.clientTop || body && body.clientTop || 0);
            }
        }
        return xy;
    },
    /**
     * @private
     * Gets the event coordinates relative to the `currentTarget`s position.
     * @param {Boolean} clip
     * The returned coordinates are guaranteed to be within
     * [0, width] and [0, height] of the target, if `clip` is set to `true`.
     * @return {Number[]} The xy values like [x, y]
     */
    getLocalXY: function(clip) {
        // TODO: check with RTL
        var pageXY = this.getXY(),
            targetXY = Ext.fly(this.currentTarget).getXY(),
            localX = pageXY[0] - targetXY[0],
            localY = pageXY[1] - targetXY[1],
            size;
        if (clip) {
            size = Ext.fly(this.currentTarget).getSize();
            localX = Math.max(0, Math.min(localX, size.width));
            localY = Math.max(0, Math.min(localY, size.height));
        }
        return [
            localX,
            localY
        ];
    },
    /**
     * Gets the y coordinate of the event.
     * @return {Number}
     */
    getY: function() {
        return this.getXY()[1];
    },
    /**
    * Returns true if the control, meta, shift or alt key was pressed during this event.
    * @return {Boolean}
    */
    hasModifier: function() {
        var me = this;
        return !!(me.ctrlKey || me.altKey || me.shiftKey || me.metaKey);
    },
    /**
     * Checks if the key pressed was a "navigation" key. A navigation key is defined by
     * these keys:
     *
     *  - Page Up
     *  - Page Down
     *  - End
     *  - Home
     *  - Left
     *  - Up
     *  - Right
     *  - Down
     *  - Return
     *  - Tab
     *  - Esc
     *
     * @param {Boolean} [scrollableOnly] Only check navigation keys that can cause
     * element scrolling by their default action.
     *
     * @return {Boolean} `true` if the press is a navigation keypress
     */
    isNavKeyPress: function(scrollableOnly) {
        var me = this,
            k = me.keyCode,
            isKeyPress = me.type === 'keypress';
        // See specs for description of behaviour
        // Page Up/Down, End, Home, Left, Up, Right, Down
        return ((!isKeyPress || Ext.isGecko) && k >= 33 && k <= 40) || (!scrollableOnly && (k === me.RETURN || k === me.TAB || k === me.ESC));
    },
    /**
     * Checks if the key pressed was a "special" key. A special key is defined as one of
     * these keys:
     *
     *  - Page Up
     *  - Page Down
     *  - End
     *  - Home
     *  - Left arrow
     *  - Up arrow
     *  - Right arrow
     *  - Down arrow
     *  - Return
     *  - Tab
     *  - Esc
     *  - Backspace
     *  - Delete
     *  - Shift
     *  - Ctrl
     *  - Alt
     *  - Pause
     *  - Caps Lock
     *  - Print Screen
     *  - Insert
     *
     * @return {Boolean} `true` if the key for this event is special
     */
    isSpecialKey: function() {
        var me = this,
            k = me.keyCode,
            isGecko = Ext.isGecko,
            isKeyPress = me.type === 'keypress';
        // See specs for description of behaviour
        return (isGecko && isKeyPress && me.charCode === 0) || (this.isNavKeyPress()) || (k === me.BACKSPACE) || (k === me.ENTER) || (k >= 16 && k <= 20) || (// Shift, Ctrl, Alt, Pause, Caps Lock
        (!isKeyPress || isGecko) && k >= 44 && k <= 46);
    },
    // Print Screen, Insert, Delete
    makeUnpreventable: function() {
        this.browserEvent.preventDefault = Ext.emptyFn;
    },
    /**
     * Prevents the browsers default handling of the event.
     * @chainable
     */
    preventDefault: function() {
        var me = this,
            parentEvent = me.parentEvent;
        me.defaultPrevented = true;
        // if the event was created by prototype-chaining a new object to an existing event
        // instance, we need to make sure the parent event is defaultPrevented as well.
        if (parentEvent) {
            parentEvent.defaultPrevented = true;
        }
        me.browserEvent.preventDefault();
        return me;
    },
    setCurrentTarget: function(target) {
        this.currentTarget = this.delegatedTarget = target;
    },
    /**
     * Stop the event (`{@link #preventDefault}` and `{@link #stopPropagation}`).
     * @chainable
     */
    stopEvent: function() {
        return this.preventDefault().stopPropagation();
    },
    // map of events that should fire global mousedown even if stopped
    mousedownEvents: {
        mousedown: 1,
        pointerdown: 1,
        touchstart: 1
    },
    // map of events that should fire global mouseup even if stopped
    mouseupEvents: {
        mouseup: 1,
        pointerup: 1,
        touchend: 1
    },
    /**
     * Cancels bubbling of the event.
     * @chainable
     */
    stopPropagation: function() {
        var me = this,
            browserEvent = me.browserEvent,
            parentEvent = me.parentEvent;
        // Fire the "unstoppable" global mousedown event
        // (used for menu hiding, etc)
        if (me.mousedownEvents[me.type]) {
            Ext.GlobalEvents.fireMouseDown(me);
        }
        // Fire the "unstoppable" global mouseup event
        // (used for component unpressing, etc)
        if (me.mouseupEvents[me.type]) {
            Ext.GlobalEvents.fireMouseUp(me);
        }
        // Set stopped for delegated event listeners.  Dom publisher will check this
        // property during its emulated propagation phase (see doPublish)
        me.stopped = true;
        // if the event was created by prototype-chaining a new object to an existing event
        // instance, we need to make sure the parent event is stopped.  This feature most
        // likely comes into play when dealing with event translation.  For example on touch
        // browsers addListener('mousedown') actually attaches a 'touchstart' listener behind
        // the scenes.  When the 'touchstart' event is dispatched, the event system will
        // create a "chained" copy of the event object before correcting its type back to
        // 'mousedown' and calling the handler.  When propagating the event we look at the
        // original event, not the chained one to determine if propagation should continue,
        // so the stopped property must be set on the parentEvent or stopPropagation
        // will not work.
        if (parentEvent && !me.isGesture) {
            parentEvent.stopped = true;
        }
        if (!browserEvent.stopPropagation) {
            // IE < 10 does not have stopPropagation()
            browserEvent.cancelBubble = true;
            return me;
        }
        // For non-delegated event listeners (those that are directly attached to the
        // DOM element) we need to call the browserEvent's stopPropagation() method.
        browserEvent.stopPropagation();
        return me;
    },
    /**
     * Claims this event as the currently active gesture.  Once a gesture is claimed
     * no other gestures will fire events until after the current gesture has completed.
     * For example, if `claimGesture()` is invoked on a dragstart or drag event, no
     * swipestart or swipe events will be fired until the drag gesture completes, even if
     * the gesture also meets the required duration and distance requirements to be recognized
     * as a swipe.
     *
     * If `claimGesture()` is invoked on a mouse, touch, or pointer event, it will disable
     * all gesture events until termination of the current gesture is indicated by a
     * mouseup, touchend, or pointerup event.
     *
     * @return {Ext.event.Event}
     */
    claimGesture: function() {
        var me = this,
            parentEvent = me.parentEvent;
        me.claimed = true;
        if (parentEvent && !me.isGesture) {
            parentEvent.claimGesture();
        } else {
            // Claiming a gesture should also prevent default browser actions like pan/zoom
            // if possible (only works on browsers that support touch events - browsers that
            // use pointer events must declare a CSS touch-action on elements to prevent the
            // default touch action from occurring.
            me.preventDefault();
        }
        return me;
    },
    /**
     * Returns true if the target of this event is a child of `el`. If the allowEl
     * parameter is set to false, it will return false if the target is `el`.
     * Example usage:
     * 
     *     // Handle click on any child of an element
     *     Ext.getBody().on('click', function(e){
     *         if(e.within('some-el')){
     *             alert('Clicked on a child of some-el!');
     *         }
     *     });
     * 
     *     // Handle click directly on an element, ignoring clicks on child nodes
     *     Ext.getBody().on('click', function(e,t){
     *         if((t.id == 'some-el') && !e.within(t, true)){
     *             alert('Clicked directly on some-el!');
     *         }
     *     });
     * 
     * @param {String/HTMLElement/Ext.dom.Element} el The id, DOM element or Ext.Element to check
     * @param {Boolean} [related] `true` to test if the related target is within el instead
     * of the target
     * @param {Boolean} [allowEl=true] `true` to allow the target to be considered "within" itself. 
     * `false` to only allow child elements.
     * @return {Boolean}
     */
    within: function(el, related, allowEl) {
        var t;
        if (el) {
            t = related ? this.getRelatedTarget() : this.getTarget();
        }
        if (!t || (allowEl === false && t === Ext.getDom(el))) {
            return false;
        }
        return Ext.fly(el).contains(t);
    },
    privates: {
        ieMimeType: {
            "text/plain": 'Text'
        }
    },
    deprecated: {
        '4.0': {
            methods: {
                /**
                 * @method getPageX
                 * Gets the x coordinate of the event.
                 * @return {Number}
                 * @deprecated 4.0 use {@link #getX} instead
                 * @member Ext.event.Event
                 */
                getPageX: 'getX',
                /**
                 * @method getPageY
                 * Gets the y coordinate of the event.
                 * @return {Number}
                 * @deprecated 4.0 use {@link #getY} instead
                 * @member Ext.event.Event
                 */
                getPageY: 'getY'
            }
        }
    }
}, function(Event) {
    var constants = {
            /** Key constant @type Number */
            BACKSPACE: 8,
            /** Key constant @type Number */
            TAB: 9,
            /** Key constant @type Number */
            NUM_CENTER: 12,
            /** Key constant @type Number */
            ENTER: 13,
            /** Key constant @type Number */
            RETURN: 13,
            /** Key constant @type Number */
            SHIFT: 16,
            /** Key constant @type Number */
            CTRL: 17,
            /** Key constant @type Number */
            ALT: 18,
            /** Key constant @type Number */
            PAUSE: 19,
            /** Key constant @type Number */
            CAPS_LOCK: 20,
            /** Key constant @type Number */
            ESC: 27,
            /** Key constant @type Number */
            SPACE: 32,
            /** Key constant @type Number */
            PAGE_UP: 33,
            /** Key constant @type Number */
            PAGE_DOWN: 34,
            /** Key constant @type Number */
            END: 35,
            /** Key constant @type Number */
            HOME: 36,
            /** Key constant @type Number */
            LEFT: 37,
            /** Key constant @type Number */
            UP: 38,
            /** Key constant @type Number */
            RIGHT: 39,
            /** Key constant @type Number */
            DOWN: 40,
            /** Key constant @type Number */
            PRINT_SCREEN: 44,
            /** Key constant @type Number */
            INSERT: 45,
            /** Key constant @type Number */
            DELETE: 46,
            /** Key constant @type Number */
            ZERO: 48,
            /** Key constant @type Number */
            ONE: 49,
            /** Key constant @type Number */
            TWO: 50,
            /** Key constant @type Number */
            THREE: 51,
            /** Key constant @type Number */
            FOUR: 52,
            /** Key constant @type Number */
            FIVE: 53,
            /** Key constant @type Number */
            SIX: 54,
            /** Key constant @type Number */
            SEVEN: 55,
            /** Key constant @type Number */
            EIGHT: 56,
            /** Key constant @type Number */
            NINE: 57,
            /** Key constant @type Number */
            A: 65,
            /** Key constant @type Number */
            B: 66,
            /** Key constant @type Number */
            C: 67,
            /** Key constant @type Number */
            D: 68,
            /** Key constant @type Number */
            E: 69,
            /** Key constant @type Number */
            F: 70,
            /** Key constant @type Number */
            G: 71,
            /** Key constant @type Number */
            H: 72,
            /** Key constant @type Number */
            I: 73,
            /** Key constant @type Number */
            J: 74,
            /** Key constant @type Number */
            K: 75,
            /** Key constant @type Number */
            L: 76,
            /** Key constant @type Number */
            M: 77,
            /** Key constant @type Number */
            N: 78,
            /** Key constant @type Number */
            O: 79,
            /** Key constant @type Number */
            P: 80,
            /** Key constant @type Number */
            Q: 81,
            /** Key constant @type Number */
            R: 82,
            /** Key constant @type Number */
            S: 83,
            /** Key constant @type Number */
            T: 84,
            /** Key constant @type Number */
            U: 85,
            /** Key constant @type Number */
            V: 86,
            /** Key constant @type Number */
            W: 87,
            /** Key constant @type Number */
            X: 88,
            /** Key constant @type Number */
            Y: 89,
            /** Key constant @type Number */
            Z: 90,
            /** Key constant @type Number */
            META: 91,
            // Command key on mac, left window key on Windows
            /** Key constant @type Number */
            CONTEXT_MENU: 93,
            /** Key constant @type Number */
            NUM_ZERO: 96,
            /** Key constant @type Number */
            NUM_ONE: 97,
            /** Key constant @type Number */
            NUM_TWO: 98,
            /** Key constant @type Number */
            NUM_THREE: 99,
            /** Key constant @type Number */
            NUM_FOUR: 100,
            /** Key constant @type Number */
            NUM_FIVE: 101,
            /** Key constant @type Number */
            NUM_SIX: 102,
            /** Key constant @type Number */
            NUM_SEVEN: 103,
            /** Key constant @type Number */
            NUM_EIGHT: 104,
            /** Key constant @type Number */
            NUM_NINE: 105,
            /** Key constant @type Number */
            NUM_MULTIPLY: 106,
            /** Key constant @type Number */
            NUM_PLUS: 107,
            /** Key constant @type Number */
            NUM_MINUS: 109,
            /** Key constant @type Number */
            NUM_PERIOD: 110,
            /** Key constant @type Number */
            NUM_DIVISION: 111,
            /** Key constant @type Number */
            F1: 112,
            /** Key constant @type Number */
            F2: 113,
            /** Key constant @type Number */
            F3: 114,
            /** Key constant @type Number */
            F4: 115,
            /** Key constant @type Number */
            F5: 116,
            /** Key constant @type Number */
            F6: 117,
            /** Key constant @type Number */
            F7: 118,
            /** Key constant @type Number */
            F8: 119,
            /** Key constant @type Number */
            F9: 120,
            /** Key constant @type Number */
            F10: 121,
            /** Key constant @type Number */
            F11: 122,
            /** Key constant @type Number */
            F12: 123,
            /**
             * @property {Number}
             * The mouse wheel delta scaling factor when the
             * [`deltaMode`](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode)
             * is DOM_DELTA_PIXEL
             *
             * To change this value:
             *
             *      Ext.event.Event.prototype.WHEEL_PIXEL_SIZE = 3;
             */
            WHEEL_PIXEL_SIZE: 1,
            /**
             * @property {Number}
             * The mouse wheel delta scaling factor when the
             * [`deltaMode`](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode)
             * is DOM_DELTA_LINE
             *
             * To change this value:
             *
             *      Ext.event.Event.prototype.WHEEL_LINE_SIZE = 16;
             */
            WHEEL_LINE_SIZE: 20,
            /**
             * @property {Number}
             * The mouse wheel delta scaling factor when the
             * [`deltaMode`](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode)
             * is DOM_DELTA_PAGE
             *
             * To change this value:
             *
             *      Ext.event.Event.prototype.WHEEL_PAGE_SIZE = 400;
             */
            WHEEL_PAGE_SIZE: 600
        },
        keyCodes = {},
        gestureEvents = Event.gestureEvents,
        prototype = Event.prototype,
        i, keyName, keyCode, keys;
    Ext.apply(gestureEvents, Event.mouseEvents);
    Ext.apply(gestureEvents, Event.pointerEvents);
    Ext.apply(gestureEvents, Event.touchEvents);
    Ext.apply(Event, constants);
    Ext.apply(prototype, constants);
    // ENTER and RETURN has the same keyCode, but since RETURN
    // comes later it will win over ENTER down there. However
    // ENTER is used more often and feels more natural.
    delete constants.RETURN;
    // We need this to do reverse lookup of key name by its code
    for (keyName in constants) {
        keyCode = constants[keyName];
        keyCodes[keyCode] = keyName;
    }
    Event.keyCodes = prototype.keyCodes = keyCodes;
    // Classic Event override will install this handler in IE9-
    if (!Ext.isIE9m) {
        document.addEventListener('keydown', Event.globalTabKeyDown, true);
        document.addEventListener('keyup', Event.globalTabKeyUp, true);
    }
    /**
     * @member Ext.event.Event
     * @private
     * Returns the X and Y coordinates of this event without regard to any RTL
     * direction settings.
     */
    prototype.getTrueXY = prototype.getXY;
    if (typeof KeyboardEvent !== 'undefined' && !('key' in KeyboardEvent.prototype)) {
        prototype._keys = keys = {
            3: 'Cancel',
            6: 'Help',
            8: 'Backspace',
            9: 'Tab',
            12: 'Clear',
            13: 'Enter',
            16: 'Shift',
            17: 'Control',
            18: 'Alt',
            19: 'Pause',
            20: 'CapsLock',
            27: 'Escape',
            28: 'Convert',
            29: 'NonConvert',
            30: 'Accept',
            31: 'ModeChange',
            32: ' ',
            33: 'PageUp',
            34: 'PageDown',
            35: 'End',
            36: 'Home',
            37: 'ArrowLeft',
            38: 'ArrowUp',
            39: 'ArrowRight',
            40: 'ArrowDown',
            41: 'Select',
            42: 'Print',
            43: 'Execute',
            44: 'PrintScreen',
            45: 'Insert',
            46: 'Delete',
            48: [
                '0',
                ')'
            ],
            49: [
                '1',
                '!'
            ],
            50: [
                '2',
                '@'
            ],
            51: [
                '3',
                '#'
            ],
            52: [
                '4',
                '$'
            ],
            53: [
                '5',
                '%'
            ],
            54: [
                '6',
                '^'
            ],
            55: [
                '7',
                '&'
            ],
            56: [
                '8',
                '*'
            ],
            57: [
                '9',
                '('
            ],
            91: 'OS',
            93: 'ContextMenu',
            144: 'NumLock',
            145: 'ScrollLock',
            181: 'VolumeMute',
            182: 'VolumeDown',
            183: 'VolumeUp',
            186: [
                ';',
                ':'
            ],
            187: [
                '=',
                '+'
            ],
            188: [
                ',',
                '<'
            ],
            189: [
                '-',
                '_'
            ],
            190: [
                '.',
                '>'
            ],
            191: [
                '/',
                '?'
            ],
            192: [
                '`',
                '~'
            ],
            219: [
                '[',
                '{'
            ],
            220: [
                '\\',
                '|'
            ],
            221: [
                ']',
                '}'
            ],
            222: [
                "'",
                '"'
            ],
            224: 'Meta',
            225: 'AltGraph',
            246: 'Attn',
            247: 'CrSel',
            248: 'ExSel',
            249: 'EraseEof',
            250: 'Play',
            251: 'ZoomOut'
        };
        for (i = 1; i < 25; ++i) {
            keys[i + 111] = 'F' + i;
        }
        // F1 - F24
        for (i = 0; i < 26; ++i) {
            // a-z, A-Z
            keys[i] = [
                String.fromCharCode(i + 97),
                String.fromCharCode(i + 65)
            ];
        }
        prototype.key = function() {
            var k = keys[this.browserEvent.which || this.keyCode];
            if (k && typeof k !== 'string') {
                k = k[+this.shiftKey];
            }
            return k;
        };
    }
});

/**
 * @class Ext.event.Event
 */
Ext.define('Ext.overrides.event.Event', {
    override: 'Ext.event.Event',
    /**
     * @method injectEvent
     * @member Ext.event.Event
     * Injects a DOM event using the data in this object and (optionally) a new target.
     * This is a low-level technique and not likely to be used by application code. The
     * currently supported event types are:
     * <p><b>HTMLEvents</b></p>
     * <ul>
     * <li>load</li>
     * <li>unload</li>
     * <li>select</li>
     * <li>change</li>
     * <li>submit</li>
     * <li>reset</li>
     * <li>resize</li>
     * <li>scroll</li>
     * </ul>
     * <p><b>MouseEvents</b></p>
     * <ul>
     * <li>click</li>
     * <li>dblclick</li>
     * <li>mousedown</li>
     * <li>mouseup</li>
     * <li>mouseover</li>
     * <li>mousemove</li>
     * <li>mouseout</li>
     * </ul>
     * <p><b>UIEvents</b></p>
     * <ul>
     * <li>focusin</li>
     * <li>focusout</li>
     * <li>activate</li>
     * <li>focus</li>
     * <li>blur</li>
     * </ul>
     * @param {Ext.Element/HTMLElement} target (optional) If specified, the target for the event.
     * This is likely to be used when relaying a DOM event. If not specified, {@link #getTarget}
     * is used to determine the target.
     */
    injectEvent: (function() {
        var API,
            dispatchers = {},
            // keyed by event type (e.g., 'mousedown')
            crazyIEButtons;
        // Good reference: http://developer.yahoo.com/yui/docs/UserAction.js.html
        // IE9 has createEvent, but this code causes major problems with htmleditor (it
        // blocks all mouse events and maybe more). TODO
        if (!Ext.isIE9m && document.createEvent) {
            // if (DOM compliant)
            API = {
                createHtmlEvent: function(doc, type, bubbles, cancelable) {
                    var event = doc.createEvent('HTMLEvents');
                    event.initEvent(type, bubbles, cancelable);
                    return event;
                },
                createMouseEvent: function(doc, type, bubbles, cancelable, detail, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget) {
                    var event = doc.createEvent('MouseEvents'),
                        view = doc.defaultView || window;
                    if (event.initMouseEvent) {
                        event.initMouseEvent(type, bubbles, cancelable, view, detail, clientX, clientY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);
                    } else {
                        // old Safari
                        event = doc.createEvent('UIEvents');
                        event.initEvent(type, bubbles, cancelable);
                        event.view = view;
                        event.detail = detail;
                        event.screenX = clientX;
                        event.screenY = clientY;
                        event.clientX = clientX;
                        event.clientY = clientY;
                        event.ctrlKey = ctrlKey;
                        event.altKey = altKey;
                        event.metaKey = metaKey;
                        event.shiftKey = shiftKey;
                        event.button = button;
                        event.relatedTarget = relatedTarget;
                    }
                    return event;
                },
                createUIEvent: function(doc, type, bubbles, cancelable, detail) {
                    var event = doc.createEvent('UIEvents'),
                        view = doc.defaultView || window;
                    event.initUIEvent(type, bubbles, cancelable, view, detail);
                    return event;
                },
                fireEvent: function(target, type, event) {
                    target.dispatchEvent(event);
                }
            };
        } else if (document.createEventObject) {
            // else if (IE)
            crazyIEButtons = {
                0: 1,
                1: 4,
                2: 2
            };
            API = {
                createHtmlEvent: function(doc, type, bubbles, cancelable) {
                    var event = doc.createEventObject();
                    event.bubbles = bubbles;
                    event.cancelable = cancelable;
                    return event;
                },
                createMouseEvent: function(doc, type, bubbles, cancelable, detail, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget) {
                    var event = doc.createEventObject();
                    event.bubbles = bubbles;
                    event.cancelable = cancelable;
                    event.detail = detail;
                    event.screenX = clientX;
                    event.screenY = clientY;
                    event.clientX = clientX;
                    event.clientY = clientY;
                    event.ctrlKey = ctrlKey;
                    event.altKey = altKey;
                    event.shiftKey = shiftKey;
                    event.metaKey = metaKey;
                    event.button = crazyIEButtons[button] || button;
                    event.relatedTarget = relatedTarget;
                    // cannot assign to/fromElement
                    return event;
                },
                createUIEvent: function(doc, type, bubbles, cancelable, detail) {
                    var event = doc.createEventObject();
                    event.bubbles = bubbles;
                    event.cancelable = cancelable;
                    return event;
                },
                fireEvent: function(target, type, event) {
                    target.fireEvent('on' + type, event);
                }
            };
        }
        //----------------
        // HTMLEvents
        Ext.Object.each({
            load: [
                false,
                false
            ],
            unload: [
                false,
                false
            ],
            select: [
                true,
                false
            ],
            change: [
                true,
                false
            ],
            submit: [
                true,
                true
            ],
            reset: [
                true,
                false
            ],
            resize: [
                true,
                false
            ],
            scroll: [
                true,
                false
            ]
        }, function(name, value) {
            var bubbles = value[0],
                cancelable = value[1];
            dispatchers[name] = function(targetEl, srcEvent) {
                var e = API.createHtmlEvent(name, bubbles, cancelable);
                API.fireEvent(targetEl, name, e);
            };
        });
        //----------------
        // MouseEvents
        function createMouseEventDispatcher(type, detail) {
            var cancelable = (type !== 'mousemove');
            return function(targetEl, srcEvent) {
                var xy = srcEvent.getXY(),
                    e;
                e = API.createMouseEvent(targetEl.ownerDocument, type, true, cancelable, detail, xy[0], xy[1], srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey, srcEvent.button, srcEvent.relatedTarget);
                API.fireEvent(targetEl, type, e);
            };
        }
        Ext.each([
            'click',
            'dblclick',
            'mousedown',
            'mouseup',
            'mouseover',
            'mousemove',
            'mouseout'
        ], function(eventName) {
            dispatchers[eventName] = createMouseEventDispatcher(eventName, 1);
        });
        //----------------
        // UIEvents
        Ext.Object.each({
            focusin: [
                true,
                false
            ],
            focusout: [
                true,
                false
            ],
            activate: [
                true,
                true
            ],
            focus: [
                false,
                false
            ],
            blur: [
                false,
                false
            ]
        }, function(name, value) {
            var bubbles = value[0],
                cancelable = value[1];
            dispatchers[name] = function(targetEl, srcEvent) {
                var e = API.createUIEvent(targetEl.ownerDocument, name, bubbles, cancelable, 1);
                API.fireEvent(targetEl, name, e);
            };
        });
        //---------
        if (!API) {
            // not even sure what ancient browsers fall into this category...
            dispatchers = {};
            // never mind all those we just built :P
            API = {};
        }
        function cannotInject(target, srcEvent) {}
        // TODO log something
        return function(target) {
            var me = this,
                dispatcher = dispatchers[me.type] || cannotInject,
                t = target ? (target.dom || target) : me.getTarget();
            dispatcher(t, me);
        };
    }()),
    // call to produce method
    preventDefault: function(browserOnly) {
        var me = this,
            event = me.browserEvent,
            parentEvent = me.parentEvent,
            unselectable, target, fn;
        // This check is for IE8/9. The event object may have been
        // invalidated, so we can't delve into the details of it. If so,
        // just fall out gracefully and don't attempt to do anything.
        // eslint-disable-next-line valid-typeof
        if (typeof event.type !== 'unknown') {
            // In some cases we want to prevent default on the browser event
            // but keep propagating it through our event system. For example,
            // in Checkbox selection where the cells with checkboxes should
            // prevent focusing on mousedown but still fire the click event.
            if (!browserOnly) {
                me.defaultPrevented = true;
            }
            // if the event was created by prototype-chaining a new object to an existing event
            // instance, we need to make sure the parent event is defaultPrevented as well.
            if (parentEvent) {
                parentEvent.defaultPrevented = true;
            }
            if (event.preventDefault) {
                event.preventDefault();
            } else {
                // The purpose of the code below is for preventDefault to stop focus from
                // occurring like it does in other modern browsers. This only happens in
                // IE8/9 when using attachEvent. The use of unselectable seems the most reliable
                // way to prevent this from happening. We need to use a timeout to restore the
                // unselectable state because if we don't setting it has no effect. It's important
                // to set the atrribute to 'on' as opposed to just setting the property on the
                // DOM element. See the link below for a discussion on the issue:
                // http://bugs.jquery.com/ticket/10345
                if (event.type === 'mousedown') {
                    target = event.target;
                    unselectable = target.getAttribute('unselectable');
                    if (unselectable !== 'on') {
                        target.setAttribute('unselectable', 'on');
                        fn = function() {
                            target.setAttribute('unselectable', unselectable);
                        };
                        // This function is hard to track, with a potential to be called
                        // for any HtmlElement in the document. It may be a Fly, it may
                        // not belong to any Component, and it may even be created by
                        // 3rd party code that we have no control over and cannot intercept
                        // the element being destroyed.
                        // On the other hand, the function is pretty simple, cannot lead
                        // to memory leaks and is only fired once. So, no harm no foul.
                        fn.$skipTimerCheck = true;
                        Ext.defer(fn, 1);
                    }
                }
                // IE9 and earlier do not support preventDefault
                event.returnValue = false;
                // Some keys events require setting the keyCode to -1 to be prevented
                // all ctrl + X and F1 -> F12
                if (event.ctrlKey || event.keyCode > 111 && event.keyCode < 124) {
                    event.keyCode = -1;
                }
            }
        }
        return me;
    },
    deprecated: {
        '5.0': {
            methods: {
                /**
                 * @method clone
                 * @member Ext.event.Event
                 * Clones this event.
                 * @return {Ext.event.Event} The cloned copy
                 * @deprecated 5.0.0 This method is deprecated.
                 */
                clone: function() {
                    return new this.self(this.browserEvent, this);
                }
            }
        }
    }
}, function() {
    var Event = this,
        btnMap;
    if (Ext.isIE9m) {
        btnMap = {
            0: 0,
            1: 0,
            4: 1,
            2: 2
        };
        Event.override({
            statics: {
                /**
                 * @member Ext.event.Event
                 * When events are attached using IE's attachEvent API instead of
                 * addEventListener accessing any members of an event object asynchronously
                 * results in "Member not found" error.  To work around this we fabricate
                 * our own event object by copying all of its members to a new object.
                 * @param {Event} browserEvent The native browser event object
                 * @private
                 * @static
                 */
                enableIEAsync: function(browserEvent) {
                    var name,
                        fakeEvent = {};
                    for (name in browserEvent) {
                        fakeEvent[name] = browserEvent[name];
                    }
                    return fakeEvent;
                }
            },
            constructor: function(event, info, touchesMap, identifiers) {
                var me = this;
                me.callParent([
                    event,
                    info,
                    touchesMap,
                    identifiers
                ]);
                me.button = btnMap[event.button];
                if (event.type === 'contextmenu') {
                    // IE8/9 reports click as 0, so we can at least attempt to infer here
                    me.button = 2;
                }
                // IE8 can throw an error when trying to access properties on a browserEvent
                // object when the event has been buffered or delayed.  Cache them here
                // so we can access them later.
                me.toElement = event.toElement;
                me.fromElement = event.fromElement;
            },
            mouseLeaveRe: /(mouseout|mouseleave)/,
            mouseEnterRe: /(mouseover|mouseenter)/,
            /**
             * @method enableIEAsync
             * @member Ext.event.Event
             * @inheritdoc Ext.event.Event#static-method-enableIEAsync
             * @private
             */
            enableIEAsync: function(browserEvent) {
                this.browserEvent = this.self.enableIEAsync(browserEvent);
            },
            getRelatedTarget: function(selector, maxDepth, returnEl) {
                var me = this,
                    type, target;
                if (!me.relatedTarget) {
                    type = me.type;
                    if (me.mouseLeaveRe.test(type)) {
                        target = me.toElement;
                    } else if (me.mouseEnterRe.test(type)) {
                        target = me.fromElement;
                    }
                    if (target) {
                        me.relatedTarget = me.self.resolveTextNode(target);
                    }
                }
                return me.callParent([
                    selector,
                    maxDepth,
                    returnEl
                ]);
            }
        });
        // We place these listeners to capture Tab and Shift-Tab key strokes
        // and pass this information in the focus/blur event if it happens
        // between keydown/keyup pair.
        document.attachEvent('onkeydown', Ext.event.Event.globalTabKeyDown);
        document.attachEvent('onkeyup', Ext.event.Event.globalTabKeyUp);
        window.attachEvent('onunload', function() {
            document.detachEvent('onkeydown', Ext.event.Event.globalTabKeyDown);
            document.detachEvent('onkeyup', Ext.event.Event.globalTabKeyUp);
        });
    }
});

/**
 * @private
 */
Ext.define('Ext.event.publisher.Dom', {
    extend: 'Ext.event.publisher.Publisher',
    requires: [
        'Ext.event.Event'
    ],
    type: 'dom',
    /**
     * @property {Array} handledDomEvents
     * An array of DOM events that this publisher handles.  Events specified in this array
     * will be added as global listeners on the {@link #target}
     */
    handledDomEvents: [],
    reEnterCount: 0,
    // The following events do not bubble, but can still be "captured" at the top of
    // the DOM,  For these events, when the delegated event model is used, we attach a
    // single listener on the window object using the "useCapture" option.
    captureEvents: {
        animationstart: 1,
        animationend: 1,
        resize: 1,
        focus: 1,
        blur: 1
    },
    // The following events do not bubble, and cannot be "captured".  The only way to
    // listen for these events is via a listener attached directly to the target element
    directEvents: {
        mouseenter: 1,
        mouseleave: 1,
        pointerenter: 1,
        pointerleave: 1,
        MSPointerEnter: 1,
        MSPointerLeave: 1,
        load: 1,
        unload: 1,
        beforeunload: 1,
        error: 1,
        DOMContentLoaded: 1,
        DOMFrameContentLoaded: 1,
        hashchange: 1,
        // Scroll can be captured, but it is listed here as one of directEvents instead of
        // captureEvents because in some browsers capturing the scroll event does not work
        // if the window object itself fired the scroll event.
        scroll: 1,
        online: 1,
        offline: 1
    },
    /**
     * In browsers that implement pointerevents when a pointerdown is triggered by touching
     * the screen, pointerover and pointerenter events will be fired immmediately before
     * the pointerdown. Also pointerout and pointerleave will be fired immediately after
     * pointerup when triggered using touch input.  For a consistent cross-browser
     * experience on touch-screens we block pointerover, pointerout, pointerenter, and
     * pointerleave when triggered by touch input, since in most cases pointerover/pointerenter
     * behavior is not desired when touching the screen.  Note: this should only affect
     * events with pointerType === 'touch' or pointerType === 'pen', we do NOT want to
     * block these events when triggered using a mouse.
     * See also:
     *     http://www.w3.org/TR/pointerevents/#the-pointerdown-event
     *     http://www.w3.org/TR/pointerevents/#the-pointerenter-event
     * @private
     */
    blockedPointerEvents: {
        pointerover: 1,
        pointerout: 1,
        pointerenter: 1,
        pointerleave: 1,
        MSPointerOver: 1,
        MSPointerOut: 1,
        MSPointerEnter: 1,
        MSPointerLeave: 1
    },
    /**
     * Browsers with pointer events may implement "compatibility" mouse events:
     * http://www.w3.org/TR/pointerevents/#compatibility-mapping-with-mouse-events
     * The behavior implemented in handlers for mouse over/out/enter/leave is not typically
     * desired when touching the screen, so we map all of these events to their pointer
     * counterparts in Ext.Element event translation code, so that they can be blocked
     * via "blockedPointerEvents".  The only scenario where this breaks down is in IE10
     * with mouseenter/mouseleave, since MSPointerEnter/MSPointerLeave were not implemented
     * in IE10.  For these 2 events we have to resort to a different method - capturing
     * the timestamp of the last pointer event that has pointerType == 'touch', and if the
     * mouse event occurred within a certain threshold we can reasonably assume it occurred
     * because of a touch on the screen (see isEventBlocked)
     * @private
     */
    blockedCompatibilityMouseEvents: {
        mouseenter: 1,
        mouseleave: 1
    },
    constructor: function() {
        var me = this,
            supportsPassive = Ext.supports.PassiveEventListener;
        me.listenerOptions = supportsPassive ? {
            passive: false
        } : false;
        me.captureOptions = supportsPassive ? {
            passive: false,
            capture: true
        } : true;
        me.bubbleSubscribers = {};
        me.captureSubscribers = {};
        me.directSubscribers = {};
        me.directCaptureSubscribers = {};
        // this map tracks all the names of the events that currently have a delegated
        // event listener attached so that they can be removed from the dom when the
        // publisher is destroyed
        me.delegatedListeners = {};
        me.initHandlers();
        Ext.onInternalReady(me.onReady, me);
        me.callParent();
        me.registerDomEvents();
    },
    registerDomEvents: function() {
        var me = this,
            publishersByEvent = Ext.event.publisher.Publisher.publishersByEvent,
            domEvents = me.handledDomEvents,
            ln = domEvents.length,
            i, eventName;
        for (i = 0; i < ln; i++) {
            eventName = domEvents[i];
            me.handles[eventName] = 1;
            publishersByEvent[eventName] = me;
        }
    },
    onReady: function() {
        var me = this,
            domEvents = me.handledDomEvents,
            ln, i;
        if (domEvents) {
            // If the publisher has handledDomEvents we attach delegated listeners up front
            // for those events. Dom publisher does not have a list of event names, but
            // attaches listeners dynamically as subscribers are subscribed.  This allows it
            // to handle all DOM events that are not explicitly handled by another publisher.
            // Subclasses such as Gesture must explicitly list their handledDomEvents.
            for (i = 0 , ln = domEvents.length; i < ln; i++) {
                me.addDelegatedListener(domEvents[i]);
            }
        }
        // DOM publishers should be the last thing to go since they are used
        // to remove any element listeners which is typically part
        // of the unload destroy process.
        Ext.getWin().on('unload', me.destroy, me, {
            priority: -10000
        });
    },
    initHandlers: function() {
        var me = this;
        me.onDelegatedEvent = Ext.bind(me.onDelegatedEvent, me);
        me.onDirectEvent = Ext.bind(me.onDirectEvent, me);
        me.onDirectCaptureEvent = Ext.bind(me.onDirectCaptureEvent, me);
    },
    addDelegatedListener: function(eventName) {
        var me = this;
        me.delegatedListeners[eventName] = 1;
        me.target.addEventListener(eventName, me.onDelegatedEvent, me.captureEvents[eventName] ? me.captureOptions : me.listenerOptions);
    },
    removeDelegatedListener: function(eventName) {
        var me = this;
        delete me.delegatedListeners[eventName];
        me.target.removeEventListener(eventName, me.onDelegatedEvent, me.captureEvents[eventName] ? me.captureOptions : me.listenerOptions);
    },
    addDirectListener: function(eventName, element, capture) {
        var me = this;
        element.dom.addEventListener(eventName, capture ? me.onDirectCaptureEvent : me.onDirectEvent, capture ? me.captureOptions : me.listenerOptions);
    },
    removeDirectListener: function(eventName, element, capture) {
        var me = this;
        element.dom.removeEventListener(eventName, capture ? me.onDirectCaptureEvent : me.onDirectEvent, capture ? me.captureOptions : me.listenerOptions);
    },
    subscribe: function(element, eventName, delegated, capture) {
        var me = this,
            subscribers, id;
        if (delegated && !me.directEvents[eventName]) {
            // delegated listeners
            subscribers = capture ? me.captureSubscribers : me.bubbleSubscribers;
            if (!me.handles[eventName] && !me.delegatedListeners[eventName]) {
                // First time we've attached a listener for this eventName - need to begin
                // listening at the dom level
                me.addDelegatedListener(eventName);
            }
            if (subscribers[eventName]) {
                ++subscribers[eventName];
            } else {
                subscribers[eventName] = 1;
            }
        } else {
            subscribers = capture ? me.directCaptureSubscribers : me.directSubscribers;
            id = element.id;
            // Direct subscribers are tracked by eventName first and by element id second.
            // This allows the element id key to be deleted when there are no more subscribers
            // so that this map does not grow indefinitely (it can only grow to a finite
            // set of event names) - see unsubscribe
            subscribers = subscribers[eventName] || (subscribers[eventName] = {});
            if (subscribers[id]) {
                ++subscribers[id];
            } else {
                subscribers[id] = 1;
                me.addDirectListener(eventName, element, capture);
            }
        }
    },
    unsubscribe: function(element, eventName, delegated, capture) {
        var me = this,
            captureSubscribers, bubbleSubscribers, subscribers, id;
        if (delegated && !me.directEvents[eventName]) {
            captureSubscribers = me.captureSubscribers;
            bubbleSubscribers = me.bubbleSubscribers;
            subscribers = capture ? captureSubscribers : bubbleSubscribers;
            if (subscribers[eventName]) {
                --subscribers[eventName];
            }
            if (!me.handles[eventName] && !bubbleSubscribers[eventName] && !captureSubscribers[eventName]) {
                // decremented subscribers back to 0 - and the event is not in "handledEvents"
                // no longer need to listen at the dom level
                this.removeDelegatedListener(eventName);
            }
        } else {
            subscribers = capture ? me.directCaptureSubscribers : me.directSubscribers;
            id = element.id;
            subscribers = subscribers[eventName];
            if (subscribers[id]) {
                --subscribers[id];
            }
            if (!subscribers[id]) {
                // no more direct subscribers for this element/id/capture, so we can safely
                // remove the dom listener
                delete subscribers[id];
                me.removeDirectListener(eventName, element, capture);
            }
        }
    },
    getPropagatingTargets: function(target) {
        var currentNode = target,
            targets = [],
            parentNode;
        while (currentNode) {
            targets.push(currentNode);
            parentNode = currentNode.parentNode;
            if (!parentNode) {
                // If the node has no parentNode it means one of two things - either it is
                // not in the dom, or we have looped all the way up to the document object.
                // If the latter is the case we need to add the window object to the targets
                // to ensure that our propagation mimics browser propagation where events
                // can bubble from the document to the window.
                parentNode = currentNode.defaultView;
            }
            currentNode = parentNode;
        }
        return targets;
    },
    /**
     *
     * @param e {Ext.event.Event/Ext.event.Event[]} An event to publish. Can also be an
     * array of events.  Gesture publisher passes an array so that gesture events and
     * the dom events from which they were synthesized can propagate together.
     * @param [targets] {HTMLElement[]} propagation targets.  Required if `e` is an array.
     * @param {Boolean} [claimed=false] pass true if we are re-entering publish() to
     * publish gesture cancellation events that are being fired as a result of something
     * being claimed.  This ensures that cancellation events cannot be claimed.
     * @protected
     */
    publish: function(e, targets, claimed) {
        var me = this,
            hasCaptureSubscribers = false,
            hasBubbleSubscribers = false,
            events, type, target, el, i, ln, j, eLn;
        claimed = claimed || false;
        // Gesture publisher passes an already created array of propagating targets.
        // For all other events we need to compute the targets for propagation now.
        if (!targets) {
            if (e instanceof Array) {
                Ext.raise("Propagation targets must be supplied when publishing " + "an array of events.");
            }
            // No targets passed, assume that e is not an array.
            target = e.target;
            if (me.captureEvents[e.type]) {
                el = Ext.cache[target.id];
                targets = el ? [
                    el
                ] : [];
            } else {
                targets = me.getPropagatingTargets(target);
            }
        }
        // "e" may be either a single event (as is the case for events fired by dom publisher)
        // or it could be an array of events containing a dom event and its recognized
        // gesture events.
        events = Ext.Array.from(e);
        ln = targets.length;
        eLn = events.length;
        for (i = 0; i < eLn; i++) {
            type = events[i].type;
            if (!hasCaptureSubscribers && me.captureSubscribers[type]) {
                hasCaptureSubscribers = true;
            }
            if (!hasBubbleSubscribers && me.bubbleSubscribers[type]) {
                hasBubbleSubscribers = true;
            }
        }
        // We will now proceed to fire events in both capture and bubble phases.  You
        // may notice that we are looping all potential targets both times, and only
        // firing on the target if there is an Ext.Element wrapper in the cache.  This is
        // done (vs. eliminating non-cached targets from the array up front) because
        // event handlers can add listeners to other elements during propagation.  Looping
        // all the potential targets ensures that these dynamically added listeners
        // are fired.  See https://sencha.jira.com/browse/EXTJS-15953
        // capture phase (top-down event propagation).
        if (hasCaptureSubscribers) {
            for (i = ln; i--; ) {
                el = Ext.cache[targets[i].id];
                if (el) {
                    for (j = 0; j < eLn; j++) {
                        e = events[j];
                        me.fire(el, e.type, e, false, true);
                        if (!claimed && e.claimed) {
                            claimed = true;
                            j = me.filterClaimed(events, e);
                            eLn = events.length;
                        }
                        // filterClaimed may remove items
                        if (e.stopped) {
                            events.splice(j, 1);
                            j--;
                            eLn--;
                        }
                    }
                }
            }
        }
        // bubble phase (bottom-up event propagation).
        // stopPropagation during capture phase cancels entire bubble phase
        if (hasBubbleSubscribers && !e.stopped) {
            for (i = 0; i < ln; i++) {
                el = Ext.cache[targets[i].id];
                if (el) {
                    for (j = 0; j < eLn; j++) {
                        e = events[j];
                        me.fire(el, e.type, e, false, false);
                        if (!claimed && e.claimed && me.filterClaimed) {
                            claimed = true;
                            j = me.filterClaimed(events, e);
                            eLn = events.length;
                        }
                        // filterClaimed may remove items
                        if (e.stopped) {
                            events.splice(j, 1);
                            j--;
                            eLn--;
                        }
                    }
                }
            }
        }
    },
    /**
     * Hook for gesture publisher to override and perform gesture recognition
     * @param {Ext.event.Event} e
     */
    publishDelegatedDomEvent: function(e) {
        this.publish(e);
    },
    fire: function(element, eventName, e, direct, capture) {
        var event;
        if (element.hasListeners[eventName]) {
            event = element.events[eventName];
            if (event) {
                if (capture && direct) {
                    event = event.directCaptures;
                } else if (capture) {
                    event = event.captures;
                } else if (direct) {
                    event = event.directs;
                }
                // yes, this second null check for event is necessary - one of the
                // above assignments might have resulted in undefined
                if (event) {
                    e.setCurrentTarget(element.dom);
                    event.fire(e, e.target);
                }
            }
        }
    },
    onDelegatedEvent: function(e) {
        if (Ext.elevateFunction) {
            // using [e] is faster than using arguments in most browsers
            // http://jsperf.com/passing-arguments
            Ext.elevateFunction(this.doDelegatedEvent, this, [
                e
            ]);
        } else {
            this.doDelegatedEvent(e);
        }
    },
    doDelegatedEvent: function(e) {
        var me = this,
            timeStamp;
        e = new Ext.event.Event(e);
        timeStamp = e.time;
        if (!me.isEventBlocked(e)) {
            me.beforeEvent(e);
            Ext.frameStartTime = timeStamp;
            me.reEnterCountAdjusted = false;
            me.reEnterCount++;
            me.publishDelegatedDomEvent(e);
            // Gesture publisher deals with exceptions in recognizers
            if (!me.reEnterCountAdjusted) {
                me.reEnterCount--;
            }
            me.afterEvent(e);
        }
    },
    /**
     * Handler for directly-attached (non-delegated) dom events
     * @param {Event} e
     * @private
     */
    onDirectEvent: function(e) {
        if (Ext.elevateFunction) {
            // using [e] is faster than using arguments in most browsers
            // http://jsperf.com/passing-arguments
            Ext.elevateFunction(this.doDirectEvent, this, [
                e,
                false
            ]);
        } else {
            this.doDirectEvent(e, false);
        }
    },
    // When eventPhase is AT_TARGET there's no way to know if we are handling a capture
    // or bubble listener, hence the need for this separate handler fn
    onDirectCaptureEvent: function(e) {
        if (Ext.elevateFunction) {
            // using [e] is faster than using arguments in most browsers
            // http://jsperf.com/passing-arguments
            Ext.elevateFunction(this.doDirectEvent, this, [
                e,
                true
            ]);
        } else {
            this.doDirectEvent(e, true);
        }
    },
    doDirectEvent: function(e, capture) {
        var me = this,
            currentTarget = e.currentTarget,
            timeStamp, el;
        e = new Ext.event.Event(e);
        timeStamp = e.time;
        if (me.isEventBlocked(e)) {
            return;
        }
        me.beforeEvent(e);
        Ext.frameStartTime = timeStamp;
        el = Ext.cache[currentTarget.id];
        // Element can be removed from the cache by this time, with the node
        // still lingering for some reason. This can happen for example when
        // load event is fired on an iframe that we constructed when submitting
        // a form for file uploads.
        if (el) {
            // Since natural DOM propagation has occurred, no emulated propagation is needed.
            // Simply dispatch the event on the currentTarget element
            me.reEnterCountAdjusted = false;
            me.reEnterCount++;
            me.fire(el, e.type, e, true, capture);
            // Gesture publisher deals with exceptions in recognizers
            if (!me.reEnterCountAdjusted) {
                me.reEnterCount--;
            }
        }
        me.afterEvent(e);
    },
    beforeEvent: function(e) {
        var browserEvent = e.browserEvent,
            // use full class name, not me.self, so that Dom and Gesture publishers will
            // both place flags on the same object.
            self = Ext.event.publisher.Dom,
            touches, touch;
        if (browserEvent.type === 'touchstart') {
            touches = browserEvent.touches;
            if (touches.length === 1) {
                // capture the coordinates of the first touchstart event so we can use
                // them to eliminate duplicate mouse events if needed, (see isEventBlocked).
                touch = touches[0];
                self.lastTouchStartX = touch.pageX;
                self.lastTouchStartY = touch.pageY;
            }
        }
    },
    afterEvent: function(e) {
        var browserEvent = e.browserEvent,
            type = browserEvent.type,
            // use full class name, not me.self, so that Dom and Gesture publishers will
            // both place flags on the same object.
            self = Ext.event.publisher.Dom,
            GlobalEvents = Ext.GlobalEvents;
        // It is important that the following time stamps are captured after the handlers
        // have been invoked because they need to represent the "exit" time, so that they
        // can be compared against the next "entry" time into onDelegatedEvent or
        // onDirectEvent to detect the time lapse in between the firing of the 2 events.
        // We set these flags on "this.self" so that they can be shared between Dom
        // publisher and subclasses
        if (e.self.pointerEvents[type] && e.pointerType !== 'mouse') {
            // track the last time a pointer event was fired as a result of interaction
            // with the screen, pointerType === 'touch' most likely but could also be
            // pointerType === 'pen' hence the reason we use !== 'mouse', This is used
            // to eliminate potential duplicate "compatibility" mouse events
            // (see isEventBlocked)
            self.lastScreenPointerEventTime = Ext.now();
        }
        if (type === 'touchend') {
            // Capture a time stamp so we can use it to eliminate potential duplicate
            // emulated mouse events on multi-input devices that have touch events,
            // e.g. Chrome on Window8 with touch-screen (see isEventBlocked).
            self.lastTouchEndTime = Ext.now();
        }
        if (!this.reEnterCount && !GlobalEvents.idleEventMask[type]) {
            Ext.fireIdle();
        }
    },
    /**
     * Detects if the given event should be blocked from firing because it is an emulated
     * "compatibility" mouse event triggered by a touch on the screen.
     * @param {Ext.event.Event} e
     * @return {Boolean}
     * @private
     */
    isEventBlocked: function(e) {
        var me = this,
            type = e.type,
            // use full class name, not me.self, so that Dom and Gesture publishers will
            // both look for flags on the same object.
            self = Ext.event.publisher.Dom,
            now = Ext.now();
        // Gecko has a bug where right clicking will trigger both a contextmenu
        // and click event. This only occurs when delegating the event onto the window
        // object like we do by default for delegated events.
        // This is not possible to feature detect using synthetic events.
        // Ticket logged: https://bugzilla.mozilla.org/show_bug.cgi?id=1156023
        if (Ext.isGecko && e.type === 'click' && e.button === 2) {
            return true;
        }
        // prevent emulated pointerover, pointerout, pointerenter, and pointerleave
        // events from firing when triggered by touching the screen.
        return (me.blockedPointerEvents[type] && e.pointerType !== 'mouse') || // prevent compatibility mouse events from firing on devices with pointer
        // events - see comment on blockedCompatibilityMouseEvents for more details
        // The time from when the last pointer event fired until when compatibility
        // events are received varies depending on the browser, device, and application
        // so we use 1 second to be safe
        (me.blockedCompatibilityMouseEvents[type] && (now - self.lastScreenPointerEventTime < 1000)) || (Ext.supports.TouchEvents && e.self.mouseEvents[e.type] && // some browsers (e.g. webkit on Windows 8 with touch screen) emulate mouse
        // events after touch events have fired.  This only seems to happen when there
        // is no movement present, so, for example, a touchstart followed immediately
        // by a touchend would result in the following sequence of events:
        // "touchstart, touchend, mousemove, mousedown, mouseup"
        // yes, you read that right, the emulated mousemove fires before mousedown.
        // However, touch events with movement (touchstart, touchmove, then touchend)
        // do not trigger the emulated mouse events.
        // The side effect of this behavior is that single-touch gestures that expect
        // no movement (e.g. tap) can double-fire - once when the touchstart/touchend
        // occurs, and then again when the emulated mousedown/up occurs.
        // We cannot solve the problem by only listening for touch events and ignoring
        // mouse events, since we may be on a multi-input device that supports both
        // touch and mouse events and we want gestures to respond to both kinds of
        // events.  Instead we have to detect if the mouse event is a "dupe" by
        // checking if its coordinates are near the last touchstart's coordinates,
        // and if it's timestamp is within a certain threshold of the last touchend
        // event's timestamp.  This is because when dealing with multi-touch events,
        // the emulated mousedown event (when it does fire) will fire with approximately
        // the same coordinates as the first touchstart, but within a short time after
        // the last touchend.  We use 15px as the distance threshold, to be on the safe
        // side because the difference in coordinates can sometimes be up to 6px.
        Math.abs(e.pageX - self.lastTouchStartX) < 15 && Math.abs(e.pageY - self.lastTouchStartY) < 15 && // in the majority of cases, the emulated mousedown is observed within
        // 5ms of touchend, however, to be certain we avoid a situation where a
        // gesture handler gets executed twice we use a threshold of 1000ms.  The
        // side effect of this is that if a user touches the screen and then quickly
        // clicks screen in the same spot, the mousedown/mouseup sequence that
        // ensues will not trigger any gesture recognizers.
        (Ext.now() - self.lastTouchEndTime) < 1000);
    },
    destroy: function() {
        var GC = Ext.dom['GarbageCollector'],
            // eslint-disable-line dot-notation
            eventName;
        for (eventName in this.delegatedListeners) {
            this.removeDelegatedListener(eventName);
        }
        // We are wired to the unload event, so we ensure cleanup of low-level stuff
        // like the Reaper and the GarbageCollector.
        Ext.Reaper.flush();
        if (GC) {
            GC.collect();
        }
        this.callParent();
    },
    /**
     * Resets the internal state of the Dom publisher.  Internally the Dom publisher
     * keeps track of timing and coordinates of events for eliminating browser duplicates
     * (e.g. emulated mousedown after pointerdown etc.).  This method resets all this
     * cached data to a state similar to when the publisher was first instantiated.
     *
     * Applications will not typically need to use this method, but it is useful for
     * Unit-testing situations where a clean slate is required for each test.
     */
    reset: function() {
        // use full class name, not me.self, so that Dom and Gesture publishers will
        // both reset flags on the same object.
        var self = Ext.event.publisher.Dom;
        this.reEnterCount = 0;
        // set to undefined, not null, because that is the initial state of these vars and
        // undefined/null return different results when used in math operations
        // (see isEventBlocked)
        self.lastScreenPointerEventTime = self.lastTouchEndTime = self.lastTouchStartX = self.lastTouchStartY = undefined;
    }
}, function(Dom) {
    var doc = document,
        defaultView = doc.defaultView,
        prototype = Dom.prototype;
    // In case of iOS if it is in iFrame then it should enter if condition
    if ((Ext.os.is.iOS && window.self !== window.top) || Ext.browser.is.AndroidStock || !(defaultView && defaultView.addEventListener)) {
        // Delegated listeners will get attached to the document object because
        // attaching to the window object will not work.  In IE8 this is needed because
        // events do not bubble up to the window - bubbling stops at the document
        // object.  The iOS < 5 check was carried forward from Sencha Touch 2.3 -
        // Not sure why it was needed.  The check for (defaultView && defaultView.addEventListener)
        // was carried forward as well - it may be required for older mobile browsers.
        // see also TOUCH-5408
        prototype.target = doc;
    } else {
        /**
         * @member Ext.event.publisher.Dom
         * @property {Object} target the DOM target to which listeners are attached for
         * delegated events.
         * @private
         */
        prototype.target = defaultView;
    }
    Dom.instance = new Dom();
});

Ext.define('Ext.overrides.event.publisher.Dom', {
    override: 'Ext.event.publisher.Dom'
}, function(DomPublisher) {
    var focusEvents = {
            focus: true,
            focusin: true,
            focusout: true,
            blur: true
        };
    if (Ext.isIE10m) {
        DomPublisher.override({
            isEventBlocked: function(e) {
                if (!focusEvents[e.type]) {
                    return this.callParent([
                        e
                    ]);
                }
                // eslint-disable-next-line vars-on-top
                var body = document.body,
                    ev = e.browserEvent,
                    el = Ext.synchronouslyFocusing;
                /* eslint-disable max-len, brace-style */
                // This horrid hack is necessary to work around the issue with input elements
                // in IE10m that can fail to focus under certain conditions. See comment in
                // Ext.dom.Element override.
                if (el && ((ev.type === 'focusout' && (ev.srcElement === el || ev.srcElement === window) && ev.toElement === body) || (ev.type === 'focusin' && (ev.srcElement === body || ev.srcElement === window) && ev.fromElement === el && ev.toElement === null))) {
                    return true;
                }
                /* eslint-enable max-len, brace-style */
                return false;
            }
        });
    }
    if (Ext.isIE9m) {
        // eslint-disable-next-line vars-on-top
        var docElement = document.documentElement,
            docBody = document.body,
            prototype = DomPublisher.prototype,
            onDirectEvent, onDirectCaptureEvent;
        // eslint-disable-line no-unused-vars
        prototype.target = document;
        prototype.directBoundListeners = {};
        // This method gets bound to the element scope in addDirectListener so that
        // the currentTarget can be captured using "this".
        onDirectEvent = function(e, publisher, capture) {
            e.target = e.srcElement || window;
            e.currentTarget = this;
            if (capture) {
                // Although directly attached capture listeners are not supported in IE9m
                // we still need to call the handler so at least the event fires.
                publisher.onDirectCaptureEvent(e);
            } else {
                publisher.onDirectEvent(e);
            }
        };
        onDirectCaptureEvent = function(e, publisher) {
            e.target = e.srcElement || window;
            e.currentTarget = this;
            // this, not DomPublisher
            publisher.onDirectCaptureEvent(e);
        };
        DomPublisher.override({
            addDelegatedListener: function(eventName) {
                this.delegatedListeners[eventName] = 1;
                // Use attachEvent for IE9 and below.  Even though IE9 strict supports
                // addEventListener, it has issues with using synthetic events.
                this.target.attachEvent('on' + eventName, this.onDelegatedEvent);
            },
            removeDelegatedListener: function(eventName) {
                delete this.delegatedListeners[eventName];
                this.target.detachEvent('on' + eventName, this.onDelegatedEvent);
            },
            addDirectListener: function(eventName, element, capture) {
                var me = this,
                    dom = element.dom,
                    // binding the listener to the element allows us to capture the
                    // "currentTarget" (see onDirectEvent)
                    boundFn = Ext.Function.bind(onDirectEvent, dom, [
                        me,
                        capture
                    ], true),
                    directBoundListeners = me.directBoundListeners,
                    handlers = directBoundListeners[eventName] || (directBoundListeners[eventName] = {});
                handlers[dom.id] = boundFn;
                // may be called with an SVG element here, which
                // does not have the attachEvent method on IE 9 strict
                if (dom.attachEvent) {
                    dom.attachEvent('on' + eventName, boundFn);
                } else {
                    me.callParent([
                        eventName,
                        element,
                        capture
                    ]);
                }
            },
            removeDirectListener: function(eventName, element, capture) {
                var dom = element.dom;
                if (dom.detachEvent) {
                    dom.detachEvent('on' + eventName, this.directBoundListeners[eventName][dom.id]);
                } else {
                    this.callParent([
                        eventName,
                        element,
                        capture
                    ]);
                }
            },
            doDelegatedEvent: function(e) {
                e.target = e.srcElement || window;
                if (e.type === 'focusin') {
                    // IE8 sometimes happen to focus <html> element instead of the body
                    // eslint-disable-next-line max-len
                    e.relatedTarget = e.fromElement === docBody || e.fromElement === docElement ? null : e.fromElement;
                } else if (e.type === 'focusout') {
                    // eslint-disable-next-line max-len
                    e.relatedTarget = e.toElement === docBody || e.toElement === docElement ? null : e.toElement;
                }
                return this.callParent([
                    e
                ]);
            }
        });
        // can't capture any events without addEventListener.  Have to have direct
        // listeners for every event that does not bubble.
        Ext.apply(prototype.directEvents, prototype.captureEvents);
        // These do not bubble in IE9m so have to attach direct listeners as well.
        Ext.apply(prototype.directEvents, {
            change: 1,
            input: 1,
            paste: 1
        });
        prototype.captureEvents = {};
    }
});

/**
 * @private
 */
Ext.define('Ext.event.publisher.Gesture', {
    extend: 'Ext.event.publisher.Dom',
    requires: [
        'Ext.util.Point',
        'Ext.AnimationQueue'
    ],
    uses: 'Ext.event.gesture.*',
    type: 'gesture',
    isCancelEvent: {
        touchcancel: 1,
        pointercancel: 1,
        MSPointerCancel: 1
    },
    isEndEvent: {
        mouseup: 1,
        touchend: 1,
        pointerup: 1,
        MSPointerUp: 1
    },
    handledEvents: [],
    handledDomEvents: [],
    constructor: function(config) {
        var me = this,
            handledDomEvents = me.handledDomEvents,
            supports = Ext.supports,
            supportsTouchEvents = supports.TouchEvents,
            onTouchStart = me.onTouchStart,
            onTouchMove = me.onTouchMove,
            onTouchEnd = me.onTouchEnd;
        me.handlers = {
            touchstart: onTouchStart,
            touchmove: onTouchMove,
            touchend: onTouchEnd,
            touchcancel: onTouchEnd,
            pointerdown: onTouchStart,
            pointermove: onTouchMove,
            pointerup: onTouchEnd,
            pointercancel: onTouchEnd,
            MSPointerDown: onTouchStart,
            MSPointerMove: onTouchMove,
            MSPointerUp: onTouchEnd,
            MSPointerCancel: onTouchEnd,
            mousedown: onTouchStart,
            mousemove: onTouchMove,
            mouseup: onTouchEnd
        };
        me.activeTouchesMap = {};
        me.activeTouches = [];
        me.changedTouches = [];
        me.recognizers = [];
        me.eventToRecognizer = {};
        me.cancelEvents = [];
        if (supportsTouchEvents) {
            // bind handlers that are only invoked when the browser has touchevents
            me.onTargetTouchMove = me.onTargetTouchMove.bind(me);
            me.onTargetTouchEnd = me.onTargetTouchEnd.bind(me);
        }
        if (supports.PointerEvents) {
            handledDomEvents.push('pointerdown', 'pointermove', 'pointerup', 'pointercancel');
            me.mousePointerType = 'mouse';
        } else if (supports.MSPointerEvents) {
            // IE10 uses vendor prefixed pointer events, IE11+ use unprefixed names.
            handledDomEvents.push('MSPointerDown', 'MSPointerMove', 'MSPointerUp', 'MSPointerCancel');
            me.mousePointerType = 4;
        } else if (supportsTouchEvents) {
            handledDomEvents.push('touchstart', 'touchmove', 'touchend', 'touchcancel');
        }
        if (!handledDomEvents.length || (supportsTouchEvents && Ext.os.is.Desktop)) {
            // If the browser doesn't have pointer events or touch events we use mouse events
            // to trigger gestures.  The exception to this rule is touch enabled desktop
            // browsers such as chrome and firefox on Windows touch screen devices.  These
            // browsers accept both touch and mouse input, so we need to listen for both
            // touch events and mouse events.
            handledDomEvents.push('mousedown', 'mousemove', 'mouseup');
        }
        me.initConfig(config);
        return me.callParent();
    },
    onReady: function() {
        this.callParent();
        Ext.Array.sort(this.recognizers, function(recognizerA, recognizerB) {
            var a = recognizerA.priority,
                b = recognizerB.priority;
            return (a > b) ? 1 : (a < b) ? -1 : 0;
        });
    },
    registerRecognizer: function(recognizer) {
        var me = this,
            handledEvents = recognizer.handledEvents,
            ln = handledEvents.length,
            eventName, i;
        // The recognizer will call our onRecognized method when it determines that a
        // gesture has occurred.
        recognizer.setOnRecognized(me.onRecognized);
        recognizer.setCallbackScope(me);
        // the gesture publishers handledEvents array is derived from the handledEvents
        // of all of its recognizers
        for (i = 0; i < ln; i++) {
            eventName = handledEvents[i];
            me.handledEvents.push(eventName);
            me.eventToRecognizer[eventName] = recognizer;
        }
        me.registerEvents(handledEvents);
        me.recognizers.push(recognizer);
    },
    onRecognized: function(recognizer, eventName, e, info, isCancel) {
        var me = this,
            touches = e.touches,
            changedTouches = e.changedTouches,
            ln = changedTouches.length,
            events = me.events,
            queueWasEmpty = !events.length,
            cancelEvents = me.cancelEvents,
            targetGroups, targets, i, touch;
        info = info || {};
        // At this point "e" still refers to the originally dispatched Ext.event.Event that
        // wraps a native browser event such as "touchend", or "mousemove".  We need to
        // dispatch with an an event object that has the correct "recognized" type such
        // as "tap", or "drag".  We don't want to change the type of the original event
        // object because it may be used asynchronously by event handlers, so we create a
        // new object that is chained to the original event.
        info.type = eventName;
        // Touch events have a handy feature - the original target of a touchstart is
        // always the target of successive touchmove/touchend events event if the touch
        // is dragged off of the original target.  Pointer events also have this behavior
        // via the setPointerCapture method, unless their target is removed from the dom
        // mid-gesture, however, we do not currently use setPointerCapture because it
        // can change the target of translated mouse events.  Mouse events do not have this
        // "capturing" feature at all - the target is always the element that was under
        // the mouse at the time the event occurred.  To be safe, and to ensure consistency,
        // we just always set the target of recognized events to be the original target
        // that was cached when the first "start" event was received.
        info.target = changedTouches[0].target;
        // reset stopped and claimed just in case the event that we are wrapping had
        // stoppedPropagation or claimGesture called
        info.stopped = false;
        info.claimed = false;
        info.isGesture = true;
        e = e.chain(info);
        if (!me.gestureTargets) {
            if (ln > 1) {
                targetGroups = [];
                for (i = 0; i < ln; i++) {
                    touch = changedTouches[i];
                    targetGroups.push(touch.targets);
                }
                targets = me.getCommonTargets(targetGroups);
            } else {
                targets = changedTouches[0].targets;
            }
            // Cache targets so that they only have to be computed once if multiple
            // gestures are currently being recognized.
            me.gestureTargets = targets;
        }
        if (isCancel && recognizer.isSingleTouch && (touches.length > 1)) {
            // single touch recognizer cancelled by the start of a second touch.
            // push into a separate queue which does not use the targets common to all
            // touches (this.gestureTargets) as the targets for publishing but rather
            // only uses the targets for the initial touch.
            e.target = touches[0].target;
            cancelEvents.push(e);
        } else {
            events.push(e);
        }
        if (queueWasEmpty) {
            // if there were no events in the queue previously, it means the dom event
            // has already been published, which means a recognizer must have recognized
            // a gesture asynchronously (e.g. singletap fires on a timer)
            // if this is the case we must publish now, otherwise we wait for the dom
            // event handler to publish after it is finished invoking the recognizers
            me.publishGestures();
        }
    },
    getCommonTargets: function(targetGroups) {
        var firstTargetGroup = targetGroups[0],
            ln = targetGroups.length,
            commonTargets = [],
            i = 1,
            target, targets, j;
        if (ln === 1) {
            return firstTargetGroup;
        }
        while (true) {
            // eslint-disable-line no-constant-condition
            target = firstTargetGroup[firstTargetGroup.length - i];
            if (!target) {
                return commonTargets;
            }
            for (j = 1; j < ln; j++) {
                targets = targetGroups[j];
                if (targets[targets.length - i] !== target) {
                    return commonTargets;
                }
            }
            commonTargets.unshift(target);
            i++;
        }
        return commonTargets;
    },
    // eslint-disable-line no-unreachable
    invokeRecognizers: function(methodName, e) {
        var recognizers = this.recognizers,
            ln = recognizers.length,
            i, recognizer;
        if (methodName === 'onStart') {
            for (i = 0; i < ln; i++) {
                recognizers[i].isActive = true;
            }
        }
        for (i = 0; i < ln; i++) {
            recognizer = recognizers[i];
            if (recognizer.isActive && recognizer[methodName].call(recognizer, e) === false) {
                recognizer.isActive = false;
            }
        }
    },
    /**
     * When a gesture has been claimed this method is invoked to remove gesture events of
     * other kinds.  See implementation in Gesture publisher.
     * @param {Ext.event.Event[]} events
     * @param {String} claimedEvent
     * @return {Number} The new index of the claimed event
     * @private
     */
    filterClaimed: function(events, claimedEvent) {
        var me = this,
            eventToRecognizer = me.eventToRecognizer,
            claimedEventType = claimedEvent.type,
            claimedRecognizer = eventToRecognizer[claimedEventType],
            claimedEventIndex, recognizer, type, i;
        for (i = events.length; i--; ) {
            type = events[i].type;
            if (type === claimedEventType) {
                claimedEventIndex = i;
            } else {
                recognizer = eventToRecognizer[type];
                // if there is no claimed recognizer it means the user must have invoked
                // claimGesture on a dom event (touchstart, touchmove etc).  If this is the
                // case we need to cease firing all gesture events, otherwise we allow only
                // the "claimed" recognizer to continue to dispatch events.
                if (!claimedRecognizer || (recognizer && (recognizer !== claimedRecognizer))) {
                    events.splice(i, 1);
                    if (claimedEventIndex) {
                        claimedEventIndex--;
                    }
                }
            }
        }
        me.claimRecognizer(claimedRecognizer, events[0]);
        return claimedEventIndex;
    },
    /**
     * Deactivates all recognizers other than the "claimed" recognizer
     * @param {Ext.event.gesture.Recognizer} claimedRecognizer
     * @param {Ext.event.Event} e
     * @private
     */
    claimRecognizer: function(claimedRecognizer, e) {
        var me = this,
            recognizers = me.recognizers,
            i, ln, recognizer;
        for (i = 0 , ln = recognizers.length; i < ln; i++) {
            recognizer = recognizers[i];
            // cancel recognition for all recognizers other than the one that was claimed
            if (recognizer !== claimedRecognizer) {
                recognizer.isActive = false;
                recognizer.cancel(e);
            }
        }
        if (me.events.length) {
            // if any recognizers added cancelation events...
            me.publishGestures(true);
        }
    },
    publishGestures: function(claimed) {
        var me = this,
            cancelEvents = me.cancelEvents,
            events = me.events,
            gestureTargets = me.gestureTargets;
        if (cancelEvents.length) {
            me.cancelEvents = [];
            // Since cancellation events cannot be claimed we pass true here which
            // prevents them from being claimed.
            me.publish(cancelEvents, me.getPropagatingTargets(cancelEvents[0].target), true);
        }
        if (events.length) {
            // It is important to reset the events property to an empty array before
            // publishing since since events may be added to the array during publishing.
            // This can happen if an event is claimed, thus triggering "cancel" gesture events.
            me.events = [];
            me.gestureTargets = null;
            me.publish(events, gestureTargets || me.getPropagatingTargets(events[0].target), claimed);
        }
    },
    updateTouches: function(e) {
        var me = this,
            browserEvent = e.browserEvent,
            type = e.type,
            // the touchSource is the object from which we get data about the changed touch
            // point or points related to an event object, such as identifier, target, and
            // coordinates. For touch event the source is changedTouches, for mouse and
            // pointer events it is the event object itself.
            touchSources = browserEvent.changedTouches || [
                browserEvent
            ],
            activeTouches = me.activeTouches,
            activeTouchesMap = me.activeTouchesMap,
            changedTouches = [],
            touchSource, identifier, touch, target, i, ln, x, y;
        for (i = 0 , ln = touchSources.length; i < ln; i++) {
            touchSource = touchSources[i];
            if ('identifier' in touchSource) {
                // touch events have an identifier property on their touches objects.
                // It can be 0, hence the "in" check
                identifier = touchSource.identifier;
            } else if ('pointerId' in touchSource) {
                // Pointer events have a pointerId on the event object itself
                identifier = touchSource.pointerId;
            } else {
                // Mouse events don't have an identifier, so we always use 1 since there
                // can only be one mouse touch point active at a time
                identifier = 1;
            }
            touch = activeTouchesMap[identifier];
            if (!touch) {
                target = Ext.event.Event.resolveTextNode(touchSource.target);
                touch = activeTouchesMap[identifier] = {
                    identifier: identifier,
                    target: target,
                    // There are 2 main advantages to caching the targets here, vs.
                    // waiting until onRecognized
                    // 1. for "move" events we don't have to construct the targets array
                    // for every event - a theoretical performance win
                    // 2. if the target is removed from the dom mid-gesture we still
                    // want any gestures listeners on elements that were above the
                    // target to complete.  This means the propagating targets must reflect
                    // the target element's initial hierarchy when the gesture began
                    targets: me.getPropagatingTargets(target)
                };
                activeTouches.push(touch);
            }
            if (me.isEndEvent[type] || me.isCancelEvent[type]) {
                delete activeTouchesMap[identifier];
                Ext.Array.remove(activeTouches, touch);
            }
            x = Math.round(touchSource.pageX);
            y = Math.round(touchSource.pageY);
            touch.pageX = x;
            touch.pageY = y;
            // recognizers frequently use Point methods, so go ahead and create a Point.
            touch.point = new Ext.util.Point(x, y);
            changedTouches.push(touch);
        }
        // decorate the event object with the touch point info so that it can be used from
        // within gesture recognizers (clone touches, just in case event object is used
        // asynchronously since this.activeTouches is continuously modified)
        e.touches = Ext.Array.clone(activeTouches);
        // no need to clone changedTouches since we just created it from scratch
        e.changedTouches = changedTouches;
    },
    publishDelegatedDomEvent: function(e) {
        var me = this;
        if (!e.button || e.button < 1) {
            // mouse gestures (and pointer gestures triggered by a mouse) can only be
            // initiated using the left button (0).  button value < 0 is also acceptable
            // (e.g. pointermove has a button value of -1)
            // Track the event on the instance so it can be fired after gesture recognition
            // completes (if any gestures are recognized they will be added to this array)
            me.events = [
                e
            ];
            // This property on the browser event object indicates that the event has bubbled
            // up to the window object and has begun being handled by the gesture publisher.
            // If the user calls stopPropagation on an event that has not yet been "handled"
            // it triggers gesture cancellation and cleanup.
            e.browserEvent.$extHandled = true;
            me.handlers[e.type].call(me, e);
        } else {
            // mouse events *with* button still need to be published.
            me.callParent([
                e
            ]);
        }
    },
    onTouchStart: function(e) {
        var me = this,
            target = e.target,
            touches = e.browserEvent.touches;
        if (e.browserEvent.type === 'touchstart') {
            // When using touch events, if the target is removed from the dom mid-gesture
            // the touchend event cannot be handled normally because it will not bubble
            // to the top of the dom since the target el is no longer attached to the dom.
            // Add some special handlers to clean everything up. (see onTargetTouchEnd)
            // use addEventListener directly so that we don't have to spin up an instance
            // of Ext.Element for every event target.
            target.addEventListener('touchmove', me.onTargetTouchMove);
            target.addEventListener('touchend', me.onTargetTouchEnd);
            target.addEventListener('touchcancel', me.onTargetTouchEnd);
        }
        // There is a bug in IOS8 where touchstart, but not touchend event is
        // fired when clicking on controls for audio/video, which can leave
        // us in a bad state here.
        if (touches && touches.length <= me.activeTouches.length) {
            me.removeGhostTouches(touches);
        }
        me.updateTouches(e);
        if (!me.isStarted) {
            // Disable garbage collection during gestures so that if the target element
            // of a gesture is removed from the dom, it does not get garbage collected
            // until the gesture is complete
            if (Ext.enableGarbageCollector) {
                Ext.dom.GarbageCollector.pause();
            }
            // this is the first active touch - invoke "onStart" which indicates the
            // beginning of a gesture
            me.isStarted = true;
            me.invokeRecognizers('onStart', e);
        }
        me.invokeRecognizers('onTouchStart', e);
        me.publishGestures();
    },
    onTouchMove: function(e) {
        var me = this,
            mousePointerType = me.mousePointerType,
            isStarted = me.isStarted;
        if (isStarted || (e.pointerType !== 'mouse')) {
            me.updateTouches(e);
        }
        if (isStarted) {
            // In IE10/11, the corresponding pointerup event is not fired after the pointerdown
            // after the mouse is released from the scrollbar. However, it does fire a pointermove
            // event with buttons: 0, so we capture that here and ensure the touch end process
            // is completed.
            if (mousePointerType && e.browserEvent.pointerType === mousePointerType && e.buttons === 0) {
                e.type = Ext.dom.Element.prototype.eventMap.touchend;
                e.button = 0;
                me.onTouchEnd(e);
                return;
            }
            if (e.changedTouches.length > 0) {
                me.invokeRecognizers('onTouchMove', e);
            }
        }
        me.publishGestures();
    },
    // This method serves as the handler for both "end" and "cancel" events.  This is
    // because they are handled identically with the exception of the recognizer method
    // that is called.
    onTouchEnd: function(e) {
        var me = this,
            isStarted = me.isStarted,
            touchCount;
        if (isStarted || (e.pointerType !== 'mouse')) {
            me.updateTouches(e);
        }
        if (!isStarted) {
            me.publishGestures();
            return;
        }
        touchCount = me.activeTouches.length;
        // If an exception is thrown in any of the recognizers, we still need to run
        // the cleanup. Otherwise the gesture might get "stuck" and *every* pointer event
        // after that will fire the same handlers over and over, potentially spewing
        // the same exceptions endlessly. See https://sencha.jira.com/browse/EXTJS-15674.
        // We don't want to mask the original exception though, let it propagate.
        try {
            me.invokeRecognizers(me.isCancelEvent[e.type] ? 'onTouchCancel' : 'onTouchEnd', e);
        } finally {
            // This can throw too
            try {
                if (!touchCount) {
                    // no more active touches - invoke onEnd to indicate the end of the gesture
                    me.isStarted = false;
                    me.invokeRecognizers('onEnd', e);
                }
            } finally {
                // Right, THIS can throw again!
                try {
                    me.publishGestures();
                } finally {
                    if (!touchCount) {
                        // Gesture is finished, safe to resume garbage collection so that any target
                        // elements destroyed while gesture was in progress can be collected
                        if (Ext.enableGarbageCollector) {
                            Ext.dom.GarbageCollector.resume();
                        }
                    }
                    // The parent code may not to be reached in this case
                    me.reEnterCountAdjusted = true;
                    me.reEnterCount--;
                }
            }
        }
    },
    onTargetTouchMove: function(e) {
        if (Ext.elevateFunction) {
            // using [e] is faster than using arguments in most browsers
            // http://jsperf.com/passing-arguments
            Ext.elevateFunction(this.doTargetTouchMove, this, [
                e
            ]);
        } else {
            this.doTargetTouchMove(e);
        }
    },
    doTargetTouchMove: function(e) {
        var me = this;
        // handle touchmove if the target el was removed from dom mid-gesture.
        // see onTouchStart/onTargetTouchEnd for further explanation
        if (!Ext.getBody().contains(e.target)) {
            me.reEnterCountAdjusted = false;
            me.reEnterCount++;
            this.onTouchMove(new Ext.event.Event(e));
            if (!me.reEnterCountAdjusted) {
                me.reEnterCount--;
            }
        }
    },
    onTargetTouchEnd: function(e) {
        if (Ext.elevateFunction) {
            // using [e] is faster than using arguments in most browsers
            // http://jsperf.com/passing-arguments
            Ext.elevateFunction(this.doTargetTouchEnd, this, [
                e
            ]);
        } else {
            this.doTargetTouchEnd(e);
        }
    },
    doTargetTouchEnd: function(e) {
        var me = this,
            target = e.target;
        target.removeEventListener('touchmove', me.onTargetTouchMove);
        target.removeEventListener('touchend', me.onTargetTouchEnd);
        target.removeEventListener('touchcancel', me.onTargetTouchEnd);
        // if the target el was removed from the dom mid-gesture, then the touchend event,
        // when it occurs, will not be handled because it will not bubble to the top of
        // the dom. This is because the "target" of the touchend is the removed element.
        // If this is the case, go ahead and trigger touchend handling now.
        // Detect whether the target was removed from the DOM mid gesture by using Element.contains.
        // Originally we attempted to detect this by listening for the DOMNodeRemovedFromDocument
        // event, and setting a flag on the element when it was removed, however that
        // approach only works when the element is removed using removedChild, and fails
        // if the element is removed because some ancestor had innerHTML assigned.
        // note: this handling is applicable for actual touchend events, pointer and mouse
        // events will fire on whatever element is under the cursor/pointer after the
        // original target has been removed.
        if (!Ext.getBody().contains(target)) {
            me.reEnterCountAdjusted = false;
            me.reEnterCount++;
            me.onTouchEnd(new Ext.event.Event(e));
            if (!me.reEnterCountAdjusted) {
                me.reEnterCount--;
            }
        }
    },
    /**
     * Resets the internal state of the Gesture publisher and all of its recognizers.
     * Applications will not typically need to use this method, but it is useful for
     * Unit-testing situations where a clean slate is required for each test.
     *
     * Calling this method will also reset the state of Ext.event.publisher.Dom
     */
    reset: function() {
        var me = this,
            recognizers = me.recognizers,
            ln = recognizers.length,
            i, recognizer;
        me.activeTouchesMap = {};
        me.activeTouches = [];
        me.changedTouches = [];
        me.isStarted = false;
        me.gestureTargets = null;
        me.events = [];
        me.cancelEvents = [];
        for (i = 0; i < ln; i++) {
            recognizer = recognizers[i];
            recognizer.reset();
            recognizer.isActive = false;
        }
        this.callParent();
    },
    privates: {
        removeGhostTouches: function(touches) {
            var ids = {},
                len = touches.length,
                activeTouches = this.activeTouches,
                map = this.activeTouchesMap,
                i, id, touch;
            // Collect the actual touches
            for (i = 0; i < len; ++i) {
                ids[touches[i].identifier] = true;
            }
            i = activeTouches.length;
            while (i--) {
                touch = activeTouches[i];
                id = touch.identifier;
                if (!touches[id]) {
                    Ext.Array.remove(activeTouches, touch);
                    delete map[id];
                }
            }
        }
    }
}, function(Gesture) {
    var EventProto = Event.prototype,
        stopPropagation = EventProto.stopPropagation;
    if (stopPropagation) {
        EventProto.stopPropagation = function() {
            var me = this,
                publisher = Gesture.instance,
                type = me.type,
                e;
            if (!me.$extHandled && publisher.handles[type]) {
                // User called stop propagation on a native event used by the gesture publisher
                // to synthesize gesture events. Cancel gesture recognition and reset the publisher.
                e = new Ext.event.Event(me);
                publisher.updateTouches(e);
                publisher.invokeRecognizers('onTouchCancel', e);
                publisher.reset();
                publisher.reEnterCountAdjusted = true;
            }
            stopPropagation.apply(me, arguments);
        };
    }
    Gesture.instance = Ext.$gesturePublisher = new Gesture();
});

Ext.define('Ext.overrides.event.publisher.Gesture', {
    override: 'Ext.event.publisher.Gesture'
}, function() {
    if (Ext.isIE9m) {
        this.override({
            updateTouches: function(e, isEnd) {
                var browserEvent = e.browserEvent,
                    xy = e.getXY();
                // I don't always set pageX and pageY on the event object, but when I do
                // it's because the Gesture publisher expects an event object that has them.
                browserEvent.pageX = xy[0];
                browserEvent.pageY = xy[1];
                this.callParent([
                    e,
                    isEnd
                ]);
            },
            doDelegatedEvent: function(e) {
                // Workaround IE's "Member not found" errors when accessing an event
                // object asynchronously.  Needed for all gesture handlers because
                // they use requestAnimationFrame (see enableIEAsync for more details)
                this.callParent([
                    Ext.event.Event.enableIEAsync(e)
                ]);
            }
        });
    }
});

/**
 *
 */
Ext.define('Ext.mixin.Templatable', {
    extend: 'Ext.Mixin',
    mixinConfig: {
        id: 'templatable'
    },
    referenceAttributeName: 'reference',
    referenceSelector: '[reference]',
    getElementConfig: function() {
        return {
            reference: 'element'
        };
    },
    getElementTemplate: function() {
        var elementTemplate = document.createDocumentFragment();
        elementTemplate.appendChild(Ext.Element.create(this.getElementConfig(), true));
        return elementTemplate;
    },
    initElement: function() {
        var prototype = this.self.prototype;
        prototype.elementTemplate = this.getElementTemplate();
        prototype.initElement = prototype.doInitElement;
        this.initElement.apply(this, arguments);
    },
    linkElement: function(reference, node) {
        this.link(reference, node);
    },
    doInitElement: function() {
        var referenceAttributeName = this.referenceAttributeName,
            renderElement, referenceNodes, i, ln, referenceNode, reference;
        renderElement = this.elementTemplate.cloneNode(true);
        referenceNodes = renderElement.querySelectorAll(this.referenceSelector);
        for (i = 0 , ln = referenceNodes.length; i < ln; i++) {
            referenceNode = referenceNodes[i];
            reference = referenceNode.getAttribute(referenceAttributeName);
            referenceNode.removeAttribute(referenceAttributeName);
            this.linkElement(reference, referenceNode);
        }
    }
});

/**
 * @private
 * Handle batch read / write of DOMs, currently used in SizeMonitor + PaintMonitor
 */
Ext.define('Ext.TaskQueue', {
    requires: 'Ext.AnimationQueue',
    singleton: true,
    pending: false,
    mode: true,
    // true for 'read', false for 'write'
    protectedReadQueue: [],
    protectedWriteQueue: [],
    readQueue: [],
    writeQueue: [],
    readRequestId: 0,
    writeRequestId: 0,
    timer: null,
    constructor: function() {
        var me = this;
        me.run = me.run.bind(me);
        // Some global things like floated wrapper are persistent and will add tasks/
        // add timers all the time, spoiling resource checks in our unit test suite.
        // To work around that we're implementing a parallel queue where only trusted
        // tasks will go, and fly under the radar of resource checker.
        me.runProtected = Ext.Function.bind(me.run, me, [
            me.protectedReadQueue,
            me.protectedWriteQueue,
            'runProtected'
        ]);
        me.runProtected.$skipTimerCheck = true;
        // iOS has a nasty bug which causes pending requestAnimationFrame to not release
        // the callback when the WebView is switched back and forth from / to being background 
        // process. We use a watchdog timer to workaround this, and restore the pending state
        // correctly if this happens. This timer has to be set as an interval from the very
        // beginning and we have to keep it running for as long as the app lives, setting it later
        // doesn't seem to work.
        // The watchdog timer must be accessible for environments to cancel.
        if (Ext.os.is.iOS) {
            me.watch.$skipTimerCheck = true;
            me.watchdogTimer = Ext.interval(this.watch, 500, this);
        }
    },
    requestRead: function(fn, scope, args) {
        var request = {
                id: ++this.readRequestId,
                fn: fn,
                scope: scope,
                args: args
            };
        if (arguments[3] === true) {
            this.protectedReadQueue.push(request);
            this.request(true, 'runProtected');
        } else {
            this.readQueue.push(request);
            this.request(true);
        }
        return request.id;
    },
    cancelRead: function(id) {
        this.cancelRequest(this.readQueue, id, true);
    },
    requestWrite: function(fn, scope, args) {
        var me = this,
            request = {
                id: ++me.writeRequestId,
                fn: fn,
                scope: scope,
                args: args
            };
        if (arguments[3] === true) {
            me.protectedWriteQueue.push(request);
            me.request(false, 'runProtected');
        } else {
            me.writeQueue.push(request);
            me.request(false);
        }
        return request.id;
    },
    cancelWrite: function(id) {
        this.cancelRequest(this.writeQueue, id, false);
    },
    request: function(mode, method) {
        var me = this;
        // Used below to cancel the correct timer.
        /* eslint-disable-next-line one-var */
        var oldMode = me.mode;
        if (!me.pending) {
            me.pendingTime = Date.now();
            me.pending = true;
            me.mode = mode;
            if (mode) {
                me.timer = Ext.defer(me[method] || me.run, 1);
            } else {
                me.timer = Ext.raf(me[method] || me.run);
            }
        }
        // Last one should win
        if (me.mode === mode && me.timer) {
            if (oldMode) {
                Ext.undefer(me.timer);
            } else {
                Ext.unraf(me.timer);
            }
            if (mode) {
                me.timer = Ext.defer(me[method] || me.run, 1);
            } else {
                me.timer = Ext.raf(me[method] || me.run);
            }
        }
    },
    cancelRequest: function(queue, id, mode) {
        var i;
        for (i = 0; i < queue.length; i++) {
            if (queue[i].id === id) {
                queue.splice(i, 1);
                break;
            }
        }
        if (!queue.length && this.mode === mode && this.timer) {
            Ext.undefer(this.timer);
        }
    },
    watch: function() {
        if (this.pending && Date.now() - this.pendingTime >= 500) {
            this.run();
        }
    },
    run: function(readQueue, writeQueue, method) {
        var me = this,
            mode = null,
            queue, tasks, task, fn, scope, args, i, len;
        readQueue = readQueue || me.readQueue;
        writeQueue = writeQueue || me.writeQueue;
        me.pending = false;
        me.pending = me.timer = false;
        if (me.mode) {
            queue = readQueue;
            if (writeQueue.length > 0) {
                mode = false;
            }
        } else {
            queue = writeQueue;
            if (readQueue.length > 0) {
                mode = true;
            }
        }
        tasks = queue.slice();
        queue.length = 0;
        for (i = 0 , len = tasks.length; i < len; i++) {
            task = tasks[i];
            fn = task.fn;
            scope = task.scope;
            args = task.args;
            if (scope && (scope.destroying || scope.destroyed)) {
                
                continue;
            }
            if (typeof fn === 'string') {
                fn = scope[fn];
            }
            if (args) {
                fn.apply(scope, args);
            } else {
                fn.call(scope);
            }
        }
        tasks.length = 0;
        if (mode !== null) {
            me.request(mode, method);
        }
    },
    clear: function() {
        var me = this,
            timer = me.timer;
        if (timer) {
            if (me.mode) {
                Ext.undefer(timer);
            } else {
                Ext.unraf(timer);
            }
        }
        me.readQueue.length = me.writeQueue.length = 0;
        me.pending = me.timer = false;
        me.mode = true;
    },
    /* eslint-disable-next-line comma-style */
    privates: {
        flush: function() {
            var me = this,
                mode = me.mode;
            while (me.readQueue.length || me.writeQueue.length) {
                if (mode) {
                    Ext.undefer(me.timer);
                } else {
                    Ext.unraf(me.timer);
                }
                me.run();
            }
            me.mode = true;
        }
    }
});

/**
 * @private
 */
Ext.define('Ext.util.sizemonitor.Abstract', {
    mixins: [
        'Ext.mixin.Templatable'
    ],
    requires: [
        'Ext.TaskQueue'
    ],
    config: {
        element: null,
        callback: Ext.emptyFn,
        scope: null,
        args: []
    },
    width: null,
    height: null,
    contentWidth: null,
    contentHeight: null,
    constructor: function(config) {
        var me = this;
        me.refresh = me.refresh.bind(me);
        me.info = {
            width: 0,
            height: 0,
            contentWidth: 0,
            contentHeight: 0,
            flag: 0
        };
        me.initElement();
        me.initConfig(config);
        me.bindListeners(true);
    },
    bindListeners: Ext.emptyFn,
    applyElement: function(element) {
        if (element) {
            return Ext.get(element);
        }
    },
    updateElement: function(element) {
        element.append(this.detectorsContainer, true);
        element.addCls(Ext.baseCSSPrefix + 'size-monitored');
    },
    applyArgs: function(args) {
        return args.concat([
            this.info
        ]);
    },
    refreshMonitors: Ext.emptyFn,
    forceRefresh: function() {
        Ext.TaskQueue.requestRead('refresh', this);
    },
    getContentBounds: function() {
        return this.detectorsContainer.getBoundingClientRect();
    },
    getContentWidth: function() {
        return this.detectorsContainer.clientWidth;
    },
    getContentHeight: function() {
        return this.detectorsContainer.clientHeight;
    },
    refreshSize: function() {
        var element = this.getElement();
        if (!element || element.destroyed) {
            return false;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            size = element.measure(),
            width = size.width,
            height = size.height,
            contentWidth = me.getContentWidth(),
            contentHeight = me.getContentHeight(),
            currentContentWidth = me.contentWidth,
            currentContentHeight = me.contentHeight,
            info = me.info,
            resized = false,
            flag;
        me.width = width;
        me.height = height;
        me.contentWidth = contentWidth;
        me.contentHeight = contentHeight;
        flag = ((currentContentWidth !== contentWidth ? 1 : 0) + (currentContentHeight !== contentHeight ? 2 : 0));
        if (flag > 0) {
            info.width = width;
            info.height = height;
            info.contentWidth = contentWidth;
            info.contentHeight = contentHeight;
            info.flag = flag;
            resized = true;
            me.getCallback().apply(me.getScope(), me.getArgs());
        }
        return resized;
    },
    refresh: function() {
        if (this.destroying || this.destroyed) {
            return;
        }
        this.refreshSize();
        // We should always refresh the monitors regardless of whether or not refreshSize
        // resulted in a new size.  This avoids race conditions in situations such as
        // panel placeholder expand where we layout the panel in its expanded state momentarily
        // just so we can measure its animation destination, then immediately collapse it.
        // In such a scenario refreshSize() will be acting on the original size since it
        // is asynchronous, so it will not detect a size change, but we still need to
        // ensure that the monitoring elements are in sync, or else the next resize event
        // will not fire.
        Ext.TaskQueue.requestWrite('refreshMonitors', this);
    },
    destroy: function() {
        var me = this,
            element = me.getElement();
        me.bindListeners(false);
        if (element && !element.destroyed) {
            element.removeCls(Ext.baseCSSPrefix + 'size-monitored');
        }
        delete me._element;
        // This is a closure so Base destructor won't null it
        me.refresh = null;
        me.callParent();
    }
});

/**
 * @private
 */
Ext.define('Ext.util.sizemonitor.Scroll', {
    extend: 'Ext.util.sizemonitor.Abstract',
    getElementConfig: function() {
        return {
            reference: 'detectorsContainer',
            classList: [
                Ext.baseCSSPrefix + 'size-monitors',
                'scroll'
            ],
            children: [
                {
                    reference: 'expandMonitor',
                    className: 'expand'
                },
                {
                    reference: 'shrinkMonitor',
                    className: 'shrink'
                }
            ]
        };
    },
    constructor: function(config) {
        this.onScroll = this.onScroll.bind(this);
        this.callParent(arguments);
    },
    bindListeners: function(bind) {
        var method = bind ? 'addEventListener' : 'removeEventListener';
        this.expandMonitor[method]('scroll', this.onScroll, true);
        this.shrinkMonitor[method]('scroll', this.onScroll, true);
    },
    onScroll: function() {
        if (!this.destroyed) {
            Ext.TaskQueue.requestRead('refresh', this);
        }
    },
    refreshMonitors: function() {
        var expandMonitor = this.expandMonitor,
            shrinkMonitor = this.shrinkMonitor,
            end = 1000000;
        if (expandMonitor && !expandMonitor.destroyed) {
            expandMonitor.scrollLeft = end;
            expandMonitor.scrollTop = end;
        }
        if (shrinkMonitor && !shrinkMonitor.destroyed) {
            shrinkMonitor.scrollLeft = end;
            shrinkMonitor.scrollTop = end;
        }
    },
    destroy: function() {
        // This is a closure so Base destructor won't null it
        this.onScroll = null;
        this.callParent();
    }
});

/**
 *
 */
Ext.define('Ext.util.SizeMonitor', {
    requires: [
        'Ext.util.sizemonitor.Scroll'
    ],
    // 'Ext.util.sizemonitor.OverflowChange'
    constructor: function(config) {
        return new Ext.util.sizemonitor.Scroll(config);
    }
});
// var namespace = Ext.util.sizemonitor;
//
// if (Ext.browser.is.Firefox) {
//     // this one decreases the grid performance in Firefox
//     return new namespace.OverflowChange(config);
// } else {
//     return new namespace.Scroll(config);
// }

/**
 * @private
 */
Ext.define('Ext.event.publisher.ElementSize', {
    extend: 'Ext.event.publisher.Publisher',
    requires: [
        'Ext.util.SizeMonitor'
    ],
    type: 'size',
    handledEvents: [
        'resize'
    ],
    constructor: function() {
        this.monitors = {};
        this.subscribers = {};
        this.callParent(arguments);
    },
    subscribe: function(element) {
        var id = element.id,
            subscribers = this.subscribers,
            monitors = this.monitors;
        if (subscribers[id]) {
            ++subscribers[id];
        } else {
            subscribers[id] = 1;
            monitors[id] = new Ext.util.SizeMonitor({
                element: element,
                callback: this.onElementResize,
                scope: this,
                args: [
                    element
                ]
            });
        }
        element.on('painted', 'forceRefresh', monitors[id]);
        return true;
    },
    unsubscribe: function(element) {
        var id = element.id,
            subscribers = this.subscribers,
            monitors = this.monitors,
            sizeMonitor;
        if (subscribers[id] && !--subscribers[id]) {
            delete subscribers[id];
            sizeMonitor = monitors[id];
            element.un('painted', 'forceRefresh', sizeMonitor);
            sizeMonitor.destroy();
            delete monitors[id];
        }
        if (element.activeRead) {
            Ext.TaskQueue.cancelRead(element.activeRead);
        }
    },
    fireElementResize: function(element, info) {
        delete element.activeRead;
        this.fire(element, 'resize', [
            element,
            info
        ]);
    },
    onElementResize: function(element, info) {
        if (!element.activeRead) {
            element.activeRead = Ext.TaskQueue.requestRead('fireElementResize', this, [
                element,
                info
            ], !!element.$skipResourceCheck);
        }
    },
    // eslint-disable-line comma-style
    // This is useful for unit testing so we can force resizes
    // to take place synchronously when we know they have changed
    privates: {
        // eslint-disable-line comma-style
        syncRefresh: function(elements) {
            var el, monitor, i, len;
            elements = Ext.Array.from(elements);
            for (i = 0 , len = elements.length; i < len; ++i) {
                el = elements[i];
                if (typeof el !== 'string') {
                    el = el.id;
                }
                monitor = this.monitors[el];
                if (monitor) {
                    monitor.forceRefresh();
                }
            }
            // This just pushes onto the RAF queue.
            Ext.TaskQueue.flush();
            // Flush the RAF queue to make this truly synchronous.
            Ext.Function.fireElevatedHandlers();
        }
    }
}, function(ElementSize) {
    ElementSize.instance = new ElementSize();
});

/**
 * @private
 */
Ext.define('Ext.util.paintmonitor.Abstract', {
    config: {
        element: null,
        callback: Ext.emptyFn,
        scope: null,
        args: []
    },
    eventName: '',
    monitorClass: '',
    constructor: function(config) {
        this.onElementPainted = this.onElementPainted.bind(this);
        this.initConfig(config);
    },
    bindListeners: function(bind) {
        // eslint-disable-next-line max-len
        this.monitorElement[bind ? 'addEventListener' : 'removeEventListener'](this.eventName, this.onElementPainted, true);
    },
    applyElement: function(element) {
        if (element) {
            return Ext.get(element);
        }
    },
    updateElement: function(element) {
        this.monitorElement = Ext.Element.create({
            classList: [
                Ext.baseCSSPrefix + 'paint-monitor',
                this.monitorClass
            ]
        }, true);
        element.appendChild(this.monitorElement, true);
        element.addCls(Ext.baseCSSPrefix + 'paint-monitored');
        this.bindListeners(true);
    },
    onElementPainted: function() {},
    destroy: function() {
        var me = this,
            monitorElement = me.monitorElement,
            parentNode = monitorElement.parentNode,
            element = me.getElement();
        me.bindListeners(false);
        delete me.monitorElement;
        if (element && !element.destroyed) {
            element.removeCls(Ext.baseCSSPrefix + 'paint-monitored');
            delete me._element;
        }
        if (parentNode) {
            parentNode.removeChild(monitorElement);
        }
        me.callParent();
    }
});

/**
 * @private
 */
Ext.define('Ext.util.paintmonitor.CssAnimation', {
    extend: 'Ext.util.paintmonitor.Abstract',
    eventName: Ext.browser.is.WebKit ? 'webkitAnimationEnd' : 'animationend',
    monitorClass: 'cssanimation',
    onElementPainted: function(e) {
        if (e.animationName === Ext.baseCSSPrefix + 'paint-monitor-helper') {
            this.getCallback().apply(this.getScope(), this.getArgs());
        }
    }
});

/**
 *
 */
Ext.define('Ext.util.PaintMonitor', {
    requires: [
        'Ext.util.paintmonitor.CssAnimation'
    ],
    constructor: function(config) {
        return new Ext.util.paintmonitor.CssAnimation(config);
    }
});

/**
 * @private
 */
Ext.define('Ext.event.publisher.ElementPaint', {
    extend: 'Ext.event.publisher.Publisher',
    requires: [
        'Ext.util.PaintMonitor',
        'Ext.TaskQueue'
    ],
    type: 'paint',
    handledEvents: [
        'painted'
    ],
    constructor: function() {
        this.monitors = {};
        this.subscribers = {};
        this.callParent(arguments);
    },
    subscribe: function(element) {
        var me = this,
            id = element.id,
            subscribers = me.subscribers;
        if (subscribers[id]) {
            ++subscribers[id];
        } else {
            subscribers[id] = 1;
            me.monitors[id] = new Ext.util.PaintMonitor({
                element: element,
                callback: me.onElementPainted,
                scope: me,
                args: [
                    element
                ]
            });
        }
    },
    unsubscribe: function(element) {
        var id = element.id,
            subscribers = this.subscribers,
            monitors = this.monitors;
        if (subscribers[id] && !--subscribers[id]) {
            delete subscribers[id];
            monitors[id].destroy();
            delete monitors[id];
        }
        if (element.activeRead) {
            Ext.TaskQueue.cancelRead(element.activeRead);
        }
    },
    fireElementPainted: function(element) {
        delete element.activeRead;
        this.fire(element, 'painted', [
            element
        ]);
    },
    onElementPainted: function(element) {
        if (!element.activeRead) {
            element.activeRead = Ext.TaskQueue.requestRead('fireElementPainted', this, [
                element
            ], !!element.$skipResourceCheck);
        }
    }
}, // eslint-disable-line comma-style
function(ElementPaint) {
    ElementPaint.instance = new ElementPaint();
});

/**
 * @class Ext.dom.Element
 * @alternateClassName Ext.Element
 * @mixins Ext.util.Positionable
 * @mixins Ext.mixin.Observable
 *
 * Encapsulates a DOM element, adding simple DOM manipulation facilities, normalizing for browser
 * differences.
 *
 * **Note:** The events included in this Class are the ones we've found to be the most commonly
 * used. Many events are not listed here due to the expedient rate of change across browsers.
 * For a more comprehensive list, please visit the following resources:
 *
 * + [Mozilla Event Reference Guide](https://developer.mozilla.org/en-US/docs/Web/Events)
 * + [W3 Pointer Events](http://www.w3.org/TR/pointerevents/)
 * + [W3 Touch Events](http://www.w3.org/TR/touch-events/)
 * + [W3 DOM 2 Events](http://www.w3.org/TR/DOM-Level-2-Events/)
 * + [W3 DOM 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/)
 *
 * ## Usage
 *
 *     // by id
 *     var el = Ext.get("my-div");
 *
 *     // by DOM element reference
 *     var el = Ext.get(myDivElement);
 *
 * ## Selecting Descendant Elements
 *
 * Ext.dom.Element instances can be used to select descendant nodes using CSS selectors.
 * There are 3 methods that can be used for this purpose, each with a slightly different
 * twist:
 *
 * - {@link #method-query}
 * - {@link #method-selectNode}
 * - {@link #method-select}
 *
 * These methods can accept any valid CSS selector since they all use
 * [querySelectorAll](http://www.w3.org/TR/css3-selectors/) under the hood. The primary
 * difference between these three methods is their return type:
 *
 * To get an array of HTMLElement instances matching the selector '.foo' use the query
 * method:
 *
 *     element.query('.foo');
 *
 * This can easily be transformed into an array of Ext.dom.Element instances by setting
 * the `asDom` parameter to `false`:
 *
 *     element.query('.foo', false);
 *
 * If the desired result is only the first matching HTMLElement use the selectNode method:
 *
 *     element.selectNode('.foo');
 *
 * Once again, the dom node can be wrapped in an Ext.dom.Element by setting the `asDom`
 * parameter to `false`:
 *
 *     element.selectNode('.foo', false);
 *
 * The `select` method is used when the desired return type is a {@link
 * Ext.CompositeElementLite CompositeElementLite} or a {@link Ext.CompositeElement
 * CompositeElement}.  These are collections of elements that can be operated on as a
 * group using any of the methods of Ext.dom.Element.  The only difference between the two
 * is that CompositeElementLite is a collection of HTMLElement instances, while
 * CompositeElement is a collection of Ext.dom.Element instances.  To retrieve a
 * CompositeElementLite that represents a collection of HTMLElements for selector '.foo':
 *
 *     element.select('.foo');
 *
 * For a {@link Ext.CompositeElement CompositeElement} simply pass `true` as the
 * `composite` parameter:
 *
 *     element.select('.foo', true);
 *
 * The query selection methods can be used even if you don't have a Ext.dom.Element to
 * start with For example to select an array of all HTMLElements in the document that match the
 * selector '.foo', simply wrap the document object in an Ext.dom.Element instance using
 * {@link Ext#fly}:
 *
 *     Ext.fly(document).query('.foo');
 *
 * # Animations
 *
 * When an element is manipulated, by default there is no animation.
 *
 *     var el = Ext.get("my-div");
 *
 *     // no animation
 *     el.setWidth(100);
 *
 * specified as boolean (true) for default animation effects.
 *
 *     // default animation
 *     el.setWidth(100, true);
 *
 * To configure the effects, an object literal with animation options to use as the Element
 * animation configuration object can also be specified. Note that the supported Element animation
 * configuration options are a subset of the {@link Ext.fx.Anim} animation options specific to Fx
 * effects. The supported Element animation configuration options are:
 *
 *     Option    Default   Description
 *     --------- --------  ---------------------------------------------
 *     duration  350       The duration of the animation in milliseconds
 *     easing    easeOut   The easing method
 *     callback  none      A function to execute when the anim completes
 *     scope     this      The scope (this) of the callback function
 *
 * Usage:
 *
 *     // Element animation options object
 *     var opt = {
 *         duration: 1000,
 *         easing: 'elasticIn',
 *         callback: this.foo,
 *         scope: this
 *     };
 *     // animation with some options set
 *     el.setWidth(100, opt);
 *
 * The Element animation object being used for the animation will be set on the options object
 * as "anim", which allows you to stop or manipulate the animation. Here is an example:
 *
 *     // using the "anim" property to get the Anim object
 *     if(opt.anim.isAnimated()){
 *         opt.anim.stop();
 *     }
 */
Ext.define('Ext.dom.Element', function(Element) {
    var WIN = window,
        DOC = document,
        docEl = DOC.documentElement,
        WIN_TOP = WIN.top,
        EMPTY = [],
        elementIdCounter, windowId, documentId,
        WIDTH = 'width',
        HEIGHT = 'height',
        MIN_WIDTH = 'min-width',
        MIN_HEIGHT = 'min-height',
        MAX_WIDTH = 'max-width',
        MAX_HEIGHT = 'max-height',
        TOP = 'top',
        RIGHT = 'right',
        BOTTOM = 'bottom',
        LEFT = 'left',
        VISIBILITY = 'visibility',
        HIDDEN = 'hidden',
        DISPLAY = "display",
        NONE = "none",
        ZINDEX = "z-index",
        POSITION = "position",
        RELATIVE = "relative",
        STATIC = "static",
        wordsRe = /\w/g,
        spacesRe = /\s+/,
        classNameSplitRegex = /[\s]+/,
        transparentRe = /^(?:transparent|(?:rgba[(](?:\s*\d+\s*[,]){3}\s*0\s*[)]))$/i,
        endsQuestionRe = /\?$/,
        topRe = /top/i,
        empty = {},
        borders = {
            t: 'border-top-width',
            r: 'border-right-width',
            b: 'border-bottom-width',
            l: 'border-left-width'
        },
        paddings = {
            t: 'padding-top',
            r: 'padding-right',
            b: 'padding-bottom',
            l: 'padding-left'
        },
        margins = {
            t: 'margin-top',
            r: 'margin-right',
            b: 'margin-bottom',
            l: 'margin-left'
        },
        selectDir = {
            b: 'backward',
            back: 'backward',
            f: 'forward'
        },
        paddingsTLRB = [
            paddings.l,
            paddings.r,
            paddings.t,
            paddings.b
        ],
        bordersTLRB = [
            borders.l,
            borders.r,
            borders.t,
            borders.b
        ],
        numberRe = /\d+$/,
        unitRe = /\d+(px|r?em|%|vh|vw|vmin|vmax|en|ch|ex|pt|in|cm|mm|pc)$/i,
        defaultUnit = 'px',
        msRe = /^-ms-/,
        camelRe = /(-[a-z])/gi,
        /* eslint-disable-next-line no-useless-escape */
        cssRe = /([a-z0-9\-]+)\s*:\s*([^;\s]+(?:\s*[^;\s]+)*);?/gi,
        pxRe = /^\d+(?:\.\d*)?px$/i,
        relativeUnitRe = /(%|r?em|auto|vh|vw|vmin|vmax|ch|ex)$/i,
        propertyCache = {},
        ORIGINALDISPLAY = 'originalDisplay',
        camelReplaceFn = function(m, a) {
            return a.charAt(1).toUpperCase();
        },
        clearData = function(node, deep) {
            var childNodes, i, len;
            // Only Element nodes may have _extData and child nodes to clear.
            // IE8 throws an error attempting to set expandos on non-Element nodes.
            if (node.nodeType === 1) {
                node._extData = null;
                if (deep) {
                    childNodes = node.childNodes;
                    for (i = 0 , len = childNodes.length; i < len; ++i) {
                        clearData(childNodes[i], deep);
                    }
                }
            }
        },
        toFloat = function(v) {
            return parseFloat(v) || 0;
        },
        opacityCls = Ext.baseCSSPrefix + 'hidden-opacity',
        visibilityCls = Ext.baseCSSPrefix + 'hidden-visibility',
        displayCls = Ext.baseCSSPrefix + 'hidden-display',
        offsetsCls = Ext.baseCSSPrefix + 'hidden-offsets',
        clipCls = Ext.baseCSSPrefix + 'hidden-clip',
        lastFocusChange = 0,
        lastKeyboardClose = 0,
        editableHasFocus = false,
        isVirtualKeyboardOpen = false,
        inputTypeSelectionSupported = /text|password|search|tel|url/i,
        visFly, scrollFly, caFly, wrapFly, grannyFly, activeElFly;
    // We use element ID counter to prevent assigning the same id to top and nested
    // window and document objects when Ext is running in <iframe>.
    // Cross-origin access might throw an exception, in which case we can't
    // reference top window. In IE8 simply getting a property does not throw
    // but trying to set it does.
    try {
        elementIdCounter = WIN_TOP.__elementIdCounter__;
        WIN_TOP.__elementIdCounter__ = elementIdCounter;
    } catch (e) {
        WIN_TOP = WIN;
    }
    WIN_TOP.__elementIdCounter__ = elementIdCounter = (WIN_TOP.__elementIdCounter__ || 0) + 1;
    windowId = 'ext-window-' + elementIdCounter;
    documentId = 'ext-document-' + elementIdCounter;
    if (Object.freeze) {
        Object.freeze(EMPTY);
    }
    return {
        alternateClassName: [
            'Ext.Element'
        ],
        mixins: [
            'Ext.util.Positionable',
            'Ext.mixin.Observable'
        ],
        requires: [
            'Ext.dom.Shadow',
            'Ext.dom.Shim',
            'Ext.dom.ElementEvent',
            'Ext.event.publisher.Dom',
            'Ext.event.publisher.Gesture',
            'Ext.event.publisher.ElementSize',
            'Ext.event.publisher.ElementPaint'
        ],
        uses: [
            'Ext.dom.Helper',
            'Ext.dom.CompositeElement',
            'Ext.dom.Fly',
            'Ext.dom.TouchAction',
            'Ext.event.publisher.Focus'
        ],
        observableType: 'element',
        isElement: true,
        skipGarbageCollection: true,
        $applyConfigs: true,
        identifiablePrefix: 'ext-element-',
        _selectDir: selectDir,
        styleHooks: {
            transform: {
                set: function(dom, value, el) {
                    var result = '',
                        prop;
                    if (typeof value !== 'string') {
                        for (prop in value) {
                            if (result) {
                                result += ' ';
                            }
                            if (prop.indexOf('translate') === 0) {
                                result += prop + '(' + Element.addUnits(value[prop], 'px') + ')';
                            } else {
                                result += prop + '(' + value[prop] + ')';
                            }
                        }
                        value = result;
                    }
                    dom.style.transform = value;
                }
            }
        },
        validIdRe: Ext.validIdRe,
        blockedEvents: Ext.supports.EmulatedMouseOver ? {
            // mobile safari emulates a mouseover event on clickable elements such as
            // anchors. This event is useless because it runs after touchend. We block
            // this event to prevent mouseover handlers from running after tap events. It
            // is up to the individual component to determine if it has an analog for
            // mouseover, and implement the appropriate event handlers.
            mouseover: 1
        } : {},
        longpressEvents: {
            longpress: 1,
            taphold: 1
        },
        /**
         * @property {Ext.Component} component
         * A reference to the `Component` that owns this element. This is `null` if there
         * is no direct owner.
         */
        //  Mouse events
        /**
         * @event click
         * Fires when a mouse click is detected within the element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event contextmenu
         * Fires when a right click is detected within the element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event dblclick
         * Fires when a mouse double click is detected within the element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event mousedown
         * Fires when a mousedown is detected within the element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event mouseup
         * Fires when a mouseup is detected within the element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event mouseover
         * Fires when a mouseover is detected within the element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event mousemove
         * Fires when a mousemove is detected with the element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event mouseout
         * Fires when a mouseout is detected with the element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event mouseenter
         * Fires when the mouse enters the element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event mouseleave
         * Fires when the mouse leaves the element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        //  Keyboard events
        /**
         * @event keypress
         * Fires when a keypress is detected within the element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event keydown
         * Fires when a keydown is detected within the element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event keyup
         * Fires when a keyup is detected within the element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        //  HTML frame/object events
        /**
         * @event load
         * Fires when the user agent finishes loading all content within the element. Only supported
         * by window, frames, objects and images.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event unload
         * Fires when the user agent removes all content from a window or frame. For elements, it
         * fires when the target element or any of its content has been removed.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event abort
         * Fires when an object/image is stopped from loading before completely loaded.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event error
         * Fires when an object/image/frame cannot be loaded properly.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event painted
         * Fires whenever this Element actually becomes visible (painted) on the screen. This is
         * useful when you need to perform 'read' operations on the DOM element, i.e: calculating
         * natural sizes and positioning.
         *
         * __Note:__ This event is not available to be used with event delegation. Instead `painted`
         * only fires if you explicitly add at least one listener to it, for performance reasons.
         *
         * @param {Ext.dom.Element} this The component instance.
         */
        /**
         * @event resize
         * Important note: For the best performance on mobile devices, use this only when you
         * absolutely need to monitor a Element's size.
         *
         * __Note:__ This event is not available to be used with event delegation. Instead `resize`
         * only fires if you explicitly add at least one listener to it, for performance reasons.
         *
         * @param {Ext.dom.Element} this The component instance.
         * @param {Object} info The element's new size parameters.
         */
        /**
         * @event scroll
         * Fires when a document view is scrolled.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        //  Form events
        /**
         * @event select
         * Fires when a user selects some text in a text field, including input and textarea.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event change
         * Fires when a control loses the input focus and its value has been modified since gaining
         * focus.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event submit
         * Fires when a form is submitted.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event reset
         * Fires when a form is reset.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event focus
         * Fires when an element receives focus either via the pointing device or by tab navigation.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event blur
         * Fires when an element loses focus either via the pointing device or by tabbing
         * navigation.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event focusmove
         * Fires when focus is moved *within* an element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {Ext.dom.Element} e.target The {@link Ext.dom.Element} element which *recieved*
         * focus.
         * @param {Ext.dom.Element} e.relatedTarget The {@link Ext.dom.Element} element which *lost*
         * focus.
         * @param {HTMLElement} t The target of the event.
         */
        //  User Interface events
        /**
         * @event DOMFocusIn
         * Where supported. Similar to HTML focus event, but can be applied to any focusable
         * element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event DOMFocusOut
         * Where supported. Similar to HTML blur event, but can be applied to any focusable element.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event DOMActivate
         * Where supported. Fires when an element is activated, for instance, through a mouse click
         * or a keypress.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        //  DOM Mutation events
        /**
         * @event DOMSubtreeModified
         * Where supported. Fires when the subtree is modified.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event DOMNodeInserted
         * Where supported. Fires when a node has been added as a child of another node.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event DOMNodeRemoved
         * Where supported. Fires when a descendant node of the element is removed.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event DOMNodeRemovedFromDocument
         * Where supported. Fires when a node is being removed from a document.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event DOMNodeInsertedIntoDocument
         * Where supported. Fires when a node is being inserted into a document.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event DOMAttrModified
         * Where supported. Fires when an attribute has been modified.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * @event DOMCharacterDataModified
         * Where supported. Fires when the character data has been modified.
         * @param {Ext.event.Event} e The {@link Ext.event.Event} encapsulating the DOM event.
         * @param {HTMLElement} t The target of the event.
         */
        /**
         * Creates new Element directly by passing an id or the HTMLElement.  This
         * constructor should not be called directly.  Always use {@link Ext#get Ext.get()}
         * or {@link Ext#fly Ext#fly()} instead.
         *
         * In older versions of Ext JS and Sencha Touch this constructor checked to see if
         * there was already an instance of this element in the cache and if so, returned
         * the same instance. As of version 5 this behavior has been removed in order to
         * avoid a redundant cache lookup since the most common path is for the Element
         * constructor to be called from {@link Ext#get Ext.get()}, which has already
         * checked for a cache entry.
         *
         * Correct way of creating a new Ext.dom.Element (or retrieving it from the cache):
         *
         *     var el = Ext.get('foo'); // by id
         *
         *     var el = Ext.get(document.getElementById('foo')); // by DOM reference
         *
         * Incorrect way of creating a new Ext.dom.Element
         *
         *     var el = new Ext.dom.Element('foo');
         *
         * For quick and easy access to Ext.dom.Element methods use a flyweight:
         *
         *     Ext.fly('foo').addCls('foo-hovered');
         *
         * This simply attaches the DOM node with id='foo' to the global flyweight Element
         * instance to avoid allocating an extra Ext.dom.Element instance.  If, however,
         * the Element instance has already been cached by a previous call to Ext.get(),
         * then Ext.fly() will return the cached Element instance.  For more info see
         * {@link Ext#fly}.
         *
         * @param {String/HTMLElement} dom
         * @private
         */
        constructor: function(dom) {
            var me = this,
                id;
            if (typeof dom === 'string') {
                dom = DOC.getElementById(dom);
            }
            if (!dom) {
                Ext.raise("Invalid domNode reference or an id of an existing domNode: " + dom);
                return null;
            }
            if (Ext.cache[dom.id]) {
                Ext.raise("Element cache already contains an entry for id '" + dom.id + "'.  Use Ext.get() to create or retrieve Element instances.");
            }
            /**
             * The DOM element
             * @property dom
             * @type HTMLElement
             */
            me.dom = dom;
            if (!(id = dom.id)) {
                dom.id = id = me.generateAutoId();
            }
            me.id = id;
            // Uncomment this when debugging orphaned Elements
            // if (id === 'ext-element-5') debugger;
            if (!me.validIdRe.test(me.id)) {
                Ext.raise('Invalid Element "id": "' + me.id + '"');
            }
            // set an "el" property that references "this".  This allows
            // Ext.util.Positionable methods to operate on this.el.dom since it
            // gets mixed into both Element and Component
            me.el = me;
            Ext.cache[id] = me;
            me.longpressListenerCount = 0;
            me.mixins.observable.constructor.call(me);
        },
        inheritableStatics: {
            /**
             * @property cache
             * @private
             * @static
             * @inheritable
             */
            cache: Ext.cache = {},
            /**
             * @property editableSelector
             * @static
             * @private
             * @inheritable
             */
            editableSelector: 'input,textarea,[contenteditable="true"]',
            /**
             * @property {Number} VISIBILITY
             * Visibility mode constant for use with {@link Ext.dom.Element#setVisibilityMode}.
             * Use the CSS 'visibility' property to hide the element.
             *
             * Note that in this mode, {@link Ext.dom.Element#isVisible isVisible} may return true
             * for an element even though it actually has a parent element that is hidden. For this
             * reason, and in most cases, using the {@link #OFFSETS} mode is a better choice.
             * @static
             * @inheritable
             */
            VISIBILITY: 1,
            /**
             * @property {Number} DISPLAY
             * Visibility mode constant for use with {@link Ext.dom.Element#setVisibilityMode}.
             * Use the CSS 'display' property to hide the element.
             * @static
             * @inheritable
             */
            DISPLAY: 2,
            /**
             * @property {Number} OFFSETS
             * Visibility mode constant for use with {@link Ext.dom.Element#setVisibilityMode}.
             * Use CSS absolute positioning and top/left offsets to hide the element.
             * @static
             * @inheritable
             */
            OFFSETS: 3,
            /**
             * @property {Number} CLIP
             * Visibility mode constant for use with {@link Ext.dom.Element#setVisibilityMode}.
             * Use CSS `clip` property to reduce element's dimensions to 0px by 0px, effectively
             * making it hidden while not being truly invisible. This is useful when an element
             * needs to be published to the Assistive Technologies such as screen readers.
             * @static
             * @inheritable
             */
            CLIP: 4,
            /**
             * @property {Number} OPACITY
             * Visibility mode constant for use with {@link Ext.dom.Element#setVisibilityMode}.
             * Use CSS `opacity` property to reduce element's opacity to 0
             * @static
             * @inheritable
             */
            OPACITY: 5,
            /**
             * @property minKeyboardHeight
             * @static
             * @inheritable
             * @private
             * This property indicates a minimum threshold of vertical resize movement for
             * virtual keyboard detection.
             *
             * On some mobile browsers the framework needs to keep track of whether window
             * resize events were triggered by the opening or closing of a virtual keyboard
             * so that it can prevent unnecessary re-layout of the viewport.  It does this
             * by detecting resize events in the horizontal direction that occur immediately
             * after an editable element is focused or blurred.
             */
            minKeyboardHeight: 100,
            unitRe: unitRe,
            /**
             * @property {Boolean} useDelegatedEvents
             * @private
             * @static
             * @inheritable
             * True to globally disable the delegated event system.  The results of
             * setting this to false are unpredictable since the Gesture publisher relies
             * on delegated events in order to work correctly.  Disabling delegated events
             * may cause Gestures to function incorrectly or to stop working completely.
             * Use at your own risk!
             */
            useDelegatedEvents: true,
            /**
             * @property {Object} validNodeTypes
             * @private
             * @static
             * @inheritable
             * The list of valid nodeTypes that are allowed to be wrapped
             */
            validNodeTypes: {
                1: 1,
                // ELEMENT_NODE
                9: 1
            },
            // DOCUMENT_NODE
            namespaceURIs: {
                html: 'http://www.w3.org/1999/xhtml',
                svg: 'http://www.w3.org/2000/svg'
            },
            selectableCls: Ext.baseCSSPrefix + 'selectable',
            unselectableCls: Ext.baseCSSPrefix + 'unselectable',
            /**
             * Determines the maximum size for all ripples
             */
            maxRippleDiameter: 75,
            /**
             * Test if size has a unit, otherwise appends the passed unit string, or the default
             * for this Element.
             * @param {Object} size The size to set.
             * @param {String} units The units to append to a numeric size value.
             * @return {String}
             * @private
             * @static
             * @inheritable
             */
            addUnits: function(size, units) {
                // Most common case first: Size is set to a number
                if (typeof size === 'number') {
                    return size + (units || defaultUnit);
                }
                // Values which mean "auto"
                // - ""
                // - "auto"
                // - undefined
                // - null
                if (size === "" || size === "auto" || size == null) {
                    return size || '';
                }
                // less common use case: number formatted as a string.  save this case until
                // last to avoid regex execution if possible.
                if (numberRe.test(size)) {
                    return size + (units || defaultUnit);
                }
                // Warn if it's not a valid CSS measurement
                if (!unitRe.test(size)) {
                    // Don't warn about calc() expressions
                    if (!(Ext.isString(size) && size.indexOf('calc') === 0)) {
                        Ext.Logger.warn("Warning, size detected (" + size + ") not a valid property value on Element.addUnits.");
                    }
                    return size || '';
                }
                return size;
            },
            /**
             * @private
             * Create method to add support for a DomHelper config. Creates
             * and appends elements/children using document.createElement/appendChild.
             * This method is used by the modern toolkit for a significant performance gain
             * in webkit browsers as opposed to using DomQuery which generates HTML
             * markup and sets it as innerHTML.
             *
             * However, the createElement/appendChild
             * method of creating elements is significantly slower in all versions of IE
             * at the time of this writing (6 - 11), so classic toolkit should not use this method,
             * but should instead use DomHelper methods, or Element methods that use
             * DomHelper under the hood (e.g. createChild).
             * see https:*fiddle.sencha.com/#fiddle/tj
             *
             * @static
             * @inheritable
             */
            create: function(attributes, domNode, namespace) {
                var me = this,
                    classes, element, elementStyle, tag, value, name, i, ln, tmp, ns;
                attributes = attributes || {};
                if (attributes.isElement) {
                    return domNode ? attributes.dom : attributes;
                } else if ('nodeType' in attributes) {
                    return domNode ? attributes : Ext.get(attributes);
                }
                if (typeof attributes === 'string') {
                    return DOC.createTextNode(attributes);
                }
                tag = attributes.tag;
                if (!tag) {
                    tag = 'div';
                }
                ns = attributes.namespace || namespace;
                if (ns) {
                    element = DOC.createElementNS(me.namespaceURIs[ns] || ns, tag);
                } else {
                    element = DOC.createElement(tag);
                }
                elementStyle = element.style;
                for (name in attributes) {
                    if (name !== 'tag' && name !== 'namespace') {
                        value = attributes[name];
                        switch (name) {
                            case 'style':
                                if (typeof value === 'string') {
                                    element.setAttribute(name, value);
                                } else {
                                    for (i in value) {
                                        elementStyle[i] = value[i];
                                    }
                                };
                                break;
                            case 'className':
                            case 'cls':
                                tmp = value.split(spacesRe);
                                classes = classes ? classes.concat(tmp) : tmp;
                                break;
                            case 'classList':
                                classes = classes ? classes.concat(value) : value;
                                break;
                            case 'text':
                                element.textContent = value;
                                break;
                            case 'html':
                                element.innerHTML = value;
                                break;
                            case 'hidden':
                                if (classes) {
                                    classes.push(displayCls);
                                } else {
                                    classes = [
                                        displayCls
                                    ];
                                };
                                break;
                            case 'children':
                                if (value != null) {
                                    for (i = 0 , ln = value.length; i < ln; i++) {
                                        element.appendChild(me.create(value[i], true, ns));
                                    }
                                };
                                break;
                            default:
                                if (value != null) {
                                    // skip null or undefined values
                                    element.setAttribute(name, value);
                                };
                        }
                    }
                }
                if (classes) {
                    element.className = classes.join(' ');
                }
                if (domNode) {
                    return element;
                } else {
                    return me.get(element);
                }
            },
            /**
             * @method fly
             * @inheritdoc Ext#method-fly
             * @inheritable
             * @static
             */
            fly: function(dom, named) {
                return Ext.fly(dom, named);
            },
            /**
             * Returns the top Element that is located at the passed coordinates in the current
             * viewport.
             * @param {Number} x The x coordinate
             * @param {Number} y The y coordinate
             * @param {Boolean} [asDom=false] `true` to return a DOM element.
             * @return {Ext.dom.Element/HTMLElement} The found element.
             * @static
             * @inheritable
             * @method
             */
            fromPoint: (function() {
                // IE has a weird bug where elementFromPoint can fail on the first call when inside
                // an iframe. It seems to happen more consistently on older IE, but sometimes crops
                // up even in IE11. This plays havoc especially while running tests.
                var elementFromPointBug;
                if (Ext.isIE || Ext.isEdge) {
                    try {
                        elementFromPointBug = window.self !== window.top;
                    } catch (e) {
                        elementFromPointBug = true;
                    }
                }
                return function(x, y, asDom) {
                    var el = null;
                    el = DOC.elementFromPoint(x, y);
                    if (!el && elementFromPointBug) {
                        el = DOC.elementFromPoint(x, y);
                    }
                    return asDom ? el : Ext.get(el);
                };
            })(),
            /**
             * Returns the top Element that is located at the passed coordinates taking into account
             * the scroll position of the document.
             * @static
             * @inheritable
             * @param {Number} x The x coordinate
             * @param {Number} y The y coordinate
             * @param {Boolean} [asDom=false] `true` to return a DOM element.
             * @return {Ext.dom.Element/HTMLElement} The found element.
             *
             * @since 6.2.0
             */
            fromPagePoint: function(x, y, asDom) {
                var scroll = Ext.getDoc().getScroll();
                return Element.fromPoint(x - scroll.left, y - scroll.top, asDom);
            },
            /**
             * Retrieves Ext.dom.Element objects. {@link Ext#get} is alias for
             * {@link Ext.dom.Element#get}.
             *
             * **This method does not retrieve {@link Ext.Component Component}s.** This method
             * retrieves Ext.dom.Element objects which encapsulate DOM elements. To retrieve
             * a Component by its ID, use {@link Ext.ComponentManager#get}.
             *
             * When passing an id, it should not include the `#` character that is used for a css
             * selector.
             *
             *     // For an element with id 'foo'
             *     Ext.get('foo'); // Correct
             *     Ext.get('#foo'); // Incorrect
             *
             * Uses simple caching to consistently return the same object. Automatically fixes
             * if an object was recreated with the same id via AJAX or DOM.
             *
             * @param {String/HTMLElement/Ext.dom.Element} el The `id` of the node, a DOM Node
             * or an existing Element.
             * @return {Ext.dom.Element} The Element object (or `null` if no matching element
             * was found).
             * @static
             * @inheritable
             */
            get: function(el) {
                var me = this,
                    cache = Ext.cache,
                    nodeType, dom, id, entry, isDoc, isWin, isValidNodeType;
                if (!el) {
                    return null;
                }
                function warnDuplicate(id) {
                    Ext.raise("DOM element with id " + id + " in Element cache is not the same as element in the DOM. " + "Make sure to clean up Element instances using destroy()");
                }
                // Ext.get(flyweight) must return an Element instance, not the flyweight
                if (el.isFly) {
                    el = el.dom;
                }
                if (typeof el === 'string') {
                    id = el;
                    if (cache.hasOwnProperty(id)) {
                        entry = cache[id];
                        if (entry.skipGarbageCollection || !Ext.isGarbage(entry.dom)) {
                            // eslint-disable-next-line max-len
                            dom = Ext.getElementById ? Ext.getElementById(id) : DOC.getElementById(id);
                            if (dom && (dom !== entry.dom)) {
                                warnDuplicate(id);
                            }
                            return entry;
                        } else {
                            entry.destroy();
                        }
                    }
                    if (id === windowId) {
                        return Element.get(WIN);
                    } else if (id === documentId) {
                        return Element.get(DOC);
                    }
                    // using Ext.getElementById() allows us to check the detached
                    // body in addition to the body (Ext JS only).
                    dom = Ext.getElementById ? Ext.getElementById(id) : DOC.getElementById(id);
                    if (dom) {
                        return new Element(dom);
                    }
                }
                nodeType = el.nodeType;
                if (nodeType) {
                    isDoc = (nodeType === 9);
                    isValidNodeType = me.validNodeTypes[nodeType];
                } else {
                    // if an object has a window property that refers to itself we can
                    // reasonably assume that it is a window object.
                    // have to use == instead of === for IE8
                    isWin = (el.window == el);
                }
                // eslint-disable-line eqeqeq
                // check if we have a valid node type or if the el is a window object before
                // proceeding. This allows elements, document fragments, and document/window
                // objects (even those inside iframes) to be wrapped.
                if (isValidNodeType || isWin) {
                    id = el.id;
                    if (el === DOC) {
                        el.id = id = documentId;
                    }
                    // Must use == here, otherwise IE fails to recognize the window
                    else if (el == WIN) {
                        // eslint-disable-line eqeqeq
                        el.id = id = windowId;
                    }
                    if (cache.hasOwnProperty(id)) {
                        entry = cache[id];
                        // eslint-disable-next-line max-len
                        if (entry.skipGarbageCollection || el === entry.dom || !Ext.isGarbage(entry.dom)) {
                            if (el !== entry.dom) {
                                warnDuplicate(id);
                            }
                            return entry;
                        } else {
                            entry.destroy();
                        }
                    }
                    el = new Element(el);
                    if (isWin || isDoc) {
                        // document and window objects can never be garbage
                        el.skipGarbageCollection = true;
                    }
                    return el;
                }
                if (el.isElement) {
                    return el;
                }
                if (el.isComposite) {
                    return el;
                }
                // Test for iterable. Allow the resulting Composite to be based upon an Array
                // or HtmlCollection of nodes.
                if (Ext.isIterable(el)) {
                    return me.select(el);
                }
                return null;
            },
            /**
             * Returns the active element in the DOM. If the browser supports activeElement
             * on the document, this is returned. If not, the focus is tracked and the active
             * element is maintained internally.
             * @static
             * @inheritable
             *
             * @param {Boolean} asElement Return Ext.Element instance instead of DOM node.
             *
             * @return {HTMLElement} The active (focused) element in the document.
             */
            getActiveElement: function(asElement) {
                var active = DOC.activeElement;
                // The activeElement can be null, however there also appears to be a very odd
                // and inconsistent bug in IE where the activeElement is simply an empty object
                // literal. Test if the returned active element has focus, if not, we've hit the bug
                // so just default back to the document body.
                if (!active || !active.focus) {
                    active = DOC.body;
                }
                return asElement ? Ext.get(active) : active;
            },
            /**
             * Retrieves the document height
             * @static
             * @inheritable
             * @return {Number} documentHeight
             */
            getDocumentHeight: function() {
                // eslint-disable-next-line max-len
                return Math.max(!Ext.isStrict ? DOC.body.scrollHeight : docEl.scrollHeight, this.getViewportHeight());
            },
            /**
             * Retrieves the document width
             * @static
             * @inheritable
             * @return {Number} documentWidth
             */
            getDocumentWidth: function() {
                // eslint-disable-next-line max-len
                return Math.max(!Ext.isStrict ? DOC.body.scrollWidth : docEl.scrollWidth, this.getViewportWidth());
            },
            /**
             * Retrieves the current orientation of the window. This is calculated by
             * determining if the height is greater than the width.
             * @static
             * @inheritable
             * @return {String} Orientation of window: 'portrait' or 'landscape'
             */
            getOrientation: function() {
                if (Ext.supports.OrientationChange) {
                    /* eslint-disable-next-line eqeqeq */
                    return (WIN.orientation == 0) ? 'portrait' : 'landscape';
                }
                return (WIN.innerHeight > WIN.innerWidth) ? 'portrait' : 'landscape';
            },
            /**
             * Retrieves the viewport height of the window.
             * @static
             * @inheritable
             * @return {Number} viewportHeight
             */
            getViewportHeight: function() {
                var viewportHeight = Element._viewportHeight;
                if (Ext.isIE9m) {
                    return DOC.documentElement.clientHeight;
                }
                return (viewportHeight != null) ? viewportHeight : docEl.clientHeight;
            },
            /**
             * Retrieves the viewport width of the window.
             * @static
             * @inheritable
             * @return {Number} viewportWidth
             */
            getViewportWidth: function() {
                var viewportWidth = Element._viewportWidth;
                if (Ext.isIE9m) {
                    return DOC.documentElement.clientWidth;
                }
                return (viewportWidth != null) ? viewportWidth : docEl.clientWidth;
            },
            /**
             * Returns the current zoom level of the viewport as a ratio of page pixels to
             * screen pixels.
             * @private
             * @static
             * @return {Number}
             */
            getViewportScale: function() {
                // on desktop devices, the devicePixel ratio gives us the level of zoom that
                // the user specified using ctrl +/- and or by selecting a zoom level from
                // the menu.
                // On android/iOS devicePixel ratio is a fixed number that represents the
                // screen pixel density (e.g. always "2" on apple retina devices)
                // WIN_TOP is guarded against cross-frame access in the closure above
                var top = WIN_TOP;
                return ((Ext.isiOS || Ext.isAndroid) ? 1 : (top.devicePixelRatio || // modern browsers
                top.screen.deviceXDPI / top.screen.logicalXDPI)) * // IE10m
                this.getViewportTouchScale();
            },
            /**
             * On touch-screen devices there may be an additional level of zooming
             * that occurs when the user performs a pinch or double-tap to zoom
             * gesture.  This is separate from and in addition to the
             * devicePixelRatio.  We can detect it by comparing the width
             * of the documentElement to window.innerWidth
             * @private
             */
            getViewportTouchScale: function(forceRead) {
                var scale = 1,
                    // WIN_TOP is guarded against cross-frame access in the closure above
                    top = WIN_TOP,
                    cachedScale;
                if (!forceRead) {
                    cachedScale = this._viewportTouchScale;
                    if (cachedScale) {
                        return cachedScale;
                    }
                }
                if (Ext.isIE10p || Ext.isEdge || Ext.isiOS) {
                    scale = docEl.offsetWidth / WIN.innerWidth;
                } else if (Ext.isChromeMobile) {
                    scale = top.outerWidth / top.innerWidth;
                }
                return scale;
            },
            /**
             * Retrieves the viewport size of the window.
             * @static
             * @inheritable
             * @return {Object} object containing width and height properties
             */
            getViewSize: function() {
                return {
                    width: Element.getViewportWidth(),
                    height: Element.getViewportHeight()
                };
            },
            /**
             * Checks if the passed size has a css unit attached.
             * @param {String} size The size.
             * @return {Boolean} `true` if the size has a css unit.
             *
             * @since 6.2.1
             */
            hasUnit: function(size) {
                return !!(size && unitRe.test(size));
            },
            /**
             * Checks if the passed css unit is a relative unit. This includes:
             * - `auto`
             * - `%`
             * - `em`
             * - `rem`
             * - `auto`
             * - `vh`
             * - `vw`
             * - `vmin`
             * - `vmax`
             * - `ex
             * - `ch`
             * @param {String} size The css unit and value.
             * @return {Boolean} `true` if the value is relative.
             *
             * @since 6.2.0
             */
            isRelativeUnit: function(size) {
                return !size || relativeUnitRe.test(size);
            },
            /**
             * Mask iframes when shim is true. See {@link Ext.util.Floating#shim}.
             * @private
             */
            maskIframes: function() {
                var iframes = document.getElementsByTagName('iframe'),
                    fly = new Ext.dom.Fly();
                Ext.each(iframes, function(iframe) {
                    var myMask;
                    myMask = fly.attach(iframe.parentNode).mask();
                    myMask.setStyle('background-color', 'transparent');
                });
            },
            /**
             * Normalizes CSS property keys from dash delimited to camel case JavaScript Syntax.
             * For example:
             *
             * - border-width -> borderWidth
             * - padding-top -> paddingTop
             *
             * @static
             * @inheritable
             * @param {String} prop The property to normalize
             * @return {String} The normalized string
             */
            normalize: function(prop) {
                // For '-ms-foo' we need msFoo
                // eslint-disable-next-line max-len
                return propertyCache[prop] || (propertyCache[prop] = prop.replace(msRe, 'ms-').replace(camelRe, camelReplaceFn));
            },
            /**
             * @private
             * @static
             * @inheritable
             */
            _onWindowFocusChange: function(e) {
                // Tracks the timestamp of focus entering or leaving an editable element
                // so that we can compare this timestamp to the time of the next window
                // resize for the purpose of determining if the virtual keyboard is displayed
                // see _onWindowResize for more details
                if (Ext.fly(e.target).is(Element.editableSelector)) {
                    lastFocusChange = new Date();
                    editableHasFocus = (e.type === 'focusin' || e.type === 'pointerup');
                }
            },
            /**
             * @private
             * @static
             * @inheritable
             */
            _onWindowResize: function() {
                var documentWidth = docEl.clientWidth,
                    documentHeight = docEl.clientHeight,
                    now = new Date(),
                    threshold = 1000,
                    deltaX, deltaY;
                deltaX = documentWidth - Element._documentWidth;
                deltaY = documentHeight - Element._documentHeight;
                Element._documentWidth = documentWidth;
                Element._documentHeight = documentHeight;
                // If the focus entered or left an editable element within a brief threshold
                // of time, then this resize event MAY be due to a virtual keyboard being
                // shown or hidden.  Let's do some additional checking to find out.
                if (((now - lastFocusChange) < threshold) || ((now - lastKeyboardClose) < threshold)) {
                    // eslint-disable-line max-len
                    // If the resize is ONLY in the vertical direction, and an editable
                    // element has the focus, and the vertical movement was significant,
                    // we can be reasonably certain that the resize event was due to
                    // a virtual keyboard being opened.
                    if (deltaX === 0 && (editableHasFocus && (deltaY <= -Element.minKeyboardHeight))) {
                        // eslint-disable-line max-len
                        isVirtualKeyboardOpen = true;
                        return;
                    }
                }
                if (isVirtualKeyboardOpen && (deltaX === 0) && (deltaY >= Element.minKeyboardHeight)) {
                    // eslint-disable-line max-len
                    isVirtualKeyboardOpen = false;
                    // when windows tablets are rotated while keyboard is open, the keyboard closes
                    // and then immediately reopens.  Track the timestamp of the last keyboard
                    // close so that we can detect a successive resize event that might indicate
                    // reopening
                    lastKeyboardClose = new Date();
                }
                if (isVirtualKeyboardOpen) {
                    return;
                }
                // These cached variables are used by getViewportWidth and getViewportHeight
                // They do not get updated if we returned early due to detecting  that the
                // resize event was triggered by virtual keyboard.
                Element._viewportWidth = documentWidth;
                Element._viewportHeight = documentHeight;
            },
            /**
             * Parses a number or string representing margin sizes into an object. Supports
             * CSS-style margin declarations (e.g. 10, "10", "10 10", "10 10 10" and "10 10 10 10"
             * are all valid options and would return the same result)
             * @static
             * @inheritable
             * @param {Number/String} box The encoded margins
             * @return {Object} An object with margin sizes for top, right, bottom and left
             * containing the unit
             */
            parseBox: function(box) {
                var type, parts, ln;
                box = box || 0;
                type = typeof box;
                if (type === 'number') {
                    return {
                        top: box,
                        right: box,
                        bottom: box,
                        left: box
                    };
                } else if (type !== 'string') {
                    // If not a number or a string, assume we've been given a box config.
                    return box;
                }
                parts = box.split(' ');
                ln = parts.length;
                if (ln === 1) {
                    parts[1] = parts[2] = parts[3] = parts[0];
                } else if (ln === 2) {
                    parts[2] = parts[0];
                    parts[3] = parts[1];
                } else if (ln === 3) {
                    parts[3] = parts[1];
                }
                return {
                    top: parseFloat(parts[0]) || 0,
                    right: parseFloat(parts[1]) || 0,
                    bottom: parseFloat(parts[2]) || 0,
                    left: parseFloat(parts[3]) || 0
                };
            },
            /**
             * Converts a CSS string into an object with a property for each style.
             *
             * The sample code below would return an object with 2 properties, one
             * for background-color and one for color.
             *
             *     var css = 'background-color: red; color: blue;';
             *     console.log(Ext.dom.Element.parseStyles(css));
             *
             * @static
             * @inheritable
             * @param {String} styles A CSS string
             * @return {Object} styles
             */
            parseStyles: function(styles) {
                var out = {},
                    matches;
                if (styles) {
                    // Since we're using the g flag on the regex, we need to set the lastIndex.
                    // This automatically happens on some implementations, but not others, see:
                    // http://stackoverflow.com/questions/2645273/javascript-regular-expression-literal-persists-between-function-calls
                    // http://blog.stevenlevithan.com/archives/fixing-javascript-regexp
                    cssRe.lastIndex = 0;
                    while ((matches = cssRe.exec(styles))) {
                        out[matches[1]] = matches[2] || '';
                    }
                }
                return out;
            },
            /**
             * Selects elements based on the passed CSS selector to enable
             * {@link Ext.dom.Element Element} methods to be applied to many related
             * elements in one statement through the returned
             * {@link Ext.dom.CompositeElementLite CompositeElementLite} object.
             * @static
             * @inheritable
             * @param {String/HTMLElement[]} selector The CSS selector or an array of
             * elements
             * @param {Boolean} [composite=false] Return a CompositeElement as opposed to
             * a CompositeElementLite. Defaults to false.
             * @param {HTMLElement/String} [root] The root element of the query or id of
             * the root
             * @return {Ext.dom.CompositeElementLite/Ext.dom.CompositeElement}
             */
            select: function(selector, composite, root) {
                return Ext.fly(root || DOC).select(selector, composite);
            },
            /**
             * Selects child nodes of a given root based on the passed CSS selector.
             * @static
             * @inheritable
             * @param {String} selector The CSS selector.
             * @param {Boolean} [asDom=true] `false` to return an array of Ext.dom.Element
             * @param {HTMLElement/String} [root] The root element of the query or id of
             * the root
             * @return {HTMLElement[]/Ext.dom.Element[]} An Array of elements that match
             * the selector.  If there are no matches, an empty Array is returned.
             */
            query: function(selector, asDom, root) {
                return Ext.fly(root || DOC).query(selector, asDom);
            },
            /**
             * Parses a number or string representing margin sizes into an object. Supports
             * CSS-style margin declarations (e.g. 10, "10", "10 10", "10 10 10" and "10 10 10 10"
             * are all valid options and would return the same result)
             * @static
             * @inheritable
             * @param {Number/String/Object} box The encoded margins, or an object with top, right,
             * @param {String} units The type of units to add
             * @return {String} An string with unitized (px if units is not specified) metrics for
             * top, right, bottom and left
             */
            unitizeBox: function(box, units) {
                var me = this;
                box = me.parseBox(box);
                return me.addUnits(box.top, units) + ' ' + me.addUnits(box.right, units) + ' ' + me.addUnits(box.bottom, units) + ' ' + me.addUnits(box.left, units);
            },
            /**
             * Unmask iframes when shim is true. See {@link Ext.util.Floating#cfg-shim}.
             * @private
             */
            unmaskIframes: function() {
                var iframes = document.getElementsByTagName('iframe'),
                    fly = new Ext.dom.Fly();
                Ext.each(iframes, function(iframe) {
                    fly.attach(iframe.parentNode).unmask();
                });
            },
            /**
             * Serializes a DOM form into a url encoded string
             * @param {Object} form The form
             * @return {String} The url encoded form
             * @static
             * @inheritable
             */
            serializeForm: function(form) {
                var fElements = form.elements || (DOC.forms[form] || Ext.getDom(form)).elements,
                    hasSubmit = false,
                    encoder = encodeURIComponent,
                    data = '',
                    eLen = fElements.length,
                    element, name, type, options, hasValue, e, o, oLen, opt;
                for (e = 0; e < eLen; e++) {
                    element = fElements[e];
                    name = element.name;
                    type = element.type;
                    options = element.options;
                    if (!element.disabled && name) {
                        if (/select-(one|multiple)/i.test(type)) {
                            oLen = options.length;
                            for (o = 0; o < oLen; o++) {
                                opt = options[o];
                                if (opt.selected) {
                                    hasValue = opt.hasAttribute('value');
                                    data += Ext.String.format('{0}={1}&', encoder(name), encoder(hasValue ? opt.value : opt.text));
                                }
                            }
                        }
                        // eslint-disable-line max-len
                        else if (!(/file|undefined|reset|button/i.test(type))) {
                            if (!(/radio|checkbox/i.test(type) && !element.checked) && !(type === 'submit' && hasSubmit)) {
                                // eslint-disable-line max-len
                                data += encoder(name) + '=' + encoder(element.value) + '&';
                                hasSubmit = /submit/i.test(type);
                            }
                        }
                    }
                }
                return data.substr(0, data.length - 1);
            },
            /**
             * Returns the common ancestor of the two passed elements.
             * @static
             * @inheritable
             *
             * @param {Ext.dom.Element/HTMLElement} nodeA
             * @param {Ext.dom.Element/HTMLElement} nodeB
             * @param {Boolean} returnDom Pass `true` to return a DOM element. Otherwise an
             * {@link Ext.dom.Element Element} will be returned.
             * @return {Ext.dom.Element/HTMLElement} The common ancestor.
             */
            getCommonAncestor: function(nodeA, nodeB, returnDom) {
                caFly = caFly || new Ext.dom.Fly();
                caFly.attach(Ext.getDom(nodeA));
                while (!caFly.isAncestor(nodeB)) {
                    if (caFly.dom.parentNode) {
                        caFly.attach(caFly.dom.parentNode);
                    } else // If Any of the nodes in in a detached state, have to use the document.body
                    {
                        caFly.attach(DOC.body);
                        break;
                    }
                }
                return returnDom ? caFly.dom : Ext.get(caFly);
            }
        },
        /**
        * Enable text selection for this element (normalized across browsers)
        * @return {Ext.dom.Element} this
        */
        selectable: function() {
            var me = this;
            // We clear this property for all browsers, not just Opera. This is so that rendering
            // templates don't need to condition on Opera when making elements unselectable.
            me.dom.unselectable = '';
            me.removeCls(Element.unselectableCls);
            me.addCls(Element.selectableCls);
            return me;
        },
        /**
         * Disables text selection for this element (normalized across browsers)
         * @return {Ext.dom.Element} this
         */
        unselectable: function() {
            // The approach used to disable text selection combines CSS, HTML attributes and DOM
            // events. Importantly the strategy is designed to be expressible in markup, so that
            // elements can be rendered unselectable without needing modifications post-render.
            // e.g.:
            //
            // <div class="x-unselectable" unselectable="on"></div>
            //
            // Changes to this method may need to be reflected elsewhere, e.g. ProtoElement.
            var me = this;
            // The unselectable property (or similar) is supported by various browsers but Opera
            // is the only browser that doesn't support any of the other techniques. The problem
            // with it is that it isn't inherited by child elements. Theoretically we could add it
            // to all children but the performance would be terrible. In certain key locations
            // (e.g. panel headers) we add unselectable="on" to extra elements during rendering
            // just for Opera's benefit.
            if (Ext.isOpera) {
                me.dom.unselectable = 'on';
            }
            // In Mozilla and WebKit the CSS properties -moz-user-select and -webkit-user-select
            // prevent a selection originating in an element. These are inherited, which is what
            // we want.
            //
            // In IE we rely on a listener for the selectstart event instead. We don't need to
            // register a listener on the individual element, instead we use a single listener
            // and rely on event propagation to listen for the event at the document level.
            // That listener will walk up the DOM looking for nodes that have either of the classes
            // x-selectable or x-unselectable. This simulates the CSS inheritance approach.
            //
            // IE 10 is expected to support -ms-user-select so the listener may not be required.
            me.removeCls(Element.selectableCls);
            me.addCls(Element.unselectableCls);
            return me;
        },
        // statics
        statics: {
            // This selector will be modified at runtime in the _init() method above
            // to include the elements with saved tabindex in the returned set
            tabbableSelector: Ext.supports.CSS3NegationSelector ? 'a[href],button,iframe,input,select,textarea,[tabindex]:not([tabindex="-1"]),[contenteditable="true"]' : // eslint-disable-line max-len
            'a[href],button,iframe,input,select,textarea,[tabindex],[contenteditable="true"]',
            // Anchor and link tags are special; they are only naturally focusable (and tabbable)
            // if they have href attribute, and tabbabledness is further platform/browser specific.
            // Thus we check it separately in the code.
            naturallyFocusableTags: {
                BUTTON: true,
                IFRAME: true,
                EMBED: true,
                INPUT: true,
                OBJECT: true,
                SELECT: true,
                TEXTAREA: true,
                HTML: Ext.isIE ? true : false,
                BODY: Ext.isIE ? false : true
            },
            // <object> element is naturally tabbable only in IE8 and below
            naturallyTabbableTags: {
                BUTTON: true,
                IFRAME: true,
                INPUT: true,
                SELECT: true,
                TEXTAREA: true,
                OBJECT: Ext.isIE8m ? true : false
            },
            inputTags: {
                INPUT: true,
                TEXTAREA: true
            },
            tabbableSavedCounterAttribute: 'data-tabindex-counter',
            tabbableSavedValueAttribute: 'data-tabindex-value',
            splitCls: function(cls) {
                if (typeof cls === 'string') {
                    cls = cls.split(spacesRe);
                }
                return cls;
            }
        },
        // statics
        _init: function(E) {
            // Allow overriding the attribute name and/or selector; this is
            // done only once for performance reasons
            E.tabbableSelector += ',[' + E.tabbableSavedCounterAttribute + ']';
        },
        /**
         * Adds the given CSS class(es) to this Element.
         * @param {String/String[]} names The CSS classes to add separated by space,
         * or an array of classes
         * @param {String} [prefix] Prefix to prepend to each class. The separator `-` will be
         * appended to the prefix.
         * @param {String} [suffix] Suffix to append to each class. The separator `-` will be
         * prepended to the suffix.
         * @return {Ext.dom.Element} this
         */
        addCls: function(names, prefix, suffix) {
            return this.replaceCls(null, names, prefix, suffix);
        },
        /**
         * Sets up event handlers to add and remove a css class when the mouse is down and then up
         * on this element (a click effect)
         * @param {String} className The class to add
         * @param {Function} [testFn] A test function to execute before adding the class. The passed
         * parameter will be the Element instance. If this functions returns false, the class
         * will not be added.
         * @param {Object} [scope] The scope to execute the testFn in.
         * @return {Ext.dom.Element} this
         */
        addClsOnClick: function(className, testFn, scope) {
            var me = this,
                hasTest = Ext.isFunction(testFn);
            me.on("mousedown", function() {
                if (hasTest && testFn.call(scope || me, me) === false) {
                    return false;
                }
                me.addCls(className);
                Ext.getDoc().on({
                    mouseup: function() {
                        // In case me was destroyed prior to mouseup
                        if (me.dom) {
                            me.removeCls(className);
                        }
                    },
                    single: true
                });
            });
            return me;
        },
        /**
         * Sets up event handlers to add and remove a css class when this element has the focus
         * @param {String} className The class to add
         * @param {Function} [testFn] A test function to execute before adding the class. The passed
         * parameter will be the Element instance. If this functions returns false, the class
         * will not be added.
         * @param {Object} [scope] The scope to execute the testFn in.
         * @return {Ext.dom.Element} this
         */
        addClsOnFocus: function(className, testFn, scope) {
            var me = this,
                hasTest = Ext.isFunction(testFn);
            me.on("focus", function() {
                if (hasTest && testFn.call(scope || me, me) === false) {
                    return false;
                }
                me.addCls(className);
            });
            me.on("blur", function() {
                // In case blur is caused by destruction of me
                if (me.dom) {
                    me.removeCls(className);
                }
            });
            return me;
        },
        /**
         * Sets up event handlers to add and remove a css class when the mouse is over this element
         * @param {String} className The class to add
         * @param {Function} [testFn] A test function to execute before adding the class. The passed
         * parameter will be the Element instance. If this functions returns false, the class
         * will not be added.
         * @param {Object} [scope] The scope to execute the testFn in.
         * @return {Ext.dom.Element} this
         */
        addClsOnOver: function(className, testFn, scope) {
            var me = this,
                hasTest = Ext.isFunction(testFn);
            me.hover(function() {
                if (hasTest && testFn.call(scope || me, me) === false) {
                    return;
                }
                me.addCls(className);
            }, function() {
                me.removeCls(className);
            });
            return me;
        },
        addStyles: function(sides, styles) {
            var totalSize = 0,
                sidesArr = (sides || '').match(wordsRe),
                styleSides = [],
                len = sidesArr.length,
                side, i;
            if (len === 1) {
                totalSize = parseFloat(this.getStyle(styles[sidesArr[0]])) || 0;
            } else if (len) {
                for (i = 0; i < len; i++) {
                    side = sidesArr[i];
                    styleSides.push(styles[side]);
                }
                // Gather all at once, returning a hash
                styleSides = this.getStyle(styleSides);
                for (i = 0; i < len; i++) {
                    side = sidesArr[i];
                    totalSize += parseFloat(styleSides[styles[side]]) || 0;
                }
            }
            return totalSize;
        },
        addUnits: function(size, units) {
            return Element.addUnits(size, units);
        },
        // The following 3 methods add just enough of an animation api to make the scroller work
        // in Sencha Touch
        // TODO: unify touch/ext animations
        animate: function(animation) {
            animation = new Ext.fx.Animation(animation);
            animation.setElement(this);
            this._activeAnimation = animation;
            animation.on({
                animationend: this._onAnimationEnd,
                scope: this
            });
            Ext.Animator.run(animation);
            return animation;
        },
        _onAnimationEnd: function() {
            this._activeAnimation = null;
        },
        getActiveAnimation: function() {
            return this._activeAnimation;
        },
        append: function() {
            return this.appendChild.apply(this, arguments);
        },
        /**
         * Appends the passed element(s) to this element
         * @param {String/HTMLElement/Ext.dom.Element/Object} el The id or element to insert
         * or a DomHelper config
         * @param {Boolean} [returnDom=false] True to return the raw DOM element instead
         * of Ext.dom.Element
         * @return {Ext.dom.Element/HTMLElement} The inserted Ext.dom.Element (or
         * HTMLElement if _returnDom_ is _true_).
         */
        appendChild: function(el, returnDom) {
            var me = this,
                insertEl, eLen, e;
            if (el.nodeType || el.dom || typeof el === 'string') {
                // element
                el = Ext.getDom(el);
                me.dom.appendChild(el);
                return !returnDom ? Ext.get(el) : el;
            } else if (el.length) {
                // append all elements to a documentFragment
                insertEl = Ext.fly(DOC.createDocumentFragment());
                eLen = el.length;
                for (e = 0; e < eLen; e++) {
                    insertEl.appendChild(el[e], returnDom);
                }
                el = Ext.Array.toArray(insertEl.dom.childNodes);
                me.dom.appendChild(insertEl.dom);
                return returnDom ? el : new Ext.dom.CompositeElementLite(el);
            } else {
                // dh config
                return me.createChild(el, null, returnDom);
            }
        },
        /**
         * Appends this element to the passed element.
         * @param {String/HTMLElement/Ext.dom.Element} el The new parent element.
         * The id of the node, a DOM Node or an existing Element.
         * @return {Ext.dom.Element} This element.
         */
        appendTo: function(el) {
            Ext.getDom(el).appendChild(this.dom);
            return this;
        },
        /**
         * More flexible version of {@link #setStyle} for setting style properties.
         *
         * Styles in object form should be a valid DOM element style property.
         * [Valid style property names](http://www.w3schools.com/jsref/dom_obj_style.asp)
         * (_along with the supported CSS version for each_)
         *
         *     // <div id="my-el">Phineas Flynn</div>
         *
         *     var el = Ext.get('my-el');
         *
         *     el.applyStyles('color: white;');
         *
         *     el.applyStyles({
         *         fontWeight: 'bold',
         *         backgroundColor: 'gray',
         *         padding: '10px'
         *     });
         *
         *     el.applyStyles(function () {
         *         if (name.initialConfig.html === 'Phineas Flynn') {
         *             return 'font-style: italic;';
         *             // OR return { fontStyle: 'italic' };
         *         }
         *     });
         *
         * @param {String/Object/Function} styles A style specification string, e.g.
         * "width:100px", or object in the form `{width:"100px"}`, or a function which returns
         * such a specification.
         * @return {Ext.dom.Element} this
         */
        applyStyles: function(styles) {
            if (styles) {
                if (typeof styles === "function") {
                    styles = styles.call();
                }
                if (typeof styles === "string") {
                    styles = Element.parseStyles(styles);
                }
                if (typeof styles === "object") {
                    this.setStyle(styles);
                }
            }
            return this;
        },
        /**
         * Tries to blur the element. Any exceptions are caught and ignored.
         * @return {Ext.dom.Element} this
         */
        blur: function() {
            var me = this,
                dom = me.dom;
            // In IE, blurring the body can cause the browser window to hide.
            // Blurring the body is redundant, so instead we just focus it
            if (dom !== DOC.body) {
                try {
                    dom.blur();
                } catch (e) {}
                // This block is intentionally left blank
                return me;
            } else {
                return me.focus(undefined, dom);
            }
        },
        /**
         * When an element is moved around in the DOM, or is hidden using `display:none`, it loses
         * layout, and therefore all scroll positions of all descendant elements are lost.
         *
         * This function caches them, and returns a function, which when run will restore the cached
         * positions. In the following example, the Panel is moved from one Container to another
         * which will cause it to lose all scroll positions:
         *
         *     var restoreScroll = myPanel.el.cacheScrollValues();
         *     myOtherContainer.add(myPanel);
         *     restoreScroll();
         *
         * @return {Function} A function which will restore all descendant elements of this Element
         * to their scroll positions recorded when this function was executed. Be aware that the
         * returned function is a closure which has captured the scope of `cacheScrollValues`, so
         * take care to dereference it as soon as not needed - if is it is a `var` it will drop out
         * of scope, and the reference will be freed.
         */
        cacheScrollValues: function() {
            var me = this,
                scrollValues = [],
                scrolledDescendants = [],
                descendants, descendant, i, len;
            scrollFly = scrollFly || new Ext.dom.Fly();
            descendants = me.query('*');
            for (i = 0 , len = descendants.length; i < len; i++) {
                descendant = descendants[i];
                // use !== 0 for scrollLeft because it can be a negative number
                // in RTL mode in some browsers.
                if (descendant.scrollTop > 0 || descendant.scrollLeft !== 0) {
                    scrolledDescendants.push(descendant);
                    scrollValues.push(scrollFly.attach(descendant).getScroll());
                }
            }
            return function() {
                var scroll, i, len;
                for (i = 0 , len = scrolledDescendants.length; i < len; i++) {
                    scroll = scrollValues[i];
                    scrollFly.attach(scrolledDescendants[i]);
                    scrollFly.setScrollLeft(scroll.left);
                    scrollFly.setScrollTop(scroll.top);
                }
            };
        },
        /**
         * Centers the Element in either the viewport, or another Element.
         * @param {String/HTMLElement/Ext.dom.Element} centerIn element in
         * which to center the element.
         * @return {Ext.dom.Element} This element
         *
         * @chainable
         */
        center: function(centerIn) {
            return this.alignTo(centerIn || DOC, 'c-c');
        },
        /**
         * Selects a single *direct* child based on the passed CSS selector (the selector
         * should not contain an id).
         * @param {String} selector The CSS selector.
         * @param {Boolean} [returnDom=false] `true` to return the DOM node instead of
         * Ext.dom.Element.
         * @return {HTMLElement/Ext.dom.Element} The child Ext.dom.Element (or DOM node
         * if `returnDom` is `true`)
         */
        child: function(selector, returnDom) {
            var me = this,
                id;
            // If possible, avoid caching the root element.
            if (Ext.supports.Selectors2) {
                return me.selectNode(':scope>' + selector, !!returnDom);
            } else {
                // Pull the ID from the DOM (Ext.id also ensures that there *is* an ID).
                // If this object is a Flyweight, it will not have an ID
                id = me.id != null ? me.id : Ext.get(me).id;
                return me.selectNode(Ext.makeIdSelector(id) + " > " + selector, !!returnDom);
            }
        },
        /**
         * Clone this element.
         * @param {Boolean} [deep=false] `true` if the children of the node should also be cloned.
         * @param {Boolean} [returnDom=false] `true` to return the DOM node instead of
         * Ext.dom.Element.
         * @return {HTMLElement/Ext.dom.Element} The newly cloned Ext.dom.Element (or DOM node
         * if `returnDom` is `true`).
         */
        clone: function(deep, returnDom) {
            var clone = this.dom.cloneNode(deep);
            if (Ext.supports.CloneNodeCopiesExpando) {
                clearData(clone, deep);
            }
            return returnDom ? clone : Ext.get(clone);
        },
        constrainScrollLeft: function(left) {
            var dom = this.dom;
            return Math.max(Math.min(left, dom.scrollWidth - dom.clientWidth), 0);
        },
        constrainScrollTop: function(top) {
            var dom = this.dom;
            return Math.max(Math.min(top, dom.scrollHeight - dom.clientHeight), 0);
        },
        /**
         * Creates the passed DomHelper config and appends it to this element or optionally
         * inserts it before the passed child element.
         * @param {Object} config DomHelper element config object.  If no tag is specified
         * (e.g., {tag:'input'}) then a div will be automatically generated with the specified
         * attributes.
         * @param {HTMLElement} [insertBefore] a child element of this element
         * @param {Boolean} [returnDom=false] true to return the dom node instead of creating
         * an Element
         * @return {Ext.dom.Element/HTMLElement} The new child element (or HTMLElement if
         * _returnDom_ is _true_)
         */
        createChild: function(config, insertBefore, returnDom) {
            config = config || {
                tag: 'div'
            };
            if (insertBefore) {
                return Ext.DomHelper.insertBefore(insertBefore, config, returnDom !== true);
            } else {
                return Ext.DomHelper.append(this.dom, config, returnDom !== true);
            }
        },
        /**
         * Returns `true` if this element is an ancestor of the passed element, or is
         * the element.
         * @param {String/HTMLElement/Ext.dom.Element} element The dom element,
         * Ext.dom.Element, or id (string) of the dom element to check.
         * @return {Boolean} True if this element is an ancestor of el or the el itself, else false
         */
        contains: function(element) {
            if (!element) {
                return false;
            }
            /* eslint-disable-next-line vars-on-top */
            var me = this,
                dom = Ext.getDom(element);
            // we need el-contains-itself logic here because isAncestor does not do that:
            // https://developer.mozilla.org/en-US/docs/Web/API/Node.contains
            return (dom === me.dom) || me.isAncestor(dom);
        },
        /**
         * Destroys this element by removing it from the cache, removing its DOM reference,
         * and removing all of its event listeners.
         */
        destroy: function() {
            var me = this,
                dom = me.dom;
            if (me.destroyed) {
                Ext.Logger.warn("Cannot destroy Element \"" + me.id + "\". Already destroyed.");
                return;
            }
            if (me.resumeFocusEventsTimer) {
                Ext.unasap(me.resumeFocusEventsTimer);
                me.resumeFocusEventsTimer = null;
            }
            if (me.repaintTimer) {
                me.repaintTimer = Ext.undefer(me.repaintTimer);
            }
            if (me.deferFocusTimer) {
                me.deferFocusTimer = Ext.undefer(me.deferFocusTimer);
            }
            if (dom) {
                if (dom === DOC.body) {
                    Ext.raise("Cannot destroy body element.");
                } else if (dom === DOC) {
                    Ext.raise("Cannot destroy document object.");
                } else if (dom === WIN) {
                    Ext.raise("Cannot destroy window object");
                }
            }
            if (dom && dom.parentNode) {
                dom.parentNode.removeChild(dom);
            }
            if (me.$ripples) {
                me.destroyAllRipples();
            }
            me.collect();
        },
        detach: function() {
            var dom = this.dom,
                component = this.component;
            if (dom && dom.parentNode && dom.tagName !== 'BODY') {
                // Ensure focus is never lost
                if (component) {
                    component.revertFocus();
                }
                dom.parentNode.removeChild(dom);
            }
            return this;
        },
        /**
         * Disables the shadow element created by {@link #enableShadow}.
         * @private
         */
        disableShadow: function() {
            var shadow = this.shadow;
            if (shadow) {
                shadow.hide();
                shadow.disabled = true;
            }
        },
        /**
         * Disables the shim element created by {@link #enableShim}.
         * @private
         */
        disableShim: function() {
            var shim = this.shim;
            if (shim) {
                shim.hide();
                shim.disabled = true;
            }
        },
        /**
         * @private
         */
        doReplaceWith: function(element) {
            var dom = this.dom;
            dom.parentNode.replaceChild(Ext.getDom(element), dom);
        },
        /**
         * @private
         * A scrollIntoView implementation for scrollIntoView/rtlScrollIntoView to call
         * after current scrollX has been determined.
         */
        doScrollIntoView: function(container, hscroll, animate, highlight, getScrollX, scrollTo) {
            scrollFly = scrollFly || new Ext.dom.Fly();
            /* eslint-disable-next-line vars-on-top */
            var me = this,
                dom = me.dom,
                scrollX = scrollFly.attach(container)[getScrollX](),
                scrollY = container.scrollTop,
                position = me.getScrollIntoViewXY(container, scrollX, scrollY),
                newScrollX = position.x,
                newScrollY = position.y;
            // Highlight upon end of scroll
            if (highlight) {
                if (animate) {
                    animate = Ext.apply({
                        listeners: {
                            afteranimate: function() {
                                scrollFly.attach(dom).highlight();
                            }
                        }
                    }, animate);
                } else {
                    scrollFly.attach(dom).highlight();
                }
            }
            if (newScrollY !== scrollY) {
                scrollFly.attach(container).scrollTo('top', newScrollY, animate);
            }
            if (hscroll !== false && (newScrollX !== scrollX)) {
                scrollFly.attach(container)[scrollTo]('left', newScrollX, animate);
            }
            return me;
        },
        /**
         * Selects a single child at any depth below this element based on the passed
         * CSS selector (the selector should not contain an id).
         *
         * Use {@link #getById} if you need to get a reference to a child element via id.
         *
         * @param {String} selector The CSS selector
         * @param {Boolean} [returnDom=false] `true` to return the DOM node instead of
         * Ext.dom.Element
         * @return {HTMLElement/Ext.dom.Element} The child Ext.dom.Element (or DOM node
         * if `returnDom` is `true`)
         */
        down: function(selector, returnDom) {
            return this.selectNode(selector, !!returnDom);
        },
        /**
         * Enables a shadow element that will always display behind this element
         * @param {Object} [options] Configuration options for the shadow
         * @param {Number} [options.offset=4] Number of pixels to offset the shadow
         * @param {String} [options.mode='sides'] The shadow display mode.  Supports the following
         * options:
         *
         *     - `'sides'`: Shadow displays on both sides and bottom only
         *     - `'frame'`: Shadow displays equally on all four sides
         *     - `'drop'`: Traditional bottom-right drop shadow
         *     - `'bottom'`: Shadow is offset to the bottom
         *
         * @param {Boolean} [options.animate=false] `true` to animate the shadow while
         * the element is animating.  By default the shadow will be hidden during animation.
         * @param {Boolean} isVisible (private)
         * @private
         */
        enableShadow: function(options, isVisible) {
            var me = this,
                shadow = me.shadow || (me.shadow = new Ext.dom.Shadow(Ext.apply({
                    target: me
                }, options))),
                shim = me.shim;
            if (shim) {
                shim.offsets = shadow.outerOffsets;
                shim.shadow = shadow;
                shadow.shim = shim;
            }
            // Components pass isVisible to avoid the extra dom read to determine
            // whether or not this element is visible.
            if (isVisible === true || (isVisible !== false && me.isVisible())) {
                // the shadow element may have just been retrieved from an OverlayPool, so
                // we need to explicitly show it to be sure hidden styling is removed
                shadow.show();
            } else {
                shadow.hide();
            }
            shadow.disabled = false;
        },
        /**
         * Enables an iframe shim for this element to keep windowed objects from
         * showing through.  The position, size, and visibility of the shim will be
         * automatically synchronized as the position, size, and visibility of this
         * Element are changed.
         * @param {Object} [options] Configuration options for the shim
         * @param {Boolean} isVisible (private)
         * @return {Ext.dom.Shim} The new Shim
         * @private
         */
        enableShim: function(options, isVisible) {
            var me = this,
                shim = me.shim || (me.shim = new Ext.dom.Shim(Ext.apply({
                    target: me
                }, options))),
                shadow = me.shadow;
            if (shadow) {
                shim.offsets = shadow.outerOffsets;
                shim.shadow = shadow;
                shadow.shim = shim;
            }
            // Components pass isVisible to avoid the extra dom read to determine
            // whether or not this element is visible.
            if (isVisible === true || (isVisible !== false && me.isVisible())) {
                // the shim element may have just been retrieved from an OverlayPool, so
                // we need to explicitly show it to be sure hidden styling is removed
                shim.show();
            } else {
                shim.hide();
            }
            shim.disabled = false;
            return shim;
        },
        /**
         * Looks at this node and then at parent nodes for a match of the passed simple selector.
         * @param {String} simpleSelector The simple selector to test. See {@link Ext.dom.Query}
         * for information about simple selectors.
         * @param {Number/String/HTMLElement/Ext.dom.Element} [limit]
         * The max depth to search as a number or an element which causes the upward traversal
         * to stop and is **not** considered for inclusion as the result.
         * (defaults to 50 || document.documentElement)
         * @param {Boolean} [returnEl=false] True to return a Ext.dom.Element object instead of
         * DOM node
         * @return {HTMLElement/Ext.dom.Element} The matching DOM node (or
         * Ext.dom.Element if _returnEl_ is _true_).  Or null if no match was found.
         */
        findParent: function(simpleSelector, limit, returnEl) {
            var me = this,
                target = me.dom,
                topmost = docEl,
                depth = 0;
            if (limit || limit === 0) {
                if (typeof limit !== 'number') {
                    topmost = Ext.getDom(limit);
                    limit = Number.MAX_VALUE;
                }
            } else {
                // No limit passed, default to 50
                limit = 50;
            }
            while (target && target.nodeType === 1 && depth < limit && target !== topmost) {
                if (Ext.fly(target).is(simpleSelector)) {
                    return returnEl ? Ext.get(target) : target;
                }
                depth++;
                target = target.parentNode;
            }
            return null;
        },
        /**
         * Looks at parent nodes for a match of the passed simple selector.
         * @param {String} simpleSelector The simple selector to test. See {@link Ext.dom.Query}
         * for information about simple selectors.
         * @param {Number/String/HTMLElement/Ext.dom.Element} [limit]
         * The max depth to search as a number or an element which causes the upward traversal
         * to stop and is **not** considered for inclusion as the result.
         * (defaults to 50 || document.documentElement)
         * @param {Boolean} [returnEl=false] True to return a Ext.dom.Element object instead of
         * DOM node
         * @return {HTMLElement/Ext.dom.Element} The matching DOM node (or
         * Ext.dom.Element if _returnEl_ is _true_).  Or null if no match was found.
         */
        findParentNode: function(simpleSelector, limit, returnEl) {
            var p = Ext.fly(this.dom.parentNode);
            return p ? p.findParent(simpleSelector, limit, returnEl) : null;
        },
        /**
         * Gets the first child, skipping text nodes
         * @param {String} [selector] Find the next sibling that matches the passed simple selector.
         * See {@link Ext.dom.Query} for information about simple selectors.
         * @param {Boolean} [returnDom=false] `true` to return a raw DOM node instead of
         * an Ext.dom.Element
         * @return {Ext.dom.Element/HTMLElement} The first child or null
         */
        first: function(selector, returnDom) {
            return this.matchNode('nextSibling', 'firstChild', selector, returnDom);
        },
        /**
         * Try to focus the element either immediately or after a timeout
         * if `defer` argument is specified.
         *
         * @param {Number} [defer] Milliseconds to defer the focus
         * @param {HTMLElement} [dom] (private)
         *
         * @return {Ext.dom.Element} this
         */
        focus: function(defer, dom) {
            var me = this;
            dom = dom || me.dom;
            if (Number(defer)) {
                Ext.defer(me.focus, defer, me, [
                    null,
                    dom
                ]);
            } else {
                Ext.fireEvent('beforefocus', dom);
                dom.focus();
            }
            return me;
        },
        /**
         * @private
         * Removes the element from the cache and removes listeners.
         * Used for cleaning up orphaned elements after they have been removed from the dom.
         * Similar to {@link #destroy} except it assumes the element has already been
         * removed from the dom.
         */
        collect: function() {
            var me = this,
                dom = me.dom,
                shadow = me.shadow,
                shim = me.shim;
            // The parent destroy sets the destroy to emptyFn, which we don't
            // want on a shared fly
            if (!me.isFly) {
                me.mixins.observable.destroy.call(me);
                delete Ext.cache[me.id];
                me.el = null;
            }
            if (dom) {
                dom._extData = me.dom = null;
            }
            // we do not destroy the shadow and shim because they are returned to their
            // OverlayPools for reuse.
            if (shadow) {
                shadow.hide();
                me.shadow = null;
            }
            if (shim) {
                shim.hide();
                me.shim = null;
            }
        },
        getAnchorToXY: function(el, anchor, local, mySize) {
            return el.getAnchorXY(anchor, local, mySize);
        },
        /**
         * Returns the value of an attribute from the element's underlying DOM node.
         * @param {String} name The attribute name.
         * @param {String} [namespace] The namespace in which to look for the attribute.
         * @return {String} The attribute value.
         */
        getAttribute: function(name, namespace) {
            var dom = this.dom;
            return namespace ? (dom.getAttributeNS(namespace, name) || dom.getAttribute(namespace + ":" + name)) : (dom.getAttribute(name) || dom[name] || null);
        },
        /**
         * Returns an object containing a map of all attributes of this element's DOM node.
         *
         * @return {Object} Key/value pairs of attribute names and their values.
         */
        getAttributes: function() {
            var attributes = this.dom.attributes,
                result = {},
                attr, i, len;
            for (i = 0 , len = attributes.length; i < len; i++) {
                attr = attributes[i];
                result[attr.name] = attr.value;
            }
            return result;
        },
        /**
         * Gets the bottom Y coordinate of the element (element Y position + element height)
         * @param {Boolean} local True to get the local css position instead of page
         * coordinate
         * @return {Number}
         */
        getBottom: function(local) {
            return (local ? this.getLocalY() : this.getY()) + this.getHeight();
        },
        /**
         * Returns a child element of this element given its `id`.
         * @param {String} id The id of the desired child element.
         * @param {Boolean} [asDom=false] True to return the DOM element, false to return a
         * wrapped Element object.
         * @return {Ext.dom.Element/HTMLElement} The child element (or HTMLElement if
         * _asDom_ is _true_).  Or null if no match was found.
         */
        getById: function(id, asDom) {
            // for normal elements getElementById is the best solution, but if the el is
            // not part of the document.body, we have to resort to querySelector
            var dom = DOC.getElementById(id) || this.dom.querySelector(Ext.makeIdSelector(id));
            return asDom ? dom : (dom ? Ext.get(dom) : null);
        },
        getBorderPadding: function() {
            var paddingWidth = this.getStyle(paddingsTLRB),
                bordersWidth = this.getStyle(bordersTLRB);
            /* eslint-disable max-len */
            return {
                beforeX: (parseFloat(bordersWidth[borders.l]) || 0) + (parseFloat(paddingWidth[paddings.l]) || 0),
                afterX: (parseFloat(bordersWidth[borders.r]) || 0) + (parseFloat(paddingWidth[paddings.r]) || 0),
                beforeY: (parseFloat(bordersWidth[borders.t]) || 0) + (parseFloat(paddingWidth[paddings.t]) || 0),
                afterY: (parseFloat(bordersWidth[borders.b]) || 0) + (parseFloat(paddingWidth[paddings.b]) || 0)
            };
        },
        /* eslint-enable max-len */
        /**
         * @private
         */
        getBorders: function() {
            var bordersWidth = this.getStyle(bordersTLRB);
            return {
                beforeX: (parseFloat(bordersWidth[borders.l]) || 0),
                afterX: (parseFloat(bordersWidth[borders.r]) || 0),
                beforeY: (parseFloat(bordersWidth[borders.t]) || 0),
                afterY: (parseFloat(bordersWidth[borders.b]) || 0)
            };
        },
        /**
         * Gets the width of the border(s) for the specified side(s)
         * @param {String} side Can be t, l, r, b or any combination of those to add
         * multiple values. For example, passing `'lr'` would get the border **l**eft
         * width + the border **r**ight width.
         * @return {Number} The width of the sides passed added together
         */
        getBorderWidth: function(side) {
            return this.addStyles(side, borders);
        },
        /**
         * Returns an object holding as properties this element's CSS classes. The object
         * can be modified to effect arbitrary class manipulations that won't immediately
         * go to the DOM. When completed, the DOM node can be updated by calling
         * {@link #setClassMap setClassMap}. The values of the properties should either
         * be set to truthy values (such as `1` or `true`) or removed via `delete`.
         *
         *      var classes = el.getClassMap();
         *
         *      // Add the 'foo' class:
         *      classes.foo = 1;
         *
         *      // Remove the 'bar' class:
         *      delete classes.bar;
         *
         *      // Update the DOM in one step:
         *      el.setClassMap(classes);
         *
         * @param {Boolean} [clone=true] (private) Pass `false` to return the underlying
         * (readonly) object.
         * @return {Object}
         * @since 6.5.0
         */
        getClassMap: function(clone) {
            var data = this.getData();
            if (data) {
                data = data.classMap;
                if (clone !== false) {
                    data = Ext.apply({}, data);
                }
            }
            return data;
        },
        /**
         * Returns this element's data object. This object holds options that may be
         * needed by an `Ext.fly()` call when there is no (cached) `Ext.dom.Element`
         * instance tracked.
         *
         * This method will only return `null` if this instance has no associated DOM
         * node. If the DOM node does not have a data object, one will be created. To
         * avoid this creation, use {@link #peekData peekData()} instead.
         *
         * @param {Boolean} [sync=true] (private) Pass `false` to skip synchronization.
         * @return {Object}
         * @private
         */
        getData: function(sync) {
            var dom = this.dom,
                data;
            if (dom) {
                data = dom._extData || (dom._extData = {});
                if (sync !== false && !data.isSynchronized) {
                    this.synchronize();
                }
            }
            return data || null;
        },
        getFirstChild: function() {
            return Ext.get(this.dom.firstElementChild);
        },
        getLastChild: function() {
            return Ext.get(this.dom.lastElementChild);
        },
        /**
         * Returns the offset height of the element.
         * @param {Boolean} [contentHeight] `true` to get the height minus borders and padding.
         * @param {Boolean} [preciseHeight] `true` to get the precise height
         * @return {Number} The element's height.
         */
        getHeight: function(contentHeight, preciseHeight) {
            var me = this,
                dom = me.dom,
                hidden = me.isStyle('display', 'none'),
                height, floating;
            if (hidden) {
                return 0;
            }
            // Use the viewport height if they are asking for body height
            if (dom.nodeName === 'BODY') {
                height = Element.getViewportHeight();
            } else {
                if (preciseHeight) {
                    height = dom.getBoundingClientRect().height;
                } else {
                    height = dom.offsetHeight;
                    // SVG nodes do not have offsetHeight, so use boundingClientRect instead.
                    if (height == null) {
                        height = dom.getBoundingClientRect().height;
                    }
                }
            }
            // IE9/10 Direct2D dimension rounding bug
            if (Ext.supports.Direct2DBug) {
                floating = me.adjustDirect2DDimension(HEIGHT);
                if (preciseHeight) {
                    height += floating;
                } else if (floating > 0 && floating < 0.5) {
                    height++;
                }
            }
            if (contentHeight) {
                height -= me.getBorderWidth("tb") + me.getPadding("tb");
            }
            return (height < 0) ? 0 : height;
        },
        /**
         * Returns the `innerHTML` of an Element or an empty string if the element's
         * dom no longer exists.
         * @return {String}
         */
        getHtml: function() {
            return this.dom ? this.dom.innerHTML : '';
        },
        /**
         * Gets the left X coordinate
         * @param {Boolean} local True to get the local css position instead of
         * page coordinate
         * @return {Number}
         */
        getLeft: function(local) {
            return local ? this.getLocalX() : this.getX();
        },
        getLocalX: function() {
            var me = this,
                offsetParent,
                x = me.getStyle('left');
            if (!x || x === 'auto') {
                x = 0;
            } else if (pxRe.test(x)) {
                x = parseFloat(x);
            } else {
                x = me.getX();
                // Reading offsetParent causes forced async layout.
                // Do not do it unless needed.
                offsetParent = me.dom.offsetParent;
                if (offsetParent) {
                    x -= Ext.fly(offsetParent).getX();
                }
            }
            return x;
        },
        getLocalXY: function() {
            var me = this,
                offsetParent,
                style = me.getStyle([
                    'left',
                    'top'
                ]),
                x = style.left,
                y = style.top;
            if (!x || x === 'auto') {
                x = 0;
            } else if (pxRe.test(x)) {
                x = parseFloat(x);
            } else {
                x = me.getX();
                // Reading offsetParent causes forced async layout.
                // Do not do it unless needed.
                offsetParent = me.dom.offsetParent;
                if (offsetParent) {
                    x -= Ext.fly(offsetParent).getX();
                }
            }
            if (!y || y === 'auto') {
                y = 0;
            } else if (pxRe.test(y)) {
                y = parseFloat(y);
            } else {
                y = me.getY();
                // Reading offsetParent causes forced async layout.
                // Do not do it unless needed.
                offsetParent = me.dom.offsetParent;
                if (offsetParent) {
                    y -= Ext.fly(offsetParent).getY();
                }
            }
            return [
                x,
                y
            ];
        },
        getLocalY: function() {
            var me = this,
                offsetParent,
                y = me.getStyle('top');
            if (!y || y === 'auto') {
                y = 0;
            } else if (pxRe.test(y)) {
                y = parseFloat(y);
            } else {
                y = me.getY();
                // Reading offsetParent causes forced async layout.
                // Do not do it unless needed.
                offsetParent = me.dom.offsetParent;
                if (offsetParent) {
                    y -= Ext.fly(offsetParent).getY();
                }
            }
            return y;
        },
        /**
         * @method
         *
         * Returns an object with properties top, left, right and bottom representing the margins
         * of this element unless sides is passed, then it returns the calculated width
         * of the sides (see {@link #getPadding}).
         * @param {String} [sides] Any combination of 'l', 'r', 't', 'b' to get the sum
         * of those sides.
         * @return {Object/Number}
         */
        getMargin: (function() {
            var hash = {
                    t: "top",
                    l: "left",
                    r: "right",
                    b: "bottom"
                },
                allMargins = [
                    'margin-top',
                    'margin-left',
                    'margin-right',
                    'margin-bottom'
                ];
            return function(side) {
                var me = this,
                    style, key, o;
                if (!side) {
                    style = me.getStyle(allMargins);
                    o = {};
                    if (style && typeof style === 'object') {
                        o = {};
                        for (key in margins) {
                            o[key] = o[hash[key]] = parseFloat(style[margins[key]]) || 0;
                        }
                    }
                } else {
                    o = me.addStyles(side, margins);
                }
                return o;
            };
        })(),
        /**
         * Gets the width of the padding(s) for the specified side(s).
         * @param {String} side Can be t, l, r, b or any combination of those to add
         * multiple values. For example, passing `'lr'` would get the padding **l**eft +
         * the padding **r**ight.
         * @return {Number} The padding of the sides passed added together.
         */
        getPadding: function(side) {
            return this.addStyles(side, paddings);
        },
        getParent: function() {
            return Ext.get(this.dom.parentNode);
        },
        /**
         * Gets the right X coordinate of the element (element X position + element width)
         * @param {Boolean} local True to get the local css position instead of page
         * coordinates
         * @return {Number}
         */
        getRight: function(local) {
            return (local ? this.getLocalX() : this.getX()) + this.getWidth();
        },
        /**
         * Returns the current scroll position of the element.
         * @return {Object} An object containing the scroll position in the format
         * `{left: (scrollLeft), top: (scrollTop)}`
         */
        getScroll: function() {
            var me = this,
                dom = me.dom,
                docElement = docEl,
                left, top,
                body = DOC.body;
            if (dom === DOC || dom === body) {
                // the scrollLeft/scrollTop may be either on the body or documentElement,
                // depending on browser. It is possible to use window.pageXOffset/pageYOffset
                // in most modern browsers but this complicates things when in rtl mode because
                // pageXOffset does not always behave the same as scrollLeft when direction is
                // rtl. (e.g. pageXOffset can be an offset from the right, while scrollLeft
                // is offset from the left, one can be positive and the other negative, etc.)
                // To avoid adding an extra layer of feature detection in rtl mode to deal with
                // these differences, it's best just to always use scrollLeft/scrollTop
                left = docElement.scrollLeft || (body ? body.scrollLeft : 0);
                top = docElement.scrollTop || (body ? body.scrollTop : 0);
            } else {
                left = dom.scrollLeft;
                top = dom.scrollTop;
            }
            return {
                left: left,
                top: top
            };
        },
        /**
         * Gets the x and y coordinates needed for scrolling an element into view within
         * a given container.  These coordinates translate into the scrollLeft and scrollTop
         * positions that will need to be set on an ancestor of the element in order to make
         * this element visible within its container.
         * @param {String/HTMLElement/Ext.Element/Ext.util.Region} container The container
         * @param {Number} scrollX The container's current scroll position on the x axis
         * @param {Number} scrollY The container's current scroll position on the y axis
         * @param {Object} [align] The alignment for the scroll.
         * @param {'start'/'center'/'end'} [align.x] The alignment of the x scroll. If not
         * specified, the minimum will be done to make the element visible. The behavior
         * is undefined if the request cannot be honored. If the alignment is suffixed with a ?,
         * the alignment will only take place if the item is not already in the visible area.
         * @param {'start'/'center'/'end'} [align.y] The alignment of the x scroll. If not
         * specified, the minimum will be done to make the element visible. The behavior
         * is undefined if the request cannot be honored. If the alignment is suffixed with a ?,
         * the alignment will only take place if the item is not already in the visible area.
         * @return {Object} An object with "x" and "y" properties
         * @private
         */
        getScrollIntoViewXY: function(container, scrollX, scrollY, align) {
            var me = this,
                dom = me.dom,
                offsets, clientWidth, clientHeight;
            align = align || empty;
            if (container.isRegion) {
                clientHeight = container.height;
                clientWidth = container.width;
            } else {
                container = Ext.getDom(container);
                clientHeight = container.clientHeight;
                clientWidth = container.clientWidth;
            }
            offsets = me.getOffsetsTo(container);
            return {
                y: me.calcScrollPos(offsets[1] + scrollY, dom.offsetHeight, scrollY, clientHeight, align.y),
                x: me.calcScrollPos(offsets[0] + scrollX, dom.offsetWidth, scrollX, clientWidth, align.x)
            };
        },
        calcScrollPos: function(start, size, viewStart, viewSize, align) {
            var end = start + size,
                viewEnd = viewStart + viewSize,
                force = align && !endsQuestionRe.test(align),
                ret = viewStart;
            if (!force) {
                if (align) {
                    align = align.slice(0, -1);
                }
                if (size > viewSize || start < viewStart) {
                    align = align || 'start';
                    force = true;
                } else if (end > viewEnd) {
                    align = align || 'end';
                    force = true;
                }
            }
            if (force) {
                if (align === 'start') {
                    ret = start;
                } else if (align === 'center') {
                    ret = Math.max(0, start - Math.floor((viewSize / 2)));
                } else if (align === 'end') {
                    ret = Math.max(0, end - viewSize);
                }
            }
            return ret;
        },
        /**
         * Gets the left scroll position
         * @return {Number} The left scroll position
         */
        getScrollLeft: function() {
            var dom = this.dom;
            if (dom === DOC || dom === DOC.body) {
                return this.getScroll().left;
            } else {
                return dom.scrollLeft;
            }
        },
        /**
         * Gets the top scroll position
         * @return {Number} The top scroll position
         */
        getScrollTop: function() {
            var dom = this.dom;
            if (dom === DOC || dom === DOC.body) {
                return this.getScroll().top;
            } else {
                return dom.scrollTop;
            }
        },
        /**
         * Returns the size of the element.
         * @param {Boolean} [contentSize] `true` to get the width/size minus borders and padding.
         * @return {Object} An object containing the element's size:
         * @return {Number} return.width
         * @return {Number} return.height
         */
        getSize: function(contentSize) {
            return {
                width: this.getWidth(contentSize),
                height: this.getHeight(contentSize)
            };
        },
        /**
         * Returns a named style property based on computed/currentStyle (primary) and
         * inline-style if primary is not available.
         *
         * @param {String/String[]} property The style property (or multiple property names
         * in an array) whose value is returned.
         * @param {Boolean} [inline=false] if `true` only inline styles will be returned.
         * @return {String/Object} The current value of the style property for this element
         * (or a hash of named style values if multiple property arguments are requested).
         * @method
         */
        getStyle: function(property, inline) {
            var me = this,
                dom = me.dom,
                multiple = typeof property !== 'string',
                hooks = me.styleHooks,
                prop = property,
                props = prop,
                len = 1,
                domStyle, camel, values, hook, out, style, i;
            if (multiple) {
                values = {};
                prop = props[0];
                i = 0;
                if (!(len = props.length)) {
                    return values;
                }
            }
            if (!dom || dom.documentElement) {
                return values || '';
            }
            domStyle = dom.style;
            if (inline) {
                style = domStyle;
            } else {
                // Caution: Firefox will not render "presentation" (i.e. computed styles) in
                // iframes that are display:none or those inheriting display:none. Similar
                // issues with legacy Safari.
                //
                style = dom.ownerDocument.defaultView.getComputedStyle(dom, null);
                // fallback to inline style if rendering context not available
                if (!style) {
                    inline = true;
                    style = domStyle;
                }
            }
            do {
                hook = hooks[prop];
                if (!hook) {
                    hooks[prop] = hook = {
                        name: Element.normalize(prop)
                    };
                }
                if (hook.get) {
                    out = hook.get(dom, me, inline, style);
                } else {
                    camel = hook.name;
                    out = style[camel];
                }
                if (!multiple) {
                    return out;
                }
                values[prop] = out;
                prop = props[++i];
            } while (i < len);
            return values;
        },
        getStyleValue: function(name) {
            return this.dom.style.getPropertyValue(name);
        },
        getCaretPos: function() {
            var dom = this.dom,
                pos, selection;
            if (inputTypeSelectionSupported.test(dom.type)) {
                pos = dom.selectionStart;
                selection = (typeof pos !== 'number') && this.getTextSelection();
                if (selection) {
                    pos = selection[0];
                }
            } else {
                Ext.raise('Input type of "' + dom.type + '" does not support selectionStart');
            }
            return pos;
        },
        setCaretPos: function(pos) {
            this.selectText(pos, pos);
        },
        /**
         * Returns the selection range of an input element as an array of three values:
         *
         *      [ start, end, direction ]
         *
         * These have the same meaning as the parameters to `selectText`.
         * @return {Array}
         * @since 6.5.0
         */
        getTextSelection: function() {
            var dom = this.dom;
            if (inputTypeSelectionSupported.test(dom.type)) {
                return [
                    dom.selectionStart,
                    dom.selectionEnd,
                    dom.selectionDirection
                ];
            } else {
                Ext.raise('Input type of "' + this.dom.type + '" does not support selectionStart, selectionEnd and selectionDirection');
                return [];
            }
        },
        // FYI - Classic overrides this for older browsers...
        /**
         * Selects the specified contents of the input element (all by default).
         * @param {Number} [start=0] The starting index to select.
         * @param {Number} [end] The end index to select (defaults to all remaining text).
         * @param {"f"/"b"/"forward"/"backward"} [direction="f"] Pass "f" for forward,
         * "b" for backwards.
         * @return {Ext.dom.Element} this
         * @chainable
         * @since 6.5.0
         */
        selectText: function(start, end, direction) {
            var me = this,
                range,
                dom = me.dom,
                len;
            if (dom && inputTypeSelectionSupported.test(dom.type)) {
                start = start || 0;
                len = dom.value.length;
                if (end === undefined) {
                    end = len;
                }
                direction = selectDir[direction] || direction || 'forward';
                if (dom.setSelectionRange) {
                    dom.setSelectionRange(start, end, direction);
                } else if (dom.createTextRange) {
                    if (start > end) {
                        start = end;
                    }
                    range = dom.createTextRange();
                    range.moveStart('character', start);
                    range.moveEnd('character', -(len - end));
                    range.select();
                }
            } else if (!inputTypeSelectionSupported.test(dom.type)) {
                Ext.raise('Input type of "' + dom.type + '" does not support setSelectionRange');
            }
            return me;
        },
        /**
         * Gets the top Y coordinate
         * @param {Boolean} local True to get the local css position instead of page
         * coordinates
         * @return {Number}
         */
        getTop: function(local) {
            return local ? this.getLocalY() : this.getY();
        },
        /**
         * Returns this element's touch action.  (see {@link #setTouchAction})
         *
         * The returned object is shared and should not be mutated.
         *
         * @returns {Object}
         */
        getTouchAction: function() {
            return Ext.dom.TouchAction.get(this.dom);
        },
        /**
         * Returns the value of the `value` attribute.
         * @param {Boolean} asNumber `true` to parse the value as a number.
         * @return {String/Number}
         */
        getValue: function(asNumber) {
            var value = this.dom.value;
            return asNumber ? parseInt(value, 10) : value;
        },
        /**
         * Returns the dimensions of the element available to lay content out in.  For
         * most elements this is the clientHeight/clientWidth.  If the element is
         * the document/document.body the window's innerHeight/innerWidth is returned
         *
         * If the element (or any ancestor element) has CSS style `display: none`, the
         * dimensions will be zero.
         *
         * @return {Object} Object describing width and height.
         * @return {Number} return.width
         * @return {Number} return.height
         */
        getViewSize: function() {
            var dom = this.dom;
            if (dom === DOC || dom === DOC.body) {
                return {
                    width: Element.getViewportWidth(),
                    height: Element.getViewportHeight()
                };
            } else {
                return {
                    width: dom.clientWidth,
                    height: dom.clientHeight
                };
            }
        },
        getVisibilityMode: function() {
            var me = this,
                data = me.getData(),
                mode = data.visibilityMode;
            if (mode === undefined) {
                data.visibilityMode = mode = Element.DISPLAY;
            }
            return mode;
        },
        /**
         * Returns the offset width of the element.
         * @param {Boolean} [contentWidth] `true` to get the width minus borders and padding.
         * @param {Boolean} [preciseWidth] `true` to get the precise width
         * @return {Number} The element's width.
         */
        getWidth: function(contentWidth, preciseWidth) {
            var me = this,
                dom = me.dom,
                hidden = me.isStyle('display', 'none'),
                rect, width, floating;
            if (hidden) {
                return 0;
            }
            // Gecko will in some cases report an offsetWidth that is actually less than the width
            // of the text contents, because it measures fonts with sub-pixel precision but rounds
            // the calculated value down. Using getBoundingClientRect instead of offsetWidth allows
            // us to get the precise subpixel measurements so we can force them to always be
            // rounded up. See
            // https://bugzilla.mozilla.org/show_bug.cgi?id=458617
            // Rounding up ensures that the width includes the full width of the text contents.
            if (Ext.supports.BoundingClientRect) {
                rect = dom.getBoundingClientRect();
                width = (me.vertical && !Ext.supports.RotatedBoundingClientRect) ? (rect.bottom - rect.top) : (rect.right - rect.left);
                width = preciseWidth ? width : Math.ceil(width);
            } else {
                width = dom.offsetWidth;
            }
            // IE9/10 Direct2D dimension rounding bug: https://sencha.jira.com/browse/EXTJSIV-603
            // there is no need make adjustments for this bug when the element is vertically
            // rotated because the width of a vertical element is its rotated height
            if (Ext.supports.Direct2DBug && !me.vertical) {
                // get the fractional portion of the sub-pixel precision width of the element's text
                // contents
                floating = me.adjustDirect2DDimension(WIDTH);
                if (preciseWidth) {
                    width += floating;
                }
                // IE9 also measures fonts with sub-pixel precision, but unlike Gecko, instead of
                // rounding the offsetWidth down, it rounds to the nearest integer. This means that
                // in order to ensure that the width includes the full width of the text contents
                // we need to increment the width by 1 only if the fractional portion is less
                // than 0.5
                else if (floating > 0 && floating < 0.5) {
                    width++;
                }
            }
            if (contentWidth) {
                width -= me.getBorderWidth("lr") + me.getPadding("lr");
            }
            return (width < 0) ? 0 : width;
        },
        /**
         * Gets element X position in page coordinates
         *
         * @return {Number}
         */
        getX: function() {
            return this.getXY()[0];
        },
        /**
         * Gets element X and Y positions in page coordinates
         *
         * @return {Array} [x, y]
         */
        getXY: function() {
            var round = Math.round,
                dom = this.dom,
                body = DOC.body,
                x = 0,
                y = 0,
                bodyRect, rect;
            if (dom !== DOC && dom !== body) {
                // IE (including IE10) throws an error when getBoundingClientRect
                // is called on an element not attached to dom
                try {
                    bodyRect = body.getBoundingClientRect();
                    rect = dom.getBoundingClientRect();
                    x = rect.left - bodyRect.left;
                    y = rect.top - bodyRect.top;
                } catch (ex) {}
            }
            // This block is intentionally left blank
            return [
                round(x),
                round(y)
            ];
        },
        /**
         * Gets element Y position in page coordinates
         *
         * @return {Number}
         */
        getY: function() {
            return this.getXY()[1];
        },
        /**
         * Returns this element's z-index
         * @return {Number}
         */
        getZIndex: function() {
            return parseInt(this.getStyle('z-index'), 10);
        },
        /**
         * Checks if the specified CSS class exists on this element's DOM node.
         * @param {String} name The CSS class to check for.
         * @return {Boolean} `true` if the class exists, else `false`.
         */
        hasCls: function(name) {
            var classMap = this.getClassMap();
            return classMap.hasOwnProperty(name);
        },
        /**
         * Hide this element - Uses display mode to determine whether to use "display",
         * "visibility", or "offsets". See {@link #setVisible}.
         * @return {Ext.dom.Element} this
         */
        hide: function() {
            return this.setVisible(false);
        },
        /**
         * Sets up event handlers to call the passed functions when the mouse is moved into and
         * out of the Element.
         * @param {Function} overFn The function to call when the mouse enters the Element.
         * @param {Function} outFn The function to call when the mouse leaves the Element.
         * @param {Object} [scope] The scope (`this` reference) in which the functions are executed.
         * Defaults to the Element's DOM element.
         * @param {Object} [options] Options for the listener. See
         * {@link Ext.util.Observable#addListener the options parameter}.
         * @return {Ext.dom.Element} this
         */
        hover: function(overFn, outFn, scope, options) {
            var me = this;
            me.on('mouseenter', overFn, scope || me.dom, options);
            me.on('mouseleave', outFn, scope || me.dom, options);
            return me;
        },
        /**
         * Returns the index of the given element in the `childNodes` of this element. If
         * not present, `-1` is returned.
         *
         * @param {String/HTMLElement/Ext.dom.Element} childEl The potential child element.
         * @return {number} The index of `childEl` in `childNodes` or `-1` if not found.
         * @since 6.5.0
         */
        indexOf: function(childEl) {
            var children = this.dom,
                c = childEl && Ext.getDom(childEl);
            children = children && children.childNodes;
            return (c && children) ? Array.prototype.indexOf.call(children, c) : -1;
        },
        /**
         * Inserts this element after the passed element in the DOM.
         * @param {String/HTMLElement/Ext.dom.Element} el The element to insert after.
         * The `id` of the node, a DOM Node or an existing Element.
         * @return {Ext.dom.Element} This element.
         */
        insertAfter: function(el) {
            el = Ext.getDom(el);
            el.parentNode.insertBefore(this.dom, el.nextSibling);
            return this;
        },
        /**
         * Inserts this element before the passed element in the DOM.
         * @param {String/HTMLElement/Ext.dom.Element} el The element before which this element
         * will be inserted. The id of the node, a DOM Node or an existing Element.
         * @return {Ext.dom.Element} This element.
         */
        insertBefore: function(el) {
            el = Ext.getDom(el);
            el.parentNode.insertBefore(this.dom, el);
            return this;
        },
        /**
         * Inserts (or creates) an element as the first child of this element
         * @param {String/HTMLElement/Ext.dom.Element/Object} el The id or element to insert
         * or a DomHelper config to create and insert
         * @param {Boolean} [returnDom=false] True to return the raw DOM element instead
         * of Ext.dom.Element
         * @return {Ext.dom.Element/HTMLElement} The new child element (or HTMLElement if
         * _returnDom_ is _true_).
         */
        insertFirst: function(el, returnDom) {
            el = el || {};
            if (el.nodeType || el.dom || typeof el === 'string') {
                // element
                el = Ext.getDom(el);
                this.dom.insertBefore(el, this.dom.firstChild);
                return !returnDom ? Ext.get(el) : el;
            } else {
                // dh config
                return this.createChild(el, this.dom.firstChild, returnDom);
            }
        },
        /**
         * Inserts an html fragment into this element
         * @param {String} where Where to insert the html in relation to this element - beforeBegin,
         * afterBegin, beforeEnd, afterEnd. See {@link Ext.dom.Helper#insertHtml} for details.
         * @param {String} html The HTML fragment
         * @param {Boolean} [returnEl=false] True to return an Ext.dom.Element
         * @return {HTMLElement/Ext.dom.Element} The inserted node (or nearest related if more than
         * 1 inserted)
         */
        insertHtml: function(where, html, returnEl) {
            var el = Ext.DomHelper.insertHtml(where, this.dom, html);
            return returnEl ? Ext.get(el) : el;
        },
        /**
         * Inserts (or creates) the passed element (or DomHelper config) as a sibling of this
         * element
         * @param {String/HTMLElement/Ext.dom.Element/Object/Array} el The id, element to insert
         * or a DomHelper config to create and insert *or* an array of any of those.
         * @param {String} [where='before'] 'before' or 'after'
         * @param {Boolean} [returnDom=false] True to return the raw DOM element instead of
         * Ext.dom.Element
         * @return {Ext.dom.Element/HTMLElement} The inserted Ext.dom.Element (or
         * HTMLElement if _returnDom_ is _true_). If an array is passed, the last
         * inserted element is returned.
         */
        insertSibling: function(el, where, returnDom) {
            var me = this,
                DomHelper = Ext.DomHelper,
                isAfter = (where || 'before').toLowerCase() === 'after',
                rt, insertEl, eLen, e;
            if (Ext.isIterable(el)) {
                eLen = el.length;
                insertEl = Ext.fly(DOC.createDocumentFragment());
                // append all elements to a documentFragment               
                if (Ext.isArray(el)) {
                    for (e = 0; e < eLen; e++) {
                        rt = insertEl.appendChild(el[e], returnDom);
                    }
                } else // Iterable, but not an Array, must be an HtmlCollection
                {
                    for (e = 0; e < eLen; e++) {
                        insertEl.dom.appendChild(rt = el[0]);
                    }
                    if (returnDom === false) {
                        rt = Ext.get(rt);
                    }
                }
                // Insert fragment into document
                me.dom.parentNode.insertBefore(insertEl.dom, isAfter ? me.dom.nextSibling : me.dom);
                return rt;
            }
            el = el || {};
            if (el.nodeType || el.dom) {
                rt = me.dom.parentNode.insertBefore(Ext.getDom(el), isAfter ? me.dom.nextSibling : me.dom);
                // eslint-disable-line max-len
                if (!returnDom) {
                    rt = Ext.get(rt);
                }
            } else {
                if (isAfter && !me.dom.nextSibling) {
                    rt = DomHelper.append(me.dom.parentNode, el, !returnDom);
                } else {
                    rt = DomHelper[isAfter ? 'insertAfter' : 'insertBefore'](me.dom, el, !returnDom);
                }
            }
            return rt;
        },
        /**
         * Returns `true` if this element matches the passed simple selector
         * (e.g. 'div.some-class' or 'span:first-child').
         * @param {String/Function} selector The simple selector to test or a function
         * which is passed candidate nodes, and should return `true` for nodes which match.
         * @return {Boolean} `true` if this element matches the selector, else `false`.
         */
        is: function(selector) {
            var dom = this.dom,
                is;
            if (!selector) {
                // In Ext 4 is() called through to DomQuery methods, and would always
                // return true if the selector was ''.  The new query() method in v5 uses
                // querySelector/querySelectorAll() which consider '' to be an invalid
                // selector and throw an error as a result.  To maintain compatibility
                // with the various users of is() we have to return true if the selector
                // is an empty string.  For example: el.up('') should return the element's
                // direct parent.
                is = true;
            } else if (!dom.tagName) {
                // document and window objects can never match a selector
                is = false;
            } else if (Ext.isFunction(selector)) {
                is = selector(dom);
            } else {
                is = dom[Ext.supports.matchesSelector](selector);
            }
            return is;
        },
        /**
         * Returns `true` if this element is an ancestor of the passed element
         * @param {String/HTMLElement/Ext.dom.Element} el The element or id of the element
         * to search for in this elements descendants.
         * @return {Boolean}
         */
        isAncestor: function(el) {
            var ret = false,
                dom = this.dom,
                child = Ext.getDom(el);
            if (dom && child) {
                // This handles the window object, which is not a Node and throws an error
                if (!child.nodeType) {
                    return false;
                }
                if (dom.contains) {
                    return dom.contains(child);
                } else if (dom.compareDocumentPosition) {
                    return !!(dom.compareDocumentPosition(child) & 16);
                } else {
                    while ((child = child.parentNode)) {
                        ret = child === dom || ret;
                    }
                }
            }
            return ret;
        },
        isPainted: (function() {
            return !Ext.browser.is.IE ? function() {
                var dom = this.dom;
                return Boolean(dom && dom.offsetParent);
            } : function() {
                var dom = this.dom;
                return Boolean(dom && (dom.offsetHeight !== 0 || dom.offsetWidth !== 0));
            };
        })(),
        /**
         * Returns true if this element is scrollable.
         * @return {Boolean}
         */
        isScrollable: function() {
            var dom = this.dom;
            return dom.scrollHeight > dom.clientHeight || dom.scrollWidth > dom.clientWidth;
        },
        /**
         * Checks if the current value of a style is equal to a given value.
         * @param {String} style property whose value is returned.
         * @param {String} val to check against.
         * @return {Boolean} `true` for when the current value equals the given value.
         */
        isStyle: function(style, val) {
            return this.getStyle(style) === val;
        },
        /**
         * Checks whether the element is currently visible using both visibility and display
         * properties.
         * @param {Boolean} [deep=false] True to walk the dom and see if parent elements are hidden.
         * If false, the function only checks the visibility of the element itself and it may return
         * `true` even though a parent is not visible.
         * @param {Number} [mode=3] Bit flag indicating which CSS properties to test:
         *
         * - `1` - check display only
         * - `2` - check visibility only
         * - `3` - check both visibility and display
         *
         * @return {Boolean} `true` if the element is currently visible, else `false`
         */
        isVisible: function(deep, mode) {
            var dom = this.dom,
                visible = true,
                end;
            if (!dom) {
                return false;
            }
            mode = mode || 3;
            if (!visFly) {
                visFly = new Ext.dom.Fly();
            }
            for (end = dom.ownerDocument.documentElement; dom !== end; dom = dom.parentNode) {
                if (!dom || dom.nodeType === 11) {
                    // parent node does not exist or is a document fragment
                    visible = false;
                }
                if (visible) {
                    visFly.attach(dom);
                    if (mode & 1) {
                        visible = !visFly.isStyle(DISPLAY, NONE);
                    }
                    if (visible && (mode & 2)) {
                        visible = !visFly.isStyle(VISIBILITY, HIDDEN);
                    }
                }
                if (!visible || !deep) {
                    break;
                }
            }
            return visible;
        },
        /**
         * Gets the last child, skipping text nodes
         * @param {String} [selector] Find the previous sibling that matches the passed simple
         * selector. See {@link Ext.dom.Query} for information about simple selectors.
         * @param {Boolean} [returnDom=false] `true` to return a raw DOM node instead of an
         * Ext.dom.Element
         * @return {Ext.dom.Element/HTMLElement} The last child Ext.dom.Element (or
         * HTMLElement if _returnDom_ is _true_).  Or null if no match is found.
         */
        last: function(selector, returnDom) {
            return this.matchNode('previousSibling', 'lastChild', selector, returnDom);
        },
        /**
         * @cfg listeners
         * @hide
         */
        matchNode: function(dir, start, selector, returnDom) {
            var dom = this.dom,
                n;
            if (!dom) {
                return null;
            }
            n = dom[start];
            while (n) {
                if (n.nodeType === 1 && (!selector || Ext.fly(n, '_matchNode').is(selector))) {
                    return !returnDom ? Ext.get(n) : n;
                }
                n = n[dir];
            }
            return null;
        },
        /**
         * Measures and returns the size of this element. When `dimension` is `null` (or
         * not specified), this will be an object with `width` and `height` properties.
         *
         * If `dimension` is `'w'` the value returned will be this element's width. If
         * `dimension` is `'h'` the returned value will be this element's height.
         *
         * Unlike `getWidth` and `getHeight` this method only returns "precise" (sub-pixel)
         * sizes based on the `getBoundingClientRect` API.
         *
         * @param {'w'/'h'} [dimension] Specifies which dimension is desired. If omitted
         * then an object with `width` and `height` properties is returned.
         * @return {Number/Object} This element's width, height or both as a readonly
         * object. This object may be the direct result of `getBoundingClientRect` and
         * hence immutable on some browsers.
         * @private
         * @since 6.5.0
         */
        measure: function(dimension) {
            // This method doesn't use getBoundingClientRect because
            // the values it returns are affected by transforms (scale etc).
            // For this method we want the real size that's not affected by
            // transforms.
            var me = this,
                dom = me.dom,
                includeWidth = dimension !== 'h',
                includeHeight = dimension !== 'w',
                width = 0,
                height = 0,
                addPadding = !Ext.supports.ComputedSizeIncludesPadding,
                style, rect, offsetParent;
            // Use the viewport height if they are asking for body height
            if (dom.nodeName === 'BODY') {
                height = includeHeight && Element.getViewportHeight();
                width = includeWidth && Element.getViewportWidth();
            } else {
                offsetParent = dom.offsetParent;
                style = dom.ownerDocument.defaultView.getComputedStyle(dom, null);
                // We also have to add the padding if the element uses content-box sizing
                addPadding |= style.boxSizing === 'content-box';
                // offsetParent will be null with position fixed
                if (offsetParent !== null || style.position === 'fixed') {
                    if (includeHeight) {
                        height = toFloat(style.height);
                        if (addPadding) {
                            height += toFloat(style.paddingTop) + toFloat(style.paddingBottom) + toFloat(style.borderTopWidth) + toFloat(style.borderBottomWidth);
                        }
                    }
                    if (includeWidth) {
                        width = toFloat(style.width);
                        if (addPadding) {
                            width += toFloat(style.paddingLeft) + toFloat(style.paddingRight) + toFloat(style.borderLeftWidth) + toFloat(style.borderRightWidth);
                        }
                    }
                }
            }
            // Don't create a temporary object unless we need to return it...
            rect = dimension ? null : {
                width: width,
                height: height
            };
            // NOTE: The modern override ignores all these IE8/9/10 issues
            return dimension ? (includeWidth ? width : height) : rect;
        },
        /**
         * Measures and returns this element's content. When `dimension` is `null` (or
         * not specified), this will be an object with `width` and `height` properties.
         *
         * If `dimension` is `'w'` the value returned will be this element's width. If
         * `dimension` is `'h'` the returned value will be this element's height.
         *
         * Unlike `getWidth` and `getHeight` this method only returns "precise" (sub-pixel)
         * sizes based on the `getBoundingClientRect` API.
         *
         * @param {'w'/'h'} [dimension] Specifies which dimension is desired. If omitted
         * then an object with `width` and `height` properties is returned.
         * @return {Number/Object} This element's width, height or both as a readonly
         * object. This object may be the direct result of `getBoundingClientRect` and
         * hence immutable on some browsers.
         * @private
         * @since 6.5.0
         */
        measureContent: function(dimension) {
            var me = this,
                includeWidth = dimension !== 'h',
                size = me.measure(dimension),
                // see modern/classic overrides
                h = dimension ? size : size.height,
                w = dimension ? size : size.width;
            if (dimension !== 'w') {
                h -= me.getBorderWidth('tb') + me.getPadding('tb');
            }
            if (includeWidth) {
                w -= me.getBorderWidth('lr') + me.getPadding('lr');
            }
            return dimension ? (includeWidth ? w : h) : {
                width: w,
                height: h
            };
        },
        /**
         * Monitors this Element for the mouse leaving. Calls the function after the specified delay
         * only if the mouse was not moved back into the Element within the delay. If the mouse
         * *was* moved back in, the function is not called.
         * @param {Number} delay The delay **in milliseconds** to wait for possible mouse re-entry
         * before calling the handler function.
         * @param {Function} handler The function to call if the mouse remains outside of this
         * Element for the specified time.
         * @param {Object} [scope] The scope (`this` reference) in which the handler function
         * executes. Defaults to this Element.
         * @return {Object} The listeners object which was added to this element so that monitoring
         * can be stopped. Example usage:
         *
         *     // Hide the menu if the mouse moves out for 250ms or more
         *     this.mouseLeaveMonitor = this.menuEl.monitorMouseLeave(250, this.hideMenu, this);
         *
         *     ...
         *     // Remove mouseleave monitor on menu destroy
         *     this.mouseLeaveMonitor.destroy();
         *
         */
        monitorMouseLeave: function(delay, handler, scope) {
            var me = this,
                timer,
                listeners = {
                    mouseleave: function(e) {
                        if (Ext.isIE9m) {
                            e.enableIEAsync();
                        }
                        timer = Ext.defer(handler, delay, scope || me, [
                            e
                        ]);
                    },
                    mouseenter: function() {
                        Ext.undefer(timer);
                    },
                    destroy: function() {
                        Ext.undefer(timer);
                        if (!me.destroyed) {
                            me.un(listeners);
                        }
                    }
                };
            me.on(listeners);
            return listeners;
        },
        /**
         * Gets the next sibling, skipping text nodes
         * @param {String} [selector] Find the next sibling that matches the passed simple selector.
         * See {@link Ext.dom.Query} for information about simple selectors.
         * @param {Boolean} [returnDom=false] `true` to return a raw dom node instead of an
         * Ext.dom.Element
         * @return {Ext.dom.Element/HTMLElement} The next sibling Ext.dom.Element (or
         * HTMLElement if _asDom_ is _true_).  Or null if no match is found.
         */
        next: function(selector, returnDom) {
            return this.matchNode('nextSibling', 'nextSibling', selector, returnDom);
        },
        /**
         * Gets the parent node for this element, optionally chaining up trying to match a selector
         * @param {String} [selector] Find a parent node that matches the passed simple selector.
         * See {@link Ext.dom.Query} for information about simple selectors.
         * @param {Boolean} [returnDom=false] True to return a raw dom node instead of an
         * Ext.dom.Element
         * @return {Ext.dom.Element/HTMLElement} The parent node (Ext.dom.Element or
         * HTMLElement if _returnDom_ is _true_).  Or null if no match is found.
         */
        parent: function(selector, returnDom) {
            return this.matchNode('parentNode', 'parentNode', selector, returnDom);
        },
        peekData: function() {
            var dom = this.dom;
            return dom && dom._extData || null;
        },
        /**
         * Initializes positioning on this element. If a desired position is not passed,
         * it will make the the element positioned relative IF it is not already positioned.
         * @param {String} [pos] Positioning to use "relative", "absolute" or "fixed"
         * @param {Number} [zIndex] The zIndex to apply
         * @param {Number} [x] Set the page X position
         * @param {Number} [y] Set the page Y position
         */
        position: function(pos, zIndex, x, y) {
            var me = this;
            if (me.dom.tagName !== 'BODY') {
                if (!pos && me.isStyle(POSITION, STATIC)) {
                    me.setStyle(POSITION, RELATIVE);
                } else if (pos) {
                    me.setStyle(POSITION, pos);
                }
                if (zIndex) {
                    me.setStyle(ZINDEX, zIndex);
                }
                if (x || y) {
                    me.setXY([
                        x || false,
                        y || false
                    ]);
                }
            }
        },
        /**
         * Gets the previous sibling, skipping text nodes
         * @param {String} [selector] Find the previous sibling that matches the passed simple
         * selector. See {@link Ext.dom.Query} for information about simple selectors.
         * @param {Boolean} [returnDom=false] `true` to return a raw DOM node instead of an
         * Ext.dom.Element
         * @return {Ext.dom.Element/HTMLElement} The previous sibling (Ext.dom.Element or
         * HTMLElement if _returnDom_ is _true_).  Or null if no match is found.
         */
        prev: function(selector, returnDom) {
            return this.matchNode('previousSibling', 'previousSibling', selector, returnDom);
        },
        /**
         * Selects child nodes based on the passed CSS selector.
         * Delegates to document.querySelectorAll. More information can be found at
         * [http://www.w3.org/TR/css3-selectors/](http://www.w3.org/TR/css3-selectors/)
         *
         * All selectors, attribute filters and pseudos below can be combined infinitely
         * in any order. For example `div.foo:nth-child(odd)[@foo=bar].bar:first` would be
         * a perfectly valid selector.
         *
         * ## Element Selectors:
         *
         * * \* any element
         * * E an element with the tag E
         * * E F All descendant elements of E that have the tag F
         * * E > F or E/F all direct children elements of E that have the tag F
         * * E + F all elements with the tag F that are immediately preceded by an element
         * with the tag E
         * * E ~ F all elements with the tag F that are preceded by a sibling element with the tag E
         *
         * ## Attribute Selectors:
         *
         * The use of @ and quotes are optional. For example, div[@foo='bar'] is also a valid
         * attribute selector.
         *
         * * E[foo] has an attribute "foo"
         * * E[foo=bar] has an attribute "foo" that equals "bar"
         * * E[foo^=bar] has an attribute "foo" that starts with "bar"
         * * E[foo$=bar] has an attribute "foo" that ends with "bar"
         * * E[foo*=bar] has an attribute "foo" that contains the substring "bar"
         * * E[foo%=2] has an attribute "foo" that is evenly divisible by 2
         * * E[foo!=bar] has an attribute "foo" that does not equal "bar"
         *
         * ## Pseudo Classes:
         *
         * * E:first-child E is the first child of its parent
         * * E:last-child E is the last child of its parent
         * * E:nth-child(n) E is the nth child of its parent (1 based as per the spec)
         * * E:nth-child(odd) E is an odd child of its parent
         * * E:nth-child(even) E is an even child of its parent
         * * E:only-child E is the only child of its parent
         * * E:checked E is an element that is has a checked attribute that is true (e.g. a radio
         * or checkbox)
         * * E:first the first E in the resultset
         * * E:last the last E in the resultset
         * * E:nth(n) the nth E in the resultset (1 based)
         * * E:odd shortcut for :nth-child(odd)
         * * E:even shortcut for :nth-child(even)
         * * E:not(S) an E element that does not match simple selector S
         * * E:has(S) an E element that has a descendant that matches simple selector S
         * * E:next(S) an E element whose next sibling matches simple selector S
         * * E:prev(S) an E element whose previous sibling matches simple selector S
         * * E:any(S1|S2|S2) an E element which matches any of the simple selectors S1, S2 or S3
         *
         * ## CSS Value Selectors:
         *
         * * E{display=none} CSS value "display" that equals "none"
         * * E{display^=none} CSS value "display" that starts with "none"
         * * E{display$=none} CSS value "display" that ends with "none"
         * * E{display*=none} CSS value "display" that contains the substring "none"
         * * E{display%=2} CSS value "display" that is evenly divisible by 2
         * * E{display!=none} CSS value "display" that does not equal "none"
         *
         * @param {String} selector The CSS selector.
         * @param {Boolean} [asDom=true] `false` to return an array of Ext.dom.Element
         * @param {Boolean} single (private)
         * @return {HTMLElement[]/Ext.dom.Element[]} An Array of elements (
         * HTMLElement or Ext.dom.Element if _asDom_ is _false_) that match the selector.
         * If there are no matches, an empty Array is returned.
         */
        query: function(selector, asDom, single) {
            var dom = this.dom,
                results, len, nlen, node, nodes, i, j;
            if (!dom) {
                return null;
            }
            asDom = (asDom !== false);
            selector = selector.split(",");
            if (!single) {
                // only allocate the results array if the full result set is being
                // requested.  selectNode() uses the 'single' param.
                results = [];
            }
            for (i = 0 , len = selector.length; i < len; i++) {
                if (typeof selector[i] === 'string') {
                    if (single) {
                        // take the "fast path" if single was requested (selectNode)
                        node = dom.querySelector(selector[i]);
                        return asDom ? node : Ext.get(node);
                    }
                    nodes = dom.querySelectorAll(selector[i]);
                    for (j = 0 , nlen = nodes.length; j < nlen; j++) {
                        results.push(asDom ? nodes[j] : Ext.get(nodes[j]));
                    }
                }
            }
            return results;
        },
        /**
         * Adds one or more CSS classes to this element and removes the same class(es) from
         * all siblings.
         * @param {String/String[]} className The CSS class to add, or an array of classes.
         * @return {Ext.dom.Element} this
         */
        radioCls: function(className) {
            var cn = this.dom.parentNode.childNodes,
                v, i, len;
            className = Ext.isArray(className) ? className : [
                className
            ];
            for (i = 0 , len = cn.length; i < len; i++) {
                v = cn[i];
                if (v && v.nodeType === 1) {
                    Ext.fly(v).removeCls(className);
                }
            }
            return this.addCls(className);
        },
        redraw: function() {
            var dom = this.dom,
                domStyle = dom.style;
            domStyle.display = 'none';
            // eslint-disable-next-line no-unused-expressions
            dom.offsetHeight;
            domStyle.display = '';
        },
        /**
         * @method remove
         * @inheritdoc Ext.dom.Element#method-destroy
         * @deprecated 5.0.0 Please use {@link #destroy} instead.
         */
        remove: function() {
            this.destroy();
        },
        removeChild: function(element) {
            this.dom.removeChild(Ext.getDom(element));
            return this;
        },
        /**
         * Removes the given CSS class(es) from this Element.
         * @param {String/String[]} names The CSS classes to remove separated by space,
         * or an array of classes
         * @param {String} [prefix] Prefix to prepend to each class. The separator `-` will be
         * appended to the prefix.
         * @param {String} [suffix] Suffix to append to each class. The separator `-` will be
         * prepended to the suffix.
         * return {Ext.dom.Element} this
         */
        removeCls: function(names, prefix, suffix) {
            return this.replaceCls(names, null, prefix, suffix);
        },
        /**
         * Forces the browser to repaint this element.
         * @return {Ext.dom.Element} this
         */
        repaint: function(cls, state) {
            var me = this,
                off, on;
            if (!cls) {
                cls = Ext.baseCSSPrefix + 'repaint';
                on = !(off = false);
            } else if (state != null) {
                // If state is null or undefined then just toggle
                on = state;
                off = !state;
            }
            me.toggleCls(cls, on);
            if (!me.repaintTimer) {
                me.repaintTimer = Ext.defer(function() {
                    me.repaintTimer = null;
                    if (me.dom) {
                        // may have been removed already on slower UAs
                        me.toggleCls(cls, off);
                    }
                }, 1);
            }
            return me;
        },
        /**
         * Replaces the passed element with this element
         * @param {String/HTMLElement/Ext.dom.Element} el The element to replace.
         * The id of the node, a DOM Node or an existing Element.
         * @param {Boolean} [destroy=true] `false` to prevent destruction of the replaced
         * element
         * @return {Ext.dom.Element} This element
         */
        replace: function(el, destroy) {
            el = Ext.getDom(el);
            /* eslint-disable-next-line vars-on-top */
            var parentNode = el.parentNode,
                id = el.id,
                dom = this.dom;
            if (!parentNode) {
                Ext.raise('Cannot replace element "' + id + '". It is not attached to a parent node.');
            }
            if (destroy !== false && id && Ext.cache[id]) {
                parentNode.insertBefore(dom, el);
                Ext.get(el).destroy();
            } else {
                parentNode.replaceChild(dom, el);
            }
            return this;
        },
        /**
         * Replaces one or more CSS classes on this element with other classes. If the old
         * name does not exist, the new name will simply be added.
         *
         * @param {String/String[]} [remove] The CSS class(es) to be removed.
         * @param {String/String[]} [add] The CSS class(es) to be added.
         * @param {String} [prefix] The string to prepend to each class name.
         * @param {String} [suffix] The string to append to each class name.
         * @return {Ext.dom.Element} this
         */
        replaceCls: function(remove, add, prefix, suffix) {
            var me = this,
                dom = me.dom,
                added = 0,
                removed = 0,
                rem = remove,
                data = (add || remove) && me.getData(),
                list, map, i, n, name;
            if (data) {
                list = data.classList;
                map = data.classMap;
                add = add ? ((typeof add === 'string') ? add.split(spacesRe) : add) : EMPTY;
                rem = rem ? ((typeof rem === 'string') ? rem.split(spacesRe) : rem) : EMPTY;
                // Include the '-' but only if the caller hasn't already...
                prefix = prefix || '';
                if (prefix && prefix[prefix.length - 1] !== '-') {
                    prefix += '-';
                }
                suffix = suffix || '';
                if (suffix && suffix[0] !== '-') {
                    suffix = '-' + suffix;
                }
                for (i = 0 , n = rem.length; i < n; i++) {
                    if (!(name = rem[i])) {
                        // Sadly ... 'foo '.split(spacesRe) == ['foo', '']
                        
                        continue;
                    }
                    name = prefix + name + suffix;
                    if (spacesRe.test(name)) {
                        Ext.raise('Class names in arrays must not contain spaces');
                    }
                    if (map[name]) {
                        delete map[name];
                        ++removed;
                    }
                }
                for (i = 0 , n = add.length; i < n; i++) {
                    if (!(name = add[i])) {
                        
                        continue;
                    }
                    name = prefix + name + suffix;
                    if (spacesRe.test(name)) {
                        Ext.raise('Class names in arrays must not contain spaces');
                    }
                    if (!map[name]) {
                        map[name] = true;
                        // If we are only adding, we can be more efficient...
                        if (!removed) {
                            list.push(name);
                            ++added;
                        }
                    }
                }
                if (removed) {
                    me.setClassMap(map, /* keep= */
                    true);
                } else if (added) {
                    list = list.join(' ');
                    if (!Ext.isIE8 && dom instanceof SVGElement) {
                        // dom.className is an instance of SVGAnimatedString
                        // for SVG elements. Setting the `class` attribute
                        // of an SVG element will update its `dom.className.baseVal`.
                        dom.setAttribute('class', list);
                    } else {
                        dom.className = list;
                    }
                }
            }
            return me;
        },
        /**
         * Replaces this element with the passed element
         * @param {String/HTMLElement/Ext.dom.Element/Object} el The new element (id of the
         * node, a DOM Node or an existing Element) or a DomHelper config of an element to create
         * @return {Ext.dom.Element} This element
         */
        replaceWith: function(el) {
            var me = this,
                dom = me.dom,
                parent = dom.parentNode,
                cache = Ext.cache,
                newDom;
            me.clearListeners();
            if (el.nodeType || el.dom || typeof el === 'string') {
                el = Ext.get(el);
                newDom = parent.insertBefore(el.dom, dom);
            } else {
                // domhelper config
                newDom = Ext.DomHelper.insertBefore(dom, el);
            }
            parent.removeChild(dom);
            me.dom = newDom;
            if (!me.isFly) {
                delete cache[me.id];
                cache[me.id = Ext.id(newDom)] = me;
            }
            return me;
        },
        resolveListenerScope: function(defaultScope) {
            // Override this to pass along to our owning component (if we have one).
            var component = this.component;
            return component ? component.resolveListenerScope(defaultScope) : this;
        },
        /**
         * Scrolls this element the specified direction. Does bounds checking to make sure
         * the scroll is within this element's scrollable range.
         * @param {String} direction Possible values are:
         *
         * - `"l"` (or `"left"`)
         * - `"r"` (or `"right"`)
         * - `"t"` (or `"top"`, or `"up"`)
         * - `"b"` (or `"bottom"`, or `"down"`)
         *
         * @param {Number} distance How far to scroll the element in pixels
         * @param {Boolean/Object} [animate] true for the default animation or a standard Element
         * animation config object
         * @return {Boolean} Returns true if a scroll was triggered or false if the element
         * was scrolled as far as it could go.
         */
        scroll: function(direction, distance, animate) {
            if (!this.isScrollable()) {
                return false;
            }
            // Allow full word, or initial to be sent.
            // (Ext.dd package uses full word)
            direction = direction.charAt(0);
            /* eslint-disable-next-line vars-on-top */
            var me = this,
                dom = me.dom,
                side = direction === 'r' || direction === 'l' ? 'left' : 'top',
                scrolled = false,
                currentScroll, constrainedScroll;
            if (direction === 'l' || direction === 't' || direction === 'u') {
                distance = -distance;
            }
            if (side === 'left') {
                currentScroll = dom.scrollLeft;
                constrainedScroll = me.constrainScrollLeft(currentScroll + distance);
            } else {
                currentScroll = dom.scrollTop;
                constrainedScroll = me.constrainScrollTop(currentScroll + distance);
            }
            if (constrainedScroll !== currentScroll) {
                this.scrollTo(side, constrainedScroll, animate);
                scrolled = true;
            }
            return scrolled;
        },
        /**
         * Scrolls this element by the passed delta values, optionally animating.
         *
         * All of the following are equivalent:
         *
         *      el.scrollBy(10, 10, true);
         *      el.scrollBy([10, 10], true);
         *      el.scrollBy({ x: 10, y: 10 }, true);
         *
         * @param {Number/Number[]/Object} deltaX Either the x delta, an Array specifying x and y
         * deltas or an object with "x" and "y" properties.
         * @param {Number/Boolean/Object} deltaY Either the y delta, or an animate flag or config
         * object.
         * @param {Boolean/Object} animate Animate flag/config object if the delta values were
         * passed separately.
         * @return {Ext.dom.Element} this
         */
        scrollBy: function(deltaX, deltaY, animate) {
            var me = this,
                dom = me.dom;
            // Extract args if deltas were passed as an Array.
            if (deltaX.length) {
                animate = deltaY;
                deltaY = deltaX[1];
                deltaX = deltaX[0];
            } else if (typeof deltaX !== 'number') {
                // or an object
                animate = deltaY;
                deltaY = deltaX.y;
                deltaX = deltaX.x;
            }
            if (deltaX) {
                me.scrollTo('left', me.constrainScrollLeft(dom.scrollLeft + deltaX), animate);
            }
            if (deltaY) {
                me.scrollTo('top', me.constrainScrollTop(dom.scrollTop + deltaY), animate);
            }
            return me;
        },
        /**
         * @private
         */
        scrollChildIntoView: function(child, hscroll) {
            // scrollFly is used inside scrollInfoView, must use a method-unique fly here
            Ext.fly(child).scrollIntoView(this, hscroll);
        },
        /**
         * Scrolls this element into view within the passed container.
         *
         *       Ext.create('Ext.data.Store', {
         *           storeId:'simpsonsStore',
         *           fields:['name', 'email', 'phone'],
         *           data:{'items':[
         *               { 'name': 'Lisa',  "email":"lisa@simpsons.com", "phone":"555-111-1224" },
         *               { 'name': 'Bart',  "email":"bart@simpsons.com",  "phone":"555-222-1234" },
         *               { 'name': 'Homer', "email":"homer@simpsons.com",  "phone":"555-222-1244" },
         *               { 'name': 'Marge', "email":"marge@simpsons.com", "phone":"555-222-1254" },
         *               { 'name': 'Milhouse', "email":"milhouse@simpsons.com",
         *                 "phone":"555-222-1244" },
         *               { 'name': 'Willy', "email":"willy@simpsons.com", "phone":"555-222-1254" },
         *               { 'name': 'Skinner', "email":"skinner@simpsons.com",
         *                 "phone":"555-222-1244" },
         *               { 'name': 'Hank (last row)', "email":"hank@simpsons.com",
         *                 "phone":"555-222-1254" }
         *           ]},
         *           proxy: {
         *               type: 'memory',
         *               reader: {
         *                   type: 'json',
         *                   rootProperty: 'items'
         *               }
         *           }
         *       });
         *
         *       var grid = Ext.create('Ext.grid.Panel', {
         *           title: 'Simpsons',
         *           store: Ext.data.StoreManager.lookup('simpsonsStore'),
         *           columns: [
         *               { text: 'Name',  dataIndex: 'name', width: 125 },
         *               { text: 'Email', dataIndex: 'email', flex: 1 },
         *               { text: 'Phone', dataIndex: 'phone' }
         *           ],
         *           height: 190,
         *           width: 400,
         *           renderTo: Ext.getBody(),
         *           tbar: [{
         *               text: 'Scroll row 7 into view',
         *               handler: function () {
         *                   var view = grid.getView();
         *
         *                   Ext.get(view.getRow(7)).scrollIntoView(view.getEl(), null, true);
         *               }
         *           }]
         *       });
         *
         * @param {String/HTMLElement/Ext.Element} [container=document.body] The container element
         * to scroll.  Should be a string (id), dom node, or Ext.Element.
         * @param {Boolean} [hscroll=true] False to disable horizontal scroll.
         * @param {Boolean/Object} [animate] true for the default animation or a standard Element
         * animation config object
         * @param {Boolean} [highlight=false] true to {@link #highlight} the element when it is
         * in view.
         * @return {Ext.dom.Element} this
         */
        scrollIntoView: function(container, hscroll, animate, highlight) {
            container = Ext.getDom(container) || Ext.getBody().dom;
            return this.doScrollIntoView(container, hscroll, animate, highlight, 'getScrollLeft', 'scrollTo');
        },
        /**
         * Scrolls this element the specified scroll point. It does NOT do bounds checking so
         * if you scroll to a weird value it will try to do it. For auto bounds checking,
         * use #scroll.
         * @param {String} side Either "left" for scrollLeft values or "top" for scrollTop values.
         * @param {Number} value The new scroll value
         * @param {Boolean/Object} [animate] true for the default animation or a standard Element
         * animation config object
         * @return {Ext.dom.Element} this
         */
        scrollTo: function(side, value, animate) {
            // check if we're scrolling top or left
            var top = topRe.test(side),
                me = this,
                prop = top ? 'scrollTop' : 'scrollLeft',
                dom = me.dom,
                animCfg;
            if (!animate || !me.anim) {
                // just setting the value, so grab the direction
                dom[prop] = value;
                // corrects IE, other browsers will ignore
                dom[prop] = value;
            } else {
                animCfg = {
                    to: {}
                };
                animCfg.to[prop] = value;
                if (Ext.isObject(animate)) {
                    Ext.applyIf(animCfg, animate);
                }
                me.animate(animCfg);
            }
            return me;
        },
        /**
         * Selects descendant elements of this element based on the passed CSS selector to
         * enable {@link Ext.dom.Element Element} methods to be applied to many related
         * elements in one statement through the returned
         * {@link Ext.dom.CompositeElementLite CompositeElementLite} object.
         *
         * @param {String/HTMLElement[]} selector The CSS selector or an array of elements
         * @param {Boolean} composite Return a CompositeElement as opposed to a
         * CompositeElementLite. Defaults to false.
         * @return {Ext.dom.CompositeElementLite/Ext.dom.CompositeElement}
         */
        select: function(selector, composite) {
            var isElementArray, elements;
            if (typeof selector === "string") {
                elements = this.query(selector, !composite);
            } else if (selector.length === undefined) {
                Ext.raise("Invalid selector specified: " + selector);
            } else {
                // if selector is not a string, assume it is already an array of
                // HTMLElement
                elements = selector;
                isElementArray = true;
            }
            // if the selector parameter was a string we will have called through
            // to query, and it will have constructed either an array of
            // HTMLElement or Ext.Element, depending on the composite param we gave
            // it.  If this is the case we can take the fast path through the 
            // CompositeElementLite constructor to avoid calling getDom() or get()
            // on every element in the array.
            return composite ? new Ext.CompositeElement(elements, !isElementArray) : new Ext.CompositeElementLite(elements, true);
        },
        /**
         * Selects a single descendant element of this element using a CSS selector
         * (see {@link #method-query}).
         * @param {String} selector The selector query
         * @param {Boolean} [asDom=true] `false` to return an Ext.dom.Element
         * @return {HTMLElement/Ext.dom.Element} The DOM element (or Ext.dom.Element if
         * _asDom_ is _false_) which matched the selector.
         */
        selectNode: function(selector, asDom) {
            return this.query(selector, asDom, true);
        },
        /**
         * Sets the passed attributes as attributes of this element (a `style` attribute
         * can be a string, object or function).
         *
         * Example component (though any Ext.dom.Element would suffice):
         *
         *     var cmp = Ext.create({
         *         xtype: 'component',
         *         html: 'test',
         *         renderTo: Ext.getBody()
         *     });
         *
         * Once the component is rendered, you can fetch a reference to its outer
         * element to use `set`:
         *
         *     cmp.el.set({
         *         foo: 'bar'
         *     });
         *
         * This sets an attribute on the element of **foo="bar"**:
         *
         *     <div class="x-component x-component-default x-border-box"
         *          id="component-1009" foo="bar">test</div>
         *
         * To remove the attribute pass a value of **undefined**:
         *
         *     cmp.el.set({
         *         foo: undefined
         *     });
         *
         * **Note:**
         *
         *  - You cannot remove an attribute by passing `undefined` when the
         * `expandos` param is set to **false**.
         *  - Passing an attribute of `style` results in the request being handed off to
         * {@link #method-applyStyles}.
         *  - Passing an attribute of `cls` results in the element's dom's
         * [className](http://www.w3schools.com/jsref/prop_html_classname.asp) property
         * being set directly.  For additional flexibility when setting / removing
         * classes see:
         *     - {@link #method-addCls}
         *     - {@link #method-removeCls}
         *     - {@link #method-replaceCls}
         *     - {@link #method-setCls}
         *     - {@link #method-toggleCls}
         *
         * @param {Object} attributes The object with the attributes.
         * @param {Boolean} [useSet=true] `false` to override the default `setAttribute`
         * to use [expandos](http://help.dottoro.com/ljvovanq.php).
         * @return {Ext.dom.Element} this
         */
        set: function(attributes, useSet) {
            var me = this,
                dom = me.dom,
                attribute, value;
            for (attribute in attributes) {
                if (attributes.hasOwnProperty(attribute)) {
                    value = attributes[attribute];
                    if (attribute === 'style') {
                        me.applyStyles(value);
                    } else if (attribute === 'cls') {
                        dom.className = value;
                    } else if (useSet !== false) {
                        if (value === undefined) {
                            dom.removeAttribute(attribute);
                        } else {
                            dom.setAttribute(attribute, value);
                        }
                    } else {
                        dom[attribute] = value;
                    }
                }
            }
            return me;
        },
        /**
         * Sets the element's CSS bottom style.
         * @param {Number/String} bottom Number of pixels or CSS string value to set as
         * the bottom CSS property value
         * @return {Ext.dom.Element} this
         */
        setBottom: function(bottom) {
            this.dom.style[BOTTOM] = Element.addUnits(bottom);
            return this;
        },
        /**
         * Sets the CSS classes of this element to the keys of the given object. The
         * `classMap` object is typically returned by {@link #getClassMap}. The values of
         * the properties in the `classMap` should be truthy (such as `1` or `true`).
         *
         * @param {Object} classMap The object whose keys will be the CSS classes.
         * @param {Boolean} [keep=false] Pass `true` to indicate the the `classMap`
         * object can be kept (instead of copied).
         */
        setClassMap: function(classMap, keep) {
            var data = this.getData(/* sync= */
                false),
                classList;
            if (data) {
                classMap = (keep && classMap) || Ext.apply({}, classMap);
                data.classMap = classMap;
                data.classList = classList = Ext.Object.getKeys(classMap);
                data.isSynchronized = true;
                // We won't get a data object if !this.dom:
                this.dom.className = classList.join(' ');
            }
        },
        /**
         * Sets the specified CSS class on this element's DOM node.
         * @param {String/String[]} className The CSS class to set on this element.
         */
        setCls: function(className) {
            var me = this,
                elementData = me.getData(/* sync= */
                false),
                i, ln, map, classList;
            if (typeof className === 'string') {
                className = className.split(spacesRe);
            }
            elementData.classList = classList = className.slice();
            elementData.classMap = map = {};
            for (i = 0 , ln = classList.length; i < ln; i++) {
                map[classList[i]] = true;
            }
            me.dom.className = classList.join(' ');
        },
        /**
         * Sets the CSS display property. Uses originalDisplay if the specified value is a
         * boolean true.
         * @param {Boolean/String} value Boolean value to display the element using its
         * default display, or a string to set the display directly.
         * @return {Ext.dom.Element} this
         */
        setDisplayed: function(value) {
            var me = this;
            if (typeof value === "boolean") {
                value = value ? me._getDisplay() : NONE;
            }
            me.setStyle(DISPLAY, value);
            if (me.shadow || me.shim) {
                me.setUnderlaysVisible(value !== NONE);
            }
            return me;
        },
        /**
         * Set the height of this Element.
         * @param {Number/String} height The new height.
         * @return {Ext.dom.Element} this
         */
        setHeight: function(height) {
            var me = this;
            me.dom.style[HEIGHT] = Element.addUnits(height);
            if (me.shadow || me.shim) {
                me.syncUnderlays();
            }
            return me;
        },
        /**
         * Sets the `innerHTML` of this element.
         * @param {String} html The new HTML.
         * @return {Ext.dom.Element} this
         */
        setHtml: function(html) {
            if (this.dom) {
                this.dom.innerHTML = html;
            }
            return this;
        },
        setId: function(id) {
            var me = this,
                currentId = me.id,
                cache = Ext.cache;
            if (currentId) {
                delete cache[currentId];
            }
            me.dom.id = id;
            /**
             * The DOM element ID
             * @property id
             * @type String
             */
            me.id = id;
            cache[id] = me;
            return me;
        },
        /**
         * Sets the element's left position directly using CSS style
         * (instead of {@link #setX}).
         * @param {Number/String} left Number of pixels or CSS string value to
         * set as the left CSS property value
         * @return {Ext.dom.Element} this
         */
        setLeft: function(left) {
            var me = this;
            me.dom.style[LEFT] = Element.addUnits(left);
            if (me.shadow || me.shim) {
                me.syncUnderlays();
            }
            return me;
        },
        setLocalX: function(x) {
            var me = this,
                style = me.dom.style;
            // clear right style just in case it was previously set by rtlSetLocalXY
            style.right = '';
            style.left = (x === null) ? 'auto' : x + 'px';
            if (me.shadow || me.shim) {
                me.syncUnderlays();
            }
            return me;
        },
        setLocalXY: function(x, y) {
            var me = this,
                style = me.dom.style;
            // clear right style just in case it was previously set by rtlSetLocalXY
            style.right = '';
            if (x && x.length) {
                y = x[1];
                x = x[0];
            }
            if (x === null) {
                style.left = 'auto';
            } else if (x !== undefined) {
                style.left = x + 'px';
            }
            if (y === null) {
                style.top = 'auto';
            } else if (y !== undefined) {
                style.top = y + 'px';
            }
            if (me.shadow || me.shim) {
                me.syncUnderlays();
            }
            return me;
        },
        setLocalY: function(y) {
            var me = this;
            me.dom.style.top = (y === null) ? 'auto' : y + 'px';
            if (me.shadow || me.shim) {
                me.syncUnderlays();
            }
            return me;
        },
        setMargin: function(margin) {
            var me = this,
                domStyle = me.dom.style;
            if (margin || margin === 0) {
                margin = me.self.unitizeBox((margin === true) ? 5 : margin);
                domStyle.setProperty('margin', margin, 'important');
            } else {
                domStyle.removeProperty('margin-top');
                domStyle.removeProperty('margin-right');
                domStyle.removeProperty('margin-bottom');
                domStyle.removeProperty('margin-left');
            }
        },
        /**
         * Set the maximum height of this Element.
         * @param {Number/String} height The new maximum height.
         * @return {Ext.dom.Element} this
         */
        setMaxHeight: function(height) {
            this.dom.style[MAX_HEIGHT] = Element.addUnits(height);
            return this;
        },
        /**
         * Set the maximum width of this Element.
         * @param {Number/String} width The new maximum width.
         * @return {Ext.dom.Element} this
         */
        setMaxWidth: function(width) {
            this.dom.style[MAX_WIDTH] = Element.addUnits(width);
            return this;
        },
        /**
         * Set the minimum height of this Element.
         * @param {Number/String} height The new minimum height.
         * @return {Ext.dom.Element} this
         */
        setMinHeight: function(height) {
            this.dom.style[MIN_HEIGHT] = Element.addUnits(height);
            return this;
        },
        /**
         * Set the minimum width of this Element.
         * @param {Number/String} width The new minimum width.
         * @return {Ext.dom.Element} this
         */
        setMinWidth: function(width) {
            this.dom.style[MIN_WIDTH] = Element.addUnits(width);
            return this;
        },
        /**
         * Set the opacity of the element
         * @param {Number} opacity The new opacity. 0 = transparent, .5 = 50% visibile,
         * 1 = fully visible, etc
         * @return {Ext.dom.Element} this
         */
        setOpacity: function(opacity) {
            var me = this;
            if (me.dom) {
                me.setStyle('opacity', opacity);
            }
            return me;
        },
        setPadding: function(padding) {
            var me = this,
                domStyle = me.dom.style;
            if (padding || padding === 0) {
                padding = me.self.unitizeBox((padding === true) ? 5 : padding);
                domStyle.setProperty('padding', padding, 'important');
            } else {
                domStyle.removeProperty('padding-top');
                domStyle.removeProperty('padding-right');
                domStyle.removeProperty('padding-bottom');
                domStyle.removeProperty('padding-left');
            }
        },
        /**
         * Sets the element's CSS right style.
         * @param {Number/String} right Number of pixels or CSS string value to
         * set as the right CSS property value
         * @return {Ext.dom.Element} this
         */
        setRight: function(right) {
            this.dom.style[RIGHT] = Element.addUnits(right);
            return this;
        },
        /**
         * Sets the left scroll position
         * @param {Number} left The left scroll position
         * @return {Ext.dom.Element} this
         */
        setScrollLeft: function(left) {
            this.dom.scrollLeft = left;
            return this;
        },
        /**
         * Sets the top scroll position
         * @param {Number} top The top scroll position
         * @return {Ext.dom.Element} this
         */
        setScrollTop: function(top) {
            this.dom.scrollTop = top;
            return this;
        },
        /**
         * Set the size of this Element.
         *
         * @param {Number/String} width The new width. This may be one of:
         *
         * - A Number specifying the new width in pixels.
         * - A String used to set the CSS width style. Animation may **not** be used.
         * - A size object in the format `{width: widthValue, height: heightValue}`.
         *
         * @param {Number/String} height The new height. This may be one of:
         *
         * - A Number specifying the new height in pixels.
         * - A String used to set the CSS height style. Animation may **not** be used.
         * @return {Ext.dom.Element} this
         */
        setSize: function(width, height) {
            var me = this,
                style = me.dom.style;
            if (Ext.isObject(width)) {
                // in case of object from getSize()
                height = width.height;
                width = width.width;
            }
            if (width !== undefined) {
                style.width = Element.addUnits(width);
            }
            if (height !== undefined) {
                style.height = Element.addUnits(height);
            }
            if (me.shadow || me.shim) {
                me.syncUnderlays();
            }
            return me;
        },
        /**
         * Wrapper for setting style properties, also takes single object parameter of
         * multiple styles.
         *
         * Styles should be a valid DOM element style property.
         * [Valid style property names](http://www.w3schools.com/jsref/dom_obj_style.asp)
         * (_along with the supported CSS version for each_)
         *
         *     // <div id="my-el">Phineas Flynn</div>
         *
         *     var el = Ext.get('my-el');
         *
         *     // two-param syntax
         *     el.setStyle('color', 'white');
         *
         *     // single-param syntax
         *     el.setStyle({
         *         fontWeight: 'bold',
         *         backgroundColor: 'gray',
         *         padding: '10px'
         *     });
         *
         * @param {String/Object} prop The style property to be set, or an object of
         * multiple styles.
         * @param {String} [value] The value to apply to the given property, or null if
         * an object was passed.
         * @return {Ext.dom.Element} this
         */
        setStyle: function(prop, value) {
            var me = this,
                dom = me.dom,
                hooks = me.styleHooks,
                style = dom.style,
                name = prop,
                hook;
            // we don't promote the 2-arg form to object-form to avoid the overhead...
            if (typeof name === 'string') {
                hook = hooks[name];
                if (!hook) {
                    hooks[name] = hook = {
                        name: Element.normalize(name)
                    };
                }
                value = (value == null) ? '' : value;
                // map null && undefined to ''
                if (hook.set) {
                    hook.set(dom, value, me);
                } else {
                    style[hook.name] = value;
                }
                if (hook.afterSet) {
                    hook.afterSet(dom, value, me);
                }
            } else {
                for (name in prop) {
                    hook = hooks[name];
                    if (!hook) {
                        hooks[name] = hook = {
                            name: Element.normalize(name)
                        };
                    }
                    value = prop[name];
                    value = (value == null) ? '' : value;
                    // map null && undefined to ''
                    if (hook.set) {
                        hook.set(dom, value, me);
                    } else {
                        style[hook.name] = value;
                    }
                    if (hook.afterSet) {
                        hook.afterSet(dom, value, me);
                    }
                }
            }
            return me;
        },
        setText: function(text) {
            this.dom.textContent = text;
        },
        getText: function() {
            return this.dom.textContent;
        },
        /**
         * Sets the element's top position directly using CSS style
         * (instead of {@link #setY}).
         * @param {Number/String} top Number of pixels or CSS string value to
         * set as the top CSS property value
         * @return {Ext.dom.Element} this
         */
        setTop: function(top) {
            var me = this;
            me.dom.style[TOP] = Element.addUnits(top);
            if (me.shadow || me.shim) {
                me.syncUnderlays();
            }
            return me;
        },
        /**
         * Sets the CSS [touch-action](https://www.w3.org/TR/pointerevents/#the-touch-action-css-property)
         * property on this element and emulates its behavior on browsers where touch-action
         * is not supported.
         *
         * @param {Object} touchAction An object with touch-action names as the keys, and
         * boolean values to enable or disable specific touch actions. Accepted keys are:
         *
         * - `panX`
         * - `panY`
         * - `pinchZoom`
         * - `doubleTapZoom`
         *
         * All touch actions are enabled (`true`) by default, so it is usually only necessary
         * to specify which touch actions to disable.  For example, the following disables
         * only vertical scrolling and double-tap-zoom on an element
         *
         *     element.setTouchAction({
         *         panY: false,
         *         doubleTapZoom: false
         *     });
         *
         * @return {Ext.dom.Element} this
         */
        setTouchAction: function(touchAction) {
            Ext.dom.TouchAction.set(this.dom, touchAction);
        },
        setUnderlaysVisible: function(visible) {
            var shadow = this.shadow,
                shim = this.shim;
            if (shadow && !shadow.disabled) {
                if (visible) {
                    shadow.show();
                } else {
                    shadow.hide();
                }
            }
            if (shim && !shim.disabled) {
                if (visible) {
                    shim.show();
                } else {
                    shim.hide();
                }
            }
        },
        /**
         * @private
         */
        setVisibility: function(isVisible) {
            var domStyle = this.dom.style;
            if (isVisible) {
                domStyle.removeProperty('visibility');
            } else {
                domStyle.setProperty('visibility', 'hidden', 'important');
            }
        },
        /* eslint-disable max-len */
        /**
         * Use this to change the visibility mode between {@link #VISIBILITY},
         * {@link #DISPLAY}, {@link #OFFSETS}, {@link #CLIP}, or {@link #OPACITY}.
         *
         * @param {Ext.dom.Element.VISIBILITY/Ext.dom.Element.DISPLAY/Ext.dom.Element.OFFSETS/Ext.dom.Element.CLIP/Ext.dom.Element.OPACITY} mode
         * The method by which the element will be {@link #hide hidden} (you can
         * also use the {@link #setVisible} or {@link #toggle} method to toggle element
         * visibility).
         *
         * @return {Ext.dom.Element} this
         */
        setVisibilityMode: function(mode) {
            /* eslint-enable max-len */
            if (mode !== 1 && mode !== 2 && mode !== 3 && mode !== 4 && mode !== 5) {
                Ext.raise("visibilityMode must be one of the following: " + "Ext.Element.DISPLAY, Ext.Element.VISIBILITY, Ext.Element.OFFSETS, " + "Ext.Element.CLIP, or Element.OPACITY");
            }
            this.getData().visibilityMode = mode;
            return this;
        },
        /**
         * Sets the visibility of the element based on the current visibility mode. Use
         * {@link #setVisibilityMode} to switch between the following visibility modes:
         *
         * - {@link #DISPLAY} (the default)
         * - {@link #VISIBILITY}
         * - {@link #OFFSETS}
         * - {@link #CLIP}
         * - {@link #OPACITY}
         *
         * @param {Boolean} visible Whether the element is visible.
         * @return {Ext.dom.Element} this
         */
        setVisible: function(visible) {
            var me = this,
                mode = me.getVisibilityMode(),
                addOrRemove = visible ? 'removeCls' : 'addCls';
            switch (mode) {
                case Element.DISPLAY:
                    me.removeCls([
                        visibilityCls,
                        offsetsCls,
                        clipCls,
                        opacityCls
                    ]);
                    me[addOrRemove](displayCls);
                    break;
                case Element.VISIBILITY:
                    me.removeCls([
                        displayCls,
                        offsetsCls,
                        clipCls,
                        opacityCls
                    ]);
                    me[addOrRemove](visibilityCls);
                    break;
                case Element.OFFSETS:
                    me.removeCls([
                        visibilityCls,
                        displayCls,
                        clipCls,
                        opacityCls
                    ]);
                    me[addOrRemove](offsetsCls);
                    break;
                case Element.CLIP:
                    me.removeCls([
                        visibilityCls,
                        displayCls,
                        offsetsCls,
                        opacityCls
                    ]);
                    me[addOrRemove](clipCls);
                    break;
                case Element.OPACITY:
                    me.removeCls([
                        visibilityCls,
                        displayCls,
                        offsetsCls,
                        clipCls
                    ]);
                    me[addOrRemove](opacityCls);
                    break;
            }
            if (me.shadow || me.shim) {
                me.setUnderlaysVisible(visible);
            }
            if (!visible && me.$ripples) {
                me.destroyAllRipples();
            }
            return me;
        },
        /**
         * Set the width of this Element.
         * @param {Number/String} width The new width.
         * @return {Ext.dom.Element} this
         */
        setWidth: function(width) {
            var me = this;
            me.dom.style[WIDTH] = Element.addUnits(width);
            if (me.shadow || me.shim) {
                me.syncUnderlays();
            }
            return me;
        },
        /**
         * Sets this Element's page-level x coordinate
         * @param {Number} x
         * @return {Ext.dom.Element} this
         */
        setX: function(x) {
            return this.setXY([
                x,
                false
            ]);
        },
        /**
         * Sets this Element's page-level x and y coordinates
         * @param {Number[]} xy
         * @return {Ext.dom.Element} this
         */
        setXY: function(xy) {
            var me = this,
                pts = me.translatePoints(xy),
                style = me.dom.style,
                pos;
            me.position();
            // right position may have been previously set by rtlSetLocalXY 
            // so clear it here just in case.
            style.right = '';
            for (pos in pts) {
                if (!isNaN(pts[pos])) {
                    style[pos] = pts[pos] + 'px';
                }
            }
            if (me.shadow || me.shim) {
                me.syncUnderlays();
            }
            return me;
        },
        /**
         * Sets this Element's page-level y coordinate
         * @param {Number} y
         * @return {Ext.dom.Element} this
         */
        setY: function(y) {
            return this.setXY([
                false,
                y
            ]);
        },
        /**
         * Sets the z-index of this Element and synchronizes the z-index of shadow and/or
         * shim if present.
         *
         * @param {Number} zindex The new z-index to set
         * @return {Ext.dom.Element} this
         */
        setZIndex: function(zindex) {
            var me = this;
            if (me.shadow) {
                me.shadow.setZIndex(zindex);
            }
            if (me.shim) {
                me.shim.setZIndex(zindex);
            }
            return me.setStyle('z-index', zindex);
        },
        /**
         * Show this element - Uses display mode to determine whether to use "display",
         * "visibility", "offsets", or "clip". See {@link #setVisible}.
         *
         * @return {Ext.dom.Element} this
         */
        show: function() {
            return this.setVisible(true);
        },
        /**
         * Stops the specified event(s) from bubbling and optionally prevents the default action
         *
         *     var store = Ext.create('Ext.data.Store', {
         *         fields: ['name', 'email'],
         *         data: [{
         *             'name': 'Finn',
         *             "email": "finn@adventuretime.com"
         *         }]
         *     });
         *
         *     Ext.create('Ext.grid.Panel', {
         *         title: 'Land of Ooo',
         *         store: store,
         *         columns: [{
         *             text: 'Name',
         *             dataIndex: 'name'
         *         }, {
         *             text: 'Email <img style="vertical-align:middle;" src="{some-image-src}" />',
         *             dataIndex: 'email',
         *             flex: 1,
         *             listeners: {
         *                 render: function(col) {
         *                     // Swallow the click event when the click occurs on the
         *                     // help icon - preventing the sorting of data by that
         *                     // column and instead performing an action specific to
         *                     // the help icon
         *                     var img = col.getEl().down('img');
         *                     img.swallowEvent(['click', 'mousedown'], true);
         *                     col.on('click', function() {
         *                         // logic to show a help dialog
         *                         console.log('image click handler');
         *                     }, col);
         *                 }
         *             }
         *         }],
         *         height: 200,
         *         width: 400,
         *         renderTo: document.body
         *     });
         *
         * @param {String/String[]} eventName an event / array of events to stop from bubbling
         * @param {Boolean} [preventDefault] true to prevent the default action too
         * @return {Object} Object with a destroy method to unswallow events
         * @return {Function} return.destroy method to clean up any listeners that are swallowing
         * events
         */
        swallowEvent: function(eventName, preventDefault) {
            var me = this,
                e, eLen,
                listeners = {
                    destroyable: true
                },
                fn = function(e) {
                    e.stopPropagation();
                    if (preventDefault) {
                        e.preventDefault();
                    }
                };
            if (Ext.isArray(eventName)) {
                eLen = eventName.length;
                for (e = 0; e < eLen; e++) {
                    listeners[eventName[e]] = fn;
                }
            } else {
                listeners[eventName] = fn;
            }
            return me.on(listeners);
        },
        /**
         * @private
         * @param {String} firstClass
         * @param {String} secondClass
         * @param {Boolean} flag
         * @param {String} prefix
         * @return {Mixed}
         */
        swapCls: function(firstClass, secondClass, flag, prefix) {
            if (flag === undefined) {
                flag = true;
            }
            /* eslint-disable-next-line vars-on-top */
            var me = this,
                addedClass = flag ? firstClass : secondClass,
                removedClass = flag ? secondClass : firstClass;
            if (removedClass) {
                me.removeCls(prefix ? prefix + '-' + removedClass : removedClass);
            }
            if (addedClass) {
                me.addCls(prefix ? prefix + '-' + addedClass : addedClass);
            }
            return me;
        },
        /**
         * @private
         */
        synchronize: function() {
            var me = this,
                dom = me.dom,
                hasClassMap = {},
                className = dom.className,
                classList, i, ln, name,
                elementData = me.getData(/* sync= */
                false);
            if (className && className.length > 0) {
                classList = dom.className.split(classNameSplitRegex);
                for (i = 0 , ln = classList.length; i < ln; i++) {
                    name = classList[i];
                    hasClassMap[name] = true;
                }
            } else {
                classList = [];
            }
            elementData.classList = classList;
            elementData.classMap = hasClassMap;
            elementData.isSynchronized = true;
            return me;
        },
        /**
         * @private
         */
        syncUnderlays: function() {
            var me = this,
                shadow = me.shadow,
                shim = me.shim,
                dom = me.dom,
                xy, x, y, w, h;
            if (me.isVisible()) {
                xy = me.getXY();
                x = xy[0];
                y = xy[1];
                w = dom.offsetWidth;
                h = dom.offsetHeight;
                if (shadow && !shadow.hidden) {
                    shadow.realign(x, y, w, h);
                }
                if (shim && !shim.hidden) {
                    shim.realign(x, y, w, h);
                }
            }
        },
        /**
         * Toggles the specified CSS class on this element (removes it if it already exists,
         * otherwise adds it).
         * @param {String} className The CSS class to toggle.
         * @param {Boolean} [state] If specified as `true`, causes the class to be added.
         * If specified as `false`, causes the class to be removed.
         * @return {Ext.dom.Element} this
         */
        toggleCls: function(className, state) {
            if (state == null) {
                state = !this.hasCls(className);
            }
            return state ? this.addCls(className) : this.removeCls(className);
        },
        /**
         * Toggles the element's visibility, depending on visibility mode.
         * @return {Ext.dom.Element} this
         */
        toggle: function() {
            this.setVisible(!this.isVisible());
            return this;
        },
        translate: function() {
            var transformStyleName = 'webkitTransform' in DOC.createElement('div').style ? 'webkitTransform' : 'transform';
            return function(x, y, z) {
                x = Math.round(x);
                y = Math.round(y);
                z = Math.round(z);
                this.dom.style[transformStyleName] = 'translate3d(' + (x || 0) + 'px, ' + (y || 0) + 'px, ' + (z || 0) + 'px)';
            };
        }(),
        /**
         * @private
         */
        unwrap: function() {
            var dom = this.dom,
                parentNode = dom.parentNode,
                activeElement = (activeElFly || (activeElFly = new Ext.dom.Fly())).attach(Ext.Element.getActiveElement()),
                // eslint-disable-line max-len
                grandparentNode, cached, resumeFocus, tabIndex;
            grannyFly = grannyFly || new Ext.dom.Fly();
            cached = Ext.cache[activeElement.dom.id];
            // If the element is in the cache, we need to get the instance so
            // we can suspend events on it. If it's not in the cache, it can't
            // have any events so we don't need to suspend on it.
            if (cached) {
                activeElement = cached;
            }
            if (this.contains(activeElement)) {
                if (cached) {
                    cached.suspendFocusEvents();
                }
                resumeFocus = true;
            }
            if (parentNode) {
                grandparentNode = parentNode.parentNode;
                // See wrap() for the explanation of this jiggery-trickery
                if (resumeFocus) {
                    tabIndex = grandparentNode.getAttribute('tabIndex');
                    grannyFly.attach(grandparentNode);
                    grannyFly.set({
                        tabIndex: -1
                    });
                    grannyFly.suspendFocusEvents();
                    grannyFly.focus();
                }
                grandparentNode.insertBefore(dom, parentNode);
                grandparentNode.removeChild(parentNode);
            } else {
                grandparentNode = DOC.createDocumentFragment();
                grandparentNode.appendChild(dom);
            }
            if (resumeFocus) {
                if (cached) {
                    cached.focus();
                    cached.resumeFocusEvents();
                } else {
                    activeElement.focus();
                }
                if (grannyFly) {
                    grannyFly.resumeFocusEvents();
                    grannyFly.set({
                        tabIndex: tabIndex
                    });
                }
            }
            return this;
        },
        /**
         * Walks up the dom looking for a parent node that matches the passed simple selector
         * (e.g. 'div.some-class' or 'span:first-child').
         * This is a shortcut for findParentNode() that always returns an Ext.dom.Element.
         * @param {String} simpleSelector The simple selector to test. See {@link Ext.dom.Query}
         * for information about simple selectors.
         * @param {Number/String/HTMLElement/Ext.dom.Element} [limit]
         * The max depth to search as a number or an element that causes the upward
         * traversal to stop and is **not** considered for inclusion as the result.
         * (defaults to 50 || document.documentElement)
         * @param {Boolean} [returnDom=false] True to return the DOM node instead of Ext.dom.Element
         * @return {Ext.dom.Element/HTMLElement} The matching DOM node (or HTMLElement if
         * _returnDom_ is _true_).  Or null if no match was found.
         */
        up: function(simpleSelector, limit, returnDom) {
            return this.findParentNode(simpleSelector, limit, !returnDom);
        },
        /**
         * @method update
         * @inheritdoc Ext.dom.Element#method-setHtml
         * @deprecated 5.0.0 Please use {@link #setHtml} instead.
         */
        update: function(html) {
            return this.setHtml(html);
        },
        /**
         * Creates and wraps this element with another element
         * @param {Object} [config] DomHelper element config object for the wrapper element or null
         * for an empty div
         * @param {Boolean} [returnDom=false] True to return the raw DOM element instead of
         * Ext.dom.Element
         * @param {String} [selector] A CSS selector to select a descendant node within the created
         * element to use as the wrapping element.
         * @return {HTMLElement/Ext.dom.Element} The newly created wrapper element
         */
        wrap: function(config, returnDom, selector) {
            var me = this,
                dom = me.dom,
                // In case they pass returnDom: true:
                result = Ext.DomHelper.insertBefore(dom, config || {
                    tag: "div"
                }, !returnDom),
                newEl = (wrapFly || (wrapFly = new Ext.dom.Fly())).attach(Ext.getDom(result)),
                target = newEl,
                activeElement = (activeElFly || (activeElFly = new Ext.dom.Fly())).attach(Ext.Element.getActiveElement()),
                // eslint-disable-line max-len
                cached, resumeFocus, tabIndex;
            cached = Ext.cache[activeElement.dom.id];
            // If the element is in the cache, we need to get the instance so
            // we can suspend events on it. If it's not in the cache, it can't
            // have any events so we don't need to suspend on it.
            if (cached) {
                activeElement = cached;
            }
            if (selector) {
                target = newEl.selectNode(selector, returnDom);
            }
            if (me.contains(activeElement)) {
                if (cached) {
                    cached.suspendFocusEvents();
                }
                // This is workaround for the nasty IE behavior w/r/t removing and adding
                // DOM nodes that contain focus. When this happens, focus will fall back
                // to the document body *after* the present code execution path finishes,
                // with no way to control this. Instead of trying to refocus the element
                // asynchronously in a callback, we're focusing the wrapper instead,
                // adding the dom to the wrapper, and then refocusing the dom;
                // all synchronous and dandy.
                // The only side effect of all this focus juggling is that focus/blur
                // events will fire asynchronously after this code path finishes (in IE),
                // but we deal with that by ignoring these events *on particular elements*.
                // Focus event publisher will look for focus suspension flag on the element,
                // and since the flag is cleared asynchronously in the immediate callback,
                // we have enough cycles to ignore unwanted events to get away with it
                // but not too many to step on someone else's toes (hopefully).
                tabIndex = Ext.getDom(newEl).getAttribute('tabIndex');
                newEl.set({
                    tabIndex: -1
                });
                newEl.suspendFocusEvents();
                newEl.focus();
                resumeFocus = true;
            }
            (target.dom || target).appendChild(dom);
            if (resumeFocus) {
                if (cached) {
                    cached.focus();
                    cached.resumeFocusEvents();
                } else {
                    activeElement.focus();
                }
                newEl.resumeFocusEvents();
                // Most often tabIndex will be undefined, and we don't want to
                // make the wrapper focusable by accident.
                newEl.set({
                    tabIndex: tabIndex
                });
            }
            return result;
        },
        /**
         * Checks whether this element can be focused programmatically or by clicking.
         * To check if an element is in the document tab flow, use {@link #isTabbable}.
         *
         * @return {Boolean} True if the element is focusable
         */
        isFocusable: function(skipVisibility) {
            var dom = this.dom,
                focusable = false,
                nodeName;
            if (dom && !dom.disabled) {
                nodeName = dom.nodeName;
                /*
                 * An element is focusable if:
                 *   - It is naturally focusable, or
                 *   - It is an anchor or link with href attribute, or
                 *   - It has a tabIndex, or
                 *   - It is an editing host (contenteditable="true")
                 *
                 * Also note that we can't check dom.tabIndex because IE will return 0
                 * for elements that have no tabIndex attribute defined, regardless of
                 * whether they are naturally focusable or not.
                 */
                focusable = !!Ext.Element.naturallyFocusableTags[nodeName] || ((nodeName === 'A' || nodeName === 'LINK') && !!dom.href) || dom.getAttribute('tabIndex') != null || dom.contentEditable === 'true';
                // In IE8, <input type="hidden"> does not have a corresponding style
                // so isVisible() will assume that it's not hidden.
                if (Ext.isIE8 && nodeName === 'INPUT' && dom.type === 'hidden') {
                    focusable = false;
                }
                // Invisible elements cannot be focused, so check that as well
                // uness the caller doesn't care.
                focusable = focusable && (skipVisibility || this.isVisible(true));
            }
            return focusable;
        },
        /**
         * Returns `true` if this Element is an input field, or is editable in any way.
         * @return {Boolean} `true` if this Element is an input field, or is editable in any way.
         */
        isInputField: function() {
            var dom = this.dom,
                contentEditable = dom.contentEditable;
            // contentEditable will default to inherit if not specified, only check if the
            // attribute has been set or explicitly set to true
            // http://html5doctor.com/the-contenteditable-attribute/
            // Also skip <input> tags of type="button", we use them for checkboxes
            // and radio buttons
            if ((Ext.Element.inputTags[dom.tagName] && dom.type !== 'button') || (contentEditable === '' || contentEditable === 'true')) {
                return true;
            }
            return false;
        },
        /**
         * Checks whether this element participates in the sequential focus navigation,
         * and can be reached by using Tab key.
         *
         * @param {Boolean} [includeHidden=false] pass `true` if hidden, or unattached elements
         * should be returned.
         * @return {Boolean} True if the element is tabbable.
         */
        isTabbable: function(includeHidden) {
            var dom = this.dom,
                tabbable = false,
                nodeName, hasIndex, tabIndex;
            if (dom && !dom.disabled) {
                nodeName = dom.nodeName;
                // Can't use dom.tabIndex here because IE will return 0 for elements
                // that have no tabindex attribute defined, regardless of whether they are
                // naturally tabbable or not.
                tabIndex = dom.getAttribute('tabIndex');
                hasIndex = tabIndex != null;
                tabIndex -= 0;
                // Anchors and links are only naturally tabbable if they have href attribute
                // See http://www.w3.org/TR/html5/editing.html#specially-focusable
                if (nodeName === 'A' || nodeName === 'LINK') {
                    if (dom.href) {
                        // It is also possible to make an anchor untabbable by setting
                        // tabIndex < 0 on it
                        tabbable = hasIndex && tabIndex < 0 ? false : true;
                    } else // Anchor w/o href is tabbable if it has tabIndex >= 0,
                    // or if it's editable 
                    {
                        if (dom.contentEditable === 'true') {
                            tabbable = !hasIndex || (hasIndex && tabIndex >= 0) ? true : false;
                        } else {
                            tabbable = hasIndex && tabIndex >= 0 ? true : false;
                        }
                    }
                }
                // If an element has contenteditable="true" or is naturally tabbable,
                // then it is a potential candidate unless its tabIndex is < 0.
                else if (dom.contentEditable === 'true' || Ext.Element.naturallyTabbableTags[nodeName]) {
                    tabbable = hasIndex && tabIndex < 0 ? false : true;
                } else // That leaves non-editable elements that can only be made tabbable
                // by slapping tabIndex >= 0 on them
                {
                    if (hasIndex && tabIndex >= 0) {
                        tabbable = true;
                    }
                }
                // In IE8, <input type="hidden"> does not have a corresponding style
                // so isVisible() will assume that it's not hidden.
                if (Ext.isIE8 && nodeName === 'INPUT' && dom.type === 'hidden') {
                    tabbable = false;
                }
                // Invisible elements can't be tabbed into. If we have a component ref
                // we'll also check if the component itself is visible before incurring
                // the expense of DOM style reads.
                // Allow caller to specify that hiddens should be included.
                tabbable = tabbable && (includeHidden || ((!this.component || this.component.isVisible(true)) && this.isVisible(true)));
            }
            return tabbable;
        },
        ripplingCls: Ext.baseCSSPrefix + 'rippling',
        ripplingTransitionCls: Ext.baseCSSPrefix + 'ripple-transition',
        ripplingUnboundCls: Ext.baseCSSPrefix + 'rippling-unbound',
        rippleBubbleCls: Ext.baseCSSPrefix + 'ripple-bubble',
        rippleContainerCls: Ext.baseCSSPrefix + 'ripple-container',
        rippleWrapperCls: Ext.baseCSSPrefix + 'ripple-wrapper',
        // elements with 'display' property in this map cannot act as ripple container
        noRippleDisplayMap: {
            table: 1,
            'table-row': 1,
            'table-row-group': 1
        },
        // elements with tag name in this map cannot act as ripple container
        noRippleTagMap: {
            TABLE: 1,
            TR: 1,
            TBODY: 1
        },
        /**
         * Creates a ripple effect over this element. The element should be positioned
         * (either `relative` or `absolute`) prior to calling this method.
         * 
         * @param {String/Event/Ext.event.Event} [event] The event to use for ripple
         * positioning.
         * 
         * @param {Object/String} [options] Ripple options object or color to use for ripple
         * @param {String} [options.color] The color to use for the ripple effect or
         * `'default'` to use the stylesheet default color
         * {@link Global_CSS#$ripple-background-color}. When no `color` is given, the
         * element's `color` style is used.
         * @param {Boolean} [options.release] Optional determines if the ripple should happen
         * on release. Defaults to down/start.
         * @param {String} [options.delegate] Optional selector for which child to add the ripple
         * into
         * @param {String} [options.measureSelector] Optional selector for which child to use
         * to measure for ripple size.
         * @param {Number[]} [options.position] The [x,y] position in which to start the ripple.
         * @param {Boolean} [options.centered] Set to `true` to override all position
         * information and forces the ripple to be centered inside its parent.
         * @param {Boolean/String} [options.bound] Determines if the ripple is bound to
         * the parent container (default). If false ripple will expand outside of container.
         * @param {Boolean/Number} [options.diameterLimit] Maximum size, in pixels, that a ripple
         * can be.
         * A value of `false` or `0` will cause the ripple to fill its container. A value of `true`
         * will cause the ripple to use the default maximum size.
         * @param {Boolean/String} [options.fit=true] For bound ripples only. Determines if
         * ripple should search up the dom for an element that will fit the ripple
         * without clipping. Setting to false will force the unbound ripple into the specified
         * container. Defaults to `true`.
         * @param {Number} [options.destroyTime] The time (in milliseconds) to wait until
         * the ripple is destroyed.
         */
        ripple: function(event, options) {
            if (options === true || !options) {
                options = {};
            } else if (Ext.isString(options)) {
                options = {
                    color: options
                };
            }
            /* eslint-disable-next-line vars-on-top */
            var me = this,
                rippleParent = Ext.isString(options.delegate) ? me.down(options.delegate) : me,
                rippleMeasureEl = Ext.isString(options.measureSelector) ? me.down(options.measureSelector) : null,
                // eslint-disable-line max-len
                color = window.getComputedStyle(rippleParent.dom).color,
                unbound = options.bound === false,
                position = options.position,
                ripplingCls = me.ripplingCls,
                ripplingTransitionCls = me.ripplingTransitionCls,
                ripplingUnboundCls = me.ripplingUnboundCls,
                rippleBubbleCls = me.rippleBubbleCls,
                rippleContainerCls = me.rippleContainerCls,
                rippleWrapperCls = me.rippleWrapperCls,
                offset, width, height, rippleDiameter, center, measureElWidth, measureElHeight, rippleSize, pos, posX, posY, rippleWrapper, rippleContainer, rippleBubble, rippleDestructor, rippleClearFn, rippleDestructionTimer, rippleBox, unboundEl, unboundElData, timeout;
            if (rippleParent) {
                offset = rippleParent.getXY();
                width = rippleParent.getWidth();
                height = rippleParent.getHeight();
                timeout = rippleParent.$rippleClearTimeout;
                if (timeout) {
                    rippleParent.$rippleClearTimeout = Ext.undefer(timeout);
                }
                // If a measure element exists use that to determine the ripple diameter
                // otherwise we will just use the parent
                if (rippleMeasureEl) {
                    measureElWidth = rippleMeasureEl.getWidth();
                    measureElHeight = rippleMeasureEl.getHeight();
                    rippleDiameter = Math.max(measureElWidth, measureElHeight);
                } else {
                    rippleDiameter = width > height ? width : height;
                }
                // Cap the diameter based on the default or provided limit
                if (options.diameterLimit === undefined || options.diameterLimit === true) {
                    rippleDiameter = Math.min(rippleDiameter, Element.maxRippleDiameter);
                } else if (options.diameterLimit && options.diameterLimit !== false && options.diameterLimit !== 0) {
                    rippleDiameter = Math.min(rippleDiameter, options.diameterLimit);
                }
                // determine the center of the element we are going to ripple over
                center = [
                    offset[0] + width / 2,
                    offset[1] + height / 2
                ];
                if (unbound) {
                    if (options.fit !== false) {
                        // Compute the bounding rippleBox that is centered over the
                        // target element.
                        rippleSize = rippleDiameter * 2.15;
                        // anim scales to 2.15
                        rippleBox = rippleParent.getRegion();
                        rippleBox.setPosition(rippleBox.getCenter()).setSize(rippleSize).translateBy(-rippleSize / 2, -rippleSize / 2);
                        // Find the nearest parent el that will be able to fully contain
                        // the ripple... we assume that the el we are rippling is not too
                        // close to the edge
                        unboundEl = me.up(function(candidate) {
                            var fly = Ext.fly(candidate, 'ripple');
                            return !(candidate.tagName in me.noRippleTagMap) && !(fly.getStyle('display') in me.noRippleDisplayMap) && (fly.getRegion().contains(rippleBox));
                        }) || Ext.getBody();
                    } else {
                        unboundEl = rippleParent;
                    }
                }
                // if the first param was a string, it was meant to be a color
                // otherwise if it an unwrapped event lets wrap it up
                if (Ext.isString(event)) {
                    options.color = event;
                    event = null;
                } else if (event && !event.isEvent) {
                    event = new Ext.event.Event(event);
                }
                // Check for preventRipple on the event and skip everything if its true
                if (event && event.isEvent) {
                    // Prevent ripples from the same event
                    if (event.browserEvent.$preventRipple) {
                        return;
                    }
                    position = event.getXY();
                    event.browserEvent.$preventRipple = true;
                }
                //  unbound or centered items always center, otherwise if position is
                // provided use it
                pos = (!unbound && !options.centered && position) || center;
                posX = pos[0] - offset[0] - (rippleDiameter / 2);
                posY = pos[1] - offset[1] - (rippleDiameter / 2);
                // Ripple Parent always needs to be notified that it should transition
                // with the ripple bound or not
                rippleParent.addCls(ripplingTransitionCls);
                if (!unbound) {
                    rippleParent.addCls(ripplingCls);
                    // Is there already a container for ripples, reuse it.
                    rippleContainer = rippleParent.child('.' + rippleContainerCls);
                } else {
                    // unbound ripples are added to the body inside the ripple wrapper.
                    // check to see if this wrapper exists, if not create it
                    unboundElData = unboundEl.getData();
                    rippleWrapper = unboundElData.rippleWrapper;
                    if (!rippleWrapper) {
                        // insertFirst is important because of Field margin-top:0 rules
                        // to collapse field spacing in form panels.
                        unboundElData.rippleWrapper = rippleWrapper = unboundEl.insertFirst({
                            style: 'position: absolute; top: 0; left: 0',
                            cls: rippleWrapperCls + ' ' + ripplingCls + ' ' + ripplingUnboundCls
                        });
                    }
                }
                if (!rippleContainer) {
                    if (unbound) {
                        // unbound ripples are positioned inside the ripple-wrapper,
                        // which is inside the body
                        rippleContainer = rippleWrapper.append({
                            cls: rippleContainerCls
                        });
                        // position the unbound ripple over-top the element
                        rippleContainer.setXY(offset);
                    } else {
                        // body ripples are positioned inside the rippleParent, which is
                        // the element being rippled
                        rippleContainer = rippleParent.append({
                            cls: rippleContainerCls
                        });
                    }
                }
                // create the actual ripple bubble element
                rippleBubble = rippleContainer.append({
                    cls: rippleBubbleCls
                });
                if (options.color !== 'default') {
                    rippleBubble.setStyle('backgroundColor', options.color || color);
                }
                rippleBubble.setWidth(rippleDiameter);
                rippleBubble.setHeight(rippleDiameter);
                rippleBubble.setTop(posY);
                rippleBubble.setLeft(posX);
                rippleClearFn = function() {
                    // Allow for transition to happen then remove classes
                    // we do this instead of a transtionend listener
                    // as we do not know which element is transitioning
                    if (!rippleParent.destroyed) {
                        rippleParent.$rippleClearTimeout = Ext.defer(function() {
                            rippleParent.removeCls([
                                ripplingCls,
                                ripplingTransitionCls
                            ]);
                            rippleParent.$rippleClearTimeout = null;
                        }, 50);
                    }
                };
                rippleDestructor = function() {
                    var ripple, timeout;
                    // destroy the ripple
                    rippleBubble.destroy();
                    // remove from lookup
                    if (me.$ripples) {
                        delete me.$ripples[rippleBubble.id];
                    }
                    timeout = rippleParent.$rippleClearTimeout;
                    if (timeout) {
                        rippleParent.$rippleClearTimeout = Ext.undefer(timeout);
                    }
                    if (unbound) {
                        // always destroy unbound ripple containers as they are never
                        // re-used. only the ripple-wrapper is reused
                        rippleContainer.destroy();
                        // Determine if there are any other ripples still active in the wrapper
                        ripple = rippleWrapper.child('.' + rippleContainerCls);
                        // If there are no other ripples, clean up all ripple related DOM
                        if (!ripple) {
                            unboundElData.rippleWrapper = null;
                            rippleWrapper.destroy();
                            rippleClearFn();
                        }
                    } else {
                        // Determine if there are any other ripples still active in the parent
                        ripple = rippleContainer.child('.' + rippleBubbleCls);
                        // If there are no other ripples, clean up all ripple related DOM
                        if (!ripple) {
                            rippleContainer.destroy();
                            rippleClearFn();
                        }
                    }
                };
                rippleDestructionTimer = Ext.defer(rippleDestructor, options.destroyTime || 1000, me);
                // Keep a list of all the current ripples, for cleanup later
                if (!me.$ripples) {
                    me.$ripples = {};
                }
                me.$ripples[rippleBubble.id] = {
                    timerId: rippleDestructionTimer,
                    destructor: rippleDestructor
                };
                rippleBubble.addCls(Ext.baseCSSPrefix + 'ripple');
            }
        },
        destroyAllRipples: function() {
            var me = this,
                rippleEl, ripple;
            for (rippleEl in me.$ripples) {
                ripple = me.$ripples[rippleEl];
                Ext.undefer(ripple.timerId);
                if (ripple.destructor) {
                    ripple.destructor();
                }
            }
            me.$ripples = null;
        },
        privates: {
            /**
             * @private
             */
            findTabbableElements: function(options) {
                var skipSelf, skipChildren, excludeRoot, includeSaved, includeHidden,
                    dom = this.dom,
                    cAttr = Ext.Element.tabbableSavedCounterAttribute,
                    selection = [],
                    idx = 0,
                    nodes, node, fly, i, len, tabIndex;
                if (!dom) {
                    return selection;
                }
                if (options) {
                    skipSelf = options.skipSelf;
                    skipChildren = options.skipChildren;
                    excludeRoot = options.excludeRoot;
                    includeSaved = options.includeSaved;
                    includeHidden = options.includeHidden;
                }
                excludeRoot = excludeRoot && Ext.getDom(excludeRoot);
                if (excludeRoot && excludeRoot.contains(dom)) {
                    return selection;
                }
                if (!skipSelf && ((includeSaved && dom.hasAttribute(cAttr)) || this.isTabbable(includeHidden))) {
                    selection[idx++] = dom;
                }
                if (skipChildren) {
                    return selection;
                }
                nodes = dom.querySelectorAll(Ext.Element.tabbableSelector);
                len = nodes.length;
                if (!len) {
                    return selection;
                }
                fly = new Ext.dom.Fly();
                // We're only interested in the elements that an user can *tab into*,
                // not all programmatically focusable elements. So we have to filter
                // these out.
                for (i = 0; i < len; i++) {
                    node = nodes[i];
                    // A node with tabIndex < 0 absolutely can't be tabbable
                    // so we can save a function call if that is the case.
                    // Note that we can't use node.tabIndex here because IE
                    // will return 0 for elements that have no tabindex
                    // attribute defined, regardless of whether they are
                    // tabbable or not.
                    tabIndex = +node.getAttribute('tabIndex');
                    // quicker than parseInt
                    // tabIndex value may be null for nodes with no tabIndex defined;
                    // most of those may be naturally tabbable. We don't want to
                    // check this here, that's isTabbable()'s job and it's not trivial.
                    // We explicitly check that tabIndex is not negative. The expression
                    // below is purposeful if hairy; this is a very hot code path so care
                    // is taken to minimize the amount of DOM calls that could be avoided.
                    // A node may have its tabindex saved by previous calls to
                    // saveTabbableState(); in that case we need to return that node
                    // so that its saved counter could be properly incremented or
                    // decremented.
                    if (((includeSaved && node.hasAttribute(cAttr)) || (!(tabIndex < 0) && fly.attach(node).isTabbable(includeHidden))) && // eslint-disable-line max-len
                    !(excludeRoot && (excludeRoot === node || excludeRoot.contains(node)))) {
                        selection[idx++] = node;
                    }
                }
                return selection;
            },
            /**
             * @private
             */
            saveTabbableState: function(options) {
                var counterAttr = Ext.Element.tabbableSavedCounterAttribute,
                    savedAttr = Ext.Element.tabbableSavedValueAttribute,
                    counter, nodes, node, i, len;
                // By default include already saved tabbables, and just increase their save counter.
                // For example, if a View with saved tabbables is covered by a modal Window,
                // saveTabbableState must disable tabbability for the whole document.
                // But upon unmask, the View must not be restored to tabbability. It must only have
                // its save level decremented.
                // AbstractView#toggleChildrenTabbability however pases this as false so that
                // it may be called upon row add and it does not increment save levels on already
                // saved tabbables.
                if (!options || options.includeSaved == null) {
                    options = Ext.Object.chain(options || null);
                    options.includeSaved = true;
                }
                nodes = this.findTabbableElements(options);
                for (i = 0 , len = nodes.length; i < len; i++) {
                    node = nodes[i];
                    counter = +node.getAttribute(counterAttr);
                    if (counter > 0) {
                        node.setAttribute(counterAttr, ++counter);
                    } else {
                        // tabIndex could be set on both naturally tabbable and generic elements.
                        // Either way we need to save it to restore later.
                        if (node.hasAttribute('tabIndex')) {
                            node.setAttribute(savedAttr, node.getAttribute('tabIndex'));
                        } else // When no tabIndex is specified, that means a naturally tabbable element.
                        {
                            node.setAttribute(savedAttr, 'none');
                        }
                        // We disable the tabbable state by setting tabIndex to -1.
                        // The element can still be focused programmatically though.
                        node.setAttribute('tabIndex', '-1');
                        node.setAttribute(counterAttr, '1');
                    }
                }
                return nodes;
            },
            /**
             * @private
             */
            restoreTabbableState: function(options) {
                var dom = this.dom,
                    counterAttr = Ext.Element.tabbableSavedCounterAttribute,
                    savedAttr = Ext.Element.tabbableSavedValueAttribute,
                    nodes = [],
                    skipSelf = options && options.skipSelf,
                    skipChildren = options && options.skipChildren,
                    reset = options && options.reset,
                    idx, counter, node, i, len;
                if (!dom) {
                    return this;
                }
                if (!skipChildren) {
                    nodes = Ext.Array.from(dom.querySelectorAll('[' + counterAttr + ']'));
                }
                if (!skipSelf) {
                    nodes.unshift(dom);
                }
                for (i = 0 , len = nodes.length; i < len; i++) {
                    node = nodes[i];
                    if (!node.hasAttribute(counterAttr) || !node.hasAttribute(savedAttr)) {
                        
                        continue;
                    }
                    counter = +node.getAttribute(counterAttr);
                    if (!reset && counter > 1) {
                        node.setAttribute(counterAttr, --counter);
                        
                        continue;
                    }
                    idx = node.getAttribute(savedAttr);
                    // That is a naturally tabbable element
                    if (idx === 'none') {
                        node.removeAttribute('tabIndex');
                    } else {
                        node.setAttribute('tabIndex', idx);
                    }
                    node.removeAttribute(savedAttr);
                    node.removeAttribute(counterAttr);
                }
                return nodes;
            },
            /**
             * @private
             */
            setTabIndex: function(tabIndex) {
                var dom = this.dom,
                    savedAttr = Ext.Element.tabbableSavedValueAttribute;
                if (dom.hasAttribute(savedAttr)) {
                    if (tabIndex == null) {
                        // Equivalent to removing tabIndex while not saved
                        dom.setAttribute(savedAttr, 'none');
                        dom.removeAttribute('tabIndex');
                    } else {
                        dom.setAttribute(savedAttr, tabIndex);
                    }
                } else {
                    if (tabIndex == null) {
                        dom.removeAttribute('tabIndex');
                    } else {
                        dom.setAttribute('tabIndex', tabIndex);
                    }
                }
            },
            doAddListener: function(eventName, fn, scope, options, order, caller, manager) {
                var me = this,
                    originalName = eventName,
                    observableDoAddListener, additiveEventName, translatedEventName;
                // Even though the superclass method does conversion to lowercase, we need
                // to do it here because we need to use the lowercase name for lookup
                // in the event translation map.
                eventName = Ext.canonicalEventName(eventName);
                // Blocked events (such as emulated mouseover in mobile webkit) are prevented
                // from firing
                if (!me.blockedEvents[eventName]) {
                    observableDoAddListener = me.mixins.observable.doAddListener;
                    options = options || {};
                    if (Element.useDelegatedEvents === false) {
                        options.delegated = options.delegated || false;
                    }
                    if (options.translate !== false) {
                        // translate events where applicable.  This allows applications that
                        // were written for desktop to work on mobile devices and vice versa.
                        additiveEventName = me.additiveEvents[eventName];
                        if (additiveEventName) {
                            // additiveEvents means the translation is "additive" - meaning we
                            // need to attach the original event in addition to the translated
                            // one.  An example of this is devices that have both mousedown
                            // and touchstart
                            options.type = eventName;
                            eventName = additiveEventName;
                            observableDoAddListener.call(me, eventName, fn, scope, options, order, caller, manager);
                        }
                        translatedEventName = me.eventMap[eventName];
                        if (translatedEventName) {
                            // options.type may have already been set above
                            options.type = options.type || eventName;
                            if (manager) {
                                options.managedName = originalName;
                            }
                            eventName = translatedEventName;
                        }
                    }
                    if (observableDoAddListener.call(me, eventName, fn, scope, options, order, caller, manager)) {
                        // eslint-disable-line max-len
                        if (me.longpressEvents[eventName] && (++me.longpressListenerCount === 1)) {
                            me.on('MSHoldVisual', 'preventMsHoldVisual', me);
                        }
                    }
                    if (manager && translatedEventName) {
                        delete options.managedName;
                    }
                    // after the listener has been added to the ListenerStack, it's original
                    // "type" (for translated events) will be stored on the listener object in
                    // the ListenerStack.  We can now delete type from the options object
                    // since it is not a user-supplied option
                    delete options.type;
                }
            },
            doRemoveListener: function(eventName, fn, scope) {
                var me = this,
                    observableDoRemoveListener, translatedEventName, additiveEventName, removed;
                // Even though the superclass method does conversion to lowercase, we need
                // to do it here because we need to use the lowercase name for lookup
                // in the event translation map.
                eventName = Ext.canonicalEventName(eventName);
                // Blocked events (such as emulated mouseover in mobile webkit) are prevented
                // from firing
                if (!me.blockedEvents[eventName]) {
                    observableDoRemoveListener = me.mixins.observable.doRemoveListener;
                    // translate events where applicable.  This allows applications that
                    // were written for desktop to work on mobile devices and vice versa.
                    additiveEventName = me.additiveEvents[eventName];
                    if (additiveEventName) {
                        // additiveEvents means the translation is "additive" - meaning we
                        // need to remove the original event in addition to the translated
                        // one.  An example of this is devices that have both mousedown
                        // and touchstart
                        eventName = additiveEventName;
                        observableDoRemoveListener.call(me, eventName, fn, scope);
                    }
                    translatedEventName = me.eventMap[eventName];
                    if (translatedEventName) {
                        removed = observableDoRemoveListener.call(me, translatedEventName, fn, scope);
                    }
                    // no "else" here because we need to ensure that we remove translate:false
                    // listeners
                    removed = observableDoRemoveListener.call(me, eventName, fn, scope) || removed;
                    if (removed) {
                        if (me.longpressEvents[eventName] && !--me.longpressListenerCount) {
                            me.un('MSHoldVisual', 'preventMsHoldVisual', me);
                        }
                    }
                }
            },
            _initEvent: function(eventName) {
                return (this.events[eventName] = new Ext.dom.ElementEvent(this, eventName));
            },
            _getDisplay: function() {
                var data = this.getData(),
                    display = data[ORIGINALDISPLAY];
                if (display === undefined) {
                    data[ORIGINALDISPLAY] = display = '';
                }
                return display;
            },
            /**
             * Returns the publisher for a given event
             * @param {String} eventName
             * @param {Boolean} [noTranslate] `true` if the event is a non translated event
             * @private
             * @return {Ext.event.publisher.Publisher}
             */
            _getPublisher: function(eventName, noTranslate) {
                var Publisher = Ext.event.publisher.Publisher,
                    publisher = Publisher.publishersByEvent[eventName],
                    isNative = noTranslate && !Ext.event.Event.gestureEvents[eventName];
                // Dom publisher acts as the default publisher for all events that are
                // not explicitly handled by another publisher.
                // ElementSize handles the 'resize' event, except on the window
                // object, where it is handled by Dom publisher.
                // If the event is a native event (not translated), we want to use the
                // DOM publisher.
                // For example the dragstart gesture would automatically shadow any native
                // drag events, so we force the lower level publisher to be used. The exception
                // is for touch events, they all need to be handled by the gesture publisher
                // so they can be interrogated and produce the correct output.
                if (isNative || !publisher || (this.dom === window && eventName === 'resize')) {
                    publisher = Publisher.publishers.dom;
                }
                return publisher;
            },
            isFocusSuspended: function() {
                var data = this.peekData();
                return data && data.suspendFocusEvents;
            },
            preventMsHoldVisual: function(e) {
                e.preventDefault();
            },
            suspendFocusEvents: function() {
                if (!this.isFly) {
                    this.suspendEvent('focus', 'blur');
                }
                this.getData().suspendFocusEvents = true;
            },
            resumeFocusEvents: function() {
                function resumeFn() {
                    var data;
                    if (!this.destroyed) {
                        data = this.getData();
                        if (data) {
                            data.suspendFocusEvents = false;
                        }
                        if (!this.isFly) {
                            this.resumeEvent('focus', 'blur');
                        }
                    }
                }
                if (!this.destroyed && this.getData().suspendFocusEvents) {
                    if (Ext.isIE && !this.isFly) {
                        this.resumeFocusEventsTimer = Ext.asap(resumeFn, this);
                    } else {
                        resumeFn.call(this);
                    }
                }
            }
        },
        deprecated: {
            '5.0': {
                methods: {
                    /**
                     * @method getHTML
                     * @inheritdoc Ext.dom.Element#getHtml
                     * @deprecated 5.0.0 Please use {@link #getHtml} instead.
                     */
                    getHTML: 'getHtml',
                    /**
                     * @method getPageBox
                     * Returns an object defining the area of this Element which can be passed to
                     * {@link Ext.util.Positionable#setBox} to set another Element's size/location
                     * to match this element.
                     *
                     * @param {Boolean} [getRegion] If true an Ext.util.Region will be returned
                     * @return {Object/Ext.util.Region} box An object in the following format:
                     *
                     *     {
                     *         left: <Element's X position>,
                     *         top: <Element's Y position>,
                     *         width: <Element's width>,
                     *         height: <Element's height>,
                     *         bottom: <Element's lower bound>,
                     *         right: <Element's rightmost bound>
                     *     }
                     *
                     * The returned object may also be addressed as an Array where index 0 contains
                     * the X position and index 1 contains the Y position. So the result may also be
                     * used for {@link #setXY}
                     * @deprecated 5.0.0 use {@link Ext.util.Positionable#getBox} to get a box
                     * object, and {@link Ext.util.Positionable#getRegion} to get a
                     * {@link Ext.util.Region Region}.
                     */
                    getPageBox: function(getRegion) {
                        var me = this,
                            dom = me.dom,
                            isDoc = dom.nodeName === 'BODY',
                            w = isDoc ? Element.getViewportWidth() : dom.offsetWidth,
                            h = isDoc ? Element.getViewportHeight() : dom.offsetHeight,
                            xy = me.getXY(),
                            t = xy[1],
                            r = xy[0] + w,
                            b = xy[1] + h,
                            l = xy[0];
                        if (getRegion) {
                            return new Ext.util.Region(t, r, b, l);
                        } else {
                            return {
                                left: l,
                                top: t,
                                width: w,
                                height: h,
                                right: r,
                                bottom: b
                            };
                        }
                    },
                    /**
                     * @method isTransparent
                     * Returns `true` if the value of the given property is visually transparent.
                     * This may be due to a 'transparent' style value or an rgba value with 0
                     * in the alpha component.
                     * @param {String} prop The style property whose value is to be tested.
                     * @return {Boolean} `true` if the style property is visually transparent.
                     * @deprecated 5.0.0 This method is deprecated.
                     */
                    isTransparent: function(prop) {
                        var value = this.getStyle(prop);
                        return value ? transparentRe.test(value) : false;
                    },
                    /**
                     * @method purgeAllListeners
                     * @inheritdoc Ext.dom.Element#clearListeners
                     * @deprecated 5.0.0 Please use {@link #clearListeners} instead.
                     */
                    purgeAllListeners: 'clearListeners',
                    /**
                     * @method removeAllListeners
                     * @inheritdoc Ext.dom.Element#clearListeners
                     * @deprecated 5.0.0 Please use {@link #clearListeners} instead.
                     */
                    removeAllListeners: 'clearListeners',
                    /**
                     * @method setHTML
                     * @inheritdoc Ext.dom.Element#setHtml
                     * @deprecated 5.0.0 Please use {@link #setHtml} instead.
                     */
                    setHTML: 'setHtml'
                }
            }
        }
    };
}, function(Element) {
    var DOC = document,
        docEl = DOC.documentElement,
        prototype = Element.prototype,
        supports = Ext.supports,
        pointerdown = 'pointerdown',
        pointermove = 'pointermove',
        pointerup = 'pointerup',
        pointercancel = 'pointercancel',
        MSPointerDown = 'MSPointerDown',
        MSPointerMove = 'MSPointerMove',
        MSPointerUp = 'MSPointerUp',
        MSPointerCancel = 'MSPointerCancel',
        mousedown = 'mousedown',
        mousemove = 'mousemove',
        mouseup = 'mouseup',
        mouseover = 'mouseover',
        mouseout = 'mouseout',
        mouseenter = 'mouseenter',
        mouseleave = 'mouseleave',
        touchstart = 'touchstart',
        touchmove = 'touchmove',
        touchend = 'touchend',
        touchcancel = 'touchcancel',
        click = 'click',
        dblclick = 'dblclick',
        tap = 'tap',
        doubletap = 'doubletap',
        eventMap = prototype.eventMap = {},
        additiveEvents = prototype.additiveEvents = {},
        oldId = Ext.id,
        eventOptions;
    prototype._init(Element);
    delete prototype._init;
    /**
     * Generates unique ids. If the element already has an id, it is unchanged
     * @member Ext
     * @param {Object/HTMLElement/Ext.dom.Element} [obj] The element to generate an id for
     * @param {String} prefix (optional) Id prefix (defaults "ext-gen")
     * @return {String} The generated Id.
     */
    Ext.id = function(obj, prefix) {
        var el = obj && Ext.getDom(obj, true),
            sandboxPrefix, id;
        if (!el) {
            id = oldId(obj, prefix);
        } else if (!(id = el.id)) {
            id = oldId(null, prefix || Element.prototype.identifiablePrefix);
            if (Ext.isSandboxed) {
                sandboxPrefix = Ext.sandboxPrefix || (Ext.sandboxPrefix = Ext.sandboxName.toLowerCase() + '-');
                id = sandboxPrefix + id;
            }
            el.id = id;
        }
        return id;
    };
    if (supports.PointerEvents) {
        eventMap[mousedown] = pointerdown;
        eventMap[mousemove] = pointermove;
        eventMap[mouseup] = pointerup;
        eventMap[touchstart] = pointerdown;
        eventMap[touchmove] = pointermove;
        eventMap[touchend] = pointerup;
        eventMap[touchcancel] = pointercancel;
        // On devices that support pointer events we block pointerover, pointerout,
        // pointerenter, and pointerleave when triggered by touch input (see
        // Ext.event.publisher.Dom#blockedPointerEvents).  This is because mouseover
        // behavior is typically not desired when touching the screen.  This covers the
        // use case where user code requested a pointer event, however mouseover/mouseout
        // events are not cancellable, period.
        // http://www.w3.org/TR/pointerevents/#mapping-for-devices-that-do-not-support-hover
        // To ensure mouseover/out handlers don't fire when touching the screen, we need
        // to translate them to their pointer equivalents
        eventMap[mouseover] = 'pointerover';
        eventMap[mouseout] = 'pointerout';
        eventMap[mouseenter] = 'pointerenter';
        // No decent way to feature detect this, pointerleave relatedTarget is
        // incorrect on IE11, so force it to use mouseleave here.
        // See: https://connect.microsoft.com/IE/feedback/details/851111/ev-relatedtarget-in-pointerleave-indicates-departure-element-not-destination-element
        if (!Ext.isIE11) {
            eventMap[mouseleave] = 'pointerleave';
        }
    } else if (supports.MSPointerEvents) {
        // IE10
        eventMap[pointerdown] = MSPointerDown;
        eventMap[pointermove] = MSPointerMove;
        eventMap[pointerup] = MSPointerUp;
        eventMap[pointercancel] = MSPointerCancel;
        eventMap[mousedown] = MSPointerDown;
        eventMap[mousemove] = MSPointerMove;
        eventMap[mouseup] = MSPointerUp;
        eventMap[touchstart] = MSPointerDown;
        eventMap[touchmove] = MSPointerMove;
        eventMap[touchend] = MSPointerUp;
        eventMap[touchcancel] = MSPointerCancel;
        // translate mouseover/out so they can be prevented on touch screens.
        // (see above comment in the PointerEvents section)
        eventMap[mouseover] = 'MSPointerOver';
        eventMap[mouseout] = 'MSPointerOut';
    } else if (supports.TouchEvents) {
        eventMap[pointerdown] = touchstart;
        eventMap[pointermove] = touchmove;
        eventMap[pointerup] = touchend;
        eventMap[pointercancel] = touchcancel;
        eventMap[mousedown] = touchstart;
        eventMap[mousemove] = touchmove;
        eventMap[mouseup] = touchend;
        eventMap[click] = tap;
        eventMap[dblclick] = doubletap;
        if (Ext.os.is.Desktop) {
            // Touch enabled desktop browsers on windows such as Firefox and Chrome fire
            // both mouse events and touch events, so we have to attach listeners for both
            // kinds when either one is requested.  There are a couple rules to keep in mind:
            // 1. When the mouse is used, only a mouse event is fired
            // 2. When interacting with the touch screen touch events are fired.
            // 3. After a touchstart/touchend sequence, if there was no touchmove in
            // between, the browser will fire a mousemove/mousedown/mousup sequence
            // immediately after.  This can cause problems because if we are listening
            // for both kinds of events, handlers may run twice.  To work around this
            // issue we filter out the duplicate emulated mouse events by checking their
            // coordinates and timing (see Ext.event.publisher.Gesture#onDelegatedEvent)
            eventMap[touchstart] = mousedown;
            eventMap[touchmove] = mousemove;
            eventMap[touchend] = mouseup;
            eventMap[touchcancel] = mouseup;
            additiveEvents[mousedown] = mousedown;
            additiveEvents[mousemove] = mousemove;
            additiveEvents[mouseup] = mouseup;
            additiveEvents[touchstart] = touchstart;
            additiveEvents[touchmove] = touchmove;
            additiveEvents[touchend] = touchend;
            additiveEvents[touchcancel] = touchcancel;
            additiveEvents[pointerdown] = mousedown;
            additiveEvents[pointermove] = mousemove;
            additiveEvents[pointerup] = mouseup;
            additiveEvents[pointercancel] = mouseup;
        }
    } else {
        // browser does not support either pointer or touch events, map all pointer and
        // touch events to their mouse equivalents
        eventMap[pointerdown] = mousedown;
        eventMap[pointermove] = mousemove;
        eventMap[pointerup] = mouseup;
        eventMap[pointercancel] = mouseup;
        eventMap[touchstart] = mousedown;
        eventMap[touchmove] = mousemove;
        eventMap[touchend] = mouseup;
        eventMap[touchcancel] = mouseup;
    }
    if (Ext.isWebKit) {
        // These properties were carried forward from touch-2.x. This translation used
        // do be done by DomPublisher.  TODO: do we still need this?
        eventMap.transitionend = Ext.browser.getVendorProperyName('transitionEnd');
        eventMap.animationstart = Ext.browser.getVendorProperyName('animationStart');
        eventMap.animationend = Ext.browser.getVendorProperyName('animationEnd');
    }
    if (!Ext.supports.MouseWheel && !Ext.isOpera) {
        eventMap.mousewheel = 'DOMMouseScroll';
    }
    eventOptions = prototype.$eventOptions = Ext.Object.chain(prototype.$eventOptions);
    eventOptions.translate = eventOptions.capture = eventOptions.delegate = eventOptions.delegated = eventOptions.stopEvent = eventOptions.preventDefault = eventOptions.stopPropagation = // Ext.Element also needs "element" as one of its event options.  Even though
    // it does not directly process an element option, it may receive a listeners
    // object that was passed through from a Component with the "element" option
    // included. Including "element" in the event options ensures we don't attempt
    // to process "element" as an event name.
    eventOptions.element = 1;
    prototype.styleHooks.opacity = {
        name: 'opacity',
        afterSet: function(dom, value, el) {
            var shadow = el.shadow;
            if (shadow) {
                shadow.setOpacity(value);
            }
        }
    };
    /**
     * @member Ext
     * @private
     * Returns the `X,Y` position of this element without regard to any RTL
     * direction settings.
     */
    prototype.getTrueXY = prototype.getXY;
    /**
     * @member Ext
     * @method getViewportHeight
     * @inheritdoc Ext.dom.Element#getViewportHeight
     * @since 6.5.0
     */
    Ext.getViewportHeight = Element.getViewportHeight;
    /**
     * @member Ext
     * @method getViewportWidth
     * @inheritdoc Ext.dom.Element#getViewportWidth
     * @since 6.5.0
     */
    Ext.getViewportWidth = Element.getViewportWidth;
    /**
     * @member Ext
     * @method select
     * Shorthand for {@link Ext.dom.Element#method-select Ext.dom.Element.select}<br><br>
     * @inheritdoc Ext.dom.Element#method-select
     */
    Ext.select = Element.select;
    /**
     * @member Ext
     * @method query
     * Shorthand for {@link Ext.dom.Element#method-query Ext.dom.Element.query}<br><br>
     * @inheritdoc Ext.dom.Element#method-query
     */
    Ext.query = Element.query;
    Ext.apply(Ext, {
        /**
         * @member Ext
         * @method get
         */
        get: function(element) {
            return Element.get(element);
        },
        /**
         * @member Ext
         * @method getDom
         * Return the dom node for the passed String (id), dom node, or Ext.Element.
         * Here are some examples:
         *
         *     // gets dom node based on id
         *     var elDom = Ext.getDom('elId');
         *
         *     // gets dom node based on the dom node
         *     var elDom1 = Ext.getDom(elDom);
         *
         *     // If we don't know if we are working with an
         *     // Ext.Element or a dom node use Ext.getDom
         *     function(el){
         *         var dom = Ext.getDom(el);
         *         // do something with the dom node
         *     }
         *
         * __Note:__ the dom node to be found actually needs to exist (be rendered, etc)
         * when this method is called to be successful.
         *
         * @param {String/HTMLElement/Ext.dom.Element} el
         * @return {HTMLElement}
         */
        getDom: function(el) {
            if (!el || !DOC) {
                return null;
            }
            // We could be passed an Element whos dom has been nulled on destruction;
            // use 'dom' in rather than truthiness.
            return typeof el === 'string' ? Ext.getElementById(el) : 'dom' in el ? el.dom : el;
        },
        /**
         * @member Ext
         * Returns the current document body as an {@link Ext.dom.Element}.
         * @return {Ext.dom.Element} The document body.
         */
        getBody: function() {
            if (!Ext._bodyEl) {
                if (!DOC.body) {
                    throw new Error("[Ext.getBody] document.body does not yet exist");
                }
                Ext._bodyEl = Ext.get(DOC.body);
                Ext._bodyEl.skipGarbageCollection = true;
            }
            return Ext._bodyEl;
        },
        /**
         * @member Ext
         * Returns the current document head as an {@link Ext.dom.Element}.
         * @return {Ext.dom.Element} The document head.
         */
        getHead: function() {
            if (!Ext._headEl) {
                Ext._headEl = Ext.get(DOC.head || DOC.getElementsByTagName('head')[0]);
                Ext._headEl.skipGarbageCollection = true;
            }
            return Ext._headEl;
        },
        /**
         * @member Ext
         * Returns the current HTML document object as an {@link Ext.dom.Element}.
         * Typically used for attaching event listeners to the document.  Note: since
         * the document object is not an HTMLElement many of the Ext.dom.Element methods
         * are not applicable and may throw errors if called on the returned
         * Element instance.
         * @return {Ext.dom.Element} The document.
         */
        getDoc: function() {
            if (!Ext._docEl) {
                Ext._docEl = Ext.get(DOC);
                Ext._docEl.skipGarbageCollection = true;
            }
            return Ext._docEl;
        },
        /**
         * @member Ext
         * Returns the current window object as an {@link Ext.dom.Element}.
         * Typically used for attaching event listeners to the window.  Note: since
         * the window object is not an HTMLElement many of the Ext.dom.Element methods
         * are not applicable and may throw errors if called on the returned
         * Element instance.
         * @return {Ext.dom.Element} The window.
         */
        getWin: function() {
            if (!Ext._winEl) {
                Ext._winEl = Ext.get(window);
                Ext._winEl.skipGarbageCollection = true;
            }
            return Ext._winEl;
        },
        /**
         * @member Ext
         * Removes an HTMLElement from the document.  If the HTMLElement was previously
         * cached by a call to Ext.get(), removeNode will call the {@link Ext.Element#destroy
         * destroy} method of the {@link Ext.dom.Element} instance, which removes all DOM
         * event listeners, and deletes the cache reference.
         * @param {HTMLElement} node The node to remove
         * @method
         */
        removeNode: function(node) {
            node = node.dom || node;
            /* eslint-disable-next-line vars-on-top */
            var id = node && node.id,
                el = Ext.cache[id],
                parent;
            if (el) {
                el.destroy();
            } else if (node && (node.nodeType === 3 || node.tagName.toUpperCase() !== 'BODY')) {
                parent = node.parentNode;
                if (parent) {
                    parent.removeChild(node);
                }
            }
        }
    });
    // TODO: make @inline work - SDKTOOLS-686
    // @inline
    Ext.isGarbage = function(dom) {
        // determines if the dom element is in the document or in the detached body element
        // use by collectGarbage and Ext.get()
        return dom && // window, document, documentElement, and body can never be garbage.
        dom.nodeType === 1 && dom.tagName !== 'BODY' && dom.tagName !== 'HTML' && (// if the element does not have a parent node, it is definitely not in the
        // DOM - we can exit immediately
        !dom.parentNode || (// If the element has an offsetParent we can bail right away, it is
        // definitely in the DOM. If offsetParent is null, the element is detached.
        // If offsetParent is undefined, the element doesn't support offsetParent
        // (e.g. SVGElement) and is not necessarily garbage; parentNode check above
        // should be sufficient in this case.
        dom.offsetParent === null && // if the element does not have an offsetParent it can mean the element is
        // either not in the dom or it is hidden.  The next step is to check to see
        // if it can be found by id using either document.all or getElementById(),
        // whichever is faster for the current browser.  Normally we would not
        // include IE-specific checks in the core package, however,  in this
        // case the function will be inlined and therefore cannot be overridden in
        // the ext package.
        ((Ext.isIE8 ? DOC.all[dom.id] : DOC.getElementById(dom.id)) !== dom) && // finally if the element was not found in the dom by id, we need to check
        // the detachedBody element
        !(Ext.detachedBodyEl && Ext.detachedBodyEl.isAncestor(dom))));
    };
    Ext.onInternalReady(function() {
        var bodyCls = [],
            theme;
        // Element.unselectable relies on this listener to prevent selection in IE. Some other
        // browsers support the event too but it is only strictly required for IE. In WebKit
        // this listener causes subtle differences to how the browser handles the non-selection,
        // e.g. whether or not the mouse cursor changes when attempting to select text.
        Ext.getDoc().on('selectstart', function(ev, dom) {
            var selectableCls = Element.selectableCls,
                unselectableCls = Element.unselectableCls,
                tagName = dom && dom.tagName,
                el = new Ext.dom.Fly();
            tagName = tagName && tagName.toLowerCase();
            // Element.unselectable is not really intended to handle selection within text fields
            // and it is important that fields inside menus or panel headers don't inherit
            // the unselectability. In most browsers this is automatic but in IE 9 the selectstart
            // event can bubble up from text fields so we have to explicitly handle that case.
            if (tagName === 'input' || tagName === 'textarea') {
                return;
            }
            // Walk up the DOM checking the nodes. This may be 'slow' but selectstart events
            // don't fire very often
            while (dom && dom.nodeType === 1 && dom !== DOC.documentElement) {
                el.attach(dom);
                // If the node has the class x-selectable then stop looking, the text selection
                // is allowed
                if (el.hasCls(selectableCls)) {
                    return;
                }
                // If the node has class x-unselectable then the text selection needs to be stopped
                if (el.hasCls(unselectableCls)) {
                    ev.stopEvent();
                    return;
                }
                dom = dom.parentNode;
            }
        });
        if (Ext.os.is.Android || (Ext.os.is.Windows && Ext.supports.Touch)) {
            // Some mobile devices (android and windows) fire window resize events
            // When the virtual keyboard is displayed. This causes unexpected visual
            // results due to extra layouts of the viewport.  Here we attach a couple
            // of event listeners that will help us detect if the virtual keyboard
            // is open so tha getViewportWidth/getViewportHeight can report the
            // original size as the viewport size while the keyboard is open
            var win = Ext.getWin();
            // eslint-disable-line vars-on-top, one-var
            Element._documentWidth = Element._viewportWidth = docEl.clientWidth;
            Element._documentHeight = Element._viewportHeight = docEl.clientHeight;
            win.on({
                // Focus in/out listeners track the last focus change so we can detect
                // the proximity of the last focus change relative to window resize events
                // alowing us to guess with reasonable certainty that a virtual keyboard
                // is being shown.
                focusin: '_onWindowFocusChange',
                focusout: '_onWindowFocusChange',
                // This pointerup listener is needed because in windowsthe virtual keyboard
                // can be hidden manually while the editable element retains focus by tapping
                // a hide button on the virtual keyboard itself. The virtual keyboard can then
                // be re-shown by tapping on the editable element.  In this case the editable
                // element does not fire a focusin event since it already has the focus, but
                // we still need to track that an event occurred which will cause the virtual
                // keyboard to show momentarily.
                pointerup: '_onWindowFocusChange',
                capture: true,
                delegated: false,
                delay: 1,
                scope: Element
            });
            win.on({
                // Resize listener for tracking virtual keyboard state.
                resize: '_onWindowResize',
                priority: 2000,
                scope: Element
            });
        }
        if (supports.Touch) {
            bodyCls.push(Ext.baseCSSPrefix + 'touch');
        }
        if (Ext.isIE && Ext.isIE9m) {
            bodyCls.push(Ext.baseCSSPrefix + 'ie', Ext.baseCSSPrefix + 'ie9m');
            // very often CSS needs to do checks like "IE7+" or "IE6 or 7". To help
            // reduce the clutter (since CSS/SCSS cannot do these tests), we add some
            // additional classes:
            //
            //      x-ie7p      : IE7+      :  7 <= ieVer
            //      x-ie7m      : IE7-      :  ieVer <= 7
            //      x-ie8p      : IE8+      :  8 <= ieVer
            //      x-ie8m      : IE8-      :  ieVer <= 8
            //      x-ie9p      : IE9+      :  9 <= ieVer
            //      x-ie78      : IE7 or 8  :  7 <= ieVer <= 8
            //
            bodyCls.push(Ext.baseCSSPrefix + 'ie8p');
            if (Ext.isIE8) {
                bodyCls.push(Ext.baseCSSPrefix + 'ie8');
            } else {
                bodyCls.push(Ext.baseCSSPrefix + 'ie9', Ext.baseCSSPrefix + 'ie9p');
            }
            if (Ext.isIE8m) {
                bodyCls.push(Ext.baseCSSPrefix + 'ie8m');
            }
        }
        if (Ext.isIE10) {
            bodyCls.push(Ext.baseCSSPrefix + 'ie10');
        }
        if (Ext.isIE10p) {
            bodyCls.push(Ext.baseCSSPrefix + 'ie10p');
        }
        if (Ext.isIE11) {
            bodyCls.push(Ext.baseCSSPrefix + 'ie11');
        }
        if (Ext.isEdge) {
            bodyCls.push(Ext.baseCSSPrefix + 'edge');
        }
        if (Ext.isGecko) {
            bodyCls.push(Ext.baseCSSPrefix + 'gecko');
        }
        if (Ext.isOpera) {
            bodyCls.push(Ext.baseCSSPrefix + 'opera');
        }
        if (Ext.isOpera12m) {
            bodyCls.push(Ext.baseCSSPrefix + 'opera12m');
        }
        if (Ext.isWebKit) {
            bodyCls.push(Ext.baseCSSPrefix + 'webkit');
        }
        if (Ext.isSafari) {
            bodyCls.push(Ext.baseCSSPrefix + 'safari');
        }
        if (Ext.isSafari9) {
            bodyCls.push(Ext.baseCSSPrefix + 'safari9');
        }
        if (Ext.isSafari10) {
            bodyCls.push(Ext.baseCSSPrefix + 'safari10');
        }
        if (Ext.isSafari) {
            if (Ext.browser.version.isLessThan(11)) {
                bodyCls.push(Ext.baseCSSPrefix + 'safari10m');
            }
            if (Ext.browser.version.isLessThan(9)) {
                bodyCls.push(Ext.baseCSSPrefix + 'safari8m');
            }
        }
        if (Ext.isChrome) {
            bodyCls.push(Ext.baseCSSPrefix + 'chrome');
        }
        if (Ext.isMac) {
            bodyCls.push(Ext.baseCSSPrefix + 'mac');
        }
        if (Ext.isWindows) {
            bodyCls.push(Ext.baseCSSPrefix + 'windows');
        }
        if (Ext.isLinux) {
            bodyCls.push(Ext.baseCSSPrefix + 'linux');
        }
        if (!supports.CSS3BorderRadius) {
            bodyCls.push(Ext.baseCSSPrefix + 'nbr');
        }
        if (!supports.CSS3LinearGradient) {
            bodyCls.push(Ext.baseCSSPrefix + 'nlg');
        }
        if (supports.Touch) {
            bodyCls.push(Ext.baseCSSPrefix + 'touch');
        }
        if (Ext.os.deviceType) {
            bodyCls.push(Ext.baseCSSPrefix + Ext.os.deviceType.toLowerCase());
        }
        if (Ext.os.is.BlackBerry) {
            bodyCls.push(Ext.baseCSSPrefix + 'bb');
            if (Ext.browser.userAgent.match(/Kbd/gi)) {
                // blackberry with physical keyboard
                bodyCls.push(Ext.baseCSSPrefix + 'bb-keyboard');
            }
        }
        if (Ext.os.is.iOS && Ext.isSafari) {
            bodyCls.push(Ext.baseCSSPrefix + 'mobile-safari');
        }
        if (Ext.os.is.iOS && Ext.browser.is.WebView && !Ext.browser.is.Standalone) {
            // ios cordova app
            bodyCls.push(Ext.baseCSSPrefix + 'ios-native');
        }
        if (Ext.supports.FlexBoxBasisBug) {
            bodyCls.push(Ext.baseCSSPrefix + 'has-flexbasis-bug');
        }
        Ext.getBody().addCls(bodyCls);
        theme = Ext.theme;
        if (theme && theme.getDocCls) {
            // hook for theme overrides to add css classes to the <html> element
            Ext.fly(document.documentElement).addCls(theme.getDocCls());
        }
    }, null, {
        priority: 1500
    });
});

/**
 * @private
 */
Ext.define('Ext.util.BasicFilter', {
    isFilter: true,
    config: {
        /**
         * @cfg {String} id
         * An identifier by which this Filter is known, for example, as a member of a
         * {@link Ext.data.Store#cfg-filters Store's filters collection}.
         *
         * Identified filters are manageable in such collections because they can be found
         * or removed using their `id`.
         */
        id: null,
        /**
         * @cfg {Boolean} disabled
         * Setting this property to `true` disables this individual filter.
         */
        disabled: false,
        /**
         * @cfg {Function} serializer
         * A function to post-process any serialization. Accepts the serialized filter
         * containing `property`, `value` and `operator` properties, and may either
         * mutate it, or return a completely new representation. Returning a falsy
         * value does not modify the representation.
         * @since 6.2.0
         */
        serializer: null
    },
    /**
     * @property {Number} generation
     * this property is a Mutation counter which is incremented whenever the filter changes
     * in a way that may change either its serialized form or its result.
     * @readonly
     * @since 6.5.0
     */
    generation: 0,
    /**
     * Initializes a filter.
     * @param {Object} config The config object
     */
    constructor: function(config) {
        this.initConfig(config);
    },
    updateDisabled: function() {
        // Developers may use this to see if a filter has changed in ways that must cause
        // a reevaluation of filtering
        if (!this.isConfiguring) {
            ++this.generation;
        }
    }
});
/**
     * @method filter
     * @param {Object} item
     * @return {Boolean}
     */
/**
     * @method serialize
     * Returns this filter's serialized state. This is used when transmitting this filter
     * to a server.
     * @return {Object}
     */
/**
     * Serialize this filter into the `out` array (if possible).
     * @param {Array} out The array of simple and-able filters.
     * @return {Boolean} `true` if any saved filters require encoding
     * @method serializeTo
     * @private
     */

/**
 * A filter that can be applied to an `Ext.util.Collection` or other data container such
 * an `Ext.data.Store`. A `Filter` can be simply a filter on a `property` and `value` pair
 * or a filter function with custom logic.
 *
 * Normally filters are added to stores or collections but they can be created directly:
 *
 *     var ageFilter = new Ext.util.Filter({
 *         property: 'age',
 *         value: 42,
 *         operator: '<'
 *     });
 *
 *     var longNameFilter = new Ext.util.Filter({
 *         filterFn: function(item) {
 *             return item.name.length > 4;
 *         }
 *     });
 */
Ext.define('Ext.util.Filter', {
    extend: 'Ext.util.BasicFilter',
    config: {
        /**
         * @cfg {String} property
         * The property to filter on. Required unless a {@link #filterFn} is passed.
         */
        property: null,
        /**
         * @cfg {String} root
         * This property is used to descend items to check for meaningful properties on
         * which to filter. For a `Ext.data.Model` for example this would be `'data'`.
         */
        root: null,
        /**
         * @cfg {RegExp/Mixed} value
         * The value you want to match against. Required unless a {@link #filterFn} is passed.
         * 
         * Can be a regular expression which will be used as a matcher or any other value
         * such as an object or an array of objects. This value is compared using the configured
         * {@link #operator}.
         */
        value: null,
        /**
         * @cfg {Function} filterFn
         * A custom filter function which is passed each item. This function must return
         * `true` to accept an item or `false` to reject it.
         */
        filterFn: null,
        /**
         * @cfg {Boolean} anyMatch
         * True to allow any match - no regex start/end line anchors will be added.
         */
        anyMatch: false,
        /**
         * @cfg {Boolean} exactMatch
         * True to force exact match (^ and $ characters added to the regex). Ignored if
         * `anyMatch` is `true`.
         */
        exactMatch: false,
        /**
         * @cfg {Boolean} caseSensitive
         * True to make the regex case sensitive (adds 'i' switch to regex).
         */
        caseSensitive: false,
        /**
         * @cfg {Boolean} disableOnEmpty
         * `true` to not have this filter participate in the filtering process when the
         * {@link #value} of this the filter is empty according to {@link Ext#isEmpty}.
         *
         * @since 5.1.0
         */
        disableOnEmpty: false,
        /**
         * @cfg {String} operator
         * The operator to use to compare the {@link #cfg!property} to this Filter's
         * {@link #cfg!value}.
         *
         * Possible values are:
         *
         *    * `<`
         *    * `<=`
         *    * `=`
         *    * `>=`
         *    * `>`
         *    * `!=`
         *    * `in`
         *    * `notin`
         *    * `like`
         *    * `/=`
         *
         * The `in` and `notin` operator expects this filter's {@link #cfg-value} to be
         * an array and matches values that are present in that array.
         * 
         * The `like` operator matches values that contain this filter's {@link #cfg-value}
         * as a substring.
         *
         * The `/=` operator uses the {@link #cfg-value} as the source for a `RegExp` and
         * tests whether the candidate value matches the regular expression.
         */
        operator: null,
        /**
         * @cfg {Function} [convert]
         * A function to do any conversion on the value before comparison. For example,
         * something that returns the date only part of a date.
         * @cfg {Object} convert.value The value to convert.
         * @cfg {Object} convert.return The converted value.
         * @private
         */
        convert: null
    },
    /**
     * @cfg {Object} [scope]
     * The context (`this` property) in which the filtering function is called. Defaults
     * to this Filter object.
     */
    scope: null,
    // Needed for scope above. If `scope` were a "config" it would be merged and lose its
    // identity.
    $configStrict: false,
    statics: {
        /**
         * Creates a single filter function which encapsulates the passed Filter array or
         * Collection.
         * @param {Ext.util.Filter[]/Ext.util.Collection} filters The filters from which to
         * create a filter function.
         * @return {Function} A function, which when passed a candidate object returns `true`
         * if the candidate passes all the specified Filters.
         */
        createFilterFn: function(filters) {
            if (!filters) {
                return Ext.returnTrue;
            }
            return function(candidate) {
                var items = filters.isCollection ? filters.items : filters,
                    length = items.length,
                    match = true,
                    i, filter;
                for (i = 0; match && i < length; i++) {
                    filter = items[i];
                    // Skip disabled filters
                    if (!filter.getDisabled()) {
                        match = filter.filter(candidate);
                    }
                }
                return match;
            };
        },
        /**
         * Checks if two filters have the same properties (Property, Operator and Value).
         *
         * @param {Ext.util.Filter} filter1 The first filter to be compared
         * @param {Ext.util.Filter} filter2 The second filter to be compared
         * @return {Boolean} `true` if they have the same properties.
         * @since 6.2.0
         */
        isEqual: function(filter1, filter2) {
            if (filter1.getProperty() !== filter2.getProperty()) {
                return false;
            }
            if (filter1.getOperator() !== filter2.getOperator()) {
                return false;
            }
            if (filter1.getValue() === filter2.getValue()) {
                return true;
            }
            if (Ext.isArray(filter1) && Ext.isArray(filter2)) {
                return Ext.Array.equals(filter1, filter2);
            }
            return false;
        },
        /**
         * Checks whether the filter will produce a meaningful value. Since filters
         * may be used in conjunction with data binding, this is a sanity check to
         * check whether the resulting filter will be able to match.
         * 
         * @param {Object} cfg The filter config object
         * @return {Boolean/String} `true` if the filter will produce a valid value
         * 
         * @private
         */
        isInvalid: function(cfg) {
            if (!cfg.filterFn) {
                // If we don't have a filterFn, we must have a property
                if (!cfg.property) {
                    return 'A Filter requires either a property or a filterFn to be set';
                }
                if (!cfg.hasOwnProperty('value') && !cfg.operator) {
                    return 'A Filter requires either a property and value, or a filterFn to be set';
                }
            }
            return false;
        }
    },
    constructor: function(config) {
        var warn = Ext.util.Filter.isInvalid(config);
        if (warn) {
            Ext.log.warn(warn);
        }
        this.callParent([
            config
        ]);
    },
    preventConvert: {
        'in': 1,
        notin: 1
    },
    filter: function(item) {
        var me = this,
            filterFn = me._filterFn || me.getFilterFn(),
            convert = me.getConvert(),
            value = me._value;
        me._filterValue = value;
        me.isDateValue = Ext.isDate(value);
        if (me.isDateValue) {
            me.dateValue = value.getTime();
        }
        if (convert && !me.preventConvert[me.getOperator()]) {
            me._filterValue = convert.call(me.scope || me, value);
        }
        return filterFn.call(me.scope || me, item);
    },
    getId: function() {
        var me = this,
            id = me._id;
        if (!id) {
            id = me.getProperty();
            if (!id) {
                id = Ext.id(null, 'ext-filter-');
            }
            me._id = id;
        }
        return id;
    },
    getFilterFn: function() {
        var me = this,
            filterFn = me._filterFn,
            operator;
        if (!filterFn) {
            operator = me.getOperator();
            if (operator) {
                filterFn = me.operatorFns[operator];
            } else {
                // This part is broken our into its own method so the function expression
                // contained there does not get hoisted and created on each call this
                // method.
                filterFn = me.createRegexFilter();
            }
            me._filterFn = filterFn;
            // Mark as generated by default. This becomes important when proxies encode
            // filters.  See proxy.Server#encodeFilters().
            me.generatedFilterFn = true;
        }
        return filterFn;
    },
    /**
     * @private
     * Creates a filter function for the configured value/anyMatch/caseSensitive options
     * for this Filter.
     */
    createRegexFilter: function() {
        var me = this,
            anyMatch = !!me.getAnyMatch(),
            exact = !!me.getExactMatch(),
            value = me.getValue(),
            matcher = Ext.String.createRegex(value, !anyMatch, // startsWith
            !anyMatch && exact, // endsWith
            !me.getCaseSensitive());
        return function(item) {
            var val = me.getPropertyValue(item);
            return matcher ? matcher.test(val) : (val == null);
        };
    },
    /**
     * Returns the property of interest from the given item, based on the configured `root`
     * and `property` configs.
     * @param {Object} item The item.
     * @return {Object} The property of the object.
     * @private
     */
    getPropertyValue: function(item) {
        var root = this._root,
            value = (root == null) ? item : item[root];
        return value[this._property];
    },
    /**
     * Returns this filter's state.
     * @return {Object}
     */
    getState: function() {
        var config = this.getInitialConfig(),
            result = {},
            name;
        for (name in config) {
            // We only want the instance properties in this case, not inherited ones,
            // so we need hasOwnProperty to filter out our class values.
            if (config.hasOwnProperty(name)) {
                result[name] = config[name];
            }
        }
        delete result.root;
        result.value = this.getValue();
        return result;
    },
    getScope: function() {
        return this.scope;
    },
    /**
     * Returns this filter's serialized state. This is used when transmitting this filter
     * to a server.
     * @return {Object}
     */
    serialize: function() {
        var result = this.getState(),
            serializer = this.getSerializer(),
            serialized;
        delete result.id;
        delete result.serializer;
        if (serializer) {
            serialized = serializer.call(this, result);
            if (serialized) {
                result = serialized;
            }
        }
        return result;
    },
    serializeTo: function(out) {
        var me = this,
            primitive, serialized;
        // Filters with a custom filterFn cannot be serialized.  But since #getFilterFn()
        // always returns a filterFn, we need to check if it's been generated by default.
        // If so, we know that the filter cannot have a custom filterFn defined, and it
        // is therefore okay to serialize.
        me.getFilterFn();
        if (me.generatedFilterFn) {
            out.push(serialized = me.serialize());
            primitive = me.primitiveRe.test(typeof serialized);
        }
        return !primitive;
    },
    updateOperator: function() {
        // Need to clear any generated local filter fn and increment generation
        this.onConfigMutation();
    },
    updateConvert: function() {
        // Need to clear any generated local filter fn and increment generation
        this.onConfigMutation();
    },
    updateProperty: function() {
        // Need to clear any generated local filter fn and increment generation
        this.onConfigMutation();
    },
    updateAnyMatch: function() {
        // Need to clear any generated local filter fn and increment generation
        this.onConfigMutation();
    },
    updateExactMatch: function() {
        // Need to clear any generated local filter fn and increment generation
        this.onConfigMutation();
    },
    updateCaseSensitive: function() {
        // Need to clear any generated local filter fn and increment generation
        this.onConfigMutation();
    },
    updateValue: function(value) {
        // Need to clear any generated local filter fn and increment generation
        this.onConfigMutation();
        if (this.getDisableOnEmpty()) {
            this.setDisabled(Ext.isEmpty(value));
        }
    },
    updateFilterFn: function(filterFn) {
        delete this.generatedFilterFn;
    },
    onConfigMutation: function() {
        // Developers may use this to see if a filter has changed in ways that must cause
        // a reevaluation of filtering
        this.generation++;
        if (this.generatedFilterFn) {
            this._filterFn = null;
        }
    },
    updateDisableOnEmpty: function(disableOnEmpty) {
        // Only poke disabled if true because otherwise we'll smash the disabled
        // config that may also be getting set.
        if (disableOnEmpty) {
            this.setDisabled(Ext.isEmpty(this.getValue()));
        }
    },
    privates: {
        primitiveRe: /string|number|boolean/,
        getCandidateValue: function(candidate, v, preventCoerce) {
            var me = this,
                convert = me._convert,
                result = me.getPropertyValue(candidate);
            if (convert) {
                result = convert.call(me.scope || me, result);
            } else if (!preventCoerce) {
                result = Ext.coerce(result, v);
            }
            return result;
        }
    }
}, function(Filter) {
    var prototype = Filter.prototype,
        operatorFns = (prototype.operatorFns = {
            "<": function(candidate) {
                var v = this._filterValue;
                return this.getCandidateValue(candidate, v) < v;
            },
            "<=": function(candidate) {
                var v = this._filterValue;
                return this.getCandidateValue(candidate, v) <= v;
            },
            "=": function(candidate) {
                var me = this,
                    v = me._filterValue;
                candidate = me.getCandidateValue(candidate, v);
                if (me.isDateValue && candidate instanceof Date) {
                    candidate = candidate.getTime();
                    v = me.dateValue;
                }
                return candidate == v;
            },
            // eslint-disable-line eqeqeq
            "===": function(candidate) {
                var me = this,
                    v = me._filterValue;
                candidate = me.getCandidateValue(candidate, v, true);
                if (me.isDateValue && candidate instanceof Date) {
                    candidate = candidate.getTime();
                    v = me.dateValue;
                }
                return candidate === v;
            },
            ">=": function(candidate) {
                var v = this._filterValue;
                return this.getCandidateValue(candidate, v) >= v;
            },
            ">": function(candidate) {
                var v = this._filterValue;
                return this.getCandidateValue(candidate, v) > v;
            },
            "!=": function(candidate) {
                var me = this,
                    v = me._filterValue;
                candidate = me.getCandidateValue(candidate, v);
                if (me.isDateValue && candidate instanceof Date) {
                    candidate = candidate.getTime();
                    v = me.dateValue;
                }
                return candidate != v;
            },
            // eslint-disable-line eqeqeq
            "!==": function(candidate) {
                var me = this,
                    v = me._filterValue;
                candidate = me.getCandidateValue(candidate, v, true);
                if (me.isDateValue && candidate instanceof Date) {
                    candidate = candidate.getTime();
                    v = me.dateValue;
                }
                return candidate !== v;
            },
            "in": function(candidate) {
                var v = this._filterValue;
                return Ext.Array.contains(v, this.getCandidateValue(candidate, v));
            },
            notin: function(candidate) {
                var v = this._filterValue;
                return !Ext.Array.contains(v, this.getCandidateValue(candidate, v));
            },
            like: function(candidate) {
                var v = this._filterValue;
                // eslint-disable-next-line max-len
                return v && this.getCandidateValue(candidate, v).toLowerCase().indexOf(v.toLowerCase()) > -1;
            },
            "/=": function(candidate) {
                var me = this,
                    v = me._filterValue;
                candidate = me.getCandidateValue(candidate, v);
                // Only compile a RegExp when the source string changes
                if (v !== me.lastRegExpSource) {
                    me.lastRegExpSource = v;
                    try {
                        me.regex = new RegExp(v, 'i');
                    } catch (e) {
                        me.regex = null;
                    }
                }
                return me.regex ? me.regex.test(candidate) : false;
            }
        });
    // Operator type '==' is the same as operator type '='
    operatorFns['=='] = operatorFns['='];
    operatorFns.gt = operatorFns['>'];
    operatorFns.ge = operatorFns['>='];
    operatorFns.lt = operatorFns['<'];
    operatorFns.le = operatorFns['<='];
    operatorFns.eq = operatorFns['='];
    operatorFns.ne = operatorFns['!='];
});

/**
 * A Ext.mixin.Observable subclass that is provided for backward compatibility.
 * Applications should avoid using this class, and use Ext.mixin.Observable instead.
 */
Ext.define('Ext.util.Observable', {
    extend: 'Ext.mixin.Observable',
    // The constructor of Ext.util.Observable instances processes the config object by
    // calling Ext.apply(this, config); instead of this.initConfig(config);
    $applyConfigs: true
}, function(Observable) {
    var Super = Ext.mixin.Observable;
    /**
     * @method releaseCapture
     * @static
     * @inheritdoc Ext.mixin.Observable#releaseCapture
     */
    Observable.releaseCapture = Super.releaseCapture;
    /**
     * @method capture
     * @static
     * @inheritdoc Ext.mixin.Observable#capture
     */
    Observable.capture = Super.capture;
    /**
     * @member Ext.util.Observable
     * @private
     */
    Observable.captureArgs = Super.captureArgs;
    /**
     * @method observe
     * @static
     * @inheritdoc Ext.mixin.Observable#observe
     */
    Observable.observe = Observable.observeClass = Super.observe;
});

/**
 * @class Ext.util.AbstractMixedCollection
 * @private
 */
Ext.define('Ext.util.AbstractMixedCollection', {
    requires: [
        'Ext.util.Filter'
    ],
    mixins: {
        observable: 'Ext.util.Observable'
    },
    /**
     * @property {Boolean} isMixedCollection
     * `true` in this class to identify an object as an instantiated MixedCollection,
     * or subclass thereof.
     */
    isMixedCollection: true,
    /**
     * Mutation counter which is incremented upon add and remove.
     *
     * @private
     */
    generation: 0,
    /**
     * Mutation counter for the index map which is synchronized with the collection's
     * mutation counter when the index map is interrogated and found to be out of sync
     * and needed a rebuild.
     *
     * @private
     */
    indexGeneration: 0,
    constructor: function(allowFunctions, keyFn) {
        var me = this;
        // Modern constructor signature using a config object
        if (arguments.length === 1 && Ext.isObject(allowFunctions)) {
            me.initialConfig = allowFunctions;
            Ext.apply(me, allowFunctions);
        } else // Old constructor signature
        {
            me.allowFunctions = allowFunctions === true;
            if (keyFn) {
                me.getKey = keyFn;
            }
            me.initialConfig = {
                allowFunctions: me.allowFunctions,
                getKey: me.getKey
            };
        }
        me.items = [];
        me.map = {};
        me.keys = [];
        me.indexMap = {};
        me.length = 0;
        /**
         * @event clear
         * Fires when the collection is cleared.
         * @since 1.1.0
         */
        /**
         * @event add
         * Fires when an item is added to the collection.
         * @param {Number} index The index at which the item was added.
         * @param {Object} o The item added.
         * @param {String} key The key associated with the added item.
         * @since 1.1.0
         */
        /**
         * @event replace
         * Fires when an item is replaced in the collection.
         * @param {String} key he key associated with the new added.
         * @param {Object} old The item being replaced.
         * @param {Object} new The new item.
         * @since 1.1.0
         */
        /**
         * @event remove
         * Fires when an item is removed from the collection.
         * @param {Object} o The item being removed.
         * @param {String} key The key associated with the removed item.
         * @since 1.1.0
         */
        me.mixins.observable.constructor.call(me);
    },
    destroy: function() {
        var me = this;
        me.items = me.map = me.keys = me.indexMap = null;
        me.callParent();
    },
    /**
     * @cfg {Boolean} allowFunctions Specify <code>true</code> if the {@link #addAll}
     * function should add function references to the collection. Defaults to
     * <code>false</code>.
     * @since 3.4.0
     */
    allowFunctions: false,
    /**
     * Adds an item to the collection. Fires the {@link #event-add} event when complete.
     *
     * @param {String/Object} key The key to associate with the item, or the new item.
     *
     * If a {@link #getKey} implementation was specified for this MixedCollection,
     * or if the key of the stored items is in a property called `id`,
     * the MixedCollection will be able to *derive* the key for the new item.
     * In this case just pass the new item in this parameter.
     *
     * @param {Object} [obj] The item to add.
     *
     * Note that when adding a value that is iterable, it must be wrapped in brackets, i.e.:
     *
     *     c.add([[1, 2]]);
     *
     * This will be needed for any value that is iterable, i.e., an array, arguments object,
     * HTML collections, etc.
     *
     * @return {Object} The item added.
     * @since 1.1.0
     */
    add: function(key, obj) {
        var len = this.length,
            out;
        if (arguments.length === 1) {
            out = this.insert(len, key);
        } else {
            out = this.insert(len, key, obj);
        }
        return out;
    },
    /**
     * A function which will be called, passing a newly added object
     * when the object is added without a separate id.  The function
     * should yield the key by which that object will be indexed.
     *
     * If no key is yielded, then the object will be added, but it
     * cannot be accessed or removed quickly. Finding it in this
     * collection for interrogation or removal will require a linear
     * scan of this collection's items.
     *
     * The default implementation simply returns `item.id` but you can
     * provide your own implementation to return a different value as
     * in the following examples:
     *
     *     // normal way
     *     var mc = new Ext.util.MixedCollection();
     *     mc.add(someEl.dom.id, someEl);
     *     mc.add(otherEl.dom.id, otherEl);
     *     //and so on
     *
     *     // using getKey
     *     var mc = new Ext.util.MixedCollection({
     *         getKey: function(el){
     *             return el.dom.id;
     *         }
     *     });
     *     mc.add(someEl);
     *     mc.add(otherEl);
     *
     * @param {Object} item The item for which to find the key.
     * @return {Object} The key for the passed item.
     * @since 1.1.0
     * @template
     */
    getKey: function(item) {
        return item.id;
    },
    /**
     * Replaces an item in the collection. Fires the {@link #event-replace} event when complete.
     * @param {String} key The key associated with the item to replace, or the replacement item.
     *
     * If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key
     * of your stored items is in a property called *`id`*, then the MixedCollection
     * will be able to <i>derive</i> the key of the replacement item. If you want to replace an item
     * with one having the same key value, then just pass the replacement item in this parameter.
     *
     * @param o {Object} o (optional) If the first parameter passed was a key, the item to associate
     * with that key.
     * @return {Object}  The new item.
     * @since 1.1.0
     */
    replace: function(key, o) {
        var me = this,
            old, index;
        if (arguments.length === 1) {
            o = arguments[0];
            key = me.getKey(o);
        }
        old = me.map[key];
        if (typeof key === 'undefined' || key === null || typeof old === 'undefined') {
            return me.add(key, o);
        }
        me.generation++;
        index = me.indexOfKey(key);
        me.items[index] = o;
        me.map[key] = o;
        if (me.hasListeners.replace) {
            me.fireEvent('replace', key, old, o);
        }
        return o;
    },
    /**
     * Reorders each of the items based on a mapping from old index to new index. Internally this
     * just translates into a sort. The 'sort' event is fired whenever reordering has
     * occurred.
     * @param {Object} mapping Mapping from old item index to new item index
     *
     *     // example of moving the last of 4 items to the front of the collection
     *     // and moving each one before it forward one
     *     collection.reorder({
     *         0: 1,
     *         1: 2,
     *         2: 3,
     *         3: 0,
     *     });
     */
    reorder: function(mapping) {
        var me = this,
            items = me.items,
            index = 0,
            length = items.length,
            order = [],
            remaining = [],
            oldIndex;
        me.suspendEvents();
        // object of {oldPosition: newPosition} reversed to {newPosition: oldPosition}
        for (oldIndex in mapping) {
            order[mapping[oldIndex]] = items[oldIndex];
        }
        for (index = 0; index < length; index++) {
            if (mapping[index] == undefined) {
                // eslint-disable-line eqeqeq
                remaining.push(items[index]);
            }
        }
        for (index = 0; index < length; index++) {
            if (order[index] == undefined) {
                // eslint-disable-line eqeqeq
                order[index] = remaining.shift();
            }
        }
        me.clear();
        me.addAll(order);
        me.resumeEvents();
    },
    /**
     * Change the key for an existing item in the collection. If the old key
     * does not exist this is a no-op.
     * @param {Object} oldKey The old key
     * @param {Object} newKey The new key
     */
    updateKey: function(oldKey, newKey) {
        var me = this,
            map = me.map,
            index = me.indexOfKey(oldKey),
            // Important: Take reference to indexMap AFTER indexOf call which may rebuild it.
            indexMap = me.indexMap,
            item;
        if (index > -1) {
            item = map[oldKey];
            delete map[oldKey];
            delete indexMap[oldKey];
            map[newKey] = item;
            indexMap[newKey] = index;
            me.keys[index] = newKey;
            // indexGeneration will be in sync since we called indexOfKey
            // And we kept it all in sync, so now generation changes we keep the indexGeneration
            // matched
            me.indexGeneration = ++me.generation;
        }
    },
    /**
     * Adds all elements of an Array or an Object to the collection.
     * @param {Object/Array} objs An Object containing properties which will be added
     * to the collection, or an Array of values, each of which are added to the collection.
     * Functions references will be added to the collection if `{@link #allowFunctions}`
     * has been set to `true`.
     * @since 1.1.0
     */
    addAll: function(objs) {
        var me = this,
            key;
        if (arguments.length > 1 || Ext.isArray(objs)) {
            me.insert(me.length, arguments.length > 1 ? arguments : objs);
        } else {
            for (key in objs) {
                if (objs.hasOwnProperty(key)) {
                    if (me.allowFunctions || typeof objs[key] !== 'function') {
                        me.add(key, objs[key]);
                    }
                }
            }
        }
    },
    /**
     * Executes the specified function once for every item in the collection.
     * The function should return a boolean value.
     * Returning false from the function will stop the iteration.
     *
     * @param {Function} fn The function to execute for each item.
     * @param {Mixed} fn.item The collection item.
     * @param {Number} fn.index The index of item.
     * @param {Number} fn.len Total length of collection.
     * @param {Object} scope (optional) The scope (<code>this</code> reference)
     * in which the function is executed. Defaults to the current item in the iteration.
     *
     * @since 1.1.0
     */
    each: function(fn, scope) {
        var items = Ext.Array.push([], this.items),
            // each safe for removal
            i = 0,
            len = items.length,
            item;
        for (; i < len; i++) {
            item = items[i];
            if (fn.call(scope || item, item, i, len) === false) {
                break;
            }
        }
    },
    /**
     * Executes the specified function once for every key in the collection, passing each
     * key, and its associated item as the first two parameters.
     * @param {Function} fn The function to execute for each item.
     * @param {String} fn.key The key of collection item.
     * @param {Mixed} fn.item The collection item.
     * @param {Number} fn.index The index of item.
     * @param {Number} fn.len Total length of collection.
     * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the
     * function is executed. Defaults to the browser window.
     *
     * @since 1.1.0
     */
    eachKey: function(fn, scope) {
        var keys = this.keys,
            items = this.items,
            i = 0,
            len = keys.length;
        for (; i < len; i++) {
            fn.call(scope || window, keys[i], items[i], i, len);
        }
    },
    /**
     * Returns the first item in the collection which elicits a true return value from the
     * passed selection function.
     * @param {Function} fn The selection function to execute for each item.
     * @param {Mixed} fn.item The collection item.
     * @param {String} fn.key The key of collection item.
     * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the
     * function is executed. Defaults to the browser window.
     * @return {Object} The first item in the collection which returned true from the selection
     * function, or null if none was found.
     */
    findBy: function(fn, scope) {
        var keys = this.keys,
            items = this.items,
            i = 0,
            len = items.length;
        for (; i < len; i++) {
            if (fn.call(scope || window, items[i], keys[i])) {
                return items[i];
            }
        }
        return null;
    },
    /**
     * Returns the first item in the collection which elicits a true return value from the
     * passed selection function.
     * @deprecated 4.0 Use {@link #findBy} instead.
     * @since 1.1.0
     */
    find: function() {
        if (Ext.isDefined(Ext.global.console)) {
            Ext.global.console.warn('Ext.util.MixedCollection: find has been deprecated. ' + 'Use findBy instead.');
        }
        return this.findBy.apply(this, arguments);
    },
    /**
     * Inserts an item at the specified index in the collection. Fires the {@link #event-add}
     * event when complete.
     * @param {Number} index The index to insert the item at.
     * @param {String/Object/String[]/Object[]} key The key to associate with the new item,
     * or the item itself. May also be an array of either to insert multiple items at once.
     * @param {Object/Object[]} obj (optional) If the second parameter was a key, the new item.
     * May also be an array to insert multiple items at once.
     * @return {Object} The item inserted or an array of items inserted.
     * @since 1.1.0
     */
    insert: function(index, key, obj) {
        var out;
        if (Ext.isIterable(key)) {
            out = this.doInsert(index, key, obj);
        } else {
            if (arguments.length > 2) {
                out = this.doInsert(index, [
                    key
                ], [
                    obj
                ]);
            } else {
                out = this.doInsert(index, [
                    key
                ]);
            }
            out = out[0];
        }
        return out;
    },
    // Private multi insert implementation.
    doInsert: function(index, keys, objects) {
        var me = this,
            itemKey, removeIndex, i,
            len = keys.length,
            deDupedLen = len,
            fireAdd = me.hasListeners.add,
            syncIndices,
            newKeys = {},
            passedDuplicates, oldKeys, oldObjects;
        // External key(s) passed. We cannot reliably find an object's index using the
        // key extraction fn. Set a flag for use by contains, indexOf and remove
        if (objects != null) {
            me.useLinearSearch = true;
        } else // No external keys: calculate keys array if not passed
        {
            objects = keys;
            keys = new Array(len);
            for (i = 0; i < len; i++) {
                keys[i] = this.getKey(objects[i]);
            }
        }
        // First, remove duplicates of the keys. If a removal point is less than insertion index,
        // decr insertion index.
        me.suspendEvents();
        for (i = 0; i < len; i++) {
            itemKey = keys[i];
            // Must use indexOf - map might be out of sync
            removeIndex = me.indexOfKey(itemKey);
            if (removeIndex !== -1) {
                if (removeIndex < index) {
                    index--;
                }
                me.removeAt(removeIndex);
            }
            if (itemKey != null) {
                // If a previous new item used this key, we will have to rebuild the input arrays
                // from the newKeys map.
                if (newKeys[itemKey] != null) {
                    passedDuplicates = true;
                    deDupedLen--;
                }
                newKeys[itemKey] = i;
            }
        }
        me.resumeEvents();
        // Duplicate keys were detected - rebuild the objects and keys arrays from the last values
        // associated with each unique key
        if (passedDuplicates) {
            oldKeys = keys;
            oldObjects = objects;
            keys = new Array(deDupedLen);
            objects = new Array(deDupedLen);
            i = 0;
            // Loop through unique key hash, properties of which point to last encountered index
            // for that key. Rebuild deduped objects and keys arrays.
            for (itemKey in newKeys) {
                keys[i] = oldKeys[newKeys[itemKey]];
                objects[i] = oldObjects[newKeys[itemKey]];
                i++;
            }
            len = deDupedLen;
        }
        // If we are appending and the indices are in sync, its cheap to kep them that way
        syncIndices = index === me.length && me.indexGeneration === me.generation;
        // Insert the new items and new keys in at the insertion point
        Ext.Array.insert(me.items, index, objects);
        Ext.Array.insert(me.keys, index, keys);
        me.length += len;
        me.generation++;
        if (syncIndices) {
            me.indexGeneration = me.generation;
        }
        for (i = 0; i < len; i++ , index++) {
            itemKey = keys[i];
            if (itemKey != null) {
                me.map[itemKey] = objects[i];
                // If the index is still in sync, keep it that way
                if (syncIndices) {
                    me.indexMap[itemKey] = index;
                }
            }
            if (fireAdd) {
                me.fireEvent('add', index, objects[i], itemKey);
            }
        }
        return objects;
    },
    /**
     * Remove an item from the collection.
     * @param {Object} o The item to remove.
     * @return {Object} The item removed or false if no item was removed.
     * @since 1.1.0
     */
    remove: function(o) {
        var me = this,
            removeKey, index;
        // If
        //     We have not been forced into using linear lookup by a usage of the 2 arg form of add
        // and
        //     The key extraction function yields a key
        // Then use indexOfKey. This will use the indexMap - rebuilding it if necessary.
        if (!me.useLinearSearch && (removeKey = me.getKey(o))) {
            index = me.indexOfKey(removeKey);
        } else // Otherwise we have to do it the slow way with a linear search.
        {
            index = Ext.Array.indexOf(me.items, o);
        }
        return (index === -1) ? false : me.removeAt(index);
    },
    /**
     * Remove all items in the collection. Can also be used
     * to remove only the items in the passed array.
     * @param {Array} [items] An array of items to be removed.
     * @return {Ext.util.MixedCollection} this object
     */
    removeAll: function(items) {
        var me = this,
            i;
        if (items || me.hasListeners.remove) {
            // Only perform expensive item-by-item removal if there's a listener or specific items
            if (items) {
                for (i = items.length - 1; i >= 0; --i) {
                    me.remove(items[i]);
                }
            } else {
                while (me.length) {
                    me.removeAt(0);
                }
            }
        } else {
            me.length = me.items.length = me.keys.length = 0;
            me.map = {};
            me.indexMap = {};
            me.generation++;
            me.indexGeneration = me.generation;
        }
    },
    /**
     * Remove an item from a specified index in the collection. Fires the {@link #event-remove}
     * event when complete.
     * @param {Number} index The index within the collection of the item to remove.
     * @return {Object} The item removed or false if no item was removed.
     * @since 1.1.0
     */
    removeAt: function(index) {
        var me = this,
            o, key;
        if (index < me.length && index >= 0) {
            me.length--;
            o = me.items[index];
            Ext.Array.erase(me.items, index, 1);
            key = me.keys[index];
            if (typeof key !== 'undefined') {
                delete me.map[key];
            }
            Ext.Array.erase(me.keys, index, 1);
            if (me.hasListeners.remove) {
                me.fireEvent('remove', o, key);
            }
            me.generation++;
            return o;
        }
        return false;
    },
    /**
     * Remove a range of items starting at a specified index in the collection.
     * Does not fire the remove event.
     * @param {Number} index The index within the collection of the item to remove.
     * @param {Number} [removeCount=1] The nuber of items to remove beginning at the
     * specified index.
     * @return {Object} The last item removed or false if no item was removed.
     */
    removeRange: function(index, removeCount) {
        var me = this,
            o, key, i, limit, syncIndices, trimming;
        if (index < me.length && index >= 0) {
            if (!removeCount) {
                removeCount = 1;
            }
            limit = Math.min(index + removeCount, me.length);
            removeCount = limit - index;
            // If we are removing from end and the indices are in sync, its cheap to keep
            // them that way
            trimming = limit === me.length;
            syncIndices = trimming && me.indexGeneration === me.generation;
            // Loop through the to remove indices deleting from the key hashes
            for (i = index; i < limit; i++) {
                key = me.keys[i];
                if (key != null) {
                    delete me.map[key];
                    if (syncIndices) {
                        delete me.indexMap[key];
                    }
                }
            }
            // Last item encountered
            o = me.items[i - 1];
            me.length -= removeCount;
            me.generation++;
            if (syncIndices) {
                me.indexGeneration = me.generation;
            }
            // Chop items and keys arrays.
            // If trimming the trailing end, we can just truncate the array.
            // We can use splice directly. The IE8 bug which Ext.Array works around only affects
            // *insertion*
            // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/
            if (trimming) {
                me.items.length = me.keys.length = me.length;
            } else {
                me.items.splice(index, removeCount);
                me.keys.splice(index, removeCount);
            }
            // Return last object removed
            return o;
        }
        return false;
    },
    /**
     * Removes an item associated with the passed key fom the collection.
     * @param {String} key The key of the item to remove. If `null` is passed,
     * all objects which yielded no key from the configured {@link #getKey} function are removed.
     * @return {Object} Only returned if removing at a specified key. The item removed or false
     * if no item was removed.
     */
    removeAtKey: function(key) {
        var me = this,
            keys = me.keys,
            i;
        // Remove objects which yielded no key from our configured getKey function
        if (key == null) {
            for (i = keys.length - 1; i >= 0; i--) {
                if (keys[i] == null) {
                    me.removeAt(i);
                }
            }
        } else // Remove object at the passed key
        {
            return me.removeAt(me.indexOfKey(key));
        }
    },
    /**
     * Returns the number of items in the collection.
     * @return {Number} the number of items in the collection.
     * @since 1.1.0
     */
    getCount: function() {
        return this.length;
    },
    /**
     * Returns index within the collection of the passed Object.
     * @param {Object} o The item to find the index of.
     * @return {Number} index of the item. Returns -1 if not found.
     * @since 1.1.0
     */
    indexOf: function(o) {
        var me = this,
            key;
        if (o != null) {
            // If
            //     We have not been forced into using linear lookup by a usage of the 2 arg form
            //     of add
            // and
            //     The key extraction function yields a key
            // Then use indexOfKey. This will use the indexMap - rebuilding it if necessary.
            if (!me.useLinearSearch && (key = me.getKey(o))) {
                return this.indexOfKey(key);
            }
            // Fallback: Use linear search
            return Ext.Array.indexOf(me.items, o);
        }
        // No object passed
        return -1;
    },
    /**
     * Returns index within the collection of the passed key.
     * @param {String} key The key to find the index of.
     * @return {Number} index of the key.
     * @since 1.1.0
     */
    indexOfKey: function(key) {
        if (!this.map.hasOwnProperty(key)) {
            return -1;
        }
        if (this.indexGeneration !== this.generation) {
            this.rebuildIndexMap();
        }
        return this.indexMap[key];
    },
    rebuildIndexMap: function() {
        var me = this,
            indexMap = me.indexMap = {},
            keys = me.keys,
            len = keys.length,
            i;
        for (i = 0; i < len; i++) {
            indexMap[keys[i]] = i;
        }
        me.indexGeneration = me.generation;
    },
    /**
     * Returns the item associated with the passed key OR index.
     * Key has priority over index.  This is the equivalent
     * of calling {@link #getByKey} first, then if nothing matched calling {@link #getAt}.
     * @param {String/Number} key The key or index of the item.
     * @return {Object} If the item is found, returns the item.  If the item was not found,
     * returns `undefined`. If an item was found, but is a Class, returns `null`.
     * @since 1.1.0
     */
    get: function(key) {
        var me = this,
            mk = me.map[key],
            item = mk !== undefined ? mk : (typeof key === 'number') ? me.items[key] : undefined;
        return typeof item !== 'function' || me.allowFunctions ? item : null;
    },
    // for prototype!
    /**
     * Returns the item at the specified index.
     * @param {Number} index The index of the item.
     * @return {Object} The item at the specified index.
     */
    getAt: function(index) {
        return this.items[index];
    },
    /**
     * Returns the item associated with the passed key.
     * @param {String/Number} key The key of the item.
     * @return {Object} The item associated with the passed key.
     */
    getByKey: function(key) {
        return this.map[key];
    },
    /**
     * Returns true if the collection contains the passed Object as an item.
     * @param {Object} o  The Object to look for in the collection.
     * @return {Boolean} True if the collection contains the Object as an item.
     * @since 1.1.0
     */
    contains: function(o) {
        var me = this,
            key;
        if (o != null) {
            // If
            //     We have not been forced into using linear lookup by a usage of the 2 arg form
            //     of add
            // and
            //     The key extraction function yields a key
            // Then use the map to determine object presence.
            if (!me.useLinearSearch && (key = me.getKey(o))) {
                return this.map[key] != null;
            }
            // Fallback: Use linear search
            return Ext.Array.indexOf(this.items, o) !== -1;
        }
        return false;
    },
    /**
     * Returns true if the collection contains the passed Object as a key.
     * @param {String} key The key to look for in the collection.
     * @return {Boolean} True if the collection contains the Object as a key.
     * @since 1.1.0
     */
    containsKey: function(key) {
        return this.map.hasOwnProperty(key);
    },
    /**
     * Removes all items from the collection.  Fires the {@link #event-clear} event when complete.
     * @since 1.1.0
     */
    clear: function() {
        var me = this;
        // Only clear if it has ever had any content
        if (me.generation) {
            me.length = 0;
            me.items = [];
            me.keys = [];
            me.map = {};
            me.indexMap = {};
            me.generation++;
            me.indexGeneration = me.generation;
        }
        if (me.hasListeners.clear) {
            me.fireEvent('clear');
        }
    },
    /**
     * Returns the first item in the collection.
     * @return {Object} the first item in the collection..
     * @since 1.1.0
     */
    first: function() {
        return this.items[0];
    },
    /**
     * Returns the last item in the collection.
     * @return {Object} the last item in the collection..
     * @since 1.1.0
     */
    last: function() {
        return this.items[this.length - 1];
    },
    /**
     * Collects all of the values of the given property and returns their sum
     * @param {String} property The property to sum by
     * @param {String} [root] 'root' property to extract the first argument from. This is used
     * mainly when summing fields in records, where the fields are all stored inside the 'data'
     * object
     * @param {Number} [start=0] The record index to start at
     * @param {Number} [end=-1] The record index to end at
     * @return {Number} The total
     */
    sum: function(property, root, start, end) {
        var values = this.extractValues(property, root),
            length = values.length,
            sum = 0,
            i;
        start = start || 0;
        end = (end || end === 0) ? end : length - 1;
        for (i = start; i <= end; i++) {
            sum += values[i];
        }
        return sum;
    },
    /**
     * Collects unique values of a particular property in this MixedCollection
     * @param {String} property The property to collect on
     * @param {String} root (optional) 'root' property to extract the first argument from. This is
     * used mainly when summing fields in records, where the fields are all stored inside the 'data'
     * object
     * @param {Boolean} allowNull (optional) Pass true to allow null, undefined or empty string
     * values
     * @return {Array} The unique values
     */
    collect: function(property, root, allowNull) {
        var values = this.extractValues(property, root),
            length = values.length,
            hits = {},
            unique = [],
            value, strValue, i;
        for (i = 0; i < length; i++) {
            value = values[i];
            strValue = String(value);
            if ((allowNull || !Ext.isEmpty(value)) && !hits[strValue]) {
                hits[strValue] = true;
                unique.push(value);
            }
        }
        return unique;
    },
    /**
     * @private
     * Extracts all of the given property values from the items in the MC. Mainly used as a
     * supporting method for functions like sum and collect.
     * @param {String} property The property to extract
     * @param {String} root (optional) 'root' property to extract the first argument from.
     * This is used mainly when extracting field data from Model instances, where the fields
     * are stored inside the 'data' object
     * @return {Array} The extracted values
     */
    extractValues: function(property, root) {
        var values = this.items;
        if (root) {
            values = Ext.Array.pluck(values, root);
        }
        return Ext.Array.pluck(values, property);
    },
    /**
     * @private
     * For API parity with Store's PageMap class. Buffered rendering checks if the Store has
     * the range required to render. The Store delegates this question to its backing data object
     * which may be an instance of its private PageMap class, or a MixedCollection.
     */
    hasRange: function(start, end) {
        return (end < this.length);
    },
    /**
     * Returns a range of items in this collection
     * @param {Number} start (optional) The starting index. Defaults to 0.
     * @param {Number} end (optional) The ending index. Defaults to the last item.
     * @return {Array} An array of items
     * @since 1.1.0
     */
    getRange: function(start, end) {
        var me = this,
            items = me.items,
            range = [],
            len = items.length,
            tmp, reverse;
        if (len < 1) {
            return range;
        }
        if (start > end) {
            reverse = true;
            tmp = start;
            start = end;
            end = tmp;
        }
        if (start < 0) {
            start = 0;
        }
        if (end == null || end >= len) {
            end = len - 1;
        }
        range = items.slice(start, end + 1);
        if (reverse && range.length) {
            range.reverse();
        }
        return range;
    },
    /**
     * Filters the objects in this collection by a set of {@link Ext.util.Filter Filter}s, or by
     * a single property/value pair with optional parameters for substring matching and
     * case sensitivity. See {@link Ext.util.Filter Filter} for an example of using Filter objects
     * (preferred). Alternatively, MixedCollection can be easily filtered by property like this:
     *
     *     // create a simple store with a few people defined
     *     var people = new Ext.util.MixedCollection();
     *     people.addAll([
     *         {id: 1, age: 25, name: 'Ed'},
     *         {id: 2, age: 24, name: 'Tommy'},
     *         {id: 3, age: 24, name: 'Arne'},
     *         {id: 4, age: 26, name: 'Aaron'}
     *     ]);
     *
     *     // a new MixedCollection containing only the items where age == 24
     *     var middleAged = people.filter('age', 24);
     *
     * @param {Ext.util.Filter[]/String} property A property on your objects, or an array
     * of {@link Ext.util.Filter Filter} objects
     * @param {String/RegExp} value Either string that the property values
     * should start with or a RegExp to test against the property
     * @param {Boolean} [anyMatch=false] True to match any part of the string,
     * not just the beginning
     * @param {Boolean} [caseSensitive=false] True for case sensitive comparison.
     * @return {Ext.util.MixedCollection} The new filtered collection
     * @since 1.1.0
     */
    filter: function(property, value, anyMatch, caseSensitive) {
        var filters = [];
        // support for the simple case of filtering by property/value
        if (Ext.isString(property)) {
            filters.push(new Ext.util.Filter({
                property: property,
                value: value,
                anyMatch: anyMatch,
                caseSensitive: caseSensitive
            }));
        } else if (Ext.isArray(property) || property instanceof Ext.util.Filter) {
            filters = filters.concat(property);
        }
        // At this point we have an array of zero or more Ext.util.Filter objects to filter with,
        // so here we construct a function that combines these filters by ANDing them together
        // and filter by that.
        return this.filterBy(Ext.util.Filter.createFilterFn(filters));
    },
    /**
     * Filter by a function. Returns a <i>new</i> collection that has been filtered.
     * The passed function will be called with each object in the collection.
     * If the function returns true, the value is included otherwise it is filtered.
     * @param {Function} fn The function to be called.
     * @param {Mixed} fn.item The collection item.
     * @param {String} fn.key The key of collection item.
     * @param {Object} scope (optional) The scope (<code>this</code> reference) in
     * which the function is executed. Defaults to this MixedCollection.
     * @return {Ext.util.MixedCollection} The new filtered collection
     * @since 1.1.0
     */
    filterBy: function(fn, scope) {
        var me = this,
            newMC = new me.self(me.initialConfig),
            keys = me.keys,
            items = me.items,
            length = items.length,
            i;
        newMC.getKey = me.getKey;
        for (i = 0; i < length; i++) {
            if (fn.call(scope || me, items[i], keys[i])) {
                newMC.add(keys[i], items[i]);
            }
        }
        // The add using an external key will make the newMC think that keys cannot be reliably
        // extracted from objects, so that an indexOf call will always have to do a linear search.
        // If the flag is not set in this object, we know that the clone will not need it either.
        newMC.useLinearSearch = me.useLinearSearch;
        return newMC;
    },
    /**
     * Finds the index of the first matching object in this collection by a specific property/value.
     * @param {String} property The name of a property on your objects.
     * @param {String/RegExp} value A string that the property values
     * should start with or a RegExp to test against the property.
     * @param {Number} [start=0] The index to start searching at.
     * @param {Boolean} [anyMatch=false] True to match any part of the string,
     * not just the beginning.
     * @param {Boolean} [caseSensitive=false] True for case sensitive comparison.
     * @return {Number} The matched index or -1
     * @since 2.3.0
     */
    findIndex: function(property, value, start, anyMatch, caseSensitive) {
        if (Ext.isEmpty(value, false)) {
            return -1;
        }
        value = this.createValueMatcher(value, anyMatch, caseSensitive);
        return this.findIndexBy(function(o) {
            return o && value.test(o[property]);
        }, null, start);
    },
    /**
     * Find the index of the first matching object in this collection by a function.
     * If the function returns <i>true</i> it is considered a match.
     * @param {Function} fn The function to be called.
     * @param {Mixed} fn.item The collection item.
     * @param {String} fn.key The key of collection item.
     * @param {Object} [scope] The scope (<code>this</code> reference) in which the function
     * is executed. Defaults to this MixedCollection.
     * @param {Number} [start=0] The index to start searching at.
     * @return {Number} The matched index or -1
     * @since 2.3.0
     */
    findIndexBy: function(fn, scope, start) {
        var me = this,
            keys = me.keys,
            items = me.items,
            i = start || 0,
            len = items.length;
        for (; i < len; i++) {
            if (fn.call(scope || me, items[i], keys[i])) {
                return i;
            }
        }
        return -1;
    },
    /**
     * Returns a regular expression based on the given value and matching options. This is used
     * internally for finding and filtering, and by Ext.data.Store#filter
     * @private
     * @param {String} value The value to create the regex for. This is escaped using Ext.escapeRe
     * @param {Boolean} anyMatch True to allow any match - no regex start/end line anchors
     * will be added. Defaults to false
     * @param {Boolean} caseSensitive True to make the regex case sensitive
     * (adds 'i' switch to regex). Defaults to false.
     * @param {Boolean} exactMatch True to force exact match (^ and $ characters added
     * to the regex). Defaults to false. Ignored if anyMatch is true.
     * @since 3.4.0
     */
    createValueMatcher: function(value, anyMatch, caseSensitive, exactMatch) {
        var er;
        if (!value.exec) {
            // not a regex
            er = Ext.String.escapeRegex;
            value = String(value);
            if (anyMatch === true) {
                value = er(value);
            } else {
                value = '^' + er(value);
                if (exactMatch === true) {
                    value += '$';
                }
            }
            value = new RegExp(value, caseSensitive ? '' : 'i');
        }
        return value;
    },
    /**
     * Creates a shallow copy of this collection
     * @return {Ext.util.MixedCollection}
     * @since 1.1.0
     */
    clone: function() {
        var me = this,
            copy = new me.self(me.initialConfig);
        copy.add(me.keys, me.items);
        // The add using external keys will make the clone think that keys cannot be reliably
        // extracted from objects, so that an indexOf call will always have to do a linear search.
        // If the flag is not set in this object, we know that the clone will not need it either.
        copy.useLinearSearch = me.useLinearSearch;
        return copy;
    }
});

/**
 * Represents a single sorter that can be used as part of the sorters configuration in
 * Ext.mixin.Sortable.
 *
 * A common place for Sorters to be used are {@link Ext.data.Store Stores}. For example:
 *
 *      @example
 *      var store = Ext.create('Ext.data.Store', {
 *           fields: ['firstName', 'level'],
 *           sorters: 'level',
 *        
 *           data: [
 *               { firstName: 'Mitch',  level: 9000},
 *               { firstName: 'Seth',   level: 42},
 *               { firstName: 'Fred',   level: 510},
 *               { firstName: 'Israel', level: 690},
 *               { firstName: 'Greg',   level: 101},
 *               { firstName: 'Pat',    level: 0},              
 *               { firstName: 'Kevin',  level: 17},
 *               { firstName: 'Brandon',level: 690},
 *               { firstName: 'Gary',   level: 409},
 *               { firstName: 'Scott',  level: 789}
 *           ]
 *        });
 *        
 *        Ext.create('Ext.grid.Panel', {
 *            title: 'Support',
 *            store: store,
 *            columns: [
 *                { text: 'Name',  dataIndex: 'firstName' },
 *                { text: 'Level', dataIndex: 'level' }
 *            ],
 *            height: 300,
 *            width: 200,
 *            renderTo: Ext.getBody()
 *        });  
 *
 * In the next example, we specify a custom sorter function:
 *
 *        @example
 *        var store = Ext.create('Ext.data.Store', {
 *           fields: ['firstName', 'spiritAnimal'],
 *            sorters: [
 *                {
 *                    // Sort by first letter of second word of spirit animal, in
 *                    // descending order
 *                    sorterFn: function(record1, record2) {
 *                        var name1 = record1.data.spiritAnimal.split(' ')[1].substr(0,1),
 *                            name2 = record2.data.spiritAnimal.split(' ')[1].substr(0,1);
 * 
 *                        return name1 > name2 ? 1 : (name1 === name2) ? 0 : -1;
 *                    },
 *                    direction: 'DESC'
 *                }
 *            ],
 *         
 *           data: [
 *               { firstName: 'Mitch',  spiritAnimal: "Panda Bear"},
 *               { firstName: 'Seth',   spiritAnimal: "Rascally Rabbit"},
 *               { firstName: 'Fred',   spiritAnimal: "Honey Badger"},
 *               { firstName: 'Israel', spiritAnimal: "Mysterious Capybara"},
 *               { firstName: 'Greg',   spiritAnimal: "Majestic Platypus"},
 *               { firstName: 'Kevin',  spiritAnimal: "Sparkling Unicorn"},
 *               { firstName: 'Brandon',spiritAnimal: "Pygmy Goat"},
 *               { firstName: 'Gary',   spiritAnimal: "Suri Alpaca"},
 *               { firstName: 'Scott',  spiritAnimal: "Ripe Armadillo"},
 *               { firstName: 'Pat',    spiritAnimal: "Wiley Coyote"}
 *           ]
 *        });
 *        
 *        Ext.create('Ext.grid.Panel', {
 *            title: 'Support',
 *            store: store,
 *            columns: [
 *                { text: 'Name',          dataIndex: 'firstName' },
 *                { text: 'Spirit Animal', dataIndex: 'spiritAnimal', flex: 1 }
 *            ],
 *            height: 310,
 *            renderTo: Ext.getBody()
 *        });
 */
Ext.define('Ext.util.Sorter', {
    isSorter: true,
    config: {
        /**
         * @cfg {String} property
         * The property to sort by. Required unless `sorterFn` is provided
         */
        property: null,
        /**
         * @cfg {Function} sorterFn
         * A specific sorter function to execute. Can be passed instead of {@link #property}.
         * This function should compare the two passed arguments, returning -1, 0 or 1
         * depending on if item 1 should be sorted before, at the same level, or after
         * item 2.
         *
         *     sorterFn: function(person1, person2) {
         *         return (person1.age > person2.age) ? 1 : (person1.age === person2.age ? 0 : -1);
         *     }
         */
        sorterFn: null,
        /**
         * @cfg {String} root Optional root property. This is mostly useful when sorting a Store,
         * in which case we set the root to 'data' to make the filter pull the {@link #property}
         * out of the data object of each item
         */
        root: null,
        /**
         * @cfg {Function} transform A function that will be run on each value before
         * it is compared in the sorter. The function will receive a single argument,
         * the value.
         */
        transform: null,
        /**
         * @cfg {String} direction The direction to sort by. Valid values are "ASC", and "DESC".
         */
        direction: "ASC",
        /**
         * @cfg {Mixed} id An optional id this sorter can be keyed by in Collections. If
         * no id is specified it will use the property name used in this Sorter. If no
         * property is specified, e.g. when adding a custom sorter function we will generate
         * a random id.
         */
        id: undefined
    },
    statics: {
        /**
         * Creates a comparator function (a function that can be passed to `Array.sort`)
         * given one or more `Sorter` instances.
         *
         * The returned function retains a reference to the collection or array of sorters
         * passed. This means the function will produce a comparison based on the current
         * content of the collection or array, and not based on the content at the time of
         * this call.
         *
         * @param {Ext.util.Sorter[]/Ext.util.Collection} sorters The `Sorter` instances.
         * @param [nextFn] The next comparator function to call if all the `sorters` end
         * with equality.
         * @return {Function} The comparator function.
         */
        createComparator: function(sorters, nextFn) {
            nextFn = nextFn || 0;
            return function(lhs, rhs) {
                var items = sorters.isCollection ? sorters.items : sorters,
                    n = items.length,
                    comp, i;
                for (i = 0; i < n; ++i) {
                    comp = items[i].sort(lhs, rhs);
                    if (comp) {
                        return comp;
                    }
                }
                return nextFn && nextFn(lhs, rhs);
            };
        }
    },
    /**
     * This value is set based on the `direction` config to be either 1 or -1. This is used
     * as a multiplier for the raw comparison value to factor in the direction.
     * @private
     * @readonly
     */
    multiplier: 1,
    constructor: function(config) {
        if (config && !this.isGrouper) {
            if (!config.property === !config.sorterFn) {
                // the above is a "not XOR" - both true or both false
                Ext.raise("A Sorter requires either a property or a sorterFn.");
            }
        }
        this.initConfig(config);
    },
    getId: function() {
        var id = this._id;
        if (!id) {
            id = this.getProperty();
            if (!id) {
                id = Ext.id(null, 'ext-sorter-');
            }
            this._id = id;
        }
        return id;
    },
    sort: function(lhs, rhs) {
        return this.multiplier * this.sortFn(lhs, rhs);
    },
    /**
     * @private
     * Basic default sorter function that just compares the defined property of each object.
     * This is hidden by the `sorterFn` provided by the user.
     */
    sortFn: function(item1, item2) {
        var me = this,
            transform = me._transform,
            root = me._root,
            property = me._property,
            lhs, rhs;
        if (root) {
            item1 = item1[root];
            item2 = item2[root];
        }
        lhs = item1[property];
        rhs = item2[property];
        if (transform) {
            lhs = transform(lhs);
            rhs = transform(rhs);
        }
        return (lhs > rhs) ? 1 : (lhs < rhs ? -1 : 0);
    },
    applyDirection: function(direction) {
        return direction ? direction : 'ASC';
    },
    updateDirection: function(direction) {
        this.multiplier = (direction.toUpperCase() === "DESC") ? -1 : 1;
    },
    updateProperty: function(property) {
        if (property) {
            // Unhide the default sortFn on our prototype
            delete this.sortFn;
        }
    },
    updateSorterFn: function(sorterFn) {
        // Hide the default sortFn on our prototype
        this.sortFn = sorterFn;
    },
    /**
     * Toggles the direction of this Sorter. Note that when you call this function,
     * the Collection this Sorter is part of does not get refreshed automatically.
     */
    toggle: function() {
        this.setDirection(Ext.String.toggle(this.getDirection(), "ASC", "DESC"));
    },
    /**
     * Returns this sorter's state.
     * @return {Object}
     */
    getState: function() {
        var me = this,
            result = {
                root: me.getRoot(),
                property: me.getProperty(),
                direction: me.getDirection()
            };
        // Do not use getId() which will create an identifier if we have none.
        // We need to know if we really are identifiable.
        if (me._id) {
            result.id = me._id;
        }
        return result;
    },
    /**
     * Returns this sorter's serialized state. This is used when transmitting this sorter
     * to a server.
     * @return {Object}
     */
    serialize: function() {
        return {
            property: this.getProperty(),
            direction: this.getDirection()
        };
    }
});

/**
 * A mixin which allows a data component to be sorted. This is used by e.g. {@link Ext.data.Store}
 * and {@link Ext.data.TreeStore}.
 *
 * **NOTE**: This mixin is mainly for internal use and most users should not need to use it
 * directly. It is more likely you will want to use one of the component classes that import
 * this mixin, such as {@link Ext.data.Store} or {@link Ext.data.TreeStore}.
 */
Ext.define("Ext.util.Sortable", {
    /**
     * @property {Boolean} isSortable
     * `true` in this class to identify an object as an instantiated Sortable, or subclass thereof.
     */
    isSortable: true,
    $configPrefixed: false,
    $configStrict: false,
    config: {
        /**
         * @cfg {Ext.util.Sorter[]/Object[]} sorters
         * The initial set of {@link Ext.util.Sorter Sorters}.
         *
         *     sorters: [{
         *         property: 'age',
         *         direction: 'DESC'
         *     }, {
         *         property: 'firstName',
         *         direction: 'ASC'
         *     }]
         */
        sorters: null
    },
    /**
     * @cfg {String} defaultSortDirection
     * The default sort direction to use if one is not specified.
     */
    defaultSortDirection: "ASC",
    requires: [
        'Ext.util.Sorter'
    ],
    /**
     * @event beforesort
     * Fires before a sort occurs.
     * @param {Ext.util.Sortable} me This object.
     * @param {Ext.util.Sorter[]} sorters The collection of Sorters being used to generate
     * the comparator function.
     */
    /**
     * @cfg {Number} [multiSortLimit=3]
     * The maximum number of sorters which may be applied to this Sortable when using the "multi"
     * insertion position when adding sorters.
     *
     * New sorters added using the "multi" insertion position are inserted at the top of the
     * sorters list becoming the new primary sort key.
     *
     * If the sorters collection has grown to longer then **`multiSortLimit`**, then it is trimmed.
     *
     */
    multiSortLimit: 3,
    statics: {
        /**
         * Creates a single comparator function which encapsulates the passed Sorter array.
         * @param {Ext.util.Sorter[]} sorters The sorter set for which to create a comparator
         * function
         * @return {Function} a function, which when passed two comparable objects returns
         * the result of the whole sorter comparator functions.
         */
        createComparator: function(sorters) {
            return sorters && sorters.length ? function(r1, r2) {
                var result = sorters[0].sort(r1, r2),
                    length = sorters.length,
                    i = 1;
                // While we have not established a comparison value,
                // loop through subsequent sorters asking for a comparison value
                for (; !result && i < length; i++) {
                    result = sorters[i].sort.call(sorters[i], r1, r2);
                }
                return result;
            } : function() {
                return 0;
            };
        }
    },
    /**
     * @cfg {String} sortRoot
     * The property in each item that contains the data to sort.
     */
    applySorters: function(sorters) {
        var me = this,
            sortersCollection;
        sortersCollection = me.getSorters() || new Ext.util.MixedCollection(false, Ext.returnId);
        // We have been configured with a non-default value.
        if (sorters) {
            sortersCollection.addAll(me.decodeSorters(sorters));
        }
        return sortersCollection;
    },
    /**
     * Updates the sorters collection and triggers sorting of this Sortable. Example usage:
     *
     *     //sort by a single field
     *     myStore.sort('myField', 'DESC');
     *
     *     //sorting by multiple fields
     *     myStore.sort([{
     *         property : 'age',
     *         direction: 'ASC'
     *     }, {
     *         property : 'name',
     *         direction: 'DESC'
     *     }]);
     *
     * Classes which use this mixin must implement a **`soSort`** method which accepts a comparator
     * function computed from the full sorter set which performs the sort
     * in an implementation-specific way.
     *
     * When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field,
     * so this code:
     *
     *     store.sort('myField');
     *     store.sort('myField');
     *
     * Is equivalent to this code, because Store handles the toggling automatically:
     *
     *     store.sort('myField', 'ASC');
     *     store.sort('myField', 'DESC');
     *
     * @param {String/Ext.util.Sorter[]} [sorters] Either a string name of one of the fields
     * in this Store's configured {@link Ext.data.Model Model}, or an array of sorter
     * configurations.
     * @param {String} [direction="ASC"] The overall direction to sort the data by.
     * @param {String} [insertionPosition="replace"] Where to put the new sorter in the collection
     * of sorters. This may take the following values:
     *
     * * `replace`: This means that the new sorter(s) becomes the sole sorter set for this Sortable.
     * This is the most useful call mode to programatically sort by multiple fields.
     *
     * * `prepend`: This means that the new sorters are inserted as the primary sorters, unchanged,
     * and the sorter list length must be controlled by the developer.
     *
     * * `multi`:  This is mainly useful for implementing intuitive "Sort by this" user interfaces
     * such as the {@link Ext.grid.Panel GridPanel}'s column sorting UI. This mode is only 
     * supported when passing a property name and a direction. This means that the new sorter
     * becomes the primary sorter. If the sorter was **already** the primary sorter, the direction
     * of sort is toggled if no direction parameter is specified. The number of sorters maintained
     * is limited by the {@link #multiSortLimit} configuration.
     *
     * * `append` : This means that the new sorter becomes the last sorter.
     * @param {Boolean} doSort True to sort using a generated sorter function that combines all
     * of the Sorters passed
     * @return {Ext.util.Sorter[]} The new sorters.
     */
    sort: function(sorters, direction, insertionPosition, doSort) {
        var me = this,
            sorter, overFlow,
            currentSorters = me.getSorters();
        if (!currentSorters) {
            me.setSorters(null);
            currentSorters = me.getSorters();
        }
        if (Ext.isArray(sorters)) {
            doSort = insertionPosition;
            insertionPosition = direction;
        } else if (Ext.isObject(sorters)) {
            sorters = [
                sorters
            ];
            doSort = insertionPosition;
            insertionPosition = direction;
        } else if (Ext.isString(sorters)) {
            sorter = currentSorters.get(sorters);
            if (!sorter) {
                sorter = {
                    property: sorters,
                    direction: direction
                };
            } else if (direction == null) {
                sorter.toggle();
            } else {
                sorter.setDirection(direction);
            }
            sorters = [
                sorter
            ];
        }
        if (sorters && sorters.length) {
            sorters = me.decodeSorters(sorters);
            switch (insertionPosition) {
                // multi sorting means always inserting the specified sorters
                // at the top.
                // If we are asked to sort by what is already the primary sorter
                // then toggle its direction.
                case "multi":
                    // Insert the new sorter at the beginning.
                    currentSorters.insert(0, sorters[0]);
                    // If we now are oversize, trim our sorters collection
                    overFlow = currentSorters.getCount() - me.multiSortLimit;
                    if (overFlow > 0) {
                        currentSorters.removeRange(me.multiSortLimit, overFlow);
                    };
                    break;
                case "prepend":
                    currentSorters.insert(0, sorters);
                    break;
                case "append":
                    currentSorters.addAll(sorters);
                    break;
                case undefined:
                case null:
                case "replace":
                    currentSorters.clear();
                    currentSorters.addAll(sorters);
                    break;
                default:
                    Ext.raise('Sorter insertion point must be "multi", "prepend", ' + '"append" or "replace"');
            }
        }
        if (doSort !== false) {
            me.fireEvent('beforesort', me, sorters);
            me.onBeforeSort(sorters);
            if (me.getSorterCount()) {
                // Sort using a generated sorter function which combines all of the Sorters passed
                me.doSort(me.generateComparator());
            }
        }
        return sorters;
    },
    /**
     * @protected
     * Returns the number of Sorters which apply to this Sortable.
     *
     * May be overridden in subclasses. {@link Ext.data.Store Store} in particlar overrides
     * this because its groupers must contribute to the sorter count so that the sort method above
     * executes doSort.
     */
    getSorterCount: function() {
        return this.getSorters().items.length;
    },
    /**
     * Returns a comparator function which compares two items and returns -1, 0, or 1 depending
     * on the currently defined set of {@link #cfg-sorters}.
     *
     * If there are no {@link #cfg-sorters} defined, it returns a function which returns `0` meaning
     * that no sorting will occur.
     */
    generateComparator: function() {
        var sorters = this.getSorters().getRange();
        return sorters.length ? this.createComparator(sorters) : this.emptyComparator;
    },
    emptyComparator: function() {
        return 0;
    },
    onBeforeSort: Ext.emptyFn,
    /**
     * @private
     * Normalizes an array of sorter objects, ensuring that they are all Ext.util.Sorter instances
     * @param {Object[]} sorters The sorters array
     * @return {Ext.util.Sorter[]} Array of Ext.util.Sorter objects
     */
    decodeSorters: function(sorters) {
        if (!Ext.isArray(sorters)) {
            if (sorters === undefined) {
                sorters = [];
            } else {
                sorters = [
                    sorters
                ];
            }
        }
        // eslint-disable-next-line vars-on-top
        var length = sorters.length,
            Sorter = Ext.util.Sorter,
            model = this.getModel ? this.getModel() : this.model,
            field, config, i;
        for (i = 0; i < length; i++) {
            config = sorters[i];
            if (!(config instanceof Sorter)) {
                if (Ext.isString(config)) {
                    config = {
                        property: config
                    };
                }
                Ext.applyIf(config, {
                    root: this.sortRoot,
                    direction: "ASC"
                });
                // support for 3.x style sorters where a function can be defined as 'fn'
                if (config.fn) {
                    config.sorterFn = config.fn;
                }
                // support a function to be passed as a sorter definition
                if (typeof config === 'function') {
                    config = {
                        sorterFn: config
                    };
                }
                // ensure sortType gets pushed on if necessary
                if (model && !config.transform) {
                    field = model.getField(config.property);
                    config.transform = field && field.sortType !== Ext.identityFn ? field.sortType : undefined;
                }
                sorters[i] = new Ext.util.Sorter(config);
            }
        }
        return sorters;
    },
    /**
     * Gets the first sorter from the sorters collection, excluding
     * any groupers that may be in place
     * @protected
     * @return {Ext.util.Sorter} The sorter, null if none exist
     */
    getFirstSorter: function() {
        var sorters = this.getSorters().items,
            len = sorters.length,
            i = 0,
            sorter;
        for (; i < len; ++i) {
            sorter = sorters[i];
            if (!sorter.isGrouper) {
                return sorter;
            }
        }
        return null;
    }
}, function() {
    // Reference the static implementation in prototype
    this.prototype.createComparator = this.createComparator;
});

/**
 * Represents a collection of a set of key and value pairs. Each key in the MixedCollection
 * must be unique, the same key cannot exist twice. This collection is ordered, items in the
 * collection can be accessed by index  or via the key. Newly added items are added to
 * the end of the collection. This class is similar to {@link Ext.util.HashMap} however it
 * is heavier and provides more functionality. Sample usage:
 *
 *     var coll = new Ext.util.MixedCollection();
 *     coll.add('key1', 'val1');
 *     coll.add('key2', 'val2');
 *     coll.add('key3', 'val3');
 *
 *     console.log(coll.get('key1')); // prints 'val1'
 *     console.log(coll.indexOfKey('key3')); // prints 2
 *
 * The MixedCollection also has support for sorting and filtering of the values in the collection.
 *
 *     var coll = new Ext.util.MixedCollection();
 *     coll.add('key1', 100);
 *     coll.add('key2', -100);
 *     coll.add('key3', 17);
 *     coll.add('key4', 0);
 *     var biggerThanZero = coll.filterBy(function(value){
 *         return value > 0;
 *     });
 *     console.log(biggerThanZero.getCount()); // prints 2
 *
 */
Ext.define('Ext.util.MixedCollection', {
    extend: 'Ext.util.AbstractMixedCollection',
    mixins: {
        sortable: 'Ext.util.Sortable'
    },
    /**
     * @cfg {Boolean} allowFunctions
     * Configure as `true` if the {@link #addAll} function should add function references
     * to the collection.
     */
    /**
     * Creates new MixedCollection.
     * @param {Object} config A configuration object.
     * @param {Boolean} [config.allowFunctions=false] Specify `true` if the
     * {@link #addAll} function should add function references to the collection.
     * @param {Function} [config.getKey] A function that can accept an item of the
     * type(s) stored in this MixedCollection and return the key value for that item.
     * This is used when available to look up the key on items that were passed without
     * an explicit key parameter to a MixedCollection method.  Passing this parameter is
     * equivalent to overriding the {@link #method-getKey} method.
     */
    constructor: function() {
        this.initConfig();
        this.callParent(arguments);
    },
    doSort: function(sorterFn) {
        this.sortBy(sorterFn);
    },
    /**
     * @private
     * Performs the actual sorting based on a direction and a sorting function.
     * Internally, this creates a temporary array of all items in the MixedCollection,
     * sorts it and then writes the sorted array data back into this.items and this.keys
     * @param {String} property Property to sort by ('key', 'value', or 'index')
     * @param {String} dir (optional) Direction to sort 'ASC' or 'DESC'. Defaults to
     * 'ASC'.
     * @param {Function} fn (optional) Comparison function that defines the sort order.
     * Defaults to sorting by numeric value.
     */
    _sort: function(property, dir, fn) {
        var me = this,
            i, len,
            dsc = String(dir).toUpperCase() === 'DESC' ? -1 : 1,
            // this is a temporary array used to apply the sorting function
            c = [],
            keys = me.keys,
            items = me.items,
            o;
        // default to a simple sorter function if one is not provided
        fn = fn || function(a, b) {
            return a - b;
        };
        // copy all the items into a temporary array, which we will sort
        for (i = 0 , len = items.length; i < len; i++) {
            c[c.length] = {
                key: keys[i],
                value: items[i],
                index: i
            };
        }
        // sort the temporary array
        Ext.Array.sort(c, function(a, b) {
            return fn(a[property], b[property]) * dsc || (// In case of equality, ensure stable sort by comparing collection index
            a.index < b.index ? -1 : 1);
        });
        // Copy the temporary array back into the main this.items and this.keys objects
        // Repopulate the indexMap hash if configured to do so.
        for (i = 0 , len = c.length; i < len; i++) {
            o = c[i];
            items[i] = o.value;
            keys[i] = o.key;
            me.indexMap[o.key] = i;
        }
        me.generation++;
        me.indexGeneration = me.generation;
        me.fireEvent('sort', me);
    },
    /**
     * Sorts the collection by a single sorter function
     * @param {Function} sorterFn The function to sort by
     */
    sortBy: function(sorterFn) {
        var me = this,
            items = me.items,
            item,
            keys = me.keys,
            key,
            length = items.length,
            i;
        // Stamp the collection index into each item so that we can implement stable sort
        for (i = 0; i < length; i++) {
            items[i].$extCollectionIndex = i;
        }
        Ext.Array.sort(items, function(a, b) {
            return sorterFn(a, b) || (// In case of equality, ensure stable sort by comparing collection index
            a.$extCollectionIndex < b.$extCollectionIndex ? -1 : 1);
        });
        // Update the keys array, and remove the index
        for (i = 0; i < length; i++) {
            item = items[i];
            key = me.getKey(item);
            keys[i] = key;
            me.indexMap[key] = i;
            delete item.$extCollectionIndex;
        }
        me.generation++;
        me.indexGeneration = me.generation;
        me.fireEvent('sort', me, items, keys);
    },
    /**
     * Calculates the insertion index of the new item based upon the comparison function passed,
     * or the current sort order.
     * @param {Object} newItem The new object to find the insertion position of.
     * @param {Function} [sorterFn] The function to sort by. This is the same as the sorting
     * function passed to {@link #sortBy}. It accepts 2 items from this MixedCollection,
     * and returns -1 0, or 1 depending on the relative sort positions of the 2 compared items.
     *
     * If omitted, a function {@link #generateComparator generated} from the currently defined
     * set of {@link #cfg-sorters} will be used.
     *
     * @return {Number} The insertion point to add the new item into this MixedCollection
     * at using {@link #insert}
     */
    findInsertionIndex: function(newItem, sorterFn) {
        var me = this,
            items = me.items,
            start = 0,
            end = items.length - 1,
            middle, comparison;
        if (!sorterFn) {
            sorterFn = me.generateComparator();
        }
        while (start <= end) {
            middle = (start + end) >> 1;
            comparison = sorterFn(newItem, items[middle]);
            if (comparison >= 0) {
                start = middle + 1;
            } else if (comparison < 0) {
                end = middle - 1;
            }
        }
        return start;
    },
    /**
     * @method reorder
     * @inheritdoc Ext.util.AbstractMixedCollection#method-reorder
     */
    reorder: function(mapping) {
        this.callParent([
            mapping
        ]);
        this.fireEvent('sort', this);
    },
    /**
     * Sorts this collection by <b>key</b>s.
     * @param {"ASC"/"DESC"} [dir="ASC"] 'ASC' or 'DESC'. Defaults to 'ASC'.
     * @param {Function} [fn] Comparison function that defines the sort order.
     * Defaults to sorting by case insensitive string.
     */
    sortByKey: function(dir, fn) {
        this._sort('key', dir, fn || function(a, b) {
            var v1 = String(a).toUpperCase(),
                v2 = String(b).toUpperCase();
            return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0);
        });
    }
});

// @tag core
/**
 * Provides the ability to execute one or more arbitrary tasks in an asynchronous manner.
 *
 * Generally, you can use the singleton {@link Ext.TaskManager}.  Or you can create 
 * separate TaskRunner instances to start and stop unique tasks independent of one 
 * another.
 * 
 * Example usage:
 *
 *     @example
 *     var runner = new Ext.util.TaskRunner(),
 *         clock, updateClock, task;
 *     
 *     clock = Ext.getBody().appendChild({
 *         id: 'clock'
 *     });
 *     
 *     // Start a simple clock task that updates a div once per second
 *     updateClock = function() {
 *         clock.setHtml(Ext.Date.format(new Date(), 'g:i:s A'));
 *     };
 *     
 *     task = runner.start({
 *         run: updateClock,
 *         interval: 1000
 *     });
 *
 * The equivalent using TaskManager:
 *
 *     @example
 *     var clock, updateClock, task;
 *     
 *     clock = Ext.getBody().appendChild({
 *         id: 'clock'
 *     });
 *     
 *     // Start a simple clock task that updates a div once per second
 *     updateClock = function() {
 *         clock.setHtml(Ext.Date.format(new Date(), 'g:i:s A'));
 *     };
 *     
 *     var task = Ext.TaskManager.start({
 *         run: updateClock,
 *         interval: 1000
 *     });
 *
 * To end a running task:
 * 
 *      task.destroy();
 *
 * If a task needs to be started and stopped repeated over time, you can create a
 * {@link Ext.util.TaskRunner.Task Task} instance.
 *
 *     var runner = new Ext.util.TaskRunner(),
 *         task;
 *     
 *     task = runner.newTask({
 *         run: function() {
 *             // useful code
 *         },
 *         interval: 1000
 *     });
 *     
 *     task.start();
 *     
 *     // ...
 *     
 *     task.stop();
 *     
 *     // ...
 *     
 *     task.start();
 *
 * A re-usable, single-run task can be managed similar to the above:
 *
 *     var runner = new Ext.util.TaskRunner(),
 *         task;
 *     
 *     task = runner.newTask({
 *         run: function() {
 *             // useful code
 *         },
 *         interval: 1000,
 *         repeat: 1
 *     });
 *     
 *     task.start();
 *     
 *     // ...
 *     
 *     task.stop();
 *     
 *     // ...
 *     
 *     task.start();
 *
 * See the {@link #start} method for details about how to configure a Task.
 *
 * Also see {@link Ext.util.DelayedTask}.
 * 
 * @constructor
 * @param {Number/Object} [interval=10] The minimum precision in milliseconds supported by 
 * this TaskRunner instance. Alternatively, a config object to apply to the new instance.
 */
Ext.define('Ext.util.TaskRunner', {
    // @require Ext.Function
    /**
     * @cfg {Boolean} fireIdleEvent
     * This may be configured `false` to inhibit firing of the {@link
     * Ext.GlobalEvents#idle idle event} after task invocation. By default the tasks
     * run in a given tick determine whether `idle` events fire.
     */
    fireIdleEvent: null,
    /**
     * @cfg {Number} interval
     * How often to run the task in milliseconds. Defaults to every 10ms.
     */
    interval: 10,
    /**
     * @property timerId
     * The id of the current timer.
     * @private
     */
    timerId: null,
    constructor: function(interval) {
        var me = this;
        if (typeof interval === 'number') {
            me.interval = interval;
        } else if (interval) {
            Ext.apply(me, interval);
        }
        me.tasks = [];
        me.timerFn = me.onTick.bind(me);
    },
    /**
     * Creates a new {@link Ext.util.TaskRunner.Task Task} instance. These instances can
     * be easily started and stopped.
     * @param {Object} config The config object. For details on the supported properties,
     * see {@link #start}.
     *
     * @return {Ext.util.TaskRunner.Task} 
     * Ext.util.TaskRunner.Task instance, which can be useful for method chaining.
     */
    newTask: function(config) {
        var task = new Ext.util.TaskRunner.Task(config);
        task.manager = this;
        if (Ext.Timer.track) {
            task.creator = new Error().stack;
        }
        return task;
    },
    /**
     * Starts a new task.
     *
     * Before each invocation, Ext injects the property `taskRunCount` into the task object
     * so that calculations based on the repeat count can be performed.
     * 
     * The returned task will contain a `destroy` method that can be used to destroy the
     * task and cancel further calls. This is equivalent to the {@link #stop} method.
     *
     * @param {Object} task A config object that supports the following properties:
     * @param {Function} task.run The function to execute each time the task is invoked. The
     * function will be called at each interval and passed the `args` argument if specified,
     * and the current invocation count if not.
     * 
     * If a particular scope (`this` reference) is required, be sure to specify it using
     * the `scope` argument.
     * 
     * @param {Function} task.onError The function to execute in case of unhandled
     * error on task.run.
     *
     * @param {Boolean} task.run.return `false` from this function to terminate the task.
     *
     * @param {Number} task.interval The frequency in milliseconds with which the task
     * should be invoked.
     *
     * @param {Object[]} [task.args] An array of arguments to be passed to the function
     * specified by `run`. If not specified, the current invocation count is passed.
     *
     * @param {Boolean} [task.addCountToArgs=false] True to add the current invocation count as 
     * one of the arguments of args. 
     * Note: This only takes effect when args is specified.
     *
     * @param {Object} [task.scope] The scope (`this` reference) in which to execute the
     * `run` function. Defaults to the task config object.
     *
     * @param {Number} [task.duration] The length of time in milliseconds to invoke the task
     * before stopping automatically (defaults to indefinite).
     *
     * @param {Number} [task.repeat] The number of times to invoke the task before stopping
     * automatically (defaults to indefinite).
     *
     * @param {Number} [task.fireIdleEvent=true] If all tasks in a TaskRunner's execution 
     * sweep are configured with `fireIdleEvent: false`, then the 
     * {@link Ext.GlobalEvents#idle idle event} is not fired when the TaskRunner's execution
     * sweep finishes.
     *
     * @param {Boolean} [task.fireOnStart=false] True to run the task immediately instead of 
     * waiting for the _interval's_ initial pass to call the _run_ function.
     */
    start: function(task) {
        var me = this,
            now = Ext.Date.now();
        if (!task.pending) {
            me.tasks.push(task);
            task.pending = true;
        }
        // don't allow the task to be added to me.tasks again
        task.stopped = false;
        // might have been previously stopped...
        task.taskStartTime = now;
        task.taskRunTime = task.fireOnStart !== false ? 0 : task.taskStartTime;
        task.taskRunCount = 0;
        if (!me.firing) {
            if (task.fireOnStart !== false) {
                me.startTimer(0, now);
            } else {
                me.startTimer(task.interval, now);
            }
        }
        return task;
    },
    /**
     * Stops an existing running task.
     * @param {Object} task The task to stop.
     * @param {Boolean} andRemove Pass `true` to also remove the task from the queue.
     * @return {Object} The task
     */
    stop: function(task, andRemove) {
        var me = this,
            tasks = me.tasks,
            pendingCount = 0,
            i;
        // NOTE: we don't attempt to remove the task from me.tasks at this point because
        // this could be called from inside a task which would then corrupt the state of
        // the loop in onTick
        if (!task.stopped) {
            task.stopped = true;
            task.pending = false;
            if (task.onStop) {
                task.onStop.call(task.scope || task, task);
            }
        }
        if (andRemove) {
            Ext.Array.remove(tasks, task);
        }
        // If there are now no pending tasks
        // we shhuld stop the timer.
        for (i = 0; !pendingCount && i < tasks.length; i++) {
            if (!tasks[i].stopped) {
                pendingCount++;
            }
        }
        if (!pendingCount) {
            Ext.undefer(me.timerId);
            me.timerId = null;
        }
        return task;
    },
    /**
     * Stops all tasks that are currently running.
     * @param {Boolean} andRemove Pass `true` to also remove the tasks from the queue.
     */
    stopAll: function(andRemove) {
        var me = this;
        // onTick will take care of cleaning up the mess after this point...
        // must use reverse in case a task is removed.
        Ext.each(this.tasks, function(task) {
            me.stop(task, andRemove);
        }, null, true);
    },
    //-------------------------------------------------------------------------
    firing: false,
    nextExpires: 1.0E99,
    /**
     * @private
     */
    onTick: function() {
        var me = this,
            tasks = me.tasks,
            fireIdleEvent = me.fireIdleEvent,
            // null by default
            now = Ext.Date.now(),
            nextExpires = 1.0E99,
            len = tasks.length,
            expires, newTasks, i, task, rt, remove, args;
        var timer = Ext.Timer.get(me.timerId);
        // eslint-disable-line vars-on-top, one-var
        if (timer) {
            timer.tasks = [];
        }
        me.timerId = null;
        me.firing = true;
        // ensure we don't startTimer during this loop...
        // tasks.length can be > len if start is called during a task.run call... so we
        // first check len to avoid tasks.length reference but eventually we need to also
        // check tasks.length. we avoid repeating use of tasks.length by setting len at
        // that time (to help the next loop)
        for (i = 0; i < len || i < (len = tasks.length); ++i) {
            task = tasks[i];
            if (!(remove = task.stopped)) {
                expires = task.taskRunTime + task.interval;
                if (expires <= now) {
                    rt = 1;
                    // otherwise we have a stale "rt"
                    // If all tasks left specify fireIdleEvent as false, then don't do it
                    if (fireIdleEvent === null && task.fireIdleEvent !== false) {
                        fireIdleEvent = true;
                    }
                    task.taskRunCount++;
                    if (task.args) {
                        args = task.addCountToArgs ? task.args.concat([
                            task.taskRunCount
                        ]) : task.args;
                    } else {
                        args = [
                            task.taskRunCount
                        ];
                    }
                    // We want the exceptions not to get caught while unit testing
                    if (timer) {
                        timer.tasks.push(task);
                    }
                    if (me.disableTryCatch) {
                        rt = task.run.apply(task.scope || task, args);
                    } else {
                        try {
                            rt = task.run.apply(task.scope || task, args);
                        } catch (taskError) {
                            try {
                                Ext.log({
                                    fn: task.run,
                                    prefix: 'Error while running task',
                                    stack: taskError.stack,
                                    msg: taskError,
                                    level: 'error'
                                });
                                if (task.onError) {
                                    rt = task.onError.call(task.scope || task, task, taskError);
                                }
                            } catch (e) {}
                        }
                    }
                    // ignore
                    task.taskRunTime = now;
                    if (rt === false || task.taskRunCount === task.repeat) {
                        me.stop(task);
                        remove = true;
                    } else {
                        remove = task.stopped;
                        // in case stop was called by run
                        expires = now + task.interval;
                    }
                }
                if (!remove && task.duration && task.duration <= (now - task.taskStartTime)) {
                    me.stop(task);
                    remove = true;
                }
            }
            if (remove) {
                task.pending = false;
                // allow the task to be added to me.tasks again
                // once we detect that a task needs to be removed, we copy the tasks that
                // will carry forward into newTasks... this way we avoid O(N*N) to remove
                // each task from the tasks array (and ripple the array down) and also the
                // potentially wasted effort of making a new tasks[] even if all tasks are
                // going into the next wave.
                if (!newTasks) {
                    newTasks = tasks.slice(0, i);
                }
            } else // we don't set me.tasks here because callbacks can also start tasks,
            // which get added to me.tasks... so we will visit them in this loop
            // and account for their expirations in nextExpires...
            {
                if (newTasks) {
                    newTasks.push(task);
                }
                // we've cloned the tasks[], so keep this one...
                if (nextExpires > expires) {
                    nextExpires = expires;
                }
            }
        }
        // track the nearest expiration time
        if (newTasks) {
            // only now can we copy the newTasks to me.tasks since no user callbacks can
            // take place
            me.tasks = newTasks;
        }
        me.firing = false;
        // we're done, so allow startTimer afterwards
        if (me.tasks.length) {
            // we create a new Date here because all the callbacks could have taken a long
            // time... we want to base the next timeout on the current time (after the
            // callback storm):
            me.startTimer(nextExpires - now, Ext.Date.now());
        }
        // If all tasks fired and had fireIdleEvent=false then our fireIdleEvent var
        // will still be null. This is to allow any task that does not suppress idle
        // to override those that do. The only other reason our var will be null is if
        // no tasks fired. In which case, no need for idle either.
        if (fireIdleEvent === null) {
            fireIdleEvent = false;
        }
        Ext._suppressIdle = !fireIdleEvent;
    },
    /**
    * @private
    */
    startTimer: function(timeout, now) {
        var me = this,
            expires = now + timeout,
            timerId = me.timerId;
        // Check to see if this request is enough in advance of the current timer. If so,
        // we reschedule the timer based on this new expiration.
        if (timerId && me.nextExpires - expires > me.interval) {
            timerId = Ext.undefer(timerId);
        }
        if (!timerId) {
            if (timeout < me.interval) {
                timeout = me.interval;
            }
            me.timerId = Ext.defer(me.timerFn, timeout);
            me.nextExpires = expires;
            var timer = Ext.Timer.get(me.timerId);
            // eslint-disable-line vars-on-top
            if (timer) {
                timer.runner = me;
            }
        }
    }
}, function() {
    var me = this,
        proto = me.prototype;
    /**
     * Destroys this instance, stopping all tasks that are currently running.
     * @method destroy
     */
    proto.destroy = proto.stopAll;
    /**
     * Instances of this class are created by {@link Ext.util.TaskRunner#newTask} method.
     * 
     * For details on config properties, see {@link Ext.util.TaskRunner#start}.
     * @class Ext.util.TaskRunner.Task
     */
    me.Task = new Ext.Class({
        isTask: true,
        /**
         * This flag is set to `true` by {@link #stop}.
         * @private
         */
        stopped: true,
        // this avoids the odd combination of !stopped && !pending
        fireOnStart: false,
        constructor: function(config) {
            Ext.apply(this, config);
        },
        /**
         * Restarts this task, clearing it duration, expiration and run count.
         * @param {Number} [interval] Optionally reset this task's interval.
         */
        restart: function(interval) {
            if (interval !== undefined) {
                this.interval = interval;
            }
            this.manager.start(this);
        },
        /**
         * Starts this task if it is not already started.
         * @param {Number} [interval] Optionally reset this task's interval.
         */
        start: function(interval) {
            if (this.stopped) {
                this.restart(interval);
            }
        },
        /**
         * Stops this task.
         */
        stop: function(andRemove) {
            this.manager.stop(this, andRemove);
        },
        destroy: function() {
            this.stop(true);
        }
    });
    proto = me.Task.prototype;
    /**
     * Destroys this instance, stopping this task's execution.
     * @method destroy
     * @member Ext.util.TaskRunner.Task
     */
    proto.destroy = proto.stop;
});

/**
 * @class Ext.fx.target.Target

This class specifies a generic target for an animation. It provides a wrapper around a
series of different types of objects to allow for a generic animation API.
A target can be a single object or a Composite object containing other objects that are 
to be animated. This class and it's subclasses are generally not created directly, the 
underlying animation will create the appropriate Ext.fx.target.Target object by passing 
the instance to be animated.

The following types of objects can be animated:

- {@link Ext.fx.target.Component Components}
- {@link Ext.fx.target.Element Elements}
- {@link Ext.fx.target.Sprite Sprites}

 * @abstract
 */
Ext.define('Ext.fx.target.Target', {
    isAnimTarget: true,
    /**
     * Creates new Target.
     * @param {Ext.Component/Ext.dom.Element/Ext.draw.sprite.Sprite} target The object
     * to be animated
     */
    constructor: function(target) {
        this.target = target;
        this.id = this.getId();
    },
    getId: function() {
        return this.target.id;
    },
    remove: function() {
        Ext.destroy(this.target);
    }
});

/**
 * @class Ext.fx.target.Element
 * 
 * This class represents a animation target for an {@link Ext.dom.Element}. In general this class
 * will not be created directly, the {@link Ext.dom.Element} will be passed to the animation and
 * and the appropriate target will be created.
 */
Ext.define('Ext.fx.target.Element', {
    extend: 'Ext.fx.target.Target',
    type: 'element',
    constructor: function(target) {
        this.callParent([
            target
        ]);
        // Allow simple local left/top style setting for top level absolute positioned elements.
        this.isAbsoluteOnPage = this.target.dom.parentNode === document.body && this.target.isStyle('position', 'absolute');
    },
    getElVal: function(el, attr, val) {
        if (val === undefined) {
            if (attr === 'x') {
                val = el.getX();
            } else if (attr === 'y') {
                val = el.getY();
            } else if (attr === 'scrollTop') {
                val = el.getScroll().top;
            } else if (attr === 'scrollLeft') {
                val = el.getScroll().left;
            } else if (attr === 'height') {
                val = el.getHeight();
            } else if (attr === 'width') {
                val = el.getWidth();
            } else {
                val = el.getStyle(attr);
            }
        }
        return val;
    },
    getAttr: function(attr, val) {
        var el = this.target;
        return [
            [
                el,
                this.getElVal(el, attr, val)
            ]
        ];
    },
    setAttr: function(targetData) {
        var ln = targetData.length,
            attrs, attr, o, i, j, ln2;
        for (i = 0; i < ln; i++) {
            attrs = targetData[i].attrs;
            for (attr in attrs) {
                if (attrs.hasOwnProperty(attr)) {
                    ln2 = attrs[attr].length;
                    for (j = 0; j < ln2; j++) {
                        o = attrs[attr][j];
                        this.setElVal(o[0], attr, o[1]);
                    }
                }
            }
        }
    },
    setElVal: function(element, attr, value) {
        if (attr === 'x') {
            if (this.isAbsoluteOnPage) {
                element.setLocalX(value);
            } else {
                element.setXY([
                    value,
                    null
                ]);
            }
        } else if (attr === 'y') {
            if (this.isAbsoluteOnPage) {
                element.setLocalY(value);
            } else {
                element.setXY([
                    null,
                    value
                ]);
            }
        } else if (attr === 'scrollTop') {
            element.scrollTo('top', value);
        } else if (attr === 'scrollLeft') {
            element.scrollTo('left', value);
        } else if (attr === 'width') {
            element.setWidth(value);
        } else if (attr === 'height') {
            element.setHeight(value);
        } else {
            element.setStyle(attr, value);
        }
    }
});

/**
 * @class Ext.fx.target.ElementCSS
 * 
 * This class represents a animation target for an {@link Ext.dom.Element} that supports CSS
 * based animation. In general this class will not be created directly, the {@link Ext.dom.Element} 
 * will be passed to the animation and the appropriate target will be created.
 */
Ext.define('Ext.fx.target.ElementCSS', {
    extend: 'Ext.fx.target.Element',
    setAttr: function(targetData, isFirstFrame) {
        var cssArr = {
                attrs: [],
                duration: [],
                easing: []
            },
            ln = targetData.length,
            cleanerFn = function() {
                this.setStyle(Ext.supports.CSS3Prefix + 'TransitionProperty', null);
                this.setStyle(Ext.supports.CSS3Prefix + 'TransitionDuration', null);
                this.setStyle(Ext.supports.CSS3Prefix + 'TransitionTimingFunction', null);
            },
            single = {
                single: true
            },
            attributes, attrs, attr, easing, duration, o, i, j, ln2;
        for (i = 0; i < ln; i++) {
            attrs = targetData[i];
            duration = attrs.duration;
            easing = attrs.easing;
            attrs = attrs.attrs;
            for (attr in attrs) {
                if (Ext.Array.indexOf(cssArr.attrs, attr) === -1) {
                    cssArr.attrs.push(attr.replace(/[A-Z]/g, function(v) {
                        return '-' + v.toLowerCase();
                    }));
                    cssArr.duration.push(duration + 'ms');
                    cssArr.easing.push(easing);
                }
            }
        }
        attributes = cssArr.attrs.join(',');
        duration = cssArr.duration.join(',');
        easing = cssArr.easing.join(', ');
        for (i = 0; i < ln; i++) {
            attrs = targetData[i].attrs;
            for (attr in attrs) {
                ln2 = attrs[attr].length;
                for (j = 0; j < ln2; j++) {
                    o = attrs[attr][j];
                    o[0].setStyle(Ext.supports.CSS3Prefix + 'TransitionProperty', isFirstFrame ? '' : attributes);
                    o[0].setStyle(Ext.supports.CSS3Prefix + 'TransitionDuration', isFirstFrame ? '' : duration);
                    o[0].setStyle(Ext.supports.CSS3Prefix + 'TransitionTimingFunction', isFirstFrame ? '' : easing);
                    o[0].setStyle(attr, o[1]);
                    // Must trigger reflow to make this get used as the start point
                    // for the transition that follows
                    if (isFirstFrame) {
                        o = o[0].dom.offsetWidth;
                    } else {
                        // Remove transition properties when completed.
                        o[0].on(Ext.supports.CSS3TransitionEnd, cleanerFn, o[0], single);
                    }
                }
            }
        }
    }
});

/**
 * @class Ext.fx.target.CompositeElement
 * 
 * This class represents a animation target for a {@link Ext.CompositeElement}. It allows
 * each {@link Ext.dom.Element} in the group to be animated as a whole. In general this class
 * will not be created directly, the {@link Ext.CompositeElement} will be passed to the animation
 * and and the appropriate target will be created.
 */
Ext.define('Ext.fx.target.CompositeElement', {
    extend: 'Ext.fx.target.Element',
    /**
     * @property {Boolean} isComposite
     * `true` in this class to identify an object as an instantiated CompositeElement,
     * or subclass thereof.
     */
    isComposite: true,
    constructor: function(target) {
        target.id = target.id || Ext.id(null, 'ext-composite-');
        this.callParent([
            target
        ]);
    },
    getAttr: function(attr, val) {
        var out = [],
            target = this.target,
            elements = target.elements,
            length = elements.length,
            i, el;
        for (i = 0; i < length; i++) {
            el = elements[i];
            if (el) {
                el = target.getElement(el);
                out.push([
                    el,
                    this.getElVal(el, attr, val)
                ]);
            }
        }
        return out;
    },
    setAttr: function(targetData) {
        var target = this.target,
            ln = targetData.length,
            elements = target.elements,
            ln3 = elements.length,
            value, k, attrs, attr, el, i, j, ln2;
        for (i = 0; i < ln; i++) {
            attrs = targetData[i].attrs;
            for (attr in attrs) {
                if (attrs.hasOwnProperty(attr)) {
                    ln2 = attrs[attr].length;
                    for (j = 0; j < ln2; j++) {
                        value = attrs[attr][j][1];
                        for (k = 0; k < ln3; ++k) {
                            el = elements[k];
                            if (el) {
                                el = target.getElement(el);
                                this.setElVal(el, attr, value);
                            }
                        }
                    }
                }
            }
        }
    },
    remove: function() {
        this.target.destroy();
    }
});

/**
 * @class Ext.fx.target.CompositeElementCSS
 * 
 * This class represents a animation target for a {@link Ext.CompositeElement}, where the
 * constituent elements support CSS based animation. It allows each {@link Ext.dom.Element} in 
 * the group to be animated as a whole. In general this class will not be created directly, 
 * the {@link Ext.CompositeElement} will be passed to the animation and the appropriate target 
 * will be created.
 */
Ext.define('Ext.fx.target.CompositeElementCSS', {
    extend: 'Ext.fx.target.CompositeElement',
    requires: [
        'Ext.fx.target.ElementCSS'
    ],
    setAttr: function() {
        return Ext.fx.target.ElementCSS.prototype.setAttr.apply(this, arguments);
    }
});

/**
 * @class Ext.fx.target.Sprite
 * This class represents an animation target for a {@link Ext.draw.sprite.Sprite}. In general
 * this class will not be created directly, the {@link Ext.draw.sprite.Sprite} will be passed
 * to the animation and and the appropriate target will be created.
 */
Ext.define('Ext.fx.target.Sprite', {
    extend: 'Ext.fx.target.Target',
    type: 'draw',
    getFromPrim: function(sprite, attr) {
        var obj;
        switch (attr) {
            case 'rotate':
            case 'rotation':
                obj = sprite.attr.rotation;
                return {
                    x: obj.x || 0,
                    y: obj.y || 0,
                    degrees: obj.degrees || 0
                };
            case 'scale':
            case 'scaling':
                obj = sprite.attr.scaling;
                return {
                    x: obj.x || 1,
                    y: obj.y || 1,
                    cx: obj.cx || 0,
                    cy: obj.cy || 0
                };
            case 'translate':
            case 'translation':
                obj = sprite.attr.translation;
                return {
                    x: obj.x || 0,
                    y: obj.y || 0
                };
            default:
                return sprite.attr[attr];
        }
    },
    getAttr: function(attr, val) {
        return [
            [
                this.target,
                val !== undefined ? val : this.getFromPrim(this.target, attr)
            ]
        ];
    },
    setAttr: function(targetData) {
        var ln = targetData.length,
            spriteArr = [],
            attrsConf, attr, attrArr, attrs, sprite, idx, value, i, j, x, y, ln2;
        for (i = 0; i < ln; i++) {
            attrsConf = targetData[i].attrs;
            for (attr in attrsConf) {
                attrArr = attrsConf[attr];
                ln2 = attrArr.length;
                for (j = 0; j < ln2; j++) {
                    sprite = attrArr[j][0];
                    attrs = attrArr[j][1];
                    if (attr === 'translate' || attr === 'translation') {
                        value = {
                            x: attrs.x,
                            y: attrs.y
                        };
                    } else if (attr === 'rotate' || attr === 'rotation') {
                        x = attrs.x;
                        if (isNaN(x)) {
                            x = null;
                        }
                        y = attrs.y;
                        if (isNaN(y)) {
                            y = null;
                        }
                        value = {
                            degrees: attrs.degrees,
                            x: x,
                            y: y
                        };
                    } else if (attr === 'scale' || attr === 'scaling') {
                        x = attrs.x;
                        if (isNaN(x)) {
                            x = null;
                        }
                        y = attrs.y;
                        if (isNaN(y)) {
                            y = null;
                        }
                        value = {
                            x: x,
                            y: y,
                            cx: attrs.cx,
                            cy: attrs.cy
                        };
                    } else if (attr === 'width' || attr === 'height' || attr === 'x' || attr === 'y') {
                        value = parseFloat(attrs);
                    } else {
                        value = attrs;
                    }
                    idx = Ext.Array.indexOf(spriteArr, sprite);
                    if (idx === -1) {
                        spriteArr.push([
                            sprite,
                            {}
                        ]);
                        idx = spriteArr.length - 1;
                    }
                    spriteArr[idx][1][attr] = value;
                }
            }
        }
        ln = spriteArr.length;
        for (i = 0; i < ln; i++) {
            spriteArr[i][0].setAttributes(spriteArr[i][1]);
        }
        this.target.redraw();
    }
});

/**
 * @class Ext.fx.target.CompositeSprite
 *
 * This class represents a animation target for a {@link Ext.draw.sprite.Composite}. It allows
 * each {@link Ext.draw.sprite.Sprite} in the group to be animated as a whole. In general this class
 * will not be created directly, the {@link Ext.draw.sprite.Composite} will be passed
 * to the animation and and the appropriate target will be created.
 */
Ext.define('Ext.fx.target.CompositeSprite', {
    extend: 'Ext.fx.target.Sprite',
    getAttr: function(attr, val) {
        var out = [],
            sprites = [].concat(this.target.items),
            length = sprites.length,
            i, sprite;
        for (i = 0; i < length; i++) {
            sprite = sprites[i];
            out.push([
                sprite,
                val !== undefined ? val : this.getFromPrim(sprite, attr)
            ]);
        }
        return out;
    }
});

/**
 * @class Ext.fx.target.Component
 * 
 * This class represents a animation target for a {@link Ext.Component}. In general this class
 * will not be created directly, the {@link Ext.Component} will be passed to the animation and
 * and the appropriate target will be created.
 */
Ext.define('Ext.fx.target.Component', {
    extend: 'Ext.fx.target.Target',
    type: 'component',
    // Methods to call to retrieve unspecified "from" values from a target Component
    getPropMethod: {
        top: function() {
            return this.getPosition(true)[1];
        },
        left: function() {
            return this.getPosition(true)[0];
        },
        x: function() {
            return this.getPosition()[0];
        },
        y: function() {
            return this.getPosition()[1];
        },
        height: function() {
            return this.getHeight();
        },
        width: function() {
            return this.getWidth();
        },
        opacity: function() {
            return this.el.getStyle('opacity');
        }
    },
    setMethods: {
        top: 'setPosition',
        left: 'setPosition',
        x: 'setPagePosition',
        y: 'setPagePosition',
        height: 'setSize',
        width: 'setSize',
        opacity: 'setOpacity'
    },
    // Read the named attribute from the target Component. Use the defined getter for the attribute
    getAttr: function(attr, val) {
        return [
            [
                this.target,
                val !== undefined ? val : this.getPropMethod[attr].call(this.target)
            ]
        ];
    },
    setAttr: function(targetData, isFirstFrame, isLastFrame) {
        var me = this,
            ln = targetData.length,
            attrs, attr, o, i, j, targets, left, top, w, h,
            methodsToCall = {},
            methodProps;
        for (i = 0; i < ln; i++) {
            attrs = targetData[i].attrs;
            for (attr in attrs) {
                targets = attrs[attr].length;
                for (j = 0; j < targets; j++) {
                    o = attrs[attr][j];
                    methodProps = methodsToCall[me.setMethods[attr]] || (methodsToCall[me.setMethods[attr]] = {});
                    methodProps.target = o[0];
                    methodProps[attr] = o[1];
                }
            }
            if (methodsToCall.setPosition) {
                o = methodsToCall.setPosition;
                left = (o.left === undefined) ? undefined : parseFloat(o.left);
                top = (o.top === undefined) ? undefined : parseFloat(o.top);
                o.target.setPosition(left, top);
            }
            if (methodsToCall.setPagePosition) {
                o = methodsToCall.setPagePosition;
                o.target.setPagePosition(o.x, o.y);
            }
            if (methodsToCall.setSize) {
                o = methodsToCall.setSize;
                // Dimensions not being animated MUST NOT be autosized.
                // They must remain at current value.
                w = (o.width === undefined) ? o.target.getWidth() : parseFloat(o.width);
                h = (o.height === undefined) ? o.target.getHeight() : parseFloat(o.height);
                // Only set the size of the Component on the last frame, or if the animation was
                // configured with dynamic: true.
                // In other cases, we just set the target element size.
                // This will result in either clipping if animating a reduction in size,
                // or the revealing of the inner elements of the Component if animating
                // an increase in size.
                // Component's animate function initially resizes to the larger size
                // before resizing the outer element to clip the contents.
                o.target.el.setSize(w, h);
                if (isLastFrame || me.dynamic) {
                    // Defer the final sizing & layout until we are outside of this frame.
                    // In case anything in the resulting layout calls animation.
                    // If it does, *this* frame will fire again... recursively
                    Ext.GlobalEvents.on({
                        idle: Ext.Function.bind(o.target.setSize, o.target, [
                            w,
                            h
                        ]),
                        single: true
                    });
                }
            }
            if (methodsToCall.setOpacity) {
                o = methodsToCall.setOpacity;
                o.target.el.setStyle('opacity', o.opacity);
            }
        }
    }
});

/**
 * @class Ext.fx.Queue
 * Animation Queue mixin to handle chaining and queueing by target.
 * @private
 */
Ext.define('Ext.fx.Queue', {
    requires: [
        'Ext.util.HashMap'
    ],
    constructor: function() {
        this.targets = new Ext.util.HashMap();
        this.fxQueue = {};
    },
    /**
     * @private
     */
    getFxDefaults: function(targetId) {
        var target = this.targets.get(targetId);
        if (target) {
            return target.fxDefaults;
        }
        return {};
    },
    /**
     * @private
     */
    setFxDefaults: function(targetId, obj) {
        var target = this.targets.get(targetId);
        if (target) {
            target.fxDefaults = Ext.apply(target.fxDefaults || {}, obj);
        }
    },
    /**
     * @private
     */
    stopAnimation: function(targetId, suppressEvent) {
        var me = this,
            queue = me.getFxQueue(targetId),
            ln = queue.length,
            item;
        while (ln) {
            item = queue[ln - 1];
            if (item) {
                item.end(suppressEvent);
            }
            ln--;
        }
    },
    /**
     * @private
     * Returns current animation object if the element has any effects actively running or queued,
     * else returns false.
     */
    getActiveAnimation: function(targetId) {
        var queue = this.getFxQueue(targetId);
        return (queue && !!queue.length) ? queue[0] : false;
    },
    /**
     * @private
     */
    hasFxBlock: function(targetId) {
        var queue = this.getFxQueue(targetId);
        return queue && queue[0] && queue[0].block;
    },
    /**
     * @private
     * Get fx queue for passed target, create if needed.
     */
    getFxQueue: function(targetId) {
        if (!targetId) {
            return false;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            fxQueue = me.fxQueue,
            queue = fxQueue[targetId],
            target = me.targets.get(targetId);
        if (!target) {
            return false;
        }
        if (!queue) {
            me.fxQueue[targetId] = fxQueue[targetId] = [];
            // GarbageCollector will need to clean up Elements since they
            // aren't currently observable
            if (target.type !== 'element') {
                target.target.on('destroy', function() {
                    fxQueue[targetId] = null;
                    delete fxQueue[targetId];
                });
            }
        }
        return me.fxQueue[targetId];
    },
    /**
     * @private
     * Clears the fx queue of any pending animations
     */
    clearFxQueue: function() {
        Ext.Object.clear(this.fxQueue);
    },
    /**
     * @private
     */
    queueFx: function(anim) {
        var me = this,
            target = anim.target,
            targetId = target.getId(),
            queue, ln;
        if (!target) {
            return;
        }
        queue = me.getFxQueue(targetId);
        ln = queue.length;
        if (ln) {
            if (anim.concurrent) {
                anim.paused = false;
            } else {
                queue[ln - 1].on('afteranimate', function() {
                    anim.paused = false;
                });
            }
        } else {
            anim.paused = false;
        }
        anim.on('afteranimate', function() {
            var el;
            Ext.Array.remove(queue, anim);
            if (queue.length === 0) {
                me.targets.remove(anim.target);
                me.fxQueue[targetId] = null;
                delete me.fxQueue[targetId];
            }
            if (anim.remove) {
                if (target.type === 'element') {
                    el = Ext.get(targetId);
                    if (el) {
                        el.destroy();
                    }
                }
            }
        }, me, {
            single: true
        });
        queue.push(anim);
    }
});

/**
 * @class Ext.fx.Manager
 * Animation Manager which keeps track of all current animations and manages them
 * on a frame by frame basis.
 * @private
 * @singleton
 */
Ext.define('Ext.fx.Manager', {
    singleton: true,
    requires: [
        'Ext.util.MixedCollection',
        'Ext.util.TaskRunner',
        'Ext.fx.target.Element',
        'Ext.fx.target.ElementCSS',
        'Ext.fx.target.CompositeElement',
        'Ext.fx.target.CompositeElementCSS',
        'Ext.fx.target.Sprite',
        'Ext.fx.target.CompositeSprite',
        'Ext.fx.target.Component'
    ],
    mixins: {
        queue: 'Ext.fx.Queue'
    },
    /**
     * @private
     */
    constructor: function() {
        var me = this;
        me.items = new Ext.util.MixedCollection();
        me.targetArr = {};
        me.mixins.queue.constructor.call(me);
        // Do not use fireIdleEvent: false. Each tick of the TaskRunner needs to fire the idleEvent
        // in case an animation callback/listener adds a listener.
        me.taskRunner = new Ext.util.TaskRunner();
    },
    /**
     * @cfg {Number} interval Default interval in miliseconds to calculate each frame.
     * Defaults to 16ms (~60fps)
     */
    interval: 16,
    /**
     * @cfg {Boolean} forceJS Force the use of JavaScript-based animation instead of CSS3 animation,
     * even when CSS3 animation is supported by the browser. This defaults to true currently,
     * as CSS3 animation support is still considered experimental at this time, and if used
     * should be thouroughly tested across all targeted browsers.
     * @protected
     */
    forceJS: true,
    /**
     * @private
     * Target Factory
     */
    createTarget: function(target) {
        var me = this,
            useCSS3 = !me.forceJS && Ext.supports.Transitions,
            targetObj;
        me.useCSS3 = useCSS3;
        if (target) {
            // dom element, string or fly
            if (target.tagName || Ext.isString(target) || target.isFly) {
                target = Ext.get(target);
                targetObj = new Ext.fx.target['Element' + (useCSS3 ? 'CSS' : '')](target);
            }
            // Element
            else if (target.dom) {
                targetObj = new Ext.fx.target['Element' + (useCSS3 ? 'CSS' : '')](target);
            }
            // Element Composite
            else if (target.isComposite) {
                targetObj = new Ext.fx.target['CompositeElement' + (useCSS3 ? 'CSS' : '')](target);
            }
            // Draw Sprite
            else if (target.isSprite) {
                targetObj = new Ext.fx.target.Sprite(target);
            }
            // Draw Sprite Composite
            else if (target.isCompositeSprite) {
                targetObj = new Ext.fx.target.CompositeSprite(target);
            }
            // Component
            else if (target.isComponent) {
                targetObj = new Ext.fx.target.Component(target);
            } else if (target.isAnimTarget) {
                return target;
            } else {
                return null;
            }
            me.targets.add(targetObj);
            return targetObj;
        } else {
            return null;
        }
    },
    /**
     * Add an Anim to the manager. This is done automatically when an Anim instance is created.
     * @param {Ext.fx.Anim} anim
     */
    addAnim: function(anim) {
        var me = this,
            items = me.items,
            task = me.task;
        // Make sure we use the anim's id, not the anim target's id here. The anim id will be unique
        // on each call to addAnim. `anim.target` is the DOM element being targeted, and since
        // multiple animations can target a single DOM node concurrently, the target id cannot
        // be assumned to be unique.
        items.add(anim.id, anim);
        // Start the timer if not already running
        if (!task && items.length) {
            task = me.task = {
                run: me.runner,
                interval: me.interval,
                scope: me
            };
            me.taskRunner.start(task);
        }
    },
    /**
     * Remove an Anim from the manager. This is done automatically when an Anim ends.
     * @param {Ext.fx.Anim} anim
     */
    removeAnim: function(anim) {
        var me = this,
            items = me.items,
            task = me.task;
        items.removeAtKey(anim.id);
        // Stop the timer if there are no more managed Anims
        if (task && !items.length) {
            me.taskRunner.stop(task);
            delete me.task;
        }
    },
    /**
     * @private
     * Runner function being called each frame
     */
    runner: function() {
        var me = this,
            items = me.items.getRange(),
            i = 0,
            len = items.length,
            anim;
        me.targetArr = {};
        // Single timestamp for all animations this interval
        me.timestamp = new Date();
        // Loop to start any new animations first before looping to
        // execute running animations (which will also include all animations
        // started in this loop). This is a subtle difference from simply
        // iterating in one loop and starting then running each animation,
        // but separating the loops is necessary to ensure that all new animations
        // actually kick off prior to existing ones regardless of array order.
        // Otherwise in edge cases when there is excess latency in overall
        // performance, allowing existing animations to run before new ones can
        // lead to dropped frames and subtle race conditions when they are
        // interdependent, which is often the case with certain Element fx.
        for (; i < len; i++) {
            anim = items[i];
            if (anim.isReady()) {
                me.startAnim(anim);
            }
        }
        for (i = 0; i < len; i++) {
            anim = items[i];
            if (anim.isRunning()) {
                me.runAnim(anim);
            }
        }
        // Apply all the pending changes to their targets
        me.applyPendingAttrs();
        // Avoid retaining target references after we are finished with anims
        me.targetArr = null;
    },
    /**
     * @private
     * Start the individual animation (initialization)
     */
    startAnim: function(anim) {
        anim.start(this.timestamp);
    },
    /**
     * @private
     * Run the individual animation for this frame
     */
    runAnim: function(anim, forceEnd) {
        if (!anim) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            useCSS3 = me.useCSS3 && anim.target.type === 'element',
            elapsedTime = me.timestamp - anim.startTime,
            lastFrame = (elapsedTime >= anim.duration),
            target, o;
        if (forceEnd) {
            elapsedTime = anim.duration;
            lastFrame = true;
        }
        target = me.collectTargetData(anim, elapsedTime, useCSS3, lastFrame);
        // For CSS3 animation, we need to immediately set the first frame's attributes
        // without any transition to get a good initial state, then add the transition
        // properties and set the final attributes.
        if (useCSS3) {
            // Flush the collected attributes, without transition
            anim.target.setAttr(target.anims[anim.id].attributes, true);
            // Add the end frame data
            me.collectTargetData(anim, anim.duration, useCSS3, lastFrame);
            // Pause the animation so runAnim doesn't keep getting called
            anim.paused = true;
            target = anim.target.target;
            // We only want to attach an event on the last element in a composite
            if (anim.target.isComposite) {
                target = anim.target.target.last();
            }
            // Listen for the transitionend event
            o = {};
            o[Ext.supports.CSS3TransitionEnd] = anim.lastFrame;
            o.scope = anim;
            o.single = true;
            target.on(o);
        }
        return target;
    },
    jumpToEnd: function(anim) {
        var me = this,
            target, clear;
        // We may not be in the middle of a tick, where targetAttr is cleared,
        // so if we don't have it, poke it in here while we jump to the end state
        if (!me.targetArr) {
            me.targetArr = {};
            clear = true;
        }
        target = me.runAnim(anim, true);
        me.applyAnimAttrs(target, target.anims[anim.id]);
        if (clear) {
            me.targetArr = null;
        }
    },
    /**
     * @private
     * Collect target attributes for the given Anim object at the given timestamp
     * @param {Ext.fx.Anim} anim The Anim instance
     * @param {Number} elapsedTime Time after the anim's start time
     * @param {Boolean} [useCSS3=false] True if using CSS3-based animation, else false
     * @param {Boolean} [isLastFrame=false] True if this is the last frame of animation to be run,
     * else false
     * @return {Object} The animation target wrapper object containing the passed animation
     * along with the new attributes to set on the target's element in the next animation frame.
     */
    collectTargetData: function(anim, elapsedTime, useCSS3, isLastFrame) {
        var targetId = anim.target.getId(),
            target = this.targetArr[targetId];
        if (!target) {
            // Create a thin wrapper around the target so that we can create a link between the
            // target element and its associated animations. This is important later when applying
            // attributes to the target so that each animation can be independently run with its own
            // duration and stopped at any point without affecting other animations for the same
            // target.
            target = this.targetArr[targetId] = {
                id: targetId,
                el: anim.target,
                anims: {}
            };
        }
        // This is a wrapper for the animation so that we can also save state along with it,
        // including the current elapsed time and lastFrame status. Even though this method only
        // adds a single anim object per call, each target element could have multiple animations
        // associated with it, which is why the anim is added to the target's `anims` hash by id.
        target.anims[anim.id] = {
            id: anim.id,
            anim: anim,
            elapsed: elapsedTime,
            isLastFrame: isLastFrame,
            // This is the object that gets applied to the target element below
            // in applyPendingAttrs():
            attributes: [
                {
                    duration: anim.duration,
                    easing: (useCSS3 && anim.reverse) ? anim.easingFn.reverse().toCSS3() : anim.easing,
                    // This is where the magic happens. The anim calculates what its new attributes
                    // should be based on the current frame and returns those as a hash of values.
                    attrs: anim.runAnim(elapsedTime)
                }
            ]
        };
        return target;
    },
    // Duplicating this code for performance reasons. We only want to apply the anims
    // to a single animation because we're hitting the end. It may be out of sequence from
    // the runner timer.
    applyAnimAttrs: function(target, animWrap) {
        var anim = animWrap.anim;
        if (animWrap.attributes && anim.isRunning()) {
            target.el.setAttr(animWrap.attributes, false, animWrap.isLastFrame);
            // If this particular anim is at the last frame end it
            if (animWrap.isLastFrame) {
                anim.lastFrame();
            }
        }
    },
    /**
     * @private
     * Apply all pending attribute changes to their targets
     */
    applyPendingAttrs: function() {
        var targetArr = this.targetArr,
            target, targetId, animWrap, anim, animId;
        // Loop through each target
        for (targetId in targetArr) {
            if (targetArr.hasOwnProperty(targetId)) {
                target = targetArr[targetId];
                // Each target could have multiple associated animations, so iterate those
                for (animId in target.anims) {
                    if (target.anims.hasOwnProperty(animId)) {
                        animWrap = target.anims[animId];
                        anim = animWrap.anim;
                        // If the animation has valid attributes, set them on the target
                        if (animWrap.attributes && anim.isRunning()) {
                            target.el.setAttr(animWrap.attributes, false, animWrap.isLastFrame);
                            // If this particular anim is at the last frame end it
                            if (animWrap.isLastFrame) {
                                anim.lastFrame();
                            }
                        }
                    }
                }
            }
        }
    },
    clear: function() {
        var me = this;
        if (me.taskRunner) {
            me.taskRunner.stopAll(true);
        }
        me.targetArr = {};
        me.items.clear();
        me.targets.clear();
        me.clearFxQueue();
    }
});

/**
 * @class Ext.fx.Animator
 *
 * This class is used to run keyframe based animations, which follows the CSS3 based animation
 * structure. Keyframe animations differ from typical from/to animations in that they offer
 * the ability to specify values at various points throughout the animation.
 *
 * ## Using Keyframes
 *
 * The {@link #keyframes} option is the most important part of specifying an animation when using
 * this class. A key frame is a point in a particular animation. We represent this as a percentage
 * of the total animation duration. At each key frame, we can specify the target values at that
 * time. Note that you *must* specify the values at 0% and 100%, the start and ending values.
 * There is also a {@link #keyframe} event that fires after each key frame is reached.
 *
 * ## Example
 *
 * In the example below, we modify the values of the element at each fifth throughout the animation.
 *
 *     @example
 *     Ext.create('Ext.fx.Animator', {
 *         target: Ext.getBody().createChild({
 *             style: {
 *                 width: '100px',
 *                 height: '100px',
 *                 'background-color': 'red'
 *             }
 *         }),
 *         duration: 10000, // 10 seconds
 *         keyframes: {
 *             0: {
 *                 opacity: 1,
 *                 backgroundColor: 'FF0000'
 *             },
 *             20: {
 *                 x: 30,
 *                 opacity: 0.5
 *             },
 *             40: {
 *                 x: 130,
 *                 backgroundColor: '0000FF'
 *             },
 *             60: {
 *                 y: 80,
 *                 opacity: 0.3
 *             },
 *             80: {
 *                 width: 200,
 *                 y: 200
 *             },
 *             100: {
 *                 opacity: 1,
 *                 backgroundColor: '00FF00'
 *             }
 *         }
 *     });
 */
Ext.define('Ext.fx.Animator', {
    mixins: {
        observable: 'Ext.util.Observable'
    },
    requires: [
        'Ext.fx.Manager'
    ],
    /**
     * @property {Boolean} isAnimator
     * `true` in this class to identify an object as an instantiated Animator, or subclass thereof.
     */
    isAnimator: true,
    /**
     * @cfg {Number} duration
     * Time in milliseconds for the animation to last. Defaults to 250.
     */
    duration: 250,
    /**
     * @cfg {Number} delay
     * Time to delay before starting the animation. Defaults to 0.
     */
    delay: 0,
    //  private used to track a delayed starting time
    delayStart: 0,
    /**
     * @cfg {Boolean} dynamic
     * Currently only for Component Animation: Only set a component's outer element size bypassing
     * layouts. Set to true to do full layouts for every frame of the animation. Defaults to false.
     */
    dynamic: false,
    /**
     * @cfg {String} easing
     *
     * This describes how the intermediate values used during a transition will be calculated.
     * It allows for a transition to change speed over its duration.
     *
     *  - backIn
     *  - backOut
     *  - bounceIn
     *  - bounceOut
     *  - ease
     *  - easeIn
     *  - easeOut
     *  - easeInOut
     *  - elasticIn
     *  - elasticOut
     *  - cubic-bezier(x1, y1, x2, y2)
     *
     * Note that cubic-bezier will create a custom easing curve following the CSS3
     * [transition-timing-function][0] specification.  The four values specify points P1 and P2
     * of the curve as (x1, y1, x2, y2). All values must be in the range [0, 1] or the definition
     * is invalid.
     *
     * [0]: http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag
     */
    easing: 'ease',
    /**
     * Flag to determine if the animation has started
     * @property running
     * @type Boolean
     */
    running: false,
    /**
     * Flag to determine if the animation is paused. Only set this to true if you need to
     * keep the Anim instance around to be unpaused later; otherwise call {@link #end}.
     * @property paused
     * @type Boolean
     */
    paused: false,
    /**
     * @private
     */
    damper: 1,
    /**
     * @cfg {Number} iterations
     * Number of times to execute the animation. Defaults to 1.
     */
    iterations: 1,
    /**
     * Current iteration the animation is running.
     * @property currentIteration
     * @type Number
     */
    currentIteration: 0,
    /**
     * Current keyframe step of the animation.
     * @property keyframeStep
     * @type Number
     */
    keyframeStep: 0,
    /**
     * @private
     */
    animKeyFramesRE: /^(from|to|\d+%?)$/,
    /**
     * @cfg {Ext.fx.target.Target} target
     * The Ext.fx.target to apply the animation to.  If not specified during initialization,
     * this can be passed to the applyAnimator method to apply the same animation to many targets.
     */
    /**
     * @event beforeanimate
     * Fires before the animation starts. A handler can return false to cancel the animation.
     * @param {Ext.fx.Animator} this
     */
    /**
      * @event keyframe
      * Fires at each keyframe.
      * @param {Ext.fx.Animator} this
      * @param {Number} keyframe step number
      */
    /**
     * @event afteranimate
     * Fires when the animation is complete.
     * @param {Ext.fx.Animator} this
     * @param {Date} startTime
     */
    /**
     * @cfg {Object} keyframes
     * Animation keyframes follow the CSS3 Animation configuration pattern. 'from' is always
     * considered '0%' and 'to' is considered '100%'.<b>Every keyframe declaration must have
     * a keyframe rule for 0% and 100%, possibly defined using "from" or "to"</b>.
     * A keyframe declaration without these keyframe selectors is invalid and will not be available
     * for animation. The keyframe declaration for a keyframe rule consists of properties and
     * values. Properties that are unable to be animated are ignored in these rules,
     * with the exception of 'easing' which can be changed at each keyframe. For example:
     * 
     *        keyframes : {
     *            '0%': {
     *                left: 100
     *            },
     *            '40%': {
     *                left: 150
     *            },
     *            '60%': {
     *                left: 75
     *            },
     *            '100%': {
     *                left: 100
     *            }
     *        }
     */
    constructor: function(config) {
        var me = this;
        config = Ext.apply(me, config || {});
        me.config = config;
        me.id = Ext.id(null, 'ext-animator-');
        me.mixins.observable.constructor.call(me, config);
        me.timeline = [];
        me.createTimeline(me.keyframes);
        if (me.target) {
            me.applyAnimator(me.target);
            Ext.fx.Manager.addAnim(me);
        }
    },
    /**
     * @private
     */
    sorter: function(a, b) {
        return a.pct - b.pct;
    },
    /**
     * @private
     * Takes the given keyframe configuration object and converts it into an ordered array
     * with the passed attributes per keyframe or applying the 'to' configuration to all keyframes.
     * Also calculates the proper animation duration per keyframe.
     */
    createTimeline: function(keyframes) {
        var me = this,
            attrs = [],
            to = me.to || {},
            duration = me.duration,
            prevMs, ms, i, ln, pct, attr;
        for (pct in keyframes) {
            if (keyframes.hasOwnProperty(pct) && me.animKeyFramesRE.test(pct)) {
                attr = {
                    attrs: Ext.apply(keyframes[pct], to)
                };
                // CSS3 spec allow for from/to to be specified.
                if (pct === "from") {
                    pct = 0;
                } else if (pct === "to") {
                    pct = 100;
                }
                // convert % values into integers
                attr.pct = parseInt(pct, 10);
                attrs.push(attr);
            }
        }
        // Sort by pct property
        Ext.Array.sort(attrs, me.sorter);
        ln = attrs.length;
        for (i = 0; i < ln; i++) {
            prevMs = (attrs[i - 1]) ? duration * (attrs[i - 1].pct / 100) : 0;
            ms = duration * (attrs[i].pct / 100);
            me.timeline.push({
                duration: ms - prevMs,
                attrs: attrs[i].attrs
            });
        }
    },
    /**
     * Applies animation to the Ext.fx.target
     * @param {String/Object} target
     * @private
     */
    applyAnimator: function(target) {
        var me = this,
            anims = [],
            timeline = me.timeline,
            ln = timeline.length,
            anim, easing, damper, attrs, i;
        if (me.fireEvent('beforeanimate', me) !== false) {
            for (i = 0; i < ln; i++) {
                anim = timeline[i];
                attrs = anim.attrs;
                easing = attrs.easing || me.easing;
                damper = attrs.damper || me.damper;
                delete attrs.easing;
                delete attrs.damper;
                anim = new Ext.fx.Anim({
                    target: target,
                    easing: easing,
                    damper: damper,
                    duration: anim.duration,
                    paused: true,
                    to: attrs
                });
                anims.push(anim);
            }
            me.animations = anims;
            me.target = anim.target;
            for (i = 0; i < ln - 1; i++) {
                anim = anims[i];
                anim.nextAnim = anims[i + 1];
                anim.on('afteranimate', function() {
                    this.nextAnim.paused = false;
                });
                anim.on('afteranimate', function() {
                    this.fireEvent('keyframe', this, ++this.keyframeStep);
                }, me);
            }
            anims[ln - 1].on('afteranimate', function() {
                this.lastFrame();
            }, me);
        }
    },
    /**
     * @private
     * Fires beforeanimate and sets the running flag.
     */
    start: function(startTime) {
        var me = this,
            delay = me.delay,
            delayStart = me.delayStart,
            delayDelta;
        if (delay) {
            if (!delayStart) {
                me.delayStart = startTime;
                return;
            } else {
                delayDelta = startTime - delayStart;
                if (delayDelta < delay) {
                    return;
                } else {
                    // Compensate for frame delay;
                    startTime = new Date(delayStart.getTime() + delay);
                }
            }
        }
        if (me.fireEvent('beforeanimate', me) !== false) {
            me.startTime = startTime;
            me.running = true;
            me.animations[me.keyframeStep].paused = false;
        }
    },
    /**
     * @private
     * Perform lastFrame cleanup and handle iterations
     * @return a hash of the new attributes.
     */
    lastFrame: function() {
        var me = this,
            iter = me.iterations,
            iterCount = me.currentIteration;
        iterCount++;
        if (iterCount < iter) {
            me.startTime = new Date();
            me.currentIteration = iterCount;
            me.keyframeStep = 0;
            me.applyAnimator(me.target);
            me.animations[me.keyframeStep].paused = false;
        } else {
            me.currentIteration = 0;
            me.end();
        }
    },
    /**
     * Fire afteranimate event and end the animation. Usually called automatically when the
     * animation reaches its final frame, but can also be called manually to pre-emptively
     * stop and destroy the running animation.
     */
    end: function() {
        var me = this;
        me.fireEvent('afteranimate', me, me.startTime, new Date() - me.startTime);
    },
    isReady: function() {
        return this.paused === false && this.running === false && this.iterations > 0;
    },
    isRunning: function() {
        // Explicitly return false, we don't want to be run continuously by the manager
        return false;
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.CubicBezier', {
    singleton: true,
    cubicBezierAtTime: function(t, p1x, p1y, p2x, p2y, duration) {
        var cx = 3 * p1x,
            bx = 3 * (p2x - p1x) - cx,
            ax = 1 - cx - bx,
            cy = 3 * p1y,
            by = 3 * (p2y - p1y) - cy,
            ay = 1 - cy - by;
        function sampleCurveX(t) {
            return ((ax * t + bx) * t + cx) * t;
        }
        function solve(x, epsilon) {
            var t = solveCurveX(x, epsilon);
            return ((ay * t + by) * t + cy) * t;
        }
        function solveCurveX(x, epsilon) {
            var t0, t1, t2, x2, d2, i;
            for (t2 = x , i = 0; i < 8; i++) {
                x2 = sampleCurveX(t2) - x;
                if (Math.abs(x2) < epsilon) {
                    return t2;
                }
                d2 = (3 * ax * t2 + 2 * bx) * t2 + cx;
                if (Math.abs(d2) < 1.0E-6) {
                    break;
                }
                t2 = t2 - x2 / d2;
            }
            t0 = 0;
            t1 = 1;
            t2 = x;
            if (t2 < t0) {
                return t0;
            }
            if (t2 > t1) {
                return t1;
            }
            while (t0 < t1) {
                x2 = sampleCurveX(t2);
                if (Math.abs(x2 - x) < epsilon) {
                    return t2;
                }
                if (x > x2) {
                    t0 = t2;
                } else {
                    t1 = t2;
                }
                t2 = (t1 - t0) / 2 + t0;
            }
            return t2;
        }
        return solve(t, 1 / (200 * duration));
    },
    cubicBezier: function(x1, y1, x2, y2) {
        var fn = function(pos) {
                return Ext.fx.CubicBezier.cubicBezierAtTime(pos, x1, y1, x2, y2, 1);
            };
        fn.toCSS3 = function() {
            return 'cubic-bezier(' + [
                x1,
                y1,
                x2,
                y2
            ].join(',') + ')';
        };
        fn.reverse = function() {
            return Ext.fx.CubicBezier.cubicBezier(1 - x2, 1 - y2, 1 - x1, 1 - y1);
        };
        return fn;
    }
});

/**
 * This class contains a series of function definitions used to modify values during an animation.
 * They describe how the intermediate values used during a transition will be calculated. It allows
 * for a transition to change speed over its duration. The following options are available: 
 *
 * - linear The default easing type
 * - backIn
 * - backOut
 * - bounceIn
 * - bounceOut
 * - ease
 * - easeIn
 * - easeOut
 * - easeInOut
 * - elasticIn
 * - elasticOut
 * - cubic-bezier(x1, y1, x2, y2)
 *
 * Note that cubic-bezier will create a custom easing curve following the CSS3
 * [transition-timing-function][0] specification.  The four values specify points P1 and P2
 * of the curve as (x1, y1, x2, y2). All values must be in the range [0, 1] or the definition
 * is invalid.
 *
 * [0]: http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag
 *
 * @singleton
 */
Ext.define('Ext.fx.Easing', function() {
    var math = Math,
        pi = math.PI,
        pow = math.pow,
        sin = math.sin,
        sqrt = math.sqrt,
        abs = math.abs,
        backInSeed = 1.70158;
    return {
        singleton: true,
        requires: [
            'Ext.fx.CubicBezier'
        ],
        linear: Ext.identityFn,
        ease: function(n) {
            var q = 0.07813 - n / 2,
                Q = sqrt(0.0066 + q * q),
                x = Q - q,
                X = pow(abs(x), 1 / 3) * (x < 0 ? -1 : 1),
                y = -Q - q,
                Y = pow(abs(y), 1 / 3) * (y < 0 ? -1 : 1),
                t = X + Y + 0.25;
            return pow(1 - t, 2) * 3 * t * 0.1 + (1 - t) * 3 * t * t + t * t * t;
        },
        easeIn: function(n) {
            return pow(n, 1.7);
        },
        easeOut: function(n) {
            return pow(n, 0.48);
        },
        easeInOut: function(n) {
            var q = 0.48 - n / 1.04,
                Q = sqrt(0.1734 + q * q),
                x = Q - q,
                X = pow(abs(x), 1 / 3) * (x < 0 ? -1 : 1),
                y = -Q - q,
                Y = pow(abs(y), 1 / 3) * (y < 0 ? -1 : 1),
                t = X + Y + 0.5;
            return (1 - t) * 3 * t * t + t * t * t;
        },
        backIn: function(n) {
            return n * n * ((backInSeed + 1) * n - backInSeed);
        },
        backOut: function(n) {
            n = n - 1;
            return n * n * ((backInSeed + 1) * n + backInSeed) + 1;
        },
        elasticIn: function(n) {
            var p = 0.3,
                s = p / 4;
            if (n === 0 || n === 1) {
                return n;
            }
            return pow(2, -10 * n) * sin((n - s) * (2 * pi) / p) + 1;
        },
        elasticOut: function(n) {
            return 1 - Ext.fx.Easing.elasticIn(1 - n);
        },
        bounceIn: function(n) {
            return 1 - Ext.fx.Easing.bounceOut(1 - n);
        },
        bounceOut: function(n) {
            var s = 7.5625,
                p = 2.75,
                l;
            if (n < (1 / p)) {
                l = s * n * n;
            } else {
                if (n < (2 / p)) {
                    n -= (1.5 / p);
                    l = s * n * n + 0.75;
                } else {
                    if (n < (2.5 / p)) {
                        n -= (2.25 / p);
                        l = s * n * n + 0.9375;
                    } else {
                        n -= (2.625 / p);
                        l = s * n * n + 0.984375;
                    }
                }
            }
            return l;
        }
    };
}, function(me) {
    // since we are a singleton, we are passed the instance (not the class)
    var Easing = me.self,
        proto = Easing.prototype;
    Easing.addMembers({
        'back-in': proto.backIn,
        'back-out': proto.backOut,
        'ease-in': proto.easeIn,
        'ease-out': proto.easeOut,
        'elastic-in': proto.elasticIn,
        'elastic-out': proto.elasticOut,
        'bounce-in': proto.bounceIn,
        'bounce-out': proto.bounceOut,
        'ease-in-out': proto.easeInOut
    });
});

/**
 * @class Ext.fx.DrawPath
 * Provides SVG Paths handling functions. Copied from Ext.draw.Draw in ExtJs 4.2 in order
 * to break the dependencies on parsePathString() and interpolatePaths() in PropertyHandler.js
 * @private
 */
Ext.define('Ext.fx.DrawPath', {
    singleton: true,
    pathToStringRE: /,?([achlmqrstvxz]),?/gi,
    pathCommandRE: /([achlmqstvz])[\s,]*((-?\d*\.?\d*(?:e[-+]?\d+)?\s*,?\s*)+)/ig,
    pathValuesRE: /(-?\d*\.?\d*(?:e[-+]?\d+)?)\s*,?\s*/ig,
    stopsRE: /^(\d+%?)$/,
    radian: Math.PI / 180,
    is: function(o, type) {
        type = String(type).toLowerCase();
        /* eslint-disable eqeqeq */
        return (type == "object" && o === Object(o)) || (type == "undefined" && typeof o == type) || (type == "null" && o === null) || (type == "array" && Array.isArray && Array.isArray(o)) || (Object.prototype.toString.call(o).toLowerCase().slice(8, -1)) == type;
    },
    /* eslint-enable eqeqeq */
    // To be deprecated, converts itself (an arrayPath) to a proper SVG path string
    path2string: function() {
        return this.join(",").replace(Ext.fx.DrawPath.pathToStringRE, "$1");
    },
    // Convert the passed arrayPath to a proper SVG path string (d attribute)
    pathToString: function(arrayPath) {
        return arrayPath.join(",").replace(Ext.fx.DrawPath.pathToStringRE, "$1");
    },
    parsePathString: function(pathString) {
        if (!pathString) {
            return null;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            paramCounts = {
                a: 7,
                c: 6,
                h: 1,
                l: 2,
                m: 2,
                q: 4,
                s: 4,
                t: 2,
                v: 1,
                z: 0
            },
            data = [];
        if (me.is(pathString, "array") && me.is(pathString[0], "array")) {
            // rough assumption
            data = me.pathClone(pathString);
        }
        if (!data.length) {
            String(pathString).replace(me.pathCommandRE, function(a, b, c) {
                var params = [],
                    name = b.toLowerCase();
                c.replace(me.pathValuesRE, function(a, b) {
                    if (b) {
                        params.push(+b);
                    }
                });
                if (name === "m" && params.length > 2) {
                    data.push([
                        b
                    ].concat(Ext.Array.splice(params, 0, 2)));
                    name = "l";
                    b = (b === "m") ? "l" : "L";
                }
                while (params.length >= paramCounts[name]) {
                    data.push([
                        b
                    ].concat(Ext.Array.splice(params, 0, paramCounts[name])));
                    if (!paramCounts[name]) {
                        break;
                    }
                }
            });
        }
        data.toString = me.path2string;
        return data;
    },
    pathClone: function(pathArray) {
        var res = [],
            j, jj, i, ii;
        // rough assumption
        if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) {
            pathArray = this.parsePathString(pathArray);
        }
        for (i = 0 , ii = pathArray.length; i < ii; i++) {
            res[i] = [];
            for (j = 0 , jj = pathArray[i].length; j < jj; j++) {
                res[i][j] = pathArray[i][j];
            }
        }
        res.toString = this.path2string;
        return res;
    },
    pathToAbsolute: function(pathArray) {
        // rough assumption
        if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) {
            pathArray = this.parsePathString(pathArray);
        }
        // eslint-disable-next-line vars-on-top
        var res = [],
            x = 0,
            y = 0,
            mx = 0,
            my = 0,
            i = 0,
            ln = pathArray.length,
            r, pathSegment, j, ln2;
        // MoveTo initial x/y position
        if (ln && pathArray[0][0] === "M") {
            x = +pathArray[0][1];
            y = +pathArray[0][2];
            mx = x;
            my = y;
            i++;
            res[0] = [
                "M",
                x,
                y
            ];
        }
        for (; i < ln; i++) {
            r = res[i] = [];
            pathSegment = pathArray[i];
            if (pathSegment[0] !== pathSegment[0].toUpperCase()) {
                r[0] = pathSegment[0].toUpperCase();
                switch (r[0]) {
                    // Elliptical Arc
                    case "A":
                        r[1] = pathSegment[1];
                        r[2] = pathSegment[2];
                        r[3] = pathSegment[3];
                        r[4] = pathSegment[4];
                        r[5] = pathSegment[5];
                        r[6] = +(pathSegment[6] + x);
                        r[7] = +(pathSegment[7] + y);
                        break;
                    // Vertical LineTo
                    case "V":
                        r[1] = +pathSegment[1] + y;
                        break;
                    // Horizontal LineTo
                    case "H":
                        r[1] = +pathSegment[1] + x;
                        break;
                    // MoveTo
                    case "M":
                        mx = +pathSegment[1] + x;
                        my = +pathSegment[2] + y;
                    // fall;
                    // eslint-disable-next-line no-fallthrough
                    default:
                        j = 1;
                        ln2 = pathSegment.length;
                        for (; j < ln2; j++) {
                            r[j] = +pathSegment[j] + ((j % 2) ? x : y);
                        };
                }
            } else {
                j = 0;
                ln2 = pathSegment.length;
                for (; j < ln2; j++) {
                    res[i][j] = pathSegment[j];
                }
            }
            switch (r[0]) {
                // ClosePath
                case "Z":
                    x = mx;
                    y = my;
                    break;
                // Horizontal LineTo
                case "H":
                    x = r[1];
                    break;
                // Vertical LineTo
                case "V":
                    y = r[1];
                    break;
                // MoveTo
                case "M":
                    pathSegment = res[i];
                    ln2 = pathSegment.length;
                    mx = pathSegment[ln2 - 2];
                    my = pathSegment[ln2 - 1];
                // fall;
                // eslint-disable-next-line no-fallthrough
                default:
                    pathSegment = res[i];
                    ln2 = pathSegment.length;
                    x = pathSegment[ln2 - 2];
                    y = pathSegment[ln2 - 1];
            }
        }
        res.toString = this.path2string;
        return res;
    },
    interpolatePaths: function(path, path2) {
        var me = this,
            p = me.pathToAbsolute(path),
            p2 = me.pathToAbsolute(path2),
            attrs = {
                x: 0,
                y: 0,
                bx: 0,
                by: 0,
                X: 0,
                Y: 0,
                qx: null,
                qy: null
            },
            attrs2 = {
                x: 0,
                y: 0,
                bx: 0,
                by: 0,
                X: 0,
                Y: 0,
                qx: null,
                qy: null
            },
            fixArc = function(pp, i) {
                var pi;
                if (pp[i].length > 7) {
                    pp[i].shift();
                    pi = pp[i];
                    while (pi.length) {
                        Ext.Array.splice(pp, i++, 0, [
                            "C"
                        ].concat(Ext.Array.splice(pi, 0, 6)));
                    }
                    Ext.Array.erase(pp, i, 1);
                    ii = Math.max(p.length, p2.length || 0);
                }
            },
            fixM = function(path1, path2, a1, a2, i) {
                if (path1 && path2 && path1[i][0] === "M" && path2[i][0] !== "M") {
                    Ext.Array.splice(path2, i, 0, [
                        "M",
                        a2.x,
                        a2.y
                    ]);
                    a1.bx = 0;
                    a1.by = 0;
                    a1.x = path1[i][1];
                    a1.y = path1[i][2];
                    ii = Math.max(p.length, p2.length || 0);
                }
            },
            i, ii, seg, seg2, seglen, seg2len;
        for (i = 0 , ii = Math.max(p.length, p2.length || 0); i < ii; i++) {
            p[i] = me.command2curve(p[i], attrs);
            fixArc(p, i);
            (p2[i] = me.command2curve(p2[i], attrs2));
            fixArc(p2, i);
            fixM(p, p2, attrs, attrs2, i);
            fixM(p2, p, attrs2, attrs, i);
            seg = p[i];
            seg2 = p2[i];
            seglen = seg.length;
            seg2len = seg2.length;
            attrs.x = seg[seglen - 2];
            attrs.y = seg[seglen - 1];
            attrs.bx = parseFloat(seg[seglen - 4]) || attrs.x;
            attrs.by = parseFloat(seg[seglen - 3]) || attrs.y;
            attrs2.bx = (parseFloat(seg2[seg2len - 4]) || attrs2.x);
            attrs2.by = (parseFloat(seg2[seg2len - 3]) || attrs2.y);
            attrs2.x = seg2[seg2len - 2];
            attrs2.y = seg2[seg2len - 1];
        }
        return [
            p,
            p2
        ];
    },
    // Returns any path command as a curveto command based on the attrs passed
    command2curve: function(pathCommand, d) {
        var me = this;
        if (!pathCommand) {
            return [
                "C",
                d.x,
                d.y,
                d.x,
                d.y,
                d.x,
                d.y
            ];
        }
        if (pathCommand[0] !== "T" && pathCommand[0] !== "Q") {
            d.qx = d.qy = null;
        }
        /* eslint-disable max-len */
        switch (pathCommand[0]) {
            case "M":
                d.X = pathCommand[1];
                d.Y = pathCommand[2];
                break;
            case "A":
                pathCommand = [
                    "C"
                ].concat(me.arc2curve.apply(me, [
                    d.x,
                    d.y
                ].concat(pathCommand.slice(1))));
                break;
            case "S":
                pathCommand = [
                    "C",
                    d.x + (d.x - (d.bx || d.x)),
                    d.y + (d.y - (d.by || d.y))
                ].concat(pathCommand.slice(1));
                break;
            case "T":
                d.qx = d.x + (d.x - (d.qx || d.x));
                d.qy = d.y + (d.y - (d.qy || d.y));
                pathCommand = [
                    "C"
                ].concat(me.quadratic2curve(d.x, d.y, d.qx, d.qy, pathCommand[1], pathCommand[2]));
                break;
            case "Q":
                d.qx = pathCommand[1];
                d.qy = pathCommand[2];
                pathCommand = [
                    "C"
                ].concat(me.quadratic2curve(d.x, d.y, pathCommand[1], pathCommand[2], pathCommand[3], pathCommand[4]));
                break;
            case "L":
                pathCommand = [
                    "C"
                ].concat(d.x, d.y, pathCommand[1], pathCommand[2], pathCommand[1], pathCommand[2]);
                break;
            case "H":
                pathCommand = [
                    "C"
                ].concat(d.x, d.y, pathCommand[1], d.y, pathCommand[1], d.y);
                break;
            case "V":
                pathCommand = [
                    "C"
                ].concat(d.x, d.y, d.x, pathCommand[1], d.x, pathCommand[1]);
                break;
            case "Z":
                pathCommand = [
                    "C"
                ].concat(d.x, d.y, d.X, d.Y, d.X, d.Y);
                break;
        }
        /* eslint-enable max-len */
        return pathCommand;
    },
    quadratic2curve: function(x1, y1, ax, ay, x2, y2) {
        var _13 = 1 / 3,
            _23 = 2 / 3;
        return [
            _13 * x1 + _23 * ax,
            _13 * y1 + _23 * ay,
            _13 * x2 + _23 * ax,
            _13 * y2 + _23 * ay,
            x2,
            y2
        ];
    },
    rotate: function(x, y, rad) {
        var cos = Math.cos(rad),
            sin = Math.sin(rad),
            X = x * cos - y * sin,
            Y = x * sin + y * cos;
        return {
            x: X,
            y: Y
        };
    },
    arc2curve: function(x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) {
        // for more information of where this Math came from visit:
        // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
        var me = this,
            PI = Math.PI,
            radian = me.radian,
            _120 = PI * 120 / 180,
            rad = radian * (+angle || 0),
            res = [],
            math = Math,
            mcos = math.cos,
            msin = math.sin,
            msqrt = math.sqrt,
            mabs = math.abs,
            masin = math.asin,
            xy, x, y, h, rx2, ry2, k, cx, cy, f1, f2, df, c1, s1, c2, s2, t, hx, hy, m1, m2, m3, m4, newres, i, ln, f2old, x2old, y2old;
        if (!recursive) {
            xy = me.rotate(x1, y1, -rad);
            x1 = xy.x;
            y1 = xy.y;
            xy = me.rotate(x2, y2, -rad);
            x2 = xy.x;
            y2 = xy.y;
            x = (x1 - x2) / 2;
            y = (y1 - y2) / 2;
            h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
            if (h > 1) {
                h = msqrt(h);
                rx = h * rx;
                ry = h * ry;
            }
            rx2 = rx * rx;
            ry2 = ry * ry;
            // eslint-disable-next-line eqeqeq
            k = (large_arc_flag == sweep_flag ? -1 : 1) * msqrt(mabs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x)));
            cx = k * rx * y / ry + (x1 + x2) / 2;
            cy = k * -ry * x / rx + (y1 + y2) / 2;
            f1 = masin(((y1 - cy) / ry).toFixed(7));
            f2 = masin(((y2 - cy) / ry).toFixed(7));
            f1 = x1 < cx ? PI - f1 : f1;
            f2 = x2 < cx ? PI - f2 : f2;
            if (f1 < 0) {
                f1 = PI * 2 + f1;
            }
            if (f2 < 0) {
                f2 = PI * 2 + f2;
            }
            if (sweep_flag && f1 > f2) {
                f1 = f1 - PI * 2;
            }
            if (!sweep_flag && f2 > f1) {
                f2 = f2 - PI * 2;
            }
        } else {
            f1 = recursive[0];
            f2 = recursive[1];
            cx = recursive[2];
            cy = recursive[3];
        }
        df = f2 - f1;
        if (mabs(df) > _120) {
            f2old = f2;
            x2old = x2;
            y2old = y2;
            f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
            x2 = cx + rx * mcos(f2);
            y2 = cy + ry * msin(f2);
            res = me.arc2curve(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [
                f2,
                f2old,
                cx,
                cy
            ]);
        }
        df = f2 - f1;
        c1 = mcos(f1);
        s1 = msin(f1);
        c2 = mcos(f2);
        s2 = msin(f2);
        t = math.tan(df / 4);
        hx = 4 / 3 * rx * t;
        hy = 4 / 3 * ry * t;
        m1 = [
            x1,
            y1
        ];
        m2 = [
            x1 + hx * s1,
            y1 - hy * c1
        ];
        m3 = [
            x2 + hx * s2,
            y2 - hy * c2
        ];
        m4 = [
            x2,
            y2
        ];
        m2[0] = 2 * m1[0] - m2[0];
        m2[1] = 2 * m1[1] - m2[1];
        if (recursive) {
            return [
                m2,
                m3,
                m4
            ].concat(res);
        } else {
            res = [
                m2,
                m3,
                m4
            ].concat(res).join().split(",");
            newres = [];
            ln = res.length;
            for (i = 0; i < ln; i++) {
                newres[i] = i % 2 ? me.rotate(res[i - 1], res[i], rad).y : me.rotate(res[i], res[i + 1], rad).x;
            }
            return newres;
        }
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.PropertyHandler', {
    requires: [
        'Ext.fx.DrawPath'
    ],
    statics: {
        defaultHandler: {
            pixelDefaultsRE: /width|height|top$|bottom$|left$|right$/i,
            unitRE: /^(-?\d*\.?\d*){1}(em|ex|px|in|cm|mm|pt|pc|%)*$/,
            scrollRE: /^scroll/i,
            computeDelta: function(from, end, damper, initial, attr) {
                var unitRE = this.unitRE,
                    match = unitRE.exec(from),
                    start, units;
                damper = (typeof damper === 'number') ? damper : 1;
                if (match) {
                    from = match[1];
                    units = match[2];
                    if (!this.scrollRE.test(attr) && !units && this.pixelDefaultsRE.test(attr)) {
                        units = 'px';
                    }
                }
                from = +from || 0;
                match = unitRE.exec(end);
                if (match) {
                    end = match[1];
                    units = match[2] || units;
                }
                end = +end || 0;
                start = (initial != null) ? initial : from;
                return {
                    from: from,
                    delta: (end - start) * damper,
                    units: units
                };
            },
            get: function(from, end, damper, initialFrom, attr) {
                var ln = from.length,
                    out = [],
                    i, initial, res, j, len;
                for (i = 0; i < ln; i++) {
                    if (initialFrom) {
                        initial = initialFrom[i][1].from;
                    }
                    if (Ext.isArray(from[i][1]) && Ext.isArray(end)) {
                        res = [];
                        j = 0;
                        len = from[i][1].length;
                        for (; j < len; j++) {
                            res.push(this.computeDelta(from[i][1][j], end[j], damper, initial, attr));
                        }
                        out.push([
                            from[i][0],
                            res
                        ]);
                    } else {
                        out.push([
                            from[i][0],
                            this.computeDelta(from[i][1], end, damper, initial, attr)
                        ]);
                    }
                }
                return out;
            },
            set: function(values, easing) {
                var ln = values.length,
                    out = [],
                    i, val, res, len, j;
                for (i = 0; i < ln; i++) {
                    val = values[i][1];
                    if (Ext.isArray(val)) {
                        res = [];
                        j = 0;
                        len = val.length;
                        for (; j < len; j++) {
                            res.push(val[j].from + val[j].delta * easing + (val[j].units || 0));
                        }
                        out.push([
                            values[i][0],
                            res
                        ]);
                    } else {
                        out.push([
                            values[i][0],
                            val.from + val.delta * easing + (val.units || 0)
                        ]);
                    }
                }
                return out;
            }
        },
        stringHandler: {
            computeDelta: function(from, end, damper, initial, attr) {
                return {
                    from: from,
                    delta: end
                };
            },
            get: function(from, end, damper, initialFrom, attr) {
                var ln = from.length,
                    out = [],
                    i, initial;
                for (i = 0; i < ln; i++) {
                    out.push([
                        from[i][0],
                        this.computeDelta(from[i][1], end, damper, initial, attr)
                    ]);
                }
                return out;
            },
            set: function(values, easing) {
                var ln = values.length,
                    out = [],
                    i, val;
                for (i = 0; i < ln; i++) {
                    val = values[i][1];
                    out.push([
                        values[i][0],
                        val.delta
                    ]);
                }
                return out;
            }
        },
        color: {
            rgbRE: /^rgb\(([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\)$/i,
            hexRE: /^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i,
            hex3RE: /^#?([0-9A-F]{1})([0-9A-F]{1})([0-9A-F]{1})$/i,
            parseColor: function(color, damper) {
                var out = false,
                    reList = [
                        this.hexRE,
                        this.rgbRE,
                        this.hex3RE
                    ],
                    length = reList.length,
                    match, base, re, i;
                damper = (typeof damper === 'number') ? damper : 1;
                for (i = 0; i < length; i++) {
                    re = reList[i];
                    base = (i % 2 === 0) ? 16 : 10;
                    match = re.exec(color);
                    if (match && match.length === 4) {
                        if (i === 2) {
                            match[1] += match[1];
                            match[2] += match[2];
                            match[3] += match[3];
                        }
                        out = {
                            red: parseInt(match[1], base),
                            green: parseInt(match[2], base),
                            blue: parseInt(match[3], base)
                        };
                        break;
                    }
                }
                return out || color;
            },
            computeDelta: function(from, end, damper, initial) {
                from = this.parseColor(from);
                end = this.parseColor(end, damper);
                // eslint-disable-next-line vars-on-top
                var start = initial ? initial : from,
                    tfrom = typeof start,
                    tend = typeof end;
                // Extra check for when the color string is not recognized.
                if (tfrom === 'string' || tfrom === 'undefined' || tend === 'string' || tend === 'undefined') {
                    return end || start;
                }
                return {
                    from: from,
                    delta: {
                        red: Math.round((end.red - start.red) * damper),
                        green: Math.round((end.green - start.green) * damper),
                        blue: Math.round((end.blue - start.blue) * damper)
                    }
                };
            },
            get: function(start, end, damper, initialFrom) {
                var ln = start.length,
                    out = [],
                    i, initial;
                for (i = 0; i < ln; i++) {
                    if (initialFrom) {
                        initial = initialFrom[i][1].from;
                    }
                    out.push([
                        start[i][0],
                        this.computeDelta(start[i][1], end, damper, initial)
                    ]);
                }
                return out;
            },
            set: function(values, easing) {
                var ln = values.length,
                    out = [],
                    i, val, parsedString, from, delta;
                for (i = 0; i < ln; i++) {
                    val = values[i][1];
                    if (val) {
                        from = val.from;
                        delta = val.delta;
                        // multiple checks to reformat the color if it can't be
                        // recognized by computeDelta.
                        val = (typeof val === 'object' && 'red' in val) ? 'rgb(' + val.red + ', ' + val.green + ', ' + val.blue + ')' : val;
                        val = (typeof val === 'object' && val.length) ? val[0] : val;
                        if (typeof val === 'undefined') {
                            return [];
                        }
                        parsedString = typeof val === 'string' ? val : 'rgb(' + [
                            (from.red + Math.round(delta.red * easing)) % 256,
                            (from.green + Math.round(delta.green * easing)) % 256,
                            (from.blue + Math.round(delta.blue * easing)) % 256
                        ].join(',') + ')';
                        out.push([
                            values[i][0],
                            parsedString
                        ]);
                    }
                }
                return out;
            }
        },
        object: {
            interpolate: function(prop, damper) {
                var out = {},
                    p;
                damper = (typeof damper === 'number') ? damper : 1;
                for (p in prop) {
                    out[p] = parseFloat(prop[p]) * damper;
                }
                return out;
            },
            computeDelta: function(from, end, damper, initial) {
                from = this.interpolate(from);
                end = this.interpolate(end, damper);
                // eslint-disable-next-line vars-on-top
                var start = initial ? initial : from,
                    delta = {},
                    p;
                for (p in end) {
                    delta[p] = end[p] - start[p];
                }
                return {
                    from: from,
                    delta: delta
                };
            },
            get: function(start, end, damper, initialFrom) {
                var ln = start.length,
                    out = [],
                    i, initial;
                for (i = 0; i < ln; i++) {
                    if (initialFrom) {
                        initial = initialFrom[i][1].from;
                    }
                    out.push([
                        start[i][0],
                        this.computeDelta(start[i][1], end, damper, initial)
                    ]);
                }
                return out;
            },
            set: function(values, easing) {
                var ln = values.length,
                    out = [],
                    outObject = {},
                    i, from, delta, val, p;
                for (i = 0; i < ln; i++) {
                    val = values[i][1];
                    from = val.from;
                    delta = val.delta;
                    for (p in from) {
                        outObject[p] = from[p] + delta[p] * easing;
                    }
                    out.push([
                        values[i][0],
                        outObject
                    ]);
                }
                return out;
            }
        },
        path: {
            computeDelta: function(from, end, damper, initial) {
                var start;
                damper = (typeof damper === 'number') ? damper : 1;
                from = +from || 0;
                end = +end || 0;
                start = (initial != null) ? initial : from;
                return {
                    from: from,
                    delta: (end - start) * damper
                };
            },
            forcePath: function(path) {
                if (!Ext.isArray(path) && !Ext.isArray(path[0])) {
                    path = Ext.fx.DrawPath.parsePathString(path);
                }
                return path;
            },
            get: function(start, end, damper, initialFrom) {
                var endPath = this.forcePath(end),
                    out = [],
                    startLn = start.length,
                    startPathLn, pointsLn, i, deltaPath, initial, j, k, path, startPath;
                for (i = 0; i < startLn; i++) {
                    startPath = this.forcePath(start[i][1]);
                    deltaPath = Ext.fx.DrawPath.interpolatePaths(startPath, endPath);
                    startPath = deltaPath[0];
                    endPath = deltaPath[1];
                    startPathLn = startPath.length;
                    path = [];
                    for (j = 0; j < startPathLn; j++) {
                        deltaPath = [
                            startPath[j][0]
                        ];
                        pointsLn = startPath[j].length;
                        for (k = 1; k < pointsLn; k++) {
                            initial = initialFrom && initialFrom[0][1][j][k].from;
                            deltaPath.push(this.computeDelta(startPath[j][k], endPath[j][k], damper, initial));
                        }
                        path.push(deltaPath);
                    }
                    out.push([
                        start[i][0],
                        path
                    ]);
                }
                return out;
            },
            set: function(values, easing) {
                var ln = values.length,
                    out = [],
                    i, j, k, newPath, calcPath, deltaPath, deltaPathLn, pointsLn;
                for (i = 0; i < ln; i++) {
                    deltaPath = values[i][1];
                    newPath = [];
                    deltaPathLn = deltaPath.length;
                    for (j = 0; j < deltaPathLn; j++) {
                        calcPath = [
                            deltaPath[j][0]
                        ];
                        pointsLn = deltaPath[j].length;
                        for (k = 1; k < pointsLn; k++) {
                            calcPath.push(deltaPath[j][k].from + deltaPath[j][k].delta * easing);
                        }
                        newPath.push(calcPath.join(','));
                    }
                    out.push([
                        values[i][0],
                        newPath.join(',')
                    ]);
                }
                return out;
            }
        }
    }
}, function() {
    // set color properties to color interpolator
    var props = [
            'outlineColor',
            'backgroundColor',
            'borderColor',
            'borderTopColor',
            'borderRightColor',
            'borderBottomColor',
            'borderLeftColor',
            'fill',
            'stroke'
        ],
        length = props.length,
        i = 0,
        prop;
    for (; i < length; i++) {
        prop = props[i];
        this[prop] = this.color;
    }
    // set string properties to string
    props = [
        'cursor'
    ];
    length = props.length;
    i = 0;
    for (; i < length; i++) {
        prop = props[i];
        this[prop] = this.stringHandler;
    }
});

/**
 * This class manages animation for a specific {@link #target}. The animation allows
 * animation of various properties on the target, such as size, position, color and others.
 *
 * ## Starting Conditions
 *
 * The starting conditions for the animation are provided by the {@link #from} configuration.
 * Any/all of the properties in the {@link #from} configuration can be specified. If a particular
 * property is not defined, the starting value for that property will be read directly from the
 * target.
 *
 * ## End Conditions
 *
 * The ending conditions for the animation are provided by the {@link #to} configuration. These mark
 * the final values once the animations has finished. The values in the {@link #from} can mirror
 * those in the {@link #to} configuration to provide a starting point.
 *
 * ## Other Options
 *
 *  - {@link #duration}: Specifies the time period of the animation.
 *  - {@link #easing}: Specifies the easing of the animation.
 *  - {@link #iterations}: Allows the animation to repeat a number of times.
 *  - {@link #alternate}: Used in conjunction with {@link #iterations}, reverses the direction
 *    every second iteration.
 *
 * ## Example Code
 *
 *     @example
 *     var myComponent = Ext.create('Ext.Component', {
 *         renderTo: document.body,
 *         width: 200,
 *         height: 200,
 *         style: 'border: 1px solid red;'
 *     });
 *
 *     Ext.create('Ext.fx.Anim', {
 *         target: myComponent,
 *         duration: 1000,
 *         from: {
 *             width: 400 //starting width 400
 *         },
 *         to: {
 *             width: 300, //end width 300
 *             height: 300 // end height 300
 *         }
 *     });
 */
Ext.define('Ext.fx.Anim', {
    mixins: {
        observable: 'Ext.util.Observable'
    },
    requires: [
        'Ext.fx.Manager',
        'Ext.fx.Animator',
        'Ext.fx.Easing',
        'Ext.fx.CubicBezier',
        'Ext.fx.PropertyHandler'
    ],
    /**
     * @property {Boolean} isAnimation
     * `true` in this class to identify an object as an instantiated Anim, or subclass thereof.
     */
    isAnimation: true,
    /**
     * @cfg {Function/String} callback
     * A function to be run after the animation has completed.
     * @controllable
     */
    /**
     * @cfg {Object} scope
     * The scope that the {@link #callback} function will be called with
     */
    /**
     * @cfg {Boolean} remove
     * `true` to remove the target when the animation is complete, using the appropriate removal
     * method for the target. For example, a component will be destroyed, elements will be removed.
     */
    /**
     * @cfg {Number} duration
     * Time in milliseconds for a single animation to last. If the {@link #iterations} property is
     * specified, then each animate will take the same duration for each iteration.
     */
    duration: 250,
    /**
     * @cfg {Number} delay
     * Time to delay before starting the animation.
     */
    delay: 0,
    /**
     * @private
     * Tracks a delayed starting time
     */
    delayStart: 0,
    /**
     * @cfg {Boolean} dynamic
     * Currently only for Component Animation: Only set a component's outer element size bypassing
     * layouts. Set to true to do full layouts for every frame of the animation.
     */
    dynamic: false,
    /**
     * @cfg {String} easing
     * This describes how the intermediate values used during a transition will be calculated.
     * It allows for a transition to change speed over its duration.
     *
     * - backIn
     * - backOut
     * - bounceIn
     * - bounceOut
     * - ease
     * - easeIn
     * - easeOut
     * - easeInOut
     * - elasticIn
     * - elasticOut
     * - cubic-bezier(x1, y1, x2, y2)
     *
     * Note that cubic-bezier will create a custom easing curve following the CSS3
     * [transition-timing-function][0] specification.  The four values specify points P1 and P2
     * of the curve as (x1, y1, x2, y2). All values must be in the range [0, 1] or the definition
     * is invalid.
     *
     * [0]: http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag
     */
    easing: 'ease',
    /**
     * @cfg {Object} keyframes
     * Animation keyframes follow the CSS3 Animation configuration pattern. 'from' is always
     * considered '0%' and 'to' is considered '100%'. **Every keyframe declaration must have
     * a keyframe rule for 0% and 100%, possibly defined using "from" or "to".**  A keyframe
     * declaration without these keyframe selectors is invalid and will not be available for
     * animation.  The keyframe declaration for a keyframe rule consists of properties and values.
     * Properties that are unable to be animated are ignored in these rules, with the exception of
     * 'easing' which can be changed at each keyframe. For example:
     *
     *     keyframes : {
     *         '0%': {
     *             left: 100
     *         },
     *         '40%': {
     *             left: 150
     *         },
     *         '60%': {
     *             left: 75
     *         },
     *         '100%': {
     *             left: 100
     *         }
     *     }
     */
    /**
     * @private
     */
    damper: 1,
    /**
     * @private
     */
    bezierRE: /^(?:cubic-)?bezier\(([^,]+),([^,]+),([^,]+),([^)]+)\)/,
    /**
     * Run the animation from the end to the beginning
     * Defaults to false.
     * @cfg {Boolean} reverse
     */
    reverse: false,
    /**
     * Flag to determine if the animation has started
     * @property running
     * @type Boolean
     */
    running: false,
    /**
     * Flag to determine if the animation is paused. Only set this to true if you need to
     * keep the Anim instance around to be un-paused later; otherwise call {@link #end}.
     * @property paused
     * @type Boolean
     */
    paused: false,
    /**
     * @cfg {Number} iterations
     * Number of times to execute the animation.
     */
    iterations: 1,
    /**
     * @cfg {Boolean} autoEnd
     * `true` to immediately force this animation to its final state. This can be useful
     * in cases where you want the final effect of an animation, but need to the actual
     * animation dynamically. Also see the {@link #jumpToEnd} method.
     */
    autoEnd: false,
    /**
     * @cfg {Boolean} alternate
     * Used in conjunction with iterations to reverse the animation each time an iteration
     * completes.
     */
    alternate: false,
    /**
     * Current iteration the animation is running.
     * @property currentIteration
     * @type Number
     */
    currentIteration: 0,
    /**
     * Starting time of the animation.
     * @property startTime
     * @type Date
     */
    startTime: 0,
    /**
     * Contains a cache of the interpolators to be used.
     * @private
     * @property propHandlers
     * @type Object
     */
    /**
     * @cfg {String/Object} target
     * The {@link Ext.fx.target.Target} to apply the animation to.  This should only be specified 
     * when creating an Ext.fx.Anim directly. The target does not need to be a
     * {@link Ext.fx.target.Target} instance, it can be the underlying object. For example, you can
     * pass a Component, Element or Sprite as the target and the Anim will create the appropriate
     * {@link Ext.fx.target.Target} object automatically.
     */
    /**
     * @cfg {Object} from
     * An object containing property/value pairs for the beginning of the animation.
     * If not specified, the current state of the Ext.fx.target will be used. For example:
     *
     *     from: {
     *         opacity: 0,       // Transparent
     *         color: '#ffffff', // White
     *         left: 0
     *     }
     *
     */
    /**
     * @cfg {Object} to (required)
     * An object containing property/value pairs for the end of the animation. For example:
     *
     *     to: {
     *         opacity: 1,       // Opaque
     *         color: '#00ff00', // Green
     *         left: 500
     *     }
     *
     */
    /**
     * @private
     */
    frameCount: 0,
    /**
     * @event beforeanimate
     * Fires before the animation starts. A handler can return false to cancel the animation.
     * @param {Ext.fx.Anim} this
     */
    /**
     * @event afteranimate
     * Fires when the animation is complete.
     * @param {Ext.fx.Anim} this
     * @param {Date} startTime
     */
    /**
     * @event lastframe
     * Fires when the animation's last frame has been set.
     * @param {Ext.fx.Anim} this
     * @param {Date} startTime
     */
    /**
     * @private
     */
    constructor: function(config) {
        var me = this,
            curve;
        config = config || {};
        // If keyframes are passed, they really want an Animator instead.
        if (config.keyframes) {
            return new Ext.fx.Animator(config);
        }
        Ext.apply(me, config);
        if (me.from === undefined) {
            me.from = {};
        }
        me.propHandlers = {};
        me.config = config;
        me.target = Ext.fx.Manager.createTarget(me.target);
        me.easingFn = Ext.fx.Easing[me.easing];
        me.target.dynamic = me.dynamic;
        // If not a pre-defined curve, try a cubic-bezier
        if (!me.easingFn) {
            me.easingFn = String(me.easing).match(me.bezierRE);
            if (me.easingFn && me.easingFn.length === 5) {
                curve = me.easingFn;
                me.easingFn = Ext.fx.CubicBezier.cubicBezier(+curve[1], +curve[2], +curve[3], +curve[4]);
            }
        }
        me.id = Ext.id(null, 'ext-anim-');
        me.mixins.observable.constructor.call(me);
        Ext.fx.Manager.addAnim(me);
        if (config.autoEnd) {
            me.running = true;
            me.jumpToEnd();
        }
    },
    /**
     * @private
     * Helper to the target
     */
    setAttr: function(attr, value) {
        return Ext.fx.Manager.items.get(this.id).setAttr(this.target, attr, value);
    },
    /**
     * @private
     * Set up the initial currentAttrs hash.
     */
    initAttrs: function() {
        var me = this,
            from = me.from,
            to = me.to,
            initialFrom = me.initialFrom || {},
            out = {},
            start, end, propHandler, attr;
        for (attr in to) {
            if (to.hasOwnProperty(attr)) {
                start = me.target.getAttr(attr, from[attr]);
                end = to[attr];
                // Use default (numeric) property handler
                if (!Ext.fx.PropertyHandler[attr]) {
                    if (Ext.isObject(end)) {
                        propHandler = me.propHandlers[attr] = Ext.fx.PropertyHandler.object;
                    } else {
                        propHandler = me.propHandlers[attr] = Ext.fx.PropertyHandler.defaultHandler;
                    }
                } else // Use custom handler
                {
                    propHandler = me.propHandlers[attr] = Ext.fx.PropertyHandler[attr];
                }
                out[attr] = propHandler.get(start, end, me.damper, initialFrom[attr], attr);
            }
        }
        me.currentAttrs = out;
    },
    /**
     * @private
     * Fires beforeanimate and sets the running flag.
     */
    start: function(startTime) {
        var me = this,
            delay = me.delay,
            delayStart = me.delayStart,
            delayDelta;
        if (delay) {
            if (!delayStart) {
                me.delayStart = startTime;
                return;
            } else {
                delayDelta = startTime - delayStart;
                if (delayDelta < delay) {
                    return;
                } else {
                    // Compensate for frame delay;
                    startTime = new Date(delayStart.getTime() + delay);
                }
            }
        }
        if (me.fireEvent('beforeanimate', me) !== false) {
            me.startTime = startTime;
            if (!me.paused && !me.currentAttrs) {
                me.initAttrs();
            }
            me.running = true;
            me.frameCount = 0;
        }
    },
    /**
     * Immediately force this animation to its final state.
     */
    jumpToEnd: function(suppressEvent) {
        var me = this;
        if (!me.endWasCalled) {
            if (!me.currentAttrs) {
                me.initAttrs();
            }
            Ext.fx.Manager.jumpToEnd(me);
            me.end(suppressEvent);
        }
    },
    /**
     * @private
     * Calculate attribute value at the passed timestamp.
     * @return a hash of the new attributes.
     */
    runAnim: function(elapsedTime) {
        var me = this,
            attrs = me.currentAttrs,
            duration = me.duration,
            easingFn = me.easingFn,
            propHandlers = me.propHandlers,
            ret = {},
            easing, values, attr, lastFrame;
        if (elapsedTime >= duration) {
            elapsedTime = duration;
            lastFrame = true;
        }
        if (me.reverse) {
            elapsedTime = duration - elapsedTime;
        }
        for (attr in attrs) {
            if (attrs.hasOwnProperty(attr)) {
                values = attrs[attr];
                easing = lastFrame ? 1 : easingFn(elapsedTime / duration);
                ret[attr] = propHandlers[attr].set(values, easing);
            }
        }
        me.frameCount++;
        return ret;
    },
    /**
     * @private
     * Perform lastFrame cleanup and handle iterations
     * @return a hash of the new attributes.
     */
    lastFrame: function() {
        var me = this,
            iter = me.iterations,
            iterCount = me.currentIteration;
        iterCount++;
        if (iterCount < iter) {
            if (me.alternate) {
                me.reverse = !me.reverse;
            }
            me.startTime = new Date();
            me.currentIteration = iterCount;
            // Turn off paused for CSS3 Transitions
            me.paused = false;
        } else {
            me.currentIteration = 0;
            me.end();
            me.fireEvent('lastframe', me, me.startTime);
        }
    },
    endWasCalled: 0,
    /**
     * Fire afteranimate event and end the animation. Usually called automatically when the
     * animation reaches its final frame, but can also be called manually to preemptively
     * stop and destroy the running animation.
     */
    end: function(suppressEvent) {
        var me = this;
        if (me.endWasCalled++) {
            return;
        }
        me.startTime = 0;
        me.paused = false;
        me.running = false;
        Ext.fx.Manager.removeAnim(me);
        if (!suppressEvent) {
            me.fireEvent('afteranimate', me, me.startTime);
            Ext.callback(me.callback, me.scope, [
                me,
                me.startTime
            ]);
        }
        if (me.remove) {
            me.target.destroy();
        }
    },
    isReady: function() {
        return this.paused === false && this.running === false && this.iterations > 0;
    },
    isRunning: function() {
        return this.paused === false && this.running === true && this.isAnimator !== true;
    }
});
/**
 * @member Ext
 * @property {Boolean} enableFx
 * True if the {@link Ext.fx.Anim} Class is available.
 */
Ext.enableFx = true;
// Indicate that Fx is available. Class might not be available immediately.

/**
 * This animation class is a mixin.
 *
 * Ext.util.Animate provides an API for the creation of animated transitions of properties
 * and styles. This class is used as a mixin and currently applied to {@link Ext.dom.Element},
 * {@link Ext.CompositeElement}, {@link Ext.draw.sprite.Sprite}, {@link Ext.draw.sprite.Composite},
 * and {@link Ext.Component}.  Note that Components have a limited subset of what attributes
 * can be animated such as top, left, x, y, height, width, and opacity (color, paddings,
 * and margins can not be animated).
 *
 * ## Animation Basics
 *
 * All animations require three things - `easing`, `duration`, and `to` (the final end value
 * for each property) you wish to animate. Easing and duration are defaulted values specified below.
 * Easing describes how the intermediate values used during a transition will be calculated.
 * {@link Ext.fx.Anim#easing Easing} allows for a transition to change speed over its duration.
 * You may use the defaults for easing and duration, but you must always set a
 * {@link Ext.fx.Anim#to to} property which is the end value for all animations.
 *
 * Popular element 'to' configurations are:
 *
 *  - opacity
 *  - x
 *  - y
 *  - color
 *  - height
 *  - width
 *
 * Popular sprite 'to' configurations are:
 *
 *  - translation
 *  - path
 *  - scale
 *  - stroke
 *  - rotation
 *
 * The default duration for animations is 250 (which is a 1/4 of a second).  Duration is denoted in
 * milliseconds.  Therefore 1 second is 1000, 1 minute would be 60000, and so on. The default
 * easing curve used for all animations is 'ease'.  Popular easing functions are included
 * and can be found in {@link Ext.fx.Anim#easing Easing}.
 *
 * For example, a simple animation to fade out an element with a default easing and duration:
 *
 *     var p1 = Ext.get('myElementId');
 *
 *     p1.animate({
 *         to: {
 *             opacity: 0
 *         }
 *     });
 *
 * To make this animation fade out in a tenth of a second:
 *
 *     var p1 = Ext.get('myElementId');
 *
 *     p1.animate({
 *        duration: 100,
 *         to: {
 *             opacity: 0
 *         }
 *     });
 *
 * ## Animation Queues
 *
 * By default all animations are added to a queue which allows for animation via a chain-style API.
 * For example, the following code will queue 4 animations which occur sequentially (one right
 * after the other):
 *
 *     p1.animate({
 *         to: {
 *             x: 500
 *         }
 *     }).animate({
 *         to: {
 *             y: 150
 *         }
 *     }).animate({
 *         to: {
 *             backgroundColor: '#f00'  //red
 *         }
 *     }).animate({
 *         to: {
 *             opacity: 0
 *         }
 *     });
 *
 * You can change this behavior by calling the {@link Ext.util.Animate#syncFx syncFx} method and all
 * subsequent animations for the specified target will be run concurrently (at the same time).
 *
 *     p1.syncFx();  //this will make all animations run at the same time
 *
 *     p1.animate({
 *         to: {
 *             x: 500
 *         }
 *     }).animate({
 *         to: {
 *             y: 150
 *         }
 *     }).animate({
 *         to: {
 *             backgroundColor: '#f00'  //red
 *         }
 *     }).animate({
 *         to: {
 *             opacity: 0
 *         }
 *     });
 *
 * This works the same as:
 *
 *     p1.animate({
 *         to: {
 *             x: 500,
 *             y: 150,
 *             backgroundColor: '#f00'  //red
 *             opacity: 0
 *         }
 *     });
 *
 * The {@link Ext.util.Animate#stopAnimation stopAnimation} method can be used to stop any
 * currently running animations and clear any queued animations.
 *
 * ## Animation Keyframes
 *
 * You can also set up complex animations with {@link Ext.fx.Anim#keyframes keyframes} which follow
 * the CSS3 Animation configuration pattern. Note rotation, translation, and scaling can only
 * be done for sprites. The previous example can be written with the following syntax:
 *
 *     p1.animate({
 *         duration: 1000,  //one second total
 *         keyframes: {
 *             25: {     //from 0 to 250ms (25%)
 *                 x: 0
 *             },
 *             50: {   //from 250ms to 500ms (50%)
 *                 y: 0
 *             },
 *             75: {  //from 500ms to 750ms (75%)
 *                 backgroundColor: '#f00'  //red
 *             },
 *             100: {  //from 750ms to 1sec
 *                 opacity: 0
 *             }
 *         }
 *     });
 *
 * ## Animation Events
 *
 * Each animation you create has events for {@link Ext.fx.Anim#beforeanimate beforeanimate},
 * {@link Ext.fx.Anim#afteranimate afteranimate}, and {@link Ext.fx.Anim#lastframe lastframe}.
 * Keyframed animations adds an additional {@link Ext.fx.Animator#keyframe keyframe} event which
 * fires for each keyframe in your animation.
 *
 * All animations support the {@link Ext.util.Observable#listeners listeners} configuration
 * to attact functions to these events.
 *
 *     startAnimate: function() {
 *         var p1 = Ext.get('myElementId');
 *         p1.animate({
 *            duration: 100,
 *             to: {
 *                 opacity: 0
 *             },
 *             listeners: {
 *                 beforeanimate:  function() {
 *                     // Execute my custom method before the animation
 *                     this.myBeforeAnimateFn();
 *                 },
 *                 afteranimate: function() {
 *                     // Execute my custom method after the animation
 *                     this.myAfterAnimateFn();
 *                 },
 *                 scope: this
 *         });
 *     },
 *     myBeforeAnimateFn: function() {
 *       // My custom logic
 *     },
 *     myAfterAnimateFn: function() {
 *       // My custom logic
 *     }
 *
 * Due to the fact that animations run asynchronously, you can determine if an animation
 * is currently running on any target by using the
 * {@link Ext.util.Animate#getActiveAnimation getActiveAnimation} method. This method will return
 * false if there are no active animations or return the currently running {@link Ext.fx.Anim}
 * instance.
 *
 * In this example, we're going to wait for the current animation to finish, then stop any other
 * queued animations before we fade our element's opacity to 0:
 *
 *     var curAnim = p1.getActiveAnimation();
 *     if (curAnim) {
 *         curAnim.on('afteranimate', function() {
 *             p1.stopAnimation();
 *             p1.animate({
 *                 to: {
 *                     opacity: 0
 *                 }
 *             });
 *         });
 *     }
 */
Ext.define('Ext.util.Animate', {
    mixinId: 'animate',
    requires: [
        'Ext.fx.Manager',
        'Ext.fx.Anim'
    ],
    isAnimate: true,
    /**
     * Performs custom animation on this object.
     *
     * This method is applicable to both the {@link Ext.Component Component} class and the
     * {@link Ext.draw.sprite.Sprite Sprite} class. It performs animated transitions of certain
     * properties of this object over a specified timeline.
     *
     * ### Animating a {@link Ext.Component Component}
     *
     * When animating a Component, the following properties may be specified in `from`, `to`,
     * and `keyframe` objects:
     *
     *   - `x` - The Component's page X position in pixels.
     *
     *   - `y` - The Component's page Y position in pixels
     *
     *   - `left` - The Component's `left` value in pixels.
     *
     *   - `top` - The Component's `top` value in pixels.
     *
     *   - `width` - The Component's `width` value in pixels.
     *
     *   - `height` - The Component's `height` value in pixels.
     *
     * The following property may be set on the animation config root:
     *
     *   - `dynamic` - Specify as true to update the Component's layout (if it is a Container)
     *     at every frame of the animation. *Use sparingly as laying out on every intermediate
     *     size change is an expensive operation.*
     *
     * For example, to animate a Window to a new size, ensuring that its internal layout
     * and any shadow is correct:
     *
     *     myWindow = Ext.create('Ext.window.Window', {
     *         title: 'Test Component animation',
     *         width: 500,
     *         height: 300,
     *         layout: {
     *             type: 'hbox',
     *             align: 'stretch'
     *         },
     *         items: [{
     *             title: 'Left: 33%',
     *             margin: '5 0 5 5',
     *             flex: 1
     *         }, {
     *             title: 'Left: 66%',
     *             margin: '5 5 5 5',
     *             flex: 2
     *         }]
     *     });
     *     myWindow.show();
     *     myWindow.header.el.on('click', function() {
     *         myWindow.animate({
     *             to: {
     *                 width: (myWindow.getWidth() == 500) ? 700 : 500,
     *                 height: (myWindow.getHeight() == 300) ? 400 : 300
     *             }
     *         });
     *     });
     *
     * For performance reasons, by default, the internal layout is only updated when the Window
     * reaches its final `"to"` size. If dynamic updating of the Window's child Components
     * is required, then configure the animation with `dynamic: true` and the two child items
     * will maintain their proportions during the animation.
     *
     * @param {Object} animObj  Configuration for {@link Ext.fx.Anim}.
     * Note that the {@link Ext.fx.Anim#to to} config is required.
     * @return {Object} this
     */
    animate: function(animObj) {
        var me = this;
        if (Ext.fx.Manager.hasFxBlock(me.id)) {
            return me;
        }
        Ext.fx.Manager.queueFx(new Ext.fx.Anim(me.anim(animObj)));
        return this;
    },
    /**
     * @private
     * Process the passed fx configuration.
     */
    anim: function(config) {
        var me = this;
        if (!Ext.isObject(config)) {
            return (config) ? {} : false;
        }
        if (config.stopAnimation) {
            me.stopAnimation();
        }
        Ext.applyIf(config, Ext.fx.Manager.getFxDefaults(me.id));
        return Ext.apply({
            target: me,
            paused: true
        }, config);
    },
    /**
     * @private
     * Get animation properties
     */
    getAnimationProps: function() {
        var me = this,
            layout = me.layout;
        return layout && layout.animate ? layout.animate : {};
    },
    /**
     * Stops any running effects and clears this object's internal effects queue if it contains
     * any additional effects that haven't started yet.
     * @deprecated 4.0 Replaced by {@link #stopAnimation}
     * @return {Ext.dom.Element} The Element
     * @method
     */
    stopFx: Ext.Function.alias(Ext.util.Animate, 'stopAnimation'),
    /**
     * Stops any running effects and clears this object's internal effects queue if it contains
     * any additional effects that haven't started yet.
     * @param {Boolean} suppressEvent (private)
     * @return {Ext.dom.Element} The Element
     */
    stopAnimation: function(suppressEvent) {
        Ext.fx.Manager.stopAnimation(this.id, suppressEvent);
        return this;
    },
    /**
     * Ensures that all effects queued after syncFx is called on this object are run concurrently.
     * This is the opposite of {@link #sequenceFx}.
     * @return {Object} this
     */
    syncFx: function() {
        Ext.fx.Manager.setFxDefaults(this.id, {
            concurrent: true
        });
        return this;
    },
    /**
     * Ensures that all effects queued after sequenceFx is called on this object are run
     * in sequence. This is the opposite of {@link #syncFx}.
     * @return {Object} this
     */
    sequenceFx: function() {
        Ext.fx.Manager.setFxDefaults(this.id, {
            concurrent: false
        });
        return this;
    },
    /**
     * @method hasActiveFx
     * @deprecated 4.0 Replaced by {@link #getActiveAnimation}
     * @inheritdoc Ext.util.Animate#method-getActiveAnimation
     */
    hasActiveFx: Ext.Function.alias(Ext.util.Animate, 'getActiveAnimation'),
    /**
     * Returns the current animation if this object has any effects actively running or queued,
     * else returns false.
     * @return {Ext.fx.Anim/Boolean} Anim if element has active effects, else false
     */
    getActiveAnimation: function() {
        return Ext.fx.Manager.getActiveAnimation(this.id);
    }
});

/**
 * A flyweight Ext.dom.Element that can be dynamically attached to a DOM node.
 * In general this class should not be instantiated directly.  Use {@link Ext#fly}
 * to create and retrieve Fly instances.
 */
Ext.define('Ext.dom.Fly', {
    extend: 'Ext.dom.Element',
    alternateClassName: 'Ext.dom.Element.Fly',
    // This adds the ability to wrap DOCUMENT_FRAGMENT_NODE
    // Document Fragments cannot have event listeners and therefore do not
    // need  the caching mechanism provided by Ext.get.
    // However the many Element APIs are useful such as Traversal, child appending/removing.
    validNodeTypes: {
        1: 1,
        // ELEMENT_NODE
        9: 1,
        // DOCUMENT_NODE
        11: 1
    },
    // DOCUMENT_FRAGMENT_NODE
    /**
     * @property {Boolean} isFly
     * This is `true` to identify Element flyweights
     */
    isFly: true,
    constructor: function(dom) {
        this.dom = dom;
        // set an "el" property that references "this".  This allows
        // Ext.util.Positionable methods to operate on this.el.dom since it
        // gets mixed into both Element and Component
        this.el = this;
    },
    attach: function(dom) {
        var me = this,
            data;
        if (!dom) {
            return me.detach();
        }
        // Sometimes we want to attach to the DOM of Ext.Element instance
        me.dom = Ext.getDom(dom);
        // If the element is not being managed by an Ext.Element instance,
        // we have to assume that the classList/classMap in the data object are out of sync
        // with reality.
        if (!Ext.cache[dom.id]) {
            data = me.peekData();
            if (data) {
                data.isSynchronized = false;
            }
        }
        return me;
    },
    detach: function() {
        return (this.dom = null);
    },
    addListener: function() {
        Ext.raise("Cannot use addListener() on Ext.dom.Fly instances. " + "Please use Ext.get() to retrieve an Ext.dom.Element instance instead.");
    } || null,
    removeListener: function() {
        Ext.raise("Cannot use removeListener() on Ext.dom.Fly instances. " + "Please use Ext.get() to retrieve an Ext.dom.Element instance instead.");
    } || null
}, function(Fly) {
    var flyweights = {},
        detachedBodyEl;
    /**
     * @member Ext
     * @property {Object} cache
     * Stores `Fly` instances keyed by their assigned or generated name.
     * @readonly
     * @private
     * @since 5.0.0
     */
    Fly.cache = flyweights;
    /**
     * @member Ext
     * @method fly
     * Gets the globally shared flyweight Element, with the passed node as the active
     * element. Do not store a reference to this element - the dom node can be overwritten
     * by other code. {@link Ext#fly} is alias for {@link Ext.dom.Element#fly}.
     *
     * Use this to make one-time references to DOM elements which are not going to be
     * accessed again either by application code, or by Ext's classes. If accessing an
     * element which will be processed regularly, then {@link Ext#get Ext.get} will be
     * more appropriate to take advantage of the caching provided by the
     * {@link Ext.dom.Element} class.
     * 
     * If this method is called with and id or element that has already been cached by
     * a previous call to Ext.get() it will return the cached Element instead of the
     * flyweight instance.
     *
     * @param {String/HTMLElement} dom The DOM node or `id`.
     * @param {String} [named] Allows for creation of named reusable flyweights to prevent 
     * conflicts (e.g. internally Ext uses "_global").
     * @return {Ext.dom.Element} The shared Element object (or `null` if no matching
     * element was found).
     */
    Ext.fly = function(dom, named) {
        var fly = null,
            fn = Ext.fly,
            nodeType, data;
        // name the flyweight after the calling method name if possible.
        named = named || (fn.caller && (fn.caller.$name || fn.caller.name)) || '_global';
        dom = Ext.getDom(dom);
        if (dom) {
            nodeType = dom.nodeType;
            // check if we have a valid node type or if the el is a window object before
            // proceeding. This allows elements, document fragments, and document/window
            // objects (even those inside iframes) to be wrapped.
            // Note: a window object can be detected by comparing it's window property to
            // itself, but we must use == for the comparison because === will return false
            // in IE8 even though the 2 window objects are the same
            /* eslint-disable-next-line eqeqeq */
            if (Fly.prototype.validNodeTypes[nodeType] || (!nodeType && (dom.window == dom))) {
                fly = Ext.cache[dom.id];
                // If there's no Element cached, or the element cached is for another DOM node,
                // return a Fly
                if (!fly || fly.dom !== dom) {
                    // Since the `flyweights` map is simply an object, it has the `constructor`
                    // property, just like any object, so to prevent the `Ext.fly` from failing
                    // when it's called from the `constructor` method, we use the `$constructor`
                    // as the key.
                    if (named === 'constructor') {
                        named = '$constructor';
                    }
                    fly = flyweights[named] || (flyweights[named] = new Fly());
                    fly.dom = dom;
                    data = fly.peekData();
                    if (data) {
                        data.isSynchronized = false;
                    }
                }
            }
        }
        return fly;
    };
    /**
     * Returns an HTML div element into which {@link Ext.container.Container#method-remove removed}
     * components are placed so that their DOM elements are not garbage collected as detached
     * Dom trees.
     * @return {Ext.dom.Element}
     * @method getDetachedBody
     * @member Ext
     * @private
     */
    Ext.getDetachedBody = function() {
        if (!detachedBodyEl) {
            Ext.detachedBodyEl = detachedBodyEl = new Fly(document.createElement('div'));
            detachedBodyEl.isDetachedBody = true;
        }
        return detachedBodyEl;
    };
});

/**
 * This class encapsulates a *collection* of DOM elements, providing methods to filter members,
 * or to perform collective actions upon the whole set.
 *
 * Although they are not listed, this class supports all of the methods of {@link Ext.dom.Element}.
 * The methods from these classes will be performed on all the elements in this collection.
 *
 * Example:
 *
 *     var els = Ext.select("#some-el div.some-class");
 *     // or select directly from an existing element
 *     var el = Ext.get('some-el');
 *     el.select('div.some-class');
 *
 *     els.setWidth(100); // all elements become 100 width
 *     els.hide(true); // all elements fade out and hide
 *     // or
 *     els.setWidth(100).hide(true);
 *
 * @mixins Ext.dom.Element
 */
Ext.define('Ext.dom.CompositeElementLite', {
    alternateClassName: [
        'Ext.CompositeElementLite'
    ],
    requires: [
        'Ext.dom.Fly'
    ],
    /**
     * @property {Boolean} isComposite
     * `true` in this class to identify an object as an instantiated CompositeElement,
     * or subclass thereof.
     */
    isComposite: true,
    /**
     * @private
     */
    isLite: true,
    // We use the @mixins tag above to document that CompositeElement has
    // all the same methods as Element, but the @mixins tag also pulls in
    // configs and properties which we don't want, so hide them explicitly:
    /**
     * @cfg bubbleEvents
     * @hide
     */
    /**
     * @cfg listeners
     * @hide
     */
    /**
     * @property dom
     * @hide
     */
    /**
     * @property id
     * @hide
     */
    statics: {
        // this method is called once in the class creation callback to import all methods
        // from Ext.dom.Element into CompositeElementLite.  It is important to remember
        // that any subsequent overrides of Ext.dom.Element need to call this method again
        // to ensure any additional methods get added.
        importElementMethods: function() {
            var Element = Ext.dom.Element,
                prototype = this.prototype;
            Ext.Object.each(Element.prototype, function(name, member) {
                if (typeof member === 'function' && !prototype[name]) {
                    prototype[name] = function() {
                        return this.invoke(name, arguments);
                    };
                }
            });
        }
    },
    constructor: function(elements, skipValidation) {
        /**
         * @property {HTMLElement[]} elements
         * @param skipValidation (private)
         * @readonly
         * The Array of DOM elements which this CompositeElement encapsulates.
         *
         * This will not *usually* be accessed in developers' code, but developers wishing
         * to augment the capabilities of the CompositeElementLite class may use it when adding
         * methods to the class.
         *
         * For example to add the `nextAll` method to the class to **add** all following siblings
         * of selected elements, the code would be
         *
         *     Ext.override(Ext.dom.CompositeElementLite, {
         *         nextAll: function() {
         *             var elements = this.elements, i, l = elements.length, n, r = [], ri = -1;
         *
         *             // Loop through all elements in this Composite, accumulating
         *             // an Array of all siblings.
         *             for (i = 0; i < l; i++) {
         *                 for (n = elements[i].nextSibling; n; n = n.nextSibling) {
         *                     r[++ri] = n;
         *                 }
         *             }
         *
         *             // Add all found siblings to this Composite
         *             return this.add(r);
         *         }
         *     });
         */
        if (skipValidation) {
            // if the caller told us that they are passing a valid elements array
            // let's take their word for it.  This is the fast path for performance-
            // critical pieces such as Ext.dom.Element.select(), this allows us to 
            // skip all the transformElement() and getDom()/Ext.get() calls.
            this.elements = elements || [];
        } else {
            this.elements = [];
            this.add(elements);
        }
    },
    /**
     * @private
     */
    getElement: function(el) {
        // Set the shared flyweight dom property to the current element
        var fly = this._fly || (this._fly = new Ext.dom.Fly());
        return fly.attach(el);
    },
    /**
     * @private
     */
    transformElement: function(el) {
        return Ext.getDom(el);
    },
    /**
     * Returns the number of elements in this Composite.
     * @return {Number}
     */
    getCount: function() {
        return this.elements.length;
    },
    /**
     * Adds elements to this Composite object.
     * @param {HTMLElement[]/Ext.dom.CompositeElementLite} els Either an Array of DOM elements
     * to add, or another Composite object who's elements should be added.
     * @param {HTMLElement/String} [root] The root element of the query or id of the root.
     * @return {Ext.dom.CompositeElementLite} This Composite object.
     */
    add: function(els, root) {
        var elements = this.elements,
            i, ln;
        if (!els) {
            return this;
        }
        if (typeof els === "string") {
            els = Ext.fly(root || document).query(els);
        } else if (els.isComposite) {
            els = els.elements;
        } else if (!Ext.isIterable(els)) {
            els = [
                els
            ];
        }
        for (i = 0 , ln = els.length; i < ln; ++i) {
            elements.push(this.transformElement(els[i]));
        }
        return this;
    },
    invoke: function(fn, args) {
        var me = this,
            elements = me.elements,
            ln = elements.length,
            prototype, element, i;
        if (i !== 0) {
            // make sure we are using the correct prototype, since Fly overrides a 
            // couple of Element methods
            prototype = (me.isLite ? Ext.dom.Fly : Ext.dom.Element).prototype;
            for (i = 0; i < ln; i++) {
                element = elements[i];
                if (element) {
                    prototype[fn].apply(me.getElement(element), args);
                }
            }
        }
        return me;
    },
    /**
     * Returns a flyweight Element of the dom element object at the specified index.
     * @param {Number} index
     * @return {Ext.dom.Element}
     */
    item: function(index) {
        var el = this.elements[index],
            out = null;
        if (el) {
            out = this.getElement(el);
        }
        return out;
    },
    /**
     * Gets a range nodes.
     * @param {Number} start (optional) The index of the first node in the range
     * @param {Number} end (optional) The index of the last node in the range
     * @return {HTMLElement[]} An array of nodes
     */
    slice: function(start, end) {
        return Ext.Array.slice(this.elements, start, end);
    },
    /**
     * Calls the passed function for each element in this composite.
     * @param {Function} fn The function to call.
     * @param {Ext.dom.Element} fn.el The current Element in the iteration. **This is the flyweight
     * (shared) Ext.dom.Element instance, so if you require a a reference to the dom node,
     * use el.dom.**
     * @param {Ext.dom.CompositeElementLite} fn.c This Composite object.
     * @param {Number} fn.index The zero-based index in the iteration.
     * @param {Object} [scope] The scope (this reference) in which the function is executed.
     * Defaults to the Element.
     * @return {Ext.dom.CompositeElementLite} this
     */
    each: function(fn, scope) {
        var me = this,
            els = me.elements,
            len = els.length,
            i, e;
        for (i = 0; i < len; i++) {
            e = els[i];
            if (e) {
                e = this.getElement(e);
                if (fn.call(scope || e, e, me, i) === false) {
                    break;
                }
            }
        }
        return me;
    },
    /**
     * Clears this Composite and adds the elements passed.
     * @param {HTMLElement[]/Ext.dom.CompositeElementLite} els Either an array of DOM elements,
     * or another Composite from which to fill this Composite.
     * @return {Ext.dom.CompositeElementLite} this
     */
    fill: function(els) {
        var me = this;
        me.elements = [];
        me.add(els);
        return me;
    },
    insert: function(index, nodes) {
        Ext.Array.insert(this.elements, index, nodes);
    },
    /**
     * Filters this composite to only elements that match the passed selector.
     * @param {String/Function} selector A string CSS selector or a comparison function.
     * The comparison function will be called with the following arguments:
     * @param {Ext.dom.Element} selector.el The current DOM element.
     * @param {Number} selector.index The current index within the collection.
     * @return {Ext.dom.CompositeElementLite} this
     */
    filter: function(selector) {
        var me = this,
            els = me.elements,
            len = els.length,
            out = [],
            i = 0,
            isFunc = typeof selector === 'function',
            add, el;
        for (; i < len; i++) {
            el = els[i];
            add = false;
            if (el) {
                el = me.getElement(el);
                if (isFunc) {
                    add = selector.call(el, el, me, i) !== false;
                } else {
                    add = el.is(selector);
                }
                if (add) {
                    out.push(me.transformElement(el));
                }
            }
        }
        me.elements = out;
        return me;
    },
    /**
     * Find the index of the passed element within the composite collection.
     * @param {String/HTMLElement/Ext.dom.Element/Number} el The id of an element, or an
     * Ext.dom.Element, or an HtmlElement to find within the composite collection.
     * @return {Number} The index of the passed Ext.dom.Element in the composite collection,
     * or -1 if not found.
     */
    indexOf: function(el) {
        return Ext.Array.indexOf(this.elements, this.transformElement(el));
    },
    /**
     * Replaces the specified element with the passed element.
     * @param {String/HTMLElement/Ext.dom.Element/Number} el The id of an element, the Element
     * itself, the index of the element in this composite to replace.
     * @param {String/Ext.dom.Element} replacement The id of an element or the Element itself.
     * @param {Boolean} [domReplace] `true` to remove and replace the element in the document too.
     * @return {Ext.dom.CompositeElementLite} this
     */
    replaceElement: function(el, replacement, domReplace) {
        var index = !isNaN(el) ? el : this.indexOf(el),
            d;
        if (index > -1) {
            replacement = Ext.getDom(replacement);
            if (domReplace) {
                d = this.elements[index];
                d.parentNode.insertBefore(replacement, d);
                Ext.removeNode(d);
            }
            Ext.Array.splice(this.elements, index, 1, replacement);
        }
        return this;
    },
    /**
     * Removes all elements from this Composite.
     * @param {Boolean} [removeDom] True to also remove the elements from the document.
     */
    clear: function(removeDom) {
        var me = this,
            els = me.elements,
            i = els.length - 1;
        if (removeDom) {
            for (; i >= 0; i--) {
                Ext.removeNode(els[i]);
            }
        }
        this.elements = [];
    },
    addElements: function(els, root) {
        if (!els) {
            return this;
        }
        if (typeof els === "string") {
            els = Ext.dom.Element.selectorFunction(els, root);
        }
        /* eslint-disable-next-line vars-on-top */
        var yels = this.elements,
            eLen = els.length,
            e;
        for (e = 0; e < eLen; e++) {
            yels.push(Ext.get(els[e]));
        }
        return this;
    },
    /**
     * Returns the first Element
     * @return {Ext.dom.Element}
     */
    first: function() {
        return this.item(0);
    },
    /**
     * Returns the last Element
     * @return {Ext.dom.Element}
     */
    last: function() {
        return this.item(this.getCount() - 1);
    },
    /**
     * Returns `true` if this composite contains the passed element
     * @param {String/HTMLElement/Ext.dom.Element/Number} el The id of an element,
     * or an Ext.Element, or an HtmlElement to find within the composite collection.
     * @return {Boolean}
     */
    contains: function(el) {
        return this.indexOf(el) !== -1;
    },
    /**
     * Removes the specified element(s).
     * @param {String/HTMLElement/Ext.dom.Element/Number} keys The id of an element, the Element
     * itself, the index of the element in this composite or an array of any of those.
     * @param {Boolean} [removeDom] `true` to also remove the element from the document
     * @return {Ext.dom.CompositeElementLite} this
     */
    removeElement: function(keys, removeDom) {
        var me = this,
            elements = me.elements,
            val, el, k, kLen;
        keys = [].concat(keys);
        for (k = 0 , kLen = keys.length; k < kLen; k++) {
            val = keys[k];
            if ((el = (elements[val] || elements[val = me.indexOf(val)]))) {
                if (removeDom) {
                    if (el.dom) {
                        el.destroy();
                    } else {
                        Ext.removeNode(el);
                    }
                }
                Ext.Array.erase(elements, val, 1);
            }
        }
        return me;
    },
    destroy: function() {
        this.invoke('destroy', arguments);
        this.callParent();
    }
}, function(CompositeElementLite) {
    var prototype = CompositeElementLite.prototype;
    CompositeElementLite.importElementMethods();
    prototype.on = prototype.addListener;
});

/**
 * @class Ext.dom.Element
 * @override Ext.dom.Element
 */
Ext.define('Ext.overrides.dom.Element', (function() {
    var Element,
        // we cannot do this yet "= Ext.dom.Element"
        WIN = window,
        DOC = document,
        HIDDEN = 'hidden',
        ISCLIPPED = 'isClipped',
        OVERFLOW = 'overflow',
        OVERFLOWX = 'overflow-x',
        OVERFLOWY = 'overflow-y',
        ORIGINALCLIP = 'originalClip',
        HEIGHT = 'height',
        WIDTH = 'width',
        VISIBILITY = 'visibility',
        DISPLAY = 'display',
        NONE = 'none',
        OFFSETS = 'offsets',
        CLIP = 'clip',
        ORIGINALDISPLAY = 'originalDisplay',
        VISMODE = 'visibilityMode',
        ISVISIBLE = 'isVisible',
        OFFSETCLASS = Ext.baseCSSPrefix + 'hidden-offsets',
        CLIPCLASS = Ext.baseCSSPrefix + 'hidden-clip',
        /* eslint-disable indent */
        boxMarkup = [
            '<div class="{0}-tl" role="presentation">',
            '<div class="{0}-tr" role="presentation">',
            '<div class="{0}-tc" role="presentation"></div>',
            '</div>',
            '</div>',
            '<div class="{0}-ml" role="presentation">',
            '<div class="{0}-mr" role="presentation">',
            '<div class="{0}-mc" role="presentation"></div>',
            '</div>',
            '</div>',
            '<div class="{0}-bl" role="presentation">',
            '<div class="{0}-br" role="presentation">',
            '<div class="{0}-bc" role="presentation"></div>',
            '</div>',
            '</div>'
        ].join(''),
        /* eslint-enable indent */
        scriptTagRe = /(?:<script([^>]*)?>)((\n|\r|.)*?)(?:<\/script>)/ig,
        replaceScriptTagRe = /(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)/ig,
        srcRe = /\ssrc=(['"])(.*?)\1/i,
        nonSpaceRe = /\S/,
        typeRe = /\stype=(['"])(.*?)\1/i,
        adjustDirect2DTableRe = /table-row|table-.*-group/,
        msRe = /^-ms-/,
        camelRe = /(-[a-z])/gi,
        camelReplaceFn = function(m, a) {
            return a.charAt(1).toUpperCase();
        },
        XMASKED = Ext.baseCSSPrefix + "masked",
        XMASKEDRELATIVE = Ext.baseCSSPrefix + "masked-relative",
        EXTELMASKMSG = Ext.baseCSSPrefix + "mask-msg",
        bodyRe = /^body/i,
        propertyCache = {},
        getVisMode = function(el) {
            var data = el.getData(),
                visMode = data[VISMODE];
            if (visMode === undefined) {
                data[VISMODE] = visMode = Element.VISIBILITY;
            }
            return visMode;
        },
        emptyRange = DOC.createRange ? DOC.createRange() : null,
        syncContentFly;
    if (Ext.isIE8) {
        // eslint-disable-next-line vars-on-top
        var garbageBin = DOC.createElement('div'),
            destroyQueue = [],
            // prevent memory leaks in IE8
            // see http://social.msdn.microsoft.com/Forums/ie/en-US/c76967f0-dcf8-47d0-8984-8fe1282a94f5/ie-appendchildremovechild-memory-problem?forum=iewebdevelopment
            // This function is called to fully destroy an element on a timer so that code
            // following the remove call can still access the element.
            clearGarbage,
            clearGarbageFn = function() {
                var len = destroyQueue.length,
                    i;
                for (i = 0; i < len; i++) {
                    garbageBin.appendChild(destroyQueue[i]);
                }
                garbageBin.innerHTML = '';
                destroyQueue.length = 0;
            };
        clearGarbageFn.$skipTimerCheck = true;
        clearGarbage = Ext.Function.createBuffered(clearGarbageFn, 10);
    }
    return {
        override: 'Ext.dom.Element',
        mixins: [
            'Ext.util.Animate'
        ],
        uses: [
            'Ext.dom.GarbageCollector',
            'Ext.dom.Fly',
            'Ext.event.publisher.MouseEnterLeave',
            'Ext.fx.Manager',
            'Ext.fx.Anim'
        ],
        skipGarbageCollection: false,
        _init: function(E) {
            Element = E;
            // now we can poke this into closure scope
            // We want to expose destroyQueue on the prototype for testing purposes
            if (WIN.__UNIT_TESTING__) {
                E.destroyQueue = destroyQueue;
            }
        },
        statics: {
            normalize: function(prop) {
                if (prop === 'float') {
                    prop = Ext.supports.Float ? 'cssFloat' : 'styleFloat';
                }
                // For '-ms-foo' we need msFoo
                return propertyCache[prop] || (propertyCache[prop] = prop.replace(msRe, 'ms-').replace(camelRe, camelReplaceFn));
            }
        },
        /**
         * Convenience method for constructing a KeyMap
         * @param {String/Number/Number[]/Object} key Either a string with the keys to listen for,
         * the numeric key code, array of key codes or an object with the following options:
         * @param {Number/Array} key.key
         * @param {Boolean} key.shift
         * @param {Boolean} key.ctrl
         * @param {Boolean} key.alt
         * @param {Function} fn The function to call
         * @param {Object} [scope] The scope (`this` reference) in which the specified function
         * is executed. Defaults to this Element.
         * @return {Ext.util.KeyMap} The KeyMap created
         */
        addKeyListener: function(key, fn, scope) {
            var config;
            if (typeof key !== 'object' || Ext.isArray(key)) {
                config = {
                    target: this,
                    key: key,
                    fn: fn,
                    scope: scope
                };
            } else {
                config = {
                    target: this,
                    key: key.key,
                    shift: key.shift,
                    ctrl: key.ctrl,
                    alt: key.alt,
                    fn: fn,
                    scope: scope
                };
            }
            return new Ext.util.KeyMap(config);
        },
        /**
         * Creates a KeyMap for this element
         * @param {Object} config The KeyMap config. See {@link Ext.util.KeyMap} for more details
         * @return {Ext.util.KeyMap} The KeyMap created
         */
        addKeyMap: function(config) {
            return new Ext.util.KeyMap(Ext.apply({
                target: this
            }, config));
        },
        /**
         * @private
         * Returns the fractional portion of this element's measurement in the given dimension.
         * (IE9+ only)
         * @return {Number}
         */
        adjustDirect2DDimension: function(dimension) {
            var me = this,
                dom = me.dom,
                display = me.getStyle('display'),
                inlineDisplay = dom.style.display,
                inlinePosition = dom.style.position,
                originIndex = dimension === WIDTH ? 0 : 1,
                currentStyle = dom.currentStyle,
                floating;
            if (display === 'inline') {
                dom.style.display = 'inline-block';
            }
            dom.style.position = display.match(adjustDirect2DTableRe) ? 'absolute' : 'static';
            // floating will contain digits that appears after the decimal point
            // if height or width are set to auto we fallback to msTransformOrigin calculation
            // Use currentStyle here instead of getStyle. In some difficult to reproduce
            // instances it resets the scrollWidth of the element
            floating = (parseFloat(currentStyle[dimension]) || parseFloat(currentStyle.msTransformOrigin.split(' ')[originIndex]) * 2) % 1;
            dom.style.position = inlinePosition;
            if (display === 'inline') {
                dom.style.display = inlineDisplay;
            }
            return floating;
        },
        /**
         * @private
         */
        afterAnimate: function() {
            var shadow = this.shadow;
            if (shadow && !shadow.disabled && !shadow.animate) {
                shadow.show();
            }
        },
        /**
         * @private
         */
        anchorAnimX: function(anchor) {
            var xName = (anchor === 'l') ? 'right' : 'left';
            this.dom.style[xName] = '0px';
        },
        /**
         * @private
         * process the passed fx configuration.
         */
        anim: function(config) {
            if (!Ext.isObject(config)) {
                return (config) ? {} : false;
            }
            // eslint-disable-next-line vars-on-top
            var me = this,
                duration = config.duration || Ext.fx.Anim.prototype.duration,
                easing = config.easing || 'ease',
                animConfig;
            if (config.stopAnimation) {
                me.stopAnimation();
            }
            Ext.applyIf(config, Ext.fx.Manager.getFxDefaults(me.id));
            // Clear any 'paused' defaults.
            Ext.fx.Manager.setFxDefaults(me.id, {
                delay: 0
            });
            animConfig = {
                // Pass the DOM reference. That's tested first so will be converted
                // to an Ext.fx.Target fastest.
                target: me.dom,
                remove: config.remove,
                alternate: config.alternate || false,
                duration: duration,
                easing: easing,
                callback: config.callback,
                listeners: config.listeners,
                iterations: config.iterations || 1,
                scope: config.scope,
                block: config.block,
                concurrent: config.concurrent,
                delay: config.delay || 0,
                paused: true,
                keyframes: config.keyframes,
                from: config.from || {},
                to: Ext.apply({}, config),
                userConfig: config
            };
            Ext.apply(animConfig.to, config.to);
            // Anim API properties - backward compat
            delete animConfig.to.to;
            delete animConfig.to.from;
            delete animConfig.to.remove;
            delete animConfig.to.alternate;
            delete animConfig.to.keyframes;
            delete animConfig.to.iterations;
            delete animConfig.to.listeners;
            delete animConfig.to.target;
            delete animConfig.to.paused;
            delete animConfig.to.callback;
            delete animConfig.to.scope;
            delete animConfig.to.duration;
            delete animConfig.to.easing;
            delete animConfig.to.concurrent;
            delete animConfig.to.block;
            delete animConfig.to.stopAnimation;
            delete animConfig.to.delay;
            return animConfig;
        },
        /**
         * Calls `{@link #addAnimation}` and returns this Element (for call chaining). For
         * details, see `{@link #addAnimation}`.
         *
         * @param {Object} config  Configuration for {@link Ext.fx.Anim}.
         * Note that the {@link Ext.fx.Anim#to to} config is required.
         * @return {Ext.dom.Element} this
         */
        animate: function(config) {
            this.addAnimation(config);
            return this;
        },
        /**
         * Starts a custom animation on this Element.
         *
         * The following properties may be specified in `from`, `to`, and `keyframe` objects:
         *
         *   - `x` - The page X position in pixels.
         *   - `y` - The page Y position in pixels
         *   - `left` - The element's CSS `left` value. Units must be supplied.
         *   - `top` - The element's CSS `top` value. Units must be supplied.
         *   - `width` - The element's CSS `width` value. Units must be supplied.
         *   - `height` - The element's CSS `height` value. Units must be supplied.
         *   - `scrollLeft` - The element's `scrollLeft` value.
         *   - `scrollTop` - The element's `scrollTop` value.
         *   - `opacity` - The element's `opacity` value (between `0` and `1`).
         *
         * **Be aware** that animating an Element which is being used by an Ext Component
         * without in some way informing the Component about the changed element state will
         * result in incorrect Component behaviour. This is because the Component will be
         * using the old state of the element. To avoid this problem, it is now possible
         * to directly animate certain properties of Components.
         *
         * @param {Object} config  Configuration for {@link Ext.fx.Anim}.
         * Note that the {@link Ext.fx.Anim#to to} config is required.
         * @return {Ext.fx.Anim} The new animation.
         */
        addAnimation: function(config) {
            var me = this,
                animId = me.dom.id || Ext.id(me.dom),
                listeners, anim, end;
            if (!Ext.fx.Manager.hasFxBlock(animId)) {
                // Bit of gymnastics here to ensure our internal listeners get bound first
                if (config.listeners) {
                    listeners = config.listeners;
                    delete config.listeners;
                }
                if (config.internalListeners) {
                    config.listeners = config.internalListeners;
                    delete config.internalListeners;
                }
                end = config.autoEnd;
                delete config.autoEnd;
                anim = new Ext.fx.Anim(me.anim(config));
                anim.on({
                    afteranimate: 'afterAnimate',
                    beforeanimate: 'beforeAnimate',
                    scope: me,
                    single: true
                });
                if (listeners) {
                    anim.on(listeners);
                }
                Ext.fx.Manager.queueFx(anim);
                if (end) {
                    anim.jumpToEnd();
                }
            }
            return anim;
        },
        /**
         * @private
         */
        beforeAnimate: function() {
            var shadow = this.shadow;
            if (shadow && !shadow.disabled && !shadow.animate) {
                shadow.hide();
            }
        },
        /**
         * Wraps the specified element with a special 9 element markup/CSS block that renders
         * by default as a gray container with a gradient background, rounded corners
         * and a 4-way shadow.
         *
         * This special markup is used throughout Ext when box wrapping elements
         * ({@link Ext.button.Button}, {@link Ext.panel.Panel} when
         * {@link Ext.panel.Panel#frame frame=true}, {@link Ext.window.Window}).
         * The markup is of this form:
         *
         *     <div class="{0}-tl"><div class="{0}-tr"><div class="{0}-tc"></div></div></div>
         *     <div class="{0}-ml"><div class="{0}-mr"><div class="{0}-mc"></div></div></div>
         *     <div class="{0}-bl"><div class="{0}-br"><div class="{0}-bc"></div></div></div>
         *
         * Example usage:
         *
         *     // Basic box wrap
         *     Ext.get("foo").boxWrap();
         *
         *     // You can also add a custom class and use CSS inheritance rules to customize
         *     // the box look.
         *     // 'x-box-blue' is a built-in alternative -- look at the related CSS definitions
         *     // as an example for how to create a custom box wrap style.
         *     Ext.get("foo").boxWrap().addCls("x-box-blue");
         *
         * @param {String} [cls='x-box'] A base CSS class to apply to the containing wrapper
         * element. Note that there are a number of CSS rules that are dependent on this name
         * to make the overall effect work, so if you supply an alternate base class, make sure
         * you also supply all of the necessary rules.
         * @return {Ext.dom.Element} The outermost wrapping element of the created box structure.
         */
        boxWrap: function(cls) {
            var el;
            cls = cls || Ext.baseCSSPrefix + 'box';
            el = Ext.get(this.insertHtml("beforeBegin", "<div class='" + cls + "' role='presentation'>" + Ext.String.format(boxMarkup, cls) + "</div>"));
            el.selectNode('.' + cls + '-mc').appendChild(this.dom);
            return el;
        },
        /**
         * Removes Empty, or whitespace filled text nodes. Combines adjacent text nodes.
         * @param {Boolean} [forceReclean=false] By default the element keeps track if it has been
         * cleaned already so you can call this over and over. However, if you update the element
         * and need to force a re-clean, you can pass true.
         */
        clean: function(forceReclean) {
            var me = this,
                dom = me.dom,
                data = me.getData(),
                n = dom.firstChild,
                ni = -1,
                nx;
            if (data.isCleaned && forceReclean !== true) {
                return me;
            }
            while (n) {
                nx = n.nextSibling;
                if (n.nodeType === 3) {
                    // Remove empty/whitespace text nodes
                    if (!(nonSpaceRe.test(n.nodeValue))) {
                        dom.removeChild(n);
                    }
                    // Combine adjacent text nodes
                    else if (nx && nx.nodeType === 3) {
                        n.appendData(Ext.String.trim(nx.data));
                        dom.removeChild(nx);
                        nx = n.nextSibling;
                        n.nodeIndex = ++ni;
                    }
                } else {
                    // Recursively clean
                    Ext.fly(n, '_clean').clean();
                    n.nodeIndex = ++ni;
                }
                n = nx;
            }
            data.isCleaned = true;
            return me;
        },
        /**
         * @method
         * Empties this element. Removes all child nodes.
         */
        empty: emptyRange ? function() {
            var dom = this.dom;
            if (dom.firstChild) {
                emptyRange.setStartBefore(dom.firstChild);
                emptyRange.setEndAfter(dom.lastChild);
                emptyRange.deleteContents();
            }
        } : function() {
            var dom = this.dom;
            while (dom.lastChild) {
                dom.removeChild(dom.lastChild);
            }
        },
        clearListeners: function() {
            this.removeAnchor();
            this.callParent();
        },
        /**
         * Clears positioning back to the default when the document was loaded.
         * @param {String} [value=''] The value to use for the left, right, top, bottom.
         * You could use 'auto'.
         * @return {Ext.dom.Element} this
         */
        clearPositioning: function(value) {
            value = value || '';
            return this.setStyle({
                left: value,
                right: value,
                top: value,
                bottom: value,
                'z-index': '',
                position: 'static'
            });
        },
        /**
         * Creates a proxy element of this element
         * @param {String/Object} config The class name of the proxy element or a DomHelper config
         * object
         * @param {String/HTMLElement} [renderTo] The element or element id to render the proxy to.
         * Defaults to: document.body.
         * @param {Boolean} [matchBox=false] True to align and size the proxy to this element now.
         * @return {Ext.dom.Element} The new proxy element
         */
        createProxy: function(config, renderTo, matchBox) {
            config = (typeof config === 'object') ? config : {
                tag: "div",
                role: 'presentation',
                cls: config
            };
            // eslint-disable-next-line vars-on-top
            var me = this,
                proxy = renderTo ? Ext.DomHelper.append(renderTo, config, true) : Ext.DomHelper.insertBefore(me.dom, config, true);
            proxy.setVisibilityMode(Element.DISPLAY);
            proxy.hide();
            // check to make sure Element_position.js is loaded
            if (matchBox && me.setBox && me.getBox) {
                proxy.setBox(me.getBox());
            }
            return proxy;
        },
        /**
         * Clears any opacity settings from this element. Required in some cases for IE.
         * @return {Ext.dom.Element} this
         */
        clearOpacity: function() {
            return this.setOpacity('');
        },
        /**
         * Store the current overflow setting and clip overflow on the element - use {@link #unclip}
         * to remove
         * @return {Ext.dom.Element} this
         */
        clip: function() {
            var me = this,
                data = me.getData(),
                style;
            if (!data[ISCLIPPED]) {
                data[ISCLIPPED] = true;
                style = me.getStyle([
                    OVERFLOW,
                    OVERFLOWX,
                    OVERFLOWY
                ]);
                data[ORIGINALCLIP] = {
                    o: style[OVERFLOW],
                    x: style[OVERFLOWX],
                    y: style[OVERFLOWY]
                };
                me.setStyle(OVERFLOW, HIDDEN);
                me.setStyle(OVERFLOWX, HIDDEN);
                me.setStyle(OVERFLOWY, HIDDEN);
            }
            return me;
        },
        destroy: function() {
            var me = this,
                dom = me.dom,
                data = me.peekData(),
                maskEl, maskMsg;
            if (dom) {
                if (me.isAnimate) {
                    me.stopAnimation(true);
                }
                me.removeAnchor();
            }
            if (me.deferredFocusTimer) {
                Ext.undefer(me.deferredFocusTimer);
                me.deferredFocusTimer = null;
            }
            me.callParent();
            // prevent memory leaks in IE8
            // see http://social.msdn.microsoft.com/Forums/ie/en-US/c76967f0-dcf8-47d0-8984-8fe1282a94f5/ie-appendchildremovechild-memory-problem?forum=iewebdevelopment
            // must not be document, documentElement, body or window object
            // Have to use != instead of !== for IE8 or it will not recognize that the window
            // objects are equal
            // eslint-disable-next-line eqeqeq
            if (dom && Ext.isIE8 && (dom.window != dom) && (dom.nodeType !== 9) && (dom.tagName !== 'BODY') && (dom.tagName !== 'HTML')) {
                destroyQueue[destroyQueue.length] = dom;
                // Will perform extra IE8 cleanup in 10 milliseconds
                // see http://social.msdn.microsoft.com/Forums/ie/en-US/c76967f0-dcf8-47d0-8984-8fe1282a94f5/ie-appendchildremovechild-memory-problem?forum=iewebdevelopment
                clearGarbage();
            }
            if (data) {
                maskEl = data.maskEl;
                maskMsg = data.maskMsg;
                if (maskEl) {
                    maskEl.destroy();
                }
                if (maskMsg) {
                    maskMsg.destroy();
                }
            }
        },
        /**
         * Convenience method for setVisibilityMode(Element.DISPLAY).
         * @param {String} [display] What to set display to when visible
         * @return {Ext.dom.Element} this
         */
        enableDisplayMode: function(display) {
            var me = this;
            me.setVisibilityMode(Element.DISPLAY);
            if (display !== undefined) {
                me.getData()[ORIGINALDISPLAY] = display;
            }
            return me;
        },
        /**
         * Fade an element in (from transparent to opaque). The ending opacity can be specified
         * using the `opacity` config option. Usage:
         *
         *     // default: fade in from opacity 0 to 100%
         *     el.fadeIn();
         *
         *     // custom: fade in from opacity 0 to 75% over 2 seconds
         *     el.fadeIn({ opacity: .75, duration: 2000});
         *
         *     // common config options shown with default values
         *     el.fadeIn({
         *         opacity: 1, //can be any value between 0 and 1 (e.g. .5)
         *         easing: 'easeOut',
         *         duration: 500
         *     });
         *
         * @param {Object} options (optional) Object literal with any of the {@link Ext.fx.Anim}
         * config options
         * @return {Ext.dom.Element} The Element
         */
        fadeIn: function(options) {
            var me = this,
                dom = me.dom,
                animFly = new Ext.dom.Fly();
            me.animate(Ext.apply({}, options, {
                opacity: 1,
                internalListeners: {
                    beforeanimate: function(anim) {
                        // Reattach to the DOM in case the caller animated a Fly
                        // in which case the dom reference will have changed by now.
                        animFly.attach(dom);
                        // restore any visibility/display that may have 
                        // been applied by a fadeout animation
                        if (animFly.isStyle('display', 'none')) {
                            animFly.setDisplayed('');
                        } else {
                            animFly.show();
                        }
                    }
                }
            }));
            return this;
        },
        /**
         * Fade an element out (from opaque to transparent). The ending opacity can be specified
         * using the `opacity` config option. Note that IE may require `useDisplay: true` in order
         * to redisplay correctly. Usage:
         *
         *     // default: fade out from the element's current opacity to 0
         *     el.fadeOut();
         *
         *     // custom: fade out from the element's current opacity to 25% over 2 seconds
         *     el.fadeOut({ opacity: .25, duration: 2000});
         *
         *     // common config options shown with default values
         *     el.fadeOut({
         *         opacity: 0, //can be any value between 0 and 1 (e.g. .5)
         *         easing: 'easeOut',
         *         duration: 500,
         *         remove: false,
         *         useDisplay: false
         *     });
         *
         * @param {Object} options (optional) Object literal with any of the {@link Ext.fx.Anim}
         * config options
         * @return {Ext.dom.Element} The Element
         */
        fadeOut: function(options) {
            var me = this,
                dom = me.dom,
                animFly = new Ext.dom.Fly();
            options = Ext.apply({
                opacity: 0,
                internalListeners: {
                    afteranimate: function(anim) {
                        if (anim.to.opacity === 0) {
                            // Reattach to the DOM in case the caller animated a Fly
                            // in which case the dom reference will have changed by now.
                            animFly.attach(dom);
                            // Reattach to the DOM in case the caller animated a Fly
                            // in which case the dom reference will have changed by now.
                            animFly.attach(dom);
                            if (options.useDisplay) {
                                animFly.setDisplayed(false);
                            } else {
                                animFly.hide();
                            }
                        }
                    }
                }
            }, options);
            me.animate(options);
            return me;
        },
        /**
         * @private
         */
        fixDisplay: function() {
            var me = this;
            if (me.isStyle(DISPLAY, NONE)) {
                me.setStyle(VISIBILITY, HIDDEN);
                me.setStyle(DISPLAY, me._getDisplay());
                // first try reverting to default
                if (me.isStyle(DISPLAY, NONE)) {
                    // if that fails, default to block
                    me.setStyle(DISPLAY, "block");
                }
            }
        },
        /**
         * Shows a ripple of exploding, attenuating borders to draw attention to an Element. Usage:
         *
         *     // default: a single light blue ripple
         *     el.frame();
         *
         *     // custom: 3 red ripples lasting 3 seconds total
         *     el.frame("#ff0000", 3, { duration: 3000 });
         *
         *     // common config options shown with default values
         *     el.frame("#C3DAF9", 1, {
         *         duration: 1000 // duration of each individual ripple.
         *         // Note: Easing is not configurable and will be ignored if included
         *     });
         *
         * @param {String} [color='#C3DAF9'] The hex color value for the border.
         * @param {Number} [count=1] The number of ripples to display.
         * @param {Object} [obj] Object literal with any of the {@link Ext.fx.Anim} config options
         * @return {Ext.dom.Element} The Element
         */
        frame: function(color, count, obj) {
            var me = this,
                dom = me.dom,
                animFly = new Ext.dom.Fly(),
                beforeAnim;
            color = color || '#C3DAF9';
            count = count || 1;
            obj = obj || {};
            beforeAnim = function() {
                var animScope = this,
                    box, proxy, proxyAnim;
                // Reattach to the DOM in case the caller animated a Fly
                // in which case the dom reference will have changed by now.
                animFly.attach(dom);
                animFly.show();
                box = animFly.getBox();
                proxy = Ext.getBody().createChild({
                    role: 'presentation',
                    id: animFly.dom.id + '-anim-proxy',
                    style: {
                        position: 'absolute',
                        'pointer-events': 'none',
                        'z-index': 35000,
                        border: '0px solid ' + color
                    }
                });
                proxyAnim = new Ext.fx.Anim({
                    target: proxy,
                    duration: obj.duration || 1000,
                    iterations: count,
                    from: {
                        top: box.y,
                        left: box.x,
                        borderWidth: 0,
                        opacity: 1,
                        height: box.height,
                        width: box.width
                    },
                    to: {
                        top: box.y - 20,
                        left: box.x - 20,
                        borderWidth: 10,
                        opacity: 0,
                        height: box.height + 40,
                        width: box.width + 40
                    }
                });
                proxyAnim.on('afteranimate', function() {
                    proxy.destroy();
                    // kill the no-op element animation created below
                    animScope.end();
                });
            };
            me.animate({
                // See "A Note About Wrapped Animations" at the top of this class:
                duration: (Math.max(obj.duration, 500) * 2) || 2000,
                listeners: {
                    beforeanimate: {
                        fn: beforeAnim
                    }
                },
                callback: obj.callback,
                scope: obj.scope
            });
            return me;
        },
        /**
         * Return the CSS color for the specified CSS attribute. rgb, 3 digit (like `#fff`)
         * and valid values are convert to standard 6 digit hex color.
         * @param {String} attr The css attribute
         * @param {String} defaultValue The default value to use when a valid color isn't found
         * @param {String} [prefix] defaults to #. Use an empty string when working with
         * color anims.
         * @private
         */
        getColor: function(attr, defaultValue, prefix) {
            var v = this.getStyle(attr),
                color = prefix || prefix === '' ? prefix : '#',
                h, len, i;
            if (!v || (/transparent|inherit/.test(v))) {
                return defaultValue;
            }
            if (/^r/.test(v)) {
                v = v.slice(4, v.length - 1).split(',');
                len = v.length;
                for (i = 0; i < len; i++) {
                    h = parseInt(v[i], 10);
                    color += (h < 16 ? '0' : '') + h.toString(16);
                }
            } else {
                v = v.replace('#', '');
                color += v.length === 3 ? v.replace(/^(\w)(\w)(\w)$/, '$1$1$2$2$3$3') : v;
            }
            return (color.length > 5 ? color.toLowerCase() : defaultValue);
        },
        /**
         * Gets this element's {@link Ext.ElementLoader ElementLoader}
         * @return {Ext.ElementLoader} The loader
         */
        getLoader: function() {
            var me = this,
                data = me.getData(),
                loader = data.loader;
            if (!loader) {
                data.loader = loader = new Ext.ElementLoader({
                    target: me
                });
            }
            return loader;
        },
        /**
         * Gets an object with all CSS positioning properties. Useful along with
         * `setPositioning` to get snapshot before performing an update and then restoring
         * the element.
         * @param {Boolean} [autoPx=false] true to return pixel values for "auto" styles.
         * @return {Object}
         */
        getPositioning: function(autoPx) {
            var styles = this.getStyle([
                    'left',
                    'top',
                    'position',
                    'z-index'
                ]),
                dom = this.dom;
            if (autoPx) {
                if (styles.left === 'auto') {
                    styles.left = dom.offsetLeft + 'px';
                }
                if (styles.top === 'auto') {
                    styles.top = dom.offsetTop + 'px';
                }
            }
            return styles;
        },
        /**
         * Slides the element while fading it out of view. An anchor point can be optionally passed
         * to set the ending point of the effect. Usage:
         *
         *     // default: slide the element downward while fading out
         *     el.ghost();
         *
         *     // custom: slide the element out to the right with a 2-second duration
         *     el.ghost('r', { duration: 2000 });
         *
         *     // common config options shown with default values
         *     el.ghost('b', {
         *         easing: 'easeOut',
         *         duration: 500
         *     });
         *
         * @param {String} [anchor] One of the valid {@link Ext.fx.Anim} anchor positions
         * (defaults to bottom: 'b')
         * @param {Object} [options] Object literal with any of the {@link Ext.fx.Anim}
         * config options
         * @return {Ext.dom.Element} The Element
         */
        ghost: function(anchor, options) {
            var me = this,
                dom = me.dom,
                animFly = new Ext.dom.Fly(),
                beforeAnim;
            anchor = anchor || "b";
            beforeAnim = function() {
                // Reattach to the DOM in case the caller animated a Fly
                // in which case the dom reference will have changed by now.
                animFly.attach(dom);
                // eslint-disable-next-line vars-on-top
                var width = animFly.getWidth(),
                    height = animFly.getHeight(),
                    xy = animFly.getXY(),
                    position = animFly.getPositioning(),
                    to = {
                        opacity: 0
                    };
                switch (anchor) {
                    case 't':
                        to.y = xy[1] - height;
                        break;
                    case 'l':
                        to.x = xy[0] - width;
                        break;
                    case 'r':
                        to.x = xy[0] + width;
                        break;
                    case 'b':
                        to.y = xy[1] + height;
                        break;
                    case 'tl':
                        to.x = xy[0] - width;
                        to.y = xy[1] - height;
                        break;
                    case 'bl':
                        to.x = xy[0] - width;
                        to.y = xy[1] + height;
                        break;
                    case 'br':
                        to.x = xy[0] + width;
                        to.y = xy[1] + height;
                        break;
                    case 'tr':
                        to.x = xy[0] + width;
                        to.y = xy[1] - height;
                        break;
                }
                this.to = to;
                this.on('afteranimate', function() {
                    // Reattach to the DOM in case the caller animated a Fly
                    // in which case the dom reference will have changed by now.
                    animFly.attach(dom);
                    if (animFly) {
                        animFly.hide();
                        animFly.clearOpacity();
                        animFly.setPositioning(position);
                    }
                });
            };
            me.animate(Ext.applyIf(options || {}, {
                duration: 500,
                easing: 'ease-out',
                listeners: {
                    beforeanimate: beforeAnim
                }
            }));
            return me;
        },
        getTextSelection: function() {
            var ret, dom, doc, range, textRange;
            ret = this.callParent();
            if (typeof ret[0] !== 'number') {
                dom = this.dom;
                doc = dom.ownerDocument;
                range = doc.selection.createRange();
                textRange = dom.createTextRange();
                textRange.setEndPoint('EndToStart', range);
                ret[0] = textRange.text.length;
                ret[1] = ret[0] + range.text.length;
            }
            return ret;
        },
        /**
         * Hide this element - Uses display mode to determine whether to use "display",
         * "visibility", "offsets", or "clip". See {@link #setVisible}.
         * @param {Boolean/Object} [animate] true for the default animation or a standard
         * Element animation config object
         * @return {Ext.dom.Element} this
         */
        hide: function(animate) {
            // hideMode override
            if (typeof animate === 'string') {
                this.setVisible(false, animate);
                return this;
            }
            this.setVisible(false, this.anim(animate));
            return this;
        },
        /**
         * Highlights the Element by setting a color (applies to the background-color by default,
         * but can be changed using the "attr" config option) and then fading back to the original
         * color. If no original color is available, you should provide the "endColor" config option
         * which will be cleared after the animation. Usage:
         *
         *     // default: highlight background to yellow
         *     el.highlight();
         *
         *     // custom: highlight foreground text to blue for 2 seconds
         *     el.highlight("0000ff", { attr: 'color', duration: 2000 });
         *
         *     // common config options shown with default values
         *     el.highlight("ffff9c", {
         *         // can be any valid CSS property (attribute) that supports a color value
         *         attr: "backgroundColor",
         *         endColor: (current color) or "ffffff",
         *         easing: 'easeIn',
         *         duration: 1000
         *     });
         *
         * @param {String} color (optional) The highlight color. Should be a 6 char hex color
         * without the leading # (defaults to yellow: 'ffff9c')
         * @param {Object} options (optional) Object literal with any of the {@link Ext.fx.Anim}
         * config options
         * @return {Ext.dom.Element} The Element
         */
        highlight: function(color, options) {
            var me = this,
                dom = me.dom,
                from = {},
                animFly = new Ext.dom.Fly(),
                restore, to, attr, lns, event, fn;
            options = options || {};
            lns = options.listeners || {};
            attr = options.attr || 'backgroundColor';
            from[attr] = color || 'ffff9c';
            if (!options.to) {
                to = {};
                to[attr] = options.endColor || me.getColor(attr, 'ffffff', '');
            } else {
                to = options.to;
            }
            // Don't apply directly on lns, since we reference it in our own callbacks below
            options.listeners = Ext.apply(Ext.apply({}, lns), {
                beforeanimate: function() {
                    // Reattach to the DOM in case the caller animated a Fly
                    // in which case the dom reference will have changed by now.
                    animFly.attach(dom);
                    restore = dom.style[attr];
                    animFly.clearOpacity();
                    animFly.show();
                    event = lns.beforeanimate;
                    if (event) {
                        fn = event.fn || event;
                        return fn.apply(event.scope || lns.scope || WIN, arguments);
                    }
                },
                afteranimate: function() {
                    if (dom) {
                        dom.style[attr] = restore;
                    }
                    event = lns.afteranimate;
                    if (event) {
                        fn = event.fn || event;
                        fn.apply(event.scope || lns.scope || WIN, arguments);
                    }
                }
            });
            me.animate(Ext.apply({}, options, {
                duration: 1000,
                easing: 'ease-in',
                from: from,
                to: to
            }));
            return me;
        },
        /**
         * Initializes a {@link Ext.dd.DD} drag drop object for this element.
         * @param {String} group The group the DD object is member of
         * @param {Object} config The DD config object
         * @param {Object} overrides An object containing methods to override/implement
         * on the DD object
         * @return {Ext.dd.DD} The DD object
         */
        initDD: function(group, config, overrides) {
            var dd = new Ext.dd.DD(Ext.id(this.dom), group, config);
            return Ext.apply(dd, overrides);
        },
        /**
         * Initializes a {@link Ext.dd.DDProxy} object for this element.
         * @param {String} group The group the DDProxy object is member of
         * @param {Object} config The DDProxy config object
         * @param {Object} overrides An object containing methods to override/implement
         * on the DDProxy object
         * @return {Ext.dd.DDProxy} The DDProxy object
         */
        initDDProxy: function(group, config, overrides) {
            var dd = new Ext.dd.DDProxy(Ext.id(this.dom), group, config);
            return Ext.apply(dd, overrides);
        },
        /**
         * Initializes a {@link Ext.dd.DDTarget} object for this element.
         * @param {String} group The group the DDTarget object is member of
         * @param {Object} config The DDTarget config object
         * @param {Object} overrides An object containing methods to override/implement
         * on the DDTarget object
         * @return {Ext.dd.DDTarget} The DDTarget object
         */
        initDDTarget: function(group, config, overrides) {
            var dd = new Ext.dd.DDTarget(Ext.id(this.dom), group, config);
            return Ext.apply(dd, overrides);
        },
        /**
         * Returns true if this element is masked. Also re-centers any displayed message
         * within the mask.
         *
         * @param {Boolean} [deep] Go up the DOM hierarchy to determine if any parent
         * element is masked.
         *
         * @return {Boolean}
         */
        isMasked: function(deep) {
            var me = this,
                data = me.getData(),
                maskEl = data.maskEl,
                maskMsg = data.maskMsg,
                hasMask = false,
                parent;
            if (maskEl && maskEl.isVisible()) {
                if (maskMsg) {
                    maskMsg.center(me);
                }
                hasMask = true;
            } else if (deep) {
                parent = me.findParentNode();
                if (parent) {
                    return Ext.fly(parent).isMasked(deep);
                }
            }
            return hasMask;
        },
        /**
         * Direct access to the Ext.ElementLoader {@link Ext.ElementLoader#method-load} method.
         * The method takes the same object parameter as {@link Ext.ElementLoader#method-load}
         * @param {Object} options a options object for Ext.ElementLoader
         * {@link Ext.ElementLoader#method-load}
         * @return {Ext.dom.Element} this
         */
        load: function(options) {
            this.getLoader().load(options);
            return this;
        },
        /**
         * Puts a mask over this element to disable user interaction.
         * This method can only be applied to elements which accept child nodes. Use
         * {@link #unmask} to remove the mask.
         *
         * @param {String} [msg] A message to display in the mask
         * @param {String} [msgCls] A css class to apply to the msg element
         * @param {Number} elHeight (private) Passed by AbstractComponent.mask to avoid the need
         * to interrogate the DOM to get the height
         * @return {Ext.dom.Element} The mask element
         */
        mask: function(msg, msgCls, elHeight) {
            var me = this,
                dom = me.dom,
                data = me.getData(),
                maskEl = data.maskEl,
                maskMsg;
            if (!(bodyRe.test(dom.tagName) && me.getStyle('position') === 'static')) {
                me.addCls(XMASKEDRELATIVE);
            }
            // We always needs to recreate the mask since the DOM element may have been re-created
            if (maskEl) {
                maskEl.destroy();
            }
            maskEl = Ext.DomHelper.append(dom, {
                role: 'presentation',
                cls: Ext.baseCSSPrefix + "mask " + Ext.baseCSSPrefix + "border-box",
                children: {
                    role: 'presentation',
                    cls: msgCls ? EXTELMASKMSG + " " + msgCls : EXTELMASKMSG,
                    cn: {
                        tag: 'div',
                        role: 'presentation',
                        cls: Ext.baseCSSPrefix + 'mask-msg-inner',
                        cn: {
                            tag: 'div',
                            role: 'presentation',
                            cls: Ext.baseCSSPrefix + 'mask-msg-text',
                            html: msg || ''
                        }
                    }
                }
            }, true);
            maskMsg = Ext.fly(maskEl.dom.firstChild);
            data.maskEl = maskEl;
            me.addCls(XMASKED);
            maskEl.setDisplayed(true);
            if (typeof msg === 'string') {
                maskMsg.setDisplayed(true);
                maskMsg.center(me);
            } else {
                maskMsg.setDisplayed(false);
            }
            if (dom === DOC.body) {
                maskEl.addCls(Ext.baseCSSPrefix + 'mask-fixed');
            }
            // When masking the body, don't touch its tabbable state
            me.saveTabbableState({
                skipSelf: dom === DOC.body
            });
            // ie will not expand full height automatically
            if (Ext.isIE9m && dom !== DOC.body && me.isStyle('height', 'auto')) {
                maskEl.setSize(undefined, elHeight || me.getHeight());
            }
            return maskEl;
        },
        /**
         * Fades the element out while slowly expanding it in all directions. When the effect
         * is completed, the element will be hidden (visibility = 'hidden') but block elements
         * will still take up space in the document. Usage:
         *
         *     // default
         *     el.puff();
         *
         *     // common config options shown with default values
         *     el.puff({
         *         easing: 'easeOut',
         *         duration: 500,
         *         useDisplay: false
         *     });
         *
         * @param {Object} obj (optional) Object literal with any of the {@link Ext.fx.Anim}
         * config options
         * @return {Ext.dom.Element} The Element
         */
        puff: function(obj) {
            var me = this,
                dom = me.dom,
                animFly = new Ext.dom.Fly(),
                beforeAnim,
                box = me.getBox(),
                originalStyles;
            originalStyles = me.getStyle([
                'width',
                'height',
                'left',
                'right',
                'top',
                'bottom',
                'position',
                'z-index',
                'font-size',
                'opacity'
            ], true);
            obj = Ext.applyIf(obj || {}, {
                easing: 'ease-out',
                duration: 500,
                useDisplay: false
            });
            beforeAnim = function() {
                // Reattach to the DOM in case the caller animated a Fly
                // in which case the dom reference will have changed by now.
                animFly.attach(dom);
                animFly.clearOpacity();
                animFly.show();
                this.to = {
                    width: box.width * 2,
                    height: box.height * 2,
                    x: box.x - (box.width / 2),
                    y: box.y - (box.height / 2),
                    opacity: 0,
                    fontSize: '200%'
                };
                this.on('afteranimate', function() {
                    // Reattach to the DOM in case the caller animated a Fly
                    // in which case the dom reference will have changed by now.
                    animFly.attach(dom);
                    if (obj.useDisplay) {
                        animFly.setDisplayed(false);
                    } else {
                        animFly.hide();
                    }
                    animFly.setStyle(originalStyles);
                    Ext.callback(obj.callback, obj.scope);
                });
            };
            me.animate({
                duration: obj.duration,
                easing: obj.easing,
                listeners: {
                    beforeanimate: {
                        fn: beforeAnim
                    }
                }
            });
            return me;
        },
        // private
        // used to ensure the mouseup event is captured if it occurs outside of the
        // window in IE9m.  The only reason this method exists, (vs just calling
        // el.dom.setCapture() directly) is so that we can override it to emptyFn
        // during testing because setCapture() can wreak havoc on emulated mouse events
        // http://msdn.microsoft.com/en-us/library/windows/desktop/ms646262(v=vs.85).aspx
        setCapture: function() {
            var dom = this.dom;
            if (Ext.isIE9m && dom.setCapture) {
                dom.setCapture();
            }
        },
        /**
         * Set the height of this Element.
         * 
         *     // change the height to 200px and animate with default configuration
         *     Ext.fly('elementId').setHeight(200, true);
         *
         *     // change the height to 150px and animate with a custom configuration
         *     Ext.fly('elId').setHeight(150, {
         *         duration : 500, // animation will have a duration of .5 seconds
         *         // will change the content to "finished"
         *         callback: function(){ this.setHtml("finished"); }
         *     });
         *     
         * @param {Number/String} height The new height. This may be one of:
         *
         * - A Number specifying the new height in pixels.
         * - A String used to set the CSS height style. Animation may **not** be used.
         *     
         * @param {Boolean/Object} [animate] a standard Element animation config object or `true`
         * for the default animation (`{duration: 350, easing: 'ease-in'}`)
         * @return {Ext.dom.Element} this
         */
        setHeight: function(height, animate) {
            var me = this;
            if (!animate || !me.anim) {
                me.callParent(arguments);
            } else {
                if (!Ext.isObject(animate)) {
                    animate = {};
                }
                me.animate(Ext.applyIf({
                    to: {
                        height: height
                    }
                }, animate));
            }
            return me;
        },
        /**
         * Removes "vertical" state from this element (reverses everything done
         * by {@link #setVertical}).
         * @private
         */
        setHorizontal: function() {
            var me = this,
                cls = me.verticalCls;
            delete me.vertical;
            if (cls) {
                delete me.verticalCls;
                me.removeCls(cls);
            }
            // delete the inverted methods and revert to inheriting from the prototype 
            delete me.setWidth;
            delete me.setHeight;
            if (!Ext.isIE8) {
                delete me.getWidth;
                delete me.getHeight;
            }
            // revert to inheriting styleHooks from the prototype
            delete me.styleHooks;
        },
        /**
         * Updates the *text* value of this element.
         * Replaces the content of this element with a *single text node* containing
         * the passed text.
         * @param {String} text The text to display in this Element.
         */
        updateText: function(text) {
            var me = this,
                dom, textNode;
            if (dom) {
                textNode = dom.firstChild;
                if (!textNode || (textNode.nodeType !== 3 || textNode.nextSibling)) {
                    textNode = DOC.createTextNode();
                    me.empty();
                    dom.appendChild(textNode);
                }
                if (text) {
                    textNode.data = text;
                }
            }
        },
        /**
         * Updates the innerHTML of this element, optionally searching for and processing scripts.
         * @param {String} html The new HTML
         * @param {Boolean} [loadScripts] Pass `true` to look for and process scripts.
         * @param {Function} [callback] For async script loading you can be notified
         * when the update completes.
         * @param {Object} [scope=`this`] The scope (`this` reference) in which to execute
         * the callback.
         * 
         * Also used as the scope for any *inline* script source if the `loadScripts` parameter
         * is `true`. Scripts with a `src` attribute cannot be executed in this scope.
         *
         * Defaults to this Element.
         * @return {Ext.dom.Element} this
         */
        setHtml: function(html, loadScripts, callback, scope) {
            var me = this,
                id, dom, interval;
            if (!me.dom) {
                return me;
            }
            html = html || '';
            dom = me.dom;
            if (loadScripts !== true) {
                // Setting innerHtml changes the DOM and replace all dom nodes
                // with the new html. For IE specifically, all dom child nodes get 
                // destroyed when removed from DOM tree even if DOM is referenced 
                // within some JS file. Thus, before setting innerHTML, remove the 
                // children so that they are not destroyed/removed from DOM tree.
                if (Ext.isIE) {
                    while (dom.firstChild) {
                        dom.removeChild(dom.firstChild);
                    }
                }
                dom.innerHTML = html;
                Ext.callback(callback, me);
                return me;
            }
            id = Ext.id();
            html += '<span id="' + id + '" role="presentation"></span>';
            interval = Ext.interval(function() {
                var hd, match, attrs, srcMatch, typeMatch, el, s;
                if (!(el = DOC.getElementById(id))) {
                    return false;
                }
                Ext.uninterval(interval);
                Ext.removeNode(el);
                hd = Ext.getHead().dom;
                while ((match = scriptTagRe.exec(html))) {
                    attrs = match[1];
                    srcMatch = attrs ? attrs.match(srcRe) : false;
                    if (srcMatch && srcMatch[2]) {
                        s = DOC.createElement("script");
                        s.src = srcMatch[2];
                        typeMatch = attrs.match(typeRe);
                        if (typeMatch && typeMatch[2]) {
                            s.type = typeMatch[2];
                        }
                        hd.appendChild(s);
                    } else if (match[2] && match[2].length > 0) {
                        if (scope) {
                            Ext.functionFactory(match[2]).call(scope);
                        } else {
                            Ext.globalEval(match[2]);
                        }
                    }
                }
                Ext.callback(callback, scope || me);
            }, 20);
            dom.innerHTML = html.replace(replaceScriptTagRe, '');
            return me;
        },
        /**
         * Set the opacity of the element
         * @param {Number} opacity The new opacity. 0 = transparent, .5 = 50% visible,
         * 1 = fully visible, etc
         * @param {Boolean/Object} [animate] a standard Element animation config object or `true`
         * for the default animation (`{duration: 350, easing: 'ease-in'}`)
         * @return {Ext.dom.Element} this
         */
        setOpacity: function(opacity, animate) {
            var me = this;
            if (!me.dom) {
                return me;
            }
            if (!animate || !me.anim) {
                me.setStyle('opacity', opacity);
            } else {
                if (typeof animate !== 'object') {
                    animate = {
                        duration: 350,
                        easing: 'ease-in'
                    };
                }
                me.animate(Ext.applyIf({
                    to: {
                        opacity: opacity
                    }
                }, animate));
            }
            return me;
        },
        /**
         * Set positioning with an object returned by `getPositioning`.
         * @param {Object} pc
         * @return {Ext.dom.Element} this
         */
        setPositioning: function(pc) {
            return this.setStyle(pc);
        },
        /**
         * Changes this Element's state to "vertical" (rotated 90 or 270 degrees).
         * This involves inverting the getters and setters for height and width,
         * and applying hooks for rotating getters and setters for border/margin/padding.
         * (getWidth becomes getHeight and vice versa), setStyle and getStyle will
         * also return the inverse when height or width are being operated on.
         * 
         * @param {Number} angle the angle of rotation - either 90 or 270
         * @param {String} cls an optional css class that contains the required
         * styles for switching the element to vertical orientation. Omit this if
         * the element already contains vertical styling.  If cls is provided,
         * it will be removed from the element when {@link #setHorizontal} is called.
         * @private
         */
        setVertical: function(angle, cls) {
            var me = this,
                proto = Element.prototype;
            me.vertical = true;
            if (cls) {
                me.addCls(me.verticalCls = cls);
            }
            me.setWidth = proto.setHeight;
            me.setHeight = proto.setWidth;
            if (!Ext.isIE8) {
                // In browsers that use CSS3 transforms we must invert getHeight and
                // get Width. In IE8 no adjustment is needed because we use
                // a BasicImage filter to rotate the element and the element's
                // offsetWidth and offsetHeight are automatically inverted.
                me.getWidth = proto.getHeight;
                me.getHeight = proto.getWidth;
            }
            // Switch to using the appropriate vertical style hooks
            me.styleHooks = (angle === 270) ? proto.verticalStyleHooks270 : proto.verticalStyleHooks90;
        },
        /**
         * Set the size of this Element. If animation is true, both width and height will be
         * animated concurrently.
         * @param {Number/String} width The new width. This may be one of:
         *
         * - A Number specifying the new width in pixels.
         * - A String used to set the CSS width style. Animation may **not** be used.
         * - A size object in the format `{width: widthValue, height: heightValue}`.
         *
         * @param {Number/String} height The new height. This may be one of:
         *
         * - A Number specifying the new height in  pixels.
         * - A String used to set the CSS height style. Animation may **not** be used.
         *
         * @param {Boolean/Object} [animate] a standard Element animation config object or `true`
         * for the default animation (`{duration: 350, easing: 'ease-in'}`)
         *
         * @return {Ext.dom.Element} this
         */
        setSize: function(width, height, animate) {
            var me = this;
            if (Ext.isObject(width)) {
                // in case of object from getSize()
                animate = height;
                height = width.height;
                width = width.width;
            }
            if (!animate || !me.anim) {
                me.dom.style.width = Element.addUnits(width);
                me.dom.style.height = Element.addUnits(height);
                if (me.shadow || me.shim) {
                    me.syncUnderlays();
                }
            } else {
                if (animate === true) {
                    animate = {};
                }
                me.animate(Ext.applyIf({
                    to: {
                        width: width,
                        height: height
                    }
                }, animate));
            }
            return me;
        },
        /**
         * Sets the visibility of the element (see details). If the visibilityMode is set
         * to Element.DISPLAY, it will use the display property to hide the element,
         * otherwise it uses visibility. The default is to hide and show using the
         * visibility property.
         *
         * @param {Boolean} visible Whether the element is visible
         * @param {Boolean/Object} [animate] True for the default animation,
         * or a standard Element animation config object.
         *
         * @return {Ext.dom.Element} this
         */
        setVisible: function(visible, animate) {
            var me = this,
                dom = me.dom,
                animFly,
                visMode = getVisMode(me);
            // hideMode string override
            if (typeof animate === 'string') {
                switch (animate) {
                    case DISPLAY:
                        visMode = Element.DISPLAY;
                        break;
                    case VISIBILITY:
                        visMode = Element.VISIBILITY;
                        break;
                    case OFFSETS:
                        visMode = Element.OFFSETS;
                        break;
                    case CLIP:
                        visMode = Element.CLIP;
                        break;
                }
                me.setVisibilityMode(visMode);
                animate = false;
            }
            if (!animate || !me.anim) {
                if (visMode === Element.DISPLAY) {
                    return me.setDisplayed(visible);
                } else if (visMode === Element.OFFSETS) {
                    me[visible ? 'removeCls' : 'addCls'](OFFSETCLASS);
                } else if (visMode === Element.CLIP) {
                    me[visible ? 'removeCls' : 'addCls'](CLIPCLASS);
                } else if (visMode === Element.VISIBILITY) {
                    me.fixDisplay();
                    // Show by clearing visibility style.
                    // Explicitly setting to "visible" overrides parent visibility setting
                    dom.style.visibility = visible ? '' : HIDDEN;
                }
            } else {
                // closure for composites
                if (visible) {
                    me.setOpacity(0.01);
                    me.setVisible(true);
                }
                if (!Ext.isObject(animate)) {
                    animate = {
                        duration: 350,
                        easing: 'ease-in'
                    };
                }
                animFly = new Ext.dom.Fly();
                me.animate(Ext.applyIf({
                    callback: function() {
                        if (!visible) {
                            // Grab the dom again, since the reference may have changed
                            // if we use fly
                            animFly.attach(dom).setVisible(false).setOpacity(1);
                        }
                    },
                    to: {
                        opacity: (visible) ? 1 : 0
                    }
                }, animate));
            }
            me.getData()[ISVISIBLE] = visible;
            if (me.shadow || me.shim) {
                me.setUnderlaysVisible(visible);
            }
            return me;
        },
        /**
         * Set the width of this Element.
         * 
         *     // change the width to 200px and animate with default configuration
         *     Ext.fly('elementId').setWidth(200, true);
         *
         *     // change the width to 150px and animate with a custom configuration
         *     Ext.fly('elId').setWidth(150, {
         *         duration : 500, // animation will have a duration of .5 seconds
         *         // will change the content to "finished"
         *         callback: function(){ this.setHtml("finished"); }
         *     });
         *     
         * @param {Number/String} width The new width. This may be one of:
         *
         * - A Number specifying the new width in pixels.
         * - A String used to set the CSS width style. Animation may **not** be used.
         * 
         * @param {Boolean/Object} [animate] a standard Element animation config object or `true`
         * for the default animation (`{duration: 350, easing: 'ease-in'}`)
         * @return {Ext.dom.Element} this
         */
        setWidth: function(width, animate) {
            var me = this;
            if (!animate || !me.anim) {
                me.callParent(arguments);
            } else {
                if (!Ext.isObject(animate)) {
                    animate = {};
                }
                me.animate(Ext.applyIf({
                    to: {
                        width: width
                    }
                }, animate));
            }
            return me;
        },
        setX: function(x, animate) {
            return this.setXY([
                x,
                this.getY()
            ], animate);
        },
        setXY: function(xy, animate) {
            var me = this;
            if (!animate || !me.anim) {
                me.callParent([
                    xy
                ]);
            } else {
                if (!Ext.isObject(animate)) {
                    animate = {};
                }
                me.animate(Ext.applyIf({
                    to: {
                        x: xy[0],
                        y: xy[1]
                    }
                }, animate));
            }
            return this;
        },
        setY: function(y, animate) {
            return this.setXY([
                this.getX(),
                y
            ], animate);
        },
        /**
         * Show this element - Uses display mode to determine whether to use "display",
         * "visibility", "offsets", or "clip". See {@link #setVisible}.
         *
         * @param {Boolean/Object} [animate] true for the default animation or a standard
         * Element animation config object.
         *
         * @return {Ext.dom.Element} this
         */
        show: function(animate) {
            // hideMode override
            if (typeof animate === 'string') {
                this.setVisible(true, animate);
                return this;
            }
            this.setVisible(true, this.anim(animate));
            return this;
        },
        /**
         * Slides the element into view. An anchor point can be optionally passed to set the point
         * of origin for the slide effect. This function automatically handles wrapping the element
         * with a fixed-size container if needed. See the {@link Ext.fx.Anim} class overview
         * for valid anchor point options. Usage:
         *
         *     // default: slide the element in from the top
         *     el.slideIn();
         *
         *     // custom: slide the element in from the right with a 2-second duration
         *     el.slideIn('r', { duration: 2000 });
         *
         *     // common config options shown with default values
         *     el.slideIn('t', {
         *         easing: 'easeOut',
         *         duration: 500
         *     });
         *
         * @param {String} [anchor] One of the valid {@link Ext.fx.Anim} anchor positions
         * (defaults to top: 't')
         * @param {Object} [options] Object literal with any of the {@link Ext.fx.Anim}
         * config options
         * @param {Boolean} options.preserveScroll Set to true if preservation of any descendant
         * elements' `scrollTop` values is required. By default the DOM wrapping operation
         * performed by `slideIn` and `slideOut` causes the browser to lose all scroll positions.
         * @param {Boolean} slideOut
         * @return {Ext.dom.Element} The Element
         */
        slideIn: function(anchor, options, slideOut) {
            var me = this,
                dom = me.dom,
                elStyle = dom.style,
                animFly = new Ext.dom.Fly(),
                beforeAnim, wrapAnim, restoreScroll, wrapDomParentNode;
            anchor = anchor || "t";
            options = options || {};
            beforeAnim = function() {
                var animScope = this,
                    listeners = options.listeners,
                    box, originalStyles, anim, wrap;
                // Reattach to the DOM in case the caller animated a Fly
                // in which case the dom reference will have changed by now.
                animFly.attach(dom);
                if (!slideOut) {
                    animFly.fixDisplay();
                }
                box = animFly.getBox();
                if ((anchor === 't' || anchor === 'b') && box.height === 0) {
                    box.height = dom.scrollHeight;
                } else if ((anchor === 'l' || anchor === 'r') && box.width === 0) {
                    box.width = dom.scrollWidth;
                }
                originalStyles = animFly.getStyle([
                    'width',
                    'height',
                    'left',
                    'right',
                    'top',
                    'bottom',
                    'position',
                    'z-index'
                ], true);
                animFly.setSize(box.width, box.height);
                // Cache all descendants' scrollTop & scrollLeft values
                // if configured to preserve scroll.
                if (options.preserveScroll) {
                    restoreScroll = animFly.cacheScrollValues();
                }
                wrap = animFly.wrap({
                    role: 'presentation',
                    id: Ext.id() + '-anim-wrap-for-' + dom.id,
                    style: {
                        visibility: slideOut ? 'visible' : 'hidden'
                    }
                });
                wrapDomParentNode = wrap.dom.parentNode;
                wrap.setPositioning(animFly.getPositioning());
                if (wrap.isStyle('position', 'static')) {
                    wrap.position('relative');
                }
                animFly.clearPositioning('auto');
                wrap.clip();
                // The wrap will have reset all descendant scrollTops.
                // Restore them if we cached them.
                if (restoreScroll) {
                    restoreScroll();
                }
                // This element is temporarily positioned absolute within its wrapper.
                // Restore to its default, CSS-inherited visibility setting.
                // We cannot explicitly poke visibility:visible into its style
                // because that overrides the visibility of the wrap.
                animFly.setStyle({
                    visibility: '',
                    position: 'absolute'
                });
                if (slideOut) {
                    wrap.setSize(box.width, box.height);
                }
                switch (anchor) {
                    case 't':
                        anim = {
                            from: {
                                width: box.width + 'px',
                                height: '0px'
                            },
                            to: {
                                width: box.width + 'px',
                                height: box.height + 'px'
                            }
                        };
                        elStyle.bottom = '0px';
                        break;
                    case 'l':
                        anim = {
                            from: {
                                width: '0px',
                                height: box.height + 'px'
                            },
                            to: {
                                width: box.width + 'px',
                                height: box.height + 'px'
                            }
                        };
                        me.anchorAnimX(anchor);
                        break;
                    case 'r':
                        anim = {
                            from: {
                                x: box.x + box.width,
                                width: '0px',
                                height: box.height + 'px'
                            },
                            to: {
                                x: box.x,
                                width: box.width + 'px',
                                height: box.height + 'px'
                            }
                        };
                        me.anchorAnimX(anchor);
                        break;
                    case 'b':
                        anim = {
                            from: {
                                y: box.y + box.height,
                                width: box.width + 'px',
                                height: '0px'
                            },
                            to: {
                                y: box.y,
                                width: box.width + 'px',
                                height: box.height + 'px'
                            }
                        };
                        break;
                    case 'tl':
                        anim = {
                            from: {
                                x: box.x,
                                y: box.y,
                                width: '0px',
                                height: '0px'
                            },
                            to: {
                                width: box.width + 'px',
                                height: box.height + 'px'
                            }
                        };
                        elStyle.bottom = '0px';
                        me.anchorAnimX('l');
                        break;
                    case 'bl':
                        anim = {
                            from: {
                                y: box.y + box.height,
                                width: '0px',
                                height: '0px'
                            },
                            to: {
                                y: box.y,
                                width: box.width + 'px',
                                height: box.height + 'px'
                            }
                        };
                        me.anchorAnimX('l');
                        break;
                    case 'br':
                        anim = {
                            from: {
                                x: box.x + box.width,
                                y: box.y + box.height,
                                width: '0px',
                                height: '0px'
                            },
                            to: {
                                x: box.x,
                                y: box.y,
                                width: box.width + 'px',
                                height: box.height + 'px'
                            }
                        };
                        me.anchorAnimX('r');
                        break;
                    case 'tr':
                        anim = {
                            from: {
                                x: box.x + box.width,
                                width: '0px',
                                height: '0px'
                            },
                            to: {
                                x: box.x,
                                width: box.width + 'px',
                                height: box.height + 'px'
                            }
                        };
                        elStyle.bottom = '0px';
                        me.anchorAnimX('r');
                        break;
                }
                wrap.show();
                wrapAnim = Ext.apply({}, options);
                delete wrapAnim.listeners;
                wrapAnim = new Ext.fx.Anim(Ext.applyIf(wrapAnim, {
                    target: wrap,
                    duration: 500,
                    easing: 'ease-out',
                    from: slideOut ? anim.to : anim.from,
                    to: slideOut ? anim.from : anim.to
                }));
                // In the absence of a callback, this listener MUST be added first
                wrapAnim.on('afteranimate', function() {
                    // Reattach to the DOM in case the caller animated a Fly
                    // in which case the dom reference will have changed by now.
                    animFly.attach(dom);
                    animFly.setStyle(originalStyles);
                    if (slideOut) {
                        if (options.useDisplay) {
                            animFly.setDisplayed(false);
                        } else {
                            animFly.hide();
                        }
                    }
                    if (wrap.dom) {
                        if (wrap.dom.parentNode) {
                            wrap.dom.parentNode.insertBefore(dom, wrap.dom);
                        } else {
                            wrapDomParentNode.appendChild(dom);
                        }
                        wrap.destroy();
                    }
                    // The unwrap will have reset all descendant scrollTops.
                    // Restore them if we cached them.
                    if (restoreScroll) {
                        restoreScroll();
                    }
                    // kill the no-op element animation created below
                    animScope.end();
                });
                // Add configured listeners after
                if (listeners) {
                    wrapAnim.on(listeners);
                }
            };
            me.animate({
                // See "A Note About Wrapped Animations" at the top of this class:
                duration: options.duration ? Math.max(options.duration, 500) * 2 : 1000,
                listeners: {
                    beforeanimate: beforeAnim
                }
            });
            // kick off the wrap animation
            return me;
        },
        /**
         * Slides the element out of view. An anchor point can be optionally passed to set the end
         * point for the slide effect. When the effect is completed, the element will be hidden
         * (visibility = 'hidden') but block elements will still take up space in the document.
         * The element must be removed from the DOM using the 'remove' config option if
         * desired. This function automatically handles wrapping the element with a fixed-size
         * container if needed. See the {@link Ext.fx.Anim} class overview for valid anchor point
         * options. Usage:
         *
         *     // default: slide the element out to the top
         *     el.slideOut();
         *
         *     // custom: slide the element out to the right with a 2-second duration
         *     el.slideOut('r', { duration: 2000 });
         *
         *     // common config options shown with default values
         *     el.slideOut('t', {
         *         easing: 'easeOut',
         *         duration: 500,
         *         remove: false,
         *         useDisplay: false
         *     });
         *
         * @param {String} anchor (optional) One of the valid {@link Ext.fx.Anim} anchor positions
         * (defaults to top: 't')
         * @param {Object} options (optional) Object literal with any of the {@link Ext.fx.Anim}
         * config options
         * @return {Ext.dom.Element} The Element
         */
        slideOut: function(anchor, options) {
            return this.slideIn(anchor, options, true);
        },
        /**
         * Blinks the element as if it was clicked and then collapses on its center (similar to
         * switching off a television). When the effect is completed, the element will be hidden
         * (visibility = 'hidden') but block elements will still take up space in the document.
         * The element must be removed from the DOM using the 'remove' config option if desired.
         * Usage:
         *
         *     // default
         *     el.switchOff();
         *
         *     // all config options shown with default values
         *     el.switchOff({
         *         easing: 'easeIn',
         *         duration: .3,
         *         remove: false,
         *         useDisplay: false
         *     });
         *
         * @param {Object} options (optional) Object literal with any of the {@link Ext.fx.Anim}
         * config options
         * @return {Ext.dom.Element} The Element
         */
        switchOff: function(options) {
            var me = this,
                dom = me.dom,
                animFly = new Ext.dom.Fly(),
                beforeAnim;
            options = Ext.applyIf(options || {}, {
                easing: 'ease-in',
                duration: 500,
                remove: false,
                useDisplay: false
            });
            beforeAnim = function() {
                // Reattach to the DOM in case the caller animated a Fly
                // in which case the dom reference will have changed by now.
                animFly.attach(dom);
                // eslint-disable-next-line vars-on-top
                var animScope = this,
                    size = animFly.getSize(),
                    xy = animFly.getXY(),
                    keyframe, position;
                animFly.clearOpacity();
                animFly.clip();
                position = animFly.getPositioning();
                keyframe = new Ext.fx.Animator({
                    target: dom,
                    duration: options.duration,
                    easing: options.easing,
                    keyframes: {
                        33: {
                            opacity: 0.3
                        },
                        66: {
                            height: 1,
                            y: xy[1] + size.height / 2
                        },
                        100: {
                            width: 1,
                            x: xy[0] + size.width / 2
                        }
                    }
                });
                keyframe.on('afteranimate', function() {
                    // Reattach to the DOM in case the caller animated a Fly
                    // in which case the dom reference will have changed by now.
                    animFly.attach(dom);
                    if (options.useDisplay) {
                        animFly.setDisplayed(false);
                    } else {
                        animFly.hide();
                    }
                    animFly.clearOpacity();
                    animFly.setPositioning(position);
                    animFly.setSize(size);
                    // kill the no-op element animation created below
                    animScope.end();
                });
            };
            me.animate({
                // See "A Note About Wrapped Animations" at the top of this class:
                duration: (Math.max(options.duration, 500) * 2),
                listeners: {
                    beforeanimate: {
                        fn: beforeAnim
                    }
                },
                callback: options.callback,
                scope: options.scope
            });
            return me;
        },
        /**
         * @private
         * Currently used for updating grid cells without modifying DOM structure
         *
         * Synchronizes content of this Element with the content of the passed element.
         * 
         * Style and CSS class are copied from source into this Element, and contents are synced
         * recursively. If a child node is a text node, the textual data is copied.
         */
        syncContent: function(source) {
            source = Ext.getDom(source);
            // eslint-disable-next-line vars-on-top
            var sourceNodes = source.childNodes,
                sourceLen = sourceNodes.length,
                dest = this.dom,
                destNodes = dest.childNodes,
                destLen = destNodes.length,
                i, destNode, sourceNode, sourceStyle, nodeType, newAttrs, attLen, attName, value,
                elData = dest._extData;
            if (!syncContentFly) {
                syncContentFly = new Ext.dom.Fly();
            }
            // Update any attributes who's values have changed..
            newAttrs = source.attributes;
            attLen = newAttrs.length;
            for (i = 0; i < attLen; i++) {
                attName = newAttrs[i].name;
                value = newAttrs[i].value;
                if (attName !== 'id' && dest.getAttribute(attName) !== value) {
                    dest.setAttribute(attName, newAttrs[i].value);
                }
            }
            // The element's data is no longer synchronized. We just overwrite it in the DOM
            if (elData) {
                elData.isSynchronized = false;
            }
            // If the number of child nodes does not match, fall back to replacing innerHTML
            if (sourceLen !== destLen) {
                dest.innerHTML = source.innerHTML;
                return;
            }
            // Loop through source nodes.
            // If there are fewer, we must remove excess
            for (i = 0; i < sourceLen; i++) {
                sourceNode = sourceNodes[i];
                destNode = destNodes[i];
                nodeType = sourceNode.nodeType;
                sourceStyle = sourceNode.style;
                // If node structure is out of sync, just drop innerHTML in and return
                if (nodeType !== destNode.nodeType || (nodeType === 1 && sourceNode.tagName !== destNode.tagName)) {
                    dest.innerHTML = source.innerHTML;
                    return;
                }
                // Update non-Element node (text, comment)
                if (!sourceStyle) {
                    destNode.data = sourceNode.data;
                } else // Sync element content
                {
                    if (sourceNode.id && destNode.id !== sourceNode.id) {
                        destNode.id = sourceNode.id;
                    }
                    destNode.style.cssText = sourceStyle.cssText;
                    destNode.className = sourceNode.className;
                    syncContentFly.attach(destNode).syncContent(sourceNode);
                }
            }
        },
        /**
         * Toggles the element's visibility, depending on visibility mode.
         * @param {Boolean/Object} [animate] True for the default animation, or a standard Element
         * animation config object
         * @return {Ext.dom.Element} this
         */
        toggle: function(animate) {
            var me = this;
            me.setVisible(!me.isVisible(), me.anim(animate));
            return me;
        },
        /**
         * Hides a previously applied mask.
         */
        unmask: function() {
            var me = this,
                data = me.getData(),
                maskEl = data.maskEl,
                style;
            if (maskEl) {
                style = maskEl.dom.style;
                // Remove resource-intensive CSS expressions as soon as they are not required.
                if (style.clearExpression) {
                    style.clearExpression('width');
                    style.clearExpression('height');
                }
                if (maskEl) {
                    maskEl.destroy();
                    delete data.maskEl;
                }
                me.removeCls([
                    XMASKED,
                    XMASKEDRELATIVE
                ]);
            }
            me.restoreTabbableState(me.dom === DOC.body);
        },
        /**
         * Return clipping (overflow) to original clipping before {@link #clip} was called
         * @return {Ext.dom.Element} this
         */
        unclip: function() {
            var me = this,
                data = me.getData(),
                clip;
            if (data[ISCLIPPED]) {
                data[ISCLIPPED] = false;
                clip = data[ORIGINALCLIP];
                if (clip.o) {
                    me.setStyle(OVERFLOW, clip.o);
                }
                if (clip.x) {
                    me.setStyle(OVERFLOWX, clip.x);
                }
                if (clip.y) {
                    me.setStyle(OVERFLOWY, clip.y);
                }
            }
            return me;
        },
        translate: function(x, y, z) {
            if (Ext.supports.CssTransforms && !Ext.isIE9m) {
                this.callParent(arguments);
            } else {
                if (x != null) {
                    this.dom.style.left = x + 'px';
                }
                if (y != null) {
                    this.dom.style.top = y + 'px';
                }
            }
        },
        deprecated: {
            '4.0': {
                methods: {
                    /**
                     * @method pause
                     * Creates a pause before any subsequent queued effects begin. If there are
                     * no effects queued after the pause it will have no effect. Usage:
                     *
                     *     el.pause(1);
                     *
                     * @deprecated 4.0 Use the `delay` config to {@link #animate} instead.
                     * @param {Number} ms The length of time to pause (in milliseconds)
                     * @return {Ext.dom.Element} The Element
                     */
                    pause: function(ms) {
                        var me = this;
                        Ext.fx.Manager.setFxDefaults(me.id, {
                            delay: ms
                        });
                        return me;
                    },
                    /**
                     * @method scale
                     * Animates the transition of an element's dimensions from a starting
                     * height/width to an ending height/width. This method is a convenience
                     * implementation of {@link #shift}. Usage:
                     *
                     *     // change height and width to 100x100 pixels
                     *     el.scale(100, 100);
                     *
                     *     // common config options shown with default values.
                     *     // The height and width will default to the element's existing values
                     *     // if passed as null.
                     *     el.scale(
                     *         [element's width],
                     *         [element's height], {
                     *             easing: 'easeOut',
                     *             duration: 350
                     *         }
                     *     );
                     *
                     * @deprecated 4.0 Just use {@link #animate} instead.
                     * @param {Number} width The new width (pass undefined to keep the original
                     * width)
                     * @param {Number} height The new height (pass undefined to keep the original
                     * height)
                     * @param {Object} options (optional) Object literal with any of the
                     * {@link Ext.fx.Anim} config options
                     * @return {Ext.dom.Element} The Element
                     */
                    scale: function(width, height, options) {
                        this.animate(Ext.apply({}, options, {
                            width: width,
                            height: height
                        }));
                        return this;
                    },
                    /**
                     * @method shift
                     * Animates the transition of any combination of an element's dimensions,
                     * xy position and/or opacity. Any of these properties not specified in the
                     * config object will not be changed. This effect requires that at least one new
                     * dimension, position or opacity setting must be passed in on the config object
                     * in order for the function to have any effect. Usage:
                     *
                     *     // slide the element horizontally to x position 200
                     *     // while changing the height and opacity
                     *     el.shift({ x: 200, height: 50, opacity: .8 });
                     *
                     *     // common config options shown with default values.
                     *     el.shift({
                     *         width: [element's width],
                     *         height: [element's height],
                     *         x: [element's x position],
                     *         y: [element's y position],
                     *         opacity: [element's opacity],
                     *         easing: 'easeOut',
                     *         duration: 350
                     *     });
                     *
                     * @deprecated 4.0 Just use {@link #animate} instead.
                     * @param {Object} options Object literal with any of the {@link Ext.fx.Anim}
                     * config options
                     * @return {Ext.dom.Element} The Element
                     */
                    shift: function(options) {
                        this.animate(options);
                        return this;
                    }
                }
            },
            '4.2': {
                methods: {
                    /**
                     * @method moveTo
                     * Sets the position of the element in page coordinates.
                     * @param {Number} x X value for new position (coordinates are page-based)
                     * @param {Number} y Y value for new position (coordinates are page-based)
                     * @param {Boolean/Object} [animate] True for the default animation,
                     * or a standard Element animation config object
                     * @return {Ext.dom.Element} this
                     * @deprecated 4.2.0 Use {@link Ext.dom.Element#setXY} instead.
                     */
                    moveTo: function(x, y, animate) {
                        return this.setXY([
                            x,
                            y
                        ], animate);
                    },
                    /**
                     * @method setBounds
                     * Sets the element's position and size in one shot. If animation is true then
                     * width, height, x and y will be animated concurrently.
                     *
                     * @param {Number} x X value for new position (coordinates are page-based)
                     * @param {Number} y Y value for new position (coordinates are page-based)
                     * @param {Number/String} width The new width. This may be one of:
                     *
                     * - A Number specifying the new width in pixels
                     * - A String used to set the CSS width style. Animation may **not** be used.
                     *
                     * @param {Number/String} height The new height. This may be one of:
                     *
                     * - A Number specifying the new height in pixels
                     * - A String used to set the CSS height style. Animation may **not** be used.
                     *
                     * @param {Boolean/Object} [animate] true for the default animation or
                     * a standard Element animation config object
                     *
                     * @return {Ext.dom.Element} this
                     * @deprecated 4.2.0 Use {@link Ext.util.Positionable#setBox} instead.
                     */
                    setBounds: function(x, y, width, height, animate) {
                        return this.setBox({
                            x: x,
                            y: y,
                            width: width,
                            height: height
                        }, animate);
                    },
                    /**
                     * @method setLeftTop
                     * Sets the element's left and top positions directly using CSS style
                     * @param {Number/String} left Number of pixels or CSS string value to
                     * set as the left CSS property value
                     * @param {Number/String} top Number of pixels or CSS string value to
                     * set as the top CSS property value
                     * @return {Ext.dom.Element} this
                     * @deprecated 4.2.0 Use {@link Ext.dom.Element#setLocalXY} instead
                     */
                    setLeftTop: function(left, top) {
                        var me = this,
                            style = me.dom.style;
                        style.left = Element.addUnits(left);
                        style.top = Element.addUnits(top);
                        if (me.shadow || me.shim) {
                            me.syncUnderlays();
                        }
                        return me;
                    },
                    /**
                     * @method setLocation
                     * Sets the position of the element in page coordinates.
                     * @param {Number} x X value for new position
                     * @param {Number} y Y value for new position
                     * @param {Boolean/Object} [animate] True for the default animation,
                     * or a standard Element animation config object
                     * @return {Ext.dom.Element} this
                     * @deprecated 4.2.0 Use {@link Ext.dom.Element#setXY} instead.
                     */
                    setLocation: function(x, y, animate) {
                        return this.setXY([
                            x,
                            y
                        ], animate);
                    }
                }
            },
            '5.0': {
                methods: {
                    /**
                     * @method getAttributeNS
                     * Returns the value of a namespaced attribute from the element's underlying
                     * DOM node.
                     * @param {String} namespace The namespace in which to look for the attribute
                     * @param {String} name The attribute name
                     * @return {String} The attribute value
                     * @deprecated 5.0.0 Please use {@link Ext.dom.Element#getAttribute} instead.
                     */
                    getAttributeNS: function(namespace, name) {
                        return this.getAttribute(name, namespace);
                    },
                    /**
                     * @method getCenterXY
                     * Calculates the x, y to center this element on the screen
                     * @return {Number[]} The x, y values [x, y]
                     * @deprecated 5.0.0 Use {@link Ext.dom.Element#getAlignToXY} instead.
                     *     el.getAlignToXY(document, 'c-c');
                     */
                    getCenterXY: function() {
                        return this.getAlignToXY(DOC, 'c-c');
                    },
                    /**
                     * @method getComputedHeight
                     * Returns either the offsetHeight or the height of this element based on CSS
                     * height adjusted by padding or borders when needed to simulate offsetHeight
                     * when offsets aren't available. This may not work on display:none elements
                     * if a height has not been set using CSS.
                     * @return {Number}
                     * @deprecated 5.0.0 use {@link Ext.dom.Element#getHeight} instead
                     */
                    getComputedHeight: function() {
                        return Math.max(this.dom.offsetHeight, this.dom.clientHeight) || parseFloat(this.getStyle(HEIGHT)) || 0;
                    },
                    /**
                     * @method getComputedWidth
                     * Returns either the offsetWidth or the width of this element based on CSS
                     * width adjusted by padding or borders when needed to simulate offsetWidth
                     * when offsets aren't available. This may not work on display:none elements
                     * if a width has not been set using CSS.
                     * @return {Number}
                     * @deprecated 5.0.0 use {@link Ext.dom.Element#getWidth} instead.
                     */
                    getComputedWidth: function() {
                        return Math.max(this.dom.offsetWidth, this.dom.clientWidth) || parseFloat(this.getStyle(WIDTH)) || 0;
                    },
                    /**
                     * @method getStyleSize
                     * Returns the dimensions of the element available to lay content out in.
                     *
                     * getStyleSize utilizes prefers style sizing if present, otherwise it chooses
                     * the larger of offsetHeight/clientHeight and offsetWidth/clientWidth.
                     * To obtain the size excluding scrollbars, use getViewSize.
                     *
                     * Sizing of the document body is handled at the adapter level which handles
                     * special cases for IE and strict modes, etc.
                     *
                     * @return {Object} Object describing width and height.
                     * @return {Number} return.width
                     * @return {Number} return.height
                     * @deprecated 5.0.0 Use {@link Ext.dom.Element#getSize} instead.
                     */
                    getStyleSize: function() {
                        var me = this,
                            d = this.dom,
                            isDoc = (d === DOC || d === DOC.body),
                            s, w, h;
                        // If the body, use static methods
                        if (isDoc) {
                            return {
                                width: Element.getViewportWidth(),
                                height: Element.getViewportHeight()
                            };
                        }
                        s = me.getStyle([
                            'height',
                            'width'
                        ], true);
                        // seek inline
                        // Use Styles if they are set
                        if (s.width && s.width !== 'auto') {
                            w = parseFloat(s.width);
                        }
                        // Use Styles if they are set
                        if (s.height && s.height !== 'auto') {
                            h = parseFloat(s.height);
                        }
                        // Use getWidth/getHeight if style not set.
                        return {
                            width: w || me.getWidth(true),
                            height: h || me.getHeight(true)
                        };
                    },
                    /**
                     * @method isBorderBox
                     * Returns true if this element uses the border-box-sizing model.
                     * This method is deprecated as of version 5.0 because border-box sizing
                     * is forced upon all elements via a style sheet rule, and the browsers
                     * that do not support border-box (IE6/7 strict mode) are no longer supported.
                     * @deprecated 5.0.0 This method is deprecated.  Browsers that do not
                     * support border-box (IE6/7 strict mode) are no longer supported.
                     * @return {Boolean}
                     */
                    isBorderBox: function() {
                        return true;
                    },
                    /**
                     * @method isDisplayed
                     * Returns true if display is not "none"
                     * @return {Boolean}
                     * @deprecated 5.0.0 use element.isStyle('display', 'none');
                     */
                    isDisplayed: function() {
                        return !this.isStyle('display', 'none');
                    },
                    /**
                     * @method focusable
                     * Checks whether this element can be focused.
                     * @return {Boolean} True if the element is focusable
                     * @deprecated 5.0.0 use {@link #isFocusable} instead
                     */
                    focusable: 'isFocusable'
                }
            }
        }
    };
})(), function() {
    var Element = Ext.dom.Element,
        proto = Element.prototype,
        useDocForId = !Ext.isIE8,
        DOC = document,
        view = DOC.defaultView,
        opacityRe = /alpha\(opacity=(.*)\)/i,
        trimRe = /^\s+|\s+$/g,
        styleHooks = proto.styleHooks,
        supports = Ext.supports,
        verticalStyleHooks90, verticalStyleHooks270, edges, k, edge, borderWidth, getBorderWidth;
    proto._init(Element);
    delete proto._init;
    Ext.plainTableCls = Ext.baseCSSPrefix + 'table-plain';
    Ext.plainListCls = Ext.baseCSSPrefix + 'list-plain';
    // ensure that any methods added by this override are also added to Ext.CompositeElementLite
    if (Ext.CompositeElementLite) {
        Ext.CompositeElementLite.importElementMethods();
    }
    if (!supports.Opacity && Ext.isIE) {
        Ext.apply(styleHooks.opacity, {
            get: function(dom) {
                var filter = dom.style.filter,
                    match, opacity;
                if (filter.match) {
                    match = filter.match(opacityRe);
                    if (match) {
                        opacity = parseFloat(match[1]);
                        if (!isNaN(opacity)) {
                            return opacity ? opacity / 100 : 0;
                        }
                    }
                }
                return 1;
            },
            set: function(dom, value) {
                var style = dom.style,
                    val = style.filter.replace(opacityRe, '').replace(trimRe, '');
                style.zoom = 1;
                // ensure dom.hasLayout
                // value can be a number or '' or null... so treat falsey as no opacity
                if (typeof (value) === 'number' && value >= 0 && value < 1) {
                    value *= 100;
                    style.filter = val + (val.length ? ' ' : '') + 'alpha(opacity=' + value + ')';
                } else {
                    style.filter = val;
                }
            }
        });
    }
    if (!supports.matchesSelector) {
        // Match basic tagName.ClassName selector syntax for is implementation
        // eslint-disable-next-line vars-on-top
        var simpleSelectorRe = /^([a-z]+|\*)?(?:\.([a-z][a-z\-_0-9]*))?$/i,
            dashRe = /-/g,
            fragment,
            classMatcher = function(tag, cls) {
                var classRe = new RegExp('(?:^|\\s+)' + cls.replace(dashRe, '\\-') + '(?:\\s+|$)');
                if (tag && tag !== '*') {
                    tag = tag.toUpperCase();
                    return function(el) {
                        return el.tagName === tag && classRe.test(el.className);
                    };
                }
                return function(el) {
                    return classRe.test(el.className);
                };
            },
            tagMatcher = function(tag) {
                tag = tag.toUpperCase();
                return function(el) {
                    return el.tagName === tag;
                };
            },
            cache = {};
        proto.matcherCache = cache;
        proto.is = function(selector) {
            var dom = this.dom,
                cls, match, testFn, root, isOrphan, is, tag;
            // Empty selector always matches
            if (!selector) {
                return true;
            }
            // Only Element node types can be matched.
            if (dom.nodeType !== 1) {
                return false;
            }
            // eslint-disable-next-line no-cond-assign
            if (!(testFn = Ext.isFunction(selector) ? selector : cache[selector])) {
                // eslint-disable-next-line no-cond-assign
                if (!(match = selector.match(simpleSelectorRe))) {
                    // Not a simple tagName.className selector, do it the hard way
                    root = dom.parentNode;
                    if (!root) {
                        isOrphan = true;
                        root = fragment || (fragment = DOC.createDocumentFragment());
                        fragment.appendChild(dom);
                    }
                    is = Ext.Array.indexOf(Ext.fly(root, '_is').query(selector), dom) !== -1;
                    if (isOrphan) {
                        fragment.removeChild(dom);
                    }
                    return is;
                }
                tag = match[1];
                cls = match[2];
                cache[selector] = testFn = cls ? classMatcher(tag, cls) : tagMatcher(tag);
            }
            return testFn(dom);
        };
    }
    // IE8 needs its own implementation of getStyle because it doesn't support getComputedStyle
    if (!view || !view.getComputedStyle) {
        proto.getStyle = function(property, inline) {
            var me = this,
                dom = me.dom,
                multiple = typeof property !== 'string',
                prop = property,
                props = prop,
                len = 1,
                isInline = inline,
                styleHooks = me.styleHooks,
                camel, domStyle, values, hook, out, style, i;
            if (multiple) {
                values = {};
                prop = props[0];
                i = 0;
                if (!(len = props.length)) {
                    return values;
                }
            }
            if (!dom || dom.documentElement) {
                return values || '';
            }
            domStyle = dom.style;
            if (inline) {
                style = domStyle;
            } else {
                style = dom.currentStyle;
                // fallback to inline style if rendering context not available
                if (!style) {
                    isInline = true;
                    style = domStyle;
                }
            }
            do {
                hook = styleHooks[prop];
                if (!hook) {
                    styleHooks[prop] = hook = {
                        name: Element.normalize(prop)
                    };
                }
                if (hook.get) {
                    out = hook.get(dom, me, isInline, style);
                } else {
                    camel = hook.name;
                    out = style[camel];
                }
                if (!multiple) {
                    return out;
                }
                values[prop] = out;
                prop = props[++i];
            } while (i < len);
            return values;
        };
    }
    // override getStyle for border-*-width
    if (Ext.isIE8) {
        getBorderWidth = function(dom, el, inline, style) {
            if (style[this.styleName] === 'none') {
                return '0px';
            }
            return style[this.name];
        };
        edges = [
            'Top',
            'Right',
            'Bottom',
            'Left'
        ];
        k = edges.length;
        while (k--) {
            edge = edges[k];
            borderWidth = 'border' + edge + 'Width';
            styleHooks['border-' + edge.toLowerCase() + '-width'] = styleHooks[borderWidth] = {
                name: borderWidth,
                styleName: 'border' + edge + 'Style',
                get: getBorderWidth
            };
        }
        // IE8 has an odd bug with handling font icons in pseudo elements;
        // it will render the icon once and not update it when something
        // like text color is changed via style addition or removal.
        // We have to force icon repaint by adding a style with forced empty
        // pseudo element content, (x-sync-repaint) and removing it back to work
        // around this issue.
        // See this: https://github.com/FortAwesome/Font-Awesome/issues/954
        // and this: https://github.com/twbs/bootstrap/issues/13863
        // eslint-disable-next-line vars-on-top
        var syncRepaintCls = Ext.baseCSSPrefix + 'sync-repaint';
        proto.syncRepaint = function() {
            this.addCls(syncRepaintCls);
            // Measuring element width will make the browser to repaint it
            this.getWidth();
            // Removing empty content makes the icon to appear again and be redrawn
            this.removeCls(syncRepaintCls);
        };
    }
    if (Ext.isIE10m) {
        Ext.override(Element, {
            focus: function(defer, dom) {
                var me = this,
                    ex;
                dom = dom || me.dom;
                if (me.deferredFocusTimer) {
                    Ext.undefer(me.deferredFocusTimer);
                }
                me.deferredFocusTimer = null;
                if (Number(defer)) {
                    me.deferredFocusTimer = Ext.defer(me.focus, defer, me, [
                        null,
                        dom
                    ]);
                } else {
                    Ext.GlobalEvents.fireEvent('beforefocus', dom);
                    // IE10m has an acute problem with focusing input elements;
                    // when the element was just shown and did not have enough
                    // time to initialize, focusing it might fail. The problem
                    // is somewhat random in nature; most of the time focusing
                    // an input element will succeed, failing only occasionally.
                    // When it fails, the focus will be thrown to the document
                    // body element, with subsequent focusout/focusin event pair
                    // on the body, which throws off our focusenter/focusleave
                    // processing.
                    // Fortunately for us, when this focus failure happens, the
                    // resulting focusout event will happen *synchronously*
                    // unlike the normal focusing events which IE will fire
                    // asynchronously. Also fortunately for us, in most cases
                    // trying to focus the given element the second time
                    // immediately after it failed to focus the first time
                    // seems to do the trick; however when second focus attempt
                    // succeeds, it will result in focusout on the body and
                    // focusin on the given element, which again wreaks havoc
                    // on our focusenter/focusleave handling.
                    // The only workable solution we have is to pretend that
                    // focus never went to the document body and ignore the
                    // focusout and focusin caused by failed first focus attempt.
                    // To this end, we fudge the event stream in Focus publisher
                    // override.
                    if (dom && (dom.tagName === 'INPUT' || dom.tagname === 'TEXTAREA')) {
                        Ext.synchronouslyFocusing = document.activeElement;
                    }
                    // Also note that trying to focus an unfocusable element
                    // might throw an exception in IE8. What a cute idea, MS. :(
                    try {
                        dom.focus();
                    } catch (xcpt) {
                        ex = xcpt;
                    }
                    // Ok so now we have this situation when we tried to focus
                    // the first time but did not succeed. Let's try again but
                    // not if there was an exception the first time - when the
                    // "focus failure" happens it does so silently. :(
                    if (Ext.synchronouslyFocusing && document.activeElement !== dom && !ex) {
                        dom.focus();
                    }
                    Ext.synchronouslyFocusing = null;
                }
                return me;
            }
        });
    }
    Ext.apply(Ext, {
        /**
         * `true` to automatically uncache orphaned Ext.Elements periodically. If set to
         * `false`, the application will be required to clean up orphaned Ext.Elements and
         * it's listeners as to not cause memory leakage.
         * @member Ext
         */
        enableGarbageCollector: true,
        // In sencha v5 isBorderBox is no longer needed since all supported browsers
        // support border-box, but it is hard coded to true for backward compatibility
        isBorderBox: true,
        /**
         * @property {Boolean} useShims
         * @member Ext
         * Set to `true` to use a {@link Ext.util.Floating#shim shim} on all floating Components
         * and {@link Ext.LoadMask LoadMasks}
         */
        useShims: false,
        getElementById: function(id) {
            var el = DOC.getElementById(id),
                detachedBodyEl;
            if (!el && (detachedBodyEl = Ext.detachedBodyEl)) {
                el = detachedBodyEl.dom.querySelector(Ext.makeIdSelector(id));
            }
            return el;
        },
        /**
         * Applies event listeners to elements by selectors when the document is ready.
         * The event name is specified with an `@` suffix.
         *
         *     Ext.addBehaviors({
         *         // add a listener for click on all anchors in element with id foo
         *         '#foo a@click': function(e, t){
         *             // do something
         *         },
         *
         *         // add the same listener to multiple selectors (separated by comma BEFORE the @)
         *         '#foo a, #bar span.some-class@mouseover': function(){
         *             // do something
         *         }
         *     });
         *
         * @param {Object} obj The list of behaviors to apply
         * @member Ext
         */
        addBehaviors: function(obj) {
            // simple cache for applying multiple behaviors to same selector
            // does query multiple times
            var cache = {},
                parts, b, s;
            if (!Ext.isReady) {
                Ext.onInternalReady(function() {
                    Ext.addBehaviors(obj);
                });
            } else {
                for (b in obj) {
                    if ((parts = b.split('@'))[1]) {
                        // for Object prototype breakers
                        s = parts[0];
                        if (!cache[s]) {
                            cache[s] = Ext.fly(document).select(s, true);
                        }
                        cache[s].on(parts[1], obj[b]);
                    }
                }
                cache = null;
            }
        }
    });
    if (Ext.isIE9m) {
        Ext.getElementById = function(id) {
            var el = DOC.getElementById(id),
                detachedBodyEl;
            if (!el && (detachedBodyEl = Ext.detachedBodyEl)) {
                el = detachedBodyEl.dom.all[id];
            }
            return el;
        };
        proto.getById = function(id, asDom) {
            var dom = this.dom,
                ret = null,
                entry, el;
            if (dom) {
                // for normal elements getElementById is the best solution, but if the el is
                // not part of the document.body, we need to use all[]
                el = (useDocForId && DOC.getElementById(id)) || dom.all[id];
                if (el) {
                    if (asDom) {
                        ret = el;
                    } else {
                        // calling Element.get here is a real hit (2x slower) because it has to
                        // redetermine that we are giving it a dom el.
                        entry = Ext.cache[id];
                        if (entry) {
                            if (entry.skipGarbageCollection || !Ext.isGarbage(entry.dom)) {
                                ret = entry;
                            } else {
                                Ext.raise("Stale Element with id '" + el.id + "' found in Element cache. " + "Make sure to clean up Element instances using destroy()");
                                entry.destroy();
                            }
                        }
                        ret = ret || new Ext.Element(el);
                    }
                }
            }
            return ret;
        };
    } else if (!DOC.querySelector) {
        Ext.getDetachedBody = Ext.getBody;
        Ext.getElementById = function(id) {
            return DOC.getElementById(id);
        };
        proto.getById = function(id, asDom) {
            var dom = DOC.getElementById(id);
            return asDom ? dom : (dom ? Ext.get(dom) : null);
        };
    }
    if (Ext.isIE && !(Ext.isIE9p && DOC.documentMode >= 9)) {
        // Essentially all web browsers (Firefox, Internet Explorer, recent versions of Opera,
        // Safari, Konqueror, and iCab, as a non-exhaustive list) return null when the specified
        // attribute does not exist on the specified element.
        // The DOM specification says that the correct return value in this case is actually
        // the empty string, and some DOM implementations implement this behavior.
        // The implementation of getAttribute in XUL (Gecko) actually follows the specification
        // and returns an empty string. Consequently, you should use hasAttribute to check
        // for an attribute's existence prior to calling getAttribute() if it is possible that
        // the requested attribute does not exist on the specified element.
        //
        // https://developer.mozilla.org/en-US/docs/DOM/element.getAttribute
        // http://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-745549614
        proto.getAttribute = function(name, ns) {
            var d = this.dom,
                type;
            if (ns) {
                type = typeof d[ns + ":" + name];
                if (type !== 'undefined' && type !== 'unknown') {
                    return d[ns + ":" + name] || null;
                }
                return null;
            }
            if (name === "for") {
                name = "htmlFor";
            }
            return d[name] || null;
        };
    }
    Ext.onInternalReady(function() {
        var transparentRe = /^(?:transparent|(?:rgba[(](?:\s*\d+\s*[,]){3}\s*0\s*[)]))$/i,
            origSetWidth = proto.setWidth,
            origSetHeight = proto.setHeight,
            origSetSize = proto.setSize,
            origUnselectable = proto.unselectable,
            pxRe = /^\d+(?:\.\d*)?px$/i,
            colorStyles, i, name, camel;
        if (supports.FixedTableWidthBug) {
            // EXTJSIV-12665
            // https://bugs.webkit.org/show_bug.cgi?id=130239
            // Webkit browsers fail to layout correctly when a form field's width is less
            // than the min-width of the body element.  The only way to fix it seems to be
            // to toggle the display style of the field's element before and after setting
            // the width. Note: once the bug has been corrected by toggling the element's
            // display, successive calls to setWidth will work without the hack.  It's only
            // when going from naturally widthed to having an explicit width that the bug
            // occurs.
            styleHooks.width = {
                name: 'width',
                set: function(dom, value, el) {
                    var style = dom.style,
                        needsFix = el._needsTableWidthFix,
                        origDisplay = style.display;
                    if (needsFix) {
                        style.display = 'none';
                    }
                    style.width = value;
                    if (needsFix) {
                        // repaint
                        // eslint-disable-next-line no-unused-expressions
                        dom.scrollWidth;
                        style.display = origDisplay;
                    }
                }
            };
            proto.setWidth = function(width, animate) {
                var me = this,
                    dom = me.dom,
                    style = dom.style,
                    needsFix = me._needsTableWidthFix,
                    origDisplay = style.display;
                if (needsFix && !animate) {
                    style.display = 'none';
                }
                origSetWidth.call(me, width, animate);
                if (needsFix && !animate) {
                    // repaint
                    // eslint-disable-next-line no-unused-expressions
                    dom.scrollWidth;
                    style.display = origDisplay;
                }
                return me;
            };
            proto.setSize = function(width, height, animate) {
                var me = this,
                    dom = me.dom,
                    style = dom.style,
                    needsFix = me._needsTableWidthFix,
                    origDisplay = style.display;
                if (needsFix && !animate) {
                    style.display = 'none';
                }
                origSetSize.call(me, width, height, animate);
                if (needsFix && !animate) {
                    // repaint
                    // eslint-disable-next-line no-unused-expressions
                    dom.scrollWidth;
                    style.display = origDisplay;
                }
                return me;
            };
        }
        if (Ext.isIE8) {
            styleHooks.height = {
                name: 'height',
                set: function(dom, value, el) {
                    var component = el.component,
                        frameInfo, frameBodyStyle;
                    if (component && component._syncFrameHeight && el === component.el) {
                        frameBodyStyle = component.frameBody.dom.style;
                        if (pxRe.test(value)) {
                            frameInfo = component.getFrameInfo();
                            if (frameInfo) {
                                frameBodyStyle.height = (parseInt(value, 10) - frameInfo.height) + 'px';
                            }
                        } else if (!value || value === 'auto') {
                            frameBodyStyle.height = '';
                        }
                    }
                    dom.style.height = value;
                }
            };
            proto.setHeight = function(height, animate) {
                var component = this.component,
                    frameInfo, frameBodyStyle;
                if (component && component._syncFrameHeight && this === component.el) {
                    frameBodyStyle = component.frameBody.dom.style;
                    if (!height || height === 'auto') {
                        frameBodyStyle.height = '';
                    } else {
                        frameInfo = component.getFrameInfo();
                        if (frameInfo) {
                            frameBodyStyle.height = (height - frameInfo.height) + 'px';
                        }
                    }
                }
                return origSetHeight.call(this, height, animate);
            };
            proto.setSize = function(width, height, animate) {
                var component = this.component,
                    frameInfo, frameBodyStyle;
                if (component && component._syncFrameHeight && this === component.el) {
                    frameBodyStyle = component.frameBody.dom.style;
                    if (!height || height === 'auto') {
                        frameBodyStyle.height = '';
                    } else {
                        frameInfo = component.getFrameInfo();
                        if (frameInfo) {
                            frameBodyStyle.height = (height - frameInfo.height) + 'px';
                        }
                    }
                }
                return origSetSize.call(this, width, height, animate);
            };
            // Override for IE8 which throws an error setting innerHTML when inside
            // an event handler invoked from that element.
            proto.setText = function(text) {
                var dom = this.dom;
                // Remove all child nodes, leave only a single textNode
                if (!(dom.childNodes.length === 1 && dom.firstChild.nodeType === 3)) {
                    while (dom.lastChild && dom.lastChild.nodeType !== 3) {
                        dom.removeChild(dom.lastChild);
                    }
                    dom.appendChild(document.createTextNode());
                }
                // Set the data of the textNode
                dom.firstChild.data = text;
            };
            proto.unselectable = function() {
                origUnselectable.call(this);
                this.dom.onselectstart = function() {
                    return false;
                };
            };
        }
        function fixTransparent(dom, el, inline, style) {
            var value = style[this.name] || '';
            return transparentRe.test(value) ? 'transparent' : value;
        }
        /*
         * Helper function to create the function that will restore the selection.
         */
        function makeSelectionRestoreFn(activeEl, start, end) {
            return function() {
                activeEl.selectionStart = start;
                activeEl.selectionEnd = end;
            };
        }
        /*
         * Creates a function to call to clean up problems with the work-around for the
         * WebKit RightMargin bug. The work-around is to add "display: 'inline-block'" to
         * the element before calling getComputedStyle and then to restore its original
         * display value. The problem with this is that it corrupts the selection of an
         * INPUT or TEXTAREA element (as in the "I-beam" goes away but the focus remains).
         * To cleanup after this, we need to capture the selection of any such element and
         * then restore it after we have restored the display style.
         *
         * @param {HTMLElement} target The top-most element being adjusted.
         * @private
         */
        function getRightMarginFixCleaner(target) {
            var hasInputBug = supports.DisplayChangeInputSelectionBug,
                hasTextAreaBug = supports.DisplayChangeTextAreaSelectionBug,
                activeEl, tag, start, end;
            if (hasInputBug || hasTextAreaBug) {
                activeEl = Element.getActiveElement();
                tag = activeEl && activeEl.tagName;
                if ((hasTextAreaBug && tag === 'TEXTAREA') || (hasInputBug && tag === 'INPUT' && activeEl.type === 'text')) {
                    if (Ext.fly(target).isAncestor(activeEl)) {
                        start = activeEl.selectionStart;
                        end = activeEl.selectionEnd;
                        if (Ext.isNumber(start) && Ext.isNumber(end)) {
                            // to be safe...
                            // We don't create the raw closure here inline because that
                            // will be costly even if we don't want to return it (nested
                            // function decls and exprs are often instantiated on entry
                            // regardless of whether execution ever reaches them):
                            return makeSelectionRestoreFn(activeEl, start, end);
                        }
                    }
                }
            }
            return Ext.emptyFn;
        }
        // avoid special cases, just return a nop
        function fixRightMargin(dom, el, inline, style) {
            var result = style.marginRight,
                domStyle, display;
            // Ignore cases when the margin is correctly reported as 0, the bug only shows
            // numbers larger.
            if (result !== '0px') {
                domStyle = dom.style;
                display = domStyle.display;
                domStyle.display = 'inline-block';
                // eslint-disable-next-line max-len
                result = (inline ? style : dom.ownerDocument.defaultView.getComputedStyle(dom, null)).marginRight;
                domStyle.display = display;
            }
            return result;
        }
        function fixRightMarginAndInputFocus(dom, el, inline, style) {
            var result = style.marginRight,
                domStyle, cleaner, display;
            if (result !== '0px') {
                domStyle = dom.style;
                cleaner = getRightMarginFixCleaner(dom);
                display = domStyle.display;
                domStyle.display = 'inline-block';
                // eslint-disable-next-line max-len
                result = (inline ? style : dom.ownerDocument.defaultView.getComputedStyle(dom, '')).marginRight;
                domStyle.display = display;
                cleaner();
            }
            return result;
        }
        // TODO - this was fixed in Safari 3 - verify if this is still an issue
        // Fix bug caused by this: https://bugs.webkit.org/show_bug.cgi?id=13343
        if (!supports.RightMargin) {
            styleHooks.marginRight = styleHooks['margin-right'] = {
                name: 'marginRight',
                // TODO - Touch should use conditional compilation here or ensure that the
                //      underlying Ext.supports flags are set correctly...
                // eslint-disable-next-line max-len
                get: (supports.DisplayChangeInputSelectionBug || supports.DisplayChangeTextAreaSelectionBug) ? fixRightMarginAndInputFocus : fixRightMargin
            };
        }
        if (!supports.TransparentColor) {
            colorStyles = [
                'background-color',
                'border-color',
                'color',
                'outline-color'
            ];
            for (i = colorStyles.length; i--; ) {
                name = colorStyles[i];
                camel = Element.normalize(name);
                styleHooks[name] = styleHooks[camel] = {
                    name: camel,
                    get: fixTransparent
                };
            }
        }
        // When elements are rotated 80 or 270 degrees, their border, margin and padding hooks
        // need to be rotated as well.
        proto.verticalStyleHooks90 = verticalStyleHooks90 = Ext.Object.chain(styleHooks);
        proto.verticalStyleHooks270 = verticalStyleHooks270 = Ext.Object.chain(styleHooks);
        verticalStyleHooks90.width = styleHooks.height || {
            name: 'height'
        };
        verticalStyleHooks90.height = styleHooks.width || {
            name: 'width'
        };
        verticalStyleHooks90['margin-top'] = {
            name: 'marginLeft'
        };
        verticalStyleHooks90['margin-right'] = {
            name: 'marginTop'
        };
        verticalStyleHooks90['margin-bottom'] = {
            name: 'marginRight'
        };
        verticalStyleHooks90['margin-left'] = {
            name: 'marginBottom'
        };
        verticalStyleHooks90['padding-top'] = {
            name: 'paddingLeft'
        };
        verticalStyleHooks90['padding-right'] = {
            name: 'paddingTop'
        };
        verticalStyleHooks90['padding-bottom'] = {
            name: 'paddingRight'
        };
        verticalStyleHooks90['padding-left'] = {
            name: 'paddingBottom'
        };
        verticalStyleHooks90['border-top'] = {
            name: 'borderLeft'
        };
        verticalStyleHooks90['border-right'] = {
            name: 'borderTop'
        };
        verticalStyleHooks90['border-bottom'] = {
            name: 'borderRight'
        };
        verticalStyleHooks90['border-left'] = {
            name: 'borderBottom'
        };
        verticalStyleHooks270.width = styleHooks.height || {
            name: 'height'
        };
        verticalStyleHooks270.height = styleHooks.width || {
            name: 'width'
        };
        verticalStyleHooks270['margin-top'] = {
            name: 'marginRight'
        };
        verticalStyleHooks270['margin-right'] = {
            name: 'marginBottom'
        };
        verticalStyleHooks270['margin-bottom'] = {
            name: 'marginLeft'
        };
        verticalStyleHooks270['margin-left'] = {
            name: 'marginTop'
        };
        verticalStyleHooks270['padding-top'] = {
            name: 'paddingRight'
        };
        verticalStyleHooks270['padding-right'] = {
            name: 'paddingBottom'
        };
        verticalStyleHooks270['padding-bottom'] = {
            name: 'paddingLeft'
        };
        verticalStyleHooks270['padding-left'] = {
            name: 'paddingTop'
        };
        verticalStyleHooks270['border-top'] = {
            name: 'borderRight'
        };
        verticalStyleHooks270['border-right'] = {
            name: 'borderBottom'
        };
        verticalStyleHooks270['border-bottom'] = {
            name: 'borderLeft'
        };
        verticalStyleHooks270['border-left'] = {
            name: 'borderTop'
        };
        /**
         * @property {Boolean} scopeCss
         * @member Ext
         * Set this to true before onReady to prevent any styling from being added to
         * the body element.  By default a few styles such as font-family, and color
         * are added to the body element via a "x-body" class.  When this is set to
         * `true` the "x-body" class is not added to the body element, but is added
         * to the elements of root-level containers instead.
         */
        if (!Ext.scopeCss) {
            Ext.getBody().addCls(Ext.baseCSSPrefix + 'body');
        }
    }, null, {
        priority: 1500
    });
});
// onReady

/**
 * An `{@link Ext.mixin.Observable Observable}` through which Ext fires global events.
 * 
 * Ext.on() and Ext.un() are shorthand for {@link #addListener} and {@link #removeListener}
 * on this Observable.  For example, to listen for the idle event:
 * 
 *     Ext.on('idle', function() {
 *         // do something
 *     });
 */
Ext.define('Ext.GlobalEvents', {
    extend: 'Ext.mixin.Observable',
    alternateClassName: 'Ext.globalEvents',
    // for compat with Ext JS 4.2 and earlier
    requires: [
        'Ext.dom.Element'
    ],
    observableType: 'global',
    singleton: true,
    /**
     * @private
     */
    resizeBuffer: 100,
    /**
     * @event added
     * Fires when a Component is added to a Container.
     * @param {Ext.Component} component
     */
    /**
     * @event beforeresponsiveupdate
     * Fires before {@link Ext.mixin.Responsive} perform any updates in response to
     * dynamic changes. This is prior to refreshing `responsiveFormulas`.
     * @param {Object} context The context object used by `responsiveConfig` expressions.
     * @since 5.0.1
     */
    /**
     * @event beginresponsiveupdate
     * Fires when {@link Ext.mixin.Responsive} is about to perform updates in response to
     * dynamic changes. At this point all `responsiveFormulas` have been refreshed.
     * @param {Object} context The context object used by `responsiveConfig` expressions.
     * @since 5.0.1
     */
    /**
     * @event responsiveupdate
     * Fires after {@link Ext.mixin.Responsive} has performed updates in response to
     * dynamic changes.
     * @param {Object} context The context object used by `responsiveConfig` expressions.
     * @since 5.0.1
     */
    /**
     * @event collapse
     * Fires when a Component is collapsed (e.g., a panel).
     * @param {Ext.Component} component
     */
    /**
     * @event expand
     * Fires when a Component is expanded (e.g., a panel).
     * @param {Ext.Component} component
     */
    /**
     * @event hide
     * Fires when a Component is hidden.
     * @param {Ext.Component} component
     */
    /**
     * @event idle
     * Fires when an event handler finishes its run, just before returning to
     * browser control.
     * 
     * This includes DOM event handlers, Ajax (including JSONP) event handlers,
     * and {@link Ext.util.TaskRunner TaskRunners}
     * 
     * When called at the tail of a DOM event, the event object is passed as the
     * sole parameter.
     * 
     * This can be useful for performing cleanup, or update tasks which need to
     * happen only after all code in an event handler has been run, but which
     * should not be executed in a timer due to the intervening browser
     * reflow/repaint which would take place.
     */
    /**
     * @event onlinechange
     * Fires when the online status of the page changes. See {@link Ext#method-isOnline}
     * @param {Boolean} online `true` if in an online state.
     *
     * @since 6.2.1
     */
    /**
     * @event removed
     * Fires when a Component is removed from a Container.
     * @param {Ext.Component} component
     */
    /**
     * @event resize
     * Fires when the browser window is resized.  To avoid running resize handlers
     * too often resulting in sluggish window resizing, the resize event uses a buffer
     * of 100 milliseconds in the Classic toolkit, and fires on animation frame
     * in the Modern toolkit.
     * @param {Number} width The new width
     * @param {Number} height The new height
     */
    /**
     * @event show
     * Fires when a Component is shown.
     * @param {Ext.Component} component
     */
    /**
     * @event beforebindnotify
     * Fires before a scheduled set of bindings are fired. This allows interested parties
     * to react and do any required work.
     *
     * @private
     * @since 5.1.0
     */
    /**
     * @event mousedown
     * A mousedown listener on the document that is immune to stopPropagation()
     * used in cases where we need to know if a mousedown event occurred on the
     * document regardless of whether some other handler tried to stop it.  An
     * example where this is useful is a menu that needs to be hidden whenever
     * there is a mousedown event on the document.
     * @param {Ext.event.Event} e The event object
     */
    /**
     * @event mouseup
     * A mouseup listener on the document that is immune to stopPropagation()
     * used in cases where we need to know if a mouseup event occurred on the
     * document regardless of whether some other handler tried to stop it.  An
     * example where this is useful is a component which enters a "pressed" state
     * upon mousedown, and needs to exit that state even if the mouse exits
     * before being released.
     * @param {Ext.event.Event} e The event object
     */
    /**
     * @property {Object} idleEventMask
     * This object holds event names for events that should not trigger an `idle` event
     * following their dispatch.
     * @private
     * @since 5.0.0
     */
    idleEventMask: {
        mousemove: 1,
        touchmove: 1,
        pointermove: 1,
        MSPointerMove: 1,
        unload: 1
    },
    // @private
    windowListeners: {
        resize: {
            fn: 'fireResize'
        }
    },
    constructor: function() {
        var me = this;
        me.callParent();
        Ext.onInternalReady(function() {
            // using a closure here instead of attaching the event directly to the
            // attachListeners method gives us a chance to override the method
            me.attachListeners();
        });
    },
    setPressedComponent: function(component, e) {
        var me = this,
            pressedComponent = me.pressedComponent;
        if (pressedComponent && pressedComponent.onRelease) {
            pressedComponent.onRelease(e);
        }
        me.pressedComponent = component;
        if (component) {
            me.pressedScrollStart = Ext.on({
                scrollstart: function() {
                    me.setPressedComponent(null, e);
                },
                destroyable: true
            });
        } else {
            me.pressedScrollStart = Ext.destroy(me.pressedScrollStart);
        }
    },
    attachListeners: function() {
        var me = this,
            win = Ext.getWin(),
            winListeners = me.windowListeners;
        me.onlineState = Ext.isOnline();
        // Capture width/height to compare later in fireResize
        me.curHeight = Ext.Element.getViewportHeight();
        me.curWidth = Ext.Element.getViewportWidth();
        win.on({
            scope: me,
            online: 'handleOnlineChange',
            offline: 'handleOnlineChange'
        });
        // This function is not entirely harmless but since it is not directly related
        // to any component, element, or other destructible entity and effectively cannot
        // be cleaned up, we have to pretend it's OK for this timer to go unnoticed.
        me.fireResize.$skipTimerCheck = true;
        // IE8 does its own thing
        if (winListeners) {
            winListeners.scope = me;
            // CSS layouts only require buffering to the next animation frame
            if (Ext.isModern) {
                winListeners.resize.onFrame = true;
            } else {
                winListeners.resize.buffer = me.resizeBuffer;
            }
            win.on(winListeners);
        }
        Ext.getDoc().on({
            touchstart: 'fireMouseDown',
            mousedown: 'fireMouseDown',
            mouseup: 'fireMouseUp',
            touchend: 'fireMouseUp',
            drop: 'fireMouseUp',
            dragend: 'fireMouseUp',
            scope: me
        });
    },
    fireMouseDown: function(e) {
        this.fireEvent('mousedown', e);
        // Synchronize floated component ordering.
        // Note that this is an ASAP method and will complete asynchronously
        // after this event has finished.
        Ext.ComponentManager.handleDocumentMouseDown(e);
    },
    fireMouseUp: function(e) {
        this.fireEvent('mouseup', e);
        this.setPressedComponent(null, e);
    },
    fireResize: function() {
        var me = this,
            Element = Ext.Element,
            w = Element.getViewportWidth(),
            h = Element.getViewportHeight();
        // In IE the resize event will sometimes fire even though the w/h are the same.
        if (me.curHeight !== h || me.curWidth !== w) {
            me.curHeight = h;
            me.curWidth = w;
            if (me.hasListeners.resize) {
                me.fireEvent('resize', w, h);
            }
        }
    },
    handleOnlineChange: function() {
        var online = Ext.isOnline();
        if (online !== this.onlineState) {
            this.onlineState = online;
            this.fireEvent('onlinechange', online);
        }
    }
}, function(GlobalEvents) {
    Ext.hasListeners = GlobalEvents.hasListeners;
    /**
     * @member Ext
     * @method on
     * Shorthand for {@link Ext.GlobalEvents#addListener}.
     * @inheritdoc Ext.mixin.Observable#addListener
     */
    Ext.on = function() {
        return GlobalEvents.addListener.apply(GlobalEvents, arguments);
    };
    /**
     * @member Ext
     * @method un
     * Shorthand for {@link Ext.GlobalEvents#removeListener}.
     * @inheritdoc Ext.mixin.Observable#removeListener
     */
    Ext.un = function() {
        return GlobalEvents.removeListener.apply(GlobalEvents, arguments);
    };
    /**
     * @member Ext
     * @method fireEvent
     * Shorthand for {@link Ext.GlobalEvents#fireEvent}.
     * @inheritdoc Ext.mixin.Observable#fireEvent
     *
     * @since 6.2.0
     */
    Ext.fireEvent = function() {
        return GlobalEvents.fireEvent.apply(GlobalEvents, arguments);
    };
    /**
     * @member Ext
     * @method fireIdle
     * Fires the global `idle` event if there are any listeners registered.
     *
     * @since 6.5.1
     * @private
     */
    Ext.fireIdle = function() {
        if (GlobalEvents.hasListeners.idle && !Ext._suppressIdle) {
            GlobalEvents.fireEventArgs('idle');
        }
        Ext._suppressIdle = false;
    };
    Ext._suppressIdle = false;
});

// @tag core
/**
 * @class Ext.GlobalEvents
 */
Ext.define('Ext.overrides.GlobalEvents', {
    override: 'Ext.GlobalEvents',
    /**
     * @event resumelayouts
     * Fires after global layout processing has been resumed in {@link
     * Ext.Component#resumeLayouts}.
     */
    attachListeners: function() {
        var me = this,
            docElement, bufferedFn;
        // In IE9- when using legacy onresize event via attachEvent or onresize property,
        // the event may fire for *content size changes* as well as actual document view
        // size changes. See this: https://msdn.microsoft.com/en-us/library/ms536959(v=vs.85).aspx
        // and this: http://stackoverflow.com/questions/1852751/window-resize-event-firing-in-internet-explorer
        // The amount of these events firing all at once can be entirely staggering, and they
        // often happen during layouts so we have to be über careful to execute as few JavaScript
        // statements as possible to improve overall framework performance.
        if (Ext.isIE8) {
            docElement = Ext.getDoc().dom.documentElement;
            bufferedFn = Ext.Function.createBuffered(me.fireResize, me.resizeBuffer, me);
            Ext.getWin().dom.attachEvent('onresize', function() {
                if (docElement.clientWidth !== Ext.GlobalEvents.curWidth || docElement.clientHeight !== Ext.GlobalEvents.curHeight) {
                    bufferedFn();
                }
            });
        }
        me.callParent();
    },
    deprecated: {
        5: {
            methods: {
                addListener: function(ename, fn, scope, options, order, caller, eventOptions) {
                    var name, readyFn;
                    // The "ready" event was removed from Ext.globalEvents in 5.0 in favor of
                    // Ext.onReady().  This function adds compatibility for the ready event
                    if (ename === 'ready') {
                        readyFn = fn;
                    } else if (typeof ename !== 'string') {
                        for (name in ename) {
                            if (name === 'ready') {
                                readyFn = ename[name];
                            }
                        }
                    }
                    if (readyFn) {
                        Ext.log.warn("Ext.on('ready', fn) is deprecated.  " + "Please use Ext.onReady(fn) instead.");
                        Ext.onReady(readyFn);
                    }
                    this.callParent([
                        ename,
                        fn,
                        scope,
                        options,
                        order,
                        caller,
                        eventOptions
                    ]);
                }
            }
        }
    }
});

/**
 * @class Ext.Glyph
 * @private
 * A class which parses a `glyph` config and provides ways of creating the relevant DOM or yielding
 * information about the selected codepoint and font.
 */
Ext.define('Ext.Glyph', {
    /**
     * @property {Boolean} isGlyph
     * `true` in this class to identify an object as an instantiated Glyph, or subclass thereof.
     */
    isGlyph: true,
    /**
     * @property {Number} codepoint
     * The unicode codepoint of the configured glyph.
     */
    /**
     * @property {String} character
     * A single character string representing the selected glyph. This may safely
     * be injected directly into HTML.
     */
    /**
     * @property {String} fontFamily
     * The name of the font family configured. If none was configured, it uses the library default.
     * The default font-family  for glyphs can be set globally using 
     * {@link Ext.app.Application#glyphFontFamily glyphFontFamily} application 
     * config or the {@link Ext#setGlyphFontFamily Ext.setGlyphFontFamily()} method.
     * It is initially set to `'Pictos'`.
     */
    /**
     * 
     * @param {String/Number} glyph
     * If a `string` is passed, it may be the character itself, or the unicode codepoint.
     * for example:
     *
     *     new Ext.Glyph('H');      // the "home" icon in the default (Pictos) font.
     *     new Ext.Glyph('x48');    // the "home" icon in the default (Pictos) font.
     *     new Ext.Glyph(72);       // the "home" icon in the default (Pictos) font.
     *
     * An `@` separator may be used to denote the font:
     *
     *     new Ext.Glyph('xf015@FontAwesome');  // The "home" icon in the FontAwesome font.
     */
    constructor: function(glyph) {
        if (glyph) {
            this.setGlyph(glyph);
        }
    },
    /**
     * 
     * @param {String/Number} glyph
     * If a `string` is passed, it may be the character itself, or the unicode codepoint.
     * for example:
     *
     *     myGlyph.setGlyph('H');      // the "home" icon in the default (Pictos) font.
     *     myGlyph.setGlyph('x48');    // the "home" icon in the default (Pictos) font.
     *     myGlyph.setGlyph(72);       // the "home" icon in the default (Pictos) font.
     *
     * An `@` separator may be used to denote the font:
     *
     *     myGlyph.setGlyph('xf015@FontAwesome');  // The "home" icon in the FontAwesome font.
     *
     */
    setGlyph: function(glyph) {
        var glyphParts;
        this.glyphConfig = glyph;
        if (typeof glyph === 'string') {
            glyphParts = glyph.split('@');
            glyph = isNaN(glyphParts[0]) ? parseInt('0' + glyphParts[0], 16) : parseInt(glyphParts[0], 10);
            // If the glyph specification cannot be parsed as a number
            // we use the codepoint of the first character.
            // If the raw string isNaN, we prepend '0' so that a possible 'xf005' will parse as hex,
            // otherwise parse it as decimal.
            if (isNaN(glyph) || !glyph) {
                glyph = glyphParts[0].charCodeAt(0);
            }
            this.fontFamily = glyphParts[1] || Ext._glyphFontFamily;
        } else {
            this.fontFamily = Ext._glyphFontFamily;
        }
        this.codepoint = glyph;
        this.character = Ext.String.fromCodePoint(this.codepoint);
        return this;
    },
    getStyle: function() {
        return {
            'font-family': this.fontFamily
        };
    },
    isEqual: function(other) {
        return other && other.isGlyph && other.codepoint === this.codepoint && other.fontFamily === this.fontFamily;
    },
    statics: (function() {
        var instance;
        return {
            /**
             * @method fly
             * @static
             * Returns a static, *singleton* `Glyph` instance encapsulating the passed
             * configuration. See {@link #method-setGlyph}.
             *
             * Note that the returned `Glyph` is reused upon each call, so only use this when
             * the encapsulated information is consumed immediately. For a persistent `Glyph`
             * instance, instantiate a new one.
             *
             * @param {String/Number} glyph
             * If a `string` is passed, it may be the character itself, or the unicode codepoint.
             * for example:
             *
             *     Ext.Glyph.fly('H');      // the "home" icon in the default (Pictos) font.
             *     Ext.Glyph.fly('x48');    // the "home" icon in the default (Pictos) font.
             *     Ext.Glyph.fly(72);       // the "home" icon in the default (Pictos) font.
             *
             * An `@` separator may be used to denote the font:
             *
             *     Ext.Glyph.fly('xf015@FontAwesome');  // The "home" icon in the FontAwesome font.
             *
             * @returns {Ext.Glyph} A static `Glyph` instance encapsulating the passed
             * configuration.
             */
            fly: function(glyph) {
                return glyph.isGlyph ? glyph : (instance || (instance = new Ext.Glyph())).setGlyph(glyph);
            }
        };
    })()
});

/**
 * @property {Boolean} [USE_NATIVE_JSON=true]
 * @member Ext
 * Indicates whether to use native browser parsing for JSON methods.
 * This option is ignored if the browser does not support native JSON methods.
 *
 * **Note:** Native JSON methods will not work with objects that have functions.
 * Also, property names must be quoted, otherwise the data will not parse.
 */
Ext.USE_NATIVE_JSON = true;
/**
 * Modified version of [Douglas Crockford's JSON.js][dc] that doesn't
 * mess with the Object prototype.
 *
 * [dc]: http://www.json.org/js.html
 *
 * @class Ext.JSON
 * @singleton
 */
Ext.JSON = (new (function() {
    // @define Ext.JSON
    // @require Ext
    // @require Ext.Error
    /* eslint-disable indent */
    var me = this,
        hasNative = window.JSON && JSON.toString() === '[object JSON]',
        useHasOwn = !!{}.hasOwnProperty,
        pad = function(n) {
            return n < 10 ? "0" + n : n;
        },
        doDecode = function(json) {
            return eval("(" + json + ')');
        },
        doEncode = function(o, newline) {
            // http://jsperf.com/is-undefined
            if (o === null || o === undefined) {
                return "null";
            } else if (Ext.isDate(o)) {
                return me.encodeDate(o);
            } else if (Ext.isString(o)) {
                if (Ext.isMSDate(o)) {
                    return me.encodeMSDate(o);
                } else {
                    return me.encodeString(o);
                }
            } else if (typeof o === "number") {
                // don't use isNumber here, since finite checks happen inside isNumber
                return isFinite(o) ? String(o) : "null";
            } else if (Ext.isBoolean(o)) {
                return String(o);
            }
            // Allow custom serialization by adding a toJSON method to any object type.
            // Date/String have a toJSON in some environments, so check these first.
            else if (typeof o.toJSON === 'function') {
                return doEncode(o.toJSON());
            } else if (Ext.isArray(o)) {
                return encodeArray(o, newline);
            } else if (Ext.isObject(o)) {
                return encodeObject(o, newline);
            } else if (typeof o === "function") {
                return "null";
            }
            return 'undefined';
        },
        m = {
            "\b": '\\b',
            "\t": '\\t',
            "\n": '\\n',
            "\f": '\\f',
            "\r": '\\r',
            '"': '\\"',
            "\\": '\\\\',
            '\v': '\\u000b'
        },
        // ie doesn't handle \v
        /* eslint-disable-next-line no-control-regex, no-useless-escape */
        charToReplace = /[\\\"\x00-\x1f\x7f-\uffff]/g,
        encodeString = function(s) {
            return '"' + s.replace(charToReplace, function(a) {
                var c = m[a];
                return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
            }) + '"';
        },
        encodeMSDate = function(o) {
            return '"' + o + '"';
        },
        encodeArrayPretty = function(o, newline) {
            var len = o.length,
                cnewline = newline + '   ',
                sep = ',' + cnewline,
                a = [
                    "[",
                    cnewline
                ],
                // Note newline in case there are no members
                i;
            for (i = 0; i < len; i += 1) {
                a.push(me.encodeValue(o[i], cnewline), sep);
            }
            // Overwrite trailing comma (or empty string)
            a[a.length - 1] = newline + ']';
            return a.join('');
        },
        encodeObjectPretty = function(o, newline) {
            var cnewline = newline + '   ',
                sep = ',' + cnewline,
                a = [
                    "{",
                    cnewline
                ],
                // Note newline in case there are no members
                i, val;
            for (i in o) {
                val = o[i];
                if (!useHasOwn || o.hasOwnProperty(i)) {
                    // To match JSON.stringify, we shouldn't encode functions or undefined
                    if (typeof val === 'function' || val === undefined || val.isInstance) {
                        
                        continue;
                    }
                    a.push(me.encodeValue(i) + ': ' + me.encodeValue(val, cnewline), sep);
                }
            }
            // Overwrite trailing comma (or empty string)
            a[a.length - 1] = newline + '}';
            return a.join('');
        },
        encodeArray = function(o, newline) {
            if (newline) {
                return encodeArrayPretty(o, newline);
            }
            /* eslint-disable-next-line vars-on-top */
            var a = [
                    "[",
                    ""
                ],
                // Note empty string in case there are no serializable members.
                len = o.length,
                i;
            for (i = 0; i < len; i += 1) {
                a.push(me.encodeValue(o[i]), ',');
            }
            // Overwrite trailing comma (or empty string)
            a[a.length - 1] = ']';
            return a.join("");
        },
        encodeObject = function(o, newline) {
            if (newline) {
                return encodeObjectPretty(o, newline);
            }
            /* eslint-disable-next-line vars-on-top */
            var a = [
                    "{",
                    ""
                ],
                // Note empty string in case there are no serializable members.
                i, val;
            for (i in o) {
                val = o[i];
                if (!useHasOwn || o.hasOwnProperty(i)) {
                    // To match JSON.stringify, we shouldn't encode functions or undefined
                    if (typeof val === 'function' || val === undefined) {
                        
                        continue;
                    }
                    a.push(me.encodeValue(i), ":", me.encodeValue(val), ',');
                }
            }
            // Overwrite trailing comma (or empty string)
            a[a.length - 1] = '}';
            return a.join("");
        };
    /**
     * Encodes a String. This returns the actual string which is inserted into the JSON string
     * as the literal expression. **The returned value includes enclosing double quotation marks.**
     *
     * To override this:
     *
     *     Ext.JSON.encodeString = function(s) {
     *         return 'Foo' + s;
     *     };
     *
     * @param {String} s The String to encode
     * @return {String} The string literal to use in a JSON string.
     * @method encodeString
     */
    me.encodeString = encodeString;
    /**
     * The function which {@link #encode} uses to encode all javascript values to their JSON
     * representations when {@link Ext#USE_NATIVE_JSON} is `false`.
     * 
     * This is made public so that it can be replaced with a custom implementation.
     *
     * @param {Object} o Any javascript value to be converted to its JSON representation
     * @return {String} The JSON representation of the passed value.
     * @method encodeValue
     */
    me.encodeValue = doEncode;
    /**
     * Encodes a Date. This returns the actual string which is inserted into the JSON string
     * as the literal expression. **The returned value includes enclosing double quotation marks.**
     *
     * The default return format is `"yyyy-mm-ddThh:mm:ss"`.
     *
     * To override this:
     *
     *     Ext.JSON.encodeDate = function(d) {
     *         return Ext.Date.format(d, '"Y-m-d"');
     *     };
     *
     * @param {Date} o The Date to encode
     * @return {String} The string literal to use in a JSON string.
     */
    me.encodeDate = function(o) {
        return '"' + o.getFullYear() + "-" + pad(o.getMonth() + 1) + "-" + pad(o.getDate()) + "T" + pad(o.getHours()) + ":" + pad(o.getMinutes()) + ":" + pad(o.getSeconds()) + '"';
    };
    /**
     * Encodes an Object, Array or other value.
     * 
     * If the environment's native JSON encoding is not being used ({@link Ext#USE_NATIVE_JSON}
     * is not set, or the environment does not support it), then ExtJS's encoding will be used.
     * This allows the developer to add a `toJSON` method to their classes which need serializing
     * to return a valid JSON representation of the object.
     * 
     * @param {Object} o The variable to encode.
     * @return {String} The JSON string.
     */
    me.encode = function(o) {
        // check USE_NATIVE_JSON here so it can be changed if needed
        if (hasNative && Ext.USE_NATIVE_JSON) {
            return JSON.stringify(o);
        }
        return me.encodeValue(o);
    };
    /**
     * Decodes (parses) a JSON string to an object. If the JSON is invalid, this function throws
     * a SyntaxError unless the safe option is set.
     *
     * @param {String} json The JSON string.
     * @param {Boolean} [safe=false] `true` to return null, otherwise throw an exception
     * if the JSON is invalid.
     * @return {Object} The resulting object.
     */
    me.decode = function(json, safe) {
        try {
            // check USE_NATIVE_JSON here so it can be changed if needed
            if (hasNative && Ext.USE_NATIVE_JSON) {
                return JSON.parse(json);
            }
            return doDecode(json);
        } catch (e) {
            if (safe) {
                return null;
            }
            Ext.raise({
                sourceClass: "Ext.JSON",
                sourceMethod: "decode",
                msg: "You're trying to decode an invalid JSON String: " + json
            });
        }
    };
    me.encodeMSDate = encodeMSDate;
    // Alias for backwards compatibility
    if (!Ext.util) {
        Ext.util = {};
    }
    Ext.util.JSON = me;
    /**
     * Shorthand for {@link Ext.JSON#encode}
     * @member Ext
     * @method encode
     * @inheritdoc Ext.JSON#encode
     */
    Ext.encode = me.encode;
    /**
     * Shorthand for {@link Ext.JSON#decode}
     * @member Ext
     * @method decode
     * @inheritdoc Ext.JSON#decode
     */
    Ext.decode = me.decode;
})());

/**
 * @class Ext.Manifest
 * **This is not a real JavaScript class and cannot be created. This is for documentation purposes
 * only.**
 *
 * This documentation is for {@link Ext#manifest Ext.manifest}
 * @since 5.0.0
 */
/**
 * @cfg {String/Object} compatibility
 * An object keyed by package name with the value being to desired compatibility level as a
 * version number. If this is just a string, this version is assumed to apply to the framework.
 * Setting this value to less than 5 for 'ext' will enable the compatibility layer to assist
 * in the application upgrade process. For details on the upgrade process, see the
 * (Upgrade Guide)[#/guides/upgrade_50].
 */
/**
 * @cfg {Object} debug
 * An object configuring the debugging characteristics of the framework. See `Ext.debugConfig`
 * which is set to this value.
 */
/**
 * @cfg {Object} packages
 * An object keyed by package name with the value being a subset of the package's `"package.json"`
 * descriptor.
 */

/**
 * A mixin that provides the functionality for inheritable configs. This allows linking
 * components and containers via a prototype-chained object for accessing inherited
 * values.
 *
 * ## Getting Inherited Properties
 *
 * A component's inherited state is used to keep track of aspects of a component's state
 * that might be influenced by its ancestors like "collapsed" and "hidden". For example:
 *
 *      var hidden = this.getInheritedConfig('hidden');
 *
 * The above will produce `true` if this or any ancestor component has its `hidden` config
 * set to `true`.
 *
 * ## Chained Objects
 *
 * Inheritable properties are implemented by chaining each component's inherited state
 * object to its parent container's inherited state object via the prototype. The result
 * is such that if a component's `inheritedState` does not have it's own property, it
 * inherits the property from the nearest ancestor that does.
 *
 * In the case of a Container, two state objects are created. The primary ("outer") object
 * is used for reading inherited properties. It is also what a child will prototype chain
 * to if that child is not part of the container's `items` collection. Anything in the
 * `items` collection will chain to the inheritedStateInner object instead. This object is
 * prototype chained to inheritedState but allows for Container's layout to set inherited
 * properties that specifically apply only to children of the container. This inner object
 * is unlikely to be needed by user code.
 *
 * ## Publishing Inherited Properties
 *
 * The first step to publishing inherited properties is to override `initInheritedState`
 * and add properties that have inheritable values.
 *
 *      initInheritedState: function (inheritedState) {
 *          this.callParent(arguments);
 *
 *          if (this.getHidden()) {
 *              inheritedState.hidden = true;
 *          }
 *      }
 *
 * The above is important because `initInheritedState` is called whenever the object needs
 * to be repopulated. As you can see, only `true` values are added to `inheritedState` in
 * this case because `false` would mask a `hidden` value of `true` from an ancestor.
 *
 * If these values change dynamically, these properties must be maintained. For example:
 *
 *      updateHidden: function (hidden) {
 *          var inherited = this.getInherited();
 *
 *          if (hidden) {
 *              inherited.hidden = true;
 *          } else {
 *              // Unmask whatever may be inherited:
 *              delete inherited.hidden;
 *          }
 *      }
 *
 * ## Proper Usage
 *
 * ALWAYS access inherited state using `getInherited` or `getInheritedConfig`, not by
 * accessing `inheritedState` directly.
 *
 * The `inheritedState` property does not exist until the first call to `getInherited`. At
 * that point `getInherited` walks up the component tree to establish the `inheritedState`
 * prototype chain. Additionally the `inheritedState` property should NOT be relied upon
 * even after the initial call to `getInherited` because it is possible for it to become
 * invalid.
 *
 * Invalidation typically happens when a component is moved to a new container. In such a
 * case the `inheritedState` remains invalid until the next time `getInherited` is called
 * on the component or one of its descendants.
 * @private
 * @since 5.0.0
 */
Ext.define('Ext.mixin.Inheritable', {
    extend: 'Ext.Mixin',
    mixinConfig: {
        id: 'inheritable'
    },
    /**
     * This method returns an object containing the inherited properties for this instance.
     *
     * @param {Boolean} [inner=false] Pass `true` to return `inheritedStateInner` instead
     * of the normal `inheritedState` object. This is only needed internally and should
     * not be passed by user code.
     *
     * @return {Object} The `inheritedState` object containing inherited properties.
     * @since 5.0.0
     */
    getInherited: function(inner) {
        var me = this,
            inheritedState = (inner && me.inheritedStateInner) || me.inheritedState,
            ownerCt = me.getRefOwner(),
            isContainer = me.isContainer,
            parent, inheritedStateInner, getInner, ownerLayout;
        if (!inheritedState || inheritedState.invalid) {
            // Use upward navigational link, not ownerCt.
            // 99% of the time, this will use ownerCt/floatParent.
            // Certain floating components do not have an ownerCt, but they are still linked
            // into a navigational hierarchy. The getRefOwner method normalizes these differences.
            parent = me.getRefOwner();
            ownerLayout = me.ownerLayout;
            if (ownerCt) {
                // For classic, this will only be true if the item is a "child" of its owning
                // container. For example, a docked item will not get the inner inheritedState.
                // For modern, we currently don't have a decent way of telling the difference
                // between a child item, or an item that belongs to the component. We may
                // need to determine this in future, but currently have no use for it.
                getInner = ownerLayout ? ownerLayout === ownerCt.layout : true;
            }
            me.inheritedState = inheritedState = // chain this component's inheritedState to that of its parent.  If it
            // doesn't have a parent, then chain to the rootInheritedState.  This is
            // done so that when there is a viewport, all component's will inherit
            // from its inheritedState, even components that are not descendants of
            // the viewport.
            Ext.Object.chain(parent ? parent.getInherited(getInner) : Ext.rootInheritedState);
            if (isContainer) {
                me.inheritedStateInner = inheritedStateInner = Ext.Object.chain(inheritedState);
            }
            me.initInheritedState(inheritedState, inheritedStateInner);
            // initInheritedState is allowed to replace the objects we provide, so we go
            // back to the instance here at the end.
            inheritedState = (isContainer && inner) ? me.inheritedStateInner : me.inheritedState;
        }
        return inheritedState;
    },
    /**
     * This method returns the value of a config property that may be inherited from some
     * ancestor.
     *
     * In some cases, a config may be explicitly set on a component with the intent of
     * *only* being presented to its children while that component should act upon the
     * inherited value (see `referenceHolder` for example). In these cases the `skipThis`
     * parameter should be specified as `true`.
     *
     * @param {String} property The name of the config property to return.
     * @param {Boolean} [skipThis=false] Pass `true` if the property should be ignored if
     * found on this instance. In other words, `true` means the property must be inherited
     * and not explicitly set on this instance.
     * @return {Mixed} The value of the requested `property`.
     * @since 5.0.0
     */
    getInheritedConfig: function(property, skipThis) {
        var state = this.inheritedState,
            old, ret;
        // Avoid the extra method call since user has already made one to get here
        if (!state || state.invalid) {
            state = this.getInherited();
        }
        ret = state[property];
        if (skipThis && state.hasOwnProperty(property)) {
            old = ret;
            delete state[property];
            ret = state[property];
            state[property] = old;
        }
        return ret;
    },
    /**
     * Gets the Controller or Component that is used as the event root for this view.
     *
     * @param {Object} [defaultScope=this] The default scope to return if none is found.
     * @param {Boolean} [skipThis] (private)
     * @return {Ext.app.ViewController/Ext.container.Container} The default listener scope.
     *
     * @protected
     * @since 5.0.0
     */
    resolveListenerScope: function(defaultScope, skipThis) {
        var me = this,
            hasSkipThis = (typeof skipThis === 'boolean'),
            namedScope = Ext._namedScopes[defaultScope],
            ret;
        if (!namedScope) {
            // If there is no named scope we know for sure that the listener was not
            // declared on the class body (i.e. !namedScope.isSelf) and so we can skip
            // this instance and resolve to defaultListenerScope upward in the hierarchy.
            // scope: not a named scope so we default to this
            ret = me.getInheritedConfig('defaultListenerScope', hasSkipThis ? skipThis : true) || defaultScope || me;
        } else if (namedScope.isController) {
            // scope:'controller' declared on the class body must include our own
            // controller before ascending the hierarchy, but scope:'controller' declared
            // on the instance must skip our own controller and search only for an
            // inherited controller.
            ret = me.getInheritedConfig('controller', hasSkipThis ? skipThis : !namedScope.isSelf);
        } else if (namedScope.isOwner) {
            ret = me.getRefOwner();
        } else if (namedScope.isSelf) {
            // scope:'self' indicates listeners declared on the class body with unspecified
            // scope. Include this instance when searching for an inherited default scope.
            ret = me.getInheritedConfig('defaultListenerScope', hasSkipThis && skipThis) || me;
        } else if (namedScope.isThis) {
            // scope:'this' always resolves to this instance, regardless of whether the
            // listener was declared on the class or instance
            ret = me;
        }
        return ret || null;
    },
    /**
     * Returns the default listener scope for a "satellite" of this component.
     * Used for resolving scope for observable objects that are not part of the normal
     * Container/Component hierarchy (for example, plugins)
     *
     * @param {Ext.mixin.Observable} satellite
     * @param {Object} [defaultScope]
     * @return {Object} The listener scope
     * @protected
     * @since 5.1.1
     */
    resolveSatelliteListenerScope: function(satellite, defaultScope) {
        var me = this,
            namedScope = Ext._namedScopes[defaultScope],
            ret;
        // The logic here is the same as that in resolveListenerScope with a couple of
        // exceptions:
        // 1. If scope resolution failed, fall back to the satellite instance, not "this"
        //    for class-declared listeners, for instance-declared use "this"
        // 2. Never pass skipThis to getInheritedConfig.  The satellite is essentially
        //    treated as a "child" of this component and therefore should always consider
        //    its component/component's controller as candidates for listener scope
        if (!namedScope) {
            ret = me.getInheritedConfig('defaultListenerScope') || defaultScope || me;
        } else if (namedScope.isController) {
            ret = me.getInheritedConfig('controller');
        } else if (namedScope.isSelf) {
            ret = me.getInheritedConfig('defaultListenerScope') || satellite;
        } else if (namedScope.isThis) {
            ret = satellite;
        }
        return ret || null;
    },
    /**
     * Gets the Form or Component that is used as the name holder for this component.
     *
     * @param {Boolean} [skipThis=true] `false` to return this as the name holder if
     * this instance has set `nameHolder`. Unlike `getInheritedConfig` this method
     * defaults to `true` because it is possible that a `name` property set by the
     * owner of a component that is also a `nameHolder` itself. In this case, the
     * `name` connects not to this component but to the parent nameHolder.
     *
     * @return {Ext.Component} The name holder.
     *
     * @private
     * @since 6.5.0
     */
    lookupNameHolder: function(skipThis) {
        return this.getInheritedConfig('nameHolder', skipThis !== false) || null;
    },
    /**
     * Gets the Controller or Component that is used as the reference holder for this view.
     *
     * @param {Boolean} [skipThis=true] `false` to return this as the reference holder if
     * this instance has set `referenceHolder`. Unlike `getInheritedConfig` this method
     * defaults to `true` because it is possible that a `reference` property set by the
     * owner of a component that is also a `referenceHolder` itself. In this case, the
     * `reference` connects not to this component but to the parent referenceHolder.
     *
     * @return {Ext.app.ViewController/Ext.container.Container} The reference holder.
     *
     * @private
     * @since 5.0.0
     */
    lookupReferenceHolder: function(skipThis) {
        return this.getInheritedConfig('referenceHolder', skipThis !== false) || null;
    },
    /**
     * Used by {@link Ext.ComponentQuery ComponentQuery}, and the {@link Ext.Component#up up}
     * method to find the owning Component in the linkage hierarchy.
     *
     * By default this returns the Container which contains this Component.
     *
     * This may be overridden by Component authors who implement ownership hierarchies
     * which are not based upon ownerCt, such as BoundLists being owned by Fields or Menus
     * being owned by Buttons.
     * @protected
     */
    getRefOwner: function() {
        var me = this;
        // Look for both ownerCt (classic toolkit) and parent (modern toolkit)
        // Look for ownerCmp before all containment links for scenarios like a button
        // menu inside a floating window, or a submenu of a menu item.
        // Floated items have the Viewport as their parent, and ownerCmp exists to
        // override the containment tree.
        return me.ownerCmp || me.ownerCt || me.parent || me.$initParent || me.floatParent;
    },
    /**
     * Bubbles up the {@link #method!getRefOwner} hierarchy, calling the specified function
     * with each component. The scope (`this` reference) of the function call will be the
     * scope provided or the current component. The arguments to the function will
     * be the args provided or the current component. If the function returns false at any
     * point, the bubble is stopped.
     *
     * @param {Function} fn The function to call
     * @param {Object} [scope] The scope of the function. Defaults to current node.
     * @param {Array} [args] The args to call the function with. Defaults to passing the current
     * component.
     */
    bubble: function(fn, scope, args) {
        var target;
        for (target = this; target; target = target.getRefOwner()) {
            if (fn.apply(scope || target, args || [
                target
            ]) === false) {
                break;
            }
        }
    },
    /**
     * Determines whether this component is the descendant of a passed component.
     * @param {Ext.Component} ancestor A Component which may contain this Component.
     * @return {Boolean} `true` if the component is the descendant of the passed component,
     * otherwise `false`.
     */
    isDescendantOf: function(ancestor) {
        return ancestor ? ancestor.isAncestor(this) : false;
    },
    /**
     * Determines whether **this Component** is an ancestor of the passed Component.
     * This will return `true` if the passed Component is anywhere within the subtree
     * beneath this Component.
     * @param {Ext.Component} possibleDescendant The Component to test for presence
     * within this Component's subtree.
     */
    isAncestor: function(possibleDescendant) {
        while (possibleDescendant) {
            if (possibleDescendant.getRefOwner() === this) {
                return true;
            }
            possibleDescendant = possibleDescendant.getRefOwner();
        }
        return false;
    },
    /**
     * This method is called to initialize the `inheritedState` objects for this instance.
     * This amounts to typically copying certain properties from the instance to the given
     * object.
     *
     * @param {Object} inheritedState The state object for this instance.
     * @param {Object} [inheritedStateInner] This object is only provided for containers.
     * @method initInheritedState
     * @protected
     * @since 5.0.0
     */
    /**
     * This method marks the current inherited state as invalid. The next time a call is
     * made to `getInherited` the objects will be recreated and initialized.
     * @private
     * @since 5.0.0
     */
    invalidateInheritedState: function() {
        var inheritedState = this.inheritedState;
        if (inheritedState) {
            // if component has a inheritedState at this point we set an invalid flag in
            // the inheritedState so descendants of this component know to re-initialize
            // their inheritedState the next time it is requested (see getInherited())
            inheritedState.invalid = true;
            // We can now delete the old inheritedState since it is invalid.  IMPORTANT:
            // the descendants are still linked to the old inheritedState via the
            // prototype chain, and their inheritedState property will be synced up
            // the next time their getInherited() method is called.  For this reason
            // inheritedState should always be accessed using getInherited()
            delete this.inheritedState;
        }
    },
    privates: {
        /**
         * Sets up a reference on our current reference holder.
         *
         * @private
         */
        _fixReference: function() {
            var me = this,
                holder;
            if (me.name && me.nameable) {
                holder = me.lookupNameHolder();
                if (holder && !holder.destroyed) {
                    holder.attachNameRef(me);
                }
            }
            if (me.reference) {
                holder = me.lookupReferenceHolder();
                if (holder && !holder.destroyed) {
                    holder.attachReference(me);
                }
            }
        },
        /**
         * Called when this Inheritable is added to a parent
         * @param parent
         * @param {Boolean} instanced
         */
        onInheritedAdd: function(parent, instanced) {
            var me = this;
            // The container constructed us, so it's not possible for our 
            // inheritedState to be invalid, so we only need to clear it
            // if we've been added as an instance 
            if (me.inheritedState && instanced) {
                me.invalidateInheritedState();
            }
            Ext.ComponentManager.markReferencesDirty();
        },
        /**
         * Called when this inheritable is removed from a parent
         * @param {Boolean} destroying `true` if this item will be destroyed by it's container
         */
        onInheritedRemove: function(destroying) {
            var me = this;
            Ext.ComponentManager.markReferencesDirty();
            if (me.inheritedState && !destroying) {
                me.invalidateInheritedState();
            }
        }
    }
}, /* eslint-disable indent */
function() {
    /**
     * @property {Object} rootInheritedState
     * The top level inheritedState to which all other inheritedStates are chained. If
     * there is a `Viewport` instance, this object becomes the Viewport's inheritedState.
     * See also {@link Ext.Component#getInherited}.
     *
     * @private
     * @member Ext
     * @since 5.0.0
     */
    Ext.rootInheritedState = {};
});

/**
 * This class is intended as a mixin for classes that want to provide a "bind" config that
 * connects to a `ViewModel`.
 * @private
 * @since 5.0.0
 */
Ext.define('Ext.mixin.Bindable', {
    mixinId: 'bindable',
    config: {
        /**
         * @cfg {Object/String} [bind]
         * Setting this config option adds or removes data bindings for other configs.
         * For example, to bind the `title` config:
         *
         *      var panel = Ext.create({
         *          xtype: 'panel',
         *          bind: {
         *              title: 'Hello {user.name}'
         *          }
         *      });
         *
         * To dynamically add bindings:
         *
         *      panel.setBind({
         *          title: 'Greetings {user.name}!'
         *      });
         *
         * To remove bindings:
         *
         *      panel.setBind({
         *          title: null
         *      });
         *
         * The bind expressions are presented to `{@link Ext.app.ViewModel#bind}`. The
         * `ViewModel` instance is determined by `lookupViewModel`.
         *
         * **Note:** If  bind is passed as a string, it will use the
         * {@link Ext.Component#property-defaultBindProperty} for the binding.
         */
        bind: {
            $value: null,
            lazy: true
        },
        // @cmd-auto-dependency { aliasPrefix: 'controller.' }
        /**
         * @cfg {String/Object/Ext.app.ViewController} controller
         * A string alias, a configuration object or an instance of a `ViewController` for
         * this container. Sample usage:
         *
         *     Ext.define('MyApp.UserController', {
         *         alias: 'controller.user'
         *     });
         *
         *     Ext.define('UserContainer', {
         *         extend: 'Ext.container.container',
         *         controller: 'user'
         *     });
         *     // Or
         *     Ext.define('UserContainer', {
         *         extend: 'Ext.container.container',
         *         controller: {
         *             type: 'user',
         *             someConfig: true
         *         }
         *     });
         *
         *     // Can also instance at runtime
         *     var ctrl = new MyApp.UserController();
         *     var view = new UserContainer({
         *         controller: ctrl
         *     });
         *
         */
        controller: null,
        /**
         * @method getController
         * Returns the {@link Ext.app.ViewController} instance associated with this 
         * component via the {@link #controller} config or {@link #setController} method.
         * @return {Ext.app.ViewController} Returns this component's ViewController or 
         * null if one was not configured
         */
        /**
         * @cfg {Boolean} defaultListenerScope
         * If `true`, this component will be the default scope (this pointer) for events
         * specified with string names so that the scope can be dynamically resolved. The
         * component will automatically become the defaultListenerScope if a
         * {@link #controller} is specified.
         *
         * See the introductory docs for {@link Ext.container.Container} for some sample
         * usages.
         *
         * **NOTE**: This value can only be reliably set at construction time. Setting it
         * after that time may not correctly rewire all of the potentially effected
         * listeners.
         */
        defaultListenerScope: false,
        /**
         * @cfg {String/String[]/Object} publishes
         * One or more names of config properties that this component should publish 
         * to its ViewModel. Generally speaking, only properties defined in a class config
         * block (including ancestor config blocks and mixins) are eligible for publishing 
         * to the viewModel. Some components override this and publish their most useful 
         * configs by default. 
         * 
         * **Note:** We'll discuss publishing properties **not** found in the config block below. 
         * 
         * Values determined to be invalid by component (often form fields and model validations) 
         * will not be published to the ViewModel.
         *
         * This config uses the `{@link #cfg-reference}` to determine the name of the data
         * object to place in the `ViewModel`. If `reference` is not set then this config
         * is ignored.
         *
         * By using this config and `{@link #cfg-reference}` you can bind configs between
         * components. For example:
         *
         *      ...
         *          items: [{
         *              xtype: 'textfield',
         *              reference: 'somefield',  // component's name in the ViewModel
         *              publishes: 'value' // value is not published by default
         *          },{
         *              ...
         *          },{
         *              xtype: 'displayfield',
         *              bind: 'You have entered "{somefield.value}"'
         *          }]
         *      ...
         *
         * Classes must provide this config as an Object:
         *
         *      Ext.define('App.foo.Bar', {
         *          publishes: {
         *              foo: true,
         *              bar: true
         *          }
         *      });
         *
         * This is required for the config system to properly merge values from derived
         * classes.
         *
         * For instances this value can be specified as a value as show above or an array
         * or object as follows:
         *
         *      {
         *          xtype: 'textfield',
         *          reference: 'somefield',
         *          publishes: [
         *              'value',
         *              'rawValue',
         *              'dirty'
         *          ]
         *      }
         *
         *      // This achieves the same result as the above array form.
         *      {
         *          xtype: 'textfield',
         *          reference: 'somefield',
         *          publishes: {
         *              value: true,
         *              rawValue: true,
         *              dirty: true
         *          }
         *      }
         *
         * In some cases, users may want to publish a property to the viewModel that is not found
         * in a class  config block. In these situations, you may utilize {@link #publishState}
         * if the property has a  setter method. Let's use
         * {@link Ext.form.Labelable#setFieldLabel setFieldLabel} as an example:
         *
         *       setFieldLabel: function(fieldLabel) {
         *           this.callParent(arguments);
         *           this.publishState('fieldLabel', fieldLabel);
         *       }        
         * 
         * With the above chunk of code, fieldLabel may now be published to the viewModel.
         * 
         * @since 5.0.0
         */
        publishes: {
            $value: null,
            lazy: true,
            merge: function(newValue, oldValue) {
                return this.mergeSets(newValue, oldValue);
            }
        },
        // @cmd-auto-dependency { directRef: 'Ext.data.Session' }
        /**
         * @cfg {Boolean/Object/Ext.data.Session} [session=null]
         * If provided this creates a new `Session` instance for this component. If this
         * is a `Container`, this will then be inherited by all child components.
         *
         * To create a new session you can specify `true`:
         *
         *      Ext.create({
         *          xtype: 'viewport',
         *          session: true,
         *
         *          items: [{
         *              ...
         *          }]
         *      });
         *
         * Alternatively, a config object can be provided:
         *
         *      Ext.create({
         *          xtype: 'viewport',
         *          session: {
         *              ...
         *          },
         *
         *          items: [{
         *              ...
         *          }]
         *      });
         *
         */
        session: {
            $value: null,
            lazy: true
        },
        /**
         * @cfg {String/String[]/Object} twoWayBindable
         * This object holds a map of `config` properties that will update their binding
         * as they are modified. For example, `value` is a key added by form fields. The
         * form of this config is the same as `{@link #cfg!publishes}`.
         *
         * This config is defined so that updaters are not created and added for all
         * bound properties since most cannot be modified by the end-user and hence are
         * not appropriate for two-way binding.
         */
        twoWayBindable: {
            $value: null,
            lazy: true,
            merge: function(newValue, oldValue) {
                return this.mergeSets(newValue, oldValue);
            }
        },
        // @cmd-auto-dependency { aliasPrefix: 'viewmodel.', defaultType: 'default' }
        /**
         * @cfg {String/Object/Ext.app.ViewModel} viewModel
         * The `ViewModel` is a data provider for this component and its children. The
         * data contained in the `ViewModel` is typically used by adding `bind` configs
         * to the components that want present or edit this data.
         *
         * When set, the `ViewModel` is created and links to any inherited `viewModel`
         * instance from an ancestor container as the "parent". The `ViewModel` hierarchy,
         * once established, only supports creation or destruction of children. The
         * parent of a `ViewModel` cannot be changed on the fly.
         *
         * If this is a root-level `ViewModel`, the data model connection is made to this
         * component's associated `{@link Ext.data.Session Data Session}`. This is
         * determined by calling `getInheritedSession`.
         *
         */
        viewModel: {
            $value: null,
            lazy: true
        }
    },
    /**
     * @property {String} [defaultBindProperty]
     * This property is used to determine the property of a `bind` config that is just
     * the value. For example, if `defaultBindProperty="value"`, then this shorthand
     * `bind` config:
     *
     *      bind: '{name}'
     *
     * Is equivalent to this object form:
     *
     *      bind: {
     *          value: '{name}'
     *      }
     *
     * The `defaultBindProperty` is set to "value" for form fields and to "store" for
     * grids and trees.
     * @protected
     */
    defaultBindProperty: null,
    /**
     * @cfg {Boolean} nameable
     * Set to `true` for this component's `name` property to be tracked by its containing
     * `nameHolder`.
     */
    nameable: false,
    /**
     * @cfg {Boolean} shareableName
     * Set to `true` to allow this component's `name` to be shared by other items in the
     * same `nameHolder`. Such items will be returned in an array from `lookupName`.
     */
    shareableName: false,
    /**
     * @cfg {String} reference
     * Specifies a name for this component inside its component hierarchy. This name
     * must be unique within its {@link Ext.container.Container#referenceHolder view}
     * or its {@link Ext.app.ViewController ViewController}. See the documentation in
     * {@link Ext.container.Container} for more information about references.
     *
     * **Note**: Valid identifiers start with a letter or underscore and are followed
     * by zero or more additional letters, underscores or digits. References are case
     * sensitive.
     */
    reference: null,
    /**
     * @property {RegExp}
     * Regular expression used for validating `reference` values.
     * @private
     */
    validRefRe: /^[a-z_][a-z0-9_]*$/i,
    getReference: function() {
        // Maintained for compatibility with <7 where reference used the config system
        return this.reference;
    },
    /**
     * Called by `getInherited` to initialize the inheritedState the first time it is
     * requested.
     * @protected
     */
    initInheritedState: function(inheritedState) {
        var me = this,
            reference = me.reference,
            controller = me.getController(),
            // Don't instantiate the view model here, we only need to know that
            // it exists
            viewModel = me.getConfig('viewModel', true),
            session = me.getConfig('session', true),
            defaultListenerScope = me.getDefaultListenerScope();
        if (controller) {
            inheritedState.controller = controller;
        }
        if (defaultListenerScope) {
            inheritedState.defaultListenerScope = me;
        } else if (controller) {
            inheritedState.defaultListenerScope = controller;
        }
        if (viewModel) {
            // If we're not configured with an instance, just stamp the current component as
            // the thing that holds the view model. When we ask to get the inherited view model,
            // we will know that it's not an instance yet so we need to spin it up on this component
            // We need to initialize them from top-down, but we don't want to do it up front.
            if (!viewModel.isViewModel) {
                viewModel = me;
            }
            inheritedState.viewModel = viewModel;
        }
        // Same checks as the view model
        if (session) {
            if (!session.isSession) {
                session = me;
            }
            inheritedState.session = session;
        }
        if (reference) {
            me.referenceKey = (inheritedState.referencePath || '') + reference;
            me.viewModelKey = (inheritedState.viewModelPath || '') + reference;
        }
    },
    /**
     * Determines if the passed property name is bound to ViewModel data.
     * @param {String} [name] The property name to test. Defaults to the
     * {@link #defaultBindProperty}
     * @returns {Boolean} `true` if the passed property receives data from a ViewModel.
     * @since 6.5.0
     */
    isBound: function(name) {
        var bind = this.getBind();
        return !!(bind && (bind[name || this.defaultBindProperty]));
    },
    /**
     * Gets the controller that controls this view. May be a controller that belongs
     * to a view higher in the hierarchy.
     * 
     * @param {Boolean} [skipThis=false] `true` to not consider the controller directly attached
     * to this view (if it exists).
     * @return {Ext.app.ViewController} The controller. `null` if no controller is found.
     *
     * @since 5.0.1
     */
    lookupController: function(skipThis) {
        return this.getInheritedConfig('controller', skipThis) || null;
    },
    /**
     * Returns the `Ext.data.Session` for this instance. This property may come
     * from this instance's `{@link #session}` or be inherited from this object's parent.
     * @param {Boolean} [skipThis=false] Pass `true` to ignore a `session` configured on
     * this instance and only consider an inherited session.
     * @return {Ext.data.Session}
     * @since 5.0.0
     */
    lookupSession: function(skipThis) {
        // See lookupViewModel
        var ret = skipThis ? null : this.getSession();
        // may be the initGetter!
        if (!ret) {
            ret = this.getInheritedConfig('session', skipThis);
            if (ret && !ret.isSession) {
                ret = ret.getInherited().session = ret.getSession();
            }
        }
        return ret || null;
    },
    /**
     * Returns the `Ext.app.ViewModel` for this instance. This property may come from this
     * this instance's `{@link #viewModel}` or be inherited from this object's parent.
     * @param {Boolean} [skipThis=false] Pass `true` to ignore a `viewModel` configured on
     * this instance and only consider an inherited view model.
     * @return {Ext.app.ViewModel}
     * @since 5.0.0
     */
    lookupViewModel: function(skipThis) {
        var ret = skipThis ? null : this.getViewModel();
        // may be the initGetter!
        if (!ret) {
            ret = this.getInheritedConfig('viewModel', skipThis);
            // If what we get back is a component, it means the component was configured
            // with a view model, however the construction of it has been delayed until
            // we need it. As such, go and construct it and store it on the inherited state.
            if (ret && !ret.isViewModel) {
                ret = ret.getInherited().viewModel = ret.getViewModel();
            }
        }
        return ret || null;
    },
    /**
     * Publish this components state to the `ViewModel`. If no arguments are given (or if
     * this is the first call), the entire state is published. This state is determined by
     * the `publishes` property.
     *
     * This method is called only by component authors.
     *
     * @param {String} [property] The name of the property to update.
     * @param {Object} [value] The value of `property`. Only needed if `property` is given.
     * @protected
     * @since 5.0.0
     */
    publishState: function(property, value) {
        var me = this,
            state = me.publishedState,
            binds = me.getBind(),
            binding = binds && property && binds[property],
            count = 0,
            name, publishes, vm, path;
        if (!(arguments.length === 0 || arguments.length === 2)) {
            Ext.raise('publishState must either be called with no args, or with both name ' + 'AND value passed');
        }
        if (binding && !binding.syncing && !binding.isReadOnly()) {
            // If the binding has never fired & our value is either:
            // a) undefined
            // b) null
            // c) The value we were initially configured with
            // Then we don't want to publish it back to the view model. If we do, we'll be
            // overwriting whatever is in the viewmodel and it will never have a chance to fire.
            if (binding.calls || !(value == null || value === me.getInitialConfig()[property])) {
                binding.setValue(value);
            }
        }
        if (!(publishes = me.getPublishes())) {
            return;
        }
        if (!(vm = me.lookupViewModel())) {
            return;
        }
        // Important to access path after lookupViewModel, which will kick off
        // our inheritedState if we don't have one
        if (!(path = me.viewModelKey)) {
            return;
        }
        state = state || (me.publishedState = {});
        if (property) {
            if (!publishes[property]) {
                return;
            }
            // If we are setting an individual property and that is not a {} or a [] then
            // check to see if it is unchanged.
            if (!(value && value.constructor === Object) && !(value instanceof Array)) {
                if (state[property] === value) {
                    return;
                }
            }
            path += '.';
            path += property;
        } else {
            for (name in publishes) {
                ++count;
                state[name] = me.getConfig(name);
            }
            if (!count) {
                // if (no properties were put in "state")
                return;
            }
            value = state;
        }
        vm.set(path, value);
    },
    //=========================================================================
    privates: {
        /**
         * @param {String/Object} binds
         * @param {Object} currentBindings
         * @return {Object}
         * @private
         * @since 5.0.0
         */
        applyBind: function(binds, currentBindings) {
            if (!binds) {
                return currentBindings;
            }
            // eslint-disable-next-line vars-on-top
            var me = this,
                viewModel = me.lookupViewModel(),
                twoWayable = me.getTwoWayBindable(),
                getBindTemplateScope = me._getBindTemplateScope,
                b, watch, property, descriptor;
            if (!viewModel) {
                Ext.raise('Cannot use bind config without a viewModel');
            }
            if (typeof binds === 'string') {
                if (!me.defaultBindProperty) {
                    Ext.raise(me.$className + ' has no defaultBindProperty - ' + 'Please specify a bind object');
                }
                b = binds;
                binds = {};
                binds[me.defaultBindProperty] = b;
            }
            for (property in binds) {
                descriptor = binds[property];
                b = currentBindings && currentBindings[property];
                if (b) {
                    b.destroy();
                    delete currentBindings[property];
                }
                if (descriptor) {
                    if (!b && twoWayable && twoWayable[property]) {
                        (watch || (watch = {}))[property] = '_onConfigPropChange';
                    }
                    b = viewModel.bind(descriptor, me.onBindNotify, me);
                    b._config = Ext.Config.get(property);
                    b.getTemplateScope = getBindTemplateScope;
                    if (!me[b._config.names.set]) {
                        Ext.raise('Cannot bind ' + property + ' on ' + me.$className + ' - missing a ' + b._config.names.set + ' method.');
                    }
                    (currentBindings || (currentBindings = {}))[property] = b;
                }
            }
            if (watch) {
                watch.scope = me;
                me.watchConfig(watch);
            }
            me.$bindings = currentBindings;
            return currentBindings;
        },
        applyController: function(controller) {
            if (controller) {
                controller = Ext.Factory.controller(controller);
                controller.setView(this);
            }
            // In classic, this is a no-op, in modern it will
            // save a local reference
            this.controller = controller;
            return controller;
        },
        updatePublishes: function(all) {
            var me = this,
                property, watch;
            if (all && me.lookupViewModel()) {
                for (property in all) {
                    if (all[property]) {
                        (watch || (watch = {}))[property] = '_onConfigPropChange';
                    }
                }
                if (watch) {
                    watch.scope = me;
                    me.watchConfig(watch);
                }
            }
            return all;
        },
        /**
         * Transforms a Session config to a proper instance.
         * @param {Object} session
         * @return {Ext.data.Session}
         * @private
         * @since 5.0.0
         */
        applySession: function(session) {
            var parentSession, config;
            if (!session) {
                return null;
            }
            if (!session.isSession) {
                parentSession = this.lookupSession(true);
                // skip this component
                config = (session === true) ? {} : session;
                if (parentSession) {
                    session = parentSession.spawn(config);
                } else {
                    // Mask this use of Session from Cmd - the dependency is not ours
                    // but the caller
                    session = new Ext.data['Session'](config);
                }
            }
            // eslint-disable-line dot-notation
            return session;
        },
        /**
         * Transforms a ViewModel config to a proper instance.
         * @param {String/Object/Ext.app.ViewModel} viewModel
         * @return {Ext.app.ViewModel}
         * @private
         * @since 5.0.0
         */
        applyViewModel: function(viewModel) {
            var me = this,
                config, session;
            if (!viewModel) {
                return null;
            }
            if (!viewModel.isViewModel) {
                config = {
                    parent: me.lookupViewModel(true),
                    // skip this component
                    // Ensure that VM construction activity can reach the view (for
                    // example events on stores)
                    view: me
                };
                config.session = me.getSession();
                if (!session && !config.parent) {
                    config.session = me.lookupSession();
                }
                if (viewModel) {
                    if (viewModel.constructor === Object) {
                        Ext.apply(config, viewModel);
                    } else if (typeof viewModel === 'string') {
                        config.type = viewModel;
                    }
                }
                viewModel = Ext.Factory.viewModel(config);
            }
            return viewModel;
        },
        _getBindTemplateScope: function() {
            // This method is called as a method on a Binding instance, so the "this" pointer
            // is that of the Binding. The "scope" of the Binding is the component owning it.
            return this.scope.resolveListenerScope();
        },
        destroyBindable: function() {
            var me = this,
                viewModel = me.getConfig('viewModel', true),
                session = me.getConfig('session', true),
                controller = me.getController();
            if (viewModel && viewModel.isViewModel) {
                viewModel.destroy();
                me.setViewModel(null);
            }
            if (session && session.isSession) {
                if (session.getAutoDestroy()) {
                    session.destroy();
                }
                me.setSession(null);
            }
            if (controller) {
                me.setController(null);
                controller.destroy();
            }
        },
        /**
         * This method triggers the lazy configs and must be called when it is time to
         * fully boot up. The configs that must be initialized are: `bind`, `publishes`,
         * `session`, `twoWayBindable` and `viewModel`.
         * @private
         * @since 5.0.0
         */
        initBindable: function() {
            var me = this,
                controller = me.controller;
            me.initBindable = Ext.emptyFn;
            me.getBind();
            me.getPublishes();
            // If we have binds, the applyBind method will call getTwoWayBindable. If we
            // have no binds then applyBind will not be called and we will ignore our
            // twoWayBindable config (which is fine).
            // If we have publishes or binds then the viewModel will be requested.
            if (!me.viewModel) {
                // Force VM creation now
                me.getViewModel();
            }
            if (controller) {
                controller.initBindings();
            }
            if (me.reference) {
                // If we have no "reference" config then we do not publish our state to the
                // viewmodel.
                me.publishState();
            }
        },
        /**
         * Checks if a particular binding is synchronizing the value.
         * @param {String} name The name of the property being bound to.
         * @return {Boolean} `true` if the binding is syncing.
         *
         * @private
         */
        isSyncing: function(name) {
            var bindings = this.getBind(),
                ret = false,
                binding;
            if (bindings) {
                binding = bindings[name];
                if (binding) {
                    ret = binding.syncing > 0;
                }
            }
            return ret;
        },
        notifyIf: function(skipThis) {
            var vm = this.lookupViewModel(skipThis);
            if (vm) {
                vm.notify();
            }
        },
        onBindNotify: function(value, oldValue, binding) {
            binding.syncing = (binding.syncing + 1) || 1;
            this[binding._config.names.set](value);
            --binding.syncing;
        },
        _onConfigPropChange: function(me, name, value) {
            me.publishState(name, value);
        },
        removeBindings: function() {
            var me = this,
                bindings = me.$bindings,
                b, key;
            if (bindings) {
                for (key in bindings) {
                    b = bindings[key];
                    if (b) {
                        b.destroy();
                        b._config = b.getTemplateScope = null;
                        bindings[key] = null;
                    }
                }
            }
        },
        /**
         * Updates the session config.
         * @param {Ext.data.Session} session
         * @private
         */
        updateSession: function(session) {
            var state = this.getInherited();
            if (session) {
                state.session = session;
            } else {
                delete state.session;
            }
        },
        /**
         * Updates the viewModel config.
         * @param {Ext.app.ViewModel} viewModel
         * @param {Ext.app.ViewModel} oldViewModel
         * @private
         */
        updateViewModel: function(viewModel, oldViewModel) {
            var me = this,
                state = me.getInherited(),
                controller = me.getController();
            if (viewModel) {
                me.hasVM = true;
                state.viewModel = viewModel;
                viewModel.setView(me);
                if (controller) {
                    controller.initViewModel(viewModel);
                }
            } else {
                delete state.viewModel;
            }
            // In classic, this is a no-op, in modern it will
            // save a local reference
            me.viewModel = viewModel;
        }
    }
});
// private

/**
 * A mixin that gives Ext.Component and Ext.Widget the ability to process the "delegate"
 * event option.
 * @private
 */
Ext.define('Ext.mixin.ComponentDelegation', {
    extend: 'Ext.Mixin',
    mixinConfig: {
        id: 'componentDelegation'
    },
    privates: {
        /**
         * @private
         * Adds a listeners with the "delegate" event option.  Users should not invoke this
         * method directly.  Use the "delegate" event option of 
         * {@link Ext.util.Observable#addListener addListener} instead.
         */
        addDelegatedListener: function(eventName, fn, scope, options, order, caller, manager) {
            var me = this,
                delegatedEvents, event, priority;
            eventName = Ext.canonicalEventName(eventName);
            // The following processing of the "order" option is typically done by the
            // doAddListener method of Ext.mixin.Observable, but that method does not
            // get called when adding a delegated listener, so we must do the conversion
            // of order to priority here.
            order = order || options.order;
            if (order) {
                priority = (options && options.priority);
                if (!priority) {
                    // priority option takes precedence over order
                    // do not mutate the user's options
                    options = options ? Ext.Object.chain(options) : {};
                    options.priority = me.$orderToPriority[order];
                }
            }
            if (options.target) {
                Ext.raise("Cannot add '" + eventName + "' listener to component: '" + me.id + "' - 'delegate' and 'target' event options are incompatible.");
            }
            // Delegated events are tracked in a map keyed by event name, where the values
            // are instances of Ext.util.Event that track all of the delegate listeners
            // for the given event name.
            delegatedEvents = me.$delegatedEvents || (me.$delegatedEvents = {});
            event = delegatedEvents[eventName] || (delegatedEvents[eventName] = new Ext.util.Event(me, eventName));
            if (event.addListener(fn, scope, options, caller, manager)) {
                me.$hasDelegatedListeners._incr_(eventName);
            }
        },
        /**
         * @private
         * Clears all listeners that were attached using the "delegate" event option.
         * Users should not invoke this method directly.  It is called automatically as
         * part of normal {@link Ext.util.Observable#clearListeners clearListeners} 
         * processing.
         */
        clearDelegatedListeners: function() {
            var me = this,
                delegatedEvents = me.$delegatedEvents,
                eventName, event, listenerCount;
            if (delegatedEvents) {
                for (eventName in delegatedEvents) {
                    event = delegatedEvents[eventName];
                    listenerCount = event.listeners.length;
                    event.clearListeners();
                    me.$hasDelegatedListeners._decr_(eventName, listenerCount);
                    delete delegatedEvents[eventName];
                }
            }
        },
        /**
         * @private
         * Fires a delegated event.  Users should not invoke this method directly.  It
         * is called automatically by the framework as needed (see the "delegate" event
         * option of {@link Ext.util.Observable#addListener addListener} for more 
         * details.
         */
        doFireDelegatedEvent: function(eventName, args) {
            var me = this,
                ret = true,
                owner, delegatedEvents, event;
            // NOTE: $hasDelegatedListeners exists on the prototype of this mixin
            // which means it is inherited by both Ext.Component and Ext.Widget
            // This means that if any Component in the universe is listening for
            // the given eventName in a delegated manner, we need to traverse up the
            // hierarchy to see if that Component is in fact our ancestor, and if so
            // we need to fire the event on the ancestor.
            if (me.$hasDelegatedListeners[eventName]) {
                owner = me.getRefOwner();
                while (owner) {
                    delegatedEvents = owner.$delegatedEvents;
                    if (delegatedEvents) {
                        event = delegatedEvents[eventName];
                        if (event) {
                            ret = event.fireDelegated(me, args);
                            if (ret === false) {
                                break;
                            }
                        }
                    }
                    owner = owner.getRefOwner();
                }
            }
            return ret;
        },
        /**
         * @private
         * Removes delegated listeners for a given eventName, function, and scope.
         * Users should not invoke this method directly.  It is called automatically by
         * the framework as part of {@link #removeListener} processing.
         */
        removeDelegatedListener: function(eventName, fn, scope) {
            var me = this,
                delegatedEvents = me.$delegatedEvents,
                event;
            if (delegatedEvents) {
                event = delegatedEvents[eventName];
                if (event && event.removeListener(fn, scope)) {
                    me.$hasDelegatedListeners._decr_(eventName);
                    if (event.listeners.length === 0) {
                        delete delegatedEvents[eventName];
                    }
                }
            }
        },
        destroyComponentDelegation: function() {
            if (this.clearPropertiesOnDestroy) {
                this.$delegatedEvents = null;
            }
        }
    },
    onClassMixedIn: function(T) {
        // When a Component listener is attached with the "delegate" option, it means
        // All components anywhere in the hierarchy MUST now fire the event just in case
        // the Component with the delegate listener is an ancestor of the component that
        // fired the event (otherwise the ancestor will not have a chance to intercept
        // and process the event - see doFireDelegatedEvent).  To ensure that this happens
        // we chain the class-level hasListeners object of Ext.Component and Ext.Widget
        // to the single $hasDelegatedListeners object (see class-creation callback
        // of this class for more info)
        function HasListeners() {}
        T.prototype.HasListeners = T.HasListeners = HasListeners;
        HasListeners.prototype = T.hasListeners = new Ext.mixin.ComponentDelegation.HasDelegatedListeners();
    }
}, function(ComponentDelegation) {
    // Here We set up a HasListeners instance ($hasDelegatedListeners) that will be incremented
    // and decremented any time a Component or Widget adds or removes a listener using the
    // "delegate" event option.  This HasListeners instance is stored on the prototype
    // of the ComponentDelegation mixin, and therefore will be applied to the prototype
    // of both Ext.Component and Ext.Widget.  This means that Ext.Widget and Ext.Component
    // (intentionally) share the same $hasDelegatedListeners instance.  To understand the
    // reason for this common instance one must first understand how delegated events are
    // fired:
    //
    // When any component or widget fires an event of any kind, it must call doFireDelegatedEvent
    // to process possible delegated listeners.  The implementation of doFireDelegatedEvent
    // traverses up the component hierarchy searching for any ancestors that may be listening
    // for the event in a delegated manner; however, this traversal of the hierarchy can
    // be skipped if there are no known Components with delegated listeners for the given event.
    // The $hasDelegatedListeners instance is used to track whether or not there are any
    // delegated listeners for the given event name for this purpose.  Since Ext.Widgets
    // and Ext.Components can be part of the same hierarchy they must share the same
    // $hasDelegatesListeners instance.
    function HasDelegatedListeners() {}
    ComponentDelegation.HasDelegatedListeners = HasDelegatedListeners;
    HasDelegatedListeners.prototype = ComponentDelegation.prototype.$hasDelegatedListeners = new Ext.mixin.Observable.HasListeners();
});

/**
 * This is the base class from which all plugins should extend.
 *
 * This class defines the essential API of plugins as used by Components by defining the
 * following methods:
 *
 *  - `init` : The plugin initialization method which the host Component calls during
 *     Component initialization. The Component passes itself as the sole parameter.
 *     Subclasses should set up bidirectional links between the plugin and its host
 *     Component here.
 *
 *  - `destroy` : The plugin cleanup method which the host Component calls at Component
 *     destruction time. Use this method to break links between the plugin and the
 *     Component and to free any allocated resources.
 */
Ext.define('Ext.plugin.Abstract', {
    alternateClassName: 'Ext.AbstractPlugin',
    mixins: [
        'Ext.mixin.Identifiable'
    ],
    /**
     * @property {Boolean} isPlugin
     * The value `true` to identify objects of this class or a subclass thereof.
     * @readonly
     */
    isPlugin: true,
    /**
     * @cfg {String} id
     * An identifier for the plugin that can be set at creation time to later retrieve the
     * plugin using the {@link #getPlugin getPlugin} method. For example:
     *
     *      var panel = Ext.create({
     *          xtype: 'panel',
     *
     *          plugins: {
     *              foobar: {
     *                  id: 'foo',
     *                  ...
     *              }
     *          }
     *      });
     *
     *      // later on:
     *      var plugin = panel.getPlugin('foo');
     * @since 6.2.0
     */
    /**
     * @cfg {String} pluginId
     * @deprecated 6.2.0 Use `id` instead
     */
    /**
     * Initializes the plugin.
     * @param {Object} [config] Configuration object.
     */
    constructor: function(config) {
        if (config) {
            this.cmp = config.cmp;
            this.pluginConfig = config;
            this.initConfig(config);
        }
    },
    /**
     * @method init
     * The init method is invoked to formally associate the host component and the plugin.
     *
     * Subclasses should perform initialization and set up any requires links between the
     * plugin and its host Component in their own implementation of this method.
     * @param {Ext.Component} host The host Component which owns this plugin.
     */
    init: Ext.emptyFn,
    /**
     * The destroy method is invoked by the owning Component at the time the Component is
     * being destroyed.
     */
    destroy: function() {
        var me = this;
        me.destroy = Ext.emptyFn;
        me.destroying = true;
        me.cmp = me.pluginConfig = null;
        me.doDestroy();
        me.callParent();
    },
    // This just makes it hard to ask "was destroy() called?":
    // me.destroying = false; // removed in 7.0
    doDestroy: Ext.emptyFn,
    /**
     * Creates clone of the plugin.
     * @param {Object} [overrideCfg] Additional config for the derived plugin.
     */
    clonePlugin: function(overrideCfg) {
        return new this.self(Ext.apply({}, overrideCfg, this.pluginConfig));
    },
    /**
     * @method detachCmp
     * Plugins that can be disconnected from their host component should implement
     * this method.
     * @since 6.2.0
     */
    /**
     * Returns the component to which this plugin is attached.
     * @return {Ext.Component} The owning host component.
     */
    getCmp: function() {
        return this.cmp;
    },
    /**
     * Sets the host component to which this plugin is attached. For a plugin to be
     * removable without being destroyed, this method should be provided and be prepared
     * to receive `null` for the component.
     * @param {Ext.Component} host The owning host component.
     */
    setCmp: function(host) {
        this.cmp = host;
    },
    getStatefulOwner: function() {
        return [
            this.cmp,
            'plugins'
        ];
    },
    onClassExtended: function(cls, data, hooks) {
        var alias = data.alias,
            prototype = cls.prototype;
        // Inject a ptype property so that findPlugin() works.
        if (alias && !data.ptype) {
            if (Ext.isArray(alias)) {
                alias = alias[0];
            }
            prototype.ptype = alias.split('plugin.')[1];
        }
    },
    resolveListenerScope: function(defaultScope) {
        var me = this,
            cmp = me.getCmp(),
            scope;
        if (cmp) {
            scope = cmp.resolveSatelliteListenerScope(me, defaultScope);
        }
        // If this method was called, it means the plugin subclass must
        // have mixed in Observable, so we can rely on there being
        // a "this.mixins.observable" even though Ext.plugin.Abstract
        // does not mix it in directly
        return scope || me.mixins.observable.resolveListenerScope.call(me, defaultScope);
    },
    statics: {
        decode: function(plugins, typeProp, include) {
            if (plugins) {
                // eslint-disable-next-line vars-on-top
                var type = Ext.typeOf(plugins),
                    // 'object', 'array', 'string'
                    entry, key, obj, value;
                if (type === 'string') {
                    obj = {};
                    // allows for findPlugin to find a plugin
                    // defined as a string
                    obj[typeProp] = plugins;
                    plugins = [
                        obj
                    ];
                } else if (plugins.isInstance) {
                    plugins = [
                        plugins
                    ];
                } else if (type === 'object') {
                    if (plugins[typeProp]) {
                        plugins = [
                            plugins
                        ];
                    } else {
                        obj = include ? Ext.merge(Ext.clone(include), plugins) : plugins;
                        plugins = [];
                        for (key in obj) {
                            if (!(value = obj[key])) {
                                
                                continue;
                            }
                            entry = {
                                id: key
                            };
                            entry[typeProp] = key;
                            Ext.apply(entry, value);
                            plugins.push(entry);
                        }
                        Ext.sortByWeight(plugins);
                    }
                } else if (type !== 'array') {
                    Ext.raise('Invalid value for "plugins" config ("' + type + '"');
                } else {
                    plugins = plugins.slice();
                }
            }
            // so that all cases return mutable array
            return plugins;
        }
    }
});

/**
 * @class Ext.plugin.Abstract
 */
Ext.define('Ext.overrides.plugin.Abstract', {
    override: 'Ext.plugin.Abstract',
    $configStrict: false,
    $configPrefixed: false,
    disabled: false,
    /**
     * @cfg {String|Array} stateEvents
     * The configured list of stateEvents used to (optionally) participate in Owner Component's
     * state management.
     * @member Ext.plugin.Abstract
     */
    /**
     * @method
     * The getState method is invoked by the client Component's State mixin when one or more of the
     * specified {@link #stateEvents} are raised.
     *
     * The supplied implementation is empty. If plugin Subclasses are to (optionally) participate
     * in the client Component's state management, implementers should provide a suitable method
     * which returns a state object.
     * @return {Object} state
     * @member Ext.plugin.Abstract
     */
    getState: null,
    /**
     * @method
     * The applyState method is invoked by the client Component's State mixin after initComponent
     * method has been run for the client.
     *
     * The supplied implementation is empty. If plugin Subclasses are to (optionally) participate
     * in the client Component's state management, implementers should provide a suitable method
     * to utilize it.
     * @param {Object} state The current plugin state object to be applied.
     * @param {Object} allState The current aggregate state of the Component and all plugins.
     * @member Ext.plugin.Abstract
     */
    applyState: null,
    /**
     * The base implementation just sets the plugin's `disabled` flag to `false`
     *
     * Plugin subclasses which need more complex processing may implement an overriding
     * implementation.
     * @member Ext.plugin.Abstract
     */
    enable: function() {
        this.disabled = false;
    },
    /**
     * The base implementation just sets the plugin's `disabled` flag to `true`
     *
     * Plugin subclasses which need more complex processing may implement an overriding
     * implementation.
     * @member Ext.plugin.Abstract
     */
    disable: function() {
        this.disabled = true;
    }
});

/**
 * This mixin provides support for a `plugins` config and related API's.
 *
 * If this mixin is used for non-Components, the statements regarding the host being a
 * Component can be translated accordingly. The only requirement on the user of this class
 * is that the plugins actually used be appropriate for their host.
 *
 * While `Ext.Component` in the Classic Toolkit supports `plugins`, it does not use this
 * class to provide that support. This is due to backwards compatibility in regard to
 * timing changes this implementation would present.
 *
 * **Important:** To ensure plugins are destroyed, call `setPlugins(null)`.
 * @protected
 * @since 6.2.0
 */
Ext.define('Ext.mixin.Pluggable', function(Pluggable) {
    return {
        // eslint-disable-line brace-style
        requires: [
            'Ext.plugin.Abstract'
        ],
        mixinId: 'pluggable',
        config: {
            /**
         * @cfg {Array/Ext.enums.Plugin/Object/Ext.plugin.Abstract} plugins
         * This config describes one or more plugin config objects used to create plugin
         * instances for this component.
         *
         * Plugins are a way to bundle and reuse custom functionality. Plugins should extend
         * `Ext.plugin.Abstract` but technically the only requirement for a valid plugin
         * is that it contain an `init` method that accepts a reference to its owner. Once
         * a plugin is created, the owner will call the `init` method, passing a reference
         * to itself. Each plugin can then call methods or respond to events on its owner
         * as needed to provide its functionality.
         *
         * This config's value can take several different forms.
         *
         * The value can be a single string with the plugin's {@link Ext.enums.Plugin alias}:
         *
         *      var list = Ext.create({
         *          xtype: 'list',
         *          itemTpl: '<div class="item">{title}</div>',
         *          store: 'Items',
         *
         *          plugins: 'listpaging'
         *      });
         *
         * In the above examples, the string "listpaging" is the type alias for
         * `Ext.dataview.plugin.ListPaging`. The full alias includes the "plugin." prefix
         * (i.e., 'plugin.listpaging').
         *
         * The preferred form for multiple plugins or to configure plugins is the
         * keyed-object form (new in version 6.5):
         *
         *      var list = Ext.create({
         *          xtype: 'list',
         *          itemTpl: '<div class="item">{title}</div>',
         *          store: 'Items',
         *
         *          plugins: {
         *              pullrefresh: true,
         *              listpaging: {
         *                  autoPaging: true,
         *                  weight: 10
         *              }
         *          }
         *      });
         *
         * The object keys are the `id`'s as well as the default type alias. This form
         * allows the value of the `plugins` to be merged from base class to derived class
         * and finally with the instance configuration. This allows classes to define a
         * set of plugins that derived classes or instantiators can further configure or
         * disable. This merge behavior is a feature of the
         * {@link Ext.Class#cfg!config config system}.
         *
         * The `plugins` config can also be an array of plugin aliases (arrays are not
         * merged so this form does not respect plugins defined by the class author):
         *
         *      var list = Ext.create({
         *          xtype: 'list',
         *          itemTpl: '<div class="item">{title}</div>',
         *          store: 'Items',
         *
         *          plugins: ['listpaging', 'pullrefresh']
         *      });
         *
         * An array can also contain elements that are config objects with a `type`
         * property holding the type alias:
         *
         *      var list = Ext.create({
         *          xtype: 'list',
         *          itemTpl: '<div class="item">{title}</div>',
         *          store: 'Items',
         *
         *          plugins: ['pullrefresh', {
         *              type: 'listpaging',
         *              autoPaging: true
         *          }]
         *      });
         */
            plugins: null
        },
        /**
     * Adds a plugin. For example:
     *
     *      list.addPlugin('pullrefresh');
     *
     * Or:
     *
     *      list.addPlugin({
     *          type: 'pullrefresh',
     *          pullRefreshText: 'Pull to refresh...'
     *      });
     *
     * @param {Object/String/Ext.plugin.Abstract} plugin The plugin or config object or
     * alias to add.
     * @since 6.2.0
     */
        addPlugin: function(plugin) {
            var me = this,
                plugins = me.getPlugins();
            if (plugins) {
                plugin = me.createPlugin(plugin);
                plugin.init(me);
                plugins.push(plugin);
            } else {
                me.setPlugins(plugin);
                plugin = me.getPlugins()[0];
            }
            return plugin;
        },
        /**
     * Removes and destroys a plugin.
     *
     * **Note:** Not all plugins are designed to be removable. Consult the documentation
     * for the specific plugin in question to be sure.
     * @param {String/Ext.plugin.Abstract} plugin The plugin or its `id` to remove.
     * @return {Ext.plugin.Abstract} plugin instance or `null` if not found.
     * @since 6.2.0
     */
        destroyPlugin: function(plugin) {
            return this.removePlugin(plugin, true);
        },
        /**
     * Retrieves plugin by its `type` alias. For example:
     *
     *      var list = Ext.create({
     *          xtype: 'list',
     *          itemTpl: '<div class="item">{title}</div>',
     *          store: 'Items',
     *
     *          plugins: ['listpaging', 'pullrefresh']
     *      });
     *
     *      list.findPlugin('pullrefresh').setPullRefreshText('Pull to refresh...');
     *
     * **Note:** See also {@link #getPlugin}.
     *
     * @param {String} type The Plugin's `type` as specified by the class's
     * {@link Ext.Class#cfg-alias alias} configuration.
     * @return {Ext.plugin.Abstract} plugin instance or `null` if not found.
     * @since 6.2.0
     */
        findPlugin: function(type) {
            var plugins = this.getPlugins(),
                n = plugins && plugins.length,
                i, plugin, ret;
            for (i = 0; i < n && !ret; i++) {
                plugin = plugins[i];
                // Classic used ptype forever, so support it too but Core/Modern just use
                // type.
                if (plugin.type === type || plugin.ptype === type) {
                    ret = plugin;
                }
            }
            return ret || null;
        },
        /**
     * Retrieves a plugin by its `id`.
     *
     *      var list = Ext.create({
     *          xtype: 'list',
     *          itemTpl: '<div class="item">{title}</div>',
     *          store: 'Items',
     *
     *          plugins: [{
     *              type: 'pullrefresh',
     *              id: 'foo'
     *          }]
     *      });
     *
     *      list.getPlugin('foo').setPullRefreshText('Pull to refresh...');
     *
     * **Note:** See also {@link #findPlugin}.
     *
     * @param {String} id The `id` of the plugin.
     * @return {Ext.plugin.Abstract} plugin instance or `null` if not found.
     * @since 6.2.0
     */
        getPlugin: function(id) {
            var plugins = this.getPlugins(),
                n = plugins && plugins.length,
                i, plugin, ret;
            for (i = 0; i < n && !ret; i++) {
                plugin = plugins[i];
                // Classic used pluginId, so support it too but Core/Modern just use id.
                if (plugin.id === id || plugin.pluginId === id) {
                    ret = plugin;
                }
            }
            return ret || null;
        },
        /**
     * Removes and (optionally) destroys a plugin.
     *
     * **Note:** Not all plugins are designed to be removable. Consult the documentation
     * for the specific plugin in question to be sure.
     * @param {String/Ext.plugin.Abstract} plugin The plugin or its `id` to remove.
     * @param {Boolean} [destroy] Pass `true` to not call `destroy()` on the plugin.
     * @return {Ext.plugin.Abstract} plugin instance or `null` if not found.
     * @since 6.2.0
     */
        removePlugin: function(plugin, destroy) {
            var plugins = this.getPlugins(),
                i = plugins && plugins.length || 0,
                p;
            while (i-- > 0) {
                p = plugins[i];
                if (p === plugin || p.id === plugin) {
                    plugins.splice(i, 1);
                    if (destroy) {
                        if (p.destroy) {
                            p.destroy();
                        }
                    } else if (p.detachCmp) {
                        p.detachCmp();
                        if (p.setCmp) {
                            p.setCmp(null);
                        }
                    }
                    break;
                }
                p = null;
            }
            return p;
        },
        privates: {
            statics: {
                idSeed: 0
            },
            /**
         * Creates a particular plugin type if defined in the `plugins` configuration.
         * @param {String} type The `type` of the plugin.
         * @return {Ext.plugin.Abstract} The plugin that was created.
         * @private
         * @since 6.2.0
         */
            activatePlugin: function(type) {
                var me = this,
                    config = me.initialConfig,
                    plugins = config && config.plugins,
                    ret = null,
                    i, include, p;
                if (plugins) {
                    include = me.config.plugins;
                    include = (include && typeof include === 'object') ? include : null;
                    plugins = Ext.plugin.Abstract.decode(plugins, 'type', include);
                    for (i = plugins.length; i-- > 0; ) {
                        p = plugins[i];
                        if (p === type || p.type === type) {
                            me.initialConfig = config = Ext.apply({}, config);
                            config.plugins = plugins;
                            // switch over to our copy
                            // Put the instance in the plugins array so it will be included in
                            // the applyPlugins loop for normal processing of plugins.
                            plugins[i] = ret = me.createPlugin(p);
                            break;
                        }
                    }
                }
                return ret;
            },
            /**
         * Applier for the `plugins` config property.
         * @param {String[]/Object[]/Ext.plugin.Abstract[]} plugins The new plugins to use.
         * @param {Ext.plugin.Abstract[]} oldPlugins The existing plugins in use.
         * @private
         */
            applyPlugins: function(plugins, oldPlugins) {
                var me = this,
                    oldCount = oldPlugins && oldPlugins.length || 0,
                    count, i, plugin;
                // Ensure we have an array if we got a single thing or a copy of the array
                // if we got an array.
                if (plugins) {
                    plugins = Ext.plugin.Abstract.decode(plugins, 'type');
                }
                // We need to destroy() old plugins that aren't being brought forward in
                // the new array...
                //
                for (i = 0; i < oldCount; ++i) {
                    oldPlugins[i].$dead = true;
                }
                // so paint the old ones
                // Pass #1 (For historical reasons): Create all of the plugins. Prior versions
                // did this pass first then called init() so we preserve the timings and do
                // the same.
                //
                count = plugins && plugins.length || 0;
                for (i = 0; i < count; ++i) {
                    plugins[i] = me.createPlugin(plugins[i]);
                }
                // ensure we have an instance
                // Pass #2: Initialize the plugins that have not been and clear $dead for
                // any returning for the next round.
                //
                for (i = 0; i < count; ++i) {
                    plugin = plugins[i];
                    if (plugin.$dead) {
                        // if (it was in oldPlugins)
                        delete plugin.$dead;
                    } else // unpaint it (it's a keeper)
                    {
                        plugin.init(me);
                    }
                }
                // this one is new to the party
                // Now we can teardown any plugins that aren't coming back.
                //
                for (i = 0; i < oldCount; ++i) {
                    if ((plugin = oldPlugins[i]).$dead) {
                        delete plugin.$dead;
                        Ext.destroy(plugin);
                    }
                }
                return plugins;
            },
            /**
         * Converts the provided type or config object into a plugin instance.
         * @param {String/Object/Ext.plugin.Abstract} config The plugin type, config
         * object or instance.
         * @return {Ext.plugin.Abstract}
         * @private
         */
            createPlugin: function(config) {
                var ret;
                if (typeof config === 'string') {
                    config = {
                        type: config
                    };
                }
                ret = config;
                if (!config.isInstance) {
                    // The owner may be needed by plugin's initConfig so provide it:
                    config.cmp = this;
                    ret = Ext.factory(config, null, null, 'plugin');
                    // Cleanup the user's config object:
                    delete config.cmp;
                }
                if (!ret.id) {
                    ret.id = ++Pluggable.idSeed;
                }
                if (ret.setCmp) {
                    ret.setCmp(this);
                }
                return ret;
            }
        }
    };
});

/**
 * A mixin for components that need to interact with the keyboard. The primary config
 * for this class is the `{@link #keyMap keyMap}` config. This config is an object
 * with key names as its properties and with values that describe how the key event
 * should be handled.
 *
 * Key names may key name as documented in `Ext.event.Event`, numbers (which are treated
 * as `keyCode` values), single characters (for those that are not defined in
 * `Ext.event.Event`) or `charCode` values prefixed by '#' (e.g., "#65" for `charCode=65`).
 *
 * Entries that use a `keyCode` will be processed in a `keydown` event listener, while
 * those that use a `charCode` will be processed in `keypress`. This can be overridden
 * if the `keyMap` entry specifies an `event` property.
 *
 * Key names may be preceded by key modifiers. The modifier keys can be specified
 * by prepending the modifier name to the key name separated by `+` or `-` (e.g.,
 * "Ctrl+A" or "Ctrl-A"). Only one of these delimiters can be used in a given
 * entry.
 *
 * Valid modifier names are:
 *
 *  - Alt
 *  - Shift
 *  - Control (or "Ctrl" for short)
 *  - Command (or "Cmd" or "Meta")
 *  - CommandOrControl (or "CmdOrCtrl") for Cmd on Mac, Ctrl otherwise.
 *
 * *All these names are case insensitive and will be stored in upper case internally.*
 *
 * For example:
 *
 *      Ext.define('MyChartPanel', {
 *          extend: 'Ext.panel.Panel',
 *
 *          mixins: [
 *              'Ext.mixin.Keyboard'
 *          ],
 *
 *          controller: 'mycontroller',
 *
 *          // Map keys to methods (typically in a ViewController):
 *          keyMap: {
 *              ENTER: 'onEnterKey',
 *
 *              "ALT+PRINT_SCREEN": 'doScreenshot',
 *
 *              // Cmd on Mac OS X, Ctrl on Windows/Linux.
 *              "CmdOrCtrl+C": 'doCopy',
 *
 *              // This one is handled by a class method.
 *              ESC: {
 *                  handler: 'destroy',
 *                  scope: 'this',
 *                  event: 'keypress'  // default would be keydown
 *              },
 *
 *              "ALT+DOWN": 'openExpander',
 *
 *              // Match any key modifiers and invoke before any other DOWN keys
 *              // handlers with lower or default priority.
 *              "*+DOWN": {
 *                  handler: 'preprocessDownKey',
 *                  priority: 100
 *              }
 *          }
 *      });
 *
 * The method names are interpreted in the same way that event listener names are
 * interpreted.
 *
 * @since 6.2.0
 */
Ext.define('Ext.mixin.Keyboard', function(Keyboard) {
    return {
        // eslint-disable-line brace-style
        extend: 'Ext.Mixin',
        mixinConfig: {
            id: 'keyboard'
        },
        /**
     * @property {Ext.event.Event} lastKeyMapEvent
     * The last key event processed is cached on the component for use in subsequent
     * event handlers.
     * @since 6.6.0
     */
        config: {
            /**
         * @cfg {Object} keyMap
         * An object containing handlers for keyboard events. The property names of this
         * object are the key name and any modifiers. The values of the properties are the
         * descriptors of how to handle each event.
         *
         * The handler descriptor can be simply the handler function(either the
         * literal function or the method name), or it can be an object with these
         * properties:
         *
         *  - `handler`: The function or its name to call to handle the event.
         *  - `scope`: The this pointer context (can be "this" or "controller").
         *  - `event`: An optional override of the key event to which to listen.
         *
         * **Important:** Calls to `setKeyMap` do not replace the entire `keyMap` but
         * instead update the provided mappings. That is, unless `null` is passed as the
         * value of the `keyMap` which will clear the `keyMap` of all entries.
         *
         * @cfg {String} [keyMap.scope] The default scope to apply to key handlers
         * which do not specify a scope. This is processed the same way as the scope of
         * {@link #cfg-listeners}. It defaults to the `"controller"`, but using `'this'`
         * means that an instance method will be used.
         */
            keyMap: {
                $value: null,
                cached: true,
                merge: function(value, baseValue, cls, mixin) {
                    var ret, key, ucKey, v, vs;
                    // Allow nulling out parent class config
                    if (value === null) {
                        return value;
                    }
                    // We promote all values into objects but these objects do not get
                    // merged with base class values. Further, the keys get toUpperCased
                    // to normalize this aspect ('esc' vs 'ESC' vs 'Esc'). We do not want
                    // to overwrite a class baseValue with an instances value since those
                    // are additive (in applyKeyMap/combineKeyMaps).
                    ret = (baseValue && !cls.isInstance) ? Ext.Object.chain(baseValue) : {};
                    for (key in value) {
                        if (key !== 'scope') {
                            ucKey = key.toUpperCase();
                            if (!mixin || ret[ucKey] === undefined) {
                                // Promote to an object so we can always store the scope.
                                v = value[key];
                                if (v) {
                                    if (typeof v === 'string' || typeof v === 'function') {
                                        v = {
                                            handler: v
                                        };
                                    } else {
                                        v = Ext.apply({
                                            handler: v.fn
                                        }, // overwritten by v.handler
                                        v);
                                    }
                                    vs = v.scope || value.scope || 'self';
                                    v.scope = (vs === 'controller') ? 'self.controller' : vs;
                                }
                                ret[ucKey] = v;
                            }
                        }
                    }
                    return ret;
                }
            },
            /**
         * @cfg {Boolean} keyMapEnabled
         * Enables or disables processing keys in the `keyMap`. This value starts as
         * `null` and if it is `null` when `initKeyMap` is called, it will automatically
         * be set to `true`. Since `initKeyMap` is called by `Ext.Component` at the
         * proper time, this is not something application code normally handles.
         */
            keyMapEnabled: null
        },
        /**
     * @cfg {String} keyMapTarget
     * The name of the member that should be used to listen for keydown/keypress events.
     * This is intended to be controlled at the class level not per instance.
     * @protected
     */
        keyMapTarget: 'el',
        applyKeyMap: function(keyMap, existingKeyMap) {
            var me = this,
                // During cached config setup, we don't yet have our own (instance) "config"
                // so we can tell from that being present that we need our own keyMap.
                own = me.hasOwnProperty('config');
            if (own && existingKeyMap && existingKeyMap.$owner !== me) {
                // As a cached config, we can be created with an existing value, but
                // we do not want to modify that shared instance, so make a copy.
                existingKeyMap = Ext.apply({}, existingKeyMap);
            }
            keyMap = keyMap ? Keyboard.combineKeyMaps(existingKeyMap, keyMap, own && me) : null;
            if (me._keyMapReady) {
                me.setKeyMapListener(keyMap && me.getKeyMapEnabled());
            }
            return keyMap;
        },
        /**
     * This method should be called when the instance is ready to start listening for
     * keyboard events. This is called automatically for `Ext.Component` and derived
     * classes. This is done after the component is rendered.
     * @protected
     */
        initKeyMap: function() {
            var me = this,
                enabled = me.getKeyMapEnabled();
            me._keyMapReady = true;
            if (enabled === null) {
                me.setKeyMapEnabled(true);
            } else {
                me.setKeyMapListener(enabled && me.getKeyMap());
            }
        },
        disableKeyMapGroup: function(group) {
            this.setKeyMapGroupEnabled(group, false);
        },
        enableKeyMapGroup: function(group) {
            this.setKeyMapGroupEnabled(group, true);
        },
        setKeyMapGroupEnabled: function(group, state) {
            var me = this,
                disabledGroups = me.disabledKeyMapGroups || (me.disabledKeyMapGroups = {});
            disabledGroups[group] = !state;
        },
        updateKeyMapEnabled: function(enabled) {
            this.setKeyMapListener(enabled && this._keyMapReady && this.getKeyMap());
        },
        privates: {
            _keyMapListenCount: 0,
            _keyMapReady: false,
            // Descending priority sort
            comparePriorities: function(lhs, rhs) {
                return (rhs.priority || 0) - (lhs.priority || 0);
            },
            findKeyMapEntries: function(e) {
                var me = this,
                    disabledGroups = me.disabledKeyMapGroups,
                    keyMap = me.getKeyMap(),
                    entries = keyMap && Keyboard.getKeyName(e),
                    result = [],
                    entry, len, i;
                entries = entries && keyMap[entries];
                if (entries) {
                    // Ensure that the entries are in priority order
                    if (!entries.sorted) {
                        Ext.Array.sort(entries, me.comparePriorities);
                        entries.sorted = true;
                    }
                    len = entries.length;
                    for (i = 0; i < len; i++) {
                        entry = entries[i];
                        // If the key code and the modifier flags match, add entry
                        // to invocation list.
                        if (!disabledGroups || !disabledGroups[entry.group]) {
                            if (Keyboard.matchEntry(entry, e)) {
                                result.push(entry);
                            }
                        }
                    }
                }
                return result;
            },
            onKeyMapEvent: function(e) {
                var me = this,
                    entries = me.getKeyMapEnabled() ? me.findKeyMapEntries(e) : null,
                    len = entries && entries.length,
                    i, entry, result;
                me.lastKeyMapEvent = e;
                for (i = 0; i < len && result !== false; i++) {
                    entry = entries[i];
                    result = Ext.callback(entry.handler, entry.scope, [
                        e,
                        this
                    ], 0, this);
                }
                return result;
            },
            setKeyMapListener: function(enabled) {
                var me = this,
                    listener = me._keyMapListener,
                    eventSource;
                if (listener) {
                    // We always destroy the old listener since the eventSource could be
                    // different now...
                    listener.destroy();
                    listener = null;
                }
                if (enabled) {
                    ++me._keyMapListenCount;
                    if (enabled) {
                        eventSource = me[me.keyMapTarget];
                        if (typeof eventSource === 'function') {
                            eventSource = eventSource.call(me);
                        }
                        // eg, 'getFocusEl'
                        listener = eventSource.on({
                            destroyable: true,
                            scope: me,
                            keydown: 'onKeyMapEvent',
                            keypress: 'onKeyMapEvent'
                        });
                    }
                }
                me._keyMapListener = listener || null;
            },
            statics: {
                _charCodeRe: /^#([\d]+)$/,
                // eslint-disable-next-line max-len, no-useless-escape
                _keySpecRe: /^(?:(?:(\*)[\+\-])|(?:([a-z\+\-]*)[\+\-]))?(?:([a-z0-9_]+|[\+\-]|(?:#?\d+))(?:\:([a-z]+))?)$/i,
                _delimiterRe: /-|\+/,
                _keyMapEvents: {
                    charCode: 'keypress',
                    keyCode: 'keydown'
                },
                combineKeyMaps: function(existingKeyMap, keyMap, owner) {
                    var defaultScope = keyMap.scope || 'controller',
                        entry, key, mapping, existingMapping;
                    for (key in keyMap) {
                        if (key === 'scope') {
                            
                            continue;
                        }
                        if (!(mapping = keyMap[key])) {
                            if (mapping === undefined) {
                                Ext.raise('keyMap entry "' + key + '" is undefined');
                            }
                            // if we have no mapping (eg, "ESC: null") and no mappings to
                            // overwrite, we can skip over it.
                            if (!existingKeyMap) {
                                
                                continue;
                            }
                        } else {
                            if (typeof mapping === 'string' || typeof mapping === 'function') {
                                // Direct calls to setKeyMap() can get here because
                                // instance and class configs go through merge
                                mapping = {
                                    handler: mapping,
                                    scope: defaultScope
                                };
                            } else if (mapping) {
                                mapping = Ext.apply({
                                    handler: mapping.fn,
                                    // mapping.handler will override
                                    scope: defaultScope
                                }, // mapping.scope will override
                                // all other properties of mapping are kept
                                mapping);
                            }
                            existingKeyMap = existingKeyMap || {};
                        }
                        // we'll need a keyMap
                        if (Keyboard.parseEntry(key, entry = mapping || {})) {
                            // Key modifiers are stripped off the key name
                            // so we end up with an object like this:
                            //
                            //  "PRINT_SCREEN": {
                            //      handler: 'doSummat',
                            //      scope: 'controller',
                            //      altKey: true
                            //  }
                            //
                            // or
                            //
                            //  "UP": {
                            //      handler: 'doSummat'
                            //      scope: 'controller',
                            //      ignoreModifiers: true
                            //  }
                            //
                            existingMapping = existingKeyMap[entry.name];
                            if (existingMapping) {
                                if (owner && existingMapping.$owner !== owner) {
                                    existingKeyMap[entry.name] = existingMapping = existingMapping.slice();
                                    existingMapping.$owner = owner;
                                }
                                existingMapping.push(mapping);
                                existingMapping.sorted = false;
                            } else {
                                existingMapping = existingKeyMap[entry.name] = [
                                    mapping
                                ];
                                existingMapping.$owner = owner;
                                existingMapping.sorted = true;
                            }
                        } else {
                            Ext.raise('Invalid keyMap key specification "' + key + '"');
                        }
                    }
                    if (existingKeyMap && owner) {
                        existingKeyMap.$owner = owner;
                    }
                    return existingKeyMap || null;
                },
                getKeyName: function(event) {
                    var keyCode;
                    if (event.isEvent) {
                        keyCode = event.keyCode || event.charCode;
                        event = event.browserEvent;
                        // If it's the combination code, 229, then use the W3C code property.
                        // https://developer.mozilla.org/en/docs/Web/API/KeyboardEvent/code
                        if (keyCode === 229 && 'code' in event) {
                            if (Ext.String.startsWith(event.code, 'Key')) {
                                return event.key.substr(3);
                            }
                            if (Ext.String.startsWith(event.code, 'Digit')) {
                                return event.key.substr(5);
                            }
                        }
                    } else {
                        keyCode = event;
                    }
                    // We are in a position of having a numeric key code, attempt to translate it
                    // to a name.
                    return Ext.event.Event.keyCodes[keyCode] || String.fromCharCode(keyCode);
                },
                matchEntry: function(entry, e) {
                    var ev = e.browserEvent,
                        code;
                    if (e.type !== entry.event) {
                        return false;
                    }
                    if (!(code = entry.charCode)) {
                        if (entry.keyCode !== e.keyCode || (!entry.ignoreModifiers && !entry.shiftKey !== !ev.shiftKey)) {
                            // when using keyCode, SHIFT must match too
                            return false;
                        }
                    } else if (e.getCharCode() !== code) {
                        return false;
                    }
                    // NOTE: All modifier key properties are !-ed to ensure boolean-ness since
                    // they can be undefined...
                    // Entry can be flagged to ignore modifiers and invoke purely on key match.
                    return entry.ignoreModifiers || (!entry.ctrlKey === !ev.ctrlKey && !entry.altKey === !ev.altKey && !entry.metaKey === !ev.metaKey && !entry.shiftKey === !ev.shiftKey);
                },
                parseEntry: function(key, entry) {
                    key = key.toUpperCase();
                    // eslint-disable-next-line vars-on-top
                    var me = this,
                        Event = Ext.event.Event,
                        keyFlags = Event.keyFlags,
                        parts = me._keySpecRe.exec(key),
                        type = 'keyCode',
                        name, code, i, match, n;
                    // The _keySpecRe will split up a string thus:
                    //
                    // 'ALT+CTRL+A:GROUP' -> [.., undefined, "ALT+CTRL", "A", "GROUP"]
                    //
                    // '*+A:GROUP' -> [.., "*", undefined, "A", "GROUP"]
                    //
                    // 'ALT+CTRL+A' -> [.., undefined, "ALT+CTRL", "A", undefined]
                    //
                    // So parts is:
                    // [0] - Whole matched string
                    // [1] - All modifiers indicator, ie: '*'
                    // [2] - Delimited modifiers list, eg: 'ctrl+alt'
                    // [3] - The key name
                    // [4] - The optional group name
                    if (parts) {
                        name = parts[3];
                        if (parts[4]) {
                            entry.group = parts[4];
                        }
                        // If "*" modifier used, then means ignore modifiers and invoke
                        // on raw key match.
                        if (!(entry.ignoreModifiers = !!parts[1]) && parts[2]) {
                            // Otherwise set flags according to modifer names if any.
                            parts = parts[2].split(me._delimiterRe);
                            n = parts.length;
                            for (i = 0; i < n; i++) {
                                if (!keyFlags[parts[i]]) {
                                    return false;
                                }
                                entry[keyFlags[parts[i]]] = true;
                            }
                        }
                        // Entry is named by the unmodified key name.
                        // Entries for the same key are kept as a prioritized array.
                        entry.name = name;
                        // Set the keyCode from the 'PRINT_SCREEN' key name.
                        if (isNaN(code = Event[name])) {
                            // Support charCode from a single letter or '#65' format.
                            if (!(match = me._charCodeRe.exec(name))) {
                                if (name.length === 1) {
                                    code = name.charCodeAt(0);
                                }
                            } else {
                                code = +match[1];
                            }
                            // #42
                            if (code) {
                                type = 'charCode';
                            } else {
                                // Last chance! Just a number (keyCode) like "27: 'onEscape'"?
                                code = +name;
                            }
                            entry.name = Keyboard.getKeyName(code);
                        }
                        entry.event = entry.event || me._keyMapEvents[type];
                        return !isNaN(code) && (entry[type] = code);
                    }
                }
            }
        }
    };
});
// statics
// privates

/**
 * This mixin implements focus management functionality in Widgets and Components
 */
Ext.define('Ext.mixin.Focusable', {
    mixinId: 'focusable',
    $isFocusableEntity: true,
    // tabIndex config is now defined in Ext.Component
    /**
     * @property {Boolean} focusable
     * @readonly
     *
     * `true` for keyboard interactive Components or Widgets, `false` otherwise.
     * For Containers, this property reflects interactiveness of the
     * Container itself, not its children. See {@link #isFocusable}.
     *
     * **Note:** It is not enough to set this property to `true` to make
     * a component keyboard interactive. You also need to make sure that
     * the component's {@link #focusEl} is reachable via Tab key (tabbable).
     * See also {@link #tabIndex}.
     */
    focusable: false,
    /**
     * @property {Boolean} hasFocus `true` if this component's {@link #focusEl} is focused.
     * See also {@link #containsFocus}.
     *
     * @readonly
     */
    hasFocus: false,
    /**
     * @property {Boolean} containsFocus `true` if this currently focused element
     * is within this Component's or Container's hierarchy. This property is set separately
     * from {@link #hasFocus}, and can be `true` when `hasFocus` is `false`.
     *
     * Examples:
     *
     * + Text field with input element focused would be:
     *      focusable: true,
     *      hasFocus: true,
     *      containsFocus: true
     *
     * + Date field with drop-down picker currently focused would be:
     *      focusable: true,
     *      hasFocus: false,
     *      containsFocus: true
     *
     * + Form Panel with a child input field currently focused would be:
     *      focusable: false,
     *      hasFocus: false,
     *      containsFocus: true
     *
     * See also {@link #hasFocus}.
     *
     * @readonly
     */
    containsFocus: false,
    /**
     * @cfg {String} [focusCls='x-focused'] CSS class that will be added to focused
     * component's {@link #focusClsEl}, and removed when component blurs.
     */
    focusCls: Ext.baseCSSPrefix + 'focused',
    /**
     * @property {Ext.dom.Element} focusEl The element that will be focused
     * when {@link #method!focus} method is called on this component. Usually this is
     * the same element that receives focus via mouse clicks, taps, and pressing
     * Tab key.
     */
    focusEl: 'el',
    /**
     * @property {Ext.dom.Element} focusClsEl The element that will have the
     * {@link #focusCls} applied when component's {@link #focusEl} is focused.
     */
    /**
     * @event focus
     * Fires when this Component's {@link #focusEl} receives focus.
     * @param {Ext.Component/Ext.Widget} this
     * @param {Ext.event.Event} event The focus event.
     */
    /**
     * @event blur
     * Fires when this Component's {@link #focusEl} loses focus.
     * @param {Ext.Component} this
     * @param {Ext.event.Event} event The blur event.
     */
    /**
     * @event focusenter
     * Fires when focus enters this Component's hierarchy.
     * @param {Ext.Component} this
     * @param {Ext.event.Event} event The focusenter event.
     */
    /**
     * @event focusleave
     * Fires when focus leaves this Component's hierarchy.
     * @param {Ext.Component} this
     * @param {Ext.event.Event} event The focusleave event.
     */
    /**
     * Returns the main focus holder element associated with this Focusable, i.e.
     * the element that will be focused when Focusable's {@link #method!focus} method is
     * called. For most Focusables, this will be the {@link #focusEl}.
     *
     * @return {Ext.dom.Element}
     * @protected
     */
    getFocusEl: function() /* private e */
    {
        var focusEl = this.focusEl;
        return focusEl && focusEl.dom ? focusEl : null;
    },
    /**
     * Returns the element used to apply focus styling CSS class when Focusable's
     * {@link #focusEl} becomes focused. By default it is {@link #focusEl}.
     *
     * @param {Ext.dom.Element} [focusEl] Return focus styling element for the given
     * focused element. This is used by Components implementing multiple focusable
     * elements.
     *
     * @return {Ext.dom.Element} The focus styling element.
     * @protected
     */
    getFocusClsEl: function() {
        return this.getFocusEl();
    },
    /**
     * Template method to do any Focusable related initialization that
     * does not involve event listeners creation.
     * @protected
     */
    initFocusable: Ext.emptyFn,
    /**
     * Template method to do any event listener initialization for a Focusable.
     * This generally happens after the focusEl is available.
     * @protected
     */
    initFocusableEvents: function(force) {
        // If *not* naturally focusable, then we look for the tabIndex property
        // to be defined which indicates that the element should be made focusable.
        this.initFocusableElement(force);
    },
    enableFocusable: Ext.emptyFn,
    disableFocusable: function() {
        var me = this;
        // If this is disabled while focused, by default, focus would return to document.body.
        // This must be avoided, both for the convenience of keyboard users, and also
        // for when focus is tracked within a tree, such as below an expanded ComboBox.
        if (me.hasFocus) {
            me.revertFocus();
        }
        me.removeFocusCls();
    },
    destroyFocusable: function() {
        var me = this;
        Ext.destroy(me.focusListeners);
        me.focusListeners = me.focusEnterEvent = me.focusTask = null;
        me.focusEl = me.ariaEl = null;
    },
    /**
     * Determine if this Focusable can receive focus at this time.
     *
     * Note that Containers can be non-focusable themselves while delegating
     * focus treatment to a child Component; see {@link Ext.Container #defaultFocus}
     * for more information.
     *
     * @param {Boolean} [deep=false] Optionally determine if the container itself
     * is focusable, or if container's focus is delegated to a child component
     * and that child is focusable.
     *
     * @return {Boolean} True if component is focusable, false if not.
     */
    isFocusable: function(deep) {
        var me = this,
            focusEl;
        if (!me.focusable && (!me.isContainer || !deep)) {
            return false;
        }
        focusEl = me.getFocusEl();
        if (focusEl && me.canFocus()) {
            // getFocusEl might return a Component if a Container wishes to
            // delegate focus to a descendant. Both Component and Element
            // implement isFocusable, so always ask that.
            return focusEl && !focusEl.destroyed && focusEl.isFocusable(deep);
        }
        return false;
    },
    /**
     * Determines if this Component is inside a Component tree which is destroyed, *or
     * is being destroyed*.
     * @return {boolean} `true` if this Component, or any ancestor is destroyed, or
     * is being destroyed.
     * @private
     */
    isDestructing: function() {
        var c;
        for (c = this; c; c = c.getRefOwner()) {
            if (c.destroying || c.destroyed) {
                return true;
            }
        }
        return false;
    },
    canFocus: function(skipVisibility, includeFocusTarget) {
        var me = this,
            ownerFC = me.ownerFocusableContainer,
            focusableIfDisabled = ownerFC && ownerFC.allowFocusingDisabledChildren,
            canFocus;
        // Containers may have focusable children while being non-focusable
        // themselves; this is why we only account for me.focusable for
        // ordinary Components here and below.
        // MenuItems must accept focus when disabled.
        canFocus = !me.destroyed && me.rendered && !me.isDestructing() && (me.isContainer || me.focusable) && (!me.isDisabled() || focusableIfDisabled) && (skipVisibility || me.isVisible(true));
        return canFocus || (includeFocusTarget && !!me.findFocusTarget());
    },
    /**
     * Try to focus this component.
     *
     * If this component is disabled or otherwise not focusable, a close relation
     * will be targeted for focus instead to keep focus localized for keyboard users.
     *
     * @param {Boolean/Number[]} [selectText] If applicable, `true` to also select all the text
     * in this component, or an array consisting of start and end (defaults to start)
     * position of selection.
     *
     * @return {Boolean} `true` if focus target was found and focusing was attempted,
     * `false` if no focusing attempt was made.
     */
    focus: function(selectText) {
        var me = this,
            focusTarget, focusElDom;
        if ((!me.focusable && !me.isContainer) || me.destroyed || me.destroying) {
            return false;
        }
        // Assignment in conditional here to fall through to else block
        // if me.canFocus() returns true but there is no focus target
        if (me.canFocus() && (focusTarget = me.getFocusEl())) {
            // getFocusEl might return a child Widget or Component if a Container wishes
            // to delegate focus to a descendant via its defaultFocus configuration.
            if (focusTarget.$isFocusableEntity) {
                return focusTarget.focus.apply(focusTarget, arguments);
            }
            focusElDom = focusTarget.dom;
            // This is an Element instance
            if (focusElDom) {
                // NOT focusElDom.focus() here! Gotta run through possible Element overrides!
                focusTarget.focus();
                if (selectText && (me.selectText || focusElDom.select)) {
                    if (me.selectText) {
                        if (Ext.isArray(selectText)) {
                            me.selectText.apply(me, selectText);
                        } else {
                            me.selectText();
                        }
                    } else {
                        focusElDom.select();
                    }
                }
            }
            // Could be a Widget or something else with no dom property but focusable
            else if (focusTarget.focus) {
                focusTarget.focus();
            } else {
                return false;
            }
        } else {
            // If we are asked to focus while not able to focus though disablement/invisibility etc,
            // focus may revert to document.body if the current focus is being hidden or destroyed.
            // This must be avoided, both for the convenience of keyboard users, and also
            // for when focus is tracked within a tree, such as below an expanded ComboBox.
            focusTarget = me.findFocusTarget();
            if (focusTarget && focusTarget !== me) {
                return focusTarget.focus.apply(focusTarget, arguments);
            } else {
                return false;
            }
        }
        return true;
    },
    /**
     * @private
     */
    onBlur: function(e) {
        var me = this,
            container = me.ownerFocusableContainer;
        me.hasFocus = false;
        if (me.beforeBlur && !me.beforeBlur.$emptyFn) {
            me.beforeBlur(e);
        }
        if (container) {
            container.beforeFocusableChildBlur(me, e);
        }
        me.removeFocusCls(e);
        if (me.hasListeners.blur) {
            me.fireEvent('blur', me, e);
        }
        if (me.postBlur && !me.postBlur.$emptyFn) {
            me.postBlur(e);
        }
        if (container) {
            container.afterFocusableChildBlur(me, e);
        }
    },
    /**
     * @private
     */
    onFocus: function(e) {
        var me = this,
            container = me.ownerFocusableContainer;
        if (me.canFocus()) {
            if (me.beforeFocus && !me.beforeFocus.$emptyFn) {
                me.beforeFocus(e);
            }
            if (container) {
                container.beforeFocusableChildFocus(me, e);
            }
            me.addFocusCls(e);
            if (!me.hasFocus) {
                me.hasFocus = true;
                me.fireEvent('focus', me, e);
            }
            if (me.postFocus && !me.postFocus.$emptyFn) {
                me.postFocus(e);
            }
            if (container) {
                container.afterFocusableChildFocus(me, e);
            }
        }
    },
    /**
     * Return the actual tabIndex for this Focusable.
     *
     * @return {Number} tabIndex attribute value
     */
    getTabIndex: function() {
        var me = this,
            el, index;
        if (!me.focusable) {
            return;
        }
        el = me.getFocusEl();
        if (el) {
            // getFocusEl may return a child Widget or Component
            if (el.$isFocusableEntity) {
                index = el.getTabIndex();
            } else if (el.isElement && el.dom) {
                // We can't query el.dom.tabIndex because IE8 will return 0
                // when tabIndex attribute is not present, and Chrome will
                // return -1. Can't trust a browser to do a simplest thing. :/
                index = el.dom.getAttribute('tabIndex');
                // This contraption is here because we can't simply coerce
                // the returned attribute value to a number. If the attribute
                // is not present, the value returned will be null, and coercing
                // null gives 0. Wondrous JavaScript. :/
                if (index !== null) {
                    index -= 0;
                }
            } else // A component can be configured with el: '#id' to look up
            // its main element from the DOM rather than render it; in
            // such case getTabIndex() may happen to be called before
            // said lookup has happened; indeterminate result follows.
            {
                return;
            }
        }
        if (typeof index !== 'number') {
            index = me.tabIndex;
        }
        return index;
    },
    /**
     * Set the tabIndex property for this Focusable. If the focusEl
     * is available, set tabIndex attribute on it, too.
     *
     * @param {Number} newTabIndex new tabIndex to set
     * @param {HTMLElement} [focusEl] (private)
     */
    setTabIndex: function(newTabIndex, focusEl) {
        var me = this,
            ownerFC = me.ownerFocusableContainer,
            focusableIfDisabled = ownerFC && ownerFC.allowFocusingDisabledChildren,
            wasFocusable = me.focusable,
            el;
        // See comments for definition of forceTabIndex as to why this is needed
        // Return early if not focusable unless we are either forcing the tabIndex to
        // be set, or we are removing the tabIndex attribute.
        if (!wasFocusable && !(me.forceTabIndex || newTabIndex == null)) {
            return;
        }
        me.tabIndex = newTabIndex;
        // We must not do this if we are destroyed, or if we are incapable of being focused.
        // Handle our owning FocusableContainer having allowFocusingDisabledChildren
        if (me.destroying || me.destroyed || (me.isDisabled() && !focusableIfDisabled)) {
            return;
        }
        // getFocusEl does not return the element if focusable is false.
        // If we are forcing the tabIndex, or setting it to null, we
        // need it to return the focusEl.
        me.focusable = true;
        el = focusEl || me.getFocusEl();
        me.focusable = wasFocusable;
        if (el) {
            // getFocusEl may return a child Widget or Component
            if (el.$isFocusableEntity) {
                el.setTabIndex(newTabIndex);
            }
            // Or if a component is configured with el: '#id', it may
            // still be a string by the time setTabIndex is called from
            // owner FocusableContainer.
            else if (el.isElement && el.dom) {
                // setTabIndex is aware of saved tabbable state
                el.setTabIndex(newTabIndex);
            }
        }
    },
    /**
     * @template
     * @protected
     * Called when focus enters this Component's hierarchy
     * @param {Object} e
     * @param {Ext.event.Event} e.event The underlying DOM event.
     * @param {HTMLElement} e.target The element gaining focus.
     * @param {HTMLElement} e.relatedTarget The element losing focus.
     * @param {Ext.Component} e.toComponent The Component gaining focus.
     * @param {Ext.Component} e.fromComponent The Component losing focus.
     * @param {Boolean} e.backwards `true` if the `fromComponent` is *after* the `toComponent*
     * in the DOM tree, indicating that the user used `SHIFT+TAB` to move focus. Note that setting
     * `tabIndex` values to affect tabbing order can cause this to be incorrect. Setting
     * `tabIndex` values is not advised.
     */
    onFocusEnter: function(e) {
        var me = this;
        // We DO NOT check if `me` is focusable here. The reason is that
        // non-focusable containers need to track focus entering their
        // children so that revertFocus would work if these children
        // become unavailable.
        if (me.destroying || me.destroyed) {
            return;
        }
        // Save all information about how we received focus so that
        // we can do appropriate things when asked to revertFocus
        me.focusEnterEvent = e;
        me.containsFocus = true;
        if (me.hasListeners.focusenter) {
            me.fireEvent('focusenter', me, e);
        }
    },
    /**
     * @template
     * @protected
     * Called when focus exits from this Component's hierarchy
     * @param {Ext.event.Event} e
     * @param {Ext.event.Event} e.event The underlying DOM event.
     * @param {HTMLElement} e.target The element gaining focus.
     * @param {HTMLElement} e.relatedTarget The element losing focus.
     * @param {Ext.Component} e.toComponent The Component gaining focus.
     * @param {Ext.Component} e.fromComponent The Component losing focus.
     */
    onFocusLeave: function(e) {
        var me = this;
        // Same as in onFocusEnter
        if (me.destroying || me.destroyed) {
            return;
        }
        me.focusEnterEvent = null;
        me.containsFocus = false;
        if (me.hasListeners.focusleave) {
            me.fireEvent('focusleave', me, e);
        }
    },
    /**
     * @template
     * @method
     * @protected
     * Called when focus moves *within* this Component's hierarchy
     * @param {Object} info
     * @param {Ext.event.Event} info.event The underlying Event object.
     * @param {HTMLElement} info.toElement The element gaining focus.
     * @param {HTMLElement} info.fromElement The element losing focus.
     * @param {Ext.Component} info.toComponent The Component gaining focus.
     * @param {Ext.Component} info.fromComponent The Component losing focus.
     * @param {Boolean} info.backwards `true` if the focus movement is backward in DOM order
     */
    onFocusMove: Ext.emptyFn,
    privates: {
        // This private flag was introduced to work around an issue where
        // the tab index would not be stamped onto the component.
        // Consider a tool, which is focusable by default and has tabIndex: 0
        // on the class definition. If a non-focusable tool is required, setting
        // focusable: false is not enough, the tabIndex also needs to be considered.
        // However, the default behaviour is to not modify the tabIndex on non
        // focusable items. This config can go away if that behaviour is changed.
        // Arguably, a non-focusable widget probably shouldn't retain a tab index
        // if it's explicitly configured.
        forceTabIndex: false,
        /**
         * Returns focus to the Component or element found in the cached
         * focusEnterEvent.
         *
         * @private
         */
        revertFocus: function() {
            var me = this,
                focusEvent = me.focusEnterEvent,
                activeElement = Ext.Element.getActiveElement(),
                focusTarget, fromComponent, reverted;
            // If we have a record of where focus arrived from,
            //  and have not been told to avoid refocusing,
            //  and we contain the activeElement.
            // Then, before hiding, restore focus to what was focused before we were focused.
            if (focusEvent && !me.preventRefocus && me.el.contains(activeElement)) {
                fromComponent = focusEvent.fromComponent;
                // If the default focus reversion target is in shifting ground, fall back
                // to document.body
                if (fromComponent && (fromComponent.destroyed || fromComponent.isDestructing())) {
                    focusTarget = document.body;
                } else // Preferred focus target is the actual element from which focus entered
                // this component. It will be up to its encapsulating component to handle this
                // in an appropriate way. For example, a grid, upon having focus pushed
                // to a certain cell will set its navigation position to that cell and highlight it
                // as focused. Likewise an input field must handle its field acquiring focus.
                {
                    focusTarget = focusEvent.relatedTarget;
                }
                // If focus was from the body, try to keep it closer than that
                if (focusTarget === document.body) {
                    fromComponent = me.findFocusTarget();
                    if (fromComponent) {
                        focusTarget = fromComponent.getFocusEl();
                    }
                }
                if (focusTarget && focusTarget.$isFocusableEntity) {
                    if (!focusTarget.destroyed && focusTarget.isFocusable()) {
                        focusTarget.focus();
                    }
                }
                // If the element is in the document and focusable, then we're good. The owning
                // component will handle it.
                else if (Ext.getDoc().contains(focusTarget) && Ext.fly(focusTarget).isFocusable()) {
                    fromComponent = Ext.Component.from(focusTarget);
                    // Allow the focus recieving component to modify the focus sequence.
                    if (fromComponent) {
                        fromComponent.revertFocusTo(focusTarget);
                    } else {
                        focusTarget.focus();
                    }
                }
                // If the element has gone, or is hidden, we will have to rely on the intelligent
                // focus diversion of components to send focus back to somewhere that is least
                // surprising for the user.
                else if (focusEvent.fromComponent && focusEvent.fromComponent.focus) {
                    reverted = focusEvent.fromComponent.focus();
                    // The component was not able to find a suitable target.
                    // Important: Touch platforms do not blur programmatically focused elements
                    // when they become hidden, so we must force the issue in order to maintain
                    // focus tracking.
                    if (!reverted) {
                        activeElement.blur();
                    }
                }
            }
        },
        /**
         * This field is on the recieving end of a call from {@link #method!revertFocus}.
         *
         * It is called when focus is being pushed back into this Component from a Component
         * that is focused and is being hidden or disabled.
         *
         * We must focus the passed element.
         *
         * Subclasses may perform some extra processing to prepare for refocusing.
         * @param target
         */
        revertFocusTo: function(target) {
            target.focus();
        },
        /**
         * Finds an alternate Component to focus if this Component is disabled while focused, or
         * focused while disabled, or otherwise unable to focus.
         * 
         * In both cases, focus must not be lost to document.body, but must move to an intuitively
         * connectible Component, either a sibling, or uncle or nephew.
         *
         * This is both for the convenience of keyboard users, and also for when focus is tracked
         * within a Component tree such as for ComboBoxes and their dropdowns.
         *
         * For example, a ComboBox with a PagingToolbar in is BoundList. If the "Next Page"
         * button is hit, the LoadMask shows and focuses, the next page is the last page, so
         * the "Next Page" button is disabled. When the LoadMask hides, it attempt to focus the
         * last focused Component which is the disabled "Next Page" button. In this situation,
         * focus should move to a sibling within the PagingToolbar.
         * 
         * @return {Ext.Component} A closely related focusable Component to which focus can move.
         * @private
         */
        findFocusTarget: function() {
            var me = this,
                parentAxis, candidate, len, i, focusTargets, focusIndex;
            if (me.preventRefocus) {
                return null;
            }
            // Create an axis of visible, enabled, stable parent components which we
            // will walk up looking for ancestors to revert focus to.
            //
            // First, find all enabled parents.
            // eslint-disable-next-line max-len
            for (parentAxis = [] , candidate = me.getRefOwner(); candidate; candidate = candidate.getRefOwner()) {
                if (!candidate.isDisabled()) {
                    parentAxis.unshift(candidate);
                }
            }
            // Then walk downwards until we encounter a non-targetable parent
            // which means hidden of destroying. All candidates *above* that
            // are potential sources of focus targets.
            for (i = 0 , len = parentAxis.length; i < len; i++) {
                candidate = parentAxis[i];
                if (candidate.destroying || !candidate.isVisible()) {
                    parentAxis.length = i;
                    break;
                }
            }
            // Walk up the parent axis checking each parent for focus targets.
            for (i = parentAxis.length - 1; i >= 0; i--) {
                candidate = parentAxis[i];
                // Use CQ to find a target that is fully focusable (:canfocus, NOT the theoretical
                // :focusable). Cannot use :focusable(true) because that consults findFocusTarget
                // and would cause infinite recursion.
                // Exclude the component which currently has focus.
                // Cannot use candidate.child() because the parent might not be a Container.
                // Non-Container Components may still have ownership relationships with
                // other Components. eg: BoundList with PagingToolbar
                focusTargets = Ext.ComponentQuery.query(':canfocus()', candidate);
                if (focusTargets.length) {
                    // eslint-disable-next-line max-len
                    focusIndex = Ext.Array.indexOf(focusTargets, Ext.ComponentManager.getActiveComponent());
                    // Return the next focusable, or the previous focusable, or the first focusable
                    return focusTargets[focusIndex + 1] || focusTargets[focusIndex - 1] || focusTargets[0];
                }
                // We found no focusable siblings in our candidate, but the candidate may itself
                // be focusable, it is not always a Container - could be the owning Field
                // of a BoundList.
                if (candidate.isFocusable && candidate.isFocusable()) {
                    return candidate;
                }
            }
        },
        /**
         * Sets up the focus listener on this Component's {@link #getFocusEl focusEl} if it has one.
         *
         * Form Components which must implicitly participate in tabbing order usually have
         * a naturally focusable element as their {@link #getFocusEl focusEl}, and it is
         * the DOM event of that receiving focus which drives the Component's `onFocus` handling,
         * and the DOM event of it being blurred which drives the `onBlur` handling.
         * @private
         */
        initFocusableElement: function(force) {
            var me = this,
                tabIndex = me.getTabIndex(),
                focusEl = me.getFocusEl();
            if (focusEl && !focusEl.$isFocusableEntity) {
                // focusEl is not available until after rendering, and rendering tabIndex
                // into focusEl is not always convenient. So we apply it here if Component's
                // tabIndex property is set and Component is otherwise focusable.
                // Note that for Widgets and Modern Components we might not have the rendered
                // flag set yet but can force setting the property.
                if (tabIndex != null && (force || me.canFocus(true))) {
                    me.setTabIndex(tabIndex, focusEl);
                }
                // This attribute is a shortcut to look up a Component by its Elements
                // It only makes sense on focusable elements, so we set it here unless
                // our focusEl is delegated to the focusEl of an owned Component and it
                // already has ownership stamped into it.
                if (!focusEl.dom.hasAttribute('data-componentid')) {
                    focusEl.dom.setAttribute('data-componentid', me.id);
                }
            }
        },
        addFocusCls: function(e) {
            var focusCls = this.focusCls,
                el;
            el = this.getFocusClsEl();
            if (focusCls) {
                el = this.getFocusClsEl(e);
                if (el) {
                    el.addCls(focusCls);
                }
            }
        },
        removeFocusCls: function(e) {
            var focusCls = this.focusCls,
                el;
            if (focusCls) {
                el = this.getFocusClsEl(e);
                if (el) {
                    el.removeCls(focusCls);
                }
            }
        },
        /**
         * @private
         */
        handleFocusEvent: function(info) {
            var me = this,
                event;
            if (!me.focusable || me.destroying || me.destroyed) {
                return;
            }
            // handleFocusEvent and handleBlurEvent are called by ComponentManager
            // passing the normalized element event that might or might not cause
            // component focus or blur. The component itself makes the decision
            // whether focus/blur happens or not. This is necessary for components
            // that might have more than one focusable element within the component's
            // DOM structure, like Ext.button.Split.
            if (me.isFocusing(info)) {
                event = new Ext.event.Event(info.event);
                event.type = 'focus';
                event.relatedTarget = info.fromElement;
                event.target = info.toElement;
                me.onFocus(event);
            }
        },
        /**
         * @private
         */
        handleBlurEvent: function(info) {
            var me = this,
                event;
            if (!me.focusable || me.destroying || me.destroyed) {
                return;
            }
            // Must process blur if toElement is document.body
            // Blurs caused by the app losing focus are processed synchronously
            // for obvious reasons, so the activeElement will still be the
            // focusEl, so isBlurring will return false, and reject this op.
            if (info.toElement === document.body || me.isBlurring(info)) {
                event = new Ext.event.Event(info.event);
                event.type = 'blur';
                event.target = info.fromElement;
                event.relatedTarget = info.toElement;
                me.onBlur(event);
            }
        },
        /**
         * @private
         */
        isFocusing: function(e) {
            var focusEl = this.getFocusEl();
            if (focusEl) {
                if (focusEl.isFocusing) {
                    return focusEl.isFocusing(e);
                } else {
                    // Sometimes focusing an element may cause reaction from other entities
                    // that will focus something else instead. So before unraveling the
                    // event chain we better make sure our focusEl is *indeed* focused.
                    return focusEl.dom === document.activeElement && e.toElement === focusEl.dom && e.fromElement !== e.toElement;
                }
            }
            return false;
        },
        /**
         * @private
         */
        isBlurring: function(e) {
            var focusEl = this.getFocusEl();
            if (focusEl) {
                if (focusEl.isFocusing) {
                    return focusEl.isBlurring(e);
                } else {
                    // Ditto for blurring: in some cases freshly blurred element can be
                    // refocused by external forces so we need to check if our focusEl
                    // *indeed* is not focused before we can call it blurring.
                    return focusEl.dom !== document.activeElement && e.fromElement === focusEl.dom && e.fromElement !== e.toElement;
                }
            }
            return false;
        },
        /**
         * @private
         */
        blur: function() {
            var me = this,
                focusEl;
            if (!me.focusable || !me.canFocus()) {
                return;
            }
            focusEl = me.getFocusEl();
            if (focusEl) {
                me.blurring = true;
                focusEl.blur();
                delete me.blurring;
            }
        },
        isTabbable: function() {
            var me = this,
                focusEl;
            if (me.focusable) {
                focusEl = me.getFocusEl();
                if (focusEl && focusEl.isTabbable()) {
                    return focusEl.isTabbable();
                }
            }
            return false;
        },
        disableTabbing: function() {
            var me = this,
                el = me.el,
                focusEl;
            // We DO NOT check for me.focusable here, because this should work
            // for containers with focus delegates, too!
            if (me.destroying || me.destroyed) {
                return;
            }
            // We're disabling tabbability for all elements of a given Component;
            // focusEl might be outside of the hierarchy which is checked below.
            if (el) {
                el.saveTabbableState();
            }
            focusEl = me.getFocusEl();
            if (focusEl) {
                // focusEl may happen to be a focus delegate for a container
                if (focusEl.$isFocusableEntity) {
                    focusEl.disableTabbing();
                }
                // Alternatively focusEl may happen to be outside of the main el,
                // or else it can be a string reference to an element that
                // has not been resolved yet
                else if (focusEl.isElement && el && !el.contains(focusEl)) {
                    focusEl.saveTabbableState();
                }
            }
        },
        enableTabbing: function(reset) {
            var me = this,
                el = me.el,
                focusEl;
            // We DO NOT check for me.focusable here, because this should work
            // for containers with focus delegates, too!
            if (me.destroying || me.destroyed) {
                return;
            }
            focusEl = me.getFocusEl();
            if (focusEl) {
                if (focusEl.$isFocusableEntity) {
                    focusEl.enableTabbing();
                } else if (focusEl.isElement && el && !el.contains(focusEl)) {
                    focusEl.restoreTabbableState();
                }
            }
            if (el) {
                el.restoreTabbableState({
                    reset: reset
                });
            }
        }
    }
}, function() {
    var keyboardModeCls = Ext.baseCSSPrefix + 'keyboard-mode',
        keyboardMode = false;
    /**
     * @cfg {Boolean} enableKeyboardMode
     * When set to `true`, focus styling will be applied to focused elements based on the
     * user interaction mode: when keyboard was used to focus an element, focus styling
     * will be visible but not when element was focused otherwise (e.g. with mouse, touch,
     * or programmatically). The {@link #keyboardMode} property will then reflect the last
     * user interaction mode.
     * Setting this option to `false` disables keyboard mode tracking and results in focus
     * styling always being applied to focused elements, which is pre-Ext JS 6.5 behavior.
     *
     * Defaults to `false` in desktop environments, `true` on mobile devices.
     * @member Ext
     * @bindable
     * @since 6.6.0
     */
    Ext.enableKeyboardMode = Ext.isModern || !Ext.os.is.Desktop;
    /**
     * @property {Boolean} keyboardMode
     * @member Ext
     * A flag which indicates that the last UI interaction from the user was a keyboard gesture
     * @since 6.5.0
     */
    /**
     * @property {Boolean} touchMode
     * @member Ext
     * A flag which indicates that the last UI interaction from the user was a touch gesture
     * @since 6.5.0
     */
    Ext.setKeyboardMode = Ext.setKeyboardMode || function(keyboardMode) {
        Ext.keyboardMode = keyboardMode;
        Ext.getBody().toggleCls(keyboardModeCls, keyboardMode);
    };
    Ext.isTouchMode = function() {
        return (Ext.now() - Ext.lastTouchTime) < 500;
    };
    /**
     * @private
     */
    Ext.syncKeyboardMode = function(e) {
        var type;
        if (!Ext.enableKeyboardMode) {
            return;
        }
        type = e.type;
        if (type === 'pointermove') {
            // On pointer move we want to track that the user has switched from keyboard
            // to mouse, but we do not remove the visual focus style until a pointerdown
            // occurs or the focus changes.  This accomodates menus which change focus
            // based on mouseenter of a menu item
            keyboardMode = false;
        } else {
            keyboardMode = (type === 'keydown');
            Ext.lastTouchTime = e.pointerType === 'touch' && Ext.now();
            Ext.setKeyboardMode(keyboardMode);
        }
    };
    function keyboardModeFocusHandler() {
        // NOT Ext.keyboardMode here; closing over variable local to class callback fn
        if (keyboardMode !== Ext.getBody().hasCls(keyboardModeCls)) {
            Ext.setKeyboardMode(keyboardMode);
        }
    }
    Ext.getEnableKeyboardMode = function() {
        return Ext.enableKeyboardMode;
    };
    Ext.setEnableKeyboardMode = function(enable) {
        var listeners = {
                pointerdown: Ext.syncKeyboardMode,
                pointermove: Ext.syncKeyboardMode,
                keydown: Ext.syncKeyboardMode,
                capture: true,
                delegated: false
            };
        Ext.enableKeyboardMode = !!enable;
        if (Ext.enableKeyboardMode) {
            Ext.getWin().on(listeners);
            Ext.on('focus', keyboardModeFocusHandler);
        } else {
            Ext.getWin().un(listeners);
            Ext.un('focus', keyboardModeFocusHandler);
        }
    };
    Ext.onReady(function() {
        // Add the CSS class once upfront if enableKeyboardMode is disabled
        if (!Ext.enableKeyboardMode) {
            Ext.getBody().addCls(keyboardModeCls);
        }
        Ext.setEnableKeyboardMode(Ext.enableKeyboardMode);
    });
});

/**
 * This mixin defines certain config options, properties, and APIs to be used
 * by Components that implement accessible traits according to WAI-ARIA 1.0 specification.
 *
 * @private
 */
Ext.define('Ext.mixin.Accessible', {
    extend: 'Ext.Mixin',
    mixinConfig: {
        id: 'accessible'
    },
    /**
     * @cfg {String} [ariaLabel] ARIA label for this Component. It is best to use
     * {@link #ariaLabelledBy} option instead, because screen readers prefer
     * `aria-labelledby` attribute to `aria-label`. {@link #ariaLabel} and
     * {@link #ariaLabelledBy} config options are mutually exclusive.
     */
    /**
     * @cfg {String} [ariaLabelledBy] DOM selector for a child element that is to be used
     * as label for this Component, set in `aria-labelledby` attribute.
     * If the selector is by `#id`, the label element can be any existing element,
     * not necessarily a child of the main Component element.
     *
     * {@link #ariaLabelledBy} and {@link #ariaLabel} config options are
     * mutually exclusive, and `ariaLabelledBy` has the higher precedence.
     */
    /**
     * @cfg {String} [ariaDescribedBy] DOM selector for a child element that is to be used
     * as description for this Component, set in `aria-describedby` attribute.
     * The selector works the same way as {@link #ariaLabelledBy}.
     */
    config: {
        /**
         * @cfg {Object} ariaAttributes An object containing ARIA attributes to be set
         * on this Component's ARIA element. Use this to set the attributes that cannot be
         * determined by the Component's state, such as `aria-live`, `aria-flowto`, etc.
         *
         * **Note** that this config is only meaningful at the Component rendering time,
         * and setting it after that will do nothing.
         */
        ariaAttributes: {
            $value: null,
            lazy: true
        }
    },
    /**
     * @property {String} [ariaRole] ARIA role for this Component, defaults to no role.
     * With no role, no other ARIA attributes are set.
     *
     * @readonly
     */
    /**
     * @property {Object} [ariaRenderAttributes] **Instance specific** ARIA attributes
     * to render into Component's ariaEl. This object is only used during rendering,
     * and is discarded afterwards.
     *
     * @private
     */
    /**
     * @property {String} [ariaEl='el'] The name of the Component property that holds
     * a reference to the Element that serves as that Component's ARIA element.
     * This property will be replaced with the actual Element reference after rendering.
     *
     * Most of the simple Components will have their main element as ariaEl.
     *
     * @private
     * @readonly
     * @since 6.0.0
     */
    ariaEl: 'el',
    privates: {
        /**
         * Find component(s) that label or describe this component,
         * and return the id(s) of their ariaEl elements.
         *
         * @param {Function/String/String[]} [reference] Component reference,
         * or array of component references, or a function that should return
         * the proper attribute string. The function will be called in the
         * context of the labelled component.
         *
         * @return {Ext.Element} Element id string, or null
         * @private
         */
        getAriaLabelEl: function(reference) {
            var ids = [],
                refHolder, i, len, cmp;
            if (reference) {
                if (Ext.isFunction(reference)) {
                    return reference.call(this);
                } else {
                    if (!Ext.isArray(reference)) {
                        reference = [
                            reference
                        ];
                    }
                    refHolder = this.lookupReferenceHolder();
                    if (refHolder) {
                        for (i = 0 , len = reference.length; i < len; i++) {
                            cmp = refHolder.lookupReference(reference[i]);
                            if (cmp) {
                                ids.push(cmp.ariaEl.id);
                            }
                        }
                    }
                }
            }
            return ids.length ? ids.join(' ') : null;
        }
    }
});

/**
 * Ext.Widget is a light-weight Component that consists of nothing more than a template
 * Element that can be cloned to quickly and efficiently replicate many instances.
 * Ext.Widget is typically not instantiated directly, because the default template is
 * just a single element with no listeners. Instead Ext.Widget should be extended to
 * create Widgets that have a useful markup structure and event listeners.
 *
 * For example:
 *
 *      Ext.define('MyWidget', {
 *          extend: 'Ext.Widget',
 *
 *          // The element template passed to Ext.Element.create()
 *          element: {
 *              reference: 'element',
 *              listeners: {
 *                  click: 'onClick'
 *              },
 *              children: [{
 *                  reference: 'innerElement',
 *                  listeners: {
 *                      click: 'onInnerClick'
 *                  }
 *              }]
 *          },
 *
 *          constructor: function(config) {
 *              // It is important to remember to call the Widget superclass constructor
 *              // when overriding the constructor in a derived class. This ensures that
 *              // the element is initialized from the template, and that initConfig() is
 *              // is called.
 *              this.callParent([config]);
 *
 *              // After calling the superclass constructor, the Element is available and
 *              // can safely be manipulated. Reference Elements are instances of
 *              // Ext.Element, and are cached on each Widget instance by reference name.
 *              Ext.getBody().appendChild(this.element);
 *          },
 *
 *          onClick: function() {
 *              // listeners use this Widget instance as their scope
 *              console.log('element clicked', this);
 *          },
 *
 *          onInnerClick: function() {
 *              // access the innerElement reference by name
 *              console.log('inner element clicked', this.innerElement);
 *          }
 *      });
 *
 * @since 5.0.0
 * @disable {DuplicateAlternateClassName}
 */
Ext.define('Ext.Widget', {
    extend: 'Ext.Evented',
    xtype: 'widget',
    alternateClassName: 'Ext.Gadget',
    requires: [
        'Ext.dom.Element'
    ],
    mixins: [
        'Ext.mixin.Inheritable',
        'Ext.mixin.Bindable',
        'Ext.mixin.ComponentDelegation',
        'Ext.mixin.Pluggable',
        'Ext.mixin.Keyboard',
        'Ext.mixin.Factoryable',
        'Ext.mixin.Focusable',
        'Ext.mixin.Accessible'
    ],
    isWidget: true,
    factoryConfig: {
        creator: null,
        defaultProperty: 'xtype',
        defaultType: 'component',
        typeProperty: 'xtype'
    },
    /* eslint-disable max-len */
    /**
     * @property {Object} element
     * A configuration object for Ext.Element.create() that is used to create the Element
     * template.  Subclasses should avoid overriding this property and instead add elements
     * using {@link #template}.
     *
     * Supports all the standard options of a Ext.Element.create() config and adds 3
     * additional options:
     *
     * 1. `reference` - this option specifies a name for Element references.  These
     * references names become properties of the Widget instance and refer to Ext.Element
     * instances that were created using the template:
     *
     *          element: {
     *              reference: 'element',
     *              children: [{
     *                  reference: 'innerElement'
     *              }]
     *          }
     *
     * After construction of a widget the reference elements are accessible as follows:
     *
     *     var foo = new FooWidget(),
     *         innerEl = foo.innerElement; // an Ext.Element that wraps the innerElement
     *
     * The reference attribute is optional, but all Widgets must have a `'element'`
     * reference on some element within the template (usually the outermost one).
     *
     * 2. `listeners` - a standard listeners object as specified by {@link Ext.mixin.Observable}.
     *
     *          element: {
     *              reference: 'element',
     *              listeners: {
     *                  click: 'onClick'
     *              },
     *              children: [{
     *                  reference: 'innerElement',
     *                  listeners: {
     *                      click: 'onInnerClick'
     *                  }
     *              }]
     *          }
     *
     * Since listeners cannot be attached without an Ext.Element reference the `reference`
     * property MUST be specified in order to use `listeners`.
     *
     * The Widget instance is used as the scope for all listeners specified in this way,
     * so it is invalid to use the `scope` option in the `listeners` config since it will
     * always be overwritten using `this`.
     *
     * 3. `uiCls` - a suffix to be appended to the ui-specific CSS class for each `{@link #ui}`
     * for this widget.  These ui classes are constructed by appending the `ui` to each
     * `{@link #classCls}` or `{@link #baseCls}` for the widget.  As such, `uiCls` should
     * never be used on the main `element` reference, as its `uiCls` is computed automatically.
     *
     * For example, assume a widget is defined with a `ui` of `'alt action'` and a
     * `uiCls` of `'inner-el'` on its `innerElement` reference element:
     *
     *          Ext.define('Doodad', {
     *              extend: 'Ext.Widget',
     *              xtype: 'doodad',
     *
     *              classCls: 'x-doodad',
     *
     *              ui: 'alt action',
     *
     *              element: {
     *                  reference: 'element',
     *
     *                  children: [{
     *                      reference: 'innerElement',
     *                      cls: 'x-inner-el',
     *                      uiCls: 'inner-el'
     *                  }]
     *              }
     *          });
     *
     * This would result in the following markup when rendered:
     *
     *     <div class="x-doodad x-doodad-alt x-doodad-action">
     *         <div class="x-inner-el x-doodad-inner-el x-doodad-alt-inner-el x-doodad-action-inner-el"></div>
     *     </div>
     *
     * These additional classes can be used to style the reference element for a particular
     * ui; however, use of `uiCls` is not typically necessary or recommended.  Reference
     * elements should usually be styled using simple descendant selectors:
     *
     *     .x-doodad-alt .x-inner-el {
     *         color: red;
     *     }
     *
     * When there is a possibility that widgets can be nested it is best to use direct
     * child selectors to avoid the possibility of selecting all descendants instead
     * of just the reference element for the intended widget:
     *
     *     .x-doodad-alt > .x-inner-el {
     *         color: red;
     *     }
     *
     * Only use `uiCls` when there is a possibility of nesting, AND there may be a variable
     * number of elements between the main `element` and the reference element in question.
     * For example, Ext.Container with docked items has a different number of elements
     * in between its `element` and its `bodyElement` than a Container without docked items
     * because of the wrapping elements that are dynamically added to support docking.
     * To ensure it does not style all descendants it must use a `uiCls` to style its
     * `bodyElement`:
     *
     *     .x-container-alt-body-el {
     *         background: #fff;
     *     }
     *
     * Note that when `uiCls` is specified it also adds a class name that does not contain
     * the `ui` using just the `classCls` and/or `baseCls` as the prefix.  This class name
     * can be used for base-level styling that does not relate to any particular UI:
     *
     *     .x-container-body-el {
     *         position: relative;
     *     }
     *
     * @protected
     */
    element: {
        reference: 'element'
    },
    /* eslint-enable */
    observableType: 'component',
    cachedConfig: {
        /**
         * @cfg {String/String[]} cls The CSS class to add to this widget's element, in
         * addition to the {@link #baseCls}. In many cases, this property will be specified
         * by the derived widget class. See {@link #userCls} for adding additional CSS
         * classes to widget instances (such as items in a {@link Ext.Container}).
         * @accessor
         */
        cls: null,
        /**
         * @cfg {Number/String} margin
         * The margin to use on this Component. Can be specified as a number (in which
         * case all edges get the same margin) or a CSS string like '5 10 10 10'
         */
        margin: null,
        /**
         * @cfg {String/Object} style
         * Additional CSS styles that will be rendered into an inline style attribute when
         * the widget is rendered.
         *
         * You can pass either a string syntax:
         *
         *     style: 'background:red'
         *
         * Or by using an object:
         *
         *     style: {
         *         background: 'red'
         *     }
         *
         * When using the object syntax, you can define CSS Properties by using a string:
         *
         *     style: {
         *         'border-left': '1px solid red'
         *     }
         *
         * Although the object syntax is much easier to read, we suggest you to use the
         * string syntax for better performance.
         * @accessor set
         */
        style: null,
        /**
         * @cfg {Boolean} border Enables or disables bordering on this component.
         * The following values are accepted:
         *
         * - `null` or `true (default): Do nothing and allow the border to be specified
         * by the theme.
         * - `false`: suppress the default border provided by the theme.
         *
         * Please note that enabling bordering via this config will not add a `border-color`
         * or `border-style` CSS property to the component; you provide the `border-color`
         * and `border-style` via CSS rule or {@link #style} configuration
         * (if not already provide by the theme).
         *
         * ## Using {@link #style}:
         *
         *     Ext.Viewport.add({
         *         centered: true,
         *         width: 100,
         *         height: 100,
         *
         *         style: 'border: 1px solid blue;'
         *         // ...
         *     });
         *
         * ## Using CSS:
         *
         *     Ext.Viewport.add({
         *         centered: true,
         *         width: 100,
         *         height: 100,
         *
         *         cls: 'my-component'
         *         // ...
         *     });
         *
         * And your CSS file:
         *
         *     .my-component {
         *         border: 1px solid red;
         *     }
         *
         * @accessor
         */
        border: null,
        /**
         * @cfg {Object}
         *
         * Emulates the behavior of the CSS
         * [touch-action](https://www.w3.org/TR/pointerevents/#the-touch-action-css-property)
         * property in a cross-browser compatible manner.
         *
         * Keys in this object are touch action names, and values are `false` to disable
         * a touch action or `true` to enable it.  Accepted keys are:
         *
         * - `panX`
         * - `panY`
         * - `pinchZoom`
         * - `doubleTapZoom`
         *
         * All touch actions are enabled (`true`) by default, so it is usually only necessary
         * to specify which touch actions to disable.  For example, the following disables
         * only horizontal scrolling and pinch-to-zoom on the component's main element:
         *
         *     touchAction: {
         *         panX: false,
         *         pinchZoom: false
         *     }
         *
         * Touch actions can be specified on reference elements using the reference element
         * name, for example:
         *
         *     // disables horizontal scrolling on the main element, and double-tap-zoom
         *     // on the child element named "body"
         *     touchAction: {
         *         panY: false
         *         body: {
         *             doubleTapZoom: false
         *         }
         *     }
         *
         * The primary motivation for setting the touch-action of an element is to prevent
         * the browser's default handling of a gesture such as pinch-to-zoom, or
         * drag-to-scroll, so that the application can implement its own handling of that
         * gesture on the element.  Suppose, for example, a component has a custom drag
         * handler on its element and wishes to prevent horizontal scrolling of its container
         * while it is being dragged:
         *
         *     Ext.create('Ext.Widget', {
         *         touchAction: {
         *             panX: false
         *         },
         *         listeners: {
         *             drag: function(e) {
         *                 // implement drag logic
         *             }
         *         }
         *     });
         */
        touchAction: null,
        /**
         * @cfg {Object} eventHandlers A map of event type to the corresponding handler method
         * name. This is used internally by native event handling mechanism.
         * @private
         * @deprecated 6.6.0 Inline event handlers are deprecated
         */
        eventHandlers: {
            focus: 'handleFocusEvent',
            blur: 'handleBlurEvent'
        }
    },
    /**
     * @cfg {String} name Name for the widget to be used with {@link Ext.Container#lookupName}
     * et al.
     */
    name: null,
    config: {
        /**
         * @cfg {Ext.Element} [renderTo] Optional element to render this Component to.
         * Not required if this component is an {@link Ext.Container#items item} of a Container
         * of a Container.
         */
        renderTo: null,
        /**
         * @cfg {String/String[]} ui The ui or uis to be used on this Component
         *
         * When a ui is configured, CSS class names are added to the {@link #element}, created
         * by appending the ui name(s) to each {@link #classCls} and/or {@link #baseCls}.
         */
        ui: null,
        /**
         * @cfg {String/String[]} userCls
         * One or more CSS classes to add to the component's primary element. This config
         * is intended solely for use by the component instantiator (the "user"), not by
         * derived classes.
         *
         * For example:
         *
         *      items: [{
         *          xtype: 'button',
         *          userCls: 'my-button'
         *      ...
         *      }]
         */
        userCls: null,
        /**
         * @cfg {Boolean/Object/String} ripple
         * Set to truthy, Color or Object value for the ripple.
         * @cfg {String} ripple.color The background color of the ripple.
         * @cfg {Array} ripple.position Position for the ripple to start at [x,y].
         * Determines if a Ripple effect should happen whenever this element is pressed.
         *
         * For example:
         *      {
         *          ripple: true
         *      }
         *
         * Or:
         *
         *      {
         *          ripple: {
         *              color: 'red'
         *          }
         *      }
         *
         * For complex components, individual elements can suppress ripples by adding the
         * `x-no-ripple` class to disable rippling for a tree of elements.
         *
         * @since 6.5.0
         */
        ripple: null,
        /**
         * @cfg {'clip'/'display'/'offsets'/'opacity'/'visibility'} [hideMode='display']
         * A String which specifies how this component's DOM element will be hidden. The
         * accepted values are any of these:
         *
         * - `'clip'` : Hide using {@link Ext.dom.Element#CLIP clip}.
         * - `'display'` : Hide using {@link Ext.dom.Element#DISPLAY display}.
         * - `'offsets'` : Hide using positioning {@link Ext.dom.Element#OFFSETS offsets}.
         * - `'opacity'` : Hide using {@link Ext.dom.Element#OPACITY opacity}.
         * - `'visibility'` : Hide using {@link Ext.dom.Element#VISIBILITY visibility}.
         *
         * Hiding using ``display`` results in having no dimensions as well as resetting
         * scroll positions to 0.
         *
         * The other modes overcome this but may have different trade-offs in certain
         * circumstances.
         *
         * @since 6.5.0
         */
        hideMode: null,
        /**
         * @cfg {String/String[]} instanceCls
         *
         * An extra CSS class or classes to augment the {@link #classCls} on an individual instance
         *
         * @private
         * @since 6.5.0
         */
        instanceCls: null
    },
    eventedConfig: {
        /**
         * @cfg {Number/String} width
         * The width of this Component; must be a valid CSS length value, e.g: `300`, `100px`,
         * `30%`, etc. By default, if this is not explicitly set, this Component's element will
         * simply have its own natural size. If set to `auto`, it will set the width to `null`
         * meaning it will have its own natural size.
         * @accessor
         * @evented
         */
        width: null,
        /**
         * @cfg {Number/String} height
         * The height of this Component; must be a valid CSS length value, e.g: `300`, `100px`,
         * `30%`, etc. By default, if this is not explicitly set, this Component's element will
         * simply have its own natural size. If set to `auto`, it will set the width to `null`
         * meaning it will have its own natural size.
         * @accessor
         * @evented
         */
        height: null,
        /**
         * @cfg {Boolean} [hidden]
         * Whether or not this Component is hidden (its CSS `display` property is set to `none`).
         *
         * Defaults to `true` for {@link #floated} Components.
         * @accessor
         * @evented
         */
        hidden: null,
        /**
         * @cfg {Boolean} [disabled]
         * Whether or not this component is disabled
         * @accessor
         * @evented
         */
        disabled: null
    },
    /**
     * @property {Array} template
     * An array of child elements to use as the children of the main element in the {@link
     * #element} template.  Only used if "children" are not specified explicitly in the
     * {@link #element} template.
     * @protected
     */
    template: [],
    /**
     * The base CSS class to apply to this widget's element.
     * Used as the prefix for {@link #ui}-specific class names.
     * Defaults to the value of {@link #classCls} or (`x-` + the {@link #xtype}) of the widget
     * if {@link #classCls} is `null`
     * @protected
     * @property
     */
    baseCls: null,
    /**
     * A CSS class to apply to the main element that will be inherited down the class
     * hierarchy.  Subclasses may override this property on their prototype to add their
     * own CSS class in addition to the CSS classes inherited from ancestor classes via
     * the prototype chain.  For example
     *
     *     Ext.define('Foo', {
     *         extend: 'Ext.Widget',
     *         classCls: 'foo'
     *     });
     *
     *     Ext.define('Bar', {
     *         extend: 'Foo',
     *         classCls: 'bar'
     *     });
     *
     *     var bar = new Bar();
     *
     *     console.log(bar.element.className); // outputs 'foo bar'
     *
     * @protected
     * @property
     */
    classCls: null,
    /**
     * When set to `true` during widget class definition, that class will be the "root" for
     * {@link #classCls} inheritance. Derived classes may set this to `true` to avoid
     * inheriting a {@link #classCls} from their superclass.
     * @property
     * @protected
     */
    classClsRoot: true,
    // default empty classClsList since Ext.Widget has no classCls of its own
    classClsList: [],
    clearPropertiesOnDestroy: 'async',
    focusEl: 'element',
    ariaEl: 'element',
    spaceRe: /\s+/,
    /**
     * @property {String} [noBorderCls] The CSS class to add to this component should not have
     * a border.
     * @private
     * @readonly
     */
    noBorderCls: Ext.baseCSSPrefix + 'noborder-trbl',
    borderedCls: Ext.baseCSSPrefix + 'bordered',
    disabledCls: Ext.baseCSSPrefix + 'disabled',
    heightedCls: Ext.baseCSSPrefix + 'heighted',
    widthedCls: Ext.baseCSSPrefix + 'widthed',
    constructor: function(config) {
        var me = this,
            baseCls = me.baseCls,
            renderTo = config && config.renderTo,
            controller;
        me.$iid = ++Ext.$nextIid;
        if (baseCls == null || baseCls === true) {
            me.baseCls = me.classCls || Ext.baseCSSPrefix + me.xtype;
        }
        if (config && ('baseCls' in config)) {
            Ext.raise('baseCls cannot be used as an instance config. It must be specified ' + 'at class definition time.');
        }
        // We want to determine very early on whether or not we are a reference holder,
        // so peek at either the incoming config or the class config to see if we have
        // a controller defined.
        if ((config && config.controller) || me.config.controller) {
            me.referenceHolder = true;
        }
        me.initId(config);
        me.initElement();
        if (renderTo) {
            config = Ext.apply({}, config);
            delete config.renderTo;
        }
        me.mixins.observable.constructor.call(me, config);
        // Wait until configs have run to do this
        if (me.focusable) {
            me.initFocusableEvents(true);
        }
        me.syncUiCls();
        Ext.ComponentManager.register(me);
        controller = me.getController();
        if (controller) {
            controller.init(me);
        }
        if (renderTo) {
            me.setRenderTo(renderTo);
        }
    },
    afterCachedConfig: function() {
        // This method runs once for the first instance of this Widget type that is
        // created.  It runs after the element config has been processed for the first
        // instance, and after all the cachedConfigs (whose appliers/updaters may modify
        // the element) have been initialized.  At this point we are ready to take the
        // DOM that was generated for the first Element instance, clone it, and cache it
        // on the prototype, so that it can be cloned by future instance to create their
        // elements (see initElement).
        var me = this,
            prototype = me.self.prototype,
            referenceList = me.referenceList,
            renderElement = me.renderElement,
            renderTemplate, element, i, ln, reference, elements;
        // This is where we take the first instance's DOM and clone it as the template
        // for future instances
        prototype.renderTemplate = renderTemplate = document.createDocumentFragment();
        renderTemplate.appendChild(renderElement.clone(true, true));
        elements = renderTemplate.querySelectorAll('[id]');
        for (i = 0 , ln = elements.length; i < ln; i++) {
            element = elements[i];
            element.removeAttribute('id');
        }
        // initElement skips removal of reference attributes for the first instance so that
        // the reference attributes will be present in the cached element when it is cloned.
        // Now that we're done cloning and caching the template element, it is safe to
        // remove the reference attributes from this instance's elements
        for (i = 0 , ln = referenceList.length; i < ln; i++) {
            reference = referenceList[i];
            me[reference].dom.removeAttribute('reference');
        }
    },
    applyHidden: function(hidden) {
        return !!hidden;
    },
    applyDisabled: function(disabled) {
        return !!disabled;
    },
    updateDisabled: function(disabled) {
        var me = this,
            container = me.ownerFocusableContainer;
        if (container) {
            if (disabled) {
                if (!container.beforeFocusableChildDisable.$nullFn) {
                    container.beforeFocusableChildDisable(me);
                }
            } else {
                if (!container.beforeFocusableChildEnable.$nullFn) {
                    container.beforeFocusableChildEnable(me);
                }
            }
        }
        me.element.toggleCls(me.disabledCls, disabled);
        if (me.focusable) {
            if (disabled) {
                me.disableFocusable();
            } else {
                me.enableFocusable();
            }
        }
        if (container) {
            if (disabled) {
                if (!container.onFocusableChildDisable.$nullFn) {
                    container.onFocusableChildDisable(me);
                }
            } else {
                if (!container.onFocusableChildEnable.$nullFn) {
                    container.onFocusableChildEnable(me);
                }
            }
        }
    },
    /**
     * Disables this Component
     */
    disable: function() {
        this.setDisabled(true);
    },
    /**
     * Enables this Component
     */
    enable: function() {
        this.setDisabled(false);
    },
    /**
     * Returns `true` if this Component is currently disabled.
     * @return {Boolean} `true` if currently disabled.
     */
    isDisabled: function() {
        return this.getDisabled();
    },
    /**
     * Returns `true` if this Component is not currently disabled.
     * @return {Boolean} `true` if not currently disabled.
     */
    isEnabled: function() {
        return !this.getDisabled();
    },
    applyTouchAction: function(touchAction, oldTouchAction) {
        if (oldTouchAction != null) {
            touchAction = Ext.merge({}, oldTouchAction, touchAction);
        }
        return touchAction;
    },
    applyWidth: function(width) {
        return this.filterLengthValue(width);
    },
    applyHeight: function(height) {
        return this.filterLengthValue(height);
    },
    updateBorder: function(border) {
        var me = this;
        // If the border is null it means we should not suppress the border
        border = border || border === null;
        me.toggleCls(me.noBorderCls, !border);
        me.toggleCls(me.borderedCls, !!border);
    },
    clearListeners: function() {
        var me = this;
        me.mixins.observable.clearListeners.call(me);
        me.mixins.componentDelegation.clearDelegatedListeners.call(me);
    },
    /**
     * Destroys the Widget. This method should not be overridden in custom Widgets,
     * because it sets the flags and does final cleanup that must go last. Instead,
     * override {@link #doDestroy} method to add functionality at destruction time.
     */
    destroy: function() {
        var me = this;
        // isDestroying added for compat reasons
        me.isDestroying = me.destroying = true;
        me.destroy = Ext.emptyFn;
        me.doDestroy();
        // We need to defer clearing listeners until after doDestroy() completes,
        // to let the interested parties fire events until the very end.
        me.clearListeners();
        // This just makes it hard to ask "was destroy() called?":
        // me.isDestroying = me.destroying = false; // removed in 7.0
        // ComponentDelegation mixin does not install "after" interceptor on the
        // base class destructor so we need to call it explicitly.
        me.mixins.componentDelegation.destroyComponentDelegation.call(me);
        me.callParent();
    },
    /**
     * Perform the actual destruction sequence. This is the method to override in your
     * subclasses to add steps specific to the destruction of custom Component or Widget.
     *
     * As a rule of thumb, subclasses should destroy their child Components, Elements,
     * and/or other objects before calling parent method. Any object references will be
     * nulled after this method has finished, to prevent the possibility of memory leaks.
     *
     * @since 6.2.0
     */
    doDestroy: function() {
        var me = this,
            referenceList = me.referenceList,
            container = me.ownerFocusableContainer,
            i, ln, reference;
        // Many non-contained Widgets use the ownerCmp link to find their logical owner.
        me.ownerCmp = null;
        if (container && !container.onFocusableChildDestroy.$nullFn) {
            container.onFocusableChildDestroy(me);
        }
        // Destroy all element references
        for (i = 0 , ln = referenceList.length; i < ln; i++) {
            reference = referenceList[i];
            if (me.hasOwnProperty(reference)) {
                me[reference].destroy();
                me[reference] = null;
            }
        }
        me.destroyBindable();
        Ext.ComponentManager.unregister(me);
    },
    doFireEvent: function(eventName, args, bubbles) {
        var me = this,
            ev, ret;
        ret = me.mixins.observable.doFireEvent.call(me, eventName, args, bubbles);
        // Could have been destroyed in event handler.
        if (ret !== false && !me.destroyed) {
            ev = me.events[eventName];
            // Also, a suspendEvent() call on the target could be in effect:
            if (!ev || !ev.suspended) {
                ret = me.mixins.componentDelegation.doFireDelegatedEvent.call(me, eventName, args);
            }
        }
        return ret;
    },
    getBubbleTarget: function() {
        return this.getRefOwner();
    },
    /**
     * A template method for modifying the {@link #element} config before it is processed.
     * By default adds the result of `this.getTemplate()` as the `children` array of
     * {@link #element} if `children` were not specified in the original
     * {@link #element} config.  Typically this method should not need to be implemented
     * in subclasses.  Instead the {@link #element} property should be use to configure
     * the element template for a given Widget subclass.
     *
     * This method is called once when the first instance of each Widget subclass is
     * created.  The element config object that is returned is cached and used as the template
     * for all successive instances.  The scope object for this method is the class prototype,
     * not the instance.
     *
     * @return {Object} the element config object
     * @protected
     */
    getElementConfig: function() {
        var me = this,
            el = me.element;
        if (!('children' in el)) {
            el = Ext.apply({
                children: me.getTemplate()
            }, el);
        }
        return el;
    },
    /**
     * Returns the height and width of the Component.
     * @return {Object} The current `height` and `width` of the Component.
     * @return {Number} return.width
     * @return {Number} return.height
     */
    getSize: function() {
        return this.el.getSize();
    },
    getTemplate: function() {
        return Ext.clone(this.template);
    },
    /**
     * @private
     */
    getClassCls: function() {
        var proto = this.self.prototype,
            prototype = proto,
            classes, classCls, i, ln;
        while (prototype) {
            classCls = prototype.hasOwnProperty('classCls') ? prototype.classCls : null;
            if (classCls) {
                if (classCls instanceof Array) {
                    for (i = 0 , ln = classCls.length; i < ln; i++) {
                        (classes || (classes = [])).push(classCls[i]);
                    }
                } else {
                    (classes || (classes = [])).push(classCls);
                }
            }
            if (prototype.classClsRoot && prototype.hasOwnProperty('classClsRoot')) {
                break;
            }
            prototype = prototype.superclass;
        }
        if (classes) {
            proto.classClsList = classes;
        }
        return classes;
    },
    hide: function() {
        this.setHidden(true);
    },
    /**
     * Initializes the Element for this Widget instance.  If this is the first time a
     * Widget of this type has been instantiated the {@link #element} config will be
     * processed to create an Element.  This Element is then cached on the prototype (see
     * afterCachedConfig) so that future instances can obtain their element by simply
     * cloning the Element that was cached by the first instance.
     * @protected
     */
    initElement: function() {
        var me = this,
            prototype = me.self.prototype,
            id = me.getId(),
            // The double assignment is intentional to workaround a JIT issue that prevents
            // me.referenceList from being assigned in random scenarios. The issue occurs
            // on 4th gen  iPads and lower, possibly other older iOS devices. See EXTJS-16494.
            referenceList = me.referenceList = me.referenceList = [],
            isFirstInstance = !prototype.hasOwnProperty('renderTemplate'),
            /**
             * @property {Object} uiReferences
             * @private
             * A map that tracks all reference elements configured with a `uiCls`.
             * Contains the `element` reference by default since the `element` always gets
             * non-suffixed ui-specific CSS class names added to it (see {@link #syncUiCls})
             */
            uiReferences = prototype.hasOwnProperty('uiReferences') ? prototype.uiReferences : (prototype.uiReferences = {
                element: ''
            }),
            renderTemplate, renderElement, renderConfig, element, referenceNodes, i, ln, referenceNode, reference, classCls, uiCls, baseCls, /* eslint-disable-next-line no-unused-vars */
            referenceElement;
        if (isFirstInstance) {
            // this is the first instantiation of this widget type.  Process the element
            // config from scratch to create our Element.
            renderTemplate = document.createDocumentFragment();
            renderConfig = me.processElementConfig.call(prototype);
            renderElement = Ext.Element.create(renderConfig, true);
            renderTemplate.appendChild(renderElement);
            // Collect all nodes that were configured with a uiCls and stash the reference
            // and uiCls names in a map so they can be used to update the ui class names
            // at a later time (see syncUiCls).
            referenceNodes = renderTemplate.querySelectorAll('[uiCls]');
            for (i = 0 , ln = referenceNodes.length; i < ln; i++) {
                referenceNode = referenceNodes[i];
                reference = referenceNode.getAttribute('reference');
                uiCls = referenceNode.getAttribute('uiCls');
                if (!reference) {
                    Ext.raise('Cannot render element with uiCls="' + uiCls + '". uiCls is only allowed on elements that have a reference name.');
                }
                uiReferences[reference] = uiCls;
                // uiCls attribute has served its purpose - we have cached it in the
                // uiReferences map so it does not need to remain in the template
                referenceNode.removeAttribute('uiCls');
            }
        } else {
            // we have already created an instance of this Widget type, so the element
            // config has already been processed, and the resulting DOM has been cached on
            // the prototype (see afterCachedConfig).  This means we can obtain our element
            // by simply cloning the cached element.
            renderTemplate = me.renderTemplate.cloneNode(true);
            renderElement = renderTemplate.firstChild;
        }
        referenceNodes = renderTemplate.querySelectorAll('[reference]');
        for (i = 0 , ln = referenceNodes.length; i < ln; i++) {
            referenceNode = referenceNodes[i];
            reference = referenceNode.getAttribute('reference');
            if (!isFirstInstance) {
                // on first instantiation we do not clean the reference attributes here.
                // This is because this instance's element will be used as the template
                // for future instances, and we need the reference attributes to be
                // present in the template so that future instances can resolve their
                // references.  afterCachedConfig is responsible for removing the
                // reference attributes from the DOM for the first instance after the
                // Element has been cloned and cached as the template.
                referenceNode.removeAttribute('reference');
            }
            if (reference === 'element') {
                if (element) {
                    // already resolved a reference named element - can't have two
                    Ext.raise("Duplicate 'element' reference detected in '" + me.$className + "' template.");
                }
                referenceNode.id = id;
                // element reference needs to be established ASAP, so add the reference
                // immediately, not "on-demand"
                element = me.el = me.addElementReference(reference, referenceNode);
                // Poke our id in our magic attribute to enable Component#from
                element.dom.setAttribute('data-componentid', id);
                if (isFirstInstance) {
                    classCls = me.getClassCls();
                    if (classCls) {
                        element.addCls(classCls);
                    }
                    baseCls = me.baseCls;
                    if (baseCls && (baseCls !== me.classCls)) {
                        element.addCls(baseCls);
                    }
                }
            } else {
                uiCls = uiReferences[reference];
                if (uiCls && isFirstInstance) {
                    // Elements configured with a uiCls must always have non-ui-specific
                    // class names for base styling.  For example, if the classCls is x-foo
                    // a reference element with a uiCls of 'bar' must always have a class
                    // name of x-foo-bar, regardless of whether or not the widget was
                    // configured with a ui.
                    // On the first instance these element must be immediately instantiated
                    // so that the CSS class names can be added in the render template,
                    // but they can be lazily instantiated on successive instances.
                    referenceElement = me.addElementReference(reference, referenceNode);
                    me.initUiReference(reference, uiCls, false);
                } else {
                    me.addElementReferenceOnDemand(reference, referenceNode);
                }
            }
            // At this point focusEl and ariaEl are still reference names
            if (reference === me.focusEl) {
                me.addElementReference('focusEl', referenceNode);
            }
            if (reference === me.ariaEl) {
                me.addElementReferenceOnDemand('ariaEl', referenceNode);
            }
            referenceList.push(reference);
        }
        if (!element) {
            Ext.raise("No 'element' reference found in '" + me.$className + "' template.");
        }
        if (renderElement === element.dom) {
            me.renderElement = element;
        } else {
            me.addElementReferenceOnDemand('renderElement', renderElement);
        }
        renderElement.setAttribute(me.dataXid, me.$iid);
    },
    dataXid: 'data-' + Ext.baseCSSPrefix.substr(0, Ext.baseCSSPrefix.length - 1) + 'id',
    /**
     * Tests whether this Widget matches a {@link Ext.ComponentQuery ComponentQuery}
     * selector string.
     * @param {String} selector The selector string to test against.
     * @return {Boolean} `true` if this Widget matches the selector.
     */
    is: function(selector) {
        return Ext.ComponentQuery.is(this, selector);
    },
    /**
     * Returns `true` if this Component is currently hidden.
     * @param {Boolean/Ext.Widget} [deep=false] `true` to check if this component
     * is hidden because a parent container is hidden. Alternatively, a reference to the
     * top-most parent at which to stop climbing.
     * @return {Boolean} `true` if currently hidden.
     */
    isHidden: function(deep) {
        var hidden = !!this.getHidden(),
            owner;
        if (!hidden && deep) {
            owner = this.getRefOwner();
            while (owner && owner !== deep && !hidden) {
                hidden = !!owner.getHidden();
                owner = owner.getRefOwner();
            }
        }
        return hidden;
    },
    /**
     * Returns `true` if this Component is currently visible.
     *
     * A Widget is visible if its element is not hidden, *and* has been
     * {@link #property!rendered} *and* has not been destroyed.
     *
     * @param {Boolean} [deep=false] `true` to check if this component
     * is visible and all parents are also visible.
     *
     * Contrast this with the {@link #isHidden} method which just checks the
     * hidden state of the component.
     * @return {Boolean} `true` if currently visible.
     */
    isVisible: function(deep) {
        return this.rendered && !this.destroyed && !this.isHidden(deep);
    },
    /**
     * Tests whether or not this Component is of a specific xtype. This can test whether this
     * Component is descended from the xtype (default) or whether it is directly of the xtype
     * specified (`shallow = true`).
     * **If using your own subclasses, be aware that a Component must register its own xtype
     * to participate in determination of inherited xtypes.__
     *
     * For a list of all available xtypes, see the {@link Ext.Component} header.
     *
     * Example usage:
     *
     *     var t = new Ext.field.Text();
     *     var isText = t.isXType('textfield'); // true
     *     var isBoxSubclass = t.isXType('field'); // true, descended from Ext.field.Field
     *     var isBoxInstance = t.isXType('field', true); // false, not a direct
     *                                                   // Ext.field.Field instance
     *
     * @param {String} xtype The xtype to check for this Component.
     * @param {Boolean} shallow (optional) `false` to check whether this Component is descended
     * from the xtype (this is the default), or `true` to check whether this Component is directly
     * of the specified xtype.
     * @return {Boolean} `true` if this component descends from the specified xtype, `false`
     * otherwise.
     */
    isXType: function(xtype, shallow) {
        return shallow ? (Ext.Array.indexOf(this.xtypes, xtype) !== -1) : !!this.xtypesMap[xtype];
    },
    /**
     * Gets a named template instance for this class. See {@link Ext.XTemplate#getTpl}.
     * @param {String} name The name of the property that holds the template.
     * @return {Ext.XTemplate} The template, `null` if not found.
     *
     * @since 6.2.0
     */
    lookupTpl: function(name) {
        return Ext.XTemplate.getTpl(this, name);
    },
    owns: function(element) {
        var result = false,
            cmp;
        if (element.isEvent) {
            element = element.target;
        } else if (element.isElement) {
            element = element.dom;
        }
        cmp = Ext.Component.from(element);
        if (cmp) {
            result = (cmp === this) || (!!cmp.up(this));
        }
        return result;
    },
    render: function(container, insertBeforeElement) {
        if (container && container.isWidget) {
            container = container.el;
        }
        /* eslint-disable-next-line vars-on-top */
        var dom = this.renderElement.dom,
            containerDom = Ext.getDom(container),
            insertBeforeChildDom;
        if (Ext.isNumber(insertBeforeChildDom)) {
            insertBeforeElement = containerDom.childNodes[insertBeforeElement];
        }
        insertBeforeChildDom = Ext.getDom(insertBeforeElement);
        if (containerDom) {
            if (insertBeforeChildDom) {
                containerDom.insertBefore(dom, insertBeforeChildDom);
            } else {
                containerDom.appendChild(dom);
            }
            // A component is rendered if it is in the document.
            // A display:none component will have no offsetParent
            // so offsetParent is not a valid test for renderedness.
            this.setRendered(Ext.getBody().contains(dom), true);
        }
    },
    /**
     * Toggles the specified CSS class on this element (removes it if it already exists,
     * otherwise adds it).
     * @param {String} className The CSS class to toggle.
     * @param {Boolean} [state] If specified as `true`, causes the class to be added. If
     * specified as `false`, causes the class to be removed.
     * @chainable
     */
    toggleCls: function(className, state) {
        this.element.toggleCls(className, state);
        return this;
    },
    resolveListenerScope: function(defaultScope, skipThis) {
        // break the tie between Observable and Inheritable resolveListenerScope
        return this.mixins.inheritable.resolveListenerScope.call(this, defaultScope, skipThis);
    },
    /**
     * Sets the size of the Component.
     * @param {Number} width The new width for the Component.
     * @param {Number} height The new height for the Component.
     */
    setSize: function(width, height) {
        // Allow setSize to be called with a result from getSize.
        if (width && typeof width === 'object') {
            return this.setSize(width.width, width.height);
        }
        if (width !== undefined) {
            this.setWidth(width);
        }
        if (height !== undefined) {
            this.setHeight(height);
        }
    },
    show: function() {
        this.setHidden(false);
    },
    /**
     * Adds a CSS class (or classes) to this Component's rendered element.
     * @param {String/String[]} cls The CSS class(es) to add.
     * @param {String} [prefix=""] Optional prefix to add to each class.
     * @param {String} [suffix=""] Optional suffix to add to each class.
     */
    addCls: function(cls, prefix, suffix) {
        if (!this.destroyed) {
            this.el.replaceCls(null, cls, prefix, suffix);
        }
    },
    applyCls: function(cls) {
        return cls && Ext.dom.Element.splitCls(cls);
    },
    applyUi: function(ui) {
        return this.parseUi(ui, true);
    },
    /**
     * Removes the given CSS class(es) from this widget's primary element.
     * @param {String/String[]} cls The class(es) to remove.
     * @param {String} [prefix=""] Optional prefix to prepend before each class.
     * @param {String} [suffix=""] Optional suffix to append to each class.
     */
    removeCls: function(cls, prefix, suffix) {
        if (!this.destroyed) {
            this.el.replaceCls(cls, null, prefix, suffix);
        }
    },
    /**
     * Replaces specified classes with the newly specified classes.
     * It uses the {@link #addCls} and {@link #removeCls} methods, so if the class(es) you
     * are removing don't exist, it will still add the new classes.
     * @param {String/String[]} oldCls The class(es) to remove.
     * @param {String/String[]} newCls The class(es) to add.
     * @param {String} [prefix=""] Optional prefix to prepend before each class.
     * @param {String} [suffix=""] Optional suffix to append to each class.
     */
    replaceCls: function(oldCls, newCls, prefix, suffix) {
        if (!this.destroyed) {
            this.el.replaceCls(oldCls, newCls, prefix, suffix);
        }
    },
    /**
     * Checks if the specified CSS class exists on this element's DOM node.
     * @param {String} className The CSS class to check for.
     * @return {Boolean} `true` if the class exists, else `false`.
     * @method
     */
    hasCls: function(className) {
        return this.el.hasCls(className);
    },
    /**
     * @private
     * All cls methods directly report to the {@link #cls} configuration, so anytime it changes,
     * {@link #updateCls} will be called
     */
    updateCls: function(newCls, oldCls) {
        this.element.replaceCls(oldCls, newCls);
    },
    updateHidden: function(hidden) {
        var me = this,
            element = me.renderElement,
            container = me.ownerFocusableContainer;
        // If we are owned by a FocusableContainer, allow that to redirect
        // focus if we are being hidden.
        if (container) {
            if (hidden) {
                if (!container.beforeFocusableChildHide.$nullFn) {
                    container.beforeFocusableChildHide(me);
                }
            } else {
                if (!container.beforeFocusableChildShow.$nullFn) {
                    container.beforeFocusableChildShow(me);
                }
            }
        }
        // Not owned by a FocusableContainer - revert focus to previous focus holder.
        else if (hidden) {
            // Part of the Focusable mixin API.
            // If we have focus now, move focus back to whatever had it before.
            me.revertFocus();
        }
        if (element && !element.destroyed) {
            if (hidden) {
                element.hide();
            } else {
                element.show();
            }
        }
        if (me.focusableContainer && me.activateFocusableContainer) {
            me.activateFocusableContainer(!hidden);
        }
        if (container) {
            if (hidden) {
                if (!container.onFocusableChildHide.$nullFn) {
                    container.onFocusableChildHide(me);
                }
            } else {
                if (!container.onFocusableChildShow.$nullFn) {
                    container.onFocusableChildShow(me);
                }
            }
        }
    },
    updateMargin: function(margin) {
        this.element.setMargin(margin);
    },
    updateRipple: function(ripple) {
        var me = this,
            el = me.el;
        if (el) {
            el.un('touchstart', 'onRippleStart', me);
            el.un('touchend', 'onRippleStart', me);
            el.destroyAllRipples();
            if (ripple.release) {
                el.on('touchend', 'onRippleStart', me);
            } else {
                el.on('touchstart', 'onRippleStart', me);
            }
        }
    },
    shouldRipple: function(e) {
        var me = this,
            disabled = me.getDisabled && me.getDisabled(),
            el = me.el,
            ripple = !disabled && me.getRipple(),
            target;
        if (ripple && e) {
            target = e.getTarget(me.noRippleSelector);
            if (target) {
                if ((el.dom === target) || el.contains(target)) {
                    ripple = null;
                }
            }
        }
        return ripple;
    },
    onRippleStart: function(e) {
        var ripple = this.shouldRipple(e);
        if (e.button === 0 && ripple) {
            this.el.ripple(e, ripple);
        }
    },
    /**
     * @protected
     */
    applyStyle: function(style, oldStyle) {
        // If we're doing something with data binding, say:
        // style: {
        //     backgroundColor: 'rgba({r}, {g}, {b}, 1)'
        // }
        // The inner values will change, but the object won't, so force
        // a copy to be created here if necessary
        if (oldStyle && style === oldStyle && Ext.isObject(oldStyle)) {
            style = Ext.apply({}, style);
        }
        this.element.applyStyles(style);
        return null;
    },
    getStyle: function() {
        Ext.Error.raise("'style' is a write-only config. To query element styles use " + "the Ext.dom.Element API.");
    },
    updateRenderTo: function(newContainer) {
        this.render(newContainer);
    },
    updateTouchAction: function(touchAction) {
        var name, childEl, value, hasRootActions;
        for (name in touchAction) {
            childEl = this[name];
            value = touchAction[name];
            if (childEl && childEl.isElement) {
                childEl.setTouchAction(value);
            } else {
                hasRootActions = true;
            }
        }
        if (hasRootActions) {
            this.el.setTouchAction(touchAction);
        }
    },
    updateUi: function() {
        if (!this.isConfiguring) {
            this.syncUiCls();
        }
    },
    /**
     * @param width
     * @protected
     */
    updateWidth: function(width) {
        var el = this.el;
        el.setWidth(width);
        el.toggleCls(this.widthedCls, width != null && width !== 'auto');
    },
    /**
     * @param height
     * @protected
     */
    updateHeight: function(height) {
        var el = this.el;
        el.setHeight(height);
        el.toggleCls(this.heightedCls, height != null && height !== 'auto');
    },
    /**
     * @private
     */
    isWidthed: function() {
        var width = this.getWidth();
        return width != null && width !== 'auto';
    },
    /**
     * @private
     */
    isHeighted: function() {
        var height = this.getHeight();
        return height != null && height !== 'auto';
    },
    /**
     * Walks up the ownership hierarchy looking for an ancestor Component which matches
     * the passed simple selector.
     *
     * Example:
     *
     *     var owningTabPanel = grid.up('tabpanel');
     *
     * @param {String} selector (optional) The simple selector to test.
     * @param {String/Number/Ext.Component} [limit] This may be a selector upon which to stop
     * the upward scan, or a limit of the number of steps, or Component reference to stop on.
     * @return {Ext.Container} The matching ancestor Container (or `undefined` if no match
     * was found).
     */
    up: function(selector, limit) {
        var result = this.getRefOwner(),
            limitSelector = typeof limit === 'string',
            limitCount = typeof limit === 'number',
            limitComponent = limit && limit.isComponent,
            steps = 0;
        if (selector) {
            for (; result; result = result.getRefOwner()) {
                if (result.destroyed) {
                    return null;
                }
                steps++;
                if (selector.isComponent || selector.isWidget) {
                    if (result === selector) {
                        return result;
                    }
                } else {
                    if (Ext.ComponentQuery.is(result, selector)) {
                        return result;
                    }
                }
                // Stop when we hit the limit selector
                if (limitSelector && result.is(limit)) {
                    return;
                }
                if (limitCount && steps === limit) {
                    return;
                }
                if (limitComponent && result === limit) {
                    return;
                }
            }
        }
        return result;
    },
    updateLayout: Ext.emptyFn,
    // empty fn for modern/classic compat
    updateInstanceCls: function(instanceCls, oldInstanceCls) {
        var me = this,
            el = me.el,
            classClsList = me.classClsList,
            Array = Ext.Array,
            uiReferences = me.uiReferences,
            referenceName, referenceElement, i, ln, cls, uiCls;
        if (oldInstanceCls) {
            el.removeCls(oldInstanceCls);
            oldInstanceCls = Array.from(oldInstanceCls);
            for (i = 0 , ln = oldInstanceCls.length; i < ln; i++) {
                cls = oldInstanceCls[i];
                Array.remove(classClsList, cls);
                for (referenceName in uiReferences) {
                    referenceElement = me[referenceName];
                    uiCls = uiReferences[referenceName];
                    referenceElement.removeCls(cls + '-' + uiCls);
                }
            }
        }
        if (instanceCls) {
            el.addCls(instanceCls);
            instanceCls = Array.from(instanceCls);
            // clone the classClsList so that the instanceCls is not shared on the prototype
            me.classClsList = classClsList.concat(instanceCls);
            for (i = 0 , ln = instanceCls.length; i < ln; i++) {
                cls = instanceCls[i];
                for (referenceName in uiReferences) {
                    referenceElement = me[referenceName];
                    uiCls = uiReferences[referenceName];
                    referenceElement.addCls(cls + '-' + uiCls);
                }
            }
        }
        if (!me.isConfiguring) {
            me.syncUiCls();
        }
    },
    // getter for backward compatibility with < 6.5 where baseCls was a config
    getBaseCls: function() {
        return this.baseCls;
    },
    setBaseCls: function() {
        Ext.raise('baseCls cannot be reconfigured. It must be specified at class definition time.');
    },
    onClassExtended: function(Class, members) {
        if (members.config && members.config.baseCls) {
            Ext.raise('baseCls must be declared directly on the class body. Please move it ' + 'outside of the config block.');
        }
    },
    //-------------------------------------------------------------------------
    privates: {
        _hideModes: {
            clip: 'CLIP',
            display: 'DISPLAY',
            offsets: 'OFFSETS',
            opacity: 'OPACITY',
            visibility: 'VISIBILITY'
        },
        noRippleSelector: '.' + Ext.baseCSSPrefix + 'no-ripple',
        /**
         * Reduces instantiation time for a Widget by lazily instantiating Ext.Element
         * references the first time they are used.  This optimization only works for elements
         * with no listeners specified.
         *
         * @param {String} name The name of the reference
         * @param {HTMLElement} domNode
         * @private
         */
        addElementReferenceOnDemand: function(name, domNode) {
            if (this._elementListeners[name]) {
                // if the element was configured with listeners then we cannot add the
                // reference on demand because we need to make sure the element responds
                // immediately to any events, even if its reference is never accessed
                this.addElementReference(name, domNode);
            } else {
                // no listeners - element reference can be resolved on demand.
                // TODO: measure if this has any significant performance impact.
                Ext.Object.defineProperty(this, name, {
                    get: function() {
                        if (this.destroyed) {
                            return null;
                        }
                        // remove the property that was defined using defineProperty because
                        // addElementReference will set the property on the instance, - the
                        // getter is not needed after the first access.
                        delete this[name];
                        return this.addElementReference(name, domNode);
                    },
                    configurable: true
                });
            }
        },
        /**
         * Adds an element reference to this Widget instance.
         * @param {String} name The name of the reference
         * @param {HTMLElement} domNode
         * @return {Ext.dom.Element}
         * @private
         */
        addElementReference: function(name, domNode) {
            var me = this,
                referenceEl = me[name] = Ext.get(domNode),
                listeners = me._elementListeners[name],
                eventName, listener;
            referenceEl.skipGarbageCollection = true;
            referenceEl.component = me;
            if (listeners) {
                // TODO: These references will be needed when we use delegation to listen
                // for element events, but for now, we'll just attach the listeners directly
                // referenceEl.reference = name;
                // referenceEl.component = me;
                // referenceEl.listeners = listeners;
                // At this point "listeners" exists on the class prototype.  We need to clone
                // it before poking the scope reference onto it, because it is used as the
                // options object by Observable and so can't be safely shared.
                //
                listeners = Ext.clone(listeners);
                // If the listener is specified as an object it needs to have the scope
                // option added to that object, for example:
                //
                //    {
                //        click: {
                //            fn: 'onClick',
                //            scope: this
                //        }
                //    }
                //
                for (eventName in listeners) {
                    listener = listeners[eventName];
                    if (typeof listener === 'object') {
                        listener.scope = me;
                    }
                }
                // The outermost listeners object always needs the scope option. This covers
                // a listeners object with the following shape:
                //
                //    {
                //        click: 'onClick'
                //        scope: this
                //    }
                //
                listeners.scope = me;
                // do this *after* the above loop over listeners
                // Hopefully in the future we can stop calling on() here, and just use
                // event delegation to dispatch events to Widgets that have declared their
                // listeners in their template.
                //
                referenceEl.on(listeners);
            }
            return referenceEl;
        },
        detachFromBody: function() {
            // See reattachToBody
            Ext.getDetachedBody().appendChild(this.element, true);
            this.isDetached = true;
        },
        reattachToBody: function() {
            var detachedBody;
            if (this.isDetached) {
                detachedBody = Ext.getDetachedBody();
                if (detachedBody.contains(this.element)) {
                    Ext.getBody().appendChild(this.element, true);
                }
            }
            // See detachFromBody
            this.isDetached = false;
        },
        /**
         * @private
         */
        doAddListener: function(name, fn, scope, options, order, caller, manager) {
            var me = this,
                elementName = options && options.element,
                delegate = options && options.delegate,
                listeners, eventOptions, option;
            if (elementName) {
                if (Ext.Array.indexOf(me.referenceList, elementName) === -1) {
                    Ext.Logger.error("Adding event listener with an invalid element reference of '" + elementName + "' for this component. Available values are: '" + me.referenceList.join("', '") + "'", me);
                }
                listeners = {};
                listeners[name] = fn;
                if (scope) {
                    listeners.scope = scope;
                }
                eventOptions = Ext.Element.prototype.$eventOptions;
                for (option in options) {
                    if (eventOptions[option]) {
                        listeners[option] = options[option];
                    }
                }
                me.mon(me[elementName], listeners);
                return;
            } else if (delegate) {
                me.mixins.componentDelegation.addDelegatedListener.call(me, name, fn, scope, options, order, caller, manager);
                return;
            }
            me.callParent([
                name,
                fn,
                scope,
                options,
                order,
                caller,
                manager
            ]);
        },
        doRemoveListener: function(eventName, fn, scope) {
            var me = this;
            me.mixins.observable.doRemoveListener.call(me, eventName, fn, scope);
            me.mixins.componentDelegation.removeDelegatedListener.call(me, eventName, fn, scope);
        },
        filterLengthValue: function(value) {
            if (!value && value !== 0) {
                return null;
            }
            return value;
        },
        /**
         * Called for the first instance of this Widget to create an object that contains the
         * listener configs for all of the element references keyed by reference name. The
         * object is cached on the prototype and has the following shape:
         *
         *     _elementListeners: {
         *         element: {
         *             click: 'onClick',
         *             scope: this
         *         },
         *         fooReference: {
         *             tap: {
         *                 fn: someFunction,
         *                 delay: 100
         *             }
         *         }
         *     }
         *
         * The returned object is prototype chained to the _elementListeners object of its
         * superclass, and each key in the object is prototype chained to object with the
         * corresponding key in the superclass _elementListeners.  This allows element
         * listeners to be inherited and overridden when subclassing widgets.
         *
         * This method is invoked with the prototype object as the scope
         *
         * @private
         */
        initElementListeners: function(elementConfig) {
            var prototype = this,
                superPrototype = prototype.self.superclass,
                superElementListeners = superPrototype._elementListeners,
                reference = elementConfig.reference,
                children = elementConfig.children,
                elementListeners, listeners, superListeners, ln, i;
            if (prototype.hasOwnProperty('_elementListeners')) {
                elementListeners = prototype._elementListeners;
            } else {
                elementListeners = prototype._elementListeners = (superElementListeners ? Ext.Object.chain(superElementListeners) : {});
            }
            if (reference) {
                listeners = elementConfig.listeners;
                if (listeners) {
                    if (superElementListeners) {
                        superListeners = superElementListeners[reference];
                        if (superListeners) {
                            listeners = Ext.Object.chain(superListeners);
                            Ext.apply(listeners, elementConfig.listeners);
                        }
                    }
                    elementListeners[reference] = listeners;
                    // null out the listeners on the elementConfig, since we are going to pass
                    // it to Element.create(), and don't want "listeners" to be treated as an
                    // attribute
                    elementConfig.listeners = null;
                }
            }
            if (children) {
                for (i = 0 , ln = children.length; i < ln; i++) {
                    prototype.initElementListeners(children[i]);
                }
            }
        },
        initId: function(config) {
            var me = this,
                defaultConfig = me.config,
                id = (config && config.id) || (defaultConfig && defaultConfig.id);
            if (id) {
                // setId() will normally be inherited from Identifiable, unless "id" is a
                // proper config, in which case it will be generated by the config system.
                me.setId(id);
                me.id = id;
            } else {
                // if no id configured, generate one (Identifiable)
                me.getId();
            }
        },
        measure: function(dimension) {
            return this.element.measure(dimension);
        },
        /**
         * Recursively processes the element templates for this class and its superclasses,
         * ascending the hierarchy until it reaches a superclass whose element template
         * has already been processed.  This method is invoked using the prototype as the scope.
         *
         * @private
         * @return {Object}
         */
        processElementConfig: function() {
            var prototype = this,
                superPrototype = prototype.self.superclass,
                elementConfig;
            if (prototype.hasOwnProperty('_elementConfig')) {
                elementConfig = prototype._elementConfig;
            } else {
                // cache the elementConfig on the prototype, since we may end up here multiple
                // times if there are multiple subclasses
                elementConfig = prototype._elementConfig = prototype.getElementConfig();
                if (superPrototype.isWidget) {
                    // Before initializing element listeners we must process the element template
                    // for our superclass so that we can chain our listeners to the superclass
                    // listeners
                    prototype.processElementConfig.call(superPrototype);
                }
                // initElementListeners needs to be called BEFORE passing the element config
                // along to Ext.Element.create().  This ensures that the listener meta data is
                // saved, and then the listeners objects are removed from the element config
                // so that they do not get added as attributes by create()
                prototype.initElementListeners(elementConfig);
            }
            return elementConfig;
        },
        parseUi: function(ui, asString) {
            ui = Ext.String.splitWords(ui);
            if (asString) {
                ui = ui.join(' ');
            }
            return ui;
        },
        addUi: function(ui) {
            this.setUi(this.doAddUi(ui, this.getUi()));
        },
        doAddUi: function(ui, oldUi) {
            var me = this,
                newUi = null,
                i, u, len;
            if (ui) {
                ui = me.parseUi(ui);
                len = ui.length;
                oldUi = me.parseUi(oldUi);
                for (i = 0; i < len; i++) {
                    u = ui[i];
                    if (Ext.Array.indexOf(oldUi, u) === -1) {
                        oldUi.push(u);
                    }
                }
                newUi = oldUi.join(' ');
            }
            return newUi;
        },
        removeUi: function(ui) {
            this.setUi(this.doRemoveUi(ui, this.getUi()));
        },
        doRemoveUi: function(ui, oldUi) {
            var me = this,
                newUi = null,
                i, u, index, len;
            if (ui) {
                ui = me.parseUi(ui);
                len = ui.length;
                oldUi = me.parseUi(oldUi);
                for (i = 0; i < len; i++) {
                    u = ui[i];
                    index = Ext.Array.indexOf(oldUi, u);
                    if (index !== -1) {
                        oldUi.splice(index, 1);
                    }
                }
                newUi = oldUi.join(' ');
            }
            return newUi;
        },
        /**
         * Initializes a "uiReference".  Ui rerefences are reference elements that have
         * classCls and ui info in their CSS class names.  They can be used by setting
         * uiCls in the template, or by invoking this method to setup the ui reference
         * after element/template initialization (Toolable uses this for its dock wrapper)
         * @param {String} referenceName
         * @param {String} uiCls
         * @param {Boolean} [isInstance=false] pass `false` if this is not an instance-level
         * reference
         * @private
         */
        initUiReference: function(referenceName, uiCls, isInstance) {
            var me = this,
                referenceElement = me[referenceName],
                baseCls = me.baseCls,
                classClsList = me.classClsList,
                cls = [],
                i, n;
            isInstance = (isInstance !== false);
            if (isInstance) {
                // clone so we don't modify the prototype uiReferences
                if (!me.hasOwnProperty('uiReferences')) {
                    me.uiReferences = Ext.clone(me.uiReferences);
                }
                me.uiReferences[referenceName] = uiCls;
            }
            uiCls = '-' + uiCls;
            if (baseCls && (baseCls !== me.classCls)) {
                cls.push(baseCls + uiCls);
            }
            if (classClsList) {
                for (i = 0 , n = classClsList.length; i < n; i++) {
                    cls.push(classClsList[i] + uiCls);
                }
            }
            referenceElement.addCls(cls);
            if (isInstance && !me.isConfiguring) {
                me.syncUiCls();
            }
        },
        syncUiCls: function(refs) {
            var me = this,
                ui = me.getUi(),
                currentUiCls = me.currentUiCls || (me.currentUiCls = {}),
                baseCls = me.baseCls,
                uiReferences = refs || me.uiReferences,
                classClsList = me.classClsList,
                classClsListLen = classClsList ? classClsList.length : 0,
                uiCls, uiLen, refName, refEl, cls, suffix, uiSuffix, i, j;
            if (ui) {
                ui = me.parseUi(ui);
                uiLen = ui.length;
            }
            for (refName in uiReferences) {
                refEl = me[refName];
                uiCls = [];
                if (refEl) {
                    cls = currentUiCls[refName];
                    if (cls) {
                        refEl.removeCls(cls);
                    }
                    if (ui) {
                        suffix = uiReferences[refName];
                        suffix = suffix ? ('-' + suffix) : '';
                        for (i = 0; i < uiLen; i++) {
                            uiSuffix = '-' + ui[i] + suffix;
                            if (baseCls && (baseCls !== me.classCls)) {
                                uiCls.push(baseCls + uiSuffix);
                            }
                            if (classClsList) {
                                for (j = 0; j < classClsListLen; j++) {
                                    uiCls.push(classClsList[j] + uiSuffix);
                                }
                            }
                        }
                        refEl.addCls(uiCls);
                        currentUiCls[refName] = uiCls;
                    }
                }
            }
        },
        applyHideMode: function(mode) {
            return mode || 'display';
        },
        updateHideMode: function(mode) {
            var me = this,
                el = me.el,
                shouldToggle = me.getHidden();
            if (!me._hideModes[mode]) {
                Ext.raise('Invalid hideMode: "' + mode + '" (must be one of: "' + Object.keys(me._hideModes).join('", "') + '")');
            }
            if (shouldToggle) {
                el.show();
            }
            me.renderElement.setVisibilityMode(Ext.Element[me._hideModes[mode]]);
            if (shouldToggle) {
                el.hide();
            }
        },
        updateUserCls: function(newCls, oldCls) {
            this.element.replaceCls(oldCls, newCls);
        }
    }
}, function(Widget) {
    var prototype = Widget.prototype;
    // event options for listeners that use the "element" event options must also include
    // event options from Ext.Element
    (prototype.$elementEventOptions = Ext.Object.chain(Ext.Element.prototype.$eventOptions)).element = 1;
    (prototype.$eventOptions = Ext.Object.chain(prototype.$eventOptions)).delegate = 1;
    /**
     * @member Ext
     * @method updateWidget
     * @inheritdoc Ext.Factory#update
     * @since 6.5.1
     */
    Ext.updateWidget = Ext.Factory.widget.update;
});

/**
 * @class Ext.Widget
 */
Ext.define('Ext.overrides.Widget', {
    override: 'Ext.Widget',
    uses: [
        'Ext.Component',
        'Ext.layout.component.Auto'
    ],
    $configStrict: false,
    isComponent: true,
    liquidLayout: true,
    // in Ext JS the rendered flag is set as soon as a component has its element.  Since
    // widgets always have an element when constructed, they are always considered to be
    // "rendered"
    rendered: true,
    rendering: true,
    config: {
        renderTo: null
    },
    constructor: function(config) {
        var me = this,
            renderTo;
        me.callParent([
            config
        ]);
        // initialize the component layout
        me.getComponentLayout();
        renderTo = me.getRenderTo();
        if (renderTo) {
            me.render(renderTo);
        }
    },
    addClsWithUI: function(cls) {
        this.el.addCls(cls);
    },
    afterComponentLayout: Ext.emptyFn,
    updateLayout: function() {
        var owner = this.getRefOwner();
        if (owner) {
            owner.updateLayout();
        }
    },
    destroy: function() {
        var me = this,
            ownerCt = me.ownerCt;
        if (ownerCt && ownerCt.remove) {
            ownerCt.remove(me, false);
        }
        me.callParent();
    },
    finishRender: function() {
        this.rendering = false;
        this.initBindable();
        this.initKeyMap();
    },
    getAnimationProps: function() {
        // see Ext.util.Animate mixin
        return {};
    },
    getComponentLayout: function() {
        var me = this,
            layout = me.componentLayout;
        if (!layout) {
            layout = me.componentLayout = new Ext.layout.component.Auto();
            layout.setOwner(me);
        }
        return layout;
    },
    getEl: function() {
        return this.element;
    },
    /**
     * @private
     * Needed for when widget is rendered into a grid cell. The class to add to the cell element.
     * @member Ext.Widget
     */
    getTdCls: function() {
        return Ext.baseCSSPrefix + this.getTdType() + '-' + (this.ui || 'default') + '-cell';
    },
    /**
     * @private
     * Partner method to {@link #getTdCls}.
     *
     * Returns the base type for the component. Defaults to return `this.xtype`, but
     * All derived classes of {@link Ext.form.field.Text TextField} can return the type 'textfield',
     * and all derived classes of {@link Ext.button.Button Button} can return the type 'button'
     * @member Ext.Widget
     */
    getTdType: function() {
        return this.xtype;
    },
    getItemId: function() {
        // needed by ComponentQuery
        return this.itemId || this.id;
    },
    getSizeModel: function() {
        return Ext.Component.prototype.getSizeModel.apply(this, arguments);
    },
    onAdded: function(container, pos, instanced) {
        var me = this;
        me.ownerCt = container;
        me.onInheritedAdd(me, instanced);
        // this component is no longer detached from the body
        me.isDetached = false;
    },
    onRemoved: function(destroying) {
        this.onInheritedRemove(destroying);
        this.ownerCt = this.ownerLayout = null;
    },
    parseBox: function(box) {
        return Ext.Element.parseBox(box);
    },
    removeClsWithUI: function(cls) {
        this.el.removeCls(cls);
    },
    render: function(container, position) {
        var me = this,
            element = me.element,
            proto = Ext.Component.prototype,
            nextSibling;
        if (!me.ownerCt || me.floating) {
            if (Ext.scopeCss) {
                element.addCls(proto.rootCls);
            }
            element.addCls(proto.borderBoxCls);
        }
        if (position) {
            nextSibling = container.childNodes[position];
            if (nextSibling) {
                Ext.fly(container).insertBefore(element, nextSibling);
                return;
            }
        }
        Ext.fly(container).appendChild(element);
        me.finishRender();
    },
    setPosition: function(x, y) {
        this.el.setLocalXY(x, y);
    },
    up: function() {
        return Ext.Component.prototype.up.apply(this, arguments);
    },
    isAncestor: function() {
        return Ext.Component.prototype.isAncestor.apply(this, arguments);
    },
    onFocusEnter: function() {
        return Ext.Component.prototype.onFocusEnter.apply(this, arguments);
    },
    onFocusLeave: function() {
        return Ext.Component.prototype.onFocusLeave.apply(this, arguments);
    },
    isLayoutChild: function(candidate) {
        var ownerCt = this.ownerCt;
        return ownerCt ? (ownerCt === candidate || ownerCt.isLayoutChild(candidate)) : false;
    },
    privates: {
        doAddListener: function(name, fn, scope, options, order, caller, manager) {
            if (name === 'painted' || name === 'resize') {
                this.element.doAddListener(name, fn, scope || this, options, order);
            }
            this.callParent([
                name,
                fn,
                scope,
                options,
                order,
                caller,
                manager
            ]);
        },
        doRemoveListener: function(name, fn, scope) {
            if (name === 'painted' || name === 'resize') {
                this.element.doRemoveListener(name, fn, scope);
            }
            this.callParent([
                name,
                fn,
                scope
            ]);
        }
    }
}, function(Cls) {
    var prototype = Cls.prototype;
    if (Ext.isIE9m) {
        // Since IE8/9 don't not support Object.defineProperty correctly we can't add the reference
        // nodes on demand, so we just fall back to adding all references up front.
        prototype.addElementReferenceOnDemand = prototype.addElementReference;
    }
});

/* eslint-disable max-len */
/**
 * This mixin provides classes with a `responsiveConfig` config that allows instances to
 * conditionally control config properties. This mixin is typically applied to the
 * `Ext.Component` and `Ext.Widget` classes via the `Ext.Responsive` override:
 *
 *      Ext.application({
 *          requires: [
 *              'Ext.Responsive'
 *          ],
 *
 *          // ...
 *      });
 *
 * Once this mixin is applied, config properties can be declaratively managed based on
 * current device characteristics (such as viewport width):
 *
 *      Ext.create({
 *          xtype: 'panel',
 *          title: 'Some Title',
 *
 *          layout: {
 *              type: 'hbox
 *          },
 *
 *          responsiveConfig: {
 *              'width < 800': {
 *                  layout: {
 *                      vertical: true
 *                  }
 *              },
 *              'width >= 800': {
 *                  layout: {
 *                      vertical: false
 *                  }
 *              }
 *          }
 *      });
 *
 * To use this mixin on other classes, you can mix it in directly:
 *
 *      Ext.define('ResponsiveClass', {
 *          mixin: [
 *              'Ext.mixin.Responsive'
 *          ],
 *
 *          responsiveConfig: {
 *              portrait: {
 *              },
 *
 *              landscape: {
 *              }
 *          }
 *      });
 *
 * For details see `{@link #responsiveConfig}`.
 * @since 5.0.0
 */
Ext.define('Ext.mixin.Responsive', function(Responsive) {
    return {
        // eslint-disable-line brace-style, max-len
        extend: 'Ext.Mixin',
        mixinConfig: {
            id: 'responsive',
            after: {
                destroy: 'destroy'
            }
        },
        config: {
            /**
         * @cfg {Object} responsiveConfig
         * This object consists of keys that represent the conditions on which configs
         * will be applied. For example:
         *
         *      responsiveConfig: {
         *          landscape: {
         *              region: 'west'
         *          },
         *          portrait: {
         *              region: 'north'
         *          }
         *      }
         *
         * In this case the keys ("landscape" and "portrait") are the criteria (or "rules")
         * and the object to their right contains the configs that will apply when that
         * rule is true.
         *
         * These rules can be any valid JavaScript expression but the following values
         * are considered in scope:
         *
         *  * `landscape` - True if the device orientation is landscape (always `true` on
         *   desktop devices).
         *  * `portrait` - True if the device orientation is portrait (always `false` on
         *   desktop devices).
         *  * `tall` - True if `width` < `height` regardless of device type.
         *  * `wide` - True if `width` > `height` regardless of device type.
         *  * `width` - The width of the viewport in pixels.
         *  * `height` - The height of the viewport in pixels.
         *  * `platform` - An object containing various booleans describing the platform
         *  (see `{@link Ext#platformTags Ext.platformTags}`). The properties of this
         *  object are also available implicitly (without "platform." prefix) but this
         *  sub-object may be useful to resolve ambiguity (for example, if one of the
         *  `{@link #responsiveFormulas}` overlaps and hides any of these properties).
         *  Previous to Ext JS 5.1, the `platformTags` were only available using this
         *  prefix.
         *
         * A more complex example:
         *
         *      responsiveConfig: {
         *          'desktop || width > 800': {
         *              region: 'west'
         *          },
         *
         *          '!(desktop || width > 800)': {
         *              region: 'north'
         *          }
         *      }
         *
         * **NOTE**: If multiple rules set a single config (like above), it is important
         * that the rules be mutually exclusive. That is, only one rule should set each
         * config. If multiple rules are actively setting a single config, the order of
         * these (and therefore the config's value) is unspecified.
         *
         * For a config to participate as a `responsiveConfig` it must have a "setter"
         * method. In the above example, a "setRegion" method must exist.
         *
         * @since 5.0.0
         */
            responsiveConfig: {
                $value: undefined,
                merge: function(newValue, oldValue, target, mixinClass) {
                    var marker, entry, ret, rule;
                    if (!newValue) {
                        return oldValue;
                    }
                    ret = oldValue ? Ext.Object.chain(oldValue) : {};
                    marker = Responsive.context;
                    for (rule in newValue) {
                        if (!mixinClass || !(rule in ret)) {
                            entry = newValue[rule];
                            // If we are called to merge A to B and then B to C we must not
                            // keep pushing the fn/config wrapper around things.
                            if (entry.fn !== marker) {
                                entry = {
                                    fn: marker,
                                    // created on first evaluation of this rule
                                    config: entry
                                };
                            }
                            ret[rule] = entry;
                        }
                    }
                    return ret;
                }
            },
            /**
         * @cfg {Object} responsiveFormulas
         * It is common when using `responsiveConfig` to have recurring expressions that
         * make for complex configurations. Using `responsiveFormulas` allows you to cut
         * down on this repetition by adding new properties to the "scope" for the rules
         * in a `responsiveConfig`.
         *
         * For example:
         *
         *      Ext.define('MyApp.view.main.Main', {
         *          extend: 'Ext.container.Container',
         *
         *          mixins: [
         *              'Ext.mixin.Responsive'
         *          ],
         *
         *          responsiveFormulas: {
         *              small: 'width < 600',
         *
         *              medium: 'width >= 600 && width < 800',
         *
         *              large: 'width >= 800',
         *
         *              tuesday: function(context) {
         *                  return (new Date()).getDay() === 2;
         *              }
         *          }
         *      });
         *
         * With the above declaration, any `responsiveConfig` can now use these values
         * like so:
         *
         *      responsiveConfig: {
         *          small: {
         *              hidden: true
         *          },
         *          'medium && !tuesday': {
         *              hidden: false,
         *              region: 'north'
         *          },
         *          large: {
         *              hidden: false,
         *              region: 'west'
         *          }
         *      }
         *
         * @since 5.0.1
         */
            responsiveFormulas: {
                $value: null,
                merge: function(newValue, oldValue, target, mixinClass) {
                    return this.mergeNew(newValue, oldValue, target, mixinClass);
                }
            }
        },
        /**
     * This method removes this instance from the Responsive collection.
     */
        destroy: function() {
            Responsive.unregister(this);
        },
        // No callParent() here, it's a mixin
        privates: {
            statics: {
                /**
             * @property {Boolean} active
             * @static
             * @private
             */
                active: false,
                /**
             * @property {Object} all
             * The collection of all `Responsive` instances. These are the instances that
             * will be notified when dynamic conditions change.
             * @static
             * @private
             */
                all: {},
                _configNames: [
                    'responsiveConfig',
                    'responsiveFormulas'
                ],
                /**
             * @property {Object} context
             * This object holds the various context values passed to the rule evaluation
             * functions.
             * @static
             * @private
             */
                context: Ext.Object.chain(Ext.platformTags),
                /**
             * @property {Number} count
             * The number of instances in the `all` collection.
             * @static
             * @private
             */
                count: 0,
                /**
             * @property {Number} nextId
             * The seed value used to assign `Responsive` instances a unique id for keying
             * in the `all` collection.
             * @static
             * @private
             */
                nextId: 0,
                /**
             * Activates event listeners for all `Responsive` instances. This method is
             * called when the first instance is registered.
             * @private
             */
                activate: function() {
                    Responsive.active = true;
                    Responsive.updateContext();
                    Ext.on('resize', Responsive.onResize, Responsive);
                },
                /**
             * Deactivates event listeners. This method is called when the last instance
             * is destroyed.
             * @private
             */
                deactivate: function() {
                    Responsive.active = false;
                    Ext.un('resize', Responsive.onResize, Responsive);
                },
                /**
             * Updates all registered the `Responsive` instances (found in the `all`
             * collection).
             * @private
             */
                notify: function() {
                    var all = Responsive.all,
                        context = Responsive.context,
                        timer = Responsive.timer,
                        id;
                    if (timer) {
                        Responsive.timer = Ext.unasap(timer);
                    }
                    Responsive.updateContext();
                    Ext.suspendLayouts();
                    Ext.fireEvent('beforeresponsiveupdate', context);
                    for (id in all) {
                        all[id].setupResponsiveContext();
                    }
                    Ext.fireEvent('beginresponsiveupdate', context);
                    for (id in all) {
                        all[id].updateResponsiveState();
                    }
                    Ext.fireEvent('responsiveupdate', context);
                    Ext.resumeLayouts(true);
                },
                /**
             * Handler of the window resize event. Schedules a timer so that we eventually
             * call `notify`.
             * @private
             */
                onResize: function() {
                    if (!Responsive.timer) {
                        Responsive.timer = Ext.asap(Responsive.onTimer);
                    }
                },
                /**
             * This method is the timer handler. When called this removes the timer and
             * calls `notify`.
             * @private
             */
                onTimer: function() {
                    Responsive.timer = null;
                    Responsive.notify();
                },
                register: function(responder) {
                    var id = responder.$responsiveId;
                    if (!id) {
                        responder.$responsiveId = id = ++Responsive.nextId;
                        Responsive.all[id] = responder;
                        if (++Responsive.count === 1) {
                            Responsive.activate();
                        }
                    }
                },
                unregister: function(responder) {
                    var id = responder.$responsiveId;
                    if (id in Responsive.all) {
                        responder.$responsiveId = null;
                        delete Responsive.all[id];
                        if (--Responsive.count === 0) {
                            Responsive.deactivate();
                        }
                    }
                },
                /**
             * Updates the `context` object base on the current environment.
             * @private
             */
                updateContext: function() {
                    var El = Ext.Element,
                        width = El.getViewportWidth(),
                        height = El.getViewportHeight(),
                        context = Responsive.context;
                    context.width = width;
                    context.height = height;
                    context.tall = width < height;
                    context.wide = !context.tall;
                    context.landscape = context.portrait = false;
                    if (!context.platform) {
                        context.platform = Ext.platformTags;
                    }
                    context[Ext.dom.Element.getOrientation()] = true;
                }
            },
            // private static
            //--------------------------------------------------------------------------
            /**
         * This class system hook method is called at the tail end of the mixin process.
         * We need to see if the `targetClass` has already got a `responsiveConfig` and
         * if so, we must add its value to the real config.
         * @param {Ext.Class} targetClass
         * @private
         */
            afterClassMixedIn: function(targetClass) {
                var proto = targetClass.prototype,
                    responsiveConfig = proto.responsiveConfig,
                    responsiveFormulas = proto.responsiveFormulas,
                    config;
                if (responsiveConfig || responsiveFormulas) {
                    config = {};
                    if (responsiveConfig) {
                        delete proto.responsiveConfig;
                        config.responsiveConfig = responsiveConfig;
                    }
                    if (responsiveFormulas) {
                        delete proto.responsiveFormulas;
                        config.responsiveFormulas = responsiveFormulas;
                    }
                    targetClass.getConfigurator().add(config);
                }
                targetClass.addConfigTransform('transformResponsiveConfig', 10);
            },
            // The reason this method exists is so to convince the config system to put the
            // "responsiveConfig" and "responsiveFormulas" in the initList. This needs to be
            // done so that the initGetter is setup prior to calling transformResponsiveConfig
            // when we need to call the getters.
            applyResponsiveConfig: function(rules) {
                var rule;
                for (rule in rules) {
                    rules[rule].fn = Ext.createRuleFn(rule);
                }
                return rules;
            },
            applyResponsiveFormulas: function(formulas) {
                var fn, name, ret;
                if (formulas) {
                    ret = {};
                    for (name in formulas) {
                        if (Ext.isString(fn = formulas[name])) {
                            fn = Ext.createRuleFn(fn);
                        }
                        ret[name] = fn;
                    }
                }
                return ret;
            },
            /**
         * Evaluates and returns the configs based on the `responsiveConfig`. This
         * method relies on the state being captured by the `updateContext` method.
         * @private
         */
            getResponsiveState: function() {
                var context = Responsive.context,
                    rules = this.getResponsiveConfig(),
                    entry, ret, rule;
                if (rules) {
                    ret = {};
                    for (rule in rules) {
                        entry = rules[rule];
                        if (entry.fn.call(this, context)) {
                            Ext.merge(ret, entry.config);
                        }
                    }
                }
                return ret;
            },
            setupResponsiveContext: function() {
                var formulas = this.getResponsiveFormulas(),
                    context = Responsive.context,
                    name;
                if (formulas) {
                    for (name in formulas) {
                        context[name] = formulas[name].call(this, context);
                    }
                }
            },
            /**
         * This config system hook method is called just prior to processing the specified
         * "instanceConfig". This hook returns the instanceConfig that will actually be
         * processed by the config system.
         * @param {Object} instanceConfig The user-supplied instance config object.
         * @param {Ext.Configurator} configurator
         * @private
         */
            transformResponsiveConfig: function(instanceConfig, configurator) {
                var me = this,
                    ret = instanceConfig,
                    state;
                // Since we are called immediately prior to the Configurator looking at the
                // instanceConfig, that incoming value has not yet been merged on to
                // "this.config". We need to call getResponsiveConfig and getResponsiveFormulas
                // and still get all that merged goodness, so we have to do the merge here.
                // TODO make sure tests cover these on class body
                // TODO make sure tests cover that we only register() if needed
                if (configurator.hoistConfigs(me, instanceConfig, Responsive._configNames)) {
                    Responsive.register(me);
                    // For updates this is done in bulk prior to updating all of the responsive
                    // objects, but for instantiation, we have to do this for ourselves now.
                    me.setupResponsiveContext();
                    // Now we can merge the current responsive state with the incoming config.
                    // The responsiveConfig takes priority.
                    state = me.getResponsiveState();
                    if (state) {
                        ret = Ext.merge({}, instanceConfig, state);
                        // We don't want these to remain since we've already handled them.
                        delete ret.responsiveConfig;
                        delete ret.responsiveFormulas;
                    }
                }
                return ret;
            },
            /**
         * Evaluates and applies the `responsiveConfig` to this instance. This is called
         * by `notify` automatically.
         * @private
         */
            updateResponsiveState: function() {
                var config = this.getResponsiveState();
                this.setConfig(config);
            }
        }
    };
});
// private

/**
 * The counterpart to `Ext.Responsive` for `Ext.Widget`. This override is required by
 * `Ext.Responsive` but will only be included if `Ext.Widget` is also used.
 *
 * @since 6.7.0
 * @private
 */
Ext.define('Ext.ResponsiveWidget', {
    override: 'Ext.Widget',
    mixins: [
        'Ext.mixin.Responsive'
    ]
});

/**
 * @private
 */
Ext.define('Ext.ProgressBase', {
    mixinId: 'progressbase',
    config: {
        /**
         * @cfg {Number} [value=0]
         * A floating point value between 0 and 1 (e.g., .5)
         */
        value: 0,
        /**
         * @cfg {String/Ext.XTemplate} [textTpl]
         * A template used to create this ProgressBar's background text given two values:
         *
         * - `value` - The raw progress value between 0 and 1
         * - `percent` - The value as a percentage between 0 and 100
         */
        textTpl: null
    },
    applyTextTpl: function(textTpl) {
        if (!textTpl.isTemplate) {
            textTpl = new Ext.XTemplate(textTpl);
        }
        return textTpl;
    },
    applyValue: function(value) {
        return value || 0;
    }
});

/**
 * A simple progress bar widget.
 *
 * You are responsible for showing, updating (via {@link #setValue}) and clearing the
 * progress bar as needed from your own code. This method is most appropriate when you
 * want to show progress throughout an operation that has predictable points of interest
 * at which you can update the control.
 */
Ext.define('Ext.Progress', {
    extend: 'Ext.Gadget',
    xtype: [
        'progress',
        'progressbarwidget'
    ],
    alternateClassName: 'Ext.ProgressBarWidget',
    mixins: [
        'Ext.ProgressBase'
    ],
    config: {
        /**
         * @cfg {String} [text]
         * The background text
         */
        text: null,
        /**
         * @cfg {Boolean} [animate=false]
         * Specify as `true` to have this progress bar animate to new extent when updated.
         */
        animate: false
    },
    cachedConfig: {
        textCls: Ext.baseCSSPrefix + 'progress-text',
        cls: null
    },
    baseCls: Ext.baseCSSPrefix + 'progress',
    template: [
        {
            reference: 'backgroundEl'
        },
        {
            reference: 'barEl',
            cls: Ext.baseCSSPrefix + 'progress-bar',
            children: [
                {
                    reference: 'textEl'
                }
            ]
        }
    ],
    defaultBindProperty: 'value',
    updateCls: function(cls, oldCls) {
        var el = this.element;
        if (oldCls) {
            el.removeCls(oldCls);
        }
        if (cls) {
            el.addCls(cls);
        }
    },
    updateUi: function(ui, oldUi) {
        var element = this.element,
            barEl = this.barEl,
            baseCls = this.baseCls + '-';
        this.callParent([
            ui,
            oldUi
        ]);
        if (oldUi) {
            element.removeCls(baseCls + oldUi);
            barEl.removeCls(baseCls + 'bar-' + oldUi);
        }
        element.addCls(baseCls + ui);
        barEl.addCls(baseCls + 'bar-' + ui);
    },
    updateTextCls: function(textCls) {
        this.backgroundEl.addCls(textCls + ' ' + textCls + '-back');
        this.textEl.addCls(textCls);
    },
    updateValue: function(value, oldValue) {
        var me = this,
            textTpl = me.getTextTpl();
        if (textTpl) {
            me.setText(textTpl.apply({
                value: value,
                percent: Math.round(value * 100)
            }));
        }
        if (!me.isConfiguring && me.getAnimate()) {
            me.stopBarAnimation();
            me.startBarAnimation(Ext.apply({
                from: {
                    width: (oldValue * 100) + '%'
                },
                to: {
                    width: (value * 100) + '%'
                }
            }, me.animate));
        } else {
            me.barEl.setStyle('width', (value * 100) + '%');
        }
    },
    updateText: function(text) {
        this.backgroundEl.setHtml(text);
        this.textEl.setHtml(text);
    },
    doDestroy: function() {
        this.stopBarAnimation();
        this.callParent();
    },
    privates: {
        startBarAnimation: Ext.privateFn,
        stopBarAnimation: Ext.privateFn
    }
});

/**
 * @class Ext.Progress
 *
 *     @example
 *     Ext.create({
 *         xtype: 'grid',
 *         title: 'Simpsons',
 *         store: {
 *             data: [
 *                 { name: 'Lisa', progress: .159 },
 *                 { name: 'Bart', progress: .216 },
 *                 { name: 'Homer', progress: .55 },
 *                 { name: 'Maggie', progress: .167 },
 *                 { name: 'Marge', progress: .145 }
 *             ]
 *         },
 *         columns: [
 *             { text: 'Name',  dataIndex: 'name' },
 *             {
 *                 text: 'Progress',
 *                 xtype: 'widgetcolumn',
 *                 width: 120,
 *                 dataIndex: 'progress',
 *                 widget: {
 *                     xtype: 'progress'
 *                 }
 *             }
 *         ],
 *         height: 200,
 *         width: 400,
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.overrides.Progress', {
    override: 'Ext.Progress',
    config: {
        ui: 'default'
    },
    updateWidth: function(width, oldWidth) {
        var me = this;
        me.callParent([
            width,
            oldWidth
        ]);
        width -= me.element.getBorderWidth('lr');
        me.backgroundEl.setWidth(width);
        me.textEl.setWidth(width);
    },
    privates: {
        startBarAnimation: function(o) {
            this.barEl.animate(o);
        },
        stopBarAnimation: function() {
            this.barEl.stopAnimation();
        }
    }
});

/**
 * @class Ext.util.Format
 *  
 * This class is a centralized place for formatting functions. It includes
 * functions to format various different types of data, such as text, dates and numeric values.
 *  
 * ## Localization
 *
 * This class contains several options for localization. These can be set once the library
 * has loaded, all calls to the functions from that point will use the locale settings
 * that were specified.
 *
 * Options include:
 *
 * - thousandSeparator
 * - decimalSeparator
 * - currencyPrecision
 * - currencySign
 * - currencyAtEnd
 *
 * This class also uses the default date format defined here: {@link Ext.Date#defaultFormat}.
 *
 * ## Using with renderers
 *
 * There are two helper functions that return a new function that can be used in conjunction with
 * grid renderers:
 *  
 *     columns: [{
 *         dataIndex: 'date',
 *         renderer: Ext.util.Format.dateRenderer('Y-m-d')
 *     }, {
 *         dataIndex: 'time',
 *         renderer: Ext.util.Format.numberRenderer('0.000')
 *     }]
 *  
 * Functions that only take a single argument can also be passed directly:
 *
 *     columns: [{
 *         dataIndex: 'cost',
 *         renderer: Ext.util.Format.usMoney
 *     }, {
 *         dataIndex: 'productCode',
 *         renderer: Ext.util.Format.uppercase
 *     }]
 *  
 * ## Using with XTemplates
 *
 * XTemplates can also directly use Ext.util.Format functions:
 *  
 *     new Ext.XTemplate([
 *         'Date: {startDate:date("Y-m-d")}',
 *         'Cost: {cost:usMoney}'
 *     ]);
 *
 * @singleton
 */
Ext.define('Ext.util.Format', function() {
    var me;
    // holds our singleton instance
    return {
        requires: [
            'Ext.Error',
            'Ext.Number',
            'Ext.String',
            'Ext.Date'
        ],
        singleton: true,
        /**
         * The global default date format.
         */
        defaultDateFormat: 'm/d/Y',
        /**
         * @property {String} thousandSeparator
         * The character that the {@link #number} function uses as a thousand separator.
         *
         * This may be overridden in a locale file.
         * @locale
         */
        thousandSeparator: ',',
        /**
         * @property {String} decimalSeparator
         * The character that the {@link #number} function uses as a decimal point.
         *
         * This may be overridden in a locale file.
         * @locale
         */
        decimalSeparator: '.',
        /**
         * @property {Number} currencyPrecision
         * The number of decimal places that the {@link #currency} function displays.
         *
         * This may be overridden in a locale file.
         * @locale
         */
        currencyPrecision: 2,
        /**
         * @property {String} currencySign
         * The currency sign that the {@link #currency} function displays.
         *
         * This may be overridden in a locale file.
         * @locale
         */
        currencySign: '$',
        /**
         * @property {String} currencySpacer
         * True to add a space between the currency and the value
         *
         * This may be overridden in a locale file.
         * @since 6.2.0
         * @locale
         */
        currencySpacer: '',
        /**
         * @property {String} percentSign
         * The percent sign that the {@link #percent} function displays.
         *
         * This may be overridden in a locale file.
         * @locale
         */
        percentSign: '%',
        /**
         * @property {Boolean} currencyAtEnd
         * This may be set to <code>true</code> to make the {@link #currency} function
         * append the currency sign to the formatted value.
         *
         * This may be overridden in a locale file.
         * @locale
         */
        currencyAtEnd: false,
        stripTagsRe: /<\/?[^>]+>/gi,
        stripScriptsRe: /(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)/ig,
        nl2brRe: /\r?\n/g,
        hashRe: /#+$/,
        allHashes: /^#+$/,
        // Match a format string characters to be able to detect remaining "literal" characters
        formatPattern: /[\d,.#]+/,
        // A RegExp to remove from a number format string, all characters except digits and '.'
        formatCleanRe: /[^\d.#]/g,
        // A RegExp to remove from a number format string, all characters except digits
        // and the local decimal separator. Created on first use. The local decimal separator
        // character must be initialized for this to be created.
        I18NFormatCleanRe: null,
        // Cache ofg number formatting functions keyed by format string
        formatFns: {},
        constructor: function() {
            me = this;
        },
        // we are a singleton, so cache our this pointer in scope
        /**
         * Returns a non-breaking space ("NBSP") for any "blank" value.
         * @param {Mixed} value
         * @param {Boolean} [strict=true] Pass `false` to convert all falsey values to an
         * NBSP. By default, only '', `null` and `undefined` will be converted.
         * @return {Mixed}
         * @since 6.2.0
         */
        nbsp: function(value, strict) {
            strict = strict !== false;
            if (strict ? value === '' || value == null : !value) {
                value = '\xa0';
            }
            return value;
        },
        /**
         * Checks a reference and converts it to empty string if it is undefined.
         * @param {Object} value Reference to check
         * @return {Object} Empty string if converted, otherwise the original value
         */
        undef: function(value) {
            return value !== undefined ? value : "";
        },
        /**
         * Checks a reference and converts it to the default value if it's empty.
         * @param {Object} value Reference to check
         * @param {String} [defaultValue=""] The value to insert of it's undefined.
         * @return {String}
         */
        defaultValue: function(value, defaultValue) {
            return value !== undefined && value !== '' ? value : defaultValue;
        },
        /**
         * Returns a substring from within an original string.
         * @param {String} value The original text
         * @param {Number} start The start index of the substring
         * @param {Number} length The length of the substring
         * @return {String} The substring
         * @method
         */
        substr: 'ab'.substr(-1) !== 'b' ? function(value, start, length) {
            var str = String(value);
            return (start < 0) ? str.substr(Math.max(str.length + start, 0), length) : str.substr(start, length);
        } : function(value, start, length) {
            return String(value).substr(start, length);
        },
        /**
         * Converts a string to all lower case letters.
         * @param {String} value The text to convert
         * @return {String} The converted text
         */
        lowercase: function(value) {
            return String(value).toLowerCase();
        },
        /**
         * Converts a string to all upper case letters.
         * @param {String} value The text to convert
         * @return {String} The converted text
         */
        uppercase: function(value) {
            return String(value).toUpperCase();
        },
        /**
         * Format a number as US currency.
         * @param {Number/String} value The numeric value to format
         * @return {String} The formatted currency string
         */
        usMoney: function(value) {
            return me.currency(value, '$', 2);
        },
        /**
         * Format a number as a currency.
         * @param {Number/String} value The numeric value to format
         * @param {String} [currencySign] The currency sign to use (defaults to
         * {@link #currencySign})
         * @param {Number} [decimals] The number of decimals to use for the currency
         * (defaults to {@link #currencyPrecision})
         * @param {Boolean} [end] True if the currency sign should be at the end of the string
         * (defaults to {@link #currencyAtEnd})
         * @param {String} [currencySpacer] True to add a space between the currency and value
         * @return {String} The formatted currency string
         */
        currency: function(value, currencySign, decimals, end, currencySpacer) {
            var negativeSign = '',
                format = ",0",
                i = 0;
            value = value - 0;
            if (value < 0) {
                value = -value;
                negativeSign = '-';
            }
            decimals = Ext.isDefined(decimals) ? decimals : me.currencyPrecision;
            format += (decimals > 0 ? '.' : '');
            for (; i < decimals; i++) {
                format += '0';
            }
            value = me.number(value, format);
            if (currencySpacer == null) {
                currencySpacer = me.currencySpacer;
            }
            if ((end || me.currencyAtEnd) === true) {
                return Ext.String.format("{0}{1}{2}{3}", negativeSign, value, currencySpacer, currencySign || me.currencySign);
            } else {
                return Ext.String.format("{0}{1}{2}{3}", negativeSign, currencySign || me.currencySign, currencySpacer, value);
            }
        },
        /**
         * Formats the passed date using the specified format pattern.
         * Note that this uses the native Javascript Date.parse() method and is therefore subject
         * to its idiosyncrasies. Most formats assume the local timezone unless specified.
         * One notable exception is 'YYYY-MM-DD' (note the dashes) which is typically interpreted
         * in UTC and can cause date shifting.
         * 
         * @param {String/Date} value The value to format. Strings must conform to the format
         * expected by the JavaScript Date object's
         * [parse() method](http://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/parse).
         * @param {String} [format] Any valid date format string. Defaults to
         * {@link Ext.Date#defaultFormat}.
         * @return {String} The formatted date string.
         */
        date: function(value, format) {
            if (!value) {
                return "";
            }
            if (!Ext.isDate(value)) {
                value = new Date(Date.parse(value));
            }
            return Ext.Date.dateFormat(value, format || Ext.Date.defaultFormat);
        },
        /**
         * Returns a date rendering function that can be reused to apply a date format multiple
         * times efficiently.
         * @param {String} format Any valid date format string. Defaults to
         * {@link Ext.Date#defaultFormat}.
         * @return {Function} The date formatting function
         */
        dateRenderer: function(format) {
            return function(v) {
                return me.date(v, format);
            };
        },
        /**
         * Returns the given number as a base 16 string at least `digits` in length. If
         * the number is fewer digits, 0's are prepended as necessary. If `digits` is
         * negative, the absolute value is the *exact* number of digits to return. In this
         * case, if then number has more digits, only the least significant digits are
         * returned.
         *
         *      expect(Ext.util.Format.hex(0x12e4, 2)).toBe('12e4');
         *      expect(Ext.util.Format.hex(0x12e4, -2)).toBe('e4');
         *      expect(Ext.util.Format.hex(0x0e, 2)).toBe('0e');
         *
         * @param {Number} value The number to format in hex.
         * @param {Number} digits
         * @return {string}
         */
        hex: function(value, digits) {
            var s = parseInt(value || 0, 10).toString(16);
            if (digits) {
                if (digits < 0) {
                    digits = -digits;
                    if (s.length > digits) {
                        s = s.substring(s.length - digits);
                    }
                }
                while (s.length < digits) {
                    s = '0' + s;
                }
            }
            return s;
        },
        /**
         * Returns this result:
         *
         *      value || orValue
         *
         * The usefulness of this formatter method is in templates. For example:
         *
         *      {foo:or("bar")}
         *
         * @param {Boolean} value The "if" value.
         * @param {Mixed} orValue
         */
        or: function(value, orValue) {
            return value || orValue;
        },
        /**
         * If `value` is a number, returns the argument from that index. For example
         *
         *      var s = Ext.util.Format.pick(2, 'zero', 'one', 'two');
         *      // s === 'two'
         *
         * Otherwise, `value` is treated in a truthy/falsey manner like so:
         *
         *      var s = Ext.util.Format.pick(null, 'first', 'second');
         *      // s === 'first'
         *
         *      s = Ext.util.Format.pick({}, 'first', 'second');
         *      // s === 'second'
         *
         * The usefulness of this formatter method is in templates. For example:
         *
         *      {foo:pick("F","T")}
         *
         *      {bar:pick("first","second","third")}
         *
         * @param {Boolean} value The "if" value.
         * @param {Mixed} firstValue
         * @param {Mixed} secondValue
         */
        pick: function(value, firstValue, secondValue) {
            var ret;
            if (Ext.isNumber(value)) {
                ret = arguments[value + 1];
                if (ret) {
                    return ret;
                }
            }
            return value ? secondValue : firstValue;
        },
        /**
         * Compares `value` against `threshold` and returns:
         *
         * - if `value` < `threshold` then it returns `below`
         * - if `value` > `threshold` then it returns `above`
         * - if `value` = `threshold` then it returns `equal` or `above` when `equal` is missing
         *
         * The usefulness of this formatter method is in templates. For example:
         *
         *      {foo:lessThanElse(0, 'negative', 'positive')}
         *
         *      {bar:lessThanElse(200, 'lessThan200', 'greaterThan200', 'equalTo200')}
         *
         * @param {Number} value Value that will be checked
         * @param {Number} threshold Value to compare against
         * @param {Mixed} below Value to return when `value` < `threshold`
         * @param {Mixed} above Value to return when `value` > `threshold`.
         * If `value` = `threshold` and `equal` is missing then `above` is returned.
         * @param {Mixed} equal Value to return when `value` = `threshold`
         * @return {Mixed}
         */
        lessThanElse: function(value, threshold, below, above, equal) {
            var v = Ext.Number.from(value, 0),
                t = Ext.Number.from(threshold, 0),
                missing = !Ext.isDefined(equal);
            return v < t ? below : (v > t ? above : (missing ? above : equal));
        },
        /**
         * Checks if `value` is a positive or negative number and returns the proper param.
         *
         * The usefulness of this formatter method is in templates. For example:
         *
         *      {foo:sign("clsNegative","clsPositive")}
         *
         * @param {Number} value
         * @param {Mixed} negative
         * @param {Mixed} positive
         * @param {Mixed} zero
         * @return {Mixed}
         */
        sign: function(value, negative, positive, zero) {
            if (zero === undefined) {
                zero = positive;
            }
            return me.lessThanElse(value, 0, negative, positive, zero);
        },
        /**
         * Strips all HTML tags.
         * @param {Object} value The text from which to strip tags
         * @return {String} The stripped text
         */
        stripTags: function(value) {
            return !value ? value : String(value).replace(me.stripTagsRe, "");
        },
        /**
         * Strips all script tags.
         * @param {Object} value The text from which to strip script tags
         * @return {String} The stripped text
         */
        stripScripts: function(value) {
            return !value ? value : String(value).replace(me.stripScriptsRe, "");
        },
        /**
         * @method
         * Simple format for a file size (xxx bytes, xxx KB, xxx MB).
         * @param {Number/String} size The numeric value to format
         * @return {String} The formatted file size
         */
        fileSize: (function() {
            var byteLimit = 1024,
                kbLimit = 1048576,
                mbLimit = 1073741824;
            return function(size) {
                var out;
                if (size < byteLimit) {
                    if (size === 1) {
                        out = '1 byte';
                    } else {
                        out = size + ' bytes';
                    }
                } else if (size < kbLimit) {
                    out = (Math.round(((size * 10) / byteLimit)) / 10) + ' KB';
                } else if (size < mbLimit) {
                    out = (Math.round(((size * 10) / kbLimit)) / 10) + ' MB';
                } else {
                    out = (Math.round(((size * 10) / mbLimit)) / 10) + ' GB';
                }
                return out;
            };
        })(),
        /**
         * It does simple math for use in a template, for example:
         *
         *     var tpl = new Ext.Template('{value} * 10 = {value:math("* 10")}');
         *
         * @return {Function} A function that operates on the passed value.
         * @method
         */
        math: (function() {
            var fns = {};
            return function(v, a) {
                if (!fns[a]) {
                    fns[a] = Ext.functionFactory('v', 'return v ' + a + ';');
                }
                return fns[a](v);
            };
        }()),
        /**
         * Rounds the passed number to the required decimal precision.
         * @param {Number/String} value The numeric value to round.
         * @param {Number} [precision] The number of decimal places to which to round the
         * first parameter's value. If `undefined` the `value` is passed to `Math.round`
         * otherwise the value is returned unmodified.
         * @return {Number} The rounded value.
         */
        round: function(value, precision) {
            var result = Number(value);
            if (typeof precision === 'number') {
                precision = Math.pow(10, precision);
                result = Math.round(value * precision) / precision;
            } else if (precision === undefined) {
                result = Math.round(result);
            }
            return result;
        },
        /**
         * Formats the passed number according to the passed format string.
         *
         * The number of digits after the decimal separator character specifies the number of
         * decimal places in the resulting string. The *local-specific* decimal character is
         * used in the result.
         *
         * The *presence* of a thousand separator character in the format string specifies that
         * the *locale-specific* thousand separator (if any) is inserted separating thousand groups.
         *
         * By default, "," is expected as the thousand separator, and "." is expected as the decimal
         * separator.
         *
         * Locale-specific characters are always used in the formatted output when inserting
         * thousand and decimal separators. These can be set using the {@link #thousandSeparator}
         * and {@link #decimalSeparator} options.
         *
         * The format string must specify separator characters according to US/UK conventions
         * ("," as the thousand separator, and "." as the decimal separator)
         *
         * To allow specification of format strings according to local conventions for separator
         * characters, add the string `/i` to the end of the format string. This format depends
         * on the {@link #thousandSeparator} and {@link #decimalSeparator} options. For example,
         * if using European style separators, then the format string can be specified
         * as `'0.000,00'`. This would be equivalent to using `'0,000.00'` when using US style
         * formatting.
         *
         * Examples (123456.789):
         * 
         * - `0` - (123457) show only digits, no precision
         * - `0.00` - (123456.79) show only digits, 2 precision
         * - `0.0000` - (123456.7890) show only digits, 4 precision
         * - `0,000` - (123,457) show comma and digits, no precision
         * - `0,000.00` - (123,456.79) show comma and digits, 2 precision
         * - `0,0.00` - (123,456.79) shortcut method, show comma and digits, 2 precision
         * - `0.####` - (123,456.789) Allow maximum 4 decimal places, but do not right pad
         * with zeroes
         * - `0.00##` - (123456.789) Show at least 2 decimal places, maximum 4, but do not
         * right pad with zeroes
         *
         * @param {Number} v The number to format.
         * @param {String} formatString The way you would like to format this text.
         * @return {String} The formatted number.
         */
        number: function(v, formatString) {
            var formatFn;
            if (!formatString) {
                return v;
            }
            if (isNaN(v)) {
                return '';
            }
            formatFn = me.formatFns[formatString];
            // Generate formatting function to be cached and reused keyed by the format string.
            // This results in a 100% performance increase over analyzing the format string
            // each invocation.
            if (!formatFn) {
                // eslint-disable-next-line vars-on-top
                var originalFormatString = formatString,
                    comma = me.thousandSeparator,
                    decimalSeparator = me.decimalSeparator,
                    precision = 0,
                    trimPart = '',
                    hasComma, splitFormat, extraChars, trimTrailingZeroes, code, len;
                // The "/i" suffix allows caller to use a locale-specific formatting string.
                // Clean the format string by removing all but numerals and the decimal separator.
                // Then split the format string into pre and post decimal segments according to
                // *what* the decimal separator is. If they are specifying "/i", they are using
                // the local convention in the format string.
                if (formatString.substr(formatString.length - 2) === '/i') {
                    // In a vast majority of cases, the separator will never change
                    // over the lifetime of the application.
                    // So we'll only regenerate this if we really need to
                    if (!me.I18NFormatCleanRe || me.lastDecimalSeparator !== decimalSeparator) {
                        me.I18NFormatCleanRe = new RegExp('[^\\d\\' + decimalSeparator + '#]', 'g');
                        me.lastDecimalSeparator = decimalSeparator;
                    }
                    formatString = formatString.substr(0, formatString.length - 2);
                    hasComma = formatString.indexOf(comma) !== -1;
                    splitFormat = formatString.replace(me.I18NFormatCleanRe, '').split(decimalSeparator);
                } else {
                    hasComma = formatString.indexOf(',') !== -1;
                    splitFormat = formatString.replace(me.formatCleanRe, '').split('.');
                }
                extraChars = formatString.replace(me.formatPattern, '');
                if (splitFormat.length > 2) {
                    Ext.raise({
                        sourceClass: "Ext.util.Format",
                        sourceMethod: "number",
                        value: v,
                        formatString: formatString,
                        msg: "Invalid number format, should have no more than 1 decimal"
                    });
                } else if (splitFormat.length === 2) {
                    precision = splitFormat[1].length;
                    // Formatting ending in .##### means maximum 5 trailing significant digits
                    trimTrailingZeroes = splitFormat[1].match(me.hashRe);
                    if (trimTrailingZeroes) {
                        len = trimTrailingZeroes[0].length;
                        // Need to escape, since this will be '.' by default
                        // eslint-disable-next-line max-len
                        trimPart = 'trailingZeroes=new RegExp(Ext.String.escapeRegex(utilFormat.decimalSeparator) + "*0{0,' + len + '}$")';
                    }
                }
                // The function we create is called immediately and returns a closure
                // which has access to vars and some fixed values; RegExes and the format string.
                code = [
                    'var utilFormat=Ext.util.Format,extNumber=Ext.Number,neg,absVal,fnum,parts' + (hasComma ? ',thousandSeparator,thousands=[],j,n,i' : '') + (extraChars ? ',formatString="' + formatString + '",formatPattern=/[\\d,\\.#]+/' : '') + // eslint-disable-line max-len
                    ',trailingZeroes;' + 'return function(v){' + 'if(typeof v!=="number"&&isNaN(v=extNumber.from(v,NaN)))return"";' + 'neg=v<0;',
                    'absVal=Math.abs(v);',
                    'fnum=Ext.Number.toFixed(absVal, ' + precision + ');',
                    trimPart,
                    ';'
                ];
                if (hasComma) {
                    // If we have to insert commas...
                    // split the string up into whole and decimal parts if there are decimals
                    if (precision) {
                        code[code.length] = 'parts=fnum.split(".");';
                        code[code.length] = 'fnum=parts[0];';
                    }
                    code[code.length] = 'if(absVal>=1000) {';
                    code[code.length] = 'thousandSeparator=utilFormat.thousandSeparator;' + 'thousands.length=0;' + 'j=fnum.length;' + 'n=fnum.length%3||3;' + 'for(i=0;i<j;i+=n){' + 'if(i!==0){' + 'n=3;' + '}' + 'thousands[thousands.length]=fnum.substr(i,n);' + '}' + 'fnum=thousands.join(thousandSeparator);' + '}';
                    if (precision) {
                        code[code.length] = 'fnum += utilFormat.decimalSeparator+parts[1];';
                    }
                } else if (precision) {
                    // If they are using a weird decimal separator, split and concat using it
                    code[code.length] = 'if(utilFormat.decimalSeparator!=="."){' + 'parts=fnum.split(".");' + 'fnum=parts[0]+utilFormat.decimalSeparator+parts[1];' + '}';
                }
                /*
                 * Edge case. If we have a very small negative number it will get rounded to 0,
                 * however the initial check at the top will still report as negative. Replace
                 * everything but 1-9 and check if the string is empty to determine a 0 value.
                 */
                code[code.length] = 'if(neg&&fnum!=="' + (precision ? '0.' + Ext.String.repeat('0', precision) : '0') + '") { fnum="-"+fnum; }';
                if (trimTrailingZeroes) {
                    code[code.length] = 'fnum=fnum.replace(trailingZeroes,"");';
                }
                code[code.length] = 'return ';
                // If there were extra characters around the formatting string,
                // replace the format string part with the formatted number.
                if (extraChars) {
                    code[code.length] = 'formatString.replace(formatPattern, fnum);';
                } else {
                    code[code.length] = 'fnum;';
                }
                code[code.length] = '};';
                formatFn = me.formatFns[originalFormatString] = Ext.functionFactory('Ext', code.join(''))(Ext);
            }
            return formatFn(v);
        },
        /**
         * Returns a number rendering function that can be reused to apply a number format multiple
         * times efficiently.
         *
         * @param {String} format Any valid number format string for {@link #number}
         * @return {Function} The number formatting function
         */
        numberRenderer: function(format) {
            return function(v) {
                return me.number(v, format);
            };
        },
        /**
         * Formats the passed number as a percentage according to the passed format string.
         * The number should be between 0 and 1 to represent 0% to 100%.
         *
         * @param {Number} value The percentage to format.
         * @param {String} [formatString="0"] See {@link #number} for details.
         * @return {String} The formatted percentage.
         */
        percent: function(value, formatString) {
            return me.number(value * 100, formatString || '0') + me.percentSign;
        },
        repeat: function(value, text, sep) {
            return Ext.String.repeat(text, value, sep);
        },
        /**
         * Formats an object of name value properties as HTML element attribute values
         * suitable for using when creating textual markup.
         * @param {Object} attributes An object containing the HTML attributes as properties
         * e.g.: `{height:40, vAlign:'top'}`
         */
        attributes: function(attributes) {
            var result, name;
            if (typeof attributes === 'object') {
                result = [];
                for (name in attributes) {
                    if (attributes.hasOwnProperty(name)) {
                        result.push(name, '="', name === 'style' ? Ext.DomHelper.generateStyles(attributes[name], null, true) : Ext.htmlEncode(attributes[name]), '" ');
                    }
                }
                attributes = result.join('');
            }
            return attributes || '';
        },
        /**
         * Selectively return the plural form of a word based on a numeric value.
         * 
         * For example, the following template would result in "1 Comment".  If the 
         * value of `count` was 0 or greater than 1, the result would be "x Comments".
         * 
         *     var tpl = new Ext.XTemplate('{count:plural("Comment")}');
         *     
         *     tpl.apply({
         *         count: 1
         *     }); // returns "1 Comment"
         * 
         * Examples using the static `plural` method call:
         * 
         *     Ext.util.Format.plural(2, 'Comment');
         *     // returns "2 Comments"
         * 
         *     Ext.util.Format.plural(4, 'person', 'people');
         *     // returns "4 people"
         *
         * @param {Number} value The value to compare against
         * @param {String} singular The singular form of the word
         * @param {String} [plural] The plural form of the word (defaults to the 
         * singular form with an "s" appended)
         * @return {String} output The pluralized output of the passed singular form
         */
        plural: function(value, singular, plural) {
            return value + ' ' + (value === 1 ? singular : (plural ? plural : singular + 's'));
        },
        /**
         * Converts newline characters to the HTML tag `<br/>`
         *
         * @param {String} v The string value to format.
         * @return {String} The string with embedded `<br/>` tags in place of newlines.
         */
        nl2br: function(v) {
            return Ext.isEmpty(v) ? '' : v.replace(me.nl2brRe, '<br/>');
        },
        /**
         * @method capitalize
         * @inheritdoc Ext.String#method-capitalize
         * Alias for {@link Ext.String#capitalize}.
         */
        capitalize: Ext.String.capitalize,
        /**
         * @method uncapitalize
         * @inheritdoc Ext.String#method-uncapitalize
         * Alias for {@link Ext.String#uncapitalize}.
         */
        uncapitalize: Ext.String.uncapitalize,
        /**
         * @method ellipsis
         * @inheritdoc Ext.String#method-ellipsis
         * Alias for {@link Ext.String#ellipsis}.
         */
        ellipsis: Ext.String.ellipsis,
        /**
         * @method escape
         * @inheritdoc Ext.String#method-escape
         * Alias for {@link Ext.String#escape}.
         */
        escape: Ext.String.escape,
        /**
         * @method escapeRegex
         * @inheritdoc Ext.String#method-escapeRegex
         * Alias for {@link Ext.String#escapeRegex}.
         */
        escapeRegex: Ext.String.escapeRegex,
        /**
         * @method htmlDecode
         * @inheritdoc Ext.String#method-htmlDecode
         * Alias for {@link Ext.String#htmlDecode}.
         */
        htmlDecode: Ext.String.htmlDecode,
        /**
         * @method htmlEncode
         * @inheritdoc Ext.String#method-htmlEncode
         * Alias for {@link Ext.String#htmlEncode}.
         */
        htmlEncode: Ext.String.htmlEncode,
        /**
         * @method leftPad
         * @inheritdoc Ext.String#method-leftPad
         * Alias for {@link Ext.String#leftPad}.
         */
        leftPad: Ext.String.leftPad,
        /**
         * @method toggle
         * @inheritdoc Ext.String#method-toggle
         * Alias for {@link Ext.String#toggle}.
         */
        toggle: Ext.String.toggle,
        /**
         * @method trim
         * @inheritdoc Ext.String#method-trim
         * Alias for {@link Ext.String#trim}.
         */
        trim: Ext.String.trim,
        /**
         * Parses a number or string representing margin sizes into an object.
         * Supports CSS-style margin declarations (e.g. 10, "10", "10 10", "10 10 10" and
         * "10 10 10 10" are all valid options and would return the same result).
         *
         * @param {Number/String} box The encoded margins
         * @return {Object} An object with margin sizes for top, right, bottom and left
         */
        parseBox: function(box) {
            box = box || 0;
            if (typeof box === 'number') {
                return {
                    top: box,
                    right: box,
                    bottom: box,
                    left: box
                };
            }
            // eslint-disable-next-line vars-on-top
            var parts = box.split(' '),
                ln = parts.length;
            if (ln === 1) {
                parts[1] = parts[2] = parts[3] = parts[0];
            } else if (ln === 2) {
                parts[2] = parts[0];
                parts[3] = parts[1];
            } else if (ln === 3) {
                parts[3] = parts[1];
            }
            return {
                top: parseInt(parts[0], 10) || 0,
                right: parseInt(parts[1], 10) || 0,
                bottom: parseInt(parts[2], 10) || 0,
                left: parseInt(parts[3], 10) || 0
            };
        },
        /**
         * Resolves the specified resource `url` with an optional `prefix`. This resolution
         * is based on {@link Ext#resolveResource}. The prefix is intended to be used for
         * a package or resource pool identifier.
         *
         * @param {String} url The resource url to resolve
         * @param {String} [prefix] A prefix/identifier to include in the resolution.
         * @return {String}
         */
        resource: function(url, prefix) {
            prefix = prefix || '';
            return Ext.resolveResource(prefix + url);
        },
        /**
         * Formats the given value using `encodeURI`.
         * @param {String} value The value to encode.
         * @returns {string}
         * @since 6.2.0
         */
        uri: function(value) {
            return encodeURI(value);
        },
        /**
         * Formats the given value using `encodeURIComponent`.
         * @param {String} value The value to encode.
         * @returns {string}
         * @since 6.2.0
         */
        uriCmp: function(value) {
            return encodeURIComponent(value);
        },
        wordBreakRe: /[\W\s]+/,
        /**
         * Returns the word at the given `index`. Spaces and punctuation are considered
         * as word separators by default. For example:
         *
         *      console.log(Ext.util.Format.word('Hello, my name is Bob.', 2);
         *      // == 'name'
         *
         * @param {String} value The sentence to break into words.
         * @param {Number} index The 0-based word index.
         * @param {String/RegExp} [sep="[\W\s]+"] The pattern by which to separate words.
         * @return {String} The requested word or empty string.
         */
        word: function(value, index, sep) {
            var re = sep ? (typeof sep === 'string' ? new RegExp(sep) : sep) : me.wordBreakRe,
                parts = (value || '').split(re);
            return parts[index || 0] || '';
        }
    };
});

/**
 * Represents an HTML fragment template. Templates may be {@link #compile precompiled} for
 * greater performance.
 *
 * An instance of this class may be created by passing to the constructor either a single argument,
 * or multiple arguments:
 *
 * # Single argument: String/Array
 *
 * The single argument may be either a String or an Array:
 *
 * - String:
 *
 *       var t = new Ext.Template("<div>Hello {0}.</div>");
 *       t.append('some-element', ['foo']);
 *
 * - Array:
 *
 *   An Array will be combined with `join('')`.
 *
 *       var t = new Ext.Template([
 *           '<div name="{id}">',
 *               '<span class="{cls}">{name:trim} {value:ellipsis(10)}</span>',
 *           '</div>',
 *       ]);
 *       t.compile();
 *       t.append('some-element', {id: 'myid', cls: 'myclass', name: 'foo', value: 'bar'});
 *
 * # Multiple arguments: String, Object, Array, ...
 *
 * Multiple arguments will be combined with `join('')`.
 *
 *     var t = new Ext.Template(
 *         '<div name="{id}">',
 *             '<span class="{cls}">{name} {value}</span>',
 *         '</div>',
 *         // a configuration object:
 *         {
 *             compiled: true,      // {@link #compile} immediately
 *         }
 *     );
 *
 * # Notes
 *
 * - For a list of available format functions, see {@link Ext.util.Format}.
 * - `disableFormats` reduces `{@link #apply}` time when no formatting is required.
 */
Ext.define('Ext.Template', {
    // @define Ext.String.format
    // @define Ext.util.Format.format
    requires: [
        'Ext.util.Format'
    ],
    inheritableStatics: {
        /**
         * Creates a template from the passed element's value (_display:none_ textarea, preferred)
         * or innerHTML.
         * @param {String/HTMLElement} el A DOM element or its id
         * @param {Object} config (optional) Config object
         * @return {Ext.Template} The created template
         * @static
         * @inheritable
         */
        from: function(el, config) {
            el = Ext.getDom(el);
            return new this(el.value || el.innerHTML, config || '');
        }
    },
    // Chrome really likes "new Function" to realize the code block (as in it is
    // 2x-3x faster to call it than using eval), but Firefox chokes on it badly.
    // IE and Opera are also fine with the "new Function" technique.
    useEval: Ext.isGecko,
    /* End Definitions */
    /**
     * Creates new template.
     * 
     * @param {String...} html List of strings to be concatenated into template.
     * Alternatively an array of strings can be given, but then no config object may be passed.
     * @param {Object} [config] A config object to apply to this instance.
     */
    constructor: function(html) {
        var me = this,
            args = arguments,
            buffer = [],
            length = args.length,
            i, value;
        me.initialConfig = {};
        // Allow an array to be passed here so we can
        // pass an array of strings and an object
        // at the end
        if (length === 1 && Ext.isArray(html)) {
            args = html;
            length = args.length;
        }
        if (length > 1) {
            for (i = 0; i < length; i++) {
                value = args[i];
                if (typeof value === 'object') {
                    Ext.apply(me.initialConfig, value);
                    Ext.apply(me, value);
                } else {
                    buffer.push(value);
                }
            }
        } else {
            buffer.push(html);
        }
        me.html = buffer.join('');
    },
    /**
     * @property {Boolean} isTemplate
     * `true` in this class to identify an object as an instantiated Template, or subclass thereof.
     */
    isTemplate: true,
    /**
     * @cfg {Boolean} compiled
     * True to immediately compile the template. Defaults to false.
     */
    /**
     * @cfg {Boolean} disableFormats
     * True to disable format functions in the template. If the template doesn't contain
     * format functions, setting disableFormats to true will reduce apply time. Defaults to false.
     */
    disableFormats: false,
    /**
     * @property {RegExp} tokenRe
     * Regular expression used to extract tokens.
     *
     * Finds the following expressions within a format string
     *
     *                      {AND?}
     *                      /   \
     *                    /       \
     *                  /           \
     *                /               \
     *             OR                  AND?
     *            /  \                 / \
     *           /    \               /   \
     *          /      \             /     \
     *     (\d+)  ([a-z_][\w\-]*)   /       \
     *      index       name       /         \
     *                            /           \
     *                           /             \
     *                    \:([a-z_\.]*)   (?:\((.*?)?\))?
     *                       formatFn           args
     *
     * Numeric index or (name followed by optional formatting function and args)
     * @private
     */
    tokenRe: /\{(?:(?:(\d+)|([a-z_$][\w\-$]*))(?::([a-z_.]+)(?:\(([^)]*?)?\))?)?)\}/gi,
    /**
     * Returns an HTML fragment of this template with the specified values applied.
     *
     * @param {Object/Array} values The template values. Can be an array if your params are numeric:
     *
     *     var tpl = new Ext.Template('Name: {0}, Age: {1}');
     *     tpl.apply(['John', 25]);
     *
     * or an object:
     *
     *     var tpl = new Ext.Template('Name: {name}, Age: {age}');
     *     tpl.apply({name: 'John', age: 25});
     *
     * @return {String} The HTML fragment
     */
    apply: function(values) {
        var me = this;
        if (me.compiled) {
            if (!me.fn) {
                me.compile();
            }
            return me.fn(values).join('');
        }
        return me.evaluate(values);
    },
    /**
     * @private
     * Do not create the substitution closure on every apply call
     */
    evaluate: function(values) {
        var me = this,
            useFormat = !me.disableFormats,
            fm = Ext.util.Format,
            tpl = me;
        function fn(match, index, name, formatFn, args) {
            // Calculate the correct name extracted from the {}
            // Certain browser pass unmatched parameters as undefined, some as an empty string.
            if (name == null || name === '') {
                name = index;
            }
            if (formatFn && useFormat) {
                if (args) {
                    args = [
                        values[name]
                    ].concat(Ext.functionFactory('return [' + args + '];')());
                } else {
                    args = [
                        values[name]
                    ];
                }
                // Caller used '{0:this.bold}'. Create a call to tpl member function
                if (formatFn.substr(0, 5) === "this.") {
                    return tpl[formatFn.substr(5)].apply(tpl, args);
                }
                // Caller used '{0:number("0.00")}'. Create a call to Ext.util.Format function
                else if (fm[formatFn]) {
                    return fm[formatFn].apply(fm, args);
                } else // Caller used '{0:someRandomText}'. We must return it unchanged
                {
                    return match;
                }
            } else {
                return values[name] !== undefined ? values[name] : "";
            }
        }
        return me.html.replace(me.tokenRe, fn);
    },
    /**
     * Appends the result of this template to the provided output array.
     * @param {Object/Array} values The template values. See {@link #apply}.
     * @param {Array} out The array to which output is pushed.
     * @return {Array} The given out array.
     */
    applyOut: function(values, out) {
        var me = this;
        if (me.compiled) {
            if (!me.fn) {
                me.compile();
            }
            out.push.apply(out, me.fn(values));
        } else {
            out.push(me.apply(values));
        }
        return out;
    },
    /**
     * @method applyTemplate
     * @member Ext.Template
     * Alias for {@link #apply}.
     * @inheritdoc Ext.Template#apply
     */
    applyTemplate: function() {
        return this.apply.apply(this, arguments);
    },
    /**
     * Sets the HTML used as the template and optionally compiles it.
     * @param {String} html
     * @param {Boolean} compile (optional) True to compile the template.
     * @return {Ext.Template} this
     */
    set: function(html, compile) {
        var me = this;
        me.html = html;
        me.compiled = !!compile;
        me.fn = null;
        return me;
    },
    compileARe: /\\/g,
    compileBRe: /(\r\n|\n)/g,
    compileCRe: /'/g,
    /**
     * Compiles the template into an internal function, eliminating the RegEx overhead.
     * @return {Ext.Template} this
     */
    compile: function() {
        var me = this,
            code;
        code = me.html.replace(me.compileARe, '\\\\').replace(me.compileBRe, '\\n').replace(me.compileCRe, "\\'").replace(me.tokenRe, me.regexReplaceFn.bind(me));
        code = (this.disableFormats !== true ? 'var fm=Ext.util.Format;' : '') + (me.useEval ? '$=' : 'return') + " function(v){return ['" + code + "'];};";
        me.fn = me.useEval ? me.evalCompiled(code) : (new Function('Ext', code))(Ext);
        me.compiled = true;
        return me;
    },
    /**
     * @private
     */
    evalCompiled: function($) {
        // We have to use eval to realize the code block and capture the inner func we also
        // don't want a deep scope chain. We only do this in Firefox and it is also unhappy
        // with eval containing a return statement, so instead we assign to "$" and return
        // that. Because we use "eval", we are automatically sandboxed properly.
        eval($);
        return $;
    },
    regexReplaceFn: function(match, index, name, formatFn, args) {
        // Calculate the correct expression to use to index into the values object/array
        // index may be a numeric string, or a quoted alphanumeric string.
        // Certain browser pass unmatched parameters as undefined, some as an empty string.
        if (index == null || index === '') {
            index = '"' + name + '"';
        }
        // If we are being used as a formatter for Ext.String.format, we must skip the string
        // itself in the argument list.
        // Doing this enables String.format to omit the Array slice call.
        else if (this.stringFormat) {
            index = parseInt(index) + 1;
        }
        if (formatFn && this.disableFormats !== true) {
            args = args ? ',' + args : "";
            // Caller used '{0:this.bold}'. Create a call to member function
            if (formatFn.substr(0, 5) === "this.") {
                formatFn = formatFn + '(';
            }
            // Caller used '{0:number("0.00")}'. Create a call to Ext.util.Format function
            else if (Ext.util.Format[formatFn]) {
                formatFn = "fm." + formatFn + '(';
            } else // Caller used '{0:someRandomText}'. We must pass it through unchanged
            {
                return match;
            }
            return "'," + formatFn + "v[" + index + "]" + args + "),'";
        } else {
            return "',v[" + index + "] == undefined ? '' : v[" + index + "],'";
        }
    },
    /**
     * Applies the supplied values to the template and inserts the new node(s) as the first child
     * of el.
     *
     * @param {String/HTMLElement/Ext.dom.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return a Ext.Element.
     * @return {HTMLElement/Ext.dom.Element} The new node or Element
     */
    insertFirst: function(el, values, returnElement) {
        return this.doInsert('afterBegin', el, values, returnElement);
    },
    /**
     * Applies the supplied values to the template and inserts the new node(s) before el.
     *
     * @param {String/HTMLElement/Ext.dom.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return a Ext.Element.
     * @return {HTMLElement/Ext.dom.Element} The new node or Element
     */
    insertBefore: function(el, values, returnElement) {
        return this.doInsert('beforeBegin', el, values, returnElement);
    },
    /**
     * Applies the supplied values to the template and inserts the new node(s) after el.
     *
     * @param {String/HTMLElement/Ext.dom.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return a Ext.Element.
     * @return {HTMLElement/Ext.dom.Element} The new node or Element
     */
    insertAfter: function(el, values, returnElement) {
        return this.doInsert('afterEnd', el, values, returnElement);
    },
    /**
     * Applies the supplied `values` to the template and appends the new node(s) to the specified
     * `el`.
     *
     * For example usage see {@link Ext.Template Ext.Template class docs}.
     *
     * @param {String/HTMLElement/Ext.dom.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return an Ext.Element.
     * @return {HTMLElement/Ext.dom.Element} The new node or Element
     */
    append: function(el, values, returnElement) {
        return this.doInsert('beforeEnd', el, values, returnElement);
    },
    doInsert: function(where, el, values, returnElement) {
        var newNode = Ext.DomHelper.insertHtml(where, Ext.getDom(el), this.apply(values));
        return returnElement ? Ext.get(newNode) : newNode;
    },
    /**
     * Applies the supplied values to the template and overwrites the content of el with the new
     * node(s).
     *
     * @param {String/HTMLElement/Ext.dom.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return a Ext.Element.
     * @return {HTMLElement/Ext.dom.Element} The new node or Element
     */
    overwrite: function(el, values, returnElement) {
        var newNode = Ext.DomHelper.overwrite(Ext.getDom(el), this.apply(values));
        return returnElement ? Ext.get(newNode) : newNode;
    }
}, function(Template) {
    var formatRe = /\{\d+\}/,
        generateFormatFn = function(format) {
            // Generate a function which substitutes value tokens
            if (formatRe.test(format)) {
                format = new Template(format, formatTplConfig);
                return function() {
                    return format.apply(arguments);
                };
            } else // No value tokens
            {
                return function() {
                    return format;
                };
            }
        },
        // Flags for the template compile process.
        // stringFormat means that token 0 consumes argument 1 etc.
        // So that String.format does not have to slice the argument list.
        formatTplConfig = {
            useFormat: false,
            compiled: true,
            stringFormat: true
        },
        formatFns = {};
    /**
     * @method format
     * @inheritdoc Ext.String#method-format
     * @member Ext.util.Format
     * Alias for {@link Ext.String#format}.
     */
    /**
     * Allows you to define a tokenized string and pass an arbitrary number of arguments to replace
     * the tokens. Each token must be unique, and must increment in the format {0}, {1}, etc.
     * Example usage:
     *
     *     var cls = 'my-class',
     *         text = 'Some text';
     *     var s = Ext.String.format('<div class="{0}">{1}</div>', cls, text);
     *     // s now contains the string: '<div class="my-class">Some text</div>'
     *
     * @param {String} format The tokenized string to be formatted.
     * @param {Mixed...} values The values to replace tokens `{0}`, `{1}`, etc in order.
     * @return {String} The formatted string.
     * @member Ext.String
     */
    Ext.String.format = Ext.util.Format.format = function(format) {
        var formatFn = formatFns[format] || (formatFns[format] = generateFormatFn(format));
        return formatFn.apply(this, arguments);
    };
    Ext.String.formatEncode = function() {
        return Ext.String.htmlEncode(Ext.String.format.apply(this, arguments));
    };
});

/**
 * This class parses the XTemplate syntax and calls abstract methods to process the parts.
 * @private
 */
Ext.define('Ext.util.XTemplateParser', {
    /* eslint-disable dot-notation */
    requires: [
        'Ext.String'
    ],
    constructor: function(config) {
        Ext.apply(this, config);
    },
    /**
     * @property {Number} level The 'for' or 'foreach' loop context level. This is adjusted
     * up by one prior to calling {@link #doFor} or {@link #doForEach} and down by one after
     * calling the corresponding {@link #doEnd} that closes the loop. This will be 1 on the
     * first {@link #doFor} or {@link #doForEach} call.
     */
    /**
     * This method is called to process a piece of raw text from the tpl.
     * @param {String} text
     * @method doText
     */
    // doText: function (text)
    /**
     * This method is called to process expressions (like `{[expr]}`).
     * @param {String} expr The body of the expression (inside "{[" and "]}").
     * @method doExpr
     */
    // doExpr: function (expr)
    /**
     * This method is called to process simple tags (like `{tag}`).
     * @method doTag
     */
    // doTag: function (tag)
    /**
     * This method is called to process `<tpl else>`.
     * @method doElse
     */
    // doElse: function ()
    /**
     * This method is called to process `{% text %}`.
     * @param {String} text
     * @method doEval
     */
    // doEval: function (text)
    /**
     * This method is called to process `<tpl if="action">`. If there are other attributes,
     * these are passed in the actions object.
     * @param {String} action
     * @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
     * @method doIf
     */
    // doIf: function (action, actions)
    /**
     * This method is called to process `<tpl elseif="action">`. If there are other attributes,
     * these are passed in the actions object.
     * @param {String} action
     * @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
     * @method doElseIf
     */
    // doElseIf: function (action, actions)
    /**
     * This method is called to process `<tpl switch="action">`. If there are other attributes,
     * these are passed in the actions object.
     * @param {String} action
     * @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
     * @method doSwitch
     */
    // doSwitch: function (action, actions)
    /**
     * This method is called to process `<tpl case="action">`. If there are other attributes,
     * these are passed in the actions object.
     * @param {String} action
     * @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
     * @method doCase
     */
    // doCase: function (action, actions)
    /**
     * This method is called to process `<tpl default>`.
     * @method doDefault
     */
    // doDefault: function ()
    /**
     * This method is called to process `</tpl>`. It is given the action type that started
     * the tpl and the set of additional actions.
     * @param {String} type The type of action that is being ended.
     * @param {Object} actions The other actions keyed by the attribute name (such as 'exec').
     * @method doEnd
     */
    // doEnd: function (type, actions) 
    /**
     * This method is called to process `<tpl for="action">`. If there are other attributes,
     * these are passed in the actions object.
     * @param {String} action
     * @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
     * @method doFor
     */
    // doFor: function (action, actions)
    /**
     * This method is called to process `<tpl foreach="action">`. If there are other
     * attributes, these are passed in the actions object.
     * @param {String} action
     * @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
     * @method doForEach
     */
    // doForEach: function (action, actions)
    /**
     * This method is called to process `<tpl exec="action">`. If there are other attributes,
     * these are passed in the actions object.
     * @param {String} action
     * @param {Object} actions Other actions keyed by the attribute name.
     * @method doExec
     */
    // doExec: function (action, actions)
    /**
     * This method is called to process an empty `<tpl>`. This is unlikely to need to be
     * implemented, so a default (do nothing) version is provided.
     * @method
     */
    doTpl: Ext.emptyFn,
    parse: function(str) {
        var me = this,
            len = str.length,
            aliases = {
                elseif: 'elif'
            },
            topRe = me.topRe,
            actionsRe = me.actionsRe,
            index, stack, s, m, t, prev, frame, subMatch, begin, end, actions, prop, expectTplNext;
        me.level = 0;
        me.stack = stack = [];
        for (index = 0; index < len; index = end) {
            topRe.lastIndex = index;
            m = topRe.exec(str);
            if (!m) {
                me.doText(str.substring(index, len));
                break;
            }
            begin = m.index;
            end = topRe.lastIndex;
            if (index < begin) {
                // In the case of a switch statement, we expect a tpl for each case.
                // However, if we have spaces they will get matched as plaintext, so
                // we want to skip over them here.
                s = str.substring(index, begin);
                if (!(expectTplNext && Ext.String.trim(s) === '')) {
                    me.doText(s);
                }
            }
            expectTplNext = false;
            if (m[1]) {
                end = str.indexOf('%}', begin + 2);
                me.doEval(str.substring(begin + 2, end));
                end += 2;
            } else if (m[2]) {
                end = str.indexOf(']}', begin + 2);
                me.doExpr(str.substring(begin + 2, end));
                end += 2;
            } else if (m[3]) {
                // if ('{' token)
                me.doTag(m[3]);
            } else if (m[4]) {
                // content of a <tpl xxxxxx xxx> tag
                actions = null;
                while ((subMatch = actionsRe.exec(m[4])) !== null) {
                    s = subMatch[2] || subMatch[3];
                    if (s) {
                        s = Ext.String.htmlDecode(s);
                        // decode attr value
                        t = subMatch[1];
                        t = aliases[t] || t;
                        actions = actions || {};
                        prev = actions[t];
                        if (typeof prev === 'string') {
                            actions[t] = [
                                prev,
                                s
                            ];
                        } else if (prev) {
                            actions[t].push(s);
                        } else {
                            actions[t] = s;
                        }
                    }
                }
                if (!actions) {
                    if (me.elseRe.test(m[4])) {
                        me.doElse();
                    } else if (me.defaultRe.test(m[4])) {
                        me.doDefault();
                    } else {
                        me.doTpl();
                        stack.push({
                            type: 'tpl'
                        });
                    }
                } else if (actions['if']) {
                    me.doIf(actions['if'], actions);
                    stack.push({
                        type: 'if'
                    });
                } else if (actions['switch']) {
                    me.doSwitch(actions['switch'], actions);
                    stack.push({
                        type: 'switch'
                    });
                    expectTplNext = true;
                } else if (actions['case']) {
                    me.doCase(actions['case'], actions);
                } else if (actions['elif']) {
                    me.doElseIf(actions['elif'], actions);
                } else if (actions['for']) {
                    ++me.level;
                    // Extract property name to use from indexed item
                    // eslint-disable-next-line no-cond-assign
                    if (prop = me.propRe.exec(m[4])) {
                        actions.propName = prop[1] || prop[2];
                    }
                    me.doFor(actions['for'], actions);
                    stack.push({
                        type: 'for',
                        actions: actions
                    });
                } else if (actions['foreach']) {
                    ++me.level;
                    // Extract property name to use from indexed item
                    // eslint-disable-next-line no-cond-assign
                    if (prop = me.propRe.exec(m[4])) {
                        actions.propName = prop[1] || prop[2];
                    }
                    me.doForEach(actions['foreach'], actions);
                    stack.push({
                        type: 'foreach',
                        actions: actions
                    });
                } else if (actions.exec) {
                    me.doExec(actions.exec, actions);
                    stack.push({
                        type: 'exec',
                        actions: actions
                    });
                }
            }
            /*
                else {
                    // todo - error
                }
                */
            else if (m[0].length === 5) {
                // if the length of m[0] is 5, assume that we're dealing with an opening tpl tag
                // with no attributes (e.g. <tpl>...</tpl>)
                // in this case no action is needed other than pushing it on to the stack
                stack.push({
                    type: 'tpl'
                });
            } else {
                frame = stack.pop();
                me.doEnd(frame.type, frame.actions);
                if (frame.type === 'for' || frame.type === 'foreach') {
                    --me.level;
                }
            }
        }
    },
    // Internal regexes
    /* eslint-disable no-useless-escape */
    topRe: /(?:(\{\%)|(\{\[)|\{([^{}]+)\})|(?:<tpl([^>]*)\>)|(?:<\/tpl>)/g,
    actionsRe: /\s*(elif|elseif|if|for|foreach|exec|switch|case|eval|between)\s*\=\s*(?:(?:"([^"]*)")|(?:'([^']*)'))\s*/g,
    propRe: /prop=(?:(?:"([^"]*)")|(?:'([^']*)'))/,
    defaultRe: /^\s*default\s*$/,
    elseRe: /^\s*else\s*$/
});

/**
 * This class compiles the XTemplate syntax into a function object. The function is used
 * like so:
 * 
 *      function (out, values, parent, xindex, xcount) {
 *          // out is the output array to store results
 *          // values, parent, xindex and xcount have their historical meaning
 *      }
 *
 * @private
 */
Ext.define('Ext.util.XTemplateCompiler', {
    extend: 'Ext.util.XTemplateParser',
    // Chrome really likes "new Function" to realize the code block (as in it is
    // 2x-3x faster to call it than using eval), but Firefox chokes on it badly.
    // IE and Opera are also fine with the "new Function" technique.
    useEval: Ext.isGecko,
    // See http://jsperf.com/nige-array-append for quickest way to append to an array
    // of unknown length. (Due to arbitrary code execution inside a template, we cannot easily
    // track the length in var)
    // On IE8 and earlier, myArray[myArray.length]='foo' is better. On other browsers
    // myArray.push('foo') is better.
    useIndex: Ext.isIE8m,
    useFormat: true,
    propNameRe: /^[\w\d$]*$/,
    compile: function(tpl) {
        var me = this,
            code = me.generate(tpl);
        // When using "new Function", we have to pass our "Ext" variable to it in order to
        // support sandboxing. If we did not, the generated function would use the global
        // "Ext", not the "Ext" from our sandbox (scope chain).
        //
        return me.useEval ? me.evalTpl(code) : (new Function('Ext', code))(Ext);
    },
    generate: function(tpl) {
        var me = this,
            // note: Ext here is properly sandboxed
            definitions = 'var fm=Ext.util.Format,ts=Object.prototype.toString;',
            code;
        // Track how many levels we use, so that we only "var" each level's variables once
        me.maxLevel = 0;
        me.body = [
            'var c0=values, a0=' + me.createArrayTest(0) + ', p0=parent, n0=xcount, i0=xindex, k0, v;\n'
        ];
        if (me.definitions) {
            if (typeof me.definitions === 'string') {
                me.definitions = [
                    me.definitions,
                    definitions
                ];
            } else {
                me.definitions.push(definitions);
            }
        } else {
            me.definitions = [
                definitions
            ];
        }
        me.switches = [];
        me.parse(tpl);
        me.definitions.push((me.useEval ? '$=' : 'return') + ' function (' + me.fnArgs + ') {', me.body.join(''), '}');
        code = me.definitions.join('\n');
        // Free up the arrays.
        me.definitions.length = me.body.length = me.switches.length = 0;
        me.definitions = me.body = me.switches = 0;
        return code;
    },
    //-----------------------------------
    // XTemplateParser callouts
    doText: function(text) {
        var me = this,
            out = me.body;
        text = text.replace(me.aposRe, "\\'").replace(me.newLineRe, '\\n');
        if (me.useIndex) {
            out.push('out[out.length]=\'', text, '\'\n');
        } else {
            out.push('out.push(\'', text, '\')\n');
        }
    },
    doExpr: function(expr) {
        var out = this.body;
        out.push('if ((v=' + expr + ') != null) out');
        // Do not coerce individual values in the buffer.
        // The final Array#join('') will coerce to strings.
        if (this.useIndex) {
            out.push('[out.length]=v\n');
        } else {
            out.push('.push(v)\n');
        }
    },
    doTag: function(tag) {
        var expr = this.parseTag(tag);
        if (expr) {
            this.doExpr(expr);
        } else {
            // if we cannot match on tagRe handle as plain text
            this.doText('{' + tag + '}');
        }
    },
    doElse: function() {
        this.body.push('} else {\n');
    },
    doEval: function(text) {
        this.body.push(text, '\n');
    },
    doIf: function(action, actions) {
        var me = this;
        // If it's just a propName, use it directly in the if
        if (action === '.') {
            me.body.push('if (values) {\n');
        } else if (me.propNameRe.test(action)) {
            me.body.push('if (', me.parseTag(action), ') {\n');
        } else // Otherwise, it must be an expression, and needs to be returned from an fn
        // which uses with(values)
        {
            me.body.push('if (', me.addFn(action), me.callFn, ') {\n');
        }
        if (actions.exec) {
            me.doExec(actions.exec);
        }
    },
    doElseIf: function(action, actions) {
        var me = this;
        // If it's just a propName, use it directly in the else if
        if (action === '.') {
            me.body.push('else if (values) {\n');
        } else if (me.propNameRe.test(action)) {
            me.body.push('} else if (', me.parseTag(action), ') {\n');
        } else // Otherwise, it must be an expression, and needs to be returned from an fn
        // which uses with(values)
        {
            me.body.push('} else if (', me.addFn(action), me.callFn, ') {\n');
        }
        if (actions.exec) {
            me.doExec(actions.exec);
        }
    },
    doSwitch: function(action) {
        var me = this,
            key;
        // If it's just a propName, use it directly in the switch
        if (action === '.' || action === '#') {
            key = action === '.' ? 'values' : 'xindex';
            me.body.push('switch (', key, ') {\n');
        } else if (me.propNameRe.test(action)) {
            me.body.push('switch (', me.parseTag(action), ') {\n');
        } else // Otherwise, it must be an expression, and needs to be returned from an fn
        // which uses with(values)
        {
            me.body.push('switch (', me.addFn(action), me.callFn, ') {\n');
        }
        me.switches.push(0);
    },
    doCase: function(action) {
        var me = this,
            cases = Ext.isArray(action) ? action : [
                action
            ],
            n = me.switches.length - 1,
            match, i;
        if (me.switches[n]) {
            me.body.push('break;\n');
        } else {
            me.switches[n]++;
        }
        for (i = 0 , n = cases.length; i < n; ++i) {
            match = me.intRe.exec(cases[i]);
            cases[i] = match ? match[1] : ("'" + cases[i].replace(me.aposRe, "\\'") + "'");
        }
        me.body.push('case ', cases.join(': case '), ':\n');
    },
    doDefault: function() {
        var me = this,
            n = me.switches.length - 1;
        if (me.switches[n]) {
            me.body.push('break;\n');
        } else {
            me.switches[n]++;
        }
        me.body.push('default:\n');
    },
    doEnd: function(type, actions) {
        var me = this,
            L = me.level - 1;
        if (type === 'for' || type === 'foreach') {
            /*
            To exit a for or foreach loop we must restore the outer loop's context. The
            code looks like this (which goes with that produced by doFor or doForEach):

                    for (...) { // the part generated by doFor or doForEach
                        ...  // the body of the for loop

                        // ... any tpl for exec statement goes here...
                    }
                    parent = p1;
                    values = r2;
                    xcount = n1;
                    xindex = i1
            */
            if (actions.exec) {
                me.doExec(actions.exec);
            }
            me.body.push('}\n');
            me.body.push('parent=p', L, ';values=r', L + 1, ';xcount=n' + L + ';xindex=i', L, '+1;xkey=k', L, ';\n');
        } else if (type === 'if' || type === 'switch') {
            me.body.push('}\n');
        }
    },
    doFor: function(action, actions) {
        var me = this,
            s,
            L = me.level,
            up = L - 1,
            parentAssignment;
        // If it's just a propName, use it directly in the switch
        if (action === '.') {
            s = 'values';
        } else if (me.propNameRe.test(action)) {
            s = me.parseTag(action);
        } else // Otherwise, it must be an expression, and needs to be returned from an fn
        // which uses with(values)
        {
            s = me.addFn(action) + me.callFn;
        }
        /*
        We are trying to produce a block of code that looks like below. We use the nesting
        level to uniquely name the control variables.

            // Omit "var " if we have already been through level 2
            var i2 = 0,
                n2 = 0,
                c2 = values['propName'],
                    // c2 is the context object for the for loop
                a2 = Array.isArray(c2);
                r2 = values,
                    // r2 is the values object 
                p2, // p2 is the parent context (of the outer for loop)
                k2; // object key - not used by for loop but doEnd needs this to be declared 

            // If iterating over the current data, the parent is always set to c2
            p2 = parent = c2;
            // If iterating over a property in an object, set the parent to the object
            p2 = parent = a1 ? c1[i1] : c1 // set parent
            if (c2) {
                if (a2) {
                    n2 = c2.length;
                } else if (c2.isMixedCollection) {
                    c2 = c2.items;
                    n2 = c2.length;
                } else if (c2.isStore) {
                    c2 = c2.data.items;
                    n2 = c2.length;
                } else {
                    c2 = [ c2 ];
                    n2 = 1;
                }
            }
            // i2 is the loop index and n2 is the number (xcount) of this for loop
            for (xcount = n2; i2 < n2; ++i2) {
                values = c2[i2]           // adjust special vars to inner scope
                xindex = i2 + 1           // xindex is 1-based

        The body of the loop is whatever comes between the tpl and /tpl statements (which
        is handled by doEnd).
        */
        // Declare the vars for a particular level only if we have not already declared them.
        if (me.maxLevel < L) {
            me.maxLevel = L;
            me.body.push('var ');
        }
        if (action === '.') {
            parentAssignment = 'c' + L;
        } else {
            parentAssignment = 'a' + up + '?c' + up + '[i' + up + ']:c' + up;
        }
        me.body.push('i', L, '=0,n', L, '=0,c', L, '=', s, ',a', L, '=', me.createArrayTest(L), ',r', L, '=values,p', L, ',k', L, ';\n', 'p', L, '=parent=', parentAssignment, '\n', 'if (c', L, '){if(a', L, '){n', L, '=c', L, '.length;}else if (c', L, '.isMixedCollection){c', L, '=c', L, '.items;n', L, '=c', L, '.length;}else if(c', L, '.isStore){c', L, '=c', L, '.data.items;n', L, '=c', L, '.length;}else{c', L, '=[c', L, '];n', L, '=1;}}\n', 'for (xcount=n', L, ';i', L, '<n' + L + ';++i', L, '){\n', 'values=c', L, '[i', L, ']');
        if (actions.propName) {
            me.body.push('.', actions.propName);
        }
        me.body.push('\n', 'xindex=i', L, '+1\n');
        if (actions.between) {
            me.body.push('if(xindex>1){ out.push("', actions.between, '"); } \n');
        }
    },
    doForEach: function(action, actions) {
        var me = this,
            L = me.level,
            up = L - 1,
            s, parentAssignment;
        // If it's just a propName, use it directly in the switch
        if (action === '.') {
            s = 'values';
        } else if (me.propNameRe.test(action)) {
            s = me.parseTag(action);
        } else // Otherwise, it must be an expression, and needs to be returned from an fn
        // which uses with(values)
        {
            s = me.addFn(action) + me.callFn;
        }
        /*
        We are trying to produce a block of code that looks like below. We use the nesting
        level to uniquely name the control variables.

            // Omit "var " if we have already been through level 2
            var i2 = -1,
                n2 = 0,
                c2 = values['propName'], // c2 is the context object for the for loop
                a2 = Array.isArray(c2);
                r2 = values, // r2 is the values object
                p2, // p2 is the parent context (of the outer for loop)
                k2; // k2 is the object key while looping

            // If iterating over the current data, the parent is always set to c2
            p2 = parent = c2;
            // If iterating over a property in an object, set the parent to the object
            p2 = parent = a1 ? c1[i1] : c1 // set parent

            for(k2 in c2){
                xindex = ++i + 1; // xindex is 1-based
                xkey = k2;
                values = c2[k2]; // values is the property value

        The body of the loop is whatever comes between the tpl and /tpl statements (which
        is handled by doEnd).
        */
        // Declare the vars for a particular level only if we have not already declared them.
        if (me.maxLevel < L) {
            me.maxLevel = L;
            me.body.push('var ');
        }
        if (action === '.') {
            parentAssignment = 'c' + L;
        } else {
            parentAssignment = 'a' + up + '?c' + up + '[i' + up + ']:c' + up;
        }
        me.body.push('i', L, '=-1,n', L, '=0,c', L, '=', s, ',a', L, '=', me.createArrayTest(L), ',r', L, '=values,p', L, ',k', L, ';\n', 'p', L, '=parent=', parentAssignment, '\n', 'for(k', L, ' in c', L, '){\n', 'xindex=++i', L, '+1;\n', 'xkey=k', L, ';\n', 'values=c', L, '[k', L, '];');
        if (actions.propName) {
            me.body.push('.', actions.propName);
        }
        if (actions.between) {
            me.body.push('if(xindex>1){ out.push("', actions.between, '"); } \n');
        }
    },
    createArrayTest: ('isArray' in Array) ? function(L) {
        return 'Array.isArray(c' + L + ')';
    } : function(L) {
        return 'ts.call(c' + L + ')==="[object Array]"';
    },
    doExec: function(action, actions) {
        var me = this,
            name = 'f' + me.definitions.length,
            guards = me.guards[me.strict ? 0 : 1];
        me.definitions.push('function ' + name + '(' + me.fnArgs + ') {', guards.doTry, ' var $v = values; with($v) {', '  ' + action, ' }', guards.doCatch, '}');
        me.body.push(name + me.callFn + '\n');
    },
    //-----------------------------------
    // Internal
    guards: [
        {
            doTry: '',
            doCatch: ''
        },
        {
            doTry: 'try { ',
            doCatch: ' } catch(e) {\n' + 'Ext.log.warn("XTemplate evaluation exception: " + e.message);\n' + '}'
        }
    ],
    addFn: function(body) {
        var me = this,
            name = 'f' + me.definitions.length,
            guards = me.guards[me.strict ? 0 : 1];
        if (body === '.') {
            me.definitions.push('function ' + name + '(' + me.fnArgs + ') {', ' return values', '}');
        } else if (body === '..') {
            me.definitions.push('function ' + name + '(' + me.fnArgs + ') {', ' return parent', '}');
        } else {
            me.definitions.push('function ' + name + '(' + me.fnArgs + ') {', guards.doTry, ' var $v = values; with($v) {', '  return(' + body + ')', ' }', guards.doCatch, '}');
        }
        return name;
    },
    parseTag: function(tag) {
        var me = this,
            m = me.tagRe.exec(tag),
            name, format, args, math, v;
        if (!m) {
            return null;
        }
        name = m[1];
        format = m[2];
        args = m[3];
        math = m[4];
        // name = "." - Just use the values object.
        if (name === '.') {
            // filter to not include arrays/objects/nulls
            if (!me.validTypes) {
                me.definitions.push('var validTypes={string:1,number:1,boolean:1};');
                me.validTypes = true;
            }
            v = 'validTypes[typeof values] || ts.call(values) === "[object Date]" ? values : ""';
        }
        // name = "#" - Use the xindex
        else if (name === '#') {
            v = 'xindex';
        }
        // name = "$" - Use the xkey
        else if (name === '$') {
            v = 'xkey';
        } else if (name.substr(0, 7) === "parent.") {
            v = name;
        }
        // compound Javascript property name (e.g., "foo.bar")
        else if (isNaN(name) && name.indexOf('-') === -1 && name.indexOf('.') !== -1) {
            v = "values." + name;
        } else // number or a '-' in it or a single word (maybe a keyword): use array notation
        // (http://jsperf.com/string-property-access/4)
        {
            v = "values['" + name + "']";
        }
        if (math) {
            v = '(' + v + math + ')';
        }
        if (format && me.useFormat) {
            args = args ? ',' + args : "";
            if (format.substr(0, 5) !== "this.") {
                format = "fm." + format + '(';
            } else {
                format += '(';
            }
        } else {
            return v;
        }
        return format + v + args + ')';
    },
    /**
     * @private
     */
    evalTpl: function($) {
        // We have to use eval to realize the code block and capture the inner func we also
        // don't want a deep scope chain. We only do this in Firefox and it is also unhappy
        // with eval containing a return statement, so instead we assign to "$" and return
        // that. Because we use "eval", we are automatically sandboxed properly.
        eval($);
        return $;
    },
    newLineRe: /\r\n|\r|\n/g,
    aposRe: /[']/g,
    intRe: /^\s*(\d+)\s*$/,
    // eslint-disable-next-line no-useless-escape
    tagRe: /^([\w-\.\#\$]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\/]\s?[\d\.\+\-\*\/\(\)]+)?$/
}, function() {
    var proto = this.prototype;
    proto.fnArgs = 'out,values,parent,xindex,xcount,xkey';
    proto.callFn = '.call(this,' + proto.fnArgs + ')';
});

/**
 * A template class that supports advanced functionality like:
 *
 * - Auto-filling arrays using templates and sub-templates
 * - Conditional processing with basic comparison operators
 * - Basic math function support
 * - Execute arbitrary inline code with special built-in template variables
 * - Custom member functions
 * - Many special tags and built-in operators that aren't defined as part of the API, but are
 * supported in the templates that can be created
 *
 * XTemplate provides the templating mechanism built into {@link Ext.view.View}.
 *
 * The {@link Ext.Template} describes the acceptable parameters to pass to the constructor.
 * The following examples demonstrate all of the supported features.
 *
 * # Sample Data
 *
 * This is the data object used for reference in each code example:
 *
 *     var data = {
 *         name: 'Don Griffin',
 *         title: 'Senior Technomage',
 *         company: 'Sencha Inc.',
 *         drinks: ['Coffee', 'Water', 'More Coffee'],
 *         kids: [
 *             { name: 'Aubrey',  age: 17 },
 *             { name: 'Joshua',  age: 13 },
 *             { name: 'Cale',    age: 10 },
 *             { name: 'Nikol',   age: 5 },
 *             { name: 'Solomon', age: 0 }
 *         ]
 *     };
 *
 * # Auto filling of arrays
 *
 * The **tpl** tag and the **for** operator are used to process the provided data object:
 *
 * - If the value specified in for is an array, it will auto-fill, repeating the template block
 * inside the tpl tag for each item in the array.
 * - If for="." is specified, the data object provided is examined.
 * - If between="..." is specified, the provided value will be inserted between the items.
 *   This is also supported in the "foreach" looping template.
 * - While processing an array, the special variable {#} will provide the current array index + 1
 * (starts at 1, not 0).
 *
 * Examples:
 *
 *     // loop through array at root node
 *     <tpl for=".">...</tpl>
 *
 *     // loop through array at foo node
 *     <tpl for="foo">...</tpl>
 *
 *     // loop through array at foo.bar node
 *     <tpl for="foo.bar">...</tpl>
 *
 *     // loop through array at root node and insert ',' between each item
 *     <tpl for="." between=",">...</tpl>
 *
 * Using the sample data above:
 *
 *     var tpl = new Ext.XTemplate(
 *         '<p>Kids: ',
 *         '<tpl for=".">',       // process the data.kids node
 *             '<p>{#}. {name}</p>',  // use current array index to autonumber
 *         '</tpl></p>'
 *     );
 *     tpl.overwrite(panel.body, data.kids); // pass the kids property of the data object
 *
 * An example illustrating how the **for** property can be leveraged to access specified members
 * of the provided data object to populate the template:
 *
 *     var tpl = new Ext.XTemplate(
 *         '<p>Name: {name}</p>',
 *         '<p>Title: {title}</p>',
 *         '<p>Company: {company}</p>',
 *         '<p>Kids: ',
 *         '<tpl for="kids">',     // interrogate the kids property within the data
 *             '<p>{name}</p>',
 *         '</tpl></p>'
 *     );
 *     tpl.overwrite(panel.body, data);  // pass the root node of the data object
 *
 * Flat arrays that contain values (and not objects) can be auto-rendered using the special
 * **`{.}`** variable inside a loop. This variable will represent the value of the array
 * at the current index:
 *
 *     var tpl = new Ext.XTemplate(
 *         '<p>{name}\'s favorite beverages:</p>',
 *         '<tpl for="drinks">',
 *             '<div> - {.}</div>',
 *         '</tpl>'
 *     );
 *     tpl.overwrite(panel.body, data);
 *
 * When processing a sub-template, for example while looping through a child array, you can access
 * the parent object's members via the **parent** object:
 *
 *     var tpl = new Ext.XTemplate(
 *         '<p>Name: {name}</p>',
 *         '<p>Kids: ',
 *         '<tpl for="kids">',
 *             '<tpl if="age &gt; 1">',
 *                 '<p>{name}</p>',
 *                 '<p>Dad: {parent.name}</p>',
 *             '</tpl>',
 *         '</tpl></p>'
 *     );
 *     tpl.overwrite(panel.body, data);
 *     
 * The **foreach** operator is used to loop over an object's properties.  The following
 * example demonstrates looping over the main data object's properties:
 * 
 *     var tpl = new Ext.XTemplate(
 *         '<dl>',
 *             '<tpl foreach=".">',
 *                 // the special **`{$}`** variable contains the property name
 *                 '<dt>{$}</dt>',
 *                 // within the loop, the **`{.}`** variable is set to the property value
 *                 '<dd>{.}</dd>',
 *             '</tpl>',
 *         '</dl>'
 *     );
 *     tpl.overwrite(panel.body, data);
 *
 * # Conditional processing with basic comparison operators
 *
 * The **tpl** tag and the **if** operator are used to provide conditional checks for deciding
 * whether or not to render specific parts of the template.
 *
 * Using the sample data above:
 *
 *     var tpl = new Ext.XTemplate(
 *         '<p>Name: {name}</p>',
 *         '<p>Kids: ',
 *         '<tpl for="kids">',
 *             '<tpl if="age &gt; 1">',
 *                 '<p>{name}</p>',
 *             '</tpl>',
 *         '</tpl></p>'
 *     );
 *     tpl.overwrite(panel.body, data);
 *
 * More advanced conditionals are also supported:
 *
 *     var tpl = new Ext.XTemplate(
 *         '<p>Name: {name}</p>',
 *         '<p>Kids: ',
 *         '<tpl for="kids">',
 *             '<p>{name} is a ',
 *             '<tpl if="age &gt;= 13">',
 *                 '<p>teenager</p>',
 *             '<tpl elseif="age &gt;= 2">',
 *                 '<p>kid</p>',
 *             '<tpl else>',
 *                 '<p>baby</p>',
 *             '</tpl>',
 *         '</tpl></p>'
 *     );
 *
 *     var tpl = new Ext.XTemplate(
 *         '<p>Name: {name}</p>',
 *         '<p>Kids: ',
 *         '<tpl for="kids">',
 *             '<p>{name} is a ',
 *             '<tpl switch="name">',
 *                 '<tpl case="Aubrey" case="Nikol">',
 *                     '<p>girl</p>',
 *                 '<tpl default>',
 *                     '<p>boy</p>',
 *             '</tpl>',
 *         '</tpl></p>'
 *     );
 *
 * A `break` is implied between each case and default, however, multiple cases can be listed
 * in a single &lt;tpl&gt; tag.
 *
 * # Using double quotes
 *
 * Examples:
 *
 *     var tpl = new Ext.XTemplate(
 *         "<tpl if='age &gt; 1 && age &lt; 10'>Child</tpl>",
 *         "<tpl if='age &gt;= 10 && age &lt; 18'>Teenager</tpl>",
 *         "<tpl if='this.isGirl(name)'>...</tpl>",
 *         '<tpl if="id == \'download\'">...</tpl>',
 *         "<tpl if='needsIcon'><img src='{icon}' class='{iconCls}'/></tpl>",
 *         "<tpl if='name == \"Don\"'>Hello</tpl>"
 *     );
 *
 * # Basic math support
 *
 * The following basic math operators may be applied directly on numeric data values:
 *
 *     + - * /
 *
 * For example:
 *
 *     var tpl = new Ext.XTemplate(
 *         '<p>Name: {name}</p>',
 *         '<p>Kids: ',
 *         '<tpl for="kids">',
 *             '<tpl if="age &gt; 1">',  // <-- Note that the > is encoded
 *                 '<p>{#}: {name}</p>',  // <-- Auto-number each item
 *                 '<p>In 5 Years: {age+5}</p>',  // <-- Basic math
 *                 '<p>Dad: {parent.name}</p>',
 *             '</tpl>',
 *         '</tpl></p>'
 *     );
 *     tpl.overwrite(panel.body, data);
 *
 * # Execute arbitrary inline code with special built-in template variables
 *
 * Anything between `{[ ... ]}` is considered code to be executed in the scope of the template.
 * The expression is evaluated and the result is included in the generated result. There are
 * some special variables available in that code:
 *
 * - **out**: The output array into which the template is being appended (using `push` to later
 *   `join`).
 * - **values**: The values in the current scope. If you are using scope changing sub-templates,
 *   you can change what values is.
 * - **parent**: The scope (values) of the ancestor template.
 * - **xindex**: If you are in a "for" or "foreach" looping template, the index of the loop you
 *  are in (1-based).
 * - **xcount**: If you are in a "for" looping template, the total length of the array you are
 *  looping.
 * - **xkey**: If you are in a "foreach" looping template, the key of the current property
 * being examined.
 *
 * This example demonstrates basic row striping using an inline code block and the xindex variable:
 *
 *     var tpl = new Ext.XTemplate(
 *         '<p>Name: {name}</p>',
 *         '<p>Company: {[values.company.toUpperCase() + ", " + values.title]}</p>',
 *         '<p>Kids: ',
 *         '<tpl for="kids">',
 *             '<div class="{[xindex % 2 === 0 ? "even" : "odd"]}">',
 *             '{name}',
 *             '</div>',
 *         '</tpl></p>'
 *      );
 *
 * Any code contained in "verbatim" blocks (using "{% ... %}") will be inserted directly in
 * the generated code for the template. These blocks are not included in the output. This
 * can be used for simple things like break/continue in a loop, or control structures or
 * method calls (when they don't produce output). The `this` references the template instance.
 *
 *     var tpl = new Ext.XTemplate(
 *         '<p>Name: {name}</p>',
 *         '<p>Company: {[values.company.toUpperCase() + ", " + values.title]}</p>',
 *         '<p>Kids: ',
 *         '<tpl for="kids">',
 *             '{% if (xindex % 2 === 0) continue; %}',
 *             '{name}',
 *             '{% if (xindex > 100) break; %}',
 *             '</div>',
 *         '</tpl></p>'
 *      );
 *
 * # Template member functions
 *
 * One or more member functions can be specified in a configuration object passed into the
 * XTemplate constructor for more complex processing:
 *
 *     var tpl = new Ext.XTemplate(
 *         '<p>Name: {name}</p>',
 *         '<p>Kids: ',
 *         '<tpl for="kids">',
 *             '<tpl if="this.isGirl(name)">',
 *                 '<p>Girl: {name} - {age}</p>',
 *             '<tpl else>',
 *                 '<p>Boy: {name} - {age}</p>',
 *             '</tpl>',
 *             '<tpl if="this.isBaby(age)">',
 *                 '<p>{name} is a baby!</p>',
 *             '</tpl>',
 *         '</tpl></p>',
 *         {
 *             // XTemplate configuration:
 *             disableFormats: true,
 *             // member functions:
 *             isGirl: function(name){
 *                return name == 'Aubrey' || name == 'Nikol';
 *             },
 *             isBaby: function(age){
 *                return age < 1;
 *             }
 *         }
 *     );
 *     tpl.overwrite(panel.body, data);
 */
Ext.define('Ext.XTemplate', {
    extend: 'Ext.Template',
    requires: [
        'Ext.util.XTemplateCompiler'
    ],
    isXTemplate: true,
    /**
     * @private
     */
    emptyObj: {},
    /**
     * @cfg {Boolean} compiled
     * Only applies to {@link Ext.Template}, XTemplates are compiled automatically on the
     * first call to {@link #apply} or {@link #applyOut}.
     * @hide
     */
    /**
     * @cfg {String/Array} definitions
     * Optional. A statement, or array of statements which set up `var`s which may then
     * be accessed within the scope of the generated function.
     * 
     *     var data = {
     *         name: 'Don Griffin',
     *         isWizard: true,
     *         title: 'Senior Technomage',
     *         company: 'Sencha Inc.'
     *     };
     *     
     *     var tpl = new Ext.XTemplate('{[values.isWizard ? wizard : notSoWizard]}' +
     *         ' {name}', {
     *         definitions: 'var wizard = "Wizard", notSoWizard = "Townsperson";'
     *     });
     *     
     *     console.log(tpl.apply(data));
     *     // LOGS: Wizard Don Griffin
     */
    /**
     * @property {Function} fn
     * The function that applies this template. This is created on first use of the
     * template (calls to `apply` or `applyOut`).
     * @private
     * @readonly
     */
    fn: null,
    /**
     * @cfg {Boolean} [strict=false]
     * Expressions in templates that traverse "dot paths" and fail (due to `null` at some
     * stage) have always been expanded as empty strings. This is convenient in most cases
     * but doing so can also mask errors in the template. Setting this to `true` changes
     * this default so that any expression errors will be thrown as exceptions.
     */
    strict: false,
    apply: function(values, parent, xindex, xcount) {
        var buffer = this.applyOut(values, [], parent, xindex, xcount);
        // If only one token, return it as its uncoerced data type.
        // This will allow things like ObjectTemplate to use
        // formatters on non-string values.
        return buffer.length === 1 ? buffer[0] : buffer.join('');
    },
    applyOut: function(values, out, parent, xindex, xcount) {
        var me = this,
            compiler;
        if (!me.fn) {
            compiler = new Ext.util.XTemplateCompiler({
                useFormat: me.disableFormats !== true,
                definitions: me.definitions,
                strict: me.strict
            });
            me.fn = compiler.compile(me.html);
        }
        // xindex is 1-based, so 0 is impossible
        xindex = xindex || 1;
        // likewise, this tpl exists in the parent, so xcount==0 is not possible
        xcount = xcount || 1;
        if (me.strict) {
            me.fn(out, values, parent || me.emptyObj, xindex, xcount);
        } else {
            try {
                me.fn(out, values, parent || me.emptyObj, xindex, xcount);
            } catch (e) {
                Ext.log.warn('XTemplate evaluation exception: ' + e.message);
            }
        }
        return out;
    },
    /**
     * Does nothing. XTemplates are compiled automatically, so this function simply returns this.
     * @return {Ext.XTemplate} this
     */
    compile: function() {
        return this;
    },
    statics: {
        get: function(config, source, defaultTpl) {
            var ret = config;
            if (config == null) {
                if (source && defaultTpl) {
                    ret = this.getTpl(source, defaultTpl);
                }
            } else if ((config || config === '') && !config.isTemplate) {
                ret = new this(config);
            }
            return ret;
        },
        /**
         * Gets an `XTemplate` from an object (an instance of an {@link Ext#define}'d class).
         * Many times, templates are configured high in the class hierarchy and are to be
         * shared by all classes that derive from that base. To further complicate matters,
         * these templates are seldom actual instances but are rather configurations. For
         * example:
         *
         *      Ext.define('MyApp.Class', {
         *          extraCls: 'extra-class',
         *
         *          someTpl: [
         *              '<div class="{%this.emitClass(out)%}"></div>',
         *          {
         *              // Member fn - outputs the owing class's extra CSS class
         *              emitClass: function(out) {
         *                  out.push(this.owner.extraCls);
         *              }
         *          }]
         *      });
         *
         * The goal being to share that template definition with all instances and even
         * instances of derived classes, until `someTpl` is overridden. This method will
         * "upgrade" these configurations to be real `XTemplate` instances *in place* (to
         * avoid creating one instance per object).
         *
         * The resulting XTemplate will have an `owner` reference injected which refers back
         * to the owning object whether that is an object which has an *own instance*, or a
         * class prototype. Through this link, XTemplate member functions will be able to access
         * prototype properties of its owning class.
         *
         * @param {Object} instance The object from which to get the `XTemplate` (must be
         * an instance of an {@link Ext#define}'d class).
         * @param {String} name The name of the property by which to get the `XTemplate`.
         * @return {Ext.XTemplate} The `XTemplate` instance or null if not found.
         * @protected
         * @static
         */
        getTpl: function(instance, name) {
            var tpl = instance[name],
                // go for it! 99% of the time we will get it!
                owner;
            if (tpl) {
                // tpl is just a configuration (not an instance)
                if (!tpl.isTemplate) {
                    // create the template instance from the configuration:
                    tpl = Ext.XTemplate.get(tpl);
                }
                if (!tpl.owner) {
                    // and replace the reference with the new instance:
                    if (instance.hasOwnProperty(name)) {
                        // the tpl is on the instance
                        owner = instance;
                    } else {
                        // must be somewhere in the prototype chain
                        /* eslint-disable-next-line max-len, no-empty */
                        for (owner = instance.self.prototype; owner && !owner.hasOwnProperty(name); owner = owner.superclass) {}
                    }
                    owner[name] = tpl;
                    tpl.owner = owner;
                }
            }
            // else !tpl (no such tpl) or the tpl is an instance already... either way, tpl
            // is ready to return
            return tpl || null;
        }
    }
});

/**
 * This class is a base class for an event domain. In the context of MVC, an "event domain"
 * is one or more base classes that fire events to which a Controller wants to listen. A
 * controller listens to events by describing the selectors for events of interest to it.
 *
 * Matching selectors to the firer of an event is one key aspect that defines an event
 * domain. All event domain instances must provide a `match` method that tests selectors
 * against the event firer.
 *
 * When an event domain instance is created (typically as a `singleton`), its `type`
 * property is used to catalog the domain in the
 * {@link Ext.app.EventDomain#instances Ext.app.EventDomain.instances} map.
 *
 * There are five event domains provided by default:
 *
 * -   {@link Ext.app.domain.Component Component domain}. This is the primary event domain that
 * has been available since Ext JS MVC was introduced. This domain is defined as any class that
 * extends {@link Ext.Component}, where the selectors use
 * {@link Ext.ComponentQuery#query Ext.ComponentQuery}.
 * -   {@link Ext.app.domain.Global Global domain}. This domain provides Controllers with access
 * to events fired from {@link Ext.GlobalEvents} Observable instance. These events represent
 * the state of the application as a whole, and are always anonymous. Because of this, Global
 * domain does not provide selectors at all.
 * -   {@link Ext.app.domain.Controller Controller domain}. This domain includes all classes
 * that extend {@link Ext.app.Controller}. Events fired by Controllers will be available
 * within this domain; selectors are either Controller's {@link Ext.app.Controller#id id} or
 * '*' wildcard for any Controller.
 * -   {@link Ext.app.domain.Store Store domain}. This domain is for classes extending
 * {@link Ext.data.AbstractStore}. Selectors are either Store's
 * {@link Ext.data.AbstractStore#storeId storeId} or '*' wildcard for any Store.
 * -   {@link Ext.app.domain.Direct Direct domain}. This domain includes all classes that extend
 * {@link Ext.direct.Provider}. Selectors are either Provider's {@link Ext.direct.Provider#id id}
 * or '*' wildcard for any Provider. This domain is optional and will be loaded only if
 * {@link Ext.direct.Manager} singleton is required in your application.
 */
Ext.define('Ext.app.EventDomain', {
    requires: [
        'Ext.util.Event'
    ],
    statics: {
        /**
         * An object map containing `Ext.app.EventDomain` instances keyed by the value
         * of their `type` property.
         */
        instances: {}
    },
    /**
     * @cfg {String} idProperty Name of the identifier property for this event domain.
     */
    isEventDomain: true,
    isInstance: false,
    constructor: function() {
        var me = this;
        if (!me.isInstance) {
            Ext.app.EventDomain.instances[me.type] = me;
        }
        me.bus = {};
        me.monitoredClasses = [];
    },
    /**
     * This method dispatches an event fired by an object monitored by this domain. This
     * is not called directly but is called by interceptors injected by the `monitor` method.
     * 
     * @param {Object} target The firer of the event.
     * @param {String} ev The event being fired.
     * @param {Array} args The arguments for the event. This array **does not** include the
     * event name. That has already been sliced off because this class intercepts the
     * {@link Ext.util.Observable#fireEventArgs fireEventArgs} method which takes an array
     * as the event's argument list.
     *
     * @return {Boolean} `false` if any listener returned `false`, otherwise `true`.
     *
     * @private
     */
    dispatch: function(target, ev, args) {
        ev = Ext.canonicalEventName(ev);
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            bus = me.bus,
            selectors = bus[ev],
            selector, controllers, id, info, events, len, i, event;
        if (!selectors) {
            return true;
        }
        // Loop over all the selectors that are bound to this event
        for (selector in selectors) {
            // Check if the target matches the selector, note that we will only have
            // me.controller when we're an instance of a domain.View attached to a view controller.
            if (selectors.hasOwnProperty(selector) && me.match(target, selector, me.controller)) {
                // Loop over all the controllers that are bound to this selector
                controllers = selectors[selector];
                for (id in controllers) {
                    if (controllers.hasOwnProperty(id)) {
                        info = controllers[id];
                        if (info.controller.isActive()) {
                            // Loop over all the events that are bound to this selector
                            // on the current controller
                            events = info.list;
                            len = events.length;
                            for (i = 0; i < len; i++) {
                                event = events[i];
                                // Fire the event!
                                if (event.fire.apply(event, args) === false) {
                                    return false;
                                }
                            }
                        }
                    }
                }
            }
        }
        return true;
    },
    /**
     * This method adds listeners on behalf of a controller. This method is passed an
     * object that is keyed by selectors. The value of these is also an object but now
     * keyed by event name. For example:
     * 
     *      domain.listen({
     *          'some[selector]': {
     *              click: function() { ... }
     *          },
     *          
     *          'other selector': {
     *              change: {
     *                  fn: function() { ... },
     *                  delay: 10
     *              }
     *          }
     *      
     *      }, controller);
     * 
     * @param {Object} selectors Config object containing selectors and listeners.
     * @param {Ext.app.BaseController} [controller] (private)
     * @private
     */
    listen: function(selectors, controller) {
        var me = this,
            bus = me.bus,
            idProperty = me.idProperty,
            monitoredClasses = me.monitoredClasses,
            monitoredClassesCount = monitoredClasses.length,
            controllerId = controller.getId(),
            isComponentDomain = (me.type === 'component'),
            refMap = isComponentDomain ? controller.getRefMap() : null,
            i, tree, info, selector, options, listener, scope, event, listeners, ev, classHasListeners;
        for (selector in selectors) {
            listeners = selectors[selector];
            if (isComponentDomain) {
                // This allows ref names to be used as selectors, e.g.
                //     refs: {
                //         nav: '#navigationList
                //     },
                //     control: {
                //         nav: {
                //             itemclick: 'onNavClick'
                //         }
                //     }
                //
                // We process this here instead of in the controller so that we don't
                // have to do multiple loops over the selectors
                selector = refMap[selector] || selector;
            }
            if (listeners) {
                if (idProperty) {
                    if (!/^[*#]/.test(selector)) {
                        Ext.raise('Selectors containing id should begin with #');
                    }
                    selector = selector === '*' ? selector : selector.substring(1);
                }
                for (ev in listeners) {
                    options = null;
                    listener = listeners[ev];
                    scope = controller;
                    ev = Ext.canonicalEventName(ev);
                    event = new Ext.util.Event(controller, ev);
                    // Normalize the listener
                    if (Ext.isObject(listener)) {
                        options = listener;
                        listener = options.fn;
                        scope = options.scope || controller;
                        delete options.fn;
                        delete options.scope;
                    }
                    if ((!options || !options.scope) && typeof listener === 'string') {
                        // Allow this lookup to be dynamic in debug mode.
                        // Super useful for testing!
                        if (!scope[listener]) {
                            Ext.raise('Cannot resolve "' + listener + '" on controller.');
                        }
                        scope = null;
                    } else if (typeof listener === 'string') {
                        listener = scope[listener];
                    }
                    event.addListener(listener, scope, options);
                    for (i = 0; i < monitoredClassesCount; ++i) {
                        classHasListeners = monitoredClasses[i].hasListeners;
                        if (classHasListeners) {
                            // Ext.mixin.Observable doesn't have hasListeners at class level
                            classHasListeners._incr_(ev);
                        }
                    }
                    // Create the bus tree if it is not there yet
                    tree = bus[ev] || (bus[ev] = {});
                    tree = tree[selector] || (tree[selector] = {});
                    info = tree[controllerId] || (tree[controllerId] = {
                        controller: controller,
                        list: []
                    });
                    // Push our listener in our bus
                    info.list.push(event);
                }
            }
        }
    },
    /**
     * This method matches the firer of the event (the `target`) to the given `selector`.
     * Default matching is very simple: a match is true when selector equals target's
     * {@link #cfg-idProperty idProperty}, or when selector is '*' wildcard to match any
     * target.
     * 
     * @param {Object} target The firer of the event.
     * @param {String} selector The selector to which to match the `target`.
     *
     * @return {Boolean} `true` if the `target` matches the `selector`.
     *
     * @protected
     */
    match: function(target, selector) {
        var idProperty = this.idProperty;
        if (idProperty) {
            return selector === '*' || target[idProperty] === selector;
        }
        return false;
    },
    /**
     * This method is called by the derived class to monitor `fireEvent` calls. Any call
     * to `fireEvent` on the target Observable will be intercepted and dispatched to any
     * listening Controllers. Assuming the original `fireEvent` method does not return
     * `false`, the event is passed to the `dispatch` method of this object.
     * 
     * This is typically called in the `constructor` of derived classes.
     * 
     * @param {Ext.Class} observable The Observable to monitor for events.
     *
     * @protected
     */
    monitor: function(observable) {
        var domain = this,
            prototype = observable.isInstance ? observable : observable.prototype,
            doFireEvent = prototype.doFireEvent;
        domain.monitoredClasses.push(observable);
        prototype.doFireEvent = function(ev, args) {
            var me = this,
                ret;
            ret = doFireEvent.apply(me, arguments);
            // Observable can be destroyed in the event handler above,
            // in which case we can't proceed with dispatching domain event.
            if (ret !== false && !me.destroyed && !me.isSuspended(ev)) {
                ret = domain.dispatch(me, ev, args);
            }
            return ret;
        };
    },
    /**
     * Removes all of a controller's attached listeners.
     *
     * @param {String} controllerId The id of the controller.
     *
     * @private
     */
    unlisten: function(controllerId) {
        var bus = this.bus,
            id = controllerId,
            monitoredClasses = this.monitoredClasses,
            monitoredClassesCount = monitoredClasses.length,
            controllers, ev, events, len, item, selector, selectors, i, j, info, classHasListeners;
        if (controllerId.isController) {
            id = controllerId.getId();
        }
        for (ev in bus) {
            ev = Ext.canonicalEventName(ev);
            if (bus.hasOwnProperty(ev) && (selectors = bus[ev])) {
                for (selector in selectors) {
                    controllers = selectors[selector];
                    info = controllers[id];
                    if (info) {
                        events = info.list;
                        if (events) {
                            for (i = 0 , len = events.length; i < len; ++i) {
                                item = events[i];
                                item.clearListeners();
                                for (j = 0; j < monitoredClassesCount; ++j) {
                                    classHasListeners = monitoredClasses[j].hasListeners;
                                    if (classHasListeners) {
                                        // Ext.mixin.Observable doesn't have hasListeners
                                        // at class level
                                        classHasListeners._decr_(item.name);
                                    }
                                }
                            }
                            delete controllers[id];
                        }
                    }
                }
            }
        }
    },
    destroy: function() {
        this.monitoredClasses = this.bus = null;
        this.callParent();
    }
});

/**
 * This class implements the component event domain. All classes extending from
 * {@link Ext.Component} are included in this domain. The matching criteria uses
 * {@link Ext.ComponentQuery}.
 * 
 * @private
 */
Ext.define('Ext.app.domain.Component', {
    extend: 'Ext.app.EventDomain',
    singleton: true,
    requires: [
        'Ext.Widget'
    ],
    type: 'component',
    constructor: function() {
        this.callParent();
        this.monitor(Ext.Widget);
    },
    dispatch: function(target, ev, args) {
        var controller = target.lookupController(false),
            // don't skip target
            domain, view;
        while (controller) {
            domain = controller.compDomain;
            if (domain) {
                if (domain.dispatch(target, ev, args) === false) {
                    return false;
                }
            }
            view = controller.getView();
            controller = view ? view.lookupController(true) : null;
        }
        return this.callParent([
            target,
            ev,
            args
        ]);
    },
    match: function(target, selector) {
        return target.is(selector);
    }
});

/*
 * The dirty implementation in this class is quite naive. The reasoning for this is that the dirty
 * state will only be used in very specific circumstances, specifically, after the render process
 * has begun but the component is not yet rendered to the DOM. As such, we want it to perform
 * as quickly as possible so it's not as fully featured as you may expect.
 */
/**
 * Manages certain element-like data prior to rendering. These values are passed
 * on to the render process. This is currently used to manage the "class" and "style" attributes
 * of a component's primary el as well as the bodyEl of panels. This allows things like
 * addBodyCls in Panel to share logic with addCls in Component.
 * @private
 */
Ext.define('Ext.util.ProtoElement', function() {
    var splitWords = Ext.String.splitWords,
        toMap = Ext.Array.toMap;
    return {
        isProtoEl: true,
        /**
         * The property name for the className on the data object passed to {@link #writeTo}.
         */
        clsProp: 'cls',
        /**
         * The property name for the style on the data object passed to {@link #writeTo}.
         */
        styleProp: 'style',
        /**
         * The property name for the removed classes on the data object passed to {@link #writeTo}.
         */
        removedProp: 'removed',
        /**
         * True if the style must be converted to text during {@link #writeTo}. When used to
         * populate tpl data, this will be true. When used to populate {@link Ext.dom.Helper}
         * specs, this will be false (the default).
         */
        styleIsText: false,
        constructor: function(config) {
            var me = this,
                cls, style;
            if (config) {
                Ext.apply(me, config);
                cls = me.cls;
                style = me.style;
                delete me.cls;
            }
            me.classList = cls ? splitWords(cls) : [];
            me.classMap = cls ? toMap(me.classList) : {};
            if (style) {
                if (typeof style === 'string') {
                    me.style = Ext.Element.parseStyles(style);
                } else if (Ext.isFunction(style)) {
                    me.styleFn = style;
                    delete me.style;
                } else {
                    me.style = Ext.apply({}, style);
                }
            }
        },
        // don't edit the given object
        /**
         * Indicates that the current state of the object has been flushed to the DOM, so we need
         * to track any subsequent changes
         */
        flush: function() {
            this.flushClassList = [];
            this.removedClasses = {};
            // clear the style, it will be recreated if we add anything new
            delete this.style;
            delete this.unselectableAttr;
        },
        /**
         * Adds class to the element.
         * @param {String} cls One or more classnames separated with spaces.
         * @return {Ext.util.ProtoElement} this
         */
        addCls: function(cls) {
            if (!cls) {
                return this;
            }
            // eslint-disable-next-line vars-on-top
            var me = this,
                add = (typeof cls === 'string') ? splitWords(cls) : cls,
                length = add.length,
                list = me.classList,
                map = me.classMap,
                flushList = me.flushClassList,
                i = 0,
                c;
            for (; i < length; ++i) {
                c = add[i];
                if (!map[c]) {
                    map[c] = true;
                    list.push(c);
                    if (flushList) {
                        flushList.push(c);
                        delete me.removedClasses[c];
                    }
                }
            }
            return me;
        },
        /**
         * True if the element has given class.
         * @param {String} cls
         * @return {Boolean}
         */
        hasCls: function(cls) {
            return cls in this.classMap;
        },
        /**
         * Removes class from the element.
         * @param {String} cls One or more classnames separated with spaces.
         * @return {Ext.util.ProtoElement} this
         */
        removeCls: function(cls) {
            var me = this,
                list = me.classList,
                newList = (me.classList = []),
                remove = toMap(splitWords(cls)),
                length = list.length,
                map = me.classMap,
                removedClasses = me.removedClasses,
                i, c;
            for (i = 0; i < length; ++i) {
                c = list[i];
                if (remove[c]) {
                    if (removedClasses) {
                        if (map[c]) {
                            removedClasses[c] = true;
                            Ext.Array.remove(me.flushClassList, c);
                        }
                    }
                    delete map[c];
                } else {
                    newList.push(c);
                }
            }
            return me;
        },
        /**
         * @method setStyle
         * @inheritdoc Ext.dom.Element#method-setStyle
         * @return {Ext.util.ProtoElement} this
         */
        setStyle: function(prop, value) {
            var me = this,
                style = me.style || (me.style = {});
            if (typeof prop === 'string') {
                if (arguments.length === 1) {
                    me.setStyle(Ext.Element.parseStyles(prop));
                } else {
                    style[prop] = value;
                }
            } else {
                Ext.apply(style, prop);
            }
            return me;
        },
        unselectable: function() {
            // See Ext.dom.Element.unselectable for an explanation of what is required
            // to make an element unselectable
            this.addCls(Ext.dom.Element.unselectableCls);
            if (Ext.isOpera) {
                this.unselectableAttr = true;
            }
        },
        /**
         * Writes style and class properties to given object.
         * Styles will be written to {@link #styleProp} and class names to {@link #clsProp}.
         * @param {Object} to
         * @return {Object} to
         */
        writeTo: function(to) {
            var me = this,
                classList = me.flushClassList || me.classList,
                removedClasses = me.removedClasses,
                style;
            if (me.styleFn) {
                style = Ext.apply({}, me.styleFn());
                Ext.apply(style, me.style);
            } else {
                style = me.style;
            }
            to[me.clsProp] = classList.join(' ');
            if (style) {
                to[me.styleProp] = me.styleIsText ? Ext.DomHelper.generateStyles(style, null, true) : style;
            }
            if (removedClasses) {
                removedClasses = Ext.Object.getKeys(removedClasses);
                if (removedClasses.length) {
                    to[me.removedProp] = removedClasses.join(' ');
                }
            }
            if (me.unselectableAttr) {
                to.unselectable = 'on';
            }
            return to;
        }
    };
});

/**
 * This class encapsulates a _collection_ of DOM elements, providing methods to filter members,
 * or to perform collective actions upon the whole set.
 *
 * Although they are not listed, this class supports all of the methods of {@link Ext.dom.Element}.
 * The methods from these classes will be performed on all the elements in this collection.
 *
 * All methods return _this_ and can be chained.
 *
 * Usage:
 *
 *      var els = Ext.select("#some-el div.some-class", true);
 *      // or select directly from an existing element
 *      var el = Ext.get('some-el');
 *      el.select('div.some-class', true);
 *
 *      els.setWidth(100); // all elements become 100 width
 *      els.hide(true); // all elements fade out and hide
 *      // or
 *      els.setWidth(100).hide(true);
 */
Ext.define('Ext.dom.CompositeElement', {
    alternateClassName: 'Ext.CompositeElement',
    extend: 'Ext.dom.CompositeElementLite',
    isLite: false,
    /**
     * @private
     */
    getElement: function(el) {
        // In this case just return it, since we already have a reference to it
        return el;
    },
    /**
     * @private
     */
    transformElement: function(el) {
        return Ext.get(el);
    }
});

/**
 * Creates plugin instances.
 *
 * A plugin may be specified simply as a *config object* as long as the correct `ptype`
 * is specified:
 *
 *     {
 *         ptype: 'gridviewdragdrop',
 *         dragText: 'Drag and drop to reorganize'
 *     }
 *
 * Or just use the ptype on its own:
 *
 *     'gridviewdragdrop'
 *
 * Alternatively you can instantiate the plugin with Ext.create:
 *
 *     Ext.create('Ext.grid.plugin.DragDrop', {
 *         dragText: 'Drag and drop to reorganize'
 *     })
 * @private
 */
Ext.define('Ext.plugin.Manager', {
    singleton: true,
    alternateClassName: [
        'Ext.PluginManager',
        'Ext.PluginMgr'
    ],
    typeName: 'ptype',
    /**
     * Creates a new Plugin from the specified config object using the config object's ptype
     * to determine the class to instantiate.
     * @param {Object} config A configuration object for the Plugin you wish to create.
     * @param {Function} defaultType (optional) The constructor to provide the default Plugin type
     * if the config object does not contain a `ptype`. (Optional if the config contains a `ptype`).
     * @param {String} host
     * @return {Ext.Component} The newly instantiated Plugin.
     */
    create: function(config, defaultType, host) {
        var result, type;
        if (config.init) {
            result = config;
        } else {
            // Inject the host into the config is we know the host
            if (host) {
                config = Ext.apply({}, config);
                // copy since we are going to modify
                config.cmp = host;
            } else // Grab the host ref if it was configured in
            {
                host = config.cmp;
            }
            if (config.xclass) {
                result = Ext.create(config);
            } else {
                // Lookup the class from the ptype and instantiate unless its a singleton
                type = 'plugin.' + (config.ptype || defaultType);
                result = Ext.ClassManager.instantiateByAlias(type, config);
            }
        }
        // If we come out with a non-null plugin, ensure that any setCmp is called once.
        if (result && host && result.setCmp && !result.setCmpCalled) {
            result.setCmp(host);
            result.setCmpCalled = true;
        }
        return result;
    }
});

/**
 * Utility class for manipulating CSS rules
 * @singleton
 */
Ext.define('Ext.util.CSS', function() {
    var CSS,
        rules = null,
        doc = document,
        camelRe = /(-[a-z])/gi,
        camelFn = function(m, a) {
            return a.charAt(1).toUpperCase();
        };
    return {
        singleton: true,
        rules: rules,
        initialized: false,
        /**
         * @private
         */
        constructor: function() {
            // Cache a reference to the singleton
            CSS = this;
        },
        /**
         * Creates a stylesheet from a text blob of rules.
         * These rules will be wrapped in a STYLE tag and appended to the HEAD of the document.
         * @param {String} cssText The text containing the css rules
         * @param {String} id An id to add to the stylesheet for later removal
         * @return {CSSStyleSheet}
         */
        createStyleSheet: function(cssText, id) {
            var ss,
                head = doc.getElementsByTagName('head')[0],
                styleEl = doc.createElement('style');
            styleEl.setAttribute('type', 'text/css');
            if (id) {
                styleEl.setAttribute('id', id);
            }
            // Feature detect old IE
            ss = styleEl.styleSheet;
            if (ss) {
                head.appendChild(styleEl);
                ss.cssText = cssText;
            } else {
                styleEl.appendChild(doc.createTextNode(cssText));
                head.appendChild(styleEl);
                ss = styleEl.sheet;
            }
            CSS.cacheStyleSheet(ss);
            return ss;
        },
        /**
         * Removes a style or link tag by id
         * @param {String/CSSStyleSheet} stylesheet The id of the style tag, or the CSSStyleSheet
         * reference to remove
         */
        removeStyleSheet: function(stylesheet) {
            var styleEl = (typeof stylesheet === 'string') ? doc.getElementById(stylesheet) : stylesheet.ownerNode;
            if (styleEl) {
                styleEl.parentNode.removeChild(styleEl);
            }
        },
        /**
         * Dynamically swaps an existing stylesheet reference for a new one
         * @param {String} id The id of an existing link tag to remove
         * @param {String} url The href of the new stylesheet to include
         */
        swapStyleSheet: function(id, url) {
            var ss;
            CSS.removeStyleSheet(id);
            ss = doc.createElement("link");
            ss.setAttribute("rel", "stylesheet");
            ss.setAttribute("type", "text/css");
            ss.setAttribute("id", id);
            ss.setAttribute("href", url);
            doc.getElementsByTagName("head")[0].appendChild(ss);
        },
        /**
         * @private
         */
        cacheStyleSheet: function(ss) {
            if (!rules) {
                rules = CSS.rules = {};
            }
            try {
                // try catch for cross domain access issue
                // eslint-disable-next-line vars-on-top
                var ssRules = ss.cssRules || ss.rules,
                    i = ssRules.length - 1,
                    imports = ss.imports,
                    len = imports ? imports.length : 0,
                    rule, j;
                // Old IE has a different way of handling imports
                for (j = 0; j < len; ++j) {
                    CSS.cacheStyleSheet(imports[j]);
                }
                for (; i >= 0; --i) {
                    rule = ssRules[i];
                    // If it's an @import rule, import its stylesheet
                    if (rule.styleSheet) {
                        CSS.cacheStyleSheet(rule.styleSheet);
                    }
                    CSS.cacheRule(rule, ss);
                }
            } catch (e) {}
        },
        // ignore
        cacheRule: function(cssRule, styleSheet) {
            var selectorText, selectorCount, j;
            // If it's an @import rule, import its stylesheet
            if (cssRule.styleSheet) {
                return CSS.cacheStyleSheet(cssRule.styleSheet);
            }
            selectorText = cssRule.selectorText;
            if (selectorText) {
                // Split in case there are multiple, comma-delimited selectors
                selectorText = selectorText.split(',');
                selectorCount = selectorText.length;
                for (j = 0; j < selectorCount; j++) {
                    // IE<8 does not keep a reference to parentStyleSheet in the rule, so we
                    // must cache an object like this until IE<8 is deprecated.
                    rules[Ext.String.trim(selectorText[j]).toLowerCase()] = {
                        parentStyleSheet: styleSheet,
                        cssRule: cssRule
                    };
                }
            }
        },
        /**
         * Gets all css rules for the document
         * @param {Boolean} refreshCache true to refresh the internal cache
         * @return {Object} An object (hash) of rules indexed by selector
         */
        getRules: function(refreshCache) {
            var result = {},
                selector;
            if (rules === null || refreshCache) {
                CSS.refreshCache();
            }
            for (selector in rules) {
                result[selector] = rules[selector].cssRule;
            }
            return result;
        },
        /**
         * Refresh the rule cache if you have dynamically added stylesheets
         * @return {Object} An object (hash) of rules indexed by selector
         */
        refreshCache: function() {
            var ds = doc.styleSheets,
                i = 0,
                len = ds.length;
            rules = CSS.rules = {};
            for (; i < len; i++) {
                try {
                    if (!ds[i].disabled) {
                        CSS.cacheStyleSheet(ds[i]);
                    }
                } catch (e) {}
            }
        },
        // ignore
        /**
         * Gets an an individual CSS rule by selector(s)
         * @param {String/String[]} selector The CSS selector or an array of selectors to try.
         * The first selector that is found is returned.
         * @param {Boolean} refreshCache true to refresh the internal cache if you have recently
         * updated any rules or added styles dynamically
         * @param rawCache The CSS rule or null if one is not found
         */
        getRule: function(selector, refreshCache, rawCache) {
            var i, result;
            if (!rules || refreshCache) {
                CSS.refreshCache();
            }
            if (!Ext.isArray(selector)) {
                result = rules[selector.toLowerCase()];
                if (result && !rawCache) {
                    result = result.cssRule;
                }
                return result || null;
            }
            for (i = 0; i < selector.length; i++) {
                if (rules[selector[i]]) {
                    return rawCache ? rules[selector[i].toLowerCase()] : rules[selector[i].toLowerCase()].cssRule;
                }
            }
            return null;
        },
        /**
         * Creates a rule.
         * @param {CSSStyleSheet} styleSheet The StyleSheet to create the rule in as returned
         * from {@link #createStyleSheet}.
         * @param {String} selector The selector to target the rule.
         * @param {String} cssText The cssText specification e.g.
         * `"color:red;font-weight:bold;text-decoration:underline"`
         * @return {CSSStyleRule} The created rule
         */
        createRule: function(styleSheet, selector, cssText) {
            var result,
                ruleSet = styleSheet.cssRules || styleSheet.rules,
                index = ruleSet.length;
            if (styleSheet.insertRule) {
                styleSheet.insertRule(selector + ' {' + cssText + '}', index);
            } else {
                styleSheet.addRule(selector, cssText || ' ');
            }
            CSS.cacheRule(result = ruleSet[index], styleSheet);
            return result;
        },
        /**
         * Updates a rule property
         * @param {String/String[]} selector If it's an array it tries each selector until
         * it finds one. Stops immediately once one is found.
         * @param {String} property The css property or a cssText specification e.g.
         * `"color:red;font-weight:bold;text-decoration:underline"`
         * @param {String} value The new value for the property
         * @return {Boolean} true If a rule was found and updated
         */
        updateRule: function(selector, property, value) {
            var rule, i, styles;
            if (!Ext.isArray(selector)) {
                rule = CSS.getRule(selector);
                if (rule) {
                    // 2 arg form means cssText sent, so parse it and update each style
                    if (arguments.length === 2) {
                        styles = Ext.Element.parseStyles(property);
                        for (property in styles) {
                            rule.style[property.replace(camelRe, camelFn)] = styles[property];
                        }
                    } else {
                        rule.style[property.replace(camelRe, camelFn)] = value;
                    }
                    return true;
                }
            } else {
                for (i = 0; i < selector.length; i++) {
                    if (CSS.updateRule(selector[i], property, value)) {
                        return true;
                    }
                }
            }
            return false;
        },
        deleteRule: function(selector) {
            var rule = CSS.getRule(selector, false, true),
                styleSheet, index;
            if (rule) {
                styleSheet = rule.parentStyleSheet;
                index = Ext.Array.indexOf(styleSheet.cssRules || styleSheet.rules, rule.cssRule);
                if (styleSheet.deleteRule) {
                    styleSheet.deleteRule(index);
                } else {
                    styleSheet.removeRule(index);
                }
                delete rules[selector];
            }
        }
    };
});

/**
 * @private
 */
Ext.define('Ext.fx.easing.Abstract', {
    config: {
        startTime: 0,
        startValue: 0
    },
    isEasing: true,
    isEnded: false,
    constructor: function(config) {
        this.initConfig(config);
        return this;
    },
    applyStartTime: function(startTime) {
        if (!startTime) {
            startTime = Ext.Date.now();
        }
        return startTime;
    },
    updateStartTime: function(startTime) {
        this.reset();
    },
    reset: function() {
        this.isEnded = false;
    },
    getValue: Ext.emptyFn
});

/**
 * @private
 */
Ext.define('Ext.fx.easing.Linear', {
    extend: 'Ext.fx.easing.Abstract',
    alias: 'easing.linear',
    config: {
        duration: 0,
        endValue: 0
    },
    updateStartValue: function(startValue) {
        this.distance = this.getEndValue() - startValue;
    },
    updateEndValue: function(endValue) {
        this.distance = endValue - this.getStartValue();
    },
    getValue: function() {
        var deltaTime = Ext.Date.now() - this.getStartTime(),
            duration = this.getDuration();
        if (deltaTime > duration) {
            this.isEnded = true;
            return this.getEndValue();
        } else {
            return this.getStartValue() + ((deltaTime / duration) * this.distance);
        }
    }
});

/**
 * @private
 *
 * The abstract class. Sub-classes are expected, at the very least, to implement translation logics
 * inside the 'translate' method
 */
Ext.define('Ext.util.translatable.Abstract', {
    extend: 'Ext.Evented',
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    factoryConfig: {
        type: 'translatable',
        defaultType: 'csstransform'
    },
    requires: [
        'Ext.fx.easing.Linear'
    ],
    config: {
        easing: null,
        easingX: {
            duration: 300
        },
        easingY: {
            duration: 300
        },
        offsetX: 0,
        offsetY: 0
    },
    /**
     * @event animationstart
     * Fires whenever the animation is started
     * @param {Ext.util.translatable.Abstract} this
     * @param {Number} x The current translation on the x axis
     * @param {Number} y The current translation on the y axis
     */
    /**
     * @event animationframe
     * Fires for each animation frame
     * @param {Ext.util.translatable.Abstract} this
     * @param {Number} x The new translation on the x axis
     * @param {Number} y The new translation on the y axis
     */
    /**
     * @event animationend
     * Fires whenever the animation is ended
     * @param {Ext.util.translatable.Abstract} this
     * @param {Number} x The current translation on the x axis
     * @param {Number} y The current translation on the y axis
     */
    /**
     * @property {Number} x
     * @private
     * The last translated x value
     */
    x: 0,
    /**
     * @property {Number} y
     * @private
     * The last translated y value
     */
    y: 0,
    activeEasingX: null,
    activeEasingY: null,
    isAnimating: false,
    isTranslatable: true,
    constructor: function(config) {
        this.callParent([
            config
        ]);
        // this.position is simply an internal reusable object for GC purposes and should
        // not be accessed directly as it's values are not kept in sync.  always use
        // getPosition() to get the position
        this.position = {
            x: 0,
            y: 0
        };
    },
    factoryEasing: function(easing) {
        return Ext.factory(easing, Ext.fx.easing.Linear, null, 'easing');
    },
    applyEasing: function(easing) {
        if (!this.getEasingX()) {
            this.setEasingX(this.factoryEasing(easing));
        }
        if (!this.getEasingY()) {
            this.setEasingY(this.factoryEasing(easing));
        }
    },
    applyEasingX: function(easing) {
        return this.factoryEasing(easing);
    },
    applyEasingY: function(easing) {
        return this.factoryEasing(easing);
    },
    updateOffsetX: function() {
        var me = this;
        if (!me.isConfiguring && !me.isAnimating) {
            me.translateXY(me.x, me.y);
        }
    },
    updateOffsetY: function() {
        var me = this;
        if (!me.isConfiguring && !me.isAnimating) {
            me.translateXY(me.x, me.y);
        }
    },
    translate: function(x, y, animation) {
        var me = this;
        if (animation) {
            return me.translateAnimated(x, y, animation);
        }
        if (me.isAnimating) {
            me.stopAnimation();
        }
        if (!isNaN(x) && typeof x === 'number') {
            me.x = x;
        } else {
            x = me.x;
        }
        if (!isNaN(y) && typeof y === 'number') {
            me.y = y;
        } else {
            y = me.y;
        }
        me.translateXY(x, y);
    },
    translateXY: function(x, y) {
        var me = this;
        if (!me.destroyed) {
            me.doTranslate(x + me.getOffsetX(), y + me.getOffsetY());
            if (me.hasListeners.translate) {
                me.fireEvent('translate', me, x, y);
            }
        }
    },
    translateAxis: function(axis, value, animation) {
        var x, y;
        if (axis === 'x') {
            x = value;
        } else {
            y = value;
        }
        return this.translate(x, y, animation);
    },
    /**
     * Returns the translatable object's current position.
     * @return {Object} position An object with x and y properties
     */
    getPosition: function() {
        var me = this,
            position = me.position;
        position.x = -me.x;
        position.y = -me.y;
        return position;
    },
    animate: function(easingX, easingY) {
        var me = this;
        me.activeEasingX = easingX;
        me.activeEasingY = easingY;
        me.isAnimating = true;
        if (me.ownerCmp) {
            me.ownerCmp.isTranslating = true;
        }
        me.lastX = null;
        me.lastY = null;
        Ext.AnimationQueue.start(me.doAnimationFrame, me);
        me.fireEvent('animationstart', me, me.x, me.y);
        return me;
    },
    translateAnimated: function(x, y, animation) {
        var me = this,
            now, easing, easingX, easingY;
        if (!Ext.isObject(animation)) {
            animation = {};
        }
        if (me.isAnimating) {
            me.stopAnimation();
        }
        // Callback must be called in stopAnimation
        me.callback = animation.callback;
        me.callbackScope = animation.scope;
        now = Ext.Date.now();
        easing = animation.easing;
        easingX = (typeof x === 'number') ? (animation.easingX || easing || me.getEasingX() || true) : null;
        easingY = (typeof y === 'number') ? (animation.easingY || easing || me.getEasingY() || true) : null;
        if (easingX) {
            easingX = me.factoryEasing(easingX);
            easingX.setStartTime(now);
            easingX.setStartValue(me.x);
            easingX.setEndValue(x);
            if ('duration' in animation) {
                easingX.setDuration(animation.duration);
            }
        }
        if (easingY) {
            easingY = me.factoryEasing(easingY);
            easingY.setStartTime(now);
            easingY.setStartValue(me.y);
            easingY.setEndValue(y);
            if ('duration' in animation) {
                easingY.setDuration(animation.duration);
            }
        }
        return me.animate(easingX, easingY);
    },
    doAnimationFrame: function() {
        var me = this,
            easingX = me.activeEasingX,
            easingY = me.activeEasingY,
            now = Date.now(),
            x, y;
        if (!me.isAnimating) {
            return;
        }
        me.lastRun = now;
        if (easingX === null && easingY === null) {
            me.stopAnimation();
            return;
        }
        if (easingX !== null) {
            me.x = x = Math.round(easingX.getValue());
            if (easingX.isEnded) {
                me.activeEasingX = null;
                me.fireEvent('axisanimationend', me, 'x', x);
            }
        } else {
            x = me.x;
        }
        if (easingY !== null) {
            me.y = y = Math.round(easingY.getValue());
            if (easingY.isEnded) {
                me.activeEasingY = null;
                me.fireEvent('axisanimationend', me, 'y', y);
            }
        } else {
            y = me.y;
        }
        if (me.lastX !== x || me.lastY !== y) {
            me.translateXY(x, y);
            me.lastX = x;
            me.lastY = y;
        }
        me.fireEvent('animationframe', me, x, y);
    },
    stopAnimation: function() {
        var me = this;
        if (!me.isAnimating) {
            return;
        }
        me.activeEasingX = null;
        me.activeEasingY = null;
        me.isAnimating = false;
        if (me.ownerCmp) {
            me.ownerCmp.isTranslating = false;
        }
        Ext.AnimationQueue.stop(me.doAnimationFrame, me);
        me.fireEvent('animationend', me, me.x, me.y);
        if (me.callback) {
            me.callback.call(me.callbackScope);
            me.callback = null;
        }
    },
    refresh: function() {
        this.translate(this.x, this.y);
    },
    resolveListenerScope: function() {
        var ownerCmp = this.ownerCmp,
            a = arguments;
        if (ownerCmp) {
            return ownerCmp.resolveListenerScope.apply(ownerCmp, a);
        }
        return this.callParent(a);
    },
    destroy: function() {
        var me = this;
        me.destroying = true;
        if (me.isAnimating) {
            me.stopAnimation();
        }
        me.callParent();
        // This just makes it hard to ask "was destroy() called?":
        // me.destroying = false; // removed in 7.0
        me.destroyed = true;
    }
});

/**
 * @private
 */
Ext.define('Ext.util.translatable.Dom', {
    extend: 'Ext.util.translatable.Abstract',
    alias: 'translatable.dom',
    // also configures Factoryable
    config: {
        element: null
    },
    applyElement: function(element) {
        if (!element) {
            return;
        }
        return Ext.get(element);
    },
    updateElement: function() {
        this.refresh();
    },
    translateXY: function(x, y) {
        var element = this.getElement();
        if (element && !element.destroyed) {
            this.callParent([
                x,
                y
            ]);
        }
    }
});

/**
 * @private
 *
 * Scroll position implementation
 */
Ext.define('Ext.util.translatable.ScrollPosition', {
    extend: 'Ext.util.translatable.Dom',
    alias: 'translatable.scrollposition',
    // also configures Factoryable
    constructor: function(config) {
        if (config && config.element) {
            this.x = config.element.getScrollLeft();
            this.y = config.element.getScrollTop();
        }
        this.callParent([
            config
        ]);
    },
    translateAnimated: function() {
        var element = this.getElement();
        this.x = element.getScrollLeft();
        this.y = element.getScrollTop();
        this.callParent(arguments);
    },
    doTranslate: function(x, y) {
        var element = this.getElement();
        element.setScrollLeft(Math.round(x));
        element.setScrollTop(Math.round(y));
    },
    getPosition: function() {
        var me = this,
            position = me.position,
            element = me.getElement();
        position.x = element.getScrollLeft();
        position.y = element.getScrollTop();
        return position;
    }
});

/**
 * Ext.scroll.Scroller allows any element to have scrollable content, both on desktop and
 * touch-screen devices, and defines a set of useful methods for manipulating the scroll
 * position and controlling the scrolling behavior.
 */
Ext.define('Ext.scroll.Scroller', {
    extend: 'Ext.Evented',
    alias: 'scroller.scroller',
    alternateClassName: 'Ext.scroll.NativeScroller',
    mixins: [
        'Ext.mixin.Factoryable',
        'Ext.mixin.Bufferable'
    ],
    requires: [
        'Ext.util.CSS',
        'Ext.util.translatable.ScrollPosition',
        'Ext.Deferred'
    ],
    factoryConfig: {
        defaultType: 'scroller'
    },
    bufferableMethods: {
        onDomScrollEnd: 100
    },
    isScroller: true,
    /**
     * @event refresh
     * Fires whenever the Scroller is refreshed.
     * @param {Ext.scroll.Scroller} this
     */
    /**
     * @event scrollstart
     * Fires whenever the scrolling is started.
     * @param {Ext.scroll.Scroller} this
     * @param {Number} x The current x position.
     * @param {Number} y The current y position.
     */
    /**
     * @event scrollend
     * Fires whenever the scrolling is ended.
     * @param {Ext.scroll.Scroller} this
     * @param {Number} x The current x position.
     * @param {Number} y The current y position.
     * @param {Number} deltaX The change in x value.
     * @param {Number} deltaY The change in y value.
     */
    /**
     * @event scroll
     * Fires whenever the Scroller is scrolled.
     * @param {Ext.scroll.Scroller} this
     * @param {Number} x The new x position.
     * @param {Number} y The new y position.
     * @param {Number} deltaX The change in x value.
     * @param {Number} deltaY The change in y value.
     */
    config: {
        /**
         * @cfg {'auto'/'vertical'/'horizontal'/'both'} [direction='auto']
         * @deprecated 5.1.0 use {@link #x} and {@link #y} instead
         */
        direction: undefined,
        // undefined because we need the updater to always run
        /**
         * @cfg {String/HTMLElement/Ext.dom.Element}
         * The element to make scrollable.
         */
        element: undefined,
        /**
         * @cfg {Boolean} [scrollbars=true]
         * `false` to hide scrollbars on browsers where it is possible via CSS,
         * Currently Webkit, Chrome, and IE10+
         * @private
         */
        scrollbars: null,
        /**
         * @cfg {String} A CSS selector that identifies items inside this scroller that
         * should be snapped into position when user scrolling ends.  By default the items
         * top/left will be aligned with the top/left of the container.  This alignment
         * can be changed using {@link #snapOffset}
         *
         * This api is highly experimental as it is based on bleeding-edge CSS implementations
         * that may change in the near future.  Do not rely on it in your applications.
         *
         * @private
         */
        snapSelector: null,
        /**
         * @cfg {Object} An object with x and y properties for offsetting the currently
         * snapped item from the top/left of the container.
         *
         * This api is highly experimental as it is based on bleeding-edge CSS implementations
         * that may change in the near future.  Do not rely on it in your applications.
         *
         * @private
         */
        snapOffset: null,
        /**
         * @cfg {Object} an object with x and y properties that specifies the size of the
         * snap points on the x and y axes.  For IE10+/Edge only, since those browsers do
         * not support the newer CSS properties for snapping to element boundaries.
         *
         * This config is experimental and may be removed in a future version of the framework.
         *
         * @private
         */
        msSnapInterval: null,
        /**
         * @cfg {Boolean/String}
         * - `true` or `'auto'` to enable horizontal auto-scrolling. In auto-scrolling mode
         * scrolling is only enabled when the {@link #element} has overflowing content.
         * - `false` to disable horizontal scrolling
         * - `'scroll'` to always enable horizontal scrolling regardless of content size.
         */
        x: true,
        /**
         * @cfg {Boolean/String}
         * - `true` or `'auto'` to enable vertical auto-scrolling. In auto-scrolling mode
         * scrolling is only enabled when the {@link #element} has overflowing content.
         * - `false` to disable vertical scrolling
         * - `'scroll'` to always enable vertical scrolling regardless of content size.
         */
        y: true,
        /**
         * @cfg {Ext.dom.Element} scrollElement
         * The element to read the scrollTop/scrollLeft from. This is used to
         * work around cross browser issues where WebKit/Blink require reading/writing
         * scrollTop/scrollLeft on the document.body, even if the documentElement is
         * the thing overflowing. In future this can be removed once document.scrollingElement
         * becomes a standard across all supported browsers.
         *
         * Note that scroll(Width/Height) and other dimensions can be read from the
         * documentElement without issue.
         * @private
         */
        scrollElement: null,
        /**
         * @cfg {Object}
         * The size of the scrollable content expressed as an object with x and y properties
         * @private
         * @readonly
         */
        size: null,
        spacerXY: null,
        /**
         * @cfg {Object} touchAction for the scroller's {@link #element}.
         *
         * For more details see {@link Ext.dom.Element#setTouchAction}
         */
        touchAction: null
    },
    snappableCls: Ext.baseCSSPrefix + 'scroller-snappable',
    elementCls: Ext.baseCSSPrefix + 'scroller',
    spacerCls: Ext.baseCSSPrefix + 'scroller-spacer',
    noScrollbarsCls: Ext.baseCSSPrefix + 'no-scrollbars',
    statics: {
        /**
         * Creates and returns an appropriate Scroller instance for the current device.
         * @param {Object} config Configuration options for the Scroller
         * @param type
         * @return {Ext.scroll.Scroller}
         */
        create: function(config, type) {
            return Ext.Factory.scroller(config, type);
        },
        /**
         * Get the scrolling element for the document based on feature detection.
         * See: https://dev.opera.com/articles/fixing-the-scrolltop-bug/
         * 
         * @return {HTMLElement}
         *
         * @private
         */
        getScrollingElement: function() {
            var doc = document,
                standard = this.$standardScrollElement,
                el = doc.scrollingElement,
                iframe, frameDoc;
            // Normalize the scrollElement we need to read/write from
            // First attempt to detect the newer standard for viewport
            // scrolling
            if (el) {
                return el;
            }
            // The newer standard doesn't exist, let the scroller
            // decide via feature detection.
            if (standard === undefined) {
                iframe = document.createElement('iframe');
                iframe.style.height = '1px';
                document.body.appendChild(iframe);
                frameDoc = iframe.contentWindow.document;
                frameDoc.write('<!DOCTYPE html><div style="height:9999em">x</div>');
                frameDoc.close();
                standard = frameDoc.documentElement.scrollHeight > frameDoc.body.scrollHeight;
                iframe.parentNode.removeChild(iframe);
                this.$standardScrollElement = standard;
            }
            return standard ? doc.documentElement : doc.body;
        },
        /**
         * @private
         */
        initViewportScroller: function() {
            var scroller = Ext.getViewportScroller();
            if (!scroller.getElement()) {
                // if the viewport component has already claimed the viewport scroller
                // it will have already set its overflow element as the scroller element,
                // otherwise, the element is always the body.
                scroller.setElement(Ext.getBody());
            }
        }
    },
    constructor: function(config) {
        var me = this;
        me.position = {
            x: 0,
            y: 0
        };
        me.callParent([
            config
        ]);
    },
    destroy: function() {
        var me = this,
            partners = me._partners,
            key;
        Ext.undefer(me.restoreTimer);
        // Clear any overflow styles
        me.setX(Ext.emptyString);
        me.setY(Ext.emptyString);
        if (me._spacer) {
            me._spacer.destroy();
        }
        if (me.scrollListener) {
            me.scrollListener.destroy();
        }
        if (partners) {
            for (key in partners) {
                me.removePartner(partners[key].scroller);
            }
        }
        // Remove element listeners, this will cause scrollElement to
        // be cleared also.
        me.setElement(null);
        me._partners = me.component = null;
        if (me.translatable) {
            me.translatable.destroy();
            me.translatable = null;
        }
        me.removeSnapStylesheet();
        me.callParent();
    },
    /**
     * Adds a "partner" scroller.  Partner scrollers reflect each other's scroll position
     * at all times - if either scroller is scrolled, the scroll position of its partner
     * will be be automatically synchronized.
     *
     * A scroller may have multiple partners.
     *
     * @param {Ext.scroll.Scroller} partner
     * @param {String} [axis='both'] The axis to synchronize (`'x'`, '`y`', or '`both`')
     */
    addPartner: function(partner, axis) {
        var me = this,
            partners = me._partners || (me._partners = {}),
            otherPartners = partner._partners || (partner._partners = {});
        // Translate to boolean flags. {x:<boolean>,y:<boolean>}
        axis = me.axisConfigs[axis || 'both'];
        partners[partner.getId()] = {
            scroller: partner,
            axes: axis
        };
        otherPartners[me.getId()] = {
            scroller: me,
            axes: axis
        };
    },
    applyElement: function(element, oldElement) {
        var me = this;
        if (oldElement && me.scrollListener) {
            me.scrollListener.destroy();
        }
        if (element) {
            if (typeof element === 'string' && !Ext.get(element)) {
                Ext.raise("Cannot create Ext.scroll.Scroller instance. " + "Element with id '" + element + "' not found.");
            }
            element = Ext.get(element);
        }
        return element;
    },
    applySize: function(size, oldSize) {
        var x, y;
        if (size === null || typeof size === 'number') {
            x = y = size;
        } else if (size) {
            x = size.x;
            y = size.y;
        }
        if (x === null) {
            x = 0;
        } else if (x === undefined) {
            x = (oldSize ? oldSize.x : 0);
        }
        if (y === null) {
            y = 0;
        } else if (y === undefined) {
            y = (oldSize ? oldSize.y : 0);
        }
        if (!oldSize || x !== oldSize.x || y !== oldSize.y) {
            return {
                x: x,
                y: y
            };
        }
    },
    /**
     * Gets the `clientWidth` and `clientHeight` of the {@link #element} for this scroller.
     * @return {Object} An object with `x` and `y` properties.
     */
    getClientSize: function() {
        var dom = this.getElement().dom;
        return {
            x: dom.clientWidth,
            y: dom.clientHeight
        };
    },
    /**
     * Returns the amount of space consumed by scrollbars in the DOM
     * @return {Object} size An object containing the scrollbar sizes.
     * @return {Number} return.width The width of the vertical scrollbar.
     * @return {Number} return.height The height of the horizontal scrollbar.
     */
    getScrollbarSize: function() {
        var me = this,
            width = 0,
            height = 0,
            element = me.getElement(),
            dom, x, y, hasXScroll, hasYScroll, scrollbarSize;
        if (element && !element.destroyed) {
            x = me.getX();
            y = me.getY();
            dom = element.dom;
            if (x || y) {
                scrollbarSize = Ext.scrollbar.size();
            }
            if (x === 'scroll') {
                hasXScroll = true;
            } else if (x) {
                hasXScroll = dom.scrollWidth > dom.clientWidth;
            }
            if (y === 'scroll') {
                hasYScroll = true;
            } else if (y) {
                hasYScroll = dom.scrollHeight > dom.clientHeight;
            }
            if (hasXScroll) {
                height = scrollbarSize.height;
            }
            if (hasYScroll) {
                width = scrollbarSize.width;
            }
        }
        return {
            width: width,
            height: height
        };
    },
    /**
     * @method getPosition
     * Returns the current scroll position
     * @return {Object} An object with `x` and `y` properties.
     */
    getPosition: function() {
        var me = this;
        if (me.positionDirty) {
            me.updateDomScrollPosition();
        }
        return me.position;
    },
    /**
     * @method getSize
     * Returns the size of the scrollable content
     * @return {Object} size
     * @return {Number} return.x The width of the scrollable content
     * @return {Number} return.y The height of the scrollable content
     */
    getSize: function() {
        var element = this.getElement(),
            size, dom;
        if (element && !element.destroyed) {
            dom = element.dom;
            size = {
                x: dom.scrollWidth,
                y: dom.scrollHeight
            };
        } else {
            size = {
                x: 0,
                y: 0
            };
        }
        return size;
    },
    /**
     * @method getMaxPosition
     * Returns the maximum scroll position for this scroller
     * @return {Object} position
     * @return {Number} return.x The maximum scroll position on the x axis
     * @return {Number} return.y The maximum scroll position on the y axis
     */
    getMaxPosition: function() {
        var element = this.getElement(),
            x = 0,
            y = 0,
            dom;
        if (element && !element.destroyed) {
            dom = element.dom;
            x = dom.scrollWidth - dom.clientWidth;
            y = dom.scrollHeight - dom.clientHeight;
        }
        return {
            x: x,
            y: y
        };
    },
    /**
     * @method getMaxUserPosition
     * Returns the maximum scroll position for this scroller for scrolling that is initiated
     * by the user via mouse or touch.  This differs from getMaxPosition in that getMaxPosition
     * returns the true maximum scroll position regardless of which axes are enabled for
     * user scrolling.
     * @return {Object} position
     * @return {Number} return.x The maximum scroll position on the x axis
     * @return {Number} return.y The maximum scroll position on the y axis
     */
    getMaxUserPosition: function() {
        var me = this,
            element = me.getElement(),
            x = 0,
            y = 0,
            dom;
        if (element && !element.destroyed) {
            dom = element.dom;
            if (me.getX()) {
                x = dom.scrollWidth - dom.clientWidth;
            }
            if (me.getY()) {
                y = dom.scrollHeight - dom.clientHeight;
            }
        }
        return {
            x: x,
            y: y
        };
    },
    /**
     * Refreshes the scroller size and maxPosition.
     * @param {Boolean} immediate `true` to refresh immediately. By default refreshes
     * are deferred until the next {@link Ext.GlobalEvents#event-idle idle} event to
     * ensure any pending writes have been flushed to the dom and any reflows have
     * taken place.
     * @return {Ext.scroll.Scroller} this
     * @chainable
     */
    refresh: function() {
        // Element size has changed.
        // Our position property may need refreshing from the DOM
        this.positionDirty = true;
        this.fireEvent('refresh', this);
        return this;
    },
    /**
     * Removes a partnership that was created via {@link #addPartner}
     * @param {Ext.scroll.Scroller} partner
     * @private
     */
    removePartner: function(partner) {
        var partners = this._partners,
            otherPartners = partner._partners;
        if (partners) {
            delete partners[partner.getId()];
        }
        if (otherPartners) {
            delete (otherPartners[this.getId()]);
        }
    },
    /**
     * Scrolls by the passed delta values, optionally animating.
     *
     * All of the following are equivalent:
     *
     *      scroller.scrollBy(10, 10, true);
     *      scroller.scrollBy([10, 10], true);
     *      scroller.scrollBy({ x: 10, y: 10 }, true);
     *
     * A null value for either `x` or `y` will result in no scrolling on the given axis,
     * for example:
     *
     *     scroller.scrollBy(null, 10);
     *
     * will scroll by 10 on the y axis and leave the x axis at its current scroll position
     *
     * @param {Number/Number[]/Object} deltaX Either the x delta, an Array specifying x
     * and y deltas or an object with "x" and "y" properties.
     * @param {Number/Boolean/Object} deltaY Either the y delta, or an animate flag or
     * config object.
     * @param {Boolean/Object} animate Animate flag/config object if the delta values were
     * passed separately.
     * @return {Ext.Promise} A promise for when the scroll completes.
     */
    scrollBy: function(deltaX, deltaY, animate) {
        var position = this.getPosition();
        if (deltaX) {
            if (deltaX.length) {
                // array
                animate = deltaY;
                deltaY = deltaX[1];
                deltaX = deltaX[0];
            } else if (typeof deltaX !== 'number') {
                // object
                animate = deltaY;
                deltaY = deltaX.y;
                deltaX = deltaX.x;
            }
        }
        deltaX = (typeof deltaX === 'number') ? deltaX + position.x : null;
        deltaY = (typeof deltaY === 'number') ? deltaY + position.y : null;
        return this.doScrollTo(deltaX, deltaY, animate);
    },
    /**
     * Ensures a descendant element of the scroller is visible by scrolling to it.
     *
     * @param {String/HTMLElement/Ext.dom.Element/Object} el
     * The descendant element to scroll into view. May also be the options object with
     * the `element` key defining the descendant element.
     *
     * @param {Object} [options] An object containing options to modify the operation.
     *
     * @param {Object} [options.align] The alignment for the scroll.
     * @param {'start'/'center'/'end'} [options.align.x] The alignment of the x scroll. If not
     * specified, the minimum will be done to make the element visible. The behavior is `undefined`
     * if the request cannot be honored. If the alignment is suffixed with a `?`, the alignment will
     * only take place if the item is not already in the visible area.
     * @param {'start'/'center'/'end'} [options.align.y] The alignment of the y scroll. If not
     * specified, the minimum will be done to make the element visible. The behavior is `undefined`
     * if the request cannot be honored. If the alignment is suffixed with a `?`, the alignment will
     * only take place if the item is not already in the visible area.
     *
     * @param {Boolean} [options.animation] Pass `true` to animate the row into view.
     *
     * @param {Boolean} [options.highlight=false] Pass `true` to highlight the row with a glow
     * animation when it is in view. Can also be a hex color to use for highlighting
     * (defaults to yellow = '#ffff9c').
     *
     * @param {Boolean} [options.x=true] `false` to disable horizontal scroll.
     * @param {Boolean} [options.y=true] `false` to disable vertical scroll.
     *
     * @return {Ext.Promise} A promise for when the scroll completes.
     * @since 6.5.1
     */
    ensureVisible: function(el, options) {
        var me = this,
            position = me.getPosition(),
            highlight, newPosition, ret;
        // Might get called before Component#onBoxReady which is when the Scroller is set up
        // with elements.
        if (el) {
            if (el && el.element && !el.isElement) {
                options = el;
                el = options.element;
            }
            options = options || {};
            highlight = options.highlight;
            newPosition = me.getEnsureVisibleXY(el, options);
            // Only attempt to scroll if it's needed.
            if (newPosition.y !== position.y || newPosition.x !== position.x) {
                if (highlight) {
                    me.on({
                        scrollend: 'doHighlight',
                        scope: me,
                        single: true,
                        args: [
                            el,
                            highlight
                        ]
                    });
                }
                ret = me.doScrollTo(newPosition.x, newPosition.y, options.animation);
            } else {
                // No scrolling needed, but still honor highlight request
                if (highlight) {
                    me.doHighlight(el, highlight);
                }
                // Resolve straight away
                ret = Ext.Deferred.getCachedResolved();
            }
        } else {
            // Can't scroll
            ret = Ext.Deferred.getCachedRejected();
        }
        return ret;
    },
    /**
     * Scrolls a descendant element of the scroller into view.
     * @param {String/HTMLElement/Ext.dom.Element} el the descendant to scroll into view
     * @param {Boolean} [hscroll=true] False to disable horizontal scroll.
     * @param {Boolean/Object} [animate] true for the default animation or a standard Element
     * animation config object
     * @param {Boolean/String} [highlight=false] true to
     * {@link Ext.dom.Element#highlight} the element when it is in view. Can also be a
     * hex color to use for highlighting (defaults to yellow = '#ffff9c').
     *
     * @deprecated 6.5.1 Use {@link #ensureVisible} instead.
     * @return {Ext.Promise} A promise for when the scroll completes.
     */
    scrollIntoView: function(el, hscroll, animate, highlight) {
        return this.ensureVisible(el, {
            animation: animate,
            highlight: highlight,
            x: hscroll
        });
    },
    /**
     * Determines if the passed element is within the visible x and y scroll viewport.
     * @param {String/HTMLElement/Ext.dom.Element} el The dom node, Ext.dom.Element, or 
     * id (string) of the dom element that is to be verified to be in view
     * @return {Object} Which ranges the element is in.
     * @return {Boolean} return.x `true` if the passed element is within the x visible range.
     * @return {Boolean} return.y `true` if the passed element is within the y visible range.
     */
    isInView: function(el) {
        return this.doIsInView(el);
    },
    /**
     * Scrolls to the given position.
     *
     * All of the following are equivalent:
     *
     *      scroller.scrollTo(10, 10, true);
     *      scroller.scrollTo([10, 10], true);
     *      scroller.scrollTo({ x: 10, y: 10 }, true);
     *
     * A null value for either `x` or `y` will result in no scrolling on the given axis,
     * for example:
     *
     *     scroller.scrollTo(null, 10);
     *
     * will scroll to 10 on the y axis and leave the x axis at its current scroll position
     *
     * A negative value for either `x` or `y` represents an offset from the maximum scroll
     * position on the given axis:
     *
     *     // scrolls to 10px from the maximum x scroll position and 20px from maximum y
     *     scroller.scrollTo(-10, -20);
     *
     * A value of Infinity on either axis will scroll to the maximum scroll position on
     * that axis:
     *
     *     // scrolls to the maximum position on both axes
     *     scroller.scrollTo(Infinity, Infinity);
     *
     * @param {Number} x The scroll position on the x axis.
     * @param {Number} y The scroll position on the y axis.
     * @param {Boolean/Object} [animation] Whether or not to animate the scrolling to the
     * new position.
     *
     * @return {Ext.Promise} A promise for when the scroll completes.
     */
    scrollTo: function(x, y, animation) {
        var maxPosition;
        if (x) {
            if (x.length) {
                // array
                animation = y;
                y = x[1];
                x = x[0];
            } else if (typeof x !== 'number') {
                // object
                animation = y;
                y = x.y;
                x = x.x;
            }
        }
        if (x < 0 || y < 0) {
            maxPosition = this.getMaxPosition();
            if (x < 0) {
                x += maxPosition.x;
            }
            if (y < 0) {
                y += maxPosition.y;
            }
        }
        return this.doScrollTo(x, y, animation);
    },
    updateDirection: function(direction) {
        var me = this,
            x, y;
        if (!direction) {
            // if no direction was configured we set its value based on the values of
            // x and y.  This ensures getDirection() always returns something useful
            // for backward compatibility.
            x = me.getX();
            y = me.getY();
            if (x && y) {
                direction = (y === 'scroll' && x === 'scroll') ? 'both' : 'auto';
            } else if (y) {
                direction = 'vertical';
            } else if (x) {
                direction = 'horizontal';
            }
            // set the _direction property directly to avoid the updater being called
            // and triggering setX/setY calls
            me._direction = direction;
        } else {
            if (direction === 'auto') {
                x = true;
                y = true;
            } else if (direction === 'vertical') {
                x = false;
                y = true;
            } else if (direction === 'horizontal') {
                x = true;
                y = false;
            } else if (direction === 'both') {
                x = 'scroll';
                y = 'scroll';
            }
            me.setX(x);
            me.setY(y);
        }
    },
    updateScrollbars: function(scrollbars, oldScrollbars) {
        this.syncScrollbarCls();
    },
    updateSize: function(size) {
        var me = this,
            element = me.getElement(),
            x = size.x,
            y = size.y,
            spacer;
        if (element) {
            me.positionDirty = true;
            spacer = me.getSpacer();
            // Typically a dom scroller simply assumes the scroll size dictated by its content.
            // In some cases, however, it is necessary to be able to manipulate this scroll size
            // (infinite lists for example).  This method positions a 1x1 px spacer element
            // within the scroller element to set a specific scroll size.
            if (!x && !y) {
                spacer.hide();
            } else {
                // Subtract spacer size from coordinates (spacer is always 1x1 px in size)
                if (x > 0) {
                    x -= 1;
                }
                if (y > 0) {
                    y -= 1;
                }
                me.setSpacerXY({
                    x: x,
                    y: y
                });
                spacer.show();
            }
        }
    },
    updateMsSnapInterval: function() {
        this.initMsSnapInterval();
    },
    updateSnapSelector: function() {
        this.initSnap();
    },
    updateSnapOffset: function() {
        this.initSnap();
    },
    updateTouchAction: function(touchAction) {
        var element = this.getElement();
        if (element) {
            element.setTouchAction(touchAction);
        }
    },
    updateElement: function(element, oldElement) {
        var me = this,
            touchAction = me.getTouchAction(),
            scrollListener = me.scrollListener,
            elementCls = me.elementCls,
            eventSource, scrollEl;
        // If we have a scrollListener, we also have a scrollElement
        if (scrollListener) {
            scrollListener.destroy();
            me.scrollListener = null;
            me.setScrollElement(null);
        }
        if (oldElement && !oldElement.destroyed) {
            // TODO: might be nice to have x-scroller-foo classes to map overflow styling
            oldElement.setStyle('overflow', 'hidden');
            oldElement.removeCls(elementCls);
        }
        if (element) {
            if (element.dom === document.documentElement || element.dom === document.body) {
                /**
                 * When the documentElement or body is scrolled, its scroll events are
                 * fired via the Document object.
                 * Few browsers like safari 10.9 and 10.10 are not sending scroll events 
                 * when using "Ext.getWin()"
                 * but it does on "Ext.getDoc()"
                 */
                eventSource = Ext.getDoc();
                scrollEl = Ext.scroll.Scroller.getScrollingElement();
            } else {
                scrollEl = eventSource = element;
            }
            me.setScrollElement(Ext.get(scrollEl));
            me.scrollListener = eventSource.on({
                scroll: me.onDomScroll,
                scope: me,
                destroyable: true
            });
            if (touchAction) {
                element.setTouchAction(touchAction);
            }
            me.initXStyle();
            me.initYStyle();
            element.addCls(elementCls);
            me.initSnap();
            me.initMsSnapInterval();
            me.syncScrollbarCls();
        }
    },
    updateX: function(x) {
        this.initXStyle();
    },
    updateY: function(y) {
        this.initYStyle();
    },
    deprecated: {
        '5': {
            methods: {
                /**
                 * @method getScroller
                 * Returns this scroller.
                 *
                 * In Sencha Touch 2, access to a Component's Scroller was provided via
                 * a Ext.scroll.View class that was returned from the Component's getScrollable()
                 * method:
                 *
                 *     component.getScrollable().getScroller();
                 *
                 * in 5.0 all the functionality of Ext.scroll.View has been rolled into
                 * Ext.scroll.Scroller, and Ext.scroll.View has been removed.  Component's
                 * getScrollable() method now returns a Ext.scroll.Scroller.  This method is
                 * provided for compatibility.
                 * @deprecated 5.0 This method is deprecated.  Please use Ext.scroll.Scroller's
                 * getScrollable() method instead.
                 */
                getScroller: function() {
                    return this;
                }
            }
        },
        '5.1.0': {
            methods: {
                /**
                 * @method scrollToTop
                 * Scrolls to 0 on both axes
                 * @param {Boolean/Object} animate
                 * @private
                 * @return {Ext.scroll.Scroller} this
                 * @chainable
                 * @deprecated 5.1.0 Use scrollTo instead
                 */
                scrollToTop: function(animate) {
                    return this.scrollTo(0, 0, animate);
                },
                /**
                 * @method scrollToEnd
                 * Scrolls to the maximum position on both axes
                 * @param {Boolean/Object} animate
                 * @private
                 * @return {Ext.scroll.Scroller} this
                 * @chainable
                 * @deprecated 5.1.0 Use scrollTo instead
                 */
                scrollToEnd: function(animate) {
                    return this.scrollTo(Infinity, Infinity, animate);
                }
            }
        }
    },
    privates: {
        axisConfigs: {
            x: {
                x: true
            },
            y: {
                y: true
            },
            both: {
                x: true,
                y: true
            }
        },
        /**
         * @private
         * Gets the x/y coordinates to ensure the element is scrolled into view.
         *
         * @param {String/HTMLElement/Ext.dom.Element/Object} el
         * The descendant element to scroll into view. May also be the options object with
         * the `element` key defining the descendant element.
         *
         * @param {Object} [options] An object containing options to modify the operation.
         *
         * @param {Object/String} [options.align] The alignment for the scroll. If a string,
         * this value will be used for both `x` and `y` alignments.
         * @param {'start'/'center'/'end'} [options.align.x] The alignment of the x scroll. If not
         * specified, the minimum will be done to make the element visible. The behavior is
         * `undefined` if the request cannot be honored. If the alignment is suffixed with a `?`,
         * the alignment will only take place if the item is not already in the visible area.
         * @param {'start'/'center'/'end'} [options.align.y] The alignment of the y scroll. If not
         * specified, the minimum will be done to make the element visible. The behavior is
         * `undefined` if the request cannot be honored. If the alignment is suffixed with a `?`,
         * the alignment will only take place if the item is not already in the visible area.
         *
         * @param {Boolean} [options.x=true] `false` to disable horizontal scroll and `x` align
         * option.
         * @param {Boolean} [options.y=true] `false` to disable vertical scroll and `y` align
         * option.
         * @return {Object} The new position that will be used to scroll the element into view.
         * @since 6.5.1
         */
        getEnsureVisibleXY: function(el, options) {
            var position = this.getPosition(),
                viewport = this.component ? this.component.getScrollableClientRegion() : this.getElement(),
                newPosition, align;
            if (el && el.element && !el.isElement) {
                options = el;
                el = options.element;
            }
            options = options || {};
            align = options.align;
            if (align) {
                if (Ext.isString(align)) {
                    align = {
                        x: options.x === false ? null : align,
                        y: options.y === false ? null : align
                    };
                } else if (Ext.isObject(align)) {
                    if (align.x && options.x === false) {
                        align.x = null;
                    }
                    if (align.y && options.y === false) {
                        align.y = null;
                    }
                }
            }
            newPosition = Ext.fly(el).getScrollIntoViewXY(viewport, position.x, position.y, align);
            newPosition.x = options.x === false ? position.x : newPosition.x;
            newPosition.y = options.y === false ? position.y : newPosition.y;
            return newPosition;
        },
        getSpacer: function() {
            var me = this,
                spacer = me._spacer,
                element;
            // In some cases (e.g. infinite lists) we need to be able to tell the scroller
            // to have a specific size, regardless of its contents.  This creates a spacer
            // element which can then be absolutely positioned to affect the element's
            // scroll size. Must be first element, so it is not translated due to being after
            // the element contrainer el.
            if (!spacer) {
                element = me.getElement();
                spacer = me._spacer = element.createChild({
                    cls: me.spacerCls,
                    role: 'presentation'
                }, element.dom.firstChild);
                spacer.setVisibilityMode(2);
                // 'display' visibilityMode
                spacer.hide();
                // make sure the element is positioned if it is not already.  This ensures
                // that the spacer's position will affect the element's scroll size
                element.position();
            }
            return spacer;
        },
        applySpacerXY: function(pos, oldPos) {
            // Opt out if we have the same value
            if (oldPos && pos.x === oldPos.x && pos.y === oldPos.y) {
                pos = undefined;
            }
            return pos;
        },
        updateSpacerXY: function(pos) {
            var me = this,
                spacer = me.getSpacer(),
                sStyle = spacer.dom.style,
                scrollHeight = pos.y,
                shortfall;
            sStyle.marginTop = '';
            me.translateSpacer(pos.x, me.constrainScrollRange(scrollHeight));
            // Force a synchronous layout to update the scrollHeight.
            // This flip-flops between 0px and 1px
            sStyle.lineHeight = Number(!parseInt(sStyle.lineHeight, 10)) + 'px';
            // See if we can get any more scrollHeight from a margin-top
            if (scrollHeight > 1000000) {
                shortfall = scrollHeight - me.getElement().dom.scrollHeight;
                if (shortfall > 0) {
                    sStyle.marginTop = Math.min(shortfall, me.maxSpacerMargin || 0) + 'px';
                }
            }
        },
        // rtl hook - rtl version sets right style
        translateSpacer: function(x, y) {
            this.getSpacer().translate(x, y);
        },
        doIsInView: function(el, skipCheck) {
            var me = this,
                c = me.component,
                result = {
                    x: false,
                    y: false
                },
                elRegion,
                myEl = me.getElement(),
                myElRegion;
            if (el && (skipCheck || (myEl.contains(el) || (c && c.owns(el))))) {
                myElRegion = myEl.getRegion();
                elRegion = Ext.fly(el).getRegion();
                result.x = elRegion.right > myElRegion.left && elRegion.left < myElRegion.right;
                result.y = elRegion.bottom > myElRegion.top && elRegion.top < myElRegion.bottom;
            }
            return result;
        },
        // Checks if the scroller contains a component by searching up the element hierarchy
        // using components. It uses component navigation as opposed to elements because we
        // want logical ownership.
        contains: function(component) {
            var el = this.getElement(),
                owner = component;
            while (owner && owner !== Ext.Viewport) {
                if (el.contains(owner.el)) {
                    return true;
                }
                owner = owner.getRefOwner();
            }
            return false;
        },
        constrainScrollRange: function(scrollRange) {
            var maxScrollHeight, tooHigh, tooLow, scrollTest, stretcher, sStyle;
            // Only do the expensive search for the browser limit if they
            // want more than a million pixels.
            if (scrollRange < 1000000) {
                return scrollRange;
            }
            if (!this.maxSpacerTranslate) {
                //
                // Find max scroll height which transform: translateY(npx) will support.
                // IE11 appears to have 21,474,834
                // Chrome and Safari have 16,777,216, but additional margin-top of 16777215px
                // allows a scrollHeight of 33,554,431
                // Firefox has 17,895,698
                // IE9-10 1,534,000
                //
                maxScrollHeight = Math.pow(2, 32);
                tooHigh = maxScrollHeight;
                tooLow = 500;
                scrollTest = Ext.getBody().createChild({
                    style: {
                        position: 'absolute',
                        left: '-10000px',
                        top: '0',
                        width: '500px',
                        height: '500px'
                    },
                    cn: {
                        cls: this.spacerCls
                    }
                }, null, true);
                stretcher = Ext.get(scrollTest.firstChild);
                sStyle = stretcher.dom.style;
                stretcher.translate(0, maxScrollHeight - 1);
                sStyle.lineHeight = Number(!parseInt(sStyle.lineHeight, 10)) + 'px';
                // See what the max translateY is which still stretches the scrollHeight
                while (tooHigh !== tooLow + 1) {
                    // eslint-disable-next-line max-len
                    stretcher.translate(0, (maxScrollHeight = tooLow + Math.floor((tooHigh - tooLow) / 2)));
                    // Force a synchronous layout to update the scrollHeight.
                    // This flip-flops between 0px and 1px
                    sStyle.lineHeight = Number(!parseInt(sStyle.lineHeight, 10)) + 'px';
                    if (scrollTest.scrollHeight < maxScrollHeight) {
                        tooHigh = maxScrollHeight;
                    } else {
                        tooLow = maxScrollHeight;
                    }
                }
                stretcher.translate(0, Ext.scroll.Scroller.prototype.maxSpacerTranslate = tooLow);
                // Go through the same steps seeing how far we can push it with margin-top
                tooHigh = tooLow * 2;
                while (tooHigh !== tooLow + 1) {
                    stretcher.dom.style.marginTop = ((maxScrollHeight = tooLow + Math.floor((tooHigh - tooLow) / 2))) + 'px';
                    // Force a synchronous layout to update the scrollHeight.
                    // This flip-flops between 0px and 1px
                    sStyle.lineHeight = Number(!parseInt(sStyle.lineHeight, 10)) + 'px';
                    if (scrollTest.scrollHeight < maxScrollHeight) {
                        tooHigh = maxScrollHeight;
                    } else {
                        tooLow = maxScrollHeight;
                    }
                }
                Ext.fly(scrollTest).destroy();
                Ext.scroll.Scroller.prototype.maxSpacerMargin = tooLow - Ext.scroll.Scroller.prototype.maxSpacerTranslate;
            }
            // The maximum a translateY transform can be pushed to stretch the scrollHeight before
            // it collapses back to offsetHeight
            return Math.min(scrollRange, this.maxSpacerTranslate);
        },
        // hook for rtl mode to convert an x coordinate to RTL space.
        convertX: function(x) {
            return x;
        },
        // highlights an element after it has been scrolled into view
        doHighlight: function(el, highlight) {
            if (highlight !== true) {
                // handle hex color
                Ext.fly(el).highlight(highlight);
            } else {
                Ext.fly(el).highlight();
            }
        },
        doScrollTo: function(x, y, animate) {
            // There is an IE8 override of this method; when making changes here
            // don't forget to update the override as well
            var me = this,
                element = me.getScrollElement(),
                maxPosition, dom, xInf, yInf, ret, translatable, deferred;
            if (element && !element.destroyed) {
                dom = element.dom;
                xInf = (x === Infinity);
                yInf = (y === Infinity);
                if (xInf || yInf) {
                    maxPosition = me.getMaxPosition();
                    if (xInf) {
                        x = maxPosition.x;
                    }
                    if (yInf) {
                        y = maxPosition.y;
                    }
                }
                if (x !== null) {
                    x = me.convertX(x);
                }
                if (animate) {
                    translatable = me.translatable;
                    if (!translatable) {
                        me.translatable = translatable = new Ext.util.translatable.ScrollPosition({
                            element: element
                        });
                    }
                    deferred = new Ext.Deferred();
                    // Use onFrame here to let the scroll complete and animations to fire.
                    translatable.on('animationend', function() {
                        // Check destroyed vs destroying since we're onFrame here
                        if (me.destroyed) {
                            deferred.reject();
                        } else {
                            deferred.resolve();
                        }
                    }, Ext.global, {
                        single: true,
                        onFrame: true
                    });
                    translatable.translate(x, y, animate);
                    ret = deferred.promise;
                } else {
                    if (y != null) {
                        dom.scrollTop = y;
                    }
                    if (x != null) {
                        dom.scrollLeft = x;
                    }
                    ret = Ext.Deferred.getCachedResolved();
                }
                // Our position object will need refreshing before returning.
                me.positionDirty = true;
            } else {
                ret = Ext.Deferred.getCachedRejected();
            }
            return ret;
        },
        fireScrollStart: function(x, y, xDelta, yDelta) {
            var me = this,
                component = me.component;
            me.invokePartners('onPartnerScrollStart', x, y, xDelta, yDelta);
            me.startX = x - xDelta;
            me.startY = y - yDelta;
            if (me.hasListeners.scrollstart) {
                me.fireEvent('scrollstart', me, x, y);
            }
            if (component && component.onScrollStart) {
                component.onScrollStart(x, y);
            }
            Ext.GlobalEvents.fireEvent('scrollstart', me, x, y);
        },
        fireScroll: function(x, y, xDelta, yDelta) {
            var me = this,
                component = me.component;
            me.invokePartners('onPartnerScroll', x, y, xDelta, yDelta);
            if (me.hasListeners.scroll) {
                me.fireEvent('scroll', me, x, y, xDelta, yDelta);
            }
            if (component && component.onScrollMove) {
                component.onScrollMove(x, y);
            }
            Ext.GlobalEvents.fireEvent('scroll', me, x, y, xDelta, yDelta);
        },
        fireScrollEnd: function(x, y, xDelta, yDelta) {
            var me = this,
                component = me.component,
                dx = x - me.startX,
                dy = y - me.startY;
            me.startX = me.startY = null;
            me.invokePartners('onPartnerScrollEnd', x, y, xDelta, yDelta);
            if (me.hasListeners.scrollend) {
                me.fireEvent('scrollend', me, x, y, dx, dy);
            }
            if (component && component.onScrollEnd) {
                component.onScrollEnd(x, y);
            }
            Ext.GlobalEvents.fireEvent('scrollend', me, x, y, dx, dy);
        },
        // rtl hook
        getElementScroll: function(element) {
            return element.getScroll();
        },
        initSnap: function() {
            var me = this,
                snapOffset = me.getSnapOffset(),
                snapSelector = me.getSnapSelector(),
                element = me.getElement(),
                offsetX, offsetY, snapCoordinate;
            if (element && snapSelector) {
                element.addCls(me.snappableCls);
                me.removeSnapStylesheet();
                if (snapOffset) {
                    offsetX = snapOffset.x || 0;
                    offsetY = snapOffset.y || 0;
                    if (offsetX) {
                        offsetX = -offsetX + 'px';
                    }
                    if (offsetY) {
                        offsetY = -offsetY + 'px';
                    }
                }
                snapCoordinate = offsetX + ' ' + offsetY + ';';
                me.snapStylesheet = Ext.util.CSS.createStyleSheet('#' + element.id + ' ' + snapSelector + '{-webkit-scroll-snap-coordinate:' + snapCoordinate + 'scroll-snap-coordinate:' + snapCoordinate + '}');
            }
        },
        initMsSnapInterval: function() {
            var element = this.getElement(),
                interval, x, y, style;
            if (element) {
                interval = this.getMsSnapInterval();
                if (interval) {
                    x = interval.x;
                    y = interval.y;
                    style = element.dom.style;
                    if (x) {
                        style['-ms-scroll-snap-points-x'] = 'snapInterval(0px, ' + x + 'px)';
                    }
                    if (y) {
                        style['-ms-scroll-snap-points-y'] = 'snapInterval(0px, ' + y + 'px)';
                    }
                }
            }
        },
        initXStyle: function() {
            var element = this.getElement(),
                x = this.getX();
            // Check that element exists and is not destroyed
            if (element && element.dom) {
                if (!x) {
                    x = 'hidden';
                } else if (x === true) {
                    x = 'auto';
                }
                element.setStyle('overflow-x', x);
            }
        },
        initYStyle: function() {
            var element = this.getElement(),
                y = this.getY();
            // Check that element exists and is not destroyed
            if (element && element.dom) {
                if (!y) {
                    y = 'hidden';
                } else if (y === true) {
                    y = 'auto';
                }
                element.setStyle('overflow-y', y);
            }
        },
        invokePartners: function(method, x, y, xDelta, yDelta) {
            var me = this,
                partners = me._partners,
                partner, id, axes;
            if (!me.suspendSync) {
                me.invokingPartners = true;
                for (id in partners) {
                    axes = partners[id].axes;
                    partner = partners[id].scroller;
                    // Only pass the scroll on to partners if we are are configured
                    // to pass on the scrolled dimension
                    if (!partner.invokingPartners && (xDelta && axes.x || yDelta && axes.y)) {
                        partner[method](me, axes.x ? x : null, axes.y ? y : null, xDelta, yDelta);
                    }
                }
                me.invokingPartners = false;
            }
        },
        suspendPartnerSync: function() {
            this.suspendSync = (this.suspendSync || 0) + 1;
        },
        resumePartnerSync: function(syncNow) {
            var me = this,
                position;
            if (me.suspendSync) {
                me.suspendSync--;
            }
            if (!me.suspendSync && syncNow) {
                position = me.getPosition();
                me.invokePartners('onPartnerScroll', position.x, position.y);
                me.invokePartners('onPartnerScrollEnd', position.x, position.y);
            }
        },
        readPosition: function(position) {
            var me = this,
                element = me.getScrollElement(),
                elScroll;
            position = position || {};
            if (element && !element.destroyed) {
                elScroll = me.getElementScroll(element);
                position.x = elScroll.left;
                position.y = elScroll.top;
            }
            return position;
        },
        updateDomScrollPosition: function(silent) {
            var me = this,
                position = me.position,
                oldX = position.x,
                oldY = position.y,
                x, y, xDelta, yDelta;
            me.readPosition(position);
            x = position.x;
            y = position.y;
            me.positionDirty = false;
            if (!silent) {
                xDelta = x - oldX;
                yDelta = y - oldY;
                // If we already know about the position. then we've been coerced there by a partner
                // and that will have been firing our event sequence synchronously, so they do not
                // not need to be fire in response to the ensuing scroll event.
                if (xDelta || yDelta) {
                    if (!me.isScrolling) {
                        me.isScrolling = Ext.isScrolling = true;
                        me.fireScrollStart(x, y, xDelta, yDelta);
                    }
                    me.fireScroll(x, y, xDelta, yDelta);
                    me.onDomScrollEnd(x, y, xDelta, yDelta);
                }
            }
            return position;
        },
        /**
         * @private
         * May be called when a Component is rendererd AFTER some scrolling partner has begun
         * its lifecycle to sync this scroller with partners which may be scrolled anywhere by now.
         */
        syncWithPartners: function() {
            var me = this,
                partners = me._partners,
                id, partner, position;
            me.suspendPartnerSync();
            for (id in partners) {
                partner = partners[id].scroller;
                position = partner.getPosition();
                me.onPartnerScroll(partner, position.x, position.y);
            }
            me.resumePartnerSync();
        },
        syncScrollbarCls: function() {
            var element = this.getElement();
            if (element) {
                element.toggleCls(this.noScrollbarsCls, this.getScrollbars() === false);
            }
        },
        onDomScroll: function() {
            var hasTimer = !!this.restoreTimer;
            this.updateDomScrollPosition(hasTimer);
            if (hasTimer) {
                Ext.undefer(this.onDomScrollEnd.timer);
                return;
            }
        },
        doOnDomScrollEnd: function(x, y, xDelta, yDelta) {
            var me = this;
            // Could be destroyed by this time
            if (me.destroying || me.destroyed) {
                return;
            }
            me.isScrolling = Ext.isScrolling = false;
            // if this is being flushed we only need to set the scrolling status to false
            if (x === undefined) {
                return;
            }
            me.trackingScrollLeft = x;
            me.trackingScrollTop = y;
            me.fireScrollEnd(x, y, xDelta, yDelta);
        },
        onPartnerScrollStart: function(partner, x, y, xDelta, yDelta) {
            // Pass the signal on immediately to all partners.
            this.isScrolling = true;
            this.fireScrollStart(x, y, xDelta, yDelta);
        },
        onPartnerScroll: function(partner, x, y, xDelta, yDelta) {
            this.doScrollTo(x, y, false);
            // Update the known scroll position so that when it reacts to its DOM,
            // it will not register a change and so will not invoke partners.
            // All scroll intentions are propagated synchronously.
            // The ensuing multiple scroll events are then ignored.
            this.updateDomScrollPosition(true);
            // Pass the signal on immediately to all partners.
            this.fireScroll(x, y, xDelta, yDelta);
        },
        onPartnerScrollEnd: function(partner, x, y, xDelta, yDelta) {
            // manually clearing the buffer queue before calling the method without buffering
            this.cancelOnDomScrollEnd();
            // Pass the signal on immediately to all partners.
            // We are called by the onDomScrollEnd of our controller
            // so we must not add another delay and call doOnScrollEnd directly.
            this.doOnDomScrollEnd(x, y, xDelta, yDelta);
        },
        removeSnapStylesheet: function() {
            var stylesheet = this.snapStylesheet;
            if (stylesheet) {
                Ext.util.CSS.removeStyleSheet(stylesheet);
                this.snapStylesheet = null;
            }
        },
        restoreState: function() {
            var me = this,
                el = me.getScrollElement();
            if (el) {
                // Only restore state if has been previously captured! For example,
                // floaters probably have not been hidden before initially shown.
                if (me.trackingScrollTop !== undefined) {
                    // If we're restoring the scroll position, we don't want to publish
                    // scroll events since the scroll position should not have changed
                    // at all as far as the user is concerned, so just do it silently
                    // while ensuring we maintain the correct internal state. 50ms is
                    // enough to capture the async scroll events, anything after that
                    // we re-enable.
                    if (!me.restoreTimer) {
                        me.restoreTimer = Ext.defer(function() {
                            me.restoreTimer = null;
                        }, 50);
                    }
                    me.doScrollTo(me.trackingScrollLeft, me.trackingScrollTop, false);
                }
            }
        }
    }
}, // Do not discard the state.
// It may need to be restored again.
function(Scroller) {
    /**
     * @private
     * @return {Ext.scroll.Scroller}
     */
    Ext.getViewportScroller = function() {
        // This method creates the global viewport scroller.  This scroller instance must
        // always exist regardless of whether or not there is a Viewport component in use
        // so that global scroll events will still fire.  Menus and some other floating
        // things use these scroll events to hide themselves.
        var scroller = Scroller.viewport;
        if (!scroller) {
            Scroller.viewport = scroller = new Scroller();
            Scroller.initViewportScroller();
        }
        return scroller;
    };
    /**
     * @private
     * @param {Ext.scroll.Scroller} scroller
     */
    Ext.setViewportScroller = function(scroller) {
        if (Scroller.viewport !== scroller) {
            Ext.destroy(Scroller.viewport);
            if (scroller && !scroller.isScroller) {
                scroller = new Scroller(scroller);
            }
            Scroller.viewport = scroller;
        }
    };
    Ext.onReady(function() {
        // The viewport scroller must always exist, but it is deferred so that the
        // viewport component has a chance to call Ext.setViewportScroller() with
        // its own scroller first.
        // We assign the timer to a property to cancel the call while setting up
        // for unit tests. We will call initViewportScroller without waiting for the
        // Viewport to initialize.
        Scroller.initViewportScrollerTimer = Ext.defer(Scroller.initViewportScroller, 100);
    });
});

/**
 * A mixin to add floating capability to a Component.
 */
Ext.define('Ext.util.Floating', {
    mixinId: 'floating',
    uses: [
        'Ext.ZIndexManager'
    ],
    /**
     * @cfg {Boolean} focusOnToFront
     * Specifies whether the floated component should be automatically
     * {@link Ext.Component#method-focus focused} when it is {@link #toFront brought to the front}.
     */
    focusOnToFront: true,
    /**
     * @cfg {Boolean} [modal=false]
     * True to make the floated component modal and mask everything behind it when displayed,
     * false to display it without restricting access to other UI elements.
     */
    /**
     * @cfg {String/Boolean} shadow
     * Specifies whether the floating component should be given a shadow. Set to true
     * to automatically create an {@link Ext.Shadow}, or a string indicating the shadow's display
     * {@link Ext.Shadow#mode}. Set to false to disable the shadow.
     */
    shadow: 'sides',
    /**
     * @cfg {Boolean} [animateShadow=false]
     * `true` to animate the shadow along with the component while the component is animating.
     * By default the shadow is hidden while the component is animating
     */
    animateShadow: false,
    /**
     * @cfg {Boolean} constrain
     * True to constrain this Components within its containing element, false to allow it to fall
     * outside of its containing element. By default this Component will be rendered to
     * `document.body`. To render and constrain this Component within another element specify
     * {@link Ext.Component#renderTo renderTo}.
     */
    constrain: false,
    /**
     * @cfg {Boolean} [alignOnScroll=true]
     * By default, when the {@link Ext.Component#alignTo alignTo} method is called, a floating
     * component will scroll to keep aligned with the anchoring element if the anchoring element
     * is part of the scroll.
     *
     * If this is not necessary, and the `alignTo` is a one-off operation then set this config
     * to `false`.
     */
    alignOnScroll: true,
    /**
     * @cfg {Boolean} [fixed=false]
     * Configure as `true` to have this Component fixed at its `X, Y` coordinates in the browser
     * viewport, immune to scrolling the document.
     */
    /**
     * @cfg {Number} shadowOffset
     * Number of pixels to offset the shadow.
     */
    /**
     * @cfg {Boolean} shim `true` to enable an iframe shim for this Component to keep
     * windowed objects from showing through.
     */
    /**
     * @property {Boolean} floating
     * The value `true` indicates that this Component is floating.
     * @private
     * @readonly
     */
    /**
     * @property {Ext.ZIndexManager} zIndexManager
     * Only present for {@link Ext.Component#cfg-floating floating} Components after 
     * they have been rendered.
     *
     * A reference to the ZIndexManager which is managing this Component's z-index.
     *
     * The {@link Ext.ZIndexManager ZIndexManager} maintains a stack of floating Component
     * z-indices, and also provides a single modal mask which is insert just beneath the topmost
     * visible modal floating Component.
     *
     * Floating Components may be {@link Ext.Component#toFront brought to the front} or
     * {@link Ext.Component#toBack sent to the back} of the z-index stack.
     *
     * This defaults to the global {@link Ext.WindowManager ZIndexManager} for floating Components
     * that are programatically {@link Ext.Component#method-render rendered}.
     *
     * For {@link Ext.Component#cfg-floating floating} Components that are added to a
     * Container, the ZIndexManager is acquired from the first ancestor Container found
     * that is floating. If no floating ancestor is found, the global
     * {@link Ext.WindowManager ZIndexManager} is used.
     *
     * See {@link Ext.Component#cfg-floating} and {@link #zIndexParent}
     * @readonly
     */
    /**
     * @property {Ext.Container} zIndexParent
     * Only present for {@link Ext.Component#cfg-floating} Components which were 
     * inserted as child items of Containers, and which have a floating Container in 
     * their containment ancestry.
     *
     * For {@link Ext.Component#cfg-floating} Components which are child items of a 
     * Container, the zIndexParent will be a floating ancestor Container which is 
     * responsible for the base z-index value of all its floating descendants. It 
     * provides a {@link Ext.ZIndexManager ZIndexManager} which provides z-indexing 
     * services for all its descendant floating Components.
     *
     * Floating Components that are programmatically {@link Ext.Component#method-render rendered}
     * will not have a `zIndexParent` property.
     *
     * For example, the dropdown {@link Ext.view.BoundList BoundList} of a ComboBox which is
     * in a Window will have the Window as its `zIndexParent`, and will always show above
     * that Window, wherever the Window is placed in the z-index stack.
     *
     * See {@link Ext.Component#cfg-floating} and {@link #zIndexManager}
     * @readonly
     */
    config: {
        /**
         * @private
         * @cfg {Number} activeCounter An incrementing numeric counter indicating activation
         * index for use by the {@link #zIndexManager} to sort its stack.
         */
        activeCounter: 0,
        /**
         * @cfg {Boolean/Number} [alwaysOnTop=false] A flag indicating that this component should be
         * on the top of the z-index stack for use by the {@link #zIndexManager} to sort its stack.
         *
         * This may be a positive number to prioritize the ordering of multiple visible
         * always on top components.
         *
         * This may be set to a *negative* number to prioritize a component to the *bottom*
         * of the z-index stack.
         */
        alwaysOnTop: false
    },
    preventDefaultAlign: false,
    _visModeMap: {
        visibility: 1,
        display: 2,
        offsets: 3
    },
    constructor: function() {
        var me = this,
            el = me.el,
            shadow = me.shadow,
            shadowOffset, shadowConfig;
        if (shadow) {
            shadowConfig = {
                mode: (shadow === true) ? 'sides' : shadow
            };
            shadowOffset = me.shadowOffset;
            if (shadowOffset) {
                shadowConfig.offset = shadowOffset;
            }
            shadowConfig.animate = me.animateShadow;
            shadowConfig.fixed = me.fixed;
            el.enableShadow(shadowConfig, false);
        }
        if (me.shim || Ext.useShims) {
            el.enableShim({
                fixed: me.fixed
            }, false);
        }
        el.setVisibilityMode(me._visModeMap[me.hideMode]);
        // mousedown brings to front
        // Use capture to see the event first before any contained DD instance stop the event.
        me.el.on({
            mousedown: me.onMouseDown,
            scope: me,
            capture: true
        });
        // Register with the configured ownerCt.
        // With this we acquire a floatParent for relative positioning, and a zIndexParent
        // which is an ancestor floater which provides zIndex management.
        me.registerWithOwnerCt();
        me.initHierarchyEvents();
    },
    alignTo: function(alignTarget, position, offsets, animate, monitorScroll) {
        var me = this,
            alignEl, destroyed, dom, myXY, anchorXY, listeners;
        // We may be aligned to a Component, an Ext.Element, or an HtmlElement
        // In the latter case we can't create Ext.Element because that might leave
        // orphan Element instances so we use our internal Fly instance instead.
        if (alignTarget.isComponent) {
            alignEl = alignTarget.el;
            destroyed = alignTarget.destroyed;
        } else {
            me.alignTargetFly = me.alignTargetFly || new Ext.dom.Fly();
            alignEl = alignTarget = me.alignTargetFly.attach(alignTarget);
            dom = alignEl.dom;
            destroyed = !dom || Ext.isGarbage(dom);
        }
        if (destroyed) {
            me._lastAlignTarget = null;
            if (me.alignListeners) {
                me.alignListeners.destroy();
            }
            return;
        }
        me.mixins.positionable.alignTo.call(me, alignEl, position, offsets, animate, monitorScroll !== false);
        // Work out the vector to maintain our relative position as the alignTarget element moves
        myXY = me.getXY();
        anchorXY = alignTarget.getXY();
        me.alignVector = [
            myXY[0] - anchorXY[0],
            myXY[1] - anchorXY[1]
        ];
        // Let's stash these on the component/element in case it's aligned to something else
        // in its little lifetime.
        me._lastAlignTarget = alignTarget;
        me._lastAlignToPos = position;
        me._lastAlignToOffsets = offsets;
        // If we are aligned to a Component which is also either floating and aligned, or
        // *inside* a floating which is aligned, find the topmost, static target which
        // is anchoring the whole cascade.
        // For example the column header trigger el at the top of a column menu set.
        me._topAlignTarget = me.getTopAlignTarget();
        // Initially we have no clipping.
        me.clearClip();
        // Since floaters May be rendered to the document.body, floaters could become marooned
        // from its alignTarget if the alignTarget is inside an element that scrolls
        // and then that element is scrolled.
        // Only add the listeners once.
        if (!me.alignListeners) {
            // Always realign on window resize - the anchor element can move as a result.
            // Buffer so that we execute after any viewport layout has finished.
            listeners = {
                resize: {
                    fn: me.alignOnResize,
                    buffer: 100
                },
                scope: me,
                destroyable: true
            };
            // If we are supposed to align on scroll aligned, then add global scroll listener
            // so that we can follow the anchor element wherever it scrolls to.
            // Do not need to do this the floater is rendered inside the element
            // that they are aligned to.
            // For example CellEditors within grid cells.
            if (me.alignOnScroll && !alignEl.contains(me.el)) {
                listeners.scroll = me.doRealign;
            }
            me.alignListeners = Ext.on(listeners);
        }
    },
    initFloatConstrain: function() {
        var me = this,
            floatParent = me.floatParent;
        // If a floating Component is configured to be constrained, but has no configured
        // constrainTo setting, set its constrainTo to be it's ownerCt before rendering.
        if ((me.constrain || me.constrainHeader) && !me.constrainTo) {
            me.constrainTo = floatParent ? floatParent.getTargetEl() : me.container;
        }
    },
    initHierarchyEvents: function() {
        var me = this;
        if (!me.hierarchyEventListeners) {
            me.hierarchyEventListeners = Ext.on({
                hide: 'syncHidden',
                collapse: 'syncHidden',
                show: 'syncHidden',
                expand: 'syncHidden',
                added: 'syncHidden',
                scope: me,
                destroyable: true
            });
        }
    },
    registerWithOwnerCt: function() {
        var me = this,
            ownerCt = me.ownerCt,
            zip = me.zIndexParent;
        if (zip) {
            zip.unregisterFloatingItem(me);
        }
        // Acquire a zIndexParent by traversing the ownerCt axis for the nearest floating ancestor.
        // This is to find a base which can allocate relative z-index values
        zip = me.zIndexParent = me.up('[floating]');
        // Set the floatParent to the ownertCt if one has been provided.
        // Otherwise use the zIndexParent.
        // Developers must only use ownerCt if there is really a containing relationship.
        me.floatParent = ownerCt || zip;
        me.initFloatConstrain();
        delete me.ownerCt;
        if (zip) {
            zip.registerFloatingItem(me);
        } else {
            Ext.WindowManager.register(me);
        }
    },
    /**
     * @private
     * Mousedown brings to front, and programmatically grabs focus
     * unless the mousedown was on a focusable element
     */
    onMouseDown: function(e) {
        var me = this,
            focusTask = me.focusTask,
            owner = me.getRefOwner(),
            // Do not autofocus the Component (which delegates onto the getFocusEl() descendant)
            // for touch events.
            preventFocus = e.pointerType === 'touch',
            target, dom, skipFronting;
        if (me.floating && (// get out of here if there is already a pending focus.  This usually means
        // that the handler for a mousedown on a child element set the focus on some
        // other component, and we so not want to steal it back. See EXTJSIV-9458
        !focusTask || !focusTask.id)) {
            // If focus is already within this floating hierarchy,
            // then do not disturb it on mousedown.
            if (me.owns(Ext.Element.getActiveElement())) {
                // Indicate that we want the component to be
                // considered as a focus target but no
                preventFocus = {
                    ownsFocus: true
                };
            }
            target = e.target;
            dom = me.el.dom;
            // loop the target's ancestors to see if we clicked on a focusable element
            // or a descendant of a focusable element,  If so we don't want to focus
            // this floating component. If we end up with no target, it probably means
            // it's been removed from the DOM, so we should attempt to bring ourselves
            // to front anyway
            while (!preventFocus && target && target !== dom) {
                if (Ext.fly(target).isFocusable()) {
                    preventFocus = {
                        ownsFocus: true
                    };
                }
                target = target.parentNode;
            }
            // We can skip toFront() if we're already active and the click was
            // within our element but not on something focusable.
            skipFronting = Ext.WindowManager.getActive() === me && (target === dom || preventFocus);
            // If what was mousedowned upon is going to claim focus anyway, pass
            // preventFocus as true.
            if (!skipFronting) {
                me.toFront(preventFocus);
            }
            // If we have not hit a focusable element, and our owner
            // contains focus, then prevent the default action of mousedown (focus movement)
            if (!preventFocus && owner && owner.containsFocus) {
                e.preventDefault();
            }
        }
    },
    onBeforeFloatLayout: function() {
        this.el.preventSync = true;
    },
    onAfterFloatLayout: function() {
        var el = this.el;
        if (el.shadow || el.shim) {
            // An element's underlays (shadow and shim) are automatically synced in response
            // to any calls to Ext.Element APIs that change the element's size or position
            // (setXY, setWidth, etc).  Since the layout system bypasses these APIs and
            // sets the element's styles directly, we need to trigger a sync now.
            // We need to show shadows only if el.getData().isVisible is not explicitly 
            // set to be false
            el.setUnderlaysVisible(el.getData().isVisible !== false);
            el.syncUnderlays();
        }
    },
    /**
     * synchronizes the hidden state of this component with the state of its hierarchy
     * @private
     */
    syncHidden: function() {
        var me = this,
            hidden = me.hidden || !me.rendered,
            hierarchicallyHidden = me.hierarchicallyHidden = me.isHierarchicallyHidden(),
            pendingShow = me.pendingShow;
        if (hidden !== hierarchicallyHidden) {
            if (hierarchicallyHidden) {
                me.hide();
                me.pendingShow = true;
            } else if (pendingShow) {
                delete me.pendingShow;
                if (pendingShow.length) {
                    me.show.apply(me, pendingShow);
                } else {
                    me.show();
                }
            }
        }
    },
    /**
     * @private
     * z-index is managed by the zIndexManager and may be overwritten at any time.
     * Returns the next z-index to be used.
     *
     * If this is a Container, then it will have rebased any managed floating Components,
     * and so the next available z-index will be approximately 10000 above that.
     */
    setZIndex: function(index) {
        var me = this;
        me.el.setZIndex(index);
        // Next item goes 10 above;
        index += 10;
        // When a Container with floating descendants has its z-index set,
        // it rebases any floating descendants it is managing.
        // The returned value is a round number approximately 10000 above the last z-index used.
        if (me.floatingDescendants) {
            index = Math.floor(me.floatingDescendants.setBase(index) / 100) * 100 + 10000;
        }
        return index;
    },
    /**
     * Moves this floating Component into a constrain region.
     *
     * By default, this Component is constrained to be within the container it was added to,
     * or the element it was rendered to.
     *
     * An alternative constraint may be passed.
     * @param {String/HTMLElement/Ext.dom.Element/Ext.util.Region} [constrainTo] The Element
     * or {@link Ext.util.Region Region} into which this Component is to be constrained.
     * Defaults to the element into which this floating Component was rendered.
     */
    doConstrain: function(constrainTo) {
        var me = this,
            // Calculate the constrained position.
            // calculateConstrainedPosition will provide a default constraint
            // region if there is no explicit constrainTo, *and* there is no floatParent
            // owner Component.
            xy = me.calculateConstrainedPosition(constrainTo, null, true);
        // false is returned if no movement is needed
        if (xy) {
            me.setPosition(xy);
        }
    },
    updateActiveCounter: function(activeCounter) {
        var me = this,
            zim = me.zIndexParent;
        // If we have a zIndexParent, it has to rebase its own zIndices
        if (zim && zim !== zim.zIndexManager.front && me.bringParentToFront !== false) {
            zim.setActiveCounter(++Ext.ZIndexManager.activeCounter);
        }
        // Rebase the local zIndices
        zim = me.zIndexManager;
        if (zim) {
            zim.onComponentUpdate(me);
        }
    },
    updateAlwaysOnTop: function(alwaysOnTop) {
        var z = this.zIndexManager;
        // Rebase the local zIndices
        if (z) {
            z.onComponentUpdate(this);
        }
    },
    /**
     * Brings this floating Component to the front of any other visible, floating Components
     * managed by the same {@link Ext.ZIndexManager ZIndexManager}
     *
     * If this Component is modal, inserts the modal mask just below this Component
     * in the z-index stack.
     *
     * @param {Boolean} [preventFocus=false] Specify `true` to prevent the Component
     * from being focused.
     * @return {Ext.Component} this
     */
    toFront: function(preventFocus) {
        var me = this;
        // ZIndexManager#onCollectionSort will call setActive if this component ends up on the top.
        // That will focus it if we have been requested to do so.
        if (me.zIndexManager.bringToFront(me, preventFocus || !me.focusOnToFront)) {
            if (me.hasListeners.tofront) {
                me.fireEvent('tofront', me, me.el.getZIndex());
            }
        }
        return me;
    },
    /**
     * @private
     * This method is called internally by {@link Ext.ZIndexManager} to signal that a floating
     * Component has either been moved to the top of its zIndex stack, or pushed from the top
     * of its zIndex stack.
     *
     * If a _Window_ is superceded by another Window, deactivating it hides its shadow.
     *
     * This method also fires the {@link Ext.Component#activate activate} or
     * {@link Ext.Component#deactivate deactivate} event depending on which action occurred.
     *
     * @param {Boolean} [isTopMost=false] True to activate the Component, false to deactivate it.
     */
    onZIndexChange: function(isTopMost) {
        var me = this;
        if (isTopMost) {
            // Check the element's visible state. Might be clipped to hide but
            // be accessible. Do not show a shadow.
            if (me.el.shadow && me.el.getData().isVisible !== false && !me.maximized) {
                me.el.enableShadow(null, true);
            }
            me.fireEvent('activate', me);
        } else // Deactivate carries no operations. It may be that this component has just moved down
        // and another component has been brought to the top, so that will automatically
        // receive focus.
        // If we have been hidden, Component#onHide handles reverting focus
        // to the previousExternalFocus element.
        {
            me.fireEvent('deactivate', me);
        }
    },
    /**
     * @private
     * This method is called internally by {@link Ext.ZIndexManager} to signal that a
     * focusable floating Component has become the topost focusable in its zIndex stack.
     */
    onFocusTopmost: function() {
        var activeCmp = Ext.ComponentManager.getActiveComponent();
        // Skip focusing if we already contain focused element
        if (!activeCmp || !activeCmp.up(this)) {
            this.focus();
        }
    },
    /**
     * Sends this Component to the back of (lower z-index than) any other visible windows
     * @return {Ext.Component} this
     */
    toBack: function() {
        this.zIndexManager.sendToBack(this);
        return this;
    },
    /**
     * Center this Component in its container.
     * @return {Ext.Component} this
     */
    center: function() {
        var me = this,
            parent = me.floatParent,
            xy;
        if (me.isVisible()) {
            xy = me.getAlignToXY(parent ? parent.getTargetEl() : me.container, 'c-c');
            me.setPagePosition(xy);
        } else {
            me.needsCenter = true;
        }
        return me;
    },
    onFloatShow: function() {
        var me = this,
            target = me._lastAlignTarget;
        if (me.needsCenter) {
            me.center();
        } else if (target) {
            if (target.destroyed) {
                me._lastAlignTarget = null;
            } else {
                // Anchor to the target. Do not track scroll if we are position:fixed
                me.alignTo(target, me._lastAlignToPos, me._lastAlignToOffsets, false, !me.fixed);
            }
        }
        me.needsCenter = false;
    },
    /**
     * @private
     */
    fitContainer: function(animate) {
        var me = this,
            parent = me.floatParent,
            container = parent ? parent.getTargetEl() : me.container,
            newBox = container.getViewSize(),
            newPosition = parent || (container.dom !== document.body) ? // If we are a contained floater, or rendered to a div, maximized position is (0,0)
            [
                0,
                0
            ] : // If no parent and rendered to body, align with origin of container el.
            container.getXY();
        newBox.x = newPosition[0];
        newBox.y = newPosition[1];
        me.setBox(newBox, animate);
    },
    privates: {
        onFloatDestroy: function() {
            var me = this,
                fly = me.alignTargetFly;
            if (me.hierarchyEventListeners) {
                me.hierarchyEventListeners.destroy();
                me.hierarchyEventListeners = null;
            }
            me.clearAlignEl();
            if (fly) {
                // We only want to destroy the instance, but leave the element intact
                fly.detach();
                fly.destroy();
            }
        },
        /**
         * Gets the topmost *non floating* alignTo target if there are multiple aligns
         * such as a menu stack hanging off a button or grid column header.
         * @return {Ext.Element/Ext.Component} The topmost, *non floating* alignTo target.
         * @private
         */
        getTopAlignTarget: function() {
            var next = this._lastAlignTarget,
                result = next;
            // Track up through aligned floaters until we hit a root element or non-floater.
            while (next && (result = next) && next.isComponent) {
                // If we hit a floater, try its alignTarget next
                if (result.isFloating()) {
                    next = result._lastAlignTarget;
                } else // If a static component, see if it's owned by a floater
                // and try its alignTarget if so.
                {
                    next = result.up('{isFloating()}');
                    next = next && next._lastAlignTarget;
                }
            }
            return result;
        },
        clearAlignEl: function() {
            var me = this;
            if (me._lastAlignTarget) {
                me.alignListeners = Ext.destroy(me.alignListeners);
                Ext.un('scroll', me.doRealign, me);
                me._lastAlignToPos = me._lastAlignTarget = me._lastAlignToOffsets = me._topAlignTarget = null;
            }
        },
        alignOnResize: function() {
            this.doRealign();
        },
        doRealign: function(scroller) {
            var me = this,
                alignEl = me._lastAlignTarget.el,
                destroyed, dom, anchorXY, myXY;
            // Only react if we are visible.
            // onFloatShow realigns upon show.
            if (alignEl && me.isVisible()) {
                // Ensure we always have an Ext.Element as our alignEl.
                // We may be aligned to a Component, an Ext.Element, or an raw HtmlElement
                if (alignEl.isComponent) {
                    destroyed = alignEl.destroyed;
                } else {
                    dom = alignEl.dom;
                    destroyed = !dom || Ext.isGarbage(dom);
                }
                // If the Component/Element we were aligning to is destroyed, clear our alignment.
                if (destroyed) {
                    me.clearAlignEl();
                }
                // Realign only if
                //      the topmost align target is within the scrolling element
                //      (it has scrolled with the content)
                //     AND our element is NOT within the scrolled element
                //     (it would move with the scroll)
                else if (!scroller || (scroller.getElement().contains(me._topAlignTarget.el) && !scroller.getElement().contains(me.el))) {
                    anchorXY = alignEl.getXY();
                    myXY = [
                        anchorXY[0] + me.alignVector[0],
                        anchorXY[1] + me.alignVector[1]
                    ];
                    me.setXY(myXY);
                    // Clip to the boundaries of the scroller which is moving us
                    if (scroller) {
                        me.clipToScroller(scroller);
                    }
                }
            }
        },
        /**
         * Clips this floating element to the scrolling element in line with how
         * its topmost anchoring element is clipped.
         * @private
         */
        clipToScroller: function(scroller) {
            var me = this,
                anchorBox = me._topAlignTarget.getBox(),
                scrollerBox = scroller.getElement().getConstrainRegion(),
                sides = 0;
            // Clip this to match the clippig of the anchor target
            if (anchorBox.top < scrollerBox.top) {
                sides = 1;
            }
            if (anchorBox.right > scrollerBox.right) {
                sides = sides | 2;
            }
            if (anchorBox.bottom > scrollerBox.bottom) {
                sides = sides | 4;
            }
            if (anchorBox.left < scrollerBox.left) {
                sides = sides | 8;
            }
            if (sides) {
                me.clipTo(scrollerBox, sides);
            } else {
                me.clearClip();
            }
        }
    }
});

/**
 * This mixin enables classes to declare relationships to child elements and provides the
 * mechanics for acquiring the {@link Ext.dom.Element elements} and storing them on an object
 * instance as properties.
 *
 * This class is used by {@link Ext.Component components} and
 * {@link Ext.layout.container.Container container layouts} to manage their child elements.
 * 
 * A typical component that uses these features might look something like this:
 * 
 *      Ext.define('Ext.ux.SomeComponent', {
 *          extend: 'Ext.Component',
 *          
 *          childEls: [
 *              'bodyEl'
 *          ],
 *          
 *          renderTpl: [
 *              '<div id="{id}-bodyEl" data-ref="bodyEl"></div>'
 *          ],
 *          
 *          // ...
 *      });
 * 
 * The `{@link #childEls}` config lists one or more relationships to child elements managed
 * by the component. The items in this array can be objects that more fully specify the
 * child. For example, the above could have used this instead to achieve the same result:
 *
 *      childEls: [
 *          { name: 'bodyEl', itemId: 'bodyEl' }
 *      ]
 *
 *
 * Unlike a `renderTpl` where there is a single value for an instance, `childEls` are aggregated
 * up the class hierarchy so that they are effectively inherited. In other words, if a
 * class where to derive from `Ext.ux.SomeComponent` in the example above, it could also
 * have a `childEls` property in the same way as `Ext.ux.SomeComponent`.
 * 
 *      Ext.define('Ext.ux.AnotherComponent', {
 *          extend: 'Ext.ux.SomeComponent',
 *          
 *          childEls: [
 *              // 'bodyEl' is inherited
 *              'innerEl'
 *          ],
 *          
 *          renderTpl: [
 *              '<div id="{id}-bodyEl" data-ref="bodyEl">'
 *                  '<div id="{id}-innerEl" data-ref="innerEl"></div>'
 *              '</div>'
 *          ],
 *          
 *          // ...
 *      });
 *
 * **IMPORTANT**
 * The `renderTpl` contains both child elements and unites them in the desired markup, but
 * the `childEls` only contains the new child element. The `data-ref` attribute must be
 * rendered on to child elements that do not use `select` or `selectNode` options. This
 * is done for performance reasons on IE8 where element lookup (even by id) is not very
 * efficient.
 * 
 * @private
 */
Ext.define('Ext.util.ElementContainer', {
    mixinId: 'elementCt',
    config: {
        /**
         * @cfg {Object/String[]/Object[]} childEls
         * The canonical form of `childEls` is an object keyed by child's property name
         * with values that are objects with the following properties.
         *
         * - `itemId` - The id to combine with the Component's id that is the id of the
         *   child element.
         * - `id` - The id of the child element.
         * - `leaf` - Set to `true` to ignore content when scanning for childEls. This
         *  should be set on things like the generated content for an `Ext.view.View`.
         * - `select`: A selector that will be passed to {@link Ext.dom.Element#method-select}.
         * - `selectNode`: A selector that will be passed to
         * {@link Ext.dom.Element#method-selectNode}.
         *
         * For example:
         *
         *      childEls: {
         *          button: true,
         *          buttonText: 'text',
         *          buttonImage: {
         *              itemId: 'image'
         *          }
         *      }
         *
         * The above is translated into the following complete form:
         *
         *      childEls: {
         *          button: {
         *              name: 'button',
         *              itemId: 'button'
         *          },
         *          buttonText: {
         *              name: 'buttonText',
         *              itemId: 'text'
         *          },
         *          buttonImage: {
         *              name: 'buttonImage',
         *              itemId: 'image'
         *          }
         *      }
         *
         * The above can be provided as an array like so:
         *
         *      childEls: [
         *          'button',
         *          { name: 'buttonText', itemId: 'text' },
         *          { name: 'buttonImage', itemId: 'image' }
         *      }
         *
         * For example, a Component which renders a title and body text:
         *
         *     @example
         *     Ext.create('Ext.Component', {
         *         renderTo: Ext.getBody(),
         *         renderTpl: [
         *             '<h1 id="{id}-title" data-ref="title">{title}</h1>',
         *             '<p>{msg}</p>',
         *         ],
         *         renderData: {
         *             title: "Error",
         *             msg: "Something went wrong"
         *         },
         *         childEls: ["title"],
         *         listeners: {
         *             afterrender: function(cmp){
         *                 // After rendering the component will have a title property
         *                 cmp.title.setStyle({color: "red"});
         *             }
         *         }
         *     });
         * 
         * **Note:** `childEl`s in the {@link Ext.Component#cfg-renderTpl renderTpl} 
         * must be referenced in a **data-ref** attribute.  Notice in the above example 
         * that the "title" `childEl` is set in the `renderTpl` using 
         * **data-ref="title"**.
         *
         * When using `select`, the property will be an instance of {@link Ext.CompositeElement}.
         * In all other cases, the property will be an {@link Ext.dom.Element} or `null`
         * if not found.
         *
         * Care should be taken when using `select` or `selectNode` to find child elements.
         * The following issues should be considered:
         *
         * - Performance: using selectors can be 10x slower than id lookup.
         * - Over-selecting: selectors are applied after the DOM elements for all children
         *   have been rendered, so selectors can match elements from child components
         *   (including nested versions of the same component) accidentally.
         *
         * This above issues are most important when using `select` since it returns multiple
         * elements.
         */
        childEls: {
            $value: {},
            cached: true,
            lazy: true,
            merge: function(newValue, oldValue, target, mixinClass) {
                var childEls = oldValue ? Ext.Object.chain(oldValue) : {},
                    i, val;
                // We'd use mergeSets except it assumes array elements are just names.
                if (newValue instanceof Array) {
                    for (i = newValue.length; i--; ) {
                        val = newValue[i];
                        if (!mixinClass || !(val in childEls)) {
                            if (typeof val === 'string') {
                                childEls[val] = {
                                    name: val,
                                    itemId: val
                                };
                            } else {
                                childEls[val.name] = val;
                            }
                        }
                    }
                } else if (newValue) {
                    if (newValue.constructor === Object) {
                        for (i in newValue) {
                            if (!mixinClass || !(i in childEls)) {
                                val = newValue[i];
                                if (val === true) {
                                    childEls[i] = {
                                        itemId: i
                                    };
                                } else if (typeof val === 'string') {
                                    childEls[i] = {
                                        itemId: val
                                    };
                                } else {
                                    childEls[i] = val;
                                    if (!('itemId' in val)) {
                                        val.itemId = i;
                                    }
                                }
                                childEls[i].name = i;
                            }
                        }
                    } else {
                        if (!mixinClass || !(newValue in childEls)) {
                            childEls[newValue] = {
                                name: newValue,
                                itemId: newValue
                            };
                        }
                    }
                }
                return childEls;
            }
        }
    },
    destroy: function() {
        var me = this,
            childEls = me.getChildEls(),
            child, childName;
        for (childName in childEls) {
            child = me[childName];
            if (child) {
                if (child.destroy) {
                    child.component = null;
                    child.destroy();
                }
                me[childName] = null;
            }
        }
    },
    privates: {
        /**
         * Add a childEl specific to this instance. This must be called before render.
         * @param childEl
         * @private
         * @since 6.0.0
         */
        addChildEl: function(childEl) {
            var me = this,
                childEls = me.getChildEls();
            if (!me.hasOwnProperty('childEls')) {
                me.childEls = childEls = Ext.Object.chain(childEls);
            }
            if (typeof childEl === 'string') {
                childEl = {
                    name: childEl,
                    itemId: childEl
                };
            }
            childEls[childEl.name] = childEl;
        },
        /**
         * Called after the mixin is applied. We need to see if `childEls` were used by
         * the `targetClass` and apply them to the config.
         * @param {Ext.Class} targetClass
         * @private
         */
        afterClassMixedIn: function(targetClass) {
            // When we are mixed in the targetClass may already have specified childEls,
            // so check the prototype for any...
            var proto = targetClass.prototype,
                childEls = proto.childEls;
            if (childEls) {
                delete proto.childEls;
                targetClass.getConfigurator().add({
                    childEls: childEls
                });
            }
        },
        /**
         * Sets references to elements inside the component.
         * @private
         */
        attachChildEls: function(el, owner) {
            var me = this,
                childEls = me.getChildEls(),
                comp = owner || me,
                // fyi - we are also used by layouts
                baseId = comp.id + '-',
                unframed = !comp.frame,
                childName, elements, entry, k, selector, value, id;
            for (childName in childEls) {
                // hasOwnProperty is a no-go here since we use prototype chains...
                entry = childEls[childName];
                if (unframed && entry.frame) {
                    
                    continue;
                }
                selector = entry.select;
                if (selector) {
                    value = el.select(selector, true);
                }
                // a CompositeElement
                else if (!(selector = entry.selectNode)) {
                    if (!(id = entry.id)) {
                        // With a normal childEl we want to rely on data-ref to populate
                        // the cache and *not* use getById since that should never find
                        // anything we don't already know about.
                        id = baseId + entry.itemId;
                        value = Ext.cache[id];
                    } else // || el.getById(id);
                    {
                        // With a specified id we may not be so lucky, so check the cache
                        // first but then fallback to getById.
                        value = Ext.cache[id] || el.getById(id);
                    }
                } else {
                    value = el.selectNode(selector, false);
                }
                if (value) {
                    if (value.isElement) {
                        value.component = comp;
                    } else if (value.isComposite && !value.isLite) {
                        elements = value.elements;
                        for (k = elements.length; k--; ) {
                            elements[k].component = comp;
                        }
                    }
                }
                me[childName] = value || null;
            }
        }
    }
});

/**
 * Given a component hierarchy of this:
 *
 *      {
 *          xtype: 'panel',
 *          id: 'ContainerA',
 *          layout: 'hbox',
 *          renderTo: Ext.getBody(),
 *          items: [
 *              {
 *                  id: 'ContainerB',
 *                  xtype: 'container',
 *                  items: [
 *                      { id: 'ComponentA' }
 *                  ]
 *              }
 *          ]
 *      }
 *
 * The rendering of the above proceeds roughly like this:
 *
 *  - ContainerA's initComponent calls #render passing the `renderTo` property as the
 *    container argument.
 *  - `render` calls the `getRenderTree` method to get a complete {@link Ext.dom.Helper} spec.
 *  - `getRenderTree` fires the "beforerender" event and calls the #beforeRender
 *    method. Its result is obtained by calling #getElConfig.
 *  - The #getElConfig method uses the `renderTpl` and its render data as the content
 *    of the `autoEl` described element.
 *  - The result of `getRenderTree` is passed to {@link Ext.dom.Helper#append}.
 *  - The `renderTpl` contains calls to render things like docked items, container items
 *    and raw markup (such as the `html` or `tpl` config properties). These calls are to
 *    methods added to the {@link Ext.XTemplate} instance by #setupRenderTpl.
 *  - The #setupRenderTpl method adds methods such as `renderItems`, `renderContent`, etc.
 *    to the template. These are directed to "doRenderItems", "doRenderContent" etc..
 *  - The #setupRenderTpl calls traverse from components to their {@link Ext.layout.Layout}
 *    object.
 *  - When a container is rendered, it also has a `renderTpl`. This is processed when the
 *    `renderContainer` method is called in the component's `renderTpl`. This call goes to
 *    Ext.layout.container.Container#doRenderContainer. This method repeats this
 *    process for all components in the container.
 *  - After the top-most component's markup is generated and placed in to the DOM, the next
 *    step is to link elements to their components and finish calling the component methods
 *    `onRender` and `afterRender` as well as fire the corresponding events.
 *  - The first step in this is to call #finishRender. This method descends the
 *    component hierarchy and calls `onRender` and fires the `render` event. These calls
 *    are delivered top-down to approximate the timing of these calls/events from previous
 *    versions.
 *  - During the pass, the component's `el` is set. Likewise, the `renderSelectors` and
 *    `childEls` are applied to capture references to the component's elements.
 *  - These calls are also made on the {@link Ext.layout.container.Container} layout to
 *    capture its elements. Both of these classes use {@link Ext.util.ElementContainer} to
 *    handle `childEls` processing.
 *
 * @private
 */
Ext.define('Ext.util.Renderable', {
    mixinId: 'renderable',
    requires: [
        'Ext.dom.Element'
    ],
    frameIdRegex: /[-]frame\d+[TMB][LCR]$/,
    frameCls: Ext.baseCSSPrefix + 'frame',
    frameElNames: [
        'TL',
        'TC',
        'TR',
        'ML',
        'MC',
        'MR',
        'BL',
        'BC',
        'BR',
        'Table'
    ],
    /* eslint-disable indent, max-len */
    frameTpl: [
        '<tpl if="hasTabGuard">{% this.renderTabGuard(out, values, \'before\'); %}</tpl>',
        '<tpl if="top">',
        '<tpl if="left"><div id="{fgid}TL" data-ref="frameTL" class="{frameCls}-tl {baseCls}-tl {baseCls}-{ui}-tl<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-tl</tpl>{frameElCls}" role="presentation"></tpl>',
        '<tpl if="right"><div id="{fgid}TR" data-ref="frameTR" class="{frameCls}-tr {baseCls}-tr {baseCls}-{ui}-tr<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-tr</tpl>{frameElCls}" role="presentation"></tpl>',
        '<div id="{fgid}TC" data-ref="frameTC" class="{frameCls}-tc {baseCls}-tc {baseCls}-{ui}-tc<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-tc</tpl>{frameElCls}" role="presentation"></div>',
        '<tpl if="right"></div></tpl>',
        '<tpl if="left"></div></tpl>',
        '</tpl>',
        '<tpl if="left"><div id="{fgid}ML" data-ref="frameML" class="{frameCls}-ml {baseCls}-ml {baseCls}-{ui}-ml<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-ml</tpl>{frameElCls}" role="presentation"></tpl>',
        '<tpl if="right"><div id="{fgid}MR" data-ref="frameMR" class="{frameCls}-mr {baseCls}-mr {baseCls}-{ui}-mr<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-mr</tpl>{frameElCls}" role="presentation"></tpl>',
        '<div id="{fgid}Body" data-ref="frameBody" class="{frameBodyCls} {frameCls}-mc {baseCls}-mc {baseCls}-{ui}-mc<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-mc</tpl>{frameElCls}" role="presentation">',
        '{%this.applyRenderTpl(out, values)%}',
        '</div>',
        '<tpl if="right"></div></tpl>',
        '<tpl if="left"></div></tpl>',
        '<tpl if="bottom">',
        '<tpl if="left"><div id="{fgid}BL" data-ref="frameBL" class="{frameCls}-bl {baseCls}-bl {baseCls}-{ui}-bl<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-bl</tpl>{frameElCls}" role="presentation"></tpl>',
        '<tpl if="right"><div id="{fgid}BR" data-ref="frameBR" class="{frameCls}-br {baseCls}-br {baseCls}-{ui}-br<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-br</tpl>{frameElCls}" role="presentation"></tpl>',
        '<div id="{fgid}BC" data-ref="frameBC" class="{frameCls}-bc {baseCls}-bc {baseCls}-{ui}-bc<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-bc</tpl>{frameElCls}" role="presentation"></div>',
        '<tpl if="right"></div></tpl>',
        '<tpl if="left"></div></tpl>',
        '</tpl>',
        '<tpl if="hasTabGuard">{% this.renderTabGuard(out, values, \'after\'); %}</tpl>'
    ],
    frameTableTpl: [
        '<tpl if="hasTabGuard">{% this.renderTabGuard(out, values, \'before\'); %}</tpl>',
        '<table id="{fgid}Table" data-ref="frameTable" class="{frameCls} ',
        Ext.baseCSSPrefix + 'table-plain" cellpadding="0" role="presentation">',
        '<tpl if="top">',
        '<tr role="presentation">',
        '<tpl if="left"><td id="{fgid}TL" data-ref="frameTL" class="{frameCls}-tl {baseCls}-tl {baseCls}-{ui}-tl<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-tl</tpl>{frameElCls}" role="presentation"></td></tpl>',
        '<td id="{fgid}TC" data-ref="frameTC" class="{frameCls}-tc {baseCls}-tc {baseCls}-{ui}-tc<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-tc</tpl>{frameElCls}" role="presentation"></td>',
        '<tpl if="right"><td id="{fgid}TR" data-ref="frameTR" class="{frameCls}-tr {baseCls}-tr {baseCls}-{ui}-tr<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-tr</tpl>{frameElCls}" role="presentation"></td></tpl>',
        '</tr>',
        '</tpl>',
        '<tr role="presentation">',
        '<tpl if="left"><td id="{fgid}ML" data-ref="frameML" class="{frameCls}-ml {baseCls}-ml {baseCls}-{ui}-ml<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-ml</tpl>{frameElCls}" role="presentation"></td></tpl>',
        '<td id="{fgid}Body" data-ref="frameBody" class="{frameBodyCls} {frameCls}-mc {baseCls}-mc {baseCls}-{ui}-mc<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-mc</tpl>{frameElCls}" style="{mcStyle}" role="presentation">',
        '{%this.applyRenderTpl(out, values)%}',
        '</td>',
        '<tpl if="right"><td id="{fgid}MR" data-ref="frameMR" class="{frameCls}-mr {baseCls}-mr {baseCls}-{ui}-mr<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-mr</tpl>{frameElCls}" role="presentation"></td></tpl>',
        '</tr>',
        '<tpl if="bottom">',
        '<tr role="presentation">',
        '<tpl if="left"><td id="{fgid}BL" data-ref="frameBL" class="{frameCls}-bl {baseCls}-bl {baseCls}-{ui}-bl<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-bl</tpl>{frameElCls}" role="presentation"></td></tpl>',
        '<td id="{fgid}BC" data-ref="frameBC" class="{frameCls}-bc {baseCls}-bc {baseCls}-{ui}-bc<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-bc</tpl>{frameElCls}" role="presentation"></td>',
        '<tpl if="right"><td id="{fgid}BR" data-ref="frameBR" class="{frameCls}-br {baseCls}-br {baseCls}-{ui}-br<tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-br</tpl>{frameElCls}" role="presentation"></td></tpl>',
        '</tr>',
        '</tpl>',
        '</table>',
        '<tpl if="hasTabGuard">{% this.renderTabGuard(out, values, \'after\'); %}</tpl>'
    ],
    /* eslint-enable indent, max-len */
    /**
     * @property {Number} _renderState
     * This property holds one of the following values during the render process:
     *
     *   * **0** - The component is not rendered.
     *   * **1** - The component has fired beforerender and is about to call beforeRender.
     *    The component has just started rendering.
     *   * **2** - The component has finished the `beforeRender` process and is about to
     *    call `onRender`. This is when `rendering` is set to `true`.
     *   * **3** - The component has started `onRender`. This is when `rendered` is set
     *    to `true`.
     *   * **4** - The component has finished its afterrender process.
     *
     * @private
     * @readonly
     * @since 5.0.0
     */
    _renderState: 0,
    _layerCls: Ext.baseCSSPrefix + 'layer',
    _fixedLayerCls: Ext.baseCSSPrefix + 'fixed-layer',
    // Some components will have such roles that do not actively participate
    // in user interaction, and thus do not need their ARIA attributes updated
    ariaStaticRoles: {
        presentation: true,
        article: true,
        definition: true,
        directory: true,
        document: true,
        img: true,
        heading: true,
        math: true,
        note: true,
        banner: true,
        complementary: true,
        contentinfo: true,
        navigation: true,
        search: true,
        // When a role is not defined it is akin to static
        'undefined': true,
        'null': true
    },
    statics: {
        makeRenderSetter: function(cfg, renderState) {
            var name = cfg.name;
            return function(value) {
                var me = this,
                    bucket = (me.renderConfigs || (me.renderConfigs = {})),
                    pending = bucket[renderState];
                if (me._renderState >= renderState) {
                    (cfg.setter || cfg.getSetter()).call(me, value);
                } else {
                    if (!pending) {
                        bucket[renderState] = pending = {};
                    }
                    if (!(name in pending)) {
                        pending[name] = me[name];
                    }
                    me[name] = value;
                }
                return me;
            };
        },
        processRenderConfig: function(source, configName, state) {
            // Even though this is not inheritableState, our onClassExtended adds it to
            // all derived classes so that our "this" pointer is the derived class.
            var proto = this.prototype,
                configurator = this.getConfigurator(),
                Renderable = Ext.util.Renderable,
                makeSetter = Renderable.makeRenderSetter,
                renderConfig = source[configName],
                cachedSetter, cfg, name, setterName;
            for (name in renderConfig) {
                cfg = Ext.Config.get(name);
                if (!proto[setterName = cfg.names.set]) {
                    cachedSetter = (cfg.renderSetter || (cfg.renderSetter = {}));
                    proto[setterName] = cachedSetter[state] || (cachedSetter[state] = makeSetter(cfg, state));
                }
            }
            delete source[configName];
            configurator.add(renderConfig);
        }
    },
    onClassMixedIn: function(targetClass) {
        var override = targetClass.override,
            processRenderConfig = this.processRenderConfig,
            processOverride = function(body) {
                if (body.beforeRenderConfig) {
                    this.processRenderConfig(body, 'beforeRenderConfig', 1);
                }
                if (body.renderConfig) {
                    this.processRenderConfig(body, 'renderConfig', 3);
                }
                override.call(this, body);
            },
            processClass = function(theClass, classBody) {
                // We need to process overrides for renderConfig declarations:
                theClass.override = processOverride;
                // While we are here we add this method (an inheritableStatic basically)
                theClass.processRenderConfig = processRenderConfig;
                if (classBody.beforeRenderConfig) {
                    theClass.processRenderConfig(classBody, 'beforeRenderConfig', 1);
                }
                if (classBody.renderConfig) {
                    theClass.processRenderConfig(classBody, 'renderConfig', 3);
                }
            };
        // Process Component itself.
        processClass(targetClass, targetClass.prototype);
        // And apply to all Component-derived classes as well:
        targetClass.onExtended(processClass);
    },
    /**
     * Allows additional behavior after rendering is complete. At this stage, the 
     * {@link Ext.Component Component's} {@link Ext.Component#getEl Element} will have 
     * been styled according to the configuration, will have had any configured CSS 
     * class names added, and will be in the configured visibility and configured enable 
     * state.
     * 
     * **Note:** If the Component has a {@link Ext.Component#controller ViewController} 
     * and the controller has an {@link Ext.app.ViewController#afterRender afterRender} 
     * method it will be called passing the Component as the single param.
     *
     * @template
     * @protected
     */
    afterRender: function() {
        var me = this,
            data = {},
            protoEl = me.protoEl,
            target = me.el,
            controller, item, pre, hidden, contentEl;
        me.finishRenderChildren();
        me._renderState = 4;
        // We need to do the contentEl here because it depends on the layout items (inner/outerCt)
        // to be rendered before we can put it in
        if (me.contentEl) {
            pre = Ext.baseCSSPrefix;
            hidden = pre + 'hidden-';
            contentEl = me.contentEl = Ext.get(me.contentEl);
            contentEl.component = me;
            contentEl.removeCls([
                pre + 'hidden',
                hidden + 'display',
                hidden + 'offsets'
            ]);
            me.getContentTarget().appendChild(contentEl.dom);
        }
        protoEl.writeTo(data);
        // Here we apply any styles that were set on the protoEl during the rendering phase
        // A majority of times this will not happen, but we still need to handle it
        item = data.removed;
        if (item) {
            target.removeCls(item);
        }
        item = data.cls;
        if (item.length) {
            target.addCls(item);
        }
        item = data.style;
        if (data.style) {
            target.setStyle(item);
        }
        me.protoEl = null;
        // If this is the outermost Container, lay it out as soon as it is rendered.
        // Some Components like Ext.LoadMask will lay out themselves and some like
        // Ext.dd.StatusProxy will even render themselves to a detached document body,
        // so we allow them to opt out of the Ext layouts.
        if (!me.ownerCt && !me.skipLayout) {
            me.updateLayout();
        }
        if (!(me.x && me.y) && (me.pageX || me.pageY)) {
            me.setPagePosition(me.pageX, me.pageY);
        }
        if (me.disableOnRender) {
            me.onDisable();
        }
        controller = me.controller;
        if (controller && controller.afterRender) {
            controller.afterRender(me);
        }
        if (me.focusableContainer && me.initFocusableContainer) {
            me.initFocusableContainer();
        }
    },
    afterFirstLayout: function(width, height) {
        var me = this,
            x = me.x,
            y = me.y,
            alignSpec = me.defaultAlign,
            alignOffset = me.alignOffset,
            controller, hasX, hasY, pos, xy;
        // We only have to set absolute position here if there is no ownerlayout
        // which should take responsibility. Consider the example of rendered components
        // outside of a viewport - these might need their positions setting.
        if (!me.ownerLayout) {
            hasX = x !== undefined;
            hasY = y !== undefined;
        }
        // For floaters, calculate x and y if they aren't defined by aligning
        // the sized element to the center of either the container or the ownerCt
        if (me.floating && !me.preventDefaultAlign && (!hasX || !hasY)) {
            if (me.floatParent) {
                pos = me.floatParent.getTargetEl().getViewRegion();
                xy = me.el.getAlignToXY(me.alignTarget || me.floatParent.getTargetEl(), alignSpec, alignOffset);
                pos.x = xy[0] - pos.x;
                pos.y = xy[1] - pos.y;
            } else {
                xy = me.el.getAlignToXY(me.alignTarget || me.container, alignSpec, alignOffset);
                pos = me.el.translateXY(xy[0], xy[1]);
            }
            x = hasX ? x : pos.x;
            y = hasY ? y : pos.y;
            hasX = hasY = true;
        }
        if (hasX || hasY) {
            me.setPosition(x, y);
        }
        me.onBoxReady(width, height);
        controller = me.controller;
        if (controller && controller.boxReady) {
            controller.boxReady(me, width, height);
        }
    },
    /**
     * Allows additional behavior before rendering.
     * 
     * **Note:** If the Component has a {@link Ext.Component#controller ViewController} 
     * and the controller has a {@link Ext.app.ViewController#beforeRender beforeRender} 
     * method it will be called passing the Component as the single param.
     *
     * @template
     * @protected
     */
    beforeRender: function() {
        var me = this,
            floating = me.floating,
            layout = me.getComponentLayout(),
            cls = me.userCls,
            controller;
        me._renderState = 1;
        me.ariaUsesMainElement = me.ariaEl === 'el';
        controller = me.controller;
        if (controller && controller.beforeRender) {
            controller.beforeRender(me);
        }
        // Force bindings to be created
        me.initBindable();
        if (me.renderConfigs) {
            me.flushRenderConfigs();
        }
        if (cls) {
            me.addCls(cls);
        }
        if (floating) {
            me.addCls(me.fixed ? me._fixedLayerCls : me._layerCls);
            cls = floating.cls;
            if (cls) {
                me.addCls(cls);
            }
        }
        // Just before rendering, set the frame flag if we are an always-framed component
        // like Window or Tip.
        me.frame = me.frame || me.alwaysFramed;
        if (!layout.initialized) {
            layout.initLayout();
        }
        // Attempt to set overflow style prior to render if the targetEl can be accessed.
        // If the targetEl does not exist yet, this will take place in finishRender
        me.initOverflow();
        me.setUI(me.ui);
    },
    /**
     * @private
     * Called from the selected frame generation template to insert this Component's inner structure
     * inside the framing structure.
     *
     * When framing is used, a selected frame generation template is used as the primary template
     * of the #getElConfig instead of the configured {@link Ext.Component#renderTpl renderTpl}.
     * The renderTpl is invoked by this method which is injected into the framing template.
     */
    doApplyRenderTpl: function(out, values) {
        // Careful! This method is bolted on to the frameTpl so all we get for context is
        // the renderData! The "this" pointer is the frameTpl instance!
        var me = values.$comp,
            tpl;
        // Don't do this if the component is already rendered:
        if (!me.rendered) {
            tpl = me.initRenderTpl();
            tpl.applyOut(values.renderData, out);
        }
    },
    getElConfig: function() {
        var me = this,
            autoEl = me.autoEl,
            frameInfo = me.getFrameInfo(),
            config = {
                tag: 'div',
                tpl: frameInfo ? me.initFramingTpl(frameInfo.table) : me.initRenderTpl()
            },
            layoutTargetCls = me.layoutTargetCls,
            protoEl = me.protoEl,
            ariaRole = me.ariaRole,
            frameData;
        me.initStyles(protoEl);
        // If we're not framing, then just add to the element, otherwise we need to add
        // it to the frameBody, since it's our targetEl, but we don't have a handle on it yet.
        if (layoutTargetCls && !frameInfo) {
            protoEl.addCls(layoutTargetCls);
        }
        protoEl.writeTo(config);
        protoEl.flush();
        if (autoEl) {
            if (Ext.isString(autoEl)) {
                config.tag = autoEl;
            } else {
                Ext.apply(config, autoEl);
            }
        }
        if (ariaRole && me.ariaUsesMainElement) {
            config.role = ariaRole;
            if (!me.ariaStaticRoles[ariaRole]) {
                config['aria-hidden'] = !!me.hidden;
                config['aria-disabled'] = !!me.disabled;
                // ariaLabelledBy takes precedence
                if (me.ariaLabel && !me.ariaLabelledBy) {
                    config['aria-label'] = me.ariaLabel;
                }
                // We don't want to handle collapsibleness in subclasses
                if (me.collapsible) {
                    config['aria-expanded'] = !me.collapsed;
                }
                // In some cases we need to force some ARIA attributes
                // to be rendered on the ariaEl upfront, e.g. certain
                // state and decorator attributes. It is not semantically
                // correct to set these in ariaAttributes config, so
                // we're using ariaRenderAttributes instance config
                // when available.
                if (me.ariaRenderAttributes) {
                    Ext.apply(config, me.ariaRenderAttributes);
                }
                if (me.config.ariaAttributes) {
                    Ext.apply(config, me.getAriaAttributes());
                }
            }
        }
        // It's important to assign the id here as an autoEl.id could have been (wrongly)
        // applied and this would get things out of sync
        config.id = me.id;
        if (config.tpl) {
            // Use the framingTpl as the main content creating template.
            // It will call out to this.applyRenderTpl(out, values)
            if (frameInfo) {
                config.tplData = frameData = me.getFrameRenderData();
                frameData.renderData = me.initRenderData();
            } else {
                config.tplData = me.initRenderData();
            }
        }
        // After we have gathered all rendering information, this is no longer needed.
        me.ariaRenderAttributes = null;
        return config;
    },
    /**
     * This function takes the position argument passed to onRender and returns a
     * DOM element that you can use in the insertBefore.
     * @param {String/Number/Ext.dom.Element/HTMLElement} position Index, element id
     * or element you want to put this component before.
     * @return {HTMLElement} DOM element that you can use in the insertBefore
     */
    getInsertPosition: function(position) {
        // Convert the position to an element to insert before
        if (position !== undefined) {
            if (Ext.isNumber(position)) {
                position = this.container.dom.childNodes[position];
            } else {
                position = Ext.getDom(position);
            }
        }
        return position;
    },
    getRenderTree: function() {
        var me = this,
            ret = null;
        if (!me.hasListeners.beforerender || me.fireEvent('beforerender', me) !== false) {
            me._renderState = 1;
            me.beforeRender();
            // Flag to let the layout's finishRenderItems and afterFinishRenderItems
            // know which items to process
            me.rendering = true;
            me._renderState = 2;
            ret = me.getElConfig();
            if (me.el) {
                // Since we are producing a render tree, we produce a "proxy el" that will
                // sit in the rendered DOM precisely where me.el belongs. We replace the
                // proxy el in the finishRender phase.
                ret.id = me.$pid = Ext.id(null, me.el.identifiablePrefix);
            }
        }
        // ret['data-cmp'] = me.id;
        return ret;
    },
    /**
     * Initialized the renderData to be used when rendering the renderTpl.
     * @return {Object} Object with keys and values that are going to be applied to the renderTpl
     * @protected
     */
    initRenderData: function() {
        var me = this,
            ariaRole = me.ariaRole,
            data, ariaAttr;
        data = Ext.apply({
            $comp: me,
            id: me.id,
            ui: me.ui,
            uiCls: me.uiCls,
            baseCls: me.baseCls,
            componentCls: me.componentCls,
            frame: me.frame,
            hasTabGuard: !!me.tabGuard,
            scrollerCls: me.scrollerCls,
            childElCls: '',
            // overridden in RTL
            ariaEl: me.ariaEl
        }, me.renderData);
        // This code is similar (in fact, almost identical) to the one in getElConfig;
        // we duplicate it for performance reasons.
        if (ariaRole && !me.ariaUsesMainElement) {
            ariaAttr = {
                role: ariaRole
            };
            if (!me.ariaStaticRoles[ariaRole]) {
                ariaAttr['aria-hidden'] = !!me.hidden;
                ariaAttr['aria-disabled'] = !!me.disabled;
                // ariaLabelledBy takes precedence
                if (me.ariaLabel && !me.ariaLabelledBy) {
                    ariaAttr['aria-label'] = me.ariaLabel;
                }
                // We don't want to handle collapsibleness in subclasses
                if (me.collapsible) {
                    ariaAttr['aria-expanded'] = !me.collapsed;
                }
                if (me.ariaRenderAttributes) {
                    Ext.apply(ariaAttr, me.ariaRenderAttributes);
                }
                if (me.config.ariaAttributes) {
                    Ext.apply(ariaAttr, me.getAriaAttributes());
                }
            }
            data.ariaAttributes = ariaAttr;
        }
        return data;
    },
    /**
     * Template method called when this Component's DOM structure is created.
     *
     * At this point, this Component's (and all descendants') DOM structure *exists* but it has not
     * been layed out (positioned and sized).
     *
     * Subclasses which override this to gain access to the structure at render time should
     * call the parent class's method before attempting to access any child elements of the
     * Component.
     *
     * @param {Ext.dom.Element} parentNode The parent Element in which this Component's
     * encapsulating element is contained.
     * @param {Number} containerIdx The index within the parent Container's child collection
     * of this Component.
     *
     * @template
     * @protected
     */
    onRender: function(parentNode, containerIdx) {
        var me = this,
            x = me.x,
            y = me.y,
            lastBox = null,
            el = me.el,
            scroller = me.scrollable,
            width, height;
        me.applyRenderSelectors();
        // Update the scroller very early in first layout so that it has the overflow element
        // before any 'boxready', onResize, or 'resize' code gets to run.
        if (scroller && scroller.isScroller) {
            scroller.setElement(me.getOverflowEl());
            // IE browsers don't restore scroll position if the component was scrolled and
            // then hidden and shown again, so we must do it manually.
            // See EXTJS-16233.
            if (Ext.isIE) {
                me.showListenerIE = Ext.on('show', me.onGlobalShow, me, {
                    destroyable: true
                });
            }
        }
        // Flag set on getRenderTree to flag to the layout's postprocessing routine that
        // the Component is in the process of being rendered and needs postprocessing.
        me.rendering = null;
        me.rendered = true;
        me._renderState = 3;
        if (me.renderConfigs) {
            me.flushRenderConfigs();
        }
        // We need to remember these to avoid writing them during the initial layout:
        if (x != null) {
            lastBox = {
                x: x
            };
        }
        if (y != null) {
            (lastBox = lastBox || {}).y = y;
        }
        // Framed components need their width/height to apply to the frame, which is
        // best handled in layout at present.
        if (!me.getFrameInfo()) {
            width = me.width;
            height = me.height;
            if (typeof width === 'number') {
                lastBox = lastBox || {};
                lastBox.width = width;
            }
            if (typeof height === 'number') {
                lastBox = lastBox || {};
                lastBox.height = height;
            }
        }
        me.lastBox = el.lastBox = lastBox;
    },
    /**
     * Renders the Component into the passed HTML element.
     * 
     * **If you are using a {@link Ext.container.Container Container} object to house this
     * Component, then do not use the render method.**
     *
     * A Container's child Components are rendered by that Container's
     * {@link Ext.container.Container#layout layout} manager when the Container is first rendered.
     *
     * When creating complex UIs, it is important to remember that sizing and positioning
     * of child items is the responsibility of the Container's
     * {@link Ext.container.Container#layout layout} manager. If you expect child items to be sized
     * in response to user interactions, you must configure the Container with a layout manager
     * which creates and manages the type of layout you have in mind.
     *
     * **Omitting the Container's {@link Ext.Container#layout layout} config means that a basic
     * layout manager is used which does nothing but render child components sequentially into the
     * Container. No sizing or positioning will be performed in this situation.**
     *
     * @param {Ext.dom.Element/HTMLElement/String} [container] The element this Component should be
     * rendered into. If it is being created from existing markup, this should be omitted.
     * @param {String/Number} [position] The element ID or DOM node index within the container
     * **before** which this component will be inserted (defaults to appending to the end
     * of the container)
     */
    render: function(container, position) {
        var me = this,
            el = me.el,
            ownerLayout = me.ownerLayout,
            vetoed, tree, nextSibling;
        if (el && !el.isElement) {
            me.wrapPrimaryEl(el);
            // ensure me.el is wrapped
            el = me.el;
        }
        if (!me.skipLayout) {
            Ext.suspendLayouts();
        }
        container = me.initContainer(container);
        nextSibling = me.getInsertPosition(position);
        if (!el) {
            tree = me.getRenderTree();
            // calls beforeRender
            if (ownerLayout && ownerLayout.transformItemRenderTree) {
                tree = ownerLayout.transformItemRenderTree(tree);
            }
            // tree will be null if a beforerender listener returns false
            if (tree) {
                if (nextSibling) {
                    el = Ext.DomHelper.insertBefore(nextSibling, tree);
                } else {
                    el = Ext.DomHelper.append(container, tree);
                }
                me.wrapPrimaryEl(el);
                // Just rendered a bunch of stuff so fill up the cache with those els we will need.
                me.cacheRefEls(el);
            }
        } else {
            if (!me.hasListeners.beforerender || me.fireEvent('beforerender', me) !== false) {
                me.beforeRender();
                // We're simulating the above block here as much as possible, but we're already
                // given an el, so we don't need to create it. We still need to initialize
                // the renderTpl later.
                me.needsRenderTpl = me.rendering = true;
                me._renderState = 2;
                // Set configured styles on pre-rendered Component's element
                me.initStyles(el);
                if (me.allowDomMove !== false) {
                    if (nextSibling) {
                        container.dom.insertBefore(el.dom, nextSibling);
                    } else {
                        container.dom.appendChild(el.dom);
                    }
                }
            } else {
                vetoed = true;
            }
        }
        if (el && !vetoed) {
            me.finishRender(position);
        }
        if (!me.skipLayout) {
            Ext.resumeLayouts(!me.hidden && !container.isDetachedBody);
        }
    },
    /**
     * Ensures that this component is attached to `document.body`. If the component was
     * rendered to {@link Ext#getDetachedBody}, then it will be appended to `document.body`.
     * Any configured position is also restored.
     * @param {Boolean} [runLayout=false] True to run the component's layout.
     */
    ensureAttachedToBody: function(runLayout) {
        var comp = this,
            body;
        while (comp.ownerCt) {
            comp = comp.ownerCt;
        }
        if (comp.container.isDetachedBody) {
            comp.container = body = Ext.getBody();
            body.appendChild(comp.el.dom);
            if (runLayout) {
                comp.updateLayout();
            }
            if (typeof comp.x === 'number' || typeof comp.y === 'number') {
                comp.setPosition(comp.x, comp.y);
            }
        }
    },
    //=========================================================================
    privates: {
        /**
         * Sets references to elements inside the component. This applies
         * {@link Ext.Component#cfg-renderSelectors renderSelectors} as well as
         * {@link Ext.Component#cfg-childEls childEls}.
         * @private
         */
        applyRenderSelectors: function() {
            var me = this,
                selectors = me.renderSelectors,
                el = me.el,
                query, selector;
            me.attachChildEls(el);
            // Cache focusEl as a property for speedier lookups
            if (typeof me.focusEl === 'string') {
                me.focusEl = me[me.focusEl];
            }
            // For the majority of Components, their ariaEl is going to be their main el.
            me.ariaEl = me[me.ariaEl] || me.el;
            // We still support renderSelectors. There are a few places in the framework that
            // need them and they are a documented part of the API. In fact, we support mixing
            // childEls and renderSelectors (no reason not to).
            if (selectors) {
                for (selector in selectors) {
                    query = selectors[selector];
                    if (query) {
                        me[selector] = el.selectNode(query, false);
                    }
                }
            }
        },
        /**
         * Ensures that all elements with "data-ref" attributes get loaded into the cache.
         * This really helps on IE8 where `getElementById` is a search not a lookup. By
         * populating our cache with one search of the DOM we then have random access to
         * the elements as we do our `childEls` wire up.
         * @private
         */
        cacheRefEls: function(el) {
            el = el || this.el;
            // eslint-disable-next-line vars-on-top
            var cache = Ext.cache,
                El = Ext.dom.Element,
                dom = el.isElement ? el.dom : el,
                refs = dom.querySelectorAll('[data-ref]'),
                len = refs.length,
                ref, i;
            for (i = 0; i < len; i++) {
                ref = refs[i];
                if (!cache[ref.id]) {
                    new El(ref);
                }
            }
        },
        /**
         * Handles autoRender.
         * Floating Components may have an ownerCt. If they are asking to be constrained,
         * constrain them within that ownerCt, and have their z-index managed locally.
         * Floating Components are always rendered to document.body
         * @private
         */
        doAutoRender: function() {
            var me = this;
            if (!me.rendered) {
                if (me.floating) {
                    me.render(me.renderTo || document.body);
                } else {
                    me.render(Ext.isBoolean(me.autoRender) ? Ext.getBody() : me.autoRender);
                }
            }
        },
        doRenderContent: function(out, renderData) {
            // Careful! This method is bolted on to the renderTpl so all we get for context is
            // the renderData! The "this" pointer is the renderTpl instance!
            var me = renderData.$comp,
                data = me.data;
            if (me.html) {
                Ext.DomHelper.generateMarkup(me.html, out);
                delete me.html;
            }
            if (me.tpl) {
                // Make sure this.tpl is an instantiated XTemplate
                if (!me.tpl.isTemplate) {
                    me.tpl = new Ext.XTemplate(me.tpl);
                }
                if (data) {
                    me.data = data = data.isEntity ? data.getData(true) : data;
                    // me.tpl[me.tplWriteMode](target, me.data);
                    me.tpl.applyOut(data, out);
                }
            }
        },
        doRenderFramingTabGuard: function(out, renderData, position) {
            // Careful! This method is bolted on to the frameTpl so all we get for context is
            // the renderData! The "this" pointer is the frameTpl instance!
            // Container layout implements doRenderTabGuard(), *not* Component itself!
            var layout = renderData.$comp.layout;
            // The "renderData" property is placed in scope for the renderTpl, but we don't
            // want to render tab guards at that level in addition to the framing level:
            renderData.renderData.$skipTabGuards = true;
            if (layout && layout.doRenderTabGuard) {
                layout.doRenderTabGuard.call(this, out, renderData, position);
            }
        },
        flushRenderConfigs: function() {
            var me = this,
                configs = me.renderConfigs,
                state = me._renderState,
                bucket, i, name, newConfigs, value;
            if (configs) {
                for (i = 0; i <= state; ++i) {
                    bucket = configs[i];
                    if (bucket) {
                        configs[i] = null;
                        for (name in bucket) {
                            value = bucket[name];
                            (newConfigs || (newConfigs = {}))[name] = me[name];
                            me[name] = value;
                        }
                    }
                }
                if (newConfigs) {
                    me.setConfig(newConfigs);
                }
            }
        },
        /**
         * This method visits the rendered component tree in a "top-down" order. That is, this
         * code runs on a parent component before running on a child. This method calls the
         * {@link #onRender} method of each component.
         * @param {Number} containerIdx The index into the Container items of this Component.
         *
         * @private
         */
        finishRender: function(containerIdx) {
            var me = this,
                cache = Ext.cache,
                // our element cache
                proxy, first, id, tpl, data, dom, el;
            // We are typically called w/me.el==null as a child of some ownerCt that is being
            // rendered. We are also called by render for a normal component (w/o a configured
            // me.el). In this case, render sets me.el and me.rendering (indirectly). Lastly
            // we are also called on a component (like a Viewport) that has a configured me.el
            // (body for a Viewport) when render is called. In this case, it is not flagged as
            // "me.rendering" yet because it does not produce a renderTree. We use this to know
            // not to regen the renderTpl.
            if (!me.el || me.$pid) {
                if (me.container) {
                    el = cache[me.id];
                    dom = el ? el.dom : me.container.getById(me.id, true);
                } else {
                    id = me.$pid || me.id;
                    el = cache[id];
                    dom = el ? el.dom : Ext.getDom(id);
                }
                if (!me.el) {
                    // Typical case: we produced the el during render
                    me.wrapPrimaryEl(dom);
                } else {
                    // We were configured with an el and created a proxy, so now we can swap
                    // the proxy for me.el:
                    delete me.$pid;
                    if (!me.el.dom) {
                        // make sure me.el is an Element
                        me.wrapPrimaryEl(me.el);
                    }
                    // Insert the configured el before the proxy el:
                    dom.parentNode.insertBefore(me.el.dom, dom);
                    // We need to transplant rendered content from the proxy to the
                    // configured el. This would included the renderTpl of the component
                    // and its layout (innerCt's and such) as well as all child items.
                    //
                    proxy = dom;
                    // hold on to the rendered DOM
                    dom = me.el.dom;
                    // we'll be using the configured el though
                    first = dom.firstChild;
                    // rendered content in proxy goes first
                    while (proxy.firstChild) {
                        dom.insertBefore(proxy.firstChild, first);
                    }
                    // We need the classes rendered on to the proxy as well (things like
                    // "x-panel"):
                    me.el.addCls(proxy.className);
                    Ext.removeNode(proxy);
                }
            }
            // now proxy can go
            // TODO - what about style?
            else if (me.needsRenderTpl) {
                // We were configured with an el and then told to render (e.g., Viewport). We
                // need to generate the proper DOM. Insert first because the layout system
                // insists that child Component elements indices match the Component indices.
                tpl = me.initRenderTpl();
                if (tpl) {
                    data = me.initRenderData();
                    tpl.insertFirst(me.getTargetEl(), data);
                }
                // Just rendered a bunch of stuff so fill up the cache with those els we
                // will need.
                me.cacheRefEls();
            }
            // else we are rendering
            me.el.component = me;
            if (!me.container) {
                // top-level rendered components will already have me.container set up
                me.container = Ext.get(me.el.dom.parentNode);
            }
            if (me.ctCls) {
                me.container.addCls(me.ctCls);
            }
            // Sets the rendered flag and clears the rendering flag
            me.onRender(me.container, containerIdx);
            // If we could not access a target protoEl in beforeRender,
            // we have to set the overflow styles here.
            if (!me.overflowInited) {
                me.initOverflow();
            }
            // Tell the encapsulating element to hide itself in the way the Component
            // is configured to hide. This means DISPLAY, VISIBILITY or OFFSETS.
            me.el.setVisibilityMode(Ext.Element[me.hideMode.toUpperCase()]);
            if (me.overCls) {
                me.el.hover(me.addOverCls, me.removeOverCls, me);
            }
            if (me.hasListeners.render) {
                me.fireEvent('render', me);
            }
            me.afterRender();
            // this can cause a layout
            if (me.hasListeners.afterrender) {
                me.fireEvent('afterrender', me);
            }
            me.initEvents();
            if (me.hidden) {
                // Hiding during the render process should not perform any ancillary
                // actions that the full hide process does; It is not hiding,
                // it begins in a hidden state.
                // So just make the element hidden according to the configured hideMode
                me.el.hide();
            }
        },
        finishRenderChildren: function() {
            var layout = this.getComponentLayout();
            layout.finishRender();
        },
        getFrameRenderData: function() {
            var me = this,
                // we are only called if framing so this has already been determined:
                frameInfo = me.frameSize,
                mcStyle = '';
            if (me._syncFrameHeight && me.height) {
                // Buttons need their frame's MC element to have an explicit height in order
                // for percentage heights to work on elements inside the frame
                mcStyle = 'height:' + (me.height - frameInfo.height) + 'px';
            }
            return {
                $comp: me,
                id: me.id,
                fgid: me.id + '-frame',
                ui: me.ui,
                uiCls: me.uiCls,
                frameCls: me.frameCls,
                frameBodyCls: me.layoutTargetCls || '',
                baseCls: me.baseCls,
                hasTabGuard: me.tabGuard,
                top: !!frameInfo.top,
                left: !!frameInfo.left,
                right: !!frameInfo.right,
                bottom: !!frameInfo.bottom,
                mcStyle: mcStyle,
                // can be optionally set by a subclass or override to be an extra class to
                // be applied to all framing elements (used by RTL)
                frameElCls: ''
            };
        },
        /**
         * @private
         * On render, reads an encoded style attribute, "filter" from the style of this Component's
         * element. This information is memoized based upon the CSS class name of this Component's
         * element. Because child Components are rendered as textual HTML as part of the topmost
         * Container, a dummy div is inserted into the document to receive the document element's
         * CSS class name, and therefore style attributes.
         */
        getFrameInfo: function() {
            // If native framing can be used, or this component is not going to be framed,
            // then do not attempt to read CSS framing info.
            if (Ext.supports.CSS3BorderRadius || !this.frame) {
                return false;
            }
            // eslint-disable-next-line vars-on-top
            var me = this,
                frameInfoCache = me.frameInfoCache,
                cls = me.getFramingInfoCls() + '-frameInfo',
                frameInfo = frameInfoCache[cls],
                styleEl, info, frameTop, frameRight, frameBottom, frameLeft, borderTopWidth, borderRightWidth, borderBottomWidth, borderLeftWidth, paddingTop, paddingRight, paddingBottom, paddingLeft;
            if (frameInfo == null) {
                // Get the singleton frame style proxy with our el class name stamped into it.
                styleEl = Ext.fly(me.getStyleProxy(cls), 'frame-style-el');
                info = styleEl.getStyle('font-family');
                info = info && info.split('-');
                if (info && info.length >= 5) {
                    // The framing data is encoded as
                    //
                    //         D=div|T=table
                    //         |   H=horz|V=vert
                    //         |   |
                    //         |   |
                    //        [DT][HV]-[T-R-B-L]-[T-R-B-L]-[T-R-B-L]
                    //                /       /  |       |  \      \
                    //              /        /   |       |   \      \
                    //            /         /   /         \   \      \
                    //          /          /    border-width   \      \
                    //          frame-size                      padding
                    //
                    // The first 2 chars hold the div/table and horizontal/vertical flags.
                    // The 3 sets of TRBL 4-tuples are the CSS3 values for border-radius,
                    // border-width and padding, respectively.
                    //
                    /* eslint-disable no-multi-spaces */
                    frameTop = parseInt(info[1], 10);
                    frameRight = parseInt(info[2], 10);
                    frameBottom = parseInt(info[3], 10);
                    frameLeft = parseInt(info[4], 10);
                    borderTopWidth = parseInt(info[5], 10) || 0;
                    borderRightWidth = parseInt(info[6], 10) || 0;
                    borderBottomWidth = parseInt(info[7], 10) || 0;
                    borderLeftWidth = parseInt(info[8], 10) || 0;
                    paddingTop = parseInt(info[9], 10) || 0;
                    paddingRight = parseInt(info[10], 10) || 0;
                    paddingBottom = parseInt(info[11], 10) || 0;
                    paddingLeft = parseInt(info[12], 10) || 0;
                    /* eslint-enable no-multi-spaces */
                    frameInfo = {
                        table: info[0].charAt(0) === 't',
                        vertical: info[0].charAt(1) === 'v',
                        top: frameTop,
                        right: frameRight,
                        bottom: frameBottom,
                        left: frameLeft,
                        // Ext.layout.ContextItem needs width/height
                        width: frameLeft + frameRight,
                        height: frameTop + frameBottom,
                        border: {
                            top: borderTopWidth,
                            right: borderRightWidth,
                            bottom: borderBottomWidth,
                            left: borderLeftWidth,
                            width: borderLeftWidth + borderRightWidth,
                            height: borderTopWidth + borderBottomWidth
                        },
                        padding: {
                            top: paddingTop,
                            right: paddingRight,
                            bottom: paddingBottom,
                            left: paddingLeft,
                            width: paddingLeft + paddingRight,
                            height: paddingTop + paddingBottom
                        }
                    };
                } else {
                    frameInfo = false;
                }
                // This happens when you set frame: true explicitly without using the x-frame mixin
                // in sass.
                // This way IE can't figure out what sizes to use and thus framing can't work.
                if (me.frame === true && !frameInfo) {
                    Ext.log.error('You have set frame: true explicity on this component (' + me.getXType() + ') and it does not have any framing defined in the CSS template. ' + 'In this case IE cannot figure out what sizes to use and thus framing ' + 'on this component will be disabled.');
                }
                frameInfoCache[cls] = frameInfo;
            }
            me.frame = !!frameInfo;
            me.frameSize = frameInfo;
            return frameInfo;
        },
        getFramingInfoCls: function() {
            return this.baseCls + '-' + this.ui;
        },
        /**
         * @private
         * Returns an offscreen div with the same class name as the element this is being rendered.
         * This is because child item rendering takes place in a detached div which, being not
         * part of the document, has no styling.
         */
        getStyleProxy: function(cls) {
            var result;
            result = this.styleProxyEl || (Ext.Component.prototype.styleProxyEl = Ext.getBody().createChild({
                // tell the spec runner to ignore this element
                // when checking if the dom is clean
                'data-sticky': true,
                role: 'presentation',
                style: {
                    position: 'absolute',
                    top: '-10000px'
                }
            }, null, true));
            result.className = cls;
            return result;
        },
        /**
         * @private
         */
        getFrameTpl: function(table) {
            return this.lookupTpl(table ? 'frameTableTpl' : 'frameTpl');
        },
        initContainer: function(container) {
            var me = this;
            // If you render a component specifying the el, we get the container
            // of the el, and make sure we dont move the el around in the dom
            // during the render
            if (!container && me.el) {
                container = me.el.dom.parentNode;
                me.allowDomMove = false;
            }
            me.container = container.dom ? container : Ext.get(container);
            return me.container;
        },
        initOverflow: function() {
            var me = this,
                // Call the style calculation early which sets the scrollFlags property
                overflowStyle = me.getOverflowStyle(),
                scrollFlags = me.scrollFlags,
                overflowEl = me.getOverflowEl(),
                hasOverflow = (scrollFlags.y || scrollFlags.x);
            // Not rendered, or the targetEl has been configured as a string, 
            // wait until the call from finishRender
            if (!hasOverflow || !overflowEl || !overflowEl.isElement) {
                return;
            }
            me.overflowInited = true;
            overflowEl.setStyle(overflowStyle);
        },
        // Create the framingTpl from the string.
        // Poke in a reference to applyRenderTpl(frameInfo, out)
        initFramingTpl: function(table) {
            var tpl = this.getFrameTpl(table);
            if (tpl && !tpl.applyRenderTpl) {
                this.setupFramingTpl(tpl);
            }
            return tpl;
        },
        /**
         * Initializes the renderTpl.
         * @return {Ext.XTemplate} The renderTpl XTemplate instance.
         * @private
         */
        initRenderTpl: function() {
            var tpl = this.lookupTpl('renderTpl');
            if (tpl && !tpl.renderContent) {
                this.setupRenderTpl(tpl);
            }
            return tpl;
        },
        /**
         * @private
         * Inject a reference to the function which applies the render template
         * into the framing template. The framing template wraps the content.
         */
        setupFramingTpl: function(frameTpl) {
            frameTpl.applyRenderTpl = this.doApplyRenderTpl;
            frameTpl.renderTabGuard = this.doRenderFramingTabGuard;
        },
        setupRenderTpl: function(renderTpl) {
            renderTpl.renderBody = renderTpl.renderContent = this.doRenderContent;
        },
        /**
         * Updates the frame elements to match new framing. The current `frameBody` is
         * preserved by transplanting it into the new frame. All other frame `childEls`
         * are destroyed and recreated if needed by the new frame. This method cannot
         * transition from framed to non-framed or vise-versa or between table and div
         * based framing.
         * @private
         */
        updateFrame: function() {
            if (Ext.supports.CSS3BorderRadius || !this.frame) {
                return;
            }
            // eslint-disable-next-line vars-on-top
            var me = this,
                dom = me.el.dom,
                frameTable = me.frameTable,
                oldFrameBody = me.frameBody,
                oldFrameBodyDom = oldFrameBody.dom,
                frameInfo = me.getFrameInfo(),
                childEls, childElName, div, el, first, frameData, frameDom, frameTpl, i, newBody, newFrameEls;
            // This is a bit tricky because we will be generating elements with the same
            // id's (mostly) as our current frame. We have to do most of this work with
            // the raw DOM nodes due to the duplicate id's (which prevents us from using
            // an Element wrapper until we resolve the duplicates).
            // First off, render the new frameTpl to an off-document element.
            div = document.createElement('div');
            frameData = me.getFrameRenderData();
            // If the container was configured with tab guards, they were already rendered.
            frameData.hasTabGuard = false;
            frameTpl = me.getFrameTpl(frameInfo.table);
            frameTpl.insertFirst(div, frameData);
            // Capture the new frameEls (we'll need to update our childEls to these later
            // once we've destroyed the old ones).
            newFrameEls = div.querySelectorAll('[data-ref]');
            newBody = div.querySelector('[data-ref="frameBody"]');
            // Now we can insert the new frameEls before the current frameBody.
            for (first = oldFrameBodyDom; first.parentNode !== dom; ) {
                first = first.parentNode;
            }
            while (div.firstChild) {
                dom.insertBefore(div.firstChild, first);
            }
            // And transplant the oldFrameBody into the new frame
            newBody.parentNode.replaceChild(oldFrameBodyDom, newBody);
            oldFrameBodyDom.className = newBody.className;
            oldFrameBody.setSize();
            // clear any size set by layout
            // Remove the old frame elements, except for frameBody of course:
            childEls = me.getChildEls();
            if (frameTable) {
                frameTable.destroy();
                me.frameTable = null;
            }
            for (childElName in childEls) {
                if (childEls[childElName].frame) {
                    el = me[childElName];
                    if (el && el !== oldFrameBody) {
                        el.destroy();
                        me[childElName] = null;
                    }
                }
            }
            // Now we are free to acquire the childEls to the new elements:
            for (i = newFrameEls.length; i--; ) {
                childElName = (frameDom = newFrameEls[i]).getAttribute('data-ref');
                if (childElName !== 'frameBody') {
                    me[childElName] = new Ext.dom.Element(frameDom);
                }
            }
        },
        // Cache the frame information object so as not to cause style recalculations
        frameInfoCache: {}
    }
});
// private

/**
 * @class Ext.state.Provider
 * <p>Abstract base class for state provider implementations. The provider is responsible
 * for setting values  and extracting values to/from the underlying storage source. The 
 * storage source can vary and the details should be implemented in a subclass. For example
 * a provider could use a server side database or the browser localstorage where supported.</p>
 *
 * <p>This class provides methods for encoding and decoding <b>typed</b> variables including 
 * dates and defines the Provider interface. By default these methods put the value and the
 * type information into a delimited string that can be stored. These should be overridden in 
 * a subclass if you want to change the format of the encoded value and subsequent decoding.</p>
 */
Ext.define('Ext.state.Provider', {
    mixins: {
        observable: 'Ext.util.Observable'
    },
    /**
     * @cfg {String} prefix A string to prefix to items stored in the underlying state store. 
     * Defaults to <tt>'ext-'</tt>
     */
    prefix: 'ext-',
    /**
     * @event statechange
     * Fires when a state change occurs.
     * @param {Ext.state.Provider} this This state provider
     * @param {String} key The state key which was changed
     * @param {String} value The encoded value for the state
     */
    constructor: function(config) {
        var me = this;
        Ext.apply(me, config);
        me.state = {};
        me.mixins.observable.constructor.call(me);
    },
    /**
     * Returns the current value for a key
     * @param {String} name The key name
     * @param {Object} defaultValue A default value to return if the key's value is not found
     * @return {Object} The state data
     */
    get: function(name, defaultValue) {
        var ret = this.state[name];
        return ret === undefined ? defaultValue : ret;
    },
    /**
     * Clears a value from the state
     * @param {String} name The key name
     */
    clear: function(name) {
        var me = this;
        delete me.state[name];
        me.fireEvent("statechange", me, name, null);
    },
    /**
     * Sets the value for a key
     * @param {String} name The key name
     * @param {Object} value The value to set
     */
    set: function(name, value) {
        var me = this;
        me.state[name] = value;
        me.fireEvent("statechange", me, name, value);
    },
    /**
     * Decodes a string previously encoded with {@link #encodeValue}.
     * @param {String} value The value to decode
     * @return {Object} The decoded value
     */
    decodeValue: function(value) {
        // a -> Array
        // n -> Number
        // d -> Date
        // b -> Boolean
        // s -> String
        // o -> Object
        // -> Empty (null)
        var me = this,
            re = /^(a|n|d|b|s|o|e):(.*)$/,
            matches = re.exec(unescape(value)),
            all, type, keyValue, values, vLen, v;
        if (!matches || !matches[1]) {
            return;
        }
        // non state
        type = matches[1];
        value = matches[2];
        switch (type) {
            case 'e':
                return null;
            case 'n':
                return parseFloat(value);
            case 'd':
                return new Date(Date.parse(value));
            case 'b':
                return (value === '1');
            case 'a':
                all = [];
                if (value) {
                    values = value.split('^');
                    vLen = values.length;
                    for (v = 0; v < vLen; v++) {
                        value = values[v];
                        all.push(me.decodeValue(value));
                    }
                };
                return all;
            case 'o':
                all = {};
                if (value) {
                    values = value.split('^');
                    vLen = values.length;
                    for (v = 0; v < vLen; v++) {
                        value = values[v];
                        keyValue = value.split('=');
                        all[keyValue[0]] = me.decodeValue(keyValue[1]);
                    }
                };
                return all;
            default:
                return value;
        }
    },
    /**
     * Encodes a value including type information.  Decode with {@link #decodeValue}.
     * @param {Object} value The value to encode
     * @return {String} The encoded value
     */
    encodeValue: function(value) {
        var flat = '',
            i = 0,
            enc, len, key;
        if (value == null) {
            return 'e:1';
        } else if (typeof value === 'number') {
            enc = 'n:' + value;
        } else if (typeof value === 'boolean') {
            enc = 'b:' + (value ? '1' : '0');
        } else if (Ext.isDate(value)) {
            enc = 'd:' + value.toUTCString();
        } else if (Ext.isArray(value)) {
            for (len = value.length; i < len; i++) {
                flat += this.encodeValue(value[i]);
                if (i !== len - 1) {
                    flat += '^';
                }
            }
            enc = 'a:' + flat;
        } else if (typeof value === 'object') {
            for (key in value) {
                if (typeof value[key] !== 'function' && value[key] !== undefined) {
                    flat += key + '=' + this.encodeValue(value[key]) + '^';
                }
            }
            enc = 'o:' + flat.substring(0, flat.length - 1);
        } else {
            enc = 's:' + value;
        }
        return escape(enc);
    }
});

/**
 * This is the global state manager. By default all components that are "state aware"
 * check this class for state information if you don't pass them a custom state provider.
 * In order for this class to be useful, it must be initialized with a provider
 * when your application initializes. Example usage:
 *
 *      // in your initialization function
 *      init: function() {
 *          Ext.state.Manager.setProvider(new Ext.state.CookieProvider());
 *      }
 *
 * This class passes on calls from components to the underlying {@link Ext.state.Provider} so that
 * there is a common interface that can be used without needing to refer to a specific provider
 * instance in every component.
 */
Ext.define('Ext.state.Manager', {
    singleton: true,
    requires: [
        'Ext.state.Provider'
    ],
    constructor: function() {
        this.provider = new Ext.state.Provider();
    },
    /**
     * Configures the default state provider for your application
     * @param {Ext.state.Provider} stateProvider The state provider to set
     */
    setProvider: function(stateProvider) {
        this.provider = stateProvider;
    },
    /**
     * Returns the current value for a key
     * @param {String} key The key name
     * @param {Object} defaultValue The default value to return if the key lookup does not match
     * @return {Object} The state data
     */
    get: function(key, defaultValue) {
        return this.provider.get(key, defaultValue);
    },
    /**
     * Sets the value for a key
     * @param {String} key The key name
     * @param {Object} value The state data
     */
    set: function(key, value) {
        this.provider.set(key, value);
    },
    /**
     * Clears a value from the state
     * @param {String} key The key name
     */
    clear: function(key) {
        this.provider.clear(key);
    },
    /**
     * Gets the currently configured state provider
     * @return {Ext.state.Provider} The state provider
     */
    getProvider: function() {
        return this.provider;
    }
});

/**
 * @class Ext.state.Stateful
 * A mixin for being able to save the state of an object to an underlying
 * {@link Ext.state.Provider}.
 */
Ext.define('Ext.state.Stateful', {
    mixinId: 'state',
    requires: [
        'Ext.state.Manager',
        'Ext.util.TaskRunner'
    ],
    config: {
        /**
         * @cfg {Boolean/Object} [stateful=false]
         * A flag which causes the object to attempt to restore the state of
         * internal properties from a saved state on startup. The object must have
         * a {@link #stateId} for state to be managed.
         *
         * Auto-generated ids are not guaranteed to be stable across page loads and
         * cannot be relied upon to save and restore the same state for a object.
         *
         * For state saving to work, the state manager's provider must have been
         * set to an implementation of {@link Ext.state.Provider} which overrides the
         * {@link Ext.state.Provider#set set} and {@link Ext.state.Provider#get get}
         * methods to save and recall name/value pairs. A built-in implementation,
         * {@link Ext.state.CookieProvider} is available.
         *
         * To set the state provider for the current page:
         *
         *     Ext.state.Manager.setProvider(new Ext.state.CookieProvider({
         *         expires: new Date(new Date().getTime()+(1000*60*60*24*7)), // 7 days from now
         *     }));
         *
         * A stateful object attempts to save state when one of the events
         * listed in the {@link #stateEvents} configuration fires.
         *
         * To save state, a stateful object first serializes its state by
         * calling *{@link #getState}*.
         *
         * The Component base class implements {@link #getState} to save its width and
         * height within the state only if they were initially configured, and have
         * changed from the configured value.
         *
         * The Panel class saves its collapsed state in addition to that.
         *
         * The Grid class saves its column state and store state (sorters and filters and grouper)
         * in addition to its superclass state.
         *
         * If there is more application state to be save, the developer must provide
         * an implementation which first calls the superclass method to inherit the above behaviour,
         * and then injects new properties into the returned object.
         *
         * The value yielded by `getState` is passed to {@link Ext.state.Manager#set}
         * which uses the configured {@link Ext.state.Provider} to save the object
         * keyed by the {@link #stateId}.
         *
         * During construction, a stateful object attempts to *restore* its state by calling
         * {@link Ext.state.Manager#get} passing the {@link #stateId}
         *
         * The resulting object is passed to {@link #applyState}*. The default implementation of
         * {@link #applyState} simply copies properties into the object, but a developer may
         * override this to support restoration of more complex application state.
         *
         * You can perform extra processing on state save and restore by attaching
         * handlers to the {@link #beforestaterestore}, {@link #staterestore},
         * {@link #beforestatesave} and {@link #statesave} events. In some simple cases,
         * passing an object for the `stateful` config may suffice. If an object is
         * provided, the properties of that object are used to include or exclude stateful
         * properties returned by `getState`. For example:
         *
         *      stateful: {
         *          height: false, // never persist the height
         *          width: true    // always persist the width
         *      }
         *
         * The above is roughly equivalent to the following:
         *
         *      getState: function () {
         *          var state = this.callParent();
         *
         *          delete state.height;
         *          state.width = this.width;
         *
         *          return state;
         *      }
         */
        stateful: null
    },
    /**
     * @cfg {String} stateId
     * The unique id for this object to use for state management purposes.
     *
     * See {@link #stateful} for an explanation of saving and restoring state.
     */
    /**
     * @cfg {String[]} stateEvents
     * An array of events that, when fired, should trigger this object to
     * save its state. `stateEvents` defaults to the `stateEvents` associated with the
     * component you are using.  Any events you statically set will be appended to that list.
     * `stateEvents` may be any type of event supported by this object, including
     * browser or custom events (e.g., `['click', 'customerchange']`).
     *
     * See `{@link #stateful}` for an explanation of saving and
     * restoring object state.
     */
    /**
     * @cfg {Number} saveDelay
     * A buffer to be applied if many state events are fired within a short period.
     */
    saveDelay: 100,
    /**
     * @event beforestaterestore
     * Fires before the state of the object is restored. Return false from an event handler to stop
     * the restore.
     * @param {Ext.state.Stateful} this
     * @param {Object} state The hash of state values returned from the StateProvider. If this
     * event is not vetoed, then the state object is passed to *`applyState`*. By default,
     * that simply copies property values into this object. The method maybe overriden to
     * provide custom state restoration.
     */
    /**
     * @event staterestore
     * Fires after the state of the object is restored.
     * @param {Ext.state.Stateful} this
     * @param {Object} state The hash of state values returned from the StateProvider.
     * This is passed to *`applyState`*. By default, that simply copies property values into this
     * object. The method maybe overridden to provide custom state restoration.
     */
    /**
     * @event beforestatesave
     * Fires before the state of the object is saved to the configured state provider.
     * Return false to stop the save.
     * @param {Ext.state.Stateful} this
     * @param {Object} state The hash of state values. This is determined by calling
     * *`getState()`* on the object. This method must be provided by the
     * developer to return whatever representation of state is required, by default,
     * Ext.state.Stateful has a null implementation.
     */
    /**
     * @event statesave
     * Fires after the state of the object is saved to the configured state provider.
     * @param {Ext.state.Stateful} this
     * @param {Object} state The hash of state values. This is determined by calling
     * *`getState()`* on the object. This method must be provided by the
     * developer to return whatever representation of state is required, by default,
     * Ext.state.Stateful has a null implementation.
     */
    constructor: function() {
        var me = this;
        if (!me.stateEvents) {
            me.stateEvents = [];
        }
        // NOTE: The stateful config will be initConfig'd before we get here, so the
        // stateful property will be correct...
        if (me.stateful) {
            me.addStateEvents(me.stateEvents);
            me.initState();
        }
    },
    /**
     * Add events that will trigger the state to be saved. If the first argument is an
     * array, each element of that array is the name of a state event. Otherwise, each
     * argument passed to this method is the name of a state event.
     *
     * @param {String/String[]} events The event name or an array of event names.
     */
    addStateEvents: function(events) {
        var me = this,
            i, event, stateEventsByName, eventArray;
        if (me.stateful && me.getStateId()) {
            eventArray = (typeof events === 'string') ? arguments : events;
            stateEventsByName = me.stateEventsByName || (me.stateEventsByName = {});
            for (i = eventArray.length; i--; ) {
                event = eventArray[i];
                if (event && !stateEventsByName[event]) {
                    stateEventsByName[event] = 1;
                    me.on(event, me.onStateChange, me);
                }
            }
        }
    },
    /**
     * This method is called when any of the {@link #stateEvents} are fired.
     * @private
     */
    onStateChange: function() {
        var me = this,
            delay = me.saveDelay,
            statics, runner;
        if (!me.stateful) {
            return;
        }
        if (delay) {
            if (!me.stateTask) {
                statics = Ext.state.Stateful;
                runner = statics.runner || (statics.runner = new Ext.util.TaskRunner());
                me.stateTask = runner.newTask({
                    run: me.saveState,
                    scope: me,
                    interval: delay,
                    repeat: 1,
                    fireIdleEvent: false
                });
            }
            me.stateTask.start();
        } else {
            me.saveState();
        }
    },
    /**
     * Saves the state of the object to the persistence store.
     */
    saveState: function() {
        var me = this,
            stateful = me.getStateful(),
            id = stateful && me.getStateId(),
            hasListeners = me.hasListeners,
            cfg, configs, plugins, plugin, i, len, state, pluginState;
        if (id) {
            state = me.getState() || {};
            // pass along for custom interactions
            if (Ext.isObject(stateful)) {
                configs = me.self.getConfigurator();
                configs = configs.configs;
                // configs[name]: Ext.Config(name)
                for (i in stateful) {
                    if (stateful[i]) {
                        if (!(i in state)) {
                            cfg = configs[i];
                            // if class declared this as a config
                            state[i] = cfg ? me[cfg.get]() : me[i];
                        }
                    } else {
                        delete state[i];
                    }
                }
            }
            /*
             * Gather state from those plugins that implement a getState method
             */
            plugins = me.getPlugins() || [];
            for (i = 0 , len = plugins.length; i < len; i++) {
                plugin = plugins[i];
                if (plugin && plugin.getState) {
                    pluginState = plugin.getState(state);
                    if (pluginState && !state[plugin.ptype]) {
                        // first duplicate plugin wins
                        state[plugin.ptype] = pluginState;
                    }
                }
            }
            if (!hasListeners.beforestatesave || me.fireEvent('beforestatesave', me, state) !== false) {
                Ext.state.Manager.set(id, state);
                if (hasListeners.statesave) {
                    me.fireEvent('statesave', me, state);
                }
            }
        }
    },
    /**
     * Gets the current state of the object. By default this function returns null,
     * it should be overridden in subclasses to implement methods for getting the state.
     * @return {Object} The current state
     */
    getState: function() {
        return null;
    },
    /**
     * Applies the state to the object. This should be overridden in subclasses to do
     * more complex state operations. By default it applies the state properties onto
     * the current object.
     * @param {Object} state The state
     */
    applyState: function(state) {
        if (state) {
            Ext.apply(this, state);
        }
    },
    /**
     * Gets the state id for this object.
     * @return {String} The 'stateId' or the implicit 'id' specified by component configuration.
     * @private
     */
    getStateId: function() {
        var me = this;
        return me.stateId || (me.autoGenId ? null : me.id);
    },
    /**
     * Initializes the state of the object upon construction.
     * @private
     */
    initState: function() {
        var me = this,
            id = me.stateful && me.getStateId(),
            hasListeners = me.hasListeners,
            state, combinedState, i, len, plugins, plugin, pluginType;
        if (id) {
            combinedState = Ext.state.Manager.get(id);
            if (combinedState) {
                state = Ext.apply({}, combinedState);
                if (!hasListeners.beforestaterestore || me.fireEvent('beforestaterestore', me, combinedState) !== false) {
                    // Notify all plugins FIRST (if interested) in new state
                    plugins = me.getPlugins() || [];
                    for (i = 0 , len = plugins.length; i < len; i++) {
                        plugin = plugins[i];
                        if (plugin) {
                            pluginType = plugin.ptype;
                            if (plugin.applyState) {
                                plugin.applyState(state[pluginType], combinedState);
                            }
                            // clean to prevent unwanted props on the component in final phase
                            delete state[pluginType];
                        }
                    }
                    me.applyState(state);
                    if (hasListeners.staterestore) {
                        me.fireEvent('staterestore', me, combinedState);
                    }
                }
            }
        }
    },
    /**
     * Conditionally saves a single property from this object to the given state object.
     * The idea is to only save state which has changed from the initial state so that
     * current software settings do not override future software settings. Only those
     * values that are user-changed state should be saved.
     *
     * @param {String} propName The name of the property to save.
     * @param {Object} state The state object in to which to save the property.
     * @param {String} stateName (optional) The name to use for the property in state.
     * @return {Boolean} True if the property was saved, false if not.
     */
    savePropToState: function(propName, state, stateName) {
        var me = this,
            value = me[propName],
            config = me.initialConfig;
        if (me.hasOwnProperty(propName)) {
            if (!config || config[propName] !== value) {
                if (state) {
                    state[stateName || propName] = value;
                }
                return true;
            }
        }
        return false;
    },
    /**
     * Gathers additional named properties of the instance and adds their current values
     * to the passed state object.
     * @param {String/String[]} propNames The name (or array of names) of the property to save.
     * @param {Object} state The state object in to which to save the property values.
     * @return {Object} state
     */
    savePropsToState: function(propNames, state) {
        var me = this,
            i, n;
        if (typeof propNames === 'string') {
            me.savePropToState(propNames, state);
        } else {
            for (i = 0 , n = propNames.length; i < n; ++i) {
                me.savePropToState(propNames[i], state);
            }
        }
        return state;
    },
    /**
     * Destroys this stateful object.
     */
    destroy: function() {
        var task = this.stateTask;
        if (task) {
            task.destroy();
            this.stateTask = null;
        }
    }
});
// No callParent() here, it's a mixin.

/**
 * Base class for all Ext components.
 *
 * The Component base class has built-in support for basic hide/show and enable/disable
 * and size control behavior.
 *
 * ## xtypes
 *
 * Every component has a specific xtype, which is its Ext-specific type name, along with
 * methods for checking the xtype like {@link #getXType} and {@link #isXType}. See the
 * [Component Guide](../guides/core_concepts/components.html) for more information on xtypes
 * and the Component hierarchy.
 *
 * ## Finding components
 *
 * All Components are registered with the {@link Ext.ComponentManager} on construction so
 * that they can be referenced at any time via {@link Ext#getCmp Ext.getCmp}, passing the
 * {@link #id}.
 *
 * Additionally the {@link Ext.ComponentQuery} provides a CSS-selectors-like way to look
 * up components by their xtype and many other attributes.  For example the following code
 * will find all textfield components inside component with `id: 'myform'`:
 *
 *     Ext.ComponentQuery.query('#myform textfield');
 *
 * ## Extending Ext.Component
 *
 * All subclasses of Component may participate in the automated Ext component
 * life cycle of creation, rendering and destruction which is provided by the
 * {@link Ext.container.Container Container} class. Components may be added to a Container
 * through the {@link Ext.container.Container#cfg-items items} config option at the time
 * the Container is created, or they may be added dynamically via the
 * {@link Ext.container.Container#method-add add} method.
 *
 * All user-developed visual widgets that are required to participate in automated
 * life cycle and size management should subclass Component.
 *
 * See the Creating new UI controls chapter in
 * [Component Guide](../guides/core_concepts/components.html) for details on how and to either
 * extend or augment Ext JS base classes to create custom Components.
 *
 * ## The Ext.Component class by itself
 *
 * Usually one doesn't need to instantiate the Ext.Component class. There are subclasses
 * which implement specialized use cases, covering most application needs. However it is
 * possible to instantiate a base Component, and it can be rendered to document, or handled
 * by layouts as the child item of a Container:
 *
 *     @example
 *     Ext.create('Ext.Component', {
 *         html: 'Hello world!',
 *         width: 300,
 *         height: 200,
 *         padding: 20,
 *         style: {
 *             color: '#FFFFFF',
 *             backgroundColor:'#000000'
 *         },
 *         renderTo: Ext.getBody()
 *     });
 *
 * The Component above creates its encapsulating `div` upon render, and use the configured
 * HTML as content. More complex internal structure may be created using the
 * {@link #renderTpl} configuration, although to display database-derived mass data, it is
 * recommended that an ExtJS data-backed Component such as a {@link Ext.view.View View},
 * {@link Ext.grid.Panel GridPanel}, or {@link Ext.tree.Panel TreePanel} be used.
 */
Ext.define('Ext.Component', {
    alternateClassName: 'Ext.AbstractComponent',
    xtype: [
        'component',
        'box'
    ],
    requires: [
        'Ext.ComponentQuery',
        'Ext.ComponentManager',
        'Ext.util.ProtoElement',
        'Ext.dom.CompositeElement',
        'Ext.plugin.Abstract',
        'Ext.plugin.Manager',
        'Ext.scroll.Scroller'
    ],
    // Note that Floating must be mixed in before Positionable.
    mixins: [
        'Ext.mixin.Inheritable',
        'Ext.util.Floating',
        'Ext.util.Positionable',
        'Ext.util.Observable',
        'Ext.mixin.ComponentDelegation',
        'Ext.mixin.Bindable',
        'Ext.util.Animate',
        'Ext.util.ElementContainer',
        'Ext.util.Renderable',
        'Ext.state.Stateful',
        'Ext.mixin.Focusable',
        'Ext.mixin.Accessible',
        'Ext.mixin.Keyboard'
    ],
    uses: [
        'Ext.overrides.*',
        'Ext.Element',
        'Ext.DomHelper',
        'Ext.XTemplate',
        'Ext.ComponentLoader',
        'Ext.layout.Context',
        'Ext.layout.Layout',
        'Ext.layout.component.Auto',
        'Ext.LoadMask',
        'Ext.ZIndexManager',
        'Ext.util.DelayedTask',
        'Ext.resizer.Resizer',
        'Ext.util.ComponentDragger'
    ],
    statics: {
        AUTO_ID: 1000,
        pendingLayouts: null,
        layoutSuspendCount: 0,
        // Collapse/expand directions
        DIRECTION_TOP: 'top',
        DIRECTION_RIGHT: 'right',
        DIRECTION_BOTTOM: 'bottom',
        DIRECTION_LEFT: 'left',
        VERTICAL_DIRECTION_Re: /^(?:top|bottom)$/,
        // RegExp whih specifies characters in an xtype which must be translated to '-'
        // when generating auto IDs. This includes dot, comma and whitespace
        INVALID_ID_CHARS_Re: /[.,\s]/g,
        /**
         * @property {String} ariaHighContrastModeCls CSS class to be applied
         * to the document body when High Contrast mode is detected in Windows.
         * @private
         */
        ariaHighContrastModeCls: Ext.baseCSSPrefix + 'aria-highcontrast',
        /**
         * Cancels layout of a component.
         * @param {Ext.Component} comp
         * @param isDestroying
         */
        cancelLayout: function(comp, isDestroying) {
            var context = this.runningLayoutContext || this.pendingLayouts;
            if (context) {
                context.cancelComponent(comp, false, isDestroying);
            }
        },
        /**
         * Find the Widget or Component to which the given event/element belongs.
         *
         * @param {Ext.event.Event/Ext.dom.Element/HTMLElement} el The element or event
         * from which to start to find an owning Component.
         * @param {Ext.dom.Element/HTMLElement} [limit] The element at which to stop upward
         * searching for an owning Component, or the number of Components to traverse
         * before giving up. Defaults to the document's HTML element.
         * @param {String} [selector] An optional {@link Ext.ComponentQuery} selector to
         * filter the target.
         * @return {Ext.Component/null} Component, or null
         *
         * @since 6.5.0
         */
        from: function(el, limit, selector) {
            return Ext.ComponentManager.from(el, limit, selector);
        },
        /**
         * Find the Widget or Component to which the given Element belongs.
         *
         * @param {Ext.dom.Element/HTMLElement} el The element from which to start to find an owning
         * Component.
         * @param {Ext.dom.Element/HTMLElement} [limit] The element at which to stop upward
         * searching for an owning Component, or the number of Components to traverse
         * before giving up. Defaults to the document's HTML element.
         * @param {String} [selector] An optional {@link Ext.ComponentQuery} selector to filter
         * the target.
         * @return {Ext.Component/null} Component, or null
         *
         * @deprecated 6.5.0 Use {@link Ext.Component#method!from} instead.
         * @since 6.0.1
         */
        fromElement: function(el, limit, selector) {
            return Ext.ComponentManager.from(el, limit, selector);
        },
        /**
         * Performs all pending layouts that were scheduled while
         * {@link Ext.Component#suspendLayouts suspendLayouts} was in effect.
         * @static
         */
        flushLayouts: function() {
            var me = this,
                context = me.pendingLayouts;
            if (context && context.invalidQueue.length) {
                me.pendingLayouts = null;
                me.runningLayoutContext = context;
                Ext.override(context, {
                    runComplete: function() {
                        var Scroller = Ext.scroll.Scroller,
                            GlobalEvents = Ext.GlobalEvents,
                            result;
                        // we need to release the layout queue before running any of the
                        // finishedLayout calls because they call afterComponentLayout
                        // which can re-enter by calling updateLayout/doComponentLayout.
                        me.runningLayoutContext = null;
                        if (Scroller.viewport) {
                            Scroller.viewport.restoreState();
                        }
                        result = this.callParent();
                        // not "me" here!
                        if (GlobalEvents.hasListeners.afterlayout) {
                            GlobalEvents.fireEvent('afterlayout');
                        }
                        return result;
                    }
                });
                context.run();
            }
        },
        /**
         * Resumes layout activity in the whole framework.
         *
         * {@link Ext#suspendLayouts} is alias of {@link Ext.Component#suspendLayouts}.
         *
         * @param {Boolean} [flush=false] `true` to perform all the pending layouts. This can also
         * be achieved by calling {@link Ext.Component#flushLayouts flushLayouts} directly.
         * @static
         */
        resumeLayouts: function(flush) {
            if (this.layoutSuspendCount && !--this.layoutSuspendCount) {
                if (flush) {
                    this.flushLayouts();
                }
                if (Ext.GlobalEvents.hasListeners.resumelayouts) {
                    Ext.GlobalEvents.fireEvent('resumelayouts');
                }
            }
        },
        /**
         * Stops layouts from happening in the whole framework.
         *
         * It's useful to suspend the layout activity while updating multiple components and
         * containers:
         *
         *     Ext.suspendLayouts();
         *     // batch of updates...
         *     Ext.resumeLayouts(true);
         *
         * {@link Ext#suspendLayouts} is alias of {@link Ext.Component#suspendLayouts}.
         *
         * See also {@link Ext#batchLayouts} for more abstract way of doing this.
         *
         * @static
         */
        suspendLayouts: function() {
            ++this.layoutSuspendCount;
        },
        /**
         * Updates layout of a component.
         *
         * @param {Ext.Component} comp The component to update.
         * @param {Boolean} [defer=false] `true` to just queue the layout if this component.
         * @static
         */
        updateLayout: function(comp, defer) {
            var me = this,
                running = me.runningLayoutContext,
                pending;
            if (running) {
                running.queueInvalidate(comp);
            } else {
                pending = me.pendingLayouts || (me.pendingLayouts = new Ext.layout.Context());
                pending.queueInvalidate(comp);
                if (!defer && !me.layoutSuspendCount && !comp.isLayoutSuspended()) {
                    me.flushLayouts();
                }
            }
        }
    },
    // <editor-fold desc="Config">
    // ***********************************************************************************
    // Begin Config
    // ***********************************************************************************
    // We do not want "_hidden" style backing properties.
    $configPrefixed: false,
    // We also want non-config system properties to go to the instance.
    $configStrict: false,
    clearPropertiesOnDestroy: 'async',
    manageLayoutScroll: true,
    config: {
        /**
         * @cfg {Object} data
         * The initial set of data to apply to the `{@link #tpl}` to update the content
         * area of the Component.
         *
         * @accessor
         * @since 3.4.0
         */
        data: null,
        /**
         * @cfg {Boolean} modelValidation
         * This config enables binding to your `{@link Ext.data.Model#validators}`. This
         * is only processed by form fields (e.g., `Ext.form.field.Text`) at present, but
         * this setting is inherited and so can be set on a parent container.
         *
         * When set to `true` by a component or not set by a component but inherited from
         * an ancestor container, `Ext.data.Validation` records are used to automatically
         * bind validation results for any form field to which a `value` is bound.
         *
         * While this config can be set arbitrarily high in the component hierarchy, doing
         * so can create a lot overhead if most of your form fields do not actually rely on
         * `validators` in your data model.
         *
         * Using this setting for a form that is bound to an `Ext.data.Model` might look
         * like this:
         *
         *      {
         *          xtype: 'panel',
         *          modelValidation: true,
         *          items: [{
         *              xtype: 'textfield',
         *              bind: '{theUser.firstName}'
         *          },{
         *              xtype: 'textfield',
         *              bind: '{theUser.lastName}'
         *          },{
         *              xtype: 'textfield',
         *              bind: '{theUser.phoneNumber}'
         *          },{
         *              xtype: 'textfield',
         *              bind: '{theUser.email}'
         *          }]
         *      }
         *
         * Notice that "validation" is a pseudo-association defined for all entities. See
         * `{@link Ext.data.Model#getValidation}` for further details.
         */
        /**
         * @cfg {Number} maxHeight
         * The maximum value in pixels which this Component will set its height to.
         *
         * **Warning:** This will override any size management applied by layout managers.
         */
        maxHeight: null,
        /**
         * @cfg {Number} maxWidth
         * The maximum value in pixels which this Component will set its width to.
         *
         * **Warning:** This will override any size management applied by layout managers.
         */
        maxWidth: null,
        /**
         * @cfg {Number} minHeight
         * The minimum value in pixels which this Component will set its height to.
         *
         * **Warning:** This will override any size management applied by layout managers.
         */
        minHeight: null,
        /**
         * @cfg {Number} minWidth
         * The minimum value in pixels which this Component will set its width to.
         *
         * **Warning:** This will override any size management applied by layout managers.
         */
        minWidth: null,
        /**
         * @cfg {Boolean/String/Object} scrollable
         * Configuration options to make this Component scrollable. Acceptable values are:
         *
         * - `true` to enable auto scrolling.
         * - `false` (or `null`) to disable scrolling - this is the default.
         * - `x` or `horizontal` to enable horizontal scrolling only
         * - `y` or `vertical` to enable vertical scrolling only
         *
         * Also accepts a configuration object for a `{@link Ext.scroll.Scroller}` if
         * if advanced configuration is needed.
         *
         * The getter for this config returns the {@link Ext.scroll.Scroller Scroller}
         * instance.  You can use the Scroller API to read or manipulate the scroll position:
         *
         *     // scrolls the component to 5 on the x axis and 10 on the y axis
         *     component.getScrollable().scrollTo(5, 10);
         */
        scrollable: null
    },
    /**
     * @cfg {Object} renderConfig
     * renderConfig wraps configs that do not get applied until after the component is
     * rendered. Unlike normal config system properties, renderConfigs use a special
     * setter method to store values on the instance instead of running the apply and
     * update methods if it is called before the component is rendered. Then, after the
     * component has been rendered, these values are processed by the normal apply and
     * update method for the config.
     *
     * This means that calling the get method for the config prior to render will return
     * whatever raw value has been set, while calling the getter after render will return
     * the value after processing by the config's apply method. If this distinction needs
     * to be made, it is the caller's responsibility to check for the rendered state and
     * handle such intermediate config values.
     */
    renderConfig: {
        /**
         * @cfg {Object} touchAction
         *
         * Emulates the behavior of the CSS
         * [touch-action](https://www.w3.org/TR/pointerevents/#the-touch-action-css-property)
         * property in a cross-browser compatible manner.
         *
         * Keys in this object are touch action names, and values are `false` to disable
         * a touch action or `true` to enable it.  Accepted keys are:
         *
         * - `panX`
         * - `panY`
         * - `pinchZoom`
         * - `doubleTapZoom`
         *
         * All touch actions are enabled (`true`) by default, so it is usually only necessary
         * to specify which touch actions to disable.  For example, the following disables
         * only horizontal scrolling and pinch-to-zoom on the component's main element:
         *
         *     touchAction: {
         *         panX: false,
         *         pinchZoom: false
         *     }
         *
         * Touch actions can be specified on child elements using the child element name,
         * for example:
         *
         *     // disables horizontal scrolling on the main element, and double-tap-zoom
         *     // on the child element named "body"
         *     touchAction: {
         *         panY: false
         *         body: {
         *             doubleTapZoom: false
         *         }
         *     }
         *
         * The primary motivation for setting the touch-action of an element is to prevent
         * the browser's default handling of a gesture such as pinch-to-zoom, or
         * drag-to-scroll, so that the application can implement its own handling of that
         * gesture on the element.  Suppose, for example, a component has a custom drag
         * handler on its element and wishes to prevent horizontal scrolling of its container
         * while it is being dragged:
         *
         *     Ext.create('Ext.Component', {
         *         touchAction: {
         *             panX: false
         *         },
         *         listeners: {
         *             drag: function(e) {
         *                 // implement drag logic
         *             }
         *         }
         *     });
         */
        touchAction: null
    },
    /**
     * @property defaultBindProperty
     * @inheritdoc
     */
    defaultBindProperty: 'html',
    /**
     * @cfg {String} alignTarget
     * A Component or Element by which to position this component according to the
     * {@link #defaultAlign}. Defaults to the owning Container.
     *
     * *Only applicable if this component is {@link #cfg-floating}*
     *
     * *Used upon first show*.
     */
    alignTarget: null,
    /**
     * @cfg anchor
     * @inheritDoc Ext.layout.container.Anchor#cfg-anchor
     */
    /**
     * @cfg {String/Object} autoEl
     * A tag name or {@link Ext.dom.Helper DomHelper} spec used to create the {@link #getEl Element}
     * which will encapsulate this Component.
     *
     * You do not normally need to specify this. For the base classes {@link Ext.Component} and
     * {@link Ext.container.Container}, this defaults to **'div'**. The more complex Sencha classes
     * use a more complex DOM structure specified by their own {@link #cfg-renderTpl}s.
     *
     * This is intended to allow the developer to create application-specific utility Components
     * encapsulated by different DOM elements. Example usage:
     *
     *     {
     *         xtype: 'component',
     *         autoEl: {
     *             tag: 'img',
     *             src: 'http://www.example.com/example.jpg'
     *         }
     *     }, {
     *         xtype: 'component',
     *         autoEl: {
     *             tag: 'blockquote',
     *             html: 'autoEl is cool!'
     *         }
     *     }, {
     *         xtype: 'container',
     *         autoEl: 'ul',
     *         cls: 'ux-unordered-list',
     *         items: {
     *             xtype: 'component',
     *             autoEl: 'li',
     *             html: 'First list item'
     *         }
     *     }
     *
     * @since 2.3.0
     */
    /**
     * @cfg {Boolean/String/HTMLElement/Ext.dom.Element} autoRender
     * This config is intended mainly for non-{@link #cfg-floating} Components which may or may not
     * be shown. Instead of using {@link #renderTo} in the configuration, and rendering upon
     * construction, this allows a Component to render itself upon first
     * _{@link Ext.Component#method-show show}_. If {@link #cfg-floating} is `true`, the value
     * of this config is omitted as if it is `true`.
     *
     * Specify as `true` to have this Component render to the document body upon first show.
     *
     * Specify as an element, or the ID of an element to have this Component render to a specific
     * element upon first show.
     */
    autoRender: false,
    /**
     * @cfg {Boolean} [autoScroll=false]
     * `true` to use overflow:'auto' on the components layout element and show scroll bars
     * automatically when necessary, `false` to clip any overflowing content.
     *
     * This should not be combined with {@link #overflowX} or  {@link #overflowY}.
     * @deprecated 5.1.0 Use {@link #scrollable} instead
     */
    /**
     * @cfg {Boolean} autoShow
     * `true` to automatically show the component upon creation. This config option may only be used
     * for {@link #cfg-floating} components or components that use {@link #autoRender}.
     *
     * @since 2.3.0
     */
    autoShow: false,
    /**
     * @cfg {String} baseCls
     * The base CSS class to apply to this component's element. This will also be prepended
     * to elements within this component like Panel's body will get a class `x-panel-body`.
     * This means that if you create a subclass of Panel, and you want it to get all the Panels
     * styling for the element and the body, you leave the `baseCls` `x-panel` and use
     * `componentCls` to add specific styling for this component.
     */
    baseCls: Ext.baseCSSPrefix + 'component',
    /**
     * @cfg {Number/String/Boolean} border
     * Specifies the border size for this component. The border can be a single numeric value
     * to apply to all sides or it can be a CSS style specification for each style,
     * for example: '10 5 3 10' (top, right, bottom, left).
     *
     * For components that have no border by default, setting this won't make the border appear
     * by itself. You also need to specify border color and style:
     *
     *     border: 5,
     *     style: {
     *         borderColor: 'red',
     *         borderStyle: 'solid'
     *     }
     *
     * To turn off the border, use `border: false`.
     */
    /**
     * @cfg childEls
     * @inheritdoc Ext.util.ElementContainer#cfg-childEls
     */
    /* eslint-disable key-spacing */
    childEls: {
        frameTable: {
            frame: true
        },
        frameTL: {
            frame: 'tl'
        },
        frameTC: {
            frame: 'tc'
        },
        frameTR: {
            frame: 'tr'
        },
        frameML: {
            frame: 'ml'
        },
        frameBody: {
            frame: 'mc'
        },
        frameMR: {
            frame: 'mr'
        },
        frameBL: {
            frame: 'bl'
        },
        frameBC: {
            frame: 'bc'
        },
        frameBR: {
            frame: 'br'
        }
    },
    /* eslint-enable key-spacing */
    /**
     * @cfg {String/String[]} [cls='']
     * An optional extra CSS class that will be added to this component's Element.
     * The value can be a string, a list of strings separated by spaces, or an array of strings.
     * This can be useful for adding customized styles to the component or any of its children
     * using standard CSS rules.
     *
     * @since 1.1.0
     */
    /**
     * @cfg {Number} [columnWidth]
     * Defines the column width inside {@link Ext.layout.container.Column column layout}.
     *
     * The columnWidth property is always evaluated as a percentage and must be a decimal value
     * greater than 0 and less than 1 (e.g., .25). See the description at the top of
     * {@link Ext.layout.container.Column column layout} for additional usage details when combining
     * width and columnWidth configs within the layout.
     */
    /**
     * @cfg {String} componentCls
     * CSS Class to be added to a components root level element to give distinction to it
     * via styling.
     */
    // @cmd-auto-dependency { aliasPrefix : "layout." }
    /**
     * @cfg {String/Object} componentLayout
     * The sizing and positioning of a Component's internal Elements is the responsibility
     * of the Component's layout manager which sizes a Component's internal structure in response
     * to the Component being sized.
     *
     * Generally, developers will not use this configuration as all provided Components which need
     * their internal elements sizing (Such as {@link Ext.form.field.Base input fields}) come with
     * their own componentLayout managers.
     *
     * The {@link Ext.layout.container.Auto default layout manager} will be used on instances of the
     * base Ext.Component class which simply sizes the Component's encapsulating element to the
     * height and width specified in the {@link #setSize} method.
     *
     */
    componentLayout: 'autocomponent',
    /**
     * @cfg {Object/String} constraintInsets
     * An object or a string (in TRBL order) specifying insets from the configured
     * {@link #constrainTo constrain region} within which this component must be constrained
     * when positioning or sizing. Example:
     *
     *     constraintInsets: '10 10 10 10' // Constrain with 10px insets from parent
     */
    /**
     * @cfg {Ext.util.Region/Ext.dom.Element} constrainTo
     * A {@link Ext.util.Region Region} (or an element from which a Region measurement will be read)
     * which is used to constrain the component. Only applies when the component is floating.
     */
    /**
     * @cfg {String} contentEl
     * Specify an existing HTML element, or the `id` of an existing HTML element to use as the
     * content for this component.
     *
     * This config option is used to take an existing HTML element and place it in the layout
     * element of a new component (it simply moves the specified DOM element _after the Component
     * is rendered_ to use as the content.
     *
     * **Notes:**
     *
     * The specified HTML element is appended to the layout element of the component _after any
     * configured {@link #html HTML} has been inserted_, and so the document will not contain
     * this element at the time the {@link #event-render} event is fired.
     *
     * The specified HTML element used will not participate in any
     * **`{@link Ext.container.Container#layout layout}`** scheme that the Component may use.
     * It is just HTML. Layouts operate on child
     * **`{@link Ext.container.Container#cfg-items items}`**.
     *
     * Add either the `x-hidden` or the `x-hidden-display` CSS class to prevent a brief flicker
     * of the content before it is rendered to the panel.
     *
     * @since 3.4.0
     */
    /**
     * @cfg {String} defaultAlign
     * The default {@link Ext.util.Positionable#getAlignToXY Ext.dom.Element#getAlignToXY} anchor
     * position value for this component relative to its {@link #alignTarget}
     * (which defaults to its owning Container).
     *
     * _Only applicable if this component is {@link #cfg-floating}_
     *
     * *Used upon first show*.
     */
    defaultAlign: 'c-c',
    /**
     * @cfg {Boolean} disabled
     * `true` to disable the component.
     * @since 2.3.0
     */
    disabled: false,
    // http://www.w3.org/TR/html5/disabled-elements.html
    disabledRe: /^(?:button|input|select|textarea|optgroup|option|fieldset)$/i,
    nonMaskableRe: (function() {
        var re = [
                'input',
                'select',
                'textarea',
                'optgroup',
                'option',
                'table'
            ];
        // All IE browsers 9 and below except for IE 9 standards.
        if (Ext.isIE9m && !(Ext.isIE9 && !Ext.isIEQuirks)) {
            // <p>.insertAdjacentHTML('BeforeEnd', '<div>...</div>') yields
            // 'Invalid source HTML for this operation' in all IEs not IE 9 standards.
            re.push('p');
        }
        return new RegExp('^(?:' + re.join('|') + ')$', 'i');
    }()),
    /**
     * @cfg {String} disabledCls
     * CSS class to add when the Component is disabled.
     */
    disabledCls: Ext.baseCSSPrefix + 'item-disabled',
    /**
     * @cfg {'top'/'bottom'/'left'/'right'} dock
     * The side of the {@link Ext.panel.Panel panel} where this component is to be
     * docked when specified in the panel's
     * {@link Ext.panel.Panel#dockedItems dockedItems} config.
     *
     * Possible values are:
     *
     *  - top
     *  - bottom
     *  - left
     *  - right
     */
    /**
     * @cfg {Boolean/Object} draggable
     * Specify as true to make a {@link #cfg-floating} Component draggable using the Component's
     * encapsulating element as the drag handle.
     *
     * This may also be specified as a config object for the
     * {@link Ext.util.ComponentDragger ComponentDragger} which is instantiated to perform dragging.
     *
     * For example to create a Component which may only be dragged around using a certain internal
     * element as the drag handle, use the delegate option:
     *
     *     new Ext.Component({
     *         constrain: true,
     *         floating: true,
     *         style: {
     *             backgroundColor: '#fff',
     *             border: '1px solid black'
     *         },
     *         html: '<h1 style="cursor:move">The title</h1><p>The content</p>',
     *         draggable: {
     *             delegate: 'h1'
     *         }
     *     }).show();
     */
    draggable: false,
    /**
     * @cfg {Number} flex
     * Flex may be applied to **child items** of a box layout
     * ({@link Ext.layout.container.VBox vbox} or {@link Ext.layout.container.HBox hbox}).
     * Each child item with a flex property will fill space (horizontally in `hbox`, vertically
     * in `vbox`) according to that item's **relative** flex value compared to the sum of all items
     * with a flex value specified.
     *
     * Any child items that have either a `flex` of `0` or `undefined`
     * will not be 'flexed' (the initial size will not be changed).
     */
    /**
     * @cfg {Boolean} floating
     * Specify as true to float the Component outside of the document flow using CSS absolute
     * positioning.
     *
     * Components such as {@link Ext.window.Window Window}s and {@link Ext.menu.Menu Menu}s are
     * floating by default.
     *
     * Floating Components that are programmatically {@link Ext.Component#method-render rendered}
     * will register themselves with the global {@link Ext.WindowManager ZIndexManager}
     *
     * ### Floating Components as child items of a Container
     *
     * A floating Component may be used as a child item of a Container. This just allows
     * the floating Component to seek a ZIndexManager by examining the ownerCt chain.
     *
     * When configured as floating, Components acquire, at render time, a
     * {@link Ext.ZIndexManager ZIndexManager} which manages a stack of related floating Components.
     * The ZIndexManager sorts its stack according to an incrementing access counter and the
     * {@link Ext.util.Floating#alwaysOnTop alwaysOnTop} config when the Component's
     * {@link #toFront} method is called.
     *
     * The ZIndexManager is found by traversing up the {@link #ownerCt} chain to find an ancestor
     * which itself is floating. This is so that descendant floating Components of floating
     * _Containers_ (Such as a ComboBox dropdown within a Window) can have its zIndex managed
     * relative to any siblings, but always **above** that floating ancestor Container.
     *
     * If no floating ancestor is found, a floating Component registers itself with the default
     * {@link Ext.WindowManager ZIndexManager}.
     *
     * Floating components _do not participate in the Container's layout_. Because of this,
     * they are not rendered until you explicitly {@link #method-show} them.
     *
     * After rendering, the ownerCt reference is deleted, and the {@link #floatParent} property
     * is set to the found floating ancestor Container. If no floating ancestor Container was found
     * the {@link #floatParent} property will not be set.
     */
    floating: false,
    /**
     * @cfg {Boolean} [formBind=false]
     * When inside FormPanel, any component configured with `formBind: true` will
     * be enabled/disabled depending on the validity state of the form.
     * See {@link Ext.form.Panel} for more information and example.
     */
    /**
     * @cfg {Boolean} frame
     * Specify as `true` to have the Component inject framing elements within the Component
     * at render time to provide a graphical rounded frame around the Component content.
     *
     * This is only necessary when running on outdated, or non standard-compliant browsers
     * such as Microsoft's Internet Explorer prior to version 9 which do not support rounded corners
     * natively.
     *
     * The extra space taken up by this framing is available from the read only property
     * {@link #frameSize}.
     */
    /**
     * @cfg {Number|String} height
     * The height of this component. A numeric value will be interpreted as the number of
     * pixels; a string value will be treated as a CSS value with units.
     */
    /**
     * @cfg {Boolean} hidden
     * `true` to hide the component.
     * @since 2.3.0
     */
    hidden: false,
    /**
     * @cfg {String} hideMode
     * A String which specifies how this Component's encapsulating DOM element will be hidden.
     * Values may be:
     *
     *   - `'display'` : The Component will be hidden using the `display: none` style.
     *   - `'visibility'` : The Component will be hidden using the `visibility: hidden` style.
     *   - `'offsets'` : The Component will be hidden by absolutely positioning it out of the
     *     visible area of the document.
     *     This is useful when a hidden Component must maintain measurable dimensions. Hiding using
     *     `display` results in a Component having zero dimensions.
     *
     * @since 1.1.0
     */
    hideMode: 'display',
    /**
     * @cfg {String/Object} [html='']
     * An HTML fragment, or a {@link Ext.dom.Helper DomHelper} specification to use as the layout
     * element content. The HTML content is added after the component is rendered, so the document
     * will not contain this HTML at the time the {@link #event-render} event is fired. This content
     * is inserted into the body _before_ any configured {@link #contentEl} is appended.
     *
     * @since 3.4.0
     */
    /**
     * @cfg {String} id
     * The **unique** id of this component instance.
     *
     * Use of this config should be considered carefully as this value must be unique across
     * all existing components. Components created with an `id` may be accessed globally
     * using {@link Ext#getCmp Ext.getCmp}.
     *
     * Instead of using assigned ids, consider a {@link #reference} config and
     * a {@link #cfg-controller ViewController} to respond to events and perform processing
     * upon this Component.
     *
     * Alternatively, {@link #itemId} and {@link Ext.ComponentQuery ComponentQuery} can be
     * used to perform selector-based searching for Components analogous to DOM querying.
     * The {@link Ext.container.Container Container} class contains several helpful
     * {@link Ext.container.Container#down shortcut methods} to query its descendant
     * Components by selector.
     *
     * Note that this `id` will also be used as the element id for the containing HTML
     * element that is rendered to the page for this component. This allows you to write
     * id-based CSS rules to style the specific instance of this component uniquely, and
     * also to select sub-elements using this component's `id` as the parent.
     *
     * Defaults to an {@link #getId auto-assigned id}.
     *
     * **Note**: Valid identifiers start with a letter or underscore and are followed by
     * (optional) additional letters, underscores, digits or hyphens.
     *
     * @since 1.1.0
     */
    /**
     * @cfg {String} itemId
     * The **unique** id of this component instance within its container. See also the
     * {@link #reference} config.
     *
     * An `itemId` can be used as an alternative way to get a reference to a component
     * when no object reference is available. Instead of using an `{@link #id}` with
     * {@link Ext#getCmp getCmp}, use `itemId` with
     * {@link Ext.container.Container#getComponent getComponent} which will retrieve `itemId`'s
     * or {@link #id}'s. Since `itemId`'s are an index to the container's internal collection,
     * the `itemId` is scoped locally to the container -- avoiding potential conflicts with
     * {@link Ext.ComponentManager}, which requires a **unique** {@link #id} value.
     *
     *     var c = new Ext.panel.Panel({ //
     *         height: 300,
     *         renderTo: document.body,
     *         layout: 'auto',
     *         items: [{
     *             itemId: 'p1',
     *             title: 'Panel 1',
     *             height: 150
     *         },{
     *             itemId: 'p2',
     *             title: 'Panel 2',
     *             height: 150
     *         }]
     *     });
     *
     *     p1 = c.getComponent('p1'); // not the same as Ext.getCmp()
     *     console.log(p1);
     *     p2 = p1.ownerCt.getComponent('p2'); // reference via a sibling
     *     console.log(p2);
     *
     * Also see {@link #id}, `{@link Ext.container.Container#query}`,
     * `{@link Ext.container.Container#down}` and `{@link Ext.container.Container#child}`.
     *
     * **Note**: Valid identifiers start with a letter or underscore and are followed by
     * (optional) additional letters, underscores, digits or hyphens.
     *
     * **Note**: to access the container of an item see {@link #ownerCt}.
     *
     * @since 3.4.0
     */
    /**
     * @cfg {Ext.ComponentLoader/Object} loader
     * A configuration object or an instance of a {@link Ext.ComponentLoader} to load remote content
     * for this Component.
     *
     *     Ext.create('Ext.Component', {
     *         loader: {
     *             url: 'content.html',
     *             autoLoad: true
     *         },
     *         renderTo: Ext.getBody()
     *     });
     */
    /**
     * @cfg {Number/String} margin
     * Specifies the margin for this component. The margin can be a single numeric value to apply
     * to all sides or it can be a CSS style specification for each style,
     * for example: '10 5 3 10' (top, right, bottom, left).
     */
    /**
     * @cfg {String} maskElement
     * Related to the {@link #cfg-childEls} configuration which specifies named properties
     * which correspond to component sub-elements.
     *
     * The name of the element property in this component to mask when masked by a LoadMask.
     *
     * Defaults to `null` to indicate that Components cannot by default contain a LoadMask,
     * and that any LoadMask should be rendered into the document body.
     *
     * For example, Panels use `"el"` to indicate that the whole panel should be masked.
     * This could be configured to be `"body"` so that only the body is masked and toolbars
     * and the header are still mouse-accessible.
     */
    maskElement: null,
    /**
     * @cfg {Object} [maskDefaults] Default LoadMask configuration for {@link #method-setLoading}.
     */
    /**
     * @cfg {String} [overCls='']
     * An optional extra CSS class that will be added to this component's Element when the mouse
     * moves over the Element, and removed when the mouse moves out. This can be useful for adding
     * customized 'active' or 'hover' styles to the component or any of its children using standard
     * CSS rules.
     *
     * @since 2.3.0
     */
    /**
     * @cfg {String} overflowX
     * Possible values are:
     *
     *  - `'auto'` to enable automatic horizontal scrollbar (Style overflow-x: 'auto').
     *  - `'scroll'` to always enable horizontal scrollbar (Style overflow-x: 'scroll').
     *
     * The default is overflow-x: 'hidden'. This should not be combined with {@link #autoScroll}.
     * @deprecated 5.1.0 Use {@link #scrollable} instead
     */
    /**
     * @cfg {String} overflowY
     * Possible values are:
     *
     *  - `'auto'` to enable automatic vertical scrollbar (Style overflow-y: 'auto').
     *  - `'scroll'` to always enable vertical scrollbar (Style overflow-y: 'scroll').
     *
     * The default is overflow-y: 'hidden'. This should not be combined with {@link #autoScroll}.
     * @deprecated 5.1.0 Use {@link #scrollable} instead
     */
    /**
     * @cfg {Number/String} padding
     * Specifies the padding for this component. The padding can be a single numeric value to apply
     * to all sides or it can be a CSS style specification for each style,
     * for example: '10 5 3 10' (top, right, bottom, left).
     */
    /**
     * @cfg {Array/Ext.enums.Plugin/Object/Ext.plugin.Abstract} plugins
     * This config describes one or more plugin config objects used to create plugin
     * instances for this component.
     *
     * Plugins are a way to bundle and reuse custom functionality. Plugins should extend
     * `Ext.plugin.Abstract` but technically the only requirement for a valid plugin
     * is that it contain an `init` method that accepts a reference to its owner. Once
     * a plugin is created, the owner will call the `init` method, passing a reference
     * to itself. Each plugin can then call methods or respond to events on its owner
     * as needed to provide its functionality.
     *
     * This config's value can take several different forms.
     *
     * The value can be a single string with the plugin's {@link Ext.enums.Plugin alias}:
     *
     *     plugins: 'cellediting',
     *
     * The preferred form for multiple plugins or to configure plugins is the keyed-object
     * form (new in version 6.5):
     *
     *      plugins: {
     *          gridviewdragdrop: true,
     *          cellediting: {
     *              clicksToEdit: 1
     *          }
     *      },
     *
     * The keys are `id`'s as well as the default type alias.
     *
     * The `plugins` config can also be an array of plugin aliases:
     *
     *     plugins: [ 'cellediting', 'gridviewdragdrop' ],
     *
     * An array can also contain elements that are config objects with a `ptype` property
     * holding the type alias:
     *
     *      plugins: ['gridviewdragdrop', {
     *          ptype: 'cellediting',
     *          clicksToEdit: 1
     *      }],
     *
     * @since 2.3.0
     */
    /**
     * @cfg {"north"/"south"/"east"/"west"/"center"} [region=undefined]
     * Defines the region inside {@link Ext.layout.container.Border border layout}.
     *
     * Possible values:
     *
     * - north - Positions component at top.
     * - south - Positions component at bottom.
     * - east - Positions component at right.
     * - west - Positions component at left.
     * - center - Positions component at the remaining space.
     *   There **must** be a component with `region: "center"` in every border layout.
     */
    /**
     * @cfg {Object} renderData
     *
     * The data used by {@link #renderTpl} in addition to the following property values
     * of the component:
     *
     * - id
     * - ui
     * - uiCls
     * - baseCls
     * - componentCls
     * - frame
     *
     * See {@link #renderSelectors} and {@link #cfg-childEls} for usage examples.
     */
    /**
     * @cfg {Object} renderSelectors
     * An object containing properties specifying CSS selectors which identify child elements
     * created by the render process.
     *
     * After the Component's internal structure is rendered according to the {@link #renderTpl},
     * this object is iterated through, and the found Elements are added as properties
     * to the Component using the `renderSelector` property name.
     *
     * For example, a Component which renders a title and description into its element:
     *
     *      Ext.create('Ext.Component', {
     *          renderTo: Ext.getBody(),
     *          renderTpl: [
     *              '<h1 class="title">{title}</h1>',
     *              '<p>{desc}</p>'
     *          ],
     *          renderData: {
     *              title: "Error",
     *              desc: "Something went wrong"
     *          },
     *          renderSelectors: {
     *              titleEl: 'h1.title',
     *              descEl: 'p'
     *          },
     *          listeners: {
     *              afterrender: function(cmp){
     *                  // After rendering the component will have a titleEl and descEl properties
     *                  cmp.titleEl.setStyle({color: "red"});
     *              }
     *          }
     *      });
     *
     * The use of `renderSelectors` is deprecated (for performance reasons). The above
     * code should be refactored into something like this:
     *
     *      Ext.create('Ext.Component', {
     *          renderTo: Ext.getBody(),
     *          renderTpl: [
     *              '<h1 class="title" id="{id}-titleEl" data-ref="titleEl">{title}</h1>',
     *              '<p id="{id}-descEl" data-ref="descEl">{desc}</p>'
     *          ],
     *          renderData: {
     *              title: "Error",
     *              desc: "Something went wrong"
     *          },
     *          childEls: [
     *              'titleEl',
     *              'descEl'
     *          ]
     *      });
     *
     * To use `childEls` yet retain the use of selectors (which remains as expensive as
     * `renderSelectors`):
     *
     *      Ext.create('Ext.Component', {
     *          renderTo: Ext.getBody(),
     *          renderTpl: [
     *              '<h1 class="title">{title}</h1>',
     *              '<p>{desc}</p>'
     *          ],
     *          renderData: {
     *              title: "Error",
     *              desc: "Something went wrong"
     *          },
     *          childEls: {
     *              titleEl: { selectNode: 'h1.title' },
     *              descEl: { selectNode: 'p' }
     *          }
     *      });
     *
     * @deprecated 5.0 Use {@link #cfg-childEls} instead.
     */
    /**
     * @cfg {String/HTMLElement/Ext.dom.Element} renderTo
     * Specify the `id` of the element, a DOM element or an existing Element that this component
     * will be rendered into.
     *
     * **Notes:**
     *
     * Do *not* use this option if the Component is to be a child item of a
     * {@link Ext.container.Container Container}. It is the responsibility of the
     * {@link Ext.container.Container Container}'s
     * {@link Ext.container.Container#layout layout manager} to render and manage its child items.
     *
     * When using this config, a call to `render()` is not required.
     *
     * See also: {@link #method-render}.
     *
     * @since 2.3.0
     */
    /**
     * @cfg {Ext.XTemplate/String/String[]} renderTpl
     * An {@link Ext.XTemplate XTemplate} used to create the internal structure inside
     * this Component's encapsulating {@link #getEl Element}.
     *
     * You do not normally need to specify this. For the base classes {@link Ext.Component} and
     * {@link Ext.container.Container}, this defaults to **`null`** which means that they will be
     * initially rendered with no internal structure; they render their {@link #getEl Element}
     * empty. The more specialized classes with complex DOM structures provide their own template
     * definitions.
     *
     * This is intended to allow the developer to create application-specific utility Components
     * with customized internal structure.
     *
     * Upon rendering, any created child elements may be automatically imported into object
     * properties using the {@link #renderSelectors} and {@link #cfg-childEls} options.
     * @protected
     */
    renderTpl: '{%this.renderContent(out,values)%}',
    /**
     * @cfg {Boolean/Object} resizable
     * Specify as `true` to apply a {@link Ext.resizer.Resizer Resizer} to this Component
     * after rendering.
     *
     * May also be specified as a config object to be passed to the constructor of
     * {@link Ext.resizer.Resizer Resizer} to override any defaults. By default the Component
     * passes its minimum and maximum size, and uses `{@link Ext.resizer.Resizer#dynamic}: false`
     */
    /**
     * @cfg {String} resizeHandles
     * A valid {@link Ext.resizer.Resizer} handles config string. Only applies when
     * resizable = true.
     */
    resizeHandles: 'all',
    /**
     * @cfg {Boolean/Number} shrinkWrap
     *
     * The possible values for shrinkWrap are:
     *
     *   - 0 (or `false`): Neither width nor height depend on content.
     *   - 1: Width depends on content (shrink wraps), but height does not.
     *   - 2: Height depends on content (shrink wraps), but width does not.
     *   - 3 (or `true`): Both width and height depend on content (shrink wrap).
     *
     * In CSS terms, shrink-wrap width is analogous to an inline-block element as opposed
     * to a block-level element.
     *
     * @localdoc ##Non-Panel Components
     *
     * The shrinkWrap config is a class-level config and should be used when defining a
     * subclass.
     * It is not intended to be set as a config on instances of a given component.
     *
     * For non-Panel components, shrinkWrap is a descriptive config only.  It should be
     * set when defining your own custom class including the DOM elements used to
     * construct the component.  The shrinkWrap property does not itself apply styling on
     * the component elements.  Rather, it should describe the CSS styling you've applied
     * to your custom component (_refer to the numeric matrix above_).
     *
     * When a component is owned by a container the layout of that container will inspect
     * the component's shrinkWrap property during layout.  The layout then uses the
     * content-wrapping policy described by shrinkWrap to correctly size and position the
     * container's child items.
     */
    shrinkWrap: 2,
    /**
     * @cfg stateEvents
     * @inheritdoc Ext.state.Stateful#cfg-stateEvents
     * @localdoc By default the following stateEvents are added:
     *
     *  - {@link #event-resize}
     */
    /**
     * @cfg {String/Object} style
     * A custom style specification to be applied to this component's Element.
     * Should be a valid argument to {@link Ext.dom.Element#applyStyles}.
     *
     *     new Ext.panel.Panel({
     *         title: 'Some Title',
     *         renderTo: Ext.getBody(),
     *         width: 400, height: 300,
     *         layout: 'form',
     *         items: [{
     *             xtype: 'textarea',
     *             style: {
     *                 width: '95%',
     *                 marginBottom: '10px'
     *             }
     *         },
     *         new Ext.button.Button({
     *             text: 'Send',
     *             minWidth: '100',
     *             style: {
     *                 marginBottom: '10px'
     *             }
     *         })
     *         ]
     *     });
     *
     * @since 1.1.0
     */
    /**
     * @cfg {Boolean} toFrontOnShow
     * True to automatically call {@link #toFront} when the {@link #method-show} method is called
     * on an already visible, floating component.
     */
    toFrontOnShow: true,
    /**
     * @cfg {Ext.XTemplate/Ext.Template/String/String[]} tpl
     * An {@link Ext.Template}, {@link Ext.XTemplate} or an array of strings to form
     * an Ext.XTemplate. Used in conjunction with the `{@link #data}` and `{@link #tplWriteMode}`
     * configurations.
     *
     * @since 3.4.0
     */
    /**
     * @property {Boolean} synthetic
     * This property is `true` if the component was created internally by the framework
     * and is not explicitly user-defined. This is set for such things as `Splitter`
     * instances managed by `border` and `box` layouts.
     * @private
     */
    synthetic: false,
    /**
     * @cfg {String} tplWriteMode
     * The Ext.(X)Template method to use when updating the content area of the Component.
     * See `{@link Ext.XTemplate#overwrite}` for information on default mode.
     *
     * @since 3.4.0
     */
    tplWriteMode: 'overwrite',
    /**
     * @cfg {String} ui
     * A UI style for a component.
     */
    ui: 'default',
    /**
     * @cfg {String[]} uiCls
     * An array of of `classNames` which are currently applied to this component.
     * @private
     */
    uiCls: [],
    /**
     * @cfg {String/String[]} userCls
     * One or more CSS classes to add to the component's primary element. This config
     * is intended solely for use by the component instantiator (the "user"), not by
     * derived classes.
     *
     * For example:
     *
     *      items: [{
     *          xtype: 'button',
     *          userCls: 'my-button'
     *      ...
     *      }]
     * @accessor
     */
    userCls: null,
    /**
     * @cfg {Number} weight
     * A value to control how Components are laid out in a
     * {@link Ext.layout.container.Border Border} layout or as docked items.
     *
     * In a Border layout, this can control how the regions (not the center) region lay out
     * if the west or east take full height or if the north or south region take full width.
     * Also look at the {@link Ext.layout.container.Border#regionWeights} on the Border layout.
     * An example to show how you can take control of this is:
     *
     *     Ext.create('Ext.container.Viewport', {
     *         layout      : 'border',
     *         defaultType : 'panel',
     *         items       : [
     *             {
     *                 region : 'north',
     *                 title  : 'North',
     *                 height : 100
     *             },
     *             {
     *                 region : 'south',
     *                 title  : 'South',
     *                 height : 100,
     *                 weight : -25
     *             },
     *             {
     *                 region : 'west',
     *                 title  : 'West',
     *                 width  : 200,
     *                 weight : 15
     *             },
     *             {
     *                 region : 'east',
     *                 title  : 'East',
     *                 width  : 200
     *             },
     *             {
     *                 region : 'center',
     *                 title  : 'center'
     *             }
     *         ]
     *     });
     *
     * If docked items, the weight will order how the items are laid out. Here is an example
     * to put a {@link Ext.toolbar.Toolbar} above a {@link Ext.panel.Panel}'s header:
     *
     *     Ext.create('Ext.panel.Panel', {
     *         renderTo    : document.body,
     *         width       : 300,
     *         height      : 300,
     *         title       : 'Panel',
     *         html        : 'Panel Body',
     *         dockedItems : [
     *             {
     *                 xtype : 'toolbar',
     *                 items : [
     *                     {
     *                         text : 'Save'
     *                     }
     *                 ]
     *             },
     *             {
     *                 xtype  : 'toolbar',
     *                 weight : -10,
     *                 items  : [
     *                     {
     *                         text : 'Remove'
     *                     }
     *                 ]
     *             }
     *         ]
     *     });
     */
    weight: null,
    /**
     * @cfg {Number|String} width
     * The width of this component. A numeric value will be interpreted as the number of
     * pixels; a string value will be treated as a CSS value with units.
     */
    /**
     * @cfg {Ext.enums.Widget} xtype
     * **Note:** Only applies to {@link Ext.Component} derived classes when used as
     * a config in {@link Ext#define Ext.define}.
     *
     * This property provides a shorter alternative to creating objects than using a full
     * class name. Using `xtype` is the most common way to define component instances,
     * especially in a container. For example, the items in a form containing text fields
     * could be created explicitly like so:
     *
     *      items: [
     *          Ext.create('Ext.form.field.Text', {
     *              fieldLabel: 'Foo'
     *          }),
     *          Ext.create('Ext.form.field.Text', {
     *              fieldLabel: 'Bar'
     *          }),
     *          Ext.create('Ext.form.field.Number', {
     *              fieldLabel: 'Num'
     *          })
     *      ]
     *
     * But by using `xtype`, the above becomes:
     *
     *      items: [
     *          {
     *              xtype: 'textfield',
     *              fieldLabel: 'Foo'
     *          },
     *          {
     *              xtype: 'textfield',
     *              fieldLabel: 'Bar'
     *          },
     *          {
     *              xtype: 'numberfield',
     *              fieldLabel: 'Num'
     *          }
     *      ]
     *
     * When the `xtype` is common to many items, {@link Ext.container.Container#defaultType}
     * is another way to specify the `xtype` for all items that don't have an explicit `xtype`:
     *
     *      defaultType: 'textfield',
     *      items: [
     *          { fieldLabel: 'Foo' },
     *          { fieldLabel: 'Bar' },
     *          { fieldLabel: 'Num', xtype: 'numberfield' }
     *      ]
     *
     * Each member of the `items` array is now just a "configuration object". These objects
     * are used to create and configure component instances. A configuration object can be
     * manually used to instantiate a component using {@link Ext#widget}:
     *
     *      var text1 = Ext.create('Ext.form.field.Text', {
     *          fieldLabel: 'Foo'
     *      });
     *
     *      // or alternatively:
     *
     *      var text1 = Ext.widget({
     *          xtype: 'textfield',
     *          fieldLabel: 'Foo'
     *      });
     *
     * This conversion of configuration objects into instantiated components is done when
     * a container is created as part of its {Ext.container.AbstractContainer#initComponent}
     * process. As part of the same process, the `items` array is converted from its raw
     * array form into a {@link Ext.util.MixedCollection} instance.
     *
     * You can define your own `xtype` on a custom {@link Ext.Component component} by specifying
     * the `xtype` property in {@link Ext#define}. For example:
     *
     *     Ext.define('MyApp.PressMeButton', {
     *         extend: 'Ext.button.Button',
     *         xtype: 'pressmebutton',
     *         text: 'Press Me'
     *     });
     *
     * Care should be taken when naming an `xtype` in a custom component because there is
     * a single, shared scope for all xtypes. Third part components should consider using
     * a prefix to avoid collisions.
     *
     *     Ext.define('Foo.form.CoolButton', {
     *         extend: 'Ext.button.Button',
     *         xtype: 'ux-coolbutton',
     *         text: 'Cool!'
     *     });
     *
     * See {@link Ext.enums.Widget} for list of all available xtypes.
     *
     * @since 2.3.0
     */
    /**
     * @cfg {Number} tabIndex
     * DOM tabIndex attribute for this component's {@link #focusEl}.
     */
    // ***********************************************************************************
    // End Config
    // ***********************************************************************************
    // </editor-fold>
    // <editor-fold desc="Properties">
    // ***********************************************************************************
    // Begin Properties
    // ***********************************************************************************
    /**
     * @private
     */
    allowDomMove: true,
    /**
     * @property {Boolean} autoGenId
     * `true` indicates an `id` was auto-generated rather than provided by configuration.
     * @private
     */
    autoGenId: false,
    /**
     * @private
     */
    borderBoxCls: Ext.baseCSSPrefix + 'border-box',
    /**
     * @property {Number} componentLayoutCounter
     * @private
     * The number of component layout calls made on this object.
     */
    componentLayoutCounter: 0,
    /**
     * @property {String} contentPaddingProperty
     * The name of the padding property that is used by the layout to manage
     * padding.  See {@link Ext.layout.container.Auto#managePadding managePadding}
     */
    contentPaddingProperty: 'padding',
    /**
     * @private
     */
    deferLayouts: false,
    /**
     * @property {Ext.Container} floatParent
     * **Only present for {@link #cfg-floating} Components which were inserted as child items
     * of Containers.**
     *
     * There are other similar relationships such as the {@link Ext.button.Button button} which
     * activates a {@link Ext.button.Button#cfg-menu menu}, or the
     * {@link Ext.menu.Item menu item} which activated a {@link Ext.menu.Item#cfg-menu submenu},
     * or the {@link Ext.grid.column.Column column header} which activated the column menu.
     *
     * These differences are abstracted away by the {@link #up} method.
     *
     * Floating Components that are programmatically {@link Ext.Component#method-render rendered}
     * will not have a `floatParent` property.
     *
     * See {@link #cfg-floating} and {@link #zIndexManager}
     * @readonly
     */
    /**
     * @property {Object} frameSize
     * @readonly
     * Indicates the width of any framing elements which were added within the encapsulating
     * element to provide graphical, rounded borders. See the {@link #frame} config. This
     * property is `null` if the component is not framed.
     *
     * This is an object containing the frame width in pixels for all four sides of the
     * Component containing the following properties:
     *
     * @property {Number} [frameSize.top=0] The width of the top framing element in pixels.
     * @property {Number} [frameSize.right=0] The width of the right framing element in pixels.
     * @property {Number} [frameSize.bottom=0] The width of the bottom framing element in pixels.
     * @property {Number} [frameSize.left=0] The width of the left framing element in pixels.
     * @property {Number} [frameSize.width=0] The total width of the left and right framing elements
     * in pixels.
     * @property {Number} [frameSize.height=0] The total height of the top and right bottom elements
     * in pixels.
     */
    frameSize: null,
    /**
     * @private
     */
    horizontalPosProp: 'left',
    /**
     * @property {Boolean} isComponent
     * `true` in this class to identify an object as an instantiated Component, or subclass thereof.
     */
    isComponent: true,
    /**
     * @property {Boolean} _isLayoutRoot
     * Setting this property to `true` causes the {@link #isLayoutRoot} method to return
     * `true` and stop the search for the top-most component for a layout.
     * @protected
     */
    _isLayoutRoot: false,
    /**
     * @private
     */
    layoutSuspendCount: 0,
    /**
     * @cfg {Boolean} liquidLayout
     * Components that achieve their internal layout results using solely CSS with no JS
     * intervention must set this to true.  This allows the component to opt out of the
     * layout run when used inside certain container layouts such as {@link
     * Ext.layout.container.Form Form} and {@link Ext.layout.container.Auto Auto}
     * resulting in a performance gain. The following components currently use liquid
     * layout (`liquidLayout: true`):
     *
     * - All Form Fields (subclasses of {@link Ext.form.field.Base})
     * - {@link Ext.button.Button}
     *
     * It is important to keep in mind that components using liquidLayout do not fire
     * the following events:
     *
     * - {@link #event-resize}
     * - {@link #event-boxready}
     *
     * In addition, liquidLayout components do not call the following template methods:
     *
     * - {@link #method!afterComponentLayout}
     * - {@link #method!onBoxReady}
     * - {@link #method!onResize}
     *
     * Any component that needs to fire these events or to have these methods called during
     * its life cycle needs to set `liquidLayout` to `false`.  The following example
     * demonstrates how to enable the resize event for a
     * {@link Ext.form.field.TextArea TextArea Field}:
     *
     *     @example
     *     var win = Ext.create({
     *             xtype: 'window',
     *             title: 'Resize This Window!',
     *             height: 100,
     *             width: 200,
     *             layout: 'anchor',
     *             items: [{
     *                 xtype: 'textarea',
     *                 anchor: '0 0',
     *                 liquidLayout: false // allows the textarea to fire "resize"
     *             }]
     *         }),
     *         textfield = win.items.getAt(0);
     *
     *     win.show();
     *
     *     textfield.on('resize', function(textfield, width, height) {
     *         Ext.Msg.alert('Text Field Resized', 'width: ' + width + ', height: ' + height);
     *     });
     *
     * Use caution when setting `liquidLayout` to `false` as it carries a performance penalty
     * since it means the layout system must perform expensive DOM reads to determine the
     * Component's size.
     */
    liquidLayout: false,
    /**
     * @property {Boolean} maskOnDisable
     * This is an internal flag that you use when creating custom components. By default this is set
     * to `true` which means that every component gets a mask when it's disabled. Components like
     * FieldContainer, FieldSet, Field, Button, Tab override this property to `false`
     * since they want to implement custom disable logic.
     */
    maskOnDisable: true,
    /**
     * @property {Ext.Container} ownerCt
     * This Component's owner {@link Ext.container.Container Container} (is set automatically
     * when this Component is added to a Container).
     *
     * *Important.* This is not a universal upwards navigation pointer. It indicates the Container
     * which owns and manages this Component if any. There are other similar relationships such as
     * the {@link Ext.button.Button button} which activates a
     * {@link Ext.button.Button#cfg-menu menu}, or the {@link Ext.menu.Item menu item} which
     * activated a {@link Ext.menu.Item#cfg-menu submenu}, or the
     * {@link Ext.grid.column.Column column header} which activated the column menu.
     *
     * These differences are abstracted away by the {@link #up} method.
     *
     * **Note**: to access items within the Container see {@link #itemId}.
     * @readonly
     * @since 2.3.0
     */
    /**
     * @property {Boolean} rendered
     * Indicates whether or not the component has been rendered.
     * @readonly
     * @since 1.1.0
     */
    rendered: false,
    /**
     * @private
     */
    rootCls: Ext.baseCSSPrefix + 'body',
    /**
     * @private
     */
    scrollerCls: Ext.baseCSSPrefix + 'scroll-scroller',
    /**
     * @property {Object} scrollFlags
     * An object property which provides unified information as to which dimensions are
     * scrollable based upon the {@link #scrollable} settings (And for *views* of trees and
     * grids, the owning panel's {@link Ext.panel.Table#scroll scroll} setting).
     *
     * Note that if you set overflow styles using the {@link #style} config or
     * {@link Ext.panel.Panel#bodyStyle bodyStyle} config, this object does not include
     * that information. Use {@link #scrollable} if you need to access these flags.
     *
     * This object has the following properties:
     * @property {Boolean} scrollFlags.x `true` if this Component is scrollable
     * horizontally - style setting may be `'auto'` or `'scroll'`.
     * @property {Boolean} scrollFlags.y `true` if this Component is scrollable
     * vertically - style setting may be `'auto'` or `'scroll'`.
     * @property {Boolean} scrollFlags.both `true` if this Component is scrollable both
     * horizontally and vertically.
     * @property {String} scrollFlags.overflowX The `overflow-x` style setting, `'auto'`
     * or `'scroll'` or `''`.
     * @property {String} scrollFlags.overflowY The `overflow-y` style setting, `'auto'`
     * or `'scroll'` or `''`.
     * @readonly
     * @private
     */
    _scrollFlags: {
        auto: {
            // x:auto, y:auto
            auto: {
                overflowX: 'auto',
                overflowY: 'auto',
                x: true,
                y: true,
                both: true
            },
            // x:auto, y:false
            'false': {
                overflowX: 'auto',
                overflowY: 'hidden',
                x: true,
                y: false,
                both: false
            },
            // x:auto, y:scroll
            scroll: {
                overflowX: 'auto',
                overflowY: 'scroll',
                x: true,
                y: true,
                both: true
            }
        },
        'false': {
            // x:false, y:auto
            auto: {
                overflowX: 'hidden',
                overflowY: 'auto',
                x: false,
                y: true,
                both: false
            },
            // x:false, y:false
            'false': {
                overflowX: 'hidden',
                overflowY: 'hidden',
                x: false,
                y: false,
                both: false
            },
            // x:false, y:scroll
            scroll: {
                overflowX: 'hidden',
                overflowY: 'scroll',
                x: false,
                y: true,
                both: false
            }
        },
        scroll: {
            // x:scroll, y:auto
            auto: {
                overflowX: 'scroll',
                overflowY: 'auto',
                x: true,
                y: true,
                both: true
            },
            // x:scroll, y:false
            'false': {
                overflowX: 'scroll',
                overflowY: 'hidden',
                x: true,
                y: false,
                both: false
            },
            // x:scroll, y:scroll
            scroll: {
                overflowX: 'scroll',
                overflowY: 'scroll',
                x: true,
                y: true,
                both: true
            }
        },
        none: {
            overflowX: '',
            overflowY: '',
            x: false,
            y: false,
            both: false
        }
    },
    _scrollableCfg: {
        x: {
            x: true,
            y: false
        },
        y: {
            x: false,
            y: true
        },
        horizontal: {
            x: true,
            y: false
        },
        vertical: {
            x: false,
            y: true
        },
        both: {
            x: true,
            y: true
        },
        'true': {
            x: true,
            y: true
        }
    },
    validIdRe: Ext.validIdRe,
    // ***********************************************************************************
    // End Properties
    // ***********************************************************************************
    // </editor-fold>
    // <editor-fold desc="Events">
    // ***********************************************************************************
    // Begin Events
    // ***********************************************************************************
    /**
     * @event afterlayoutanimation
     * This event first after a component's layout has been updated by a layout that
     * included animation (e.g., a {@link Ext.panel.Panel panel} in an
     * {@link Ext.layout.container.Accordion accordion} layout).
     * @param {Ext.Component} this
     * @since 6.0.0
     */
    /**
     * @event beforeactivate
     * Fires before a Component has been visually activated. Returning `false` from an event
     * listener can prevent the activate from occurring.
     *
     * **Note** This event is only fired if this Component is a child of a
     * {@link Ext.container.Container} that uses {@link Ext.layout.container.Card} as it's layout.
     * @param {Ext.Component} this
     */
    /**
     * @event activate
     * Fires after a Component has been visually activated.
     *
     * **Note** This event is only fired if this Component is a child of a
     * {@link Ext.container.Container} that uses {@link Ext.layout.container.Card} as it's layout
     * or this Component is a floating Component.
     * @param {Ext.Component} this
     */
    /**
     * @event beforedeactivate
     * Fires before a Component has been visually deactivated. Returning `false` from an event
     * listener can prevent the deactivate from occurring.
     *
     * **Note** This event is only fired if this Component is a child of a
     * {@link Ext.container.Container} that uses {@link Ext.layout.container.Card} as it's layout.
     * @param {Ext.Component} this
     */
    /**
     * @event deactivate
     * Fires after a Component has been visually deactivated.
     *
     * **Note** This event is only fired if this Component is a child of a
     * {@link Ext.container.Container} that uses {@link Ext.layout.container.Card} as it's layout
     * or this Component is a floating Component.
     * @param {Ext.Component} this
     */
    /**
     * @event added
     * Fires after a Component had been added to a Container.
     * @param {Ext.Component} this
     * @param {Ext.container.Container} container Parent Container
     * @param {Number} pos position of Component
     * @since 3.4.0
     */
    /**
     * @event disable
     * Fires after the component is disabled.
     * @param {Ext.Component} this
     * @since 1.1.0
     */
    /**
     * @event enable
     * Fires after the component is enabled.
     * @param {Ext.Component} this
     * @since 1.1.0
     */
    /**
     * @event beforeshow
     * Fires before the component is shown when calling the {@link Ext.Component#method-show show}
     * method. Return `false` from an event handler to stop the show.
     * @param {Ext.Component} this
     * @since 1.1.0
     */
    /**
     * @event show
     * Fires after the component is shown when calling the {@link Ext.Component#method-show show}
     * method.
     * @param {Ext.Component} this
     * @since 1.1.0
     */
    /**
     * @event beforehide
     * Fires before the component is hidden when calling the {@link Ext.Component#method-hide hide}
     * method. Return `false` from an event handler to stop the hide.
     * @param {Ext.Component} this
     * @since 1.1.0
     */
    /**
     * @event hide
     * Fires after the component is hidden. Fires after the component is hidden when calling
     * the {@link Ext.Component#method-hide hide} method.
     * @param {Ext.Component} this
     * @since 1.1.0
     */
    /**
     * @event removed
     * Fires when a component is removed from an Ext.container.Container
     * @param {Ext.Component} this
     * @param {Ext.container.Container} ownerCt Container which holds the component
     * @since 3.4.0
     */
    /**
     * @event beforerender
     * Fires before the component is {@link #rendered}. Return `false` from an event handler
     * to stop the {@link #method-render}.
     * @param {Ext.Component} this
     * @since 1.1.0
     */
    /**
     * @event render
     * Fires after the component markup is {@link #rendered}.
     * @param {Ext.Component} this
     * @since 1.1.0
     */
    /**
     * @event afterrender
     * Fires after the component rendering is finished.
     *
     * The `afterrender` event is fired after this Component has been {@link #rendered},
     * been post-processed by any `afterRender` method defined for the Component.
     * @param {Ext.Component} this
     * @since 3.4.0
     */
    /**
     * @event boxready
     * Fires *one time* - after the component has been laid out for the first time at its initial
     * size.
     *
     * This event does not fire on components that use {@link #cfg-liquidLayout}, such as
     * {@link Ext.button.Button Buttons} and {@link Ext.form.field.Base Form Fields}.
     * @param {Ext.Component} this
     * @param {Number} width The initial width.
     * @param {Number} height The initial height.
     */
    /**
     * @event beforedestroy
     * Fires before the component is destroyed.
     *
     * **Note:** This event should not be used to try to veto the destruction sequence by returning
     *  `false`, even though this is often permitted in other "before" events. Doing so will have
     * unpredictable side-effects and can result in partially destroyed objects. Instead look to
     * other events like {@link Ext.panel.Panel#event!beforeclose beforeclose} that occur prior to
     * the call to the {@link Ext.Base#method!destroy destroy} method.
     *
     * @param {Ext.Component} this
     * @since 1.1.0
     */
    /**
     * @event destroy
     * Fires after the component is {@link #method-destroy}ed.
     * @param {Ext.Component} this
     * @since 1.1.0
     */
    /**
     * @event resize
     * Fires after the component is resized. Note that this does *not* fire when the component
     * is first laid out at its initial size. To hook that point in the life cycle, use the
     * {@link #boxready} event.
     *
     * This event does not fire on components that use {@link #cfg-liquidLayout}, such as
     * {@link Ext.button.Button Buttons} and {@link Ext.form.field.Base Form Fields}.
     * @param {Ext.Component} this
     * @param {Number} width The new width that was set.
     * @param {Number} height The new height that was set.
     * @param {Number} oldWidth The previous width.
     * @param {Number} oldHeight The previous height.
     */
    /**
     * @event move
     * Fires after the component is moved.
     * @param {Ext.Component} this
     * @param {Number} x The new x position.
     * @param {Number} y The new y position.
     */
    // ***********************************************************************************
    // End Events
    // ***********************************************************************************
    // </editor-fold>
    /**
     * Creates new Component.
     * @param {Ext.dom.Element/String/Object} config The configuration options may be specified
     * as either:
     *
     * - **an element** : it is set as the internal element and its id used as the component id
     * - **a string** : it is assumed to be the id of an existing element and is used as the
     * component id
     * - **anything else** : it is assumed to be a standard config object and is applied to the
     * component
     */
    constructor: function(config) {
        var me = this,
            i, len, xhooks, controller, autoScroll, overflowX, overflowY, scrollable;
        config = config || {};
        if (config.initialConfig) {
            // Being initialized from an Ext.Action instance...
            if (config.isAction) {
                me.baseAction = config;
            }
            config = config.initialConfig;
        }
        // component cloning / action set up
        else if (config.tagName || config.dom || Ext.isString(config)) {
            // element object
            config = {
                applyTo: config,
                id: config.id || config
            };
        }
        // Make initialConfig available early so that config getters may access it
        /**
         * @property {Object} initialConfig
         * @readonly
         * The config object passed to the constructor during Component creation.
         */
        me.initialConfig = config;
        me.$iid = ++Ext.$nextIid;
        // We want to determine very early on whether or not we are a reference holder,
        // so peek at either the incoming config or the class config to see if we have
        // a controller defined.
        if ((config && config.controller) || me.config.controller) {
            me.referenceHolder = true;
        }
        // Ensure that we have an id early so that config getters may access it
        me.getId();
        me.protoEl = new Ext.util.ProtoElement();
        me.$calledInitConfig = true;
        me.initConfig(config);
        delete me.$calledInitConfig;
        if (me.scrollable == null) {
            autoScroll = me.autoScroll;
            if (autoScroll) {
                scrollable = !!autoScroll;
            } else {
                overflowX = me.overflowX;
                overflowY = me.overflowY;
                if (overflowX || overflowY) {
                    scrollable = {
                        x: (overflowX && overflowX !== 'hidden') ? overflowX : false,
                        y: (overflowY && overflowY !== 'hidden') ? overflowY : false
                    };
                }
            }
            if (scrollable) {
                me.setScrollable(scrollable);
            }
        }
        xhooks = me.xhooks;
        if (xhooks) {
            delete me.xhooks;
            Ext.override(me, xhooks);
        }
        me.mixins.elementCt.constructor.call(me);
        if (!me.validIdRe.test(me.id)) {
            Ext.raise('Invalid component "id": "' + me.id + '"');
        }
        if (!me.validIdRe.test(me.itemId)) {
            Ext.raise('Invalid component "itemId": "' + me.itemId + '"');
        }
        me.setupProtoEl();
        // initComponent, beforeRender, or event handlers may have set the style
        // or `cls` property since the `protoEl` was set up so we must apply styles
        // and classes here too.
        if (me.cls) {
            me.initialCls = me.cls;
            me.protoEl.addCls(me.cls);
        }
        if (me.style) {
            me.initialStyle = me.style;
            me.protoEl.setStyle(me.style);
        }
        me.renderData = me.renderData || {};
        me.initComponent();
        // initComponent gets a chance to change the id property before registering
        if (!me.preventRegister) {
            Ext.ComponentManager.register(me);
        }
        me.mixins.state.constructor.call(me);
        me.addStateEvents('resize');
        controller = me.getController();
        if (controller) {
            controller.init(me);
        }
        // Move this into Observable?
        if (me.plugins) {
            for (i = 0 , len = me.plugins.length; i < len; i++) {
                me.plugins[i] = me.initPlugin(me.plugins[i]);
            }
        }
        me.loader = me.getLoader();
        if (me.disabled) {
            me.disabled = false;
            me.disable(true);
        }
        if (me.renderTo) {
            me.render(me.renderTo);
        }
        // EXTJSIV-1935 - should be a way to do afterShow or something, but that
        // won't work. Likewise, rendering hidden and then showing (w/autoShow) has
        // implications to afterRender so we cannot do that.
        // Auto show only works unilaterally on *uncontained* Components.
        // If contained, then it is the Container's responsibility to do the showing
        // at next layout time.
        if (me.autoShow && !me.$initParent) {
            me.show();
        }
        if (Ext.isDefined(me.disabledClass)) {
            if (Ext.isDefined(Ext.global.console)) {
                Ext.global.console.warn('Ext.Component: disabledClass has been deprecated. ' + 'Please use disabledCls.');
            }
            me.disabledCls = me.disabledClass;
            delete me.disabledClass;
        }
        // If we were configured from an instance of Ext.Action, 
        // (or configured with a baseAction option), register this Component as one of its items
        if (me.baseAction) {
            me.baseAction.addComponent(me);
        }
    },
    beforeInitConfig: function() {
        if (!this.$calledInitConfig) {
            Ext.raise('initConfig should not be called by subclasses, it will be ' + 'called by Ext.Component');
        }
        this.mixins.observable.constructor.call(this);
    },
    // <editor-fold desc="Component Methods">
    // ***********************************************************************************
    // Begin Component Methods
    // ***********************************************************************************
    /**
     * Adds a CSS class to the top level element representing this component.
     * @param {String/String[]} cls The CSS class name to add.
     * @return {Ext.Component} Returns the Component to allow method chaining.
     */
    addCls: function(cls) {
        var me = this,
            el = me.rendered ? me.el : me.protoEl;
        el.addCls.apply(el, arguments);
        return me;
    },
    /**
     * Adds a `cls` to the `uiCls` array, which will also call {@link #addUIClsToElement}
     * and adds to all elements of this component.
     * @param {String/String[]} classes A string or an array of strings to add to the `uiCls`.
     * @param {Boolean} [skip] `true` to skip adding it to the class and do it later
     * (via the return).
     */
    addClsWithUI: function(classes, skip) {
        var me = this,
            clsArray = [],
            i = 0,
            uiCls = me.uiCls = Ext.Array.clone(me.uiCls),
            activeUI = me.activeUI,
            length, cls;
        if (typeof classes === "string") {
            classes = (classes.indexOf(' ') < 0) ? [
                classes
            ] : Ext.String.splitWords(classes);
        }
        length = classes.length;
        for (; i < length; i++) {
            cls = classes[i];
            if (cls && !me.hasUICls(cls)) {
                uiCls.push(cls);
                // We can skip this bit if there isn't an activeUI because
                // we'll be called again from setUI
                if (activeUI) {
                    clsArray = clsArray.concat(me.addUIClsToElement(cls));
                }
            }
        }
        if (skip !== true && activeUI) {
            me.addCls(clsArray);
        }
        return clsArray;
    },
    /**
     * Called by the layout system after the Component has been laid out.
     *
     * This method is not called on components that use {@link #cfg-liquidLayout}, such as
     * {@link Ext.button.Button Buttons} and {@link Ext.form.field.Base Form Fields}.
     *
     * @param {Number} width The width that was set
     * @param {Number} height The height that was set
     * @param {Number/undefined} oldWidth The old width, or `undefined` if this was the initial
     * layout.
     * @param {Number/undefined} oldHeight The old height, or `undefined` if this was the initial
     * layout.
     *
     * @template
     * @protected
     */
    afterComponentLayout: function(width, height, oldWidth, oldHeight) {
        var me = this,
            scroller = me.scrollable;
        if (++me.componentLayoutCounter === 1) {
            me.afterFirstLayout(width, height);
        } else if (me.manageLayoutScroll && scroller) {
            scroller.restoreState();
        }
        if (width !== oldWidth || height !== oldHeight) {
            me.onResize(width, height, oldWidth, oldHeight);
        }
        if (me.floating) {
            me.onAfterFloatLayout();
        }
    },
    /**
     * @protected
     * Adds a plugin. May be called at any time in the component's life cycle.
     */
    addPlugin: function(plugin) {
        var me = this;
        plugin = me.constructPlugin(plugin);
        if (me.plugins) {
            me.plugins.push(plugin);
        } else {
            me.plugins = [
                plugin
            ];
        }
        if (me.pluginsInitialized) {
            me.initPlugin(plugin);
        }
        return plugin;
    },
    /**
     * Save a property to the given state object if it is not its default or configured
     * value.
     *
     * @param {Object} state The state object.
     * @param {String} propName The name of the property on this object to save.
     * @param {String} [value] The value of the state property (defaults to `this[propName]`).
     * @return {Object} The state object or a new object if state was `null` and the property
     * was saved.
     * @protected
     */
    addPropertyToState: function(state, propName, value) {
        var me = this,
            len = arguments.length;
        // If the property is inherited, it is a default and we don't want to save it to
        // the state, however if we explicitly specify a value, always save it
        if (len === 3 || me.hasOwnProperty(propName)) {
            if (len < 3) {
                value = me[propName];
            }
            // If the property has the same value as was initially configured, again, we
            // don't want to save it.
            if (value !== me.initialConfig[propName]) {
                (state || (state = {}))[propName] = value;
            }
        }
        return state;
    },
    /**
     * Method which adds a specified UI + `uiCls` to the components element. Can be overridden
     * to add the UI to more than just the component's element.
     * @param {String} uiCls The UI class to add to the element.
     * @protected
     */
    addUIClsToElement: function(uiCls) {
        var me = this,
            baseClsUI = me.baseCls + '-' + me.ui + '-' + uiCls,
            result = [
                Ext.baseCSSPrefix + uiCls,
                me.baseCls + '-' + uiCls,
                baseClsUI
            ],
            childEls, childElName, el, suffix;
        if (me.rendered && me.frame && !Ext.supports.CSS3BorderRadius) {
            // Loop through each frame element, and if they are defined add the ui:
            baseClsUI += '-';
            childEls = me.getChildEls();
            for (childElName in childEls) {
                suffix = childEls[childElName].frame;
                if (suffix && suffix !== true) {
                    el = me[childElName];
                    if (el) {
                        el.addCls(baseClsUI + suffix);
                    }
                }
            }
        }
        return result;
    },
    /**
     * Method which removes a specified UI + `uiCls` from the components element. The `cls`
     * which is added to the element will be: `this.baseCls + '-' + ui + uiCls`.
     * @param {String} uiCls The UI class to remove from the element.
     * @protected
     */
    removeUIClsFromElement: function(uiCls) {
        var me = this,
            baseClsUI = me.baseCls + '-' + me.ui + '-' + uiCls,
            result = [
                Ext.baseCSSPrefix + uiCls,
                me.baseCls + '-' + uiCls,
                baseClsUI
            ],
            childEls, childElName, el, suffix;
        if (me.rendered && me.frame && !Ext.supports.CSS3BorderRadius) {
            // Loop through each frame element, and if they are defined remove the ui:
            baseClsUI += '-';
            childEls = me.getChildEls();
            for (childElName in childEls) {
                suffix = childEls[childElName].frame;
                if (suffix && suffix !== true) {
                    el = me[childElName];
                    if (el) {
                        el.removeCls(baseClsUI + suffix);
                    }
                }
            }
        }
        return result;
    },
    /**
     * @private
     */
    adjustPosition: function(x, y) {
        var me = this,
            floatParentBox;
        // Floating Components being positioned in their ownerCt have to be made absolute.
        if (me.isContainedFloater()) {
            floatParentBox = me.floatParent.getTargetEl().getViewRegion();
            x += floatParentBox.left;
            y += floatParentBox.top;
        }
        return {
            x: x,
            y: y
        };
    },
    /**
     * Invoked after the Component has been hidden.
     *
     * Gets passed the same `callback` and `scope` parameters that #onHide received.
     *
     * @param {Function} [callback]
     * @param {Object} [scope]
     * @template
     * @protected
     */
    afterHide: function(callback, scope) {
        var me = this,
            container = me.ownerFocusableContainer;
        // Top level focusEnter is only valid when a floating component stack is visible.
        delete me.getInherited().topmostFocusEvent;
        me.hiddenByLayout = null;
        // Only lay out if there is an owning layout which might be affected by the hide
        if (me.ownerLayout) {
            me.updateLayout({
                isRoot: false
            });
        }
        if (container && !container.onFocusableChildHide.$nullFn) {
            container.onFocusableChildHide(me);
        }
        me.fireHierarchyEvent('hide');
        me.fireEvent('hide', me);
        // Have to fire callback the last, because it may destroy the Component
        // and firing subsequent events will become impossible. Strictly speaking,
        // hide event handler above could have destroyed the Component too, but
        // in such case it is the responsibility of the callback to accommodate.
        Ext.callback(callback, scope || me);
    },
    /**
     * Template method called after a Component has been positioned.
     *
     * @param {Number} x
     * @param {Number} y
     *
     * @template
     * @protected
     */
    afterSetPosition: function(x, y) {
        var me = this;
        me.onPosition(x, y);
        if (me.hasListeners.move) {
            me.fireEvent('move', me, x, y);
        }
    },
    /**
     * Invoked after the Component is shown (after #onShow is called).
     *
     * Gets passed the same parameters as #show.
     *
     * @param {String/Ext.dom.Element} [animateTarget]
     * @param {Function} [callback]
     * @param {Object} [scope]
     * @template
     * @protected
     */
    afterShow: function(animateTarget, callback, scope) {
        var me = this,
            myEl = me.el,
            zim = me.zIndexManager,
            fromBox, toBox, ghostPanel;
        // Default to configured animate target if none passed
        animateTarget = me.getAnimateTarget(animateTarget);
        // We are going to churn the zIndex stack, so suspend its reactions.
        if (zim) {
            zim.suspendReflow();
        }
        // Need to be able to ghost the Component
        if (!me.ghost) {
            animateTarget = null;
        }
        // If we're animating, kick of an animation of the ghost from the target
        // to the *Element* current box
        if (animateTarget) {
            toBox = {
                x: myEl.getX(),
                y: myEl.getY(),
                width: myEl.dom.offsetWidth,
                height: myEl.dom.offsetHeight
            };
            fromBox = {
                x: animateTarget.getX(),
                y: animateTarget.getY(),
                width: animateTarget.dom.offsetWidth,
                height: animateTarget.dom.offsetHeight
            };
            // Will move to front with underlying mask, and focus if necessary.
            me.fireHierarchyEvent('show');
            // Ghost will brifly be topmost, but it is focusable: false
            // And ghosting does not disturb focus.
            ghostPanel = me.ghost();
            ghostPanel.el.stopAnimation();
            // Shunting it offscreen immediately, *before* the Animation class grabs it
            // ensure no flicker.
            ghostPanel.setX(-10000);
            me.ghostBox = toBox;
            ghostPanel.el.animate({
                from: fromBox,
                to: toBox,
                listeners: {
                    beforeanimate: function() {
                        if (me.modal) {
                            ghostPanel.toFront();
                        }
                    },
                    afteranimate: function() {
                        if (!me.destroying) {
                            ghostPanel.componentLayout.lastComponentSize = null;
                            me.unghost();
                            me.ghostBox = null;
                            me.onShowComplete(callback, scope);
                        }
                    }
                }
            });
        } else {
            me.onShowComplete(callback, scope);
            me.fireHierarchyEvent('show');
        }
        // Take a run through the zIndex stack ensuring modal, focus and topmost is correct
        if (zim) {
            zim.resumeReflow(true);
        }
    },
    animate: function(animObj) {
        var me = this,
            hasToWidth, hasToHeight, toHeight, toWidth, to, clearWidth, clearHeight, curWidth, w, curHeight, h, isExpanding, wasConstrained, wasConstrainedHeader, passedCallback, oldOverflow;
        animObj = animObj || {};
        to = animObj.to || {};
        if (Ext.fx.Manager.hasFxBlock(me.id)) {
            return me;
        }
        hasToWidth = Ext.isDefined(to.width);
        if (hasToWidth) {
            toWidth = Ext.Number.constrain(to.width, me.minWidth, me.maxWidth);
        }
        hasToHeight = Ext.isDefined(to.height);
        if (hasToHeight) {
            toHeight = Ext.Number.constrain(to.height, me.minHeight, me.maxHeight);
        }
        // Special processing for animating Component dimensions.
        if (!animObj.dynamic && (hasToWidth || hasToHeight)) {
            curWidth = (animObj.from ? animObj.from.width : undefined) || me.getWidth();
            w = curWidth;
            curHeight = (animObj.from ? animObj.from.height : undefined) || me.getHeight();
            h = curHeight;
            isExpanding = false;
            if (hasToHeight && toHeight > curHeight) {
                h = toHeight;
                isExpanding = true;
            }
            if (hasToWidth && toWidth > curWidth) {
                w = toWidth;
                isExpanding = true;
            }
            // During animated sizing, overflow has to be hidden to clip expanded content
            if (hasToHeight || hasToWidth) {
                oldOverflow = me.el.getStyle('overflow');
                if (oldOverflow !== 'hidden') {
                    me.el.setStyle('overflow', 'hidden');
                }
            }
            // If any dimensions are being increased, we must resize the internal structure
            // of the Component, but then clip it by sizing its encapsulating element
            // back to original dimensions.
            // The animation will then progressively reveal the larger content.
            if (isExpanding) {
                clearWidth = !Ext.isNumber(me.width);
                clearHeight = !Ext.isNumber(me.height);
                // Lay out this component at the new, larger size to get the internals correctly
                // laid out. Then size the encapsulating **Element** back down to size.
                // We will then just animate the element to reveal the correctly laid out content.
                me.setSize(w, h);
                me.el.setSize(curWidth, curHeight);
                if (clearWidth) {
                    delete me.width;
                }
                if (clearHeight) {
                    delete me.height;
                }
            }
            if (hasToWidth) {
                to.width = toWidth;
            }
            if (hasToHeight) {
                to.height = toHeight;
            }
        }
        // No constraining during the animate - the "to" size has already been calculated
        // with respect to all settings.
        // Arrange to reinstate any constraining after the animation has completed
        wasConstrained = me.constrain;
        wasConstrainedHeader = me.constrainHeader;
        if (wasConstrained || wasConstrainedHeader) {
            me.constrain = me.constrainHeader = false;
            passedCallback = animObj.callback;
            animObj.callback = function() {
                me.constrain = wasConstrained;
                me.constrainHeader = wasConstrainedHeader;
                // Call the original callback if any
                if (passedCallback) {
                    passedCallback.call(animObj.scope || me, arguments);
                }
                if (oldOverflow !== 'hidden') {
                    me.el.setStyle('overflow', oldOverflow);
                }
            };
        }
        return me.mixins.animate.animate.apply(me, arguments);
    },
    applyScrollable: function(scrollable, oldScrollable) {
        var me = this,
            rendered = me.rendered,
            scrollableCfg;
        if (scrollable) {
            if (scrollable === true || typeof scrollable === 'string') {
                scrollableCfg = me._scrollableCfg[scrollable];
                if (!scrollableCfg) {
                    Ext.raise("'" + scrollable + "' is not a valid value for 'scrollable'");
                }
                scrollable = scrollableCfg;
            }
            if (oldScrollable) {
                oldScrollable.setConfig(scrollable);
                scrollable = oldScrollable;
            } else {
                scrollable = Ext.Object.chain(scrollable);
                // don't mutate the user's config
                if (rendered) {
                    // we create the scroller without an element by default (because the
                    // element is not available at configuration time) and then assign
                    // the element in onBoxReady. If we got here it means the scroller
                    // is being configured after render, so we need to make sure the
                    // element is in its config object
                    scrollable.element = me.getOverflowEl();
                }
                scrollable = Ext.scroll.Scroller.create(scrollable, me.scrollableType);
                scrollable.component = me;
            }
        }
        // We are disabling scrolling for this Component.
        else if (oldScrollable) {
            scrollable = oldScrollable;
            oldScrollable.setConfig({
                x: false,
                y: false
            });
        }
        if (me.rendered && !me.destroying && !me.destroyed) {
            if (scrollable) {
                me.getOverflowStyle();
            } else // refresh the scrollFlags
            {
                me.scrollFlags = me._scrollFlags.none;
            }
            me.updateLayout();
        }
        return scrollable;
    },
    /**
     * This method is required by the Scroller to return the scrollable client region
     * @return {Ext.util.Region} The scrolling viewport region.
     * @private
     */
    getScrollableClientRegion: function() {
        return this.getScrollable().getElement().getClientRegion();
    },
    applyTouchAction: function(touchAction, oldTouchAction) {
        if (oldTouchAction != null) {
            touchAction = Ext.merge({}, oldTouchAction, touchAction);
        }
        return touchAction;
    },
    /**
     * Invoked before the Component is destroyed.
     * This method is deprecated, override {@link #onDestroy} instead.
     *
     * @method
     * @template
     * @protected
     * @deprecated 6.2.0 Please override {@link #onDestroy} instead
     */
    beforeDestroy: Ext.emptyFn,
    /**
     * Occurs before componentLayout is run. In previous releases, this method could
     * return `false` to prevent its layout but that is not supported in Ext JS 4.1 or
     * higher. This method is simply a notification of the impending layout to give the
     * component a chance to adjust the DOM. Ideally, DOM reads should be avoided at this
     * time to reduce expensive document reflows.
     *
     * @template
     * @protected
     */
    beforeLayout: function() {
        if (this.floating) {
            this.onBeforeFloatLayout();
        }
    },
    /**
     * @private
     * Template method called before a Component is positioned.
     *
     * Ensures that the position is adjusted so that the Component is constrained if so configured.
     */
    beforeSetPosition: function(x, y, animate) {
        var me = this,
            pos = null,
            x0, hasX, hasY, adj;
        // Decode members of x if x is an array or an object.
        // If it is numeric (including zero), we need do nothing.
        if (x) {
            // Position in first argument as an array of [x, y]
            if (Ext.isNumber(x0 = x[0])) {
                animate = y;
                y = x[1];
                x = x0;
            }
            // Position in first argument as object w/ x & y properties
            else if ((x0 = x.x) !== undefined) {
                animate = y;
                y = x.y;
                x = x0;
            }
        }
        if (me.constrain || me.constrainHeader) {
            pos = me.calculateConstrainedPosition(null, [
                x,
                y
            ], true);
            if (pos) {
                x = pos[0];
                y = pos[1];
            }
        }
        hasX = (x !== undefined);
        hasY = (y !== undefined);
        if (hasX || hasY) {
            // The component's position is the position it was told to be at.
            // If it is contained, adjustPosition will add the floatParent's offsets.
            me.x = x;
            me.y = y;
            adj = me.adjustPosition(x, y);
            // Set up the return info and store the position in this object
            pos = {
                x: adj.x,
                y: adj.y,
                anim: animate,
                hasX: hasX,
                hasY: hasY
            };
        }
        return pos;
    },
    /**
     * Invoked before the Component is shown.
     *
     * @method
     * @template
     * @protected
     */
    beforeShow: Ext.emptyFn,
    /**
     * Bubbles up the component/container hierarchy, calling the specified function with each
     * component. The scope (*this*) of function call will be the scope provided or the current
     * component. The arguments to the function will be the args provided or the current component.
     * If the function returns false at any point, the bubble is stopped.
     *
     * @param {Function} fn The function to call
     * @param {Object} [scope] The scope of the function. Defaults to current node.
     * @param {Array} [args] The args to call the function with. Defaults to passing the current
     * component.
     * @return {Ext.Component} this
     */
    bubble: function(fn, scope, args) {
        var p = this;
        while (p) {
            if (fn.apply(scope || p, args || [
                p
            ]) === false) {
                break;
            }
            p = p.getBubbleTarget();
        }
        return this;
    },
    clearListeners: function() {
        var me = this;
        me.mixins.observable.clearListeners.call(me);
        me.mixins.componentDelegation.clearDelegatedListeners.call(me);
    },
    /**
     * Clone the current component using the original config values passed into this instance
     * by default.
     * @param {Object} overrides A new config containing any properties to override in the cloned
     * version. An id property can be passed on this object, otherwise one will be generated
     * to avoid duplicates.
     * @return {Ext.Component} clone The cloned copy of this component
     */
    cloneConfig: function(overrides) {
        overrides = overrides || {};
        // eslint-disable-next-line vars-on-top
        var id = overrides.id || Ext.id(),
            cfg = Ext.applyIf(overrides, this.initialConfig),
            self;
        cfg.id = id;
        self = Ext.getClass(this);
        // prevent dup id
        return new self(cfg);
    },
    /**
     * Destroys the Component. This method **must not** be overridden because Component
     * destruction sequence is conditional; if a `beforedestroy` handler returns `false`
     * we must abort destruction.
     *
     * To add extra functionality to destruction time in a subclass, override the
     * {@link #doDestroy} method.
     *
     * @since 1.1.0
     */
    destroy: function() {
        var me = this;
        if (!me.hasListeners.beforedestroy || me.fireEvent('beforedestroy', me) !== false) {
            // isDestroying added for compat reasons
            me.isDestroying = me.destroying = true;
            me.doDestroy();
            // We need to defer clearing listeners until after doDestroy() completes,
            // to let the interested parties fire events until the very end.
            me.clearListeners();
            // isDestroying added for compat reasons
            me.isDestroying = me.destroying = false;
            me.callParent();
            // Ext.Base
            // ComponentDelegation mixin does not install "after" interceptor on the
            // base class destructor; Observable mixin does install the interceptor
            // but cannot destroy itself automatically because Components are
            // conditionally destructible.
            me.mixins.componentDelegation.destroyComponentDelegation.call(me);
            me.mixins.observable.destroyObservable.call(me, true);
        }
    },
    /**
     * Perform the actual destruction sequence.
     *
     * As a rule of thumb, subclasses should destroy their child Components and/or other objects
     * before calling parent method. Any object references will be nulled after this method
     * has finished, to prevent the possibility of memory leaks.
     *
     * @private
     * @since 6.2.0
     */
    doDestroy: function() {
        var me = this,
            focusableContainer = me.ownerFocusableContainer,
            container = me.container,
            selectors = me.renderSelectors,
            selector, ownerCt, el;
        ownerCt = me.floatParent || me.ownerCt;
        if (me.floating) {
            delete me.floatParent;
            // A zIndexManager is stamped into a *floating* Component when it is added
            // to a Container. If it has no zIndexManager at render time, it is assigned
            // to the global Ext.WindowManager instance.
            // It can also happen that Container's zIndexManager is destroyed before this.
            if (me.zIndexManager && !me.zIndexManager.destroyed) {
                me.zIndexManager.unregister(me);
            }
            // Some components may set floating as config object, which will be nulled
            // in the base destructor. We need this property in Containers, so set it
            // to Boolean instead.
            me.floating = true;
        }
        me.removeBindings();
        if (!me.beforeDestroy.$emptyFn) {
            me.beforeDestroy();
        }
        me.destroyBindable();
        if (ownerCt && ownerCt.remove) {
            ownerCt.remove(me, {
                destroy: false
            });
        }
        me.stopAnimation();
        // Ensure that any ancillary components are destroyed.
        if (me.rendered) {
            Ext.destroy(me.loadMask, me.dd, me.resizer, me.proxy, me.proxyWrap, me.resizerComponent, me.scrollable, me.contentEl);
        }
        if (focusableContainer && !focusableContainer.onFocusableChildDestroy.$nullFn) {
            focusableContainer.onFocusableChildDestroy(me);
        }
        if (me.focusable) {
            me.destroyFocusable();
        }
        // Destroying the floatingItems ZIndexManager will also destroy descendant floating
        // Components
        Ext.destroy(me.componentLayout, me.loadMask, me.floatingDescendants);
        if (!me.onDestroy.$emptyFn) {
            me.onDestroy();
        }
        // Attempt to destroy all plugins
        Ext.destroy(me.plugins);
        if (me.rendered) {
            Ext.Component.cancelLayout(me, true);
        }
        me.componentLayout = null;
        if (me.hasListeners.destroy) {
            me.fireEvent('destroy', me);
        }
        if (!me.preventRegister) {
            Ext.ComponentManager.unregister(me);
        }
        me.mixins.state.destroy.call(me);
        if (me.floating) {
            me.onFloatDestroy();
        }
        // make sure we clean up the element references after removing all events
        if (me.rendered) {
            if (me.showListenerIE) {
                me.showListenerIE.destroy();
                me.showListenerIE = null;
            }
            if (!me.preserveElOnDestroy) {
                me.el.destroy();
            }
            // We don't own the container element so can't just destroy it as that would
            // remove it from the DOM; we have to remove the Element instance from cache
            // though.
            if (me.collectContainerElement && container) {
                if (!container.destroyed) {
                    container.collect();
                }
                me.container = null;
            }
            me.el.component = null;
            me.mixins.elementCt.destroy.call(me);
            // removes childEls
            if (selectors) {
                for (selector in selectors) {
                    if (selectors.hasOwnProperty(selector)) {
                        el = me[selector];
                        if (el) {
                            // in case any other code may have already removed it
                            delete me[selector];
                            el.destroy();
                        }
                    }
                }
            }
            // This is a very special boolean that warrants explicit clearing
            me.rendered = false;
        }
    },
    /**
     * Disable the component.
     * @param {Boolean} [silent=false] Passing `true` will suppress the `disable` event
     * from being fired.
     * @param fromParent (private)
     * @since 1.1.0
     */
    disable: function(silent, fromParent) {
        var me = this,
            container = me.ownerFocusableContainer,
            inherited = me.getInherited();
        if (!fromParent) {
            inherited.disabled = true;
            me.savedDisabled = true;
        }
        if (me.maskOnDisable) {
            inherited.disableMask = true;
        }
        if (!me.disabled) {
            if (container && !container.beforeFocusableChildDisable.$nullFn) {
                container.beforeFocusableChildDisable(me);
            }
            me.addCls(me.disabledCls);
            if (me.rendered) {
                me.onDisable();
            } else {
                me.disableOnRender = true;
            }
            me.disabled = true;
            if (silent !== true) {
                me.fireEvent('disable', me);
            }
            if (container && !container.onFocusableChildDisable.$nullFn) {
                container.onFocusableChildDisable(me);
            }
        }
        return me;
    },
    doFireEvent: function(eventName, args, bubbles) {
        var me = this,
            ret;
        ret = me.mixins.observable.doFireEvent.call(me, eventName, args, bubbles);
        // The Component instance can be destroyed in the handler, in which case
        // we can't fire delegated events on it anymore.
        if (ret !== false && !me.destroyed) {
            ret = me.mixins.componentDelegation.doFireDelegatedEvent.call(me, eventName, args);
        }
        return ret;
    },
    /**
     * Enable the component
     * @param {Boolean} [silent=false] Passing `true` will suppress the `enable` event
     * from being fired.
     * @param fromParent (private)
     * @since 1.1.0
     */
    enable: function(silent, fromParent) {
        var me = this,
            container = me.ownerFocusableContainer,
            inherited = me.getInherited();
        if (!fromParent) {
            delete me.getInherited().disabled;
            me.savedDisabled = false;
        }
        if (me.maskOnDisable) {
            delete inherited.disableMask;
        }
        if (me.disabled) {
            // A parent is asking us to enable, but if we were disabled directly, keep
            // our current state
            if (!(fromParent && inherited.hasOwnProperty('disabled'))) {
                if (container && !container.beforeFocusableChildEnable.$nullFn) {
                    container.beforeFocusableChildEnable(me);
                }
                me.disableOnRender = false;
                me.removeCls(me.disabledCls);
                if (me.rendered) {
                    me.onEnable();
                }
                me.disabled = false;
                if (silent !== true) {
                    me.fireEvent('enable', me);
                }
                if (container && !container.onFocusableChildEnable.$nullFn) {
                    container.onFocusableChildEnable(me);
                }
            }
        }
        return me;
    },
    /**
     * Find a container above this component at any level by a custom function. If the passed
     * function returns true, the container will be returned.
     *
     * See also the {@link Ext.Component#up up} method.
     *
     * @param {Function} fn The custom function to call with the arguments
     * (container, this component).
     * @return {Ext.container.Container} The first Container for which the custom function
     * returns true
     */
    findParentBy: function(fn) {
        var p;
        // Iterate up the owner chain until we don't have one, or we find an ancestor
        // which matches using the selector function.
        for (p = this.getRefOwner(); p && !fn(p, this); p = p.getRefOwner()) {}
        // do nothing
        return p || null;
    },
    /**
     * Find a container above this component at any level by xtype or class
     *
     * See also the {@link Ext.Component#up up} method.
     *
     * @param {String/Ext.Class} xtype The xtype string for a component, or the class
     * of the component directly
     * @return {Ext.container.Container} The first Container which matches the given xtype or class
     */
    findParentByType: function(xtype) {
        return Ext.isFunction(xtype) ? this.findParentBy(function(p) {
            return p.self === xtype || p.constructor === xtype;
        }) : this.up(xtype);
    },
    /**
     * Retrieves plugin from this component's collection by its `ptype`.
     *
     *     var grid = Ext.create('Ext.grid.Panel', {
     *         store: {
     *             fields: ['name'],
     *             data: [{
     *                 name: 'Scott Pilgrim'
     *             }]
     *         },
     *         columns: [{
     *             header: 'Name',
     *             dataIndex: 'name',
     *             editor: 'textfield',
     *             flex: 1
     *         }],
     *         selType: 'cellmodel',
     *         plugins: [{
     *             ptype: 'cellediting',
     *             clicksToEdit: 1,
     *             id: 'myplugin'
     *         }],
     *         height: 200,
     *         width: 400,
     *         renderTo: Ext.getBody()
     *     });
     *
     *     grid.findPlugin('cellediting');  // the cellediting plugin
     *
     * **Note:** See also {@link #getPlugin}
     *
     * @param {String} ptype The Plugin's `ptype` as specified by the class's
     * {@link Ext.Class#cfg-alias alias} configuration.
     * @return {Ext.plugin.Abstract} plugin instance or `undefined` if not found
     */
    findPlugin: function(ptype) {
        var i,
            plugins = this.plugins,
            ln = plugins && plugins.length;
        for (i = 0; i < ln; i++) {
            if (plugins[i].ptype === ptype) {
                return plugins[i];
            }
        }
    },
    getAnimateTarget: function(target) {
        target = target || this.animateTarget;
        if (target) {
            target = target.isComponent ? target.getEl() : Ext.get(target);
        }
        return target || null;
    },
    /**
     * @protected
     * Implements an upward event bubbling policy. By default a Component bubbles events
     * up to its {@link #getRefOwner reference owner}.
     *
     * Component subclasses may implement a different bubbling strategy by overriding this method.
     */
    getBubbleTarget: function() {
        return this.getRefOwner();
    },
    getComponentLayout: function() {
        var me = this;
        if (!me.componentLayout || !me.componentLayout.isLayout) {
            me.setComponentLayout(Ext.layout.Layout.create(me.componentLayout, 'autocomponent'));
        }
        return me.componentLayout;
    },
    /**
     * Retrieves the top level element representing this component.
     * @return {Ext.dom.Element}
     * @since 1.1.0
     */
    getEl: function() {
        return this.el;
    },
    /**
     * Gets the current height of the component's underlying element.
     * @return {Number}
     */
    getHeight: function() {
        return this.el.getHeight();
    },
    /**
     * Called by `getInherited` to initialize the inheritedState the first time it is
     * requested.
     * @protected
     */
    initInheritedState: function(inheritedState) {
        var me = this,
            layout = me.componentLayout;
        if (me.hidden) {
            inheritedState.hidden = true;
        }
        if (me.collapseImmune) {
            inheritedState.collapseImmune = true;
        }
        if (me.modelValidation !== undefined) {
            inheritedState.modelValidation = me.modelValidation;
        }
        if (me.savedDisabled) {
            inheritedState.disabled = true;
        }
        me.mixins.bindable.initInheritedState.call(me, inheritedState);
        if (layout && layout.initInheritedState) {
            layout.initInheritedState(inheritedState);
        }
    },
    /**
     * Retrieves the `id` of this component. Will auto-generate an `id` if one has not already
     * been set.
     * @return {String}
     */
    getId: function() {
        var me = this,
            xtype;
        // If we have no id, attempt to gather it from our configuration.
        // Autogenerate it if none was configured.
        if (!(me.id || (me.id = me.initialConfig.id))) {
            xtype = me.getXType();
            if (xtype) {
                xtype = xtype.replace(Ext.Component.INVALID_ID_CHARS_Re, '-');
            } else {
                xtype = Ext.name.toLowerCase() + '-comp';
            }
            me.id = xtype + '-' + me.getAutoId();
        }
        return me.id;
    },
    /**
     * Returns the value of {@link #itemId} assigned to this component, or when that
     * is not set, returns the value of {@link #id}.
     * @return {String}
     */
    getItemId: function() {
        return this.itemId || this.id;
    },
    /**
     * Gets the {@link Ext.ComponentLoader} for this Component.
     * @return {Ext.ComponentLoader} The loader instance, null if it doesn't exist.
     */
    getLoader: function() {
        var me = this,
            loader = me.loader;
        if (loader) {
            if (!loader.isLoader) {
                me.loader = new Ext.ComponentLoader(Ext.apply({
                    target: me
                }, loader));
            } else {
                loader.setTarget(me);
            }
            return me.loader;
        }
        return null;
    },
    /**
     * @protected
     * Returns the element which is masked by the {@link #mask} method, or into which the
     * {@link #setLoading LoadMask} is rendered into.
     *
     * The default implementation uses the {@link #maskElement} configuration to access
     * the Component's child element by name. By default, {@link #maskElement} is `null` which means
     * that `null` is returned from this method indicating that the mask needs to be rendered into
     * the document because component structure should not be contaminated by mask elements.
     *
     * Some subclasses may override this method if they have knowledge about external structures
     * where a mask could usefully be rendered.
     *
     * For example a {@link Ext.view.Table GridView} will request that its owning
     * {@link Ext.panel.Table GridPanel} be masked. The GridPanel will have its own implementation
     * of `getMaskTarget` which will return the element dictated by its own {@link #maskElement}
     * Panels use `"el"` as their {@link #maskElement} by default, but that could be overridden
     * to be `"body"` to leave toolbars and the header mouse-accessible.
     *
     */
    getMaskTarget: function() {
        return this.maskElement ? this[this.maskElement] : null;
    },
    /**
     * Retrieves a plugin from this component's collection by its `id`.
     *
     *     var grid = Ext.create('Ext.grid.Panel', {
     *         store: {
     *             fields: ['name'],
     *             data: [{
     *                 name: 'Scott Pilgrim'
     *             }]
     *         },
     *         columns: [{
     *             header: 'Name',
     *             dataIndex: 'name',
     *             editor: 'textfield',
     *             flex: 1
     *         }],
     *         selType: 'cellmodel',
     *         plugins: [{
     *             ptype: 'cellediting',
     *             clicksToEdit: 1,
     *             id: 'myplugin'
     *         }],
     *         height: 200,
     *         width: 400,
     *         renderTo: Ext.getBody()
     *     });
     *
     *     grid.getPlugin('myplugin');  // the cellediting plugin
     *
     * **Note:** See also {@link #findPlugin}. Prior to 6.2.0 the plugin had to have a
     * `{@link Ext.plugin.Abstract#pluginId pluginId}` property but this can now be just
     * `{@link Ext.plugin.Abstract#id id}`. Both are supported (so plugins with a
     * matching `pluginId` are still found) but `id` is preferred.
     *
     * @param {String} id The `id` set on the plugin config object.
     * @return {Ext.plugin.Abstract} plugin instance or `null` if not found
     */
    getPlugin: function(id) {
        var i,
            plugins = this.plugins,
            ln = plugins && plugins.length,
            plugin;
        for (i = 0; i < ln; i++) {
            plugin = plugins[i];
            // pre-6.2 we only considered pluginId property...
            if (plugin.id === id || plugin.pluginId === id) {
                return plugin;
            }
        }
        return null;
    },
    /**
     * Gets the current XY position of the component's underlying element.
     * @param {Boolean} [local=false] If true the element's left and top are returned instead of
     * page XY.
     * @return {Number[]} The XY position of the element (e.g., [100, 200])
     */
    getPosition: function(local) {
        var me = this,
            xy,
            isContainedFloater = me.isContainedFloater(),
            floatParentBox;
        // Local position for non-floaters means element's local position
        if ((local === true) && !isContainedFloater) {
            return [
                me.getLocalX(),
                me.getLocalY()
            ];
        }
        xy = me.getXY();
        // Local position for floaters means position relative to the container's target element
        if ((local === true) && isContainedFloater) {
            floatParentBox = me.floatParent.getTargetEl().getViewRegion();
            xy[0] -= floatParentBox.left;
            xy[1] -= floatParentBox.top;
        }
        return xy;
    },
    /**
     * Returns the "x" scroll position for this component.  Only applicable for
     * {@link #scrollable} components
     * @return {Number}
     */
    getScrollX: function() {
        var scroller = this.getScrollable();
        return scroller ? scroller.getPosition().x : 0;
    },
    /**
     * Returns the "y" scroll position for this component.  Only applicable for
     * {@link #scrollable} components
     * @return {Number}
     */
    getScrollY: function() {
        var scroller = this.getScrollable();
        return scroller ? scroller.getPosition().y : 0;
    },
    /**
     * Gets the current size of the component's underlying element.
     * @param {Boolean} [contentSize] true to get the width/size minus borders and padding
     * @return {Object} An object containing the element's size:
     * @return {Number} return.width
     * @return {Number} return.height
     */
    getSize: function(contentSize) {
        return this.el.getSize(contentSize);
    },
    /**
     * Returns an object that describes how this component's width and height are managed.
     * All of these objects are shared and should not be modified.
     *
     * @return {Object} The size model for this component.
     * @return {Ext.layout.SizeModel} return.width The {@link Ext.layout.SizeModel size model}
     * for the width.
     * @return {Ext.layout.SizeModel} return.height The {@link Ext.layout.SizeModel size model}
     * for the height.
     * @protected
     */
    getSizeModel: function(ownerCtSizeModel) {
        var me = this,
            models = Ext.layout.SizeModel,
            ownerContext = me.componentLayout.ownerContext,
            width = me.width,
            height = me.height,
            typeofWidth, typeofHeight, hasPixelWidth, hasPixelHeight, hasWidthStyle, hasHeightStyle, heightModel, ownerLayout, policy, shrinkWrap, topLevel, widthModel,
            // floating === a floating Component, floated === a border layout's slideout view
            // of a region.
            isFloating = me.floating || me.floated;
        if (ownerContext) {
            // If we are in the middle of a running layout, always report the current,
            // dynamic size model rather than recompute it. This is not (only) a time
            // saving thing, but a correctness thing since we cannot get the right answer
            // otherwise.
            widthModel = ownerContext.widthModel;
            heightModel = ownerContext.heightModel;
        }
        if (!widthModel || !heightModel) {
            hasPixelWidth = ((typeofWidth = typeof width) === 'number');
            hasPixelHeight = ((typeofHeight = typeof height) === 'number');
            topLevel = isFloating || !(ownerLayout = me.ownerLayout);
            // Floating or no owner layout, e.g. rendered using renderTo
            if (topLevel) {
                policy = Ext.layout.Layout.prototype.autoSizePolicy;
                shrinkWrap = isFloating ? 3 : me.shrinkWrap;
                if (hasPixelWidth) {
                    widthModel = models.configured;
                }
                if (hasPixelHeight) {
                    heightModel = models.configured;
                }
            } else {
                policy = ownerLayout.getItemSizePolicy(me, ownerCtSizeModel);
                shrinkWrap = ownerLayout.isItemShrinkWrap(me);
            }
            if (ownerContext) {
                ownerContext.ownerSizePolicy = policy;
            }
            shrinkWrap = (shrinkWrap === true) ? 3 : (shrinkWrap || 0);
            // false->0, true->3
            // Now that we have shrinkWrap as a 0-3 value, we need to turn off shrinkWrap
            // bits for any dimension that has a configured size not in pixels. These must
            // be read from the DOM.
            //
            if (topLevel && shrinkWrap) {
                if (width && typeofWidth === 'string') {
                    shrinkWrap &= 2;
                    // percentage, "30em" or whatever - not width shrinkWrap
                    hasWidthStyle = true;
                }
                if (height && typeofHeight === 'string') {
                    shrinkWrap &= 1;
                    // percentage, "30em" or whatever - not height shrinkWrap
                    hasHeightStyle = true;
                }
            }
            if (shrinkWrap !== 3) {
                if (!ownerCtSizeModel) {
                    ownerCtSizeModel = me.ownerCt && me.ownerCt.getSizeModel();
                }
                if (ownerCtSizeModel) {
                    shrinkWrap |= (ownerCtSizeModel.width.shrinkWrap ? 1 : 0) | (ownerCtSizeModel.height.shrinkWrap ? 2 : 0);
                }
            }
            if (!widthModel) {
                if (!policy.setsWidth && !(me.frame && hasWidthStyle)) {
                    if (hasPixelWidth) {
                        widthModel = models.configured;
                    } else {
                        widthModel = (shrinkWrap & 1) ? models.shrinkWrap : models.natural;
                    }
                } else if (policy.readsWidth) {
                    if (hasPixelWidth) {
                        widthModel = models.calculatedFromConfigured;
                    } else {
                        widthModel = (shrinkWrap & 1) ? models.calculatedFromShrinkWrap : models.calculatedFromNatural;
                    }
                } else {
                    widthModel = models.calculated;
                }
            }
            if (!heightModel) {
                if (!policy.setsHeight && !(me.frame && hasHeightStyle)) {
                    if (hasPixelHeight) {
                        heightModel = models.configured;
                    } else {
                        heightModel = (shrinkWrap & 2) ? models.shrinkWrap : models.natural;
                    }
                } else if (policy.readsHeight) {
                    if (hasPixelHeight) {
                        heightModel = models.calculatedFromConfigured;
                    } else {
                        heightModel = (shrinkWrap & 2) ? models.calculatedFromShrinkWrap : models.calculatedFromNatural;
                    }
                } else {
                    heightModel = models.calculated;
                }
            }
        }
        // We return one of the cached objects with the proper "width" and "height" as the
        // sizeModels we have determined.
        return widthModel.pairsByHeightOrdinal[heightModel.ordinal];
    },
    /**
     * The supplied default state gathering method for the Component class.
     *
     * This method returns dimension settings such as `flex`, `anchor`, `width` and `height`
     * along with `collapsed` state.
     *
     * Subclasses which implement more complex state should call the superclass's implementation,
     * and apply their state to the result if this basic state is to be saved.
     *
     * Note that Component state will only be saved if the Component has a {@link #stateId}
     * and there as a StateProvider configured for the document.
     *
     * @return {Object}
     */
    getState: function() {
        var me = this,
            state = null,
            sizeModel = me.getSizeModel();
        if (sizeModel.width.configured) {
            state = me.addPropertyToState(state, 'width');
        }
        if (sizeModel.height.configured) {
            state = me.addPropertyToState(state, 'height');
        }
        return state;
    },
    getUserCls: function() {
        return this.userCls;
    },
    setUserCls: function(cls) {
        var me = this,
            was = me.userCls;
        if (cls !== was) {
            me.userCls = cls;
            if (me.rendered) {
                me.el.replaceCls(was, cls);
            }
        }
        return was;
    },
    /**
     * Gets the current width of the component's underlying element.
     * @return {Number}
     */
    getWidth: function() {
        return this.el.getWidth();
    },
    /**
     * Gets the xtype for this component as registered with {@link Ext.ComponentManager}.
     * For a list of all available xtypes, see the {@link Ext.Component} header.
     * Example usage:
     *
     *     var t = new Ext.form.field.Text();
     *     alert(t.getXType());  // alerts 'textfield'
     *
     * @return {String} The xtype
     */
    getXType: function() {
        return this.self.xtype;
    },
    /**
     * Returns this Component's xtype hierarchy as a slash-delimited string. For a list of all
     * available xtypes, see the {@link Ext.Component} header.
     *
     * **If using your own subclasses, be aware that a Component must register its own xtype
     * to participate in determination of inherited xtypes.**
     *
     * Example usage:
     *
     *     @example
     *     var t = new Ext.form.field.Text();
     *     alert(t.getXTypes());  // alerts 'component/field/textfield'
     *
     * @return {String} The xtype hierarchy string
     *
     * @since 2.3.0
     */
    getXTypes: function() {
        var self = this.self,
            xtypes, parentPrototype, parentXtypes;
        if (!self.xtypes) {
            xtypes = [];
            parentPrototype = this;
            while (parentPrototype) {
                parentXtypes = parentPrototype.xtypes;
                if (parentXtypes !== undefined) {
                    xtypes.unshift.apply(xtypes, parentXtypes);
                }
                parentPrototype = parentPrototype.superclass;
            }
            self.xtypeChain = xtypes;
            self.xtypes = xtypes.join('/');
        }
        return self.xtypes;
    },
    /**
     * Checks if the specified CSS class exists on this element's DOM node.
     * @param {String} className The CSS class to check for.
     * @return {Boolean} `true` if the class exists, else `false`.
     * @method
     */
    hasCls: function(className) {
        var el = this.rendered ? this.el : this.protoEl;
        return el.hasCls.apply(el, arguments);
    },
    /**
     * Checks if there is currently a specified `uiCls`.
     * @param {String} cls The `cls` to check.
     */
    hasUICls: function(cls) {
        var me = this,
            uiCls = me.uiCls || [];
        return Ext.Array.contains(uiCls, cls);
    },
    /**
     * Hides this Component, setting it to invisible using the configured {@link #cfg-hideMode}.
     * @param {String/Ext.dom.Element/Ext.Component} [animateTarget=null] **only valid for
     * {@link #cfg-floating} Components such as {@link Ext.window.Window Window}s or
     * {@link Ext.tip.ToolTip ToolTip}s, or regular Components which have been configured with
     * `floating: true`.**. The target to which the Component should animate while hiding.
     * @param {Function} [callback] A callback function to call after the Component is hidden.
     * @param {Object} [scope] The scope (`this` reference) in which the callback is executed.
     * Defaults to this Component.
     * @return {Ext.Component} this
     */
    hide: function(animateTarget, callback, scope) {
        var me = this,
            container = me.ownerFocusableContainer;
        if (me.pendingShow) {
            // If this is a hierarchically hidden floating component with a pending show
            // hide() simply cancels the pending show.
            me.pendingShow = false;
        }
        if (!(me.rendered && !me.isVisible())) {
            if (!me.hasListeners.beforehide || me.fireEvent('beforehide', me) !== false || me.hierarchicallyHidden) {
                me.getInherited().hidden = me.hidden = true;
                if (container && !container.beforeFocusableChildHide.$nullFn) {
                    container.beforeFocusableChildHide(me);
                }
                // Order of events is important here. Hierarchy event kicks off
                // ZIndexManager's collection sorting and floater activation;
                // The hidden flag must be set so that ZIndexManager takes it out of its stack.
                me.fireHierarchyEvent('beforehide');
                if (me.rendered) {
                    me.onHide.apply(me, arguments);
                }
            }
        }
        return me;
    },
    /**
     * The initComponent template method is an important initialization step for a Component.
     * It is intended to be implemented by each subclass of Ext.Component to provide any needed
     * constructor logic. The initComponent method of the class being created is called first,
     * with each initComponent method up the hierarchy to Ext.Component being called thereafter.
     * This makes it easy to implement and, if needed, override the constructor logic
     * of the Component at any step in the hierarchy.
     *
     * The initComponent method **must** contain a call to
     * {@link Ext.Base#method!callParent callParent} in order to ensure that the parent class'
     * initComponent method is also called.
     *
     * All config options passed to the constructor are applied to `this` before initComponent
     * is called, so you can simply access them with `this.someOption`.
     *
     * The following example demonstrates using a dynamic string for the text of a button
     * at the time of instantiation of the class.
     *
     *     Ext.define('DynamicButtonText', {
     *         extend: 'Ext.button.Button',
     *
     *         initComponent: function() {
     *             this.text = new Date();
     *             this.renderTo = Ext.getBody();
     *             this.callParent();
     *         }
     *     });
     *
     *     Ext.onReady(function() {
     *         Ext.create('DynamicButtonText');
     *     });
     *
     * @template
     * @protected
     * @since 1.1.0
     */
    initComponent: function() {
        var me = this,
            width = me.width,
            height = me.height;
        // If plugins have been added by a subclass's initComponent before calling up to here
        // (or any components that don't have a table view), the processed flag will not have been
        // set, and we must process them again.
        // We could just call getPlugins here however most components don't have them so prevent
        // the extra function call.
        if (me.plugins && !me.plugins.processed) {
            me.constructPlugins();
        }
        me.pluginsInitialized = true;
        // this will properly (ignore or) constrain the configured width/height to their
        // min/max values for consistency.
        if (width != null || height != null) {
            me.setSize(width, height);
        }
        if (me.listeners) {
            me.on(me.listeners);
            me.listeners = null;
        }
        // change the value to remove any on prototype
        if (me.focusable) {
            me.initFocusable();
        }
    },
    /**
     * Initialize any events on this component
     * @protected
     */
    initEvents: function() {
        var me = this,
            afterRenderEvents = me.afterRenderEvents,
            afterRenderEvent, el, property, index, len;
        if (afterRenderEvents) {
            for (property in afterRenderEvents) {
                el = me[property];
                if (el && el.on) {
                    afterRenderEvent = afterRenderEvents[property];
                    for (index = 0 , len = afterRenderEvent.length; index < len; ++index) {
                        me.mon(el, afterRenderEvent[index]);
                    }
                }
            }
        }
        if (me.focusable) {
            me.initFocusableEvents();
        }
        // FocusableContainers are not themselves focusable, but they must process
        // their keyMap config
        me.initKeyMap();
    },
    /**
     * Tests whether this Component matches a {@link Ext.ComponentQuery ComponentQuery}
     * selector string.
     * @param {String} selector The selector string to test against.
     * @return {Boolean} `true` if this Component matches the selector.
     */
    is: function(selector) {
        return Ext.ComponentQuery.is(this, selector);
    },
    /**
     * Method to determine whether this Component is currently disabled.
     * @return {Boolean} the disabled state of this Component.
     */
    isDisabled: function() {
        return this.disabled;
    },
    /**
     * Method to determine whether this Component is draggable.
     * @return {Boolean} the draggable state of this component.
     */
    isDraggable: function() {
        return !!this.draggable;
    },
    /**
     * Method to determine whether this Component is droppable.
     * @return {Boolean} the droppable state of this component.
     */
    isDroppable: function() {
        return !!this.droppable;
    },
    /**
     * Method to determine whether this Component is floating.
     * @return {Boolean} the floating state of this component.
     */
    isFloating: function() {
        return this.floating;
    },
    /**
     * Method to determine whether this Component is currently set to hidden.
     * @return {Boolean} the hidden state of this Component.
     */
    isHidden: function() {
        return this.hidden;
    },
    getHidden: function() {
        return this.hidden;
    },
    isHierarchicallyHidden: function() {
        var child = this,
            hidden = false,
            parent, parentInheritedState;
        // It is possible for some components to be immune to collapse meaning the immune
        // component remains visible when its direct parent is collapsed, e.g. panel header.
        // Because of this, we must walk up the component hierarchy to determine the true
        // visible state of the component.
        for (; (parent = child.ownerCt || child.floatParent); child = parent) {
            parentInheritedState = parent.getInherited();
            if (parentInheritedState.hidden) {
                hidden = true;
                break;
            }
            if (child.getInherited().collapseImmune) {
                // The child or one of its ancestors is immune to collapse.
                if (parent.collapsed && !child.collapseImmune) {
                    // If the child's direct parent is collapsed, and the child
                    // itself does not have collapse immunity we know that
                    // the child is not visible.
                    hidden = true;
                    break;
                }
            } else {
                // We have ascended the tree to a point where collapse immunity
                // is not in play.  This means if any ancestor above this point
                // is collapsed, then the component is not visible.
                hidden = !!parentInheritedState.collapsed;
                break;
            }
        }
        return hidden;
    },
    /**
     * Checks if this component will be contained by the passed component as part of its
     * layout run. If `true`, then the layout on `this` can be skipped because it will be
     * encompassed when the layout for `comp` runs. Typical cases where this may be be `false`
     * is when asking about floaters nested in containers.
     * @param {Ext.Component} ownerCandidate The potential owner.
     * @return {Boolean} `true` if this component is a layout child of `comp`.
     *
     * @private
     */
    isLayoutChild: function(ownerCandidate) {
        return !this.floating && !!this.up(ownerCandidate);
    },
    /**
     * Determines whether this Component is the root of a layout. This returns `true` if
     * this component can run its layout without assistance from or impact on its owner.
     * If this component cannot run its layout given these restrictions, `false` is returned
     * and its owner will be considered as the next candidate for the layout root.
     *
     * Setting the {@link #_isLayoutRoot} property to `true` causes this method to always
     * return `true`. This may be useful when updating a layout of a Container which shrink
     * wraps content, and you know that it will not change size, and so can safely be the
     * topmost participant in the layout run.
     * @protected
     */
    isLayoutRoot: function() {
        var me = this,
            ownerLayout = me.ownerLayout;
        // Return true if we have been explicitly flagged as the layout root, or if we are floating.
        // Sometimes floating Components get an ownerCt ref injected into them which is *not*
        // a true ownerCt, merely an upward link for reference purposes. For example a grid column
        // menu is linked to the owning header via an ownerCt reference.
        if (!ownerLayout || me._isLayoutRoot || me.floating) {
            return true;
        }
        return ownerLayout.isItemLayoutRoot(me);
    },
    /**
     * Returns `true` if layout is suspended for this component. This can come from direct
     * suspension of this component's layout activity ({@link Ext.Container#suspendLayout})
     * or if one of this component's containers is suspended.
     *
     * @return {Boolean} `true` layout of this component is suspended.
     */
    isLayoutSuspended: function() {
        var comp = this,
            ownerLayout;
        while (comp) {
            if (comp.layoutSuspendCount || comp.suspendLayout) {
                return true;
            }
            ownerLayout = comp.ownerLayout;
            if (!ownerLayout) {
                break;
            }
            // TODO - what about suspending a Layout instance?
            // this works better than ownerCt since ownerLayout means "is managed by" in
            // the proper sense... some floating components have ownerCt but won't have an
            // ownerLayout
            comp = ownerLayout.owner;
        }
        return false;
    },
    /**
     * Returns `true` if this component is visible.
     *
     * @param {Boolean} [deep=false] Pass `true` to interrogate the visibility status of all
     * parent Containers to determine whether this Component is truly visible to the user.
     *
     * Generally, to determine whether a Component is hidden, the no argument form is needed.
     * For example when creating dynamically laid out UIs in a hidden Container before showing them.
     *
     * @return {Boolean} `true` if this component is visible, `false` otherwise.
     *
     * @since 1.1.0
     */
    isVisible: function(deep) {
        var me = this,
            hidden;
        if (me.hidden || !me.rendered || me.destroyed) {
            hidden = true;
        } else if (deep) {
            hidden = me.isHierarchicallyHidden();
        }
        return !hidden;
    },
    /**
     * Tests whether or not this Component is of a specific xtype. This can test whether this
     * Component is descended from the xtype (default) or whether it is directly of the xtype
     * specified (`shallow = true`).
     *
     * **If using your own subclasses, be aware that a Component must register its own xtype
     * to participate in determination of inherited xtypes.**
     *
     * For a list of all available xtypes, see the {@link Ext.Component} header.
     *
     * Example usage:
     *
     *     @example
     *     var t = new Ext.form.field.Text();
     *     var isText = t.isXType('textfield');          // true
     *     var isBoxSubclass = t.isXType('field');       // true, descended from Ext.form.field.Base
     *     var isBoxInstance = t.isXType('field', true); // false, not a direct Ext.form.field.Base
     *                                                   // instance
     *
     * @param {String} xtype The xtype to check for this Component
     * @param {Boolean} [shallow=false] `true` to check whether this Component is directly
     * of the specified xtype, `false` to check whether this Component is descended from the xtype.
     * @return {Boolean} `true` if this component descends from the specified xtype,
     * `false` otherwise.
     *
     * @since 2.3.0
     */
    isXType: function(xtype, shallow) {
        return shallow ? (Ext.Array.indexOf(this.xtypes, xtype) !== -1) : !!this.xtypesMap[xtype];
    },
    /**
     * Returns masked state for this Component.
     *
     * @param {Boolean} [deep=false] True to look up this Component's parent masked state.
     *
     * @return {Boolean} True if masked, false otherwise.
     */
    isMasked: function(deep) {
        var me = this;
        return !!(me.masked || (me.loadMask && me.loadMask.isVisible()) || (deep && me.getInherited().masked));
    },
    /**
     * Gets a named template instance for this class. See {@link Ext.XTemplate#getTpl}.
     * @param {String} name The name of the property that holds the template.
     * @return {Ext.XTemplate} The template, `null` if not found.
     *
     * @since 6.2.0
     */
    lookupTpl: function(name) {
        return Ext.XTemplate.getTpl(this, name);
    },
    /**
     * Set masked state for this Component.
     *
     * @param {Boolean} isMasked True if masked, false otherwise.
     * @private
     */
    setMasked: function(isMasked) {
        var me = this,
            container = me.ownerFocusableContainer;
        if (isMasked) {
            me.masked = true;
            me.getInherited().masked = isMasked;
        } else {
            me.masked = false;
            delete me.getInherited().masked;
        }
        if (container && !container.onFocusableChildMasked.$nullFn) {
            container.onFocusableChildMasked(me, isMasked);
        }
        return me;
    },
    /**
     * Masks this component with a semi-opaque layer and makes the contents unavailable to clicks.
     *
     * See {@link #unmask}.
     *
     * @param {String} [msg] A message to show in the center of the mask layer.
     * @param {String} [msgCls] A CSS class name to use on the message element in the center
     * of the layer.
     * @param {Number} elHeight (private) The height of element to be masked
     */
    mask: function(msg, msgCls, elHeight) {
        var box = this.lastBox,
            // getMaskTarget may be overridden in subclasses/
            // null means that a LoadMask has to be rendered to document.body
            // Element masking falls back to masking the local el
            target = this.getMaskTarget() || this.el;
        // Pass it the height of our element if we know it.
        if (box) {
            elHeight = box.height;
        }
        target.mask(msg, msgCls, elHeight);
        this.setMasked(true);
    },
    /**
     * Returns the next node in the Component tree in tree traversal order.
     *
     * Note that this is not limited to siblings, and if invoked upon a node with no matching
     * siblings, will walk the tree to attempt to find a match. Contrast with {@link #nextSibling}.
     * @param {String} [selector] A {@link Ext.ComponentQuery ComponentQuery} selector to filter
     * the following nodes.
     * @param includeSelf (private)
     * @return {Ext.Component} The next node (or the next node which matches the selector).
     * Returns `null` if there is no matching node.
     */
    nextNode: function(selector, includeSelf) {
        var node = this,
            ownerCt = node.ownerCt,
            result, it, len, i, sib;
        // If asked to include self, test me
        if (includeSelf && node.is(selector)) {
            return node;
        }
        if (ownerCt) {
            // eslint-disable-next-line max-len
            for (it = ownerCt.items.items , i = Ext.Array.indexOf(it, node) + 1 , len = it.length; i < len; i++) {
                sib = it[i];
                if (sib.is(selector)) {
                    return sib;
                }
                if (sib.down) {
                    result = sib.down(selector);
                    if (result) {
                        return result;
                    }
                }
            }
            return ownerCt.nextNode(selector);
        }
        return null;
    },
    /**
     * Returns the next sibling of this Component.
     *
     * Optionally selects the next sibling which matches the passed
     * {@link Ext.ComponentQuery ComponentQuery} selector.
     *
     * May also be referred to as **`next()`**
     *
     * Note that this is limited to siblings, and if no siblings of the item match,
     * `null` is returned. Contrast with {@link #nextNode}
     * @param {String} [selector] A {@link Ext.ComponentQuery ComponentQuery} selector to filter
     * the following items.
     * @return {Ext.Component} The next sibling (or the next sibling which matches the selector).
     * Returns `null` if there is no matching sibling.
     */
    nextSibling: function(selector) {
        var o = this.ownerCt,
            it, last, idx, c;
        if (o) {
            it = o.items;
            idx = it.indexOf(this) + 1;
            if (idx) {
                if (selector) {
                    for (last = it.getCount(); idx < last; idx++) {
                        if ((c = it.getAt(idx)).is(selector)) {
                            return c;
                        }
                    }
                } else {
                    if (idx < it.getCount()) {
                        return it.getAt(idx);
                    }
                }
            }
        }
        return null;
    },
    /**
     * Method to manage awareness of when components are added to their
     * respective Container, firing an #added event. References are
     * established at add time rather than at render time.
     *
     * Allows addition of behavior when a Component is added to a
     * Container. At this stage, the Component is in the parent
     * Container's collection of child items. After calling the
     * superclass's `onAdded`, the `ownerCt` reference will be present,
     * and if configured with a ref, the `refOwner` will be set.
     *
     * @param {Ext.container.Container} container Container which holds the component.
     * @param {Number} pos Position at which the component was added.
     * @param {Boolean} instanced `false` if this component was instanced by the parent
     * container. `true` if the instance already existed when it was passed to the container.
     *
     * @template
     * @protected
     * @since 3.4.0
     */
    onAdded: function(container, pos, instanced) {
        var me = this;
        me.ownerCt = container;
        me.onInheritedAdd(me, instanced);
        if (me.hasListeners && me.hasListeners.added) {
            me.fireEvent('added', me, container, pos);
        }
        if (Ext.GlobalEvents.hasListeners.added) {
            me.fireHierarchyEvent('added');
        }
    },
    /**
     * Method to manage awareness of when components are removed from their
     * respective Container, firing a #removed event. References are properly
     * cleaned up after removing a component from its owning container.
     *
     * Allows addition of behavior when a Component is removed from
     * its parent Container. At this stage, the Component has been
     * removed from its parent Container's collection of child items,
     * but has not been destroyed (It will be destroyed if the parent
     * Container's `autoDestroy` is `true`, or if the remove call was
     * passed a truthy second parameter). After calling the
     * superclass's `onRemoved`, the `ownerCt` and the `refOwner` will not
     * be present.
     * @param {Boolean} destroying Will be passed as `true` if the Container performing
     * the remove operation will delete this Component upon remove.
     *
     * @template
     * @protected
     * @since 3.4.0
     */
    onRemoved: function(destroying) {
        var me = this,
            focusTarget;
        // Revert focus to closest sibling or ancestor unless we are being moved
        // In which case Ext.container.Container's move methods will handle
        // focus restoration.
        if (!me.isLayoutMoving && me.el && me.el.contains(Ext.Element.getActiveElement())) {
            focusTarget = me.findFocusTarget();
            if (focusTarget) {
                focusTarget.focus();
            }
        }
        if (Ext.GlobalEvents.hasListeners.removed) {
            me.fireHierarchyEvent('removed');
        }
        if (me.hasListeners.removed) {
            me.fireEvent('removed', me, me.ownerCt);
        }
        me.onInheritedRemove(destroying);
        me.ownerCt = me.ownerLayout = null;
    },
    /**
     * Invoked when this component has first achieved size. This occurs after the
     * {@link #componentLayout} has completed its initial run.
     *
     * This method is not called on components that use {@link #cfg-liquidLayout}, such as
     * {@link Ext.button.Button Buttons} and {@link Ext.form.field.Base Form Fields}.
     *
     * **Note:** If the Component has a {@link Ext.Component#controller ViewController}
     * and the controller has a {@link Ext.app.ViewController#boxReady boxReady} method
     * it will be called passing the Component and its width and height.
     *
     *      boxReady: function(view, width, height) {
     *          // ...
     *      }
     *
     * @param {Number} width The width of this component
     * @param {Number} height The height of this component
     *
     * @template
     * @protected
     */
    onBoxReady: function(width, height) {
        var me = this,
            label;
        // We have to do this lookup onBoxReady instead of afterRender
        // to ensure that the components that could be referenced in
        // me.ariaLabelledBy or me.ariaDescribedBy are already rendered
        if (me.ariaLabelledBy || me.ariaDescribedBy) {
            if (me.ariaLabelledBy) {
                label = me.getAriaLabelEl(me.ariaLabelledBy);
                if (label) {
                    me.ariaEl.dom.setAttribute('aria-labelledby', label);
                }
            }
            if (me.ariaDescribedBy) {
                label = me.getAriaLabelEl(me.ariaDescribedBy);
                if (label) {
                    me.ariaEl.dom.setAttribute('aria-describedby', label);
                }
            }
        }
        if (me.resizable) {
            me.initResizable(me.resizable);
        }
        // Draggability must be initialized after resizability
        // Because if we have to be wrapped, the resizer wrapper must be dragged
        // as a pseudo-Component
        if (me.draggable) {
            me.initDraggable();
        }
        if (me.hasListeners.boxready) {
            me.fireEvent('boxready', me, width, height);
        }
    },
    /**
     * Allows addition of behavior to the destroy operation.
     *
     * @template
     * @method
     * @protected
     */
    onDestroy: Ext.emptyFn,
    /**
     * Allows addition of behavior to the disable operation.
     * After calling the superclass's `onDisable`, the Component will be disabled.
     *
     * @template
     * @protected
     */
    onDisable: function() {
        var me = this,
            dom, nodeName;
        if (me.focusable) {
            me.disableFocusable();
        }
        if (!me.ariaStaticRoles[me.ariaRole]) {
            me.ariaEl.dom.setAttribute('aria-disabled', true);
        }
        // Only mask if we're set to & nobody above us will do so
        if (me.maskOnDisable && !me.getInheritedConfig('disableMask', true)) {
            dom = me.el.dom;
            nodeName = dom.nodeName;
            if (me.disabledRe.test(nodeName)) {
                dom.disabled = true;
            }
            if (!me.nonMaskableRe.test(nodeName)) {
                me.mask();
            }
        }
    },
    /**
     * Allows addition of behavior to the enable operation.
     * After calling the superclass's `onEnable`, the Component will be enabled.
     *
     * @template
     * @protected
     */
    onEnable: function() {
        var me = this,
            dom, nodeName;
        if (me.focusable) {
            me.enableFocusable();
        }
        if (!me.ariaStaticRoles[me.ariaRole]) {
            me.ariaEl.dom.setAttribute('aria-disabled', false);
        }
        if (me.maskOnDisable && me.getInherited().hasOwnProperty('masked')) {
            dom = me.el.dom;
            nodeName = dom.nodeName;
            if (me.disabledRe.test(nodeName)) {
                dom.disabled = false;
            }
            if (!me.nonMaskableRe.test(nodeName)) {
                me.unmask();
            }
        }
    },
    onGlobalShow: function(c) {
        if (this.up(c)) {
            this.getScrollable().restoreState();
        }
    },
    /**
     * Allows addition of behavior to the hide operation. After
     * calling the superclass's onHide, the Component will be hidden.
     *
     * Gets passed the same parameters as #hide.
     *
     * @param {String/Ext.dom.Element/Ext.Component} [animateTarget]
     * @param {Function} [callback] Callback function to be called when finished
     * @param {Object} [scope]
     *
     * @template
     * @protected
     */
    onHide: function(animateTarget, callback, scope) {
        var me = this,
            ghostPanel, fromSize, toBox;
        if (!me.ariaStaticRoles[me.ariaRole] && !me.destroying && !me.destroyed) {
            me.ariaEl.dom.setAttribute('aria-hidden', true);
        }
        // Part of the Focusable mixin API.
        // If we have focus now, move focus back to whatever had it before.
        me.revertFocus();
        // Default to configured animate target if none passed
        animateTarget = me.getAnimateTarget(animateTarget);
        // Need to be able to ghost the Component
        if (!me.ghost) {
            animateTarget = null;
        }
        // If we're animating, kick off an animation of the ghost down to the target
        if (animateTarget) {
            toBox = {
                x: animateTarget.getX(),
                y: animateTarget.getY(),
                width: animateTarget.dom.offsetWidth,
                height: animateTarget.dom.offsetHeight
            };
            ghostPanel = me.ghost();
            ghostPanel.el.stopAnimation();
            fromSize = me.getSize();
            ghostPanel.el.animate({
                to: toBox,
                listeners: {
                    afteranimate: function() {
                        if (!me.destroying) {
                            ghostPanel.componentLayout.lastComponentSize = null;
                            me.unghost(false);
                            ghostPanel.el.setSize(fromSize);
                            me.afterHide(callback, scope);
                        }
                    }
                }
            });
        } else {
            me.el.hide();
        }
        if (!animateTarget) {
            me.afterHide(callback, scope);
        }
    },
    /**
     * @method
     * Called after the component is moved, this method is empty by default but can be implemented
     * by any subclass that needs to perform custom logic after a move occurs.
     *
     * @param {Number} x The new x position.
     * @param {Number} y The new y position.
     *
     * @template
     * @protected
     */
    onPosition: Ext.emptyFn,
    /**
     * Called when the component is resized.
     *
     * This method is not called on components that use {@link #cfg-liquidLayout}, such as
     * {@link Ext.button.Button Buttons} and {@link Ext.form.field.Base Form Fields}.
     *
     * @param {Number} width The new width that was set
     * @param {Number} height The new height that was set
     * @param {Number} oldWidth The previous width
     * @param {Number} oldHeight The previous height
     *
     * @method
     * @template
     * @protected
     */
    onResize: function(width, height, oldWidth, oldHeight) {
        var me = this;
        // constrain is a config on Floating
        if (me.floating && me.constrain) {
            me.doConstrain();
        }
        if (me.hasListeners.resize) {
            me.fireEvent('resize', me, width, height, oldWidth, oldHeight);
        }
    },
    /**
     * Invoked when a scroll is initiated on this component via its {@link #scrollable scroller}.
     * @method onScrollStart
     * @param {Number} x The current x position
     * @param {Number} y The current y position
     * @template
     * @protected
     */
    /**
     * Invoked when this component is scrolled via its {@link #scrollable scroller}.
     * @method onScrollMove
     * @param {Number} x The current x position
     * @param {Number} y The current y position
     * @template
     * @protected
     */
    /**
     * Invoked when a scroll operation is completed via this component's
     * {@link #scrollable scroller}.
     * @method onScrollEnd
     * @param {Number} x The current x position
     * @param {Number} y The current y position
     * @template
     * @protected
     */
    /**
     * Allows addition of behavior to the show operation. After
     * calling the superclass's onShow, the Component will be visible.
     *
     * Override in subclasses where more complex behaviour is needed.
     *
     * Gets passed the same parameters as #show.
     *
     * @param {String/Ext.dom.Element} [animateTarget]
     * @param {Function} [callback]
     * @param {Object} [scope]
     *
     * @template
     * @protected
     */
    onShow: function() {
        var me = this;
        if (!me.ariaStaticRoles[me.ariaRole]) {
            me.ariaEl.dom.setAttribute('aria-hidden', false);
        }
        me.el.show();
        me.updateLayout({
            isRoot: false,
            context: me._showContext
        });
        // Constraining/containing element may have changed size while this Component was hidden
        if (me.floating) {
            if (me.maximized) {
                me.fitContainer();
            } else if (me.constrain) {
                me.doConstrain();
            }
        }
    },
    _showContext: {
        show: true
    },
    /**
     * Invoked after the #afterShow method is complete.
     *
     * Gets passed the same `callback` and `scope` parameters that #afterShow received.
     *
     * @param {Function} [callback]
     * @param {Object} [scope]
     *
     * @template
     * @protected
     */
    onShowComplete: function(callback, scope) {
        var me = this,
            container = me.ownerFocusableContainer;
        if (me.floating) {
            me.onFloatShow();
        }
        Ext.callback(callback, scope || me);
        me.fireEvent('show', me);
        if (container && !container.onFocusableChildShow.$nullFn) {
            container.onFocusableChildShow(me);
        }
        delete me.hiddenByLayout;
    },
    onShowVeto: Ext.emptyFn,
    /**
     * Returns the previous node in the Component tree in tree traversal order.
     *
     * Note that this is not limited to siblings, and if invoked upon a node with no matching
     * siblings, will walk the tree in reverse order to attempt to find a match. Contrast with
     * {@link #previousSibling}.
     * @param {String} [selector] A {@link Ext.ComponentQuery ComponentQuery} selector to filter
     * the preceding nodes.
     * @param includeSelf (private)
     * @return {Ext.Component} The previous node (or the previous node which matches the selector).
     * Returns `null` if there is no matching node.
     */
    previousNode: function(selector, includeSelf) {
        var node = this,
            ownerCt = node.ownerCt,
            result, it, i, sib;
        // If asked to include self, test me
        if (includeSelf && node.is(selector)) {
            return node;
        }
        if (ownerCt) {
            for (it = ownerCt.items.items , i = Ext.Array.indexOf(it, node) - 1; i > -1; i--) {
                sib = it[i];
                if (sib.query) {
                    result = sib.query(selector);
                    result = result[result.length - 1];
                    if (result) {
                        return result;
                    }
                }
                if (sib.is(selector)) {
                    return sib;
                }
            }
            return ownerCt.previousNode(selector, true);
        }
        return null;
    },
    /**
     * Returns the previous sibling of this Component.
     *
     * Optionally selects the previous sibling which matches the passed
     * {@link Ext.ComponentQuery ComponentQuery} selector.
     *
     * May also be referred to as **`prev()`**
     *
     * Note that this is limited to siblings, and if no siblings of the item match,
     * `null` is returned. Contrast with {@link #previousNode}
     * @param {String} [selector] A {@link Ext.ComponentQuery ComponentQuery} selector to filter
     * the preceding items.
     * @return {Ext.Component} The previous sibling (or the previous sibling which matches
     * the selector).
     * Returns `null` if there is no matching sibling.
     */
    previousSibling: function(selector) {
        var o = this.ownerCt,
            it, idx, c;
        if (o) {
            it = o.items;
            idx = it.indexOf(this);
            if (idx !== -1) {
                if (selector) {
                    for (--idx; idx >= 0; idx--) {
                        if ((c = it.getAt(idx)).is(selector)) {
                            return c;
                        }
                    }
                } else {
                    if (idx) {
                        return it.getAt(--idx);
                    }
                }
            }
        }
        return null;
    },
    /**
     * Called by Component#doAutoRender
     *
     * Register a Container configured `floating: true` with this Component's
     * {@link Ext.ZIndexManager ZIndexManager}.
     *
     * Components added in this way will not participate in any layout, but will be rendered
     * upon first show in the way that {@link Ext.window.Window Window}s are.
     */
    registerFloatingItem: function(cmp) {
        var me = this;
        if (!me.floatingDescendants) {
            me.floatingDescendants = new Ext.ZIndexManager(me);
        }
        me.floatingDescendants.register(cmp);
    },
    /**
     * Removes a CSS class from the top level element representing this component.
     * @param {String/String[]} cls The CSS class name to remove.
     * @return {Ext.Component} Returns the Component to allow method chaining.
     */
    removeCls: function(cls) {
        var me = this,
            el = me.rendered ? me.el : me.protoEl;
        el.removeCls.apply(el, arguments);
        return me;
    },
    /**
     * Removes a `cls` to the `uiCls` array, which will also call {@link #removeUIClsFromElement}
     * and removes it from all elements of this component.
     * @param {String/String[]} classes A string or an array of strings to remove to the `uiCls`.
     * @param {Boolean} skip True to remove the class
     */
    removeClsWithUI: function(classes, skip) {
        var me = this,
            clsArray = [],
            i = 0,
            extArray = Ext.Array,
            remove = extArray.remove,
            uiCls = me.uiCls = extArray.clone(me.uiCls),
            activeUI = me.activeUI,
            length, cls;
        if (typeof classes === "string") {
            classes = (classes.indexOf(' ') < 0) ? [
                classes
            ] : Ext.String.splitWords(classes);
        }
        length = classes.length;
        for (i = 0; i < length; i++) {
            cls = classes[i];
            if (cls && me.hasUICls(cls)) {
                remove(uiCls, cls);
                // If there's no activeUI then there's nothing to remove
                if (activeUI) {
                    clsArray = clsArray.concat(me.removeUIClsFromElement(cls));
                }
            }
        }
        if (skip !== true && activeUI) {
            me.removeCls(clsArray);
        }
        return clsArray;
    },
    resumeLayouts: function(flushOptions) {
        var me = this;
        if (!me.rendered) {
            return;
        }
        if (!me.layoutSuspendCount) {
            Ext.log.warn('Mismatched call to resumeLayouts - layouts are currently not suspended.');
        }
        if (me.layoutSuspendCount && !--me.layoutSuspendCount) {
            me.suspendLayout = false;
            if (flushOptions && !me.isLayoutSuspended()) {
                me.updateLayout(flushOptions);
            }
        }
    },
    /**
     * Scrolls this Component by the passed delta values, optionally animating.
     *
     * All of the following are equivalent:
     *
     *      comp.scrollBy(10, 10, true);
     *      comp.scrollBy([10, 10], true);
     *      comp.scrollBy({ x: 10, y: 10 }, true);
     *
     * @param {Number/Number[]/Object} deltaX Either the x delta, an Array specifying x and y deltas
     * or an object with "x" and "y" properties.
     * @param {Number/Boolean/Object} deltaY Either the y delta, or an animate flag
     * or config object.
     * @param {Boolean/Object} animate Animate flag/config object if the delta values were passed
     * separately.
     */
    scrollBy: function(deltaX, deltaY, animate) {
        var scroller = this.getScrollable();
        if (scroller) {
            scroller.scrollBy(deltaX, deltaY, animate);
        }
    },
    /**
     * Scrolls this component to the specified `x` and `y` coordinates.  Only applicable
     * for {@link #scrollable} components.
     * @param {Number} x
     * @param {Number} y
     * @param {Boolean/Object} [animate] true for the default animation or a standard Element
     * animation config object
     */
    scrollTo: function(x, y, animate) {
        var scroller = this.getScrollable();
        if (scroller) {
            scroller.scrollTo(x, y, animate);
        }
    },
    /**
     * Sets the overflow on the content element of the component.
     * @param {Boolean} scroll True to allow the Component to auto scroll.
     * @return {Ext.Component} this
     * @deprecated 5.0.0 Use {@link #setScrollable} instead
     */
    setAutoScroll: function(scroll) {
        this.setScrollable(!!scroll);
        return this;
    },
    /**
     * @param {String/Number} border The border, see {@link #border}. If a falsey value is passed
     * @param targetEl (private)
     * the border will be removed.
     */
    setBorder: function(border, targetEl) {
        var me = this,
            initial = !!targetEl;
        if (me.rendered || initial) {
            if (!initial) {
                targetEl = me.el;
            }
            if (!border) {
                border = 0;
            } else if (border === true) {
                border = '1px';
            } else {
                border = this.unitizeBox(border);
            }
            targetEl.setStyle('border-width', border);
            if (!initial) {
                me.updateLayout();
            }
        }
        me.border = border;
    },
    /**
     * Sets the dock position of this component in its parent panel. Note that this only has effect
     * if this item is part of the `dockedItems` collection of a parent that has a DockLayout
     * (note that any Panel has a DockLayout by default)
     * @param {Object} dock The dock position.
     * @return {Ext.Component} this
     */
    setDock: function(dock) {
        var me = this,
            ownerCt = me.ownerCt;
        if (dock !== me.dock) {
            if (ownerCt && ownerCt.moveDocked) {
                ownerCt.moveDocked(me, dock);
            } else {
                me.dock = dock;
            }
        }
        return me;
    },
    /**
     * Enable or disable the component.
     * @param {Boolean} disabled `true` to disable.
     */
    setDisabled: function(disabled) {
        return this[disabled ? 'disable' : 'enable']();
    },
    /**
     * Sets the flex property of this component. Only applicable when this component is
     * an item of a box layout
     * @private
     * @param {Number} flex
     */
    setFlex: function(flex) {
        this.flex = flex;
    },
    /**
     * Sets the height of the component. This method fires the {@link #resize} event.
     *
     * @param {Number} height The new height to set. This may be one of:
     *
     *   - A Number specifying the new height in pixels.
     *   - A String used to set the CSS height style.
     *   - `undefined` to leave the height unchanged.
     *   - `null` to clear the height.
     *
     * @return {Ext.Component} this
     */
    setHeight: function(height) {
        return this.setSize(undefined, height);
    },
    /**
     * This method allows you to show or hide a LoadMask on top of this component.
     *
     * The mask will be rendered into the element returned by {@link #getMaskTarget} which for most
     * Components is the Component's element. See {@link #getMaskTarget} and {@link #maskElement}.
     *
     * Most Components will return `null` indicating that their LoadMask cannot reside inside
     * their element, but must be rendered into the document body.
     *
     * {@link Ext.view.Table Grid Views} however will direct a LoadMask to be rendered into
     * the owning {@link Ext.panel.Table GridPanel}.
     *
     * @param {Boolean/Object/String} load True to show the default LoadMask, a config object
     * that will be passed to the LoadMask constructor, or a message String to show. False to hide
     * the current LoadMask.
     * @param targetEl (private) This param is deprecated
     * @return {Ext.LoadMask} The LoadMask instance that has just been shown.
     */
    setLoading: function(load, targetEl) {
        var me = this,
            config = {
                target: me
            };
        if (me.rendered) {
            // Shows mask for anything but false.
            if (load !== false) {
                if (Ext.isString(load)) {
                    config.msg = load;
                } else {
                    Ext.apply(config, load, me.maskDefaults);
                }
                // We do not already have a LoadMask: create one
                if (!me.loadMask || !me.loadMask.isLoadMask) {
                    // Deprecated second parameter.
                    // maskElement config replaces this
                    if (targetEl && config.useTargetEl == null) {
                        config.useTargetEl = true;
                    }
                    me.loadMask = new Ext.LoadMask(config);
                } else // Change any settings according to load config
                {
                    Ext.apply(me.loadMask, config);
                }
                // If already visible, just update display with passed configs.
                if (me.loadMask.isVisible()) {
                    me.loadMask.syncMaskState();
                } else // Otherwise show with new configs
                {
                    me.loadMask.show();
                }
            } else // load == falsy: Hide the mask if it exists
            {
                if (me.loadMask && me.loadMask.isLoadMask) {
                    me.loadMask.hide();
                }
            }
        }
        return me.loadMask;
    },
    /**
     * Sets the margin on the target element.
     * @param {Number/String} margin The margin to set. See the {@link #margin} config.
     * @param preventLayout (private)
     */
    setMargin: function(margin, preventLayout) {
        var me = this;
        if (me.rendered) {
            if (!margin && margin !== 0) {
                margin = '';
            } else {
                if (margin === true) {
                    margin = 5;
                }
                margin = this.unitizeBox(margin);
            }
            me.margin = margin;
            // See: EXTJS-13359
            me.margin$ = null;
            me.getEl().setStyle('margin', margin);
            if (!preventLayout) {
                // Changing the margins can impact the position of this (and possibly)
                // other subsequent components in the layout.
                me.updateLayout(me._notAsLayoutRoot);
            }
        } else {
            me.margin = margin;
        }
    },
    /**
     * Sets the overflow x/y on the content element of the component. The x/y overflow
     * values can be any valid CSS overflow (e.g., 'auto' or 'scroll'). By default, the
     * value is 'hidden'.  Passing `undefined` will preserve the current value.
     *
     * @param {String} overflowX The overflow-x value.
     * @param {String} overflowY The overflow-y value.
     * @return {Ext.Component} this
     * @deprecated 5.0.0 Use {@link #setScrollable} instead
     */
    setOverflowXY: function(overflowX, overflowY) {
        this.setScrollable({
            x: (overflowX && overflowX !== 'hidden') ? overflowX : false,
            y: (overflowY && overflowY !== 'hidden') ? overflowY : false
        });
        return this;
    },
    /**
     * Sets the page XY position of the component. To set the left and top instead,
     * use {@link #setPosition}. This method fires the {@link #event-move} event.
     * @param {Number/Number[]} x The new x position or an array of `[x,y]`.
     * @param {Number} [y] The new y position.
     * @param {Boolean/Object} [animate] True to animate the Component into its new position.
     * You may also pass an animation configuration.
     * @return {Ext.Component} this
     */
    setPagePosition: function(x, y, animate) {
        var me = this,
            p, floatParentBox;
        if (Ext.isArray(x)) {
            y = x[1];
            x = x[0];
        }
        me.pageX = x;
        me.pageY = y;
        if (me.floating) {
            // Floating Components which are registered with a Container have to have
            // their x and y properties made relative
            if (me.isContainedFloater()) {
                floatParentBox = me.floatParent.getTargetEl().getViewRegion();
                if (Ext.isNumber(x) && Ext.isNumber(floatParentBox.left)) {
                    x -= floatParentBox.left;
                }
                if (Ext.isNumber(y) && Ext.isNumber(floatParentBox.top)) {
                    y -= floatParentBox.top;
                }
            } else {
                p = me.el.translateXY(x, y);
                x = p.x;
                y = p.y;
            }
            me.setPosition(x, y, animate);
        } else {
            p = me.el.translateXY(x, y);
            me.setPosition(p.x, p.y, animate);
        }
        return me;
    },
    /**
     * @member Ext.Component
     * Sets the left and top of the component. To set the page XY position instead,
     * use {@link Ext.Component#setPagePosition setPagePosition}. This method fires the
     * {@link #event-move} event.
     * @param {Number/Number[]/Object} x The new left, an array of `[x,y]`, or animation config
     * object containing `x` and `y` properties.
     * @param {Number} [y] The new top.
     * @param {Boolean/Object} [animate] If `true`, the Component is _animated_ into
     * its new position. You may also pass an animation configuration.
     * @return {Ext.Component} this
     */
    setPosition: function(x, y, animate) {
        var me = this,
            pos = me.beforeSetPosition.apply(me, arguments);
        if (pos && me.rendered) {
            x = pos.x;
            y = pos.y;
            if (animate) {
                // Proceed only if the new position is different from the current
                // one. We only do these DOM reads in the animate case as we don't
                // want to incur the penalty of read/write on every call to setPosition
                if (x !== me.getLocalX() || y !== me.getLocalY()) {
                    me.stopAnimation();
                    me.animate(Ext.apply({
                        duration: 1000,
                        listeners: {
                            afteranimate: Ext.Function.bind(me.afterSetPosition, me, [
                                x,
                                y
                            ])
                        },
                        to: {
                            // Use local coordinates for a component
                            // We don't need to normalize this for RTL, the anim.target.Component
                            // calls setPosition, which will normalize the x value to right when
                            // it's necessary
                            left: x,
                            top: y
                        }
                    }, animate));
                }
            } else {
                me.setLocalXY(x, y);
                me.afterSetPosition(x, y);
            }
        }
        return me;
    },
    /**
     * Sets the "x" scroll position for this component.  Only applicable for
     * {@link #scrollable} components
     * @param {Number} x
     * @param {Boolean/Object} [animate] true for the default animation or a standard Element
     * animation config object
     */
    setScrollX: function(x, animate) {
        var scroller = this.getScrollable();
        if (scroller) {
            scroller.scrollTo(x, null, animate);
        }
    },
    /**
     * Sets the "y" scroll position for this component.  Only applicable for
     * {@link #scrollable} components
     * @param {Number} y
     * @param {Boolean/Object} [animate] true for the default animation or a standard Element
     * animation config object
     */
    setScrollY: function(y, animate) {
        var scroller = this.getScrollable();
        if (scroller) {
            scroller.scrollTo(null, y, animate);
        }
    },
    /**
     * Sets the width and height of this Component. This method fires the {@link #resize} event.
     * This method can accept either width and height as separate arguments, or you can pass
     * a size object like `{ width:10, height:20 }`.
     *
     * @param {Number/String/Object} width The new width to set. This may be one of:
     *
     *   - A Number specifying the new width in pixels.
     *   - A String used to set the CSS width style.
     *   - A size object in the format `{width: widthValue, height: heightValue}`.
     *   - `undefined` to leave the width unchanged.
     *
     * @param {Number/String} height The new height to set (not required if a size object is passed
     * as the first arg). This may be one of:
     *
     *   - A Number specifying the new height in pixels.
     *   - A String used to set the CSS height style. Animation may **not** be used.
     *   - `undefined` to leave the height unchanged.
     *
     * @return {Ext.Component} this
     */
    setSize: function(width, height) {
        var me = this,
            oldWidth = me.width,
            oldHeight = me.height,
            widthIsString, heightIsString;
        // support for standard size objects
        if (width && typeof width === 'object') {
            height = width.height;
            width = width.width;
        }
        // Constrain within configured maximum
        if (typeof width === 'number') {
            me.width = Ext.Number.constrain(width, me.minWidth, me.maxWidth);
        } else if (width === null) {
            delete me.width;
        } else if (typeof width === 'string') {
            // handle width specified as a string css style
            widthIsString = true;
            me.width = width;
        }
        if (typeof height === 'number') {
            me.height = Ext.Number.constrain(height, me.minHeight, me.maxHeight);
        } else if (height === null) {
            delete me.height;
        } else if (typeof height === 'string') {
            // handle width specified as a string css style
            heightIsString = true;
            me.height = height;
        }
        // If not rendered, all we need to is set the properties.
        // The initial layout will set the size
        if (me.rendered && me.isVisible()) {
            if (oldWidth !== me.width || oldHeight !== me.height) {
                if (me.liquidLayout || widthIsString || heightIsString) {
                    // if we have a liquid layout we must setSize now, since the following
                    // updateLayout call will not set our size in the dom if we successfully
                    // opt out of the layout run
                    me.el.setSize(me.width, me.height);
                }
                // If we are changing size, then we are not the root.
                me.updateLayout(me._notAsLayoutRoot);
            }
        }
        return me;
    },
    /**
     * Sets the style for this Component's primary element.
     *
     * Styles should be a valid DOM element style property.
     * [Valid style property names](http://www.w3schools.com/jsref/dom_obj_style.asp)
     * (_along with the supported CSS version for each_)
     *
     *     var name = Ext.create({
     *         xtype: 'component',
     *         renderTo: Ext.getBody(),
     *         html: 'Phineas Flynn'
     *     });
     *
     *     // two-param syntax
     *     name.setStyle('color', 'white');
     *
     *     // single-param syntax
     *     name.setStyle({
     *         fontWeight: 'bold',
     *         backgroundColor: 'gray',
     *         padding: '10px'
     *     });
     *
     * @param {String/Object} property The style property to be set, or an object of
     * multiple styles.
     * @param {String} [value] The value to apply to the given property, or null if an
     * object was passed.
     * @return {Ext.Component} this
     */
    setStyle: function(property, value) {
        var el = this.el || this.protoEl;
        el.setStyle(property, value);
        return this;
    },
    /**
     * Sets the UI for the component. This will remove any existing UIs on the component.
     * It will also loop through any `uiCls` set on the component and rename them so they include
     * the new UI.
     * @param {String} ui The new UI for the component.
     */
    setUI: function(ui) {
        var me = this,
            uiCls = me.uiCls,
            activeUI = me.activeUI,
            classes;
        if (ui === activeUI) {
            // The ui hasn't changed
            return;
        }
        // activeUI will only be set if setUI has been called before.
        // If it hasn't there's no need to remove anything
        if (activeUI) {
            classes = me.removeClsWithUI(uiCls, true);
            if (classes.length) {
                me.removeCls(classes);
            }
            // Remove the UI from the element
            me.removeUIFromElement();
        } else {
            // We need uiCls to be empty otherwise our call to addClsWithUI won't do anything
            me.uiCls = [];
        }
        // Set the UI
        me.ui = ui;
        // After the first call to setUI the values ui and activeUI should track each other
        // but initially we need some way to tell whether the ui has really been set.
        me.activeUI = ui;
        // Add the new UI to the element
        me.addUIToElement();
        classes = me.addClsWithUI(uiCls, true);
        if (classes.length) {
            me.addCls(classes);
        }
        // Changing the ui can lead to significant changes to a component's appearance,
        // so the layout needs to be updated. Internally most calls to setUI are pre-render.
        // Buttons are a notable exception as setScale changes the ui and often requires
        // the layout to be updated.
        if (me.rendered) {
            me.updateLayout();
        }
    },
    /**
     * Convenience function to hide or show this component by Boolean.
     * @param {Boolean} visible `true` to show, `false` to hide.
     * @return {Ext.Component} this
     * @since 1.1.0
     */
    setVisible: function(visible) {
        return this[visible ? 'show' : 'hide']();
    },
    /**
     * Sets the hidden state of this component. This is basically the same as
     * `{@link #setVisible}` but the boolean parameter has the opposite meaning.
     * @param {Boolean} hidden
     * @return {Ext.Component}
     */
    setHidden: function(hidden) {
        return this.setVisible(!hidden);
    },
    /**
     * Sets the width of the component. This method fires the {@link #resize} event.
     *
     * @param {Number} width The new width to set. This may be one of:
     *
     *   - A Number specifying the new width in pixels.
     *   - A String used to set the CSS width style.
     *   - `undefined` to leave the width unchanged.
     *   - `null` to clear the width.
     *
     * @return {Ext.Component} this
     */
    setWidth: function(width) {
        return this.setSize(width);
    },
    /**
     * Shows this Component, rendering it first if {@link #autoRender} or {@link #cfg-floating}
     * are `true`.
     *
     * After being shown, a {@link #cfg-floating} Component (such as a {@link Ext.window.Window}),
     * is activated it and brought to the front of its {@link #zIndexManager z-index stack}.
     *
     * @param {String/Ext.dom.Element} [animateTarget=null] **only valid for {@link #cfg-floating}
     * Components such as {@link Ext.window.Window Window}s or {@link Ext.tip.ToolTip ToolTip}s,
     * or regular Components which have been configured with `floating: true`.** The target
     * from which the Component should animate from while opening.
     * @param {Function} [callback] A callback function to call after the Component is displayed.
     * Only necessary if animation was specified.
     * @param {Object} [scope] The scope (`this` reference) in which the callback is executed.
     * Defaults to this Component.
     * @return {Ext.Component} this
     */
    show: function(animateTarget, callback, scope) {
        var me = this,
            rendered = me.rendered,
            container = me.ownerFocusableContainer;
        if (me.hierarchicallyHidden || (me.floating && !rendered && me.isHierarchicallyHidden())) {
            // If this is a hierarchically hidden floating component, we need to stash
            // the arguments to this call so that the call can be deferred until the next
            // time syncHidden() is called.
            if (!rendered) {
                // If the component has not yet been rendered it requires special treatment.
                // Normally, for rendered components we can just set the pendingShow property
                // and syncHidden() listens to events in the hierarchyEventSource and calls
                // show() when this component becomes hierarchically visible.  However,
                // if the component has not yet been rendered the hierarchy event listeners
                // have not yet been attached (since Floating is initialized during the
                // render phase.  This means we have to initialize the hierarchy event
                // listeners right now to ensure that the component will show itself when
                // it becomes hierarchically visible.
                me.initHierarchyEvents();
            }
            // defer the show call until next syncHidden(), but ignore animateTarget.
            if (arguments.length > 1) {
                arguments[0] = null;
                me.pendingShow = arguments;
            } else {
                me.pendingShow = true;
            }
        } else if (rendered && me.isVisible()) {
            if (me.floating) {
                me.onFloatShow();
                // onFloatShow does not toFront this Component
                // because that is done by our ZIndexManager in response to the global
                // show event fired from afterShow.
                // Since we will not execute afterShow if already visible,
                // maintain behaviour by explicitly toFronting here.
                if (me.toFrontOnShow) {
                    me.toFront();
                }
            }
        } else {
            if (me.fireEvent('beforeshow', me) !== false) {
                me.hidden = false;
                delete this.getInherited().hidden;
                // Render on first show if there is an autoRender config, or if this
                // is a floater (Window, Menu, BoundList etc).
                if (container && !container.beforeFocusableChildShow.$nullFn) {
                    container.beforeFocusableChildShow(me);
                }
                // We suspend layouts here because floaters/autoRenders
                // will layout when onShow is called. If the render succeeded,
                // the layout will be trigger inside onShow, so we don't flush
                // in the first block. If, for some reason we couldn't render, then
                // we resume layouts and force a flush because we don't know if something
                // will force it.
                Ext.suspendLayouts();
                if (!rendered && (me.autoRender || me.floating)) {
                    me.doAutoRender();
                    rendered = me.rendered;
                }
                if (rendered) {
                    me.beforeShow();
                    Ext.resumeLayouts();
                    me.onShow.apply(me, arguments);
                    me.afterShow.apply(me, arguments);
                } else {
                    Ext.resumeLayouts(true);
                }
            } else {
                me.onShowVeto();
            }
        }
        return me;
    },
    /**
     * Displays component at specific xy position.
     * A floating component (like a menu) is positioned relative to its ownerCt if any.
     * Useful for popping up a context menu:
     *
     *     listeners: {
     *         itemcontextmenu: function(view, record, item, index, event, options) {
     *             Ext.create('Ext.menu.Menu', {
     *                 width: 100,
     *                 height: 100,
     *                 margin: '0 0 10 0',
     *                 items: [{
     *                     text: 'regular item 1'
     *                 },{
     *                     text: 'regular item 2'
     *                 },{
     *                     text: 'regular item 3'
     *                 }]
     *             }).showAt(event.getXY());
     *         }
     *     }
     *
     * @param {Number/Number[]} x The new x position or array of `[x,y]`.
     * @param {Number} [y] The new y position
     * @param {Boolean/Object} [animate] True to animate the Component into its new position.
     * You may also pass an animation configuration.
     * @return {Ext.Component} this
     */
    showAt: function(x, y, animate) {
        var me = this;
        // Not rendered, then animating to a position is meaningless,
        // just set the x,y position and allow show's processing to work.
        if (!me.rendered && (me.autoRender || me.floating)) {
            me.x = x;
            me.y = y;
            return me.show();
        }
        if (me.floating) {
            me.setPosition(x, y, animate);
        } else {
            me.setPagePosition(x, y, animate);
        }
        return me.show();
    },
    /**
     * Shows this component by the specified {@link Ext.Component Component} or
     * {@link Ext.dom.Element Element}. Used when this component is {@link #cfg-floating}.
     * @param {Ext.Component/Ext.dom.Element} component The {@link Ext.Component} or
     * {@link Ext.dom.Element} to show the component by.
     * @param {String} [position] Alignment position as used by
     * {@link Ext.util.Positionable#getAlignToXY}. Defaults to `{@link #defaultAlign}`.
     * See {@link #alignTo} for possible values.
     * @param {Number[]} [offset] Alignment offsets as used by
     * {@link Ext.util.Positionable#getAlignToXY}. See {@link #alignTo} for possible values.
     * @return {Ext.Component} this
     */
    showBy: function(component, position, offset) {
        var me = this;
        if (!me.floating) {
            Ext.log.warn('Using showBy on a non-floating component');
        }
        if (me.floating && component) {
            me._lastAlignTarget = component;
            me._lastAlignToPos = position || me.defaultAlign;
            me._lastAlignToOffsets = offset || me.alignOffset;
            me.show();
        }
        return me;
    },
    suspendLayouts: function() {
        var me = this;
        if (!me.rendered) {
            return;
        }
        if (++me.layoutSuspendCount === 1) {
            me.suspendLayout = true;
        }
    },
    /**
     * Toggles the specified CSS class on this component (removes it if it already exists,
     * otherwise adds it).
     * @param {String} className The CSS class to toggle.
     * @param {Boolean} [state] If specified as `true`, causes the class to be added. If
     * specified as `false`, causes the class to be removed.
     * @return {Ext.Component} Returns the Component to allow method chaining.
     * @chainable
     */
    toggleCls: function(className, state) {
        if (state === undefined) {
            state = !this.hasCls(className);
        }
        return this[state ? 'addCls' : 'removeCls'](className);
    },
    unitizeBox: function(box) {
        return Ext.Element.unitizeBox(box);
    },
    /**
     * Removes the mask applied by {@link #mask}
     */
    unmask: function() {
        (this.getMaskTarget() || this.el).unmask();
        this.setMasked(false);
    },
    unregisterFloatingItem: function(cmp) {
        var me = this;
        if (me.floatingDescendants) {
            me.floatingDescendants.unregister(cmp);
        }
    },
    /**
     * Navigates up the ownership hierarchy searching for an ancestor Container which matches
     * any passed selector or component.
     *
     * *Important.* There is not a universal upwards navigation pointer. There are several upwards
     * relationships such as the {@link Ext.button.Button button} which activates
     * a {@link Ext.button.Button#cfg-menu menu}, or the {@link Ext.menu.Item menu item}
     * which activated a {@link Ext.menu.Item#cfg-menu submenu}, or the
     * {@link Ext.grid.column.Column column header} which activated the column menu.
     *
     * These differences are abstracted away by this method.
     *
     * Example:
     *
     *     var owningTabPanel = grid.up('tabpanel');
     *
     * @param {String/Ext.Component} [selector] The selector component or actual component to test.
     * If not passed the immediate owner/activator is returned.
     * @param {String/Number/Ext.Component} [limit] This may be a selector upon which to stop
     * the upward scan, or a limit of the number of steps, or Component reference to stop on.
     * @return {Ext.container.Container} The matching ancestor Container (or `undefined` if no match
     * was found).
     */
    up: function(selector, limit) {
        var result = this.getRefOwner(),
            limitSelector = typeof limit === 'string',
            limitCount = typeof limit === 'number',
            limitComponent = limit && limit.isComponent,
            steps = 0;
        if (selector) {
            for (; result; result = result.getRefOwner()) {
                if (result.destroyed) {
                    return null;
                }
                steps++;
                if (selector.isComponent) {
                    if (result === selector) {
                        return result;
                    }
                } else {
                    if (Ext.ComponentQuery.is(result, selector)) {
                        return result;
                    }
                }
                // Stop when we hit the limit selector
                if (limitSelector && result.is(limit)) {
                    return;
                }
                if (limitCount && steps === limit) {
                    return;
                }
                if (limitComponent && result === limit) {
                    return;
                }
            }
        }
        return result;
    },
    /**
     * Update the content area of a component.
     * @param {String/Object} htmlOrData If this component has been configured with a
     * template via the tpl config then it will use this argument as data to populate the
     * template. If this component was not configured with a template, the components
     * content area will be updated via Ext.Element update.
     * @param {Boolean} [loadScripts=false] Only legitimate when using the `html`
     * configuration. Causes embedded script tags to be executed. Inline source will be executed
     * with this Component as the scope (`this` reference).
     * @param {Function} [callback] Only legitimate when using the `html` configuration.
     * Callback to execute when scripts have finished loading.
     * @param {Object} [scriptScope=`this`] The scope (`this` reference) in which to
     * execute *inline* script elements content. Scripts with a `src` attribute cannot
     * be executed with this scope.
     *
     * @since 3.4.0
     */
    update: function(htmlOrData, loadScripts, callback, scriptScope) {
        var me = this,
            isData = (me.tpl && !Ext.isString(htmlOrData)),
            container = me.ownerFocusableContainer,
            sizeModel, doLayout, el;
        if (isData) {
            me.data = (htmlOrData && htmlOrData.isEntity) ? htmlOrData.getData(true) : htmlOrData;
        } else {
            me.html = Ext.isObject(htmlOrData) ? Ext.DomHelper.markup(htmlOrData) : htmlOrData;
        }
        if (me.rendered) {
            sizeModel = me.getSizeModel();
            doLayout = sizeModel.width.shrinkWrap || sizeModel.height.shrinkWrap;
            if (me.isContainer) {
                el = me.layout.getRenderTarget();
                // If we are a non-empty container being updated with raw content we have to lay out
                doLayout = doLayout || me.items.items.length > 0;
            } else {
                el = me.getTargetEl();
            }
            if (isData) {
                me.tpl[me.tplWriteMode](el, me.data || {});
            } else {
                el.setHtml(me.html, loadScripts, callback, scriptScope || me);
            }
            if (doLayout) {
                me.updateLayout();
            }
            if (container && !container.onFocusableChildUpdate.$nullFn) {
                container.onFocusableChildUpdate(me);
            }
        }
    },
    /**
     * Update the content area of a component.
     * @param {String/Object} html If this component has been configured with a
     * template via the tpl config then it will use this argument as data to populate the
     * template. If this component was not configured with a template, the components
     * content area will be updated via Ext.Element update.
     * @param {Boolean} [loadScripts=false] Only legitimate when using the `html`
     * configuration. Causes embedded script tags to be executed. Inline source will be executed
     * with this Component as the scope (`this` reference).
     * @param {Function} [callback] Only legitimate when using the `html` configuration.
     * Callback to execute when scripts have finished loading.
     * @param {Object} [scriptScope='this'] The scope (`this` reference) in which to
     * execute *inline* script elements content. Scripts with a `src` attribute cannot
     * be executed with this scope.
     *
     * @since 3.4.0
     */
    setHtml: function(html, loadScripts, callback, scriptScope) {
        this.update(html, loadScripts, null, scriptScope);
    },
    applyData: function(data) {
        // Don't return data here, update will set this.data
        this.update(data);
    },
    /**
     * Sets the current box measurements of the component's underlying element.
     * @param {Object} box An object in the format {x, y, width, height}
     * @return {Ext.Component} this
     */
    updateBox: function(box) {
        this.setSize(box.width, box.height);
        this.setPagePosition(box.x, box.y);
        return this;
    },
    _asLayoutRoot: {
        isRoot: true
    },
    _notAsLayoutRoot: {
        isRoot: false
    },
    /**
     * Updates this component's layout. If this update affects this components {@link #ownerCt},
     * that component's `updateLayout` method will be called to perform the layout instead.
     * Otherwise, just this component (and its child items) will layout.
     *
     * @param {Object} [options] An object with layout options.
     * @param {Boolean} options.defer `true` if this layout should be deferred.
     * @param {Boolean} options.isRoot `true` if this layout should be the root of the layout.
     */
    updateLayout: function(options) {
        var me = this,
            defer,
            lastBox = me.lastBox,
            isRoot = options && options.isRoot,
            context = options && options.context;
        if (lastBox) {
            // remember that this component's last layout result is invalid and must be
            // recalculated
            lastBox.invalid = true;
        }
        if (!me.rendered || me.isDetached || me.layoutSuspendCount || me.suspendLayout) {
            return;
        }
        if (me.hidden) {
            Ext.Component.cancelLayout(me);
        } else if (typeof isRoot !== 'boolean') {
            isRoot = me.isLayoutRoot();
        }
        // if we aren't the root, see if our ownerLayout will handle it...
        if (isRoot || !me.ownerLayout || !me.ownerLayout.onContentChange(me, context)) {
            // either we are the root or our ownerLayout doesn't care
            if (!me.isLayoutSuspended()) {
                // we aren't suspended (knew that), but neither is any of our ownerCt's...
                defer = (options && options.hasOwnProperty('defer')) ? options.defer : me.deferLayouts;
                Ext.Component.updateLayout(me, defer);
            }
        }
    },
    updateMaxHeight: function(maxHeight, oldMaxHeight) {
        this.changeConstraint(maxHeight, oldMaxHeight, 'min', 'max-height', 'height');
    },
    updateMaxWidth: function(maxWidth, oldMaxWidth) {
        this.changeConstraint(maxWidth, oldMaxWidth, 'min', 'max-width', 'width');
    },
    updateMinHeight: function(minHeight, oldMinHeight) {
        this.changeConstraint(minHeight, oldMinHeight, 'max', 'min-height', 'height');
    },
    updateMinWidth: function(minWidth, oldMinWidth) {
        this.changeConstraint(minWidth, oldMinWidth, 'max', 'min-width', 'width');
    },
    updateTouchAction: function(touchAction) {
        var name, childEl, value, hasRootActions;
        for (name in touchAction) {
            childEl = this[name];
            value = touchAction[name];
            if (childEl && childEl.isElement) {
                childEl.setTouchAction(value);
            } else {
                hasRootActions = true;
            }
        }
        if (hasRootActions) {
            this.el.setTouchAction(touchAction);
        }
    },
    // ***********************************************************************************
    // End Component methods
    // ***********************************************************************************
    // </editor-fold>
    // <editor-fold desc="Positionable Methods">
    // ***********************************************************************************
    // Begin Positionable methods
    // ***********************************************************************************
    getAnchorToXY: function(el, anchor, local, mySize) {
        return el.getAnchorXY(anchor, local, mySize);
    },
    getBorderPadding: function() {
        return this.el.getBorderPadding();
    },
    getLocalX: function() {
        return this.el.getLocalX();
    },
    getLocalXY: function() {
        return this.el.getLocalXY();
    },
    getLocalY: function() {
        return this.el.getLocalY();
    },
    getX: function() {
        return this.el.getX();
    },
    getXY: function() {
        return this.el.getXY();
    },
    getY: function() {
        return this.el.getY();
    },
    setLocalX: function(x) {
        this.el.setLocalX(x);
    },
    setLocalXY: function(x, y) {
        this.el.setLocalXY(x, y);
    },
    setLocalY: function(y) {
        this.el.setLocalY(y);
    },
    setX: function(x, animate) {
        this.el.setX(x, animate);
    },
    setXY: function(xy, animate) {
        this.el.setXY(xy, animate);
    },
    setY: function(y, animate) {
        this.el.setY(y, animate);
    },
    // ***********************************************************************************
    // End Positionable methods
    // ***********************************************************************************
    // </editor-fold>
    privates: {
        addOverCls: function() {
            var me = this;
            if (!me.disabled) {
                me.el.addCls(me.overCls);
            }
        },
        /**
         * Method which adds a specified UI to the components element.
         * @private
         */
        addUIToElement: function() {
            var me = this,
                baseClsUI = me.baseCls + '-' + me.ui,
                childEls, childElName, el, suffix;
            me.addCls(baseClsUI);
            if (me.rendered && me.frame && !Ext.supports.CSS3BorderRadius) {
                // Loop through each frame element, and if they are defined add the ui:
                baseClsUI += '-';
                childEls = me.getChildEls();
                for (childElName in childEls) {
                    suffix = childEls[childElName].frame;
                    if (suffix && suffix !== true) {
                        el = me[childElName];
                        if (el) {
                            el.addCls(baseClsUI + suffix);
                        }
                    }
                }
            }
        },
        /**
         * @private
         */
        changeConstraint: function(newValue, oldValue, constrainMethod, styleName, sizeName) {
            var me = this,
                size = me[sizeName];
            if (newValue != null && typeof size === 'number') {
                me[sizeName] = Math[constrainMethod](size, newValue);
            }
            if (me.liquidLayout) {
                // components that use liquidLayout need their size constraints set in the dom
                if (newValue != null) {
                    me.setStyle(styleName, newValue + 'px');
                } else if (oldValue) {
                    me.setStyle(styleName, '');
                }
            }
            if (me.rendered) {
                me.updateLayout();
            }
        },
        /**
         * @param {String/Object} plugin string or config object containing a ptype property.
         *
         * Constructs a plugin according to the passed config object/ptype string.
         *
         * Ensures that the constructed plugin always has a `cmp` reference back to this component.
         * The setting up of this is done in PluginManager. The PluginManager ensures that
         * a reference to this component is passed to the constructor. It also ensures that
         * the plugin's `setCmp` method (if any) is called.
         * @private
         */
        constructPlugin: function(plugin) {
            var me = this,
                PM = Ext.plugin.Manager;
            // ptype only, pass as the defultType
            if (typeof plugin === 'string') {
                plugin = PM.create({}, plugin, me);
            } else // Object (either config with ptype or an instantiated plugin)
            {
                plugin = PM.create(plugin, null, me);
            }
            return plugin;
        },
        /**
         * Returns an array of fully constructed plugin instances. This converts any configs
         * into their appropriate instances.
         *
         * It does not mutate the plugins array. It creates a new array.
         * @private
         */
        constructPlugins: function() {
            var me = this,
                plugins = me.plugins,
                result, i, len;
            if (plugins) {
                me.plugins = result = Ext.plugin.Abstract.decode(plugins, 'ptype');
                // The processed flag indicates that the plugins have been constructed.
                // This is usually done at construction time, so if at initComponent time,
                // there is a non-zero array of plugins which does NOT have the processed flag,
                // it needs to be processed again.
                result.processed = true;
                for (i = 0 , len = result.length; i < len; i++) {
                    // this just returns already-constructed plugin instances...
                    result[i] = me.constructPlugin(result[i]);
                }
            }
            me.pluginsInitialized = true;
            return result;
        },
        detachFromBody: function() {
            // Also see reattachToBody
            var me = this;
            if (!me.isDetached) {
                Ext.getDetachedBody().appendChild(me.el, true);
                Ext.Component.cancelLayout(me);
                me.isDetached = true;
            }
        },
        doAddListener: function(ename, fn, scope, options, order, caller, manager) {
            var me = this,
                listeners, option, eventOptions, elementName, element, delegate;
            if (Ext.isObject(fn) || (options && options.element)) {
                if (options.element) {
                    elementName = options.element;
                    listeners = {};
                    listeners[ename] = fn;
                    if (scope) {
                        listeners.scope = scope;
                    }
                    eventOptions = me.$elementEventOptions;
                    for (option in options) {
                        if (eventOptions[option]) {
                            listeners[option] = options[option];
                        }
                    }
                } else {
                    listeners = fn;
                    elementName = ename;
                }
                element = me[elementName];
                // can be any kind of observable, not just element
                if (element && element.isObservable) {
                    me.mon(element, listeners);
                } else {
                    me.afterRenderEvents = me.afterRenderEvents || {};
                    if (!me.afterRenderEvents[elementName]) {
                        me.afterRenderEvents[elementName] = [];
                    }
                    me.afterRenderEvents[elementName].push(listeners);
                }
                return;
            }
            if (options) {
                delegate = options.delegate;
                if (delegate) {
                    me.mixins.componentDelegation.addDelegatedListener.call(me, ename, fn, scope, options, order, caller, manager);
                    return;
                }
            }
            me.mixins.observable.doAddListener.call(me, ename, fn, scope, options, order, caller, manager);
        },
        doRemoveListener: function(eventName, fn, scope) {
            var me = this;
            me.mixins.observable.doRemoveListener.call(me, eventName, fn, scope);
            me.mixins.componentDelegation.removeDelegatedListener.call(me, eventName, fn, scope);
        },
        /**
         * This method fires an event on `Ext.GlobalEvents` allowing interested parties to know
         * of certain critical events for this component. This is done globally because the
         * (few) listeners can immediately receive the event rather than bubbling the event
         * only to reach the top and have no listeners.
         *
         * The main usage for these events is to do with floating components. For example, the
         * load mask is a floating component. The component it is masking may be inside several
         * containers. As such, they need to know when component is hidden, either directly, or
         * via a parent container being hidden. To do this they subscribe to these events and
         * filter out the appropriate container.
         *
         * This functionality is contained in Component (as opposed to Container) because a
         * Component can be the ownerCt for a floating component (loadmask), and the loadmask
         * needs to know when its owner is shown/hidden so that its hidden state can be
         * synchronized.
         *
         * @param {String} eventName The event name.
         * @since 4.2.0
         * @private
         */
        fireHierarchyEvent: function(eventName) {
            var globalEvents = Ext.GlobalEvents;
            if (globalEvents.hasListeners[eventName]) {
                globalEvents.fireEvent(eventName, this);
            }
        },
        getActionEl: function() {
            return this.el;
        },
        /**
         * @private
         */
        getAutoId: function() {
            this.autoGenId = true;
            return ++Ext.Component.AUTO_ID;
        },
        /**
         * @private
         */
        getContentTarget: function() {
            return this.el;
        },
        getDragEl: function() {
            return this.el;
        },
        /**
         * Get an el for overflowing, defaults to the target el
         * @private
         */
        getOverflowEl: function() {
            return this.getTargetEl();
        },
        /**
         * @private
         * Returns the CSS style object which will set the Component's scroll styles.
         * This must be applied to the {@link #getTargetEl target element}.
         */
        getOverflowStyle: function() {
            var me = this,
                scroller = me.getScrollable(),
                flags = me._scrollFlags,
                x, y, scrollFlags;
            if (scroller) {
                x = scroller.getX();
                if (x === true) {
                    x = 'auto';
                }
                y = scroller.getY();
                if (y === true) {
                    y = 'auto';
                }
                scrollFlags = flags[x][y];
            } else {
                scrollFlags = flags.none;
            }
            me.scrollFlags = scrollFlags;
            return {
                overflowX: scrollFlags.overflowX,
                overflowY: scrollFlags.overflowY
            };
        },
        /**
         * Returns an array of current fully constructed plugin instances.
         * @private
         */
        getPlugins: function() {
            var plugins = this.plugins;
            plugins = (plugins && plugins.processed) ? plugins : this.constructPlugins();
            return plugins || null;
        },
        getProxy: function() {
            var me = this,
                target;
            if (!me.proxy) {
                target = Ext.getBody();
                me.proxy = me.el.createProxy(Ext.baseCSSPrefix + 'proxy-el', target, true);
            }
            return me.proxy;
        },
        /**
         * This is used to determine where to insert the 'html', 'contentEl' and 'items' in this
         * component.
         * @private
         */
        getTargetEl: function() {
            return this.frameBody || this.el;
        },
        /**
         * @private
         * Needed for when widget is rendered into a grid cell. The class to add to the cell
         * element.
         */
        getTdCls: function() {
            return Ext.baseCSSPrefix + this.getTdType() + '-' + this.ui + '-cell';
        },
        /**
         * @private
         * Partner method to {@link #getTdCls}.
         *
         * Returns the base type for the component. Defaults to return `this.xtype`, but
         * All derived classes of {@link Ext.form.field.Text TextField} can return the type
         * 'textfield', and all derived classes of {@link Ext.button.Button Button} can return
         * the type 'button'
         */
        getTdType: function() {
            return this.xtype;
        },
        /**
         * @private
         */
        getTpl: function(name) {
            Ext.log.warn('getTpl is deprecated, use lookupTpl.');
            return this.lookupTpl(name);
        },
        initCls: function() {
            var me = this,
                cls = [
                    me.baseCls
                ],
                targetCls = me.getComponentLayout().targetCls;
            if (targetCls) {
                cls.push(targetCls);
            }
            if (Ext.isDefined(me.cmpCls)) {
                if (Ext.isDefined(Ext.global.console)) {
                    Ext.global.console.warn('Ext.Component: cmpCls has been deprecated. Please use componentCls.');
                }
                me.componentCls = me.cmpCls;
                delete me.cmpCls;
            }
            if (me.componentCls) {
                cls.push(me.componentCls);
            } else {
                me.componentCls = me.baseCls;
            }
            return cls;
        },
        initDraggable: function() {
            var me = this,
                dragTarget, ddConfig;
            // If we are resizable, and the resizer had to wrap this Component's el (e.g. an Img)
            // Then we have to create a pseudo-Component out of the resizer to drag that,
            // otherwise, we just drag this Component
            dragTarget = (me.resizer && me.resizer.el !== me.el) ? me.resizerComponent = new Ext.Component({
                el: me.resizer.el,
                rendered: true,
                container: me.container
            }) : me;
            ddConfig = Ext.applyIf({
                el: dragTarget.getDragEl(),
                // eslint-disable-next-line max-len
                constrainTo: (me.constrain || me.draggable.constrain) ? (me.constrainTo || (me.floatParent ? me.floatParent.getTargetEl() : me.container)) : undefined
            }, me.draggable);
            // Add extra configs if Component is specified to be constrained
            if (me.constrain || me.constrainDelegate) {
                ddConfig.constrain = me.constrain;
                ddConfig.constrainDelegate = me.constrainDelegate;
            }
            me.dd = new Ext.util.ComponentDragger(dragTarget, ddConfig);
        },
        /**
         * Initializes padding by applying it to the target element, or if the layout manages
         * padding ensures that the padding on the target element is "0".
         * @private
         */
        initPadding: function(targetEl) {
            var me = this,
                padding = me.padding;
            if (padding != null) {
                // eslint-disable-next-line max-len
                if (me.layout && me.layout.managePadding && me.contentPaddingProperty === 'padding') {
                    // If the container layout manages padding, or if a touch scroller is in
                    // use, the padding will be applied to an inner layout element, or the
                    // touch scroller element.  This is done as a workaround for the browser bug
                    // where right and/or bottom padding is lost when the element has overflow.
                    // The assumed intent is for the configured padding to override any padding
                    // that is applied to the target element via style sheet rules.  It is
                    // therefore necessary to set the target element's padding to "0".
                    targetEl.setStyle('padding', 0);
                } else {
                    // Convert the padding, margin and border properties
                    // from a space separated string into a proper style string
                    targetEl.setStyle('padding', this.unitizeBox((padding === true) ? 5 : padding));
                }
            }
        },
        /**
         * @private
         */
        initPlugin: function(plugin) {
            plugin.init(this);
            return plugin;
        },
        initResizable: function(resizable) {
            var me = this;
            resizable = Ext.apply({
                target: me,
                dynamic: false,
                // eslint-disable-next-line max-len
                constrainTo: (me.constrain || (resizable && resizable.constrain)) ? (me.constrainTo || (me.floatParent ? me.floatParent.getTargetEl() : me.container)) : undefined,
                handles: me.resizeHandles
            }, resizable);
            resizable.target = me;
            me.resizer = new Ext.resizer.Resizer(resizable);
        },
        /**
         * Applies padding, margin, border, top, left, height, and width configs to the
         * appropriate elements.
         * @private
         */
        initStyles: function(targetEl) {
            var me = this,
                margin = me.margin,
                border = me.border,
                cls = me.cls,
                style = me.style,
                x = me.x,
                y = me.y,
                liquidLayout = me.liquidLayout,
                width, height;
            me.initPadding(targetEl);
            if (margin != null) {
                targetEl.setStyle('margin', this.unitizeBox((margin === true) ? 5 : margin));
            }
            if (border != null) {
                me.setBorder(border, targetEl);
            }
            // initComponent, beforeRender, or event handlers may have set the style
            // or cls property since the protoEl was set up so we must apply styles
            // and classes here too.
            if (cls && cls !== me.initialCls) {
                targetEl.addCls(cls);
                me.cls = me.initialCls = null;
            }
            if (style && style !== me.initialStyle) {
                targetEl.setStyle(style);
                me.style = me.initialStyle = null;
            }
            if (x != null) {
                targetEl.setStyle(me.horizontalPosProp, (typeof x === 'number') ? (x + 'px') : x);
            }
            if (y != null) {
                targetEl.setStyle('top', (typeof y === 'number') ? (y + 'px') : y);
            }
            if (!me.ownerCt || me.floating) {
                if (Ext.scopeCss) {
                    targetEl.addCls(me.rootCls);
                }
                targetEl.addCls(me.borderBoxCls);
            }
            // Framed components need their width/height to apply to the frame, which is
            // best handled in layout at present. The exception to this rule is component
            // that use liquidLayout and so cannot rely on the layout to set their size.
            if (liquidLayout || !me.getFrameInfo()) {
                width = me.width;
                height = me.height;
                // If we're using the content box model, we also cannot assign
                // numeric initial sizes since we do not know the border widths to subtract
                if (width != null) {
                    if (typeof width === 'number') {
                        targetEl.setStyle('width', width + 'px');
                    } else {
                        targetEl.setStyle('width', width);
                    }
                }
                if (height != null) {
                    if (typeof height === 'number') {
                        targetEl.setStyle('height', height + 'px');
                    } else {
                        targetEl.setStyle('height', height);
                    }
                }
            }
        },
        // Utility method to determine if a Component is floating, and has an owning Container
        // whose coordinate system it must be positioned in when using setPosition.
        isContainedFloater: function() {
            return (this.floating && this.floatParent);
        },
        isDescendant: function(ancestor) {
            var c;
            if (ancestor.isContainer) {
                for (c = this.ownerCt; c; c = c.ownerCt) {
                    if (c === ancestor) {
                        return true;
                    }
                }
            }
            return false;
        },
        /**
         * @private
         * Returns `true` if the passed element is within the container tree of this component.
         *
         * For example if a menu's submenu contains an {@link Ext.form.field.Date}, that top level
         * menu owns the elements of the date picker. Using this method, you can tell if an event
         * took place within a certain component tree.
         */
        owns: function(element) {
            var result = false,
                cmp;
            if (element.isEvent) {
                element = element.target;
            } else if (element.isElement) {
                element = element.dom;
            }
            cmp = Ext.Component.fromElement(element);
            if (cmp) {
                result = (cmp === this) || (!!cmp.up(this));
            }
            return result;
        },
        parseBox: function(box) {
            return Ext.Element.parseBox(box);
        },
        reattachToBody: function() {
            // Also see detachFromBody
            this.isDetached = false;
        },
        removeManagedListenerItem: function(isClear, managedListener, item, ename, fn, scope) {
            var me = this,
                element = managedListener.options ? managedListener.options.element : null;
            if (element) {
                element = me[element];
                if (element && element.un) {
                    // eslint-disable-next-line max-len
                    if (isClear || (managedListener.item === item && managedListener.ename === ename && (!fn || managedListener.fn === fn) && (!scope || managedListener.scope === scope))) {
                        element.un(managedListener.ename, managedListener.fn, managedListener.scope);
                        if (!isClear) {
                            Ext.Array.remove(me.managedListeners, managedListener);
                        }
                    }
                }
            } else {
                return me.mixins.observable.removeManagedListenerItem.apply(me, arguments);
            }
        },
        removeOverCls: function() {
            this.el.removeCls(this.overCls);
        },
        removePlugin: function(plugin) {
            Ext.Array.remove(this.plugins, plugin);
            plugin.destroy();
        },
        /**
         * Method which removes a specified UI from the components element.
         * @private
         */
        removeUIFromElement: function() {
            var me = this,
                baseClsUI = me.baseCls + '-' + me.ui,
                childEls, childElName, el, suffix;
            me.removeCls(baseClsUI);
            if (me.rendered && me.frame && !Ext.supports.CSS3BorderRadius) {
                // Loop through each frame element, and if they are defined remove the ui:
                baseClsUI += '-';
                childEls = me.getChildEls();
                for (childElName in childEls) {
                    suffix = childEls[childElName].frame;
                    if (suffix && suffix !== true) {
                        el = me[childElName];
                        if (el) {
                            el.removeCls(baseClsUI + suffix);
                        }
                    }
                }
            }
        },
        /**
         * @private
         */
        setComponentLayout: function(layout) {
            var currentLayout = this.componentLayout;
            if (currentLayout && currentLayout.isLayout && currentLayout !== layout) {
                currentLayout.setOwner(null);
            }
            this.componentLayout = layout;
            layout.setOwner(this);
        },
        setHiddenState: function(hidden) {
            var me = this,
                inheritedState = me.getInherited(),
                zIndexManager = me.zIndexManager;
            me.hidden = hidden;
            if (hidden) {
                inheritedState.hidden = true;
            } else {
                delete inheritedState.hidden;
            }
            // Ensure any ZIndexManager knowns about visibility state change
            // to keep its filtering correct
            if (zIndexManager) {
                zIndexManager.onComponentShowHide(me);
            }
        },
        setupProtoEl: function() {
            var cls = this.initCls();
            this.protoEl.addCls(cls);
        },
        wrapPrimaryEl: function(dom) {
            var me = this,
                el = me.el;
            if (!el || !el.isElement) {
                // allow subclass override to instantiate the element
                me.el = Ext.get(dom);
            }
            if (me.floating) {
                this.mixins.floating.constructor.call(this);
            }
        }
    },
    // private
    deprecated: {
        5: {
            methods: {
                /**
                 * @method addClass
                 * @inheritdoc Ext.Component#addCls
                 * @deprecated 4.1 Use {@link #addCls} instead.
                 * @since 2.3.0
                 */
                addClass: 'addCls',
                /**
                 * @method doComponentLayout
                 * This method needs to be called whenever you change something on this
                 * component that equires the Component's layout to be recalculated.
                 * @return {Ext.Component} this
                 * @deprecated 4.1 Use {@link Ext.Component#method-updateLayout} instead.
                 */
                doComponentLayout: function() {
                    this.updateLayout();
                    return this;
                },
                /**
                 * @method removeClass
                 * @inheritdoc Ext.Component#removeCls
                 * @deprecated 4.1 Use {@link #addCls} instead.
                 * @since 2.3.0
                 */
                removeClass: 'removeCls',
                /**
                 * @method forceComponentLayout
                 * @inheritdoc Ext.Component#updateLayout
                 * @deprecated 4.1 Use {@link Ext.Component#method-updateLayout} instead.
                 */
                forceComponentLayout: 'updateLayout',
                /**
                 * @method setDocked
                 * @inheritdoc Ext.Component#setDock
                 * @deprecated 5.0 Use {@link #setDock} instead.
                 */
                setDocked: 'setDock'
            }
        }
    }
}, function(Component) {
    var prototype = Component.prototype;
    // event options for listeners that use the "element" event options must also include
    // event options from Ext.Element
    (prototype.$elementEventOptions = Ext.Object.chain(Ext.Element.prototype.$eventOptions)).element = 1;
    (prototype.$eventOptions = Ext.Object.chain(prototype.$eventOptions)).delegate = 1;
    Component.createAlias({
        on: 'addListener',
        prev: 'previousSibling',
        next: 'nextSibling'
    });
    /**
     * @method resumeLayouts
     * @inheritdoc Ext.Component#static-method-resumeLayouts
     * @member Ext
     */
    Ext.resumeLayouts = function(flush) {
        Component.resumeLayouts(flush);
    };
    /**
     * @method suspendLayouts
     * @inheritdoc Ext.Component#static-method-suspendLayouts
     * @member Ext
     */
    Ext.suspendLayouts = function() {
        Component.suspendLayouts();
    };
    /**
     * @method batchLayouts
     * Utility wrapper that suspends layouts of all components for the duration of a given
     * function.
     * @param {Function} fn The function to execute.
     * @param {Object} [scope] The scope (`this` reference) in which the specified function
     * is executed.
     * @member Ext
     */
    Ext.batchLayouts = function(fn, scope) {
        Component.suspendLayouts();
        // Invoke the function
        // note: try/finally works in IE8 standards mode
        try {
            fn.call(scope);
        } finally {
            Component.resumeLayouts(true);
        }
    };
    /**
     * @method setGlyphFontFamily
     * Sets the default font-family to use for components that support a `glyph` config.
     * @param {String} fontFamily The name of the font-family
     * @member Ext
     */
    Ext.setGlyphFontFamily = function(fontFamily) {
        Ext._glyphFontFamily = fontFamily;
    };
    // These are legacy names which are now subsumed by Ext.GlobalEvents
    Component.hierarchyEventSource = prototype.hierarchyEventSource = Ext.GlobalEvents;
    // High Contrast mode in Windows disables background images and usually
    // causes much pain by this. We feature detect it early and add this class
    // to allow potential workarounds.
    Ext.onReady(function() {
        if (Ext.supports.HighContrastMode) {
            Ext.getBody().addCls(Component.ariaHighContrastModeCls);
        }
    });
});
/* TODO
 *
 * ## Child Sessions
 *
 * By default, sessions created in child components are called "child sessions".
 * All data and edits are copied from the parent session in to the child session.
 * These changes can be written back to the parent session much like a session
 * can save its data to a server.
 *
 *      Ext.create({
 *          xtype: 'window',
 *          modal: true,
 *
 *          // Creates a new session for this Window but that session is linked
 *          // to a parent session (probably from the application's Viewport).
 *          //
 *          session: true,
 *
 *          items: [{
 *              ...
 *          },{
 *              xtype: 'button',
 *              text: 'OK',
 *              listeners: {
 *                  click: function() {
 *                      // Get the Session this component has inherited (from
 *                      // the Window) and save it back to the parent session.
 *                      //
 *                      this.lookupSession().save();
 *                  }
 *              }
 *          }]
 *      });
 *
 * ### Isolated Sessions
 *
 * This connection of a child session to its parent session is determined by the
 * `{@link Ext.data.Session#isolated isolated}` config. If `isolated` is
 * set to `true` then this session will not copy anything from a higher level
 * session and will instead do its own communication with the server.
 *
 *      Ext.create({
 *          xtype: 'window',
 *          modal: true,
 *
 *          // Creates a new session for this Window that is isolated from any
 *          // other sessions. All data for this session will be fetched from
 *          // the server and ultimately saved back to the server.
 *          //
 *          session: {
 *              isolated: true
 *          },
 *
 *          items: [{
 *              ...
 *          },{
 *              xtype: 'button',
 *              text: 'OK',
 *              listeners: {
 *                  click: function() {
 *                      // Get the Session this component has inherited (from
 *                      // the Window) and save it back to the server.
 *                      //
 *                      this.lookupSession().save();
 *                  }
 *              }
 *          }]
 *      });
 */

/**
 * @class Ext.mixin.Focusable
 */
Ext.define('Ext.overrides.mixin.Focusable', {
    override: 'Ext.Component',
    /**
     * @cfg {String} [focusCls='focus'] CSS class suffix that will be used to
     * compose the CSS class name that will be added to Component's {@link #focusClsEl},
     * and removed when Component blurs.
     *
     * **Note** that this is not a full CSS class name; this suffix will be combined
     * with component's UI class via {@link #addClsWithUI} and {@link #removeClsWithUI} methods.
     */
    focusCls: 'focus',
    /**
     * Try to focus this component.
     *
     * If this component is disabled, a close relation will be targeted for focus instead
     * to keep focus localized for keyboard users.
     * @param {Mixed} [selectText] If applicable, `true` to also select all the text in this
     * component, or an array consisting of start and end (defaults to start) position of selection.
     * @param {Boolean/Number} [delay] Delay the focus this number of milliseconds (true for
     * 10 milliseconds).
     * @param {Function} [callback] Only needed if the `delay` parameter is used. A function to call
     * upon focus.
     * @param {Function} [scope] Only needed if the `delay` parameter is used. The scope (`this`
     * reference) in which to execute the callback.
     * @return {Ext.Component} The focused Component. Usually `this` Component. Some Containers may
     * delegate focus to a descendant Component ({@link Ext.window.Window Window}s can do this
     * through their {@link Ext.window.Window#defaultFocus defaultFocus} config option. If this
     * component is disabled, a closely related component will be focused and that will be returned.
     */
    focus: function(selectText, delay, callback, scope) {
        var me = this,
            containerScrollTop;
        if ((!me.focusable && !me.isContainer) || me.destroyed || me.destroying) {
            return me;
        }
        // If delay is wanted, queue a call to this function.
        if (delay) {
            me.getFocusTask().delay(Ext.isNumber(delay) ? delay : 10, me.focus, me, [
                selectText,
                false,
                callback,
                scope
            ]);
            return me;
        }
        // An immediate focus call must cancel any outstanding delayed focus calls.
        me.cancelFocus();
        if (me.floating && me.container && me.container.dom) {
            containerScrollTop = me.container.dom.scrollTop;
        }
        // Core Focusable method will return true if focusing was attempted
        if (me.mixins.focusable.focus.apply(me, arguments) !== false) {
            if (callback) {
                Ext.callback(callback, scope);
            }
            // Focusing a floating Component brings it to the front of its stack.
            // this is performed by its zIndexManager. Pass preventFocus true to avoid recursion.
            if (me.floating && containerScrollTop !== undefined) {
                me.container.dom.scrollTop = containerScrollTop;
            }
        }
        return me;
    },
    /**
     * Cancel any deferred focus on this component
     * @protected
     */
    cancelFocus: function() {
        var me = this,
            task = me.getFocusTask();
        if (task) {
            task.cancel();
        }
    },
    /**
     * @method
     * Template method to do any pre-blur processing.
     * @protected
     * @param {Ext.event.Event} e The event object
     */
    beforeBlur: Ext.emptyFn,
    /**
     * @method
     * Template method to do any post-blur processing.
     * @protected
     * @param {Ext.event.Event} e The event object
     */
    postBlur: Ext.emptyFn,
    /**
     * @method
     * Template method to do any pre-focus processing.
     * @protected
     * @param {Ext.event.Event} e The event object
     */
    beforeFocus: Ext.emptyFn,
    /**
     * @method
     * Template method to do any post-focus processing.
     * @protected
     * @param {Ext.event.Event} e The event object
     */
    postFocus: Ext.emptyFn,
    onFocusEnter: function(e) {
        var me = this;
        if (me.destroying || me.destroyed) {
            return;
        }
        // Focusing must being a floating component to the front.
        // Only bring to front if this component is not the manager's
        // topmost component (may be a result of focusOnToFront).
        if (me.floating && me !== me.zIndexManager.getActive()) {
            me.toFront(true);
        }
        me.callParent([
            e
        ]);
    },
    destroyFocusable: function() {
        var me = this;
        // Calling cancelFocus() will assign focusTask property,
        // which we don't want during destruction
        if (me.focusTask) {
            me.focusTask.stop(me.focus, me);
        }
        me.callParent();
    },
    privates: {
        addFocusCls: function(e) {
            var me = this,
                focusCls = me.focusCls,
                el;
            if (focusCls) {
                el = me.getFocusClsEl(e);
                if (el) {
                    el.addCls(me.addClsWithUI(focusCls, true));
                }
            }
        },
        removeFocusCls: function(e) {
            var me = this,
                focusCls = me.focusCls,
                el;
            if (focusCls) {
                el = me.getFocusClsEl(e);
                if (el) {
                    el.removeCls(me.removeClsWithUI(focusCls, true));
                }
            }
        },
        /**
         * @private
         */
        getFocusTask: function() {
            if (!this.focusTask) {
                this.focusTask = Ext.focusTask;
            }
            return this.focusTask;
        },
        updateMaskState: function(state, mask) {
            var me = this,
                ariaEl = me.ariaEl.dom,
                skipMask = me.getInherited().disabled && me.getInherited().disableMask,
                value;
            if (state) {
                me.disableTabbing();
                if (!skipMask) {
                    me.setMasked(true);
                }
                if (ariaEl) {
                    ariaEl.setAttribute('aria-busy', 'true');
                    // It is possible that ariaEl already has aria-describedby attribute;
                    // in that case we need to save it to restore later.
                    value = ariaEl.getAttribute('aria-describedby');
                    if (value) {
                        me._savedAriaDescribedBy = value;
                    }
                    ariaEl.setAttribute('aria-describedby', mask.ariaEl.id);
                }
            } else {
                me.enableTabbing();
                if (!skipMask) {
                    me.setMasked(false);
                }
                if (ariaEl) {
                    ariaEl.removeAttribute('aria-busy');
                    value = ariaEl.getAttribute('aria-describedby');
                    ariaEl.removeAttribute('aria-describedby');
                    if (value === mask.ariaEl.id && me._savedAriaDescribedBy) {
                        ariaEl.setAttribute('aria-describedby', me._savedAriaDescribedBy);
                        delete me._savedAriaDescribedBy;
                    }
                }
            }
        }
    }
}, function() {
    // One global DelayedTask to assign focus
    // So that the last focus call wins.
    if (!Ext.focusTask) {
        Ext.focusTask = new Ext.util.DelayedTask();
    }
});

/**
 * This is an override, not a class. To use `Ext.Responsive` you simply require it:
 *
 *      Ext.application({
 *          requires: [
 *              'Ext.Responsive'
 *          ],
 *
 *          // ...
 *      });
 *
 * Once required, this override mixes in {@link Ext.mixin.Responsive} into `Ext.Component`
 * and `Ext.Widget` so that these classes both gain the
 * {@link Ext.mixin.Responsive#cfg!responsiveConfig responsiveConfig} and
 * {@link Ext.mixin.Responsive#cfg!responsiveFormulas responsiveFormulas} configs.
 * @since 6.7.0
 */
Ext.define('Ext.Responsive', {
    override: 'Ext.Component',
    mixins: [
        'Ext.mixin.Responsive'
    ],
    requires: [
        // Also an override, so it will drop out if Ext.Widget isn't used
        'Ext.ResponsiveWidget'
    ]
});

/**
 * This override provides extra, border layout specific methods for `Ext.Component`. The
 * `Ext.layout.container.Border` class requires this override so that the added functions
 * are only included in a build when `border` layout is used.
 */
Ext.define('Ext.layout.container.border.Region', {
    override: 'Ext.Component',
    /**
     * This method is called by the `Ext.layout.container.Border` class when instances are
     * added as regions to the layout. Since it is valid to add any component to a border
     * layout as a region, this method must be added to `Ext.Component` but is only ever
     * called when that component is owned by a `border` layout.
     * @private
     */
    initBorderRegion: function() {
        var me = this;
        if (!me._borderRegionInited) {
            me._borderRegionInited = true;
            // Now that border regions can have dynamic region/weight, these need to be
            // saved to state as they change:
            me.addStateEvents([
                'changeregion',
                'changeweight'
            ]);
            // We override getState on the instance for a couple reasons. Firstly, since
            // there are very few border regions in an application, the overhead here is
            // not a concern. Secondly, if this method were on the prototype it would
            // impact all components.
            Ext.override(me, {
                getState: function() {
                    var state = me.callParent();
                    // Now that border regions can have dynamic region/weight,
                    // these need to be saved to state:
                    state = me.addPropertyToState(state, 'region');
                    state = me.addPropertyToState(state, 'weight');
                    return state;
                }
            });
        }
    },
    /**
     * Returns the owning container if that container uses `border` layout. Otherwise
     * this method returns `null`.
     * @return {Ext.container.Container} The owning border container or `null`.
     * @private
     */
    getOwningBorderContainer: function() {
        var layout = this.getOwningBorderLayout();
        return layout && layout.owner;
    },
    /**
     * Returns the owning `border` (`Ext.layout.container.Border`) instance if there is
     * one. Otherwise this method returns `null`.
     * @return {Ext.layout.container.Border} The owning border layout or `null`.
     * @private
     */
    getOwningBorderLayout: function() {
        // the ownerLayot (if set) may or may not be a border layout
        var layout = this.ownerLayout;
        return (layout && layout.isBorderLayout) ? layout : null;
    },
    /**
     * This method changes the `region` config property for this border region. This is
     * only valid if this component is in a `border` layout (`Ext.layout.container.Border`).
     * @param {String} region The new `region` value (`"north"`, `"south"`, `"east"` or
     * `"west"`).
     * @return {String} The previous value of the `region` property.
     */
    setRegion: function(region) {
        var me = this,
            borderLayout,
            old = me.region;
        if (typeof region !== 'string') {
            // This method used to be basically the same as setBox, so check for an
            // accidental use of the old signature.
            Ext.raise('Use setBox to set the size or position of a component.');
        }
        if (region !== old) {
            borderLayout = me.getOwningBorderLayout();
            if (borderLayout) {
                // eslint-disable-next-line vars-on-top, one-var
                var regionFlags = borderLayout.regionFlags[region],
                    placeholder = me.placeholder,
                    splitter = me.splitter,
                    owner = borderLayout.owner,
                    regionMeta = borderLayout.regionMeta,
                    collapsed = me.collapsed || me.floated,
                    delta, items, index;
                if (me.fireEventArgs('beforechangeregion', [
                    me,
                    region
                ]) === false) {
                    return old;
                }
                Ext.suspendLayouts();
                me.region = region;
                Ext.apply(me, regionFlags);
                if (me.updateCollapseTool) {
                    me.updateCollapseTool();
                }
                if (splitter) {
                    // splitter.region = region; -- we don't set "region" on splitters!
                    Ext.apply(splitter, regionFlags);
                    splitter.updateOrientation();
                    items = owner.items;
                    index = items.indexOf(me);
                    if (index >= 0) {
                        delta = regionMeta[region].splitterDelta;
                        if (items.getAt(index + delta) !== splitter) {
                            // splitter is not where we expect it, so move it there
                            items.remove(splitter);
                            index = items.indexOf(me);
                            // could have changed
                            if (delta > 0) {
                                ++index;
                            }
                            // else, insert at index and splitter will be before the item
                            items.insert(index, splitter);
                        }
                    }
                }
                // Now that the splitter is in the right place in me.items,
                // the layout will fix up the DOM childNode to be at the same
                // index as well.
                if (placeholder) {
                    // The collapsed item is potentially remembering wrong things (for
                    // example, if it was collapsed as a West region and changed to be
                    // North). The only simple answer here is to expand/collapse the
                    // item (w/o animation).
                    if (collapsed) {
                        me.expand(false);
                    }
                    owner.remove(placeholder);
                    me.placeholder = null;
                    // force creation of a new placeholder
                    if (collapsed) {
                        me.collapse(null, false);
                    }
                }
                owner.updateLayout();
                Ext.resumeLayouts(true);
                me.fireEventArgs('changeregion', [
                    me,
                    old
                ]);
            } else {
                me.region = region;
            }
        }
        // maybe not added yet
        return old;
    },
    /**
     * Sets the `weight` config property for this component. This is only valid if this
     * component is in a `border` layout (`Ext.layout.container.Border`).
     * @param {Number} weight The new `weight` value.
     * @return {Number} The previous value of the `weight` property.
     */
    setWeight: function(weight) {
        var me = this,
            ownerCt = me.getOwningBorderContainer(),
            placeholder = me.placeholder,
            old = me.weight;
        if (weight !== old) {
            if (me.fireEventArgs('beforechangeweight', [
                me,
                weight
            ]) !== false) {
                me.weight = weight;
                if (placeholder) {
                    placeholder.weight = weight;
                }
                if (ownerCt) {
                    ownerCt.updateLayout();
                }
                me.fireEventArgs('changeweight', [
                    me,
                    old
                ]);
            }
        }
        return old;
    }
}, function(Component) {
    var proto = Component.prototype;
    // Aliases for v4 compat
    proto.setBorderRegion = proto.setRegion;
    proto.setRegionWeight = proto.setWeight;
});

Ext.define('Ext.overrides.app.domain.Component', {
    override: 'Ext.app.domain.Component',
    requires: [
        'Ext.Component'
    ]
}, function(ComponentDomain) {
    // The core Component domain monitors events on the Ext.Widget class
    // in Ext Components are not widgets so we need to monitor Ext.Component as well.
    ComponentDomain.monitor(Ext.Component);
});

/**
 * This class manages event dispatching for Controllers. The details of connecting classes
 * to this dispatching mechanism is delegated to {@link Ext.app.EventDomain} instances.
 *
 * @private
 */
Ext.define('Ext.app.EventBus', {
    singleton: true,
    requires: [
        'Ext.app.domain.Component'
    ],
    constructor: function() {
        var me = this,
            domains = Ext.app.EventDomain.instances;
        me.callParent();
        me.domains = domains;
        me.bus = domains.component.bus;
    },
    // compat
    /**
     * Adds a set of component event listeners for a controller. To work with event domains
     * other than component, see {@link #listen}.
     *
     * @param {Object} selectors Config object containing selectors and listeners.
     * @param {Ext.app.BaseController} controller The listening controller instance.
     */
    control: function(selectors, controller) {
        return this.domains.component.listen(selectors, controller);
    },
    /**
     * Adds a set of event domain listeners for a controller. For more information on event
     * domains, see {@link Ext.app.EventDomain} and {@link Ext.app.BaseController}.
     *
     * @param {Object} to Config object containing domains, selectors and listeners.
     * @param {Ext.app.BaseController} controller The listening controller instance.
     */
    listen: function(to, controller) {
        var domains = this.domains,
            domain;
        for (domain in to) {
            if (to.hasOwnProperty(domain)) {
                domains[domain].listen(to[domain], controller);
            }
        }
    },
    /**
     * Removes all of a controller's attached listeners.
     *
     * @param {String/Ext.app.BaseController} controllerId The id or the controller instance.
     */
    unlisten: function(controllerId) {
        var domains = Ext.app.EventDomain.instances,
            domain;
        for (domain in domains) {
            domains[domain].unlisten(controllerId);
        }
    }
});

/**
 * This class implements the global event domain. This domain represents event fired from
 * {@link Ext.GlobalEvents} Observable instance. No selectors are supported for this domain.
 * 
 * @private
 */
Ext.define('Ext.app.domain.Global', {
    extend: 'Ext.app.EventDomain',
    requires: [
        'Ext.GlobalEvents'
    ],
    singleton: true,
    type: 'global',
    constructor: function() {
        var me = this;
        me.callParent();
        me.monitor(Ext.GlobalEvents);
    },
    /**
     * This method adds listeners on behalf of a controller. Since Global domain does not
     * support selectors, we skip this layer and just accept an object keyed by events.
     * For example:
     *
     *      domain.listen({
     *          idle: function() { ... },
     *          afterlayout: {
     *              fn: function() { ... },
     *              delay: 10
     *          }
     *      });
     *
     * @param {Object} listeners Config object containing listeners.
     * @param {Object} controller A controller to force execution scope on
     *
     * @private
     */
    listen: function(listeners, controller) {
        this.callParent([
            {
                global: listeners
            },
            controller
        ]);
    },
    match: Ext.returnTrue
});

/**
 * This class is used to hold the handler functions for when a route is executed. It also
 * keeps track of the {@link #lastToken last token} this handler was last executed on.
 *
 * @since 6.6.0
 */
Ext.define('Ext.route.Handler', {
    /**
     * @cfg {Function/String} action
     * The handler to execute when the route recognizes a token in the hash.
     *
     * This can be prevented from executing by the {@link #before} handler.
     *
     * If defined as a String, the Function will be resolved from the {@link #scope}.
     */
    /**
     * @cfg {Function/String} before
     * The handler to execute before the {@link #action} handler. The `before` handler
     * can prevent the {@link #action} handler by executing the {@link Ext.route.Action#stop stop}
     * method on the {@link Ext.route.Action action} argument or by returning a
     * {@link Ext.Promise promise} and rejecting it:
     *
     *     routes: {
     *         'user/:id': {
     *             before: 'onBefore
     *         }
     *     },
     *
     *     onBefore: function (id, action) {
     *         action.stop();
     *     }
     *
     *     // or
     *
     *     onBefore: function (id) {
     *         return new Ext.Promise(function (resolve, reject) {
     *             reject();
     *         });
     *     }
     *
     * If using the `action` argument, the `action` argument will always be the last argument passed
     * after any configured url parameters.
     *
     * If defined as a String, the Function will be resolved from the {@link #scope}.
     */
    /**
     * @cfg {Boolean} lazy
     * If `true`, the defined routes will get executed when created.
     */
    lazy: false,
    /**
     * @cfg {Function/String} exit
     * The handler to execute when the route no longer recognizes a token in the current hash.
     *
     * If defined as a String, the Function will be resolved from the {@link #scope}.
     */
    /**
     * @property {Ext.route.Route} route
     * The route this handler is connected to.
     */
    /**
     * @cfg {Ext.Base} scope
     * The scope to call the handlers with. If the handlers are defined with a String,
     * the handlers will be resolved from this scope.
     */
    /**
      * @cfg {Boolean} single
      * Controls if this handler should be removed after first execution. There are
      * a veriety of values that control when in the execution this should be removed:
      *
      * - **true** Remove this handler after a successful and full execution. The handler
      *  will be removed after the handler's {@link #cfg!action} has been executed. If a
      *  {@link #cfg!before} has been rejected, the {@link #cfg!action} will not be
      *  executed meaning this handler will **not** be removed.
      * - **after** Remove this handler after the {@cfg!link #before} has been resolved.
      *  If {@cfg!link #before} has been rejected, this handler will **not** be removed.
      *  If no {@cfg!link #before} exists, this handler will be removed prior to the
      *  {@link #cfg!action} being executed.
      * - **before** Remove this handler before the {@link #cfg!before} has been
      *  executed. If no {@link #cfg!before} exists, this handler will be removed prior
      *  to the {@link #cfg!action} being executed.
      */
    /**
     * @private
     * @property {String} lastToken
     * The last token this handler is connected to in the current hash.
     */
    statics: {
        /**
         * @private
         *
         * Creates a {@link Ext.route.Handler Handler} instance from the config
         * defined in the {@link Ext.route.Mixin#routes} config.
         *
         * @param {Object} config The config from the routes config.
         * @param {Ext.Base} scope The scope the handlers will be called/resolved with.
         * @return {Ext.route.Handler}
         */
        fromRouteConfig: function(config, scope) {
            var handler = {
                    action: config.action,
                    before: config.before,
                    lazy: config.lazy,
                    exit: config.exit,
                    scope: scope,
                    single: config.single
                };
            return new this(handler);
        }
    },
    constructor: function(config) {
        Ext.apply(this, config);
    }
});

/**
 * Class that can manage the execution of route handlers. All {@link #befores} handlers
 * will be executed prior to the {@link #actions} handlers. If at any point this `Action`
 * class is stopped, no other handler (before or action) will be executed.
 */
Ext.define('Ext.route.Action', {
    config: {
        /**
         * @cfg {Function[]} actions
         * The action handlers to execute in response to the route executing.
         * The individual functions will be executed with the scope of the class
         * that connected the route and the arguments will be the configured URL
         * parameters in order they appear in the token.
         *
         * See {@link #befores} also.
         */
        actions: null,
        /**
         * @cfg {Function[]} befores
         * The before handlers to execute prior to the {@link #actions} handlers.
         * The individual functions will be executed with the scope of the class
         * that connected the route and the arguments will be the configured URL
         * parameters in the order they appear in the token plus this `Action` instance
         * as the last argument.
         *
         * **IMPORTANT** A before function must have a resolution. You can do this
         * by executing the {@link #resume} or {@link #stop} function or you can
         * return a promise and resolve/reject it.
         *
         *     var action = new Ext.route.Action({
         *         before: {
         *             fn: function (action) {
         *                 action.resume(); //or action.stop();
         *             }
         *         }
         *     });
         *     action.run();
         *
         *     var action = new Ext.route.Action({
         *         before: {
         *             fn: function () {
         *                 return new Ext.Promise(function (resolve, reject) {
         *                     resolve(); //or reject();
         *                 });
         *             }
         *         }
         *     });
         *     action.run();
         *
         * See {@link #actions} also.
         */
        befores: null,
        /**
         * @cfg {Array} urlParams
         * The URL parameters that were matched by the {@link Ext.route.Route}.
         */
        urlParams: []
    },
    /**
     * @property {Ext.Deferred} deferred
     * The deferral object that will resolve after all functions have executed
     * ({@link #befores} and {@link #actions}) or reject if any {@link #befores}
     * function stops this action.
     * @private
     */
    /**
     * @property {Boolean} [started=false]
     * Whether or not this class has started executing any {@link #befores} or {@link #actions}.
     * @readonly
     * @protected
     */
    started: false,
    /**
     * @property {Boolean} [stopped=false]
     * Whether or not this class was stopped by a {@link #befores} function.
     * @readonly
     * @protected
     */
    stopped: false,
    constructor: function(config) {
        var me = this;
        me.deferred = new Ext.Deferred();
        me.resume = me.resume.bind(me);
        me.stop = me.stop.bind(me);
        me.initConfig(config);
        me.callParent([
            config
        ]);
    },
    applyActions: function(actions) {
        if (actions) {
            actions = Ext.Array.from(actions);
        }
        return actions;
    },
    applyBefores: function(befores) {
        if (befores) {
            befores = Ext.Array.from(befores);
        }
        return befores;
    },
    destroy: function() {
        this.deferred = null;
        this.setBefores(null).setActions(null).setUrlParams(null);
        this.callParent();
    },
    /**
     * Allow further function execution of other functions if any.
     *
     * @return {Ext.route.Action} this
     */
    resume: function() {
        return this.next();
    },
    /**
     * Prevent other functions from executing and resolve the {@link #deferred}.
     *
     * @return {Ext.route.Action} this
     */
    stop: function() {
        this.stopped = true;
        return this.done();
    },
    /**
     * Executes the next {@link #befores} or {@link #actions} function. If {@link #stopped}
     * is `true` or no functions are left to execute, the {@link #done} function will be called.
     *
     * @private
     * @return {Ext.route.Action} this
     */
    next: function() {
        var me = this,
            actions = me.getActions(),
            befores = me.getBefores(),
            urlParams = me.getUrlParams(),
            config, ret, args;
        if (Ext.isArray(urlParams)) {
            args = urlParams.slice();
        } else {
            args = [
                urlParams
            ];
        }
        if (me.stopped || (befores ? !befores.length : true) && (actions ? !actions.length : true)) {
            me.done();
        } else {
            if (befores && befores.length) {
                config = befores.shift();
                args.push(me);
                ret = Ext.callback(config.fn, config.scope, args);
                if (ret && ret.then) {
                    ret.then(function(arg) {
                        me.resume(arg);
                    }, function(arg) {
                        me.stop(arg);
                    });
                }
            } else if (actions && actions.length) {
                config = actions.shift();
                Ext.callback(config.fn, config.scope, args);
                me.next();
            } else {
                // needed?
                me.next();
            }
        }
        return me;
    },
    /**
     * Starts the execution of {@link #befores} and/or {@link #actions} functions.
     *
     * @return {Ext.promise.Promise}
     */
    run: function() {
        var deferred = this.deferred;
        if (!this.started) {
            this.next();
            this.started = true;
        }
        return deferred.promise;
    },
    /**
     * When no {@link #befores} or {@link #actions} functions are left to execute
     * or {@link #stopped} is `true`, this function will be executed to resolve
     * or reject the {@link #deferred} object.
     *
     * @private
     * @return {Ext.route.Action} this
     */
    done: function() {
        var deferred = this.deferred;
        if (this.stopped) {
            deferred.reject();
        } else {
            deferred.resolve();
        }
        this.destroy();
        return this;
    },
    /**
     * Add a function to the {@link #befores} stack.
     *
     *     action.before(function() {}, this);
     *
     * By default, the function will be added to the end of the {@link #befores} stack. If
     * instead the function should be placed at the beginning of the stack, you can pass
     * `true` as the first argument:
     *
     *     action.before(true, function() {}, this);
     *
     * @param {Boolean} [first=false] Pass `true` to add the function to the beginning of the
     * {@link #befores} stack instead of the end.
     * @param {Function/String} fn The function to add to the {@link #befores}.
     * @param {Object} [scope] The scope of the function to execute with. This is normally
     * the class that is adding the function to the before stack.
     * @return {Ext.route.Action} this
     */
    before: function(first, fn, scope) {
        if (!Ext.isBoolean(first)) {
            scope = fn;
            fn = first;
            first = false;
        }
        // eslint-disable-next-line vars-on-top
        var befores = this.getBefores(),
            config = {
                fn: fn,
                scope: scope
            };
        if (this.destroyed) {
            Ext.raise('This action has has already resolved and therefore will never ' + 'execute this function.');
            return;
        }
        if (befores) {
            if (first) {
                befores.unshift(config);
            } else {
                befores.push(config);
            }
        } else {
            this.setBefores(config);
        }
        return this;
    },
    /**
     * Add a function to the {@link #actions} stack.
     *
     *     action.action(function() {}, this);
     *
     * By default, the function will be added to the end of the {@link #actions} stack. If
     * instead the function should be placed at the beginning of the stack, you can pass
     * `true` as the first argument:
     *
     *     action.action(true, function() {}, this);
     *
     * @param {Boolean} [first=false] Pass `true` to add the function to the beginning of the
     * {@link #befores} stack.
     * @param {Function/String} fn The function to add to the {@link #actions}.
     * @param {Object} [scope] The scope of the function to execute with. This is normally
     * the class that is adding the function to the action stack.
     * @return {Ext.route.Action} this
     */
    action: function(first, fn, scope) {
        if (!Ext.isBoolean(first)) {
            scope = fn;
            fn = first;
            first = false;
        }
        // eslint-disable-next-line vars-on-top
        var actions = this.getActions(),
            config = {
                fn: fn,
                scope: scope
            };
        if (this.destroyed) {
            Ext.raise('This action has has already resolved and therefore will never ' + 'execute this function.');
            return;
        }
        if (actions) {
            if (first) {
                actions.unshift(config);
            } else {
                actions.push(config);
            }
        } else {
            this.setActions(config);
        }
        return this;
    },
    /**
     * Execute functions when this action has been resolved or rejected.
     *
     * @param {Function} resolve The function to execute when this action has been resolved.
     * @param {Function} reject The function to execute when a before function stopped this action.
     * @return {Ext.Promise}
     */
    then: function(resolve, reject) {
        if (this.destroyed) {
            Ext.raise('This action has has already resolved and therefore will never ' + 'execute either function.');
            return;
        }
        return this.deferred.then(resolve, reject);
    }
});

/**
 * Enables reactive actions to handle changes in the hash by using the
 * {@link Ext.route.Mixin#routes routes} configuration in a controller.
 * An example configuration would be:
 *
 *     Ext.define('MyApp.view.main.MainController', {
 *         extend: 'Ext.app.ViewController',
 *         alias: 'controller.app-main',
 *
 *         routes: {
 *             'user/:{id}': 'onUser'
 *         },
 *
 *         onUser: function (values) {
 *             var id = values.id;
 *             // ...
 *         }
 *     });
 *
 * The `routes` object can also receive an object to further configure
 * the route, for example you can configure a `before` action that will
 * be executed before the `action` or can cancel the route execution:
 *
 *     Ext.define('MyApp.view.main.MainController', {
 *         extend: 'Ext.app.ViewController',
 *         alias: 'controller.app-main',
 *
 *         routes: {
 *             'user/:{id}': {
 *                 action: 'onUser',
 *                 before: 'onBeforeUser',
 *                 name: 'user'
 *             }
 *         },
 *
 *         onBeforeUser: function (values) {
 *             return Ext.Ajax
 *                 .request({
 *                     url: '/check/permission',
 *                     params: {
 *                         route: 'user',
 *                         meta: {
 *                             id: values.id
 *                         }
 *                     }
 *                 });
 *         },
 *
 *         onUser: function (values) {
 *             var id = values.id;
 *             // ...
 *         }
 *     });
 *
 * URL Parameters in a route can also define a type that will be used
 * when matching hashes when finding routes that recognize a hash and
 * also parses the value into numbers:
 *
 *     Ext.define('MyApp.view.main.MainController', {
 *         extend: 'Ext.app.ViewController',
 *         alias: 'controller.app-main',
 *
 *         routes: {
 *             'user/:{id:num}': {
 *                 action: 'onUser',
 *                 before: 'onBeforeUser',
 *                 name: 'user'
 *             }
 *         },
 *
 *         onBeforeUser: function (values) {
 *             return Ext.Ajax
 *                 .request({
 *                     url: '/check/permission',
 *                     params: {
 *                         route: 'user',
 *                         meta: {
 *                             id: values.id
 *                         }
 *                     }
 *                 });
 *         },
 *
 *         onUser: function (values) {
 *             var id = values.id;
 *             // ...
 *         }
 *     });
 *
 * In this example, the id parameter added `:num` to the parameter which
 * will now mean the route will only recognize a value for the id parameter
 * that is a number such as `#user/123` and will not recognize `#user/abc`.
 * The id passed to the action and before handlers will also get cast into
 * a number instead of a string. If a type is not provided, it will use
 * the {@link #defaultMatcher default matcher}.
 *
 * For more on types, see the {@link #cfg!types} config.
 *
 * For backwards compatibility, there is `positional` mode which is like
 * `named` mode but how you define the url parameters and how they are passed
 * to the action and before handlers is slightly different:
 *
 *     Ext.define('MyApp.view.main.MainController', {
 *         extend: 'Ext.app.ViewController',
 *         alias: 'controller.app-main',
 *
 *         routes: {
 *             'user/:id:action': {
 *                 action: 'onUser',
 *                 before: 'onBeforeUser',
 *                 name: 'user',
 *                 conditions: {
 *                     ':action': '(edit|delete)?'
 *                 }
 *             }
 *         },
 *
 *         onBeforeUser: function (id, action) {
 *             return Ext.Ajax
 *                 .request({
 *                     url: '/check/permission',
 *                     params: {
 *                         route: 'user',
 *                         meta: {
 *                             action: action,
 *                             id: id
 *                         }
 *                     }
 *                 });
 *         },
 *
 *         onUser: function (id) {
 *             // ...
 *         }
 *     });
 *
 * The parameters are defined without curly braces (`:id`, `:action`) and
 * they are passed as individual arguments to the action and before handlers.
 *
 * It's important to note you cannot mix positional and named parameter formats
 * in the same route since how they are passed to the handlers is different.
 *
 * Routes can define sections of a route pattern that are optional by surrounding
 * the section that is to be optional with parenthesis. For example, if a route
 * should match both `#user` and `#user/1234` to either show a grid of all users
 * or details or a single user, you can define the route such as:
 *
 *     Ext.define('MyApp.view.main.MainController', {
 *         extend: 'Ext.app.ViewController',
 *         alias: 'controller.app-main',
 *
 *         routes: {
 *             'user(/:{id:num})': {
 *                 action: 'onUser',
 *                 name: 'user'
 *             }
 *         },
 *
 *         onUser: function (params) {
 *             if (params.id) {
 *                 // load user details
 *             } else {
 *                 // load grid of users
 *             }
 *         }
 *     });
 */
Ext.define('Ext.route.Route', {
    requires: [
        'Ext.route.Action',
        'Ext.route.Handler'
    ],
    /**
     * @event beforeroute
     * @member Ext.GlobalEvents
     *
     * Fires when a route is about to be executed. This allows pre-processing to add additional
     * {@link Ext.route.Action#before before} or {@link Ext.route.Action#action action} handlers
     * when the {@link Ext.route.Action Action} is run.
     *
     * The route can be prevented from executing by returning `false` in a listener
     * or executing the {@link Ext.route.Action#stop stop} method on the action.
     *
     * @param {Ext.route.Route} route The route being executed.
     * @param {Ext.route.Action} action The action that will be run.
     */
    /**
     * @event beforerouteexit
     * @member Ext.GlobalEvents
     *
     * Fires when a route is being exited meaning when a route
     * was executed but no longer matches a token in the current hash.
     *
     * The exit handlers can be prevented from executing by returning `false` in a listener
     * or executing the {@link Ext.route.Action#stop stop} method on the action.
     *
     * @param {Ext.route.Action} action The action with defined exit actions. Each
     * action will execute with the last token this route was connected with.
     * @param {Ext.route.Route} route
     */
    config: {
        /**
         * @cfg {String} name The name of this route. The name can be used when using
         * {@link Ext.route.Mixin#redirectTo}.
         */
        name: null,
        /**
         * @cfg {String} url (required) The url regex to match against.
         */
        url: null,
        /**
         * @cfg {Boolean} [allowInactive=false] `true` to allow this route to be triggered on
         * a controller that is not active.
         */
        allowInactive: false,
        /**
         * @cfg {Object} conditions
         * Optional set of conditions for each token in the url string. Each key should
         * be one of the tokens, each value should be a regex that the token should accept.
         *
         * For `positional` mode, if you have a route with a url like `'files/:fileName'` and
         * you want it to match urls like `files/someImage.jpg` then you can set these
         * conditions to allow the :fileName token to accept strings containing a period:
         *
         *     conditions: {
         *         ':fileName': '([0-9a-zA-Z\.]+)'
         *     }
         *
         * For `named` mode, if you have a route with a url like `'files/:{fileName}'`
         * and you want it to match urls like `files/someImage.jpg` then you can set these
         * conditions to allow the :{fileName} token to accept strings containing a period:
         *
         *     conditions: {
         *         'fileName': '([0-9a-zA-Z\.]+)'
         *     }
         *
         * You can also define a condition to parse the value or even split it on a character:
         *
         *     conditions: {
         *         'fileName': {
         *             re: '([0-9a-zA-Z\.]+)',
         *             split: '.', // split the value so you get an array ['someImage', 'jpg']
         *             parse: function (values) {
         *                 return values[0]; // return a string without the extension
         *             }
         *         }
         *     }
         */
        conditions: {},
        /**
         * @cfg {Boolean} [caseInsensitive=false] `true` to allow the tokens to be matched with
         * case-insensitive.
         */
        caseInsensitive: false,
        /**
         * @cfg {Object[]} [handlers=[]]
         * The array of connected handlers to this route. Each handler must defined a
         * `scope` and can define an `action`, `before` and/or `exit` handler:
         *
         *     handlers: [{
         *         action: function() {
         *             //...
         *         },
         *         scope: {}
         *     }, {
         *         action: function() {
         *             //...
         *         },
         *         before: function() {
         *             //...
         *         },
         *         scope: {}
         *     }, {
         *         exit: function() {
         *             //...
         *         },
         *         scope: {}
         *     }]
         *
         * The `action`, `before` and `exit` handlers can be a string that will be resolved
         * from the `scope`:
         *
         *     handlers: [{
         *         action: 'onAction',
         *         before: 'onBefore',
         *         exit: 'onExit',
         *         scope: {
         *             onAction: function () {
         *                 //...
         *             },
         *             onBefore: function () {
         *                 //...
         *             },
         *             onExit: function () {
         *                 //...
         *             }
         *         }
         *     }]
         */
        handlers: [],
        /* eslint-disable max-len */
        /**
         * @since 6.6.0
         * @property {Object} types
         * An object of types that will be used to match and parse values from a matched
         * url. There are four default types:
         *
         * - `alpha` This will only match values that have only alpha characters using
         * the regex `([a-zA-Z]+)`.
         * - `alphanum` This will only match values that have alpha and numeric characters
         * using the regex `([a-zA-Z0-9]+|[0-9]*(?:\\.[0-9]*)?)`. If a value is a number,
         * which a number can have a period (`10.4`), the value will be case into a float
         * using `parseFloat`.
         * - `num` This will only match values that have numeric characters using the regex
         * `([0-9]*(?:\\.[0-9]*)?)`. The value, which can have a period (`10.4`), will be
         * case into a float using `parseFloat`.
         * - `...` This is meant to be the last argument in the url and will match all
         * characters using the regex `(.+)?`. If a value is matched, this is an optional
         * type, the value will be split by `/` and an array will be sent to the handler
         * methods. If no value was matched, the value will be `undefined`.
         *
         * When defining routes, a type is optional and will use the
         * {@link #defaultMatcher default matcher} but the url parameter must be enclosed
         * in curly braces which will send a single object to the route handlers:
         *
         *     Ext.define('MyApp.view.MainController', {
         *         extend: 'Ext.app.ViewController',
         *         alias: 'controller.myapp-main',
         *
         *         routes: {
         *             'view/:{view}/:{child:alphanum}:{args...}': {
         *                 action: 'onView',
         *                 before: 'onBeforeView',
         *                 name: 'view'
         *             }
         *         },
         *
         *         onBeforeView: function (values) {
         *             return Ext.Ajax.request({
         *                 url: 'check/permission',
         *                 params: {
         *                     view: values.view,
         *                     info: { childView: values.child }
         *                 }
         *             });
         *         },
         *
         *         onView: function (values) {}
         *     });
         *
         * In this example, there are 3 parameters defined. The `:{view}` parameter has no
         * type which will match characters using the {@link #defaultMatcher default matcher}
         * but is required to be in the matched url. The `:{child:alphanum}` will only match
         * characters that are alpha or numeric but is required to be in the matched url. The
         * `:{args...}` is the only optional parameter in this route but can match any
         * character and will be an array of values split by `/` unless there are no values
         * in which case `undefined` will be sent in the object.
         *
         * If the hash is `#view/user/edit`, the `values` argument sent to the handlers would be:
         *
         *     {
         *         view: 'user',
         *         child: 'edit',
         *         args: undefined
         *     }
         *
         * Since there were no more values for the `args` parameter, it's value is `undefined`.
         *
         * If the hash is `#view/user/1234`, the `values` argument sent to the handlers would be:
         *
         *     {
         *         view: 'user',
         *         child: 1234,
         *         args: undefined
         *     }
         *
         * Notice the `child` value is a number instead of a string.
         *
         * If the hash is `#view/user/1234/edit/settings`, the `values` argument sent to the
         * handlers would be:
         *
         *     {
         *         view: 'user',
         *         child: 1234,
         *         args: ['edit', 'settings']
         *     }
         *
         * The `args` parameter matched the `edit/settings` and split it by the `/` producing
         * the array.
         *
         * To add custom types, you can override `Ext.route.Route`:
         *
         *     Ext.define('Override.route.Route', {
         *         override: 'Ext.route.Route',
         *
         *         config: {
         *             types: {
         *                 uuid: {
         *                     re: '([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})'
         *                 }
         *             }
         *         }
         *     });
         *
         * You can now use the `uuid` type in your routes:
         *
         *     Ext.define('MyApp.view.MainController', {
         *         extend: 'Ext.app.ViewController',
         *         alias: 'controller.myapp-main',
         *
         *         routes: {
         *             'user/:{userid:uuid}': {
         *                 action: 'onUser',
         *                 caseInsensitive: true,
         *                 name: 'user'
         *             }
         *         },
         *
         *         onUser: function (values) {}
         *     });
         *
         * This would match if the hash was like `#user/C56A4180-65AA-42EC-A945-5FD21DEC0538`
         * and the `values` object would then be:
         *
         *     {
         *         user: 'C56A4180-65AA-42EC-A945-5FD21DEC0538'
         *     }
         */
        /* eslint-enable max-len */
        types: {
            cached: true,
            $value: {
                alpha: {
                    re: '([a-zA-Z]+)'
                },
                alphanum: {
                    re: '([a-zA-Z0-9]+|[0-9]+(?:\\.[0-9]+)?|[0-9]*(?:\\.[0-9]+){1})',
                    parse: function(value) {
                        var test;
                        if (value && this.numRe.test(value)) {
                            test = parseFloat(value);
                            if (!isNaN(test)) {
                                value = test;
                            }
                        }
                        return value;
                    }
                },
                num: {
                    // allow `1`, `10`, 10.1`, `.1`
                    re: '([0-9]+(?:\\.[0-9]+)?|[0-9]*(?:\\.[0-9]+){1})',
                    parse: function(value) {
                        if (value) {
                            value = parseFloat(value);
                        }
                        return value;
                    }
                },
                '...': {
                    re: '(.+)?',
                    split: '/',
                    parse: function(values) {
                        var length, i, value;
                        if (values) {
                            length = values.length;
                            for (i = 0; i < length; i++) {
                                value = parseFloat(values[i]);
                                if (!isNaN(value)) {
                                    values[i] = value;
                                }
                            }
                        }
                        return values;
                    }
                }
            }
        }
    },
    /**
     * @property {String} [defaultMatcher='([%a-zA-Z0-9\\-\\_\\s,]+)'] The default RegExp string
     * to use to match parameters with.
     */
    defaultMatcher: '([%a-zA-Z0-9\\-\\_\\s,]+)',
    /**
     * @private
     * @property {RegExp} matcherRegex A regular expression to match the token to the
     * configured {@link #url}.
     */
    /**
     * @since 6.6.0
     * @property {RegExp} numRe A regular expression to match against float numbers for
     * `alphanum`, `num` and `...` {@link #cfg!types} in order to cast into floats.
     */
    numRe: /^[0-9]*(?:\.[0-9]*)?$/,
    /**
     * @private
     * @since 6.6.0
     * @property {RegExp} typeParamRegex
     * A regular expression to determine if the parameter may contain type information.
     * If a parameter does have type information, the url parameters sent to the
     * {@link Ext.route.Handler#before} and {@link Ext.route.Handler#after} will
     * be in an object instead of separate arguments.
     */
    typeParamRegex: /:{([0-9A-Za-z_]+)(?::?([0-9A-Za-z_]+|.{3})?)}/g,
    /**
     * @private
     * @since 6.6.0
     * @property {RegExp} optionalParamRegex
     * A regular expression to find groups intended to be optional values within the
     * hash. This means that if they are in the hash they will match and return the
     * values present. But, if they are not and the rest of the hash matches, the route
     * will still execute passing `undefined` as the values of any parameters
     * within an optional group.
     *
     *     routes: {
     *         'user(\/:{id:num})': {
     *             action: 'onUser',
     *             name: 'user'
     *         }
     *     }
     *
     * In this example, the `id` parameter and the slash will be optional since they
     * are wrapped in the parentheses. This route would execute if the hash is `#user`
     * or `#user/1234`.
     */
    optionalGroupRegex: /\((.+?)\)/g,
    /**
     * @private
     * @property {RegExp} paramMatchingRegex
     * A regular expression to check if there are parameters in the configured
     * {@link #url}.
     */
    paramMatchingRegex: /:([0-9A-Za-z_]+)/g,
    /**
     * @private
     * @property {Array/Object} paramsInMatchString
     * An array or object of parameters in the configured {@link #url}.
     */
    /**
     * @private
     * @since 6.6.0
     * @property {String} mode
     * The mode based on the {@link #cfg!url} pattern this route is configured with.
     * Valid values are:
     *
     * - `positional` The {@link #cfg!url} was configured with the parameter format
     * as `:param`. The values in the handler functions will be individual arguments.
     * Example:
     *
     *     Ext.define('MyApp.view.MainController', {
     *         extend: 'Ext.app.ViewController',
     *         alias: 'controller.myapp-main',
     *
     *         routes: {
     *             'view/:view/:child': {
     *                 action: 'onView',
     *                 before: 'onBeforeView',
     *                 name: 'view'
     *             }
     *         },
     *
     *         onBeforeView: function (view, child) {
     *             return Ext.Ajax.request({
     *                 url: 'check/permission',
     *                 params: {
     *                     view: view,
     *                     info: { childView: child }
     *                 }
     *             });
     *         },
     *
     *         onView: function (view, child) {}
     *     });
     *
     * The values from the matched url that the `view` route would execute with are
     * separate arguments in the before and action handlers.
     * - `named` The {@link #cfg!url} was configured with the parameter format as
     * `:{param:type}` where the `:type` is optional. Example:
     *
     *     Ext.define('MyApp.view.MainController', {
     *         extend: 'Ext.app.ViewController',
     *         alias: 'controller.myapp-main',
     *
     *         routes: {
     *             'view/:{view}/:{child:alphanum}': {
     *                 action: 'onView',
     *                 before: 'onBeforeView',
     *                 name: 'view'
     *             }
     *         },
     *
     *         onBeforeView: function (values) {
     *             return Ext.Ajax.request({
     *                 url: 'check/permission',
     *                 params: {
     *                     view: values.view,
     *                     info: { childView: values.child }
     *                 }
     *             });
     *         },
     *
     *         onView: function (values) {}
     *     });
     *
     * The values from the matched url the `view` route would execute with are collected
     * into an object with the parameter name as the key and the associated value as
     * the value. See {@link #cfg!types} for more about this named mode.
     */
    /**
     * @protected
     * @property {Boolean} isRoute
     */
    isRoute: true,
    constructor: function(config) {
        var me = this,
            url;
        this.initConfig(config);
        url = me.getUrl().replace(me.optionalGroupRegex, function(match, middle) {
            return '(?:' + middle + ')?';
        });
        if (url.match(me.typeParamRegex)) {
            me.handleNamedPattern(url);
        } else {
            me.handlePositionalPattern(url);
        }
    },
    /**
     * @private
     * @since 6.6.0
     * Handles a pattern that will enable positional {@link #property!mode}.
     *
     * @param {String} url The url pattern.
     */
    handlePositionalPattern: function(url) {
        var me = this;
        me.paramsInMatchString = url.match(me.paramMatchingRegex) || [];
        me.matcherRegex = me.createMatcherRegex(url);
        me.mode = 'positional';
    },
    /**
     * @private
     * @since 6.6.0
     * Handles a pattern that will enable named {@link #property!mode}.
     *
     * @param {String} url The url pattern.
     */
    handleNamedPattern: function(url) {
        var me = this,
            typeParamRegex = me.typeParamRegex,
            conditions = me.getConditions(),
            types = me.getTypes(),
            defaultMatcher = me.defaultMatcher,
            params = {},
            re = url.replace(typeParamRegex, function(match, param, typeMatch) {
                var type = typeMatch && types[typeMatch],
                    matcher = conditions[param] || type || defaultMatcher;
                if (params[param]) {
                    Ext.raise('"' + param + '" already defined in route "' + url + '"');
                }
                if (typeMatch && !type) {
                    Ext.raise('Unknown parameter type "' + typeMatch + '" in route "' + url + '"');
                }
                if (Ext.isObject(matcher)) {
                    matcher = matcher.re;
                }
                params[param] = {
                    matcher: matcher,
                    type: typeMatch
                };
                return matcher;
            });
        if (re.search(me.paramMatchingRegex) !== -1) {
            Ext.raise('URL parameter mismatch. Positional url parameter found ' + 'while in named mode.');
        }
        me.paramsInMatchString = params;
        me.matcherRegex = new RegExp('^' + re + '$', me.getCaseInsensitive() ? 'i' : '');
        me.mode = 'named';
    },
    /**
     * Attempts to recognize a given url string and return a meta data object including
     * any URL parameter matches.
     *
     * @param {String} url The url to recognize.
     * @return {Object/Boolean} The matched data, or `false` if no match.
     */
    recognize: function(url) {
        var me = this,
            recognized = me.recognizes(url),
            handlers, length, hasHandler, handler, matches, urlParams, i;
        if (recognized) {
            handlers = me.getHandlers();
            length = handlers.length;
            for (i = 0; i < length; i++) {
                handler = handlers[i];
                if (handler.lastToken !== url) {
                    // there is a handler that can execute
                    hasHandler = true;
                    break;
                }
            }
            if (!hasHandler && url === me.lastToken) {
                // url matched the lastToken
                return true;
            }
            // backwards compat
            matches = me.matchesFor(url);
            urlParams = me.getUrlParams(url);
            return Ext.applyIf(matches, {
                historyUrl: url,
                urlParams: urlParams
            });
        }
        return false;
    },
    /**
     * @private
     * @since 6.6.0
     * Returns the url parameters matched in the given url.
     *
     * @param {String} url The url this route is executing on.
     * @return {Array/Object} If {@link #property!mode} is `named`,
     * an object from {@link #method!getNamedUrlParams} will be returned.
     * If is `positional`, an array from {@link #method!getPositionalUrlParams}
     * will be returned.
     */
    getUrlParams: function(url) {
        if (this.mode === 'named') {
            return this.getNamedUrlParams(url);
        } else {
            return this.getPositionalUrlParams(url);
        }
    },
    /**
     * @private
     * @since 6.6.0
     * Returns an array of url parameters values in order they appear in the url.
     *
     * @param {String} url The url the route is executing on.
     * @return {Array}
     */
    getPositionalUrlParams: function(url) {
        var params = [],
            conditions = this.getConditions(),
            keys = this.paramsInMatchString,
            values = url.match(this.matcherRegex),
            length = keys.length,
            i, key, type, value;
        // remove the full match
        values.shift();
        for (i = 0; i < length; i++) {
            key = keys[i];
            value = values[i];
            if (conditions[key]) {
                type = conditions[key];
            } else if (key[0] === ':') {
                key = key.substr(1);
                if (conditions[key]) {
                    type = conditions[key];
                }
            }
            value = this.parseValue(value, type);
            if (Ext.isDefined(value) && value !== '') {
                if (Ext.isArray(value)) {
                    params.push.apply(params, value);
                } else {
                    params.push(value);
                }
            }
        }
        return params;
    },
    /**
     * @private
     * @since 6.6.0
     * Returns an object of url parameters with parameter name as the
     * object key and the value.
     *
     * @param {String} url The url the route is executing on.
     * @return {Array}
     */
    getNamedUrlParams: function(url) {
        var conditions = this.getConditions(),
            types = this.getTypes(),
            params = {},
            keys = this.paramsInMatchString,
            values = url.match(this.matcherRegex),
            name, obj, value, type, condition;
        // remove the full match
        values.shift();
        for (name in keys) {
            obj = keys[name];
            value = values.shift();
            condition = conditions[name];
            type = types[obj.type];
            if (condition || type) {
                type = Ext.merge({}, condition, types[obj.type]);
            }
            params[name] = this.parseValue(value, type);
        }
        return params;
    },
    /**
     * @private
     * @since 6.6.0
     * Parses the value from the url with a {@link #cfg!types type}
     * or a matching {@link #cfg!conditions condition}.
     *
     * @param {String} value The value from the url.
     * @param {Object} [type] The type object that will be used to parse the value.
     * @return {String/Number/Array}
     */
    parseValue: function(value, type) {
        if (type) {
            if (value && type.split) {
                value = value.split(type.split);
                // If first is empty string, remove.
                // This could be because the value prior
                // was `/foo/bar` which would lead to
                // `['', 'foo', 'bar']`.
                if (!value[0]) {
                    value.shift();
                }
                // If last is empty string, remove.
                // This could be because the value prior
                // was `foo/bar/` which would lead to
                // `['foo', 'bar', '']`.
                if (!value[value.length - 1]) {
                    value.pop();
                }
            }
            if (type.parse) {
                value = type.parse.call(this, value);
            }
        }
        if (!value && Ext.isString(value)) {
            // IE8 may have values as an empty string
            // if there was no value that was matched
            value = undefined;
        }
        return value;
    },
    /**
     * Returns `true` if this {@link Ext.route.Route} matches the given url string.
     *
     * @param {String} url The url to test.
     * @return {Boolean} `true` if this {@link Ext.route.Route} recognizes the url.
     */
    recognizes: function(url) {
        return this.matcherRegex.test(url);
    },
    /**
     * The method to execute the action using the configured before function which will
     * kick off the actual {@link #actions} on the {@link #controller}.
     *
     * @param {String} token The token this route is being executed with.
     * @param {Object} argConfig The object from the {@link Ext.route.Route}'s
     * recognize method call.
     * @return {Ext.promise.Promise}
     */
    execute: function(token, argConfig) {
        var me = this,
            allowInactive = me.getAllowInactive(),
            handlers = me.getHandlers(),
            queue = Ext.route.Router.getQueueRoutes(),
            length = handlers.length,
            urlParams = (argConfig && argConfig.urlParams) || [],
            i, handler, scope, action, promises, single, remover;
        me.lastToken = token;
        if (!queue) {
            promises = [];
        }
        return new Ext.Promise(function(resolve, reject) {
            if (argConfig === false) {
                reject();
            } else {
                if (queue) {
                    action = new Ext.route.Action({
                        urlParams: urlParams
                    });
                }
                for (i = 0; i < length; i++) {
                    handler = handlers[i];
                    if (token != null && handler.lastToken === token) {
                        // no change on this handler
                        
                        continue;
                    }
                    scope = handler.scope;
                    handler.lastToken = token;
                    if (!allowInactive && scope.isActive && !scope.isActive()) {
                        
                        continue;
                    }
                    if (!queue) {
                        action = new Ext.route.Action({
                            urlParams: urlParams
                        });
                    }
                    single = handler.single;
                    if (handler.before) {
                        action.before(handler.before, scope);
                    }
                    if (handler.action) {
                        action.action(handler.action, scope);
                    }
                    if (single) {
                        remover = Ext.bind(me.removeHandler, me, [
                            null,
                            handler
                        ]);
                        if (single === true) {
                            if (handler.action) {
                                action.action(remover, me);
                            } else {
                                action.before(function() {
                                    remover();
                                    return Ext.Promise.resolve();
                                }, me);
                            }
                        } else {
                            // all before actions have to resolve,
                            // resolve a promise to allow the action
                            // chain to continue
                            action.before(single === 'before', function() {
                                remover();
                                return Ext.Promise.resolve();
                            }, me);
                        }
                    }
                    if (!queue) {
                        if (Ext.fireEvent('beforeroute', action, me) === false) {
                            action.destroy();
                        } else {
                            promises.push(action.run());
                        }
                    }
                }
                if (queue) {
                    if (Ext.fireEvent('beforeroute', action, me) === false) {
                        action.destroy();
                        reject();
                    } else {
                        action.run().then(resolve, reject);
                    }
                } else {
                    Ext.Promise.all(promises).then(resolve, reject);
                }
            }
        });
    },
    /**
     * Returns a hash of matching url segments for the given url.
     *
     * @param {String} url The url to extract matches for
     * @return {Object} matching url segments
     */
    matchesFor: function(url) {
        var params = {},
            keys = this.mode === 'named' ? Ext.Object.getKeys(this.paramsInMatchString) : this.paramsInMatchString,
            values = url.match(this.matcherRegex),
            length = keys.length,
            i;
        // first value is the entire match so reject
        values.shift();
        for (i = 0; i < length; i++) {
            params[keys[i].replace(':', '')] = values[i];
        }
        return params;
    },
    /**
     * Takes the configured url string including wildcards and returns a regex that can be
     * used to match against a url.
     *
     * This is only used in `positional` {@link #property!mode}.
     *
     * @param {String} url The url string.
     * @return {RegExp} The matcher regex.
     */
    createMatcherRegex: function(url) {
        // Converts a route string into an array of symbols starting with a colon. e.g.
        // ":controller/:action/:id" => [':controller', ':action', ':id']
        var me = this,
            paramsInMatchString = me.paramsInMatchString,
            conditions = me.getConditions(),
            defaultMatcher = me.defaultMatcher,
            length = paramsInMatchString.length,
            modifiers = me.getCaseInsensitive() ? 'i' : '',
            i, param, matcher;
        if (url === '*') {
            // handle wildcard routes, won't have conditions
            url = url.replace('*', '\\*');
        } else {
            for (i = 0; i < length; i++) {
                param = paramsInMatchString[i];
                // Even if the param is a named param, we need to
                // allow "local" overriding.
                if (conditions[param]) {
                    matcher = conditions[param];
                }
                // without colon
                else if (param[0] === ':' && conditions[param.substr(1)]) {
                    matcher = conditions[param.substr(1)];
                } else {
                    matcher = defaultMatcher;
                }
                if (Ext.isObject(matcher)) {
                    matcher = matcher.re;
                }
                url = url.replace(new RegExp(param), matcher || defaultMatcher);
            }
        }
        // we want to match the whole string, so include the anchors
        return new RegExp('^' + url + '$', modifiers);
    },
    /**
     * Adds a handler to the {@link #cfg!handlers} stack.
     *
     * @param {Object} handler
     * An object to describe the handler. A handler should define a `fn` and `scope`.
     * If the `fn` is a String, the function will be resolved from the `scope`.
     * @return {Ext.route.Route} this
     */
    addHandler: function(handler) {
        var handlers = this.getHandlers();
        if (!handler.isInstance) {
            handler = new Ext.route.Handler(handler);
        }
        handlers.push(handler);
        return handler.route = this;
    },
    /**
     * Removes a handler from the {@link #cfg!handlers} stack. This normally happens when
     * destroying a class instance.
     *
     * @param {Object/Ext.Base} scope The class instance to match handlers with.
     * @param {Ext.route.Handler} [handler] An optional {@link Ext.route.Handler Handler}
     * to only remove from the array of handlers. If no handler is passed, all handlers
     * will be removed.
     * @return {Ext.route.Route} this
     */
    removeHandler: function(scope, handler) {
        var handlers = this.getHandlers(),
            length = handlers.length,
            newHandlers = [],
            i, item;
        for (i = 0; i < length; i++) {
            item = handlers[i];
            if (handler) {
                if (item !== handler) {
                    newHandlers.push(item);
                }
            } else if (item.scope !== scope) {
                newHandlers.push(item);
            }
        }
        this.setHandlers(newHandlers);
        return this;
    },
    /**
     * Clears the last token properties of this route and all handlers.
     */
    clearLastTokens: function() {
        var handlers = this.getHandlers(),
            length = handlers.length,
            i;
        for (i = 0; i < length; i++) {
            handlers[i].lastToken = null;
        }
        this.lastToken = null;
    },
    /**
     * @private
     * @since 6.6.0
     *
     * When a route is exited (no longer recognizes a token in the current hash)
     * we need to clear all last tokens and execute any exit handlers.
     */
    onExit: function() {
        var me = this,
            handlers = me.getHandlers(),
            allowInactive = me.getAllowInactive(),
            length = handlers.length,
            action = new Ext.route.Action({
                urlParams: [
                    me.lastToken
                ]
            }),
            i, handler, scope;
        // Need to reset handlers' `lastToken` so that when a token
        // is added to the document fragment it will not be falsely
        // matched.
        me.clearLastTokens();
        for (i = 0; i < length; i++) {
            handler = handlers[i];
            if (handler.exit) {
                scope = handler.scope;
                if (!allowInactive && scope.isActive && !scope.isActive()) {
                    
                    continue;
                }
                action.action(handler.exit, scope);
            }
        }
        if (Ext.fireEvent('beforerouteexit', action, me) === false) {
            action.destroy();
        } else {
            action.run();
        }
    }
});

/**
 * History management component that allows you to register arbitrary tokens that signify
 * application history state on navigation actions.  You can then handle the history
 * {@link #change} event in order to reset your application UI to the appropriate state when
 * the user navigates forward or backward through the browser history stack.
 *
 * ## Initializing
 *
 * The {@link #init} method of the History object must be called before using History. This sets up
 * the internal state and must be the first thing called before using History.
 */
Ext.define('Ext.util.History', {
    singleton: true,
    alternateClassName: 'Ext.History',
    mixins: {
        observable: 'Ext.util.Observable'
    },
    /**
     * @property
     * True to use `window.top.location.hash` or false to use `window.location.hash`. Must be set
     * before {@link #init} is called because the `hashchange` event listener is added to the window
     * at initialization time.
     */
    useTopWindow: false,
    /**
     * @property {Boolean} hashbang If set to `true`, when a hash is set, the hash will be prefixed
     * with an exclamation making it a hash bang instead of just a hash.
     *
     *     Ext.util.History.add('foo'); // will result in #foo
     *
     *     Ext.util.History.hashbang = true;
     *     Ext.util.History.add('bar'); // will result in #!bar
     */
    /**
     * @property {String} currentToken The current token.
     * @private
     */
    /**
     * @event ready
     * Fires when the Ext.util.History singleton has been initialized and is ready for use.
     * @param {Ext.util.History} history The Ext.util.History singleton.
     */
    /**
     * @event change
     * Fires when navigation back or forwards within the local page's history occurs.
     * @param {String} token An identifier associated with the page state at that point
     * in its history.
     */
    hashRe: /^(#?!?)/,
    constructor: function() {
        var me = this;
        me.ready = false;
        me.currentToken = null;
        me.mixins.observable.constructor.call(me);
    },
    /**
     * Gets the actual hash from the url. This shouldn't need to be used directly but use the
     * {@link #getToken} method instead.
     *
     * @return {String} The hash from the window object.
     * @private
     */
    getHash: function() {
        return (this.win.location.hash || '').replace(this.hashRe, '');
    },
    /**
     * Updates the hash on the window. This shouldn't need to be used directly but use the
     * {@link #add} method instead.
     *
     * @param {String} hash The hash to use
     * @param {Boolean} replace If `true`, the hash passed in will replace the current resource
     * by using the `location.replace()` API.
     * @private
     */
    setHash: function(hash, replace) {
        var me = this,
            hashRe = me.hashRe,
            loc = me.win.location;
        // may or may not already be prefixed with # or #! already
        hash = hash.replace(hashRe, me.hashbang ? '#!' : '#');
        try {
            if (replace) {
                loc.replace(hash);
            } else {
                loc.hash = hash;
            }
            // need to make sure currentToken is not prefixed
            me.currentToken = hash.replace(hashRe, '');
        } catch (e) {}
    },
    // IE can give Access Denied (esp. in popup windows)
    /**
     * Handles when the hash in the URL has been updated. Will also fired the change event.
     *
     * @param {String} token The token that was changed to
     * @private
     */
    handleStateChange: function(token) {
        // browser won't have # here but may have !
        token = token.replace(this.hashRe, '');
        this.fireEvent('change', this.currentToken = token);
    },
    /**
     * Bootstraps the initialization the location.hash.
     * @private
     */
    startUp: function() {
        var me = this;
        me.currentToken = me.getHash();
        Ext.get(me.win).on('hashchange', me.onHashChange, me);
        me.ready = true;
        me.fireEvent('ready', me);
    },
    onHashChange: function() {
        var me = this,
            newHash = me.getHash();
        if (newHash !== me.hash) {
            me.hash = newHash;
            me.handleStateChange(newHash);
        }
    },
    /**
     * Initializes the global History instance.
     * @param {Function} [onReady] A callback function that will be called once the history
     * component is fully initialized.
     * @param {Object} [scope] The scope (`this` reference) in which the callback is executed.
     * Defaults to the browser window.
     */
    init: function(onReady, scope) {
        var me = this;
        if (me.ready) {
            Ext.callback(onReady, scope, [
                me
            ]);
            return;
        }
        if (!Ext.isReady) {
            Ext.onInternalReady(function() {
                me.init(onReady, scope);
            });
            return;
        }
        me.win = me.useTopWindow ? window.top : window;
        me.hash = me.getHash();
        if (onReady) {
            me.on('ready', onReady, scope, {
                single: true
            });
        }
        me.startUp();
    },
    /**
     * Add a new token to the history stack. This can be any arbitrary value, although it would
     * commonly be the concatenation of a component id and another id marking the specific history
     * state of that component. Example usage:
     *
     *     // Handle tab changes on a TabPanel
     *     tabPanel.on('tabchange', function(tabPanel, tab){
     *          Ext.History.add(tabPanel.id + ':' + tab.id);
     *     });
     *
     * @param {String} token The value that defines a particular application-specific history state
     * @param {Boolean} [preventDuplicates=true] When true, if the passed token matches the current
     * token it will not save a new history step. Set to false if the same state can be saved more
     * than once at the same history stack location.
     *
     * @return {Boolean} Whether the token was set in the case if the current token matches
     * the token passed.
     */
    add: function(token, preventDuplicates) {
        var me = this,
            set = false;
        if (preventDuplicates === false || me.getToken() !== token) {
            me.setHash(token);
            set = true;
        }
        return set;
    },
    /**
     * Replaces the current resource in history.
     *
     * @param {String} token The value that will replace the current resource in the history state.
     * @param {Boolean} [preventDuplicates=true] When `true`, if the passed token matches
     * the current token it will not save a new history step. Set to `false` if the same state
     * can be saved more than once at the same history stack location.
     *
     * @return {Boolean} Whether the token was set in the case if the current token matches
     * the token passed.
     */
    replace: function(token, preventDuplicates) {
        var me = this,
            set = false;
        if (preventDuplicates === false || me.getToken() !== token) {
            this.setHash(token, true);
            set = true;
        }
        return set;
    },
    /**
     * Programmatically steps back one step in browser history (equivalent to the user pressing
     * the Back button).
     */
    back: function() {
        this.win.history.go(-1);
    },
    /**
     * Programmatically steps forward one step in browser history (equivalent to the user pressing
     * the Forward button).
     */
    forward: function() {
        this.win.history.go(1);
    },
    /**
     * Retrieves the currently-active history token.
     * @return {String} The token
     */
    getToken: function() {
        return this.ready ? this.currentToken : this.getHash();
    }
});

/**
 * The Router is an ordered set of {@link Ext.route.Route} definitions that decode a
 * url into a controller function to execute. Each `route` defines a type of url to match,
 * along with the controller function to call if it is matched. The Router uses the
 * {@link Ext.util.History} singleton to find out when the browser's url has changed.
 *
 * Routes are almost always defined inside a {@link Ext.Controller Controller}, as
 * opposed to on the Router itself. End-developers should not usually need to interact
 * directly with the Router as the Controllers manage everything automatically. See the
 * {@link Ext.Controller Controller documentation} for more information on specifying
 * routes.
 */
Ext.define('Ext.route.Router', {
    singleton: true,
    requires: [
        'Ext.route.Action',
        'Ext.route.Route',
        'Ext.util.History'
    ],
    /**
     * @event beforeroutes
     * @member Ext.GlobalEvents
     *
     * Fires when the hash has changed and before any routes are executed. This allows
     * pre-processing to add additional {@link Ext.route.Action#before before} or
     * {@link Ext.route.Action#action action} handlers when the
     * {@link Ext.route.Action Action} is run.
     *
     * Route execution can be prevented by returning `false` in the listener
     * or executing the {@link Ext.route.Action#stop stop} method on the action.
     *
     * @param {Ext.route.Action} action An action that will be executed
     * prior to any route execution.
     * @param {String[]} tokens An array of individual tokens in the hash.
     */
    /**
     * @event routereject
     * @member Ext.GlobalEvents
     *
     * Fires when a route was rejected from either a before action,
     * {@link Ext.GlobalEvents#beforeroutes} event or {@link Ext.GlobalEvents#beforeroute} event.
     *
     * @param {Ext.route.Route} route The route which had it's execution rejected.
     */
    config: {
        /**
         * @cfg {Boolean} hashBang Sets {@link Ext.util.History#hashbang} to enable/disable
         * hashbang support.
         */
        hashbang: null,
        /**
         * @cfg {String} [multipleToken=|] The token to split the routes to support multiple routes.
         */
        multipleToken: '|',
        /**
         * @cfg {Boolean} [queueRoutes=true] `true` to queue routes to be executed one after the
         * other, false to execute routes immediately.
         */
        queueRoutes: true
    },
    /**
     * @property {Object} routes The connected {@link Ext.route.Route}
     * instances.
     */
    /**
     * @property {Boolean} isSuspended `true` if the router is currently suspended.
     */
    constructor: function() {
        var History = Ext.util.History;
        if (!History.ready) {
            History.init();
        }
        History.on('change', this.onStateChange, this);
        this.initConfig();
        this.clear();
    },
    updateHashbang: function(hashbang) {
        Ext.util.History.hashbang = hashbang;
    },
    /**
     * React to a token
     *
     * @private
     * @param {String} token The token to react to.
     */
    onStateChange: function(token) {
        var me = this,
            tokens = token.split(me.getMultipleToken()),
            queue, i, length;
        if (me.isSuspended) {
            queue = me.suspendedQueue;
            i = 0;
            length = tokens.length;
            if (queue) {
                for (; i < length; i++) {
                    token = tokens[i];
                    // shouldn't keep track of duplicates
                    if (!Ext.Array.contains(queue, token)) {
                        queue.push(token);
                    }
                }
            }
        } else {
            me.handleBefore(tokens);
        }
    },
    /**
     * Fires the {@link Ext.GlobalEvents#beforeroutes} event and if
     * `false` is returned can prevent any routes from executing.
     *
     * @private
     * @param {String[]} tokens The individual tokens that were split from the hash
     * using {@link #multipleToken}.
     */
    handleBefore: function(tokens) {
        var me = this,
            action = new Ext.route.Action();
        if (Ext.fireEvent('beforeroutes', action, tokens) === false) {
            action.destroy();
        } else {
            action.run().then(me.handleBeforeRoute.bind(me, tokens), Ext.emptyFn);
        }
    },
    /**
     * If a wildcard route was connected, that route needs to execute prior
     * to any other route.
     *
     * @private
     * @param {String[]} tokens The individual tokens that were split from the hash
     * using {@link #multipleToken}.
     */
    handleBeforeRoute: function(tokens) {
        var me = this,
            beforeRoute = me.getByName('*');
        if (beforeRoute) {
            beforeRoute.execute().then(me.doRun.bind(me, tokens), Ext.emptyFn);
        } else {
            // no befores, go ahead with route determination
            me.doRun(tokens);
        }
    },
    /**
     * Find routes that recognize one of the tokens in the document fragment
     * and then exeucte the routes.
     *
     * @private
     * @param {String[]} tokens The individual tokens that were split from the hash
     * using {@link #multipleToken}.
     */
    doRun: function(tokens) {
        var me = this,
            app = me.application,
            routes = me.routes,
            i = 0,
            length = tokens.length,
            matched = {},
            unmatched = [],
            token, found, name, route, recognize;
        for (; i < length; i++) {
            token = tokens[i];
            found = false;
            for (name in routes) {
                route = routes[name];
                recognize = route.recognize(token);
                if (recognize) {
                    found = true;
                    if (recognize !== true) {
                        // The document fragment may have changed but the token
                        // part that the route recognized did not change. Therefore
                        // is was matched but we should not execute the route again.
                        route.execute(token, recognize).then(null, Ext.bind(me.onRouteRejection, me, [
                            route
                        ], 0));
                    }
                    Ext.Array.remove(unmatched, route);
                    if (!matched[name]) {
                        matched[name] = 1;
                    }
                } else if (!matched[name]) {
                    unmatched.push(route);
                }
            }
            if (!found) {
                if (app) {
                    // backwards compat
                    app.fireEvent('unmatchedroute', token);
                }
                Ext.fireEvent('unmatchedroute', token);
            }
        }
        i = 0;
        length = unmatched.length;
        for (; i < length; i++) {
            unmatched[i].onExit();
        }
    },
    /**
     * @private
     * Called when a route was rejected.
     */
    onRouteRejection: function(route, error) {
        Ext.fireEvent('routereject', route, error);
        if (error) {
            Ext.raise(error);
        }
    },
    /**
     * Create the {@link Ext.route.Route} instance and connect to the
     * {@link Ext.route.Router} singleton.
     *
     * @param {String} url The url to recognize.
     * @param {String} config The config on the controller to execute when the url is
     * matched.
     * @param {Ext.Base} instance The class instance associated with the
     * {@link Ext.route.Route}
     * @return {Ext.route.Handler} The handler that was added.
     */
    connect: function(url, config, instance) {
        var routes = this.routes,
            delimiter = this.getMultipleToken(),
            name = config.name || url,
            handler, route;
        if (url[0] === '!') {
            if (!Ext.util.History.hashbang) {
                Ext.log({
                    level: 'error',
                    msg: 'Route found with "!" ("' + url + '"). Should use new hashbang functionality instead. ' + 'Please see the router guide for more: https://docs.sencha.com/extjs/' + Ext.getVersion().version + '/guides/application_architecture/router.html'
                });
            }
            url = url.substr(1);
            this.setHashbang(true);
        }
        if (Ext.isString(config)) {
            config = {
                action: config
            };
        }
        handler = Ext.route.Handler.fromRouteConfig(config, instance);
        route = routes[name];
        if (!route) {
            config.name = name;
            config.url = url;
            route = routes[name] = new Ext.route.Route(config);
        }
        route.addHandler(handler);
        if (handler.lazy) {
            // eslint-disable-next-line vars-on-top
            var currentHash = Ext.util.History.getToken(),
                tokens = currentHash.split(delimiter),
                length = tokens.length,
                matched = [],
                i, token;
            for (i = 0; i < length; i++) {
                token = tokens[i];
                if (Ext.Array.indexOf(matched, token) === -1 && route.recognize(token)) {
                    matched.push(token);
                }
            }
            this.onStateChange(matched.join(delimiter));
        }
        return handler;
    },
    /**
     * Disconnects all route handlers for a class instance.
     *
     * @param {Ext.Base} instance The class instance to disconnect route handlers from.
     * @param {Object/Ext.route.Handler} [config]
     * An optional config object to match a handler for. This will check all route
     * handlers connected to the instance for match based on the action and before
     * configurations. This can also be the actual {@link Ext.route.Handler handler}
     * instance.
     */
    disconnect: function(instance, config) {
        var routes = this.routes,
            route, name;
        if (config) {
            route = config.route || this.getByName(config.name || config.url);
            if (route) {
                route.removeHandler(instance, config);
            }
        } else {
            for (name in routes) {
                route = routes[name];
                route.removeHandler(instance);
            }
        }
    },
    /**
     * Recognizes a url string connected to the Router, return the controller/action pair
     * plus any additional config associated with it.
     *
     * @param {String} url The url to recognize.
     * @return {Object/Boolean} If the url was recognized, the controller and action to
     * call, else `false`.
     */
    recognize: function(url) {
        var routes = this.routes,
            matches = [],
            name, arr, i, length, route, urlParams;
        for (name in routes) {
            arr = routes[name];
            length = arr && arr.length;
            if (length) {
                i = 0;
                for (; i < length; i++) {
                    route = arr[i];
                    urlParams = route.recognize(url);
                    if (urlParams) {
                        matches.push({
                            route: route,
                            urlParams: urlParams
                        });
                    }
                }
            }
        }
        return matches.length ? matches : false;
    },
    /**
     * Convenience method which just calls the supplied function with the
     * {@link Ext.route.Router} singleton. Example usage:
     *
     *     Ext.route.Router.draw(function(map) {
     *         map.connect('activate/:token', {controller: 'users', action: 'activate'});
     *         map.connect('home',            {controller: 'index', action: 'home'});
     *     });
     *
     * @param {Function} fn The function to call
     */
    draw: function(fn) {
        fn.call(this, this);
    },
    /**
     * Clear all the recognized routes.
     */
    clear: function() {
        this.routes = {};
    },
    /**
     * Resets the connected routes' last token they were executed on.
     * @param {String} [token] If passed, only clear matching routes.
     * @private
     */
    clearLastTokens: function(token) {
        var routes = this.routes,
            name, route;
        for (name in routes) {
            route = routes[name];
            if (!token || route.recognize(token)) {
                route.clearLastTokens();
            }
        }
    },
    /**
     * Gets all routes by {@link Ext.route.Route#name}.
     *
     * @return {Ext.route.Route[]} If no routes found, `undefined` will be returned otherwise
     * the array of {@link Ext.route.Route Routes} will be returned.
     */
    getByName: function(name) {
        var routes = this.routes;
        if (routes) {
            return routes[name];
        }
    },
    /**
     * Suspends the handling of tokens (see {@link #resume}).
     *
     * @param {Boolean} [trackTokens] `false` to prevent any tokens to be
     * queued while being suspended.
     */
    suspend: function(trackTokens) {
        this.isSuspended = true;
        if (!this.suspendedQueue && trackTokens !== false) {
            this.suspendedQueue = [];
        }
    },
    /**
     * Resumes the execution of routes (see {@link #suspend}).
     *
     * @param {Boolean} [discardQueue] `true` to prevent any previously queued
     * tokens from being enacted on.
     */
    resume: function(discardQueue) {
        var me = this,
            queue = me.suspendedQueue,
            token;
        if (me.isSuspended) {
            me.isSuspended = false;
            me.suspendedQueue = null;
            if (!discardQueue && queue) {
                token = queue.join(me.getMultipleToken());
                me.onStateChange(token);
            }
        }
    }
});

/**
 * A mixin to allow any class to configure and listen to routes and also change the hash.
 */
Ext.define('Ext.route.Mixin', {
    extend: 'Ext.Mixin',
    requires: [
        'Ext.route.Handler',
        'Ext.route.Router'
    ],
    mixinConfig: {
        id: 'routerable',
        before: {
            destroy: 'destroyRouterable'
        }
    },
    config: {
        /**
         * @cfg {Object} routes
         * @accessor
         *
         * An object of routes to handle hash changes. A route can be defined in a simple way:
         *
         *     routes: {
         *         'foo/bar': 'handleFoo',
         *         'user/:id': 'showUser'
         *     }
         *
         * Where the property is the hash (which can accept a parameter defined by a colon)
         * and the value is the method on the controller to execute. The parameters will get sent
         * in the action method.
         *
         * If no routes match a given hash, an {@link Ext.GlobalEvents#unmatchedroute} event
         * will be fired. This can be listened to in four ways:
         *
         *     Ext.on('unmatchedroute', function(token) {});
         *
         *     Ext.define('MyApp.controller.Foo', {
         *         extend: 'Ext.app.Controller',
         *
         *         listen: {
         *             global: {
         *                 unmatchedroute: 'onUnmatchedRoute'
         *             }
         *         },
         *
         *         onUnmatchedRoute: function(token) {}
         *     });
         *
         *     Ext.application({
         *         name: 'MyApp',
         *
         *         listen: {
         *             global: {
         *                 unmatchedroute: 'onUnmatchedRoute'
         *             }
         *         },
         *
         *         onUnmatchedRoute: function(token) {}
         *     });
         *
         *     Ext.application({
         *         name: 'MyApp',
         *
         *         listeners: {
         *             unmatchedroute: 'onUnmatchedRoute'
         *         },
         *
         *         onUnmatchedRoute: function(token) {}
         *     });
         *
         * There is also a complex means of defining a route where you can use a before action
         * and even specify your own RegEx for the parameter:
         *
         *     routes: {
         *         'foo/bar': {
         *             action: 'handleFoo',
         *             before: 'beforeHandleFoo'
         *         },
         *         'user/:id': {
         *             action: 'showUser',
         *             before: 'beforeShowUser',
         *             conditions: {
         *                 ':id': '([0-9]+)'
         *             }
         *         }
         *     }
         *
         * This will only match if the `id` parameter is a number.
         *
         * The before action allows you to cancel an action. Every before action will get passed
         * an `action` argument with a `resume` and `stop` methods as the last argument of the
         * method and you *MUST* execute either method:
         *
         *     beforeHandleFoo: function (action) {
         *         // some logic here
         *
         *         // this will allow the handleFoo action to be executed
         *         action.resume();
         *     },
         *     handleFoo: function () {
         *         // will get executed due to true being passed in callback in beforeHandleFoo
         *     },
         *     beforeShowUser: function (id, action) {
         *         // allows for async process like an Ajax
         *         Ext.Ajax.request({
         *             url: 'foo.php',
         *             success: function () {
         *                 // will not allow the showUser method to be executed
         *                 // but will continue other queued actions.
         *                 action.stop();
         *             },
         *             failure: function () {
         *                 // will not allow the showUser method to be executed
         *                 // and will not allow other queued actions to be executed.
         *                 action.stop(true);
         *             }
         *         });
         *     },
         *     showUser: function (id) {
         *         // will not get executed due to false being passed in callback in beforeShowUser
         *     }
         *
         * You **MUST** execute the `{@link Ext.route.Action#resume resume}` or
         * `{@link Ext.route.Action#stop stop}` method on the `action` argument. Executing
         * `action.resume();` will continue the action, `action.stop();` will prevent
         * further execution.
         *
         * The default RegEx that will be used is `([%a-zA-Z0-9\\-\\_\\s,]+)` but you can specify
         * any that may suit what you need to accomplish. An example of an advanced condition
         * may be to make a parameter optional and case-insensitive:
         *
         *     routes: {
         *         'user:id': {
         *             action: 'showUser',
         *             before: 'beforeShowUser',
         *             conditions: {
         *                 ':id': '(?:(?:\/){1}([%a-z0-9_,\s\-]+))?'
         *             }
         *         }
         *     }
         *
         * Each route can be named; this allows for the route to be looked up by name instead of
         * url. By default, the route's name will be the url you configure but you can provide
         * the `{@link Ext.route.Route#name name}` config to override the default:
         *
         *     routes: {
         *         'user:id': {
         *             action: 'showUser',
         *             before: 'beforeShowUser',
         *             name: 'user',
         *             conditions: {
         *                 ':id': '(?:(?:\/){1}([%a-z0-9_,\s\-]+))?'
         *             }
         *         }
         *     }
         *
         * The `user:id` route can not be looked up via the `user` name which is useful when using
         * `{@link #redirectTo}`.
         *
         * A wildcard route can also be defined which works exactly like any other route but will
         * always execute before any other route.  To specify a wildcard route, use the `*`
         * as the url:
         *
         *     routes: {
         *         '*': 'onToken'
         *     }
         *
         * Since a wildcard route will execute before any other route, it can delay the execution
         * of other routes allowing for such things like a user session to be retrieved:
         *
         *     routes: {
         *         '*': {
         *             before: 'onBeforeToken'
         *         }
         *     },
         *
         *     onBeforeToken: function () {
         *         return Ext.Ajax.request({
         *             url: '/user/session'
         *         });
         *     }
         *
         * In the above example, no other route will execute unless that
         * {@link Ext.Ajax#request request} returns successfully.
         *
         * You can also use a wildcard route if you need to defer routes until a store has been
         * loaded when an application first starts up:
         *
         *     routes: {
         *         '*': {
         *             before: 'onBeforeToken'
         *         }
         *     },
         *
         *     onBeforeToken: function (action) {
         *         var store = Ext.getStore('Settings');
         *
         *         if (store.loaded) {
         *             action.resume();
         *         } else {
         *             store.on('load', action.resume, action, { single: true });
         *         }
         *     }
         *
         * The valid options are configurations from {@link Ext.route.Handler} and
         * {@link Ext.route.Route}.
         */
        routes: null
    },
    destroyRouterable: function() {
        Ext.route.Router.disconnect(this);
    },
    applyRoutes: function(routes, oldRoutes) {
        var Router = Ext.route.Router,
            url;
        if (routes) {
            for (url in routes) {
                routes[url] = Router.connect(url, routes[url], this);
            }
        }
        if (oldRoutes) {
            for (url in oldRoutes) {
                Router.disconnect(this, oldRoutes[url]);
            }
        }
        return routes;
    },
    /**
     * Update the hash. By default, it will not execute the routes if the current token and the
     * token passed are the same.
     *
     * @param {String/Number/Object/Ext.data.Model} hash The hash to redirect to. The hash can be
     * of several values:
     *  - **String** The hash to exactly be set to.
     *  - **Number** If `1` is passed, {@link Ext.util.History#forward forward} function will be
     * executed. If `-1` is passed, {@link Ext.util.History#bck back} function will be executed.
     *  - **Ext.data.Model** If a model instance is passed, the Model's
     * {@link Ext.data.Model#toUrl toUrl} function will be executed to convert it into a String
     * value to set the hash to.
     *  - **Object** An Object can be passed to control individual tokens in the full hash.
     * The key should be an associated {@link Ext.route.Route Route}'s
     * {@link Ext.route.Route#name name} and the value should be the value of that token
     * in the complete hash. For example, if you have two routes configured, each token in the
     * hash that can be matched for each route can be individually controlled:
     *
     *     routes: {
     *         'foo/bar': 'onFooBar',
     *         'baz/:id': {
     *             action: 'onBaz',
     *             name: 'baz'
     *         }
     *     }
     *
     * If you pass in a hash of `#foo/bar|baz/1`, each route will execute in response. If you want
     * to change only the `baz` route but leave the `foo/bar` route in the hash, you can pass only
     * the `baz` key in an object:
     *
     *     this.redirectTo({
     *         baz : 'baz/5'
     *     });
     *
     * and the resulting hash will be `#foo/bar/|baz/5` and only the `baz` route will execute
     * in reaction but the `foo/bar` will not react since it's associated token in the hash
     * remained the same. If you wanted to update the `baz` route and remove `foo/bar`
     * from the hash, you can set the value to `null`:
     *
     *     this.redirectTo({
     *         'foo/bar': null,
     *         baz: 'baz/3'
     *     });
     *
     * and the resulting hash will be `#baz/3`. Like before, the `baz` route will execute
     * in reaction.
     *
     * @param {Object} opt An optional `Object` describing how to enact the hash being passed in.
     * Valid options are:
     *
     *  - `force` Even if the hash will not change, setting this to `true` will force the
     * {@link Ext.route.Router Router} to react.
     *  - `replace` When set to `true`, this will replace the current resource in the history stack
     * with the hash being set.
     *
     * For backwards compatibility, if `true` is passed instead of an `Object`, this will set
     * the `force` option to `true`.
     *
     * @return {Boolean} Will return `true` if the token was updated.
     */
    redirectTo: function(hash, opt) {
        var currentHash = Ext.util.History.getToken(),
            Router = Ext.route.Router,
            delimiter = Router.getMultipleToken(),
            tokens = currentHash ? currentHash.split(delimiter) : [],
            length = tokens.length,
            force, i, name, obj, route, token, match;
        if (hash === -1) {
            return Ext.util.History.back();
        } else if (hash === 1) {
            return Ext.util.History.forward();
        } else if (hash.isModel) {
            hash = hash.toUrl();
        } else if (Ext.isObject(hash)) {
            // Passing an object attempts to replace a token in the hash.
            for (name in hash) {
                obj = hash[name];
                if (!Ext.isObject(obj)) {
                    obj = {
                        token: obj
                    };
                }
                if (length) {
                    route = Router.getByName(name);
                    if (route) {
                        match = false;
                        for (i = 0; i < length; i++) {
                            token = tokens[i];
                            if (route.matcherRegex.test(token)) {
                                match = true;
                                if (obj.token) {
                                    // a token was found in the hash, replace it
                                    if (obj.fn && obj.fn.call(this, token, tokens, obj) === false) {
                                        // if the fn returned false, skip update
                                        
                                        continue;
                                    }
                                    tokens[i] = obj.token;
                                    if (obj.force) {
                                        // clear lastToken to force recognition
                                        route.lastToken = null;
                                    }
                                } else {
                                    // remove token
                                    tokens.splice(i, 1);
                                    i--;
                                    length--;
                                    // reset lastToken
                                    route.lastToken = null;
                                }
                            }
                        }
                        if (obj && obj.token && !match) {
                            // a token was not found in the hash, push to the end
                            tokens.push(obj.token);
                        }
                    }
                } else if (obj && obj.token) {
                    // there is no current hash, push to the end
                    tokens.push(obj.token);
                }
            }
            hash = tokens.join(delimiter);
        }
        if (opt === true) {
            // for backwards compatibility
            force = opt;
            opt = null;
        } else if (opt) {
            force = opt.force;
        }
        length = tokens.length;
        if (force && length) {
            for (i = 0; i < length; i++) {
                token = tokens[i];
                Router.clearLastTokens(token);
            }
        }
        if (currentHash === hash) {
            if (force) {
                // hash won't change, trigger handling anyway
                Router.onStateChange(hash);
            }
            // hash isn't going to change, return false
            return false;
        }
        if (opt && opt.replace) {
            Ext.util.History.replace(hash);
        } else {
            Ext.util.History.add(hash);
        }
        return true;
    },
    privates: {
        afterClassMixedIn: function(targetClass) {
            var proto = targetClass.prototype,
                routes = proto.routes;
            if (routes) {
                delete proto.routes;
                targetClass.getConfigurator().add({
                    routes: routes
                });
            }
        }
    }
});

/**
 * @protected
 * @class Ext.app.BaseController
 * Base class for Controllers.
 * 
 */
Ext.define('Ext.app.BaseController', {
    requires: [
        'Ext.app.EventBus',
        'Ext.app.domain.Global'
    ],
    uses: [
        'Ext.app.domain.Controller'
    ],
    mixins: [
        'Ext.mixin.Observable',
        'Ext.route.Mixin'
    ],
    isController: true,
    config: {
        /**
         * @cfg {String} id The id of this controller. You can use this id when dispatching.
         * 
         * For an example of dispatching, see the examples under the 
         * {@link Ext.app.Controller#cfg-listen listen} config.
         *
         * If an id is not explicitly set, it will default to the controller's full classname.
         * 
         * @accessor
         */
        id: undefined,
        /**
         * @cfg {Object} control
         * @accessor
         *
         * Adds listeners to components selected via {@link Ext.ComponentQuery}. Accepts an
         * object containing component paths mapped to a hash of listener functions.  
         * The function value may also be a string matching the name of a method on the 
         * controller.
         *
         * In the following example the `updateUser` function is mapped to to the `click`
         * event on a button component, which is a child of the `useredit` component.
         *
         *      Ext.define('MyApp.controller.Users', {
         *          extend: 'Ext.app.Controller',
         *
         *          control: {
         *              'useredit button[action=save]': {
         *                  click: 'updateUser'
         *              }
         *          },
         *
         *          updateUser: function(button) {
         *              console.log('clicked the Save button');
         *          }
         *      });
         *
         * The method you pass to the listener will automatically be resolved on the controller.
         * In this case, the `updateUser` method that will get executed on the `button` `click`
         * event will resolve to the `updateUser` method on the controller,
         *
         * See {@link Ext.ComponentQuery} for more information on component selectors.
         */
        control: null,
        /**
         * @cfg {Object} listen
         * @accessor
         *
         * Adds listeners to different event sources (also called "event domains"). The
         * primary event domain is that of components, but there are also other event domains:
         * {@link Ext.app.domain.Global Global} domain that intercepts events fired from
         * {@link Ext.GlobalEvents} Observable instance, 
         * {@link Ext.app.domain.Controller Controller} domain can be used to listen to events 
         * fired by other Controllers, {@link Ext.app.domain.Store Store} domain gives access to 
         * Store events, and {@link Ext.app.domain.Direct Direct} domain can be used with 
         * Ext Direct Providers to listen to their events.
         *
         * To listen to "bar" events fired by a controller with id="foo":
         *
         *      Ext.define('AM.controller.Users', {
         *          extend: 'Ext.app.Controller',
         *
         *          listen: {
         *              controller: {
         *                  '#foo': {
         *                      bar: 'onFooBar'
         *                  }
         *              }
         *          }
         *      });
         *
         * To listen to "bar" events fired by any controller, and "baz" events
         * fired by Store with storeId="baz":
         *
         *      Ext.define('AM.controller.Users', {
         *          extend: 'Ext.app.Controller',
         *
         *          listen: {
         *              controller: {
         *                  '*': {
         *                      bar: 'onAnyControllerBar'
         *                  }
         *              },
         *              store: {
         *                  '#baz': {
         *                      baz: 'onStoreBaz'
         *                  }
         *              }
         *          }
         *      });
         *
         * To listen to "idle" events fired by {@link Ext.GlobalEvents} when other event
         * processing is complete and Ext JS is about to return control to the browser:
         *
         *      Ext.define('AM.controller.Users', {
         *          extend: 'Ext.app.Controller',
         *
         *          listen: {
         *              global: {            // Global events are always fired
         *                  idle: 'onIdle'   // from the same object, so there
         *              }                    // are no selectors
         *          }
         *      });
         *
         * As this relates to components, the following example:
         *
         *      Ext.define('AM.controller.Users', {
         *          extend: 'Ext.app.Controller',
         *
         *          listen: {
         *              component: {
         *                  'useredit button[action=save]': {
         *                      click: 'updateUser'
         *                  }
         *              }
         *          }
         *      });
         *
         * Is equivalent to:
         *
         *      Ext.define('AM.controller.Users', {
         *          extend: 'Ext.app.Controller',
         *
         *          control: {
         *              'useredit button[action=save]': {
         *                  click: 'updateUser'
         *              }
         *          }
         *      });
         *
         * Of course, these can all be combined in a single call and used instead of
         * `control`, like so:
         *
         *      Ext.define('AM.controller.Users', {
         *          extend: 'Ext.app.Controller',
         *
         *          listen: {
         *              global: {
         *                  idle: 'onIdle'
         *              },
         *              controller: {
         *                  '*': {
         *                      foobar: 'onAnyFooBar'
         *                  },
         *                  '#foo': {
         *                      bar: 'onFooBar'
         *                  }
         *              },
         *              component: {
         *                  'useredit button[action=save]': {
         *                      click: 'updateUser'
         *                  }
         *              },
         *              store: {
         *                  '#qux': {
         *                      load: 'onQuxLoad'
         *                  }
         *              }
         *          }
         *      });
         */
        listen: null
    },
    /**
     * Creates new Controller.
     *
     * @param {Object} [config] Configuration object.
     */
    constructor: function(config) {
        var me = this;
        // In versions prior to 5.1, this constructor used to call the Ext.util.Observable
        // constructor (which applied the config properties directly to the instance)
        // AND it used to call initConfig as well.  Since the constructor of
        // Ext.mixin.Observable calls initConfig, but does not apply the properties to
        // the instance, we do that here for backward compatibility.
        Ext.apply(me, config);
        // The control and listen properties are also methods so we need to delete them
        // from the instance after applying the config object.
        delete me.control;
        delete me.listen;
        me.eventbus = Ext.app.EventBus;
        // need to have eventbus property set before we initialize the config
        me.mixins.observable.constructor.call(me, config);
    },
    updateId: function(id) {
        this.id = id;
    },
    applyListen: function(listen) {
        if (Ext.isObject(listen)) {
            listen = Ext.clone(listen);
        }
        return listen;
    },
    applyControl: function(control) {
        if (Ext.isObject(control)) {
            control = Ext.clone(control);
        }
        return control;
    },
    /**
     * @param {Object} control The object to pass to the {@link #method-control} method
     * @private
     */
    updateControl: function(control) {
        this.getId();
        if (control) {
            this.control(control);
        }
    },
    /**
     * @param {Object} listen The object to pass to the {@link #method-listen} method
     * @private
     */
    updateListen: function(listen) {
        this.getId();
        if (listen) {
            this.listen(listen);
        }
    },
    isActive: function() {
        return true;
    },
    /**
     * Adds listeners to components selected via {@link Ext.ComponentQuery}. Accepts an
     * object containing component paths mapped to a hash of listener functions.
     *
     * In the following example the `updateUser` function is mapped to to the `click`
     * event on a button component, which is a child of the `useredit` component.
     *
     *      Ext.define('AM.controller.Users', {
     *          init: function() {
     *              this.control({
     *                  'useredit button[action=save]': {
     *                      click: this.updateUser
     *                  }
     *              });
     *          },
     *          
     *          updateUser: function(button) {
     *              console.log('clicked the Save button');
     *          }
     *      });
     *
     * Or alternatively one call `control` with two arguments:
     *
     *      this.control('useredit button[action=save]', {
     *          click: this.updateUser
     *      });
     *
     * See {@link Ext.ComponentQuery} for more information on component selectors.
     *
     * @param {String/Object} selectors If a String, the second argument is used as the
     * listeners, otherwise an object of selectors -> listeners is assumed
     * @param {Object} [listeners] Config for listeners.
     * @param {Ext.app.BaseController} [controller] (private)
     */
    control: function(selectors, listeners, controller) {
        var me = this,
            ctrl = controller,
            obj;
        if (Ext.isString(selectors)) {
            obj = {};
            obj[selectors] = listeners;
        } else {
            obj = selectors;
            ctrl = listeners;
        }
        me.eventbus.control(obj, ctrl || me);
    },
    /**
     * Adds listeners to different event sources (also called "event domains"). The
     * primary event domain is that of components, but there are also other event domains:
     * {@link Ext.app.domain.Global Global} domain that intercepts events fired from
     * {@link Ext.GlobalEvents} Observable instance, {@link Ext.app.domain.Controller Controller}
     * domain can be used to listen to events fired by other Controllers,
     * {@link Ext.app.domain.Store Store} domain gives access to Store events, and
     * {@link Ext.app.domain.Direct Direct} domain can be used with Ext Direct Providers
     * to listen to their events.
     * 
     * To listen to "bar" events fired by a controller with id="foo":
     *
     *      Ext.define('AM.controller.Users', {
     *          init: function() {
     *              this.listen({
     *                  controller: {
     *                      '#foo': {
     *                         bar: this.onFooBar
     *                      }
     *                  }
     *              });
     *          },
     *          ...
     *      });
     * 
     * To listen to "bar" events fired by any controller, and "baz" events
     * fired by Store with storeId="baz":
     *
     *      Ext.define('AM.controller.Users', {
     *          init: function() {
     *              this.listen({
     *                  controller: {
     *                      '*': {
     *                         bar: this.onAnyControllerBar
     *                      }
     *                  },
     *                  store: {
     *                      '#baz': {
     *                          baz: this.onStoreBaz
     *                      }
     *                  }
     *              });
     *          },
     *          ...
     *      });
     *
     * To listen to "idle" events fired by {@link Ext.GlobalEvents} when other event
     * processing is complete and Ext JS is about to return control to the browser:
     *
     *      Ext.define('AM.controller.Users', {
     *          init: function() {
     *              this.listen({
     *                  global: {               // Global events are always fired
     *                      idle: this.onIdle   // from the same object, so there
     *                  }                       // are no selectors
     *              });
     *          }
     *      });
     * 
     * As this relates to components, the following example:
     *
     *      Ext.define('AM.controller.Users', {
     *          init: function() {
     *              this.listen({
     *                  component: {
     *                      'useredit button[action=save]': {
     *                         click: this.updateUser
     *                      }
     *                  }
     *              });
     *          },
     *          ...
     *      });
     * 
     * Is equivalent to:
     *
     *      Ext.define('AM.controller.Users', {
     *          init: function() {
     *              this.control({
     *                  'useredit button[action=save]': {
     *                     click: this.updateUser
     *                  }
     *              });
     *          },
     *          ...
     *      });
     *
     * Of course, these can all be combined in a single call and used instead of
     * `control`, like so:
     *
     *      Ext.define('AM.controller.Users', {
     *          init: function() {
     *              this.listen({
     *                  global: {
     *                      idle: this.onIdle
     *                  },
     *                  controller: {
     *                      '*': {
     *                         foobar: this.onAnyFooBar
     *                      },
     *                      '#foo': {
     *                         bar: this.onFooBar
     *                      }
     *                  },
     *                  component: {
     *                      'useredit button[action=save]': {
     *                         click: this.updateUser
     *                      }
     *                  },
     *                  store: {
     *                      '#qux': {
     *                          load: this.onQuxLoad
     *                      }
     *                  }
     *              });
     *          },
     *          ...
     *      });
     *
     * @param {Object} to Config object containing domains, selectors and listeners.
     * @param {Ext.app.Controller} [controller] The controller to add the listeners to. Defaults
     * to the current controller.
     */
    listen: function(to, controller) {
        this.eventbus.listen(to, controller || this);
    },
    destroy: function() {
        var me = this,
            bus = me.eventbus;
        if (bus) {
            bus.unlisten(me);
            me.eventbus = null;
        }
        me.callParent();
    }
});

/**
 * @private 
 * @class Ext.app.Util
 */
Ext.define('Ext.app.Util', {}, function() {
    Ext.apply(Ext.app, {
        namespaces: {
            Ext: {}
        },
        /**
        * Adds namespace(s) to known list.
        * @private
        *
        * @param {String/String[]} namespace
        */
        addNamespaces: function(namespace) {
            var namespaces = Ext.app.namespaces,
                i, l;
            if (!Ext.isArray(namespace)) {
                namespace = [
                    namespace
                ];
            }
            for (i = 0 , l = namespace.length; i < l; i++) {
                namespaces[namespace[i]] = true;
            }
        },
        /**
        * Clear all namespaces from known list.
        * @private
        */
        clearNamespaces: function() {
            Ext.app.namespaces = {};
        },
        /**
        * Get namespace prefix for a class name.
        * @private
        * @param {String} className
        *
        * @return {String} Namespace prefix if it's known, otherwise undefined
        */
        getNamespace: function(className) {
            var namespaces = Ext.apply({}, Ext.ClassManager.paths, Ext.app.namespaces),
                deepestPrefix = '',
                prefix;
            for (prefix in namespaces) {
                if (namespaces.hasOwnProperty(prefix) && prefix.length > deepestPrefix.length && (prefix + '.' === className.substring(0, prefix.length + 1))) {
                    deepestPrefix = prefix;
                }
            }
            return deepestPrefix === '' ? undefined : deepestPrefix;
        },
        /**
         * Sets up paths based on the `appFolder` and `paths` configs.
         * @param {String} appName The application name (root namespace).
         * @param {String} appFolder The folder for app sources ("app" by default).
         * @param {Object} paths A set of namespace to path mappings.
         * @private
         * @since 6.0.0
         */
        setupPaths: function(appName, appFolder, paths) {
            var manifestPaths = Ext.manifest,
                ns;
            // Ignore appFolder:null
            if (appName && appFolder !== null) {
                manifestPaths = manifestPaths && manifestPaths.paths;
                // If the manifest has paths, only honor appFolder if defined. If the
                // manifest has no paths (old school mode), then we want to default an
                // unspecified appFolder value to "app". Sencha Cmd will pass in paths
                // to configure the loader via the "paths" property of the manifest so
                // we don't want to try and be "helpful" in that case.
                if (!manifestPaths || appFolder !== undefined) {
                    Ext.Loader.setPath(appName, (appFolder === undefined) ? 'app' : appFolder);
                }
            }
            if (paths) {
                for (ns in paths) {
                    if (paths.hasOwnProperty(ns)) {
                        Ext.Loader.setPath(ns, paths[ns]);
                    }
                }
            }
        }
    });
    /**
     * @method getNamespace
     * @member Ext
     * @param {String} className
     *
     * @return {String} Namespace prefix if it's known, otherwise undefined
     */
    Ext.getNamespace = Ext.app.getNamespace;
});

/**
 * Maintains an additional key map for an `Ext.util.Collection`. Instances of this class
 * are seldom created manually. Rather they are created by the `Ext.util.Collection' when
 * given an `extraKeys` config.
 *
 * @since 5.0.0
 */
Ext.define('Ext.util.CollectionKey', {
    mixins: [
        'Ext.mixin.Identifiable'
    ],
    isCollectionKey: true,
    observerPriority: -200,
    config: {
        collection: null,
        /**
         * @cfg {Function/String} [keyFn]
         * A function to retrieve the key of an item in the collection. This can be normal
         * function that takes an item and returns the key or it can be the name of the
         * method to call on an item to get the key.
         *
         * For example:
         *
         *      new Ext.util.Collection({
         *          keys: {
         *              byName: {
         *                  keyFn: 'getName' // each item has a "getName" method
         *              }
         *          }
         *      });
         *
         * Or equivalently:
         *
         *      new Ext.util.Collection({
         *          keys: {
         *              byName: {
         *                  keyFn: function (item) {
         *                      return item.getName();
         *                  }
         *              }
         *          }
         *      });
         *
         * @since 5.0.0
         */
        keyFn: null,
        /**
         * @cfg {String} property
         * The name of the property on each item that is its key.
         *
         *      new Ext.util.Collection({
         *          keys: {
         *              byName: 'name'
         *          }
         *      });
         *
         * Or equivalently:
         *
         *      new Ext.util.Collection({
         *          keys: {
         *              byName: {
         *                  property: 'name'
         *              }
         *          }
         *      });
         *
         *      var item = collection.byName.get('fooname');
         */
        property: null,
        /**
         * @cfg {String} rootProperty
         * The name of the sub-object property on each item that is its key. This value
         * overrides `{@link Ext.util.Collection#rootProperty}`.
         *
         *      new Ext.util.Collection({
         *          keys: {
         *              byName: {
         *                  property: 'name',
         *                  rootProperty: 'data'
         *              }
         *          }
         *      });
         *
         *      var item = collection.byName.get('fooname');
         */
        rootProperty: null,
        unique: true
    },
    /**
     * This property is used to know when this `Index` is in sync with the `Collection`.
     * When the two are synchronized, their `generation` values match.
     * @private
     * @readonly
     * @since 5.0.0
     */
    generation: 0,
    /**
     * @property {Object} map
     * An object used as map to get an object based on its key.
     * @since 5.0.0
     * @private
     */
    map: null,
    /**
     * @property {Number} mapRebuilds
     * The number of times the `map` has been rebuilt. This is for diagnostic use.
     * @private
     * @readonly
     */
    mapRebuilds: 0,
    /**
     * @property {String} name
     * This property is set by `Ext.util.Collection` when added via `extraKeys`.
     * @readonly
     */
    constructor: function(config) {
        this.initConfig(config);
        if (!Ext.isFunction(this.getKey)) {
            Ext.raise('CollectionKey requires a keyFn or property config');
        }
    },
    /**
     * Returns the item or, if not `unique` possibly array of items that have the given
     * key.
     * @param {Mixed} key The key that will match the `keyFn` return value or value of
     * the specified `property`.
     * @return {Object}
     */
    get: function(key) {
        var map = this.map || this.getMap();
        return map[key] || null;
    },
    /**
     * @private
     * Clears this index;
     *
     * Called by {@link Ext.util.Collection#clear} when the collection is cleared.
     */
    clear: function() {
        this.map = null;
    },
    getRootProperty: function() {
        var me = this,
            root = this.callParent();
        return root !== null ? root : me.getCollection().getRootProperty();
    },
    /**
     * Returns the index of the item with the given key in the collection. If this is not
     * a `unique` result, the index of the first item in the collection with the matching
     * key.
     *
     * To iterate the indices of all items with a matching (not `unique`) key:
     *
     *      for (index = collection.byName.indexOf('foo');
     *              index >= 0;
     *              index = collection.byName.indexOf('foo', index)) {
     *          // process item at "index"
     *      }
     *
     * @param {Mixed} key The key that will match the `keyFn` return value or value of
     * the specified `property`.
     * @param {Number} [startAt=-1] The index at which to start. Only occurrences beyond
     * this index are returned.
     * @return {Number} The index of the first item with the given `key` beyond the given
     * `startAt` index or -1 if there are no such items.
     */
    indexOf: function(key, startAt) {
        var map = this.map || this.getMap(),
            item = map[key],
            collection = this.getCollection(),
            length = collection.length,
            i, index, items, n;
        if (!item) {
            return -1;
        }
        if (startAt === undefined) {
            startAt = -1;
        }
        if (item instanceof Array) {
            items = item;
            index = length;
            // greater than any actual indexOf
            for (n = items.length; n-- > 0; ) {
                i = collection.indexOf(items[n]);
                if (i < index && i > startAt) {
                    index = i;
                }
            }
            if (index === length) {
                return -1;
            }
        } else {
            index = collection.indexOf(item);
        }
        return (index > startAt) ? index : -1;
    },
    /**
     * Change the key for an existing item in the collection. If the old key does not
     * exist this call does nothing.
     * @param {Object} item The item whose key has changed.
     * @param {String} oldKey The old key for the `item`.
     * @since 5.0.0
     */
    updateKey: function(item, oldKey) {
        var me = this,
            map = me.map,
            bucket, index;
        if (map) {
            bucket = map[oldKey];
            if (bucket instanceof Array) {
                index = Ext.Array.indexOf(bucket, item);
                if (index >= 0) {
                    if (bucket.length > 2) {
                        bucket.splice(index, 1);
                    } else {
                        // If there is an array of 2 items, replace the array with the
                        // one remaining item. Since index then is either 0 or 1, the
                        // index of the other item is easy.
                        map[oldKey] = bucket[1 - index];
                    }
                }
            }
            // "1 - 0" = 1, "1 - 1" = 0
            else if (bucket) {
                if (me.getUnique() && bucket !== item) {
                    Ext.raise('Incorrect oldKey "' + oldKey + '" for item with newKey "' + me.getKey(item) + '"');
                }
                delete map[oldKey];
            }
            me.add([
                item
            ]);
        }
    },
    //-------------------------------------------------------------------------
    // Calls from our Collection:
    onCollectionAdd: function(collection, add) {
        if (this.map) {
            this.add(add.items);
        }
    },
    onCollectionItemChange: function(collection, details) {
        this.map = null;
    },
    onCollectionRefresh: function() {
        this.map = null;
    },
    onCollectionRemove: function(collection, remove) {
        var me = this,
            map = me.map,
            items = remove.items,
            length = items.length,
            i, key;
        if (map) {
            if (me.getUnique() && length < collection.length / 2) {
                for (i = 0; i < length; ++i) {
                    key = me.getKey(items[i]);
                    delete map[key];
                }
            } else {
                me.map = null;
            }
        }
    },
    //-------------------------------------------------------------------------
    // Private
    add: function(items) {
        var me = this,
            map = me.map,
            bucket, i, item, key, length, unique;
        length = items.length;
        unique = me.getUnique();
        for (i = 0; i < length; ++i) {
            key = me.getKey(item = items[i]);
            if (unique || !(key in map)) {
                map[key] = item;
            } else {
                if (!((bucket = map[key]) instanceof Array)) {
                    map[key] = bucket = [
                        bucket
                    ];
                }
                bucket.push(item);
            }
        }
    },
    applyKeyFn: function(keyFn) {
        if (Ext.isString(keyFn)) {
            this.getKey = function(item) {
                return item[keyFn]();
            };
        } else {
            this.getKey = keyFn;
        }
    },
    updateProperty: function(property) {
        var root = this.getRootProperty();
        this.getKey = function(item) {
            return (root ? item[root] : item)[property];
        };
    },
    getMap: function() {
        var me = this,
            map = me.map;
        if (!map) {
            me.map = map = {};
            me.keysByItemKey = {};
            ++me.mapRebuilds;
            me.add(me.getCollection().items);
        }
        return map;
    },
    updateCollection: function(collection, oldCollection) {
        if (collection) {
            collection.addObserver(this);
        }
        if (oldCollection) {
            oldCollection.removeObserver(this);
        }
    },
    clone: function() {
        return new Ext.util.CollectionKey(this.getCurrentConfig());
    },
    destroy: function() {
        this.clear();
        this.getCollection().removeObserver(this);
        this.destroyed = true;
    }
});

/**
 * Represents a grouping of items. The grouper works in a similar fashion as the
 * `Ext.util.Sorter` except that groups must be able to extract a value by which all items
 * in the group can be collected. By default this is derived from the `property` config
 * but can be customized using the `groupFn` if necessary.
 *
 * All items with the same group value compare as equal. If the group values do not compare
 * equally, the sort can be controlled further by setting `sortProperty` or `sorterFn`.
 */
Ext.define('Ext.util.Grouper', {
    extend: 'Ext.util.Sorter',
    isGrouper: true,
    config: {
        /**
         * @cfg {Function} groupFn This function is called for each item in the collection
         * to determine the group to which it belongs. By default the `property` value is
         * used to group items.
         * @cfg {Object} groupFn.item The current item from the collection.
         * @cfg {String} groupFn.return The group identifier for the item.
         */
        groupFn: null,
        /**
         * @cfg {String} property The field by which records are grouped. Groups are 
         * sorted alphabetically by group value as the default. To sort groups by a different 
         * property, use the {@link #sortProperty} configuration.
         */
        /**
         * @cfg {String} sortProperty You can set this configuration if you want the groups
         * to be sorted on something other then the group string returned by the `groupFn`.
         * This serves the same role as `property` on a normal `Ext.util.Sorter`.
         */
        sortProperty: null
    },
    constructor: function(config) {
        if (config) {
            if (config.getGroupString) {
                Ext.raise("Cannot set getGroupString - use groupFn instead");
            }
        }
        this.callParent(arguments);
    },
    /**
     * Returns the value for grouping to be used.
     * @param {Ext.data.Model} item The Model instance
     * @return {String}
     */
    getGroupString: function(item) {
        var group = item.$collapsedGroupPlaceholder ? item.$groupKey : this._groupFn(item);
        return (group != null) ? String(group) : '';
    },
    sortFn: function(item1, item2) {
        var me = this,
            lhs = me.getGroupString(item1),
            rhs = me.getGroupString(item2),
            property = me._sortProperty,
            // Sorter's sortFn uses "_property"
            root = me._root,
            sorterFn = me._sorterFn,
            transform = me._transform;
        // Items with the same groupFn result must be equal... otherwise we sort them
        // by sorterFn or sortProperty.
        if (lhs === rhs) {
            return 0;
        }
        if (property || sorterFn) {
            if (sorterFn) {
                return sorterFn.call(this, item1, item2);
            }
            if (root) {
                item1 = item1[root];
                item2 = item2[root];
            }
            lhs = item1[property];
            rhs = item2[property];
            if (transform) {
                lhs = transform(lhs);
                rhs = transform(rhs);
            }
        }
        return (lhs > rhs) ? 1 : (lhs < rhs ? -1 : 0);
    },
    standardGroupFn: function(item) {
        var root = this._root;
        return (root ? item[root] : item)[this._property];
    },
    updateSorterFn: function() {},
    // don't callParent here - we don't want to smash sortFn w/sorterFn
    updateProperty: function() {
        // we don't callParent since that is related to sorterFn smashing sortFn
        if (!this.getGroupFn()) {
            this.setGroupFn(this.standardGroupFn);
        }
    }
});

/**
 * This class manages uniquely keyed objects such as {@link Ext.data.Model records} or
 * {@link Ext.Component components}.
 *
 * ## Keys
 *
 * Unlike `Ext.util.MixedCollection` this class can only manage objects whose key can be
 * extracted from the instance. That is, this class does not support "external" keys. This
 * makes this class more efficient because it does not need to track keys in parallel with
 * items. It also means key-to-item lookup will be optimal and never need to perform a
 * linear search.
 *
 * ### Extra Keys
 *
 * In some cases items may need to be looked up by multiple property values. To enable this
 * there is the `extraKeys` config.
 *
 * For example, to quickly look up items by their "name" property:
 *
 *      var collection = new Ext.util.Collection({
 *          extraKeys: {
 *              byName: 'name' // based on "name" property of each item
 *          }
 *      });
 *
 * ## Ranges
 *
 * When methods accept index arguments to indicate a range of items, these are either an
 * index and a number of items or a "begin" and "end" index.
 *
 * In the case of "begin" and "end", the "end" is the first item outside the range. This
 * definition makes it simple to expression empty ranges because "length = end - begin".
 *
 * ### Negative Indices
 *
 * When an item index is provided, negative values are treated as offsets from the end of
 * the collection. In other words the follow are equivalent:
 *
 *      +---+---+---+---+---+---+
 *      |   |   |   |   |   |   |
 *      +---+---+---+---+---+---+
 *        0   1   2   3   4   5
 *       -6  -5  -4  -3  -2  -1
 *
 * ## Legacy Classes
 *
 * The legacy classes `Ext.util.MixedCollection' and `Ext.util.AbstractMixedCollection`
 * may be needed if external keys are required, but for all other situations this class
 * should be used instead.
 */
Ext.define('Ext.util.Collection', {
    mixins: [
        'Ext.mixin.Observable'
    ],
    requires: [
        'Ext.util.CollectionKey',
        'Ext.util.Filter',
        'Ext.util.Sorter',
        'Ext.util.Grouper'
    ],
    uses: [
        'Ext.util.SorterCollection',
        'Ext.util.FilterCollection',
        'Ext.util.GroupCollection'
    ],
    /**
     * @property {Boolean} isCollection
     * `true` in this class to identify an object as an instantiated Collection, or subclass
     * thereof.
     * @readonly
     */
    isCollection: true,
    config: {
        autoFilter: true,
        /**
         * @cfg {Boolean} [autoSort=true] `true` to maintain sorted order when items
         * are added regardless of requested insertion point, or when an item mutation
         * results in a new sort position.
         *
         * This does not affect a filtered Collection's reaction to mutations of the source
         * Collection. If sorters are present when the source Collection is mutated,
         * this Collection's sort order will always be maintained.
         * @private
         */
        autoSort: true,
        /**
         * @cfg {Boolean} [autoGroup=true] `true` to sort by the grouper
         * @private
         */
        autoGroup: true,
        /**
         * @cfg {Function} decoder
         * A function that can convert newly added items to a proper type before being
         * added to this collection.
         */
        decoder: null,
        /**
         * @cfg {Object} extraKeys
         * One or more `Ext.util.CollectionKey` configuration objects or key properties.
         * Each property of the given object is the name of the `CollectionKey` instance
         * that is stored on this collection. The value of each property configures the
         * `CollectionKey` instance.
         *
         *      var collection = new Ext.util.Collection({
         *          extraKeys: {
         *              byName: 'name' // based on "name" property of each item
         *          }
         *      });
         *
         * Or equivalently:
         *
         *      var collection = new Ext.util.Collection({
         *          extraKeys: {
         *              byName: {
         *                  property: 'name'
         *              }
         *          }
         *      });
         *
         * To provide a custom key extraction function instead:
         *
         *      var collection = new Ext.util.Collection({
         *          extraKeys: {
         *              byName: {
         *                  keyFn: function (item) {
         *                      return item.name;
         *                  }
         *              }
         *          }
         *      });
         *
         * Or to call a key getter method from each item:
         *
         *      var collection = new Ext.util.Collection({
         *          extraKeys: {
         *              byName: {
         *                  keyFn: 'getName'
         *              }
         *          }
         *      });
         *
         * To use the above:
         *
         *      var item = collection.byName.get('somename');
         *
         * **NOTE** Either a `property` or `keyFn` must be be specified to define each
         * key.
         * @since 5.0.0
         */
        extraKeys: null,
        /**
         * @cfg {Array/Ext.util.FilterCollection} filters
         * The collection of {@link Ext.util.Filter Filters} for this collection. At the
         * time a collection is created `filters` can be specified as a unit. After that
         * time the normal `setFilters` method can also be given a set of replacement
         * filters for the collection.
         *
         * Individual filters can be specified as an `Ext.util.Filter` instance, a config
         * object for `Ext.util.Filter` or simply a function that will be wrapped in a
         * instance with its {@link Ext.util.Filter#filterFn filterFn} set.
         *
         * For fine grain control of the filters collection, call `getFilters` to return
         * the `Ext.util.Collection` instance that holds this collection's filters.
         *
         *      var collection = new Ext.util.Collection();
         *      var filters = collection.getFilters(); // an Ext.util.FilterCollection
         *
         *      function legalAge (item) {
         *          return item.age >= 21;
         *      }
         *
         *      filters.add(legalAge);
         *
         *      //...
         *
         *      filters.remove(legalAge);
         *
         * Any changes to the `filters` collection will cause this collection to adjust
         * its items accordingly (if `autoFilter` is `true`).
         * @since 5.0.0
         */
        filters: null,
        /**
         * @cfg {Object} grouper
         * A configuration object for this collection's {@link Ext.util.Grouper grouper}.
         *
         * For example, to group items by the first letter of the last name:
         *
         *      var collection = new Ext.util.Collection({
         *          grouper: {
         *              groupFn: function (item) {
         *                  return item.lastName.substring(0, 1);
         *              }
         *          }
         *      });
         */
        grouper: null,
        /**
         * @cfg {Ext.util.GroupCollection} groups
         * The collection of to hold each group container. This collection is created and
         * removed dynamically based on `grouper`. Application code should only need to
         * call `getGroups` to retrieve the collection and not `setGroups`.
         */
        groups: null,
        /**
         * @cfg {Object} groupConfig
         * A default configuration to be passed to any groups created by the
         * {@link Ext.util.GroupCollection}. See {@link #groups}.
         *
         * @private
         * @since 6.5.0
         */
        groupConfig: null,
        /**
         * @cfg {String} rootProperty
         * The root property to use for aggregation, filtering and sorting. By default
         * this is `null` but when containing things like {@link Ext.data.Model records}
         * this config would likely be set to "data" so that property names are applied
         * to the fields of each record.
         */
        rootProperty: null,
        /**
         * @cfg {Array/Ext.util.SorterCollection} sorters
         * Array of {@link Ext.util.Sorter sorters} for this collection. At the time a
         * collection is created the `sorters` can be specified as a unit. After that time
         * the normal `setSorters` method can be also be given a set of replacement
         * sorters.
         *
         * Individual sorters can be specified as an `Ext.util.Sorter` instance, a config
         * object for `Ext.util.Sorter` or simply the name of a property by which to sort.
         *
         * An alternative way to extend the sorters is to call the `sort` method and pass
         * a property or sorter config to add to the sorters.
         *
         * For fine grain control of the sorters collection, call `getSorters` to return
         * the `Ext.util.Collection` instance that holds this collection's sorters.
         *
         *      var collection = new Ext.util.Collection();
         *      var sorters = collection.getSorters(); // an Ext.util.SorterCollection
         *
         *      sorters.add('name');
         *
         *      //...
         *
         *      sorters.remove('name');
         *
         * Any changes to the `sorters` collection will cause this collection to adjust
         * its items accordingly (if `autoSort` is `true`).
         *
         * @since 5.0.0
         */
        sorters: null,
        /**
         * @cfg {Number} [multiSortLimit=3]
         * The maximum number of sorters which may be applied to this Sortable when using
         * the "multi" insertion position when adding sorters.
         *
         * New sorters added using the "multi" insertion position are inserted at the top
         * of the sorters list becoming the new primary sort key.
         *
         * If the sorters collection has grown to longer then **`multiSortLimit`**, then
         * the it is trimmed.
         */
        multiSortLimit: 3,
        /**
         * @cfg {String} defaultSortDirection
         * The default sort direction to use if one is not specified.
         */
        defaultSortDirection: 'ASC',
        /**
         * @cfg {Ext.util.Collection} source
         * The base `Collection`. This collection contains the items to which filters
         * are applied to populate this collection. In this configuration, only the
         * root `source` collection can have items truly added or removed.
         * @since 5.0.0
         */
        source: null,
        /**
         * @cfg {Boolean} trackGroups
         * `true` to track individual groups in a Ext.util.GroupCollection
         * @private
         */
        trackGroups: true
    },
    /**
     * @property {Number} generation
     * Mutation counter which is incremented when the collection changes.
     * @readonly
     * @since 5.0.0
     */
    generation: 0,
    /**
     * @property {Object} indices
     * An object used as map to get the index of an item.
     * @private
     * @since 5.0.0
     */
    indices: null,
    /**
     * @property {Number} indexRebuilds
     * The number of times the `indices` have been rebuilt. This is for diagnostic use.
     * @private
     * @readonly
     * @since 5.0.0
     */
    indexRebuilds: 0,
    /**
     * @property {Number} updating
     * A counter that is increased by `beginUpdate` and decreased by `endUpdate`. When
     * this transitions from 0 to 1 the `{@link #event-beginupdate beginupdate}` event is
     * fired. When it transitions back from 1 to 0 the `{@link #event-endupdate endupdate}`
     * event is fired.
     * @readonly
     * @since 5.0.0
     */
    updating: 0,
    /**
     * @property {Boolean} grouped
     * A read-only flag indicating if this object is grouped.
     * @readonly
     */
    grouped: false,
    /**
     * @property {Boolean} sorted
     * A read-only flag indicating if this object is sorted. This flag may not be correct
     * during an update of the sorter collection but will be correct before `onSortChange`
     * is called. This flag is `true` if `grouped` is `true` because the collection is at
     * least sorted by the `grouper`.
     * @readonly
     */
    sorted: false,
    /**
     * @property {Boolean} filtered
     * A read-only flag indicating if this object is filtered.
     * @readonly
     */
    filtered: false,
    /**
     * @private
     * Priority that is used for endupdate listeners on the filters and sorters.
     * set to a very high priority so that our processing of these events takes place prior
     * to user code - data must already be filtered/sorted when the user's handler runs
     */
    $endUpdatePriority: 1001,
    /**
     * @private
     * `true` to destroy the sorter collection on destroy.
     */
    manageSorters: true,
    /**
     * @event add
     * Fires after items have been added to the collection.
     *
     * All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
     * `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
     * events so it is best to do only the minimal amount of work in response to these
     * events and move the more expensive side-effects to an `endupdate` listener.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @param {Object} details An object describing the addition.
     *
     * @param {Number} details.at The index in the collection where the add occurred.
     *
     * @param {Object} details.atItem The item after which the new items were inserted or
     * `null` if at the beginning of the collection.
     * 
     * @param {Object[]} details.items The items that are now added to the collection.
     *
     * @param {Array} [details.keys] If available this array holds the keys (extracted by
     * `getKey`) for each item in the `items` array.
     *
     * @param {Object} [details.next] If more `{@link #event-add add}` events are in queue
     * to be delivered this is a reference to the `details` instance for the next
     * `{@link #event-add add}` event. This will only be the case when the collection is
     * sorted as the new items often need to be inserted at multiple locations to maintain
     * the sort. In this case, all of the new items have already been added not just those
     * described by the first `{@link #event-add add}` event.
     *
     * @param {Object} [details.replaced] If this addition has a corresponding set of
     * `{@link #event-remove remove}` events this reference holds the `details` object for
     * the first `remove` event. That `details` object may have a `next` property if there
     * are multiple associated `remove` events.
     *
     * @since 5.0.0
     */
    /**
     * @event beginupdate
     * Fired before changes are made to the collection. This event fires when the
     * `beginUpdate` method is called and the counter it manages transitions from 0 to 1.
     *
     * All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
     * `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
     * events so it is best to do only the minimal amount of work in response to these
     * events and move the more expensive side-effects to an `endupdate` listener.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @since 5.0.0
     */
    /**
     * @event endupdate
     * Fired after changes are made to the collection. This event fires when the `endUpdate`
     * method is called and the counter it manages transitions from 1 to 0.
     *
     * All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
     * `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
     * events so it is best to do only the minimal amount of work in response to these
     * events and move the more expensive side-effects to an `endupdate` listener.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @since 5.0.0
     */
    /**
     * @event beforeitemchange
     * This event fires before an item change is reflected in the collection. This event
     * is always followed by an `itemchange` event and, depending on the change, possibly
     * an `add`, `remove` and/or `updatekey` event.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @param {Object} details An object describing the change.
     *
     * @param {Object} details.item The item that has changed.
     *
     * @param {String} details.key The key of the item that has changed.
     *
     * @param {Boolean} details.filterChanged This is `true` if the filter status of the
     * `item` has changed. That is, the item was previously filtered out and is no longer
     * or the opposite.
     *
     * @param {Ext.util.Group} details.group The group containing the `item`. **(since 6.5.1)**
     *
     * @param {Boolean} details.groupChanged This is `true` if the item is moving between
     * groups. See also the `group` and `oldGroup` properties. **(since 6.5.1)**
     *
     * @param {Boolean} details.keyChanged This is `true` if the item has changed keys. If
     * so, check `oldKey` for the old key. An `updatekey` event will follow.
     *
     * @param {Boolean} details.indexChanged This is `true` if the item needs to move to
     * a new index in the collection due to sorting. The index can be seen in `index`.
     * The old index is in `oldIndex`.
     *
     * @param {String[]} [details.modified] If known this property holds the array of names
     * of the modified properties of the item.
     *
     * @param {Boolean} [details.filtered] This value is `true` if the item will be filtered
     * out of the collection.
     *
     * @param {Number} [details.index] The new index in the collection for the item if
     * the item is being moved (see `indexChanged`). If the item is being removed due to
     * filtering, this will be -1.
     *
     * @param {Ext.util.Group} details.oldGroup The group that previously contained the
     * `item`. **(since 6.5.1)**
     *
     * @param {Number} [details.oldIndex] The old index in the collection for the item if
     * the item is being moved (see `indexChanged`). If the item was being removed due to
     * filtering, this will be -1.
     *
     * @param {Object} [details.oldKey] The old key for the `item` if the item's key has
     * changed (see `keyChanged`).
     *
     * @param {Boolean} [details.wasFiltered] This value is `true` if the item was filtered
     * out of the collection.
     *
     * @since 5.0.0
     */
    /**
     * @event itemchange
     * This event fires after an item change is reflected in the collection. This event
     * always follows a `beforeitemchange` event and its corresponding `add`, `remove`
     * and/or `updatekey` events.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @param {Object} details An object describing the change.
     *
     * @param {Object} details.item The item that has changed.
     *
     * @param {String} details.key The key of the item that has changed.
     *
     * @param {Boolean} details.filterChanged This is `true` if the filter status of the
     * `item` has changed. That is, the item was previously filtered out and is no longer
     * or the opposite.
     *
     * @param {Ext.util.Group} details.group The group containing the `item`. **(since 6.5.1)**
     *
     * @param {Boolean} details.groupChanged This is `true` if the item is moving between
     * groups. See also the `group` and `oldGroup` properties. **(since 6.5.1)**
     *
     * @param {Object} details.keyChanged This is `true` if the item has changed keys. If
     * so, check `oldKey` for the old key. An `updatekey` event will have been sent.
     *
     * @param {Boolean} details.indexChanged This is `true` if the item was moved to a
     * new index in the collection due to sorting. The index can be seen in `index`.
     * The old index is in `oldIndex`.
     *
     * @param {String[]} [details.modified] If known this property holds the array of names
     * of the modified properties of the item.
     *
     * @param {Boolean} [details.filtered] This value is `true` if the item is filtered
     * out of the collection.
     *
     * @param {Number} [details.index] The new index in the collection for the item if
     * the item has been moved (see `indexChanged`). If the item is removed due to
     * filtering, this will be -1.
     *
     * @param {Ext.util.Group} details.oldGroup The group that previously contained the
     * `item`. **(since 6.5.1)**
     *
     * @param {Number} [details.oldIndex] The old index in the collection for the item if
     * the item has been moved (see `indexChanged`). If the item was being removed due to
     * filtering, this will be -1.
     *
     * @param {Object} [details.oldKey] The old key for the `item` if the item's key has
     * changed (see `keyChanged`).
     *
     * @param {Boolean} [details.wasFiltered] This value is `true` if the item was filtered
     * out of the collection.
     *
     * @since 5.0.0
     */
    /**
     * @event refresh
     * This event fires when the collection has changed entirely. This event is fired in
     * cases where the collection's filter is updated or the items are sorted. While the
     * items previously in the collection may remain the same, the order at a minimum has
     * changed in ways that cannot be simply translated to other events.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     */
    /**
     * @event remove
     * Fires after items have been removed from the collection. Some properties of this
     * object may not be present if calculating them is deemed too expensive. These are
     * marked as "optional".
     *
     * All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
     * `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
     * events so it is best to do only the minimal amount of work in response to these
     * events and move the more expensive side-effects to an `endupdate` listener.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @param {Object} details An object describing the removal.
     *
     * @param {Number} details.at The index in the collection where the removal occurred.
     *
     * @param {Object[]} details.items The items that are now removed from the collection.
     *
     * @param {Array} [details.keys] If available this array holds the keys (extracted by
     * `getKey`) for each item in the `items` array.
     *
     * @param {Object} [details.map] If available this is a map keyed by the key of each
     * item in the `items` array. This will often contain all of the items being removed
     * and not just the items in the range described by this event. The value held in this
     * map is the item.
     *
     * @param {Object} [details.next] If more `{@link #event-remove remove}` events are in
     * queue to be delivered this is a reference to the `details` instance for the next
     * remove event.
     *
     * @param {Object} [details.replacement] If this removal has a corresponding
     * `{@link #event-add add}` taking place this reference holds the `details` object for
     * that `add` event. If the collection is sorted, the new items are pre-sorted but the
     * `at` property for the `replacement` will **not** be correct. The new items will be
     * added in one or more chunks at their proper index.
     *
     * @since 5.0.0
     */
    /**
     * @event sort
     * This event fires after the contents of the collection have been sorted.
     *
     * @param {Ext.util.Collection} collection The collection being sorted.
     */
    /**
     * @event beforesort
     * @private
     * This event fires before the contents of the collection have been sorted.
     *
     * @param {Ext.util.Collection} collection The collection being sorted.
     * @param {Ext.util.Sorter[]} sorters Array of sorters applied to the Collection.
     */
    /**
     * @event updatekey
     * Fires after the key for an item has changed.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @param {Object} details An object describing the update.
     *
     * @param {Object} details.item The item whose key has changed.
     *
     * @param {Object} details.newKey The new key for the `item`.
     *
     * @param {Object} details.oldKey The old key for the `item`.
     *
     * @since 5.0.0
     */
    constructor: function(config) {
        var me = this;
        me.callParent([
            config
        ]);
        /**
         * @property {Object[]} items
         * An array containing the items.
         * @private
         * @since 5.0.0
         */
        me.items = [];
        /**
         * @property {Object} map
         * An object used as a map to find items based on their key.
         * @private
         * @since 5.0.0
         */
        me.map = {};
        /**
         * @property {Number} length
         * The count of items in the collection.
         * @readonly
         * @since 5.0.0
         */
        me.length = 0;
        /**
         * @cfg {Function} [keyFn]
         * A function to retrieve the key of an item in the collection. If provided,
         * this replaces the default `getKey` method. The default `getKey` method handles
         * items that have either an "id" or "_id" property or failing that a `getId`
         * method to call.
         * @since 5.0.0
         */
        if (config && config.keyFn) {
            me.getKey = config.keyFn;
        }
        me.mixins.observable.constructor.call(me, config);
    },
    /**
     * Destroys this collection. This is only necessary if this collection uses a `source`
     * collection as that relationship will keep a reference from the `source` to this
     * collection and potentially leak memory.
     * @since 5.0.0
     */
    destroy: function() {
        var me = this,
            filters = me._filters,
            sorters = me._sorters,
            groups = me._groups;
        if (filters) {
            filters.destroy();
            me._filters = null;
        }
        if (sorters) {
            // Set to false here so updateSorters doesn't trigger
            // the template methods
            me.grouped = me.sorted = false;
            me.setSorters(null);
            if (me.manageSorters) {
                sorters.destroy();
            }
        }
        if (groups) {
            groups.destroy();
            me._groups = null;
        }
        me.setSource(null);
        me.observers = me.items = me.map = null;
        me.callParent();
    },
    /**
     * Adds an item to the collection. If the item already exists or an item with the
     * same key exists, the old item will be removed and the new item will be added to
     * the end.
     *
     * This method also accepts an array of items or simply multiple items as individual
     * arguments. The following 3 code sequences have the same end result:
     *
     *      // Call add() once per item (not optimal - best avoided):
     *      collection.add(itemA);
     *      collection.add(itemB);
     *      collection.add(itemC);
     *      collection.add(itemD);
     *
     *      // Call add() with each item as an argument:
     *      collection.add(itemA, itemB, itemC, itemD);
     *
     *      // Call add() with the items as an array:
     *      collection.add([ itemA, itemB, itemC, itemD ]);
     *
     * The first form should be avoided where possible because the collection and all
     * parties "watching" it will be updated 4 times.
     *
     * @param {Object/Object[]} item The item or items to add.
     * @return {Object/Object[]} The item or items added.
     * @since 5.0.0
     */
    add: function(item) {
        var me = this,
            items = me.decodeItems(arguments, 0),
            ret = items;
        if (items.length) {
            me.splice(me.length, 0, items);
            ret = (items.length === 1) ? items[0] : items;
        }
        return ret;
    },
    /**
     * Adds an item to the collection while removing any existing items.
     * Similar to {@link #method-add}.
     * @param {Object/Object[]} item The item or items to add.
     * @return {Object/Object[]} The item or items added.
     * @since 5.0.0
     */
    replaceAll: function() {
        var me = this,
            ret, items;
        items = me.decodeItems(arguments, 0);
        ret = items;
        if (items.length) {
            me.splice(0, me.length, items);
            ret = (items.length === 1) ? items[0] : items;
        } else {
            me.removeAll();
        }
        return ret;
    },
    /**
     * Returns the result of the specified aggregation operation against all items in this
     * collection.
     *
     * This method is not typically called directly because there are convenience methods
     * for each of the supported `operation` values. These are:
     *
     *   * **average** - Returns the average value.
     *   * **bounds**  - Returns an array of `[min, max]`.
     *   * **max**     - Returns the maximum value or `undefined` if empty.
     *   * **min**     - Returns the minimum value or `undefined` if empty.
     *   * **sum**     - Returns the sum of all values.
     *
     * For example:
     *
     *      result = collection.aggregate('age', 'sum');
     *
     *      result = collection.aggregate('age', 'sum', 2, 10); // the 8 items at index 2
     *
     * To provide a custom operation function:
     *
     *      function averageAgeOfMinors (items, values) {
     *          var sum = 0,
     *              count = 0;
     *
     *          for (var i = 0; i < values.length; ++i) {
     *              if (values[i] < 18) {
     *                  sum += values[i];
     *                  ++count;
     *              }
     *          }
     *
     *          return count ? sum / count : 0;
     *      }
     *
     *      result = collection.aggregate('age', averageAgeOfMinors);
     *
     * @param {String} property The name of the property to aggregate from each item.
     * @param {String/Function} operation The operation to perform.
     * @param {Array} operation.items The items on which the `operation` function is to
     * operate.
     * @param {Array} operation.values The values on which the `operation` function is to
     * operate.
     * @param {Number} [begin] The index of the first item in `items` to include in the
     * aggregation.
     * @param {Number} [end] The index at which to stop aggregating `items`. The item at
     * this index will *not* be included in the aggregation.
     * @param {Object} [scope] The `this` pointer to use if `operation` is a function.
     * Defaults to this collection.
     * @return {Object}
     */
    aggregate: function(property, operation, begin, end, scope) {
        var me = this,
            args = Ext.Array.slice(arguments);
        args.unshift(me.items);
        return me.aggregateItems.apply(me, args);
    },
    /**
     * See {@link #aggregate}. The functionality is the same, however the aggregates are
     * provided per group. Assumes this collection has an active {@link #grouper}.
     * 
     * @param {String} property The name of the property to aggregate from each item.
     * @param {String/Function} operation The operation to perform.
     * @param {Array} operation.items The items on which the `operation` function is to
     * operate.
     * @param {Array} operation.values The values on which the `operation` function is to
     * operate.
     * @param {Object} [scope] The `this` pointer to use if `operation` is a function.
     * Defaults to this collection.
     * @return {Object}
     */
    aggregateByGroup: function(property, operation, scope) {
        var groups = this.getGroups();
        return this.aggregateGroups(groups, property, operation, scope);
    },
    /**
     * Returns the result of the specified aggregation operation against the given items.
     * For details see `aggregate`.
     *
     * @param {Array} items The items to aggregate.
     * @param {String} property The name of the property to aggregate from each item.
     * @param {String/Function} operation The operation to perform.
     * @param {Array} operation.items The items on which the `operation` function is to
     * operate.
     * @param {Array} operation.values The values on which the `operation` function is to
     * operate.
     * @param {Number} [begin] The index of the first item in `items` to include in the
     * aggregation.
     * @param {Number} [end] The index at which to stop aggregating `items`. The item at
     * this index will *not* be included in the aggregation.
     * @param {Object} [scope] The `this` pointer to use if `operation` is a function.
     * Defaults to this collection.
     * 
     * @private
     * @return {Object}
     */
    aggregateItems: function(items, property, operation, begin, end, scope) {
        var me = this,
            range = Ext.Number.clipIndices(items.length, [
                begin,
                end
            ]),
            // Only extract items into new array if a subset is required
            subsetRequested = (begin !== 0 && end !== items.length),
            i, j, rangeLen, root, value, values, valueItems;
        begin = range[0];
        end = range[1];
        if (!Ext.isFunction(operation)) {
            operation = me._aggregators[operation];
            return operation.call(me, items, begin, end, property, me.getRootProperty());
        }
        root = me.getRootProperty();
        // Preallocate values array with known set size.
        // valueItems can be just the items array is a subset has not been requested
        values = new Array(rangeLen);
        valueItems = subsetRequested ? new Array(rangeLen) : items;
        // Collect the extracted property values and the items for passing to the operation.
        for (i = begin , j = 0; i < end; ++i , j++) {
            if (subsetRequested) {
                valueItems[j] = value = items[i];
            }
            values[j] = (root ? value[root] : value)[property];
        }
        return operation.call(scope || me, items, values, 0);
    },
    /**
     * Aggregates a set of groups.
     * @param {Ext.util.GroupCollection} groups The groups
     * @param {String} property The name of the property to aggregate from each item.
     * @param {String/Function} operation The operation to perform.
     * @param {Array} operation.values The values on which the `operation` function is to
     * operate.
     * @param {Array} operation.items The items on which the `operation` function is to
     * operate.
     * @param {Number} operation.index The index in `items` at which the `operation`
     * function is to start. The `values.length` indicates the number of items involved.
     * @param {Object} [scope] The `this` pointer to use if `operation` is a function.
     * Defaults to this collection.
     * 
     * @return {Object}
     * @private
     */
    aggregateGroups: function(groups, property, operation, scope) {
        var items = groups.items,
            len = items.length,
            callDirect = !Ext.isFunction(operation),
            out = {},
            i, group, result;
        for (i = 0; i < len; ++i) {
            group = items[i];
            if (!callDirect) {
                result = this.aggregateItems(group.items, property, operation, null, null, scope);
            } else {
                result = group[operation](property);
            }
            out[group.getGroupKey()] = result;
        }
        return out;
    },
    /**
     * This method is called to indicate the start of multiple changes to the collection.
     * Application code should seldom need to call this method as it is called internally
     * when needed. If multiple collection changes are needed, consider wrapping them in
     * an `update` call rather than calling `beginUpdate` directly.
     *
     * Internally this method increments a counter that is decremented by `endUpdate`. It
     * is important, therefore, that if you call `beginUpdate` directly you match that
     * call with a call to `endUpdate` or you will prevent the collection from updating
     * properly.
     *
     * For example:
     *
     *      var collection = new Ext.util.Collection();
     *
     *      collection.beginUpdate();
     *
     *      collection.add(item);
     *      // ...
     *
     *      collection.insert(index, otherItem);
     *      //...
     *
     *      collection.endUpdate();
     *
     * @since 5.0.0
     */
    beginUpdate: function() {
        if (!this.updating++) {
            // jshint ignore:line
            this.notify('beginupdate');
        }
    },
    /**
     * Removes all items from the collection. This is similar to `removeAll` except that
     * `removeAll` fire events to inform listeners. This means that this method should be
     * called only when you are sure there are no listeners.
     * @since 5.0.0
     */
    clear: function() {
        var me = this,
            generation = me.generation,
            ret = generation ? me.items : [],
            extraKeys, indexName;
        if (generation) {
            me.items.length = me.length = 0;
            me.map = {};
            me.indices = {};
            me.generation++;
            // Clear any extraKey indices associated with this Collection
            extraKeys = me.getExtraKeys();
            if (extraKeys) {
                for (indexName in extraKeys) {
                    extraKeys[indexName].clear();
                }
            }
        }
        return ret;
    },
    /**
     * Creates a shallow copy of this collection
     * @return {Ext.util.Collection}
     * @since 5.0.0
     */
    clone: function() {
        var me = this,
            copy = new me.self(me.initialConfig);
        copy.add(me.items);
        return copy;
    },
    /**
     * Collects unique values of a particular property in this Collection.
     * @param {String} property The property to collect on
     * @param {String} root (optional) 'root' property to extract the first argument from.
     * This is used mainly when summing fields in records, where the fields are all stored
     * inside the 'data' object
     * @param {Boolean} [allowNull] Pass `true` to include `null`, `undefined` or empty
     * string values.
     * @return {Array} The unique values
     * @since 5.0.0
     */
    collect: function(property, root, allowNull) {
        var items = this.items,
            length = items.length,
            map = {},
            ret = [],
            i, strValue, value;
        for (i = 0; i < length; ++i) {
            value = items[i];
            value = (root ? value[root] : value)[property];
            strValue = String(value);
            if ((allowNull || !Ext.isEmpty(value)) && !map[strValue]) {
                map[strValue] = 1;
                ret.push(value);
            }
        }
        return ret;
    },
    /**
     * Returns true if the collection contains the passed Object as an item.
     * @param {Object} item The item to look for in the collection.
     * @return {Boolean} `true` if the collection contains the item.
     * @since 5.0.0
     */
    contains: function(item) {
        var ret = false,
            key;
        if (item != null) {
            key = this.getKey(item);
            ret = this.map[key] === item;
        }
        return ret;
    },
    /**
     * Returns true if the collection contains all the passed items. If the first argument
     * is an array, then the items in that array are checked. Otherwise, all arguments
     * passed to this method are checked.
     *
     * @param {Object.../Object[]} items The item(s) that must be in the collection.
     * @return {Boolean} `true` if the collection contains all the items.
     * @since 6.5.2
     */
    containsAll: function(items) {
        var all = Ext.isArray(items) ? items : arguments,
            i;
        for (i = all.length; i-- > 0; ) {
            if (!this.contains(all[i])) {
                return false;
            }
        }
        return true;
    },
    /**
     * Returns true if the collection contains the passed Object as a key.
     * @param {String} key The key to look for in the collection.
     * @return {Boolean} True if the collection contains the Object as a key.
     * @since 5.0.0
     */
    containsKey: function(key) {
        return key in this.map;
    },
    /**
     * Creates a new collection that is a filtered subset of this collection. The filter
     * passed can be a function, a simple property name and value, an `Ext.util.Filter`
     * instance, an array of `Ext.util.Filter` instances.
     *
     * If the passed filter is a function the second argument is its "scope" (or "this"
     * pointer). The function should return `true` given each item in the collection if
     * that item should be included in the filtered collection.
     *
     *      var people = new Ext.util.Collection();
     *
     *      people.add([
     *          { id: 1, age: 25, name: 'Ed' },
     *          { id: 2, age: 24, name: 'Tommy' },
     *          { id: 3, age: 24, name: 'Arne' },
     *          { id: 4, age: 26, name: 'Aaron' }
     *      ]);
     *
     *      // Create a collection of people who are older than 24:
     *      var oldPeople = people.createFiltered(function (item) {
     *          return item.age > 24;
     *      });
     *
     * If the passed filter is a `Ext.util.Filter` instance or array of `Ext.util.Filter`
     * instances the filter(s) are used to produce the filtered collection and there are
     * no further arguments.
     *
     * If the passed filter is a string it is understood as the name of the property by
     * which to filter. The second argument is the "value" used to compare each item's
     * property value. This comparison can be further tuned with the `anyMatch` and
     * `caseSensitive` (optional) arguments.
     *
     *     // Create a new Collection containing only the items where age == 24
     *     var middleAged = people.createFiltered('age', 24);
     *
     * Alternatively you can apply `filters` to this Collection by calling `setFilters`
     * or modifying the filter collection returned by `getFilters`.
     *
     * @param {Ext.util.Filter[]/String/Function} property A property on your objects, an
     * array of {@link Ext.util.Filter Filter} objects or a filter function.
     *
     * @param {Object} value If `property` is a function, this argument is the "scope"
     * (or "this" pointer) for the function. Otherwise this is either a `RegExp` to test
     * property values or the value with which to compare.
     *
     * @param {Boolean} [anyMatch=false] True to match any part of the string, not just
     * the beginning.
     *
     * @param {Boolean} [caseSensitive=false] True for case sensitive comparison.
     *
     * @param {Boolean} [exactMatch=false] `true` to force exact match (^ and $ characters
     * added to the regex).
     *
     * @return {Ext.util.Collection} The new, filtered collection.
     *
     * @since 5.0.0
     */
    createFiltered: function(property, value, anyMatch, caseSensitive, exactMatch) {
        var me = this,
            ret = new me.self(Ext.applyIf({
                source: null
            }, me.initialConfig)),
            root = me.getRootProperty(),
            items = me.items,
            length, i, filters, fn, scope;
        if (Ext.isFunction(property)) {
            fn = property;
            scope = value;
        } else {
            // support for the simple case of filtering by property/value
            if (Ext.isString(property)) {
                filters = [
                    new Ext.util.Filter({
                        property: property,
                        value: value,
                        root: root,
                        anyMatch: anyMatch,
                        caseSensitive: caseSensitive,
                        exactMatch: exactMatch
                    })
                ];
            } else if (property instanceof Ext.util.Filter) {
                filters = [
                    property
                ];
                property.setRoot(root);
            } else if (Ext.isArray(property)) {
                filters = property.slice(0);
                for (i = 0 , length = filters.length; i < length; ++i) {
                    filters[i].setRoot(root);
                }
            }
            // At this point we have an array of zero or more Ext.util.Filter objects to
            // filter with, so here we construct a function that combines these filters by
            // ANDing them together and filter by that.
            fn = Ext.util.Filter.createFilterFn(filters);
        }
        scope = scope || me;
        for (i = 0 , length = items.length; i < length; i++) {
            if (fn.call(scope, items[i])) {
                ret.add(items[i]);
            }
        }
        return ret;
    },
    /**
     * Filter by a function. Returns a <i>new</i> collection that has been filtered.
     * The passed function will be called with each object in the collection.
     * If the function returns true, the value is included otherwise it is filtered.
     * @param {Function} fn The function to be called.
     * @param {Mixed} fn.item The collection item.
     * @param {String} fn.key The key of collection item.
     * @param {Object} scope (optional) The scope (<code>this</code> reference) in
     * which the function is executed. Defaults to this Collection.
     * @return {Ext.util.Collection} The new filtered collection
     * @deprecated 5.0.0 This method is deprecated.
     */
    filterBy: function(fn, scope) {
        return this.createFiltered(fn, scope);
    },
    /**
     * Executes the specified function once for every item in the collection. If the value
     * returned by `fn` is `false` the iteration stops. In all cases, the last value that
     * `fn` returns is returned by this method.
     *
     * @param {Function} fn The function to execute for each item.
     * @param {Object} fn.item The collection item.
     * @param {Number} fn.index The index of item.
     * @param {Number} fn.len Total length of collection.
     * @param {Object} [scope=this] The scope (`this` reference) in which the function
     * is executed. Defaults to this collection.
     * @since 5.0.0
     */
    each: function(fn, scope) {
        var items = this.items,
            len = items.length,
            i, ret;
        if (len) {
            scope = scope || this;
            items = items.slice(0);
            // safe for re-entrant calls
            for (i = 0; i < len; i++) {
                ret = fn.call(scope, items[i], i, len);
                if (ret === false) {
                    break;
                }
            }
        }
        return ret;
    },
    /**
     * Executes the specified function once for every key in the collection, passing each
     * key, and its associated item as the first two parameters. If the value returned by
     * `fn` is `false` the iteration stops. In all cases, the last value that `fn` returns
     * is returned by this method.
     *
     * @param {Function} fn The function to execute for each item.
     * @param {String} fn.key The key of collection item.
     * @param {Object} fn.item The collection item.
     * @param {Number} fn.index The index of item.
     * @param {Number} fn.len Total length of collection.
     * @param {Object} [scope=this] The scope (`this` reference) in which the function
     * is executed. Defaults to this collection.
     * @since 5.0.0
     */
    eachKey: function(fn, scope) {
        var me = this,
            items = me.items,
            len = items.length,
            i, item, key, ret;
        if (len) {
            scope = scope || me;
            items = items.slice(0);
            // safe for re-entrant calls
            for (i = 0; i < len; i++) {
                key = me.getKey(item = items[i]);
                ret = fn.call(scope, key, item, i, len);
                if (ret === false) {
                    break;
                }
            }
        }
        return ret;
    },
    /**
     * This method is called after modifications are complete on a collection. For details
     * see `beginUpdate`.
     * @since 5.0.0
     */
    endUpdate: function() {
        if (!--this.updating) {
            this.notify('endupdate');
        }
    },
    /**
     * Finds the first matching object in this collection by a specific property/value.
     *
     * @param {String} property The name of a property on your objects.
     * @param {String/RegExp} value A string that the property values
     * should start with or a RegExp to test against the property.
     * @param {Number} [start=0] The index to start searching at.
     * @param {Boolean} [startsWith=true] Pass `false` to allow a match start anywhere in
     * the string. By default the `value` will match only at the start of the string.
     * @param {Boolean} [endsWith=true] Pass `false` to allow the match to end before the
     * end of the string. By default the `value` will match only at the end of the string.
     * @param {Boolean} [ignoreCase=true] Pass `false` to make the `RegExp` case
     * sensitive (removes the 'i' flag).
     * @return {Object} The first item in the collection which matches the criteria or
     * `null` if none was found.
     * @since 5.0.0
     */
    find: function(property, value, start, startsWith, endsWith, ignoreCase) {
        var regex, root;
        if (Ext.isEmpty(value, false)) {
            return null;
        }
        regex = Ext.String.createRegex(value, startsWith, endsWith, ignoreCase);
        root = this.getRootProperty();
        return this.findBy(function(item) {
            return item && regex.test((root ? item[root] : item)[property]);
        }, null, start);
    },
    /**
     * Returns the first item in the collection which elicits a true return value from the
     * passed selection function.
     * @param {Function} fn The selection function to execute for each item.
     * @param {Object} fn.item The collection item.
     * @param {String} fn.key The key of collection item.
     * @param {Object} [scope=this] The scope (`this` reference) in which the function
     * is executed. Defaults to this collection.
     * @param {Number} [start=0] The index at which to start searching.
     * @return {Object} The first item in the collection which returned true from the selection
     * function, or null if none was found.
     * @since 5.0.0
     */
    findBy: function(fn, scope, start) {
        var me = this,
            items = me.items,
            len = items.length,
            i, item, key;
        scope = scope || me;
        for (i = start || 0; i < len; i++) {
            key = me.getKey(item = items[i]);
            if (fn.call(scope, item, key)) {
                return items[i];
            }
        }
        return null;
    },
    /**
     * Finds the index of the first matching object in this collection by a specific
     * property/value.
     *
     * @param {String} property The name of a property on your objects.
     * @param {String/RegExp} value A string that the property values
     * should start with or a RegExp to test against the property.
     * @param {Number} [start=0] The index to start searching at.
     * @param {Boolean} [startsWith=true] Pass `false` to allow a match start anywhere in
     * the string. By default the `value` will match only at the start of the string.
     * @param {Boolean} [endsWith=true] Pass `false` to allow the match to end before the
     * end of the string. By default the `value` will match only at the end of the string.
     * @param {Boolean} [ignoreCase=true] Pass `false` to make the `RegExp` case
     * sensitive (removes the 'i' flag).
     * @return {Number} The matched index or -1 if not found.
     * @since 5.0.0
     */
    findIndex: function(property, value, start, startsWith, endsWith, ignoreCase) {
        var item = this.find(property, value, start, startsWith, endsWith, ignoreCase);
        return item ? this.indexOf(item) : -1;
    },
    /**
     * Find the index of the first matching object in this collection by a function.
     * If the function returns `true` it is considered a match.
     * @param {Function} fn The function to be called.
     * @param {Object} fn.item The collection item.
     * @param {String} fn.key The key of collection item.
     * @param {Object} [scope=this] The scope (`this` reference) in which the function
     * is executed. Defaults to this collection.
     * @param {Number} [start=0] The index at which to start searching.
     * @return {Number} The matched index or -1
     * @since 5.0.0
     */
    findIndexBy: function(fn, scope, start) {
        var item = this.findBy(fn, scope, start);
        return item ? this.indexOf(item) : -1;
    },
    /**
     * Returns the first item in the collection.
     * @param {Boolean} [grouped] `true` to extract the first item in each group. Only applies if
     * a {@link #grouper} is active in the collection.
     * @return {Object} The first item in the collection. If the grouped parameter is passed,
     * see {@link #aggregateByGroup} for information on the return type.
     * @since 5.0.0
     */
    first: function(grouped) {
        var groups = grouped ? this.getGroups() : undefined;
        return groups ? this.aggregateGroups(groups, null, 'first') : this.items[0];
    },
    /**
     * Returns the last item in the collection.
     * @param {Boolean} [grouped] `true` to extract the first item in each group. Only applies if
     * a {@link #grouper} is active in the collection.
     * @return {Object} The last item in the collection. If the grouped parameter is passed,
     * see {@link #aggregateByGroup} for information on the return type.
     * @since 5.0.0
     */
    last: function(grouped) {
        var groups = grouped ? this.getGroups() : undefined;
        return groups ? this.aggregateGroups(groups, null, 'last') : this.items[this.length - 1];
    },
    /**
     * Returns the item associated with the passed key.
     * @param {String/Number} key The key of the item.
     * @return {Object} The item associated with the passed key.
     * @since 5.0.0
     */
    get: function(key) {
        return this.map[key];
    },
    /**
     * Returns the item at the specified index.
     * @param {Number} index The index of the item.
     * @return {Object} The item at the specified index.
     * @since 5.0.0
     */
    getAt: function(index) {
        return this.items[index];
    },
    /**
     * Returns the item associated with the passed key.
     * @param {String/Number} key The key of the item.
     * @return {Object} The item associated with the passed key.
     * @since 5.0.0
     */
    getByKey: function(key) {
        return this.map[key];
    },
    /**
     * Returns the number of items in the collection.
     * @return {Number} the number of items in the collection.
     * @since 5.0.0
     */
    getCount: function() {
        return this.length;
    },
    /**
     * A function which will be called, passing an object belonging to this collection.
     * The function should return the key by which that object will be indexed. This key
     * must be unique to this item as only one item with this key will be retained.
     *
     * The default implementation looks basically like this (give or take special case
     * handling of 0):
     *
     *      function getKey (item) {
     *          return item.id || item._id || item.getId();
     *      }
     *
     * You can provide your own implementation by passing the `keyFn` config.
     *
     * For example, to hold items that have a unique "name" property:
     *
     *     var elementCollection = new Ext.util.Collection({
     *         keyFn: function (item) {
     *             return item.name;
     *         }
     *     });
     *
     * The collection can have `extraKeys` if items need to be quickly looked up by other
     * (potentially non-unique) properties.
     *
     * @param {Object} item The item.
     * @return {Object} The key for the passed item.
     * @since 5.0.0
     */
    getKey: function(item) {
        var id = item.id;
        return (id === 0 || id) ? id : ((id = item._id) === 0 || id) ? id : item.getId();
    },
    /**
     * Returns a range of items in this collection
     * @param {Number} [begin=0] The index of the first item to get.
     * @param {Number} [end] The ending index. The item at this index is *not* included.
     * @return {Array} An array of items
     * @since 5.0.0
     */
    getRange: function(begin, end) {
        var items = this.items,
            length = items.length,
            range;
        if (begin > end) {
            Ext.raise('Inverted range passed to Collection.getRange: [' + begin + ',' + end + ']');
        }
        if (!length) {
            range = [];
        } else {
            range = Ext.Number.clipIndices(length, [
                begin,
                end
            ]);
            range = items.slice(range[0], range[1]);
        }
        return range;
    },
    /**
     * @method getSource
     * Returns all unfiltered items in the Collection when the Collection has been 
     * filtered.  Returns `null` when the Collection is not filtered.
     * @return {Ext.util.Collection} items All unfiltered items (or `null` when the 
     * Collection is not filtered)
     */
    /**
     * Returns an array of values for the specified (sub) property.
     *
     * For example, to get an array of "name" properties from a collection of records (of
     * `Ext.data.Model` objects):
     *
     *      var names = collection.getValues('name', 'data');
     *
     * @param {String} property The property to collect on
     * @param {String} [root] 'root' property to extract the first argument from. This is
     * used mainly when operating on fields in records, where the fields are all stored
     * inside the 'data' object.
     * @param {Number} [start=0] The index of the first item to include.
     * @param {Number} [end] The index at which to stop getting values. The value of this
     * item is *not* included.
     * @return {Object[]} The array of values.
     * @since 5.0.0
     */
    getValues: function(property, root, start, end) {
        var items = this.items,
            range = Ext.Number.clipIndices(items.length, [
                start,
                end
            ]),
            ret = [],
            i, value;
        for (i = range[0] , end = range[1]; i < end; ++i) {
            value = items[i];
            value = (root ? value[root] : value)[property];
            ret.push(value);
        }
        return ret;
    },
    /**
     * Returns index within the collection of the passed Object.
     * @param {Object} item The item to find.
     * @return {Number} The index of the item or -1 if not found.
     * @since 5.0.0
     */
    indexOf: function(item) {
        var key;
        if (!item) {
            return -1;
        }
        key = this.getKey(item);
        return this.indexOfKey(key);
    },
    /**
     * Returns index within the collection of the passed key.
     * @param {Object} key The key to find.
     * @return {Number} The index of the item or -1 if not found.
     * @since 5.0.0
     */
    indexOfKey: function(key) {
        var me = this,
            indices = me.indices;
        if (key in me.map) {
            if (!indices) {
                indices = me.getIndices();
            }
            return indices[key];
        }
        return -1;
    },
    /**
     * Inserts one or more items to the collection. The `index` value is the position at
     * which the first item will be placed. The items starting at that position will be
     * shifted to make room.
     *
     * @param {Number} index The index at which to insert the item(s).
     * @param {Object/Object[]} item The item or items to add.
     * @return {Object/Object[]} The item or items added.
     * @since 5.0.0
     */
    insert: function(index, item) {
        var me = this,
            items = me.decodeItems(arguments, 1),
            ret = items;
        if (items.length) {
            me.splice(index, 0, items);
            ret = (items.length === 1) ? items[0] : items;
        }
        return ret;
    },
    /**
     * This method should be called when an item in this collection has been modified. If
     * the collection is sorted or filtered the result of modifying an item needs to be
     * reflected in the collection. If the item's key is also being modified, it is best
     * to pass the `oldKey` to this same call rather than call `updateKey` separately.
     *
     * @param {Object} item The item that was modified.
     * @param {String[]} [modified] The names of the modified properties of the item.
     * @param {String/Number} [oldKey] Passed if the item's key was also modified.
     * @param {Object} meta (private)
     * @since 5.0.0
     */
    itemChanged: function(item, modified, oldKey, meta) {
        var me = this,
            keyChanged = oldKey !== undefined,
            filtered = me.filtered && me.getAutoFilter(),
            filterChanged = false,
            itemMovement = 0,
            items = me.items,
            last = me.length - 1,
            // one or zero items is not really sorted
            // CAN be called on an empty Collection
            // A TreeStore can call afterEdit on a hidden root before
            // any child nodes exist in the store.
            sorted = me.sorted && last > 0 && me.getAutoSort(),
            source = me.getSource(),
            toRemove = 0,
            itemFiltered = false,
            wasFiltered = false,
            details, newKey, sortFn, toAdd, index, newIndex;
        // We are owned, we cannot react, inform owning collection.
        if (source && !source.updating) {
            me.sourceUpdating = true;
            source.itemChanged(item, modified, oldKey, meta);
            me.sourceUpdating = false;
        } else // Root Collection has been informed.
        // Change is propagating downward from root.
        {
            newKey = me.getKey(item);
            if (filtered) {
                index = me.indexOfKey(keyChanged ? oldKey : newKey);
                wasFiltered = (index < 0);
                itemFiltered = me.isItemFiltered(item);
                filterChanged = (wasFiltered !== itemFiltered);
            }
            if (filterChanged) {
                if (itemFiltered) {
                    toRemove = [
                        item
                    ];
                    newIndex = -1;
                } else {
                    toAdd = [
                        item
                    ];
                    newIndex = me.length;
                }
            }
            // this will be ignored if sorted
            // If sorted, the newIndex must be reported correctly in the beforeitemchange
            // and itemchange events.
            // Even though splice ignores the parameter and calculates the insertion point
            else if (sorted && !itemFiltered) {
                // If we are sorted and there are 2 or more items make sure this item is at
                // the proper index.
                if (!filtered) {
                    // If the filter has not changed we may need to move the item but if
                    // there is a filter we have already determined its index.
                    index = me.indexOfKey(keyChanged ? oldKey : newKey);
                }
                sortFn = me.getSortFn();
                if (index !== -1) {
                    if (index && sortFn(items[index - 1], items[index]) > 0) {
                        // If this item is not the first and the item before it compares as
                        // greater-than then item needs to move left since it is less-than
                        // item[index - 1].
                        itemMovement = -1;
                        // We have to bound the binarySearch or else the presence of the
                        // out-of-order "item" would break it.
                        newIndex = Ext.Array.binarySearch(items, item, 0, index, sortFn);
                    } else if (index < last && sortFn(items[index], items[index + 1]) > 0) {
                        // If this item is not the last and the item after it compares as
                        // less-than then item needs to move right since it is greater-than
                        // item[index + 1].
                        itemMovement = 1;
                        // We have to bound the binarySearch or else the presence of the
                        // out-of-order "item" would break it.
                        newIndex = Ext.Array.binarySearch(items, item, index + 1, sortFn);
                    }
                    if (itemMovement) {
                        toAdd = [
                            item
                        ];
                    }
                }
            }
            // One may be tempted to avoid this notification when none of our three vars
            // are true, *but* the problem with that is that these three changes we care
            // about are only what this collection cares about. Child collections or
            // outside parties still need to know that the item has changed in some way.
            // We do NOT adjust the newIndex reported here to allow for position *after*
            // the item has been removed
            // We report the "visual" position at which the item would be inserted as if
            // it were new.
            details = {
                item: item,
                key: newKey,
                index: newIndex,
                filterChanged: filterChanged,
                keyChanged: keyChanged,
                indexChanged: !!itemMovement,
                filtered: itemFiltered,
                oldIndex: index,
                newIndex: newIndex,
                wasFiltered: wasFiltered,
                meta: meta
            };
            if (keyChanged) {
                details.oldKey = oldKey;
            }
            if (modified) {
                details.modified = modified;
            }
            ++me.generation;
            me.beginUpdate();
            me.notify('beforeitemchange', [
                details
            ]);
            if (keyChanged) {
                me.updateKey(item, oldKey, details);
            }
            if (toAdd || toRemove) {
                // In sorted mode (which is the only time we get here), newIndex is
                // correct but *ignored* by splice since it has to assume that *insert*
                // index values need to be determined internally. In other words, the
                // first argument here is both the remove and insert index but in sorted
                // mode the insert index is calculated by splice.
                me.splice(newIndex, toRemove, toAdd);
            }
            // Ensure that the newIndex always refers to the item the insertion is *before*.
            // Ensure that the oldIndex always refers to the item the insertion was *before*.
            //
            // Before change to "c" to "h":     |   Before change "i" to "d":
            //                                  |
            //      +---+---+---+---+---+---+   |   +---+---+---+---+---+---+
            //      | a | c | e | g | i | k |   |   | a | c | e | g | i | k |
            //      +---+---+---+---+---+---+   |   +---+---+---+---+---+---+
            //        0   1   2   3   4   5     |     0   1   2   3   4   5   
            //            ^           ^         |             ^       ^
            //            |           |         |             |       |
            //        oldIndex    newIndex      |       newIndex     oldIndex
            //                                  |
            // After change to "c" to "h":      |   After change "i" to "d":
            //                                  |
            //      +---+---+---+---+---+---+   |   +---+---+---+---+---+---+
            //      | a | e | g | h | i | k |   |   | a | c | d | e | g | k |
            //      +---+---+---+---+---+---+   |   +---+---+---+---+---+---+
            //        0   1   2   3   4   5     |     0   1   2   3   4   5  
            //            ^       ^             |             ^           ^
            //            |       |             |             |           |
            //      oldIndex    newIndex        |        newIndex     oldIndex
            //
            if (itemMovement > 0) {
                details.newIndex--;
            } else if (itemMovement < 0) {
                details.oldIndex++;
            }
            // Divergence depending on whether the record if filtered out at this level
            // in a chaining hierarchy. Child collections of this collection will not care
            // about filtereditemchange because the record is not in them.
            // Stores however will still need to know because the record *is* in them,
            // just filtered.
            me.notify(itemFiltered ? 'filtereditemchange' : 'itemchange', [
                details
            ]);
            me.endUpdate();
        }
    },
    /**
     * Remove an item from the collection.
     * @param {Object/Object[]} item The item or items to remove.
     * @return {Number} The number of items removed.
     * @since 5.0.0
     */
    remove: function(item) {
        var me = this,
            items = me.decodeRemoveItems(arguments, 0),
            length = me.length;
        me.splice(0, items);
        return length - me.length;
    },
    /**
     * Remove all items in the collection.
     * @return {Ext.util.Collection} This object.
     * @since 5.0.0
     */
    removeAll: function() {
        var me = this,
            length = me.length;
        if (me.generation && length) {
            me.splice(0, length);
        }
        return me;
    },
    /**
     * Remove an item from a specified index in the collection.
     * @param {Number} index The index within the collection of the item to remove.
     * @param {Number} [count=1] The number of items to remove.
     * @return {Object/Number} If `count` was 1 and the item was removed, that item is
     * returned. Otherwise the number of items removed is returned.
     * @since 5.0.0
     */
    removeAt: function(index, count) {
        var me = this,
            length = me.length,
            Num = Ext.Number,
            range = Num.clipIndices(length, [
                index,
                (count === undefined) ? 1 : count
            ], Num.Clip.COUNT),
            n = range[0],
            removeCount = range[1] - n,
            item = (removeCount === 1) && me.getAt(n),
            removed;
        me.splice(n, removeCount);
        removed = me.length - length;
        return (item && removed) ? item : removed;
    },
    /**
     * Removes the item associated with the passed key from the collection.
     * @param {String} key The key of the item to remove.
     * @return {Object} Only returned if removing at a specified key. The item removed or
     * `false` if no item was removed.
     * @since 5.0.0
     */
    removeByKey: function(key) {
        var item = this.getByKey(key);
        if (!item || !this.remove(item)) {
            return false;
        }
        return item;
    },
    /**
     * @private
     * Replace an old entry with a new entry of the same key if the entry existed.
     * @param {Object} item The item to insert.
     * @return {Object} inserted The item inserted.
     */
    replace: function(item) {
        var index = this.indexOf(item);
        if (index === -1) {
            this.add(item);
        } else {
            this.insert(index, item);
        }
    },
    /**
     * This method is basically the same as the JavaScript Array splice method.
     *
     * Negative indexes are interpreted starting at the end of the collection. That is,
     * a value of -1 indicates the last item, or equivalent to `length - 1`.
     *
     * @param {Number} index The index at which to add or remove items.
     * @param {Number/Object[]} toRemove The number of items to remove or an array of the
     * items to remove.
     * @param {Object[]} [toAdd] The items to insert at the given `index`.
     * @since 5.0.0
     */
    splice: function(index, toRemove, toAdd) {
        var me = this,
            autoSort = me.sorted && me.getAutoSort(),
            map = me.map,
            items = me.items,
            length = me.length,
            removeItems = (toRemove instanceof Array) ? me.decodeRemoveItems(toRemove) : null,
            isRemoveCount = !removeItems,
            Num = Ext.Number,
            range = Num.clipIndices(length, [
                index,
                isRemoveCount ? toRemove : 0
            ], Num.Clip.COUNT),
            begin = range[0],
            end = range[1],
            // Determine how many items we might actually remove:
            removeCount = end - begin,
            newItems = me.decodeItems(arguments, 2),
            newCount = newItems ? newItems.length : 0,
            addItems, newItemsMap, removeMap,
            insertAt = begin,
            indices = me.indices || ((newCount || removeItems) ? me.getIndices() : null),
            adds = null,
            removes = removeCount ? [
                begin
            ] : null,
            newKeys = null,
            source = me.getSource(),
            chunk, chunkItems, chunks, i, item, itemIndex, k, key, keys, n, duplicates, sorters;
        if (source && !source.updating) {
            // Modifying the content of a child collection has to be translated into a
            // change of its source. Because the source has all of the items of the child
            // (but likely at different indices) we can translate "index" and convert a
            // "removeCount" request into a "removeItems" request.
            if (isRemoveCount) {
                removeItems = [];
                for (i = 0; i < removeCount; ++i) {
                    removeItems.push(items[begin + i]);
                }
            }
            if (begin < length) {
                // Map index based on the item at that index since that item will be in
                // the source collection.
                i = source.indexOf(items[begin]);
            } else {
                // Map end of this collection to end of the source collection.
                i = source.length;
            }
            // When we react to the source add in onCollectionAdd, we must honour this
            // requested index.
            me.requestedIndex = index;
            source.splice(i, removeItems, newItems);
            delete me.requestedIndex;
            return me;
        }
        // Loop over the newItems because they could already be in the collection or may
        // be replacing items in the collection that just happen to have the same key. In
        // this case, those items must be removed as well. Since we need to call getKey
        // on each newItem to do this we may as well keep those keys for later.
        if (newCount) {
            addItems = newItems;
            newKeys = [];
            newItemsMap = {};
            // If this collection is sorted we will eventually need to sort addItems so
            // do that now so we can line up the newKeys properly. We optimize for the
            // case where we have no duplicates. It would be more expensive to do this
            // in two passes in an attempt to take advantage of removed duplicates.
            if (autoSort) {
                // We'll need the sorters later as well
                sorters = me.getSorters();
                if (newCount > 1) {
                    if (!addItems.$cloned) {
                        newItems = addItems = addItems.slice(0);
                    }
                    me.sortData(addItems);
                }
            }
            for (i = 0; i < newCount; ++i) {
                key = me.getKey(item = newItems[i]);
                if ((k = newItemsMap[key]) !== undefined) {
                    // Duplicates in the incoming newItems need to be discarded keeping the
                    // last of the duplicates. We add the index of the last duplicate of
                    // this key to the "duplicates" map.
                    (duplicates || (duplicates = {}))[k] = 1;
                } else {
                    // This item's index is outside the remove range, so we need to remove
                    // some extra stuff. Only the first occurrence of a given key in the
                    // newItems needs this processing.
                    itemIndex = indices[key];
                    if (itemIndex < begin || end <= itemIndex) {
                        (removes || (removes = [])).push(itemIndex);
                    }
                }
                // might be the first
                newItemsMap[key] = i;
                // track the last index of this key in newItems
                newKeys.push(key);
            }
            // must correspond 1-to-1 with newItems
            if (duplicates) {
                keys = newKeys;
                addItems = [];
                newKeys = [];
                addItems.$cloned = true;
                for (i = 0; i < newCount; ++i) {
                    if (!duplicates[i]) {
                        item = newItems[i];
                        addItems.push(item);
                        newKeys.push(keys[i]);
                    }
                }
                newCount = addItems.length;
            }
            adds = {
                // at: insertAt, // must fill this in later
                // next: null,  // only set by spliceMerge
                // replaced: null,  // must fill this in later
                items: addItems,
                keys: newKeys
            };
        }
        // If we are given a set of items to remove, map them to their indices.
        for (i = removeItems ? removeItems.length : 0; i-- > 0; ) {
            key = me.getKey(removeItems[i]);
            if ((itemIndex = indices[key]) !== undefined) {
                // ignore items we don't have (probably due to filtering)
                (removes || (removes = [])).push(itemIndex);
            }
        }
        // might be the first remove
        if (!adds && !removes) {
            return me;
        }
        me.beginUpdate();
        // Now we that everything we need to remove has its index in the removes array.
        // We start by sorting the array so we can coalesce the index values into chunks
        // or ranges.
        if (removes) {
            chunk = null;
            chunks = [];
            removeMap = {};
            if (removes.length > 1) {
                removes.sort(Ext.Array.numericSortFn);
            }
            // Coalesce the index array into chunks of (index, count) pairs for efficient
            // removal.
            for (i = 0 , n = removes.length; i < n; ++i) {
                key = me.getKey(item = items[itemIndex = removes[i]]);
                if (!(key in map)) {
                    
                    continue;
                }
                // Avoids 2nd loop of removed items but also means we won't process any
                // given item twice (in case of duplicates in removeItems).
                delete map[key];
                // Consider chunk = { at: 1, items: [ item1, item2 ] }
                //
                //      +---+---+---+---+---+---+
                //      |   | x | x |   |   |   |
                //      +---+---+---+---+---+---+
                //        0   1   2   3   4   5
                //
                // If we are adding an itemIndex > 3 we need a new chunk.
                //
                if (!chunk || itemIndex > (chunk.at + chunkItems.length)) {
                    chunks.push(chunk = {
                        at: itemIndex,
                        items: (chunkItems = []),
                        keys: (keys = []),
                        map: removeMap,
                        next: chunk,
                        replacement: adds
                    });
                    // Point "replaced" at the last chunk
                    if (adds) {
                        adds.replaced = chunk;
                    }
                }
                chunkItems.push(removeMap[key] = item);
                keys.push(key);
                // NOTE: For a long time the test below was "itemIndex < insertAt - 1",
                // but that does not work if the itemIndex is at the very end. This would
                // produce the "at" and "atItem" referencing the item being inserted
                // rather than the item to which the insert was relative.
                if (itemIndex < insertAt) {
                    // If the removal is ahead of the insertion point specified, we need
                    // to move the insertAt backwards.
                    //
                    // Consider the following splice:
                    //
                    //      collection.splice(3, 2, [ { id: 'b' } ]);
                    //
                    //      +---+---+---+---+---+---+
                    //      | a | b | c | x | y | d |
                    //      +---+---+---+---+---+---+
                    //        0   1   2   3   4   5
                    //            ^       ^   ^
                    //            |        \ /
                    //         replace    remove
                    //
                    // The intent is to replace x and y with the new item at index=3. But
                    // since the new item has the same key as the item at index=1, that
                    // item must be replaced. The resulting collection will be:
                    //
                    //      +---+---+---+---+
                    //      | a | c | b | d |
                    //      +---+---+---+---+
                    //        0   1   2   3
                    //
                    --insertAt;
                }
                if (removeCount > 1 && itemIndex === begin) {
                    // To account for the given range to remove we started by putting the
                    // index of the first such item ("begin") in the array. When we find
                    // it in this loop we have to process all of the items and add them
                    // to the current chunk. The following trick allows us to repeat the
                    // loop for each item in the removeCount.
                    //
                    --removeCount;
                    // countdown...
                    removes[i--] = ++begin;
                }
            }
            // backup and increment begin
            // for (removes)
            if (adds) {
                adds.at = insertAt;
            }
            // we have the correct(ed) insertAt now
            // Loop over the chunks in reverse so as to not invalidate index values on
            // earlier chunks.
            for (k = chunks.length; k-- > 0; ) {
                chunk = chunks[k];
                i = chunk.at;
                n = chunk.items.length;
                if (i + n < length) {
                    // If we are removing the tail of the collection, we can keep the
                    // indices for the rest of the things... otherwise we need to zap it
                    // and fix up later.
                    me.indices = indices = null;
                }
                me.length = length -= n;
                // We can use splice directly. The IE8 bug which Ext.Array works around
                // only affects *insertion*
                // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/
                // Ext.Array.erase(items, i, n);
                items.splice(i, n);
                if (indices) {
                    keys = chunk.keys;
                    for (i = 0; i < n; ++i) {
                        delete indices[keys[i]];
                    }
                }
                ++me.generation;
                me.notify('remove', [
                    chunk
                ]);
            }
        }
        // if (removes)
        if (adds) {
            if (autoSort && newCount > 1 && length) {
                me.spliceMerge(addItems, newKeys);
            } else {
                if (autoSort) {
                    if (newCount > 1) {
                        // We have multiple addItems but we are empty, so just add at 0
                        insertAt = 0;
                        me.indices = indices = null;
                    } else {
                        // If we are adding one item we can position it properly now and
                        // avoid a full sort.
                        insertAt = sorters.findInsertionIndex(adds.items[0], items, me.getSortFn(), index);
                    }
                }
                if (insertAt === length) {
                    end = insertAt;
                    // Inser items backwards. This way, when the first item is pushed the
                    // array is sized to as large as we're going to need it to be.
                    for (i = addItems.length - 1; i >= 0; --i) {
                        items[end + i] = addItems[i];
                    }
                    // The indices may have been regenerated, so we need to check if they have been
                    // and update them 
                    indices = me.indices;
                    if (indices) {
                        for (i = 0; i < newCount; ++i) {
                            indices[newKeys[i]] = insertAt + i;
                        }
                    }
                } else {
                    // inserting
                    me.indices = null;
                    Ext.Array.insert(items, insertAt, addItems);
                }
                for (i = 0; i < newCount; ++i) {
                    map[newKeys[i]] = addItems[i];
                }
                me.length += newCount;
                adds.at = insertAt;
                adds.atItem = insertAt === 0 ? null : items[insertAt - 1];
                ++me.generation;
                me.notify('add', [
                    adds
                ]);
            }
        }
        // if (adds)
        me.endUpdate();
        return me;
    },
    /**
     * This method calls the supplied function `fn` between `beginUpdate` and `endUpdate`
     * calls.
     *
     *      collection.update(function () {
     *          // Perform multiple collection updates...
     *
     *          collection.add(item);
     *          // ...
     *
     *          collection.insert(index, otherItem);
     *          //...
     *
     *          collection.remove(someItem);
     *      });
     *
     * @param {Function} fn The function to call that will modify this collection.
     * @param {Ext.util.Collection} fn.collection This collection.
     * @param {Object} [scope=this] The `this` pointer to use when calling `fn`.
     * @return {Object} Returns the value returned from `fn` (typically `undefined`).
     * @since 5.0.0
     */
    update: function(fn, scope) {
        var me = this;
        me.beginUpdate();
        try {
            return fn.call(scope || me, me);
        } catch (e) {
            Ext.log.error(this.$className + ': Unhandled Exception: ', e.description || e.message);
            throw e;
        } finally {
            me.endUpdate();
        }
    },
    /**
     * Change the key for an existing item in the collection. If the old key does not
     * exist this call does nothing. Even so, it is highly recommended to *avoid* calling
     * this method for an `item` that is not a member of this collection.
     *
     * @param {Object} item The item whose key has changed. The `item` should be a member
     * of this collection.
     * @param {String} oldKey The old key for the `item`.
     * @param details
     * @since 5.0.0
     */
    updateKey: function(item, oldKey, details) {
        var me = this,
            map = me.map,
            indices = me.indices,
            source = me.getSource(),
            newKey;
        if (source && !source.updating) {
            // If we are being told of the key change and the source has the same idea
            // on keying the item, push the change down instead.
            source.updateKey(item, oldKey);
        } else if ((newKey = me.getKey(item)) !== oldKey) {
            // If the key has changed and "item" is the item mapped to the oldKey and
            // there is no collision with an item with the newKey, we can proceed.
            if (map[oldKey] === item && !(newKey in map)) {
                delete map[oldKey];
                // We need to mark ourselves as updating so that observing collections
                // don't reflect the updateKey back to us (see above check) but this is
                // not really a normal update cycle so we don't call begin/endUpdate.
                me.updating++;
                me.generation++;
                map[newKey] = item;
                if (indices) {
                    indices[newKey] = indices[oldKey];
                    delete indices[oldKey];
                }
                me.notify('updatekey', [
                    Ext.apply({
                        item: item,
                        newKey: newKey,
                        oldKey: oldKey
                    }, details)
                ]);
                me.updating--;
            } else {
                // It may be that the item is (somehow) already in the map using the
                // newKey or that there is no item in the map with the oldKey. These
                // are not errors.
                if (newKey in map && map[newKey] !== item) {
                    // There is a different item in the map with the newKey which is an
                    // error. To properly handle this, add the item instead.
                    Ext.raise('Duplicate newKey "' + newKey + '" for item with oldKey "' + oldKey + '"');
                }
                if (oldKey in map && map[oldKey] !== item) {
                    // There is a different item in the map with the oldKey which is also
                    // an error. Do not call this method for items that are not part of
                    // the collection.
                    Ext.raise('Incorrect oldKey "' + oldKey + '" for item with newKey "' + newKey + '"');
                }
            }
        }
    },
    findInsertIndex: function(item, defaultIndex) {
        var source = this.getSource(),
            sourceItems = source.items,
            i = source.indexOf(item) - 1,
            sourceItem, index;
        while (i > -1) {
            sourceItem = sourceItems[i];
            index = this.indexOf(sourceItem);
            if (index > -1) {
                return index + 1;
            }
            --i;
        }
        // If we get here we didn't find any item in the parent before us...
        return defaultIndex;
    },
    //-------------------------------------------------------------------------
    // Calls from the source Collection:
    /**
     * This method is called when items are added to the `source` collection. This is
     * equivalent to the `{@link #event-add add}` event but is called before the `add`
     * event is fired.
     * @param {Ext.util.Collection} source The source collection.
     * @param {Object} details The `details` of the `{@link #event-add add}` event.
     * @private
     * @since 5.0.0
     */
    onCollectionAdd: function(source, details) {
        var me = this,
            atItem = details.atItem,
            items = me.transformItems(details.items),
            requestedIndex = me.requestedIndex,
            filtered, index, copy, i, item, n;
        // If we have a requestedIndex, it means the add/insert was on our collection,
        // so try use that specified index to do the insertion.
        if (requestedIndex !== undefined) {
            index = requestedIndex;
        } else {
            // If !atItem, the insert was at 0 in the source, so use [1] as beforeItem:
            item = atItem || source.items[1];
            index = item ? me.indexOf(item) : -1;
            if (index > -1) {
                // We also have that item in our collection, we need to insert after it
                // if atItem was passed (since that is the item after which the new item
                // was inserted in the source).
                if (atItem) {
                    ++index;
                }
            }
            // We can't find the reference item in our collection, which means it's probably
            // filtered out, so we need to search for an appropriate index. Pass the first
            // item and work back to find at good reference. Failing that, insert at front
            // or back based on front/back in source.
            else if (!me.sorted) {
                index = me.findInsertIndex(items[0], details.at ? me.length : 0);
            }
        }
        if (me.getAutoFilter() && me.filtered) {
            for (i = 0 , n = items.length; i < n; ++i) {
                item = items[i];
                if (me.isItemFiltered(item)) {
                    // If we have an item that is filtered out of this collection, we need
                    // to make a copy of the items up to this point.
                    if (!copy) {
                        copy = items.slice(0, i);
                    }
                    if (!filtered) {
                        filtered = [];
                    }
                    filtered.push(item);
                } else if (copy) {
                    // If we have a copy of the items, we need to put this item in that
                    // copy since it is not being filtered out.
                    copy.push(item);
                }
            }
        }
        me.splice((index < 0) ? me.length : index, 0, copy || items);
        if (filtered) {
            // Private for now. We may want to let any observers know we just
            // added these items but got filtered out
            me.notify('filteradd', [
                filtered
            ]);
        }
    },
    /**
     * This method is called when an item is modified in the `source` collection. This is
     * equivalent to the `{@link #event-beforeitemchange beforeitemchange}` event but is
     * called before the `beforeitemchange` event is fired.
     * @param {Ext.util.Collection} source The source collection.
     * @param {Object} details The `details` of the
     * `{@link #event-beforeitemchange beforeitemchange}` event.
     * @private
     * @since 5.0.0
     */
    onCollectionBeforeItemChange: function(source, details) {
        var me = this;
        // Drop the next few confusing events:
        me.onCollectionUpdateKey = null;
        // If this flag is true it means we're inside itemchanged, so this will be fired
        // shortly, don't fire it twice
        if (!me.sourceUpdating) {
            me.notify('beforeitemchange', [
                details
            ]);
        }
    },
    /**
     * This method is called when the `source` collection starts updating. This is
     * equivalent to the `{@link #event-beginupdate beginupdate}` event but is called
     * before the `beginupdate` event is fired.
     * @param {Ext.util.Collection} source The source collection.
     * @private
     * @since 5.0.0
     */
    onCollectionBeginUpdate: function() {
        this.beginUpdate();
    },
    /**
     * This method is called when the `source` collection finishes updating. This is
     * equivalent to the `{@link #event-endupdate endupdate}` event but is called before
     * the `endupdate` event is fired.
     * @param {Ext.util.Collection} source The source collection.
     * @private
     * @since 5.0.0
     */
    onCollectionEndUpdate: function() {
        this.endUpdate();
    },
    /**
     * This method is called when an item is modified in the `source` collection. This is
     * equivalent to the `{@link #event-itemchange itemchange}` event but is called before
     * the `itemchange` event is fired.
     * @param {Ext.util.Collection} source The source collection.
     * @param {Object} details The `details` of the `{@link #event-itemchange itemchange}`
     * event.
     * @private
     * @since 5.0.0
     */
    onCollectionItemChange: function(source, details) {
        // Restore things:
        delete this.onCollectionUpdateKey;
        this.itemChanged(details.item, details.modified, details.oldKey, details.meta);
    },
    onCollectionFilteredItemChange: function() {
        // Restore things:
        delete this.onCollectionUpdateKey;
    },
    /**
     * This method is called when the `source` collection refreshes. This is equivalent to
     * the `{@link #event-refresh refresh}` event but is called before the `refresh` event
     * is fired.
     * @param {Ext.util.Collection} source The source collection.
     * @private
     * @since 5.0.0
     */
    onCollectionRefresh: function(source) {
        var me = this,
            map = {},
            indices = {},
            items = me.items,
            sourceItems = me.transformItems(source.items),
            filterFn = me.getFilterFn(),
            i, item, key, length, newLength;
        // Perform a non-destructive filter of the source's items array into the
        // *existing* items array because stores give away references to this
        // collection's items array.
        if (me.filtered && me.getAutoFilter()) {
            for (i = 0 , newLength = 0 , length = sourceItems.length; i < length; i++) {
                if (filterFn(sourceItems[i])) {
                    items[newLength++] = sourceItems[i];
                }
            }
            items.length = newLength;
        } else {
            items.length = 0;
            items.push.apply(items, sourceItems);
        }
        if (me.sorted) {
            me.sortData(items);
        }
        me.length = length = items.length;
        me.map = map;
        me.indices = indices;
        for (i = 0; i < length; ++i) {
            key = me.getKey(item = items[i]);
            map[key] = item;
            indices[key] = i;
        }
        ++me.generation;
        me.notify('refresh');
    },
    /**
     * This method is called when items are removed from the `source` collection. This is
     * equivalent to the `{@link #event-remove remove}` event but is called before the
     * `remove` event is fired.
     * @param {Ext.util.Collection} source The source collection.
     * @param {Object} details The `details` of the `remove` event.
     * @private
     * @since 5.0.0
     */
    onCollectionRemove: function(source, details) {
        this.splice(0, details.items);
    },
    /**
     * @method onCollectionSort
     * This method is called when the `source` collection is sorted. This is equivalent to
     * the `{@link #event-sort sort}` event but is called before the `sort` event is fired.
     * @param {Ext.util.Collection} source The source collection.
     * @private
     * @since 5.0.0
     */
    // onCollectionSort: function (source) {
    //    we ignore sorting of the source collection because we prefer our own order.
    // },
    /**
     * This method is called when key changes in the `source` collection. This is
     * equivalent to the `updatekey` event but is called before the `updatekey` event is
     * fired.
     * @param {Ext.util.Collection} source The source collection.
     * @param {Object} details The `details` of the `updatekey` event.
     * @private
     * @since 5.0.0
     */
    onCollectionUpdateKey: function(source, details) {
        this.updateKey(details.item, details.oldKey, details);
    },
    //-------------------------------------------------------------------------
    // Private
    /**
     * @method average
     * Averages property values from some or all of the items in this collection.
     *
     * @param {String} property The name of the property to average from each item.
     * @param {Number} [begin] The index of the first item to include in the average.
     * @param {Number} [end] The index at which to stop averaging `items`. The item at
     * this index will *not* be included in the average.
     * @return {Object} The result of averaging the specified property from the indicated
     * items.
     * @since 5.0.0
     */
    /**
     * @method averageByGroup
     * See {@link #average}. The result is partitioned by group.
     *
     * @param {String} property The name of the property to average from each item.
     * @return {Object} The result of {@link #average}, partitioned by group. See
     * {@link #aggregateByGroup}.
     * @since 5.0.0
     */
    /**
     * @method bounds
     * Determines the minimum and maximum values for the specified property over some or
     * all of the items in this collection.
     *
     * @param {String} property The name of the property from each item.
     * @param {Number} [begin] The index of the first item to include in the bounds.
     * @param {Number} [end] The index at which to stop in `items`. The item at this index
     * will *not* be included in the bounds.
     * @return {Array} An array `[min, max]` with the minimum and maximum of the specified
     * property.
     * @since 5.0.0
     */
    /**
     * @method boundsByGroup
     * See {@link #bounds}. The result is partitioned by group.
     *
     * @param {String} property The name of the property from each item.
     * @return {Object} The result of {@link #bounds}, partitioned by group. See
     * {@link #aggregateByGroup}.
     * @since 5.0.0
     */
    /**
     * @method count
     * Determines the number of items in the collection.
     * 
     * @return {Number} The number of items.
     * @since 5.0.0
     */
    /**
     * @method countByGroup
     * See {@link #count}. The result is partitioned by group.
     *
     * @return {Object} The result of {@link #count}, partitioned by group. See
     * {@link #aggregateByGroup}.
     * @since 5.0.0
     */
    /**
     * @method extremes
     * Finds the items with the minimum and maximum for the specified property over some
     * or all of the items in this collection.
     *
     * @param {String} property The name of the property from each item.
     * @param {Number} [begin] The index of the first item to include.
     * @param {Number} [end] The index at which to stop in `items`. The item at this index
     * will *not* be included.
     * @return {Array} An array `[minItem, maxItem]` with the items that have the minimum
     * and maximum of the specified property.
     * @since 5.0.0
     */
    /**
     * @method extremesByGroup
     * See {@link #extremes}. The result is partitioned by group.
     *
     * @param {String} property The name of the property from each item.
     * @return {Object} The result of {@link #extremes}, partitioned by group. See
     * {@link #aggregateByGroup}.
     * @since 5.0.0
     */
    /**
     * @method max
     * Determines the maximum value for the specified property over some or all of the
     * items in this collection.
     *
     * @param {String} property The name of the property from each item.
     * @param {Number} [begin] The index of the first item to include in the maximum.
     * @param {Number} [end] The index at which to stop in `items`. The item at this index
     * will *not* be included in the maximum.
     * @return {Object} The maximum of the specified property from the indicated items.
     * @since 5.0.0
     */
    /**
     * @method maxByGroup
     * See {@link #max}. The result is partitioned by group.
     *
     * @param {String} property The name of the property from each item.
     * @return {Object} The result of {@link #max}, partitioned by group. See
     * {@link #aggregateByGroup}.
     * @since 5.0.0
     */
    /**
     * @method maxItem
     * Finds the item with the maximum value for the specified property over some or all
     * of the items in this collection.
     *
     * @param {String} property The name of the property from each item.
     * @param {Number} [begin] The index of the first item to include in the maximum.
     * @param {Number} [end] The index at which to stop in `items`. The item at this index
     * will *not* be included in the maximum.
     * @return {Object} The item with the maximum of the specified property from the
     * indicated items.
     * @since 5.0.0
     */
    /**
     * @method maxItemByGroup
     * See {@link #maxItem}. The result is partitioned by group.
     *
     * @param {String} property The name of the property from each item.
     * @return {Object} The result of {@link #maxItem}, partitioned by group. See
     * {@link #aggregateByGroup}.
     * @since 5.0.0
     */
    /**
     * @method min
     * Determines the minimum value for the specified property over some or all of the
     * items in this collection.
     *
     * @param {String} property The name of the property from each item.
     * @param {Number} [begin] The index of the first item to include in the minimum.
     * @param {Number} [end] The index at which to stop in `items`. The item at this index
     * will *not* be included in the minimum.
     * @return {Object} The minimum of the specified property from the indicated items.
     * @since 5.0.0
     */
    /**
     * @method minByGroup
     * See {@link #min}. The result is partitioned by group.
     *
     * @param {String} property The name of the property from each item.
     * @return {Object} The result of {@link #min}, partitioned by group. See
     * {@link #aggregateByGroup}.
     * @since 5.0.0
     */
    /**
     * @method minItem
     * Finds the item with the minimum value for the specified property over some or all
     * of the items in this collection.
     *
     * @param {String} property The name of the property from each item.
     * @param {Number} [begin] The index of the first item to include in the minimum.
     * @param {Number} [end] The index at which to stop in `items`. The item at this index
     * will *not* be included in the minimum.
     * @return {Object} The item with the minimum of the specified property from the
     * indicated items.
     * @since 5.0.0
     */
    /**
     * @method minItemByGroup
     * See {@link #minItem}. The result is partitioned by group.
     *
     * @param {String} property The name of the property from each item.
     * @return {Object} The result of {@link #minItem}, partitioned by group. See
     * {@link #aggregateByGroup}.
     * @since 5.0.0
     */
    /**
     * @method sum
     * Sums property values from some or all of the items in this collection.
     *
     * @param {String} property The name of the property to sum from each item.
     * @param {Number} [begin] The index of the first item to include in the sum.
     * @param {Number} [end] The index at which to stop summing `items`. The item at this
     * index will *not* be included in the sum.
     * @return {Object} The result of summing the specified property from the indicated
     * items.
     * @since 5.0.0
     */
    /**
     * @method sumByGroup
     * See {@link #sum}. The result is partitioned by group.
     *
     * @param {String} property The name of the property to sum from each item.
     * @return {Object} The result of {@link #sum}, partitioned by group. See
     * {@link #aggregateByGroup}.
     * @since 5.0.0
     */
    _aggregators: {
        average: function(items, begin, end, property, root) {
            var n = end - begin;
            return n && this._aggregators.sum.call(this, items, begin, end, property, root) / n;
        },
        bounds: function(items, begin, end, property, root) {
            var value, max, min, i;
            for (i = begin; i < end; ++i) {
                value = items[i];
                value = (root ? value[root] : value)[property];
                // First pass max and min are undefined and since nothing is less than
                // or greater than undefined we always evaluate these "if" statements as
                // true to pick up the first value as both max and min.
                if (!(value < max)) {
                    // jshint ignore:line
                    max = value;
                }
                if (!(value > min)) {
                    // jshint ignore:line
                    min = value;
                }
            }
            return [
                min,
                max
            ];
        },
        count: function(items) {
            return items.length;
        },
        extremes: function(items, begin, end, property, root) {
            var most = null,
                least = null,
                i, item, max, min, value;
            for (i = begin; i < end; ++i) {
                item = items[i];
                value = (root ? item[root] : item)[property];
                // Same trick as "bounds"
                if (!(value < max)) {
                    // jshint ignore:line
                    max = value;
                    most = item;
                }
                if (!(value > min)) {
                    // jshint ignore:line
                    min = value;
                    least = item;
                }
            }
            return [
                least,
                most
            ];
        },
        max: function(items, begin, end, property, root) {
            var b = this._aggregators.bounds.call(this, items, begin, end, property, root);
            return b[1];
        },
        maxItem: function(items, begin, end, property, root) {
            var b = this._aggregators.extremes.call(this, items, begin, end, property, root);
            return b[1];
        },
        min: function(items, begin, end, property, root) {
            var b = this._aggregators.bounds.call(this, items, begin, end, property, root);
            return b[0];
        },
        minItem: function(items, begin, end, property, root) {
            var b = this._aggregators.extremes.call(this, items, begin, end, property, root);
            return b[0];
        },
        sum: function(items, begin, end, property, root) {
            var value, sum, i;
            for (sum = 0 , i = begin; i < end; ++i) {
                value = items[i];
                value = (root ? value[root] : value)[property];
                sum += value;
            }
            return sum;
        }
    },
    _eventToMethodMap: {
        add: 'onCollectionAdd',
        beforeitemchange: 'onCollectionBeforeItemChange',
        beginupdate: 'onCollectionBeginUpdate',
        endupdate: 'onCollectionEndUpdate',
        itemchange: 'onCollectionItemChange',
        filtereditemchange: 'onCollectionFilteredItemChange',
        refresh: 'onCollectionRefresh',
        remove: 'onCollectionRemove',
        beforesort: 'beforeCollectionSort',
        sort: 'onCollectionSort',
        filter: 'onCollectionFilter',
        filteradd: 'onCollectionFilterAdd',
        updatekey: 'onCollectionUpdateKey'
    },
    /**
     * Adds an observing object to this collection. Observers are given first view of all
     * events that we may fire. For any event an observer may implement a method whose
     * name starts with "onCollection" to receive the event. The `{@link #event-add add}`
     * event for example would be passed to `"onCollectionAdd"`.
     *
     * The only restriction to observers is that they are not allowed to add or remove
     * observers from inside these methods.
     *
     * @param {Ext.util.Collection} observer The observer instance.
     * @private
     * @since 5.0.0
     */
    addObserver: function(observer) {
        var me = this,
            observers = me.observers;
        if (!observers) {
            me.observers = observers = [];
        }
        if (Ext.Array.contains(observers, observer)) {
            Ext.Error.raise('Observer already added');
        }
        // if we're in the middle of notifying, we need to clone the observers
        if (me.notifying) {
            me.observers = observers = observers.slice(0);
        }
        observers.push(observer);
        if (observers.length > 1) {
            // Allow observers to be inserted with a priority.
            // For example GroupCollections must react to Collection mutation before views.
            Ext.Array.sort(observers, me.prioritySortFn);
        }
    },
    prioritySortFn: function(o1, o2) {
        var a = o1.observerPriority || 0,
            b = o2.observerPriority || 0;
        return a - b;
    },
    applyExtraKeys: function(extraKeys, oldExtraKeys) {
        var me = this,
            ret = oldExtraKeys || {},
            config, name, value;
        for (name in extraKeys) {
            value = extraKeys[name];
            if (!value.isCollectionKey) {
                config = {
                    collection: me
                };
                if (Ext.isString(value)) {
                    config.property = value;
                } else {
                    config = Ext.apply(config, value);
                }
                value = new Ext.util.CollectionKey(config);
            } else {
                value.setCollection(me);
            }
            ret[name] = me[name] = value;
            value.name = name;
        }
        return ret;
    },
    applyGrouper: function(grouper) {
        if (grouper) {
            grouper = this.getSorters().decodeSorter(grouper, Ext.util.Grouper);
        }
        return grouper;
    },
    /**
     * Returns the items array on which to operate. This is called to handle the two
     * possible forms used by various methods that accept items:
     *
     *      collection.add(item1, item2, item3);
     *      collection.add([ item1, item2, item3 ]);
     *
     * Things get interesting when other arguments are involved:
     *
     *      collection.insert(index, item1, item2, item3);
     *      collection.insert(index, [ item1, item2, item3 ]);
     *
     * As well as below because we have to distinguish the one item from from the array:
     *
     *      collection.add(item);
     *      collection.insert(index, item);
     *
     * @param {Arguments} args The arguments object from the caller.
     * @param {Number} index The index in `args` (the caller's arguments) of `items`.
     * @return {Object[]} The array of items on which to operate.
     * @private
     * @since 5.0.0
     */
    decodeItems: function(args, index) {
        var me = this,
            ret = (index === undefined) ? args : args[index],
            cloned, decoder, i;
        if (!ret || !ret.$cloned) {
            cloned = args.length > index + 1 || !Ext.isIterable(ret);
            if (cloned) {
                ret = Ext.Array.slice(args, index);
                if (ret.length === 1 && ret[0] === undefined) {
                    ret.length = 0;
                }
            }
            decoder = me.getDecoder();
            if (decoder) {
                if (!cloned) {
                    ret = ret.slice(0);
                    cloned = true;
                }
                for (i = ret.length; i-- > 0; ) {
                    if ((ret[i] = decoder.call(me, ret[i])) === false) {
                        ret.splice(i, 1);
                    }
                }
            }
            if (cloned) {
                ret.$cloned = true;
            }
        }
        return ret;
    },
    /**
     * Returns the map of key to index for all items in this collection. This method will
     * lazily populate this map on request. This map is maintained when doing so does not
     * involve too much overhead. When this threshold is cross, the index map is discarded
     * and must be rebuilt by calling this method.
     *
     * @return {Object}
     * @private
     * @since 5.0.0
     */
    getIndices: function() {
        var me = this,
            indices = me.indices,
            items = me.items,
            n = items.length,
            i, key;
        if (!indices) {
            me.indices = indices = {};
            ++me.indexRebuilds;
            for (i = 0; i < n; ++i) {
                key = me.getKey(items[i]);
                indices[key] = i;
            }
        }
        return indices;
    },
    /**
     * This method wraps all fired events and gives observers first view of the change.
     *
     * @param {String} eventName The name of the event to fire.
     * @param {Array} [args] The event arguments. This collection instance is inserted at
     * the front of this array if there is any receiver for the notification.
     *
     * @private
     * @since 5.0.0
     */
    notify: function(eventName, args) {
        var me = this,
            observers = me.observers,
            methodName = me._eventToMethodMap[eventName],
            added = 0,
            index, length, method, observer;
        args = args || [];
        if (observers && methodName) {
            me.notifying = true;
            for (index = 0 , length = observers.length; index < length; ++index) {
                method = (observer = observers[index])[methodName];
                if (method) {
                    if (!added++) {
                        // jshint ignore:line
                        args.unshift(me);
                    }
                    // put this Collection as the first argument
                    method.apply(observer, args);
                }
            }
            me.notifying = false;
        }
        // During construction, no need to fire an event here
        if (!me.hasListeners) {
            return;
        }
        if (me.hasListeners[eventName]) {
            if (!added) {
                args.unshift(me);
            }
            // put this Collection as the first argument
            me.fireEventArgs(eventName, args);
        }
    },
    /**
     * Returns the filter function.
     * @return {Function} sortFn The sort function.
     */
    getFilterFn: function() {
        return this.getFilters().getFilterFn();
    },
    /**
     * Returns the `Ext.util.FilterCollection`. Unless `autoCreate` is explicitly passed
     * as `false` this collection will be automatically created if it does not yet exist.
     * @param [autoCreate=true] Pass `false` to disable auto-creation of the collection.
     * @return {Ext.util.FilterCollection} The collection of filters.
     */
    getFilters: function(autoCreate) {
        var ret = this._filters;
        if (!ret && autoCreate !== false) {
            ret = new Ext.util.FilterCollection({
                id: this.getId() + '-filters' + (this.generation || '')
            });
            this.setFilters(ret);
        }
        return ret;
    },
    /**
     * This method can be used to conveniently test whether an individual item would be
     * removed due to the current filter.
     * @param {Object} item The item to test.
     * @return {Boolean} The value `true` if the item would be "removed" from the
     * collection due to filters or `false` otherwise.
     */
    isItemFiltered: function(item) {
        return !this.getFilters().filterFn(item);
    },
    /**
     * Called after a change of the filter is complete.
     *
     * For example:
     *
     *      onFilterChange: function (filters) {
     *          if (this.filtered) {
     *              // process filters
     *          } else {
     *              // no filters present
     *          }
     *      }
     *
     * @template
     * @method
     * @param {Ext.util.FilterCollection} filters The filters collection.
     */
    onFilterChange: function(filters) {
        var me = this,
            source = me.getSource(),
            extraKeys, newKeys, key;
        if (!source) {
            // In this method, we have changed the filter but since we don't start with
            // any and we create the source collection as needed that means we are getting
            // our first filter.
            extraKeys = me.getExtraKeys();
            if (extraKeys) {
                newKeys = {};
                for (key in extraKeys) {
                    newKeys[key] = extraKeys[key].clone(me);
                }
            }
            source = new Ext.util.Collection({
                keyFn: me.getKey,
                extraKeys: newKeys,
                rootProperty: me.getRootProperty()
            });
            if (me.length) {
                source.add(me.items);
            }
            me.setSource(source);
            me.autoSource = source;
        } else {
            if (source.destroyed) {
                return;
            }
            if (source.length || me.length) {
                // if both us and the source are empty then we can skip this
                me.onCollectionRefresh(source);
            }
        }
        me.notify('filter');
    },
    //-------------------------------------------------------------------------
    // Private
    applyFilters: function(filters, collection) {
        if (!filters || filters.isFilterCollection) {
            return filters;
        }
        if (filters) {
            if (!collection) {
                collection = this.getFilters();
            }
            collection.splice(0, collection.length, filters);
        }
        return collection;
    },
    updateFilters: function(newFilters, oldFilters) {
        var me = this;
        if (oldFilters) {
            // Do not disconnect from owning Filterable because
            // default options (eg _rootProperty) are read from there.
            // FilterCollections are detached from the Collection when the owning Store
            // is remoteFilter: true or the owning store is a TreeStore and only filters
            // new nodes before filling a parent node.
            oldFilters.un('endupdate', 'onEndUpdateFilters', me);
        }
        if (newFilters) {
            newFilters.on({
                endupdate: 'onEndUpdateFilters',
                scope: me,
                priority: me.$endUpdatePriority
            });
            newFilters.$filterable = me;
        }
        me.onEndUpdateFilters(newFilters);
    },
    onEndUpdateFilters: function(filters) {
        var me = this,
            was = me.filtered,
            is = !!filters && (filters.getFilterCount() > 0);
        // booleanize filters
        if (was || is) {
            me.filtered = is;
            me.onFilterChange(filters);
        }
    },
    /**
     * Returns an up to date sort function.
     * @return {Function} The sort function.
     */
    getSortFn: function() {
        return this._sortFn || this.createSortFn();
    },
    /**
     * Returns the `Ext.util.SorterCollection`. Unless `autoCreate` is explicitly passed
     * as `false` this collection will be automatically created if it does not yet exist.
     * @param [autoCreate=true] Pass `false` to disable auto-creation of the collection.
     * @return {Ext.util.SorterCollection} The collection of sorters.
     */
    getSorters: function(autoCreate) {
        var ret = this._sorters;
        if (!ret && autoCreate !== false) {
            ret = new Ext.util.SorterCollection({
                id: this.getId() + '-sorters' + (this.generation || '')
            });
            this.setSorters(ret);
        }
        return ret;
    },
    /**
     * Called after a change of the sort is complete.
     *
     * For example:
     *
     *      onSortChange: function (sorters) {
     *          if (this.sorted) {
     *              // process sorters
     *          } else {
     *              // no sorters present
     *          }
     *      }
     *
     * @template
     * @method
     * @param {Ext.util.SorterCollection} sorters The sorters collection.
     */
    onSortChange: function() {
        if (this.sorted) {
            this.sortItems();
        }
    },
    /**
     * Updates the sorters collection and triggers sorting of this Sortable.
     *
     * For example:
     *
     *     //sort by a single field
     *     myStore.sort('myField', 'DESC');
     *
     *     //sorting by multiple fields
     *     myStore.sort([{
     *         property : 'age',
     *         direction: 'ASC'
     *     }, {
     *         property : 'name',
     *         direction: 'DESC'
     *     }]);
     *
     * When passing a single string argument to sort, the `direction` is maintained for
     * each field and is toggled automatically. So this code:
     *
     *     store.sort('myField');
     *     store.sort('myField');
     *
     * Is equivalent to the following:
     *
     *     store.sort('myField', 'ASC');
     *     store.sort('myField', 'DESC');
     *
     * @param {String/Function/Ext.util.Sorter[]} [property] Either the name of a property
     * (such as a field of a `Ext.data.Model` in a `Store`), an array of configurations
     * for `Ext.util.Sorter` instances or just a comparison function.
     * @param {String} [direction] The direction by which to sort the data. This parameter
     * is only valid when `property` is a String, otherwise the second parameter is the
     * `mode`.
     * @param {String} [mode="replace"] Where to put new sorters in the collection. This
     * should be one the following values:
     *
     * - **`replace`** : The new sorter(s) become the sole sorter set for this Sortable.
     *   This is the most useful call mode to programmatically sort by multiple fields.
     *
     * - **`prepend`** : The new sorters are inserted as the primary sorters. The sorter
     *   collection length must be controlled by the developer.
     *
     * - **`multi`** : Similar to **`prepend`** the new sorters are inserted at the front
     *   of the collection of sorters. Following the insertion, however, this mode trims
     *   the sorter collection to enforce the `multiSortLimit` config. This is useful for
     *   implementing intuitive "Sort by this" user interfaces.
     *
     * - **`append`** : The new sorters are added at the end of the collection.
     * @return {Ext.util.Collection} This instance.
     */
    sort: function(property, direction, mode) {
        var sorters = this.getSorters();
        sorters.addSort.apply(sorters, arguments);
        return this;
    },
    /**
     * This method will sort an array based on the currently configured {@link #sorters}.
     * @param {Array} data The array you want to have sorted.
     * @return {Array} The array you passed after it is sorted.
     */
    sortData: function(data) {
        Ext.Array.sort(data, this.getSortFn());
        return data;
    },
    /**
     * Sorts the items of the collection using the supplied function. This should only be
     * called for collections that have no `sorters` defined.
     * @param {Function} sortFn The function by which to sort the items.
     * @since 5.0.0
     */
    sortItems: function(sortFn) {
        var me = this;
        if (me.sorted) {
            if (sortFn) {
                Ext.raise('Collections with sorters cannot resorted');
            }
            sortFn = me.getSortFn();
        }
        me.indices = null;
        me.notify('beforesort', [
            me.getSorters(false)
        ]);
        if (me.length) {
            Ext.Array.sort(me.items, sortFn);
        }
        // Even if there's no data, notify interested parties.
        // Eg: Stores must react and fire their refresh and sort events.
        me.notify('sort');
    },
    /**
     * Sorts the collection by a single sorter function
     * @param {Function} sortFn The function to sort by
     * @deprecated 6.5.0 This method is deprecated.
     */
    sortBy: function(sortFn) {
        return this.sortItems(sortFn);
    },
    /**
     * @private
     * Can be called to find the insertion index of a passed object in this collection.
     * Or can be passed an items array to search in, and may be passed a comparator
     */
    findInsertionIndex: function(item, items, comparatorFn, index) {
        return Ext.Array.findInsertionIndex(item, items || this.items, comparatorFn || this.getSortFn(), index);
    },
    applySorters: function(sorters, collection) {
        if (!sorters || sorters.isSorterCollection) {
            return sorters;
        }
        if (sorters) {
            if (!collection) {
                collection = this.getSorters();
            }
            collection.splice(0, collection.length, sorters);
        }
        return collection;
    },
    createSortFn: function() {
        var me = this,
            grouper = me.getGrouper(),
            sorters = me.getSorters(false),
            sorterFn = sorters ? sorters.getSortFn() : null;
        if (!grouper) {
            return sorterFn;
        }
        return function(lhs, rhs) {
            var ret = grouper.sort(lhs, rhs);
            if (!ret && sorterFn) {
                ret = sorterFn(lhs, rhs);
            }
            return ret;
        };
    },
    updateGrouper: function(grouper) {
        var me = this,
            groups = me.getGroups(),
            sorters = me.getSorters(),
            populate;
        me.onSorterChange();
        me.grouped = !!grouper;
        if (grouper) {
            if (me.getTrackGroups()) {
                if (!groups) {
                    groups = new Ext.util.GroupCollection({
                        id: me.getId() + '-groups' + (me.generation || ''),
                        itemRoot: me.getRootProperty(),
                        groupConfig: me.getGroupConfig()
                    });
                    groups.$groupable = me;
                    me.setGroups(groups);
                }
                groups.setGrouper(grouper);
                populate = true;
            }
        } else {
            if (groups) {
                me.removeObserver(groups);
                groups.destroy();
            }
            me.setGroups(null);
        }
        if (!sorters.updating) {
            me.onEndUpdateSorters(sorters);
        }
        if (populate) {
            groups.onCollectionRefresh(me);
        }
    },
    updateSorters: function(newSorters, oldSorters) {
        var me = this;
        if (oldSorters && !oldSorters.destroyed) {
            // Do not disconnect from owning Filterable because
            // default options (eg _rootProperty) are read from there.
            // SorterCollections are detached from the Collection when the owning Store
            // is remoteSort: true or the owning store is a TreeStore and only sorts
            // new nodes before filling a parent node.
            oldSorters.un('endupdate', 'onEndUpdateSorters', me);
        }
        if (newSorters) {
            newSorters.on({
                endupdate: 'onEndUpdateSorters',
                scope: me,
                priority: me.$endUpdatePriority
            });
            if (me.manageSorters) {
                newSorters.$sortable = me;
            }
        }
        me.onSorterChange();
        me.onEndUpdateSorters(newSorters);
    },
    onSorterChange: function() {
        this._sortFn = null;
    },
    onEndUpdateSorters: function(sorters) {
        var me = this,
            was = me.sorted,
            is = (me.grouped && me.getAutoGroup()) || (sorters && sorters.length > 0);
        if (was || is) {
            // ensure flag property is a boolean.
            // sorters && (sorters.length > 0) may evaluate to null
            me.sorted = !!is;
            me.onSortChange(sorters);
        }
    },
    /**
     * Removes an observing object to this collection. See `addObserver` for details.
     *
     * @param {Ext.util.Collection} observer The observer instance.
     * @private
     * @since 5.0.0
     */
    removeObserver: function(observer) {
        var observers = this.observers;
        if (observers) {
            Ext.Array.remove(observers, observer);
        }
    },
    /**
     * This method is what you might find in the core of a merge sort. We have an items
     * array that is sorted so we sort the newItems and merge the two sorted arrays. In
     * the general case, newItems will be no larger than all items so sorting it will be
     * faster than simply concatenating the arrays and calling sort on it.
     *
     * We take advantage of the nature of this process to generate add events as ranges.
     *
     * @param {Object[]} newItems
     * @param {Object[]} newKeys
     * @private
     * @since 5.0.0
     */
    spliceMerge: function(newItems, newKeys) {
        var me = this,
            map = me.map,
            newLength = newItems.length,
            oldIndex = 0,
            oldItems = me.items,
            oldLength = oldItems.length,
            adds = [],
            count = 0,
            items = [],
            sortFn = me.getSortFn(),
            // account for grouper and sorter(s)
            addItems, end, i, newItem, oldItem, newIndex;
        me.items = items;
        //
        //  oldItems
        //      +----+----+----+----+
        //      | 15 | 25 | 35 | 45 |
        //      +----+----+----+----+
        //        0    1    2    3
        //
        //  newItems
        //      +----+----+----+----+----+----+
        //      | 10 | 11 | 20 | 21 | 50 | 51 |
        //      +----+----+----+----+----+----+
        //        0    1    2    3    4    5
        //
        for (newIndex = 0; newIndex < newLength; newIndex = end) {
            newItem = newItems[newIndex];
            // Flush out any oldItems that are <= newItem
            for (; oldIndex < oldLength; ++oldIndex) {
                // Consider above arrays...
                //  at newIndex == 0 this loop sets oldItem but breaks immediately
                //  at newIndex == 2 this loop pushes 15 and breaks w/oldIndex=1
                //  at newIndex == 4 this loop pushes 25, 35 and 45 and breaks w/oldIndex=4
                if (sortFn(newItem, oldItem = oldItems[oldIndex]) < 0) {
                    break;
                }
                items.push(oldItem);
            }
            if (oldIndex === oldLength) {
                // Consider above arrays...
                //  at newIndex == 0 we won't come in here (oldIndex == 0)
                //  at newIndex == 2 we won't come in here (oldIndex == 1)
                //  at newIndex == 4 we come here (oldIndex == 4), push 50 & 51 and break
                adds[count++] = {
                    at: items.length,
                    itemAt: items[items.length - 1],
                    items: (addItems = [])
                };
                if (count > 1) {
                    adds[count - 2].next = adds[count - 1];
                }
                for (; newIndex < newLength; ++newIndex) {
                    addItems.push(newItem = newItems[newIndex]);
                    items.push(newItem);
                }
                break;
            }
            // else oldItem is the item from oldItems that is > newItem
            // Consider above arrays...
            //  at newIndex == 0 we will push 10
            //  at newIndex == 2 we will push 20
            adds[count++] = {
                at: items.length,
                itemAt: items[items.length - 1],
                items: (addItems = [
                    newItem
                ])
            };
            if (count > 1) {
                adds[count - 2].next = adds[count - 1];
            }
            items.push(newItem);
            for (end = newIndex + 1; end < newLength; ++end) {
                // Consider above arrays...
                //  at newIndex == 0 this loop pushes 11 then breaks w/end == 2
                //  at newIndex == 2 this loop pushes 21 the breaks w/end == 4
                if (sortFn(newItem = newItems[end], oldItem) >= 0) {
                    break;
                }
                items.push(newItem);
                addItems.push(newItem);
            }
        }
        // if oldItems had 55 as its final element, the above loop would have pushed
        // all of newItems so the outer for loop would then fall out
        for (; oldIndex < oldLength; ++oldIndex) {
            // In the above example, we won't come in here, but if you imagine a 55 in
            // oldItems we would have oldIndex == 4 and oldLength == 5
            items.push(oldItems[oldIndex]);
        }
        for (i = 0; i < newLength; ++i) {
            map[newKeys[i]] = newItems[i];
        }
        me.length = items.length;
        ++me.generation;
        me.indices = null;
        // Tell users of the adds in increasing index order.
        for (i = 0; i < count; ++i) {
            me.notify('add', [
                adds[i]
            ]);
        }
    },
    transformItems: Ext.identityFn,
    getGroups: function() {
        return this.callParent() || null;
    },
    updateAutoGroup: function(autoGroup) {
        var groups = this.getGroups();
        if (groups) {
            groups.setAutoGroup(autoGroup);
        }
        // Important to call this so it can clear the .sorted flag
        // as needed
        this.onEndUpdateSorters(this._sorters);
    },
    updateGroups: function(newGroups, oldGroups) {
        if (oldGroups) {
            this.removeObserver(oldGroups);
        }
        if (newGroups) {
            this.addObserver(newGroups);
        }
    },
    updateSource: function(newSource, oldSource) {
        var me = this,
            auto = me.autoSource;
        if (oldSource) {
            if (!oldSource.destroyed) {
                oldSource.removeObserver(me);
            }
            if (oldSource === auto) {
                auto.destroy();
                me.autoSource = null;
            }
        }
        if (newSource) {
            newSource.addObserver(me);
            if (newSource.length || me.length) {
                me.onCollectionRefresh(newSource);
            }
        }
    }
}, function() {
    var prototype = this.prototype;
    // Minor compat method
    prototype.removeAtKey = prototype.removeByKey;
    /**
     * This method is an alias for `decodeItems` but is called when items are being
     * removed. If a `decoder` is provided it may be necessary to also override this
     * method to achieve symmetry between adding and removing items. This is the case
     * for `Ext.util.FilterCollection' and `Ext.util.SorterCollection' for example.
     *
     * @method decodeRemoveItems
     * @protected
     * @since 5.0.0
     */
    prototype.decodeRemoveItems = prototype.decodeItems;
    Ext.Object.each(prototype._aggregators, function(name) {
        prototype[name] = function(property, begin, end) {
            return this.aggregate(property, name, begin, end);
        };
        prototype[name + 'ByGroup'] = function(property) {
            return this.aggregateByGroup(property, name);
        };
    });
});

/**
 * This class provides access to a range of records in a {@link Ext.data.Store store}.
 * Instances of this class are not created directly but are rather returned by a store's
 * {@link Ext.data.AbstractStore#createActiveRange createActiveRange} method.
 *
 * This class is useful for buffered rendering where only a portion of the total set of
 * records are needed. By passing that information to a `Range`, the access to records
 * can be abstracted even across {@link Ext.data.virtual.Store virtual stores} where
 * only those records needed by views are fetched from the server.
 * @since 6.5.0
 */
Ext.define('Ext.data.Range', {
    requires: [
        'Ext.util.DelayedTask'
    ],
    isDataRange: true,
    /**
     * @cfg {Number} begin
     * The first record index of interest.
     *
     * This property is set by the `goto` method and is stored on the instance for
     * readonly use.
     * @readonly
     */
    begin: 0,
    /**
     * @cfg {Number} buffer
     * The buffer to execute server requests.
     */
    buffer: 0,
    /**
     * @cfg {Number} end
     * The first record beyond the range of interest. This is to make "length" of the
     * range simply `end - begin`.
     *
     * This property is set by the `goto` method and is stored on the instance for
     * readonly use.
     */
    end: 0,
    /**
     * @property (Number} length
     * The number of records in the range of `[begin, end)`. This is equal to the
     * difference `end - begin`.
     *
     * This property is maintained by the `goto` method and is stored on the instance for
     * readonly use.
     * @readonly
     */
    length: 0,
    /**
     * @property {Ext.data.Model[]} records
     * The records corresponding to the `begin` and `end` of this range. For normal
     * stores this is the standard array of records.
     *
     * For a {@link Ext.data.virtual.Store virtual store} this is a sparse object of
     * available records bounded by the limits of this range.
     *
     * In all cases, this object is keyed by the record index and (except for the
     * `length` property) should be treated as an array.
     * @readonly
     */
    /**
     * @cfg {Ext.data.AbstractStore} store
     * The associated store. This config must be supplied at construction and cannot
     * be changed after that time.
     * @readonly
     */
    store: null,
    constructor: function(config) {
        var me = this,
            activeRanges, store;
        Ext.apply(me, config);
        store = me.store;
        if (!(activeRanges = store.activeRanges)) {
            store.activeRanges = activeRanges = [];
        }
        activeRanges.push(me);
        me.refresh();
        if ('begin' in config) {
            me.begin = me.end = 0;
            // Applied on us above, so clear it
            /* eslint-disable-next-line dot-notation */
            me.goto(config.begin, config.end);
        }
    },
    destroy: function() {
        var me = this,
            store = me.store,
            activeRanges = store && store.activeRanges;
        Ext.destroy(me.storeListeners);
        if (activeRanges) {
            Ext.Array.remove(activeRanges, me);
        }
        me.callParent();
    },
    "goto": function(begin, end) {
        var me = this,
            buffer = me.buffer,
            task = me.task;
        me.begin = begin;
        me.end = end;
        me.length = end - begin;
        if (buffer > 0) {
            if (!task) {
                me.task = task = new Ext.util.DelayedTask(me.doGoto, me);
            }
            task.delay(buffer);
        } else {
            me.doGoto();
        }
    },
    privates: {
        lastBegin: 0,
        lastEnd: 0,
        doGoto: Ext.privateFn,
        refresh: function() {
            this.records = this.store.getData().items;
        }
    }
});

/**
 * This class accepts an object that serves as a template for creating new objects. Like
 * other templates (`Ext.XTemplate`) this creation step requires a context object to give
 * the template its values.
 *
 * For example:
 *
 *      var tpl = new Ext.util.ObjectTemplate({
 *          property: 'Hello {name}',
 *          data: {
 *              value: '{age}'
 *          }
 *      });
 *
 *      var obj = tpl.apply({
 *          name: 'Bill',
 *          age: 42
 *      });
 *
 *      // obj = {
 *      //     property: 'Hello Bill',
 *      //     data: {
 *      //         value: 42
 *      //     }
 *      // }
 *
 * @since 5.0.0
 */
Ext.define('Ext.util.ObjectTemplate', {
    requires: [
        'Ext.XTemplate'
    ],
    isObjectTemplate: true,
    excludeProperties: {},
    valueRe: /^[{][a-z.]+[}]$/i,
    statics: {
        /**
         * Creates an `ObjectTemplate` given a config object or instance.
         * @param {Object/Ext.util.ObjectTemplate} template The template object.
         * @param {Object} [options]
         * @return {Ext.util.ObjectTemplate}
         * @since 5.0.0
         */
        create: function(template, options) {
            if (!Ext.isObject(template)) {
                Ext.raise('The template is not an Object');
            }
            return template.isObjectTemplate ? template : new Ext.util.ObjectTemplate(template, options);
        }
    },
    /**
     * Constructs the `ObjectTemplate`. The actual compilation of the object to a ready to
     * apply form happens on the first call to `apply`.
     * @param {Object} template
     * @param {Object} [options]
     * @since 5.0.0
     */
    constructor: function(template, options) {
        Ext.apply(this, options);
        this.template = template;
    },
    /**
     * Applies the given `context` object to this template and returns a new object with
     * the appropriate pieces replaced.
     * @param {Object} context The data used to populate the template.
     * @return {Object}
     * @since 5.0.0
     */
    apply: function(context) {
        var me = this;
        delete me.apply;
        me.apply = me.compile(me.template);
        return me.apply(context);
    },
    privates: {
        /**
         * Compiles the  given template into an `apply` method that is ready to run. This
         * method is used recursively to process object properties and array elements.
         * @param {Mixed} template
         * @return {Function}
         * @since 5.0.0
         */
        compile: function(template) {
            var me = this,
                exclude = me.excludeProperties,
                compiled, i, len, fn;
            // TODO: loops over array or objects
            if (Ext.isString(template)) {
                if (template.indexOf('{') < 0) {
                    fn = function() {
                        return template;
                    };
                } else if (me.valueRe.test(template)) {
                    template = template.substring(1, template.length - 1).split('.');
                    fn = function(context) {
                        var v, i;
                        for (v = context , i = 0; v && i < template.length; ++i) {
                            v = v[template[i]];
                        }
                        return v;
                    };
                } else {
                    template = new Ext.XTemplate(template);
                    fn = function(context) {
                        return template.apply(context);
                    };
                }
            } else if (!template || Ext.isPrimitive(template) || Ext.isFunction(template)) {
                fn = function() {
                    return template;
                };
            } else if (template instanceof Array) {
                compiled = [];
                for (i = 0 , len = template.length; i < len; ++i) {
                    compiled[i] = me.compile(template[i]);
                }
                fn = function(context) {
                    var ret = [],
                        i;
                    for (i = 0; i < len; ++i) {
                        ret[i] = compiled[i](context);
                    }
                    return ret;
                };
            } else {
                compiled = {};
                for (i in template) {
                    if (!exclude[i]) {
                        compiled[i] = me.compile(template[i]);
                    }
                }
                fn = function(context) {
                    var ret = {},
                        i, v;
                    for (i in template) {
                        v = exclude[i] ? template[i] : compiled[i](context);
                        if (v !== undefined) {
                            ret[i] = v;
                        }
                    }
                    return ret;
                };
            }
            return fn;
        }
    }
});

/**
 * @private
 */
Ext.define('Ext.data.schema.Role', {
    /**
     * @property {Ext.data.schema.Association} association
     * @readonly
     */
    isRole: true,
    /**
     * @property {Boolean} left
     * @readonly
     */
    left: true,
    /**
     * @property {Boolean} owner
     * @readonly
     */
    owner: false,
    /**
     * @property {String} side
     * @readonly
     */
    side: 'left',
    /**
     * @property {Boolean} isMany
     * @readonly
     */
    isMany: false,
    /**
     * @property {Ext.Class} cls
     * The `Ext.data.Model` derived class.
     * @readonly
     */
    /**
     * @property {Ext.data.schema.Role} inverse
     * @readonly
     */
    /**
     * @property {String} type
     * The `{@link Ext.data.Model#entityName}` derived class.
     * @readonly
     */
    /**
     * @property {String} role
     * @readonly
     */
    defaultReaderType: 'json',
    _internalReadOptions: {
        recordsOnly: true,
        asRoot: true
    },
    constructor: function(association, config) {
        var me = this,
            extra = config.extra;
        Ext.apply(me, config);
        if (extra) {
            extra = Ext.apply({}, extra);
            delete extra.type;
            Ext.apply(me, extra);
            delete me.extra;
        }
        me.association = association;
        // The Association's owner property starts as either "left" or "right" (a string)
        // and we promote it to a reference to the appropriate Role instance here.
        if (association.owner === me.side) {
            association.owner = me;
            me.owner = true;
        }
    },
    processUpdate: function() {
        Ext.raise('Only the "many" for an association may be processed. "' + this.role + '" is not valid.');
    },
    /**
     * Exclude any locally modified records that don't belong in the store. Include locally
     * modified records that should be in the store. Also correct any foreign keys that
     * need to be updated.
     *
     * @param {Ext.data.Store} store The store.
     * @param {Ext.data.Model} associatedEntity The entity that owns the records.
     * @param {Ext.data.Model[]} records The records to check.
     * @param {Ext.data.Session} session The session holding the records
     * @return {Ext.data.Model[]} The corrected set of records.
     *
     * @private
     */
    processLoad: function(store, associatedEntity, records, session) {
        return records;
    },
    /**
     * @method
     *
     * Check whether a record belongs to any stores when it is added to the session.
     * 
     * @param {Ext.data.Session} session The session
     * @param {Ext.data.Model} record The model being added to the session
     * @private
     */
    checkMembership: Ext.emptyFn,
    /**
     * Adopt the associated items when a record is adopted.
     * @param {Ext.data.Model} record The record being adopted.
     * @param {Ext.data.Session} session The session being adopted into
     * 
     * @private
     */
    adoptAssociated: function(record, session) {
        var other = this.getAssociatedItem(record);
        if (other) {
            session.adopt(other);
        }
    },
    $roleFilterId: '$associationRoleFilter',
    createAssociationStore: function(session, from, records, isComplete) {
        var me = this,
            association = me.association,
            foreignKeyName = association.getFieldName(),
            isMany = association.isManyToMany,
            storeConfig = me.storeConfig,
            id = from.getId(),
            config = {
                // Always want immediate load
                asynchronousLoad: false,
                model: me.cls,
                role: me,
                session: session,
                associatedEntity: from,
                disableMetaChangeEvent: true,
                pageSize: null,
                remoteFilter: true,
                trackRemoved: !session
            },
            store;
        if (isMany) {
            // For many-to-many associations each role has a field
            config.filters = [
                {
                    id: me.$roleFilterId,
                    property: me.inverse.field,
                    // @TODO filterProperty
                    value: id,
                    exactMatch: true
                }
            ];
        } else if (foreignKeyName) {
            config.filters = [
                {
                    id: me.$roleFilterId,
                    property: foreignKeyName,
                    // @TODO filterProperty
                    value: id,
                    exactMatch: true
                }
            ];
            config.foreignKeyName = foreignKeyName;
        }
        if (storeConfig) {
            Ext.apply(config, storeConfig);
        }
        store = Ext.Factory.store(config);
        me.onStoreCreate(store, session, id);
        // Want to run these in all cases for M-1, only with a session M-M
        if (!isMany || session) {
            store.on({
                scope: me,
                add: 'onAddToMany',
                remove: 'onRemoveFromMany',
                clear: 'onRemoveFromMany'
            });
        }
        if (records) {
            store.loadData(records);
        }
        store.complete = !!isComplete;
        return store;
    },
    onStoreCreate: Ext.emptyFn,
    getAssociatedStore: function(inverseRecord, options, scope, records, allowInfer) {
        // Consider the Comment entity with a ticketId to a Ticket entity. The Comment
        // is on the left (the FK holder's side) so we are implementing the guts of
        // the comments() method to load the Store of Comment entities. This trek
        // begins from a Ticket (inverseRecord).
        var me = this,
            storeName = me.getStoreName(),
            store = inverseRecord[storeName],
            hadStore = store,
            session = inverseRecord.session,
            load = options && options.reload,
            source = inverseRecord.$source,
            isComplete = false,
            phantom = false,
            hadSourceStore, args, i, len, raw, rec, sourceStore, hadRecords, isLoading;
        if (!store) {
            if (session) {
                // We want to check whether we can automatically get the store contents from the
                // parent session. For this to occur, we need to have a parent in the session,
                // and the store needs to be created and loaded with the initial dataset.
                if (source) {
                    phantom = source.phantom;
                }
                if (!records && source) {
                    sourceStore = source[storeName];
                    if (sourceStore && !sourceStore.isLoading()) {
                        records = [];
                        raw = sourceStore.getData().items;
                        for (i = 0 , len = raw.length; i < len; ++i) {
                            rec = raw[i];
                            records.push(session.getRecord(rec.self, rec.id));
                        }
                        isComplete = !!sourceStore.complete;
                        hadSourceStore = true;
                    }
                }
                if (!hadSourceStore) {
                    // We'll only hit here if we didn't have a usable source
                    hadRecords = !!records;
                    records = me.findRecords(session, inverseRecord, records, allowInfer);
                    if (!hadRecords && (!records || !records.length)) {
                        records = null;
                    }
                    isComplete = phantom || hadRecords;
                }
            } else {
                // As long as we had the collection exist, we're complete, even if it's empty.
                isComplete = !!records;
            }
            // If the inverse is a phantom, we can't be loading any data so we're complete
            store = me.createAssociationStore(session, inverseRecord, records, isComplete || inverseRecord.phantom);
            store.$source = sourceStore;
            if (!records && (me.autoLoad || options)) {
                load = true;
            }
            inverseRecord[storeName] = store;
        }
        if (options) {
            // We need to trigger a load or the store is already loading. Defer
            // callbacks until that happens
            if (load || store.isLoading()) {
                store.on('load', function(store, records, success, operation) {
                    args = [
                        store,
                        operation
                    ];
                    scope = scope || options.scope || inverseRecord;
                    if (success) {
                        Ext.callback(options.success, scope, args);
                    } else {
                        Ext.callback(options.failure, scope, args);
                    }
                    args.push(success);
                    Ext.callback(options, scope, args);
                    Ext.callback(options.callback, scope, args);
                }, null, {
                    single: true
                });
            } else {
                // Trigger straight away
                args = [
                    store,
                    null
                ];
                scope = scope || options.scope || inverseRecord;
                Ext.callback(options.success, scope, args);
                args.push(true);
                Ext.callback(options, scope, args);
                Ext.callback(options.callback, scope, args);
            }
        }
        isLoading = store.isLoading();
        if (load) {
            if (!isLoading) {
                store.load();
            }
        } else if (hadStore && records && !isLoading) {
            store.loadData(records);
        }
        return store;
    },
    /**
     * Gets the store/record associated with this role from an existing record.
     * Will only return if the value is loaded.
     * 
     * @param {Ext.data.Model} rec The record
     * 
     * @return {Ext.data.Model/Ext.data.Store} The associated item. `null` if not loaded.
     * @private
     */
    getAssociatedItem: function(rec) {
        var key = this.isMany ? this.getStoreName() : this.getInstanceName();
        return rec[key] || null;
    },
    onDrop: Ext.emptyFn,
    onIdChanged: Ext.emptyFn,
    getReaderRoot: function() {
        var me = this;
        return me.associationKey || (me.associationKey = me.association.schema.getNamer().readerRoot(me.role));
    },
    getReader: function() {
        var me = this,
            reader = me.reader,
            Model = me.cls,
            useSimpleAccessors = !me.associationKey,
            root = this.getReaderRoot();
        if (reader && !reader.isReader) {
            if (Ext.isString(reader)) {
                reader = {
                    type: reader
                };
            }
            Ext.applyIf(reader, {
                model: Model,
                rootProperty: root,
                useSimpleAccessors: useSimpleAccessors,
                type: me.defaultReaderType
            });
            reader = me.reader = Ext.createByAlias('reader.' + reader.type, reader);
        }
        return reader;
    },
    getInstanceName: function() {
        var me = this;
        return me.instanceName || (me.instanceName = me.association.schema.getNamer().instanceName(me.role));
    },
    getOldInstanceName: function() {
        return this.oldInstanceName || (this.oldInstanceName = '$old' + this.getInstanceName());
    },
    getStoreName: function() {
        var me = this;
        return me.storeName || (me.storeName = me.association.schema.getNamer().storeName(me.role));
    },
    constructReader: function(fromReader) {
        var me = this,
            reader = me.getReader(),
            Model = me.cls,
            useSimpleAccessors = !me.associationKey,
            root = me.getReaderRoot(),
            proxyReader, proxy;
        // No reader supplied
        if (!reader) {
            proxy = Model.getProxy();
            // if the associated model has a Reader already, use that, otherwise attempt to
            // create a sensible one
            if (proxy) {
                proxyReader = proxy.getReader();
                reader = new proxyReader.self();
                reader.copyFrom(proxyReader);
                reader.setRootProperty(root);
            } else {
                reader = new fromReader.self({
                    model: Model,
                    useSimpleAccessors: useSimpleAccessors,
                    rootProperty: root
                });
            }
            me.reader = reader;
        }
        return reader;
    },
    read: function(record, data, fromReader, readOptions) {
        var reader = this.constructReader(fromReader),
            root = reader.getRoot(data);
        if (root) {
            return reader.readRecords(root, readOptions, this._internalReadOptions);
        }
    },
    getCallbackOptions: function(options, scope, defaultScope) {
        if (typeof options === 'function') {
            options = {
                callback: options,
                scope: scope || defaultScope
            };
        } else if (options) {
            options = Ext.apply({}, options);
            options.scope = scope || options.scope || defaultScope;
        }
        return options;
    },
    doGetFK: function(leftRecord, options, scope) {
        // Consider the Department entity with a managerId to a User entity. This method
        // is the guts of the getManager method that we place on the Department entity to
        // acquire a User entity. We are the "manager" role and that role describes a
        // User. This method is called, however, given a Department (leftRecord) as the
        // start of this trek.
        var me = this,
            // the "manager" role
            cls = me.cls,
            // User
            foreignKey = me.association.getFieldName(),
            // "managerId"
            instanceName = me.getInstanceName(),
            // "manager"
            rightRecord = leftRecord[instanceName],
            // = department.manager
            reload = options && options.reload,
            done = rightRecord !== undefined && !reload,
            session = leftRecord.session,
            foreignKeyId, args;
        if (!done) {
            // We don't have the User record yet, so try to get it.
            if (session) {
                foreignKeyId = leftRecord.get(foreignKey);
                if (foreignKeyId || foreignKeyId === 0) {
                    done = session.peekRecord(cls, foreignKeyId, true) && !reload;
                    rightRecord = session.getRecord(cls, foreignKeyId, false);
                } else {
                    done = true;
                    leftRecord[instanceName] = rightRecord = null;
                }
            } else if (foreignKey) {
                // The good news is that we do indeed have a FK so we can do a load using
                // the value of the FK.
                foreignKeyId = leftRecord.get(foreignKey);
                if (!foreignKeyId && foreignKeyId !== 0) {
                    // A value of null ends that hope though... but we still need to do
                    // some callbacks perhaps.
                    done = true;
                    leftRecord[instanceName] = rightRecord = null;
                } else {
                    // foreignKeyId is the managerId from the Department (record), so
                    // make a new User, set its idProperty and load the real record via
                    // User.load method.
                    if (!rightRecord) {
                        // We may be reloading, let's check if we have one.
                        rightRecord = cls.createWithId(foreignKeyId);
                    }
                }
            } else // we are not done in this case, so don't set "done"
            {
                // Without a FK value by which to request the User record, we cannot do
                // anything. Declare victory and get out.
                done = true;
                rightRecord = null;
            }
        } else if (rightRecord) {
            // If we're still loading, call load again which will handle the extra callbacks.
            done = !rightRecord.isLoading();
        }
        if (done) {
            if (options) {
                args = [
                    rightRecord,
                    null
                ];
                scope = scope || options.scope || leftRecord;
                Ext.callback(options.success, scope, args);
                args.push(true);
                Ext.callback(options, scope, args);
                Ext.callback(options.callback, scope, args);
            }
        } else {
            leftRecord[instanceName] = rightRecord;
            options = me.getCallbackOptions(options, scope, leftRecord);
            rightRecord.load(options);
        }
        return rightRecord;
    },
    doSetFK: function(leftRecord, rightRecord, options, scope) {
        // Consider the Department entity with a managerId to a User entity. This method
        // is the guts of the setManager method that we place on the Department entity to
        // store the User entity. We are the "manager" role and that role describes a
        // User. This method is called, however, given a Department (record) and the User
        // (value).
        var me = this,
            foreignKey = me.association.getFieldName(),
            // "managerId"
            instanceName = me.getInstanceName(),
            // "manager"
            current = leftRecord[instanceName],
            inverse = me.inverse,
            inverseSetter = inverse.setterName,
            // setManagerDepartment for User
            modified, oldInstanceName;
        if (rightRecord && rightRecord.isEntity) {
            if (current !== rightRecord) {
                oldInstanceName = me.getOldInstanceName();
                leftRecord[oldInstanceName] = current;
                leftRecord[instanceName] = rightRecord;
                if (current && current.isEntity) {
                    current[inverse.getInstanceName()] = undefined;
                }
                if (foreignKey) {
                    leftRecord.set(foreignKey, rightRecord.getId());
                }
                delete leftRecord[oldInstanceName];
                leftRecord.onAssociatedRecordSet(rightRecord, me);
                if (inverseSetter) {
                    // Because the rightRecord has a reference back to the leftRecord
                    // we pass on to its setter (if there is one). We've already set
                    // the value on this side so we won't recurse back-and-forth.
                    rightRecord[inverseSetter](leftRecord);
                }
            }
        } else {
            // The value we received could just be the id of the rightRecord so we just
            // need to set the FK accordingly and cleanup any cached references.
            if (!foreignKey) {
                Ext.raise('No foreignKey specified for "' + me.association.left.role + '" by ' + leftRecord.$className);
            }
            modified = (leftRecord.changingKey && !inverse.isMany) || leftRecord.set(foreignKey, rightRecord);
            // set returns the modifiedFieldNames[] or null if nothing was change
            if (modified && current && current.isEntity && !current.isEqual(current.getId(), rightRecord)) {
                // If we just modified the FK value and it no longer matches the id of the
                // record we had cached (ret), remove references from *both* sides:
                leftRecord[instanceName] = undefined;
                if (!inverse.isMany) {
                    current[inverse.getInstanceName()] = undefined;
                }
            }
        }
        if (options) {
            if (Ext.isFunction(options)) {
                options = {
                    callback: options,
                    scope: scope || leftRecord
                };
            }
            return leftRecord.save(options);
        }
    }
});

/**
 * **This class is never created directly. It should be constructed through associations
 * in `Ext.data.Model`.**
 *
 * Associations enable you to express relationships between different {@link Ext.data.Model Models}.
 * Consider an ecommerce system where Users can place Orders - there is a one to many relationship
 * between these Models, one user can have many orders (including 0 orders). Here is what a sample
 * implementation of this association could look like. This example will be referred to in the
 * following sections.
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: [{
 *             name: 'id',
 *             type: 'int'
 *         }, 'name']
 *     });
 *
 *     Ext.define('Order', {
 *         extend: 'Ext.data.Model',
 *         fields: [{
 *             name: 'id',
 *             type: 'int'
 *         }, {
 *             name: 'userId',
 *             type: 'int',
 *             reference: 'User'
 *         }]
 *     });
 *
 * # Association Types
 *
 * Assocations can describe relationships in 3 ways:
 *
 * ## Many To One
 *
 * A single entity (`A`) has a relationship with many (`B`) entities. An example of this is
 * an ecommerce system `User` can have many `Order` entities.
 *
 * This can be defined using `Ext.data.schema.ManyToOne` for keyed associations, or 
 * `Ext.data.schema.HasMany` for keyless associations.
 * 
 * ## One To One
 *
 * A less common form of Many To One, a single entity (`A`) has a relationship with at most 1 entity
 * (`B`). This is often used when partitioning data. For example a `User` may have a single
 * `UserInfo` object that stores extra metadata about the user.
 *
 * This can be defined using `Ext.data.schema.OneToOne` for keyed associations, or 
 * `Ext.data.schema.HasOne` for keyless associations.
 * 
 * ## Many To Many
 *
 * An entity (`A`) may have a have a relationship with many (`B`) entities. That (`B`) entity may
 * also have a relationship with many `A` entities. For example a single `Student` can have many
 * `Subject` entities and a single `Subject` can have many `Student` entities.
 *
 * This can be defined using `Ext.data.schema.ManyToMany`. Many To Many relationships are read-only
 * unless used with a `Ext.data.Session`.
 *     
 *
 * # Keyed vs Keyless Associations
 *
 * Associations can be declared in 2 ways, which are outlined below.
 *
 * ## Keyed associations
 *
 * A keyed association relies on a field in the model matching the id of another model. Membership
 * is driven by the key. This is the type of relationship that is typically used in a relational
 * database. This is declared using the ||reference|| configuration on a model field. An example
 * of this can be seen  above for `User/Order`.
 *
 * # Keyless associations
 *
 * A keyless association relies on data hierarchy to determine membership. Items are members because
 * they are contained by another entity. This type of relationship is common with NoSQL databases.
 * formats. A simple example definition using `User/Order`:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: [{
 *             name: 'id',
 *             type: 'int'
 *         }, 'name'],
 *         hasMany: 'Order'
 *     });
 *
 *     Ext.define('Order', {
 *         extend: 'Ext.data.Model',
 *         fields: [{
 *             name: 'id',
 *             type: 'int'
 *         }]
 *     });
 *
 * # Advantages of Associations
 * 
 * Assocations make it easier to work with Models that share a connection. Some of the main
 * functionality includes:
 *
 * ## Generated Accessors/Setters
 *
 * Associated models will automatically generate named methods that allow for accessing the
 * associated data. The names for these are created using a {@link Ext.data.schema.Schema Schema},
 * to provide a consistent and predictable naming structure.
 *
 * Using the example code above, there will be 3 generated methods:
 * + `User` will have an `orders()` function that returns a `Ext.data.Store` of`Orders`. 
 * + `Order` will have a `getUser` method which will return a `User` Model.
 * + `Order` will have a `setUser` method that will accept a `User` model or a key value.
 *
 * ## Nested Loading
 *
 * Nested loading is the ability to load hierarchical associated data from a remote source within
 * a single request. In the following example, each `User` in the `users` store has an `orders`
 * store. Each `orders` store is populated with `Order` models read from the request. Each `Order`
 * model also has a reference back to the appropriate `User`.
 *
 *     // Sample JSON data returned by /Users
 *     [{
 *         "id": 1,
 *         "name": "User Foo",
 *         "orders": [{
 *             "id": 101,
 *             "userId": 1
 *         }, {
 *             "id": 102,
 *             "userId": 1
 *         }, {
 *             "id": 103,
 *             "userId": 1
 *         }]
 *     }, {
 *         "id": 2,
 *         "name": "User Bar",
 *         "orders": [{
 *             "id": 201,
 *             "userId": 2
 *         }, {
 *             "id": 202,
 *             "userId": 2
 *         }]
 *     }]
 *
 *     // Application code
 *     var users = new Ext.data.Store({
 *         model: 'User',
 *         proxy: {
 *             type: 'ajax',
 *             url: '/Users'
 *         }
 *     });
 *     users.load(function() {
 *         var user1 = users.first(),
 *             user2 = users.last(),
 *             orders1 = user1.orders(),
 *             orders2 = user2.orders();
 *
 *         // 3 orders, same reference back to user1
 *         console.log(orders1.getCount(), orders1.first().getUser() === user1);
 *         // 2 orders, same reference back to user2
 *         console.log(orders2.getCount(), orders2.first().getUser() === user2);
 *     });
 *
 * ## Binding
 *
 * Data binding using {@link Ext.app.ViewModel ViewModels} have functionality to be able
 * to recognize associated data as part of a bind statement. For example:
 * + `{user.orders}` binds to the orders store for a user.
 * + `{order.user.name}` binds to the name of the user taken from the order.
 * 
 *
 * # Association Concepts
 *
 * ## Roles
 *
 * The role is used to determine generated names for an association. By default, the role is
 * generated from either the field name (in a keyed association) or the model name. This naming
 * follows a pattern defined by the `Ext.data.schema.Namer`. To change a specific instance,
 * an explicit role can be specified:
 *
 *     Ext.define('Thread', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'title']
 *     });
 *
 *     Ext.define('Post', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'content', {
 *             name: 'threadId',
 *             reference: {
 *                 type: 'Thread',
 *                 role: 'discussion',
 *                 inverse: 'comments'
 *                 
 *             }
 *         }]
 *     });
 *
 * In the above example, the `Thread` will be decorated with a `comments` method that returns
 * the store. The `Post` will be decorated with `getDiscussion/setDiscussion` methods.
 *
 * ## Generated Methods
 *
 * Associations generate methods to allow reading and manipulation on associated data. 
 * 
 * On records that have a "to many" relationship, a single methods that returns a `Ext.data.Store`
 * is created.  See {@link #storeGetter}. On records that have a "to one" relationship, 2 methods
 * are generated, a {@link #recordGetter getter} and a {@link #recordSetter setter}.
 *
 * ## Reflexive
 *
 * Associations are reflexive. By declaring one "side" of the relationship, the other is
 * automatically setup. In the example below, there is no code in the `Thread` entity regarding
 * the association, however by virtue of the declaration in post, `Thread` is decorated with the
 * appropriate infrastructure to participate in the association.
 *
 *     Ext.define('Thread', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'title']
 *     });
 *
 *     Ext.define('Post', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'content', {
 *             name: 'threadId',
 *             reference: 'Thread'
 *         }]
 *     });
 *
 * ## Naming
 *
 * Referring to model names in associations depends on their {@link Ext.data.Model#entityName}. See
 * the "Relative Naming" section in the `Ext.data.schema.Schema` documentation.
 */
Ext.define('Ext.data.schema.Association', {
    requires: [
        'Ext.data.schema.Role'
    ],
    isOneToOne: false,
    isManyToOne: false,
    isManyToMany: false,
    /**
     * @cfg {String} associationKey
     * The name of the property in the data to read the association from. Defaults to the
     * name of the associated model.
     */
    /**
     * @method storeGetter
     * **This is not a real method, it is placeholder documentation for a generated method on
     * a `Ext.data.Model`.**
     *
     * Gets a store configured with the model of the "many" record.
     * @param {Object/Function} [options] The options for the getter, or a callback function
     * to execute. If specified as a function, it will act as the `callback` option.
     *
     * @param {Boolean} [options.reload] `true` to force the store to reload from the server.
     *
     * @param {Object} [options.scope] The `this` reference for the callback.
     * Defaults to the record.
     * 
     * @param {Function} [options.success] A function to execute when the store loads successfully.
     * If the store has already loaded, this will be called immediately and the `Operation` will be
     * `null`. The success is passed the following parameters:
     * @param {Ext.data.Store} [options.success.store] The store.
     * @param {Ext.data.operation.Operation} [options.success.operation] The operation. `null`
     * if no load occurred.
     *
     * @param {Function} [options.failure] A function to execute when the store load fails.
     * If the store has already loaded, this will not be called.
     * The failure is passed the following parameters:
     * @param {Ext.data.Store} [options.failure.store] The store.
     * @param {Ext.data.operation.Operation} [options.failure.operation] The operation
     * 
     * @param {Function} [options.callback] A function to execute when the store loads, whether
     * it is successful or failed. If the store has already loaded, this will be called immediately
     * and the `Operation` will be `null`. The callback is passed the following parameters:
     * @param {Ext.data.Store} [options.callback.store] The store.
     * @param {Ext.data.operation.Operation} [options.callback.operation] The operation. `null`
     * if no load occurred.
     * @param {Boolean} [options.callback.success] `true` if the load was successful. If already
     * loaded this will always be true.
     *
     * @param {Object} [scope] The `this` reference for the callback. Defaults to the record.
     *
     * @return {Ext.data.Store} The store.
     */
    /**
     * @method recordGetter
     * **This is not a real method, it is placeholder documentation for a generated method on
     * a `Ext.data.Model`.**
     *
     * Gets a model of the "one" type.
     * @param {Object/Function} [options] The options for the getter, or a callback function
     * to execute. If specified as a function, it will act as the `callback` option.
     *
     * @param {Boolean} [options.reload] `true` to force the record to reload from the server.
     *
     * @param {Object} [options.scope] The `this` reference for the callback.
     * Defaults to the record.
     * 
     * @param {Function} [options.success] A function to execute when the record loads successfully.
     * If the record has already loaded, this will be called immediately and the `Operation` will be
     * `null`. The success is passed the following parameters:
     * @param {Ext.data.Model} [options.success.record] The record.
     * @param {Ext.data.operation.Operation} [options.success.operation] The operation. `null`
     * if no load occurred.
     *
     * @param {Function} [options.failure] A function to execute when the record load fails.
     * If the record has already loaded, this will not be called.
     * The failure is passed the following parameters:
     * @param {Ext.data.Model} [options.failure.record] The record.
     * @param {Ext.data.operation.Operation} [options.failure.operation] The operation
     * 
     * @param {Function} [options.callback] A function to execute when the record loads, whether
     * it is successful or failed. If the record has already loaded, this will be called immediately
     * and the `Operation` will be `null`. The callback is passed the following parameters:
     * @param {Ext.data.Model} [options.callback.record] The record.
     * @param {Ext.data.operation.Operation} [options.callback.operation] The operation. `null`
     * if no load occurred.
     * @param {Boolean} [options.callback.success] `true` if the load was successful. If already
     * loaded this will always be true.
     * 
     * @param {Object} [scope] The `this` reference for the callback. Defaults to the record.
     * @return {Ext.data.Model} The record. `null` if the reference has been previously specified
     * as empty.
     */
    /**
     * @method recordSetter **This is not a real method, it is placeholder documentation
     * for a generated method on a `Ext.data.Model`.**
     *
     * Sets a model of the "one" type.
     * @param {Ext.data.Model/Object} value The value to set. This can be a model instance,
     * a key value (if a keyed association) or `null` to clear the value.
     *
     * @param {Object/Function} [options] Options to handle callback. If specified as
     * a function, it will act as the `callback` option. If specified as an object, the params
     * are the same as {@link Ext.data.Model#save}. If  options is specified,
     * {@link Ext.data.Model#save} will be called on this record.
     */
    /**
     * @cfg {String} name
     * The name of this association.
     */
    /**
     * @property {Object} owner
     * Points at either `left` or `right` objects if one is the owning party in this
     * association or is `null` if there is no owner.
     * @readonly
     */
    owner: null,
    /**
     * @property {Ext.Class} definedBy
     * @readonly
     */
    /**
     * @property {Ext.data.field.Field} field
     * @readonly
     */
    field: null,
    /**
     * @property {Ext.data.schema.Schema} schema
     * @readonly
     */
    /**
     * @property {Boolean} nullable
     * @readonly
     */
    /**
     * @property {Ext.data.schema.Role} left
     * @readonly
     */
    /**
     * @property {Ext.data.schema.Role} right
     * @readonly
     */
    constructor: function(config) {
        var me = this,
            left, right;
        Ext.apply(me, config);
        me.left = left = new me.Left(me, me.left);
        me.right = right = new me.Right(me, me.right);
        left.inverse = right;
        right.inverse = left;
    },
    hasField: function() {
        return !!this.field;
    },
    getFieldName: function() {
        var field = this.field;
        return field ? field.name : '';
    }
});

/**
 * **This class is never created directly. It should be constructed through associations
 * in `Ext.data.Model`.**
 *
 * This is a specialized version of `Ext.data.schema.ManyToOne` that declares a relationship between
 * a single  entity type and a single related entities. The relationship can be declared as a keyed
 * or keyless relationship.
 *
 *     // Keyed
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name', {
 *             name: 'userInfoId',
 *             reference: {
 *                 type: 'UserInfo',
 *                 unique: true
 *             }
 *         }]
 *     });
 *
 *     Ext.define('UserInfo', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'secretKey']
 *     });
 *
 *     // Keyless
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name'],
 *         hasOne: 'UserInfo'
 *     });
 *
 *     Ext.define('Ticket', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'secretKey']
 *     });
 *
 *     // Generated methods
 *     var user = new User();
 *     user.getUserInfo();
 *     user.setUserInfo();
 *     
 *     var info = new UserInfo();
 *     info.getUser();
 *     info.setUser();
 *     
 *
 *     var ticket = new Ticket();
 *     ticket.setCustomer(customer);
 *     console.log(ticket.getCustomer()); // The customer object
 *
 * By declaring a keyed relationship, extra functionality is gained that maintains
 * the key field in the model as changes are made to the association. 
 * 
 * For available configuration options, see {@link Ext.data.schema.Reference}.
 * Each record type will have a {@link Ext.data.schema.Association#recordGetter getter} and
 * {@link Ext.data.schema.Association#recordSetter setter}.
 */
Ext.define('Ext.data.schema.OneToOne', {
    extend: 'Ext.data.schema.Association',
    isOneToOne: true,
    isToOne: true,
    kind: 'one-to-one',
    Left: Ext.define(null, {
        extend: 'Ext.data.schema.Role',
        onDrop: function(rightRecord, session) {
            var leftRecord = this.getAssociatedItem(rightRecord);
            rightRecord[this.getInstanceName()] = null;
            if (leftRecord) {
                leftRecord[this.inverse.getInstanceName()] = null;
            }
        },
        onIdChanged: function(rightRecord, oldId, newId) {
            var leftRecord = this.getAssociatedItem(rightRecord),
                fieldName = this.association.getFieldName();
            if (!rightRecord.session && leftRecord && fieldName) {
                leftRecord.set(fieldName, newId);
            }
        },
        createGetter: function() {
            var me = this;
            return function() {
                // 'this' refers to the Model instance inside this function
                return me.doGet(this);
            };
        },
        createSetter: function() {
            var me = this;
            return function(value) {
                // 'this' refers to the Model instance inside this function
                return me.doSet(this, value);
            };
        },
        doGet: function(rightRecord) {
            // Consider the Department entity with a managerId to a User entity. The
            // Department is on the left (the FK holder's side) so we are implementing the
            // guts of the getManagerDepartment method we place on the User entity. Since
            // we represent the "managerDepartment" role and as such our goal is to get a
            // Department instance, we start that from the User (rightRecord). Sadly that
            // record has no FK back to us.
            var instanceName = this.getInstanceName(),
                // ex "managerDepartment"
                ret = rightRecord[instanceName],
                session = rightRecord.session;
            if (!ret && session) {}
            // @TODO: session - we'll cache the result on the record as always
            // but to get it we must ask the session
            return ret || null;
        },
        doSet: function(rightRecord, leftRecord) {
            // We are the guts of the setManagerDepartment method we place on the User
            // entity. Our goal here is to establish the relationship between the new
            // Department (leftRecord) and the User (rightRecord).
            var instanceName = this.getInstanceName(),
                // ex "managerDepartment"
                ret = rightRecord[instanceName],
                inverseSetter = this.inverse.setterName;
            // setManager for Department
            if (ret !== leftRecord) {
                rightRecord[instanceName] = leftRecord;
                if (inverseSetter) {
                    // Because the FK is owned by the inverse record, we delegate the
                    // majority of work to its setter. We've already locked in the only
                    // thing we keep on this side so we won't recurse back-and-forth.
                    leftRecord[inverseSetter](rightRecord);
                }
                rightRecord.onAssociatedRecordSet(leftRecord, this);
            }
            return ret;
        },
        read: function(rightRecord, node, fromReader, readOptions) {
            var me = this,
                leftRecords = me.callParent([
                    rightRecord,
                    node,
                    fromReader,
                    readOptions
                ]),
                leftRecord;
            if (leftRecords) {
                leftRecord = leftRecords[0];
                if (leftRecord) {
                    leftRecord[me.inverse.getInstanceName()] = rightRecord;
                    rightRecord[me.getInstanceName()] = leftRecord;
                    // Inline associations should *not* arrive on the "data" object:
                    delete rightRecord.data[me.role];
                }
            }
        }
    }),
    Right: Ext.define(null, {
        extend: 'Ext.data.schema.Role',
        left: false,
        side: 'right',
        createGetter: function() {
            // As the target of the FK (say "manager" for the Department entity) this
            // getter is responsible for getting the entity referenced by the FK value.
            var me = this;
            return function(options, scope) {
                // 'this' refers to the Model instance inside this function
                return me.doGetFK(this, options, scope);
            };
        },
        createSetter: function() {
            var me = this;
            return function(value, options, scope) {
                // 'this' refers to the Model instance inside this function
                return me.doSetFK(this, value, options, scope);
            };
        },
        onDrop: function(leftRecord, session) {
            var me = this,
                field = me.association.field,
                rightRecord = me.getAssociatedItem(leftRecord),
                id;
            if (me.inverse.owner) {
                if (session && field) {
                    id = leftRecord.get(field.name);
                    if (id || id === 0) {
                        rightRecord = session.getEntry(me.cls, id).record;
                        if (rightRecord) {
                            rightRecord.drop();
                        }
                    }
                } else {
                    if (rightRecord) {
                        rightRecord.drop();
                    }
                }
            }
            if (field) {
                leftRecord.set(field.name, null);
            }
            leftRecord[me.getInstanceName()] = null;
            if (rightRecord) {
                rightRecord[me.inverse.getInstanceName()] = null;
            }
        },
        onValueChange: function(leftRecord, session, newValue) {
            // Important to get the record before changing the key.
            var me = this,
                rightRecord = leftRecord[me.getOldInstanceName()] || me.getAssociatedItem(leftRecord),
                // eslint-disable-line max-len
                hasNewValue = newValue || newValue === 0,
                instanceName = me.getInstanceName(),
                cls = me.cls;
            leftRecord.changingKey = true;
            me.doSetFK(leftRecord, newValue);
            if (!hasNewValue) {
                leftRecord[instanceName] = null;
            } else if (session && cls) {
                // Setting to undefined is important so that we can load the record later.
                leftRecord[instanceName] = session.peekRecord(cls, newValue) || undefined;
            }
            if (me.inverse.owner && rightRecord) {
                me.association.schema.queueKeyCheck(rightRecord, me);
            }
            leftRecord.changingKey = false;
        },
        checkKeyForDrop: function(rightRecord) {
            var leftRecord = this.inverse.getAssociatedItem(rightRecord);
            if (!leftRecord) {
                // Not reassigned to another parent
                rightRecord.drop();
            }
        },
        read: function(leftRecord, node, fromReader, readOptions) {
            var me = this,
                rightRecords = me.callParent([
                    leftRecord,
                    node,
                    fromReader,
                    readOptions
                ]),
                rightRecord, field, fieldName, session, refs, id, oldId, setKey, data;
            if (rightRecords) {
                rightRecord = rightRecords[0];
                field = me.association.field;
                if (field) {
                    fieldName = field.name;
                }
                session = leftRecord.session;
                data = leftRecord.data;
                if (rightRecord) {
                    if (session) {
                        refs = session.getRefs(rightRecord, this.inverse, true);
                        // If we have an existing reference in the session, or we don't and the data
                        // is undefined, allow the nested load to go ahead
                        setKey = (refs && refs[leftRecord.id]) || (data[fieldName] === undefined);
                    } else {
                        setKey = true;
                    }
                    if (setKey) {
                        // We want to poke the inferred key onto record if it exists, but we don't
                        // want to mess with the dirty or modified state of the record.
                        if (field) {
                            oldId = data[fieldName];
                            id = rightRecord.id;
                            if (oldId !== id) {
                                data[fieldName] = id;
                                if (session) {
                                    session.updateReference(leftRecord, field, id, oldId);
                                }
                            }
                        }
                        rightRecord[me.inverse.getInstanceName()] = leftRecord;
                        leftRecord[me.getInstanceName()] = rightRecord;
                    }
                    // Inline associations should *not* arrive on the "data" object:
                    delete data[me.role];
                }
            }
        }
    })
});

/**
 * **This class is never created directly. It should be constructed through associations
 * in `Ext.data.Model`.**
 *
 * Declares a relationship between a single entity type and multiple related entities.
 * The relationship can be declared as a keyed or keyless relationship.
 *
 *     // Keyed
 *     Ext.define('Customer', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name']
 *     });
 *
 *     Ext.define('Ticket', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'title', {
 *             name: 'customerId',
 *             reference: 'Customer'
 *         }]
 *     });
 *
 *     // Keyless
 *     Ext.define('Customer', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name'],
 *         hasMany: 'Ticket'
 *     });
 *
 *     Ext.define('Ticket', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'title']
 *     });
 *
 *     // Generated methods
 *     var customer = new Customer();
 *     customer.tickets();
 *
 *     var ticket = new Ticket();
 *     ticket.getCustomer();
 *     ticket.setCustomer();
 *
 * By declaring a keyed relationship, extra functionality is gained that maintains
 * the key field in the model as changes are made to the association. 
 * 
 * For available configuration options, see {@link Ext.data.schema.Reference}.
 * The "one" record type will have a generated {@link Ext.data.schema.Association#storeGetter}.
 * The "many" record type will have a {@link Ext.data.schema.Association#recordGetter getter}
 * and {@link Ext.data.schema.Association#recordSetter setter}.
 */
Ext.define('Ext.data.schema.ManyToOne', {
    extend: 'Ext.data.schema.Association',
    isManyToOne: true,
    isToOne: true,
    kind: 'many-to-one',
    Left: Ext.define(null, {
        extend: 'Ext.data.schema.Role',
        isMany: true,
        onDrop: function(rightRecord, session) {
            var me = this,
                store = me.getAssociatedItem(rightRecord),
                leftRecords, len, i, id;
            if (store) {
                // Removing will cause the foreign key to be set to null.
                leftRecords = store.removeAll();
                if (leftRecords && me.inverse.owner) {
                    // If we're a child, we need to destroy all the "tickets"
                    for (i = 0 , len = leftRecords.length; i < len; ++i) {
                        leftRecords[i].drop();
                    }
                }
                store.destroy();
                rightRecord[me.getStoreName()] = null;
            } else if (session) {
                leftRecords = session.getRefs(rightRecord, me);
                if (leftRecords) {
                    for (id in leftRecords) {
                        leftRecords[id].drop();
                    }
                }
            }
        },
        onIdChanged: function(rightRecord, oldId, newId) {
            var fieldName = this.association.getFieldName(),
                store = this.getAssociatedItem(rightRecord),
                leftRecords, i, len, filter;
            if (store) {
                filter = store.getFilters().get(this.$roleFilterId);
                if (filter) {
                    filter.setValue(newId);
                }
                // A session will automatically handle this updating. If we don't have a field
                // then there's nothing to do here.
                if (!rightRecord.session && fieldName) {
                    leftRecords = store.getDataSource().items;
                    for (i = 0 , len = leftRecords.length; i < len; ++i) {
                        leftRecords[i].set(fieldName, newId);
                    }
                }
            }
        },
        processUpdate: function(session, associationData) {
            var me = this,
                entityType = me.inverse.cls,
                items = associationData.R,
                id, rightRecord, store, leftRecords;
            if (items) {
                for (id in items) {
                    rightRecord = session.peekRecord(entityType, id);
                    if (rightRecord) {
                        leftRecords = session.getEntityList(me.cls, items[id]);
                        store = me.getAssociatedItem(rightRecord);
                        if (store) {
                            store.loadData(leftRecords);
                            store.complete = true;
                        } else {
                            // We don't have a store. Create it and add the records.
                            rightRecord[me.getterName](null, null, leftRecords);
                        }
                    } else {
                        session.onInvalidAssociationEntity(entityType, id);
                    }
                }
            }
        },
        findRecords: function(session, rightRecord, leftRecords, allowInfer) {
            var ret = leftRecords,
                refs = session.getRefs(rightRecord, this, true),
                field = this.association.field,
                fieldName, leftRecord, id, i, len, seen;
            if (field && (refs || allowInfer)) {
                fieldName = field.name;
                ret = [];
                if (leftRecords) {
                    seen = {};
                    // Loop over the records returned by the server and
                    // check they all still belong. If the session doesn't have any prior knowledge
                    // and we're allowed to infer the parent id (via nested loading), only do so if
                    // we explicitly have an id specified
                    for (i = 0 , len = leftRecords.length; i < len; ++i) {
                        leftRecord = leftRecords[i];
                        id = leftRecord.id;
                        if (refs && refs[id]) {
                            ret.push(leftRecord);
                        } else if (allowInfer && leftRecord.data[fieldName] === undefined) {
                            ret.push(leftRecord);
                            leftRecord.data[fieldName] = rightRecord.id;
                            session.updateReference(leftRecord, field, rightRecord.id, undefined);
                        }
                        seen[id] = true;
                    }
                }
                // Loop over the expected set and include any missing records.
                if (refs) {
                    for (id in refs) {
                        if (!seen || !seen[id]) {
                            ret.push(refs[id]);
                        }
                    }
                }
            }
            return ret;
        },
        processLoad: function(store, rightRecord, leftRecords, session) {
            var ret = leftRecords;
            if (session) {
                // Allow infer here, we only get called when loading an associated store
                ret = this.findRecords(session, rightRecord, leftRecords, true);
            }
            this.onLoadMany(rightRecord, ret, session);
            return ret;
        },
        adoptAssociated: function(rightRecord, session) {
            var store = this.getAssociatedItem(rightRecord),
                leftRecords, i, len;
            if (store) {
                store.setSession(session);
                leftRecords = store.getData().items;
                for (i = 0 , len = leftRecords.length; i < len; ++i) {
                    session.adopt(leftRecords[i]);
                }
            }
        },
        createGetter: function() {
            var me = this;
            return function(options, scope, leftRecords) {
                // 'this' refers to the Model instance inside this function
                return me.getAssociatedStore(this, options, scope, leftRecords, true);
            };
        },
        createSetter: null,
        // no setter for an isMany side
        onAddToMany: function(store, leftRecords) {
            var rightRecord = store.getAssociatedEntity();
            if (this.association.field) {
                this.syncFK(leftRecords, rightRecord, false);
            } else {
                this.setInstances(rightRecord, leftRecords);
            }
        },
        onLoadMany: function(rightRecord, leftRecords, session) {
            this.setInstances(rightRecord, leftRecords, session);
        },
        onRemoveFromMany: function(store, leftRecords) {
            if (this.association.field) {
                this.syncFK(leftRecords, store.getAssociatedEntity(), true);
            } else {
                this.setInstances(null, leftRecords);
            }
        },
        read: function(rightRecord, node, fromReader, readOptions) {
            var me = this,
                // We use the inverse role here since we're setting ourselves
                // on the other record
                instanceName = me.inverse.getInstanceName(),
                leftRecords = me.callParent([
                    rightRecord,
                    node,
                    fromReader,
                    readOptions
                ]),
                store, len, i;
            if (leftRecords) {
                // Create the store and dump the data
                store = rightRecord[me.getterName](null, null, leftRecords);
                // Inline associations should *not* arrive on the "data" object:
                delete rightRecord.data[me.role];
                leftRecords = store.getData().items;
                for (i = 0 , len = leftRecords.length; i < len; ++i) {
                    leftRecords[i][instanceName] = rightRecord;
                }
            }
        },
        setInstances: function(rightRecord, leftRecords, session) {
            var instanceName = this.inverse.getInstanceName(),
                id = rightRecord ? rightRecord.getId() : null,
                field = this.association.field,
                len = leftRecords.length,
                i, leftRecord, oldId, data, name;
            for (i = 0; i < len; ++i) {
                leftRecord = leftRecords[i];
                leftRecord[instanceName] = rightRecord;
                if (field) {
                    name = field.name;
                    data = leftRecord.data;
                    oldId = data[name];
                    if (oldId !== id) {
                        data[name] = id;
                        if (session) {
                            session.updateReference(leftRecord, field, id, oldId);
                        }
                    }
                }
            }
        },
        syncFK: function(leftRecords, rightRecord, clearing) {
            // We are called to set things like the FK (ticketId) of an array of Comment
            // entities. The best way to do that is call the setter on the Comment to set
            // the Ticket. Since we are setting the Ticket, the name of that setter is on
            // our inverse role.
            var foreignKeyName = this.association.getFieldName(),
                inverse = this.inverse,
                setter = inverse.setterName,
                // setTicket
                instanceName = inverse.getInstanceName(),
                i = leftRecords.length,
                id = rightRecord.getId(),
                different, leftRecord, val;
            while (i-- > 0) {
                leftRecord = leftRecords[i];
                different = !leftRecord.isEqual(id, leftRecord.get(foreignKeyName));
                val = clearing ? null : rightRecord;
                if (different !== clearing) {
                    // clearing === true
                    //      different === true  :: leave alone (not associated anymore)
                    //   ** different === false :: null the value (no longer associated)
                    //
                    // clearing === false
                    //   ** different === true  :: set the value (now associated)
                    //      different === false :: leave alone (already associated)
                    //
                    leftRecord.changingKey = true;
                    leftRecord[setter](val);
                    leftRecord.changingKey = false;
                } else {
                    // Ensure we set the instance, we may only have the key
                    leftRecord[instanceName] = val;
                }
            }
        }
    }),
    Right: Ext.define(null, {
        extend: 'Ext.data.schema.Role',
        left: false,
        side: 'right',
        onDrop: function(leftRecord, session) {
            // By virtue of being dropped, this record will be removed
            // from any stores it belonged to. The only case we have
            // to worry about is if we have a session but were not yet
            // part of any stores, so we need to clear the foreign key.
            var field = this.association.field;
            if (field) {
                leftRecord.set(field.name, null);
            }
            leftRecord[this.getInstanceName()] = null;
        },
        createGetter: function() {
            // As the target of the FK (say "ticket" for the Comment entity) this
            // getter is responsible for getting the entity referenced by the FK value.
            var me = this;
            return function(options, scope) {
                // 'this' refers to the Comment instance inside this function
                return me.doGetFK(this, options, scope);
            };
        },
        createSetter: function() {
            var me = this;
            return function(rightRecord, options, scope) {
                // 'this' refers to the Comment instance inside this function
                return me.doSetFK(this, rightRecord, options, scope);
            };
        },
        checkMembership: function(session, leftRecord) {
            var field = this.association.field,
                store;
            if (field) {
                store = this.getSessionStore(session, leftRecord.get(field.name));
                // Check we're not in the middle of an add to the store.
                if (store && !store.contains(leftRecord)) {
                    store.add(leftRecord);
                }
            }
        },
        onValueChange: function(leftRecord, session, newValue, oldValue) {
            // If we have a session, we may be able to find the new store this belongs to
            // If not, the best we can do is to remove the record from the associated store/s.
            var me = this,
                instanceName = me.getInstanceName(),
                cls = me.cls,
                hasNewValue, joined, store, i, associated, rightRecord;
            if (!leftRecord.changingKey) {
                hasNewValue = newValue || newValue === 0;
                if (!hasNewValue) {
                    leftRecord[instanceName] = null;
                }
                if (session) {
                    // Find the store that holds this record and remove it if possible.
                    store = me.getSessionStore(session, oldValue);
                    if (store) {
                        store.remove(leftRecord);
                    }
                    // If we have a new value, try and find it and push it into the new store.
                    if (hasNewValue) {
                        store = me.getSessionStore(session, newValue);
                        if (store && !store.isLoading()) {
                            store.add(leftRecord);
                        }
                        if (cls) {
                            rightRecord = session.peekRecord(cls, newValue);
                        }
                        // Setting to undefined is important so that we can load the record later.
                        leftRecord[instanceName] = rightRecord || undefined;
                    }
                } else {
                    joined = leftRecord.joined;
                    if (joined) {
                        // Loop backwards because the store remove may cause unjoining, which means 
                        // removal from the joined array.
                        for (i = joined.length - 1; i >= 0; i--) {
                            store = joined[i];
                            if (store.isStore) {
                                associated = store.getAssociatedEntity();
                                if (associated && associated.self === me.cls && associated.getId() === oldValue) {
                                    store.remove(leftRecord);
                                }
                            }
                        }
                    }
                }
            }
            if (me.owner && newValue === null) {
                me.association.schema.queueKeyCheck(leftRecord, me);
            }
        },
        checkKeyForDrop: function(leftRecord) {
            var field = this.association.field;
            if (leftRecord.get(field.name) === null) {
                leftRecord.drop();
            }
        },
        getSessionStore: function(session, value) {
            // May not have the cls loaded yet
            var cls = this.cls,
                rec;
            if (cls) {
                rec = session.peekRecord(cls, value);
                if (rec) {
                    return this.inverse.getAssociatedItem(rec);
                }
            }
        },
        read: function(leftRecord, node, fromReader, readOptions) {
            var rightRecords = this.callParent([
                    leftRecord,
                    node,
                    fromReader,
                    readOptions
                ]),
                rightRecord;
            if (rightRecords) {
                rightRecord = rightRecords[0];
                if (rightRecord) {
                    leftRecord[this.getInstanceName()] = rightRecord;
                    delete leftRecord.data[this.role];
                }
            }
        }
    })
});

/**
 * This relationship describes the case where any one entity of one type may relate to any
 * number of entities of another type, and also in the reverse.
 * 
 * This form of association cannot store id's in the related entities since that would
 * limit the number of related entities to one for the entity with the foreign key. Instead,
 * these relationships are typically implemented using a so-called "matrix" table. This
 * table typically has two columns to hold the id's of a pair of related entities. This
 * pair of id's is unique in the matrix table.
 * 
 * # Declaration Forms
 * 
 *      // Fully spelled out - all properties are their defaults:
 *      
 *      Ext.define('App.models.Group', {
 *          extend: 'Ext.data.Model',
 *          
 *          manyToMany: {
 *              UserGroups: {
 *                  type: 'User',
 *                  role: 'users',
 *                  field: 'userId',
 *                  right: {
 *                      field: 'groupId',
 *                      role: 'groups'
 *                  }
 *              }
 *          }
 *      });
 *
 *      // Eliminate "right" object and use boolean to indicate Group is on the
 *      // right. By default, left/right is determined by alphabetic order.
 *      
 *      Ext.define('App.models.Group', {
 *          extend: 'Ext.data.Model',
 *          
 *          manyToMany: {
 *              UserGroups: {
 *                  type: 'User',
 *                  role: 'users',
 *                  field: 'userId',
 *                  right: true
 *              }
 *          }
 *      });
 *
 *      // Eliminate object completely and rely on string to name the other type. Still
 *      // keep Group on the "right".
 *      
 *      Ext.define('App.models.Group', {
 *          extend: 'Ext.data.Model',
 *          
 *          manyToMany: {
 *              UserGroups: 'User#'   // '#' is on the side (left or right) of Group
 *          }
 *      });
 *
 *      // Remove explicit matrix name and keep Group on the "right". Generated matrixName
 *      // remains "UserGroups".
 *      
 *      Ext.define('App.models.Group', {
 *          extend: 'Ext.data.Model',
 *          
 *          manyToMany: [
 *              'User#'
 *          ]
 *      });
 *
 *      // Minimal definition but now Group is on the "left" since "Group" sorts before
 *      // "User". Generated matrixName is now "GroupUsers".
 *      
 *      Ext.define('App.models.Group', {
 *          extend: 'Ext.data.Model',
 *          
 *          manyToMany: [
 *              'User'
 *          ]
 *      });
 */
Ext.define('Ext.data.schema.ManyToMany', {
    extend: 'Ext.data.schema.Association',
    isManyToMany: true,
    isToMany: true,
    kind: 'many-to-many',
    Left: Ext.define(null, {
        extend: 'Ext.data.schema.Role',
        isMany: true,
        digitRe: /^\d+$/,
        findRecords: function(session, rightRecord, leftRecords) {
            var slice = session.getMatrixSlice(this.inverse, rightRecord.id),
                members = slice.members,
                ret = [],
                cls = this.cls,
                seen, i, len, id, member, leftRecord;
            if (leftRecords) {
                seen = {};
                // Loop over the records returned by the server and
                // check they all still belong
                for (i = 0 , len = leftRecords.length; i < len; ++i) {
                    leftRecord = leftRecords[i];
                    id = leftRecord.id;
                    member = members[id];
                    if (!(member && member[2] === -1)) {
                        ret.push(leftRecord);
                    }
                    seen[id] = true;
                }
            }
            // Loop over the expected set and include any missing records.
            for (id in members) {
                member = members[id];
                if (!seen || !seen[id] && (member && member[2] !== -1)) {
                    leftRecord = session.peekRecord(cls, id);
                    if (leftRecord) {
                        ret.push(leftRecord);
                    }
                }
            }
            return ret;
        },
        onIdChanged: function(rightRecord, oldId, newId) {
            var store = this.getAssociatedItem(rightRecord);
            if (store) {
                store.getFilters().get(this.$roleFilterId).setValue(newId);
            }
        },
        processLoad: function(store, rightRecord, leftRecords, session) {
            var ret = leftRecords;
            if (session) {
                ret = this.findRecords(session, rightRecord, leftRecords);
                this.onAddToMany(store, ret, true);
            }
            return ret;
        },
        processUpdate: function(session, associationData) {
            var me = this,
                entityType = me.inverse.cls,
                items = associationData.R,
                id, rightRecord, store, leftRecords;
            if (items) {
                for (id in items) {
                    rightRecord = session.peekRecord(entityType, id);
                    if (rightRecord) {
                        leftRecords = session.getEntityList(me.cls, items[id]);
                        store = me.getAssociatedItem(rightRecord);
                        if (store) {
                            store.loadData(leftRecords);
                            store.complete = true;
                        } else {
                            // We don't have a store. Create it and add the records.
                            rightRecord[me.getterName](null, null, leftRecords);
                        }
                    } else {
                        session.onInvalidAssociationEntity(entityType, id);
                    }
                }
            }
            me.processMatrixBlock(session, associationData.C, 1);
            me.processMatrixBlock(session, associationData.D, -1);
        },
        checkMembership: function(session, rightRecord) {
            var matrix = session.getMatrix(this.association, true),
                side, entityType, inverse, slice, slices, id, members, member, leftRecord, store;
            if (!matrix) {
                return;
            }
            side = this.left ? matrix.right : matrix.left;
            entityType = side.inverse.role.cls;
            inverse = this.inverse;
            slices = side.slices;
            if (slices) {
                slice = slices[rightRecord.id];
                if (slice) {
                    members = slice.members;
                    for (id in members) {
                        member = members[id];
                        if (member[2] !== -1) {
                            // Do we have the record in the session?
                            // If so, do we also have the store?
                            leftRecord = session.peekRecord(entityType, id);
                            if (leftRecord) {
                                store = inverse.getAssociatedItem(leftRecord);
                                if (store) {
                                    store.matrixUpdate = 1;
                                    store.add(rightRecord);
                                    store.matrixUpdate = 0;
                                }
                            }
                        }
                    }
                }
            }
        },
        onStoreCreate: function(store, session, id) {
            var me = this,
                matrix;
            if (session) {
                // If we are creating a store of say Groups in a UserGroups matrix, we want
                // to traverse the inverse side of the matrix (Users) because the id we have
                // is that of the User to which these Groups are associated.
                matrix = session.getMatrixSlice(me.inverse, id);
                matrix.attach(store);
                matrix.notify = me.onMatrixUpdate;
                matrix.scope = me;
            }
        },
        processMatrixBlock: function(session, leftKeys, state) {
            var inverse = this.inverse,
                digitRe = this.digitRe,
                slice, id;
            if (leftKeys) {
                for (id in leftKeys) {
                    // We may not have the record available to pull out the id, so the best we can
                    // do here is try to detect a number id.
                    if (digitRe.test(id)) {
                        id = parseInt(id, 10);
                    }
                    slice = session.getMatrixSlice(inverse, id);
                    slice.update(leftKeys[id], state);
                }
            }
        },
        createGetter: function() {
            var me = this;
            return function(options, scope, leftRecords) {
                // 'this' refers to the Model instance inside this function
                return me.getAssociatedStore(this, options, scope, leftRecords, false);
            };
        },
        /*
         * This method is called when records are added to the association store. If this
         * is happening as a side-effect of the underlying matrix update, we skip telling
         * the matrix what it already knows. Otherwise we need to tell the matrix of the
         * changes on this side so that they can be reflected on the other side.
         */
        onAddToMany: function(store, leftRecords, load) {
            if (!store.matrixUpdate) {
                store.matrixUpdate = 1;
                // By default the "load" param is really the index, but we call this manually
                // in a few spots to indicate it's a default load
                store.matrix.update(leftRecords, load === true ? 0 : 1);
                store.matrixUpdate = 0;
            }
        },
        /*
         * This method is called when records are removed from the association store. The
         * same logic applies here as in onAddToMany with respect to the update that may
         * or may not be taking place on the underlying matrix.
         */
        onRemoveFromMany: function(store, records) {
            if (!store.matrixUpdate) {
                store.matrixUpdate = 1;
                store.matrix.update(records, -1);
                store.matrixUpdate = 0;
            }
        },
        read: function(rightRecord, node, fromReader, readOptions) {
            var me = this,
                leftRecords = me.callParent([
                    rightRecord,
                    node,
                    fromReader,
                    readOptions
                ]);
            if (leftRecords) {
                // Create the store and dump the data
                rightRecord[me.getterName](null, null, leftRecords);
                // Inline associations should *not* arrive on the "data" object:
                delete rightRecord.data[me.role];
            }
        },
        onMatrixUpdate: function(matrixSlice, id, state) {
            var store = matrixSlice.store,
                index, leftRecord, entry;
            if (store && !store.loading && !store.matrixUpdate) {
                store.matrixUpdate = 1;
                index = store.indexOfId(id);
                if (state < 0) {
                    if (index >= 0) {
                        store.remove([
                            index
                        ]);
                    }
                } else if (index < 0) {
                    entry = store.getSession().getEntry(this.type, id);
                    leftRecord = entry && entry.record;
                    if (leftRecord) {
                        store.add(leftRecord);
                    }
                }
                store.matrixUpdate = 0;
            }
        },
        adoptAssociated: function(record, session) {
            var store = this.getAssociatedItem(record),
                records, i, len;
            if (store) {
                store.setSession(session);
                this.onStoreCreate(store, session, record.getId());
                records = store.getData().items;
                for (i = 0 , len = records.length; i < len; ++i) {
                    session.adopt(records[i]);
                }
            }
        }
    }, function() {
        var Left = this;
        // Left is created but ManyToMany may not yet be created
        Ext.ClassManager.onCreated(function() {
            Ext.data.schema.ManyToMany.prototype.Right = Ext.define(null, {
                extend: Left,
                left: false,
                side: 'right'
            });
        }, null, 'Ext.data.schema.ManyToMany');
    })
});

/**
 * General purpose inflector class that {@link #pluralize pluralizes},
 * {@link #singularize singularizes} and {@link #ordinalize ordinalizes} words. Sample usage:
 *
 *     //turning singular words into plurals
 *     Ext.util.Inflector.pluralize('word'); //'words'
 *     Ext.util.Inflector.pluralize('person'); //'people'
 *     Ext.util.Inflector.pluralize('sheep'); //'sheep'
 *
 *     //turning plurals into singulars
 *     Ext.util.Inflector.singularize('words'); //'word'
 *     Ext.util.Inflector.singularize('people'); //'person'
 *     Ext.util.Inflector.singularize('sheep'); //'sheep'
 *
 *     //ordinalizing numbers
 *     Ext.util.Inflector.ordinalize(11); //"11th"
 *     Ext.util.Inflector.ordinalize(21); //"21st"
 *     Ext.util.Inflector.ordinalize(1043); //"1043rd"
 *
 * # Customization
 *
 * The Inflector comes with a default set of US English pluralization rules. These can be augmented
 * with additional rules if the default rules do not meet your application's requirements,
 * or swapped out entirely for other languages. Here is how we might add a rule that pluralizes
 * "ox" to "oxen":
 *
 *     Ext.util.Inflector.plural(/^(ox)$/i, "$1en");
 *
 * Each rule consists of two items - a regular expression that matches one or more rules,
 * and a replacement string. In this case, the regular expression will only match the string "ox",
 * and will replace that match with "oxen". Here's how we could add the inverse rule:
 *
 *     Ext.util.Inflector.singular(/^(ox)en$/i, "$1");
 *
 * Note that the ox/oxen rules are present by default.
 */
Ext.define('Ext.util.Inflector', {
    /* Begin Definitions */
    singleton: true,
    /* End Definitions */
    /* eslint-disable no-multi-spaces */
    /**
     * @private
     * The registered plural tuples. Each item in the array should contain two items - the first
     * must be a regular expression that matchers the singular form of a word, the second must be
     * a String that replaces the matched part of the regular expression. This is managed by the
     * {@link #plural} method.
     * @property {Array} plurals
     */
    plurals: [
        [
            (/(quiz)$/i),
            "$1zes"
        ],
        [
            (/^(ox)$/i),
            "$1en"
        ],
        [
            (/([m|l])ouse$/i),
            "$1ice"
        ],
        [
            (/(matr|vert|ind)ix|ex$/i),
            "$1ices"
        ],
        [
            (/(x|ch|ss|sh)$/i),
            "$1es"
        ],
        [
            (/([^aeiouy]|qu)y$/i),
            "$1ies"
        ],
        [
            (/(hive)$/i),
            "$1s"
        ],
        [
            (/(?:([^f])fe|([lr])f)$/i),
            "$1$2ves"
        ],
        [
            (/sis$/i),
            "ses"
        ],
        [
            (/([ti])um$/i),
            "$1a"
        ],
        [
            (/(buffal|tomat|potat)o$/i),
            "$1oes"
        ],
        [
            (/(bu)s$/i),
            "$1ses"
        ],
        [
            (/(alias|status|sex)$/i),
            "$1es"
        ],
        [
            (/(octop|vir)us$/i),
            "$1i"
        ],
        [
            (/(ax|test)is$/i),
            "$1es"
        ],
        [
            (/^(p)erson$/i),
            "$1eople"
        ],
        [
            (/^(m)an$/i),
            "$1en"
        ],
        [
            (/(.*)(child)(ren)?$/i),
            "$1$2ren"
        ],
        [
            (/s$/i),
            "s"
        ],
        [
            (/$/),
            "s"
        ]
    ],
    /**
     * @private
     * The set of registered singular matchers. Each item in the array should contain two items -
     * the first must be a regular expression that matches the plural form of a word, the second
     * must be a String that replaces the matched part of the regular expression. This is managed
     * by the {@link #singular} method.
     * @property {Array} singulars
     */
    singulars: [
        [
            (/(address)$/i),
            "$1"
        ],
        [
            (/(quiz)zes$/i),
            "$1"
        ],
        [
            (/(matr)ices$/i),
            "$1ix"
        ],
        [
            (/(vert|ind)ices$/i),
            "$1ex"
        ],
        [
            (/^(ox)en/i),
            "$1"
        ],
        [
            (/(alias|status)es$/i),
            "$1"
        ],
        [
            (/(octop|vir)i$/i),
            "$1us"
        ],
        [
            (/(cris|ax|test)es$/i),
            "$1is"
        ],
        [
            (/(shoe)s$/i),
            "$1"
        ],
        [
            (/(o)es$/i),
            "$1"
        ],
        [
            (/(bus)es$/i),
            "$1"
        ],
        [
            (/([m|l])ice$/i),
            "$1ouse"
        ],
        [
            (/(x|ch|ss|sh)es$/i),
            "$1"
        ],
        [
            (/(m)ovies$/i),
            "$1ovie"
        ],
        [
            (/(s)eries$/i),
            "$1eries"
        ],
        [
            (/([^aeiouy]|qu)ies$/i),
            "$1y"
        ],
        [
            (/([lr])ves$/i),
            "$1f"
        ],
        [
            (/(tive)s$/i),
            "$1"
        ],
        [
            (/(hive)s$/i),
            "$1"
        ],
        [
            (/([^f])ves$/i),
            "$1fe"
        ],
        [
            (/(^analy)ses$/i),
            "$1sis"
        ],
        [
            (/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i),
            "$1$2sis"
        ],
        [
            (/([ti])a$/i),
            "$1um"
        ],
        [
            (/(n)ews$/i),
            "$1ews"
        ],
        [
            (/(p)eople$/i),
            "$1erson"
        ],
        [
            (/s$/i),
            ""
        ]
    ],
    /* eslint-enable no-multi-spaces */
    /**
     * @private
     * The registered uncountable words
     * @property {String[]} uncountable
     */
    uncountable: [
        "sheep",
        "fish",
        "series",
        "species",
        "money",
        "rice",
        "information",
        "equipment",
        "grass",
        "mud",
        "offspring",
        "deer",
        "means"
    ],
    /**
     * Adds a new singularization rule to the Inflector. See the intro docs for more information
     * @param {RegExp} matcher The matcher regex
     * @param {String} replacer The replacement string, which can reference matches from the matcher
     * argument
     */
    singular: function(matcher, replacer) {
        this.singulars.unshift([
            matcher,
            replacer
        ]);
    },
    /**
     * Adds a new pluralization rule to the Inflector. See the intro docs for more information
     * @param {RegExp} matcher The matcher regex
     * @param {String} replacer The replacement string, which can reference matches from the matcher
     * argument
     */
    plural: function(matcher, replacer) {
        this.plurals.unshift([
            matcher,
            replacer
        ]);
    },
    /**
     * Removes all registered singularization rules
     */
    clearSingulars: function() {
        this.singulars = [];
    },
    /**
     * Removes all registered pluralization rules
     */
    clearPlurals: function() {
        this.plurals = [];
    },
    /**
     * Returns true if the given word is transnumeral (the word is its own singular and
     * plural form - e.g. sheep, fish)
     * @param {String} word The word to test
     * @return {Boolean} True if the word is transnumeral
     */
    isTransnumeral: function(word) {
        return Ext.Array.indexOf(this.uncountable, word) !== -1;
    },
    /**
     * Returns the pluralized form of a word (e.g. Ext.util.Inflector.pluralize('word')
     * returns 'words')
     * @param {String} word The word to pluralize
     * @return {String} The pluralized form of the word
     */
    pluralize: function(word) {
        if (this.isTransnumeral(word)) {
            return word;
        }
        // eslint-disable-next-line vars-on-top
        var plurals = this.plurals,
            length = plurals.length,
            tuple, regex, i;
        for (i = 0; i < length; i++) {
            tuple = plurals[i];
            regex = tuple[0];
            // eslint-disable-next-line eqeqeq
            if (regex == word || (regex.test && regex.test(word))) {
                return word.replace(regex, tuple[1]);
            }
        }
        return word;
    },
    /**
     * Returns the singularized form of a word (e.g. Ext.util.Inflector.singularize('words')
     * returns 'word')
     * @param {String} word The word to singularize
     * @return {String} The singularized form of the word
     */
    singularize: function(word) {
        if (this.isTransnumeral(word)) {
            return word;
        }
        // eslint-disable-next-line vars-on-top
        var singulars = this.singulars,
            length = singulars.length,
            tuple, regex, i;
        for (i = 0; i < length; i++) {
            tuple = singulars[i];
            regex = tuple[0];
            // eslint-disable-next-line eqeqeq
            if (regex == word || (regex.test && regex.test(word))) {
                return word.replace(regex, tuple[1]);
            }
        }
        return word;
    },
    /**
     * Returns the correct {@link Ext.data.Model Model} name for a given string. Mostly used
     * internally by the data
     * package
     * @param {String} word The word to classify
     * @return {String} The classified version of the word
     */
    classify: function(word) {
        return Ext.String.capitalize(this.singularize(word));
    },
    /**
     * Ordinalizes a given number by adding a prefix such as 'st', 'nd', 'rd' or 'th' based on
     * the last digit of the number. 21 -> 21st, 22 -> 22nd, 23 -> 23rd, 24 -> 24th etc
     * @param {Number} number The number to ordinalize
     * @return {String} The ordinalized number
     */
    ordinalize: function(number) {
        var parsed = parseInt(number, 10),
            mod10 = parsed % 10,
            mod100 = parsed % 100;
        // 11 through 13 are a special case
        if (11 <= mod100 && mod100 <= 13) {
            return number + "th";
        } else {
            switch (mod10) {
                case 1:
                    return number + "st";
                case 2:
                    return number + "nd";
                case 3:
                    return number + "rd";
                default:
                    return number + "th";
            }
        }
    }
}, function() {
    // aside from the rules above, there are a number of words that have irregular pluralization
    // so we add them here
    var singular,
        irregulars = {
            alumnus: 'alumni',
            cactus: 'cacti',
            focus: 'foci',
            nucleus: 'nuclei',
            radius: 'radii',
            stimulus: 'stimuli',
            ellipsis: 'ellipses',
            paralysis: 'paralyses',
            oasis: 'oases',
            appendix: 'appendices',
            index: 'indexes',
            beau: 'beaux',
            bureau: 'bureaux',
            tableau: 'tableaux',
            woman: 'women',
            child: 'children',
            man: 'men',
            corpus: 'corpora',
            criterion: 'criteria',
            curriculum: 'curricula',
            genus: 'genera',
            memorandum: 'memoranda',
            phenomenon: 'phenomena',
            foot: 'feet',
            goose: 'geese',
            tooth: 'teeth',
            antenna: 'antennae',
            formula: 'formulae',
            nebula: 'nebulae',
            vertebra: 'vertebrae',
            vita: 'vitae'
        };
    for (singular in irregulars) {
        if (irregulars.hasOwnProperty(singular)) {
            this.plural(singular, irregulars[singular]);
            this.singular(irregulars[singular], singular);
        }
    }
});

/**
 * This class provides name derivation methods for use by a `Schema`.
 * 
 * # Caching
 * 
 * Because most name derivations are only textual manipulations of input strings, the
 * results can be cached. This is handled by the `apply` method by giving it the name of
 * the method to call. For example:
 * 
 *      var str = namer.capitalize('foo'); //  = "Foo"
 *      
 *      var str = namer.apply('capitalize', 'foo');
 * 
 * The return value of the second call (using `apply`) is the same as the first, however,
 * the results of `capitalize` are cached. This allows repeated calls to `apply` given the
 * same operation and string to avoid the extra string manipulation.
 * 
 * # Usage
 * 
 * This class is not intended to be created by application code. It is created by `Schema`
 * instances as directed by the `namer` config. Application code can derive from this
 * class and set the `namer` config to customize naming conventions used by the `Schema`.
 * 
 * @protected
 */
Ext.define('Ext.data.schema.Namer', {
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    requires: [
        'Ext.util.Inflector'
    ],
    alias: 'namer.default',
    // also configures Factoryable
    isNamer: true,
    //-------------------------------------------------------------------------
    // Cacheable methods
    capitalize: function(name) {
        return Ext.String.capitalize(name);
    },
    /**
     * Given the name of a foreign key field, return the role of the related entity. For
     * example, fields like "fooId" or "foo_id" this implementation returns "foo".
     * @template
     */
    fieldRole: function(name) {
        var match = name.match(this.endsWithIdRe, '');
        if (match) {
            name = name.substr(0, name.length - (match[1] || match[2]).length);
        }
        return this.apply('uncapitalize', name);
    },
    idField: function(name) {
        // ex: User ==> userId
        return this.apply('uncapitalize,singularize', name) + 'Id';
    },
    instanceName: function(roleName) {
        return this.apply('underscore', roleName);
    },
    multiRole: function(name) {
        return this.apply('undotted,uncapitalize,pluralize', name);
    },
    pluralize: function(name) {
        return Ext.util.Inflector.pluralize(name);
    },
    readerRoot: function(roleName) {
        return this.apply('uncapitalize', roleName);
    },
    singularize: function(name) {
        return Ext.util.Inflector.singularize(name);
    },
    storeName: function(roleName) {
        return this.apply('underscore', roleName);
    },
    uncapitalize: function(name) {
        return Ext.String.uncapitalize(name);
    },
    underscore: function(name) {
        return '_' + name;
    },
    uniRole: function(name) {
        return this.apply('undotted,uncapitalize,singularize', name);
    },
    undotted: function(name) {
        var parts, index;
        if (name.indexOf('.') < 0) {
            return name;
        }
        parts = name.split('.');
        index = parts.length;
        while (index-- > 1) {
            parts[index] = this.apply('capitalize', parts[index]);
        }
        return parts.join('');
    },
    //-------------------------------------------------------------------------
    // Non-Cacheable methods
    getterName: function(role) {
        var name = role.role;
        if (role && role.isMany) {
            // return this.apply('uncapitalize,pluralize', name);
            return name;
        }
        // return this.apply('capitalize,singularize', name);
        return 'get' + this.apply('capitalize', name);
    },
    inverseFieldRole: function(leftType, unique, rightRole, rightType) {
        // In a FK association, the left side may be unique in which case we have a
        // one-to-one otherwise we have a one-to-many. If the FK field is just the
        // name of the right side class (e.g., if it is "order"), then we don't want
        // to include the field name in the left role.
        var me = this,
            leftRole = me.apply(unique ? 'uniRole' : 'multiRole', leftType),
            s1 = me.apply('pluralize', rightRole),
            s2 = me.apply('undotted,pluralize', rightType);
        if (s1.toLowerCase() !== s2.toLowerCase()) {
            // Otherwise, we have something like "creatorId" on Ticket that holds a
            // reference to User. This makes the right role "creator" so rather than
            // make the left role "tickets" we make it "creatorTickets".
            leftRole = rightRole + me.apply('capitalize', leftRole);
        }
        return leftRole;
    },
    manyToMany: function(relation, leftType, rightType) {
        var me = this,
            // ex: UserGroups
            ret = me.apply('undotted,capitalize,singularize', leftType) + me.apply('undotted,capitalize,pluralize', rightType);
        if (relation) {
            ret = me.apply('capitalize', relation + ret);
        }
        return ret;
    },
    /**
     * Returns the name for a one-to-many association given the left and right type and
     * the associating `role`.
     * 
     * In many cases the `role` matches the target type. For example, an OrderItem might
     * have an "orderId" field which would have a `role` of "order". If this is a reference
     * to an Order entity then the association name will be "OrderOrderItems".
     * 
     * When the `role` does not match, it is included in the association name. For example,
     * consider a Ticket entity with a "creatorId" field that references a User entity.
     * The `role` of that field will (by default) be "creator". The returned association
     * name will be "UserCreatorTickets".
     */
    manyToOne: function(leftType, leftRole, rightType, rightRole) {
        // ex: OrderItem -> Order  ==> OrderOrderItems
        //  Ticket (creator) -> User ==> UserCreatorTickets
        return this.apply('capitalize,singularize', rightType) + this.apply('capitalize', leftRole);
    },
    matrixRole: function(relation, entityType) {
        var ret = this.apply(relation ? 'multiRole,capitalize' : 'multiRole', entityType);
        return relation ? relation + ret : ret;
    },
    oneToOne: function(leftType, leftRole, rightType, rightRole) {
        return this.apply('undotted,capitalize,singularize', rightType) + this.apply('capitalize', leftRole);
    },
    setterName: function(role) {
        return 'set' + this.apply('capitalize', role.role);
    },
    //-------------------------------------------------------------------------
    // Private
    endsWithIdRe: /(?:(_id)|[^A-Z](Id))$/,
    cache: {},
    apply: function(operation, name) {
        var me = this,
            cache = me.cache,
            entry = cache[name] || (cache[name] = {}),
            ret = entry[operation],
            i, length, operations;
        if (!ret) {
            if (operation.indexOf(',') < 0) {
                ret = me[operation](name);
            } else {
                length = (operations = operation.split(',')).length;
                ret = name;
                for (i = 0; i < length; ++i) {
                    ret = me.apply(operations[i], ret);
                }
            }
            entry[operation] = ret;
        }
        return ret;
    }
});

/**
 * A Schema is a collection of related {@link Ext.data.Model entities} and their respective
 * {@link Ext.data.schema.Association associations}.
 * 
 * # Schema Instances
 * 
 * By default a single instance of this class is created which serves as the schema for all
 * entities that do not have an explicit `{@link Ext.data.Model#cfg-schema schema}` config
 * either specified or inherited. This is sufficient in most cases.
 * 
 * When an entity does specify a `{@link Ext.data.Model#cfg-schema schema}`, however, that
 * looks up (or creates) an instance for that entity class which is then inherited.
 * 
 * **Important:** All related entities *must* belong to a single schema instance in order
 * to properly link up their associations.
 * 
 * ## Configuring Schemas
 * 
 * The best way to control the configuration of your `schema` is to define a base class for
 * all of your entities and use the `{@link Ext.data.Model#cfg-schema schema}` config like
 * this:
 * 
 *      Ext.define('MyApp.model.Base', {
 *          extend: 'Ext.data.Model',
 *
 *          // This configures the default schema because we don't assign an "id":
 *          schema: {
 *              // configs go here
 *          }
 *      });
 * 
 * **Note:** Only one explicit configuration can be applied to the default schema. In most
 * applications this will not be an issue.
 *
 * By using a base class for your entities you can ensure that the default schema is fully
 * configured before declaration of your classes proceeds. This is especially helpful if
 * you need to set the `namespace` for your schema (see below).
 *
 * ## Relative Naming
 * 
 * When describing associations between entities, it is desirable to use shorthand names
 * that do not contain the common namespace portion. This is called the `entityName` as
 * opposed to its class name. By default, the `entityName` is the full class name. However,
 * if a namespace is used, the common portion can be discarded and we can derive a shorter name.
 * In the following code, `"MyApp.model.Foo"` has an `entityName` of `"Foo"` and the schema has
 * a `namespace` of "MyApp.model".
 * 
 * If you use deeper nesting for entities, you may need to set the `namespace` config to
 * account for this. For example:
 * 
 *      Ext.define('MyApp.model.Base', {
 *          extend: 'Ext.data.Model',
 *
 *          schema: {
 *              namespace: 'MyApp.model'
 *          }
 *      });
 *
 * Your derived classes now will generate proper default `entityName` values even if they
 * have further namespaces. For example, "MyApp.model.foo.Thing" will produce "foo.Thing"
 * as the `entityName` given the above as a base class.
 *
 * # Association Naming
 * 
 * There are various terms involved when describing associations. Perhaps the simplest
 * example that will clarify these terms is that of the common many-to-many association
 * of User and Group.
 * 
 *   * `entityName` - The names "User" and "Group" are the `entityName` values associated
 *   with these two classes. These are derived from their full classnames (perhaps
 *   something like "App.model.User" and "App.model.Group").
 *   
 *   * `associationName` - When talking about associations, especially the many-to-many
 *   variety, it is important to give them names. Associations are not owned by either of
 *   the entities involved, so this name is similar to an `entityName`. In the case of
 *   "User" and "Group", the default `associationName` would be "GroupUsers".
 *   
 *   * `left` and `right` - Associations describe a relationship between two entities. To
 *   talk about specific associations we would use the `entityName` of the parties (such
 *   as "User" or "Group"). When discussing associations in the abstract, however, it is
 *   very helpful to be able to talk about the entities in an association in a general way.
 *   In the case of the "GroupUsers" association, "User" is said to be the `left` while
 *   "Group" is said to be the `right`. In a many-to-many association the selection of
 *   `left` and `right` is arbitrary. When a foreign-key is involved, the `left` entity
 *   is the one containing the foreign-key.
 *
 * ## Custom Naming Conventions
 * 
 * One of the jobs the the `Schema` is to manage name generation (such as `entityName`).
 * This job is delegated to a class called the `namer`. If you need to generate names in
 * other ways, you can provide a custom `namer` for your classes:
 *
 *      Ext.define('MyApp.model.Base', {
 *          extend: 'Ext.data.Model',
 *
 *          schema: {
 *              namespace: 'MyApp.model',
 *              namer: 'custom'
 *          }
 *      });
 *
 * This will create a class using the alias "namer.custom". For example:
 *
 *      Ext.define('MyApp.model.CustomNamer', {
 *          extend: 'Ext.data.schema.Namer',
 *
 *          alias: 'namer.custom',
 *          ...
 *      });
 *
 * For details see the documentation for {@link Ext.data.schema.Namer Namer}.
 */
Ext.define('Ext.data.schema.Schema', {
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    requires: [
        'Ext.util.ObjectTemplate',
        'Ext.data.schema.OneToOne',
        'Ext.data.schema.ManyToOne',
        'Ext.data.schema.ManyToMany',
        'Ext.data.schema.Namer'
    ],
    alias: 'schema.default',
    // also configures Factoryable
    aliasPrefix: 'schema.',
    isSchema: true,
    /**
     * @property {String} type
     * The name of the schema's type. This should be the suffix of the `alias` for this
     * class following the "schema." prefix. For example, if the `alias` for a schema is
     * "schema.foo" then `type` should "foo". If an `alias` is specified on the derived
     * class, this property is set automatically.
     * @readonly
     */
    type: 'default',
    statics: {
        /**
         * @property {Object} instances
         * A collection of `Schema` instances keyed by its `type`.
         * 
         *      var mySchema = Ext.data.schema.Schema.instances.mySchema;
         *
         * If the `Schema` may not have been created yet, use the {@link #get} method to
         * create the instance on first request:
         * 
         *      var mySchema = Ext.data.schema.Schema.get('mySchema');
         * 
         * @readonly
         * @private
         */
        instances: {},
        // Method used for testing to clear cache for custom instances.
        clearInstance: function(id) {
            var schema = this.instances[id];
            delete this.instances[id];
            if (schema) {
                schema.clear(true);
                schema.destroy();
            }
        },
        /**
         * Returns the `Schema` instance given its `id` or config object. If only the `id`
         * is specified, that `Schema` instance is looked up and returned. If there is no
         * instance already created, the `id` is assumed to be the `type`. For example:
         *
         *      schema: 'foo'
         *
         * Would be created from the alias `"schema.foo"` and assigned the `id` of "foo"
         * as well.
         *
         * @param {String/Object} config The id, type or config object of the schema.
         * @param {String} [config.type] The type alias of the schema. A "schema." prefix
         * is added to this string, if provided, to complete the alias. This should match
         * match the "alias" of some class derived from `Ext.data.schema.Schema`.
         * @return {Ext.data.schema.Schema} The previously existing or newly created
         * instance.
         */
        get: function(config) {
            var Schema = this,
                cache = Schema.instances,
                id = 'default',
                isString = config && Ext.isString(config),
                instance, newConfig;
            if (config) {
                if (config.isSchema) {
                    return config;
                }
                id = isString ? config : (config.id || id);
            }
            if (!(instance = cache[id])) {
                cache[id] = instance = Schema.create(config);
                instance.id = id;
            } else if (config && !isString) {
                if (id !== 'default') {
                    Ext.raise('Only the default Schema instance can be reconfigured');
                }
                // When a Model contains a "schema" config object it is allowed to set the
                // configuration of the default schema. This is the default behavior of
                // this config on a model unless there is an "id" specified on it. So
                // the trick is that we already have an instance so we want to merge the
                // incoming config with the initial config of the default schema and then
                // make that the effective initial config.
                newConfig = Ext.merge({}, instance.config);
                Ext.merge(newConfig, config);
                instance.setConfig(newConfig);
                instance.config = newConfig;
                instance.setConfig = function() {
                    Ext.raise('The schema can only be reconfigured once');
                };
            }
            return instance;
        },
        lookupEntity: function(entity) {
            var ret = null,
                instances = this.instances,
                match, name, schema;
            if (entity) {
                if (entity.isEntity) {
                    ret = entity.self;
                }
                // a record
                else if (Ext.isFunction(entity)) {
                    // A function (assume that a constructor is the Class).
                    ret = entity;
                } else if (Ext.isString(entity)) {
                    ret = Ext.ClassManager.get(entity);
                    // If we've found a singleton or non-Entity class by that name, ignore it.
                    if (ret && (!ret.prototype || !ret.prototype.isEntity)) {
                        ret = null;
                    }
                    if (!ret) {
                        for (name in instances) {
                            schema = instances[name];
                            match = schema.getEntity(entity);
                            if (match) {
                                if (ret) {
                                    Ext.raise('Ambiguous entity name "' + entity + '". Defined by schema "' + ret.schema.type + '" and "' + name + '"');
                                }
                                ret = match;
                            }
                        }
                    }
                    if (!ret) {
                        Ext.raise('No such Entity "' + entity + '".');
                    }
                }
            }
            return ret;
        }
    },
    /**
     * @property {Number} assocCount The number of {@link Ext.data.schema.Association associations}
     * in this `schema`.
     * @readonly
     */
    assocCount: 0,
    /**
     * @property {Number} entityCount The number of {@link Ext.data.Model entities} in this
     * `schema`.
     * @readonly
     */
    entityCount: 0,
    config: {
        /**
         * @cfg {Object} defaultIdentifier
         * This config is used to initialize the `{@link Ext.data.Model#identifier}` config
         * for classes that do not define one.
         */
        defaultIdentifier: null,
        /**
        * @cfg {Number} keyCheckDelay
        * The time to wait (in ms) before checking for null foreign keys on records that
        * will cause them to be dropped. This is useful for allowing records to be moved to
        * a different source.
        * @private
        * @since 5.0.1
        */
        keyCheckDelay: 10,
        /**
         * @cfg {String/Object/Ext.data.schema.Namer} namer
         * Specifies or configures the name generator for the schema.
         */
        namer: 'default',
        /**
         * @cfg {String} namespace
         * The namespace for entity classes in this schema.
         */
        namespace: null,
        /**
         * @cfg {Object/Ext.util.ObjectTemplate} proxy
         * This is a template used to produce `Ext.data.proxy.Proxy` configurations for
         * Models that do not define an explicit `{@link Ext.data.Model#cfg-proxy proxy}`.
         *
         * This template is processed with the Model class as the data object which means
         * any static properties of the Model are available. The most useful of these are
         *
         *  * `prefix` - The `urlPrefix` property of this instance.
         *  * `entityName` - The {@link Ext.data.Model#entityName name} of the Model
         *      (for example, "User").
         *  * `schema` - This instance.
         */
        proxy: {
            type: 'ajax',
            url: '{prefix}/{entityName}'
        },
        /**
         * @cfg {String} [urlPrefix=""]
         * This is the URL prefix used for all requests to the server. It could be something
         * like "/~api". This value is included in the `proxy` template data as "prefix".
         */
        urlPrefix: ''
    },
    onClassExtended: function(cls, data) {
        var alias = data.alias;
        if (alias && !data.type) {
            if (!Ext.isString(alias)) {
                alias = alias[0];
            }
            cls.prototype.type = alias.substring(this.prototype.aliasPrefix.length);
        }
    },
    constructor: function(config) {
        this.initConfig(config);
        this.clear();
    },
    //-------------------------------------------------------------------------
    // Config
    // <editor-fold>
    applyDefaultIdentifier: function(identifier) {
        return identifier && Ext.Factory.dataIdentifier(identifier);
    },
    applyNamer: function(namer) {
        var ret = Ext.data.schema.Namer.create(namer);
        ret.schema = this;
        return ret;
    },
    applyNamespace: function(namespace) {
        var end;
        if (namespace) {
            end = namespace.length - 1;
            if (namespace.charAt(end) !== '.') {
                namespace += '.';
            }
        }
        return namespace;
    },
    applyProxy: function(proxy) {
        return Ext.util.ObjectTemplate.create(proxy);
    },
    // </editor-fold>
    //-------------------------------------------------------------------------
    // Public
    eachAssociation: function(fn, scope) {
        var associations = this.associations,
            name;
        for (name in associations) {
            if (associations.hasOwnProperty(name)) {
                if (fn.call(scope, name, associations[name]) === false) {
                    break;
                }
            }
        }
    },
    eachEntity: function(fn, scope) {
        var entities = this.entities,
            name;
        for (name in entities) {
            if (entities.hasOwnProperty(name)) {
                if (fn.call(scope, name, entities[name].cls) === false) {
                    break;
                }
            }
        }
    },
    /**
     * Returns an `Association` by name.
     * @param {String} name The name of the association.
     * @return {Ext.data.schema.Association} The association instance.
     */
    getAssociation: function(name) {
        var entry = this.associations[name];
        return entry || null;
    },
    /**
     * Returns an entity by name.
     * @param {String} name The name of the entity
     * @return {Ext.data.Model} The entity class.
     */
    getEntity: function(name) {
        var entry = this.entityClasses[name] || this.entities[name];
        return (entry && entry.cls) || null;
    },
    /**
     * Get the entity name taking into account the {@link #namespace}.
     * @param {String/Ext.data.Model} cls The model class or name of the class.
     * @return {String} The entity name
     */
    getEntityName: function(cls) {
        var ns = this.getNamespace(),
            index, name;
        if (typeof cls === 'string') {
            name = cls;
        } else {
            name = cls.$className || null;
        }
        if (name) {
            // if (not anonymous class)
            if (ns) {
                index = ns.length;
                if (name.substring(0, index) !== ns) {
                    return name;
                }
            }
            if (index) {
                name = name.substring(index);
            }
        }
        return name;
    },
    /**
     * Checks if the passed entity has attached associations that need to be read when
     * using nested loading.
     * 
     * @param {String/Ext.Class/Ext.data.Model} name The name, instance, or Model class.
     * @return {Boolean} `true` if there are associations attached to the entity.
     */
    hasAssociations: function(name) {
        name = name.entityName || name;
        return !!this.associationEntityMap[name];
    },
    /**
     * Checks if an entity is defined
     * @param {String/Ext.data.Model} entity The name or model
     * @return {Boolean} True if this entity is defined
     */
    hasEntity: function(entity) {
        var name = this.getEntityName(entity);
        return !!(this.entities[name] || this.entityClasses[name]);
    },
    //-------------------------------------------------------------------------
    // Protected
    /**
     * Adds an entry from a {@link Ext.data.schema.ManyToMany matrix config} declared by an
     * entity.
     * 
     * This is the ideal method to override in a derived class if the standard, default
     * naming conventions need to be adjusted. In the override, apply whatever logic is
     * appropriate to determine the missing values and pass along the proper results to
     * this method in the `callParent`.
     * 
     * @param {Ext.Class} entityType A class derived from `Ext.data.Model`.
     *
     * @param {String} matrixName The name of the matrix association.
     *
     * @param {String} [relation] A base name for the matrix. For information about the
     * meaning of this see {@link Ext.data.schema.Schema#ManyToMany}.
     * 
     * @param {Object} left The descriptor for the "left" of the matrix.
     * @param {String} left.type The type of the entity on the "left" of the matrix.
     * 
     * @param {String} [left.field] The name of the field in the matrix table for the "left"
     * side entity. If not provided, this defaults to the `left.type` name
     * {@link Ext.util.Inflector#singularize singularized} and uncapitalized followed by
     * "Id". For example, "userId" for a `left.type` of "Users".
     * 
     * @param {String} [left.role] The name of the relationship from the `left.type` to the
     * `right.type`. If not provided, this defaults to the `left.type` name
     * {@link Ext.util.Inflector#pluralize pluralized} and uncapitalized. For example,
     * "users" for a `left.type` of "User".
     * 
     * @param {Object} right The descriptor for the "right" of the matrix.
     * @param {String} right.type The type of the entity on the "right" of the matrix.
     * 
     * @param {String} [right.field] The name of the field in the matrix table for the
     * "right" side entity. If not provided, this defaults in the same way as `left.field`
     * except this is based on `right.type`.
     * 
     * @param {String} [right.role] The name of the relationship from the `right.type` to
     * the `left.type`. If not provided, this defaults in the same way as `left.role`
     * except this is based on `right.type`.
     * 
     * @protected
     */
    addMatrix: function(entityType, matrixName, relation, left, right) {
        var me = this,
            namer = me.getNamer(),
            associations = me.associations,
            entities = me.entities,
            leftType = left.type,
            rightType = right.type,
            leftField = left.field || namer.apply('idField', leftType),
            rightField = right.field || namer.apply('idField', rightType),
            leftRole = left.role || namer.matrixRole(relation, leftType),
            rightRole = right.role || namer.matrixRole(relation, rightType),
            matrix, leftEntry, rightEntry;
        leftEntry = entities[leftType] || (entities[leftType] = {
            cls: null,
            name: leftType,
            associations: {}
        });
        rightEntry = entities[rightType] || (entities[rightType] = {
            cls: null,
            name: rightType,
            associations: {}
        });
        ++me.assocCount;
        associations[matrixName] = matrix = new Ext.data.schema.ManyToMany({
            name: matrixName,
            schema: me,
            definedBy: entityType,
            left: {
                cls: leftEntry.cls,
                type: leftType,
                role: leftRole,
                field: leftField,
                associationKey: left.associationKey
            },
            right: {
                cls: rightEntry.cls,
                type: rightType,
                role: rightRole,
                field: rightField,
                associationKey: right.associationKey
            }
        });
        leftEntry.associations[matrix.right.role] = matrix.right;
        rightEntry.associations[matrix.left.role] = matrix.left;
        if (leftEntry.cls) {
            me.associationEntityMap[leftEntry.cls.entityName] = true;
        }
        if (rightEntry.cls) {
            me.associationEntityMap[rightEntry.cls.entityName] = true;
        }
        me.decorateModel(matrix);
    },
    /**
     * Adds a {@link Ext.data.Field#reference reference} field association for an entity
     * to this `schema`.
     * 
     * This is the ideal method to override in a derived class if the standard, default
     * naming conventions need to be adjusted. In the override, apply whatever logic is
     * appropriate to determine the missing values and pass along the proper results to
     * this method in the `callParent`.
     * 
     * @param {Ext.Class} entityType A class derived from `Ext.data.Model`.
     * 
     * @param {Ext.data.field.Field} referenceField The `field` with the `reference` config.
     * 
     * @param {Object} [descr] The `reference` descriptor from the `referenceField` if one
     * was given in the field definition.
     *
     * @param {String} [descr.association] The name of the association. If empty or null, this
     * will be derived from `entityType`, `role`, `inverse` and
     * `referenceField.unique`.
     *
     * @param {String} [descr.role] The name of the relationship from `entityType` to the target
     * `type`. If not specified, the default is the `referenceField.name` (minus any "Id"
     * suffix if present).
     *
     * @param {String} [descr.inverse] The name of the relationship from the target `type`
     * to the `entityType`. If not specified, this is derived from the
     * {@link Ext.data.Model#entityName entityName} of the `entityType`
     * ({@link Ext.util.Inflector#singularize singularized} or
     * {@link Ext.util.Inflector#pluralize pluralized} based on `referenceField.unique`).
     *
     * @param {String} descr.type The {@link Ext.data.Model#entityName entityName} of the
     * target of the reference.
     *
     * @param {Boolean} [unique=false] Indicates if the reference is one-to-one.
     * @param {Boolean} [dupeCheck] (private)
     * 
     * @protected
     */
    addReference: function(entityType, referenceField, descr, unique, dupeCheck) {
        var me = this,
            namer = me.getNamer(),
            entities = me.entities,
            associations = me.associations,
            entityName = entityType.entityName,
            association = descr.association,
            child = descr.child,
            parent = descr.parent,
            rightRole = descr.role,
            // Allow { child: 'OrderItem' } or the reverse (for one-to-one mostly):
            rightType = descr.type || parent || child,
            leftVal = descr.inverse,
            left = Ext.isString(leftVal) ? {
                role: leftVal
            } : leftVal,
            leftRole = left && left.role,
            entry, T;
        if (!rightRole) {
            // In a FK association, the left side has the key in a field named something
            // like "orderId". The default implementation of "fieldRole" namer is to drop
            // the id suffix which gives is the role of the right side.
            if (!referenceField || descr.legacy) {
                rightRole = namer.apply('uncapitalize', rightType);
            } else {
                rightRole = namer.apply('fieldRole', referenceField.name);
            }
        }
        if (!leftRole) {
            leftRole = namer.inverseFieldRole(entityName, unique, rightRole, rightType);
        }
        if (!association) {
            if (unique) {
                association = namer.oneToOne(entityType, leftRole, rightType, rightRole);
            } else {
                association = namer.manyToOne(entityType, leftRole, rightType, rightRole);
            }
        }
        if (dupeCheck && association in associations) {
            if (dupeCheck(associations[association], association, leftRole, rightRole) !== false) {
                return;
            }
        }
        if (association in associations) {
            Ext.raise('Duplicate association: "' + association + '" declared by ' + entityName + (referenceField ? ('.' + referenceField.name) : '') + ' (collides with ' + associations[association].definedBy.entityName + ')');
        }
        if (referenceField && referenceField.definedBy === entities[rightType]) {
            Ext.raise('ForeignKey reference should not be owned by the target model');
        }
        // Lookup the entry for the target of the reference. Since it may not as yet be
        // defined, we may need to create the entry.
        entry = entities[rightType] || (entities[rightType] = {
            cls: null,
            name: rightType,
            associations: {}
        });
        // as a field w/reference we are always "left":
        T = unique ? Ext.data.schema.OneToOne : Ext.data.schema.ManyToOne;
        association = new T({
            name: association,
            // Note: "parent" or "child" can be strings so don't assume otherwise
            owner: child ? 'left' : (parent ? 'right' : null),
            definedBy: entityType,
            schema: me,
            field: referenceField,
            nullable: referenceField ? !!referenceField.allowBlank : true,
            left: {
                cls: entityType,
                type: entityName,
                role: leftRole,
                extra: left
            },
            right: {
                cls: entry.cls,
                type: rightType,
                role: rightRole,
                extra: descr
            },
            meta: descr
        });
        // Add the left and right association "sides" to the appropriate collections, but
        // remember that the right-side entity class may not yet be declared (that's ok as
        // we store the associations in the entry):
        entityType.associations[rightRole] = association.right;
        entry.associations[leftRole] = association.left;
        if (referenceField) {
            // Store the role on the FK field. This "upgrades" legacy associations to the
            // new "field.reference" form.
            referenceField.reference = association.right;
            entityType.references.push(referenceField);
        }
        ++me.assocCount;
        me.associationEntityMap[entityName] = true;
        if (entry.cls) {
            me.associationEntityMap[entry.cls.entityName] = true;
        }
        associations[association.name] = association;
        if (association.right.cls) {
            me.decorateModel(association);
        }
    },
    //-------------------------------------------------------------------------
    privates: {
        /**
         * Adds an {@link Ext.data.Model entity} to this `schema`.
         * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model}.
         * @private
         */
        addEntity: function(entityType) {
            var me = this,
                entities = me.entities,
                entityName = entityType.entityName,
                entry = entities[entityName],
                fields = entityType.fields,
                associations, field, i, length, name;
            if (!entry) {
                entities[entityName] = entry = {
                    name: entityName,
                    associations: {}
                };
            } else if (entry.cls) {
                Ext.raise('Duplicate entity name "' + entityName + '": ' + entry.cls.$className + ' and ' + entityType.$className);
            } else {
                associations = entry.associations;
                for (name in associations) {
                    // the associations collection describes the types to which this entity is
                    // related, but the inverse descriptors need this entityType:
                    associations[name].inverse.cls = entityType;
                    me.associationEntityMap[entityName] = true;
                    // We already have an entry, which means other associations have likely
                    // been added for us, so go ahead and do the inverse decoration
                    me.decorateModel(associations[name].association);
                }
            }
            entry.cls = entityType;
            entityType.prototype.associations = entityType.associations = entry.associations;
            me.entityClasses[entityType.$className] = entry;
            ++me.entityCount;
            for (i = 0 , length = fields.length; i < length; ++i) {
                field = fields[i];
                if (field.reference) {
                    me.addReferenceDescr(entityType, field);
                }
            }
        },
        /**
         * Adds the matrix associations of an {@link Ext.data.Model entity} to this `schema`.
         * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Entity}.
         * @param {Object/String[]} matrices The manyToMany matrices for the class.
         * @private
         */
        addMatrices: function(entityType, matrices) {
            var me = this,
                i, length, matrixName;
            if (Ext.isString(matrices)) {
                me.addMatrixDescr(entityType, null, matrices);
            } else if (matrices[0]) {
                // if (isArray)
                for (i = 0 , length = matrices.length; i < length; ++i) {
                    me.addMatrixDescr(entityType, null, matrices[i]);
                }
            } else {
                for (matrixName in matrices) {
                    me.addMatrixDescr(entityType, matrixName, matrices[matrixName]);
                }
            }
        },
        /**
         * Adds an entry from a {@link Ext.data.schema.ManyToMany matrix config} declared by an
         * {@link Ext.data.Model entity}.
         *
         * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Entity}.
         * @param {String} [matrixName] The name of the matrix association.
         * @param {String/Object} matrixDef A {@link Ext.data.schema.ManyToMany matrix config}
         * declared by an {@link Ext.data.Model entity}.
         * @private
         */
        addMatrixDescr: function(entityType, matrixName, matrixDef) {
            var me = this,
                entityName = entityType.entityName,
                associations = me.associations,
                namer = me.getNamer(),
                left = matrixDef.left,
                right = matrixDef.right,
                last, relation;
            if (Ext.isString(matrixDef)) {
                if (matrixDef.charAt(0) === '#') {
                    // "#User" (entity is on the left)
                    /*
                     *  Ext.define('User', {
                     *      extend: 'Ext.data.Model',
                     *      manyToMany: '#Group'
                     *  });
                     */
                    left = {
                        type: entityName
                    };
                    // User
                    right = {
                        type: matrixDef.substring(1)
                    };
                }
                // Group
                else if (matrixDef.charAt(last = matrixDef.length - 1) === '#') {
                    // "User#"
                    /*
                     *  Ext.define('Group', {
                     *      extend: 'Ext.data.Model',
                     *      manyToMany: 'User#'
                     *  });
                     */
                    left = {
                        type: matrixDef.substring(0, last)
                    };
                    // User
                    right = {
                        type: entityName
                    };
                }
                // Group
                else if (namer.apply('multiRole', entityName) < namer.apply('multiRole', matrixDef)) {
                    /*
                     *  Ext.define('Group', {
                     *      extend: 'Ext.data.Model',
                     *      manyToMany: 'User'
                     *  });
                     */
                    left = {
                        type: entityName
                    };
                    // Group
                    right = {
                        type: matrixDef
                    };
                } else // User
                {
                    /*
                     *  Ext.define('User', {
                     *      extend: 'Ext.data.Model',
                     *      manyToMany: 'Group'
                     *  });
                     */
                    left = {
                        type: matrixDef
                    };
                    // Group
                    right = {
                        type: entityName
                    };
                }
            } else // User
            {
                Ext.Assert.isString(matrixDef.type, 'No "type" for manyToMany in ' + entityName);
                relation = matrixDef.relation;
                /* eslint-disable-next-line max-len */
                if (left || (!right && namer.apply('multiRole', entityName) < namer.apply('multiRole', matrixDef.type))) {
                    if (!left || left === true) {
                        /*
                         *  Ext.define('User', {
                         *      extend: 'Ext.data.Model',
                         *      manyToMany: {
                         *          type: 'Group',
                         *          left: true
                         *      }
                         *  });
                         */
                        left = {
                            type: entityName
                        };
                    } else // User
                    {
                        /*
                         *  Ext.define('User', {
                         *      extend: 'Ext.data.Model',
                         *      manyToMany: {
                         *          type: 'Group',
                         *          left: {
                         *              role: 'useroids'
                         *          }
                         *      }
                         *  });
                         */
                        left = Ext.apply({
                            type: entityName
                        }, left);
                    }
                    // User
                    right = matrixDef;
                } else // Group
                {
                    if (!right || right === true) {
                        /*
                         *  Ext.define('Group', {
                         *      extend: 'Ext.data.Model',
                         *      manyToMany: {
                         *          type: 'User',
                         *          right: true
                         *      }
                         *  });
                         */
                        right = {
                            type: entityName
                        };
                    } else // Group
                    {
                        /*
                         *  Ext.define('Group', {
                         *      extend: 'Ext.data.Model',
                         *      manyToMany: {
                         *          type: 'User',
                         *          right: {
                         *              role: 'groupoids'
                         *          }
                         *      }
                         *  });
                         */
                        right = Ext.apply({
                            type: entityName
                        }, right);
                    }
                    // Group
                    left = matrixDef;
                }
            }
            // User
            if (!matrixName) {
                matrixName = namer.manyToMany(relation, left.type, right.type);
            }
            if (!(matrixName in associations)) {
                me.addMatrix(entityType, matrixName, relation, left, right);
            } else //
            // In the case of a matrix association, both sides may need to declare it to allow
            // them to be used w/o the other present. In development mode, we want to check
            // that they declare the same thing!
            //
            {
                /* eslint-disable-next-line vars-on-top, one-var */
                var entry = associations[matrixName],
                    before = [
                        entry.kind,
                        entry.left.type,
                        entry.left.role,
                        entry.left.field,
                        entry.right.type,
                        entry.right.role,
                        entry.right.field
                    ].join('|'),
                    after;
                // Call back in to bypass this check and realize the new association:
                delete associations[matrixName];
                me.addMatrix(entityType, matrixName, relation, left, right);
                after = associations[matrixName];
                // Restore the originals so we match production behavior (for testing)
                associations[matrixName] = entry;
                entry.left.cls.associations[entry.right.role] = entry.right;
                entry.right.cls.associations[entry.left.role] = entry.left;
                --me.assocCount;
                // Now we can compare the old and the new to see if they are the same.
                after = [
                    after.kind,
                    after.left.type,
                    after.left.role,
                    after.left.field,
                    after.right.type,
                    after.right.role,
                    after.right.field
                ].join('|');
                if (before != after) {
                    // eslint-disable-line eqeqeq
                    Ext.log.warn(matrixName + '(' + entry.definedBy.entityName + '): ' + before);
                    Ext.log.warn(matrixName + '(' + entityName + '): ' + after);
                    Ext.raise('Conflicting association: "' + matrixName + '" declared by ' + entityName + ' was previously declared by ' + entry.definedBy.entityName);
                }
            }
        },
        /**
         * Adds a {@link Ext.data.Field#reference reference} {@link Ext.data.Field field}
         * association for an entity to this `schema`. This method decodes the `reference`
         * config of the `referenceField` and calls {@link #addReference}.
         *
         * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Model}.
         * @param {Ext.data.Field} referenceField The `field` with the `reference` config.
         * @private
         */
        addReferenceDescr: function(entityType, referenceField) {
            var me = this,
                descr = referenceField.$reference;
            if (Ext.isString(descr)) {
                descr = {
                    type: descr
                };
            } else {
                descr = Ext.apply({}, descr);
            }
            me.addReference(entityType, referenceField, descr, referenceField.unique);
        },
        addBelongsTo: function(entityType, assoc) {
            this.addKeylessSingle(entityType, assoc, false);
        },
        addHasOne: function(entityType, assoc) {
            this.addKeylessSingle(entityType, assoc, true);
        },
        addKeylessSingle: function(entityType, assoc, unique) {
            var foreignKey, referenceField;
            assoc = Ext.apply({}, this.checkLegacyAssociation(entityType, assoc));
            assoc.type = this.getEntityName(assoc.child || assoc.parent || assoc.type);
            foreignKey = assoc.foreignKey || (assoc.type.toLowerCase() + '_id');
            referenceField = entityType.getField(foreignKey);
            assoc.fromSingle = true;
            if (referenceField) {
                referenceField.$reference = assoc;
                referenceField.unique = true;
                assoc.legacy = true;
                Ext.log.warn('Using foreignKey is deprecated, use a keyed association. ' + 'See Ext.data.field.Field.reference');
            }
            this.addReference(entityType, referenceField, assoc, unique);
        },
        addHasMany: function(entityType, assoc) {
            var me = this,
                entities = me.entities,
                pending = me.pending,
                cls, name, referenceField, target, foreignKey, inverseOptions, child, declaredInverse;
            assoc = Ext.apply({}, this.checkLegacyAssociation(entityType, assoc));
            assoc.type = this.getEntityName(assoc.child || assoc.parent || assoc.type);
            name = assoc.type;
            target = entities[name];
            cls = target && target.cls;
            if (cls) {
                name = entityType.entityName;
                foreignKey = assoc.foreignKey || (name.toLowerCase() + '_id');
                delete assoc.foreignKey;
                // The assoc is really the inverse, so we only set the minimum.
                // We copy the inverse from assoc and apply it over assoc!
                declaredInverse = Ext.apply({}, assoc.inverse);
                delete assoc.inverse;
                inverseOptions = Ext.apply({}, assoc);
                delete inverseOptions.type;
                assoc = Ext.apply({
                    type: name,
                    inverse: inverseOptions
                }, declaredInverse);
                child = inverseOptions.child;
                if (child) {
                    delete inverseOptions.child;
                    assoc.parent = name;
                }
                referenceField = cls.getField(foreignKey);
                if (referenceField) {
                    referenceField.$reference = assoc;
                    assoc.legacy = true;
                    Ext.log.warn('Using foreignKey is deprecated, use a keyed association. ' + 'See Ext.data.field.Field.reference');
                }
                // We already have the entity, we can process it
                me.addReference(cls, referenceField, assoc, false, /* eslint-disable-next-line comma-style */
                function(association, name, leftRole, rightRole) {
                    // Check to see if the user has used belongsTo/hasMany in conjunction.
                    var result = !!association.meta.fromSingle && cls === association.left.cls,
                        l, r;
                    if (result) {
                        l = cls.entityName;
                        r = entityType.entityName;
                        Ext.raise('hasMany ("' + r + '") and belongsTo ("' + l + '") should not be used in conjunction to declare ' + 'a relationship. Use only one.');
                    }
                    return result;
                });
            } else {
                // Pending, push it in the queue for when we load it
                if (!pending[name]) {
                    pending[name] = [];
                }
                pending[name].push([
                    entityType,
                    assoc
                ]);
            }
        },
        checkLegacyAssociation: function(entityType, assoc) {
            var name;
            if (Ext.isString(assoc)) {
                assoc = {
                    type: assoc
                };
            } else {
                assoc = Ext.apply({}, assoc);
            }
            if (assoc.model) {
                assoc.type = assoc.model;
                // TODO: warn
                delete assoc.model;
            }
            name = assoc.associatedName || assoc.name;
            if (name) {
                // TODO: warn
                delete assoc.associatedName;
                delete assoc.name;
                assoc.role = name;
            }
            return assoc;
        },
        afterKeylessAssociations: function(cls) {
            var pending = this.pending,
                name = cls.entityName,
                mine = pending[name],
                i, len;
            if (mine) {
                for (i = 0 , len = mine.length; i < len; ++i) {
                    this.addHasMany.apply(this, mine[i]);
                }
                delete pending[name];
            }
        },
        clear: function(clearNamespace) {
            // for testing
            var me = this,
                timer = me.timer;
            delete me.setConfig;
            if (timer) {
                window.clearTimeout(timer);
                me.timer = null;
            }
            me.associations = {};
            me.associationEntityMap = {};
            me.entities = {};
            me.entityClasses = {};
            me.pending = {};
            me.assocCount = me.entityCount = 0;
            if (clearNamespace) {
                me.setNamespace(null);
            }
        },
        constructProxy: function(Model) {
            var me = this,
                data = Ext.Object.chain(Model),
                proxy = me.getProxy();
            data.schema = me;
            data.prefix = me.getUrlPrefix();
            return proxy.apply(data);
        },
        applyDecoration: function(role) {
            var me = this,
                // To decorate a role like "users" (of a User / Group matrix) we need to add
                // getter/setter methods to access the "users" collection ... to Group! All
                // other data about the "users" role and the User class belong to the given
                // "role" but the receiver class is the inverse.
                cls = role.inverse.cls,
                namer = me.getNamer(),
                getterName, setterName, proto;
            // The cls may not be loaded yet, so we need to check if it is before
            // we can decorate it.
            if (cls && !role.decorated) {
                role.decorated = true;
                proto = cls.prototype;
                if (!(getterName = role.getterName)) {
                    role.getterName = getterName = namer.getterName(role);
                }
                proto[getterName] = role.createGetter();
                // Not all associations will create setters
                if (role.createSetter) {
                    if (!(setterName = role.setterName)) {
                        role.setterName = setterName = namer.setterName(role);
                    }
                    proto[setterName] = role.createSetter();
                }
            }
        },
        decorateModel: function(association) {
            this.applyDecoration(association.left);
            this.applyDecoration(association.right);
        },
        processKeyChecks: function(processAll) {
            var me = this,
                keyCheckQueue = me.keyCheckQueue,
                timer = me.timer,
                len, i, item;
            if (timer) {
                window.clearTimeout(timer);
                me.timer = null;
            }
            if (!keyCheckQueue) {
                return;
            }
            // It's possible that processing a drop may cause another drop
            // to occur. If we're trying to forcibly resolve the state, then
            // we need to trigger all the drops at once. With processAll: false,
            // the loop will jump out after the first iteration.
            do {
                keyCheckQueue = me.keyCheckQueue;
                me.keyCheckQueue = [];
                for (i = 0 , len = keyCheckQueue.length; i < len; ++i) {
                    item = keyCheckQueue[i];
                    item.role.checkKeyForDrop(item.record);
                }
            } while (processAll && me.keyCheckQueue.length);
        },
        queueKeyCheck: function(record, role) {
            var me = this,
                keyCheckQueue = me.keyCheckQueue,
                timer = me.timer;
            if (!keyCheckQueue) {
                me.keyCheckQueue = keyCheckQueue = [];
            }
            keyCheckQueue.push({
                record: record,
                role: role
            });
            if (!timer) {
                me.timer = timer = Ext.defer(me.processKeyChecks, me.getKeyCheckDelay(), me);
            }
        },
        rankEntities: function() {
            var me = this,
                entities = me.entities,
                entityNames = Ext.Object.getKeys(entities),
                length = entityNames.length,
                entityType, i;
            me.nextRank = 1;
            // We do an alpha sort to make the results more stable.
            entityNames.sort();
            for (i = 0; i < length; ++i) {
                entityType = entities[entityNames[i]].cls;
                if (!entityType.rank) {
                    me.rankEntity(entityType);
                }
            }
            me.topoStack = null;
        },
        // cleanup diagnostic stack
        rankEntity: function(entityType) {
            var associations = entityType.associations,
                associatedType, role, roleName;
            /* eslint-disable-next-line vars-on-top, one-var */
            var topoStack = this.topoStack || (this.topoStack = []),
                entityName = entityType.entityName;
            topoStack.push(entityName);
            if (entityType.rank === 0) {
                Ext.raise(entityName + " has circular foreign-key references: " + topoStack.join(" --> "));
            }
            entityType.rank = 0;
            // mark as "adding" so we can detect cycles
            for (roleName in associations) {
                role = associations[roleName];
                // The role describes the thing to which entityType is associated, so we
                // want to know about *this* type and whether it has a foreign-key to the
                // associated type. The left side is the FK owner so if the associated
                // type is !left then entityType is left.
                //
                if (!role.left && role.association.field) {
                    // This entityType has a foreign-key to the associated type, so add
                    // that type first.
                    associatedType = role.cls;
                    if (!associatedType.rank) {
                        this.rankEntity(associatedType);
                    }
                }
            }
            entityType.rank = this.nextRank++;
            topoStack.pop();
        }
    }
});
// private

/**
 * AbstractStore is a superclass of {@link Ext.data.ProxyStore} and {@link Ext.data.ChainedStore}.
 * It's never used directly, but offers a set of methods used by both of those subclasses.
 *
 * Unless you need to make a whole new type of Store, see {@link Ext.data.Store} instead.
 */
Ext.define('Ext.data.AbstractStore', {
    mixins: [
        'Ext.mixin.Observable',
        'Ext.mixin.Factoryable'
    ],
    requires: [
        'Ext.util.Collection',
        'Ext.data.Range',
        'Ext.data.schema.Schema',
        'Ext.util.Filter'
    ],
    factoryConfig: {
        defaultType: 'store',
        type: 'store'
    },
    $configPrefixed: false,
    $configStrict: false,
    config: {
        /**
         * @cfg {Object[]/Function[]} filters
         * Array of {@link Ext.util.Filter Filters} for this store. Can also be passed array of
         * functions which will be used as the {@link Ext.util.Filter#filterFn filterFn} config
         * for filters:
         *
         *     filters: [
         *         function(item) {
         *             return item.weight > 0;
         *         }
         *     ]
         *
         * Individual filters can be specified as an `Ext.util.Filter` instance, a config
         * object for `Ext.util.Filter` or simply a function that will be wrapped in a
         * instance with its {@link Ext.util.Filter#filterFn filterFn} set.
         *
         * For fine grain control of the filters collection, call `getFilters` to return
         * the `Ext.util.Collection` instance that holds this store's filters.
         *
         *      var filters = store.getFilters(); // an Ext.util.FilterCollection
         *
         *      function legalAge (item) {
         *          return item.age >= 21;
         *      }
         *
         *      filters.add(legalAge);
         *
         *      //...
         *
         *      filters.remove(legalAge);
         *
         * Any changes to the `filters` collection will cause this store to adjust
         * its items accordingly.
         */
        filters: null,
        /**
         * @cfg {Boolean} [autoDestroy]
         * When a Store is used by only one {@link Ext.view.View DataView}, and should only exist
         * for the lifetime of that view, then configure the autoDestroy flag as `true`. This
         * causes the destruction of the view to trigger the destruction of its Store.
         */
        autoDestroy: undefined,
        /**
         * @cfg {String} storeId
         * Unique identifier for this store. If present, this Store will be registered with the
         * {@link Ext.data.StoreManager}, making it easy to reuse elsewhere.
         *
         * Note that when a store is instantiated by a Controller, the storeId will default
         * to the name of the store if not specified in the class.
         */
        storeId: null,
        /**
         * @cfg {Boolean} statefulFilters
         * Configure as `true` to have the filters saved when a client {@link Ext.grid.Panel grid}
         * saves its state.
         */
        statefulFilters: false,
        /**
         * @cfg {Ext.util.Sorter[]/Object[]} sorters
         * The initial set of {@link Ext.util.Sorter Sorters}
         *
         * Individual sorters can be specified as an `Ext.util.Sorter` instance, a config
         * object for `Ext.util.Sorter` or simply the name of a property by which to sort.
         *
         * An alternative way to extend the sorters is to call the `sort` method and pass
         * a property or sorter config to add to the sorters.
         *
         * For fine grain control of the sorters collection, call `getSorters` to return
         * the `Ext.util.Collection` instance that holds this collection's sorters.
         *
         *      var sorters = store.getSorters(); // an Ext.util.SorterCollection
         *
         *      sorters.add('name');
         *
         *      //...
         *
         *      sorters.remove('name');
         *
         * Any changes to the `sorters` collection will cause this store to adjust
         * its items accordingly.
         */
        sorters: null,
        /**
        * @cfg {Boolean} [remoteSort=false]
        * `true` if the sorting should be performed on the server side, false if it is local only.
        */
        remoteSort: {
            lazy: true,
            $value: false
        },
        /**
        * @cfg {Boolean} [remoteFilter=false]
        * `true` to defer any filtering operation to the server. If `false`, filtering is done
        * locally on the client.
        */
        remoteFilter: {
            lazy: true,
            $value: false
        },
        /**
        * @cfg {String} groupField
        * The field by which to group data in the store. Internally, grouping is very similar to
        * sorting - the groupField and {@link #groupDir} are injected as the first sorter
        * (see {@link #method-sort}). Stores support a single level of grouping, and groups can be
        * fetched via the {@link #getGroups} method.
        */
        groupField: undefined,
        /**
        * @cfg {String} groupDir
        * The direction in which sorting should be applied when grouping. Supported values are
        * "ASC" and "DESC".
        */
        groupDir: 'ASC',
        /**
         * @cfg {Object/Ext.util.Grouper} grouper
         * The grouper by which to group the data store. May also be specified by the
         * {@link #groupField} config, however
         * they should not be used together.
         */
        grouper: null,
        /**
        * @cfg {Number} pageSize
        * The number of records considered to form a 'page'. This is used to power the built-in
        * paging using the nextPage and previousPage functions when the grid is paged using a
        * {@link Ext.toolbar.Paging PagingToolbar} Defaults to 25.
        *
        * To disable paging, set the pageSize to `0`.
        */
        pageSize: 25,
        /**
         * @cfg {Boolean} [autoSort=true] `true` to maintain sorted order when records
         * are added regardless of requested insertion point, or when an item mutation
         * results in a new sort position.
         *
         * This does not affect a ChainedStore's reaction to mutations of the source
         * Store. If sorters are present when the source Store is mutated, this ChainedStore's
         * sort order will always be maintained.
         * @private
         */
        autoSort: null,
        /**
         * @cfg {Boolean} reloadOnClearSorters
         * Set this to `true` to trigger a reload when the last sorter is removed (only
         * applicable when {@link #cfg!remoteSort} is `true`).
         *
         * By default, the store reloads itself when a sorter is added or removed.
         *
         * When the last sorter is removed, however, the assumption is that the data
         * does not need to become "unsorted", and so no reload is triggered.
         *
         * If the server has a default order to which it reverts in the absence of any
         * sorters, then it is useful to set this config to `true`.
         * @since 6.5.1
         */
        reloadOnClearSorters: false
    },
    /**
     * @property {Number} currentPage
     * The page that the Store has most recently loaded
     * (see {@link Ext.data.Store#loadPage loadPage})
     */
    currentPage: 1,
    /**
     * @property {Boolean} loading
     * `true` if the Store is currently loading via its Proxy.
     * @private
     */
    loading: false,
    /**
     * @property {Boolean} isStore
     * `true` in this class to identify an object as an instantiated Store, or subclass thereof.
     */
    isStore: true,
    /**
     * @property {Number} updating
     * A counter that is increased by `beginUpdate` and decreased by `endUpdate`. When
     * this transitions from 0 to 1 the `{@link #event-beginupdate beginupdate}` event is
     * fired. When it transitions back from 1 to 0 the `{@link #event-endupdate endupdate}`
     * event is fired.
     * @readonly
     * @since 5.0.0
     */
    updating: 0,
    constructor: function(config) {
        var me = this,
            storeId;
        me.callParent([
            config
        ]);
        /**
         * @event add
         * Fired when a Model instance has been added to this Store.
         *
         * @param {Ext.data.Store} store The store.
         * @param {Ext.data.Model[]} records The records that were added.
         * @param {Number} index The index at which the records were inserted.
         * @since 1.1.0
         */
        /**
         * @event remove
         * Fired when one or more records have been removed from this Store.
         *
         * **The signature for this event has changed in 5.0:**
         *
         * @param {Ext.data.Store} store The Store object
         * @param {Ext.data.Model[]} records The records that were removed. In previous
         * releases this was a single record, not an array.
         * @param {Number} index The index at which the records were removed.
         * @param {Boolean} isMove `true` if the child node is being removed so it can be
         * moved to another position in this Store.
         * @since 5.0.0
         */
        /**
         * @event update
         * Fires when a Model instance has been updated.
         * @param {Ext.data.Store} this
         * @param {Ext.data.Model} record The Model instance that was updated
         * @param {String} operation The update operation being performed. Value may be one of:
         *
         *     Ext.data.Model.EDIT
         *     Ext.data.Model.REJECT
         *     Ext.data.Model.COMMIT
         * @param {String[]} modifiedFieldNames Array of field names changed during edit.
         * @param {Object} details An object describing the change. See the
         * {@link Ext.util.Collection#event-itemchange itemchange event} of the store's backing
         * collection
         * @since 1.1.0
         */
        /**
         * @event clear
         * Fired after the {@link Ext.data.Store#removeAll removeAll} method is called.
         * @param {Ext.data.Store} this
         * @since 1.1.0
         */
        /**
         * @event datachanged
         * Fires for any data change in the store. This is a catch-all event that is typically fired
         * in conjunction with other events (such as `add`, `remove`, `update`, `refresh`).
         * @param {Ext.data.Store} this The data store
         * @since 1.1.0
         */
        /**
         * @event refresh
         * Fires when the data cache has changed in a bulk manner (e.g., it has been sorted,
         * filtered, etc.) and a widget that is using this Store as a Record cache should refresh
         * its view.
         * @param {Ext.data.Store} this The data store
         */
        /**
         * @event beginupdate
         * Fires when the {@link #beginUpdate} method is called. Automatic synchronization as
         * configured by the {@link Ext.data.ProxyStore#autoSync autoSync} flag is deferred until
         * the {@link #endUpdate} method is called, so multiple mutations can be coalesced into one
         * synchronization operation.
         */
        /**
         * @event endupdate
         * Fires when the {@link #endUpdate} method is called. Automatic synchronization as
         * configured by the {@link Ext.data.ProxyStore#autoSync autoSync} flag is deferred until
         * the {@link #endUpdate} method is called, so multiple mutations can be coalesced into one
         * synchronization operation.
         */
        /**
         * @event beforesort
         * Fires before a store is sorted.
         *
         * For {@link #remoteSort remotely sorted} stores, this will be just before the load
         * operation triggered by changing the store's sorters.
         *
         * For locally sorted stores, this will be just before the data items in the store's
         * backing collection are sorted.
         * @param {Ext.data.Store} store The store being sorted
         * @param {Ext.util.Sorter[]} sorters Array of sorters applied to the store
         */
        /**
         * @event sort
         * Fires after a store is sorted.
         *
         * For {@link #remoteSort remotely sorted} stores, this will be upon the success of a load
         * operation triggered by changing the store's sorters.
         *
         * For locally sorted stores, this will be just after the data items in the store's backing
         * collection are sorted.
         * @param {Ext.data.Store} store The store being sorted
         */
        me.isInitializing = true;
        me.mixins.observable.constructor.call(me, config);
        me.isInitializing = false;
        storeId = me.getStoreId();
        if (!storeId && (config && config.id)) {
            me.setStoreId(storeId = config.id);
        }
        if (storeId) {
            Ext.data.StoreManager.register(me);
        }
    },
    /**
     * Create a `Range` instance to access records by their index.
     *
     * @param {Object/Ext.data.Range} [config]
     * @return {Ext.data.Range}
     * @since 6.5.0
     */
    createActiveRange: function(config) {
        var range = Ext.apply({
                store: this
            }, config);
        return new Ext.data.Range(range);
    },
    /**
     * @private
     * Called from onCollectionItemsAdd. Collection add changes the items reference of the
     * collection, and that array object if directly referenced by Ranges. The ranges
     * have to refresh themselves upon add.
     */
    syncActiveRanges: function() {
        var activeRanges = this.activeRanges,
            len = activeRanges && activeRanges.length,
            i;
        for (i = 0; i < len; i++) {
            activeRanges[i].refresh();
        }
    },
    /**
     * Gets the number of records in store.
     *
     * If using paging, this may not be the total size of the dataset. If the data object used by
     * the Reader contains the dataset size, then the {@link Ext.data.ProxyStore#getTotalCount}
     * function returns the dataset size. **Note**: see the Important note in
     * {@link Ext.data.ProxyStore#method-load}.
     *
     * When store is filtered, it's the number of records matching the filter.
     *
     * @return {Number} The number of Records in the Store.
     */
    getCount: function() {
        var data = this.getData();
        // We may be destroyed, in which case "data" will be null... best to just
        // report 0 items vs throw an exception
        return data ? data.getCount() : 0;
    },
    /**
     * Determines if the passed range is available in the page cache.
     * @private
     * @param {Number} start The start index
     * @param {Number} end The end index in the range
     */
    rangeCached: function(start, end) {
        return this.getData().getCount() >= Math.max(start, end);
    },
    /**
     * Checks if a record is in the current active data set.
     * @param {Ext.data.Model} record The record
     * @return {Boolean} `true` if the record is in the current active data set.
     * @method contains
     */
    /**
     * Finds the index of the first matching Record in this store by a specific field value.
     *
     * When store is filtered, finds records only within filter.
     *
     * **IMPORTANT**
     *
     * **If this store is {@link Ext.data.BufferedStore Buffered}, this can ONLY find records
     * which happen to be cached in the page cache. This will be parts of the dataset around the
     * currently visible zone, or recently visited zones if the pages have not yet been purged from
     * the cache.**
     *
     * @param {String} property The name of the Record field to test.
     * @param {String/RegExp} value Either a string that the field value
     * should begin with, or a RegExp to test against the field.
     * @param {Number} [startIndex=0] The index to start searching at
     * @param {Boolean} [anyMatch=false] True to match any part of the string, not just the
     * beginning.
     * @param {Boolean} [caseSensitive=false] True for case sensitive comparison
     * @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters
     * added to the regex). Ignored if `anyMatch` is `true`.
     * @return {Number} The matched index or -1
     */
    find: function(property, value, startIndex, anyMatch, caseSensitive, exactMatch) {
        //             exactMatch
        //  anyMatch    F       T
        //      F       ^abc    ^abc$
        //      T       abc     abc
        //
        var startsWith = !anyMatch,
            endsWith = !!(startsWith && exactMatch);
        return this.getData().findIndex(property, value, startIndex, startsWith, endsWith, !caseSensitive);
    },
    /**
     * Finds the first matching Record in this store by a specific field value.
     *
     * When store is filtered, finds records only within filter.
     *
     * **IMPORTANT**
     *
     * **If this store is {@link Ext.data.BufferedStore Buffered}, this can ONLY find records which
     * happen to be cached in the page cache. This will be parts of the dataset around the
     * currently visible zone, or recently visited zones if the pages have not yet been purged
     * from the cache.**
     *
     * @param {String} fieldName The name of the Record field to test.
     * @param {String/RegExp} value Either a string that the field value
     * should begin with, or a RegExp to test against the field.
     * @param {Number} [startIndex=0] The index to start searching at
     * @param {Boolean} [anyMatch=false] True to match any part of the string, not just the
     * beginning.
     * @param {Boolean} [caseSensitive=false] True for case sensitive comparison
     * @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters
     * added to the regex). Ignored if `anyMatch` is `true`.
     * @return {Ext.data.Model} The matched record or null
     */
    findRecord: function() {
        var me = this,
            index = me.find.apply(me, arguments);
        return index !== -1 ? me.getAt(index) : null;
    },
    /**
     * Finds the index of the first matching Record in this store by a specific field value.
     *
     * When store is filtered, finds records only within filter.
     *
     * **IMPORTANT**
     *
     * **If this store is {@link Ext.data.BufferedStore Buffered}, this can ONLY find records which
     * happen to be cached in the page cache. This will be parts of the dataset around the
     * currently visible zone, or recently visited zones if the pages have not yet been purged
     * from the cache.**
     *
     * @param {String} fieldName The name of the Record field to test.
     * @param {Object} value The value to match the field against.
     * @param {Number} [startIndex=0] The index to start searching at
     * @return {Number} The matched index or -1
     */
    findExact: function(fieldName, value, startIndex) {
        return this.getData().findIndexBy(function(rec) {
            return rec.isEqual(rec.get(fieldName), value);
        }, this, startIndex);
    },
    /**
     * Find the index of the first matching Record in this Store by a function.
     * If the function returns `true` it is considered a match.
     *
     * When store is filtered, finds records only within filter.
     *
     * **IMPORTANT**
     *
     * **If this store is {@link Ext.data.BufferedStore Buffered}, this can ONLY find records which
     * happen to be cached in the page cache. This will be parts of the dataset around the
     * currently visible zone, or recently visited zones if the pages have not yet been purged
     * from the cache.**
     *
     * @param {Function} fn The function to be called. It will be passed the following parameters:
     *  @param {Ext.data.Model} fn.record The record to test for filtering. Access field values
     *  using {@link Ext.data.Model#get}.
     *  @param {Object} fn.id The ID of the Record passed.
     * @param {Object} [scope] The scope (this reference) in which the function is executed.
     * Defaults to this Store.
     * @param {Number} [start=0] The index at which to start searching.
     * @return {Number} The matched index or -1
     */
    findBy: function(fn, scope, start) {
        return this.getData().findIndexBy(fn, scope, start);
    },
    /**
     * Get the Record at the specified index.
     *
     * The index is effected by filtering.
     *
     * @param {Number} index The index of the Record to find.
     * @return {Ext.data.Model} The Record at the passed index. Returns null if not found.
     */
    getAt: function(index) {
        return this.getData().getAt(index) || null;
    },
    /**
     * Gathers a range of Records between specified indices.
     *
     * This method is affected by filtering.
     *
     * @param {Number} start The starting index. Defaults to zero.
     * @param {Number} end The ending index. Defaults to the last record. The end index
     * **is included**.
     * @param [options] (private) Used by BufferedRenderer when using a BufferedStore.
     * @return {Ext.data.Model[]} An array of records.
     */
    getRange: function(start, end, options) {
        // Collection's getRange is exclusive. Do NOT mutate the value: it is passed to the
        // callback.
        var result = this.getData().getRange(start, Ext.isNumber(end) ? end + 1 : end);
        // BufferedRenderer requests a range with a callback to process that range.
        // Because it may be dealing with a buffered store and the range may not be available
        // synchronously.
        if (options && options.callback) {
            options.callback.call(options.scope || this, result, start, end, options);
        }
        return result;
    },
    /**
     * Gets the filters for this store.
     * @param {Boolean} [autoCreate] (private)
     * @return {Ext.util.FilterCollection} The filters
     */
    getFilters: function(autoCreate) {
        var me = this,
            result = me.callParent();
        if (!result && autoCreate !== false) {
            me.setFilters([]);
            result = me.callParent();
        }
        return result;
    },
    applyFilters: function(filters, filtersCollection) {
        var me = this,
            created;
        if (!filtersCollection) {
            filtersCollection = me.createFiltersCollection();
            created = true;
        }
        filtersCollection.add(filters);
        if (created) {
            me.onRemoteFilterSet(filtersCollection, me.getRemoteFilter());
        }
        return filtersCollection;
    },
    /**
     * Gets the sorters for this store.
     * @param {Boolean} [autoCreate] (private)
     * @return {Ext.util.SorterCollection} The sorters
     */
    getSorters: function(autoCreate) {
        var me = this,
            result = me.callParent();
        if (!result && autoCreate !== false) {
            // If not preventing creation, force it here
            me.setSorters([]);
            result = me.callParent();
        }
        return result;
    },
    applySorters: function(sorters, sortersCollection) {
        var me = this,
            created;
        if (!sortersCollection) {
            sortersCollection = me.createSortersCollection();
            created = true;
        }
        sortersCollection.add(sorters);
        if (created) {
            me.onRemoteSortSet(sortersCollection, me.getRemoteSort());
        }
        return sortersCollection;
    },
    /**
     * Filters the data in the Store by one or more fields. Example usage:
     *
     *     //filter with a single field
     *     myStore.filter('firstName', 'Don');
     *
     *     //filtering with multiple filters
     *     myStore.filter([
     *         {
     *             property : 'firstName',
     *             value    : 'Don'
     *         },
     *         {
     *             property : 'lastName',
     *             value    : 'Griffin'
     *         }
     *     ]);
     *
     * Internally, Store converts the passed arguments into an array of
     * {@link Ext.util.Filter} instances, and delegates the actual filtering to its internal
     * {@link Ext.util.Collection} or the remote server.
     *
     * @param {String/Ext.util.Filter[]} [filters] Either a string name of one of the
     * fields in this Store's configured {@link Ext.data.Model Model}, or an array of
     * filter configurations.
     * @param {String} [value] The property value by which to filter. Only applicable if
     * `filters` is a string.
     * @param {Boolean} [suppressEvent] (private)
     */
    filter: function(filters, value, suppressEvent) {
        if (Ext.isString(filters)) {
            filters = {
                property: filters,
                value: value
            };
        }
        this.suppressNextFilter = !!suppressEvent;
        this.getFilters().add(filters);
        this.suppressNextFilter = false;
    },
    /**
     * Removes an individual Filter from the current {@link #cfg-filters filter set}
     * using the passed Filter/Filter id and by default, applies the updated filter set
     * to the Store's unfiltered dataset.
     *
     * @param {String/Ext.util.Filter} toRemove The id of a Filter to remove from the
     * filter set, or a Filter instance to remove.
     * @param {Boolean} [suppressEvent] If `true` the filter is cleared silently.
     */
    removeFilter: function(toRemove, suppressEvent) {
        var me = this,
            filters = me.getFilters();
        me.suppressNextFilter = !!suppressEvent;
        if (toRemove instanceof Ext.util.Filter) {
            filters.remove(toRemove);
        } else {
            filters.removeByKey(toRemove);
        }
        me.suppressNextFilter = false;
    },
    updateAutoSort: function(autoSort) {
        // Keep collection synced with our autoSort setting
        this.getData().setAutoSort(autoSort);
    },
    updateRemoteSort: function(remoteSort) {
        // Don't call the getter here, we don't want to force sorters to be created here.
        // Also, applySorters calls getRemoteSort, which may trigger the initGetter.
        this.onRemoteSortSet(this.getSorters(false), remoteSort);
    },
    updateRemoteFilter: function(remoteFilter) {
        this.onRemoteFilterSet(this.getFilters(false), remoteFilter);
    },
    /**
     * Adds a new Filter to this Store's {@link #cfg-filters filter set} and
     * by default, applies the updated filter set to the Store's unfiltered dataset.
     * @param {Object[]/Ext.util.Filter[]} filters The set of filters to add to the current
     * {@link #cfg-filters filter set}.
     * @param {Boolean} [suppressEvent] If `true` the filter is cleared silently.
     */
    addFilter: function(filters, suppressEvent) {
        this.suppressNextFilter = !!suppressEvent;
        this.getFilters().add(filters);
        this.suppressNextFilter = false;
    },
    /**
     * Filters by a function. The specified function will be called for each
     * Record in this Store. If the function returns `true` the Record is included,
     * otherwise it is filtered out.
     *
     * When store is filtered, most of the methods for accessing store data will be working only
     * within the set of filtered records. The notable exception is {@link #getById}.
     *
     * @param {Function} fn The function to be called. It will be passed the following parameters:
     *  @param {Ext.data.Model} fn.record The record to test for filtering. Access field values
     *  using {@link Ext.data.Model#get}.
     * @param {Object} [scope] The scope (this reference) in which the function is executed.
     * Defaults to this Store.
     */
    filterBy: function(fn, scope) {
        this.getFilters().add({
            filterFn: fn,
            scope: scope || this
        });
    },
    /**
     * Reverts to a view of the Record cache with no filtering applied.
     * @param {Boolean} [suppressEvent] If `true` the filter is cleared silently.
     *
     * For a locally filtered Store, this means that the filter collection is cleared without
     * firing the {@link #datachanged} event.
     *
     * For a remotely filtered Store, this means that the filter collection is cleared, but
     * the store is not reloaded from the server.
     */
    clearFilter: function(suppressEvent) {
        var me = this,
            filters = me.getFilters(false);
        if (!filters || filters.getCount() === 0) {
            return;
        }
        me.suppressNextFilter = !!suppressEvent;
        filters.removeAll();
        me.suppressNextFilter = false;
    },
    /**
     * Tests whether the store currently has any active filters.
     * @return {Boolean} `true` if the store is filtered.
     */
    isFiltered: function() {
        return this.getFilters().getCount() > 0;
    },
    /**
     * Tests whether the store currently has any active sorters.
     * @return {Boolean} `true` if the store is sorted.
     */
    isSorted: function() {
        var sorters = this.getSorters(false);
        return !!(sorters && sorters.length > 0) || this.isGrouped();
    },
    addFieldTransform: function(sorter) {
        // Transform already specified, leave it
        if (sorter.getTransform()) {
            return;
        }
        /* eslint-disable-next-line vars-on-top */
        var fieldName = sorter.getProperty(),
            Model = this.getModel(),
            field, sortType;
        if (Model) {
            field = Model.getField(fieldName);
            sortType = field ? field.getSortType() : null;
        }
        if (sortType && sortType !== Ext.identityFn) {
            sorter.setTransform(sortType);
        }
    },
    /**
     * This method may be called to indicate the start of multiple changes to the store.
     *
     * Automatic synchronization as configured by the {@link Ext.data.ProxyStore#autoSync autoSync}
     * flag is deferred until the {@link #endUpdate} method is called, so multiple mutations can be
     * coalesced into one synchronization operation.
     *
     * Internally this method increments a counter that is decremented by `endUpdate`. It
     * is important, therefore, that if you call `beginUpdate` directly you match that
     * call with a call to `endUpdate` or you will prevent the collection from updating
     * properly.
     *
     * For example:
     *
     *      var store = Ext.StoreManager.lookup({
     *          //...
     *          autoSync: true
     *      });
     *
     *      store.beginUpdate();
     *
     *      record.set('fieldName', 'newValue');
     *
     *      store.add(item);
     *      // ...
     *
     *      store.insert(index, otherItem);
     *      //...
     *
     *      // Interested parties will listen for the endupdate event
     *      store.endUpdate();
     *
     * @since 5.0.0
     */
    beginUpdate: function() {
        if (!this.updating++ && this.hasListeners.beginupdate) {
            this.fireEvent('beginupdate');
        }
    },
    /**
     * This method is called after modifications are complete on a store. For details
     * see `{@link #beginUpdate}`.
     * @since 5.0.0
     */
    endUpdate: function() {
        if (this.updating && !--this.updating) {
            if (this.hasListeners.endupdate) {
                this.fireEvent('endupdate');
            }
            this.onEndUpdate();
        }
    },
    /**
     * @private
     * Returns the grouping, sorting and filtered state of this Store.
     */
    getState: function() {
        var me = this,
            sorters = [],
            filters = me.getFilters(),
            grouper = me.getGrouper(),
            filterState, hasState, result;
        // Create sorters config array.
        me.getSorters().each(function(s) {
            sorters[sorters.length] = s.getState();
            hasState = true;
        });
        // Because we do not provide a filter changing mechanism, only statify the filters if they
        // opt in. Otherwise filters would get "stuck".
        if (me.statefulFilters && me.saveStatefulFilters) {
            // If saveStatefulFilters is turned on then we know that the filter collection has
            // changed since page load. Initiate the filterState as an empty stack, which is
            // meaningful in itself. If there are any filter in the collection, persist them.
            hasState = true;
            filterState = [];
            filters.each(function(f) {
                filterState[filterState.length] = f.getState();
            });
        }
        if (grouper) {
            hasState = true;
        }
        // If there is any state to save, return it as an object
        if (hasState) {
            result = {};
            if (sorters.length) {
                result.sorters = sorters;
            }
            if (filterState) {
                result.filters = filterState;
            }
            if (grouper) {
                result.grouper = grouper.getState();
            }
        }
        return result;
    },
    /**
     * @private
     * Restores state to the passed state
     */
    applyState: function(state) {
        var me = this,
            stateSorters = state.sorters,
            stateFilters = state.filters,
            stateGrouper = state.grouper;
        if (stateSorters) {
            me.getSorters().replaceAll(stateSorters);
        }
        if (stateFilters) {
            // We found persisted filters so let's save stateful filters from this point forward.
            me.saveStatefulFilters = true;
            me.getFilters().replaceAll(stateFilters);
        }
        if (stateGrouper) {
            me.setGrouper(stateGrouper);
        }
    },
    /**
     * Get the Record with the specified id.
     *
     * This method is not affected by filtering, lookup will be performed from all records
     * inside the store, filtered or not.
     *
     * @param {Mixed} id The id of the Record to find.
     * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.
     * @method getById
     */
    /**
     * Returns true if the store has a pending load task.
     * @return {Boolean} `true` if the store has a pending load task.
     * @private
     * @method
     */
    hasPendingLoad: Ext.emptyFn,
    /**
     * Returns `true` if the Store has been loaded.
     * @return {Boolean} `true` if the Store has been loaded.
     * @method
     */
    isLoaded: Ext.emptyFn,
    /**
     * Returns `true` if the Store is currently performing a load operation.
     * @return {Boolean} `true` if the Store is currently loading.
     * @method
     */
    isLoading: Ext.emptyFn,
    destroy: function() {
        var me = this;
        if (me.hasListeners.beforedestroy) {
            me.fireEvent('beforedestroy', me);
        }
        me.destroying = true;
        if (me.getStoreId()) {
            Ext.data.StoreManager.unregister(me);
        }
        me.doDestroy();
        if (me.hasListeners.destroy) {
            me.fireEvent('destroy', me);
        }
        // This just makes it hard to ask "was destroy() called?":
        // me.destroying = false; // removed in 7.0
        // This will finish the sequence and null object references
        me.callParent();
    },
    /**
     * Perform the Store destroying sequence. Override this method to add destruction
     * behaviors to your custom Stores.
     *
     */
    doDestroy: Ext.emptyFn,
    /**
     * Sorts the data in the Store by one or more of its properties. Example usage:
     *
     *     //sort by a single field
     *     myStore.sort('myField', 'DESC');
     *
     *     //sorting by multiple fields
     *     myStore.sort([
     *         {
     *             property : 'age',
     *             direction: 'ASC'
     *         },
     *         {
     *             property : 'name',
     *             direction: 'DESC'
     *         }
     *     ]);
     *
     * Internally, Store converts the passed arguments into an array of {@link Ext.util.Sorter}
     * instances, and either delegates the actual sorting to its internal
     * {@link Ext.util.Collection} or the remote server.
     *
     * When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field,
     * so this code:
     *
     *     store.sort('myField');
     *     store.sort('myField');
     *
     * Is equivalent to this code, because Store handles the toggling automatically:
     *
     *     store.sort('myField', 'ASC');
     *     store.sort('myField', 'DESC');
     *
     * @param {String/Ext.util.Sorter[]} [field] Either a string name of one of the
     * fields in this Store's configured {@link Ext.data.Model Model}, or an array of
     * sorter configurations.
     * @param {"ASC"/"DESC"} [direction="ASC"] The overall direction to sort the data by.
     * @param {"append"/"prepend"/"replace"/"multi"} [mode="replace"]
     */
    sort: function(field, direction, mode) {
        var me = this;
        if (arguments.length === 0) {
            if (me.getRemoteSort()) {
                me.load();
            } else {
                me.forceLocalSort();
            }
        } else {
            me.getSorters().addSort(field, direction, mode);
        }
    },
    // This is attached to the data Collection's beforesort event only if not remoteSort
    // If remoteSort, the event is fired before the reload call in Ext.data.ProxyStore#load.
    onBeforeCollectionSort: function(store, sorters) {
        if (sorters) {
            this.fireEvent('beforesort', this, sorters.getRange());
        }
    },
    onSorterEndUpdate: function() {
        var me = this,
            fireSort = true,
            sorters = me.getSorters(false),
            sorterCount;
        // If we're in the middle of grouping, it will take care of loading.
        // If the collection is not instantiated yet, it's because we are constructing.
        if (me.settingGroups || !sorters) {
            return;
        }
        sorters = sorters.getRange();
        sorterCount = sorters.length;
        if (me.getRemoteSort()) {
            // Only reload if there are sorters left to influence the sort order.
            // Unless reloadOnClearSorters is set to indicate that there's a default
            // order used by the server which must be returned to when there is no
            // explicit sort order.
            if (sorters.length || me.getReloadOnClearSorters()) {
                // The sort event will fire in the load callback;
                fireSort = false;
                me.load({
                    callback: function() {
                        me.fireEvent('sort', me, sorters);
                    }
                });
            }
        } else if (sorterCount) {
            me.fireEvent('datachanged', me);
            me.fireEvent('refresh', me);
        }
        if (fireSort) {
            // Sort event must fire when sorters collection is updated to empty.
            me.fireEvent('sort', me, sorters);
        }
    },
    onFilterEndUpdate: function() {
        var me = this,
            suppressNext = me.suppressNextFilter,
            filters = me.getFilters(false);
        // If the collection is not instantiated yet, it's because we are constructing.
        if (!filters) {
            return;
        }
        if (me.getRemoteFilter()) {
            me.getFilters().each(function(filter) {
                if (filter.getInitialConfig().filterFn) {
                    Ext.raise('Unable to use a filtering function in conjunction with ' + 'remote filtering.');
                }
            });
            me.currentPage = 1;
            if (!suppressNext) {
                me.load();
            }
        } else if (!suppressNext) {
            me.fireEvent('datachanged', me);
            me.fireEvent('refresh', me);
        }
        if (me.trackStateChanges) {
            // We just mutated the filter collection so let's save stateful filters
            // from this point forward.
            me.saveStatefulFilters = true;
        }
        // This is not affected by suppressEvent.
        me.fireEvent('filterchange', me, me.getFilters().getRange());
    },
    updateGroupField: function(field) {
        if (field) {
            this.setGrouper({
                property: field,
                direction: this.getGroupDir()
            });
        } else {
            this.setGrouper(null);
        }
    },
    /**
     * @method setFilters
     */
    /**
     * @method setSorters
     */
    getGrouper: function() {
        return this.getData().getGrouper();
    },
    /**
     * Groups data inside the store.
     * @param {String/Object} grouper Either a string name of one of the fields in this Store's
     * configured {@link Ext.data.Model Model}, or an object, or a {@link Ext.util.Grouper grouper}
     * configuration object.
     * @param {String} [direction] The overall direction to group the data by. Defaults to the
     * value of {@link #groupDir}.
     */
    group: function(grouper, direction) {
        var me = this,
            sorters = me.getSorters(false),
            change = grouper || (sorters && sorters.length),
            data = me.getData();
        if (grouper && typeof grouper === 'string') {
            grouper = {
                property: grouper,
                direction: direction || me.getGroupDir()
            };
        }
        me.settingGroups = true;
        // The config system would reject this case as no change
        // Assume the caller has changed a configuration of the Grouper
        // and requires the sorting to be redone.
        if (grouper === data.getGrouper()) {
            data.updateGrouper(grouper);
        } else {
            data.setGrouper(grouper);
        }
        delete me.settingGroups;
        if (change) {
            if (me.getRemoteSort()) {
                if (!me.isInitializing) {
                    me.load({
                        scope: me,
                        callback: function() {
                            me.fireGroupChange();
                        }
                    });
                }
            } else // do not pass on args
            {
                me.fireEvent('datachanged', me);
                me.fireEvent('refresh', me);
                me.fireGroupChange();
            }
        } else // groupchange event must fire when group is cleared.
        // The Grouping feature forces a view refresh when changed to a null grouper
        {
            me.fireGroupChange();
        }
    },
    fireGroupChange: function(grouper) {
        var me = this;
        if (!me.isConfiguring && !me.destroying && !me.destroyed) {
            me.fireGroupChangeEvent(grouper || me.getGrouper());
        }
    },
    fireGroupChangeEvent: function(grouper) {
        this.fireEvent('groupchange', this, grouper);
    },
    /**
     * Clear the store grouping
     */
    clearGrouping: function() {
        this.group(null);
    },
    getGroupField: function() {
        var grouper = this.getGrouper(),
            group = '';
        if (grouper) {
            group = grouper.getProperty();
        }
        return group;
    },
    /**
     * Tests whether the store currently has an active grouper.
     * @return {Boolean} `true` if the store is grouped.
     */
    isGrouped: function() {
        return !!this.getGrouper();
    },
    applyGrouper: function(grouper) {
        this.group(grouper);
        return this.getData().getGrouper();
    },
    /**
     * Returns a collection of readonly sub-collections of your store's records
     * with grouping applied. These sub-collections are maintained internally by
     * the collection.
     *
     * See {@link #groupField}, {@link #groupDir}. Example for a store
     * containing records with a color field:
     *
     *     var myStore = Ext.create('Ext.data.Store', {
     *         groupField: 'color',
     *         groupDir  : 'DESC'
     *     });
     *
     *     myStore.getGroups();
     *
     * The above should result in the following format:
     *
     *     [
     *         {
     *             name: 'yellow',
     *             children: [
     *                 // all records where the color field is 'yellow'
     *             ]
     *         },
     *         {
     *             name: 'red',
     *             children: [
     *                 // all records where the color field is 'red'
     *             ]
     *         }
     *     ]
     *
     * Group contents are affected by filtering.
     *
     * @return {Ext.util.Collection} The grouped data
     */
    getGroups: function() {
        return this.getData().getGroups();
    },
    onEndUpdate: Ext.emptyFn,
    privates: {
        _metaProperties: {
            count: 'getCount',
            first: 'first',
            last: 'last',
            loading: 'hasPendingLoad',
            totalCount: 'getTotalCount'
        },
        interpret: function(name) {
            var me = this,
                accessor = me._metaProperties[name];
            return accessor && me[accessor]();
        },
        // e.g., me.getCount()
        loadsSynchronously: Ext.privateFn,
        onRemoteFilterSet: function(filters, remoteFilter) {
            if (filters) {
                filters[remoteFilter ? 'on' : 'un']('endupdate', 'onFilterEndUpdate', this);
            }
        },
        // If remoteSort is set, we react to the endUpdate of the sorters Collection by reloading
        // if there are still some sorters, or we're configured to reload on sorter remove.
        // If remoteSort is set, we do not need to listen for the data Collection's beforesort
        // event.
        //
        // If local sorting, we do not need to react to the endUpdate of the sorters Collection.
        // If local sorting, we listen for the data Collection's beforesort event to fire our
        // beforesort event.
        onRemoteSortSet: function(sorters, remoteSort) {
            var me = this,
                data;
            if (sorters) {
                sorters[remoteSort ? 'on' : 'un']('endupdate', 'onSorterEndUpdate', me);
                data = me.getData();
                if (data) {
                    data[remoteSort ? 'un' : 'on']('beforesort', 'onBeforeCollectionSort', me);
                }
            }
        }
    },
    deprecated: {
        5: {
            methods: {
                destroyStore: function() {
                    this.destroy();
                }
            }
        }
    }
});

/**
 * This class hols the results of a validator for an `Ext.data.Model`. These objects are
 * placed in an `Ext.data.ErrorCollection` and returned by `{@link Ext.data.Model#validate}`.
 *
 * Usually this class does not need to be instantiated directly - instances are instead created
 * automatically when {@link Ext.data.Model#validate validate} on a model instance.
 *
 * @deprecated 5.0 Use `Ext.data.Validation` instead.
 */
Ext.define('Ext.data.Error', {
    isError: true,
    $configPrefixed: false,
    // compat
    config: {
        /**
         * @cfg {String} field
         * The name of the field this error belongs to.
         */
        field: null,
        /**
         * @cfg {String} message
         * The message containing the description of the error.
         */
        message: ''
    },
    constructor: function(config) {
        this.initConfig(config);
        this.msg = this.message;
    }
});
// compat

/**
 * Wraps a collection of validation error responses and provides convenient functions for
 * accessing and errors for specific fields.
 *
 * Usually this class does not need to be instantiated directly - instances are instead
 * created automatically when {@link Ext.data.Model#validate validate} on a model instance:
 *
 *      // Validate some existing model instance - in this case it returned 2 failures
 *      // messages
 *      
 *      var errors = myModel.validate();
 *      errors.isValid(); //false
 *      
 *      errors.length; //2
 *      errors.getByField('name');  // [{field: 'name',  message: 'must be present'}]
 *      errors.getByField('title'); // [{field: 'title', message: 'is too short'}]
 */
Ext.define('Ext.data.ErrorCollection', {
    extend: 'Ext.util.MixedCollection',
    // not Ext.util.Collection due to API differences
    alternateClassName: 'Ext.data.Errors',
    requires: [
        'Ext.data.Error'
    ],
    init: function(record) {
        var me = this,
            fields = record.fields,
            data = record.data,
            before, field, i, len, msg, val, name;
        for (i = 0 , len = fields.length; i < len; ++i) {
            field = fields[i];
            name = field.name;
            val = data[name];
            if (field.validate && !field.validate.$nullFn) {
                before = me.length;
                msg = field.validate(val, null, me, record);
                if (before === me.length && msg !== true) {
                    me.add(name, msg);
                }
            }
        }
        return me;
    },
    add: function(key, value) {
        var me = this,
            defaultMessage = Ext.data.field.Field.defaultInvalidMessage,
            obj = key,
            // for single argument form
            current;
        if (Ext.isString(key)) {
            obj = new Ext.data.Error({
                field: key,
                message: value || defaultMessage
            });
        } else {
            if (!(obj.isError)) {
                obj = new Ext.data.Error({
                    field: obj.field || obj.name,
                    message: obj.error || obj.message || obj.msg || defaultMessage
                });
            }
            key = obj.field;
        }
        current = me.get(key);
        if (current) {
            if (Ext.isArray(current)) {
                current.push(obj);
                return current;
            }
            me.removeAtKey(key);
            obj = [
                current,
                obj
            ];
            obj.field = key;
            // Because the value we want in the collection is an array, we need to wrap it
            // another layer of array or the base add method will add each element.
            obj = [
                obj
            ];
        }
        return me.callParent([
            obj
        ]);
    },
    getKey: function(item) {
        return item.field;
    },
    /**
     * Returns true if there are no errors in the collection
     * @return {Boolean}
     */
    isValid: function() {
        return this.length === 0;
    },
    /**
     * Returns all of the errors for the given field
     * @param {String} fieldName The field to get errors for
     * @return {Object[]} All errors for the given field
     */
    getByField: function(fieldName) {
        var values = this.get(fieldName);
        if (values && !Ext.isArray(values)) {
            values = [
                values
            ];
        }
        return values || [];
    }
});

/**
 * Represents a read or write operation performed by a {@link Ext.data.proxy.Proxy Proxy}.
 * Operation objects are used to enable communication between Stores and Proxies.
 * Application developers should rarely need to interact with Operation objects directly.
 *
 * Several Operations can be batched together in a {@link Ext.data.Batch batch}.
 */
Ext.define('Ext.data.operation.Operation', {
    alternateClassName: 'Ext.data.Operation',
    isOperation: true,
    config: {
        /**
         * @cfg {Boolean} synchronous
         * True if this Operation is to be executed synchronously. This property is inspected by a
         * {@link Ext.data.Batch Batch} to see if a series of Operations can be executed in parallel
         * or not.
         */
        synchronous: false,
        /**
         * @cfg {String} url
         * The url for this operation. Typically this will be provided by a proxy and not configured
         * here.
         */
        url: '',
        /**
         * @cfg {Object} params
         * Parameters to pass along with the request when performing the operation.
         */
        params: undefined,
        /**
         * @cfg {Function} callback
         * Function to execute when operation completed.
         * @cfg {Ext.data.Model[]} callback.records Array of records.
         * @cfg {Ext.data.operation.Operation} callback.operation The Operation itself.
         * @cfg {Boolean} callback.success True when operation completed successfully.
         */
        callback: undefined,
        /**
         * @cfg {Object} scope
         * Scope for the {@link #callback} function.
         */
        scope: undefined,
        /**
         * @cfg {Ext.data.ResultSet} resultSet
         * The ResultSet for this operation.
         * @accessor
         */
        resultSet: null,
        /**
         * @private
         * @cfg {Object} response
         * The response for this operation.
         */
        response: null,
        /**
         * @cfg {Ext.data.Request} request
         * The request for this operation.
         */
        request: null,
        /**
         * @cfg {Ext.data.Model[]} records
         * The records associated with this operation. If this is a `read` operation, this will be
         * `null` until data is returned from the {@link Ext.data.proxy.Proxy}.
         */
        records: null,
        /**
         * @cfg {Object} id
         * The id of the operation.
         */
        id: undefined,
        /**
         * @cfg {Ext.data.proxy.Proxy} proxy
         * The proxy for this operation
         */
        proxy: null,
        /**
         * @cfg {Ext.data.Batch} 
         * The batch for this operation, if applicable
         */
        batch: null,
        /**
         * @cfg {Function} recordCreator
         * Passed to the reader, see {@link Ext.data.reader.Reader#read}
         * @private
         */
        recordCreator: null,
        // We use this because in a lot of cases the developer can indirectly pass
        // a callback/scope and that will get pushed on to the operation. As such,
        // create our own hook for the callback that will fire first
        /**
         * @cfg {Function} internalCallback
         * A callback to run before the {@link #callback}. 
         * @private
         */
        internalCallback: null,
        /**
         * @cfg {Object} internalScope
         * Scope to run the {@link #internalCallback}
         * @private
         */
        internalScope: null
    },
    /**
     * @property {Number} order
     * This number is used by `{@link Ext.data.Batch#sort}` to order operations. Order of
     * operations of the same type is determined by foreign-key dependencies. The array of
     * operations is sorted by ascending (increasing) values of `order`.
     * @private
     * @readonly
     * @since 5.0.0
     */
    order: 0,
    /**
     * @property {Number} foreignKeyDirection
     * This number is used by `{@link Ext.data.Batch#sort}` to order operations of the
     * same type. This value is multipled by the "entity rank" (which is determined by
     * foreign-key dependencies) to invert the direction of the sort based on the type of
     * operation. Only `Ext.data.operation.Destroy` overrides this value.
     * @private
     * @readonly
     * @since 5.0.0
     */
    foreignKeyDirection: 1,
    /**
     * @property {Boolean} started
     * The start status of this Operation. Use {@link #isStarted}.
     * @readonly
     * @private
     */
    started: false,
    /**
     * @property {Boolean} running
     * The run status of this Operation. Use {@link #isRunning}.
     * @readonly
     * @private
     */
    running: false,
    /**
     * @property {Boolean} complete
     * The completion status of this Operation. Use {@link #isComplete}.
     * @readonly
     * @private
     */
    complete: false,
    /**
     * @property {Boolean} success
     * Whether the Operation was successful or not. This starts as undefined and is set to true
     * or false by the Proxy that is executing the Operation. It is also set to false by
     * {@link #setException}. Use {@link #wasSuccessful} to query success status.
     * @readonly
     * @private
     */
    success: undefined,
    /**
     * @property {Boolean} exception
     * The exception status of this Operation. Use {@link #hasException} and see {@link #getError}.
     * @readonly
     * @private
     */
    exception: false,
    /**
     * @property {String/Object} error
     * The error object passed when {@link #setException} was called. This could be any object or
     * primitive.
     * @private
     */
    error: undefined,
    idPrefix: 'ext-operation-',
    /**
     * Creates new Operation object.
     * @param {Object} config (optional) Config object.
     */
    constructor: function(config) {
        // This ugliness is here to prevent an issue when specifying scope
        // as an object literal. The object will be pulled in as part of
        // the merge() during initConfig which will change the object 
        // reference. As such, we attempt to fudge it here until we come
        // up with a better solution for describing how specific config
        // objects should behave during init() time.
        var scope = config && config.scope;
        this.initConfig(config);
        if (config) {
            config.scope = scope;
        }
        if (scope) {
            this.setScope(scope);
            this.initialConfig.scope = scope;
        }
        // We need an internal id to track operations in Proxy
        this._internalId = Ext.id(this, this.idPrefix);
    },
    getAction: function() {
        return this.action;
    },
    /**
     * @private
     * Executes the operation on the configured {@link #proxy}.
     * @return {Ext.data.Request} The request object
     */
    execute: function() {
        var me = this,
            request;
        delete me.error;
        delete me.success;
        me.complete = me.exception = false;
        me.setStarted();
        me.request = request = me.doExecute();
        if (request) {
            request.setOperation(me);
        }
        return request;
    },
    doExecute: Ext.emptyFn,
    /**
     * Aborts the processing of this operation on the {@link #proxy}.
     * This is only valid for proxies that make asynchronous requests.
     */
    abort: function() {
        var me = this,
            request = me.request,
            proxy;
        me.aborted = true;
        if (me.running && request) {
            proxy = me.getProxy();
            if (proxy && !proxy.destroyed) {
                proxy.abort(request);
            }
            me.request = null;
        }
        me.running = false;
    },
    process: function(resultSet, request, response, autoComplete) {
        var me = this;
        autoComplete = autoComplete !== false;
        me.setResponse(response);
        me.setResultSet(resultSet);
        if (resultSet.getSuccess()) {
            me.doProcess(resultSet, request, response);
            me.setSuccessful(autoComplete);
        } else if (autoComplete) {
            me.setException(resultSet.getMessage());
        }
    },
    /**
     * @private
     * This private object is used to save on memory allocation. This instance is used to
     * apply server record updates as part of a record commit performed by calling the
     * set() method on the record. See doProcess.
     */
    _commitSetOptions: {
        convert: true,
        commit: true
    },
    /**
     * Process records in the operation after the response is successful and the result
     * set is parsed correctly. The base class implementation of this method is used by
     * "create" and "update" operations to allow the server response to update the client
     * side records.
     * 
     * @param {Ext.data.ResultSet} resultSet The result set
     * @param {Ext.data.Request} request The request
     * @param {Object} response The response
     * @protected
     */
    doProcess: function(resultSet, request, response) {
        var me = this,
            commitSetOptions = me._commitSetOptions,
            clientRecords = me.getRecords(),
            clientLen = clientRecords.length,
            clientIdProperty = clientRecords[0].clientIdProperty,
            serverRecords = resultSet.getRecords(),
            // a data array, not records yet
            serverLen = serverRecords ? serverRecords.length : 0,
            clientMap, serverRecord, clientRecord, i;
        if (serverLen && clientIdProperty) {
            // Linear pass over clientRecords to map them by their idProperty
            clientMap = Ext.Array.toValueMap(clientRecords, 'id');
            // Linear pass over serverRecords to match them by clientIdProperty to the
            // corresponding clientRecord (if one exists).
            for (i = 0; i < serverLen; ++i) {
                serverRecord = serverRecords[i];
                clientRecord = clientMap[serverRecord[clientIdProperty]];
                if (clientRecord) {
                    // Remove this one so we don't commit() on it next
                    delete clientMap[clientRecord.id];
                    // Remove the clientIdProperty value since we don't want to store it
                    delete serverRecord[clientIdProperty];
                    clientRecord.set(serverRecord, commitSetOptions);
                } else // set & commit
                {
                    Ext.log.warn('Ignoring server record: ' + Ext.encode(serverRecord));
                }
            }
            // Linear pass over any remaining client records.
            for (i in clientMap) {
                clientMap[i].commit();
            }
        } else {
            // Either no serverRecords or no clientIdProperty, so index correspondence is
            // all we have to go on. If there is no serverRecord at a given index we just
            // commit() the record.
            for (i = 0; i < clientLen; ++i) {
                clientRecord = clientRecords[i];
                if (serverLen === 0 || !(serverRecord = serverRecords[i])) {
                    // once i > serverLen then serverRecords[i] will be undefined...
                    clientRecord.commit();
                } else {
                    clientRecord.set(serverRecord, commitSetOptions);
                }
            }
        }
    },
    /**
     * Marks the Operation as started.
     */
    setStarted: function() {
        this.started = this.running = true;
    },
    /**
     * Marks the Operation as completed.
     */
    setCompleted: function() {
        var me = this,
            proxy;
        me.complete = true;
        me.running = false;
        if (!me.destroying) {
            me.triggerCallbacks();
        }
        // Operation can be destroyed in callback
        if (me.destroyed) {
            return;
        }
        proxy = me.getProxy();
        // Store and proxy could be destroyed in callbacks
        if (proxy && !proxy.destroyed) {
            proxy.completeOperation(me);
        }
    },
    /**
     * Marks the Operation as successful.
     * @param {Boolean} [complete] `true` to also mark this operation
     * as being complete See {@link #setCompleted}.
     */
    setSuccessful: function(complete) {
        this.success = true;
        if (complete) {
            this.setCompleted();
        }
    },
    /**
     * Marks the Operation as having experienced an exception. Can be supplied with an option
     * error message/object.
     * @param {String/Object} error (optional) error string/object
     */
    setException: function(error) {
        var me = this;
        me.exception = true;
        me.success = me.running = false;
        me.error = error;
        me.setCompleted();
    },
    triggerCallbacks: function() {
        var me = this,
            callback = me.getInternalCallback();
        // Call internal callback first (usually the Store's onProxyLoad method)
        if (callback) {
            callback.call(me.getInternalScope() || me, me);
            // Operation callback can cause it to be destroyed
            if (me.destroyed) {
                return;
            }
            me.setInternalCallback(null);
            me.setInternalScope(null);
        }
        // Call the user's callback as passed to Store's read/write
        callback = me.getCallback();
        if (callback) {
            // Maintain the public API for callback
            callback.call(me.getScope() || me, me.getRecords(), me, me.wasSuccessful());
            if (me.destroyed) {
                return;
            }
            me.setCallback(null);
            me.setScope(null);
        }
    },
    /**
     * Returns true if this Operation encountered an exception (see also {@link #getError})
     * @return {Boolean} True if there was an exception
     */
    hasException: function() {
        return this.exception;
    },
    /**
     * Returns the error string or object that was set using {@link #setException}
     * @return {String/Object} The error object
     */
    getError: function() {
        return this.error;
    },
    /**
     * Returns the {@link Ext.data.Model record}s associated with this operation. For read
     * operations the records as set by the {@link Ext.data.proxy.Proxy Proxy} will be
     * returned (returns `null` if the proxy has not yet set the records).
     *
     * For create, update, and destroy operations the operation's initially configured
     * records will be returned, although the proxy may modify these records' data at some
     * point after the operation is initialized.
     *
     * @return {Ext.data.Model[]}
     */
    getRecords: function() {
        var resultSet;
        /* eslint-disable-next-line no-cond-assign */
        return this._records || ((resultSet = this.getResultSet()) ? resultSet.getRecords() : null);
    },
    /**
     * Returns true if the Operation has been started. Note that the Operation may have started
     * AND completed, see {@link #isRunning} to test if the Operation is currently running.
     * @return {Boolean} True if the Operation has started
     */
    isStarted: function() {
        return this.started;
    },
    /**
     * Returns true if the Operation has been started but has not yet completed.
     * @return {Boolean} True if the Operation is currently running
     */
    isRunning: function() {
        return this.running;
    },
    /**
     * Returns true if the Operation has been completed
     * @return {Boolean} True if the Operation is complete
     */
    isComplete: function() {
        return this.complete;
    },
    /**
     * Returns true if the Operation has completed and was successful
     * @return {Boolean} True if successful
     */
    wasSuccessful: function() {
        return this.isComplete() && this.success === true;
    },
    // success can be undefined
    /**
     * Checks whether this operation should cause writing to occur.
     * @return {Boolean} Whether the operation should cause a write to occur.
     */
    allowWrite: function() {
        return true;
    },
    destroy: function() {
        var me = this;
        me.destroying = true;
        if (me.running) {
            me.abort();
        }
        // Cleanup upon destruction can be turned off
        me._params = me._callback = me._scope = me._resultSet = me._response = null;
        me.request = me._request = me._records = me._proxy = me._batch = null;
        me._recordCreator = me._internalCallback = me._internalScope = null;
        me.callParent();
    }
});

/**
 * Encapsulates a create operation as performed by a {@link Ext.data.proxy.Proxy proxy}.
 *
 * This class is instantiated by {@link Ext.data.Store stores} and {@link Ext.data.Model records}
 * and should not need to be instantiated in user code.
 */
Ext.define('Ext.data.operation.Create', {
    extend: 'Ext.data.operation.Operation',
    alias: 'data.operation.create',
    action: 'create',
    isCreateOperation: true,
    order: 10,
    config: {
        recordCreator: Ext.identityFn
    },
    doExecute: function() {
        return this.getProxy().create(this);
    }
});

/**
 * Encapsulates a destroy operation as performed by a {@link Ext.data.proxy.Proxy proxy}.
 *
 * This class is instantiated by {@link Ext.data.Store stores} and {@link Ext.data.Model records}
 * and should not need to be instantiated in user code.
 */
Ext.define('Ext.data.operation.Destroy', {
    extend: 'Ext.data.operation.Operation',
    alias: 'data.operation.destroy',
    action: 'destroy',
    isDestroyOperation: true,
    order: 30,
    foreignKeyDirection: -1,
    doProcess: function() /* resultSet, request, response */
    {
        var clientRecords = this.getRecords(),
            clientLen = clientRecords.length,
            i;
        for (i = 0; i < clientLen; ++i) {
            clientRecords[i].setErased();
        }
    },
    doExecute: function() {
        return this.getProxy().erase(this);
    },
    getRecordData: function(record, operation) {
        var data = {},
            idField = record.idField,
            nameProperty = this.getNameProperty() || 'name';
        data[idField[nameProperty]] = record.id;
        return data;
    }
});

/**
 * Encapsulates a read operation as performed by a {@link Ext.data.proxy.Proxy proxy}.
 *
 * This class is instantiated by {@link Ext.data.Store stores} and {@link Ext.data.Model records}
 * and should not need to be instantiated in user code.
 */
Ext.define('Ext.data.operation.Read', {
    extend: 'Ext.data.operation.Operation',
    alias: 'data.operation.read',
    action: 'read',
    isReadOperation: true,
    config: {
        /**
         * @cfg {Ext.util.Filter[]} filters
         * Optional array of filter objects. Only applies to 'read' actions.
         */
        filters: undefined,
        /**
         * @cfg {Ext.util.Sorter[]} sorters
         * Optional array of sorter objects. Only applies to 'read' actions.
         */
        sorters: undefined,
        /**
         * @cfg {Ext.util.Grouper} grouper
         * Optional grouping configuration. Only applies to 'read' actions where grouping is
         * desired.
         */
        grouper: undefined,
        /**
         * @cfg {Number} start
         * The start index (offset), used in paging when running a 'read' action.
         */
        start: undefined,
        /**
         * @cfg {Number} limit
         * The number of records to load. Used on 'read' actions when paging is being used.
         */
        limit: undefined,
        /**
         * @cfg {Number} page
         * The page for this operation.
         */
        page: undefined,
        /**
         * @cfg {Boolean} addRecords
         * Passed internally to loadRecords when the load completes
         * @private
         */
        addRecords: false
    },
    doExecute: function() {
        return this.getProxy().read(this);
    },
    doProcess: Ext.emptyFn,
    allowWrite: function() {
        return false;
    }
});

/**
 * Encapsulates a update operation as performed by a {@link Ext.data.proxy.Proxy proxy}.
 *
 * This class is instantiated by {@link Ext.data.Store stores} and {@link Ext.data.Model records}
 * and should not need to be instantiated in user code.
 */
Ext.define('Ext.data.operation.Update', {
    extend: 'Ext.data.operation.Operation',
    alias: 'data.operation.update',
    action: 'update',
    isUpdateOperation: true,
    order: 20,
    config: {
        recordCreator: Ext.identityFn
    },
    doExecute: function() {
        return this.getProxy().update(this);
    }
});

/**
 * This class defines a series of static methods that are used on a
 * {@link Ext.data.Field} for performing sorting. The methods cast the
 * underlying values into a data type that is appropriate for sorting on
 * that particular field.  If a {@link Ext.data.Field#type} is specified,
 * the sortType will be set to a sane default if the sortType is not
 * explicitly defined on the field. The sortType will make any necessary
 * modifications to the value and return it.
 *
 *  - **`asText`** - Removes any tags and converts the value to a string
 *  - **`asUCText`** - Removes any tags and converts the value to an uppercase string
 *  - **`asUCString`** - Converts the value to an uppercase string
 *  - **`asDate`** - Converts the value into Unix epoch time
 *  - **`asFloat`** - Converts the value to a floating point number
 *  - **`asInt`** - Converts the value to an integer number
 *
 * It is also possible to create a custom sortType that can be used throughout
 * an application.
 *
 *      Ext.apply(Ext.data.SortTypes, {
 *          asPerson: function(person){
 *              // expects an object with a first and last name property
 *              return person.lastName.toUpperCase() + person.firstName.toLowerCase();
 *          }
 *      });
 *
 *      Ext.define('Employee', {
 *          extend: 'Ext.data.Model',
 *          fields: [{
 *              name: 'person',
 *              sortType: 'asPerson'
 *          }, {
 *              name: 'salary',
 *              type: 'float' // sortType set to asFloat
 *          }]
 *      });
 *
 * @singleton
 */
Ext.define('Ext.data.SortTypes', function() {
    var me;
    return {
        singleton: true,
        constructor: function() {
            me = this;
        },
        /**
         * Default sort that does nothing
         * @param {Object} s The value being converted
         * @return {Object} The comparison value
         */
        none: Ext.identityFn,
        /**
         * The regular expression used to strip commas
         * @type {RegExp}
         * @property
         */
        stripCommasRe: /,/g,
        /**
         * The regular expression used to strip tags
         * @type {RegExp}
         * @property
         */
        stripTagsRE: /<\/?[^>]+>/gi,
        /**
         * Strips all HTML tags to sort on text only
         * @param {Object} s The value being converted
         * @return {String} The comparison value
         */
        asText: function(s) {
            // If allowNull, return the Unicode null character.
            return (s != null) ? String(s).replace(me.stripTagsRE, '') : '\x00';
        },
        /**
         * Strips all HTML tags to sort on text only - Case insensitive
         * @param {Object} s The value being converted
         * @return {String} The comparison value
         */
        asUCText: function(s) {
            // If allowNull, return the Unicode null character.
            /* eslint-disable-next-line newline-per-chained-call */
            return (s != null) ? String(s).toUpperCase().replace(me.stripTagsRE, '') : '\x00';
        },
        /**
         * Case insensitive string
         * @param {Object} s The value being converted
         * @return {String} The comparison value
         */
        asUCString: function(s) {
            // If allowNull, return the Unicode null character.
            return (s != null) ? String(s).toUpperCase() : '\x00';
        },
        /**
         * Date sorting
         * @param {Object} s The value being converted
         * @return {Number} The comparison value
         */
        asDate: function(s) {
            if (!s) {
                return 0;
            }
            if (Ext.isDate(s)) {
                return s.getTime();
            }
            return Date.parse(String(s));
        },
        /**
         * Float sorting
         * @param {Object} s The value being converted
         * @return {Number} The comparison value
         */
        asFloat: function(s) {
            var val = parseFloat(String(s).replace(me.stripCommasRe, ''));
            return isNaN(val) ? 0 : val;
        },
        /**
         * Integer sorting
         * @param {Object} s The value being converted
         * @return {Number} The comparison value
         */
        asInt: function(s) {
            var val = parseInt(String(s).replace(me.stripCommasRe, ''), 10);
            return isNaN(val) ? 0 : val;
        }
    };
});

/**
 * The base class for validators to be used to validate {@link Ext.data.Field fields} in
 * a {@link Ext.data.Model model}.
 *
 * The model will call the {@link #validate} method, which may be overridden by subclasses.
 */
Ext.define('Ext.data.validator.Validator', {
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    alias: 'data.validator.base',
    // also configures Factoryable
    isValidator: true,
    factoryConfig: {
        cacheable: true
    },
    /**
     * @property {String} type
     * A string representation of this format.
     */
    type: 'base',
    /**
     * Creates new Validator.
     * @param {Object/Function} config A config object. A function may also be passed,
     * which will be used as the {@link #validate} method for this validator.
     */
    constructor: function(config) {
        if (typeof config === 'function') {
            this.fnOnly = true;
            this.validate = config;
        } else {
            this.initConfig(config);
        }
    },
    /**
     * Validates the passed value.
     * @param {Object} value The value
     * @param {Ext.data.Model} record The record
     * @return {Boolean/String} `true` if the value is valid. A string may be returned if the value
     * is not valid, to indicate an error message. Any other non `true` value indicates the value
     * is not valid.
     */
    validate: function() {
        return true;
    },
    /**
     * Creates a copy of this validator
     * @private
     * @return {Ext.data.validator.Validator} The clone
     */
    clone: function() {
        var me = this;
        if (me.fnOnly) {
            return new Ext.data.validator.Validator(me.validate);
        }
        return new me.self(me.getCurrentConfig());
    }
}, function(Validator) {
    Ext.Factory.validator = Ext.Factory.dataValidator;
});

/**
 * Fields are used to define the members of a Model. They aren't instantiated directly;
 * instead, when we create a class that extends {@link Ext.data.Model}, it automatically
 * creates Field instances for each field configured in a {@link Ext.data.Model Model}.
 * For example, we might set up a model like this:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             'name', 'email',
 *             { name: 'age', type: 'int' },
 *             { name: 'gender', type: 'string', defaultValue: 'Unknown' }
 *         ]
 *     });
 *
 * Four fields will have been created for the User Model - name, email, age and gender.
 * Note that we specified a couple of different formats here; if we only pass in the string
 * name of the field (as with name and email), the field is set up with the 'auto' type.
 * It's as if we'd done this instead:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             { name: 'name', type: 'auto' },
 *             { name: 'email', type: 'auto' },
 *             { name: 'age', type: 'int' },
 *             { name: 'gender', type: 'string', defaultValue: 'Unknown' }
 *         ]
 *     });
 *
 * # Field Types
 *
 * Fields come in various types. When declaring a field, the `type` property is used to
 * specify the type of `Field` derived class used to manage values.
 *
 * The predefined set of types are:
 *
 *  - {@link Ext.data.field.Field auto} (Default, implies no conversion)
 *  - {@link Ext.data.field.String string}
 *  - {@link Ext.data.field.Integer int}
 *  - {@link Ext.data.field.Number number}
 *  - {@link Ext.data.field.Boolean boolean}
 *  - {@link Ext.data.field.Date date}
 *
 * # Conversion
 *
 * When reading fields it is often necessary to convert the values received before using
 * them or storing them in records. To handle these cases there is the
 * `{@link #method-convert convert}` method. This method is passed the received value (as
 * well as the current record instance, but see below) and it returns the value to carry
 * forward.
 *
 * For `auto` fields there is no `{@link #method-convert convert}` method. This is for
 * efficiency. For other field types, there are often `convert` methods. You can provide
 * a `{@link #cfg-convert convert}` config when the field is defined like this:
 *
 *      {
 *          name: 'timestamp',
 *
 *          convert: function (value) {
 *              return new Date(value);
 *          }
 *      }
 *
 * While this can be convenient, see below for details on defining Custom Types as that is
 * often a better practice and avoids repeating these functions.
 *
 * Note that when a `defaultValue` is specified, it will also be passed through to
 * `convert` (either to the `{@link #method-convert convert}` method or to the
 * `{@link #cfg-convert convert} config)`.
 *
 * ## Calculated Values
 *
 * In some cases fields are the result of a calculation from other fields. Historically
 * this was a second role for `{@link #method-convert convert}` but that has some short
 * comings. The simpler solution is the `{@link #cfg-calculate calculate}` config.
 *
 * Values produced by `{@link #cfg-calculate calculate}` and `{@link #method-convert convert}`
 * are stored in the record as with any other field. In fact, if we define a calculated
 * "firstName" field and log out all of the data, we'll see this:
 *
 *     var ed = Ext.create('User', { name: 'Ed Spencer' });
 *
 *     console.log(ed.data);
 *
 *     //outputs this:
 *     {
 *         age: 0,
 *         email: "",
 *         firstName: "Ed",  // calculated field
 *         gender: "Unknown",
 *         name: "Ed Spencer"
 *     }
 *
 * ### Using `calculate`
 *
 *      {
 *          name: 'firstName',
 *
 *          calculate: function (data) {
 *              return data.name.split(' ')[0];
 *          }
 *      }
 *
 * Using `{@link #cfg-calculate calculate}` is the simplest and safest way to define a
 * calculated field. The most important part of this is that, internally, the code of the
 * supplied function is parsed to extract its dependencies. In this case, the "name" field
 * is the only dependency. This means that "firstName" will only need to be recalculated
 * when "name" is modified.
 *
 * **Note:** Fields used by the calculate method must be explicitly defined in the
 * {@link Ext.data.Model#cfg-fields #fields} of the model.
 *
 * ### Using `convert`
 *
 * Following is the equivalent technique using `{@link #cfg-convert convert}`
 *
 *      {
 *          name: 'firstName',
 *
 *          convert: function (value, record) {
 *              return record.get('name').split(' ')[0];
 *          },
 *
 *          depends: [ 'name' ]
 *      }
 *
 * When a `{@link #method-convert convert}` function accepts a 2nd argument (a reference to
 * the record), it is considered a calculated field. If a `{@link #cfg-depends depends}`
 * config is not provided then this field's dependencies are unknown. In this case, the
 * `{@link #cfg-depends depends}` are provided as would be automatically determined with
 * the `{@link #cfg-calculate calculate}` config.
 *
 * ### Updating
 *
 * Fields modified with the {@link Ext.data.Model#set set} method will have their stored 
 * value set using the convert / calculate method when present.
 * 
 * For example:
 *
 *     Ext.define('MyApp.model.Employee', {
 *         extend: 'Ext.data.Model',
 *         fields: [{
 *             name: 'salary',
 *             convert: function (val) {
 *                 var startingBonus = val * .1;
 *                 return val + startingBonus;
 *             }
 *         }],
 *         convertOnSet: false
 *     });
 *     
 *     var tina = Ext.create('MyApp.model.Employee', {
 *         salary: 50000
 *     });
 *     
 *     console.log(tina.get('salary')); // logs 55000
 *     
 *     tina.set('salary', 60000);
 *     console.log(tina.get('salary')); // logs 60000
 * 
 * This default behavior can be disabled by setting the Model's 
 * `{@link Ext.data.Model#cfg-convertOnSet}` config to `false`.
 * 
 * **Note:** convertOnSet `false` only prevents the convert / calculate call when the 
 * set `fieldName` param matches the field's `{@link #name}`.  See 
 * {@link Ext.data.Model#convertOnSet convertOnSet} for additional details.
 *
 * ### Dependencies
 *
 * When a field's `{@link #method-convert convert}` method processes values from the record
 * (vs. just the field's value), it is best to also provide a `depends` config as shown
 * above. Fields that provide a `{@link #cfg-calculate calculate}` method must follow the
 * proper form for using fields so that dependencies can be extracted.
 *
 * Calculated fields are processed after other fields based on their dependencies. Fields
 * with `{@link #method-convert convert}` methods that use the provided record that do *not*
 * specify a `{@link #cfg-depends depends}` config are processed as a group after all other
 * fields since such converters can rely on anything in the record. The order of processing
 * these fields with respect to each other is unspecified and should not be relied upon.
 *
 * # Serialization
 *
 * To handle the inverse scenario of `convert` there is the `serialize` method. This
 * method is called to produce the value to send to a server based on the internal value
 * as would be returned from `convert`. In most cases, these methods should "round trip"
 * a value:
 *
 *      assertEqual(value, field.serialize(field.convert(value)));
 *
 * By default, only `{@link Ext.data.field.Date date}` fields have a `serialize` method.
 * Other types simply send their value unmodified.
 *
 * # Custom Types
 *
 * Developers may create their own application-specific data types by deriving from this
 * class. This is typically much better than applying multiple configuration values on
 * field instances as these often become repetitive.
 *
 * To illustrate, we define a "time" field type that stores a time-of-day represented as a
 * number of minutes since Midnight.
 *
 *      Ext.define('App.field.Time', {
 *          extend: 'Ext.data.field.Field',
 *
 *          alias: 'data.field.time',
 *
 *          timeFormat: 'g:i',
 *
 *          convert: function (value) {
 *              if (value && Ext.isString(value)) {
 *                  var date = Ext.Date.parse(value, this.timeFormat);
 *                  if (!date) {
 *                      return null;
 *                  }
 *                  return (date.getHours() - 1) * 60 + date.getMinutes();
 *              }
 *              return value;
 *          }
 *      });
 *
 * ## Validation
 *
 * Custom field types can override the `{@link #method-validate validate}` method or
 * provide a set of `{@link #cfg-validators validators}`.
 *
 *      Ext.define('App.field.PhoneNumber', {
 *          extend: 'Ext.data.field.Field',
 *
 *          alias: 'data.field.phonenumber',
 *
 *          // Match U.S. phone numbers for example purposes
 *          validators: {
 *              type: 'format',
 *              matcher: /\d{3}\-\d{3}\-\d{4}/
 *          }
 *      });
 *
 * Once the class is defined, fields can be declared using the new type (based on its
 * `alias`) like so:
 *
 *      Ext.define('App.model.PhoneCall', {
 *          fields: [
 *              { name: 'startTime', type: 'time' },
 *              { name: 'phoneNumber', type: 'phonenumber' }
 *          ]
 *      });
 */
Ext.define('Ext.data.field.Field', {
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    requires: [
        'Ext.data.SortTypes',
        'Ext.data.validator.Validator'
    ],
    alternateClassName: 'Ext.data.Field',
    alias: 'data.field.auto',
    // also configures Factoryable
    aliasPrefix: 'data.field.',
    type: 'auto',
    /**
     * @property factoryConfig
     * @inheritdoc
     */
    factoryConfig: {
        defaultProperty: 'name'
    },
    isDataField: true,
    isField: true,
    // NOTE: We do not use "config: {}" here because these configs are simple, never really
    // set after creation and expensive enough when processed per-instance that avoiding
    // the overhead is worth while. Remember that a large app may have many dozens of
    // entities in their data model and these may have many fields each. Easily hundreds
    // of Field instances. Using config with inherited things (like convert methods) just
    // pushes the set to the constructor where it needs to just be a normal method.
    /**
     * @cfg {Boolean} allowBlank
     * @private
     *
     * Used for validating a {@link Ext.data.Model model}. Defaults to true. An empty value here
     * will cause {@link Ext.data.Model}.{@link Ext.data.Model#isValid isValid} to evaluate to
     * `false`.
     */
    allowBlank: true,
    /**
     * @cfg {Boolean} allowNull
     *
     * Use when converting received data into a {@link Ext.data.field.Integer `int`},
     * {@link Ext.data.field.Number `float`}, {@link Ext.data.field.Boolean `bool`}
     * or {@link Ext.data.field.String `string`} type. If the value cannot be
     * parsed, `null` will be used if allowNull is true, otherwise a default value for
     * that type will be used:
     *
     * - for `int` and `float` - `0`.
     * - for `string` - `""`.
     * - for `bool` - `false`.
     *
     * Note that when parsing of {@link Ext.data.field.Date `date`} type fails, the value
     * will be `null` regardless of this setting.
     */
    allowNull: false,
    /**
     * @cfg {Function} calculate
     * This config defines a simple field calculation function. A calculate method only
     * has access to the record data and should return the value of the calculated field.
     * When provided in this way, the `depends` config is automatically determined by
     * parsing the `calculate` function. For example:
     *
     *      fields: [{
     *          name: 'firstName',
     *          type: 'string'
     *      },{
     *          name: 'lastName',
     *          type: 'string'
     *      },{
     *          name: 'fullName',
     *          calculate: function (data) {
     *              return data.firstName + ' ' + data.lastName;
     *          }
     *      }]
     *
     * The above 'fullName' field is equivalent to:
     *
     *      {
     *          name: 'fullName',
     *          convert: function (v, rec) {
     *              return rec.get('firstName') + ' ' + rec.get('lastName');
     *          },
     *          depends: ['firstName', 'lastName']
     *      }
     *
     * The restrictions on form for a `calculate` method are that the accesses to field
     * values must match the following regular expression (case insensitive):
     *
     *      data.([a-z_][a-z0-9_]*)
     *      // where 'data' is the param passed to the calculate method
     *
     * The only advantage of a `calculate` method over a `convert` method is automatic
     * determination of `depends`.
     * 
     * **Note:** The use of calculate and {@link #method-convert} are exclusive.  The 
     * calculate method will override the convert method if both are configured.
     * 
     * **Note:** Fields used by the calculate method must be explicitly defined in the
     * {@link Ext.data.Model#cfg-fields #fields} of the model.
     *
     * @param {Object} data An object with all values for each field in the parent 
     * model.  See {@link Ext.data.Model#getData getData}.
     * @return {Mixed} value The value of the calculated field
     */
    /**
     * @cfg {Function} convert
     * If specified this config overrides the `{@link #method-convert convert}` method.
     * See also `{@link #cfg-calculate calculate}` for simple field calculations.
     * 
     * **Note:** The use of {@link #calculate} and convert are exclusive.  The calculate 
     * method will override the convert method if both are configured.
     */
    /**
     * @cfg {Boolean} critical
     * A critical field is a field that must always be sent to the server even if it has
     * not changed. The most common example of such a field is the "id" of a record (see
     * `{@link Ext.data.Model#idProperty}` but the 
     * `{@link Ext.data.Model#versionProperty}` is similarly a `critical` field.
     */
    critical: false,
    /**
     * @property {String} defaultInvalidMessage
     * The default message to present for an invalid field.
     * @since 5.0.0
     */
    defaultInvalidMessage: 'This field is invalid',
    /**
     * @cfg {Object} defaultValue
     *
     * The default value used when the creating an instance from a raw data object,
     * and the property referenced by the `{@link Ext.data.field.Field#mapping mapping}`
     * does not exist in that data object.
     *
     * The value `undefined` prevents defaulting in a value.
     */
    defaultValue: undefined,
    /**
     * @property {Ext.Class} definedBy
     * The class (derived from {@link Ext.data.Model}) that defined this field.
     *
     *      Ext.define('MyApp.models.Foo', {
     *          extend: 'Ext.data.Model',
     *
     *          fields: [
     *              { name: 'bar' }
     *          ],
     *          ...
     *      });
     *
     *      var barField = MyApp.models.Foo.getField('bar');
     *
     *      alert(barField.definedBy === MyApp.models.Foo); // alerts 'true'
     *
     * When a field is inherited, this value will reference the class that originally
     * defined the field.
     *
     *      Ext.define('MyApp.models.Base', {
     *          extend: 'Ext.data.Model',
     *
     *          fields: [
     *              { name: 'foo' }
     *          ],
     *          ...
     *      });
     *
     *      Ext.define('MyApp.models.Derived', {
     *          extend: 'MyApp.models.Base',
     *
     *          fields: [
     *              { name: 'bar' }
     *          ],
     *          ...
     *      });
     *
     *      var fooField = MyApp.models.Derived.getField('foo');
     *
     *      alert(fooField.definedBy === MyApp.models.Base); // alerts 'true'
     */
    definedBy: null,
    /**
     * @cfg {String/String[]} depends
     * The field name or names within the {@link Ext.data.Model Model} on which the value
     * of this field depends, and from which a new value may be calculated. These values
     * are the values used by the `convert` method. If you do not have a `convert` method
     * then this config should not be specified.
     *
     * Before using this config you should consider if using a `calculate` method instead
     * of a `convert` method would be simpler.
     *
     * Whenever any of the named fields are set using the {@link Ext.data.Model#set set}
     * method, this fields will have its `convert` method called passing the
     * {@link Ext.data.Model record} so that the dependent value can be calculated from
     * all fields which it needs.
     *
     * For example, to display a person's full name, using two separate `firstName` and
     * `lastName` fields, configure the name field like this:
     *
     *     {
     *         name: 'name',
     *     
     *         // Will be called whenever forename or surname fields are set
     *         convert: function (v, rec) {
     *             return rec.get('firstName') + ' ' + rec.get('lastName');
     *         },
     *     
     *         depends: [ 'firstName', 'lastName' ],
     *     
     *         // It should not be returned to the server - it's not a database field
     *         persist: false
     *     }
     *
     * Note that if you do not want the calculated field to be part of the field set sent
     * back to the server when the store is synchronized, you should configure the field
     * with `persist` set to `false`.
     */
    depends: null,
    /**
     * @property {Ext.data.field.Field[]} dependents
     * This array tracks the fields that have indicated this field in their `depends`
     * list. If no fields depend on this field, this will be `null`.
     * @readonly
     * @private
     */
    dependents: null,
    /**
     * @cfg {String/Number/Function} mapping
     *
     * (Optional) A path expression for use by the {@link Ext.data.reader.Reader}
     * implementation that is creating the {@link Ext.data.Model Model} to extract the
     * Field value from the data object. If the path expression is the same as the field
     * name, the mapping may be omitted. A function may be passed to do complex data
     * extraction. The examples below are simple just to demonstrate the capability,
     * typically, a function would not be used to extract such simple data.
     *
     * The form of the mapping expression depends on the Reader being used.
     *
     * - {@link Ext.data.reader.Json}
     *
     *   The mapping is a string containing the javascript expression to reference the
     *   data from an element of the data item's 
     *   {@link Ext.data.reader.Json#cfg-rootProperty rootProperty} Array. Defaults to
     *   the field name. If a function is passed, a single argument is received which
     *   contains the raw json object:
     *
     *       // Server returns [{"name": "Foo", "age": 1}, {"name": "Bar", "age": 2}]
     *       mapping: function(data) {
     *           return data.name;
     *       }
     *
     * - {@link Ext.data.reader.Xml}
     *
     *   The mapping is an {@link Ext.DomQuery} path to the data item relative to the DOM
     *   element that represents the {@link Ext.data.reader.Xml#record record}. Defaults
     *   to the field name. If a function is passed, a single argument is received which
     *   contains the record node:
     *
     *       // Server returns:
     *       // <Root>
     *       //     <Person>
     *       //         <Name>Foo</Name>
     *       //         <Age>1</Age>
     *       //     </Person>
     *       //     <Person>
     *       //         <Name>Bar</Name>
     *       //         <Age>2</Age>
     *       //     </Person>
     *       // </Root>
     *       mapping: function(data) {
     *           return data.firstChild.textContent;
     *       }
     *
     * - {@link Ext.data.reader.Array}
     *
     *   The mapping is a number indicating the Array index of the field's value.
     *   Defaults to the field specification's Array position. If a function is passed, a
     *   single argument is received which contains the child array.
     *
     *       // Server returns [["Foo", 1], ["Bar", 2]]
     *       mapping: function(data) {
     *           return data[0];
     *       }
     *
     * If a more complex value extraction strategy is required, then configure the Field
     * with a {@link #cfg-convert} function. This is passed the whole row object, and may
     * interrogate it in whatever way is necessary in order to return the desired data.
     */
    mapping: null,
    /**
     * @cfg {String} name
     *
     * The name by which the field is referenced within the Model. This is referenced by,
     * for example, the `dataIndex` property in column definition objects passed to
     * {@link Ext.grid.property.HeaderContainer}.
     *
     * Note: In the simplest case, if no properties other than `name` are required, a
     * field definition may consist of just a String for the field name.
     */
    name: null,
    /**
     * @property {Number} ordinal
     *
     * The position of this field in the {@link Ext.data.Model} in which it was defined.
     */
    ordinal: undefined,
    /**
     * @cfg {Boolean} persist
     *
     * False to exclude this field from the {@link Ext.data.Model#modified} fields in a
     * record. This will also exclude the field from being written using a
     * {@link Ext.data.writer.Writer}. This option is useful when fields are used to keep
     * state on the client but do not need to be persisted to the server.
     *
     * Defaults to `false` for `calculated` fields and `true` otherwise.
     */
    persist: null,
    /**
     * @cfg {String/Object} reference
     * The {@link Ext.data.Model#entityName name} of the entity referenced by this field.
     * In most databases, this relationship is represented by a "foreign key". That is, a
     * value for such a field matches the value of the 
     * {@link Ext.data.Model#idProperty id} for an entity of this type.
     *
     * For further documentation, see {@link Ext.data.schema.Reference}.
     */
    reference: null,
    /**
     * @cfg serialize
     * @inheritdoc Ext.data.field.Field#method-serialize
     */
    /**
     * @cfg {String/Object/Function} summary
     * The summary type for this field. This is used to calculate a
     * summary value by the {@link Ext.data.Model Model}.
     *
     * - If a string, it should be an alias for one of the Ext.data.summary types.
     * - If an object, a config for one of the Ext.data.summary types.
     * - If a function, it should match the signature for
     * {@link Ext.data.summary.Base#method-calculate calculate}.
     * 
     * @since 6.5.0
     */
    summary: null,
    /**
     * @cfg {String} summaryField
     * A field to use as the basis for calculating a summary. This is used in
     * conjunction with the virtual summary fields. See
     * {@link Ext.data.Model#cfg-summary}.
     *
     * @since 6.5.0
     * @private
     */
    summaryField: '',
    /**
     * @cfg {Function/String} sortType
     *
     * A function which converts a Field's value to a comparable value in order to ensure
     * correct sort ordering.
     *
     * Predefined functions are provided in {@link Ext.data.SortTypes}. A custom sort
     * example:
     *
     *     // current sort     after sort we want
     *     // +-+------+          +-+------+
     *     // |1|First |          |1|First |
     *     // |2|Last  |          |3|Second|
     *     // |3|Second|          |2|Last  |
     *     // +-+------+          +-+------+
     *
     *     sortType: function(value) {
     *        switch (value.toLowerCase()) // native toLowerCase():
     *        {
     *           case 'first': return 1;
     *           case 'second': return 2;
     *           default: return 3;
     *        }
     *     }
     *
     * May also be set to a String value, corresponding to one of the named sort types in
     * {@link Ext.data.SortTypes}.
     */
    /**
     * @cfg {Boolean} [unique=false]
     * `true` if the value of this field is unique amongst all instances. When used with a
     * `reference` this describes a "one-to-one" relationship. It is almost always the
     * case that a `unique` field cannot also be {@link #allowBlank nullable}.
     */
    unique: false,
    /**
     * @cfg {Object[]} validators
     * An array of {@link Ext.data.validator.Validator validators} for this field. These
     * `validators` will only be passed a field value to validate.
     */
    /**
     * @property {Number} rank
     * This is a 1-based value that describes the dependency order of this field. This is
     * initialized to `null` (falsy) so we can cheaply topo-sort the fields of a class.
     * @private
     * @readonly
     */
    rank: null,
    /**
     * @property {RegExp} stripRe
     * A regular expression for stripping non-numeric characters from a numeric value.
     * This should be overridden for localization.
     * @readonly
     * @protected
     */
    stripRe: /[$,%]/g,
    /**
     * @property {Boolean} calculated
     * This property is `true` if this field has a `{@link #cfg-calculate calculate}`
     * method or a `{@link #method-convert convert}` method that operates on the entire
     * record as opposed to just the data value. This property is determined from the
     * `length` of the `{@link #method-convert convert}` function which means this is
     * *not* calculated:
     *
     *      convert: function (value) {
     *          return ...
     *      }
     *
     * While this *is* calculated:
     *
     *      convert: function (value, record) {
     *          return ...
     *      }
     *
     * **NOTE:** It is recommended for such fields to use 
     * `{@link #cfg-calculate calculate}` or explicitly specify the fields used by 
     * `{@link #method-convert convert}` using `{@link #cfg-depends depends}`.
     *
     * @readonly
     */
    calculated: false,
    /**
     * @property {Boolean} evil
     * This flag is set to true for fields that have `convert` methods which take the 2nd
     * argument (the record) and do not specify a `depends` set. Good fields indicate the
     * fields on which they depend (if any).
     * @private
     * @readonly
     */
    evil: false,
    /**
     * @property {Boolean} identifier
     * This property is set to `true` if this is an {@link Ext.data.Model#idProperty id}
     * field.
     * @readonly
     */
    identifier: false,
    onClassExtended: function(cls, data) {
        var sortType = data.sortType,
            proto = cls.prototype,
            superValidators = proto.validators,
            validators = data.validators,
            alias = data.alias;
        if (alias) {
            if (typeof alias !== 'string') {
                alias = alias[0];
            }
            if (alias) {
                proto.type = alias.substr(alias.lastIndexOf('.') + 1);
            }
        }
        if (sortType && Ext.isString(sortType)) {
            proto.sortType = Ext.data.SortTypes[sortType];
        }
        if (validators) {
            // Force validators to be an array
            if (!Ext.isArray(validators)) {
                validators = [
                    validators
                ];
            }
            delete data.validators;
            // Need to join them
            if (superValidators) {
                validators = superValidators.concat(validators);
            }
            proto.validators = validators;
        }
    },
    argumentNamesRe: /^function\s*\(\s*([^,\)\s]+)/,
    // eslint-disable-line no-useless-escape
    calculateRe: /[^.a-z0-9_]([a-z_][a-z_0-9]*)\.([a-z_][a-z_0-9]*)/gi,
    constructor: function(config) {
        var me = this,
            calculateRe = me.calculateRe,
            calculate, calculated, defaultValue, sortType, depends, map, match, dataProp, str, fld, validators;
        // NOTE: In bigger apps we create *lots* of these fellows so we really need to be
        // very lean here.
        if (config) {
            if (Ext.isString(config)) {
                me.name = config;
            } else {
                validators = config.validators;
                if (validators) {
                    delete config.validators;
                    me.instanceValidators = validators;
                }
                Ext.apply(me, config);
            }
        }
        if (!me.allowNull) {
            me.allowNull = !!me.reference;
        }
        calculate = me.calculate;
        depends = me.depends;
        if (calculate) {
            me.convert = me.doCalculate;
            if (!depends) {
                if (!(depends = calculate.$depends)) {
                    map = {};
                    str = Ext.Function.toCode(calculate);
                    // strips comments in debug
                    calculate.$depends = depends = [];
                    match = me.argumentNamesRe.exec(str);
                    dataProp = match ? match[1] : 'data';
                    while ((match = calculateRe.exec(str))) {
                        if (dataProp === match[1] && !map[fld = match[2]]) {
                            map[fld] = 1;
                            depends.push(fld);
                        }
                    }
                }
                me.depends = depends;
            }
        }
        defaultValue = me.defaultValue;
        if (me.convert) {
            me.calculated = calculated = me.convert.length > 1;
            me.evil = calculated && !depends;
        }
        if (me.persist === null) {
            me.persist = !calculate;
        }
        sortType = me.sortType;
        if (!me.sortType) {
            me.sortType = Ext.data.SortTypes.none;
        } else if (Ext.isString(sortType)) {
            me.sortType = Ext.data.SortTypes[sortType];
        }
        if (depends && typeof depends === 'string') {
            me.depends = [
                depends
            ];
        }
        me.cloneDefaultValue = defaultValue !== undefined && (Ext.isDate(defaultValue) || Ext.isArray(defaultValue) || Ext.isObject(defaultValue));
    },
    setModelValidators: function(modelValidators) {
        this._validators = null;
        this.modelValidators = modelValidators;
    },
    constructValidators: function(validators) {
        var all, length, i, item, validator, presence;
        if (validators) {
            if (!(validators instanceof Array)) {
                validators = [
                    validators
                ];
            }
            all = this._validators;
            // we are inside getValidators so this is OK
            for (i = 0 , length = validators.length; i < length; ++i) {
                item = validators[i];
                if (item.fn) {
                    item = item.fn;
                }
                validator = Ext.Factory.dataValidator(item);
                if (!validator.isPresence) {
                    all.push(validator);
                } else {
                    presence = validator;
                }
            }
            if (presence) {
                this.presence = [
                    presence
                ];
            }
        }
    },
    /**
     * Compares two values to retrieve their relative position in sort order, taking into account
     * any {@link #sortType}. Also see {@link #compare}.
     * @param {Object} value1 The first value.
     * @param {Object} value2 The second value.
     * @return {Number} `-1` if `value1` is less than `value2`; `1` if `value1` is greater than
     * `value2`; `0` otherwise.
     */
    collate: function(value1, value2) {
        var me = this,
            lhs = value1,
            rhs = value2;
        if (me.sortType) {
            lhs = me.sortType(lhs);
            rhs = me.sortType(rhs);
        }
        return (lhs === rhs) ? 0 : ((lhs < rhs) ? -1 : 1);
    },
    /**
     * Compares two values to retrieve their relative position in sort order. Also see
     * {@link #collate}.
     * @param {Object} value1 The first value.
     * @param {Object} value2 The second value.
     * @return {Number} `-1` if `value1` is less than `value2`; `1` if `value1` is greater than
     * `value2`; `0` otherwise.
     */
    compare: function(value1, value2) {
        return (value1 === value2) ? 0 : ((value1 < value2) ? -1 : 1);
    },
    /**
     * Tests whether two values are equal based on this field type.
     * This uses the {@link #compare} method to determine equality, so
     * this method should generally not be overridden.
     * @param {Object} value1 The first value.
     * @param {Object} value2 The second value.
     * @return {Boolean} `true` if the values are equal.
     */
    isEqual: function(value1, value2) {
        return this.compare(value1, value2) === 0;
    },
    /**
     * A function which converts the value provided by the Reader into the value that will
     * be stored in the record. This method can be overridden by a derived class or set as
     * a `{@link #cfg-convert convert}` config.
     *
     * If configured as `null`, then no conversion will be applied to the raw data property
     * when this Field is read. This will increase performance. but you must ensure that
     * the data is of the correct type and does not *need* converting.
     *
     * Example of convert functions:
     *
     *     function fullName(v, record){
     *         return record.data.last + ', ' + record.data.first;
     *     }
     *
     *     function location(v, record){
     *         return !record.data.city ? '' : (record.data.city + ', ' + record.data.state);
     *     }
     *
     *     Ext.define('Dude', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'fullname',  convert: fullName},
     *             {name: 'firstname', mapping: 'name.first'},
     *             {name: 'lastname',  mapping: 'name.last'},
     *             {name: 'city', defaultValue: 'unknown'},
     *             'state',
     *             {name: 'location',  convert: location}
     *         ]
     *     });
     *
     *     // create the data store
     *     var store = Ext.create('Ext.data.Store', {
     *         model: 'Dude',
     *         proxy: {
     *             type: 'memory',
     *             reader: {
     *                 type: 'json',
     *                 rootProperty: 'daRoot',
     *                 totalProperty: 'total'
     *             }
     *         }
     *     });
     *
     *     var myData = [
     *         { key: 1,
     *           name: { first: 'Fat',    last:  'Albert' }
     *           // notice no city, state provided in data object
     *         },
     *         { key: 2,
     *           name: { first: 'Barney', last:  'Rubble' },
     *           city: 'Bedrock', state: 'Stoneridge'
     *         },
     *         { key: 3,
     *           name: { first: 'Cliff',  last:  'Claven' },
     *           city: 'Boston',  state: 'MA'
     *         }
     *     ];
     *
     * @method
     * @param {Mixed} value The data value as read by the Reader, if undefined will use
     * the configured `defaultValue`.
     * @param {Ext.data.Model} record The data object containing the Model as read so far
     * by the Reader. Note that the Model may not be fully populated at this point as the
     * fields are read in the order that they are defined.
     * {@link Ext.data.Model#cfg-fields fields} array.
     * @return {Mixed} The converted value for storage in the record.
     */
    convert: null,
    /**
     * A function which converts the Model's value for this Field into a form which can be used
     * by whatever {@link Ext.data.writer.Writer Writer} is being used to sync data with the server.
     *
     * @method
     * @param {Mixed} value The Field's value - the value to be serialized.
     * @param {Ext.data.Model} record The record being serialized.
     * @return {String} The string that represents the Field's value.
     */
    serialize: null,
    /**
     * Validates the passed value for this field.
     *
     * @param {Object} value The value to validate.
     * @param {String} [separator] This string is passed if the caller wants all validation
     * messages concatenated with this string between each. This can be handled as a
     * "falsy" value because concatenating with no separator is seldom desirable.
     * @param {Ext.data.ErrorCollection/Ext.util.Collection/Array} [errors] This parameter is
     * passed if the caller wants all validation results individually added to the collection.
     * @param {Ext.data.Model} record The record being validated
     * @return {Boolean/String} `true` if the value is valid. A string may be returned if
     * the value is not valid, to indicate an error message. Any other non `true` value
     * indicates the value is not valid. This method is not implemented by default,
     * subclasses may override it to provide an implementation.
     *
     * @protected
     * @template
     * @since 5.0.0
     */
    validate: function(value, separator, errors, record) {
        var me = this,
            validators = me.getValidators(),
            result, presence;
        presence = this.presence;
        if (presence && (value == null || value === '')) {
            result = me.validateGroup(presence, value, separator, errors, record);
            if (result !== true) {
                return result;
            }
        }
        return me.validateGroup(validators, value, separator, errors, record);
    },
    validateGroup: function(validators, value, separator, errors, record) {
        var ret = '',
            validator, length, i, result;
        for (i = 0 , length = validators.length; i < length; ++i) {
            validator = validators[i];
            result = validator.validate(value, record);
            if (result !== true) {
                result = result || this.defaultInvalidMessage;
                if (errors) {
                    if (errors.isMixedCollection) {
                        errors.add(this.name, result);
                    } else if (errors.isCollection) {
                        errors.add(result);
                    } else {
                        errors.push(result);
                    }
                    ret = ret || result;
                } else if (separator) {
                    if (ret) {
                        ret += separator;
                    }
                    ret += result;
                } else {
                    ret = result;
                    break;
                }
            }
        }
        return ret || true;
    },
    doCalculate: function(v, rec) {
        return rec ? this.calculate(rec.data) : v;
    },
    /**
     * Gets the name for this field. See {@link #name}.
     * @return {String} name
     */
    getName: function() {
        return this.name;
    },
    /**
     * Gets allowBlank for this field. See {@link #allowBlank}.
     * @return {Boolean} allowBlank
     */
    getAllowBlank: function() {
        return this.allowBlank;
    },
    /**
     * Gets allowNull for this field. See {@link #allowNull}.
     * @return {Boolean} allowNull
     */
    getAllowNull: function() {
        return this.allowNull;
    },
    /**
     * Gets converter for this field. See {@link #method-convert}.
     * @return {Function} convert
     */
    getConvert: function() {
        return this.convert;
    },
    /**
     * Gets the defaultValue for this field. See {@link #defaultValue}.
     * @return {Object} defaultValue
     */
    getDefaultValue: function() {
        return this.defaultValue;
    },
    /**
     * Gets the depends for this field. See {@link #depends}.
     * @return {String[]} depends
     */
    getDepends: function() {
        return this.depends;
    },
    /**
     * Get the mapping for this field. See {@link #mapping}.
     * @return {Object} mapping
     */
    getMapping: function() {
        return this.mapping;
    },
    /**
     * Checks if this field has a mapping applied.
     * @return {Boolean} `true` if this field has a mapping.
     */
    hasMapping: function() {
        var map = this.mapping;
        return !!(map || map === 0);
    },
    /**
     * Gets the persist for this field. See {@link #persist}.
     * @return {Boolean} persist
     */
    getPersist: function() {
        return this.persist;
    },
    /**
     * Gets the sortType for this field. See {@link #sortType}.
     * @return {Function} sortType
     */
    getSortType: function() {
        return this.sortType;
    },
    /**
     * Gets the summary for this field. See {@link #summary}.
     * @return {Ext.data.summary.Base} The summary.
     */
    getSummary: function() {
        var me = this,
            doneSummary = me.doneSummary,
            summary = me.summary;
        if (!doneSummary) {
            me.doneSummary = true;
            if (summary) {
                me.summary = summary = Ext.Factory.dataSummary(summary);
            }
        }
        return summary || null;
    },
    /**
     * Gets a string representation of the type of this field.
     * @return {String} type
     */
    getType: function() {
        return 'auto';
    },
    privates: {
        getValidators: function() {
            var me = this,
                validators = me._validators;
            if (!validators) {
                me._validators = validators = [];
                me.constructValidators(me.validators);
                me.constructValidators(me.modelValidators);
                me.constructValidators(me.instanceValidators);
            }
            return validators;
        }
    },
    deprecated: {
        5.1: {
            methods: {
                /**
                 * @method getSortDir
                 * Gets the sortDir for this field.
                 * @return {String} sortDir
                 * @deprecated 5.1 Setting sortDir and calling getSortDir were never applied by the
                 * the Sorter.  This functionality does not natively exist on field instances.
                 */
                getSortDir: function() {
                    return this.sortDir;
                }
            }
        }
    }
});

/**
 * A data field that automatically {@link #convert converts} its value to a boolean.
 *
 *     @example
 *     Ext.define('Task', {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             { name: 'completed', type: 'boolean' }
 *         ]
 *     });
 *
 *     var record = Ext.create('Task', { completed: true }),
 *         value = record.get('completed');
 *
 *     Ext.toast("completed is " + value);
 */
Ext.define('Ext.data.field.Boolean', {
    extend: 'Ext.data.field.Field',
    alias: [
        'data.field.bool',
        'data.field.boolean'
    ],
    isBooleanField: true,
    /**
     * @property [trueRe]
     * Values matching this regular expression are considered `true`.
     */
    trueRe: /^\s*(?:true|yes|on|1)\s*$/i,
    convert: function(v) {
        if (typeof v === 'boolean') {
            return v;
        }
        if (this.allowNull && (v === undefined || v === null || v === '')) {
            return null;
        }
        return this.trueRe.test(String(v));
    },
    getType: function() {
        return 'bool';
    }
});

/**
 * A data field that automatically {@link #convert converts} its value to a date.
 *
 *     @example
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             { name: 'birthDate', type: 'date' }
 *         ]
 *     });
 *
 *     var record = Ext.create('User', { birthDate: 'Tuesday, June 27, 2017' }),
 *         value = record.get('birthDate');
 *
 *     Ext.toast("birthDate is " + value);
 */
Ext.define('Ext.data.field.Date', {
    extend: 'Ext.data.field.Field',
    alias: 'data.field.date',
    sortType: 'asDate',
    isDateField: true,
    /**
     * @cfg {String} dateFormat
     *
     * Serves as a default for the {@link #dateReadFormat} and {@link #dateWriteFormat} config
     * options. This will be used in place of those other configurations if not specified.
     * 
     * A format string for the {@link Ext.Date#parse Ext.Date.parse} function, or "timestamp"
     * if the value provided by the Reader is a UNIX timestamp, or "time" if the value provided by
     * the Reader is a javascript millisecond timestamp. See {@link Ext.Date}.
     * 
     * It is quite important to note that while this config is optional, it will default to using
     * the base JavaScript Date object's `parse` function if not specified, rather than
     * {@link Ext.Date#parse Ext.Date.parse}. This can cause unexpected issues, especially when
     * converting between timezones, or when converting dates that do not have a timezone specified.
     * The behavior of the native `Date.parse` is implementation-specific, and depending on the
     * value of the date string, it might return the UTC date or the local date. __For this reason
     * it is strongly recommended that you always specify an explicit date format when parsing
     * dates.__
     */
    dateFormat: null,
    /**
     * @cfg {String} dateReadFormat
     * Used when converting received data into a Date when the {@link #type} is specified as
     * `"date"`. This configuration takes precedence over {@link #dateFormat}.
     * See {@link #dateFormat} for more information.
     */
    dateReadFormat: null,
    /** 
     * @cfg {String} dateWriteFormat
     * Provides a custom format when serializing dates with a {@link Ext.data.writer.Writer}.
     * If this is not specified, the {@link #dateFormat} will be used. If no `dateFormat`
     * is specified, 'timestamp' format is used.
     *
     * See the {@link Ext.data.writer.Writer} docs for more information on writing dates.
     *
     * **Note** It is not possible to use the standard date serialization pathway or
     * {@link Ext#USE_NATIVE_JSON native browser JSON production} to use a
     * {@link Ext.data.JsonWriter JsonWriter} to send Microsoft formated "JSON" dates.
     *
     * To use a {@link Ext.data.JsonWriter JsonWriter} to write dates in a JSON packet in
     * the form `"\/Date(1357372800000)\/"` configure the field like this:
     *
     *     {
     *         type: 'date',
     *         dateFormat: 'MS',     // To parse incoming dates from server correctly
     *         serialize: null       // Avoid formatting or conversion by the Writer
     *     }
     *
     * Then override the `Ext.JSON` date serialize function:
     *
     *     Ext.JSON.encodeDate = function (d) {
     *         return '"' + Ext.Date.format(d, 'MS') + '"';
     *     };
     */
    dateWriteFormat: null,
    /**
     * @cfg {Boolean} useStrict
     * @since 6.2.0
     * Used to manually set strict date parsing on a per-field basis. If no `useStrict`
     * is specified, will use value of {@link Ext.Date#useStrict} to determine how to
     * process dates.
     */
    compare: function(lhs, rhs) {
        var lhsIsDate = lhs instanceof Date,
            rhsIsDate = rhs instanceof Date,
            result;
        if (rhsIsDate && lhsIsDate) {
            result = lhs.getTime() - rhs.getTime();
            if (result === 0) {
                result = 0;
            } else {
                result = result < 0 ? -1 : 1;
            }
        } else if (lhsIsDate === rhsIsDate) {
            result = 0;
        } else {
            result = lhsIsDate ? 1 : -1;
        }
        return result;
    },
    convert: function(v) {
        if (!v) {
            return null;
        }
        // instanceof check ~10 times faster than Ext.isDate. Values here will not be
        // cross-document objects
        if (v instanceof Date) {
            return v;
        }
        /* eslint-disable-next-line vars-on-top */
        var dateFormat = this.dateReadFormat || this.dateFormat,
            parsed;
        if (dateFormat) {
            return Ext.Date.parse(v, dateFormat, this.useStrict);
        }
        parsed = Date.parse(v);
        return parsed ? new Date(parsed) : null;
    },
    serialize: function(value) {
        var result = null,
            format;
        if (Ext.isDate(value)) {
            format = this.getDateWriteFormat();
            result = format ? Ext.Date.format(value, format) : value;
        }
        return result;
    },
    /**
     * Gets the dateFormat for this field. See {@link #dateFormat}.
     * @return {String} dateFormat
     */
    getDateFormat: function() {
        return this.dateFormat;
    },
    /**
     * Gets the dateReadFormat for this field. See {@link #dateReadFormat}.
     * @return {String} dateReadFormat
     */
    getDateReadFormat: function() {
        return this.dateReadFormat;
    },
    /**
     * Gets the dateWriteFormat for this field. See {@link #dateWriteFormat}.
     * @return {String} dateWriteFormat
     */
    getDateWriteFormat: function() {
        var me = this;
        if (me.hasOwnProperty('dateWriteFormat')) {
            return me.dateWriteFormat;
        }
        if (me.hasOwnProperty('dateFormat')) {
            return me.dateFormat;
        }
        return me.dateWriteFormat || me.dateFormat || 'timestamp';
    },
    getType: function() {
        return 'date';
    }
});

/**
 * A data field that automatically {@link #convert converts} its value to an integer.
 *
 * **Note:** As you can see in the example below, casting data as an integer
 * can result in a loss of precision. (5.1 is converted to 5).
 *
 *     @example
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             { name: 'age', type: 'integer' }
 *         ]
 *     });
 *
 *     var record = Ext.create('User', { age: "5.1" }),
 *         value = record.get('age');
 *
 *     Ext.toast("age is " + value);
 */
Ext.define('Ext.data.field.Integer', {
    extend: 'Ext.data.field.Field',
    alias: [
        'data.field.int',
        'data.field.integer'
    ],
    isNumeric: true,
    isIntegerField: true,
    numericType: 'int',
    convert: function(v) {
        // Handle values which are already numbers.
        // Value truncation behaviour of parseInt is historic and must be maintained.
        // parseInt(35.9)  and parseInt("35.9") returns 35
        if (typeof v === 'number') {
            return this.getNumber(v);
        }
        /* eslint-disable-next-line vars-on-top */
        var empty = v == null || v === '',
            allowNull = this.allowNull,
            out;
        if (empty) {
            out = allowNull ? null : 0;
        } else {
            out = this.parse(v);
            if (allowNull && isNaN(out)) {
                out = null;
            }
        }
        return out;
    },
    getNumber: function(v) {
        return parseInt(v, 10);
    },
    getType: function() {
        return this.numericType;
    },
    parse: function(v) {
        return parseInt(String(v).replace(this.stripRe, ''), 10);
    },
    sortType: function(s) {
        // If allowNull, null values needed to be sorted last.
        if (s == null) {
            s = Infinity;
        }
        return s;
    }
});

/**
 * A data field that automatically {@link #convert converts} its value to a floating-point
 * number.
 *
 *     @example
 *     Ext.define('Product', {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             { name: 'price', type: 'number' }
 *         ]
 *     });
 *
 *     var record = Ext.create('Product', { price: "5.1" }),
 *         value = record.get('price');
 *
 *     Ext.toast("price is " + value);
 */
Ext.define('Ext.data.field.Number', {
    extend: 'Ext.data.field.Integer',
    alias: [
        'data.field.float',
        'data.field.number'
    ],
    isIntegerField: false,
    isNumberField: true,
    numericType: 'float',
    getNumber: Ext.identityFn,
    parse: function(v) {
        return parseFloat(String(v).replace(this.stripRe, ''));
    }
});

/**
 * A data field that automatically {@link #convert converts} its value to a string.
 *
 *     @example
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             { name: 'firstName', type: 'string' }
 *         ]
 *     });
 *
 *     var record = Ext.create('User', { firstName: "Phil" }),
 *         value = record.get('firstName');
 *
 *     Ext.toast("firstName is " + value);
 */
Ext.define('Ext.data.field.String', {
    extend: 'Ext.data.field.Field',
    alias: 'data.field.string',
    sortType: 'asUCString',
    isStringField: true,
    convert: function(v) {
        var defaultValue = this.allowNull ? null : '';
        return (v === undefined || v === null) ? defaultValue : String(v);
    },
    getType: function() {
        return 'string';
    }
});

/**
 * This class is a base for all id generators. It also provides lookup of id generators by
 * their id.
 * 
 * Generally, id generators are used to generate a primary key for new model instances. There
 * are different approaches to solving this problem, so this mechanism has both simple use
 * cases and is open to custom implementations. A {@link Ext.data.Model} requests id generation
 * using the {@link Ext.data.Model#identifier} property.
 *
 * The following types of `identifiers` are provided:
 *
 *   * `{@link Ext.data.identifier.Sequential sequential}`
 *   * `{@link Ext.data.identifier.Negative negative}`
 *   * `{@link Ext.data.identifier.Uuid uuid}`
 *
 * In most cases (other than `uuid`), the server is the only party that can generate
 * authoritative id values. This means that any id generated by an `identifier` should be
 * consider "provisional" and must eventually be reconciled with the server. This makes a
 * `uuid` very attractive as an `identifier` because they are designed to be generated in
 * a distributed manner and therefore never require reconciliation.
 *
 * It is common for id values to be generated as increasing integer values (1, 2, etc.) by
 * the server when records are inserted. A `{@link Ext.data.identifier.Negative negative}`
 * `identifier` may be useful as it generates client-side values of -1, -2, etc.. These
 * values are of the same data type (integer) and so can typically be read by servers
 * using typed languages (such as Java or C#) and easily recognized as provisional.
 *
 * In the end, the choice of `identifier` strategy requires agreement between client and
 * server.
 *
 * # Identity, Type and Shared Generators
 *
 * It is often desirable to share Generators to ensure uniqueness or common configuration.
 * This is done by giving Generator instances an id property by which they can be looked
 * up using the {@link Ext.Factory#dataIdentifier dataIdentifier} method. To configure two
 * {@link Ext.data.Model Model} classes to share one
 * {@link Ext.data.identifier.Sequential sequential} id generator, you simply assign them
 * the same id:
 *
 *     Ext.define('MyApp.data.MyModelA', {
 *         extend: 'Ext.data.Model',
 *         identifier: {
 *             type: 'sequential',
 *             id: 'foo'
 *         }
 *     });
 *
 *     Ext.define('MyApp.data.MyModelB', {
 *         extend: 'Ext.data.Model',
 *         identifier: {
 *             type: 'sequential',
 *             id: 'foo'
 *         }
 *     });
 *
 * To make this as simple as possible for generator types that are shared by many (or all)
 * Models, the Generator types (such as 'sequential' or 'uuid') are also reserved as
 * generator ids. This is used by the {@link Ext.data.identifier.Uuid} which has an id equal
 * to its type ('uuid'). In other words, the following Models share the same generator:
 *
 *     Ext.define('MyApp.data.MyModelX', {
 *         extend: 'Ext.data.Model',
 *         identifier: 'uuid'
 *     });
 *
 *     Ext.define('MyApp.data.MyModelY', {
 *         extend: 'Ext.data.Model',
 *         identifier: 'uuid'
 *     });
 *
 * This can be overridden (by specifying the id explicitly), but there is no particularly
 * good reason to do so for this generator type.
 *
 * # Creating Custom Generators
 * 
 * An id generator should derive from this class and implement the {@link #generate} method.
 *
 * To register an id generator type, a derived class should provide an `alias` like so:
 *
 *     Ext.define('MyApp.data.identifier.Custom', {
 *         extend: 'Ext.data.identifier.Generator',
 *         alias: 'data.identifier.custom',
 *         config: {
 *             configProp: 42 // some config property w/default value
 *         }
 *
 *         generate: function () {
 *             return ... // a new id
 *         }
 *     });
 *
 * Using the custom id generator is then straightforward:
 *
 *     Ext.define('MyApp.data.MyModel', {
 *         extend: 'Ext.data.Model',
 *         identifier: 'custom'
 *     });
 *     // or...
 *
 *     Ext.define('MyApp.data.MyModel', {
 *         extend: 'Ext.data.Model',
 *         identifier: {
 *             type: 'custom',
 *             configProp: value
 *         }
 *     });
 *
 * It is not recommended to mix shared generators with generator configuration. This leads
 * to unpredictable results unless all configurations match (which is also redundant). In
 * such cases, a custom generator with a default id is the best approach.
 *
 *     Ext.define('MyApp.data.identifier.Custom', {
 *         extend: 'Ext.data.identifier.Sequential',
 *         alias: 'data.identifier.custom',
 *         
 *         config: {
 *             id: 'custom',
 *             prefix: 'ID_',
 *             seed: 1000
 *         }
 *     });
 *
 *     Ext.define('MyApp.data.MyModelX', {
 *         extend: 'Ext.data.Model',
 *         identifier: 'custom'
 *     });
 *
 *     Ext.define('MyApp.data.MyModelY', {
 *         extend: 'Ext.data.Model',
 *         identifier: 'custom'
 *     });
 *
 *     // the above models share a generator that produces ID_1000, ID_1001, etc..
 *
 */
Ext.define('Ext.data.identifier.Generator', {
    'abstract': true,
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    alias: 'data.identifier.default',
    // this is used by Factoryable
    factoryConfig: {
        defaultType: 'sequential'
    },
    // this is not a suitable type to create
    /**
     * @property {Boolean} isGenerator
     * `true` in this class to identify an object as an instantiated IdGenerator, or subclass
     * thereof.
     */
    isGenerator: true,
    config: {
        /**
         * @cfg {String} id
         * The id for this generator.
         */
        id: null
    },
    /**
     * Initializes a new instance.
     * @param {Object} config (optional) Configuration object to be applied to the new instance.
     */
    constructor: function(config) {
        var me = this,
            id;
        me.initConfig(config);
        id = me.getId();
        if (id) {
            Ext.data.identifier.Generator.all[id] = me;
        }
    },
    /**
     * Generates and returns the next id. This method must be implemented by the derived
     * class.
     *
     * @return {Number/String} The next id.
     * @method generate
     * @abstract
     */
    privates: {
        /**
         * Create a copy of this identifier.
         * @private
         * @return {Ext.data.identifier.Generator} The clone
         */
        clone: function(config) {
            var cfg = this.getInitialConfig();
            cfg = config ? Ext.apply({}, config, cfg) : cfg;
            return new this.self(cfg);
        },
        statics: {
            /**
             * @property {Object} all
             * This object is keyed by id to lookup instances.
             * @private
             * @static
             */
            all: {}
        }
    }
}, function() {
    var Generator = this,
        Factory = Ext.Factory,
        factory = Factory.dataIdentifier;
    // If there is an id property passed we need to lookup that id in the cache. If that
    // produces a cache miss, call the normal factory.
    /**
     * @member Ext.Factory
     * @method dataIdentifier
     * Returns an instance of an ID generator based on the ID you pass in.
     * @param {String/Object} config The config object or `id` to lookup.
     * @return {Object} Ext.data.identifier.* The data identifier
     */
    Factory.dataIdentifier = function(config) {
        var id = Ext.isString(config) ? config : (config && config.id),
            existing = id && Generator.all[id];
        return existing || factory(config);
    };
});

/**
 * This class is a sequential id generator. A simple use of this class would be like so:
 *
 *     Ext.define('MyApp.data.MyModel', {
 *         extend: 'Ext.data.Model',
 *         identifier: 'sequential'
 *     });
 *     // assign id's of 1, 2, 3, etc.
 *
 * An example of a configured generator would be:
 *
 *     Ext.define('MyApp.data.MyModel', {
 *         extend: 'Ext.data.Model',
 *         identifier: {
 *             type: 'sequential',
 *             prefix: 'ID_',
 *             seed: 1000,
 *             increment: 10
 *         }
 *     });
 *     // assign id's of ID_1000, ID_1010, ID_1020, etc.
 *
 */
Ext.define('Ext.data.identifier.Sequential', {
    extend: 'Ext.data.identifier.Generator',
    alias: 'data.identifier.sequential',
    config: {
        /**
         * @cfg {Number} increment
         * The number by which to adjust the `seed` after for the next sequential id.
         */
        increment: 1,
        /**
         * @cfg {String} prefix
         * The string to place in front of the sequential number for each generated id.
         */
        prefix: null,
        /**
         * @cfg {Number} seed
         * The number at which to start generating sequential id's.
         */
        seed: 1
    },
    /**
     * Generates and returns the next id.
     * @return {String/Number} The next id. If a {@link #prefix} was specified, returns
     * a String, otherwise returns a Number.
     */
    generate: function() {
        var me = this,
            seed = me._seed,
            prefix = me._prefix;
        me._seed += me._increment;
        return (prefix !== null) ? prefix + seed : seed;
    }
});

/**
 * A Model or Entity represents some object that your application manages. For example, one
 * might define a Model for Users, Products, Cars, or other real-world object that we want
 * to model in the system. Models are used by {@link Ext.data.Store stores}, which are in
 * turn used by many of the data-bound components in Ext.
 *
 * # Fields
 *
 * Models are defined as a set of fields and any arbitrary methods and properties relevant
 * to the model. For example:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             {name: 'name',  type: 'string'},
 *             {name: 'age',   type: 'int', convert: null},
 *             {name: 'phone', type: 'string'},
 *             {name: 'alive', type: 'boolean', defaultValue: true, convert: null}
 *         ],
 *
 *         changeName: function() {
 *             var oldName = this.get('name'),
 *                 newName = oldName + " The Barbarian";
 *
 *             this.set('name', newName);
 *         }
 *     });
 *
 * Now we can create instances of our User model and call any model logic we defined:
 *
 *     var user = Ext.create('User', {
 *         id   : 'ABCD12345',
 *         name : 'Conan',
 *         age  : 24,
 *         phone: '555-555-5555'
 *     });
 *
 *     user.changeName();
 *     user.get('name'); //returns "Conan The Barbarian"
 *
 * By default, the built in field types such as number and boolean coerce string values
 * in the raw data by virtue of their {@link Ext.data.field.Field#method-convert} method.
 * When the server can be relied upon to send data in a format that does not need to be
 * converted, disabling this can improve performance. The {@link Ext.data.reader.Json Json}
 * and {@link Ext.data.reader.Array Array} readers are likely candidates for this
 * optimization. To disable field conversions you simply specify `null` for the field's
 * {@link Ext.data.field.Field#cfg-convert convert config}.
 *
 * ## The "id" Field and `idProperty`
 *
 * A Model definition always has an *identifying field* which should yield a unique key
 * for each instance. By default, a field named "id" will be created with a
 * {@link Ext.data.Field#mapping mapping} of "id". This happens because of the default
 * {@link #idProperty} provided in Model definitions.
 *
 * To alter which field is the identifying field, use the {@link #idProperty} config.
 *
 * # Validators
 *
 * Models have built-in support for field validators. Validators are added to models as in
 * the follow example:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             { name: 'name',     type: 'string' },
 *             { name: 'age',      type: 'int' },
 *             { name: 'phone',    type: 'string' },
 *             { name: 'gender',   type: 'string' },
 *             { name: 'username', type: 'string' },
 *             { name: 'alive',    type: 'boolean', defaultValue: true }
 *         ],
 *
 *         validators: {
 *             age: 'presence',
 *             name: { type: 'length', min: 2 },
 *             gender: { type: 'inclusion', list: ['Male', 'Female'] },
 *             username: [
 *                 { type: 'exclusion', list: ['Admin', 'Operator'] },
 *                 { type: 'format', matcher: /([a-z]+)[0-9]{2,3}/i }
 *             ]
 *         }
 *     });
 *
 * The derived type of `Ext.data.field.Field` can also provide validation. If `validators`
 * need to be duplicated on multiple fields, instead consider creating a custom field type.
 *
 * ## Validation
 *
 * The results of the validators can be retrieved via the "associated" validation record:
 *
 *     var instance = Ext.create('User', {
 *         name: 'Ed',
 *         gender: 'Male',
 *         username: 'edspencer'
 *     });
 *
 *     var validation = instance.getValidation();
 *
 * The returned object is an instance of `Ext.data.Validation` and has as its fields the
 * result of the field `validators`. The validation object is "dirty" if there are one or
 * more validation errors present.
 *
 * This record is also available when using data binding as a "pseudo-association" called
 * "validation". This pseudo-association can be hidden by an explicitly declared
 * association by the same name (for compatibility reasons), but doing so is not
 * recommended.
 *
 * The `{@link Ext.Component#modelValidation}` config can be used to enable automatic
 * binding from the "validation" of a record to the form fields that may be bound to its
 * values.
 *
 * # Associations
 * 
 * Models often have associations with other Models. These associations can be defined by
 * fields (often called "foreign keys") or by other data such as a many-to-many (or "matrix").
 * See {@link Ext.data.schema.Association} for information about configuring and using associations.
 *
 * # Using a Proxy
 *
 * Models are great for representing types of data and relationships, but sooner or later we're
 * going to want to load or save that data somewhere. All loading and saving of data is handled
 * via a {@link Ext.data.proxy.Proxy Proxy}, which can be set directly on the Model:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name', 'email'],
 *
 *         proxy: {
 *             type: 'rest',
 *             url : '/users'
 *         }
 *     });
 *
 * Here we've set up a {@link Ext.data.proxy.Rest Rest Proxy}, which knows how to load and save
 * data to and from a RESTful backend. Let's see how this works:
 *
 *     var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'});
 *
 *     user.save(); //POST /users
 *
 * Calling {@link #save} on the new Model instance tells the configured RestProxy that we wish to
 * persist this Model's data onto our server. RestProxy figures out that this Model hasn't been
 * saved before because it doesn't have an id, and performs the appropriate action - in this case
 * issuing a POST request to the url we configured (/users). We configure any Proxy on any Model
 * and always follow this API - see {@link Ext.data.proxy.Proxy} for a full list.
 *
 * Loading data via the Proxy is accomplished with the static `load` method:
 *
 *     // Uses the configured RestProxy to make a GET request to /users/123
 *     User.load(123, {
 *         success: function(user) {
 *             console.log(user.getId()); //logs 123
 *         }
 *     });
 *
 * Models can also be updated and destroyed easily:
 *
 *     // the user Model we loaded in the last snippet:
 *     user.set('name', 'Edward Spencer');
 *
 *     // tells the Proxy to save the Model. In this case it will perform a PUT request
 *     // to /users/123 as this Model already has an id
 *     user.save({
 *         success: function() {
 *             console.log('The User was updated');
 *         }
 *     });
 *
 *     // tells the Proxy to destroy the Model. Performs a DELETE request to /users/123
 *     user.erase({
 *         success: function() {
 *             console.log('The User was destroyed!');
 *         }
 *     });
 * 
 * # HTTP Parameter names when using a {@link Ext.data.proxy.Ajax Ajax proxy}
 *
 * By default, the model ID is specified in an HTTP parameter named `id`. To change the
 * name of this parameter use the Proxy's {@link Ext.data.proxy.Ajax#idParam idParam}
 * configuration.
 *
 * Parameters for other commonly passed values such as
 * {@link Ext.data.proxy.Ajax#pageParam page number} or
 * {@link Ext.data.proxy.Ajax#startParam start row} may also be configured.
 *
 * # Usage in Stores
 *
 * It is very common to want to load a set of Model instances to be displayed and manipulated
 * in the UI. We do this by creating a {@link Ext.data.Store Store}:
 *
 *     var store = Ext.create('Ext.data.Store', {
 *         model: 'User'
 *     });
 *
 *     //uses the Proxy we set up on Model to load the Store data
 *     store.load();
 *
 * A Store is just a collection of Model instances - usually loaded from a server somewhere. Store
 * can also maintain a set of added, updated and removed Model instances to be synchronized with
 * the server via the Proxy. See the {@link Ext.data.Store Store docs} for more information
 * on Stores.
 */
Ext.define('Ext.data.Model', {
    alternateClassName: 'Ext.data.Record',
    requires: [
        'Ext.data.ErrorCollection',
        'Ext.data.operation.*',
        'Ext.data.field.*',
        'Ext.data.validator.Validator',
        'Ext.data.schema.Schema',
        'Ext.data.identifier.Generator',
        'Ext.data.identifier.Sequential'
    ],
    uses: [
        'Ext.data.Validation'
    ],
    /**
     * @property {Boolean} isEntity
     * The value `true` to identify this class and its subclasses.
     * @readonly
     */
    isEntity: true,
    /**
     * @property {Boolean} isModel
     * The value `true` to identify this class and its subclasses.
     * @readonly
     */
    isModel: true,
    // Record ids are more flexible.
    validIdRe: null,
    erasing: false,
    loadOperation: null,
    loadCount: 0,
    observableType: 'record',
    /**
     * @property {"C"/"R"/"U"/"D"} crudState
     * This value is initially "R" or "C" indicating the initial CRUD state. As the
     * record changes and the various joined parties (stores, sessions, etc.) are notified
     * this value is updated prior to these calls. In other words, the joined parties
     * are notified after the `crudState` is updated. This means that the `crudState`
     * property may be briefly out of sync with underlying changes if this state is used
     * outside of these notifications.
     *
     * The possible states have these meanings:
     *
     *  * "R" - The record is in a cleanly retrieved (unmodified) state.
     *  * "C" - The record is in a newly created (`phantom`) state.
     *  * "U" - The record is in an updated, `modified` (`dirty`) state.
     *  * "D" - The record is in a `dropped` state.
     *
     * @readonly
     * @protected
     * @since 6.2.0
     */
    crudState: 'R',
    /**
     * @property {"C"/"R"/"U"/"D"} crudStateWas
     * This value is initially `null` indicating there is no previous CRUD state. As the
     * record changes and the various joined parties (stores, sessions, etc.) are notified
     * this value is updated for the *subsequent* calls. In other words, the joined parties
     * are notified and then `crudStateWas` is modified for the next update.
     *
     * The value of this property has the same meaning as `crudState`.
     *
     * @readonly
     * @protected
     * @since 6.2.0
     */
    crudStateWas: null,
    constructor: function(data, session, skipStoreAddition) {
        var me = this,
            cls = me.self,
            identifier = cls.identifier,
            Model = Ext.data.Model,
            modelIdentifier = Model.identifier,
            idProperty = me.idField.name,
            array, id, initializeFn, internalId, len, i, fields;
        // Yes, this is here on purpose. See EXTJS-16494. The second
        // assignment seems to work around a strange JIT issue that prevents
        // this.data being assigned in random scenarios, even though the data
        // is passed into the constructor. The issue occurs on 4th gen iPads and
        // lower, possibly other older iOS devices.
        // A similar issue can occur with the hasListeners property of Observable
        // (see the constructor of Ext.mixin.Observable)
        me.data = me.data = data || (data = {});
        me.internalId = internalId = modelIdentifier.generate();
        var dataId = data[idProperty];
        // eslint-disable-line vars-on-top, one-var
        if (session && !session.isSession) {
            Ext.raise('Bad Model constructor argument 2 - "session" is not a Session');
        }
        if ((array = data) instanceof Array) {
            me.data = data = {};
            fields = me.getFields();
            len = Math.min(fields.length, array.length);
            for (i = 0; i < len; ++i) {
                data[fields[i].name] = array[i];
            }
        }
        if (!(initializeFn = cls.initializeFn)) {
            cls.initializeFn = initializeFn = Model.makeInitializeFn(cls);
        }
        if (!initializeFn.$nullFn) {
            cls.initializeFn(me);
        }
        // Must do this after running the initializeFn due to converters on idField
        if (!me.isSummaryModel) {
            if (!(me.id = id = data[idProperty]) && id !== 0) {
                if (dataId) {
                    Ext.raise('The model ID configured in data ("' + dataId + '") has been rejected by the ' + me.fieldsMap[idProperty].type + ' field converter for the ' + idProperty + ' field');
                }
                if (session) {
                    identifier = session.getIdentifier(cls);
                    id = identifier.generate();
                } else if (modelIdentifier === identifier) {
                    id = internalId;
                } else {
                    id = identifier.generate();
                }
                data[idProperty] = me.id = id;
                me.phantom = true;
                me.crudState = 'C';
            }
            if (session && !skipStoreAddition) {
                session.add(me);
            }
            // Needs to be set after the add to the session
            if (me.phantom) {
                me.crudStateWas = 'C';
            }
        }
        if (me.init && Ext.isFunction(me.init)) {
            me.init();
        }
    },
    /**
     * @property {String} entityName
     * The short name of this entity class. This name is derived from the `namespace` of
     * the associated `schema` and this class name. By default, a class is not given a
     * shortened name.
     *
     * All entities in a given `schema` must have a unique `entityName`.
     * 
     * For more details see "Relative Naming" in {@link Ext.data.schema.Schema}.
     */
    /**
     * @property {Boolean} editing
     * Internal flag used to track whether or not the model instance is currently being edited.
     * @readonly
     */
    editing: false,
    /**
     * @property {Boolean} dirty
     * True if this record has been modified.
     * @readonly
     */
    dirty: false,
    /**
     * @property {Ext.data.Session} session
     * The {@link Ext.data.Session} for this record.
     * @readonly
     */
    session: null,
    /**
     * @property {Boolean} dropped
     * True if this record is pending delete on the server. This is set by the `drop`
     * method and transmitted to the server by the `save` method.
     * @readonly
     */
    dropped: false,
    /**
     * @property {Boolean} erased
     * True if this record has been erased on the server. This flag is set of the `erase`
     * method.
     * @readonly
     */
    erased: false,
    /**
     * @cfg {String} clientIdProperty
     * The name of the property a server will use to send back a client-generated id in a
     * `create` or `update` `{@link Ext.data.operation.Operation operation}`.
     *
     * If specified, this property cannot have the same name as any other field.
     *
     * For example:
     *
     *      Ext.define('Person', {
     *          idProperty: 'id',  // this is the default value (for clarity)
     *
     *          clientIdProperty: 'clientId',
     *
     *          identifier: 'negative', // to generate -1, -2 etc on the client
     *
     *          fields: [ 'name' ]
     *      });
     *
     *      var person = new Person({
     *          // no id provided, so -1 is generated
     *          name: 'Clark Kent'
     *      });
     *
     * The server is given this data during the `create`:
     *
     *      {
     *          id: -1,
     *          name: 'Clark Kent'
     *      }
     *
     * The server allocates a real id and responds like so:
     *
     *      {
     *          id: 427,
     *          clientId: -1
     *      }
     *
     * This property is most useful when creating multiple entities in a single call to
     * the server in a `{@link Ext.data.operation.Create create operation}`. Alternatively,
     * the server could respond with records that correspond one-to-one to those sent in
     * the `operation`.
     *
     * For example the client could send a `create` with this data:
     *
     *      [ { id: -1, name: 'Clark Kent' },
     *        { id: -2, name: 'Peter Parker' },
     *        { id: -3, name: 'Bruce Banner' } ]
     *
     * And the server could respond in the same order:
     *
     *      [ { id: 427 },      // updates id = -1
     *        { id: 428 },      // updates id = -2
     *        { id: 429 } ]     // updates id = -3
     *
     * Or using `clientIdProperty` the server could respond in arbitrary order:
     *
     *      [ { id: 427, clientId: -3 },
     *        { id: 428, clientId: -1 },
     *        { id: 429, clientId: -2 } ]
     *
     * **IMPORTANT:** When upgrading from previous versions be aware that this property
     * used to perform the role of `{@link Ext.data.writer.Writer#clientIdProperty}` as
     * well as that described above. To continue send a client-generated id as other than
     * the `idProperty`, set `clientIdProperty` on the `writer`. A better solution, however,
     * is most likely a properly configured `identifier` as that would work better with
     * associations.
     */
    clientIdProperty: null,
    evented: false,
    /**
     * @property {Boolean} phantom
     * True when the record does not yet exist in a server-side database. Any record which
     * has a real database identity set as its `idProperty` is NOT a phantom -- it's real.
     */
    phantom: false,
    /**
     * @cfg {String} idProperty
     * The name of the field treated as this Model's unique id.
     *
     * If changing the idProperty in a subclass, the generated id field will replace the
     * one generated by the superclass, for example;
     *
     *      Ext.define('Super', {
     *          extend: 'Ext.data.Model',
     *          fields: ['name']
     *      });
     *
     *      Ext.define('Sub', {
     *          extend: 'Super',
     *          idProperty: 'customId'
     *      });
     *
     *      var fields = Super.getFields();
     *      // Has 2 fields, "name" & "id"
     *      console.log(fields[0].name, fields[1].name, fields.length);
     *
     *      fields = Sub.getFields();
     *      // Has 2 fields, "name" & "customId", "id" is replaced
     *      console.log(fields[0].name, fields[1].name, fields.length);
     *
     * The data values for this field must be unique or there will be id value collisions
     * in the {@link Ext.data.Store Store}.
     */
    idProperty: 'id',
    /**
     * @cfg {Object} manyToMany
     * A config object for a {@link Ext.data.schema.ManyToMany ManyToMany} association.
     * See the class description for {@link Ext.data.schema.ManyToMany ManyToMany} for
     * configuration examples.
     */
    manyToMany: null,
    /**
     * @cfg {String/Object} identifier
     * The id generator to use for this model. The `identifier` generates values for the
     * {@link #idProperty} when no value is given. Records with client-side generated
     * values for {@link #idProperty} are called {@link #phantom} records since they are
     * not yet known to the server.
     *
     * This can be overridden at the model level to provide a custom generator for a
     * model. The simplest form of this would be:
     *
     *      Ext.define('MyApp.data.MyModel', {
     *          extend: 'Ext.data.Model',
     *          requires: ['Ext.data.identifier.Sequential'],
     *          identifier: 'sequential',
     *          ...
     *      });
     *
     * The above would generate {@link Ext.data.identifier.Sequential sequential} id's
     * such as 1, 2, 3 etc..
     *
     * Another useful id generator is {@link Ext.data.identifier.Uuid}:
     *
     *      Ext.define('MyApp.data.MyModel', {
     *          extend: 'Ext.data.Model',
     *          requires: ['Ext.data.identifier.Uuid'],
     *          identifier: 'uuid',
     *          ...
     *      });
     *
     * An id generator can also be further configured:
     *
     *      Ext.define('MyApp.data.MyModel', {
     *          extend: 'Ext.data.Model',
     *          identifier: {
     *              type: 'sequential',
     *              seed: 1000,
     *              prefix: 'ID_'
     *          }
     *      });
     *
     * The above would generate id's such as ID_1000, ID_1001, ID_1002 etc..
     *
     * If multiple models share an id space, a single generator can be shared:
     *
     *      Ext.define('MyApp.data.MyModelX', {
     *          extend: 'Ext.data.Model',
     *          identifier: {
     *              type: 'sequential',
     *              id: 'xy'
     *          }
     *      });
     *
     *      Ext.define('MyApp.data.MyModelY', {
     *          extend: 'Ext.data.Model',
     *          identifier: {
     *              type: 'sequential',
     *              id: 'xy'
     *          }
     *      });
     *
     * For more complex, shared id generators, a custom generator is the best approach.
     * See {@link Ext.data.identifier.Generator} for details on creating custom id
     * generators.
     */
    identifier: null,
    // Fields config and property
    // @cmd-auto-dependency {aliasPrefix: "data.field."}
    /**
     * @cfg {Object[]/String[]} fields
     * An Array of `Ext.data.field.Field` config objects, simply the field 
     * {@link Ext.data.field.Field#name name}, or a mix of config objects and strings. 
     * If just a name is given, the field type defaults to `auto`.
     * 
     * In a {@link Ext.data.field.Field Field} config object you may pass the alias of 
     * the `Ext.data.field.*` type using the `type` config option.
     * 
     *     // two fields are set:
     *     // - an 'auto' field with a name of 'firstName'
     *     // - and an Ext.data.field.Integer field with a name of 'age'
     *     fields: ['firstName', {
     *         type: 'int',
     *         name: 'age'
     *     }]
     * 
     * Fields will automatically be created at read time for any for any keys in the 
     * data passed to the Model's {@link #proxy proxy's} 
     * {@link Ext.data.reader.Reader reader} whose name is not explicitly configured in 
     * the `fields` config.
     * 
     * Extending a Model class will inherit all the `fields` from the superclass / 
     * ancestor classes.
     */
    /**
     * @property {Ext.data.field.Field[]} fields
     * An array fields defined for this Model (including fields defined in superclasses)
     * in ordinal order; that is in declaration order.
     * @private
     * @readonly
     */
    /**
     * @property {Object} fieldOrdinals
     * This property is indexed by field name and contains the ordinal of that field. The
     * ordinal often has meaning to servers and is derived based on the position in the
     * `fields` array.
     * 
     * This can be used like so:
     * 
     *      Ext.define('MyApp.models.User', {
     *          extend: 'Ext.data.Model',
     *
     *          fields: [
     *              { name: 'name' }
     *          ]
     *      });
     * 
     *      var nameOrdinal = MyApp.models.User.fieldOrdinals.name;
     *      
     *      // or, if you have an instance:
     *
     *      var user = new MyApp.models.User();
     *      var nameOrdinal = user.fieldOrdinals.name;
     *
     * @private
     * @readonly
     */
    /**
      * @property {Object} modified
      * A hash of field values which holds the initial values of fields before a set of
      * edits are {@link #commit committed}.
      */
    /**
     * @property {Object} previousValues
     * This object is similar to the `modified` object except it holds the data values as
     * they were prior to the most recent change.
     * @readonly
     * @private
     */
    previousValues: undefined,
    // Not "null" so getPrevious returns undefined first time
    // @cmd-auto-dependency { aliasPrefix : "proxy.", defaultPropertyName : "defaultProxyType"}
    /**
     * @cfg {String/Object/Ext.data.proxy.Proxy} proxy
     * The {@link Ext.data.proxy.Proxy proxy} to use for this class.
     *
     * By default, the proxy is configured from the {@link Ext.data.schema.Schema schema}.
     * You can ignore the schema defaults by setting `schema: false` on the `proxy` config.
     *
     *      Ext.define('MyApp.data.CustomProxy', {
     *          extend: 'Ext.data.proxy.Ajax',
     *          alias: 'proxy.customproxy',
     *
     *          url: 'users.json'
     *      });
     *
     *      Ext.define('MyApp.models.CustomModel', {
     *          extend: 'Ext.data.Model',
     *
     *          fields: ['name'],
     *          proxy: {
     *              type: 'customproxy,
     *              schema: false
     *          }
     *      });
     *
     * With `schema: false`, the `url` of the proxy will be used instead of what has been defined
     * on the schema.
     */
    proxy: undefined,
    /**
     * @cfg {String/Object} [schema='default']
     * The name of the {@link Ext.data.schema.Schema schema} to which this entity and its
     * associations belong. For details on custom schemas see `Ext.data.schema.Schema`.
     */
    /**
     * @property {Ext.data.schema.Schema} schema
     * The `Ext.data.schema.Schema` to which this entity and its associations belong.
     * @readonly
     */
    schema: 'default',
    /**
     * @cfg {Object} summary
     * Summary fields are a special kind of field that is used to assist in creating an
     * aggregation for this model. A new model type that extends this model will be
     * created, accessible via {@link #method-getSummaryModel}. This summary model will
     * have these virtual aggregate fields in the fields collection like a normal model.
     * Each key in the object is the field name. The value for each field should mirror
     * the {@link #cfg-fields}, excluding the `name` option. The summary model generated
     * will have 2 fields, 'rate', which will aggregate using an average and maxRate,
     * which will aggregate using the maximum value.
     *
     * See {@link Ext.data.summary.Base} for more information.
     *
     *      Ext.define('User', {
     *          extend: 'Ext.data.Model',
     *          fields: [{
     *              name: 'rate',
     *              summary: 'avg'
     *          }],
     *
     *          summary: {
     *              maxRate: {
     *                  field: 'rate', // calculated from rate field
     *                  summary: 'max'
     *              }
     *          }
     *      });
     *
     * @since 6.5.0
     */
    summary: null,
    /**
     * @cfg {String} versionProperty
     * If specified, this is the name of the property that contains the entity "version".
     * The version property is used to manage a long-running transaction and allows the
     * detection of simultaneous modification.
     * 
     * The way a version property is used is that the client receives the version as it
     * would any other entity property. When saving an entity, this property is always
     * included in the request and the server uses the value in a "conditional update".
     * If the current version of the entity on the server matches the version property
     * sent by the client, the update is allowed. Otherwise, the update fails.
     * 
     * On successful update, both the client and server increment the version. This is
     * done on the server in the conditional update and on the client when it receives a
     * success on its update request.
     */
    versionProperty: null,
    /**
     * @property {Number} generation
     * This property is incremented on each modification of a record.
     * @readonly
     * @since 5.0.0
     */
    generation: 1,
    /**
     * @cfg {Object[]} validators
     * An array of {@link Ext.data.validator.Validator validators} for this model.
     */
    /**
     * @cfg {String} validationSeparator
     * If specified this property is used to concatenate multiple errors for each field
     * as reported by the `validators`.
     */
    validationSeparator: null,
    /**
     * @cfg {Boolean} convertOnSet
     * Set to `false` to prevent any converters from being called on fields specified in 
     * a {@link Ext.data.Model#set set} operation.
     * 
     * **Note:** Setting the config to `false` will only prevent the convert / calculate 
     * call when the set `fieldName` param matches the field's `{@link #name}`.  In the 
     * following example the calls to set `salary` will not execute the convert method 
     * on `set` while the calls to set `vested` will execute the convert method on the 
     * initial read as well as on `set`.
     * 
     * Example model definition:
     * 
     *     Ext.define('MyApp.model.Employee', {
     *         extend: 'Ext.data.Model',
     *         fields: ['yearsOfService', {
     *             name: 'salary',
     *             convert: function (val) {
     *                 var startingBonus = val * .1;
     *                 return val + startingBonus;
     *             }
     *         }, {
     *             name: 'vested',
     *             convert: function (val, record) {
     *                 return record.get('yearsOfService') >= 4;
     *             },
     *             depends: 'yearsOfService'
     *         }],
     *         convertOnSet: false
     *     });
     *     
     *     var tina = Ext.create('MyApp.model.Employee', {
     *         salary: 50000,
     *         yearsOfService: 3
     *     });
     *     
     *     console.log(tina.get('salary')); // logs 55000
     *     console.log(tina.get('vested')); // logs false
     *     
     *     tina.set({
     *         salary: 60000,
     *         yearsOfService: 4
     *     });
     *     console.log(tina.get('salary')); // logs 60000
     *     console.log(tina.get('vested')); // logs true
     */
    convertOnSet: true,
    // Associations configs and properties
    /**
     * @cfg {Object[]} associations
     * An array of {@link Ext.data.schema.Association associations} for this model.
     *
     * For further documentation, see {@link Ext.data.schema.Association}.
     *
     * @deprecated 6.2.0 Use `hasMany/hasOne/belongsTo`.
     */
    /**
     * @cfg {String/Object/String[]/Object[]} hasMany
     * One or more `Ext.data.schema.HasMany` associations for this model.
     */
    /**
     * @cfg {String/Object/String[]/Object[]} hasOne
     * One or more `Ext.data.schema.HasOne` associations for this model.
     */
    /**
     * @cfg {String/Object/String[]/Object[]} belongsTo
     * One or more `Ext.data.schema.BelongsTo` associations for this model.
     */
    /**
     * Begins an edit. While in edit mode, no events (e.g.. the `update` event) are
     * relayed to the containing store. When an edit has begun, it must be followed by
     * either `endEdit` or `cancelEdit`.
     */
    beginEdit: function() {
        var me = this,
            modified = me.modified,
            previousValues = me.previousValues;
        if (!me.editing) {
            me.editing = true;
            me.editMemento = {
                dirty: me.dirty,
                data: Ext.apply({}, me.data),
                generation: me.generation,
                modified: modified && Ext.apply({}, modified),
                previousValues: previousValues && Ext.apply({}, previousValues)
            };
        }
    },
    /**
     * Calculate all summary fields on this record.
     * @param {Ext.data.Model[]} records The records to use for calculation.
     *
     * @since 6.5.0
     */
    calculateSummary: function(records) {
        var fields = this.getFields(),
            len = fields.length,
            recLen = records.length,
            i, result, summary, prop, name, field;
        for (i = 0; i < len; ++i) {
            field = fields[i];
            summary = field.getSummary();
            if (summary) {
                result = result || {};
                name = field.name;
                prop = field.summaryField || name;
                result[name] = summary.calculate(records, prop, 'data', 0, recLen);
            }
        }
        if (result) {
            this.set(result, this._commitOptions);
        }
    },
    /**
     * Cancels all changes made in the current edit operation.
     */
    cancelEdit: function() {
        var me = this,
            editMemento = me.editMemento,
            validation = me.validation;
        if (editMemento) {
            me.editing = false;
            // reset the modified state, nothing changed since the edit began
            Ext.apply(me, editMemento);
            me.editMemento = null;
            if (validation && validation.syncGeneration !== me.generation) {
                validation.syncGeneration = 0;
            }
        }
    },
    /**
     * Ends an edit. If any data was modified, the containing store is notified
     * (ie, the store's `update` event will fire).
     * @param {Boolean} [silent] True to not notify any stores of the change.
     * @param {String[]} [modifiedFieldNames] Array of field names changed during edit.
     */
    endEdit: function(silent, modifiedFieldNames) {
        var me = this,
            editMemento = me.editMemento;
        if (editMemento) {
            me.editing = false;
            me.editMemento = null;
            // Since these reflect changes we never notified others about, the real set
            // of "previousValues" is what we captured in the memento:
            me.previousValues = editMemento.previousValues;
            if (!silent) {
                if (!modifiedFieldNames) {
                    modifiedFieldNames = me.getModifiedFieldNames(editMemento.data);
                }
                if (me.dirty || (modifiedFieldNames && modifiedFieldNames.length)) {
                    me.callJoined('afterEdit', [
                        modifiedFieldNames
                    ]);
                }
            }
        }
    },
    getField: function(name) {
        return this.self.getField(name);
    },
    /**
     * Get the fields array for this model.
     * @return {Ext.data.field.Field[]} The fields array
     */
    getFields: function() {
        return this.self.getFields();
    },
    getFieldsMap: function() {
        return this.fieldsMap;
    },
    /**
     * Get the idProperty for this model.
     * @return {String} The idProperty
     */
    getIdProperty: function() {
        return this.idProperty;
    },
    /**
     * Returns the unique ID allocated to this model instance as defined by `idProperty`.
     * @return {Number/String} The id
     */
    getId: function() {
        return this.id;
    },
    /**
     * Return a unique observable ID. Model is not observable but tree nodes
     * (`Ext.data.NodeInterface`) are, so they must be globally unique within the
     * {@link #observableType}.
     * @protected
     */
    getObservableId: function() {
        return this.internalId;
    },
    /**
     * Sets the model instance's id field to the given id.
     * @param {Number/String} id The new id.
     * @param {Object} [options] See {@link #set}.
     */
    setId: function(id, options) {
        this.set(this.idProperty, id, options);
    },
    /**
     * This method returns the value of a field given its name prior to its most recent
     * change.
     * @param {String} fieldName The field's {@link Ext.data.field.Field#name name}.
     * @return {Object} The value of the given field prior to its current value. `undefined`
     * if there is no previous value;
     */
    getPrevious: function(fieldName) {
        var previousValues = this.previousValues;
        return previousValues && previousValues[fieldName];
    },
    /**
     * Returns true if the passed field name has been `{@link #modified}` since the load
     * or last commit.
     * @param {String} fieldName The field's {@link Ext.data.field.Field#name name}.
     * @return {Boolean}
     */
    isModified: function(fieldName) {
        var modified = this.modified;
        return !!(modified && modified.hasOwnProperty(fieldName));
    },
    /**
     * Returns the original value of a modified field. If there is no modified value,
     * `undefined` will be return. Also see {@link #isModified}.
     * @param {String} fieldName The name of the field for which to return the original value.
     * @return {Object} modified
     */
    getModified: function(fieldName) {
        var out;
        if (this.isModified(fieldName)) {
            out = this.modified[fieldName];
        }
        return out;
    },
    /**
     * Returns the value of the given field.
     * @param {String} fieldName The name of the field.
     * @return {Object} The value of the specified field.
     */
    get: function(fieldName) {
        return this.data[fieldName];
    },
    // This object is used whenever the set() method is called and given a string as the
    // first argument. This approach saves memory (and GC costs) since we could be called
    // a lot.
    _singleProp: {},
    _rejectOptions: {
        convert: false,
        silent: true
    },
    /**
     * Sets the given field to the given value. For example:
     * 
     *      record.set('name', 'value');
     * 
     * This method can also be passed an object containing multiple values to set at once.
     * For example:
     * 
     *      record.set({
     *          name: 'value',
     *          age: 42
     *      });
     * 
     * The following store events are fired when the modified record belongs to a store:
     *
     *  - {@link Ext.data.Store#event-beginupdate beginupdate}
     *  - {@link Ext.data.Store#event-update update}
     *  - {@link Ext.data.Store#event-endupdate endupdate}
     * 
     * @param {String/Object} fieldName The field to set, or an object containing key/value 
     * pairs.
     * @param {Object} newValue The value for the field (if `fieldName` is a string).
     * @param {Object} [options] Options for governing this update.
     * @param {Boolean} [options.convert=true] Set to `false` to  prevent any converters from 
     * being called during the set operation. This may be useful when setting a large bunch of 
     * raw values.
     * @param {Boolean} [options.dirty=true] Pass `false` if the field values are to be
     * understood as non-dirty (fresh from the server). When `true`, this change will be
     * reflected in the `modified` collection.
     * @param {Boolean} [options.commit=false] Pass `true` to call the {@link #commit} method 
     * after setting fields. If this option is passed, the usual after change processing will 
     * be bypassed. {@link #commit Commit} will be called even if there are no field changes.
     * @param {Boolean} [options.silent=false] Pass `true` to suppress notification of any
     * changes made by this call. Use with caution.
     * @return {String[]} The array of modified field names or null if nothing was modified.
     */
    set: function(fieldName, newValue, options) {
        var me = this,
            cls = me.self,
            data = me.data,
            modified = me.modified,
            prevVals = me.previousValues,
            session = me.session,
            single = Ext.isString(fieldName),
            opt = (single ? options : newValue),
            convertOnSet = opt ? opt.convert !== false : me.convertOnSet,
            fieldsMap = me.fieldsMap,
            silent = opt && opt.silent,
            commit = opt && opt.commit,
            updateRefs = !(opt && opt.refs === false) && session,
            // Don't need to do dirty processing with commit, since we'll always
            // end up with nothing modified and not dirty
            dirty = !(opt && opt.dirty === false && !commit),
            modifiedFieldNames = null,
            dirtyRank = 0,
            associations = me.associations,
            currentValue, field, idChanged, key, name, oldId, comparator, dep, dependents, i, numFields, newId, rankedFields, reference, value, values, roleName;
        if (single) {
            values = me._singleProp;
            values[fieldName] = newValue;
        } else {
            values = fieldName;
        }
        if (!(rankedFields = cls.rankedFields)) {
            // On the first edit of a record of this type we need to ensure we have the
            // topo-sort done:
            rankedFields = cls.rankFields();
        }
        numFields = rankedFields.length;
        do {
            for (name in values) {
                value = values[name];
                currentValue = data[name];
                comparator = me;
                field = fieldsMap[name];
                if (field) {
                    if (convertOnSet && field.convert) {
                        value = field.convert(value, me);
                    }
                    comparator = field;
                    reference = field.reference;
                } else {
                    reference = null;
                }
                if (comparator.isEqual(currentValue, value)) {
                    
                    continue;
                }
                data[name] = value;
                (modifiedFieldNames || (modifiedFieldNames = [])).push(name);
                (prevVals || (me.previousValues = prevVals = {}))[name] = currentValue;
                if (reference && reference.cls) {
                    if (updateRefs) {
                        session.updateReference(me, field, value, currentValue);
                    }
                    reference.onValueChange(me, session, value, currentValue);
                }
                i = (dependents = field && field.dependents) && dependents.length;
                while (i-- > 0) {
                    (dep = dependents[i]).dirty = true;
                    dirtyRank = dirtyRank ? Math.min(dirtyRank, dep.rank) : dep.rank;
                }
                if (!field || field.persist) {
                    if (modified && modified.hasOwnProperty(name)) {
                        if (!dirty || comparator.isEqual(modified[name], value)) {
                            delete modified[name];
                            me.dirty = -1;
                        }
                    } else if (dirty) {
                        if (!modified) {
                            me.modified = modified = {};
                        }
                        me.dirty = true;
                        modified[name] = currentValue;
                    }
                }
                if (name === me.idField.name) {
                    idChanged = true;
                    oldId = currentValue;
                    newId = value;
                }
            }
            if (!dirtyRank) {
                break;
            }
            field = rankedFields[dirtyRank - 1];
            field.dirty = false;
            if (single) {
                delete values[fieldName];
            } else {
                values = me._singleProp;
                single = true;
            }
            fieldName = field.name;
            values[fieldName] = data[fieldName];
            convertOnSet = true;
            for (; dirtyRank < numFields; ++dirtyRank) {
                if (rankedFields[dirtyRank].dirty) {
                    break;
                }
            }
            if (dirtyRank < numFields) {
                ++dirtyRank;
            } else {
                dirtyRank = 0;
            }
        } while (// new value is the same, so no change...
        // We need the cls to be present because it means the association class is loaded,
        // otherwise it could be pending.
        // we use the field instance to hold the dirty bit to avoid any
        // extra allocations... we'll clear this before we depart. We do
        // this so we can perform the fewest recalculations possible as
        // each dependent field only needs to be recalculated once.
        // The original value in me.modified equals the new value, so
        // the field is no longer modified:
        // fix me.dirty later (still truthy)
        // create only when needed
        // Unless there are dependent fields to process we can break now. This is
        // what will happen for all code pre-dating the depends or simply not
        // using it, so it will add very little overhead when not used.
        // dirtyRank has the minimum rank (a 1-based value) of any dependent field
        // that needs recalculating due to changes above. The way we go about this
        // is to use our helper object for processing single argument invocations
        // to process just this one field. This is because the act of setting it
        // may cause another field to be invalidated, so while we cannot know at
        // this moment all the fields we need to recalculate, we know that only
        // those following this field in rankedFields can possibly be among them.
        // dirtyRank is 1-based
        // clear just this field's dirty state
        // cleanup last value
        // switch over
        // We are now processing a dependent field, so we want to force a
        // convert to occur because it's the only way it will get a value
        // Since dirtyRank is 1-based and refers to the field we need to handle
        // on this pass, we can treat it like an index for a minute and look at
        // the next field on towards the end to find the index of the next dirty
        // field.
        // We found a field after this one marked as dirty so make the index
        // a proper 1-based rank:
        // We did not find any more dirty fields after this one, so clear the
        // dirtyRank and we will perhaps fall out after the next update
        1);
        // eslint-disable-line no-constant-condition
        if (me.dirty < 0) {
            // We might have removed the last modified field, so check to see if there
            // are any modified fields remaining and correct me.dirty:
            me.dirty = false;
            for (key in modified) {
                if (modified.hasOwnProperty(key)) {
                    me.dirty = true;
                    break;
                }
            }
        }
        if (single) {
            // cleanup our reused object for next time... important to do this before
            // we fire any events or call anyone else (like afterEdit)!
            delete values[fieldName];
        }
        ++me.generation;
        if (idChanged) {
            me.id = newId;
            me.onIdChanged(newId, oldId);
            me.callJoined('onIdChanged', [
                oldId,
                newId
            ]);
            if (associations) {
                for (roleName in associations) {
                    associations[roleName].onIdChanged(me, oldId, newId);
                }
            }
        }
        if (commit) {
            me.commit(silent, modifiedFieldNames);
        } else if (!silent && !me.editing && modifiedFieldNames) {
            me.callJoined('afterEdit', [
                modifiedFieldNames
            ]);
        }
        return modifiedFieldNames;
    },
    /**
     * Usually called by the {@link Ext.data.Store} to which this model instance has been
     * {@link #join joined}. Rejects all changes made to the model instance since either creation,
     * or the last commit operation. Modified fields are reverted to their original values.
     *
     * Developers should subscribe to the {@link Ext.data.Store#event-update} event to have their
     * code notified of reject operations.
     *
     * @param {Boolean} [silent=false] `true` to skip notification of the owning store of the
     * change.
     */
    reject: function(silent) {
        var me = this,
            modified = me.modified;
        if (me.erased) {
            Ext.raise('Cannot reject once a record has been erased.');
        }
        if (modified) {
            me.set(modified, me._rejectOptions);
        }
        me.dropped = false;
        me.clearState();
        if (!silent) {
            me.callJoined('afterReject');
        }
    },
    /**
     * Usually called by the {@link Ext.data.Store} which owns the model instance. Commits all
     * changes made to the instance since either creation or the last commit operation.
     *
     * Developers should subscribe to the {@link Ext.data.Store#event-update} event to have their
     * code notified of commit operations.
     *
     * @param {Boolean} [silent=false] Pass `true` to skip notification of the owning store of the
     * change.
     * @param {String[]} [modifiedFieldNames] Array of field names changed during sync with server
     * if known. Omit or pass `null` if unknown. An empty array means that it is known that
     * no fields were modified by the server's response.
     * Defaults to false.
     */
    commit: function(silent, modifiedFieldNames) {
        var me = this,
            versionProperty = me.versionProperty,
            data = me.data,
            erased;
        me.clearState();
        if (versionProperty && !me.phantom && !isNaN(data[versionProperty])) {
            ++data[versionProperty];
        }
        me.phantom = false;
        if (me.dropped) {
            me.erased = erased = true;
        }
        if (!silent) {
            if (erased) {
                me.callJoined('afterErase');
            } else {
                me.callJoined('afterCommit', [
                    modifiedFieldNames
                ]);
            }
        }
    },
    clearState: function() {
        var me = this;
        me.dirty = me.editing = false;
        me.editMemento = me.modified = null;
    },
    /**
     * Marks this record as `dropped` and waiting to be deleted on the server. When a
     * record is dropped, it is automatically removed from all association stores and
     * any child records associated to this record are also dropped (a "cascade delete")
     * depending on the `cascade` parameter.
     *
     * @param {Boolean} [cascade=true] Pass `false` to disable the cascade to drop child
     * records.
     * @since 5.0.0
     */
    drop: function(cascade) {
        var me = this,
            associations = me.associations,
            session = me.session,
            roleName;
        if (me.erased || me.dropped) {
            return;
        }
        me.dropped = true;
        if (associations && cascade !== false) {
            for (roleName in associations) {
                associations[roleName].onDrop(me, session);
            }
        }
        me.callJoined('afterDrop');
        if (me.phantom) {
            me.setErased();
        }
    },
    /**
     * Tells this model instance that an observer is looking at it.
     * @param {Ext.data.Store} owner The store or other owner object to which this model
     * has been added.
     */
    join: function(owner) {
        var me = this,
            joined = me.joined;
        // Optimize this, gets called a lot
        if (!joined) {
            joined = me.joined = [
                owner
            ];
        } else if (!joined.length) {
            joined[0] = owner;
        } else {
            // TODO: do we need joined here? Perhaps push will do.
            Ext.Array.include(joined, owner);
        }
        if (owner.isStore && !me.store) {
            /**
            * @property {Ext.data.Store} store
            * The {@link Ext.data.Store Store} to which this instance belongs.
            *
            * **Note:** If this instance is bound to multiple stores, this property
            * will reference only the first.
            */
            me.store = owner;
        }
    },
    /**
     * Tells this model instance that it has been removed from the store.
     * @param {Ext.data.Store} owner The store or other owner object from which this
     * model has been removed.
     */
    unjoin: function(owner) {
        var me = this,
            joined = me.joined,
            // TreeModels are never joined to their TreeStore.
            // But unjoin is called by the base class's onCollectionRemove, so joined may be
            // undefined.
            len = joined && joined.length,
            store = me.store,
            i;
        if (owner === me.session) {
            me.session = null;
        } else {
            if (len === 1 && joined[0] === owner) {
                joined.length = 0;
            } else if (len) {
                Ext.Array.remove(joined, owner);
            }
            if (store === owner) {
                store = null;
                if (joined) {
                    for (i = 0 , len = joined.length; i < len; ++i) {
                        owner = joined[i];
                        if (owner.isStore) {
                            store = owner;
                            break;
                        }
                    }
                }
                me.store = store;
            }
        }
    },
    /**
     * Creates a clone of this record. States like `dropped`, `phantom` and `dirty` are
     * all preserved in the cloned record.
     *
     * @param {Ext.data.Session} [session] The session to which the new record
     * belongs.
     * @return {Ext.data.Model} The cloned record.
     */
    clone: function(session) {
        var me = this,
            modified = me.modified,
            ret = me.copy(me.id, session);
        if (modified) {
            // Restore the modified fields state
            ret.modified = Ext.apply({}, modified);
        }
        ret.dirty = me.dirty;
        ret.dropped = me.dropped;
        ret.phantom = me.phantom;
        return ret;
    },
    /**
     * Creates a clean copy of this record. The returned record will not consider any its
     * fields as modified.
     *
     * To generate a phantom instance with a new id pass `null`:
     *
     *     var rec = record.copy(null); // clone the record but no id (one is generated)
     *
     * @param {String} [newId] A new id, defaults to the id of the instance being copied.
     * See `{@link Ext.data.Model#idProperty idProperty}`.
     * @param {Ext.data.Session} [session] The session to which the new record
     * belongs.
     *
     * @return {Ext.data.Model}
     */
    copy: function(newId, session) {
        var me = this,
            data = Ext.apply({}, me.data),
            idProperty = me.idProperty,
            T = me.self;
        if (newId || newId === 0) {
            data[idProperty] = newId;
        } else if (newId === null) {
            delete data[idProperty];
        }
        return new T(data, session);
    },
    /**
     * Returns the configured Proxy for this Model.
     * @return {Ext.data.proxy.Proxy} The proxy
     */
    getProxy: function() {
        return this.self.getProxy();
    },
    /**
     * Returns the `Ext.data.Validation` record holding the results of this record's
     * `validators`. This record is lazily created on first request and is then kept on
     * this record to be updated later.
     *
     * See the class description for more about `validators`.
     *
     * @param {Boolean} [refresh] Pass `false` to not call the `refresh` method on the
     * validation instance prior to returning it. Pass `true` to force a `refresh` of the
     * validation instance. By default the returned record is only refreshed if changes
     * have been made to this record.
     * @return {Ext.data.Validation} The `Validation` record for this record.
     * @since 5.0.0
     */
    getValidation: function(refresh) {
        var me = this,
            ret = me.validation;
        if (!ret) {
            me.validation = ret = new Ext.data.Validation();
            ret.attach(me);
        }
        if (refresh === true || (refresh !== false && ret.syncGeneration !== me.generation)) {
            ret.refresh(refresh);
        }
        return ret;
    },
    /**
     * Validates the current data against all of its configured {@link #validators}. The
     * returned collection holds an object for each reported problem from a `validator`.
     *
     * @return {Ext.data.ErrorCollection} The errors collection.
     * @deprecated 5.0 Use `getValidation` instead.
     */
    validate: function() {
        return new Ext.data.ErrorCollection().init(this);
    },
    /**
     * Checks if the model is valid. See {@link #getValidation}.
     * @return {Boolean} True if the model is valid.
     */
    isValid: function() {
        return this.getValidation().isValid();
    },
    /**
     * Returns a url-suitable string for this model instance. By default this just returns the
     * name of the Model class followed by the instance ID - for example an instance of
     * MyApp.model.User with ID 123 will return 'user/123'.
     * @return {String} The url string for this model instance.
     */
    toUrl: function() {
        var pieces = this.$className.split('.'),
            name = pieces[pieces.length - 1].toLowerCase();
        return name + '/' + this.getId();
    },
    /**
     * @method erase
     * @localdoc Destroys the model using the configured proxy.  The erase action is
     * asynchronous.  Any processing of the erased record should be done in a callback.
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User({
     *         name: 'Foo'
     *     });
     *
     *     // pass the phantom record data to the server to be saved
     *     user.save({
     *         success: function(record, operation) {
     *             // do something if the save succeeded
     *             // erase the created record
     *             record.erase({
     *                 failure: function(record, operation) {
     *                     // do something if the erase failed
     *                 },
     *                 success: function(record, operation) {
     *                     // do something if the erase succeeded
     *                 },
     *                 callback: function(record, operation, success) {
     *                     // do something if the erase succeeded or failed
     *                 }
     *             });
     *         }
     *     });
     *
     * **NOTE:** If a {@link #phantom} record is erased it will not be processed via the
     * proxy.  However, any passed `success` or `callback` functions will be called.
     *
     * The options param is an {@link Ext.data.operation.Destroy} config object
     * containing success, failure and callback functions, plus optional scope.
     *
     * @inheritdoc #method-load
     * @return {Ext.data.operation.Destroy} The destroy operation
     */
    erase: function(options) {
        var me = this;
        me.erasing = true;
        // Drop causes a removal from the backing Collection.
        // The store's onCollectionRemove will respond to this by adding the record to
        // its "to remove" stack and setting its needsSync
        // flag unless the above "erasing" flag is set.
        me.drop();
        me.erasing = false;
        return me.save(options);
    },
    setErased: function() {
        this.erased = true;
        this.callJoined('afterErase');
    },
    /**
     * Gets an object of only the fields that have been modified since this record was
     * created or committed. Only persistent fields are tracked in the `modified` set so
     * this method will only return changes to persistent fields.
     *
     * For more control over the returned data, see `{@link #getData}`.
     * @return {Object}
     */
    getChanges: function() {
        return this.getData(this._getChangesOptions);
    },
    /**
     * Returns the array of fields that are declared as critical (must always send).
     * @return {Ext.data.field.Field[]}
     */
    getCriticalFields: function() {
        var cls = this.self,
            ret = cls.criticalFields;
        if (!ret) {
            cls.rankFields();
            ret = cls.criticalFields;
        }
        return ret;
    },
    /**
     * This method is called by the {@link Ext.data.reader.Reader} after loading a model from
     * the server. This is after processing any inline associations that are available.
     * 
     * @method onLoad
     *
     * @protected
     * @template
     */
    /**
     * Gets all of the data from this Models *loaded* associations. It does this
     * recursively. For example if we have a User which hasMany Orders, and each Order
     * hasMany OrderItems, it will return an object like this:
     *
     *     {
     *         orders: [
     *             {
     *                 id: 123,
     *                 status: 'shipped',
     *                 orderItems: [
     *                     ...
     *                 ]
     *             }
     *         ]
     *     }
     *
     * @param {Object} [result] The object on to which the associations will be added. If
     * no object is passed one is created. This object is then returned.
     * @param {Boolean/Object} [options] An object containing options describing the data
     * desired.
     * @param {Boolean} [options.associated=true] Pass `true` to include associated data from
     * other associated records.
     * @param {Boolean} [options.changes=false] Pass `true` to only include fields that
     * have been modified. Note that field modifications are only tracked for fields that
     * are not declared with `persist` set to `false`. In other words, only persistent
     * fields have changes tracked so passing `true` for this means `options.persist` is
     * redundant.
     * @param {Boolean} [options.critical] Pass `true` to include fields set as `critical`.
     * This is only meaningful when `options.changes` is `true` since critical fields may
     * not have been modified.
     * @param {Boolean} [options.persist] Pass `true` to only return persistent fields.
     * This is implied when `options.changes` is set to `true`.
     * @param {Boolean} [options.serialize=false] Pass `true` to invoke the `serialize`
     * method on the returned fields.
     * @return {Object} The nested data set for the Model's loaded associations.
     */
    getAssociatedData: function(result, options) {
        var me = this,
            associations = me.associations,
            deep, i, item, items, itemData, length, record, role, roleName, opts, clear, associated;
        result = result || {};
        me.$gathering = 1;
        if (options) {
            options = Ext.apply({}, options);
        }
        for (roleName in associations) {
            role = associations[roleName];
            item = role.getAssociatedItem(me);
            if (!item || item.$gathering) {
                
                continue;
            }
            if (item.isStore) {
                item.$gathering = 1;
                items = item.getData().items;
                // get the records for the store
                length = items.length;
                itemData = [];
                for (i = 0; i < length; ++i) {
                    // NOTE - we don't check whether the record is gathering here because
                    // we cannot remove it from the store (it would invalidate the index
                    // values and misrepresent the content). Instead we tell getData to
                    // only get the fields vs descend further.
                    record = items[i];
                    deep = !record.$gathering;
                    record.$gathering = 1;
                    if (options) {
                        associated = options.associated;
                        if (associated === undefined) {
                            options.associated = deep;
                            clear = true;
                        } else if (!deep) {
                            options.associated = false;
                            clear = true;
                        }
                        opts = options;
                    } else {
                        opts = deep ? me._getAssociatedOptions : me._getNotAssociatedOptions;
                    }
                    itemData.push(record.getData(opts));
                    if (clear) {
                        options.associated = associated;
                        clear = false;
                    }
                    delete record.$gathering;
                }
                delete item.$gathering;
            } else {
                opts = options || me._getAssociatedOptions;
                if (options && options.associated === undefined) {
                    opts.associated = true;
                }
                itemData = item.getData(opts);
            }
            result[roleName] = itemData;
        }
        delete me.$gathering;
        return result;
    },
    /**
     * Gets all values for each field in this model and returns an object containing the
     * current data. This can be tuned by passing an `options` object with various
     * properties describing the desired result. Passing `true` simply returns all fields
     * *and* all associated record data.
     *
     * To selectively gather some associated data, the `options` object can be used as
     * follows:
     *
     *      var data = order.getData({
     *          associated: {
     *              orderItems: true
     *          }
     *      });
     *
     * This will include all data fields as well as an "orderItems" array with the data
     * for each `OrderItem`. To include the associated `Item` for each `OrderItem`, the
     * call would look like:
     *
     *      var data = order.getData({
     *          associated: {
     *              orderItems: {
     *                  item: true
     *              }
     *          }
     *      });
     *
     * @param {Boolean/Object} [options] An object containing options describing the data
     * desired. If `true` is passed it is treated as an object with `associated` set to
     * `true`.
     * @param {Boolean/Object} [options.associated=false] Pass `true` to recursively
     * include all associated data. This is equivalent to pass `true` as the only argument.
     * See `getAssociatedData`. If `associated` is an object, it describes the specific
     * associations to gather.
     * @param {Boolean} [options.changes=false] Pass `true` to only include fields that
     * have been modified. Note that field modifications are only tracked for fields that
     * are not declared with `persist` set to `false`. In other words, only persistent
     * fields have changes tracked so passing `true` for this means `options.persist` is
     * redundant.
     * @param {Boolean} [options.critical] Pass `true` to include fields set as `critical`.
     * This is only meaningful when `options.changes` is `true` since critical fields may
     * not have been modified.
     * @param {Boolean} [options.persist] Pass `true` to only return persistent fields.
     * This is implied when `options.changes` is set to `true`.
     * @param {Boolean} [options.serialize=false] Pass `true` to invoke the `serialize`
     * method on the returned fields.
     * @return {Object} An object containing all the values in this model.
     */
    getData: function(options) {
        var me = this,
            ret = {},
            opts = (options === true) ? me._getAssociatedOptions : (options || ret),
            // cheat
            data = me.data,
            associated = opts.associated,
            changes = opts.changes,
            critical = changes && opts.critical,
            content = changes ? me.modified : data,
            fieldsMap = me.fieldsMap,
            persist = opts.persist,
            serialize = opts.serialize,
            criticalFields, field, n, name, value;
        // DON'T use "opts" from here on...
        // Keep in mind the two legacy use cases:
        //  - getData() ==> Ext.apply({}, me.data)
        //  - getData(true) ==> Ext.apply(Ext.apply({}, me.data), me.getAssociatedData())
        if (content) {
            // when processing only changes, me.modified could be null
            for (name in content) {
                value = data[name];
                field = fieldsMap[name];
                if (field) {
                    if (persist && !field.persist) {
                        
                        continue;
                    }
                    if (serialize && field.serialize) {
                        value = field.serialize(value, me);
                    }
                }
                ret[name] = value;
            }
        }
        if (critical) {
            criticalFields = me.self.criticalFields || me.getCriticalFields();
            for (n = criticalFields.length; n-- > 0; ) {
                name = (field = criticalFields[n]).name;
                if (!(name in ret)) {
                    value = data[name];
                    if (serialize && field.serialize) {
                        value = field.serialize(value, me);
                    }
                    ret[name] = value;
                }
            }
        }
        if (associated) {
            if (typeof associated === 'object') {
                me.getNestedData(opts, ret);
            } else {
                me.getAssociatedData(ret, opts);
            }
        }
        return ret;
    },
    getNestedData: function(options, result) {
        var me = this,
            associations = me.associations,
            graph = options.associated,
            i, item, items, itemData, length, record, role, roleName, opts;
        result = result || {};
        // For example:
        //
        //      associated: {
        //          orderItems: true
        //      }
        //
        //      associated: {
        //          orderItems: {
        //              item: true
        //          }
        //      }
        //
        for (roleName in graph) {
            role = associations[roleName];
            opts = graph[roleName];
            if (opts === true) {
                delete options.associated;
            } else {
                options.associated = opts;
            }
            item = role.getAssociatedItem(me);
            if (item.isStore) {
                items = item.getData().items;
                // get the records for the store
                length = items.length;
                itemData = [];
                for (i = 0; i < length; ++i) {
                    record = items[i];
                    itemData.push(record.getData(options));
                }
            } else {
                itemData = item.getData(options);
            }
            result[roleName] = itemData;
        }
        options.associated = graph;
        // restore the original value
        return result;
    },
    /**
     * Returns the array of fields that are declared as non-persist or "transient".
     * @return {Ext.data.field.Field[]}
     * @since 5.0.0
     */
    getTransientFields: function() {
        var cls = this.self,
            ret = cls.transientFields;
        if (!ret) {
            cls.rankFields();
            // populates transientFields as well as rank
            ret = cls.transientFields;
        }
        return ret;
    },
    /**
     * Checks whether this model is loading data from the {@link #proxy}.
     * @return {Boolean} `true` if in a loading state.
     */
    isLoading: function() {
        return !!this.loadOperation;
    },
    /**
     * Aborts a pending {@link #method!load} operation. If the record is not loading, this does
     * nothing.
     */
    abort: function() {
        var operation = this.loadOperation;
        if (operation) {
            operation.abort();
        }
    },
    /**
     * @localdoc Loads the model instance using the configured proxy.  The load action
     * is asynchronous.  Any processing of the loaded record should be done in a
     * callback.
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User();
     *     user.load({
     *         scope: this,
     *         failure: function(record, operation) {
     *             // do something if the load failed
     *         },
     *         success: function(record, operation) {
     *             // do something if the load succeeded
     *         },
     *         callback: function(record, operation, success) {
     *             // do something whether the load succeeded or failed
     *         }
     *     });
     *
     * The options param is an {@link Ext.data.operation.Read} config object containing
     * success, failure and callback functions, plus optional scope.
     *
     * @param {Object} [options] Options to pass to the proxy.
     * @param {Function} options.success A function to be called when the
     * model is processed by the proxy successfully.
     * The callback is passed the following parameters:
     * @param {Ext.data.Model} options.success.record The record.
     * @param {Ext.data.operation.Operation} options.success.operation The operation.
     * 
     * @param {Function} options.failure A function to be called when the
     * model is unable to be processed by the server.
     * The callback is passed the following parameters:
     * @param {Ext.data.Model} options.failure.record The record.
     * @param {Ext.data.operation.Operation} options.failure.operation The operation.
     * 
     * @param {Function} options.callback A function to be called whether the proxy
     * transaction was successful or not.
     * The callback is passed the following parameters:
     * @param {Ext.data.Model} options.callback.record The record.
     * @param {Ext.data.operation.Operation} options.callback.operation The operation.
     * @param {Boolean} options.callback.success `true` if the operation was successful.
     * 
     * @param {Object} options.scope The scope in which to execute the callback
     * functions.  Defaults to the model instance.
     *
     * @return {Ext.data.operation.Read} The read operation.
     */
    load: function(options) {
        options = Ext.apply({}, options);
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            scope = options.scope || me,
            proxy = me.getProxy(),
            callback = options.callback,
            operation = me.loadOperation,
            id = me.getId(),
            extras;
        if (operation) {
            // Already loading, push any callbacks on and jump out
            extras = operation.extraCalls;
            if (!extras) {
                extras = operation.extraCalls = [];
            }
            extras.push(options);
            return operation;
        }
        var doIdCheck = true;
        // eslint-disable-line vars-on-top, one-var
        if (me.phantom) {
            doIdCheck = false;
        }
        options.id = id;
        // Always set the recordCreator. If we have a session, we're already
        // part of said session, so we don't need to handle that.
        options.recordCreator = function(data, type, readOptions) {
            // Important to change this here, because we might be loading associations,
            // so we do not want this to propagate down. If we have a session, use that
            // so that we end up getting the same record. Otherwise, just remove it.
            var session = me.session;
            if (readOptions) {
                readOptions.recordCreator = session ? session.recordCreator : null;
            }
            me.set(data, me._commitOptions);
            // Do the id check after set since converters may have run
            if (doIdCheck && me.getId() !== id) {
                Ext.raise('Invalid record id returned for ' + id + '@' + me.entityName);
            }
            return me;
        };
        options.internalCallback = function(operation) {
            var success = operation.wasSuccessful() && operation.getRecords().length > 0,
                op = me.loadOperation,
                extras = op.extraCalls,
                successFailArgs = [
                    me,
                    operation
                ],
                callbackArgs = [
                    me,
                    operation,
                    success
                ],
                i, len;
            me.loadOperation = null;
            ++me.loadCount;
            if (success) {
                Ext.callback(options.success, scope, successFailArgs);
            } else {
                Ext.callback(options.failure, scope, successFailArgs);
            }
            Ext.callback(callback, scope, callbackArgs);
            // Some code repetition here, however in a vast majority of cases
            // we'll only have a single callback, so optimize for that case rather
            // than setup arrays for all the callback options
            if (extras) {
                for (i = 0 , len = extras.length; i < len; ++i) {
                    options = extras[i];
                    if (success) {
                        Ext.callback(options.success, scope, successFailArgs);
                    } else {
                        Ext.callback(options.failure, scope, successFailArgs);
                    }
                    Ext.callback(options.callback, scope, callbackArgs);
                }
            }
            me.callJoined('afterLoad');
        };
        delete options.callback;
        me.loadOperation = operation = proxy.createOperation('read', options);
        operation.execute();
        return operation;
    },
    /**
     * Merge incoming data from the server when this record exists
     * in an active session. This method is not called if this record is
     * loaded directly via {@link #method!load}. The default behaviour is to use incoming
     * data if the record is not {@link #dirty}, otherwise the data is
     * discarded. This method should be overridden in subclasses to
     * provide a different behavior.
     * @param {Object} data The model data retrieved from the server.
     *
     * @protected
     *
     * @since 6.5.0
     */
    mergeData: function(data) {
        if (!this.dirty) {
            this.set(data, this._commitOptions);
        }
    },
    /**
     * @method save
     * @localdoc Saves the model instance using the configured proxy.  The save action
     * is asynchronous.  Any processing of the saved record should be done in a callback.
     *
     * Create example:
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User({
     *         name: 'Foo'
     *     });
     *
     *     // pass the phantom record data to the server to be saved
     *     user.save({
     *         failure: function(record, operation) {
     *             // do something if the save failed
     *         },
     *         success: function(record, operation) {
     *             // do something if the save succeeded
     *         },
     *         callback: function(record, operation, success) {
     *             // do something whether the save succeeded or failed
     *         }
     *     });
     *
     * The response from a create operation should include the ID for the newly created
     * record:
     *
     *     // sample response
     *     {
     *         success: true,
     *         id: 1
     *     }
     *
     *     // the id may be nested if the proxy's reader has a rootProperty config
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url',
     *             reader: {
     *                 type: 'ajax',
     *                 rootProperty: 'data'
     *             }
     *         }
     *     });
     *
     *     // sample nested response
     *     {
     *         success: true,
     *         data: {
     *             id: 1
     *         }
     *     }
     *
     * (Create + ) Update example:
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User({
     *         name: 'Foo'
     *     });
     *     user.save({
     *         success: function(record, operation) {
     *             record.set('name', 'Bar');
     *             // updates the remote record via the proxy
     *             record.save();
     *         }
     *     });
     *
     * (Create + ) Destroy example - see also {@link #erase}:
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User({
     *         name: 'Foo'
     *     });
     *     user.save({
     *         success: function(record, operation) {
     *             record.drop();
     *             // destroys the remote record via the proxy
     *             record.save();
     *         }
     *     });
     *
     * **NOTE:** If a {@link #phantom} record is {@link #drop dropped} and subsequently
     * saved it will not be processed via the proxy.  However, any passed `success`
     * or `callback` functions will be called.
     *
     * The options param is an Operation config object containing success, failure and
     * callback functions, plus optional scope.  The type of Operation depends on the
     * state of the model being saved.
     *
     *  - {@link #phantom} model - {@link Ext.data.operation.Create}
     *  - {@link #isModified modified} model - {@link Ext.data.operation.Update}
     *  - {@link #dropped} model - {@link Ext.data.operation.Destroy}
     *
     * @inheritdoc #method-load
     * @return {Ext.data.operation.Create/Ext.data.operation.Update/Ext.data.operation.Destroy}
     * The operation instance for saving this model.  The type of operation returned
     * depends on the model state at the time of the action.
     *
     *  - {@link #phantom} model - {@link Ext.data.operation.Create}
     *  - {@link #isModified modified} model - {@link Ext.data.operation.Update}
     *  - {@link #dropped} model - {@link Ext.data.operation.Destroy}
     */
    save: function(options) {
        options = Ext.apply({}, options);
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            phantom = me.phantom,
            dropped = me.dropped,
            action = dropped ? 'destroy' : (phantom ? 'create' : 'update'),
            scope = options.scope || me,
            callback = options.callback,
            proxy = me.getProxy(),
            operation;
        options.records = [
            me
        ];
        options.internalCallback = function(operation) {
            var args = [
                    me,
                    operation
                ],
                success = operation.wasSuccessful();
            if (success) {
                Ext.callback(options.success, scope, args);
            } else {
                Ext.callback(options.failure, scope, args);
            }
            args.push(success);
            Ext.callback(callback, scope, args);
        };
        delete options.callback;
        operation = proxy.createOperation(action, options);
        // Not a phantom, then we must perform this operation on the remote datasource.
        // Record will be removed from the store in the callback upon a success response
        if (dropped && phantom) {
            // If it's a phantom, then call the callback directly with a dummy successful ResultSet
            operation.setResultSet(Ext.data.reader.Reader.prototype.nullResultSet);
            me.setErased();
            operation.setSuccessful(true);
        } else {
            operation.execute();
        }
        return operation;
    },
    //-------------------------------------------------------------------------
    // Statics
    statics: {
        /**
         * @property {String/Object}
         * The default proxy to use for instances of this Model when no proxy is configured
         * on the instance.  When specified, the model will use this proxy instead of
         * requesting one from the {@link Ext.data.Session Session}.
         *
         * Can be a string "type", or a {@link Ext.data.proxy.Proxy Proxy} config object.
         *
         * This proxy is not inherited by subclasses.
         * @static
         * @protected
         */
        defaultProxy: 'memory'
    },
    inheritableStatics: {
        /**
         * @property {Object} _associatedReadOptions
         * The options for the proxy reader for loadData.
         *
         * @private
         */
        _associatedReadOptions: {
            recordsOnly: true,
            asRoot: true
        },
        /**
         * Create a model while also parsing any data for associations.
         * @param {Object} data The model data, including any associated data if required.
         * The type of data should correspond to what the configured data reader would expect.
         * @param {Ext.data.Session} [session] The session.
         * @return {Ext.data.Model} The model.
         *
         * @static
         * @inheritable
         * @since 6.5.0
         */
        loadData: function(data, session) {
            var rec;
            if (data) {
                /* eslint-disable-next-line max-len, newline-per-chained-call */
                rec = this.getProxy().getReader().readRecords([
                    data
                ], session ? {
                    recordCreator: session.recordCreator
                } : undefined, this._associatedReadOptions)[0];
            } else {
                rec = new this(data, session);
            }
            return rec;
        },
        /**
         * Get the summary model type. If {@link #summary} is specified, it is
         * a new type that extends from this type. If not, then it is the same
         * model type.
         * @return {Ext.Class} The summary model type.
         *
         * @static
         * @inheritable
         * @since 6.5.0
         */
        getSummaryModel: function() {
            var me = this,
                proto = me.prototype,
                summaryModel = me.summaryModel;
            if (!summaryModel) {
                summaryModel = Ext.define(null, {
                    extend: me,
                    fields: proto.summaryFields || [],
                    isSummaryModel: true
                });
                summaryModel.isSummaryModel = true;
                me.summaryModel = proto.summaryModel = summaryModel;
            }
            return summaryModel || null;
        },
        /**
         * This method adds the given set of fields to this model class.
         *
         * @param {String[]/Object[]} newFields The new fields to add. Based on the `name`
         * of a field this may replace a previous field definition.
         *
         * @protected
         * @static
         * @inheritable
         * @since 5.0.0
         */
        addFields: function(newFields) {
            this.replaceFields(newFields);
        },
        /**
         * This method replaces the specified set of fields with a given set of new fields.
         * Fields should normally be considered immutable, but if the timing is right (that
         * is, before derived classes are declared), it is permissible to change the fields
         * collection.
         *
         * @param {String[]/Object[]} newFields The new fields to add. Based on the `name`
         * of a field this may replace a previous field definition.
         * @param {Boolean/String[]} removeFields The names of fields to remove or `true`
         * to remove all existing fields. Removes are processed first followed by adds so
         * if a field name appears in `newFields` as well that field will effectively be
         * added (however, in that case there is no need to include the field in this
         * array).
         *
         * @protected
         * @static
         * @inheritable
         * @since 5.0.0
         */
        replaceFields: function(newFields, removeFields) {
            var me = this,
                proto = me.prototype,
                Field = Ext.data.field.Field,
                fields = me.fields,
                fieldsMap = me.fieldsMap,
                ordinals = me.fieldOrdinals,
                field, i, idField, len, name, ordinal, cleared;
            if (removeFields === true) {
                fields.length = 0;
                me.fieldsMap = fieldsMap = {};
                me.fieldOrdinals = ordinals = {};
                cleared = true;
            } else if (removeFields) {
                for (i = removeFields.length; i-- > 0; ) {
                    name = removeFields[i];
                    if (name in ordinals) {
                        delete ordinals[name];
                        delete fieldsMap[name];
                    }
                }
                for (i = 0 , len = fields.length; i < len; ++i) {
                    name = (field = fields[i]).name;
                    if (name in ordinals) {
                        ordinals[name] = i;
                    } else {
                        // This field is being removed (it is no longer in ordinals).
                        fields.splice(i, 1);
                        --i;
                        --len;
                    }
                }
            }
            // we need to do this forwards so that ordinals don't become
            // invalid due to a splice
            for (i = 0 , len = newFields ? newFields.length : 0; i < len; i++) {
                name = (field = newFields[i]).name;
                if (!(name in ordinals)) {
                    ordinals[name] = ordinal = fields.length;
                    // 0-based
                    fields.push(field = Field.create(field));
                    fieldsMap[name] = field;
                    field.ordinal = ordinal;
                    field.definedBy = field.owner = this;
                }
            }
            // Ext.data.NodeInterface
            // Reset all ranks if we didn't get cleared, since this could
            // alter the dependencies
            if (!cleared) {
                for (i = 0 , len = fields.length; i < len; ++i) {
                    fields[i].rank = null;
                }
            }
            // The idField could have been replaced, so reacquire it.
            me.idField = proto.idField = idField = fieldsMap[proto.idProperty];
            if (idField) {
                idField.allowNull = idField.critical = idField.identifier = true;
                idField.defaultValue = null;
            }
            // In case we've created the initializer we need to zap it so we recreate it
            // next time. Likewise with field ranking.
            me.initializeFn = me.rankedFields = me.transientFields = me.criticalFields = null;
        },
        /**
         * Removes the given set of fields from this model.
         *
         * @param {Boolean/String[]} removeFields The names of fields to remove or `true`
         * to remove all existing fields. Removes are processed first followed by adds so
         * if a field name appears in `newFields` as well that field will effectively be
         * added (however, in that case there is no need to include the field in this
         * array).
         *
         * @protected
         * @static
         * @inheritable
         * @since 5.0.0
         */
        removeFields: function(removeFields) {
            this.replaceFields(null, removeFields);
        },
        /**
         * @private
         * @static
         * @inheritable
         */
        getIdFromData: function(data) {
            var T = this,
                idField = T.idField,
                id = idField.calculated ? (new T(data)).id : data[idField.name];
            return id;
        },
        /**
         * @private
         * @static
         * @inheritable
         */
        createWithId: function(id, data, session) {
            var d = data,
                T = this;
            if (id || id === 0) {
                d = {};
                if (data) {
                    Ext.apply(d, data);
                }
                d[T.idField.name] = id;
            }
            return new T(d, session);
        },
        /**
         * @private
         * @static
         * @inheritable
         */
        getFields: function() {
            return this.fields;
        },
        /**
         * @private
         * @static
         * @inheritable
         */
        getFieldsMap: function() {
            return this.fieldsMap;
        },
        /**
         * @private
         * @static
         * @inheritable
         */
        getField: function(name) {
            return this.fieldsMap[name] || null;
        },
        /**
         * Returns the configured Proxy for this Model.
         * @return {Ext.data.proxy.Proxy} The proxy
         * @static
         * @inheritable
         */
        getProxy: function() {
            var me = this,
                proxy = me.proxy,
                defaultProxy = me.defaultProxy,
                defaults;
            if (!proxy) {
                // Check what was defined by the class (via onClassExtended):
                proxy = me.proxyConfig;
                if (!proxy && defaultProxy) {
                    proxy = defaultProxy;
                }
                if (!proxy || !proxy.isProxy) {
                    if (typeof proxy === 'string') {
                        proxy = {
                            type: proxy
                        };
                    }
                    // We have nothing or a config for the proxy. Get some defaults from
                    // the Schema and smash anything we've provided over the top.
                    defaults = Ext.merge(me.schema.constructProxy(me), proxy);
                    if (proxy && proxy.type) {
                        proxy = proxy.schema === false ? proxy : defaults;
                    } else {
                        proxy = defaults;
                    }
                }
                proxy = me.setProxy(proxy);
            }
            return proxy;
        },
        /**
         * Sets the Proxy to use for this model. Accepts any options that can be accepted by
         * {@link Ext#createByAlias Ext.createByAlias}.
         * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy
         * @return {Ext.data.proxy.Proxy}
         * @static
         * @inheritable
         */
        setProxy: function(proxy) {
            var me = this,
                model;
            if (proxy) {
                if (!proxy.isProxy) {
                    proxy = Ext.Factory.proxy(proxy);
                } else {
                    model = proxy.getModel();
                    if (model && model !== me) {
                        proxy = proxy.clone();
                    }
                }
                proxy.setModel(me);
            }
            return (me.prototype.proxy = me.proxy = proxy);
        },
        /**
         * Asynchronously loads a model instance by id. Any processing of the loaded
         * record should be done in a callback.
         *
         * Sample usage:
         *
         *     Ext.define('MyApp.User', {
         *         extend: 'Ext.data.Model',
         *         fields: [
         *             {name: 'id', type: 'int'},
         *             {name: 'name', type: 'string'}
         *         ]
         *     });
         *
         *     MyApp.User.load(10, {
         *         scope: this,
         *         failure: function(record, operation) {
         *             //do something if the load failed
         *         },
         *         success: function(record, operation) {
         *             //do something if the load succeeded
         *         },
         *         callback: function(record, operation, success) {
         *             //do something whether the load succeeded or failed
         *         }
         *     });
         *
         * @param {Number/String} id The ID of the model to load.
         * **NOTE:** The model returned must have an ID matching the param in the load
         * request.
         *
         * @param {Object} [options] The options param is an
         * {@link Ext.data.operation.Read} config object containing success, failure and
         * callback functions, plus optional scope.
         *
         * @param {Function} options.success A function to be called when the
         * model is processed by the proxy successfully.
         * The callback is passed the following parameters:
         * @param {Ext.data.Model} options.success.record The record.
         * @param {Ext.data.operation.Operation} options.success.operation The operation.
         * 
         * @param {Function} options.failure A function to be called when the
         * model is unable to be processed by the server.
         * The callback is passed the following parameters:
         * @param {Ext.data.Model} options.failure.record The record.
         * @param {Ext.data.operation.Operation} options.failure.operation The operation.
         * 
         * @param {Function} options.callback A function to be called whether the proxy
         * transaction was successful or not.
         * The callback is passed the following parameters:
         * @param {Ext.data.Model} options.callback.record The record.
         * @param {Ext.data.operation.Operation} options.callback.operation The
         * operation.
         * @param {Boolean} options.callback.success `true` if the operation was
         * successful.
         * 
         * @param {Object} options.scope The scope in which to execute the callback
         * functions.  Defaults to the model instance.
         *
         * @param {Ext.data.Session} [session] The session for this record.
         *
         * @return {Ext.data.Model} The newly created model. Note that the model will
         * (probably) still be loading once it is returned from this method. To do any
         * post-processing on the data, the appropriate place to do see is in the
         * callback.
         * 
         * @static
         * @inheritable
         */
        load: function(id, options, session) {
            var data = {},
                rec;
            if (session) {
                rec = session.peekRecord(this, id);
            }
            if (!rec) {
                data[this.prototype.idProperty] = id;
                rec = new this(data, session);
            }
            rec.load(options);
            return rec;
        }
    },
    deprecated: {
        5: {
            methods: {
                hasId: null,
                markDirty: null,
                setDirty: null,
                eachStore: function(callback, scope) {
                    var me = this,
                        stores = me.stores,
                        len = stores.length,
                        i;
                    for (i = 0; i < len; ++i) {
                        callback.call(scope, stores[i]);
                    }
                },
                join: function(item) {
                    var me = this,
                        stores = me.stores,
                        joined = me.joined;
                    if (!joined) {
                        joined = me.joined = [
                            item
                        ];
                    } else {
                        joined.push(item);
                    }
                    if (item.isStore) {
                        me.store = me.store || item;
                        if (!stores) {
                            stores = me.stores = [];
                        }
                        stores.push(item);
                    }
                },
                unjoin: function(item) {
                    var me = this,
                        stores = me.stores,
                        joined = me.joined;
                    if (joined.length === 1) {
                        joined.length = 0;
                    } else {
                        Ext.Array.remove(joined, item);
                    }
                    if (item.isStore) {
                        Ext.Array.remove(stores, item);
                        me.store = stores[0] || null;
                    }
                }
            },
            properties: {
                persistenceProperty: null
            },
            inheritableStatics: {
                methods: {
                    setFields: null
                }
            }
        }
    },
    //-------------------------------------------------------------------------
    privates: {
        _commitOptions: {
            commit: true
        },
        _getChangesOptions: {
            changes: true
        },
        _getAssociatedOptions: {
            associated: true
        },
        _getNotAssociatedOptions: {
            associated: false
        },
        _metaProperties: {
            dirty: 'isDirty',
            phantom: 'isPhantom',
            valid: 'isValid'
        },
        /**
         * Copies data from the passed record into this record. If the passed record is undefined,
         * does nothing.
         *
         * If this is a phantom record (represented only in the client, with no corresponding
         * database entry), and the source record is not a phantom, then this record acquires
         * the id of the source record.
         *
         * @param {Ext.data.Model} sourceRecord The record to copy data from.
         * @return {String[]} The names of the fields which changed value.
         * @private
         */
        copyFrom: function(sourceRecord) {
            var me = this,
                fields = me.fields,
                fieldCount = fields.length,
                modifiedFieldNames = [],
                idProperty = me.idProperty,
                i = 0,
                field, myData, sourceData, name, value;
            if (sourceRecord) {
                myData = me.data;
                sourceData = sourceRecord.data;
                for (; i < fieldCount; i++) {
                    field = fields[i];
                    name = field.name;
                    // Do not use setters.
                    // Copy returned values in directly from the data object.
                    // Converters have already been called because new Records
                    // have been created to copy from.
                    // This is a direct record-to-record value copy operation.
                    // don't copy the id, we'll do it at the end
                    if (name !== idProperty) {
                        value = sourceData[name];
                        // If source property is specified, and value is different
                        // copy field value in and build updatedFields
                        if (value !== undefined && !me.isEqual(myData[name], value)) {
                            myData[name] = value;
                            modifiedFieldNames.push(name);
                        }
                    }
                }
                // If this is a phantom record being updated from a concrete record, copy the ID in.
                if (me.phantom && !sourceRecord.phantom) {
                    // beginEdit to prevent events firing
                    // commit at the end to prevent dirty being set
                    me.beginEdit();
                    me.setId(sourceRecord.getId());
                    me.endEdit(true);
                    me.commit(true);
                }
            }
            return modifiedFieldNames;
        },
        /**
         * Helper function used by afterEdit, afterReject and afterCommit. Calls the given
         * method on the `Ext.data.Store` that this instance has {@link #join joined}, if any.
         * The store function will always be called with the model instance as its single
         * argument. If this model is joined to a Ext.data.NodeStore, then this method calls
         * the given method on the NodeStore and the associated Ext.data.TreeStore.
         * @param {String} funcName The name function to call on each store.
         * @param {Array} [args] The arguments to pass to the method. This instance is
         * always inserted as the first argument.
         * @private
         */
        callJoined: function(funcName, args) {
            var me = this,
                joined = me.joined,
                session = me.session,
                state = me.dropped ? 'D' : (me.phantom ? 'C' : (me.dirty ? 'U' : 'R')),
                i, len, fn, item;
            me.crudState = state;
            if (joined || session) {
                if (args) {
                    args.unshift(me);
                } else {
                    args = [
                        me
                    ];
                }
                fn = session && session[funcName];
                if (fn) {
                    fn.apply(session, args);
                }
                if (joined) {
                    for (i = 0 , len = joined.length; i < len; ++i) {
                        item = joined[i];
                        if (item && (fn = item[funcName])) {
                            fn.apply(item, args);
                        }
                    }
                }
            }
            me.crudStateWas = state;
        },
        /**
         * Currently this only checks the loading state, this method exists for API
         * parity with stores.
         * @return {Boolean} `true` if the model is loading or has a pending load.
         *
         * @private
         */
        hasPendingLoad: function() {
            return this.isLoading();
        },
        interpret: function(name) {
            var me = this,
                accessor = me._metaProperties[name];
            if (!accessor) {
                accessor = me.associations;
                // e.g. "orderItems"
                accessor = accessor && accessor[name] && accessor[name].getterName;
            }
            if (accessor) {
                return me[accessor]();
            }
            // e.g., me.isPhantom()
            return me.data[name];
        },
        /**
         * Gets the dirty state of this record.
         * @return {Boolean} The dirty state.
         *
         * @private
         */
        isDirty: function() {
            // Added as a method to be used by data binding
            return this.dirty;
        },
        /**
         * Gets the phantom state of this record.
         * @return {Boolean} The phantom state.
         *
         * @private
         */
        isPhantom: function() {
            // Added as a method to be used by data binding
            return this.phantom;
        },
        /**
         * Called when an associated record instance has been set.
         * @param {Ext.data.Model} record The record.
         * @param {Ext.data.schema.Role} role The role.
         *
         * @private
         */
        onAssociatedRecordSet: function(record, role) {
            this.callJoined('afterAssociatedRecordSet', [
                record,
                role
            ]);
        },
        /**
         * @method
         * Called when the model id is changed.
         * @param {Object} id The new id.
         * @param {Object} oldId The old id.
         */
        onIdChanged: Ext.privateFn,
        /**
         * Set the session for this record.
         * @param {Ext.data.Session} session The session
         */
        setSession: function(session) {
            if (session) {
                if (this.session) {
                    Ext.raise('This model already belongs to a session.');
                }
                if (!this.id) {
                    Ext.raise('The model must have an id to participate in a session.');
                }
            }
            this.session = session;
            if (session) {
                session.add(this);
            }
        },
        /**
         * Gets the names of all the fields that were modified during an edit.
         * @param {Object} [old] The saved data from `beginEdit`.
         * @return {String[]} The array of modified field names.
         * @private
         */
        getModifiedFieldNames: function(old) {
            var me = this,
                data = me.data,
                modified = [],
                oldData = old || me.editMemento.data,
                key;
            for (key in data) {
                if (data.hasOwnProperty(key)) {
                    if (!me.isEqual(data[key], oldData[key], key)) {
                        modified.push(key);
                    }
                }
            }
            return modified;
        },
        /**
         * Checks if two values are equal, taking into account certain special factors, for
         * example dates.
         * @param {Object} lhs The first value.
         * @param {Object} rhs The second value.
         * @param {String/Ext.data.Field} [field] The field name or instance.
         * @return {Boolean} True if the values are equal.
         * @private
         */
        isEqual: function(lhs, rhs, field) {
            var f;
            if (field) {
                f = field.isField ? field : this.fieldsMap[field];
                if (f) {
                    return f.isEqual(lhs, rhs);
                }
            }
            // instanceof is ~10 times faster then Ext.isDate. Values here will not be
            // cross-document objects
            if (lhs instanceof Date && rhs instanceof Date) {
                return lhs.getTime() === rhs.getTime();
            }
            return lhs === rhs;
        },
        statics: {
            /**
             * @property
             * @static
             * @private
             * @readonly
             * @deprecated 5.0 Use the string `"edit"` directly.
             * The update operation of type 'edit'. Used by the
             * {@link Ext.data.Store#event-update Store.update} event.
             */
            EDIT: 'edit',
            /**
             * @property
             * @static
             * @private
             * @readonly
             * @deprecated 5.0 Use the string `"reject"` directly.
             * The update operation of type 'reject'. Used by the
             * {@link Ext.data.Store#event-update Store.update} event.
             */
            REJECT: 'reject',
            /**
             * @property
             * @static
             * @private
             * @readonly
             * @deprecated 5.0 Use the string `"commit"` directly.
             * The update operation of type 'commit'. Used by the
             * {@link Ext.data.Store#event-update Store.update} event.
             */
            COMMIT: 'commit',
            rankFields: function() {
                var cls = this,
                    prototype = cls.prototype,
                    fields = cls.fields,
                    length = fields.length,
                    rankedFields = [],
                    criticalFields = [],
                    transientFields = [],
                    evilFields, field, i;
                cls.rankedFields = prototype.rankedFields = rankedFields;
                cls.criticalFields = prototype.criticalFields = criticalFields;
                cls.transientFields = prototype.transientFields = transientFields;
                // This first pass brings over any fields that have no dependencies at all
                // and gathers the evil fields to the side (the fields that could depend on
                // anything). This avoids the call to topoAdd that we must perform on all of
                // the fields that do have depends (which is good since most fields will be
                // handled here).
                for (i = 0; i < length; ++i) {
                    field = fields[i];
                    if (field.critical) {
                        criticalFields.push(field);
                    }
                    if (!field.persist) {
                        transientFields.push(field);
                    }
                    if (field.evil) {
                        (evilFields || (evilFields = [])).push(field);
                    } else if (!field.depends) {
                        rankedFields.push(field);
                        field.rank = rankedFields.length;
                    }
                }
                // 1-based
                for (i = 0; i < length; ++i) {
                    if (!(field = fields[i]).rank && !field.evil) {
                        cls.topoAdd(field);
                    }
                }
                if (evilFields) {
                    for (i = 0 , length = evilFields.length; i < length; ++i) {
                        rankedFields.push(field = evilFields[i]);
                        field.rank = rankedFields.length;
                    }
                }
                // 1-based
                cls.topoStack = null;
                // cleanup diagnostic stack
                return rankedFields;
            },
            topoAdd: function(field) {
                var cls = this,
                    dep = field.depends,
                    dependsLength = dep ? dep.length : 0,
                    rankedFields = cls.rankedFields,
                    i, targetField;
                /* eslint-disable-next-line vars-on-top, one-var */
                var topoStack = cls.topoStack || (cls.topoStack = []);
                topoStack.push(field.name);
                if (field.rank === 0) {
                    // if (adding)
                    Ext.raise(cls.$className + " has circular field dependencies: " + topoStack.join(" --> "));
                }
                if (topoStack.length && field.evil) {
                    Ext.raise(cls.$className + ": Field " + topoStack[topoStack.length - 1] + " cannot depend on depends-less field " + field.name);
                }
                field.rank = 0;
                // adding (falsey but we can still detect cycles)
                for (i = 0; i < dependsLength; ++i) {
                    // Get the targetField on which we depend and add this field to the
                    // targetField.dependents[]
                    targetField = cls.fieldsMap[dep[i]];
                    if (!targetField) {
                        Ext.raise(cls.$className + ": Field " + field.name + " depends on undefined field " + dep[i]);
                    }
                    (targetField.dependents || (targetField.dependents = [])).push(field);
                    if (!targetField.rank) {
                        // if (!added)
                        cls.topoAdd(targetField);
                    }
                }
                rankedFields.push(field);
                field.rank = rankedFields.length;
                // 1-based (truthy to track "added" state)
                topoStack.pop();
            },
            initFields: function(data, cls, proto) {
                var Field = Ext.data.field.Field,
                    fieldDefs = data.fields,
                    // allocate fields [] and ordinals {} for the new class:
                    fields = [],
                    fieldOrdinals = {},
                    fieldsMap = {},
                    references = [],
                    superFields = proto.fields,
                    versionProperty = data.versionProperty || proto.versionProperty,
                    idProperty = cls.idProperty,
                    idField, field, i, length, name, ordinal, reference, superIdField, superIdFieldName, superIdDeclared, idDeclared;
                // Process any inherited fields to produce a fields [] and ordinals {} for
                // this class:
                cls.fields = proto.fields = fields;
                cls.fieldOrdinals = proto.fieldOrdinals = fieldOrdinals;
                cls.fieldsMap = proto.fieldsMap = fieldsMap;
                cls.references = proto.references = references;
                if (superFields) {
                    // We chain the super field so we can write to it
                    for (i = 0 , length = superFields.length; i < length; ++i) {
                        fields[i] = field = Ext.Object.chain(superFields[i]);
                        field.dependents = null;
                        // we need to recalculate these
                        field.owner = cls;
                        fieldOrdinals[name = field.name] = i;
                        fieldsMap[name] = field;
                        // Clear the rank because it needs to be set on the first pass through
                        // the fields in the subclass, don't inherit it from the parent
                        field.rank = null;
                        if (field.generated) {
                            superIdField = field;
                            superIdFieldName = field.name;
                        }
                    }
                }
                // Merge in any fields from this class:
                delete data.fields;
                if (fieldDefs) {
                    for (i = 0 , length = fieldDefs.length; i < length; ++i) {
                        field = fieldDefs[i];
                        reference = field.reference;
                        // Create a copy of the reference since we'll modify
                        // the reference on the field. Needed for subclasses
                        if (reference && typeof reference !== 'string') {
                            // Can have child objects, so merge it deeply
                            reference = Ext.merge({}, reference);
                        }
                        field.$reference = reference;
                        field = Field.create(fieldDefs[i]);
                        name = field.name;
                        ordinal = fieldOrdinals[name];
                        if (ordinal === undefined) {
                            // If the field is new, add it to the end of the fields[]
                            fieldOrdinals[name] = ordinal = fields.length;
                        }
                        // else, overwrite the field at the established ordinal
                        fieldsMap[name] = field;
                        fields[ordinal] = field;
                        field.definedBy = field.owner = cls;
                        field.ordinal = ordinal;
                        if (name === idProperty) {
                            idDeclared = field;
                        }
                        if (name === superIdFieldName) {
                            superIdDeclared = true;
                        }
                    }
                }
                // Lookup the idProperty in the ordinals map and create a synthetic field if
                // we don't have one.
                idField = fieldsMap[idProperty];
                if (!idField) {
                    if (superIdField && superIdField.generated) {
                        ordinal = superIdField.ordinal;
                    } else {
                        ordinal = fields.length;
                    }
                    delete fieldsMap[superIdFieldName];
                    delete fieldOrdinals[superIdFieldName];
                    idField = new Field(idProperty);
                    fields[ordinal] = idField;
                    fieldOrdinals[idProperty] = ordinal;
                    fieldsMap[idProperty] = idField;
                    idField.definedBy = cls;
                    idField.ordinal = ordinal;
                    idField.generated = true;
                } else if (idDeclared && !superIdDeclared && superIdField && superIdField.generated) {
                    // If we're declaring the id as a field in our fields array and it's different
                    // to the super id field that has been generated, pull it out and fix up
                    // the ordinals. This likely won't happen often, to do it earlier we would need
                    // to know the contents of the fields which would mean iterating over them
                    // twice.
                    Ext.Array.remove(fields, superIdField);
                    delete fieldsMap[superIdFieldName];
                    delete fieldOrdinals[superIdFieldName];
                    fieldsMap[idProperty] = idDeclared;
                    for (i = 0 , length = fields.length; i < length; ++i) {
                        field = fields[i];
                        fields.ordinal = i;
                        fieldOrdinals[field.name] = i;
                    }
                }
                idField.allowNull = idField.critical = idField.identifier = true;
                idField.defaultValue = null;
                cls.idField = proto.idField = idField;
                if (versionProperty) {
                    field = fieldsMap[versionProperty];
                    if (!field) {
                        ordinal = fields.length;
                        field = new Field({
                            name: versionProperty,
                            type: 'int'
                        });
                        fields[ordinal] = field;
                        fieldOrdinals[versionProperty] = ordinal;
                        fieldsMap[versionProperty] = field;
                        field.definedBy = cls;
                        field.ordinal = ordinal;
                        field.generated = true;
                    }
                    field.defaultValue = 1;
                    field.critical = true;
                }
            },
            // NOTE: Be aware that the one fellow that manipulates these after this
            // point is Ext.data.NodeInterface.
            initSummaries: function(data, cls, proto) {
                var summaryDefs = data.summary,
                    superSummaries = proto.summaryFields,
                    summaries, summaryMap, name, summary, len, i, index, field;
                if (superSummaries) {
                    summaries = [];
                    summaryMap = {};
                    for (i = 0 , len = superSummaries.length; i < len; ++i) {
                        summary = superSummaries[i];
                        summaries.push(summary);
                        summaries[summary.name] = i;
                    }
                }
                if (summaryDefs) {
                    delete data.summary;
                    summaries = summaries || [];
                    summaryMap = summaryMap || {};
                    for (name in summaryDefs) {
                        summary = summaryDefs[name];
                        if (typeof summary === 'function') {
                            summary = {
                                summary: summary
                            };
                        }
                        // If it's not in the summaries, it's new here. We've already
                        // applied when copying down so this is safe to do
                        index = summaryMap[name];
                        summary = Ext.apply({
                            name: name
                        }, summary);
                        field = summary.field;
                        if (field) {
                            delete summary.field;
                            summary.summaryField = field;
                        }
                        if (index === undefined) {
                            index = summaries.length;
                            summaryMap[name] = summary;
                        }
                        summaries[index] = summary;
                    }
                }
                if (summaries) {
                    for (i = 0 , len = summaries.length; i < len; ++i) {
                        if (summaries[i].name in proto.fieldsMap) {
                            Ext.raise('Cannot redefine field, use the summary property ' + 'on the field.');
                        }
                    }
                    // Store these in an array so we have a predictable order when subclassing
                    proto.summaryFields = summaries;
                }
            },
            initValidators: function(data, cls, proto) {
                var superValidators = proto.validators,
                    validators, field, copy, validatorDefs, i, length, fieldValidator, name, validator, item;
                if (superValidators) {
                    validators = {};
                    for (field in superValidators) {
                        validators[field] = Ext.Array.clone(superValidators[field]);
                    }
                }
                validatorDefs = data.validators || data.validations;
                if (data.validations) {
                    delete data.validations;
                    Ext.log.warn((cls.$className || 'Ext.data.Model') + ': validations has been deprecated. Please use validators ' + 'instead.');
                }
                if (validatorDefs) {
                    delete data.validators;
                    validators = validators || {};
                    // Support older array syntax
                    if (Ext.isArray(validatorDefs)) {
                        copy = {};
                        for (i = 0 , length = validatorDefs.length; i < length; ++i) {
                            item = validatorDefs[i];
                            name = item.field;
                            if (!copy[name]) {
                                copy[name] = [];
                            }
                            // Check for function form
                            item = item.fn || item;
                            copy[name].push(item);
                        }
                        validatorDefs = copy;
                    }
                    for (name in validatorDefs) {
                        fieldValidator = validatorDefs[name];
                        if (!Ext.isArray(fieldValidator)) {
                            fieldValidator = [
                                fieldValidator
                            ];
                        }
                        validator = validators[name];
                        if (validator) {
                            // Declared in super
                            Ext.Array.push(validator, fieldValidator);
                        } else {
                            validators[name] = fieldValidator;
                        }
                    }
                }
                if (validators) {
                    for (name in validators) {
                        field = cls.getField(name);
                        if (field) {
                            field.setModelValidators(validators[name]);
                        }
                    }
                }
                cls.validators = proto.validators = validators;
            },
            initAssociations: function(schema, data, cls) {
                // Handle keyless associations
                var associations = data.associations,
                    belongsTo = data.belongsTo,
                    hasMany = data.hasMany,
                    hasOne = data.hasOne,
                    // manyToMany can't be declared via reference
                    matrices = data.manyToMany,
                    i, length, assoc, o;
                delete data.associations;
                delete data.belongsTo;
                delete data.hasMany;
                delete data.hasOne;
                delete data.manyToMany;
                if (matrices) {
                    schema.addMatrices(cls, matrices);
                }
                if (associations) {
                    associations = Ext.isArray(associations) ? associations : [
                        associations
                    ];
                    for (i = 0 , length = associations.length; i < length; ++i) {
                        assoc = associations[i];
                        o = Ext.apply({}, assoc);
                        delete o.type;
                        switch (assoc.type) {
                            case 'belongsTo':
                                schema.addBelongsTo(cls, o);
                                break;
                            case 'hasMany':
                                schema.addHasMany(cls, o);
                                break;
                            case 'hasOne':
                                schema.addHasOne(cls, o);
                                break;
                            default:
                                Ext.raise('Invalid association type: "' + assoc.type + '"');
                        }
                    }
                }
                if (belongsTo) {
                    belongsTo = Ext.isArray(belongsTo) ? belongsTo : [
                        belongsTo
                    ];
                    for (i = 0 , length = belongsTo.length; i < length; ++i) {
                        schema.addBelongsTo(cls, belongsTo[i]);
                    }
                }
                if (hasMany) {
                    hasMany = Ext.isArray(hasMany) ? hasMany : [
                        hasMany
                    ];
                    for (i = 0 , length = hasMany.length; i < length; ++i) {
                        schema.addHasMany(cls, hasMany[i]);
                    }
                }
                if (hasOne) {
                    hasOne = Ext.isArray(hasOne) ? hasOne : [
                        hasOne
                    ];
                    for (i = 0 , length = hasOne.length; i < length; ++i) {
                        schema.addHasOne(cls, hasOne[i]);
                    }
                }
                schema.afterKeylessAssociations(cls);
            },
            initIdentifier: function(data, cls, proto) {
                var identifier = data.identifier || data.idgen,
                    superIdent = proto.identifier || cls.schema._defaultIdentifier,
                    generatorPrefix;
                if (data.idgen) {
                    Ext.log.warn('Ext.data.Model: idgen has been deprecated. Please use ' + 'identifier instead.');
                }
                if (identifier) {
                    delete data.identifier;
                    delete data.idgen;
                    // An idgen was specified on the definition, use it explicitly.
                    identifier = Ext.Factory.dataIdentifier(identifier);
                } else if (superIdent) {
                    // If we have a cloneable instance, and we don't have an id
                    // clone it. If we have an id, then we should use the same
                    // instance since it's the same as looking it up via id.
                    if (superIdent.clone && !superIdent.getId()) {
                        identifier = superIdent.clone();
                    } else if (superIdent.isGenerator) {
                        identifier = superIdent;
                    } else {
                        identifier = Ext.Factory.dataIdentifier(superIdent);
                    }
                }
                cls.identifier = proto.identifier = identifier;
                if (!identifier) {
                    // If we didn't find one, create it and push it onto the class.
                    // Don't put it on the prototype, so a subclass will create
                    // it's own generator. If we have an anonymous model, go ahead and
                    // generate a unique prefix for it.
                    generatorPrefix = cls.entityName;
                    if (!generatorPrefix) {
                        generatorPrefix = Ext.id(null, 'extModel');
                    }
                    cls.identifier = Ext.Factory.dataIdentifier({
                        type: 'sequential',
                        prefix: generatorPrefix + '-'
                    });
                }
            },
            findValidator: function(validators, name, cfg) {
                var type = cfg.type || cfg,
                    field = validators[name],
                    len, i, item;
                if (field) {
                    for (i = 0 , len = field.length; i < len; ++i) {
                        item = field[i];
                        if (item.type === type) {
                            return item;
                        }
                    }
                }
                return null;
            },
            /**
             * This method produces the `initializeFn` for this class. If there are no fields
             * requiring {@link Ext.data.field.Field#cfg-convert conversion} and no fields requiring
             * a {@link Ext.data.field.Field#defaultValue default value} then this method will
             * return `null`.
             * @return {Function} The `initializeFn` for this class (or null).
             * @private
             */
            makeInitializeFn: function(cls) {
                var code = [
                        'var '
                    ],
                    body = [
                        '\nreturn function (e) {\n    var data = e.data, v;\n'
                    ],
                    work = 0,
                    bc, ec, // == beginClone, endClone
                    convert, expr, factory, field, fields, fs, hasDefValue, i, length;
                if (!(fields = cls.rankedFields)) {
                    // On the first edit of a record of this type we need to ensure we have the
                    // topo-sort done:
                    fields = cls.rankFields();
                }
                for (i = 0 , length = fields.length; i < length; ++i) {
                    // The generated method declares vars for each field using "f0".."fN' as the
                    // name. These are used to access properties of the field (e.g., the convert
                    // method or defaultValue).
                    field = fields[i];
                    fs = 'f' + i;
                    convert = field.convert;
                    if (i) {
                        code.push(',  \n    ');
                    }
                    code.push(fs, ' = $fields[' + i + ']');
                    // this can be helpful when debugging (at least in Chrome):
                    code.push('  /*  ', field.name, '  */');
                    // NOTE: added string literals are "folded" by the compiler so we
                    // are better off doing an "'foo' + 'bar'" then "'foo', 'bar'". But
                    // for variables we are better off pushing them into the array for
                    // the final join.
                    if ((hasDefValue = (field.defaultValue !== undefined)) || convert) {
                        // For non-calculated fields that have some work required (a convert method
                        // and/or defaultValue), generate a chunk of logic appropriate for the
                        // field.
                        // expr = data["fieldName"];
                        expr = 'data["' + field.name + '"]';
                        ++work;
                        bc = ec = '';
                        if (field.cloneDefaultValue) {
                            bc = 'Ext.clone(';
                            ec = ')';
                        }
                        body.push('\n');
                        if (convert && hasDefValue) {
                            // v = data.fieldName;
                            // if (v !== undefined) {
                            //     v = f2.convert(v, e);
                            // }
                            // if (v === undefined) {
                            //     v = f2.defaultValue;
                            //      // or
                            //     v = Ext.clone(f2.defaultValue);
                            // }
                            // data.fieldName = v;
                            //
                            body.push('    v = ', expr, ';\n' + '    if (v !== undefined) {\n' + '        v = ', fs, '.convert(v, e);\n' + '    }\n' + '    if (v === undefined) {\n' + '        v = ', bc, fs, '.defaultValue', ec, ';\n' + '    }\n' + '    ', expr, ' = v;');
                        } else if (convert) {
                            // no defaultValue
                            // v = f2.convert(data.fieldName,e);
                            // if (v !== undefined) {
                            //     data.fieldName = v;
                            // }
                            //
                            body.push('    v = ', fs, '.convert(', expr, ',e);\n' + '    if (v !== undefined) {\n' + '        ', expr, ' = v;\n' + '    }\n');
                        } else if (hasDefValue) {
                            // no convert
                            // if (data.fieldName === undefined) {
                            //     data.fieldName = f2.defaultValue;
                            //          // or
                            //     data.fieldName = Ext.clone(f2.defaultValue);
                            // }
                            //
                            body.push('    if (', expr, ' === undefined) {\n' + '        ', expr, ' = ', bc, fs, '.defaultValue', ec, ';\n' + '    }\n');
                        }
                    }
                }
                if (!work) {
                    // There are no fields that need special processing
                    return Ext.emptyFn;
                }
                code.push(';\n');
                code.push.apply(code, body);
                code.push('}');
                code = code.join('');
                // Ensure that Ext in the function code refers to the same Ext that we are
                // using here. If we are in a sandbox, global.Ext might be different.
                factory = new Function('$fields', 'Ext', code);
                return factory(fields, Ext);
            }
        }
    }
}, // static
// privates
/* eslint-disable indent */
function() {
    var Model = this,
        proto = Model.prototype,
        Schema = Ext.data.schema.Schema,
        defaultSchema;
    Model.proxyConfig = proto.proxy;
    delete proto.proxy;
    // Base Model class may be used. It needs an empty fields array.
    Model.fields = [];
    // Base Model class may be used. It needs an empty fieldsMap hash.
    Model.fieldsMap = proto.fieldsMap = {};
    Model.schema = proto.schema = Schema.get(proto.schema);
    proto.idField = new Ext.data.field.Field(proto.idProperty);
    Model.identifier = new Ext.data.identifier.Sequential();
    Model.onExtended(function(cls, data) {
        var proto = cls.prototype,
            schemaName = data.schema,
            superCls = proto.superclass.self,
            schema, entityName, proxy;
        cls.idProperty = data.idProperty || proto.idProperty;
        if (schemaName) {
            delete data.schema;
            schema = Schema.get(schemaName);
        } else if (!(schema = proto.schema)) {
            schema = defaultSchema || (defaultSchema = Schema.get('default'));
        }
        // These are in "privates" so we manually make them inherited:
        cls.rankFields = Model.rankFields;
        cls.topoAdd = Model.topoAdd;
        // if we picked up a schema from cls.prototype.schema, it is because it was found
        // in the prototype chain on a base class.
        proto.schema = cls.schema = schema;
        // Unless specified on the declaration data, we need to provide the entityName of
        // the new Entity-derived class. Store it on the prototype and the class.
        if (!(entityName = data.entityName)) {
            proto.entityName = entityName = schema.getEntityName(cls);
            if (!entityName) {
                if (data.associations) {
                    Ext.raise('Anonymous entities cannot specify "associations"');
                }
                if (data.belongsTo) {
                    Ext.raise('Anonymous entities cannot specify "belongsTo"');
                }
                if (data.hasMany) {
                    Ext.raise('Anonymous entities cannot specify "hasMany"');
                }
                if (data.hasOne) {
                    Ext.raise('Anonymous entities cannot specify "hasOne"');
                }
                if (data.matrices) {
                    Ext.raise('Anonymous entities cannot specify "manyToMany"');
                }
            }
        }
        cls.entityName = entityName;
        cls.fieldExtractors = {};
        Model.initIdentifier(data, cls, proto);
        Model.initFields(data, cls, proto);
        Model.initValidators(data, cls, proto);
        if (!data.isSummaryModel) {
            Model.initSummaries(data, cls, proto);
        }
        // This is a compat hack to allow "rec.fields.items" to work as it used to when
        // fields was a MixedCollection
        cls.fields.items = cls.fields;
        if (entityName) {
            schema.addEntity(cls);
            Model.initAssociations(schema, data, cls);
        }
        proxy = data.proxy;
        if (proxy) {
            delete data.proxy;
        } else if (superCls !== Model) {
            proxy = superCls.proxyConfig || superCls.proxy;
        }
        cls.proxyConfig = proxy;
    });
});

/**
 * @protected
 * Simple wrapper class that represents a set of records returned by a Proxy.
 */
Ext.define('Ext.data.ResultSet', {
    /**
     * @property {Boolean} isResultSet
     * Identifies this class as a result set.
     */
    isResultSet: true,
    $configPrefixed: false,
    config: {
        /**
         * @cfg {Boolean} loaded
         * True if the records have already been loaded. This is only meaningful when dealing with
         * SQL-backed proxies.
         */
        loaded: true,
        /**
         * @cfg {Number} count
         * The number of records in this ResultSet. Note that total may differ from this number.
         */
        count: null,
        /**
         * @cfg {Number} total
         * The total number of records reported by the data source. This ResultSet may form
         * a subset of those records (see {@link #count}).
         */
        total: null,
        /**
         * @cfg {Number} remoteTotal
         * The total number of records reported by the remote data source.
         */
        remoteTotal: null,
        /**
         * @cfg {Boolean} success
         * True if the ResultSet loaded successfully, false if any errors were encountered.
         */
        success: false,
        /**
         * @cfg {Ext.data.Model[]/Object[]} records (required)
         * The array of record instances or record config objects.
         */
        records: null,
        /**
         * @cfg {String} message
         * The message that was read in from the data
         */
        message: null,
        /**
         * @cfg {Object} metadata
         * The metadata object from a server sourced JSON data packet.
         */
        metadata: null,
        /**
         * @cfg {Ext.data.Model[]} groupData
         * The grouping data.
         */
        groupData: null,
        /**
         * @cfg {Ext.data.Model} summaryData
         * The summary data.
         */
        summaryData: null
    },
    /**
     * Creates the resultSet
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        this.initConfig(config);
    },
    getCount: function() {
        var count = this.callParent(),
            records;
        if (!count) {
            records = this.getRecords();
            if (records) {
                count = records.length;
            }
        }
        return count;
    }
});

/**
 * Readers are used to interpret data to be loaded into a {@link Ext.data.Model Model} instance
 * or a {@link Ext.data.Store Store} - often in response to an AJAX request. In general there is
 * usually no need to create a Reader instance directly, since a Reader is almost always used
 * together with a {@link Ext.data.proxy.Proxy Proxy}, and is configured using the Proxy's
 * {@link Ext.data.proxy.Proxy#cfg-reader reader} configuration property:
 * 
 *     Ext.create('Ext.data.Store', {
 *         model: 'User',
 *         proxy: {
 *             type: 'ajax',
 *             url: 'users.json',
 *             reader: {
 *                 type: 'json',
 *                 rootProperty: 'users'
 *             }
 *         },
 *     });
 *     
 * The above reader is configured to consume a JSON string that looks something like this:
 *  
 *     {
 *         "success": true,
 *         "users": [
 *             { "name": "User 1" },
 *             { "name": "User 2" }
 *         ]
 *     }
 * 
 *
 * # Loading Nested Data
 *
 * Readers have the ability to automatically load deeply-nested data objects based on the
 * {@link Ext.data.schema.Association associations} configured on each Model. Below is an example
 * demonstrating the flexibility of these associations in a fictional CRM system which manages
 * a User, their Orders, OrderItems and Products. First we'll define the models:
 *
 *     Ext.define("User", {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             'id', 'name'
 *         ],
 *
 *         hasMany: {model: 'Order', name: 'orders'},
 *
 *         proxy: {
 *             type: 'rest',
 *             url : 'users.json',
 *             reader: {
 *                 type: 'json',
 *                 rootProperty: 'users'
 *             }
 *         }
 *     });
 *
 *     Ext.define("Order", {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             'id', 'total'
 *         ],
 *
 *         hasMany  : {model: 'OrderItem', name: 'orderItems', associationKey: 'order_items'},
 *         belongsTo: 'User'
 *     });
 *
 *     Ext.define("OrderItem", {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             'id', 'price', 'quantity', 'order_id', 'product_id'
 *         ],
 *
 *         belongsTo: ['Order', {model: 'Product', associationKey: 'product'}]
 *     });
 *
 *     Ext.define("Product", {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             'id', 'name'
 *         ],
 *
 *         hasMany: 'OrderItem'
 *     });
 *
 * This may be a lot to take in - basically a User has many Orders, each of which is composed of
 * several OrderItems. Finally, each OrderItem has a single Product. This allows us to consume data
 * like this:
 *
 *     {
 *         "users": [
 *             {
 *                 "id": 123,
 *                 "name": "Ed",
 *                 "orders": [
 *                     {
 *                         "id": 50,
 *                         "total": 100,
 *                         "order_items": [
 *                             {
 *                                 "id": 20,
 *                                 "price": 40,
 *                                 "quantity": 2,
 *                                 "product": {
 *                                     "id": 1000,
 *                                     "name": "MacBook Pro"
 *                                 }
 *                             },
 *                             {
 *                                 "id": 21,
 *                                 "price": 20,
 *                                 "quantity": 3,
 *                                 "product": {
 *                                     "id": 1001,
 *                                     "name": "iPhone"
 *                                 }
 *                             }
 *                         ]
 *                     }
 *                 ]
 *             }
 *         ]
 *     }
 *
 * The JSON response is deeply nested - it returns all Users (in this case just 1 for simplicity's
 * sake), all of the Orders for each User (again just 1 in this case), all of the OrderItems
 * for each Order (2 order items in this case), and finally the Product associated with each
 * OrderItem. Now we can read the data and use it as follows:
 *
 *     var store = Ext.create('Ext.data.Store', {
 *         model: "User"
 *     });
 *
 *     store.load({
 *         callback: function() {
 *             // the user that was loaded
 *             var user = store.first();
 *
 *             console.log("Orders for " + user.get('name') + ":")
 *
 *             // iterate over the Orders for each User
 *             user.orders().each(function(order) {
 *                 console.log("Order ID: " + order.getId() + ", which contains items:");
 *
 *                 // iterate over the OrderItems for each Order
 *                 order.orderItems().each(function(orderItem) {
 *                     // we know that the Product data is already loaded,
 *                     // so we can use the synchronous getProduct
 *                     // usually, we would use the asynchronous version (see #belongsTo)
 *                     var product = orderItem.getProduct();
 *
 *                     console.log(orderItem.get('quantity') + ' orders of ' + product.get('name'));
 *                 });
 *             });
 *         }
 *     });
 *
 * Running the code above results in the following:
 *
 *     Orders for Ed:
 *     Order ID: 50, which contains items:
 *     2 orders of MacBook Pro
 *     3 orders of iPhone
 */
Ext.define('Ext.data.reader.Reader', {
    alternateClassName: [
        'Ext.data.Reader',
        'Ext.data.DataReader'
    ],
    requires: [
        'Ext.data.ResultSet',
        'Ext.XTemplate',
        'Ext.util.LruCache'
    ],
    mixins: [
        'Ext.mixin.Observable',
        'Ext.mixin.Factoryable'
    ],
    alias: 'reader.base',
    factoryConfig: {
        defaultType: null
    },
    config: {
        /**
         * @cfg {String/Function} [groupRootProperty]
         * Name of the property from which to retrieve remote grouping summary information.
         * There should be an item for each group.
         *
         * The remote summary data should be parseable as a {@link #model} used by this reader.
         */
        groupRootProperty: '',
        /**
        * @cfg {Boolean} [implicitIncludes]
        * True to automatically parse models nested within other models in a response object.
        * See the Ext.data.reader.Reader intro docs for full explanation.
        */
        implicitIncludes: true,
        /**
         * @cfg {Boolean} [keepRawData] Determines if the Reader will keep raw data
         * received from the server in the {@link #rawData} property.
         *
         * While this might seem useful to do additional data processing, keeping raw data
         * might cause adverse effects such as memory leaks. It is recommended to set
         * `keepRawData` to `false` if you do not need the raw data.
         *
         * If you need to process data packet to extract additional data such as row summaries,
         * it is recommended to use {@link #transform} function for that purpose.
         *
         * Note that starting with Ext JS 6.0 the default behavior has been changed to
         * **not** keep the raw data because of the high potential for memory leaks.
         * @since 5.1.1
         */
        keepRawData: null,
        /**
        * @cfg {String/Function} messageProperty
        * The name of the property which contains a response message for exception handling. If you
        * want to return a false success response from the server, maybe due to some server-side
        * validation, the messageProperty can hold the error message. For example:
        *
        *     {
        *         "success": false,
        *         "error": "There was an error with your request"
        *     }
        *
        * You can retrieve this error message in a callback when loading a
        * {@link Ext.data.Store Store} or {@link Ext.data.Model Model} like:
        *
        *     var store = new Ext.data.Store({
        *         fields : ['foo'],
        *         proxy  : {
        *             type   : 'ajax',
        *             url    : 'data.json',
        *             reader : {
        *                 type            : 'json',
        *                 rootProperty    : 'data',
        *                 messageProperty : 'error'
        *             }
        *         }
        *     });
        *
        *     store.load({
        *         callback: function(records, operation, success) {
        *             if (success) {
        *                 // ...
        *             } else {
        *                 var error = operation.getError();
        *
        *                 Ext.Msg.alert('Error', error);
        *             }
        *         }
        *     });
        *
        * In this example, the callback will execute with `success` being `false` and will
        * therefore show the {@link Ext.MessageBox#alert Ext.Msg.alert} with the error string
        * returned in the response.
        */
        messageProperty: '',
        /**
         * @cfg {String/Ext.data.Model} [model]
         * The model to use for this reader. This config is only required if the reader is being
         * used without a proxy, otherwise the proxy will automatically set the model.
         */
        model: null,
        /**
         * @cfg {Ext.data.proxy.Proxy} [proxy]
         * The proxy attached to this reader. Typically only needed onMetaChange so that
         * we can set the new model on the proxy.
         * @private
         */
        proxy: null,
        /**
        * @cfg {Boolean} [readRecordsOnFailure]
        * True to extract the records from a data packet even if the {@link #successProperty}
        * returns false.
        */
        readRecordsOnFailure: true,
        /**
         * @cfg {String/Function} rootProperty
         * The property that contains data items corresponding to the 
         * Model(s) of the configured Reader. `rootProperty` varies by Reader type.
         * 
         * ##JSON Reader 
         * `rootProperty` is a property name. It may also be a dot-separated 
         * list of property names if the root is nested. The root JSON array will be 
         * used by default.
         * 
         *     // rootPropety config
         *     rootProperty: 'embedded.myresults'
         *     
         *     // server response
         *     {
         *         embedded: {
         *             myresults: [{
         *                 name: 'Scott',
         *                 age: 22
         *             }, {
         *                 name: 'Ramona',
         *                 age: 24
         *             }]
         *         },
         *         success: true
         *     }
         * 
         * ##XML Reader 
         * `rootProperty` is a CSS selector. The root XML element will be used
         * by default.
         * 
         *     // rootProperty config (plus record config)
         *     rootProperty: 'myresults',
         *     record: 'user'
         *     
         *     // server response
         *     <?xml version="1.0" encoding="UTF-8"?>
         *     <embedded>
         *         <myresults>
         *             <user>
         *                 <name>Scott</name>
         *                 <age>22</age>
         *             </user>
         *             <user>
         *                 <name>Ramona</name>
         *                 <age>24</age>
         *             </user>
         *         </myresults>
         *     </embedded>
         * 
         * ##Array Reader 
         * `rootProperty` is not typically applicable since the data is assumed to be a
         * single-level array of arrays.  However, if the array of records is returned 
         * within a JSON response a `rootProperty` config may be used:
         * 
         *     // rootProperty config
         *     rootProperty: 'embedded.myresults'
         *     
         *     // server response
         *     {
         *         embedded: {
         *             myresults: [['Scott', 22], ['Ramona', 24]]
         *         },
         *         success: true
         *     }
         * 
         * ##rootProperty as a function
         * The `rootProperty` may also be a function that returns the root node from 
         * the dataset. For example:
         *
         *     var store = Ext.create('Ext.data.TreeStore', {
         *         proxy: {
         *             type: 'memory',
         *             reader: {
         *                 type: 'json',
         *                 rootProperty: function(data){
         *                     // Extract child nodes from the items or children property
         *                     // in the dataset
         *                     return data.items || data.children;
         *                 }
         *             }
         *         }, 
         *         data: {
         *             items: [{
         *                 text: 'item 1',
         *                 children: [{
         *                     text: 'child A',
         *                     leaf: true
         *                 }]
         *             }]
         *         }
         *     });
         *
         *     Ext.create('Ext.tree.Panel', {
         *         title: 'rootProperty as a function',
         *         width: 200,
         *         height:150,
         *         store: store,
         *         rootVisible: false,
         *         renderTo: Ext.getBody()
         *     });
         */
        rootProperty: '',
        /**
        * @cfg {String} [successProperty]
        * Name of the property from which to retrieve the `success` attribute, the value of which
        * indicates whether a given request succeeded or failed (typically a boolean or
        * 'true'|'false'). See
        * {@link Ext.data.proxy.Server}.{@link Ext.data.proxy.Server#exception exception} for
        * additional information.
        */
        successProperty: 'success',
        /**
         * @cfg {String/Function} [summaryRootProperty]
         * Name of the property from which to retrieve remote summary information.
         *
         * The remote summary data should be parseable as a {@link #model} used by this reader.
         */
        summaryRootProperty: '',
        /**
        * @cfg {String} [totalProperty]
        * Name of the property from which to retrieve the total number of records in the dataset.
        * This is only needed if the whole dataset is not passed in one go, but is being paged from
        * the remote server.
        */
        totalProperty: 'total',
        /**
         * @cfg {Function|String|Object} [transform]
         * If a transform function is set, it will be invoked just before {@link #readRecords}
         * executes. It is passed the raw (deserialized) data object. The transform function
         * returns a data object, which can be a modified version of the original data object,
         * or a completely new data object. The transform can be a function, or a method name
         * on the Reader instance, or an object with a 'fn' key and an optional 'scope' key.
         *
         * Example usage:
         *
         *     Ext.create('Ext.data.Store', {
         *         model: 'User',
         *         proxy: {
         *             type: 'ajax',
         *             url : 'users.json',
         *             reader: {
         *                 type: 'json',
         *                 transform: {
         *                     fn: function(data) {
         *                         // do some manipulation of the raw data object
         *                         return data;
         *                     },
         *                     scope: this
         *                 }
         *             }
         *         },
         *     });
         *
         */
        transform: null,
        /**
        * @cfg {String} [typeProperty]
        * The name of the property in a node raw data block which indicates the type of the model
        * to be created from that raw data. Useful for heterogeneous trees.
        *
        * For example, hierarchical geographical data may look like this:
        *
        *     {
        *         nodeType: 'Territory',
        *         name: 'EMEA',
        *         children: [{
        *             nodeType: 'Country',
        *             name: 'United Kingdon',
        *             children: [{
        *                 nodeType: 'City',
        *                 name: 'London'
        *             }]
        *         }]
        *     }
        *
        * You would configure the typeProperty in this case to be `"nodeType"` which would cause
        * the models named "Territory", "Country" and "City" to be used.
        */
        typeProperty: ''
    },
    /**
     * @property {Object} rawData
     * The raw data object that was last passed to {@link #readRecords}. rawData is populated 
     * based on the results of {@link Ext.data.proxy.Server#processResponse}. rawData will 
     * maintain a cached copy of the last successfully returned records. In other words, 
     * if processResponse is unsuccessful, the records from the last successful response 
     * will remain cached in rawData.
     *
     * Since Ext JS 5.1.1 you can use the {@link #keepRawData} config option to
     * control this behavior.
     */
    /**
     * @property {Object} metaData
     * The raw meta data that was most recently read, if any. Meta data can include existing
     * Reader config options like {@link #totalProperty}, etc. that get
     * automatically applied to the Reader, and those can still be accessed directly from the Reader
     * if needed. However, meta data is also often used to pass other custom data to be processed
     * by application code. For example, it is common when reconfiguring the data model of a grid to
     * also pass a corresponding column model config to be applied to the grid. Any such data will
     * not get applied to the Reader directly (it just gets passed through and is ignored by Ext).
     * This metaData property gives you access to all meta data that was passed, including any such
     * custom data ignored by the reader.
     * 
     * This is a read-only property, and it will get replaced each time a new meta data object is
     * passed to the reader. Note that typically you would handle proxy's
     * {@link Ext.data.proxy.Proxy#metachange metachange} event which passes this exact same meta
     * object to listeners. However this property is available if it's more convenient to access it
     * via the reader directly in certain cases.
     * @readonly
     */
    /**
     * @property {Boolean} isReader
     * `true` in this class to identify an object as an instantiated Reader, or subclass thereof.
     */
    isReader: true,
    /**
     * @event exception
     * Fires when the reader receives improperly encoded data from the server
     * @param {Ext.data.reader.Reader} reader A reference to this reader
     * @param {XMLHttpRequest} response The XMLHttpRequest response object
     * @param {Ext.data.ResultSet} error The error object
     */
    /**
     * Creates new Reader.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        var me = this;
        if (config && config.hasOwnProperty('root')) {
            config = Ext.apply({}, config);
            config.rootProperty = config.root;
            delete config.root;
            Ext.log.error('Ext.data.reader.Reader: Using the deprecated "root" configuration. ' + 'Use "rootProperty" instead.');
        }
        me.duringInit = 1;
        // Will call initConfig
        me.mixins.observable.constructor.call(me, config);
        --me.duringInit;
        me.buildExtractors();
    },
    forceBuildExtractors: function() {
        if (!this.duringInit) {
            this.buildExtractors(true);
        }
    },
    updateGroupRootProperty: function() {
        this.forceBuildExtractors();
    },
    updateMessageProperty: function() {
        this.forceBuildExtractors();
    },
    applyModel: function(model) {
        return Ext.data.schema.Schema.lookupEntity(model);
    },
    updateSuccessProperty: function() {
        this.forceBuildExtractors();
    },
    updateTotalProperty: function() {
        this.forceBuildExtractors();
    },
    applyTransform: function(transform) {
        if (transform) {
            if (Ext.isFunction(transform)) {
                transform = {
                    fn: transform
                };
            } else if (transform.charAt) {
                // faster than Ext.isString()
                transform = {
                    fn: this[transform]
                };
            }
            return transform.fn.bind(transform.scope || this);
        }
        return transform;
    },
    /**
     * Reads the given response object. This method normalizes the different types of response
     * object that may be passed to it. If it's an XMLHttpRequest object, hand off to the subclass'
     * {@link #getResponseData} method. Else, hand off the reading of records to the
     * {@link #readRecords} method.
     * @param {Object} response The response object. This may be either an XMLHttpRequest object or
     * a plain JS object
     * @param {Object} [readOptions] Various options that instruct the reader on how to read the
     * data
     * @param {Function} [readOptions.recordCreator] A function to construct the model based on the
     * processed data. By default, this just calls the model constructor and passes the raw data.
     * @return {Ext.data.ResultSet} The parsed or default ResultSet object
     */
    read: function(response, readOptions) {
        var data, result, responseText;
        if (response) {
            responseText = response.responseText;
            if (response.responseType || responseText) {
                result = this.getResponseData(response);
                if (result && result.__$isError) {
                    return new Ext.data.ResultSet({
                        total: 0,
                        count: 0,
                        records: [],
                        success: false,
                        message: result.msg
                    });
                } else {
                    data = this.readRecords(result, readOptions);
                }
            } else if (responseText !== '') {
                data = this.readRecords(response, readOptions);
            }
        }
        return data || this.nullResultSet;
    },
    /**
     * Returns the shared null result set.
     * @return {Ext.data.ResultSet} The null result set.
     * 
     * @private
     */
    getNullResultSet: function() {
        return this.nullResultSet;
    },
    /**
     * Creates an object that identifies a read error occurred.
     * @param {String} msg An error message to include
     * @return {Object} An error object
     * 
     * @private
     */
    createReadError: function(msg) {
        return {
            __$isError: true,
            msg: msg
        };
    },
    /**
     * Abstracts common functionality used by all Reader subclasses. Each subclass is expected to
     * call this function before running its own logic and returning the Ext.data.ResultSet
     * instance. For most Readers additional processing should not be needed.
     * @param {Object} data The raw data object
     * @param {Object} [readOptions] See {@link #read} for details.
     * @param {Object} [internalReadOptions] (private)
     * @return {Ext.data.ResultSet} A ResultSet object
     */
    readRecords: function(data, readOptions, internalReadOptions) {
        var me = this,
            recordsOnly = internalReadOptions && internalReadOptions.recordsOnly,
            asRoot = internalReadOptions && internalReadOptions.asRoot,
            groupData = null,
            summaryData = null,
            success, recordCount, records, root, remoteTotal, total, value, message, transform, meta, summaryOptions;
        // Extract the metadata to return with the ResultSet.
        // If found reconfigure accordingly.
        // The calling Proxy fires its metachange event if it finds metadata in the ResultSet.
        meta = me.getMeta ? me.getMeta(data) : data.metaData;
        if (meta) {
            me.onMetaChange(meta);
        }
        transform = me.getTransform();
        if (transform) {
            data = transform(data);
        }
        me.buildExtractors();
        if (me.getKeepRawData()) {
            me.rawData = data;
        }
        if (me.hasListeners.rawdata) {
            me.fireEventArgs('rawdata', [
                data
            ]);
        }
        data = me.getData(data);
        success = true;
        recordCount = 0;
        records = [];
        if (me.getSuccessProperty()) {
            value = me.getSuccess(data);
            if (value === false || value === 'false') {
                success = false;
            }
        }
        if (me.getMessageProperty()) {
            message = me.getMessage(data);
        }
        // Only try and extract other data if call was successful
        if (success || me.getReadRecordsOnFailure()) {
            // If we pass an array as the data, we don't use getRoot on the data.
            // Instead the root equals to the data.
            root = (asRoot || Ext.isArray(data)) ? data : me.getRoot(data);
            if (root) {
                total = root.length;
            }
            if (me.getTotalProperty()) {
                value = parseInt(me.getTotal(data), 10);
                if (!isNaN(value)) {
                    remoteTotal = total = value;
                }
            }
            if (root) {
                records = me.extractData(root, readOptions);
                recordCount = records.length;
            }
            if (me.getGroupRootProperty()) {
                root = me.getGroupRoot(data);
                if (root) {
                    summaryOptions = {
                        includes: false,
                        model: me.getModel().getSummaryModel()
                    };
                    groupData = me.extractData(root, summaryOptions) || null;
                }
            }
            if (me.getSummaryRootProperty()) {
                root = me.getSummaryRoot(data);
                if (root) {
                    summaryOptions = summaryOptions || {
                        includes: false,
                        model: me.getModel().getSummaryModel()
                    };
                    summaryData = me.extractData(root, summaryOptions) || null;
                    // This always returns an array, so transform it
                    if (summaryData) {
                        summaryData = summaryData[0];
                    }
                }
            }
        }
        /* eslint-disable-next-line multiline-ternary */
        return recordsOnly ? records : new Ext.data.ResultSet({
            total: total || recordCount,
            remoteTotal: remoteTotal,
            metadata: meta,
            count: recordCount,
            records: records,
            success: success,
            message: message,
            groupData: groupData,
            summaryData: summaryData
        });
    },
    /**
     * Returns extracted, type-cast rows of data.
     * @param {Object[]/Object} root from server response
     * @param {Object} [readOptions] An object containing extra options.
     * @param {Function} [readOptions.model] The Model constructor to use.
     * @param {Function} [readOptions.recordCreator] A function to use to create and initialize
     * records. By default a function is supplied which creates *non-phantom* records on the
     * assumnption that a Reader is going to be used to read server-supplied data.
     * @param {Object} [readOptions.recordCreator.data] The raw data used to create a record.
     * @param {Function} [readOptions.recordCreator.Model] The Model constructor to use to create
     * the record.
     * @return {Array} An array of records containing the extracted data		
     * @private
     */
    extractData: function(root, readOptions) {
        var me = this,
            entityType = readOptions && readOptions.model ? Ext.data.schema.Schema.lookupEntity(readOptions.model) : me.getModel(),
            // eslint-disable-line max-len
            schema = entityType.schema,
            includes = readOptions && 'includes' in readOptions ? readOptions.includes : schema.hasAssociations(entityType) && me.getImplicitIncludes(),
            // eslint-disable-line max-len
            fieldExtractorInfo = me.getFieldExtractorInfo(entityType),
            length = root.length,
            records = new Array(length),
            typeProperty = me.getTypeProperty(),
            reader, node, nodeType, record, i;
        if (!length && Ext.isObject(root)) {
            root = [
                root
            ];
            length = 1;
        }
        for (i = 0; i < length; i++) {
            record = root[i];
            if (!record.isModel) {
                // If we're given a model instance in the data, just push it on
                // without doing any conversion. Otherwise, create a record.
                node = record;
                // This Reader may be configured to produce different model types based on
                // a differentiator field in the incoming data:
                // typeProperty name be a string, a function which yields the child type, or an
                // object: {
                //     name: 'mtype',
                //     namespace: 'MyApp'
                // }
                if (typeProperty && (nodeType = me.getChildType(schema, node, typeProperty))) {
                    reader = nodeType.getProxy().getReader();
                    record = reader.extractRecord(node, readOptions, nodeType, schema.hasAssociations(nodeType) && reader.getImplicitIncludes(), reader.getFieldExtractorInfo(nodeType));
                } else {
                    record = me.extractRecord(node, readOptions, entityType, includes, fieldExtractorInfo);
                }
                // Generally we don't want to have references to XML documents
                // or XML nodes to hang around in memory but Trees need to be able
                // to access the raw XML node data in order to process its children.
                // See https://sencha.jira.com/browse/EXTJS-15785 and
                // https://sencha.jira.com/browse/EXTJS-14286
                if (record.isModel && record.isNode) {
                    record.raw = node;
                }
            }
            if (record.onLoad) {
                record.onLoad();
            }
            records[i] = record;
        }
        return records;
    },
    // Based upon a Reader's typeProperty config, determine the type of child node to create from
    // the raw data
    getChildType: function(schema, rawNode, typeProperty) {
        var namespace;
        switch (typeof typeProperty) {
            case 'string':
                return schema.getEntity(rawNode[typeProperty]);
            case 'object':
                namespace = typeProperty.namespace;
                return schema.getEntity((namespace ? namespace + '.' : '') + rawNode[typeProperty.name]);
            case 'function':
                return schema.getEntity(typeProperty(rawNode));
        }
    },
    extractRecordData: function(node, readOptions) {
        /* eslint-disable-next-line max-len */
        var entityType = readOptions && readOptions.model ? Ext.data.schema.Schema.lookupEntity(readOptions.model) : this.getModel(),
            fieldExtractorInfo = this.getFieldExtractorInfo(entityType);
        return this.extractRecord(node, readOptions, entityType, false, fieldExtractorInfo);
    },
    extractRecord: function(node, readOptions, entityType, includes, fieldExtractorInfo) {
        var me = this,
            creatorFn = (readOptions && readOptions.recordCreator) || me.defaultRecordCreator,
            modelData, record;
        // Create a record with an empty data object.
        // Populate that data object by extracting and converting field values from raw data.
        // Must pass the ID to use because we pass no data for the constructor to pluck an ID from
        modelData = me.extractModelData(node, fieldExtractorInfo);
        record = creatorFn.call(me, modelData, entityType || me.getModel(), readOptions);
        if (includes && record.isModel) {
            me.readAssociated(record, node, readOptions);
        }
        return record;
    },
    getFieldExtractorInfo: function(entityType) {
        var extractors = entityType.fieldExtractors,
            type, extractor;
        // If the base Ext.data.Model class is being used, there will be no extractor info
        // The raw data block will be imported unchanged.
        if (!extractors) {
            return;
        }
        type = this.$className;
        extractor = extractors[type];
        // If we have no extractors, buildFieldExtractors will return null,
        // so we never need to rebuild them
        if (extractor === undefined) {
            extractors[type] = extractor = this.buildFieldExtractors(entityType);
        }
        return extractor;
    },
    buildFieldExtractors: function(entityType) {
        var fields = entityType.getFields(),
            len = fields.length,
            buffer = [],
            extractors = [],
            out = null,
            cnt = 0,
            field, name, i, extractor;
        for (i = 0; i < len; ++i) {
            field = fields[i];
            extractor = this.createFieldAccessor(field);
            if (extractor) {
                name = field.name;
                // Use [] property access since we may have non-JS looking field names
                buffer.push('val = extractors[' + cnt + '](raw, self); if (val !== undefined) { data[\'' + name + '\'] = val; }');
                extractors.push(extractor);
                ++cnt;
            }
        }
        if (buffer.length) {
            out = {
                extractors: extractors,
                fn: new Function('raw', 'data', 'extractors', 'self', 'var val;' + buffer.join('\n'))
            };
        }
        return out;
    },
    defaultRecordCreator: function(data, Model) {
        return new Model(data);
    },
    defaultRecordCreatorFromServer: function(data, Model) {
        var record = new Model(data);
        // If the server did not include an id in the response data, the Model constructor will
        // mark the record as phantom. We need to set phantom to false here because records created
        // from a server response using a reader by definition are not phantom records.
        record.phantom = false;
        return record;
    },
    getModelData: function(raw) {
        return {};
    },
    extractModelData: function(raw, fieldExtractorInfo) {
        var data = this.getModelData(raw),
            fn;
        // We may not have any mappings to process
        if (fieldExtractorInfo) {
            fn = fieldExtractorInfo.fn;
            fn(raw, data, fieldExtractorInfo.extractors, this);
        }
        return data;
    },
    /**
     * Loads the record associations from the data object.
     * @param {Ext.data.Model} record The record to load associations for.
     * @param {Object} data The raw data object.
     * @param {Object} readOptions See {@link #read}.
     *
     * @private
     */
    readAssociated: function(record, data, readOptions) {
        var roles = record.associations,
            key, role;
        for (key in roles) {
            if (roles.hasOwnProperty(key)) {
                role = roles[key];
                // The class for the other role may not have loaded yet
                if (role.cls) {
                    role.read(record, data, this, readOptions);
                }
            }
        }
    },
    /**
     * @method
     * This method provides a hook to do any data transformation before the reading process
     * begins. By default this function just returns what is passed to it. It can be
     * overridden in a subclass to return something else.
     * See {@link Ext.data.reader.Xml XmlReader} for an example.
     * 
     * @param {Object} data The data object
     * @return {Object} The normalized data object
     * @protected
     * @template
     */
    getData: Ext.identityFn,
    /**
     * @method
     * This will usually need to be implemented in a subclass. Given a generic data object
     * (the type depends on the type of data we are reading), this function should return the
     * object as configured by the Reader's 'root' meta data config. See XmlReader's getRoot
     * implementation for an example. By default the same data object will simply be returned.
     *
     * @param {Object} data The data object
     * @return {Object} The same data object
     * @private
     */
    getRoot: Ext.identityFn,
    /**
     * Takes a raw response object (as passed to the {@link #read} method) and returns the useful
     * data segment from it. This must be implemented by each subclass.
     * @param {Object} response The response object
     * @return {Object} The extracted data from the response. For example, a JSON object or an XML
     * document.
     */
    getResponseData: function(response) {
        Ext.raise("getResponseData must be implemented in the Ext.data.reader.Reader subclass");
    },
    /**
     * @private
     * Reconfigures the meta data tied to this Reader
     */
    onMetaChange: function(meta) {
        var me = this,
            fields = meta.fields,
            model, newModel, clientIdProperty, proxy;
        // save off the raw meta data
        me.metaData = meta;
        // set any reader-specific configs from meta if available
        if (meta.root) {
            me.setRootProperty(meta.root);
        }
        if (meta.totalProperty) {
            me.setTotalProperty(meta.totalProperty);
        }
        if (meta.successProperty) {
            me.setSuccessProperty(meta.successProperty);
        }
        if (meta.messageProperty) {
            me.setMessageProperty(meta.messageProperty);
        }
        clientIdProperty = meta.clientIdProperty;
        if (fields) {
            newModel = Ext.define(null, {
                extend: 'Ext.data.Model',
                fields: fields,
                clientIdProperty: clientIdProperty
            });
            me.setModel(newModel);
            proxy = me.getProxy();
            if (proxy) {
                proxy.setModel(newModel);
            }
        } else if (clientIdProperty) {
            model = me.getModel();
            if (model) {
                model.self.prototype.clientIdProperty = clientIdProperty;
            }
        }
    },
    /**
     * @private
     * This builds optimized functions for retrieving record data and meta data from an object.
     * Subclasses may need to implement their own getRoot function.
     * @param {Boolean} [force=false] True to automatically remove existing extractor functions
     * first
     */
    buildExtractors: function(force) {
        var me = this,
            totalProp, successProp, messageProp;
        if (force || !me.hasExtractors) {
            totalProp = me.getTotalProperty();
            successProp = me.getSuccessProperty();
            messageProp = me.getMessageProperty();
            // build the extractors for all the meta data
            if (totalProp) {
                me.getTotal = me.getAccessor(totalProp);
            }
            if (successProp) {
                me.getSuccess = me.getAccessor(successProp);
            }
            if (messageProp) {
                me.getMessage = me.getAccessor(messageProp);
            }
            me.hasExtractors = true;
            return true;
        }
    },
    getAccessor: function(prop) {
        var me = this,
            cache = me.extractorCache,
            ret, key;
        if (typeof prop === 'string') {
            key = me.getAccessorKey(prop);
            if (key) {
                ret = cache.get(key);
            }
            if (!ret) {
                ret = me.createAccessor(prop);
                if (key) {
                    cache.add(key, ret);
                }
            }
        } else {
            ret = me.createAccessor(prop);
        }
        return ret;
    },
    getAccessorKey: function(prop) {
        var className = this.$className;
        return className ? className + prop : '';
    },
    createAccessor: Ext.emptyFn,
    createFieldAccessor: Ext.emptyFn,
    destroy: function() {
        var me = this;
        me.model = me.getTotal = me.getSuccess = me.getMessage = me.rawData = null;
        // Proxy could have created a sequence
        me.onMetaChange = null;
        // Transform function can be bound
        me.transform = null;
        me.callParent();
    },
    privates: {
        copyFrom: function(reader) {
            var me = this;
            reader.buildExtractors();
            me.getTotal = reader.getTotal;
            me.getSuccess = reader.getSuccess;
            me.getMessage = reader.getMessage;
            ++me.duringInit;
            me.setConfig(reader.getConfig());
            --me.duringInit;
            me.hasExtractors = true;
        },
        getGroupRoot: Ext.privateFn,
        getSummaryRoot: Ext.privateFn
    }
}, function(Cls) {
    var proto = Cls.prototype;
    Ext.apply(proto, {
        // Private. Empty ResultSet to return when response is falsy (null|undefined|empty string)
        nullResultSet: new Ext.data.ResultSet({
            total: 0,
            count: 0,
            records: [],
            success: true,
            message: ''
        })
    });
    proto.extractorCache = new Ext.util.LruCache();
});

/**
 * Base Writer class used by most subclasses of {@link Ext.data.proxy.Server}. This class
 * is responsible for taking a set of {@link Ext.data.operation.Operation} objects and a
 * {@link Ext.data.Request} object and modifying that request based on the Operations.
 *
 * For example a Ext.data.writer.Json would format the Operations and their
 * {@link Ext.data.Model} instances based on the config options passed to the JsonWriter's
 * constructor.
 *
 * Writers are not needed for any kind of local storage - whether via a
 * {@link Ext.data.proxy.WebStorage Web Storage proxy} (see
 * {@link Ext.data.proxy.LocalStorage localStorage} and
 * {@link Ext.data.proxy.SessionStorage sessionStorage})
 * or just in memory via a {@link Ext.data.proxy.Memory MemoryProxy}.
 * 
 * # Dates
 *
 * Before sending dates to the server, they can be formatted using an {@link Ext.Date}
 * format. These formats can be specified both on the field and the writer itself. In terms
 * of precedence, from highest to lowest:
 * 
 * - {@link #dateFormat Writer.dateFormat} The writer `dateFormat` will always have the
 *   highest precedence.
 * - {@link Ext.data.field.Date#dateWriteFormat} The `dateWriteFormat` given to the field
 *   instance. This is handled by {@link Ext.data.field.Date#method-serialize}.
 * - {@link Ext.data.field.Date#dateFormat Field.dateFormat} This is handled by the field's
 *   `serialize` method.
 * - {@link Ext.data.field.Date#dateReadFormat Field.dateReadFormat} Also handled by the
 *   field's `serialize` method.
 */
Ext.define('Ext.data.writer.Writer', {
    alias: 'writer.base',
    alternateClassName: [
        'Ext.data.DataWriter',
        'Ext.data.Writer'
    ],
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    factoryConfig: {
        defaultType: null
    },
    /**
     * @property {Boolean} isWriter
     * `true` to identify an object as an instance of this class, or subclass thereof.
     * @readonly
     */
    isWriter: true,
    config: {
        /**
         * @cfg {String} clientIdProperty
         * When specified this property causes the `{@link Ext.data.Model#idProperty}` of
         * newly created records to be sent to the server as this name instead of the
         * value of the `idProperty`.
         *
         * For example, by default, the following code:
         *
         *      Ext.define('Person', {
         *          idProperty: 'id',  // this is the default value (for clarity)
         *
         *          fields: [ 'name' ]
         *      });
         *
         *      var person = new Person({
         *          // no id provided, so one is generated
         *          name: 'Clark Kent'
         *      });
         *
         * Will send this to the server:
         *
         *      {
         *          id: 'Person-1',
         *          name: 'Clark Kent'
         *      }
         *
         * This can be an issue if the server expects an integer for the "id" property.
         * You can use `{@link Ext.data.Model#identifier}` to produce identifiers that
         * the server would recognize or use this config to send the client's id in a
         * different property.
         *
         *      Ext.define('Person', {
         *          idProperty: 'id',  // this is the default value (for clarity)
         *
         *          proxy: {
         *              writer: {
         *                  clientIdProperty: 'clientId'
         *              }
         *          },
         *
         *          fields: [ 'name' ]
         *      });
         *
         * Given the above, the server is sent this data now:
         *
         *      {
         *          clientId: 'Person-1',
         *          name: 'Clark Kent'
         *      }
         *
         * While this config provides the behavior of `{@link Ext.data.Model#clientIdProperty}`
         * from previous releases, this property is not as useful as a suitable
         * `{@link Ext.data.Model#identifier}` due to id's appearing in foreign-key fields
         * and in `{@link Ext.data.Model#manyToMany}` associations.
         *
         * See `{@link Ext.data.Model#identifier}` for more on id generation.
         */
        clientIdProperty: null,
        /**
         * @cfg {Object} allDataOptions
         * This object contains the options passed to `{@link Ext.data.Model#getData}` when
         * writing `{@link Ext.data.Model#phantom}` records or when `writeAllFields` is set
         * to `true`.
         *
         * *NOTE:* The `serialize` option cannot be used here.
         */
        allDataOptions: {
            persist: true
        },
        /**
         * @cfg {Object} partialDataOptions
         * This object contains the options passed to `{@link Ext.data.Model#getData}` when
         * writing non `{@link Ext.data.Model#phantom}` records or when `writeAllFields` is
         * set to `false`.
         *
         * *NOTE:* The `serialize` option cannot be used here.
         */
        partialDataOptions: {
            changes: true,
            critical: true
        },
        /**
         * @cfg {Boolean} writeAllFields `true` to write all fields from the record to the
         * server. If set to `false` it will only send the fields that were modified. Note
         * that any fields that have `{@link Ext.data.field.Field#persist}` set to `false`
         * will still be ignored while those with `{@link Ext.data.field.Field#critical}`
         * set to `true` will be included.
         *
         * The exact set of fields written is determined by `allDataOptions` (when `true`)
         * or `partialDataOptions` (when `false`). This option is ignored and treated as
         * `true` when writing `{@link Ext.data.Model#phantom}` records.
         *
         * It is seldom a good idea to use this config. Rather use `allDataOptions` or
         * `partialDataOptions` to control what fields are sent for records based on their
         * `{@link Ext.data.Model#phantom}` state.
         *
         * In the previous release, this was default `true`.
         */
        writeAllFields: false,
        /**
         * @cfg {String} dateFormat
         * This is used for each field of type date in the model to format the value before
         * it is sent to the server.
         */
        dateFormat: null,
        /**
         * @cfg {String} nameProperty
         * This property is used to read the key for each value that will be sent to the
         * server.
         *
         * For example:
         *
         *     Ext.define('Person', {
         *         extend: 'Ext.data.Model',
         *         fields: [{
         *             name: 'first',
         *             mapping: 'firstName'
         *         }, {
         *             name: 'last',
         *             mapping: 'lastName'
         *         }, {
         *             name: 'age'
         *         }]
         *     });
         *
         *     new Ext.data.writer.Writer({
         *         nameProperty: 'mapping'
         *     });
         *
         *     // This will be sent to the server
         *     {
         *         firstName: 'first name value',
         *         lastName: 'last name value',
         *         age: 1
         *     }
         *
         * If the value is not present, the field name will always be used.
         */
        nameProperty: 'name',
        /**
         * @cfg {Boolean} [writeRecordId]
         * By default, each record's id is always included in the output for non-phantom
         * records since in most cases the id will be required on the server to process
         * the record action. This is helpful since the id will normally not be modified,
         * and so would not be sent to the server unless {@link #writeAllFields} was
         * explicitly enabled.
         * 
         * However, there are cases where it is not desirable for the record id to be passed
         * in the data directly. For example, when using a RESTful API the record id would
         * typically be appended to the url instead.
         */
        writeRecordId: true,
        /**
         * @cfg {Function|Object} [transform]
         * If a transform function is set, it will be invoked just before {@link #writeRecords} 
         * executes. It is passed the unserialized data object and the
         * {@link Ext.data.Request request} object. The transform function returns a data object,
         * which can be a modified version of the original  data object, or a completely new data
         * object. The transform can be a function, or an object  with a 'fn' key and an optional
         * 'scope' key. Example usage:
         *
         *     Ext.create('Ext.data.Store', {
         *         model: 'User',
         *         proxy: {
         *             type: 'ajax',
         *             url : 'users.json',
         *             writer: {
         *                 type: 'json',
         *                 transform: {
         *                     fn: function(data, request) {
         *                         // do some manipulation of the unserialized data object
         *                         return data;
         *                     },
         *                     scope: this
         *                 }
         *             }
         *         },
         *     });
         *
         */
        transform: null
    },
    /**
     * Creates new Writer.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        this.initConfig(config);
    },
    applyTransform: function(transform) {
        if (transform) {
            if (Ext.isFunction(transform)) {
                transform = {
                    fn: transform
                };
            }
            return transform.fn.bind(transform.scope || this);
        }
        return transform;
    },
    /**
     * Prepares a Proxy's Ext.data.Request object.
     * @param {Ext.data.Request} request The request object.
     * @return {Ext.data.Request} The modified request object.
     */
    write: function(request) {
        var operation = request.getOperation(),
            records = operation.getRecords() || [],
            len = records.length,
            data = [],
            i;
        for (i = 0; i < len; i++) {
            data.push(this.getRecordData(records[i], operation));
        }
        return this.writeRecords(request, data);
    },
    /**
     * @method
     *
     * Write the record data to the request in the appropriate format.
     * @protected
     * @param {Ext.data.Request} request The request.
     * @param {Array} data An array of objects containing data.
     * @return {Ext.data.Request} The request.
     */
    writeRecords: Ext.emptyFn,
    /**
     * Formats the data for each record before sending it to the server. This method should
     * be overridden to format the data in a way that differs from the default.
     *
     * @param {Ext.data.Model} record The record that we are writing to the server.
     * @param {Ext.data.operation.Operation} [operation] An operation object.
     * @return {Object} An object of name/value keys to be written to the server.
     */
    getRecordData: function(record, operation) {
        var me = this,
            nameProperty = me.getNameProperty(),
            mapping = nameProperty !== 'name',
            idField = record.self.idField,
            key = idField ? (idField[nameProperty] || idField.name) : 'id',
            value = record.id,
            writeAll = me.getWriteAllFields(),
            ret, dateFormat, phantom, options, clientIdProperty, fieldsMap, data, field;
        if (idField && idField.serialize) {
            value = idField.serialize(value);
        }
        if (!writeAll && operation && operation.isDestroyOperation) {
            ret = {};
            ret[key] = value;
        } else {
            dateFormat = me.getDateFormat();
            phantom = record.phantom;
            options = (phantom || writeAll) ? me.getAllDataOptions() : me.getPartialDataOptions();
            clientIdProperty = phantom && me.getClientIdProperty();
            fieldsMap = record.getFieldsMap();
            options.serialize = false;
            // we must take over this here
            data = record.getData(options);
            // If we are mapping we need to pour data into a new object, otherwise we do
            // our work in-place:
            ret = mapping ? {} : data;
            if (clientIdProperty) {
                // if (phantom and have clientIdProperty)
                ret[clientIdProperty] = value;
                // must read data and write ret
                delete data[key];
            }
            // in case ret === data (must not send "id")
            else if (!me.getWriteRecordId()) {
                delete data[key];
            }
            for (key in data) {
                value = data[key];
                if (!(field = fieldsMap[key])) {
                    // No defined field, so clearly no nameProperty to look up for this field
                    // but if we are mapping we need to copy over the value. Also there is no
                    // serializer to call in this case.
                    if (mapping) {
                        ret[key] = value;
                    }
                } else {
                    // Allow this Writer to take over formatting date values if it has a
                    // dateFormat specified. Only check isDate on fields declared as dates
                    // for efficiency.
                    if (field.isDateField && dateFormat && Ext.isDate(value)) {
                        value = Ext.Date.format(value, dateFormat);
                    } else if (field.serialize) {
                        value = field.serialize(value, record);
                    }
                    if (mapping) {
                        key = field[nameProperty] || key;
                    }
                    ret[key] = value;
                }
            }
        }
        return ret;
    }
});

/**
 * Proxies are used by {@link Ext.data.Store Stores} to handle the loading and saving of
 * {@link Ext.data.Model Model} data. Usually developers will not need to create or interact
 * with proxies directly.
 *
 * # Types of Proxy
 *
 * There are two main types of Proxy - {@link Ext.data.proxy.Client Client} and
 * {@link Ext.data.proxy.Server Server}. The Client proxies save their data locally and include
 * the following subclasses:
 *
 * - {@link Ext.data.proxy.LocalStorage LocalStorageProxy} - saves its data to localStorage
 *   if the browser supports it
 * - {@link Ext.data.proxy.SessionStorage SessionStorageProxy} - saves its data to sessionStorage
 *   if the browsers supports it
 * - {@link Ext.data.proxy.Memory MemoryProxy} - holds data in memory only, any data is lost
 *   when the page is refreshed
 *
 * The Server proxies save their data by sending requests to some remote server. These proxies
 * include:
 *
 * - {@link Ext.data.proxy.Ajax Ajax} - sends requests to a server on the same domain
 * - {@link Ext.data.proxy.JsonP JsonP} - uses JSON-P to send requests to a server on a different
 *   domain
 * - {@link Ext.data.proxy.Rest Rest} - uses RESTful HTTP methods (GET/PUT/POST/DELETE)
 *   to communicate with server
 * - {@link Ext.data.proxy.Direct Direct} - uses {@link Ext.direct.Manager} to send requests
 *
 * Proxies operate on the principle that all operations performed are either Create, Read, Update
 * or Delete. These four operations are mapped to the methods {@link #method!create},
 * {@link #method!read}, {@link #method!update} and {@link #method!erase} respectively. Each Proxy
 * subclass implements these functions.
 *
 * The CRUD methods each expect an {@link Ext.data.operation.Operation Operation} object as the only
 * argument. The Operation encapsulates information about the action the Store wishes to perform,
 * the {@link Ext.data.Model model} instances that are to be modified, etc. See the
 * {@link Ext.data.operation.Operation Operation} documentation for more details. Each CRUD
 * method also accepts a callback function to be called asynchronously on completion.
 *
 * Proxies also support batching of Operations via a {@link Ext.data.Batch batch} object, invoked
 * by the {@link #batch}
 * method.
 */
Ext.define('Ext.data.proxy.Proxy', {
    mixins: [
        'Ext.mixin.Factoryable',
        'Ext.mixin.Observable'
    ],
    $configPrefixed: false,
    alias: 'proxy.proxy',
    // also configures Factoryable
    alternateClassName: [
        'Ext.data.DataProxy',
        'Ext.data.Proxy'
    ],
    requires: [
        'Ext.data.schema.Schema',
        'Ext.data.reader.Reader',
        'Ext.data.writer.Writer'
    ],
    uses: [
        'Ext.data.Batch',
        'Ext.data.operation.*',
        'Ext.data.Model'
    ],
    config: {
        /**
         * @cfg {String} batchOrder
         * Comma-separated ordering 'create', 'update' and 'destroy' actions when batching.
         * Override this to set a different order for the batched CRUD actions to be executed in.
         * Defaults to 'create,update,destroy'.
         */
        batchOrder: 'create,update,destroy',
        /**
         * @cfg {Boolean} batchActions
         * True to batch actions of a particular type when synchronizing the store. Defaults to
         * `true`.
         */
        batchActions: true,
        /**
         * @cfg {String/Ext.data.Model} model
         * The name of the Model to tie to this Proxy. Can be either the string name of the Model,
         * or a reference to the Model constructor. Required.
         */
        model: undefined,
        // @cmd-auto-dependency {aliasPrefix : "reader.", defaultPropertyName : "defaultReaderType"}
        /**
         * @cfg {Object/String/Ext.data.reader.Reader} reader
         * The Ext.data.reader.Reader to use to decode the server's response or data read
         * from client. This can either be a Reader instance, a config object or just a
         * valid Reader type name (e.g. 'json', 'xml').
         */
        reader: {
            type: 'json'
        },
        // @cmd-auto-dependency {aliasPrefix : "writer.", defaultPropertyName : "defaultWriterType"}
        /**
         * @cfg {Object/String/Ext.data.writer.Writer} writer
         * The Ext.data.writer.Writer to use to encode any request sent to the server or
         * saved to client. This can either be a Writer instance, a config object or just
         * a valid Writer type name (e.g. 'json', 'xml').
         */
        writer: {
            type: 'json'
        }
    },
    /**
     * @property {Boolean} isProxy
     * `true` in this class to identify an object as an instantiated Proxy, or subclass thereof.
     */
    isProxy: true,
    /**
     * @property {Boolean} [isSynchronous=false]
     * Identifies the proxy as (a)synchronous.
     */
    isSynchronous: false,
    /**
     * @event metachange
     * Fires when this proxy's reader provides new metadata. Metadata usually consists
     * of new field definitions, but can include any configuration data required by an
     * application, and can be processed as needed in the event handler.
     * This event is currently only fired for JsonReaders. Note that this event is also
     * propagated by {@link Ext.data.Store}, which is typically where it would be handled.
     * @param {Ext.data.proxy.Proxy} this
     * @param {Object} meta The JSON metadata
     */
    /**
     * Creates the Proxy
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        // Will call initConfig
        this.mixins.observable.constructor.call(this, config);
        // We need to abort all pending operations when destroying
        this.pendingOperations = {};
    },
    applyModel: function(model) {
        return Ext.data.schema.Schema.lookupEntity(model);
    },
    updateModel: function(model) {
        var reader;
        if (model) {
            reader = this.getReader();
            if (reader && !reader.getModel()) {
                reader.setModel(model);
            }
        }
    },
    applyReader: function(reader) {
        // Synchronous proxies need to force keepRawData to allow Grid features
        // like Summary and Grouping access rawData after the Reader processed records.
        // It doesn't do much harm since synchronous proxies are Client side ones,
        // which will keep their datasets in memory or local storage anyway.
        if (this.isSynchronous) {
            reader = reader || {};
            reader.keepRawData = true;
        }
        return Ext.Factory.reader(reader);
    },
    updateReader: function(reader) {
        var me = this,
            model;
        if (reader) {
            model = me.getModel();
            if (!model) {
                model = reader.getModel();
                if (model) {
                    me.setModel(model);
                }
            } else {
                reader.setModel(model);
            }
            if (reader.responseType != null) {
                me.responseType = reader.responseType;
            }
        }
    },
    applyWriter: function(writer) {
        var reader = this.getReader();
        writer = Ext.Factory.writer(writer);
        // XML Writers may have a record config to define the node name of each record tag.
        // If not set, but the Reader has a record config, use the Reader's record config.
        if (writer.getRecord && !writer.getRecord() && reader && reader.getRecord) {
            reader = reader.getRecord();
            if (reader) {
                writer.setRecord(reader);
            }
        }
        return writer;
    },
    abort: Ext.emptyFn,
    /**
     * @private
     * Called each time the reader's onMetaChange is called so that the proxy can fire the
     * metachange event
     */
    onMetaChange: function(meta) {
        this.fireEvent('metachange', this, meta);
    },
    /**
     * Performs the given create operation.
     * @param {Ext.data.operation.Operation} operation The Operation to perform
     * @method
     */
    create: Ext.emptyFn,
    /**
     * Performs the given read operation.
     * @param {Ext.data.operation.Operation} operation The Operation to perform
     * @method
     */
    read: Ext.emptyFn,
    /**
     * Performs the given update operation.
     * @param {Ext.data.operation.Operation} operation The Operation to perform
     * @method
     */
    update: Ext.emptyFn,
    /**
     * Performs the given destroy operation.
     * @param {Ext.data.operation.Operation} operation The Operation to perform
     * @method
     */
    erase: Ext.emptyFn,
    /**
     * Performs a batch of {@link Ext.data.operation.Operation Operations}, in the order specified
     * by {@link #batchOrder}. Used internally by {@link Ext.data.Store}'s
     * {@link Ext.data.Store#sync sync} method. Example usage:
     *
     *     myProxy.batch({
     *         create : [myModel1, myModel2],
     *         update : [myModel3],
     *         destroy: [myModel4, myModel5]
     *     });
     *
     * Where the myModel* above are {@link Ext.data.Model Model} instances - in this case 1 and 2
     * are new instances and have not been saved before, 3 has been saved previously but needs to be
     * updated, and 4 and 5 have already been saved but should now be destroyed.
     * 
     * Note that the previous version of this method took 2 arguments (operations and listeners).
     * While this is still supported for now, the current signature is now a single `options`
     * argument that can contain both operations and listeners, in addition to other options.
     * The multi-argument signature will likely be deprecated in a future release.
     *
     * @param {Object} options Object containing one or more properties supported by the batch
     * method:
     * 
     * @param {Object} options.operations Object containing the Model instances to act upon, keyed
     * by action name
     * 
     * @param {Object} [options.listeners] Event listeners object passed straight through to the
     * Batch - see {@link Ext.data.Batch} for details
     * 
     * @param {Ext.data.Batch/Object} [options.batch] A {@link Ext.data.Batch} object (or batch
     * config to apply  to the created batch). If unspecified a default batch will be auto-created.
     * 
     * @param {Function} [options.callback] The function to be called upon completion of processing
     * the batch. The callback is called regardless of success or failure and is passed the
     * following parameters:
     * @param {Ext.data.Batch} options.callback.batch The {@link Ext.data.Batch batch} that was
     * processed, containing all operations in their current state after processing
     * @param {Object} options.callback.options The options argument that was originally passed
     * into batch
     * 
     * @param {Function} [options.success] The function to be called upon successful completion
     * of the batch. The  success function is called only if no exceptions were reported in any
     * operations. If one or more exceptions occurred then the `failure` function will be called
     * instead. The success function is called  with the following parameters:
     * @param {Ext.data.Batch} options.success.batch The {@link Ext.data.Batch batch} that was
     * processed, containing all operations in their current state after processing
     * @param {Object} options.success.options The options argument that was originally passed into
     * batch
     * 
     * @param {Function} [options.failure] The function to be called upon unsuccessful completion
     * of the batch. The  failure function is called when one or more operations returns an
     * exception during processing (even if some operations were also successful). In this case you
     * can check the batch's {@link Ext.data.Batch#exceptions exceptions} array to see exactly
     * which operations had exceptions. The failure function is called with the following
     * parameters:
     * @param {Ext.data.Batch} options.failure.batch The {@link Ext.data.Batch batch} that was
     * processed, containing all operations in their current state after processing
     * @param {Object} options.failure.options The options argument that was originally passed into
     * batch
     * 
     * @param {Object} [options.scope] The scope in which to execute any callbacks (i.e. the `this`
     * object inside the callback, success and/or failure functions). Defaults to the proxy.
     *
     * @param {Object} [listeners] (deprecated) If `options` is the `operations`, this
     * parameter is the listeners. Instead of passing these two arguments, the proper form
     * is to pass them as:
     *
     *      batch({
     *          operations: ...
     *          listeners: ...
     *      });
     *
     * @return {Ext.data.Batch} The newly created Batch
     */
    batch: function(options, listeners) {
        var me = this,
            useBatch = me.getBatchActions(),
            batch, records, actions, aLen, action, a, r, rLen, record;
        if (options.operations === undefined) {
            // the old-style (operations, listeners) signature was called
            // so convert to the single options argument syntax
            options = {
                operations: options,
                listeners: listeners
            };
        }
        if (options.batch) {
            if (Ext.isDefined(options.batch.runOperation)) {
                batch = Ext.applyIf(options.batch, {
                    proxy: me,
                    listeners: {}
                });
            }
        } else {
            options.batch = {
                proxy: me,
                listeners: options.listeners || {}
            };
        }
        if (!batch) {
            batch = new Ext.data.Batch(options.batch);
        }
        // Use single so that the listener gets removed upon completion.
        batch.on('complete', Ext.bind(me.onBatchComplete, me, [
            options
        ], 0), null, {
            single: true,
            priority: 1000
        });
        batch.$destroyOwner = options.$destroyOwner;
        actions = me.getBatchOrder().split(',');
        aLen = actions.length;
        for (a = 0; a < aLen; a++) {
            action = actions[a];
            records = options.operations[action];
            if (records) {
                if (useBatch) {
                    batch.add(me.createOperation(action, {
                        records: records,
                        // Relay any additional params through to the Operation (and Request).
                        params: options.params
                    }));
                } else {
                    rLen = records.length;
                    for (r = 0; r < rLen; r++) {
                        record = records[r];
                        batch.add(me.createOperation(action, {
                            records: [
                                record
                            ],
                            // Relay any additional params through to the Operation (and Request).
                            params: options.params
                        }));
                    }
                }
            }
        }
        batch.start();
        return batch;
    },
    /**
     * @private
     * The internal callback that the proxy uses to call any specified user callbacks after
     * completion of a batch
     */
    onBatchComplete: function(batchOptions, batch) {
        var scope = batchOptions.scope || this;
        if (batch.hasException()) {
            if (Ext.isFunction(batchOptions.failure)) {
                Ext.callback(batchOptions.failure, scope, [
                    batch,
                    batchOptions
                ]);
            }
        } else if (Ext.isFunction(batchOptions.success)) {
            Ext.callback(batchOptions.success, scope, [
                batch,
                batchOptions
            ]);
        }
        if (Ext.isFunction(batchOptions.callback)) {
            Ext.callback(batchOptions.callback, scope, [
                batch,
                batchOptions
            ]);
        }
        // In certain cases when the batch was created by a ProxyStore we need to
        // defer destruction until the store can process the batch results.
        // The store will then destroy the batch.
        if (!batch.$destroyOwner) {
            batch.destroy();
        }
    },
    createOperation: function(action, config) {
        var operation = Ext.createByAlias('data.operation.' + action, config);
        operation.setProxy(this);
        this.pendingOperations[operation._internalId] = operation;
        return operation;
    },
    completeOperation: function(operation) {
        delete this.pendingOperations[operation._internalId];
    },
    clone: function() {
        return new this.self(this.getInitialConfig());
    },
    destroy: function() {
        var ops = this.pendingOperations,
            opId, op;
        for (opId in ops) {
            op = ops[opId];
            if (op && op.isRunning()) {
                op.abort();
            }
            op.destroy();
        }
        this.pendingOperations = null;
        this.callParent();
    }
});

/**
 * Base class for any client-side storage. Used as a superclass for
 * {@link Ext.data.proxy.Memory Memory} and {@link Ext.data.proxy.WebStorage Web Storage} proxies.
 * Do not use directly, use one of the subclasses instead.
 * @private
 */
Ext.define('Ext.data.proxy.Client', {
    extend: 'Ext.data.proxy.Proxy',
    alternateClassName: 'Ext.data.ClientProxy',
    /**
     * @property {Boolean} isSynchronous
     * `true` in this class to identify that requests made on this proxy are
     * performed synchronously
     */
    isSynchronous: true,
    /**
     * Abstract function that must be implemented by each ClientProxy subclass. This should purge
     * all record data from the client side storage, as well as removing any supporting data
     * (such as lists of record IDs)
     */
    clear: function() {
        Ext.raise("The Ext.data.proxy.Client subclass that you are using has not defined " + "a 'clear' function. See src/data/ClientProxy.js for details.");
    }
});

/**
 * In-memory proxy. This proxy simply uses a local variable for data storage/retrieval, so its
 * contents are lost on every page refresh.
 *
 * Usually this Proxy isn't used directly, serving instead as a helper to a
 * {@link Ext.data.Store Store} where a reader is required to load data. For example, say we have
 * a Store for a User model and have some inline data we want to load, but this data isn't in quite
 * the right format: we can use a MemoryProxy with a JsonReader to read it into our Store:
 *
 *     // this is the model we will be using in the store
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             { name: 'id',    type: 'int' },
 *             { name: 'name',  type: 'string' },
 *             { name: 'phone', type: 'string', mapping: 'phoneNumber' }
 *         ]
 *     });
 *
 *     // this data does not line up to our model fields - the phone field is called phoneNumber
 *     var data = {
 *         users: [
 *             {
 *                 id: 1,
 *                 name: 'Ed Spencer',
 *                 phoneNumber: '555 1234'
 *             },
 *             {
 *                 id: 2,
 *                 name: 'Abe Elias',
 *                 phoneNumber: '666 1234'
 *             }
 *         ]
 *     };
 *
 *     // note how we set the 'root' in the reader to match the data structure above
 *     var store = Ext.create('Ext.data.Store', {
 *         autoLoad: true,
 *         model: 'User',
 *         data: data,
 *         proxy: {
 *             type: 'memory',
 *             reader: {
 *                 type: 'json',
 *                 rootProperty: 'users'
 *             }
 *         }
 *     });
 */
Ext.define('Ext.data.proxy.Memory', {
    extend: 'Ext.data.proxy.Client',
    alias: 'proxy.memory',
    alternateClassName: 'Ext.data.MemoryProxy',
    isMemoryProxy: true,
    config: {
        /**
        * @cfg {Boolean} [enablePaging=false]
        * Configure as `true` to enable this MemoryProxy to honour a read operation's `start`
        * and `limit` options.
        *
        * When `true`, read operations will be able to read *pages* of records from the data object.
        */
        enablePaging: null,
        /**
        * @cfg {Object} data
        * Optional data to pass to configured Reader.
        */
        data: {
            $value: null,
            // Because of destructive association reading, we always need to clone incoming data
            // to protect externally owned data objects from mutation
            merge: function(newValue, currentValue, target, mixinClass) {
                return newValue ? Ext.clone(newValue) : newValue;
            }
        },
        /**
         * @cfg {Boolean} [clearOnRead=false]
         * By default MemoryProxy data is persistent, and subsequent reads will read the
         * same data. If this is not required, configure the proxy using `clearOnRead: true`.
         */
        clearOnRead: null
    },
    /**
     * @private
     * Fake processing function to commit the records, set the current operation
     * to successful and call the callback if provided. This function is shared
     * by the create, update and destroy methods to perform the bare minimum
     * processing required for the proxy to register a result from the action.
     */
    finishOperation: function(operation) {
        var recs = operation.getRecords(),
            len = recs.length,
            i;
        for (i = 0; i < len; i++) {
            // Because Memory proxy is synchronous, the commit must call store#afterErase
            recs[i].dropped = !!operation.isDestroyOperation;
            recs[i].commit();
        }
        operation.setSuccessful(true);
    },
    /**
     * Currently this is a hard-coded method that simply commits any records and sets the operation
     * to successful, then calls the callback function, if provided. It is essentially mocking
     * a server call in memory, but since there is no real back end in this case there's not much
     * else to do. This method can be easily overridden to  implement more complex logic if needed.
     * @param {Ext.data.operation.Operation} operation The Operation to perform
     * @method
     */
    create: function(operation) {
        this.finishOperation(operation);
    },
    /**
     * Currently this is a hard-coded method that simply commits any records and sets the operation
     * to successful, then calls the callback function, if provided. It is essentially mocking
     * a server call in memory, but since there is no real back end in this case there's not much
     * else to do. This method can be easily overridden to  implement more complex logic if needed.
     * @param {Ext.data.operation.Operation} operation The Operation to perform
     * @method
     */
    update: function(operation) {
        this.finishOperation(operation);
    },
    /**
     * Currently this is a hard-coded method that simply commits any records and sets the operation
     * to successful, then calls the callback function, if provided. It is essentially mocking
     * a server call in memory, but since there is no real back end in this case there's not much
     * else to do. This method can be easily overridden to  implement more complex logic if needed.
     * @param {Ext.data.operation.Operation} operation The Operation to perform
     * @method
     */
    erase: function(operation) {
        this.finishOperation(operation);
    },
    /**
     * Reads data from the configured {@link #data} object. Uses the Proxy's {@link #reader},
     * if present.
     * @param {Ext.data.operation.Operation} operation The read Operation
     */
    read: function(operation) {
        var me = this,
            reader = me.getReader(),
            resultSet = reader.read(me.getData(), {
                recordCreator: reader.defaultRecordCreatorFromServer
            }),
            records = resultSet.getRecords(),
            sorters = operation.getSorters(),
            grouper = operation.getGrouper(),
            filters = operation.getFilters(),
            start = operation.getStart(),
            limit = operation.getLimit(),
            meta;
        // Apply filters, sorters, and start/limit options
        if (operation.process(resultSet, null, null, false) !== false) {
            // If we are configured to read the data one time only, clear our data
            if (operation.success && me.getClearOnRead()) {
                this.setData(null);
            }
            // Filter the resulting array of records
            if (filters && filters.length) {
                // Total will be updated by setting records
                /* eslint-disable-next-line max-len */
                resultSet.setRecords(records = Ext.Array.filter(records, Ext.util.Filter.createFilterFn(filters)));
                resultSet.setTotal(records.length);
            }
            // Remotely, grouper just mean top priority sorters
            if (grouper) {
                // Must concat so as not to mutate passed sorters array which could be the items
                // property of the sorters collection
                sorters = sorters ? sorters.concat(grouper) : sorters;
            }
            // Sort by the specified grouper and sorters
            if (sorters && sorters.length) {
                /* eslint-disable-next-line max-len */
                resultSet.setRecords(records = Ext.Array.sort(records, Ext.util.Sortable.createComparator(sorters)));
            }
            // Reader reads the whole passed data object.
            // If successful and we were given a start and limit, slice the result.
            if (me.getEnablePaging() && start !== undefined && limit !== undefined) {
                // Attempt to read past end of memory dataset - convert to failure
                if (start >= resultSet.getTotal()) {
                    resultSet.setConfig({
                        success: false,
                        records: [],
                        total: 0
                    });
                } else // Range is valid, slice it up.
                {
                    resultSet.setRecords(Ext.Array.slice(records, start, start + limit));
                }
            }
            operation.setCompleted();
            // If a JsonReader detected metadata, process it now.
            // This will fire the 'metachange' event which the Store processes to fire its own
            // 'metachange'
            meta = resultSet.getMetadata();
            if (meta) {
                me.onMetaChange(meta);
            }
        }
    },
    clear: Ext.emptyFn
});

/**
 * ProxyStore is a superclass of {@link Ext.data.Store} and {@link Ext.data.BufferedStore}.
 * It's never used directly, but offers a set of methods used by both of those subclasses.
 *
 * We've left it here in the docs for reference purposes, but unless you need to make a whole new
 * type of Store, what you're probably looking for is {@link Ext.data.Store}. If you're still
 * interested, here's a brief description of what ProxyStore is and is not.
 *
 * ProxyStore provides the basic configuration for anything that can be considered a Store.
 * It expects to be given a {@link Ext.data.Model Model} that represents the type of data
 * in the Store. It also expects to be given a {@link Ext.data.proxy.Proxy Proxy} that handles
 * the loading of data into the Store.
 *
 * ProxyStore provides a few helpful methods such as {@link #method-load} and {@link #sync},
 * which load and save data respectively, passing the requests through the configured
 * {@link #proxy}.
 *
 * Built-in Store subclasses add extra behavior to each of these functions. Note also that each
 * ProxyStore subclass has its own way of storing data - in {@link Ext.data.Store} the data
 * is saved as a flat {@link Ext.util.Collection Collection}, whereas in
 * {@link Ext.data.BufferedStore BufferedStore} we use a {@link Ext.data.PageMap} to maintain
 * a client side cache of pages of records.
 *
 * The store provides filtering and sorting support. This sorting/filtering can happen on the
 * client side or can be completed on the server. This is controlled by the
 * {@link Ext.data.Store#remoteSort remoteSort} and {@link Ext.data.Store#remoteFilter remoteFilter}
 * config options. For more information see the {@link #method-sort} and
 * {@link Ext.data.Store#filter filter} methods.
 */
Ext.define('Ext.data.ProxyStore', {
    extend: 'Ext.data.AbstractStore',
    requires: [
        'Ext.data.Model',
        'Ext.data.proxy.Proxy',
        'Ext.data.proxy.Memory',
        'Ext.data.operation.*'
    ],
    config: {
        // @cmd-auto-dependency {aliasPrefix: "model.", mvc: true, blame: "all"}
        /**
         * @cfg {String/Ext.data.Model} model
         * Name of the {@link Ext.data.Model Model} associated with this store. See
         * {@link Ext.data.Model#entityName}.
         *
         * May also be the actual Model subclass.
         *
         * This config is required for the store to be able to read data unless you have
         * defined the {@link #fields} config which will create an anonymous
         * `Ext.data.Model`.
         */
        model: undefined,
        // @cmd-auto-dependency {aliasPrefix: "data.field."}
        /**
         * @cfg fields
         * @inheritdoc Ext.data.Model#cfg-fields
         * 
         * @localdoc **Note:** In general, this configuration option should only be used 
         * for simple stores like a two-field store of 
         * {@link Ext.form.field.ComboBox ComboBox}. For anything more complicated, such 
         * as specifying a particular id property or associations, a 
         * {@link Ext.data.Model Model} should be defined and specified for the 
         * {@link #model} config.
         * 
         * @since 2.3.0
         */
        fields: null,
        // @cmd-auto-dependency {aliasPrefix : "proxy."}
        /**
         * @cfg {String/Ext.data.proxy.Proxy/Object} proxy
         * The Proxy to use for this Store. This can be either a string, a config object
         * or a Proxy instance - see {@link #setProxy} for details.
         * @since 1.1.0
         */
        proxy: undefined,
        /**
         * @cfg {Boolean/Object} autoLoad
         * If data is not specified, and if autoLoad is true or an Object, this store's
         * load method is automatically called after creation. If the value of autoLoad
         * is an Object, this Object will be passed to the store's load method.
         *
         * It's important to note that {@link Ext.data.TreeStore Tree Stores} will  
         * load regardless of autoLoad's value if expand is set to true on the 
         * {@link Ext.data.TreeStore#root root node}.
         * 
         * @since 2.3.0
         */
        autoLoad: undefined,
        /**
         * @cfg {Boolean} autoSync
         * True to automatically sync the Store with its Proxy after every edit to one of
         * its Records. Defaults to false.
         */
        autoSync: false,
        /**
         * @cfg {String} batchUpdateMode
         * Sets the updating behavior based on batch synchronization. 'operation' (the
         * default) will update the Store's internal representation of the data after
         * each operation of the batch has completed, 'complete' will wait until the
         * entire batch has been completed before updating the Store's data. 'complete'
         * is a good choice for local storage proxies, 'operation' is better for remote
         * proxies, where there is a comparatively high latency.
         */
        batchUpdateMode: 'operation',
        /**
         * @cfg {Boolean} sortOnLoad
         * If true, any sorters attached to this Store will be run after loading data,
         * before the datachanged event is fired. Defaults to true, ignored if
         * {@link Ext.data.Store#remoteSort remoteSort} is true
         */
        sortOnLoad: true,
        /**
         * @cfg {Boolean} trackRemoved
         * This config controls whether removed records are remembered by this store for
         * later saving to the server.
         */
        trackRemoved: true,
        /**
         * @cfg {Boolean} asynchronousLoad
         * This defaults to `true` when this store's {@link #cfg-proxy} is asynchronous,
         * such as an {@link Ext.data.proxy.Ajax Ajax proxy}.
         *
         * When the proxy is synchronous, such as a {@link Ext.data.proxy.Memory} memory
         * proxy, this defaults to `false`.
         *
         * *NOTE:* This does not cause synchronous Ajax requests if configured `false`
         * when an Ajax proxy is used. It causes immediate issuing of an Ajax request
         * when {@link #method-load} is called rather than issuing the request at the end
         * of the current event handler run.
         *
         * What this means is that when using an Ajax proxy, calls to 
         * {@link #method-load} do not fire the request to the remote resource 
         * immediately, but schedule a request to be made. This is so that multiple 
         * requests are not fired when mutating a store's remote filters and sorters (as 
         * happens during state restoration). The request is made only once after all 
         * relevant store state is fully set.
         *
         * @since 6.0.1
         */
        asynchronousLoad: undefined
    },
    onClassExtended: function(cls, data, hooks) {
        var model = data.model,
            onBeforeClassCreated;
        if (typeof model === 'string') {
            onBeforeClassCreated = hooks.onBeforeCreated;
            hooks.onBeforeCreated = function() {
                var me = this,
                    args = arguments;
                Ext.require(model, function() {
                    onBeforeClassCreated.apply(me, args);
                });
            };
        }
    },
    /**
     * @private
     * @property {Boolean} implicitModel
     * The class name of the model that this store uses if no explicit {@link #model} is
     * given
     */
    implicitModel: 'Ext.data.Model',
    /**
     * @property {Object} lastOptions
     * Property to hold the last options from a {@link #method-load} method call. This
     * object is used for the {@link #method-reload} to reuse the same options. Please
     * see {@link #method-reload} for a simple example on how to use the lastOptions
     * property.
     */
    /**
     * @property {Number} autoSyncSuspended
     * A counter to track suspensions.
     * @private
     */
    autoSyncSuspended: 0,
    /**
     * @property {Ext.data.Model[]} removed
     * Temporary cache in which removed model instances are kept until successfully
     * synchronised with a Proxy, at which point this is cleared.
     *
     * This cache is maintained unless you set `trackRemoved` to `false`.
     *
     * @protected
     * @readonly
     */
    removed: null,
    /**
     * @event beforeload
     * Fires before a request is made for a new data object. If the beforeload handler returns
     * `false` the load action will be canceled.
     *
     * **Note:** If you are using a buffered store, you should use
     * {@link Ext.data.Store#beforeprefetch beforeprefetch}.
     * @param {Ext.data.Store} store This Store
     * @param {Ext.data.operation.Operation} operation The Ext.data.operation.Operation object
     * that will be passed to the Proxy to load the Store
     * @since 1.1.0
     */
    /**
     * @event load
     * Fires whenever the store reads data from a remote data source.
     *
     * **Note:** If you are using a buffered store, you should use
     * {@link Ext.data.Store#prefetch prefetch}.
     * @param {Ext.data.Store} this
     * @param {Ext.data.Model[]} records An array of records
     * @param {Boolean} successful True if the operation was successful.
     * @param {Ext.data.operation.Read} operation The
     * {@link Ext.data.operation.Read Operation} object that was used in the data
     * load call
     * @since 1.1.0
     */
    /**
     * @event write
     * Fires whenever a successful write has been made via the configured {@link #proxy Proxy}
     * @param {Ext.data.Store} store This Store
     * @param {Ext.data.operation.Operation} operation The
     * {@link Ext.data.operation.Operation Operation} object that was used in the write
     * @since 3.4.0
     */
    /**
     * @event beforesync
     * Fired before a call to {@link #sync} is executed. Return false from any listener to cancel
     * the sync
     * @param {Object} options Hash of all records to be synchronized, broken down into create,
     * update and destroy
     */
    /**
     * @event metachange
     * Fires when this store's underlying reader (available via the proxy) provides new metadata.
     * Metadata usually consists of new field definitions, but can include any configuration data
     * required by an application, and can be processed as needed in the event handler.
     * This event is currently only fired for JsonReaders.
     * @param {Ext.data.Store} this
     * @param {Object} meta The JSON metadata
     * @since 1.1.0
     */
    constructor: function(config) {
        var me = this;
        var configModel = me.model;
        // eslint-disable-line vars-on-top, one-var
        me.callParent(arguments);
        if (me.getAsynchronousLoad() === false) {
            me.flushLoad();
        }
        if (!me.getModel() && me.useModelWarning !== false && me.getStoreId() !== 'ext-empty-store') {
            // There are a number of ways things could have gone wrong, try to give as much
            // information as possible
            var logMsg = [
                    // eslint-disable-line vars-on-top, one-var
                    Ext.getClassName(me) || 'Store',
                    ' created with no model.'
                ];
            if (typeof configModel === 'string') {
                logMsg.push(" The name '", configModel, "'", ' does not correspond to a valid model.');
            }
            Ext.log.warn(logMsg.join(''));
        }
    },
    /**
     * @private
     */
    doDestroy: function() {
        var me = this,
            proxy = me.getProxy();
        me.clearLoadTask();
        Ext.destroy(me.getData());
        me.data = null;
        me.setProxy(null);
        if (proxy.autoCreated) {
            proxy.destroy();
        }
        me.setModel(null);
        me.callParent();
    },
    applyAsynchronousLoad: function(asynchronousLoad) {
        // Default in an asynchronousLoad setting.
        // It defaults to false if the proxy is synchronous, and true if the proxy is asynchronous.
        if (asynchronousLoad == null) {
            asynchronousLoad = !this.loadsSynchronously();
        }
        return asynchronousLoad;
    },
    updateAutoLoad: function(autoLoad) {
        // Ensure the data collection is set up
        this.getData();
        if (autoLoad) {
            // Defer the load until idle, when the store (and probably the view)
            // is fully constructed
            this.load(Ext.isObject(autoLoad) ? autoLoad : undefined);
        }
    },
    /**
     * Returns the total number of {@link Ext.data.Model Model} instances that the
     * {@link Ext.data.proxy.Proxy Proxy} indicates exist. This will usually differ from
     * {@link #getCount} when using paging - getCount returns the number of records loaded into
     * the Store at the moment, getTotalCount returns the number of records that could be loaded
     * into the Store if the Store contained all data
     * @return {Number} The total number of Model instances available via the Proxy. 0 returned if
     * no value has been set via the reader.
     */
    getTotalCount: function() {
        return this.totalCount || 0;
    },
    applyFields: function(fields) {
        if (fields) {
            this.createImplicitModel(fields);
        }
    },
    applyModel: function(model) {
        if (model) {
            model = Ext.data.schema.Schema.lookupEntity(model);
        } else if (!this.destroying) {
            // If no model, ensure that the fields config is converted to a model.
            this.getFields();
            model = this.getModel() || this.createImplicitModel();
        }
        return model;
    },
    applyProxy: function(proxy) {
        var model = this.getModel();
        if (proxy !== null) {
            if (proxy) {
                if (proxy.isProxy) {
                    proxy.setModel(model);
                } else {
                    if (Ext.isString(proxy)) {
                        proxy = {
                            type: proxy,
                            model: model
                        };
                    } else if (!proxy.model) {
                        proxy = Ext.apply({
                            model: model
                        }, proxy);
                    }
                    proxy = Ext.createByAlias('proxy.' + proxy.type, proxy);
                    proxy.autoCreated = true;
                }
            } else if (model) {
                proxy = model.getProxy();
                this.useModelProxy = true;
            }
            if (!proxy) {
                proxy = Ext.createByAlias('proxy.memory');
                proxy.autoCreated = true;
            }
        }
        return proxy;
    },
    applyState: function(state) {
        var me = this;
        me.callParent([
            state
        ]);
        // This is called during construction. Sorters and filters might have changed
        // which require a reload.
        // If autoLoad is true, it might have loaded synchronously from a memory proxy,
        // so needs to reload.
        // If it is already loaded, we definitely need to reload to apply the state.
        if (me.getAutoLoad() || me.isLoaded()) {
            me.load();
        }
    },
    updateProxy: function(proxy, oldProxy) {
        this.proxyListeners = Ext.destroy(this.proxyListeners);
    },
    updateTrackRemoved: function(track) {
        this.cleanRemoved();
        this.removed = track ? [] : null;
    },
    /**
     * @private
     */
    onMetaChange: function(proxy, meta) {
        this.fireEvent('metachange', this, meta);
    },
    // saves any phantom records
    create: function(data, options) {
        var me = this,
            Model = me.getModel(),
            instance = new Model(data),
            operation;
        options = Ext.apply({}, options);
        if (!options.records) {
            options.records = [
                instance
            ];
        }
        options.internalScope = me;
        options.internalCallback = me.onProxyWrite;
        operation = me.createOperation('create', options);
        return operation.execute();
    },
    read: function() {
        return this.load.apply(this, arguments);
    },
    update: function(options) {
        var me = this,
            operation;
        options = Ext.apply({}, options);
        if (!options.records) {
            options.records = me.getUpdatedRecords();
        }
        options.internalScope = me;
        options.internalCallback = me.onProxyWrite;
        operation = me.createOperation('update', options);
        return operation.execute();
    },
    /**
     * @private
     * Callback for any write Operation over the Proxy. Updates the Store's MixedCollection
     * to reflect the updates provided by the Proxy
     */
    onProxyWrite: function(operation) {
        var me = this,
            success = operation.wasSuccessful(),
            records = operation.getRecords();
        switch (operation.getAction()) {
            case 'create':
                me.onCreateRecords(records, operation, success);
                break;
            case 'update':
                me.onUpdateRecords(records, operation, success);
                break;
            case 'destroy':
                me.onDestroyRecords(records, operation, success);
                break;
        }
        if (success) {
            me.fireEvent('write', me, operation);
            me.fireEvent('datachanged', me);
        }
    },
    // may be implemented by store subclasses
    onCreateRecords: Ext.emptyFn,
    // may be implemented by store subclasses
    onUpdateRecords: Ext.emptyFn,
    /**
     * Removes any records when a write is returned from the server.
     * @private
     * @param {Ext.data.Model[]} records The array of removed records
     * @param {Ext.data.operation.Operation} operation The operation that just completed
     * @param {Boolean} success True if the operation was successful
     */
    onDestroyRecords: function(records, operation, success) {
        if (success) {
            this.cleanRemoved();
        }
    },
    // tells the attached proxy to destroy the given records
    // @since 3.4.0
    erase: function(options) {
        var me = this,
            operation;
        options = Ext.apply({}, options);
        if (!options.records) {
            options.records = me.getRemovedRecords();
        }
        options.internalScope = me;
        options.internalCallback = me.onProxyWrite;
        operation = me.createOperation('destroy', options);
        return operation.execute();
    },
    /**
     * @private
     * Attached as the 'operationcomplete' event listener to a proxy's Batch object. By default
     * just calls through to onProxyWrite.
     */
    onBatchOperationComplete: function(batch, operation) {
        return this.onProxyWrite(operation);
    },
    /**
     * @private
     * Attached as the 'complete' event listener to a proxy's Batch object. Iterates over the batch
     * operations and updates the Store's internal data MixedCollection.
     */
    onBatchComplete: function(batch, operation) {
        var me = this,
            operations = batch.operations,
            length = operations.length,
            i;
        if (me.batchUpdateMode !== 'operation') {
            me.suspendEvents();
            for (i = 0; i < length; i++) {
                me.onProxyWrite(operations[i]);
            }
            me.resumeEvents();
        }
        me.isSyncing = false;
        if (batch.$destroyOwner === me) {
            batch.destroy();
        }
        me.fireEvent('datachanged', me);
    },
    /**
     * @private
     */
    onBatchException: function(batch, operation) {},
    // //decide what to do... could continue with the next operation
    // batch.start();
    //
    // //or retry the last operation
    // batch.retry();
    /**
     * @private
     * Filter function for new records.
     */
    filterNew: function(item) {
        // only want phantom records that are valid
        return item.phantom && item.isValid();
    },
    /**
     * Returns all `{@link Ext.data.Model#property-phantom phantom}` records in this store.
     * @return {Ext.data.Model[]} A possibly empty array of `phantom` records.
     */
    getNewRecords: function() {
        return [];
    },
    /**
     * Returns all valid, non-phantom Model instances that have been updated in the Store but
     * not yet synchronized with the Proxy.
     * @return {Ext.data.Model[]} The updated Model instances
     */
    getUpdatedRecords: function() {
        return [];
    },
    /**
     * Gets all {@link Ext.data.Model records} added or updated since the last commit. Note that
     * the order of records returned is not deterministic and does not indicate the order in which
     * records were modified. Note also that removed records are not included
     * (use {@link #getRemovedRecords} for that).
     * @return {Ext.data.Model[]} The added and updated Model instances
     */
    getModifiedRecords: function() {
        return [].concat(this.getNewRecords(), this.getUpdatedRecords());
    },
    /**
     * @private
     * Filter function for updated records.
     */
    filterUpdated: function(item) {
        // only want dirty records, not phantoms that are valid
        return item.dirty && !item.phantom && item.isValid();
    },
    /**
     * Returns any records that have been removed from the store but not yet destroyed on the proxy.
     * @return {Ext.data.Model[]} The removed Model instances. Note that this is a *copy* of the
     * store's array, so may be mutated.
     */
    getRemovedRecords: function() {
        var removed = this.getRawRemovedRecords();
        return removed ? Ext.Array.clone(removed) : [];
    },
    /**
     * Synchronizes the store with its {@link #proxy}. This asks the proxy to batch together any
     * new, updated and deleted records in the store, updating the store's internal representation
     * of the records as each operation completes.
     * 
     * @param {Object} [options] Object containing one or more properties supported by the sync
     * method (these get  passed along to the underlying proxy's {@link Ext.data.Proxy#batch batch}
     * method):
     * 
     * @param {Ext.data.Batch/Object} [options.batch] A {@link Ext.data.Batch} object (or batch
     * config to apply  to the created batch). If unspecified a default batch will be auto-created
     * as needed.
     * 
     * @param {Function} [options.callback] The function to be called upon completion of the sync.
     * The callback is called regardless of success or failure and is passed the following
     * parameters:
     * @param {Ext.data.Batch} options.callback.batch The {@link Ext.data.Batch batch} that was
     * processed, containing all operations in their current state after processing
     * @param {Object} options.callback.options The options argument that was originally passed
     * into sync
     * 
     * @param {Function} [options.success] The function to be called upon successful completion
     * of the sync. The success function is called only if no exceptions were reported in any
     * operations. If one or more exceptions occurred then the failure function will be called
     * instead. The success function is called  with the following parameters:
     * @param {Ext.data.Batch} options.success.batch The {@link Ext.data.Batch batch} that was
     * processed, containing all operations in their current state after processing
     * @param {Object} options.success.options The options argument that was originally passed
     * into sync
     * 
     * @param {Function} [options.failure] The function to be called upon unsuccessful completion
     * of the sync. The failure function is called when one or more operations returns an exception
     * during processing (even if some operations were also successful). In this case you can check
     * the batch's {@link Ext.data.Batch#exceptions  exceptions} array to see exactly which
     * operations had exceptions. The failure function is called with the  following parameters:
     * @param {Ext.data.Batch} options.failure.batch The {@link Ext.data.Batch} that was processed,
     * containing all operations in their current state after processing
     * @param {Object} options.failure.options The options argument that was originally passed
     * into sync
     * 
     * @param {Object} [options.params] Additional params to send during the sync Operation(s).
     *
     * @param {Object} [options.scope] The scope in which to execute any callbacks (i.e. the `this`
     * object inside the callback, success and/or failure functions). Defaults to the store's proxy.
     * 
     * @return {Ext.data.Store} this
     */
    sync: function(options) {
        var me = this,
            operations = {},
            toCreate = me.getNewRecords(),
            toUpdate = me.getUpdatedRecords(),
            toDestroy = me.getRemovedRecords(),
            needsSync = false;
        if (me.isSyncing) {
            Ext.log.warn('Sync called while a sync operation is in progress. ' + 'Consider configuring autoSync as false.');
        }
        me.needsSync = false;
        if (toCreate.length > 0) {
            operations.create = toCreate;
            needsSync = true;
        }
        if (toUpdate.length > 0) {
            operations.update = toUpdate;
            needsSync = true;
        }
        if (toDestroy.length > 0) {
            operations.destroy = toDestroy;
            needsSync = true;
        }
        if (needsSync && me.fireEvent('beforesync', operations) !== false) {
            me.isSyncing = true;
            options = options || {};
            me.proxy.batch(Ext.apply(options, {
                operations: operations,
                listeners: me.getBatchListeners(),
                $destroyOwner: me
            }));
        }
        return me;
    },
    /**
     * @private
     * Returns an object which is passed in as the listeners argument to proxy.batch inside
     * this.sync. This is broken out into a separate function to allow for customisation
     * of the listeners
     * @return {Object} The listeners object
     */
    getBatchListeners: function() {
        var me = this,
            listeners = {
                scope: me,
                exception: me.onBatchException,
                complete: me.onBatchComplete
            };
        if (me.batchUpdateMode === 'operation') {
            listeners.operationcomplete = me.onBatchOperationComplete;
        }
        return listeners;
    },
    /**
     * Saves all pending changes via the configured {@link #proxy}. Use {@link #sync} instead.
     * @deprecated 4.0.0 Will be removed in the next major version
     */
    save: function() {
        return this.sync.apply(this, arguments);
    },
    /**
     * Marks this store as needing a load. When the current executing event handler exits,
     * this store will send a request to load using its configured {@link #proxy}.
     *
     * Upon return of the data from whatever data source the proxy connected to, the retrieved
     * {@link Ext.data.Model records} will be loaded into this store, and the optional callback
     * will be called. Example usage:
     *
     *     store.load({
     *         scope: this,
     *         callback: function(records, operation, success) {
     *             // the operation object
     *             // contains all of the details of the load operation
     *             console.log(records);
     *         }
     *     });
     *
     * If the callback scope does not need to be set, a function can simply be passed:
     *
     *     store.load(function(records, operation, success) {
     *         console.log('loaded records');
     *     });
     *
     * @param {Object} [options] This is passed into the
     * {@link Ext.data.operation.Operation Operation} object that is created and then sent to the
     * proxy's {@link Ext.data.proxy.Proxy#read} function. In addition to the options listed below,
     * this object may contain properties to configure the
     * {@link Ext.data.operation.Operation Operation}.
     * @param {Function} [options.callback] A function which is called when the response arrives.
     * @param {Ext.data.Model[]} options.callback.records Array of records.
     * @param {Ext.data.operation.Operation} options.callback.operation The Operation itself.
     * @param {Boolean} options.callback.success `true` when operation completed successfully.
     * @param {Boolean} [options.addRecords=false] Specify as `true` to *add* the incoming records
     * rather than the default which is to have the incoming records *replace* the existing store
     * contents.
     * 
     * @return {Ext.data.Store} this
     * @since 1.1.0
     */
    load: function(options) {
        var me = this;
        // Legacy option. Specifying a function was allowed.
        if (typeof options === 'function') {
            options = {
                callback: options
            };
        } else {
            // We may mutate the options object in setLoadOptions.
            options = options ? Ext.Object.chain(options) : {};
        }
        me.pendingLoadOptions = options;
        // If we are configured to load asynchronously (the default for async proxies)
        // then schedule a flush, unless one is already scheduled.
        if (me.getAsynchronousLoad()) {
            if (!me.loadTimer) {
                me.loadTimer = Ext.asap(me.flushLoad, me);
            }
        } else // If we are configured to load synchronously (the default for sync proxies)
        // then flush the load now.
        {
            me.flushLoad();
        }
        return me;
    },
    /**
     * Called when the event handler which called the {@link #method-load} method exits.
     */
    flushLoad: function() {
        var me = this,
            options = me.pendingLoadOptions,
            operation;
        if (me.destroying || me.destroyed) {
            return;
        }
        // If it gets called programatically before the timer fired, the listener will need
        // cancelling.
        me.clearLoadTask();
        if (!options) {
            return;
        }
        me.setLoadOptions(options);
        if (me.getRemoteSort() && options.sorters) {
            me.fireEvent('beforesort', me, options.sorters);
        }
        operation = Ext.apply({
            internalScope: me,
            internalCallback: me.onProxyLoad,
            scope: me
        }, options);
        me.lastOptions = operation;
        operation = me.createOperation('read', operation);
        if (me.fireEvent('beforeload', me, operation) !== false) {
            me.onBeforeLoad(operation);
            me.loading = true;
            // Internal event, fired after the flag is set, we need
            // to fire this beforeload is too early
            if (me.hasListeners.beginload) {
                me.fireEvent('beginload', me, operation);
            }
            operation.execute();
        } else {
            if (me.getAsynchronousLoad()) {
                operation.abort();
            }
            operation.setCompleted();
        }
    },
    /**
     * Reloads the store using the last options passed to the {@link #method-load} method.
     * You can use the reload method to reload the store using the parameters from the last load()
     * call. For example:
     *
     *     store.load({
     *         params : {
     *             userid : 22216
     *         }
     *     });
     *
     *     //...
     *
     *     store.reload();
     *
     * The initial {@link #method-load} execution will pass the `userid` parameter in the request.
     * The {@link #reload} execution will also send the same `userid` parameter in its request
     * as it will reuse the `params` object from the last {@link #method-load} call.
     *
     * You can override a param by passing in the config object with the `params` object:
     *
     *     store.load({
     *         params : {
     *             userid : 22216,
     *             foo    : 'bar'
     *         }
     *     });
     *
     *     //...
     *
     *     store.reload({
     *         params : {
     *             userid : 1234
     *         }
     *     });
     *
     * The initial {@link #method-load} execution sends the `userid` and `foo` parameters but in the
     * {@link #reload} it only sends the `userid` paramter because you are overriding the `params`
     * config not just overriding the one param. To only change a single param but keep other
     * params, you will have to get the last params from the {@link #lastOptions} property:
     *
     *     // make a copy of the last params so we don't affect future reload() calls
     *     var lastOptions = store.lastOptions,
     *         lastParams = Ext.clone(lastOptions.params);
     *
     *     lastParams.userid = 1234;
     *
     *     store.reload({
     *         params : lastParams
     *     });
     *
     * This will now send the `userid` parameter as `1234` and the `foo` param as `'bar'`.
     *
     * @param {Object} [options] A config object which contains options which may override the
     * options passed to the previous load call. See the
     * {@link #method-load} method for valid configs.
     */
    reload: function(options) {
        return this.load(Ext.apply({}, options, this.lastOptions));
    },
    onEndUpdate: function() {
        var me = this;
        if (me.needsSync && me.autoSync && !me.autoSyncSuspended) {
            me.sync();
        }
    },
    /**
     * @private
     * A model instance should call this method on the Store it has been
     * {@link Ext.data.Model#join joined} to.
     * @param {Ext.data.Model} record The model instance that was edited
     * @since 3.4.0
     */
    afterReject: function(record) {
        var me = this;
        // Must pass the 5th param (modifiedFieldNames) as null, otherwise the
        // event firing machinery appends the listeners "options" object to the arg list
        // which may get used as the modified fields array by a handler.
        // This array is used for selective grid cell updating by Grid View.
        // Null will be treated as though all cells need updating.
        if (me.contains(record)) {
            me.onUpdate(record, Ext.data.Model.REJECT, null);
            me.fireEvent('update', me, record, Ext.data.Model.REJECT, null);
            me.fireEvent('datachanged', me);
        }
    },
    /**
     * A model instance should call this method on the Store it has been
     * {@link Ext.data.Model#join joined} to.
     * @param {Ext.data.Model} record The model instance that was edited.
     * @param {String[]} [modifiedFieldNames] (private)
     * @since 3.4.0
     * @private
     */
    afterCommit: function(record, modifiedFieldNames) {
        var me = this;
        if (!modifiedFieldNames) {
            modifiedFieldNames = null;
        }
        if (me.contains(record)) {
            me.onUpdate(record, Ext.data.Model.COMMIT, modifiedFieldNames);
            me.fireEvent('update', me, record, Ext.data.Model.COMMIT, modifiedFieldNames);
            me.fireEvent('datachanged', me);
        }
    },
    afterErase: function(record) {
        this.onErase(record);
    },
    onErase: Ext.emptyFn,
    onUpdate: Ext.emptyFn,
    /**
     * Returns true if the store has a pending load task.
     * @return {Boolean} `true` if the store has a pending load task.
     * @private
     */
    hasPendingLoad: function() {
        return !!this.pendingLoadOptions || this.isLoading();
    },
    /**
     * Returns true if the Store is currently performing a load operation
     * @return {Boolean} `true` if the Store is currently loading
     */
    isLoading: function() {
        return !!this.loading;
    },
    /**
     * Returns `true` if the Store has been loaded.
     * @return {Boolean} `true` if the Store has been loaded.
     */
    isLoaded: function() {
        return this.loadCount > 0;
    },
    /**
     * Suspends automatically syncing the Store with its Proxy. Only applicable if
     * {@link #autoSync} is `true`
     */
    suspendAutoSync: function() {
        ++this.autoSyncSuspended;
    },
    /**
     * Resumes automatically syncing the Store with its Proxy. Only applicable if
     * {@link #autoSync} is `true`
     * @param {Boolean} syncNow Pass `true` to synchronize now. Only synchronizes with the Proxy
     * if the suspension count has gone to zero (We are not under a higher level of suspension)
     * 
     */
    resumeAutoSync: function(syncNow) {
        var me = this;
        if (!me.autoSyncSuspended) {
            Ext.log.warn('Mismatched call to resumeAutoSync - auto synchronization ' + 'is currently not suspended.');
        }
        if (me.autoSyncSuspended && !--me.autoSyncSuspended) {
            if (syncNow) {
                me.sync();
            }
        }
    },
    /**
     * Removes all records from the store. This method does a "fast remove",
     * individual remove events are not called. The {@link #clear} event is
     * fired upon completion.
     * @method
     * @since 1.1.0
     */
    removeAll: Ext.emptyFn,
    // individual store subclasses should implement a "fast" remove
    // and fire a clear event afterwards
    // to be implemented by subclasses
    clearData: Ext.emptyFn,
    privates: {
        /**
         * @private
         * Returns the array of records which have been removed since the last time this store
         * was synced.
         *
         * This is used internally, when purging removed records after a successful sync.
         * This is overridden by TreeStore because TreeStore accumulates deleted records on removal
         * of child nodes from their parent, *not* on removal of records from its collection.
         * The collection has records added on expand, and removed on collapse.
         */
        getRawRemovedRecords: function() {
            return this.removed;
        },
        onExtraParamsChanged: function() {},
        clearLoadTask: function() {
            this.pendingLoadOptions = this.loadTimer = Ext.unasap(this.loadTimer);
        },
        cleanRemoved: function() {
            // Must use class-specific getRawRemovedRecords.
            // Regular Stores add to the "removed" property on remove.
            // TreeStores are having records removed all the time; node collapse removes.
            // TreeStores add to the "removedNodes" property onNodeRemove
            var removed = this.getRawRemovedRecords(),
                len, i;
            if (removed) {
                for (i = 0 , len = removed.length; i < len; ++i) {
                    removed[i].unjoin(this);
                }
                removed.length = 0;
            }
        },
        createOperation: function(type, options) {
            var me = this,
                proxy = me.getProxy(),
                listeners;
            if (!me.proxyListeners) {
                listeners = {
                    scope: me,
                    destroyable: true,
                    beginprocessresponse: me.beginUpdate,
                    endprocessresponse: me.endUpdate
                };
                if (!me.disableMetaChangeEvent) {
                    listeners.metachange = me.onMetaChange;
                }
                me.proxyListeners = proxy.on(listeners);
            }
            return proxy.createOperation(type, options);
        },
        createImplicitModel: function(fields) {
            var me = this,
                modelCfg = {
                    extend: me.implicitModel,
                    statics: {
                        defaultProxy: 'memory'
                    }
                },
                proxy, model;
            if (fields) {
                modelCfg.fields = fields;
            }
            model = Ext.define(null, modelCfg);
            me.setModel(model);
            proxy = me.getProxy();
            if (proxy) {
                model.setProxy(proxy);
            } else {
                me.setProxy(model.getProxy());
            }
        },
        loadsSynchronously: function() {
            return this.getProxy().isSynchronous;
        },
        onBeforeLoad: Ext.privateFn,
        removeFromRemoved: function(record) {
            // Must use class-specific getRawRemovedRecords.
            // Regular Stores add to the "removed" property on remove.
            // TreeStores are having records removed all the time; node collapse removes.
            // TreeStores add to the "removedNodes" property onNodeRemove
            var removed = this.getRawRemovedRecords();
            if (removed) {
                Ext.Array.remove(removed, record);
                record.unjoin(this);
            }
        },
        setLoadOptions: function(options) {
            var me = this,
                filters, sorters;
            if (me.getRemoteFilter()) {
                filters = me.getFilters(false);
                if (filters && filters.getCount()) {
                    options.filters = filters.getRange();
                }
            }
            if (me.getRemoteSort()) {
                sorters = me.getSorters(false);
                if (sorters && sorters.getCount()) {
                    options.sorters = sorters.getRange();
                }
            }
        }
    }
});

/**
 * Encapsulates a grouped collection of records within a {@link Ext.util.Collection}
 */
Ext.define('Ext.util.Group', {
    extend: 'Ext.util.Collection',
    isGroup: true,
    config: {
        groupKey: null
    },
    // Group collections must have a higher priority than normal collections.  This ensures
    // that their endupdate handlers for filters and sorters run prior to the endupdate
    // handler of the store's main collection, and so when the user handles events such
    // as sort/datachanged, the groups have already been sorted and filtered.
    $endUpdatePriority: 2001,
    manageSorters: false
});

/**
 * Encapsulates a group of records. Can provide a
 * {@link #getSummaryRecord} summary record.
 *
 * @since 6.5.0
 */
Ext.define('Ext.data.Group', {
    extend: 'Ext.util.Group',
    isDataGroup: true,
    store: null,
    /**
     * Returns the summary results for the group.
     * @return {Ext.data.Model}
     */
    getSummaryRecord: function() {
        var me = this,
            summaryRecord = me.summaryRecord,
            store = me.store,
            generation = store.getData().generation,
            M, T;
        if (!summaryRecord) {
            M = store.getModel();
            T = M.getSummaryModel();
            me.summaryRecord = summaryRecord = new T();
        }
        if (!summaryRecord.isRemote && summaryRecord.summaryGeneration !== generation) {
            summaryRecord.calculateSummary(me.items);
            summaryRecord.summaryGeneration = generation;
        }
        return summaryRecord;
    }
});

/**
 * A mixin that provides common store methods for Ext.data.Store & Ext.data.ChainedStore.
 * @private
 */
Ext.define('Ext.data.LocalStore', {
    extend: 'Ext.Mixin',
    requires: [
        'Ext.data.Group'
    ],
    mixinConfig: {
        id: 'localstore',
        after: {
            fireGroupChangeEvent: 'onGrouperChange'
        }
    },
    config: {
        extraKeys: null
    },
    applyExtraKeys: function(extraKeys) {
        var indexName,
            data = this.getData();
        // Add the extra keys to the data collection
        data.setExtraKeys(extraKeys);
        // Pluck the extra keys out so that we can keep them by index name
        extraKeys = data.getExtraKeys();
        for (indexName in extraKeys) {
            this[indexName] = extraKeys[indexName];
        }
    },
    /**
     * Adds Model instance to the Store. This method accepts either:
     *
     * - An array of Model instances or Model configuration objects.
     * - Any number of Model instance or Model configuration object arguments.
     *
     * The new Model instances will be added at the end of the existing collection.
     *
     * Sample usage:
     *
     *     myStore.add({some: 'data'}, {some: 'other data'});
     *
     * Note that if this Store is sorted, the new Model instances will be inserted
     * at the correct point in the Store to maintain the sort order.
     *
     * @param {Ext.data.Model[]/Ext.data.Model.../Object[]/Object...} record An array of
     * records or configuration objects, or variable number of record or config arguments.
     * @return {Ext.data.Model[]} The record instances that were added.
     */
    add: function(record) {
        return this.insert(this.getCount(), arguments.length === 1 ? record : arguments);
    },
    constructDataCollection: function() {
        var result = new Ext.util.Collection({
                id: this.getId() + '-data',
                rootProperty: 'data',
                groupConfig: {
                    xclass: 'Ext.data.Group',
                    store: this
                }
            });
        // Add this store as an observer immediately so that we are informed of any
        // synchronous autoLoad which may occur in this event.
        result.addObserver(this);
        return result;
    },
    /**
     * Converts a literal to a model, if it's not a model already
     * @private
     * @param {Ext.data.Model/Object} record The record to create
     * @return {Ext.data.Model}
     */
    createModel: function(record) {
        var session = this.getSession(),
            Model;
        if (!record.isModel) {
            Model = this.getModel();
            record = new Model(record, session);
        }
        return record;
    },
    createFiltersCollection: function() {
        return this.getData().getFilters();
    },
    createSortersCollection: function() {
        var sorters = this.getData().getSorters();
        sorters.setSorterConfigure(this.addFieldTransform, this);
        return sorters;
    },
    /**
     * Get the summary record for this store. See {@link Ext.data.Model#summary}.
     * @return {Ext.data.Model}
     * @since 6.5.0
     */
    getSummaryRecord: function() {
        var me = this,
            summaryRecord = me.summaryRecord,
            data = me.getData(),
            generation = data.generation,
            T;
        if (!summaryRecord) {
            T = me.getModel().getSummaryModel();
            me.summaryRecord = summaryRecord = new T();
        }
        if (!summaryRecord.isRemote && summaryRecord.summaryGeneration !== generation) {
            summaryRecord.calculateSummary(data.items);
            summaryRecord.summaryGeneration = generation;
        }
        return summaryRecord;
    },
    onCollectionBeginUpdate: function() {
        this.beginUpdate();
    },
    onCollectionEndUpdate: function() {
        this.endUpdate();
    },
    // When the collection informs us that it has sorted, this LocalStore must react.
    // AbstractStore#onSorterEndUpdate does the correct thing (fires a refresh) if remote sorting
    // is false
    onCollectionSort: function() {
        this.onSorterEndUpdate();
    },
    // When the collection informs us that it has filtered, this LocalStore must react.
    // AbstractStore#onFilterEndUpdate does the correct thing (fires a refresh) if remote sorting
    // is false
    onCollectionFilter: function() {
        this.onFilterEndUpdate();
    },
    onGrouperChange: function(grouper) {
        this.callObservers('GrouperChange', [
            grouper
        ]);
    },
    notifySorterChange: function() {
        this.getData().onSorterChange();
    },
    forceLocalSort: function() {
        var sorters = this.getSorters();
        // Sorter collection must inform all interested parties.
        // We cannot just tell our data Collection to react - there
        // may be GroupCollections hooked into the endUpdate call.
        sorters.beginUpdate();
        sorters.endUpdate();
    },
    // Inherit docs
    contains: function(record) {
        return this.indexOf(record) > -1;
    },
    /**
     * Calls the specified function for each {@link Ext.data.Model record} in the store.
     *
     * When store is filtered, only loops over the filtered records.
     *
     * @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed
     * as the first parameter. Returning `false` aborts and exits the iteration.
     * @param {Object} [scope] The scope (`this` reference) in which the function is executed.
     * Defaults to the current {@link Ext.data.Model record} in the iteration.
     * @param {Object/Boolean} [includeOptions] An object which contains options which
     * modify how the store is traversed. Or simply the `filtered` option.
     * @param {Boolean} [includeOptions.filtered] Pass `true` to include filtered out
     * nodes in the iteration.
     */
    each: function(fn, scope, includeOptions) {
        var data = this.getData(),
            bypassFilters = includeOptions,
            len, record, i;
        if (typeof includeOptions === 'object') {
            bypassFilters = includeOptions.filtered;
        }
        if (bypassFilters && data.filtered) {
            data = data.getSource();
        }
        data = data.items.slice(0);
        // safe for re-entrant calls
        len = data.length;
        for (i = 0; i < len; ++i) {
            record = data[i];
            if (fn.call(scope || record, record, i, len) === false) {
                break;
            }
        }
    },
    /**
     * Collects unique values for a particular dataIndex from this store.
     *
     * Note that the `filtered` option can also be passed as a separate parameter for
     * compatibility with previous versions.
     *
     *     var store = Ext.create('Ext.data.Store', {
     *         fields: ['name'],
     *         data: [{
     *             name: 'Larry'
     *         }, {
     *             name: 'Darryl'
     *         }, {
     *             name: 'Darryl'
     *         }]
     *     });
     *
     *     store.collect('name');
     *     // returns ["Larry", "Darryl"]
     *
     * @param {String} property The property to collect
     * @param {Object} [includeOptions] An object which contains options which modify how
     * the store is traversed. For compatibility, this argument may be the `allowNull`
     * value itself. If so, the next argument is the `filtered` value.
     * @param {Boolean} [includeOptions.allowNull] Pass true to allow null, undefined or
     * empty string values.
     * @param {Boolean} [includeOptions.filtered] Pass `true` to collect from all records,
     * even ones which are filtered.
     * @param {Boolean} [filtered] This argument only applies when the legacy call form
     * is used and `includeOptions` is actually the `allowNull` value.
     *
     * @return {Object[]} An array of the unique values
     */
    collect: function(property, includeOptions, filtered) {
        var me = this,
            allowNull = includeOptions,
            data = me.getData();
        if (typeof includeOptions === 'object') {
            filtered = includeOptions.filtered;
            allowNull = includeOptions.allowNull;
        }
        if (filtered && data.filtered) {
            data = data.getSource();
        }
        return data.collect(property, 'data', allowNull);
    },
    /**
     * Get the Record with the specified id.
     *
     * This method is not affected by filtering, lookup will be performed from all records
     * inside the store, filtered or not.
     *
     * @param {Mixed} id The id of the Record to find.
     * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.
     */
    getById: function(id) {
        var data = this.getData();
        if (data.filtered) {
            data = data.getSource();
        }
        return data.get(id) || null;
    },
    /**
     * @private
     * Get the Record with the specified internalId.
     *
     * This method is not affected by filtering, lookup will be performed from all records
     * inside the store, filtered or not.
     *
     * @param {Mixed} internalId The id of the Record to find.
     * @return {Ext.data.Model} The Record with the passed internalId. Returns null if not found.
     */
    getByInternalId: function(internalId) {
        var data = this.getData(),
            keyCfg;
        if (data.filtered) {
            if (!data.$hasExtraKeys) {
                keyCfg = this.makeInternalKeyCfg();
                data.setExtraKeys(keyCfg);
                data.$hasExtraKeys = true;
            }
            data = data.getSource();
        }
        if (!data.$hasExtraKeys) {
            data.setExtraKeys(keyCfg || this.makeInternalKeyCfg());
            data.$hasExtraKeys = true;
        }
        return data.byInternalId.get(internalId) || null;
    },
    /**
     * Returns the complete unfiltered collection.
     * @private
     */
    getDataSource: function() {
        var data = this.getData();
        return data.getSource() || data;
    },
    /**
     * Get the index of the record within the store.
     *
     * When store is filtered, records outside of filter will not be found.
     *
     * @param {Ext.data.Model} record The Ext.data.Model object to find.
     * @return {Number} The index of the passed Record. Returns -1 if not found.
     */
    indexOf: function(record) {
        return this.getData().indexOf(record);
    },
    /**
     * Get the index within the store of the Record with the passed id.
     *
     * Like #indexOf, this method is affected by filtering.
     *
     * @param {String} id The id of the Record to find.
     * @return {Number} The index of the Record. Returns -1 if not found.
     */
    indexOfId: function(id) {
        return this.indexOf(this.getById(id));
    },
    /**
     * Inserts Model instances into the Store at the given index and fires the add event.
     * See also {@link #method-add}.
     *
     * @param {Number} index The start index at which to insert the passed Records.
     * @param {Ext.data.Model/Ext.data.Model[]/Object/Object[]} records An `Ext.data.Model`
     * instance, the data needed to populate an instance or an array of either of these.
     * 
     * @return {Ext.data.Model[]} records The added records
     */
    insert: function(index, records) {
        var me = this,
            len, i;
        if (records) {
            if (!Ext.isIterable(records)) {
                records = [
                    records
                ];
            } else {
                records = Ext.Array.clone(records);
            }
            len = records.length;
        }
        if (!len) {
            return [];
        }
        for (i = 0; i < len; ++i) {
            records[i] = me.createModel(records[i]);
        }
        me.getData().insert(index, records);
        return records;
    },
    /**
     * Query all the cached records in this Store using a filtering function. The specified function
     * will be called with each record in this Store. If the function returns `true` the record is
     * included in the results.
     *
     * This method is not affected by filtering, it will always search *all* records in the store
     * regardless of filtering.
     *
     * @param {Function} fn The function to be called. It will be passed the following parameters:
     *  @param {Ext.data.Model} fn.record The record to test for filtering. Access field values
     *  using {@link Ext.data.Model#get}.
     *  @param {Object} fn.id The ID of the Record passed.
     * @param {Object} [scope] The scope (this reference) in which the function is executed
     * Defaults to this Store.
     * @return {Ext.util.Collection} The matched records
     */
    queryBy: function(fn, scope) {
        var data = this.getData();
        return (data.getSource() || data).createFiltered(fn, scope);
    },
    /**
     * Query all the cached records in this Store by name/value pair.
     * The parameters will be used to generated a filter function that is given
     * to the queryBy method.
     *
     * This method complements queryBy by generating the query function automatically.
     *
     * This method is not affected by filtering, it will always search *all* records in the store
     * regardless of filtering.
     *
     * @param {String} property The property to create the filter function for
     * @param {String/RegExp} value The string/regex to compare the property value to
     * @param {Boolean} [anyMatch=false] True to match any part of the string, not just the
     * beginning.
     * @param {Boolean} [caseSensitive=false] `true` to create a case-sensitive regex.
     * @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters
     * added to the regex). Ignored if `anyMatch` is `true`.
     * @return {Ext.util.Collection} The matched records
     */
    query: function(property, value, anyMatch, caseSensitive, exactMatch) {
        var data = this.getData();
        return (data.getSource() || data).createFiltered(property, value, anyMatch, caseSensitive, exactMatch);
    },
    /**
     * Convenience function for getting the first model instance in the store.
     *
     * When store is filtered, will return first item within the filter.
     *
     * @param {Boolean} [grouped] True to perform the operation for each group
     * in the store. The value returned will be an object literal with the key being the group
     * name and the first record being the value. The grouped parameter is only honored if
     * the store has a groupField.
     * @return {Ext.data.Model/undefined} The first model instance in the store, or undefined
     */
    first: function(grouped) {
        return this.getData().first(grouped) || null;
    },
    /**
     * Convenience function for getting the last model instance in the store.
     *
     * When store is filtered, will return last item within the filter.
     *
     * @param {Boolean} [grouped] True to perform the operation for each group
     * in the store. The value returned will be an object literal with the key being the group
     * name and the last record being the value. The grouped parameter is only honored if
     * the store has a groupField.
     * @return {Ext.data.Model/undefined} The last model instance in the store, or undefined
     */
    last: function(grouped) {
        return this.getData().last(grouped) || null;
    },
    /**
     * Sums the value of `field` for each {@link Ext.data.Model record} in store
     * and returns the result.
     *
     * When store is filtered, only sums items within the filter.
     *
     * @param {String} field A field in each record
     * @param {Boolean} [grouped] True to perform the operation for each group
     * in the store. The value returned will be an object literal with the key being the group
     * name and the sum for that group being the value. The grouped parameter is only honored if
     * the store has a groupField.
     * @return {Number} The sum
     */
    sum: function(field, grouped) {
        var data = this.getData();
        return (grouped && this.isGrouped()) ? data.sumByGroup(field) : data.sum(field);
    },
    /**
     * Gets the count of items in the store.
     *
     * When store is filtered, only items within the filter are counted.
     *
     * @param {Boolean} [grouped] True to perform the operation for each group
     * in the store. The value returned will be an object literal with the key being the group
     * name and the count for each group being the value. The grouped parameter is only honored if
     * the store has a groupField.
     * @return {Number} the count
     */
    count: function(grouped) {
        var data = this.getData();
        return (grouped && this.isGrouped()) ? data.countByGroup() : data.count();
    },
    /**
     * Gets the minimum value in the store.
     *
     * When store is filtered, only items within the filter are aggregated.
     *
     * @param {String} field The field in each record
     * @param {Boolean} [grouped] True to perform the operation for each group
     * in the store. The value returned will be an object literal with the key being the group
     * name and the minimum in the group being the value. The grouped parameter is only honored if
     * the store has a groupField.
     * @return {Object} The minimum value, if no items exist, undefined.
     */
    min: function(field, grouped) {
        var data = this.getData();
        return (grouped && this.isGrouped()) ? data.minByGroup(field) : data.min(field);
    },
    /**
     * Gets the maximum value in the store.
     *
     * When store is filtered, only items within the filter are aggregated.
     *
     * @param {String} field The field in each record
     * @param {Boolean} [grouped] True to perform the operation for each group
     * in the store. The value returned will be an object literal with the key being the group
     * name and the maximum in the group being the value. The grouped parameter is only honored if
     * the store has a groupField.
     * @return {Object} The maximum value, if no items exist, undefined.
     */
    max: function(field, grouped) {
        var data = this.getData();
        return (grouped && this.isGrouped()) ? data.maxByGroup(field) : data.max(field);
    },
    /**
     * Gets the average value in the store.
     *
     * When store is filtered, only items within the filter are aggregated.
     *
     * @param {String} field The field in each record
     * @param {Boolean} [grouped] True to perform the operation for each group
     * in the store. The value returned will be an object literal with the key being the group
     * name and the group average being the value. The grouped parameter is only honored if
     * the store has a groupField.
     * @return {Object} The average value, if no items exist, 0.
     */
    average: function(field, grouped) {
        var data = this.getData();
        return (grouped && this.isGrouped()) ? data.averageByGroup(field) : data.average(field);
    },
    /**
     * Runs the aggregate function for all the records in the store.
     *
     * When store is filtered, only items within the filter are aggregated.
     *
     * @param {Function} fn The function to execute. The function is called with a single parameter,
     * an array of records for that group.
     * @param {Object} [scope] The scope to execute the function in. Defaults to the store.
     * @param {Boolean} [grouped] True to perform the operation for each group
     * in the store. The value returned will be an object literal with the key being the group
     * name and the group average being the value. The grouped parameter is only honored if
     * the store has a groupField.
     * @param {String} field The field to get the value from
     * @return {Object} An object literal with the group names and their appropriate values.
     */
    aggregate: function(fn, scope, grouped, field) {
        var me = this,
            groups, len, out, group, i;
        if (grouped && me.isGrouped()) {
            groups = me.getGroups().items;
            len = groups.length;
            out = {};
            for (i = 0; i < len; ++i) {
                group = groups[i];
                out[group.getGroupKey()] = me.getAggregate(fn, scope || me, group.items, field);
            }
            return out;
        } else {
            return me.getAggregate(fn, scope, me.getData().items, field);
        }
    },
    getAggregate: function(fn, scope, records, field) {
        var values = [],
            len = records.length,
            i;
        // TODO EXTJSIV-12307 - not the right way to call fn
        for (i = 0; i < len; ++i) {
            values[i] = records[i].get(field);
        }
        return fn.call(scope || this, records, values);
    },
    addObserver: function(observer) {
        var observers = this.observers;
        if (!observers) {
            this.observers = observers = new Ext.util.Collection();
        }
        observers.add(observer);
    },
    removeObserver: function(observer) {
        var observers = this.observers;
        if (observers) {
            observers.remove(observer);
        }
    },
    callObservers: function(action, args) {
        var observers = this.observers,
            len, items, i, methodName, item;
        if (observers) {
            items = observers.items;
            if (args) {
                args.unshift(this);
            } else {
                args = [
                    this
                ];
            }
            for (i = 0 , len = items.length; i < len; ++i) {
                item = items[i];
                methodName = 'onSource' + action;
                if (item[methodName]) {
                    item[methodName].apply(item, args);
                }
            }
        }
    },
    /**
     * Query all the cached records in this Store using a filtering function. The specified function
     * will be called with each record in this Store. If the function returns `true` the record is
     * included in the results.
     *
     * This method is not affected by filtering, it will always search *all* records in the store
     * regardless of filtering.
     * 
     * @param {Function} fn The function to be called. It will be passed the following parameters:
     *   @param {Ext.data.Model} fn.record The record to test for filtering.
     * @param {Object} [scope] The scope (this reference) in which the function is executed
     * Defaults to this Store.
     * @return {Ext.data.Model[]} The matched records.
     *
     * @private
     */
    queryRecordsBy: function(fn, scope) {
        var data = this.getData(),
            matches = [],
            len, i, record;
        data = (data.getSource() || data).items;
        scope = scope || this;
        for (i = 0 , len = data.length; i < len; ++i) {
            record = data[i];
            if (fn.call(scope, record) === true) {
                matches.push(record);
            }
        }
        return matches;
    },
    /**
     * Query all the cached records in this Store by field.
     *
     * This method is not affected by filtering, it will always search *all* records in the store
     * regardless of filtering.
     * 
     * @param {String} field The field from each record to use.
     * @param {Object} value The value to match.
     * @return {Ext.data.Model[]} The matched records.
     *
     * @private
     */
    queryRecords: function(field, value) {
        var data = this.getData(),
            matches = [],
            len, i, record;
        data = (data.getSource() || data).items;
        for (i = 0 , len = data.length; i < len; ++i) {
            record = data[i];
            if (record.get(field) === value) {
                matches.push(record);
            }
        }
        return matches;
    },
    privates: {
        isLast: function(record) {
            return record === this.last();
        },
        makeInternalKeyCfg: function() {
            return {
                byInternalId: {
                    property: 'internalId',
                    rootProperty: ''
                }
            };
        }
    }
});

/**
 * ServerProxy is a superclass of {@link Ext.data.proxy.JsonP JsonPProxy} and
 * {@link Ext.data.proxy.Ajax AjaxProxy}, and would not usually be used directly.
 * @protected
 */
Ext.define('Ext.data.proxy.Server', {
    extend: 'Ext.data.proxy.Proxy',
    alias: 'proxy.server',
    alternateClassName: 'Ext.data.ServerProxy',
    uses: [
        'Ext.data.Request'
    ],
    isRemote: true,
    config: {
        /**
         * @cfg {String} url
         * The URL from which to request the data object.
         */
        url: '',
        /**
         * @cfg {String} [pageParam="page"]
         * The name of the 'page' parameter to send in a request. Defaults to 'page'. Set this to
         * `''` if you don't want to send a page parameter.
         */
        pageParam: 'page',
        /**
         * @cfg {String} [startParam="start"]
         * The name of the 'start' parameter to send in a request. Defaults to 'start'. Set this to
         * `''` if you don't want to send a start parameter.
         */
        startParam: 'start',
        /**
         * @cfg {String} [limitParam="limit"]
         * The name of the 'limit' parameter to send in a request. Defaults to 'limit'. Set this to
         * `''` if you don't want to send a limit parameter.
         */
        limitParam: 'limit',
        /**
         * @cfg {String} [groupParam="group"]
         * The name of the 'group' parameter to send in a request. Defaults to 'group'. Set this to
         * `''` if you don't want to send a group parameter.
         */
        groupParam: 'group',
        /**
         * @cfg {String} [groupDirectionParam="groupDir"]
         * The name of the direction parameter to send in a request. **This is only used when
         * simpleGroupMode is set to true.**
         * If this is set to the same value as the {@link #groupParam}, then the group property
         * name *and* direction of each grouper is passed as a single, space separated parameter,
         * looking like a database `group by` specification.
         *
         * So if there are multiple groupers, the single group parameter will look like this:
         *
         *     ?group=name%20ASC&group=age%20DESC
         */
        groupDirectionParam: 'groupDir',
        /**
         * @cfg {String} [sortParam="sort"]
         * The name of the 'sort' parameter to send in a request. Defaults to 'sort'. Set this to
         * `''` if you don't want to send a sort parameter.
         */
        sortParam: 'sort',
        /**
         * @cfg {String} [filterParam="filter"]
         * The name of the 'filter' parameter to send in a request. Defaults to 'filter'. Set this
         * to `''` if you don't want to send a filter parameter.
         */
        filterParam: 'filter',
        /**
         * @cfg {String} [directionParam="dir"]
         * The name of the direction parameter to send in a request. **This is only used when
         * simpleSortMode is set to true.**
         * 
         * If this is set to the same value as the {@link #sortParam}, then the sort property name
         * *and* direction of each sorter is passed as a single, space separated parameter, looking
         * like a database `order by` specification.
         *
         * So if there are multiple sorters, the single sort parameter will look like this:
         *
         *     ?sort=name%20ASC&sort=age%20DESC
         */
        directionParam: 'dir',
        /**
         * @cfg {String} [idParam="id"]
         * The name of the parameter which carries the id of the entity being operated upon.
         */
        idParam: 'id',
        /**
         * @cfg {Boolean} [simpleSortMode=false]
         * Enabling simpleSortMode in conjunction with remoteSort will send the sorted field names
         * in the parameter named by {@link #sortParam}, and the directions for each sorted field
         * in a parameter named by {@link #directionParam}.
         *
         * In the simplest case, with one Sorter, this will result in HTTP parameters like this:
         *
         *     ?sort=name&dir=ASC
         *
         * If there are multiple sorters, the parameters will be encoded like this:
         *
         *     ?sort=name&sort=age&dir=ASC&dir=DESC
         */
        simpleSortMode: false,
        /**
         * @cfg {Boolean} [simpleGroupMode=false]
         * Enabling simpleGroupMode in conjunction with remoteGroup will only send one group
         * property and a direction when a remote group is requested. The
         * {@link #groupDirectionParam} and {@link #groupParam} will be sent with the property name
         * and either 'ASC' or 'DESC'.
         */
        simpleGroupMode: false,
        /**
         * @cfg {Boolean} [noCache=true]
         * Disable caching by adding a unique parameter name to the request. Set to false to allow
         * caching. Defaults to true.
         */
        noCache: true,
        /**
         * @cfg {String} [cacheString="_dc"]
         * The name of the cache param added to the url when using noCache. Defaults to "_dc".
         */
        cacheString: "_dc",
        /**
         * @cfg {Number} timeout
         * The number of milliseconds to wait for a response. Defaults to 30000 milliseconds
         * (30 seconds).
         */
        timeout: 30000,
        /**
         * @cfg {Object} api
         * Specific urls to call on CRUD action methods "create", "read", "update" and "destroy".
         * Defaults to:
         *
         *     api: {
         *         create  : undefined,
         *         read    : undefined,
         *         update  : undefined,
         *         destroy : undefined
         *     }
         *
         * The url is built based upon the action being executed [create|read|update|destroy] using
         * the commensurate {@link #api} property, or if undefined default to the configured
         * {@link Ext.data.Store}.{@link Ext.data.proxy.Server#url url}.
         *
         * For example:
         *
         *     api: {
         *         create  : '/controller/new',
         *         read    : '/controller/load',
         *         update  : '/controller/update',
         *         destroy : '/controller/destroy_action'
         *     }
         *
         * If the specific URL for a given CRUD action is undefined, the CRUD action request will
         * be directed to the configured {@link Ext.data.proxy.Server#url url}.
         */
        api: {
            create: undefined,
            read: undefined,
            update: undefined,
            destroy: undefined
        },
        /**
         * @cfg {Object} extraParams
         * Extra parameters that will be included on every request. Individual requests with params
         * of the same name will override these params when they are in conflict.
         */
        extraParams: {}
    },
    /**
     * @event exception
     * Fires when the server returns an exception. This event may also be listened
     * to in the event that a request has timed out or has been aborted.
     * @param {Ext.data.proxy.Proxy} this
     * @param {Ext.data.Response} response The response that was received
     * @param {Ext.data.operation.Operation} operation The operation that triggered the request
     */
    // in a ServerProxy all four CRUD operations are executed in the same manner, so we delegate to
    // doRequest in each case
    create: function() {
        return this.doRequest.apply(this, arguments);
    },
    read: function() {
        return this.doRequest.apply(this, arguments);
    },
    update: function() {
        return this.doRequest.apply(this, arguments);
    },
    erase: function() {
        return this.doRequest.apply(this, arguments);
    },
    /**
     * Sets a value in the underlying {@link #extraParams}.
     * @param {String} name The key for the new value
     * @param {Object} value The value
     */
    setExtraParam: function(name, value) {
        var extraParams = this.getExtraParams();
        extraParams[name] = value;
        this.fireEvent('extraparamschanged', extraParams);
    },
    updateExtraParams: function(newExtraParams, oldExtraParams) {
        this.fireEvent('extraparamschanged', newExtraParams);
    },
    /**
     * Creates an {@link Ext.data.Request Request} object from
     * {@link Ext.data.operation.Operation Operation}.
     *
     * This gets called from doRequest methods in subclasses of Server proxy.
     * 
     * @param {Ext.data.operation.Operation} operation The operation to execute
     * @return {Ext.data.Request} The request object
     */
    buildRequest: function(operation) {
        var me = this,
            initialParams = Ext.apply({}, operation.getParams()),
            // Clone params right now so that they can be mutated at any point further down the
            // call stack
            params = Ext.applyIf(initialParams, me.getExtraParams() || {}),
            request, operationId, idParam;
        // copy any sorters, filters etc into the params so they can be sent over the wire
        Ext.applyIf(params, me.getParams(operation));
        // Set up the entity id parameter according to the configured name.
        // This defaults to "id". But TreeStore has a "nodeParam" configuration which
        // specifies the id parameter name of the node being loaded.
        operationId = operation.getId();
        idParam = me.getIdParam();
        if (operationId !== undefined && params[idParam] === undefined) {
            params[idParam] = operationId;
        }
        request = new Ext.data.Request({
            params: params,
            action: operation.getAction(),
            records: operation.getRecords(),
            url: operation.getUrl(),
            operation: operation,
            // this is needed by JsonSimlet in order to properly construct responses for
            // requests from this proxy
            proxy: me
        });
        request.setUrl(me.buildUrl(request));
        /*
         * Save the request on the Operation. Operations don't usually care about Request and
         * Response data, but in the ServerProxy and any of its subclasses we add both request
         * and response as they may be useful for further processing
         */
        operation.setRequest(request);
        return request;
    },
    /**
     * Processes response, which may involve updating or committing records, each of which
     * will inform the owning stores and their interested views. Finally, we may perform
     * an additional layout if the data shape has changed. 
     *
     * @protected
     */
    processResponse: function(success, operation, request, response) {
        var me = this,
            exception, reader, resultSet, meta, destroyOp;
        // Async callback could have landed at any time, including during and after
        // destruction. We don't want to unravel the whole response chain in such case.
        if (me.destroying || me.destroyed) {
            return;
        }
        // Processing a response may involve updating or committing many records
        // each of which will inform the owning stores, which will ultimately
        // inform interested views which will most likely have to do a layout
        // assuming that the data shape has changed.
        // Bracketing the processing with this event gives owning stores the ability
        // to fire their own beginupdate/endupdate events which can be used by interested
        // views to suspend layouts.
        me.fireEvent('beginprocessresponse', me, response, operation);
        if (success === true) {
            reader = me.getReader();
            if (response.status === 204) {
                resultSet = reader.getNullResultSet();
            } else {
                resultSet = reader.read(me.extractResponseData(response), {
                    // If we're doing an update, we want to construct the models ourselves.
                    recordCreator: operation.getRecordCreator() || reader.defaultRecordCreatorFromServer
                });
            }
            if (!operation.$destroyOwner) {
                operation.$destroyOwner = me;
                destroyOp = true;
            }
            operation.process(resultSet, request, response);
            exception = !operation.wasSuccessful();
        } else {
            me.setException(operation, response);
            exception = true;
        }
        // It is possible that exception callback destroyed the store and owning proxy,
        // in which case we can't do nothing except punt.
        if (me.destroyed) {
            if (!operation.destroyed && destroyOp && operation.$destroyOwner === me) {
                operation.destroy();
            }
            return;
        }
        if (exception) {
            me.fireEvent('exception', me, response, operation);
        } else // If a JsonReader detected metadata, process it now.
        // This will fire the 'metachange' event which the Store processes to fire its own
        // 'metachange'
        {
            meta = resultSet.getMetadata();
            if (meta) {
                me.onMetaChange(meta);
            }
        }
        // Ditto
        if (me.destroyed) {
            if (!operation.destroyed && destroyOp && operation.$destroyOwner === me) {
                operation.destroy();
            }
            return;
        }
        me.afterRequest(request, success);
        // Tell owning store processing has finished.
        // It will fire its endupdate event which will cause interested views to 
        // resume layouts.
        me.fireEvent('endprocessresponse', me, response, operation);
        if (!operation.destroyed && destroyOp && operation.$destroyOwner === me) {
            operation.destroy();
        }
    },
    /**
     * Sets up an exception on the operation
     * @private
     * @param {Ext.data.operation.Operation} operation The operation
     * @param {Object} response The response
     */
    setException: function(operation, response) {
        operation.setException({
            status: response.status,
            statusText: response.statusText,
            response: response
        });
    },
    /**
     * @method
     * Template method to allow subclasses to specify how to get the response for the reader.
     * @template
     * @private
     * @param {Object} response The server response
     * @return {Object} The response data to be used by the reader
     */
    extractResponseData: Ext.identityFn,
    /**
     * Encode any values being sent to the server. Can be overridden in subclasses.
     * @protected
     * @param {Array} value An array of sorters/filters.
     * @return {Object} The encoded value
     */
    applyEncoding: function(value) {
        return Ext.encode(value);
    },
    /**
     * Encodes the array of {@link Ext.util.Sorter} objects into a string to be sent in the request
     * url. By default, this simply JSON-encodes the sorter data
     * @param {Ext.util.Sorter[]} sorters The array of {@link Ext.util.Sorter Sorter} objects
     * @param {Boolean} [preventArray=false] Prevents the items from being output as an array.
     * @return {String} The encoded sorters
     */
    encodeSorters: function(sorters, preventArray) {
        var out = [],
            length = sorters.length,
            i;
        for (i = 0; i < length; i++) {
            out[i] = sorters[i].serialize();
        }
        return this.applyEncoding(preventArray ? out[0] : out);
    },
    /**
     * Encodes the array of {@link Ext.util.Filter} objects into a string to be sent in the request
     * url. By default, this simply JSON-encodes the filter data
     * @param {Ext.util.Filter[]} filters The array of {@link Ext.util.Filter Filter} objects
     * @return {String} The encoded filters
     */
    encodeFilters: function(filters) {
        var out = [],
            length = filters.length,
            encode, i;
        for (i = 0; i < length; i++) {
            encode |= filters[i].serializeTo(out);
        }
        // If any Filters return Objects encapsulating their full state, then the parameters
        // needs JSON encoding.
        return encode ? this.applyEncoding(out) : out;
    },
    /**
     * @private
     * Copy any sorters, filters etc into the params so they can be sent over the wire
     */
    getParams: function(operation) {
        if (!operation.isReadOperation) {
            return {};
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            params = {},
            grouper = operation.getGrouper(),
            sorters = operation.getSorters(),
            filters = operation.getFilters(),
            page = operation.getPage(),
            start = operation.getStart(),
            limit = operation.getLimit(),
            simpleSortMode = me.getSimpleSortMode(),
            simpleGroupMode = me.getSimpleGroupMode(),
            pageParam = me.getPageParam(),
            startParam = me.getStartParam(),
            limitParam = me.getLimitParam(),
            groupParam = me.getGroupParam(),
            groupDirectionParam = me.getGroupDirectionParam(),
            sortParam = me.getSortParam(),
            filterParam = me.getFilterParam(),
            directionParam = me.getDirectionParam(),
            hasGroups, index;
        if (pageParam && page) {
            params[pageParam] = page;
        }
        if (startParam && (start || start === 0)) {
            params[startParam] = start;
        }
        if (limitParam && limit) {
            params[limitParam] = limit;
        }
        hasGroups = groupParam && grouper;
        if (hasGroups) {
            // Grouper is a subclass of sorter, so we can just use the sorter method
            if (simpleGroupMode) {
                params[groupParam] = grouper.getProperty();
                // Allow for direction to be encoded into the same parameter
                if (groupDirectionParam === groupParam) {
                    params[groupParam] += ' ' + grouper.getDirection();
                } else {
                    params[groupDirectionParam] = grouper.getDirection();
                }
            } else {
                params[groupParam] = me.encodeSorters([
                    grouper
                ], true);
            }
        }
        /* eslint-disable max-len */
        if (sortParam && sorters && sorters.length > 0) {
            if (simpleSortMode) {
                // Group will be included in sorters, so skip sorter 0 if groups
                for (index = (sorters.length > 1 && hasGroups) ? 1 : 0; index < sorters.length; index++) {
                    // Allow for direction to be encoded into the same parameter
                    if (directionParam === sortParam) {
                        params[sortParam] = Ext.Array.push(params[sortParam] || [], sorters[index].getProperty() + ' ' + sorters[index].getDirection());
                    } else {
                        params[sortParam] = Ext.Array.push(params[sortParam] || [], sorters[index].getProperty());
                        params[directionParam] = Ext.Array.push(params[directionParam] || [], sorters[index].getDirection());
                    }
                }
            } else {
                params[sortParam] = me.encodeSorters(sorters);
            }
        }
        /* eslint-enable max-len */
        if (filterParam && filters && filters.length > 0) {
            params[filterParam] = me.encodeFilters(filters);
        }
        return params;
    },
    /**
     * Generates a url based on a given Ext.data.Request object. By default, ServerProxy's buildUrl
     * will add the cache-buster param to the end of the url. Subclasses may need to perform
     * additional modifications to the url.
     * @param {Ext.data.Request} request The request object
     * @return {String} The url
     */
    buildUrl: function(request) {
        var me = this,
            url = me.getUrl(request);
        if (!url) {
            Ext.raise("You are using a ServerProxy but have not supplied it with a url.");
        }
        if (me.getNoCache()) {
            url = Ext.urlAppend(url, Ext.String.format("{0}={1}", me.getCacheString(), Ext.Date.now()));
        }
        return url;
    },
    /**
     * Get the url for the request taking into account the order of priority,
     * - The request
     * - The api
     * - The url
     * @private
     * @param {Ext.data.Request} request The request
     * @return {String} The url
     */
    getUrl: function(request) {
        var url;
        if (request) {
            url = request.getUrl() || this.getApi()[request.getAction()];
        }
        return url ? url : this.callParent();
    },
    /**
     * In ServerProxy subclasses, the {@link #method-create}, {@link #method-read},
     * {@link #method-update}, and {@link #method-erase} methods all pass through to doRequest.
     * Each ServerProxy subclass must implement the doRequest method - see
     * {@link Ext.data.proxy.JsonP} and {@link Ext.data.proxy.Ajax} for examples. This method
     * carries the same signature as each of the methods that delegate to it.
     *
     * @param {Ext.data.operation.Operation} operation The Ext.data.operation.Operation object
     * @param {Function} callback The callback function to call when the Operation has completed
     * @param {Object} scope The scope in which to execute the callback
     */
    doRequest: function(operation, callback, scope) {
        Ext.raise("The doRequest function has not been implemented on your " + "Ext.data.proxy.Server subclass. See src/data/ServerProxy.js for details");
    },
    /**
     * Optional callback function which can be used to clean up after a request has been completed.
     * @param {Ext.data.Request} request The Request object
     * @param {Boolean} success True if the request was successful
     * @protected
     * @template
     * @method
     */
    afterRequest: Ext.emptyFn,
    destroy: function() {
        var me = this;
        me.destroying = true;
        // Don't force Reader and Writer creation if they weren't yet instantiated
        me.reader = me.writer = Ext.destroy(me.reader, me.writer);
        me.callParent();
        // This just makes it hard to ask "was destroy() called?":
        // me.destroying = false; // removed in 7.0
        me.destroyed = true;
    }
});

/**
 * AjaxProxy is one of the most widely-used ways of getting data into your application. It uses AJAX
 * requests to load data from the server, usually to be placed into a {@link Ext.data.Store Store}.
 * Let's take a look at a typical setup. Here we're going to set up a Store that has an Ajax Proxy.
 * To prepare, we'll also set up a {@link Ext.data.Model Model}:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name', 'email']
 *     });
 *
 *     // The Store contains the AjaxProxy as an inline configuration
 *     var store = Ext.create('Ext.data.Store', {
 *         model: 'User',
 *         proxy: {
 *             type: 'ajax',
 *             url: 'users.json'
 *         }
 *     });
 *
 *     store.load();
 *
 * Our example is going to load user data into a Store, so we start off by defining a
 * {@link Ext.data.Model Model} with the fields that we expect the server to return. Next we set up
 * the Store itself, along with a {@link Ext.data.Store#proxy proxy} configuration.
 * This configuration was automatically turned into an Ext.data.proxy.Ajax instance, with the url
 * we specified being passed into AjaxProxy's constructor. It's as if we'd done this:
 *
 *     new Ext.data.proxy.Ajax({
 *         url: 'users.json',
 *         model: 'User',
 *         reader: 'json'
 *     });
 *
 * A couple of extra configurations appeared here - {@link #model} and {@link #reader}. These are
 * set by default when we create the proxy via the Store - the Store already knows about the Model,
 * and Proxy's default {@link Ext.data.reader.Reader Reader} is
 * {@link Ext.data.reader.Json JsonReader}.
 *
 * Now when we call store.load(), the AjaxProxy springs into action, making a request to the url
 * we configured ('users.json' in this case). As we're performing a read, it sends a GET request
 * to that url (see {@link #actionMethods} to customize this - by default any kind of read will be
 * sent as a GET request and any kind of write will be sent as a POST request).
 *
 * # Limitations
 *
 * AjaxProxy cannot be used to retrieve data from other domains. If your application is running
 * on http://domainA.com it cannot load data from http://domainB.com because browsers have
 * a built-in security policy that prohibits domains talking to each other via AJAX.
 *
 * If you need to read data from another domain and can't set up a proxy server (some software
 * that runs on your own domain's web server and transparently forwards requests to
 * http://domainB.com, making it look like they actually came from http://domainA.com), you can use
 * {@link Ext.data.proxy.JsonP} and a technique known as JSON-P (JSON with Padding), which can help
 * you get around the problem so long as the server on http://domainB.com is set up to support
 * JSON-P responses. See {@link Ext.data.proxy.JsonP JsonPProxy}'s introduction docs for more
 * details.
 *
 * # Readers and Writers
 *
 * AjaxProxy can be configured to use any type of {@link Ext.data.reader.Reader Reader} to decode
 * the server's response. If no Reader is supplied, AjaxProxy will default to using a
 * {@link Ext.data.reader.Json JsonReader}. Reader configuration can be passed in as a simple
 * object, which the Proxy automatically turns into a {@link Ext.data.reader.Reader Reader}
 * instance:
 *
 *     var proxy = new Ext.data.proxy.Ajax({
 *         model: 'User',
 *         reader: {
 *             type: 'xml',
 *             rootProperty: 'users'
 *         }
 *     });
 *
 *     proxy.getReader(); // returns an XmlReader instance based on the config we supplied
 *
 * # Url generation
 *
 * AjaxProxy automatically inserts any sorting, filtering, paging and grouping options into the url
 * it generates for each request. These are controlled with the following configuration options:
 *
 * - {@link #pageParam} - controls how the page number is sent to the server (see also
 *   {@link #startParam} and {@link #limitParam})
 * - {@link #sortParam} - controls how sort information is sent to the server
 * - {@link #groupParam} - controls how grouping information is sent to the server
 * - {@link #filterParam} - controls how filter information is sent to the server
 *
 * Each request sent by AjaxProxy is described by an {@link Ext.data.operation.Operation Operation}.
 * To see how we can customize the generated urls, let's say we're loading the Proxy
 * with the following Operation:
 *
 *     var proxy = new Ext.data.proxy.Ajax({
 *         url: '/users'
 *     });
 *
 *     var operation = proxy.createOperation('read', {
 *         page: 2
 *     });
 *
 * Now we'll issue the request for this Operation by calling {@link #read}:
 *
 *     proxy.read(operation); // GET /users?page=2
 *
 * Easy enough - the Proxy just copied the page property from the Operation. We can customize
 * how this page data is sent to the server:
 *
 *     var proxy = new Ext.data.proxy.Ajax({
 *         url: '/users',
 *         pageParam: 'pageNumber'
 *     });
 *
 *     proxy.read(operation); // GET /users?pageNumber=2
 *
 * Alternatively, our Operation could have been configured to send start and limit parameters
 * instead of page:
 *
 *     var proxy = new Ext.data.proxy.Ajax({
 *         url: '/users'
 *     });
 *
 *     var operation = proxy.createOperation('read', {
 *         start: 50,
 *         limit: 25
 *     });
 *
 *     proxy.read(operation); // GET /users?start=50&limit;=25
 *
 * Again we can customize this url:
 *
 *     var proxy = new Ext.data.proxy.Ajax({
 *         url: '/users',
 *         startParam: 'startIndex',
 *         limitParam: 'limitIndex'
 *     });
 *
 *     proxy.read(operation); // GET /users?startIndex=50&limitIndex;=25
 *
 * AjaxProxy will also send sort and filter information to the server. Let's take a look at how
 * this looks with a more expressive Operation object:
 *
 *     var operation = proxy.createOperation('read', {
 *         sorters: [
 *             new Ext.util.Sorter({
 *                 property: 'name',
 *                 direction: 'ASC'
 *             }),
 *             new Ext.util.Sorter({
 *                 property: 'age',
 *                 direction: 'DESC'
 *             })
 *         ],
 *         filters: [
 *             new Ext.util.Filter({
 *                 property: 'eyeColor',
 *                 value: 'brown'
 *             })
 *         ]
 *     });
 *
 * This is the type of object that is generated internally when loading a
 * {@link Ext.data.Store Store} with sorters and filters defined. By default the AjaxProxy will
 * JSON encode the sorters and filters, resulting in something like this (note that the url
 * is escaped before sending the request, but is left unescaped here for clarity):
 *
 *     var proxy = new Ext.data.proxy.Ajax({
 *         url: '/users'
 *     });
 *
 *     // GET /users?sort=[{"property":"name","direction":"ASC"},
 *                         {"property":"age","direction":"DESC"}]
 *                         &filter;=[{"property":"eyeColor","value":"brown"}]
 *     proxy.read(operation);
 *
 * We can again customize how this is created by supplying a few configuration options. Let's say
 * our server is set up to receive sorting information is a format like "sortBy=name#ASC,age#DESC".
 * We can configure AjaxProxy to provide that format like this:
 *
 *      var proxy = new Ext.data.proxy.Ajax({
 *          url: '/users',
 *          sortParam: 'sortBy',
 *          filterParam: 'filterBy',
 *
 *          // our custom implementation of sorter encoding -
 *          // turns our sorters into "name#ASC,age#DESC"
 *          encodeSorters: function(sorters) {
 *              var length = sorters.length,
 *                  sortStrs = [],
 *                  sorter, i;
 *
 *              for (i = 0; i < length; i++) {
 *                  sorter = sorters[i];
 *
 *                  sortStrs[i] = sorter.property + '#' + sorter.direction
 *              }
 *
 *              return sortStrs.join(",");
 *          }
 *      });
 *
 *      // GET /users?sortBy=name#ASC,age#DESC&filterBy;=[{"property":"eyeColor","value":"brown"}]
 *      proxy.read(operation);
 *
 * We can also provide a custom {@link #encodeFilters} function to encode our filters.
 *
 * # Debugging your Ajax Proxy
 *
 * If the data is not being loaded into the store as expected, it could be due to a mismatch
 * between the the way that the {@link #reader} is configured, and the shape of the incoming data.
 *
 * To debug from the point that your data arrives back from the network, set a breakpoint inside
 * the callback function created in the `createRequestCallback` method of the Ajax Proxy class,
 * and follow the data to where the {@link #reader} attempts to consume it.
 *
 * @constructor
 * Note that if this HttpProxy is being used by a {@link Ext.data.Store Store}, then the Store's
 * call to {@link Ext.data.Store#method-load load} will override any specified callback and params
 * options. In this case, use the {@link Ext.data.Store Store}'s events to modify parameters,
 * or react to loading events.
 *
 * @param {Object} config (optional) Config object.
 * If an options parameter is passed, the singleton {@link Ext.Ajax} object will be used to make
 * the request.
 */
Ext.define('Ext.data.proxy.Ajax', {
    requires: [
        'Ext.Ajax'
    ],
    extend: 'Ext.data.proxy.Server',
    alias: 'proxy.ajax',
    alternateClassName: [
        'Ext.data.HttpProxy',
        'Ext.data.AjaxProxy'
    ],
    isAjaxProxy: true,
    // Keep a default copy of the action methods here. Ideally could just null
    // out actionMethods and just check if it exists & has a property, otherwise
    // fallback to the default. But at the moment it's defined as a public property,
    // so we need to be able to maintain the ability to modify/access it. 
    defaultActionMethods: {
        create: 'POST',
        read: 'GET',
        update: 'POST',
        destroy: 'POST'
    },
    config: {
        /**
        * @cfg {Boolean} binary
        * True to request binary data from the server.  This feature requires
        * the use of a binary reader such as {@link Ext.data.amf.Reader AMF Reader}
        */
        binary: false,
        /**
         * @cfg {Object} [headers]
         * Any headers to add to the Ajax request.
         *
         * example:
         *
         *     proxy: {
         *         headers: {'Content-Type': "text/plain" }
         *         ...
         *     }
         */
        headers: undefined,
        /**
         * @cfg {Boolean} paramsAsJson
         * Set to `true` to have any request parameters sent as
         * {@link Ext.data.Connection#method-request jsonData} where they can be parsed from the
         * raw request. By default, parameters are sent via the
         * {@link Ext.data.Connection#method-request params} property.
         * **Note**: This setting does not apply when the request is sent as a 'GET' request.
         * See {@link #cfg!actionMethods} for controlling the HTTP verb that is used when sending
         * requests.
         */
        paramsAsJson: false,
        /**
         * @cfg {Boolean} withCredentials
         * This configuration is sometimes necessary when using cross-origin resource sharing.
         * @accessor
         */
        withCredentials: false,
        /**
         * @cfg {Boolean} useDefaultXhrHeader
         * Set this to false to not send the default Xhr header (X-Requested-With) with every
         * request. This should be set to false when making CORS (cross-domain) requests.
         * @accessor
         */
        useDefaultXhrHeader: true,
        /**
         * @cfg {String} username
         * Most oData feeds require basic HTTP authentication. This configuration allows
         * you to specify the username.
         * @accessor
         */
        username: null,
        /**
         * @cfg {String} password
         * Most oData feeds require basic HTTP authentication. This configuration allows
         * you to specify the password.
         * @accessor
         */
        password: null,
        /**
        * @cfg {Object} actionMethods
        * Mapping of action name to HTTP request method. In the basic AjaxProxy these are set to
        * 'GET' for 'read' actions and 'POST' for 'create', 'update' and 'destroy' actions. The
        * {@link Ext.data.proxy.Rest} maps these to the correct RESTful methods.
        */
        actionMethods: {
            create: 'POST',
            read: 'GET',
            update: 'POST',
            destroy: 'POST'
        }
    },
    doRequest: function(operation) {
        var me = this,
            writer = me.getWriter(),
            request = me.buildRequest(operation),
            method = me.getMethod(request),
            jsonData, params;
        if (writer && operation.allowWrite()) {
            request = writer.write(request);
        }
        request.setConfig({
            binary: me.getBinary(),
            headers: me.getHeaders(),
            timeout: me.getTimeout(),
            scope: me,
            callback: me.createRequestCallback(request, operation),
            method: method,
            useDefaultXhrHeader: me.getUseDefaultXhrHeader(),
            disableCaching: false
        });
        // explicitly set it to false, ServerProxy handles caching
        if (me.responseType != null && Ext.supports.XHR2) {
            request.setResponseType(me.responseType);
        }
        if (method.toUpperCase() !== 'GET' && me.getParamsAsJson()) {
            params = request.getParams();
            if (params) {
                jsonData = request.getJsonData();
                if (jsonData) {
                    jsonData = Ext.Object.merge({}, jsonData, params);
                } else {
                    jsonData = params;
                }
                request.setJsonData(jsonData);
                request.setParams(undefined);
            }
        }
        if (me.getWithCredentials()) {
            request.setWithCredentials(true);
            request.setUsername(me.getUsername());
            request.setPassword(me.getPassword());
        }
        return me.sendRequest(request);
    },
    /**
     * Fires a request
     * @param {Ext.data.Request} request The request
     * @return {Ext.data.Request} The request
     * @private
     */
    sendRequest: function(request) {
        request.setRawRequest(Ext.Ajax.request(request.getCurrentConfig()));
        this.lastRequest = request;
        return request;
    },
    /**
     * Aborts a running request.
     * @param {Ext.data.Request} [request] The request to abort. If not passed, the most recent
     * active request will be aborted.
     */
    abort: function(request) {
        request = request || this.lastRequest;
        if (request) {
            Ext.Ajax.abort(request.getRawRequest());
        }
    },
    /**
     * Returns the HTTP method name for a given request. By default this returns based on a lookup
     * on {@link #cfg!actionMethods}.
     * @param {Ext.data.Request} request The request object
     * @return {String} The HTTP method to use (should be one of 'GET', 'POST', 'PUT' or 'DELETE')
     */
    getMethod: function(request) {
        var actions = this.getActionMethods(),
            action = request.getAction(),
            method;
        if (actions) {
            method = actions[action];
        }
        return method || this.defaultActionMethods[action];
    },
    /**
     * @private
     * TODO: This is currently identical to the JsonPProxy version except for the return function's
     * signature. There is a lot of code duplication inside the returned function so we need to
     * find a way to DRY this up.
     * @param {Ext.data.Request} request The Request object
     * @param {Ext.data.operation.Operation} operation The Operation being executed
     * @return {Function} The callback function
     */
    createRequestCallback: function(request, operation) {
        return function(options, success, response) {
            var me = this;
            if (request === me.lastRequest) {
                me.lastRequest = null;
            }
            if (!me.destroying && !me.destroyed) {
                me.processResponse(success, operation, request, response);
            }
        };
    },
    destroy: function() {
        this.lastRequest = null;
        this.callParent();
    }
});

/**
 * The JSON Reader is used by a Proxy to read a server response that is sent back in JSON format.
 * This usually happens as a result of loading a Store - for example we might create something
 * like this:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name', 'email']
 *     });
 *
 *     var store = Ext.create('Ext.data.Store', {
 *         model: 'User',
 *         proxy: {
 *             type: 'ajax',
 *             url: 'users.json',
 *             reader: {
 *                 type: 'json'
 *             }
 *         }
 *     });
 *
 * The example above creates a 'User' model. Models are explained in the
 * {@link Ext.data.Model Model} docs if you're not already familiar with them.
 *
 * We created the simplest type of JSON Reader possible by simply telling our
 * {@link Ext.data.Store Store}'s {@link Ext.data.proxy.Proxy Proxy} that we want a JSON Reader.
 * The Store automatically passes the configured model to the Store, so it is as if we passed
 * this instead:
 *
 *     reader: {
 *         type: 'json',
 *         model: 'User'
 *     }
 *
 * The reader we set up is ready to read data from our server - at the moment it will accept
 * a response like this:
 *
 *     [
 *         {
 *             "id": 1,
 *             "name": "Ed Spencer",
 *             "email": "ed@sencha.com"
 *         },
 *         {
 *             "id": 2,
 *             "name": "Abe Elias",
 *             "email": "abe@sencha.com"
 *         }
 *     ]
 *
 * ## Reading other JSON formats
 *
 * If you already have your JSON format defined and it doesn't look quite like what we have above,
 * you can usually pass JsonReader a couple of configuration options to make it parse your format.
 * For example, we can use the {@link #cfg-rootProperty} configuration to parse data that comes back
 * like this:
 *
 *     {
 *         "users": [
 *            {
 *                "id": 1,
 *                "name": "Ed Spencer",
 *                "email": "ed@sencha.com"
 *            },
 *            {
 *                "id": 2,
 *                "name": "Abe Elias",
 *                "email": "abe@sencha.com"
 *            }
 *         ]
 *     }
 *
 * To parse this we just pass in a {@link #rootProperty} configuration that matches the 'users'
 * above:
 *
 *     reader: {
 *         type: 'json',
 *         rootProperty: 'users'
 *     }
 *
 * Sometimes the JSON structure is even more complicated. Document databases like CouchDB often
 * provide metadata around each record inside a nested structure like this:
 *
 *     {
 *         "total": 122,
 *         "offset": 0,
 *         "users": [
 *             {
 *                 "id": "ed-spencer-1",
 *                 "value": 1,
 *                 "user": {
 *                     "id": 1,
 *                     "name": "Ed Spencer",
 *                     "email": "ed@sencha.com"
 *                 }
 *             }
 *         ]
 *     }
 *
 * In the case above the record data is nested an additional level inside the "users" array as each
 * "user" item has additional metadata surrounding it ('id' and 'value' in this case). To parse
 * data out of each "user" item in the JSON above we need to specify the {@link #record}
 * configuration like this:
 *
 *     reader: {
 *         type: 'json',
 *         rootProperty: 'users',
 *         record: 'user'
 *     }
 *
 * ## Response MetaData
 *
 * The server can return metadata in its response, in addition to the record data, that describe
 * attributes of the data set itself or are used to reconfigure the Reader. To pass metadata
 * in the response you simply add a `metaData` attribute to the root of the response data.
 * The metaData attribute can contain anything, but supports a specific set of properties
 * that are handled by the Reader if they are present:
 * 
 * - {@link #rootProperty}: the property name of the root response node containing the record data
 * - {@link #totalProperty}: property name for the total number of records in the data
 * - {@link #successProperty}: property name for the success status of the response
 * - {@link #messageProperty}: property name for an optional response message
 * - {@link Ext.data.Model#cfg-fields fields}: Config used to reconfigure the Model's fields
 *   before converting the response data into records
 * 
 * An initial Reader configuration containing all of these properties might look like this
 * ("fields" would be included in the Model definition, not shown):
 *
 *     reader: {
 *         type: 'json',
 *         rootProperty: 'root',
 *         totalProperty: 'total',
 *         successProperty: 'success',
 *         messageProperty: 'message'
 *     }
 *
 * If you were to pass a response object containing attributes different from those initially
 * defined above, you could use the `metaData` attribute to reconfigure the Reader on the fly.
 * For example:
 *
 *     {
 *         "count": 1,
 *         "ok": true,
 *         "msg": "Users found",
 *         "users": [{
 *             "userId": 123,
 *             "name": "Ed Spencer",
 *             "email": "ed@sencha.com"
 *         }],
 *         "metaData": {
 *             "rootProperty": "users",
 *             "totalProperty": 'count',
 *             "successProperty": 'ok',
 *             "messageProperty": 'msg'
 *         }
 *     }
 *
 * You can also place any other arbitrary data you need into the `metaData` attribute which will be
 * ignored by the Reader, but will be accessible via the Reader's {@link #metaData} property
 * (which is also passed to listeners via the Proxy's
 * {@link Ext.data.proxy.Proxy#metachange metachange} event (also relayed by the store).
 * Application code can then process the passed metadata in any way it chooses.
 * 
 * A simple example for how this can be used would be customizing the fields for a Model that is
 * bound to a grid. By passing the `fields` property the Model will be automatically updated by the
 * Reader internally, but that change will not be reflected automatically in the grid unless you
 * also update the column configuration. You could do this manually, or you could simply pass
 * a standard grid {@link Ext.panel.Table#columns column} config object as part of the `metaData`
 * attribute and then pass that along to the grid. Here's a very simple example for how that
 * could be accomplished:
 *
 *     // response format:
 *     {
 *         ...
 *         "metaData": {
 *             "fields": [
 *                 { "name": "userId", "type": "int" },
 *                 { "name": "name", "type": "string" },
 *                 { "name": "birthday", "type": "date", "dateFormat": "Y-j-m" },
 *             ],
 *             "columns": [
 *                 { "text": "User ID", "dataIndex": "userId", "width": 40 },
 *                 { "text": "User Name", "dataIndex": "name", "flex": 1 },
 *                 { "text": "Birthday", "dataIndex": "birthday", "flex": 1,
 *                   "format": 'Y-j-m', "xtype": "datecolumn" }
 *             ]
 *         }
 *     }
 *
 * The Reader will automatically read the meta fields config and rebuild the Model based on the new
 * fields, but to handle the new column configuration you would need to handle the metadata within
 * the application code. This is done simply enough by handling the metachange event on either
 * the store or the proxy, e.g.:
 *
 *     var store = Ext.create('Ext.data.Store', {
 *         ...
 *         listeners: {
 *             'metachange': function(store, meta) {
 *                 myGrid.reconfigure(store, meta.columns);
 *             }
 *         }
 *     });
 *
 */
Ext.define('Ext.data.reader.Json', {
    extend: 'Ext.data.reader.Reader',
    alternateClassName: 'Ext.data.JsonReader',
    alias: 'reader.json',
    requires: [
        'Ext.JSON'
    ],
    config: {
        /**
        * @cfg {String} record The optional location within the JSON response that the record data
        * itself can be found at. See the JsonReader intro docs for more details. This is not often
        * needed.
        */
        record: null,
        /**
        * @cfg {String} [metaProperty]
        * Name of the property from which to retrieve the `metaData` attribute. See
        * {@link #metaData}.
        */
        metaProperty: 'metaData',
        /**
        * @cfg {Boolean} useSimpleAccessors True to ensure that field names/mappings are treated
        * as literals when reading values.
        *
        * For example, by default, using the mapping "foo.bar.baz" will try and read a property foo
        * from the root, then a property bar from foo, then a property baz from bar. Setting the
        * simple accessors to true will read the property with the name "foo.bar.baz" direct from
        * the root object.
        */
        useSimpleAccessors: false,
        /**
         * @cfg {Boolean} preserveRawData
         * The reader will keep a copy of the most recent request in the {@link #rawData} property.
         * For performance reasons, the data object for each record is used directly as the model
         * data. This means that these objects may be modified and thus modify the raw data.
         * To ensure the objects are copied, set this option to `true`.
         * NB: This only applies to items  that are read as part of the data array, any other
         * metadata will not be modified:
         * 
         *     {
         *         "someOtherData": 1, // Won't be modified
         *         "root": [{}, {}, {}] // The objects here will be modified
         *     }
         */
        preserveRawData: false
    },
    /**
     * @private
     */
    responseType: 'json',
    updateRootProperty: function() {
        this.forceBuildExtractors();
    },
    updateMetaProperty: function() {
        this.forceBuildExtractors();
    },
    /**
     * @method readRecords
     * Reads a JSON object and returns a ResultSet. Uses the internal getTotal and getSuccess
     * extractors to retrieve meta data from the response, and extractData to turn the JSON data
     * into model instances.
     * @param {Object} data The raw JSON data
     * @param {Object} [readOptions] See {@link #read} for details.
     * @return {Ext.data.ResultSet} A ResultSet containing model instances and meta data about
     *  the results
     */
    getResponseData: function(response) {
        var error;
        if (typeof response.responseJson === 'object') {
            return response.responseJson;
        }
        try {
            return Ext.decode(response.responseText);
        } catch (ex) {
            error = this.createReadError(ex.message);
            Ext.Logger.warn('Unable to parse the JSON returned by the server');
            this.fireEvent('exception', this, response, error);
            return error;
        }
    },
    buildExtractors: function(force) {
        var me = this,
            emptyFn = Ext.emptyFn;
        // Will only return true if we need to build
        if (me.callParent([
            force
        ])) {
            me.getRoot = me.setupExtractor(me.getRootProperty(), Ext.identityFn);
            me.getGroupRoot = me.setupExtractor(me.getGroupRootProperty(), emptyFn);
            me.getSummaryRoot = me.setupExtractor(me.getSummaryRootProperty(), emptyFn);
            me.getMeta = me.setupExtractor(me.getMetaProperty(), emptyFn);
        }
    },
    /**
     * @private
     * We're just preparing the data for the superclass by pulling out the record objects we want.
     * If a {@link #record} was specified we have to pull those out of the larger JSON object,
     * which is most of what this function is doing
     * @param {Object} root The JSON root node
     * @param {Object} [readOptions] See {@link #read} for details.
     * @return {Ext.data.Model[]} The records
     */
    extractData: function(root, readOptions) {
        var recordName = this.getRecord(),
            data = [],
            length, i;
        if (recordName) {
            length = root.length;
            if (!length && Ext.isObject(root)) {
                length = 1;
                root = [
                    root
                ];
            }
            for (i = 0; i < length; i++) {
                data[i] = root[i][recordName];
            }
        } else {
            data = root;
        }
        return this.callParent([
            data,
            readOptions
        ]);
    },
    getModelData: function(raw) {
        return this.getPreserveRawData() ? Ext.apply({}, raw) : raw;
    },
    /**
     * @private
     * @method
     * Returns an accessor function for the given property string. Gives support for properties
     * such as the following:
     *
     * - 'someProperty'
     * - 'some.property'
     * - '["someProperty"]'
     * - 'values[0]'
     * 
     * This is used by {@link #buildExtractors} to create optimized extractor functions for
     * properties that are looked up directly on the source object (e.g. {@link #successProperty},
     * {@link #messageProperty}, etc.).
     */
    createAccessor: (function() {
        var re = /[\[\.]/;
        // eslint-disable-line no-useless-escape
        return function(expr) {
            var simple = this.getUseSimpleAccessors(),
                operatorIndex, result, current, parts, part, inExpr, isDot, isLeft, isRight, special, c, i, bracketed, len;
            if (!(expr || expr === 0)) {
                return;
            }
            if (typeof expr === 'function') {
                return expr;
            }
            if (!simple) {
                operatorIndex = String(expr).search(re);
            }
            if (simple === true || operatorIndex < 0) {
                result = function(raw) {
                    return raw == null ? null : raw[expr];
                };
            } else {
                // The purpose of this part is to generate a "safe" accessor for any complex 
                // json expression. For example 'foo.bar.baz' will get transformed:
                // raw.foo && raw.foo.bar && raw.foo.bar.baz
                current = 'raw';
                parts = [];
                part = '';
                inExpr = 0;
                len = expr.length;
                // The <= is intentional here. We handle the last character
                // being undefined so that we can append any final values at
                // the end
                for (i = 0; i <= len; ++i) {
                    c = expr[i];
                    isDot = c === '.';
                    isLeft = c === '[';
                    isRight = c === ']';
                    special = isDot || isLeft || isRight || !c;
                    // If either:
                    // a) Not a special char
                    // b) We're nested more than 1 deep, no single char can bring us out
                    // c) We are in an expr & it's not an ending brace
                    // Then just push the character on
                    if (!special || inExpr > 1 || (inExpr && !isRight)) {
                        part += c;
                    } else if (special) {
                        bracketed = false;
                        if (isLeft) {
                            ++inExpr;
                        } else if (isRight) {
                            --inExpr;
                            bracketed = true;
                        }
                        if (part) {
                            if (bracketed) {
                                part = '[' + part + ']';
                            } else {
                                part = '.' + part;
                            }
                            current += part;
                            // Concatting the empty string to the start fixes a very odd
                            // intermittent bug with IE9/10.
                            // On some occasions, without it, it will end up generating
                            // raw.foo.bar.baz && raw.foo.bar.baz && raw.foo.bar.baz
                            // At this point, not really sure why forcibly casting it to a string
                            // makes a difference
                            parts.push('' + current);
                            part = '';
                        }
                    }
                }
                result = parts.join(' && ');
                result = Ext.functionFactory('raw', 'return ' + result);
            }
            return result;
        };
    }()),
    /**
     * @private
     * @method
     * Returns an accessor function for the passed Field. Gives support for properties such as
     * the following:
     * 
     * - 'someProperty'
     * - 'some.property'
     * - '["someProperty"]'
     * - 'values[0]'
     * 
     * This is used by {@link #buildExtractors} to create optimized extractor expressions when
     * converting raw data into model instances. This method is used at the field level to
     * dynamically map values to model fields.
     */
    createFieldAccessor: function(field) {
        // Need to capture me for the extractor
        var me = this,
            mapping = field.mapping,
            hasMap = mapping || mapping === 0,
            map = hasMap ? mapping : field.name;
        if (hasMap) {
            if (typeof map === 'function') {
                return function(raw, self) {
                    return field.mapping(raw, self);
                };
            } else {
                return me.createAccessor(map);
            }
        }
    },
    getAccessorKey: function(prop) {
        var simple = this.getUseSimpleAccessors() ? 'simple' : '';
        return this.callParent([
            simple + prop
        ]);
    },
    privates: {
        copyFrom: function(reader) {
            this.callParent([
                reader
            ]);
            this.getRoot = reader.getRoot;
        },
        setupExtractor: function(prop, defaultFn) {
            return prop ? this.getAccessor(prop) : defaultFn;
        }
    }
});

/**
 * This class is used to write {@link Ext.data.Model} data to the server in a JSON format.
 * The {@link #allowSingle} configuration can be set to false to force the records to always
 * be encoded in an array, even if there is only a single record being sent.
 */
Ext.define('Ext.data.writer.Json', {
    extend: 'Ext.data.writer.Writer',
    alternateClassName: 'Ext.data.JsonWriter',
    alias: 'writer.json',
    config: {
        /**
        * @cfg {String} rootProperty The HTTP parameter name by which JSON encoded records
        * will be passed to the server if the {@link #encode} option is `true`.
        */
        rootProperty: undefined,
        /**
        * @cfg {Boolean} [encode=false] Configure `true` to send record data (all record fields
        * if {@link #writeAllFields} is `true`) as a JSON encoded HTTP parameter named by the
        * {@link #rootProperty} configuration.
        * 
        * The encode option should only be set to true when a {@link #rootProperty} is defined,
        * because the values will be sent as part of the request parameters as opposed to
        * a raw post. The root will be the name of the parameter sent to the server.
        */
        encode: false,
        /**
        * @cfg {Boolean} [allowSingle=true] Configure with `false` to ensure that records are
        * always wrapped in an array, even if there is only one record being sent. When there
        * is more than one record, they will always be encoded into an array.
        */
        allowSingle: true,
        /**
        * @cfg {Boolean} [expandData=false] By default, when dot-delimited field
        * {@link #nameProperty mappings} are used (e.g. `name: 'myProperty', mapping:
        * 'my.nested.property'`) the writer will simply output a flat data object containing
        * the mapping string literal as the property name (e.g. `{ 'my.nested.property': 'foo' }`).
        * 
        * Mappings are used to map incoming nested JSON to flat Ext models. In many case, the data
        * output by the writer should preferrably match the original nested data format. Setting
        * this config to `true` will ensure that the output will instead look like
        * `{ my: { nested: { property: 'foo' }}}`. The output is generated by
        * {@link #getExpandedData}, which can optionally be overridden to apply
        * more customized logic.
        */
        expandData: false
    },
    constructor: function(config) {
        if (config && config.hasOwnProperty('root')) {
            config = Ext.apply({}, config);
            config.rootProperty = config.root;
            delete config.root;
            Ext.log.warn('Ext.data.writer.Json: Using the deprecated "root" configuration. ' + 'Use "rootProperty" instead.');
        }
        this.callParent([
            config
        ]);
    },
    /**
     * @protected
     * The Reader classes support dot-delimited data mappings for extracting nested raw data
     * into fields, so the writer must support converting the flat {@link Ext.data.Model} structure
     * back into the original nested data format. Using the same mappings when available, the Writer
     * will simply split each delimiter into a nested object in the output, which should exactly
     * match the input format. For example, record data like this:
     * 
     *     my.nested.property: 'foo',
     *     my.nested.another: 'bar',
     *     my.somethingElse: 123
     * 
     * should write out as...
     * 
     *     my: {
     *         nested: {
     *             property: 'foo',
     *             another: 'bar
     *         },
     *         somethingElse: 123
     *     }
     *
     * This behavior is governed by the {@link #expandData} config. By default, this option is
     * `false` for compatibility reasons, and will output a flat structure matching the flat record
     * format. Setting this config to `true` will enable the expanded mapping behavior as shown
     * here. This method could also be overridden to provide an even more customized output data
     * structure.
     */
    getExpandedData: function(data) {
        var dataLength = data.length,
            i = 0,
            item, prop, nameParts, j, tempObj, toObject;
        toObject = function(name, value) {
            var o = {};
            o[name] = value;
            return o;
        };
        for (; i < dataLength; i++) {
            item = data[i];
            for (prop in item) {
                if (item.hasOwnProperty(prop)) {
                    // e.g. my.nested.property: 'foo'
                    nameParts = prop.split('.');
                    j = nameParts.length - 1;
                    if (j > 0) {
                        // Initially this will be the value 'foo'.
                        // Equivalent to rec['my.nested.property']
                        tempObj = item[prop];
                        for (; j > 0; j--) {
                            // Starting with the value above, we loop inside out, assigning the
                            // current object as the value for the parent name. Work all
                            // the way up until only the root name is left to assign.
                            tempObj = toObject(nameParts[j], tempObj);
                        }
                        // At this point we'll have all child properties rolled up into a single
                        // object like `{ nested: { property: 'foo' }}`. Now add the root name
                        // (e.g. 'my') to the record data if needed (do not overwrite existing):
                        item[nameParts[0]] = item[nameParts[0]] || {};
                        // Since there could be duplicate names at any level of the nesting be sure
                        // to merge rather than assign when setting the object as the value:
                        Ext.Object.merge(item[nameParts[0]], tempObj);
                        // Finally delete the original mapped property from the record
                        delete item[prop];
                    }
                }
            }
        }
        return data;
    },
    writeRecords: function(request, data) {
        var me = this,
            root = me.getRootProperty(),
            json, single, transform;
        if (me.getExpandData()) {
            data = me.getExpandedData(data);
        }
        if (me.getAllowSingle() && data.length === 1) {
            // convert to single object format
            data = data[0];
            single = true;
        }
        transform = this.getTransform();
        if (transform) {
            data = transform(data, request);
        }
        if (me.getEncode()) {
            if (root) {
                // sending as a param, need to encode
                request.setParam(root, Ext.encode(data));
            } else {
                Ext.raise('Must specify a root when using encode');
            }
        } else if (single || (data && data.length)) {
            // send as jsonData
            json = request.getJsonData() || {};
            if (root) {
                json[root] = data;
            } else {
                json = data;
            }
            request.setJsonData(json);
        }
        return request;
    }
});

/**
 * This class provides a flexible means to control the
 * `{@link Ext.util.Collection#cfg!sorters sorters}` of a
 * `{@link Ext.util.Collection Collection}`. Instances of this class are created
 * automatically when sorters are added to added to Collections.
 *
 * This collection can be directly manipulated by application code to gain full
 * control over the sorters of the owner collection. Be aware that some components
 * create sorters internally (such as grids) so be careful in such cases to not disturb
 * these additional sorters.
 *
 * Items in this collection are `Ext.util.Sorter` instances and can be managed
 * individually by their `id`. This is the recommended way to manage application
 * filters while preserving sorter applied from other sources.
 *
 * Bulk changes to this collection should be wrapped in
 * `{@link Ext.util.Collection#method!beginUpdate beginUpdate}` and
 * `{@link Ext.util.Collection#method!endUpdate endUpdate}` (as with any collection).
 * During these bulk updates all reactions to sorter changes will be suspended.
 */
Ext.define('Ext.util.SorterCollection', {
    extend: 'Ext.util.Collection',
    requires: [
        'Ext.util.Sorter'
    ],
    isSorterCollection: true,
    /**
     * @property {Ext.util.Sortable} sortable
     * The owning sortable instance. The sortable's configuration governs this
     * collection.
     * @private
     * @readonly
     */
    $sortable: null,
    /**
     * @property sortFn
     * This is the cached sorting function which is a generated function that calls all the
     * configured sorters in the correct order.
     * @readonly
     */
    sortFn: null,
    config: {
        /**
         * @cfg {Function} applySorterOptionsFn
         * A template method that can be used to apply options to a sorter during creation
         * @private
         */
        sorterOptionsFn: null,
        /**
         * @cfg {Object} applySorterOptionsScope
         * The scope to execute the {@link #applySorterOptionsFn}
         * @private
         */
        sorterOptionsScope: null
    },
    constructor: function(config) {
        var me = this;
        me.sortFn = Ext.util.Sorter.createComparator(me);
        me.callParent([
            config
        ]);
        me.setDecoder(me.decodeSorter);
    },
    addSort: function(property, direction, mode) {
        var me = this,
            count, index, limit, options, primary, sorter, sorters;
        if (!property) {
            // nothing specified so just trigger a sort...
            me.beginUpdate();
            me.endUpdate();
        } else {
            options = me.getOptions();
            if (property instanceof Array) {
                sorters = property;
                mode = direction;
                direction = null;
            } else if (Ext.isString(property)) {
                if (!(sorter = me.get(property))) {
                    sorters = [
                        {
                            property: property,
                            direction: direction || options.getDefaultSortDirection()
                        }
                    ];
                } else {
                    sorters = [
                        sorter
                    ];
                }
            } else if (Ext.isFunction(property)) {
                sorters = [
                    {
                        sorterFn: property,
                        direction: direction || options.getDefaultSortDirection()
                    }
                ];
            } else {
                if (!Ext.isObject(property)) {
                    Ext.raise('Invalid sort descriptor: ' + property);
                }
                sorters = [
                    property
                ];
                mode = direction;
                direction = null;
            }
            if (mode && !me._sortModes[mode]) {
                Ext.raise('Sort mode should be "multi", "append", "prepend" or "replace", not "' + mode + '"');
            }
            mode = me._sortModes[mode || 'replace'];
            primary = me.getAt(0);
            count = me.length;
            index = mode.append ? count : 0;
            // We have multiple changes to make, so mark the sorters collection as updating
            // before we start.
            me.beginUpdate();
            // Leverage the decode logic wired to the collection to up-convert sorters to
            // real instances.
            me.splice(index, mode.replace ? count : 0, sorters);
            if (mode.multi) {
                count = me.length;
                limit = options.getMultiSortLimit();
                if (count > limit) {
                    me.removeAt(limit, count);
                }
            }
            // count will be truncated
            if (sorter && direction) {
                sorter.setDirection(direction);
            } else if (index === 0 && primary && primary === me.getAt(0)) {
                // If we just adjusted the sorters at the front and the primary sorter is
                // still the primary sorter, toggle its direction:
                primary.toggle();
            }
            me.endUpdate();
        }
    },
    clear: function() {
        // The owning Collection needs to have its onSortersEndUpdate called on sorter clear so that
        // it clears its sorted flag.
        this.beginUpdate();
        this.callParent();
        this.endUpdate(this.items);
    },
    /**
     * Returns an up to date sort function.
     * @return {Function} The sort function.
     */
    getSortFn: function() {
        return this.sortFn;
    },
    /**
     * Get the first matching sorter with a matching property.
     * @param {String} prop The property name
     * @return {Ext.util.Sorter} The sorter. `null` if not found.
     * @private
     */
    getByProperty: function(prop) {
        var items = this.items,
            len = items.length,
            i, item;
        for (i = 0; i < len; ++i) {
            item = items[i];
            if (item.getProperty() === prop) {
                return item;
            }
        }
        return null;
    },
    //-------------------------------------------------------------------------
    // Private
    _sortModes: {
        append: {
            append: 1
        },
        multi: {
            multi: 1
        },
        prepend: {
            prepend: 1
        },
        replace: {
            replace: 1
        }
    },
    decodeSorter: function(sorter, xclass) {
        var me = this,
            options = me.getOptions(),
            root = options.getRootProperty(),
            sorterOptionsFn = me.getSorterOptionsFn(),
            currentSorter, sorterConfig, type;
        if (sorter.isSorter) {
            if (!sorter.getRoot()) {
                sorter.setRoot(root);
            }
        } else {
            sorterConfig = {
                direction: options.getDefaultSortDirection(),
                root: root
            };
            type = typeof sorter;
            // If we are dealing with a string we assume it is a property they want to sort on.
            if (type === 'string') {
                currentSorter = me.get(sorter);
                if (currentSorter) {
                    return currentSorter;
                }
                sorterConfig.property = sorter;
            }
            // If it is a function, we assume its a sorting function.
            else if (type === 'function') {
                sorterConfig.sorterFn = sorter;
            } else // If we are dealing with an object, we assume its a Sorter configuration. In
            // this case we create an instance of Sorter passing this configuration.
            {
                // Finally we get to the point where it has to be invalid
                if (!Ext.isObject(sorter)) {
                    Ext.raise('Invalid sorter specified: ' + sorter);
                }
                sorterConfig = Ext.apply(sorterConfig, sorter);
                if (sorterConfig.fn) {
                    sorterConfig.sorterFn = sorterConfig.fn;
                    delete sorterConfig.fn;
                }
            }
            // If a sorter config was created, make it an instance
            sorter = Ext.create(xclass || Ext.util.Sorter, sorterConfig);
        }
        if (sorterOptionsFn) {
            sorterOptionsFn.call(me.getSorterOptionsScope() || me, sorter);
        }
        return sorter;
    },
    setSorterConfigure: function(fn, scope) {
        this.setSorterOptionsFn(fn);
        this.setSorterOptionsScope(scope);
    },
    decodeRemoveItems: function(args, index) {
        var me = this,
            ret = (index === undefined) ? args : args[index];
        if (!ret || !ret.$cloned) {
            if (args.length > index + 1 || !Ext.isIterable(ret)) {
                ret = Ext.Array.slice(args, index);
            }
            // eslint-disable-next-line vars-on-top
            var currentSorters = me.items,
                ln = ret.length,
                remove = [],
                i, item, n, sorter, type;
            for (i = 0; i < ln; i++) {
                sorter = ret[i];
                if (sorter && sorter.isSorter) {
                    remove.push(sorter);
                } else {
                    type = typeof sorter;
                    if (type === 'string') {
                        sorter = me.get(sorter);
                        if (sorter) {
                            remove.push(sorter);
                        }
                    } else if (type === 'function') {
                        for (n = currentSorters.length; n-- > 0; ) {
                            item = currentSorters[n];
                            if (item.getSorterFn() === sorter) {
                                remove.push(item);
                            }
                        }
                    } else {
                        Ext.raise('Invalid sorter specification: ' + sorter);
                    }
                }
            }
            ret = remove;
            ret.$cloned = true;
        }
        return ret;
    },
    getOptions: function() {
        // Odd thing this. We need a Sortable to know how to manage our collection, but
        // we may not have one. Of course as a Collection, we *are* one as well... just
        // that is not really useful to sort the sorters themselves, but we do have the
        // default options for Sortables baked in, so we'll do.
        return this.$sortable || this;
    }
});

/**
 * This class provides a flexible means to control the
 * `{@link Ext.util.Collection#cfg!filters filters}` of a
 * `{@link Ext.util.Collection Collection}`. Instances of this class are created
 * automatically when filters are added to added to Collections.
 *
 * This collection can be directly manipulated by application code to gain full
 * control over the filters of the owner collection. Be aware that some components
 * create filters internally (such as `Ext.form.field.ComboBox` and the
 * `Ext.grid.filters.Filters` plugin) so be careful in such cases to not disturb
 * these additional filters.
 *
 * Items in this collection are `Ext.util.Filter` instances and can be managed
 * individually by their `id`. This is the recommended way to manage application
 * filters while preserving filters applied from other sources.
 *
 * Bulk changes to this collection should be wrapped in
 * `{@link Ext.util.Collection#method!beginUpdate beginUpdate}` and
 * `{@link Ext.util.Collection#method!endUpdate endUpdate}` (as with any collection).
 * During these bulk updates all reactions to filter changes will be suspended.
 */
Ext.define('Ext.util.FilterCollection', {
    extend: 'Ext.util.Collection',
    requires: [
        'Ext.util.Filter'
    ],
    isFilterCollection: true,
    /**
     * @property {Ext.util.Collection} $filterable
     * The owning filterable instance. The filterable's configuration governs this
     * collection.
     * @private
     * @readonly
     */
    $filterable: null,
    /**
     * @property filterFn
     * This is the cached filter function.
     * @readonly
     */
    filterFn: null,
    constructor: function(config) {
        var me = this;
        // Because this closure operates on the collection, we are able to use it for as
        // long as we have the Collection instance.
        me.filterFn = Ext.util.Filter.createFilterFn(me);
        me.callParent([
            config
        ]);
        me.setDecoder(me.decodeFilter);
    },
    /**
     * This method will filter an array based on the currently configured `filters`.
     * @param {Array} data The array you want to have filtered.
     * @return {Array} The array you passed after it is filtered.
     */
    filterData: function(data) {
        return this.filtered ? Ext.Array.filter(data, this.filterFn) : data;
    },
    /**
     * Returns the filter function.
     * @return {Function} The filter function.
     */
    getFilterFn: function() {
        return this.filterFn;
    },
    isItemFiltered: function(item) {
        return !this.filterFn(item);
    },
    /**
     * returns the number of *enabled* filters in this `FilterCollection`
     * @returns {Number}
     */
    getFilterCount: function() {
        var filters = this.items,
            len = filters.length,
            i;
        for (i = len - 1; i >= 0; i--) {
            if (filters[i].getDisabled()) {
                len--;
            }
        }
        return len;
    },
    //-------------------------------------------------------------------------
    // Private
    decodeFilter: function(filter) {
        var options = this.getOptions(),
            filterRoot = options.getRootProperty(),
            filterConfig;
        if (filter.isFilter) {
            if (filter.setRoot && !filter.getRoot()) {
                filter.setRoot(filterRoot);
            }
        } else {
            filterConfig = {
                root: filterRoot
            };
            if (Ext.isFunction(filter)) {
                filterConfig.filterFn = filter;
            } else // If we are dealing with an object, we assume its a Filter configuration. In
            // this case we create an instance of Ext.util.Filter passing the config.
            {
                // Finally we get to the point where it has to be invalid
                if (!Ext.isObject(filter)) {
                    Ext.raise('Invalid filter specified: ' + filter);
                }
                filterConfig = Ext.apply(filterConfig, filter);
                if (filterConfig.fn) {
                    filterConfig.filterFn = filterConfig.fn;
                    delete filterConfig.fn;
                }
                if (Ext.util.Filter.isInvalid(filterConfig)) {
                    return false;
                }
            }
            filter = new Ext.util.Filter(filterConfig);
        }
        return filter;
    },
    decodeRemoveItems: function(args, index) {
        var me = this,
            ret = (index === undefined) ? args : args[index];
        if (!ret.$cloned) {
            if (args.length > index + 1 || !Ext.isIterable(ret)) {
                ret = Ext.Array.slice(args, index);
            }
            // eslint-disable-next-line vars-on-top
            var currentFilters = me.items,
                ln = ret.length,
                remove = [],
                filter, i, isFunction, isProp, isString, item, match, n, type;
            for (i = 0; i < ln; i++) {
                filter = ret[i];
                if (filter && filter.isFilter) {
                    remove.push(filter);
                } else {
                    type = typeof filter;
                    isFunction = type === 'function';
                    isProp = filter.property !== undefined && filter.value !== undefined;
                    isString = type === 'string';
                    if (!isFunction && !isProp && !isString) {
                        Ext.raise('Invalid filter specification: ' + filter);
                    }
                    for (n = currentFilters.length; n-- > 0; ) {
                        item = currentFilters[n];
                        match = false;
                        if (isString) {
                            match = item.getProperty() === filter;
                        } else if (isFunction) {
                            match = item.getFilterFn() === filter;
                        } else if (isProp) {
                            match = item.getProperty() === filter.property && item.getValue() === filter.value;
                        }
                        if (match) {
                            remove.push(item);
                        }
                    }
                }
            }
            ret = remove;
            ret.$cloned = true;
        }
        return ret;
    },
    getOptions: function() {
        // Odd thing this. We need a Filterable to know how to manage our collection, but
        // we may not have one. Of course as a Collection, we *are* one as well... just
        // that is not really useful to filter the filters themselves, but we do have the
        // default options for Filterable baked in, so we'll do.
        return this.$filterable || this;
    }
});

/**
 * @private
 * A collection containing the result of applying grouping to the records in the store.
 */
Ext.define('Ext.util.GroupCollection', {
    extend: 'Ext.util.Collection',
    requires: [
        'Ext.util.Group',
        // Since Collection uses sub-collections of various derived types we step up to
        // list all the requirements of Collection. The idea being that instead of a
        // "requires" of Ext.util.Collection (which cannot pull everything) you instead
        // do a "requires" of Ext.util.GroupCollection and it will.
        'Ext.util.SorterCollection',
        'Ext.util.FilterCollection'
    ],
    isGroupCollection: true,
    config: {
        grouper: null,
        groupConfig: null,
        itemRoot: null
    },
    observerPriority: -100,
    emptyGroupRetainTime: 300000,
    // Private timer to hang on to emptied groups. Milliseconds.
    constructor: function(config) {
        this.emptyGroups = {};
        this.callParent([
            config
        ]);
        this.on('remove', 'onGroupRemove', this);
    },
    /**
     * Returns the `Ext.util.Group` associated with the given record.
     *
     * @param {Object} item The item for which the group is desired.
     * @return {Ext.util.Group}
     * @since 6.5.0
     */
    getItemGroup: function(item) {
        var key = this.getGrouper().getGroupString(item);
        return this.get(key);
    },
    //-------------------------------------------------------------------------
    // Calls from the source Collection:
    onCollectionAdd: function(source, details) {
        if (!this.isConfiguring) {
            this.addItemsToGroups(source, details.items, details.at);
        }
    },
    onCollectionBeforeItemChange: function(source, details) {
        this.changeDetails = details;
    },
    onCollectionBeginUpdate: function() {
        this.beginUpdate();
    },
    onCollectionEndUpdate: function() {
        this.endUpdate();
    },
    onCollectionItemChange: function(source, details) {
        // Check if the change to the item caused the item to move. If it did, the group
        // ordering will be handled by virtue of being removed/added to the collection.
        // If not, check whether we're in the correct group and fix up if not.
        if (!details.indexChanged) {
            this.syncItemGrouping(source, details);
        }
        this.changeDetails = null;
    },
    onCollectionRefresh: function(source) {
        if (source.generation) {
            // eslint-disable-next-line vars-on-top
            var me = this,
                itemGroupKeys = me.itemGroupKeys = {},
                groupData = me.createEntries(source, source.items),
                entries = groupData.entries,
                groupKey, i, len, entry, j;
            // The magic of Collection will automatically update the group with its new
            // members.
            for (i = 0 , len = entries.length; i < len; ++i) {
                entry = entries[i];
                // Will add or replace
                entry.group.splice(0, 1.0E99, entry.items);
                // Add item key -> group mapping for every entry
                for (j = 0; j < entry.items.length; j++) {
                    itemGroupKeys[source.getKey(entry.items[j])] = entry.group;
                }
            }
            // Remove groups to which we have not added items.
            entries = null;
            for (groupKey in me.map) {
                if (!(groupKey in groupData.groups)) {
                    (entries || (entries = [])).push(me.map[groupKey]);
                }
            }
            if (entries) {
                me.remove(entries);
            }
            // autoSort is disabled when adding new groups because
            // it relies on there being at least one record in the group
            me.sortItems();
        }
    },
    onCollectionRemove: function(source, details) {
        var me = this,
            changeDetails = me.changeDetails,
            itemGroupKeys = me.itemGroupKeys || (me.itemGroupKeys = {}),
            entries, entry, group, i, n, j, removeGroups, item;
        if (source.getCount()) {
            if (changeDetails) {
                // The item has changed, so the group key may be different, need
                // to look it up
                item = changeDetails.item || changeDetails.items[0];
                entries = me.createEntries(source, [
                    item
                ], false).entries;
                entries[0].group = itemGroupKeys['oldKey' in details ? details.oldKey : source.getKey(item)];
            } else {
                entries = me.createEntries(source, details.items, false).entries;
            }
            for (i = 0 , n = entries.length; i < n; ++i) {
                group = (entry = entries[i]).group;
                if (group) {
                    group.remove(entry.items);
                }
                // Delete any item key -> group mapping
                for (j = 0; j < entry.items.length; j++) {
                    delete itemGroupKeys[source.getKey(entry.items[j])];
                }
                if (group && !group.length) {
                    (removeGroups || (removeGroups = [])).push(group);
                }
            }
        } else // Straight cleardown
        {
            me.itemGroupKeys = {};
            removeGroups = me.items;
            for (i = 0 , n = removeGroups.length; i < n; ++i) {
                removeGroups[i].clear();
            }
        }
        if (removeGroups) {
            me.remove(removeGroups);
        }
    },
    // If the SorterCollection instance is not changing, the Group will react to
    // changes inside the SorterCollection, but if the instance changes we need
    // to sync the Group to the new SorterCollection.
    onCollectionSort: function(source) {
        // sorting the collection effectively sorts the items in each group...
        var me = this,
            sorters = source.getSorters(false),
            items, length, i, group;
        if (sorters) {
            items = me.items;
            length = me.length;
            for (i = 0; i < length; ++i) {
                group = items[i];
                if (group.getSorters() === sorters) {
                    group.sortItems();
                } else {
                    group.setSorters(sorters);
                }
            }
        }
    },
    onCollectionUpdateKey: function(source, details) {
        if (!details.indexChanged) {
            details.oldIndex = source.indexOf(details.item);
            this.syncItemGrouping(source, details);
        }
    },
    //-------------------------------------------------------------------------
    // Private
    addItemsToGroups: function(source, items, at, oldIndex) {
        var me = this,
            itemGroupKeys = me.itemGroupKeys || (me.itemGroupKeys = {}),
            entries = me.createEntries(source, items).entries,
            index = -1,
            sourceStartIndex, entry, i, len, j, group, firstIndex, item;
        for (i = 0 , len = entries.length; i < len; ++i) {
            entry = entries[i];
            group = entry.group;
            // A single item moved - from onCollectionItemChange
            if (oldIndex || oldIndex === 0) {
                item = items[0];
                if (group.getCount() > 0 && source.getSorters().getCount() === 0) {
                    // We have items in the group & it's not sorted, so find the
                    // correct position in the group to insert.
                    firstIndex = source.indexOf(group.items[0]);
                    if (oldIndex < firstIndex) {
                        index = 0;
                    } else {
                        index = oldIndex - firstIndex;
                    }
                }
                if (index === -1) {
                    group.add(item);
                } else {
                    group.insert(index, item);
                }
            } else {
                if (me.length > 1 && at) {
                    sourceStartIndex = source.indexOf(entries[0].group.getAt(0));
                    at = Math.max(at - sourceStartIndex, 0);
                }
                entry.group.insert(at != null ? at : group.items.length, entry.items);
                // Add item key -> group mapping
                for (j = 0; j < entry.items.length; j++) {
                    itemGroupKeys[source.getKey(entry.items[j])] = entry.group;
                }
            }
        }
        // autoSort is disabled when adding new groups because
        // it relies on there being at least one record in the group
        me.sortItems();
    },
    createEntries: function(source, items, createGroups) {
        // Separate the items out into arrays by group
        var me = this,
            groups = {},
            entries = [],
            grouper = me.getGrouper(),
            entry, group, groupKey, i, item, len;
        for (i = 0 , len = items.length; i < len; ++i) {
            groupKey = grouper.getGroupString(item = items[i]);
            if (!(entry = groups[groupKey])) {
                group = me.getGroup(source, groupKey, createGroups);
                entries.push(groups[groupKey] = entry = {
                    group: group,
                    items: []
                });
            }
            // Collect items to add/remove for each group
            // which has items in the array
            entry.items.push(item);
        }
        return {
            groups: groups,
            entries: entries
        };
    },
    syncItemGrouping: function(source, details) {
        var me = this,
            itemGroupKeys = me.itemGroupKeys || (me.itemGroupKeys = {}),
            item = details.item,
            oldKey, itemKey, oldGroup, group;
        itemKey = source.getKey(item);
        oldKey = 'oldKey' in details ? details.oldKey : itemKey;
        // The group the item was in before the change took place.
        oldGroup = itemGroupKeys[oldKey];
        // Look up/create the group into which the item now must be added.
        group = me.getGroup(source, me.getGrouper().getGroupString(item));
        details.group = group;
        details.oldGroup = oldGroup;
        // The change did not cause a change in group
        if (!(details.groupChanged = group !== oldGroup)) {
            // Inform group about change
            oldGroup.itemChanged(item, details.modified, details.oldKey, details);
        } else {
            // Remove from its old group if there was one.
            if (oldGroup) {
                // Ensure Geoup knows about any unknown key changes, or item will not be removed.
                oldGroup.updateKey(item, oldKey, itemKey);
                oldGroup.remove(item);
                // Queue newly empy group for destruction.
                if (!oldGroup.length) {
                    me.remove(oldGroup);
                }
            }
            // Add to new group
            me.addItemsToGroups(source, [
                item
            ], null, details.oldIndex);
        }
        // Keep item key -> group mapping up to date
        delete itemGroupKeys[oldKey];
        itemGroupKeys[itemKey] = group;
    },
    getGroup: function(source, key, createGroups) {
        var me = this,
            group = me.get(key),
            autoSort = me.getAutoSort();
        if (group) {
            group.setSorters(source.getSorters());
        } else if (createGroups !== false) {
            group = me.emptyGroups[key] || Ext.create(Ext.apply({
                xclass: 'Ext.util.Group',
                id: me.getId() + '-group-' + key,
                groupKey: key,
                rootProperty: me.getItemRoot(),
                sorters: source.getSorters()
            }, me.getGroupConfig()));
            group.ejectTime = null;
            me.setAutoSort(false);
            me.add(group);
            me.setAutoSort(autoSort);
        }
        return group;
    },
    getKey: function(item) {
        return item.getGroupKey();
    },
    createSortFn: function() {
        var me = this,
            grouper = me.getGrouper(),
            sorterFn = me.getSorters().getSortFn();
        if (!grouper) {
            return sorterFn;
        }
        return function(lhs, rhs) {
            // The grouper has come from the collection, so we pass the items in
            // the group for comparison because the grouper is also used to
            // sort the data in the collection
            return grouper.sort(lhs.items[0], rhs.items[0]) || sorterFn(lhs, rhs);
        };
    },
    updateGrouper: function(grouper) {
        var me = this;
        me.grouped = !!(grouper && me.$groupable.getAutoGroup());
        me.onSorterChange();
        me.onEndUpdateSorters(me.getSorters());
    },
    destroy: function() {
        var me = this;
        me.$groupable = null;
        // Ensure group objects get destroyed, they may have
        // added listeners to the main collection sorters.
        me.destroyGroups(me.items);
        Ext.undefer(me.checkRemoveQueueTimer);
        me.callParent();
    },
    privates: {
        destroyGroups: function(groups) {
            var len = groups.length,
                i;
            for (i = 0; i < len; ++i) {
                groups[i].destroy();
            }
        },
        onGroupRemove: function(collection, info) {
            var me = this,
                groups = info.items,
                emptyGroups = me.emptyGroups,
                len, group, i;
            groups = Ext.Array.from(groups);
            for (i = 0 , len = groups.length; i < len; i++) {
                group = groups[i];
                group.setSorters(null);
                emptyGroups[group.getGroupKey()] = group;
                group.ejectTime = Ext.now();
            }
            // Removed empty groups are reclaimable by getGroup
            // for emptyGroupRetainTime milliseconds
            me.checkRemoveQueue();
        },
        checkRemoveQueue: function() {
            var me = this,
                emptyGroups = me.emptyGroups,
                groupKey, group, reschedule;
            for (groupKey in emptyGroups) {
                group = emptyGroups[groupKey];
                // If the group's retain time has expired, destroy it.
                if (!group.getCount() && Ext.now() - group.ejectTime > me.emptyGroupRetainTime) {
                    Ext.destroy(group);
                    delete emptyGroups[groupKey];
                } else {
                    reschedule = true;
                }
            }
            // Still some to remove in the future. Check back in emptyGroupRetainTime
            if (reschedule) {
                Ext.undefer(me.checkRemoveQueueTimer);
                me.checkRemoveQueueTimer = Ext.defer(me.checkRemoveQueue, me.emptyGroupRetainTime, me);
            }
        }
    }
});

/**
 * The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects.
 * Stores load data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions
 * for {@link #method-sort sorting}, {@link #filter filtering} and querying the
 * {@link Ext.data.Model model} instances contained within it.
 *
 * Creating a Store is easy - we just tell it the Model and the Proxy to use for loading and saving
 * its data:
 *
 *      // Set up a model to use in our Store
 *      Ext.define('User', {
 *          extend: 'Ext.data.Model',
 *          fields: [
 *              {name: 'firstName', type: 'string'},
 *              {name: 'lastName',  type: 'string'},
 *              {name: 'age',       type: 'int'},
 *              {name: 'eyeColor',  type: 'string'}
 *          ]
 *      });
 *
 *      var myStore = Ext.create('Ext.data.Store', {
 *          model: 'User',
 *          proxy: {
 *              type: 'ajax',
 *              url: '/users.json',
 *              reader: {
 *                  type: 'json',
 *                  rootProperty: 'users'
 *              }
 *          },
 *          autoLoad: true
 *      });
 *
 * In the example above we configured an AJAX proxy to load data from the url '/users.json'.
 * We told our Proxy to use a {@link Ext.data.reader.Json JsonReader} to parse the response
 * from the server into Model object - {@link Ext.data.reader.Json see the docs on JsonReader}
 * for details.
 *
 * ## Inline data
 *
 * Stores can also load data inline. Internally, Store converts each of the objects we pass in
 * as {@link #cfg-data} into Model instances:
 *
 *      Ext.create('Ext.data.Store', {
 *          model: 'User',
 *          data : [
 *              {firstName: 'Peter',   lastName: 'Venkman'},
 *              {firstName: 'Egon',    lastName: 'Spengler'},
 *              {firstName: 'Ray',     lastName: 'Stantz'},
 *              {firstName: 'Winston', lastName: 'Zeddemore'}
 *          ]
 *      });
 *
 * Loading inline data using the method above is great if the data is in the correct format already
 * (e.g. it doesn't need to be processed by a {@link Ext.data.reader.Reader reader}). If your inline
 * data requires processing to decode the data structure, use a
 * {@link Ext.data.proxy.Memory MemoryProxy} instead (see the
 * {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).
 *
 * Additional data can also be loaded locally using {@link #method-add}.
 * 
 * ## Dynamic Loading
 *
 * Stores can be dynamically updated by calling the {@link #method-load} method:
 *
 *     store.load({
 *         params: {
 *             group: 3,
 *             type: 'user'
 *         },
 *         callback: function(records, operation, success) {
 *             // do something after the load finishes
 *         },
 *         scope: this
 *     });
 *
 * Here a bunch of arbitrary parameters is passed along with the load request and a callback
 * function is set up to do something after the loading is over.
 *
 * ## Loading Nested Data
 *
 * Applications often need to load sets of associated data - for example a CRM system might load
 * a User and her Orders. Instead of issuing an AJAX request for the User and a series of additional
 * AJAX requests for each Order, we can load a nested dataset and allow the Reader to automatically
 * populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader}
 * intro docs for a full explanation:
 *
 *      var store = Ext.create('Ext.data.Store', {
 *          autoLoad: true,
 *          model: "User",
 *          proxy: {
 *              type: 'ajax',
 *              url: 'users.json',
 *              reader: {
 *                  type: 'json',
 *                  rootProperty: 'users'
 *              }
 *          }
 *      });
 *
 * Which would consume a response like this:
 *
 *      {
 *          "users": [{
 *              "id": 1,
 *              "name": "Peter",
 *              "orders": [{
 *                  "id": 10,
 *                  "total": 10.76,
 *                  "status": "invoiced"
 *             },{
 *                  "id": 11,
 *                  "total": 13.45,
 *                  "status": "shipped"
 *             }]
 *          }]
 *      }
 *
 * See the {@link Ext.data.reader.Reader} intro docs for a full explanation.
 *
 * ## Filtering and Sorting
 *
 * Stores can be sorted and filtered - in both cases either remotely or locally. The
 * {@link #cfg-sorters} and {@link #cfg-filters} are held inside
 * {@link Ext.util.Collection Collection} instances to make them easy to manage. Usually it is
 * sufficient to either just specify sorters and filters in the Store configuration or call
 * {@link #method-sort} or {@link #filter}:
 *
 *      var store = Ext.create('Ext.data.Store', {
 *          model: 'User',
 *          sorters: [{
 *              property: 'age',
 *              direction: 'DESC'
 *          }, {
 *              property: 'firstName',
 *              direction: 'ASC'
 *          }],
 *
 *          filters: [{
 *              property: 'firstName',
 *              value: /Peter/
 *          }]
 *      });
 *
 * The new Store will keep the configured sorters and filters in the Collection instances mentioned
 * above. By default, sorting and filtering are both performed locally by the Store - see
 * {@link #remoteSort} and {@link #remoteFilter} to allow the server to perform these operations
 * instead.
 *
 * Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter}
 * adds another filter to the Store and automatically filters the dataset (calling {@link #filter}
 * with no arguments simply re-applies all existing filters).
 *
 *     store.filter('eyeColor', 'Brown');
 *
 * Change the sorting at any time by calling {@link #method-sort}:
 *
 *     store.sort('height', 'ASC');
 *
 * Note that all existing sorters will be removed in favor of the new sorter data (if
 * {@link #method-sort} is called with no arguments, the existing sorters are just reapplied
 * instead of being removed). To keep existing sorters and add new ones, just add them to the
 * Collection:
 *
 *     store.sorters.add(new Ext.util.Sorter({
 *         property : 'shoeSize',
 *         direction: 'ASC'
 *     }));
 *
 *     store.sort();
 *
 * ## Registering with StoreManager
 *
 * Any Store that is instantiated with a {@link #storeId} will automatically be registered with
 * the {@link Ext.data.StoreManager StoreManager}. This makes it easy to reuse the same store
 * in multiple views:
 *
 *     //this store can be used several times
 *     Ext.create('Ext.data.Store', {
 *         model: 'User',
 *         storeId: 'usersStore'
 *     });
 *
 *     new Ext.List({
 *         store: 'usersStore',
 *         //other config goes here
 *     });
 *
 *     new Ext.view.View({
 *         store: 'usersStore',
 *         //other config goes here
 *     });
 *
 * ## Further Reading
 *
 * Stores are backed up by an ecosystem of classes that enables their operation. To gain a full
 * understanding of these pieces and how they fit together, see:
 *
 *   - {@link Ext.data.proxy.Proxy Proxy} - overview of what Proxies are and how they are used
 *   - {@link Ext.data.Model Model} - the core class in the data package
 *   - {@link Ext.data.reader.Reader Reader} - used by any subclass of
 *     {@link Ext.data.proxy.Server ServerProxy} to read a response
 */
Ext.define('Ext.data.Store', {
    extend: 'Ext.data.ProxyStore',
    alias: 'store.store',
    mixins: [
        'Ext.data.LocalStore'
    ],
    // Required classes must be loaded before the definition callback runs
    // The class definition callback creates a dummy Store which requires that
    // all the classes below have been loaded.
    requires: [
        'Ext.data.Model',
        'Ext.data.proxy.Ajax',
        'Ext.data.reader.Json',
        'Ext.data.writer.Json',
        // This ensures that we have Ext.util.Collection and all of its requirements.
        'Ext.util.GroupCollection',
        'Ext.util.DelayedTask'
    ],
    uses: [
        'Ext.data.StoreManager',
        'Ext.util.Grouper'
    ],
    config: {
        /**
         * @cfg {Object[]/Ext.data.Model[]} data
         * Array of Model instances or data objects to load locally. See "Inline data"
         * above for details.
         */
        data: undefined,
        // undefined so the applier is always called
        /**
         * @cfg {Boolean} [clearRemovedOnLoad=true]
         * `true` to clear anything in the {@link #removed} record collection when the store loads.
         */
        clearRemovedOnLoad: true,
        /**
        * @cfg {Boolean} [clearOnPageLoad=true]
        * True to empty the store when loading another page via {@link #loadPage},
        * {@link #nextPage} or {@link #previousPage}. Setting to false keeps existing records,
        * allowing large data sets to be loaded one page at a time but rendered all together.
        */
        clearOnPageLoad: true,
        /**
         * @cfg {Ext.data.Model} [associatedEntity]
         * The owner of this store if the store is used as part of an association.
         *
         * @private
         */
        associatedEntity: null,
        /**
         * @cfg {Ext.data.schema.Role} [role]
         * The role for the {@link #associatedEntity}.
         *
         * @private
         */
        role: null,
        /**
         * @cfg {Ext.data.Session} session
         * The session for this store. By specifying a session, it ensures any records that are
         * added to this store are also included in the session. This store does not become a member
         * of the session itself.
         *
         * @since  5.0.0
         */
        session: null
    },
    /**
     * @property {Ext.util.Collection} data
     * The `data` property is a `Collection` which holds this store's local cache of records.
     * @private
     * @readonly
     */
    /**
     * @private
     * Used as a parameter to loadRecords
     */
    addRecordsOptions: {
        addRecords: true
    },
    /**
     * @property {Number} loadCount
     * The number of times records have been loaded into the store. This includes loads via
     * {@link #loadData} & {@link #loadRecords}.
     * @readonly
     */
    loadCount: 0,
    /**
     * `true` once the store has loaded data from the server.
     * @property {Boolean} complete
     *
     * @private
     */
    complete: false,
    moveMapCount: 0,
    /**
     * Creates the store.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        var me = this,
            data;
        if (config) {
            if (config.buffered) {
                if (this.self !== Ext.data.Store) {
                    Ext.raise('buffered config not supported on derived Store classes. ' + 'Please derive from Ext.data.BufferedStore.');
                }
                // Hide this from Cmd
                /* eslint-disable-next-line dot-notation */
                return new Ext.data['BufferedStore'](config);
            }
            if (config.remoteGroup) {
                Ext.log.warn('Ext.data.Store: remoteGroup has been removed. ' + 'Use remoteSort instead.');
            }
        }
        /**
         * @event beforeprefetch
         * Fires before a prefetch occurs. Return `false` to cancel.
         * @param {Ext.data.Store} this
         * @param {Ext.data.operation.Operation} operation The associated operation.
         */
        /**
         * @event groupchange
         * Fired whenever the grouping in the grid changes.
         * @param {Ext.data.Store} store The store.
         * @param {Ext.util.Grouper} grouper The grouper object.
         */
        /**
         * @event prefetch
         * Fires whenever records have been prefetched.
         * @param {Ext.data.Store} this
         * @param {Ext.data.Model[]} records An array of records.
         * @param {Boolean} successful `true` if the operation was successful.
         * @param {Ext.data.operation.Operation} operation The associated operation.
         */
        /**
         * @event filterchange
         * Fired whenever the filter set changes.
         * @param {Ext.data.Store} store The store.
         * @param {Ext.util.Filter[]} filters The array of Filter objects.
         */
        me.callParent([
            config
        ]);
        // See applyData for the details.
        data = me.inlineData;
        if (data) {
            delete me.inlineData;
            me.loadInlineData(data);
        }
    },
    /**
     * @method getData
     * Returns the store's records.
     *
     * **Note:** If your store has been filtered, getData() will return a filtered
     * collection.  Use `getData().{@link Ext.util.Collection#getSource getSource()}` to
     * fetch all unfiltered records.
     *
     * @return {Ext.util.Collection} An Ext.util.Collection of records
     * (an empty Collection if no records are held by the store).
     */
    /**
     * @method setData
     * Loads an array of data directly into the Store.
     *
     * setData() is ideal if your data's format is already in its appropriate format (e.g. it
     * doesn't need to be processed by a reader). If your data's structure requires processing,
     * use a {@link Ext.data.proxy.Memory MemoryProxy} or {@link #loadRawData}.
     *
     * Use {@link #loadData}, {@link #method-add}, or {@link #insert} if records need to be
     * appended to the current recordset.
     *
     * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances
     * will be cast into model instances first.
     */
    applyData: function(data, dataCollection) {
        // We bring up the Collection for records which forms the bottom of the config
        // dependency graph. The appliers for "filters" and "sorters" depend on "data"
        // and "remoteFilter" and "remoteSort" depend on both in their updaters.
        var me = this;
        // Ensure that we process our Model config first.
        me.getFields();
        me.getModel();
        // We might be configured with a Collection instance
        if (data && data.isCollection) {
            data.setRootProperty('data');
            dataCollection = data;
            dataCollection.addObserver(this);
            // Perform a load postprocess if the incoming collection is loaded.
            if (data.getCount()) {
                me.afterLoadRecords(data.items);
                // This is not fired by afterLoadRecords because loadRecords
                // which calls afterLoadRecords is a public API which simply adds some
                // records. This situation here though, is anaologous to a load.
                if (me.hasListeners.load) {
                    me.fireEvent('load', me, data.items, true);
                }
            }
        } else {
            if (!dataCollection) {
                dataCollection = me.constructDataCollection();
            }
            if (data) {
                if (me.isInitializing) {
                    // When data is configured on the instance of a Store we must wait for
                    // all the things to initialize (sorters, filters, groupers) so that we
                    // can properly process that data. All of those appliers, however, depend
                    // on the dataCollection (us) to get booted up first so we must defer
                    // this back to after initConfig. In previous versions this was hacked
                    // at by the constructor via "config.data" but "data" can also be set on
                    // the Ext.define level so best to pick it up here and store aside to be
                    // finished in the constructor.
                    me.inlineData = data;
                } else {
                    // If we are not constructing the Store than a setData call needs to be
                    // equivalent to the legacy loadData method with respect to events that fire,
                    // etc.
                    me.loadData(data);
                }
            }
        }
        return dataCollection;
    },
    loadInlineData: function(data) {
        var me = this,
            proxy = me.getProxy();
        if (proxy && proxy.isMemoryProxy) {
            proxy.setData(data);
            // Allow a memory proxy to trigger a load initially
            me.suspendEvents();
            me.read();
            me.resumeEvents();
        } else {
            // We make it silent because we don't want to fire a refresh event
            me.removeAll(true);
            // We don't want to fire addrecords event since we will be firing
            // a refresh event later which will already take care of updating
            // any views bound to this store
            me.suspendEvents();
            me.loadData(data);
            me.resumeEvents();
        }
    },
    /**
     * @method insert
     * @inheritdoc Ext.data.LocalStore#insert
     */
    onCollectionAdd: function(collection, info) {
        this.loadCount = this.loadCount || 1;
        this.onCollectionAddItems(collection, info.items, info);
    },
    onCollectionFilterAdd: function(collection, items) {
        this.onCollectionAddItems(collection, items);
    },
    onCollectionAddItems: function(collection, records, info) {
        var me = this,
            len = records.length,
            lastChunk = info ? !info.next : false,
            // Must use class-specific removed property.
            // Regular Stores add to the "removed" property on remove.
            // TreeStores are having records removed all the time; node collapse removes.
            // TreeStores add to the "removedNodes" property onNodeRemove
            removed = me.removed,
            ignoreAdd = me.ignoreCollectionAdd,
            session = me.getSession(),
            replaced = info && info.replaced,
            i, sync, record, replacedItems;
        // Collection add changes the items reference of the collection, and that array
        // object if directly referenced by Ranges. The ranges have to refresh themselves
        // upon add.
        if (me.activeRanges) {
            me.syncActiveRanges();
        }
        for (i = 0; i < len; ++i) {
            record = records[i];
            if (session) {
                session.adopt(record);
            }
            // If ignoring, we don't want to do anything other than pull
            // the added records into the session    
            if (!ignoreAdd) {
                record.join(me);
                if (removed && removed.length) {
                    Ext.Array.remove(removed, record);
                }
                sync = sync || record.phantom || record.dirty;
            }
        }
        if (ignoreAdd) {
            return;
        }
        if (replaced) {
            replacedItems = [];
            do {
                Ext.Array.push(replacedItems, replaced.items);
                replaced = replaced.next;
            } while (replaced);
            me.setMoving(replacedItems, true);
        }
        if (info) {
            // If this is a replacement operation, there will have been a
            // previous call to onCollectionRemove which will have fired no
            // events in anticipation of a final refresh event.
            // Here is where we inform interested parties of all the changes.
            if (info.replaced) {
                if (lastChunk) {
                    me.fireEvent('datachanged', me);
                    me.fireEvent('refresh', me);
                }
            } else {
                me.fireEvent('add', me, records, info.at);
                // If there is a next property, that means there is another range that needs
                // to be removed after this. Wait until everything is gone before firing
                // datachanged since it should be a bulk operation
                if (lastChunk) {
                    me.fireEvent('datachanged', me);
                }
            }
        }
        if (replacedItems) {
            me.setMoving(replacedItems, false);
        }
        // Addition means a sync is needed.
        me.needsSync = me.needsSync || sync;
    },
    onCollectionBeforeItemChange: function(collection, info) {
        var record = info.item,
            modifiedFieldNames = info.modified || null,
            type = info.meta;
        // This is currently intended to be private
        this.fireEvent('beforeupdate', this, record, type, modifiedFieldNames, info);
    },
    // If our source collection informs us that a filtered out item has changed, we must still
    // fire the events...
    onCollectionFilteredItemChange: function() {
        this.onCollectionItemChange.apply(this, arguments);
    },
    onCollectionItemChange: function(collection, info) {
        var me = this,
            record = info.item,
            modifiedFieldNames = info.modified || null,
            type = info.meta;
        if (me.fireChangeEvent(record)) {
            // Inform any interested parties that a record has been mutated.
            // This will be invoked on TreeStores in which the invoking record
            // is an descendant of a collapsed node, and so *will not be contained by this store
            me.onUpdate(record, type, modifiedFieldNames, info);
            me.fireEvent('update', me, record, type, modifiedFieldNames, info);
            me.fireEvent('datachanged', me);
        }
    },
    afterChange: function(record, modifiedFieldNames, type) {
        this.getData().itemChanged(record, modifiedFieldNames || null, undefined, type);
    },
    afterCommit: function(record, modifiedFieldNames) {
        this.afterChange(record, modifiedFieldNames, Ext.data.Model.COMMIT);
    },
    afterEdit: function(record, modifiedFieldNames) {
        this.needsSync = this.needsSync || record.dirty;
        this.afterChange(record, modifiedFieldNames, Ext.data.Model.EDIT);
    },
    afterReject: function(record) {
        this.afterChange(record, null, Ext.data.Model.REJECT);
    },
    afterDrop: function(record) {
        this.getData().remove(record);
    },
    afterErase: function(record) {
        this.removeFromRemoved(record);
    },
    /**
     * @method add
     * @inheritdoc Ext.data.LocalStore#add
     */
    /**
     * (Local sort only) Inserts the passed Record into the Store at the index where it
     * should go based on the current sort information.
     *
     * @param {Ext.data.Record} record
     */
    addSorted: function(record) {
        var me = this,
            remote = me.getRemoteSort(),
            data = me.getData(),
            index;
        if (remote) {
            data.setSorters(me.getSorters());
        }
        index = data.findInsertionIndex(record);
        if (remote) {
            data.setSorters(null);
        }
        return me.insert(index, record);
    },
    /**
     * Removes the specified record(s) from the Store, firing the {@link #event-remove}
     * event for the removed records.
     *
     * After all records have been removed a single `datachanged` is fired.
     *
     * @param {Ext.data.Model/Ext.data.Model[]/Number/Number[]} records Model instance or
     * array of instances to remove or an array of indices from which to remove records.
     * @param isMove (private)
     * @param silent (private)
     */
    remove: function(records, isMove, silent) {
        var me = this,
            data = me.getDataSource(),
            len, i, toRemove, record;
        if (records) {
            if (records.isModel) {
                if (data.indexOf(records) > -1) {
                    toRemove = [
                        records
                    ];
                    len = 1;
                } else {
                    len = 0;
                }
            } else {
                toRemove = [];
                for (i = 0 , len = records.length; i < len; ++i) {
                    record = records[i];
                    if (record && record.isEntity) {
                        if (!data.contains(record)) {
                            
                            continue;
                        }
                    } else if (!(record = data.getAt(record))) {
                        // an index
                        
                        continue;
                    }
                    toRemove.push(record);
                }
                len = toRemove.length;
            }
        }
        if (!len) {
            return [];
        }
        me.removeIsMove = isMove === true;
        me.removeIsSilent = silent;
        data.remove(toRemove);
        me.removeIsSilent = false;
        return toRemove;
    },
    onCollectionRemove: function(collection, info) {
        var me = this,
            // Must use class-specific removed property.
            // Regular Stores add to the "removed" property on remove.
            // TreeStores are having records removed all the time; node collapse removes.
            // TreeStores add to the "removedNodes" property onNodeRemove
            removed = me.removed,
            records = info.items,
            len = records.length,
            index = info.at,
            replacement = info.replacement,
            /* eslint-disable-next-line max-len */
            isMove = me.removeIsMove || (replacement && Ext.Array.equals(records, replacement.items)),
            silent = me.removeIsSilent,
            lastChunk = !info.next,
            data = me.getDataSource(),
            i, record;
        if (me.ignoreCollectionRemove) {
            return;
        }
        if (replacement) {
            me.setMoving(replacement.items, true);
        }
        for (i = len - 1; i >= 0; i--) {
            record = records[i];
            // If the data contains the record, that means the record is filtered out, so
            // it's not being removed, nor should it be unjoined
            if (!data.contains(record)) {
                // Don't push interally moving, or phantom (client side only), 
                // erasing (informing server through its own proxy) records
                if (removed && !isMove && !record.phantom && !record.erasing) {
                    // Store the index the record was removed from so that rejectChanges can
                    // re-insert at the correct place.
                    // The record's index property won't do, as that is the index in the overall
                    // dataset when Store is buffered.
                    record.removedFrom = index + i;
                    removed.push(record);
                    // Removal of a non-phantom record which is NOT erasing (informing the server
                    // through its own proxy) requires that the store be synced at some point.
                    me.needsSync = true;
                } else {
                    // Only unjoin if we're not being pushed into the removed collection. We still
                    // have an interest in that record otherwise.
                    record.unjoin(me);
                }
            }
        }
        if (!silent) {
            // If this removal is just the first part of a replacement operation,
            // do not fire the events now.
            //
            // onCollectionAddItems will fire a refresh event, and convert multiple
            // remove and add operations to an atomic refresh event.
            // This will provide a better UI update.
            // Also, focus can only be preserved around one operation, so
            // editing a field which is the sorted field could result in 
            // incorrect focus..
            if (!replacement || !replacement.items.length) {
                me.fireEvent('remove', me, records, index, isMove);
                // If there is a next property, that means there is another range that needs
                // to be removed after this. Wait until everything is gone before firing datachanged
                // since it should be a bulk operation
                if (lastChunk) {
                    me.fireEvent('datachanged', me);
                }
            }
        }
        if (replacement) {
            me.setMoving(replacement.items, false);
        }
    },
    onFilterEndUpdate: function() {
        var me = this;
        if (me.destroying || me.destroyed) {
            return;
        }
        // Filtering changes the items reference of the collection, and that array
        // object if directly referenced by Ranges. The ranges have to refresh themselves
        // upon add.
        if (me.activeRanges) {
            me.syncActiveRanges();
        }
        me.callParent(arguments);
        me.callObservers('Filter');
    },
    /**
     * Removes the model instance(s) at the given index
     * @param {Number} index The record index
     * @param {Number} [count=1] The number of records to delete
     */
    removeAt: function(index, count) {
        var data = this.getData();
        // Sanity check input.
        index = Math.max(index, 0);
        if (index < data.length) {
            if (arguments.length === 1) {
                count = 1;
            } else if (!count) {
                return;
            }
            data.removeAt(index, count);
        }
    },
    /**
     * Removes all unfiltered items from the store.  Filtered records will not be removed.
     * Individual record `{@link #event-remove}` events are not fired by this method.
     *
     * @param {Boolean} [silent=false] Pass `true` to prevent the `{@link #event-clear}` event
     * from being fired.
     * @return {Ext.data.Model[]} The removed records.
     */
    removeAll: function(silent) {
        var me = this,
            data = me.getData(),
            records = data.getRange();
        // We want to remove and mute any events here
        if (data.length) {
            // Explicit true here, we never want to fire remove events
            me.removeIsSilent = true;
            me.callObservers('BeforeRemoveAll');
            data.removeAll();
            me.removeIsSilent = false;
            if (!silent) {
                me.fireEvent('clear', me, records);
                me.fireEvent('datachanged', me);
            }
            me.callObservers('AfterRemoveAll', [
                !!silent
            ]);
        }
        return records;
    },
    /**
     * Make a set of records be current in the store. This means that unneeded records
     * will be removed and new records will be added.
     * @param {Ext.data.Model[]} records The records to be current in the store.
     *
     * @private
     */
    setRecords: function(records) {
        var count = this.getCount();
        ++this.loadCount;
        if (count) {
            this.getData().splice(0, count, records);
        } else {
            this.add(records);
        }
    },
    /**
     * This method is basically the same as the JavaScript Array splice method.
     *
     * Negative indexes are interpreted starting at the end of the collection. That is,
     * a value of -1 indicates the last item, or equivalent to `length - 1`.
     *
     * @param {Number} index The index at which to add or remove items.
     * @param {Number/Object[]} toRemove The number of items to remove or an array of the
     * items to remove.
     * @param  {Object[]} [toAdd] The items to insert at the given `index`.
     * @private
     */
    splice: function(index, toRemove, toAdd) {
        return this.getData().splice(index, toRemove, toAdd);
    },
    /**
     * @protected
     * Called internally when a Proxy has completed a load request
     */
    onProxyLoad: function(operation) {
        var me = this,
            resultSet = operation.getResultSet(),
            records = operation.getRecords(),
            successful = operation.wasSuccessful();
        if (me.destroyed) {
            return;
        }
        if (resultSet) {
            me.totalCount = resultSet.getTotal();
        }
        if (successful) {
            records = me.processAssociation(records);
            me.loadRecords(records, operation.getAddRecords() ? {
                addRecords: true
            } : undefined);
            me.attachSummaryRecord(resultSet);
        } else {
            me.loading = false;
        }
        if (me.hasListeners.load) {
            me.fireEvent('load', me, records, successful, operation);
        }
        me.callObservers('AfterLoad', [
            records,
            successful,
            operation
        ]);
    },
    onProxyWrite: function(operation) {
        if (operation.wasSuccessful()) {
            this.attachSummaryRecord(operation.getResultSet());
        }
        this.callParent([
            operation
        ]);
    },
    // private
    filterDataSource: function(fn) {
        var source = this.getDataSource(),
            items = source.items,
            len = items.length,
            ret = [],
            i;
        for (i = 0; i < len; i++) {
            if (fn.call(source, items[i])) {
                ret.push(items[i]);
            }
        }
        return ret;
    },
    getNewRecords: function() {
        return this.filterDataSource(this.filterNew);
    },
    getRejectRecords: function() {
        return this.filterDataSource(this.filterRejects);
    },
    getUpdatedRecords: function() {
        return this.filterDataSource(this.filterUpdated);
    },
    /**
     * Loads an array of data straight into the Store.
     *
     * Using this method is great if the data is in the correct format already (e.g. it doesn't
     * need to be processed by a reader). If your data requires processing to decode the data
     * structure, use a {@link Ext.data.proxy.Memory MemoryProxy} or {@link #loadRawData}.
     *
     * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances will
     * be cast into model instances first.
     * @param {Boolean} [append=false] `true` to add the records to the existing records in the
     * store, `false` to remove the old ones first.
     */
    loadData: function(data, append) {
        var me = this,
            length = data.length,
            newData = [],
            i;
        // make sure each data element is an Ext.data.Model instance
        for (i = 0; i < length; i++) {
            newData.push(me.createModel(data[i]));
        }
        newData = me.processAssociation(newData);
        me.loadRecords(newData, append ? me.addRecordsOptions : undefined);
    },
    /**
     * Loads data via the bound Proxy's reader
     *
     * Use this method if you are attempting to load data and want to utilize the configured data
     * reader.
     *
     * As of 4.2, this method will no longer fire the {@link #event-load} event.
     *
     * @param {Object[]} data The full JSON object you'd like to load into the Data store.
     * @param {Boolean} [append=false] `true` to add the records to the existing records in the
     * store, `false` to remove the old ones first.
     * 
     * @return {Boolean} `true` if the reader processed the records correctly. See
     * {@link Ext.data.reader.Reader#successProperty}. If the reader did not process the records,
     * nothing will be added.
     */
    loadRawData: function(data, append) {
        var me = this,
            session = me.getSession(),
            result, records, success;
        /* eslint-disable-next-line max-len */
        result = me.getProxy().getReader().read(data, session ? {
            recordCreator: session.recordCreator
        } : undefined);
        records = result.getRecords();
        success = result.getSuccess();
        if (success) {
            me.totalCount = result.getTotal();
            me.loadRecords(records, append ? me.addRecordsOptions : undefined);
        }
        return success;
    },
    /**
     * Loads an array of {@link Ext.data.Model model} instances into the store, fires the
     * datachanged event. This should only usually be called internally when loading from the
     * {@link Ext.data.proxy.Proxy Proxy}, when adding records manually use {@link #method-add}
     * instead
     * @param {Ext.data.Model[]} records The array of records to load
     * @param {Object} options
     * @param {Boolean} [options.addRecords=false] Pass `true` to add these records to the existing
     * records, `false` to remove the Store's existing records first.
     */
    loadRecords: function(records, options) {
        var me = this,
            data = me.getData(),
            addRecords, skipSort;
        if (options) {
            addRecords = options.addRecords;
        }
        if (!me.getRemoteSort() && !me.getSortOnLoad()) {
            skipSort = true;
            data.setAutoSort(false);
        }
        if (!addRecords) {
            me.clearData(true);
        }
        // Clear the flag AFTER the stores collection has been cleared down so that
        // observers of that collection know that it was due to a load, and a refresh is imminent.
        me.loading = false;
        me.ignoreCollectionAdd = true;
        me.callObservers('BeforePopulate');
        data.add(records);
        me.ignoreCollectionAdd = false;
        if (skipSort) {
            data.setAutoSort(true);
        }
        me.afterLoadRecords(records);
    },
    afterLoadRecords: function(records) {
        var me = this,
            length = records.length,
            i;
        for (i = 0; i < length; i++) {
            records[i].join(me);
        }
        if (!me.isEmptyStore) {
            ++me.loadCount;
            me.complete = true;
        }
        if (me.hasListeners.datachanged) {
            me.fireEvent('datachanged', me);
        }
        if (me.hasListeners.refresh) {
            me.fireEvent('refresh', me);
        }
        me.callObservers('AfterPopulate');
    },
    // PAGING METHODS
    /**
     * Loads a given 'page' of data by setting the start and limit values appropriately. Internally
     * this just causes a normal load operation, passing in calculated 'start' and 'limit' params.
     * @param {Number} page The number of the page to load.
     * @param {Object} [options] See options for {@link #method-load}.
     */
    loadPage: function(page, options) {
        var me = this,
            size = me.getPageSize();
        me.currentPage = page;
        // Copy options into a new object so as not to mutate passed in objects
        options = Ext.apply({
            page: page,
            start: (page - 1) * size,
            limit: size,
            addRecords: !me.getClearOnPageLoad()
        }, options);
        me.read(options);
    },
    /**
     * Loads the next 'page' in the current data set
     * @param {Object} options See options for {@link #method-load}
     */
    nextPage: function(options) {
        this.loadPage(this.currentPage + 1, options);
    },
    /**
     * Loads the previous 'page' in the current data set
     * @param {Object} options See options for {@link #method-load}
     */
    previousPage: function(options) {
        this.loadPage(this.currentPage - 1, options);
    },
    /**
     * @private
     */
    clearData: function(isLoad) {
        var me = this,
            removed = me.removed,
            data = me.getDataSource(),
            clearRemovedOnLoad = me.getClearRemovedOnLoad(),
            needsUnjoinCheck = removed && isLoad && !clearRemovedOnLoad,
            records, record, i, len;
        // We only have to do the unjoining if not buffered. PageMap will unjoin its records when
        // it clears itself.
        // There is a potential for a race condition in stores configured with autoDestroy: true;
        // if loading was initiated but didn't complete by the time the store is destroyed,
        // the data MC may not have been created yet so we have to check for its existence
        // here and below.
        if (data) {
            records = data.items;
            for (i = 0 , len = records.length; i < len; ++i) {
                record = records[i];
                if (needsUnjoinCheck && Ext.Array.contains(removed, record)) {
                    
                    continue;
                }
                record.unjoin(me);
            }
            me.ignoreCollectionRemove = true;
            me.callObservers('BeforeClear');
            data.removeAll();
            me.ignoreCollectionRemove = false;
            me.callObservers('AfterClear');
        }
        if (removed && (!isLoad || clearRemovedOnLoad)) {
            removed.length = 0;
        }
    },
    onIdChanged: function(rec, oldId, newId) {
        this.getData().updateKey(rec, oldId);
        // This event is used internally
        this.fireEvent('idchanged', this, rec, oldId, newId);
    },
    /**
     * Commits all Records with {@link #getModifiedRecords outstanding changes}. To handle updates
     * for changes, subscribe to the Store's {@link #event-update update event}, and perform
     * updating when the third parameter is Ext.data.Record.COMMIT.
     */
    commitChanges: function() {
        var me = this,
            recs = me.getModifiedRecords(),
            len = recs.length,
            i = 0;
        Ext.suspendLayouts();
        me.beginUpdate();
        for (; i < len; i++) {
            recs[i].commit();
        }
        me.cleanRemoved();
        me.endUpdate();
        Ext.resumeLayouts(true);
        /**
         * @private
         * @event commit
         * Fired when all changes were committed and the Store is clean.
         *
         * **Note** Used internally.
         *
         * @param {Ext.data.Store} store The Store object
         */
        me.fireEvent('commit', me);
    },
    filterNewOnly: function(item) {
        return item.phantom === true;
    },
    filterRejects: function(item) {
        return item.phantom || item.dirty;
    },
    /**
     * {@link Ext.data.Model#reject Rejects} outstanding changes on all {@link #getModifiedRecords
     * modified records} and re-insert any records that were removed locally. Any phantom records
     * will be removed.
     */
    rejectChanges: function() {
        var me = this,
            recs = me.getRejectRecords(),
            len = recs.length,
            i, rec, toRemove, sorted, data, currentAutoSort;
        Ext.suspendLayouts();
        me.beginUpdate();
        for (i = 0; i < len; i++) {
            rec = recs[i];
            if (rec.phantom) {
                toRemove = toRemove || [];
                toRemove.push(rec);
            } else {
                rec.reject();
            }
        }
        if (toRemove) {
            me.remove(toRemove);
            for (i = 0 , len = toRemove.length; i < len; ++i) {
                toRemove[i].reject();
            }
        }
        // Restore removed records back to their original positions.
        recs = me.getRawRemovedRecords();
        if (recs) {
            len = recs.length;
            sorted = !me.getRemoteSort() && me.isSorted();
            if (sorted) {
                // Temporarily turn off sorting so .reject() doesn't attempt to sort the record.
                // It would throw b/c the record isn't yet in its collection.
                data = me.getData();
                currentAutoSort = data.getAutoSort();
                data.setAutoSort(false);
            }
            for (i = len - 1; i >= 0; i--) {
                rec = recs[i];
                rec.reject();
                if (!sorted) {
                    me.insert(rec.removedFrom || 0, rec);
                }
            }
            if (sorted) {
                // Turn sorting back on so the collection is auto-sorted when added.
                data.setAutoSort(currentAutoSort);
                me.add(recs);
            }
            // Don't need to call cleanRemoved because we've re-added everything, don't
            // need to unjoin the store
            recs.length = 0;
        }
        me.endUpdate();
        Ext.resumeLayouts(true);
        /**
         * @private
         * @event reject
         * Fired when all changes were rejected and the Store is clean.
         *
         * **Note** Used internally.
         *
         * @param {Ext.data.Store} store The Store object
         */
        me.fireEvent('reject', me);
    },
    doDestroy: function() {
        var me = this,
            task = me.loadTask,
            data = me.getData(),
            source = data.getSource();
        // clearData ensures everything is unjoined
        me.clearData();
        me.setSession(null);
        me.observers = null;
        if (task) {
            task.cancel();
            me.loadTask = null;
        }
        if (source) {
            source.destroy();
        }
        me.callParent();
    },
    privates: {
        commitOptions: {
            commit: true
        },
        attachSummaryRecord: function(resultSet) {
            if (!resultSet) {
                return;
            }
            /* eslint-disable-next-line vars-on-top */
            var me = this,
                summary = resultSet.getSummaryData(),
                grouper = me.getGrouper(),
                current = me.summaryRecord,
                commitOptions = me.commitOptions,
                groups, len, i, rec, group;
            if (summary) {
                if (current) {
                    current.set(summary.data, commitOptions);
                } else {
                    me.summaryRecord = summary;
                    summary.isRemote = true;
                }
            }
            if (grouper) {
                summary = resultSet.getGroupData();
                if (summary) {
                    groups = me.getGroups();
                    for (i = 0 , len = summary.length; i < len; ++i) {
                        rec = summary[i];
                        group = groups.getItemGroup(rec);
                        if (group) {
                            current = group.summaryRecord;
                            if (current) {
                                current.set(rec.data, commitOptions);
                            } else {
                                group.summaryRecord = rec;
                                rec.isRemote = true;
                            }
                        }
                    }
                }
            }
        },
        /**
         * Similar to a load, however no records are added to the store. This is useful
         * in allowing the developer to decide what to do with the new records.
         * @param {Object} [options] See {@link #method-load load options}.
         *
         * @private
         */
        fetch: function(options) {
            var operation;
            options = Ext.apply({}, options);
            this.setLoadOptions(options);
            operation = this.createOperation('read', options);
            operation.execute();
        },
        fireChangeEvent: function(record) {
            return this.getDataSource().contains(record);
        },
        onBeforeLoad: function(operation) {
            this.callObservers('BeforeLoad', [
                operation
            ]);
        },
        onRemoteFilterSet: function(filters, remoteFilter) {
            if (filters) {
                this.getData().setFilters(remoteFilter ? null : filters);
            }
            this.callParent([
                filters,
                remoteFilter
            ]);
        },
        onRemoteSortSet: function(sorters, remoteSort) {
            var data = this.getData();
            if (sorters) {
                data.setSorters(remoteSort ? null : sorters);
            }
            data.setAutoGroup(!remoteSort);
            this.callParent([
                sorters,
                remoteSort
            ]);
        },
        /**
         * Checks whether records are being moved within the store. This can be used in conjunction
         * with the {@link #event-add} and {@link #event-remove} events to determine whether
         * the records are being removed/added or just having the position changed.
         * @param {Ext.data.Model[]/Ext.data.Model} [records] The record(s).
         * @param {Object} [getMap] (private)
         * @return {Number} The number of records being moved. `0` if no records are moving.
         * If records are passed the number will refer to how many of the passed records are moving.
         *
         * @private
         */
        isMoving: function(records, getMap) {
            var map = this.moveMap,
                moving = 0,
                len, i;
            if (map) {
                if (records) {
                    if (Ext.isArray(records)) {
                        for (i = 0 , len = records.length; i < len; ++i) {
                            moving += map[records[i].id] ? 1 : 0;
                        }
                    } else if (map[records.id]) {
                        ++moving;
                    }
                } else {
                    moving = getMap ? map : this.moveMapCount;
                }
            }
            return moving;
        },
        setLoadOptions: function(options) {
            // Only add grouping options if grouping is remote
            var me = this,
                pageSize = me.getPageSize(),
                session, grouper;
            if (me.getRemoteSort() && !options.grouper) {
                grouper = me.getGrouper();
                if (grouper) {
                    options.grouper = grouper;
                }
            }
            if (pageSize || 'start' in options || 'limit' in options || 'page' in options) {
                options.page = options.page != null ? options.page : me.currentPage;
                options.start = (options.start !== undefined) ? options.start : (options.page - 1) * pageSize;
                options.limit = options.limit != null ? options.limit : pageSize;
                me.currentPage = options.page;
            }
            options.addRecords = options.addRecords || false;
            if (!options.recordCreator) {
                session = me.getSession();
                if (session) {
                    options.recordCreator = session.recordCreator;
                }
            }
            me.callParent([
                options
            ]);
        },
        setMoving: function(records, isMoving) {
            var me = this,
                map = me.moveMap || (me.moveMap = {}),
                len = records.length,
                i, id;
            for (i = 0; i < len; ++i) {
                id = records[i].id;
                if (isMoving) {
                    if (map[id]) {
                        ++map[id];
                    } else {
                        map[id] = 1;
                        ++me.moveMapCount;
                    }
                } else {
                    if (--map[id] === 0) {
                        delete map[id];
                        --me.moveMapCount;
                    }
                }
            }
            if (me.moveMapCount === 0) {
                me.moveMap = null;
            }
        },
        processAssociation: function(records) {
            var me = this,
                associatedEntity = me.getAssociatedEntity();
            if (associatedEntity) {
                records = me.getRole().processLoad(me, associatedEntity, records, me.getSession());
            }
            return records;
        }
    }
});
// Provides docs from the mixin
/**
     * @method each
     * @inheritdoc Ext.data.LocalStore#each
     */
/**
     * @method collect
     * @inheritdoc Ext.data.LocalStore#collect
     */
/**
     * @method getById
     * @inheritdoc Ext.data.LocalStore#getById
     */
/**
     * @method getByInternalId
     * @inheritdoc Ext.data.LocalStore#getByInternalId
     */
/**
     * @method indexOf
     * @inheritdoc Ext.data.LocalStore#indexOf
     */
/**
     * @method indexOfId
     * @inheritdoc Ext.data.LocalStore#indexOfId
     */
/**
     * @method queryBy
     * @inheritdoc Ext.data.LocalStore#queryBy
     */
/**
     * @method query
     * @inheritdoc Ext.data.LocalStore#query
     */
/**
     * @method first
     * @inheritdoc Ext.data.LocalStore#first
     */
/**
     * @method last
     * @inheritdoc Ext.data.LocalStore#last
     */
/**
     * @method sum
     * @inheritdoc Ext.data.LocalStore#sum
     */
/**
     * @method count
     * @inheritdoc Ext.data.LocalStore#count
     */
/**
     * @method min
     * @inheritdoc Ext.data.LocalStore#min
     */
/**
     * @method max
     * @inheritdoc Ext.data.LocalStore#max
     */
/**
     * @method average
     * @inheritdoc Ext.data.LocalStore#average
     */
/**
     * @method aggregate
     * @inheritdoc Ext.data.LocalStore#aggregate
     */

/**
 * Data reader class to create an Array of {@link Ext.data.Model} objects from an Array.
 * Each element of that Array represents a row of data fields. The
 * fields are pulled into a Record object using as a subscript, the `mapping` property
 * of the field definition if it exists, or the field's ordinal position in the definition.
 *
 * ##Example code:
 *
 *      Employee = Ext.define('Employee', {
 *          extend: 'Ext.data.Model',
 *          fields: [
 *              'id',
 *              // "mapping" only needed if an "id" field is present which
 *              // precludes using the ordinal position as the index.
 *              { name: 'name', mapping: 1 },
 *              { name: 'occupation', mapping: 2 }
 *          ]
 *      });
 *
 *       var myReader = new Ext.data.reader.Array({
 *            model: 'Employee'
 *       }, Employee);
 *
 * This would consume an Array like this:
 *
 *      [ [1, 'Bill', 'Gardener'], [2, 'Ben', 'Horticulturalist'] ]
 *
 */
Ext.define('Ext.data.reader.Array', {
    extend: 'Ext.data.reader.Json',
    alternateClassName: 'Ext.data.ArrayReader',
    alias: 'reader.array',
    // For Array Reader, methods in the base which use these properties must not see the defaults
    config: {
        /**
         * @cfg totalProperty
         * @inheritdoc
         */
        totalProperty: undefined,
        /**
         * @cfg successProperty
         * @inheritdoc
         */
        successProperty: undefined
    },
    /**
         * @cfg {Boolean} preserveRawData
         * @hide
         */
    /**
     * @method constructor
     * @constructor
     * Create a new ArrayReader
     * @param {Object} meta Metadata configuration options.
     */
    createFieldAccessor: function(field) {
        // In the absence of a mapping property, use the original ordinal position
        // at which the Model inserted the field into its collection.
        var oldMap = field.mapping,
            index = field.hasMapping() ? oldMap : field.ordinal,
            result;
        // Temporarily overwrite the mapping and use the superclass method.
        field.mapping = index;
        result = this.callParent(arguments);
        field.mapping = oldMap;
        return result;
    },
    getModelData: function(raw) {
        // Can't preserve raw data here
        return {};
    }
});

/**
 * Small helper class to make creating {@link Ext.data.Store}s from Array data easier. An
 * ArrayStore will be automatically configured with a {@link Ext.data.reader.Array}.
 *
 * A store configuration would be something like:
 *
 *     var store = Ext.create('Ext.data.ArrayStore', {
 *         // store configs
 *         storeId: 'myStore',
 *         // reader configs
 *         fields: [
 *            'company',
 *            {name: 'price', type: 'float'},
 *            {name: 'change', type: 'float'},
 *            {name: 'pctChange', type: 'float'},
 *            {name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
 *         ]
 *     });
 *
 * This store is configured to consume a returned object of the form:
 *
 *     var myData = [
 *         ['3m Co',71.72,0.02,0.03,'9/1 12:00am'],
 *         ['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am'],
 *         ['Boeing Co.',75.43,0.53,0.71,'9/1 12:00am'],
 *         ['Hewlett-Packard Co.',36.53,-0.03,-0.08,'9/1 12:00am'],
 *         ['Wal-Mart Stores, Inc.',45.45,0.73,1.63,'9/1 12:00am']
 *     ];
 *
 * An object literal of this form could also be used as the {@link #cfg-data} config option.
 */
Ext.define('Ext.data.ArrayStore', {
    extend: 'Ext.data.Store',
    alias: 'store.array',
    alternateClassName: [
        'Ext.data.SimpleStore'
    ],
    requires: [
        'Ext.data.proxy.Memory',
        'Ext.data.reader.Array'
    ],
    config: {
        proxy: {
            type: 'memory',
            reader: 'array'
        }
    },
    loadData: function(data, append) {
        if (this.expandData) {
            /* eslint-disable-next-line vars-on-top */
            var r = [],
                i = 0,
                ln = data.length;
            for (; i < ln; i++) {
                r[r.length] = [
                    data[i]
                ];
            }
            data = r;
        }
        this.callParent([
            data,
            append
        ]);
    }
});

/**
 * Contains a collection of all stores that are created that have an identifier. An identifier
 * can be assigned by setting the {@link Ext.data.AbstractStore#storeId storeId} property. When
 * a store is in the StoreManager, it can be referred to via it's identifier:
 *
 *     Ext.create('Ext.data.Store', {
 *         model: 'SomeModel',
 *         storeId: 'myStore'
 *     });
 *
 *     var store = Ext.data.StoreManager.lookup('myStore');
 *
 * Also note that the {@link #lookup} method is aliased to {@link Ext#getStore} for convenience.
 *
 * If a store is registered with the StoreManager, you can also refer to the store by its
 * identifier when registering it with any Component that consumes data from a store:
 *
 *     Ext.create('Ext.data.Store', {
 *         model: 'SomeModel',
 *         storeId: 'myStore'
 *     });
 *
 *     Ext.create('Ext.view.View', {
 *         store: 'myStore',
 *         // other configuration here
 *     });
 *
 */
Ext.define('Ext.data.StoreManager', {
    extend: 'Ext.util.MixedCollection',
    alternateClassName: [
        'Ext.StoreMgr',
        'Ext.data.StoreMgr',
        'Ext.StoreManager'
    ],
    singleton: true,
    requires: [
        'Ext.data.ArrayStore'
    ],
    /**
     * @cfg {Object} listeners
     * @private
     */
    /**
     * Registers one or more Stores with the StoreManager. You do not normally need to register
     * stores manually. Any store initialized with a {@link Ext.data.Store#storeId} will be
     * auto-registered.
     * @param {Ext.data.Store...} stores Any number of Store instances
     */
    register: function() {
        var i, s;
        for (i = 0; (s = arguments[i]); i++) {
            this.add(s);
        }
    },
    /**
     * Unregisters one or more Stores with the StoreManager
     * @param {String/Object...} stores Any number of Store instances or ID-s
     */
    unregister: function() {
        var i, s;
        for (i = 0; (s = arguments[i]); i++) {
            this.remove(this.lookup(s));
        }
    },
    /**
     * Gets a registered Store by id
     * @param {String/Object} store The id of the Store, or a Store instance, or a store
     * configuration
     * @param {String} [defaultType] The store type to create when used with store configuration
     * and there is no type specified on the config.
     * @return {Ext.data.Store}
     */
    lookup: function(store, defaultType) {
        var first, data, arrays, fields, i, len;
        // handle the case when we are given an array or an array of arrays.
        if (Ext.isArray(store)) {
            first = store[0];
            data = store;
            if (Ext.isObject(first)) {
                // store: [ { foo: 42, ... }, { foo: 427, ... }, ... ]
                store = {
                    data: data
                };
            } else {
                arrays = Ext.isArray(first);
                fields = [
                    'field1'
                ];
                if (arrays) {
                    // store: [ [1,2], [3,4], ... ]
                    for (i = 2 , len = first.length; i <= len; ++i) {
                        fields.push('field' + i);
                    }
                } else {
                    // store: [ 1,2,3, ... ]
                    data = [];
                    for (i = 0 , len = store.length; i < len; ++i) {
                        data.push([
                            store[i]
                        ]);
                    }
                }
                return new Ext.data.ArrayStore({
                    data: data,
                    fields: fields,
                    autoDestroy: true,
                    autoCreated: true,
                    expanded: !arrays
                });
            }
        }
        if (Ext.isString(store)) {
            // store id
            return this.get(store);
        } else {
            // store instance or store config
            return Ext.Factory.store(store, defaultType);
        }
    },
    // getKey implementation for MixedCollection
    getKey: function(o) {
        return o.storeId;
    },
    addEmptyStore: function() {
        // A dummy empty store with a fieldless Model defined in it.
        // Just for binding to Views which are instantiated with no Store defined.
        // They will be able to run and render fine, and be bound to a generated Store later.
        var emptyStore = this.$emptyStore,
            destoryable = {
                destroy: Ext.emptyFn
            };
        if (!emptyStore) {
            emptyStore = this.$emptyStore = Ext.regStore('ext-empty-store', {
                proxy: 'memory',
                useModelWarning: false
            });
            emptyStore.ignoreLeaked = true;
            emptyStore.isEmptyStore = true;
            emptyStore.on = emptyStore.addListener = function() {
                return destoryable;
            };
            emptyStore.un = emptyStore.removeListener = Ext.emptyFn;
            emptyStore.add = emptyStore.remove = emptyStore.insert = emptyStore.destroy = emptyStore.loadData = function() {
                Ext.raise('Cannot modify ext-empty-store');
            };
        }
        this.add(emptyStore);
    },
    clear: function() {
        this.callParent();
        this.addEmptyStore();
    }
}, function() {
    /**
     * Creates a new store for the given id and config, then registers it with the
     * {@link Ext.data.StoreManager Store Manager}.  Sample usage:
     *
     *     Ext.regStore('AllUsers', {
     *         model: 'User'
     *     });
     *
     *     // the store can now easily be used throughout the application
     *     new Ext.List({
     *         store: 'AllUsers',
     *         ... other config
     *     });
     *
     * @param {String/Object} id The id to set on the new store, or the `config` object
     * that contains the `storeId` property.
     * @param {Object} config The store config if the first parameter (`id`) is just the
     * id.
     * @member Ext
     * @method regStore
     */
    Ext.regStore = function(id, config) {
        var store;
        if (Ext.isObject(id)) {
            config = id;
        } else {
            if (Ext.data.StoreManager.containsKey(id)) {
                return Ext.data.StoreManager.lookup(id);
            }
            config.storeId = id;
        }
        if (config instanceof Ext.data.Store) {
            store = config;
        } else {
            store = new Ext.data.Store(config);
        }
        Ext.data.StoreManager.register(store);
        return store;
    };
    /**
     * @method getStore
     * Shortcut to {@link Ext.data.StoreManager#lookup}.
     * @member Ext
     * @inheritdoc Ext.data.StoreManager#method-lookup
     */
    Ext.getStore = function(name) {
        return Ext.data.StoreManager.lookup(name);
    };
    Ext.data.StoreManager.addEmptyStore();
});

/**
 * This class implements the data store event domain. All classes extending from 
 * {@link Ext.data.AbstractStore} are included in this domain. The selectors are simply
 * store id, alias or the wildcard "*" to match any store.
 *
 * @private
 */
Ext.define('Ext.app.domain.Store', {
    extend: 'Ext.app.EventDomain',
    singleton: true,
    requires: [
        'Ext.data.AbstractStore'
    ],
    type: 'store',
    prefix: 'store.',
    idMatchRe: /^#/,
    constructor: function() {
        var me = this;
        me.callParent();
        me.monitor(Ext.data.AbstractStore);
    },
    match: function(target, selector) {
        var result = false,
            alias = target.alias;
        if (selector === '*') {
            result = true;
        } else if (this.idMatchRe.test(selector)) {
            result = target.getStoreId() === selector.substring(1);
        } else if (alias) {
            result = Ext.Array.indexOf(alias, this.prefix + selector) > -1;
        }
        return result;
    }
});

/**
 * Controllers are the glue that binds an application together. That said, their main
 * purpose is to listen for events (usually from views) and take some action. Here's how
 * we might create a Controller to manage Users:
 *
 *      Ext.define('MyApp.controller.Users', {
 *          extend: 'Ext.app.Controller',
 *
 *          init: function() {
 *              console.log('Initialized Users! This happens before ' +
 *                          'the Application launch() function is called');
 *          }
 *      });
 *
 * The init function is a special method that is called when your application boots. It is
 * called before the {@link Ext.app.Application Application}'s launch function is executed.
 * This creates an area you can run code prior to Viewport creation.
 *
 * The controller's {@link #method-control} function
 * makes it easy to listen to events on your view classes and take some action with a
 * handler function. Let's update our Users controller to tell us when the panel is
 * rendered:
 *
 *      Ext.define('MyApp.controller.Users', {
 *          extend: 'Ext.app.Controller',
 *
 *          control: {
 *              'viewport > panel': {
 *                  render: 'onPanelRendered'
 *              }
 *          }
 *
 *          onPanelRendered: function() {
 *              console.log('The panel was rendered');
 *          }
 *      });
 *
 * The {@link Ext.app.BaseController#method-control control method} has now set up
 * listeners on views in our application. The control method uses the ComponentQuery
 * engine to quickly and easily get references to components on the page. If you are not
 * familiar with ComponentQuery yet, be sure to check out the
 * {@link Ext.ComponentQuery documentation}. In brief, it allows us to pass a
 * CSS-like selector that will find every matching component on the page.
 *
 * In our init function above, we supplied 'viewport > panel', which translates to "find me
 * every Panel that is a direct child of a Viewport". We then supplied an object that maps
 * event names (just 'render' in this case) to handler functions. In short, whenever
 * a component that matches our selector fires a 'render' event, our
 * onPanelRendered function is called.
 *
 * ## Event domains
 *
 * In Ext JS 4.2, we introduced the concept of event domains. In terms of MVC, an event
 * domain is one or more base classes that fire events to which a Controller wants to
 * listen. Besides Component event domain that encompass {@link Ext.Component}-descended
 * Views, Controllers now can listen to events from data Stores, Ext Direct Providers,
 * other Controllers, and Ext.GlobalEvents. This feature provides a way to communicate
 * between parts of the whole application without the need to bind controllers together
 * tightly, and allows to develop and test application parts in isolation.
 *
 * See usage examples in {@link #method-listen} method documentation.
 *
 * ## Using refs
 *
 * One of the most useful parts of Controllers is the ref system. These use
 * {@link Ext.ComponentQuery} to make it really easy to get references to Views on your
 * page. Let's look at an example of this now:
 *
 *      Ext.define('MyApp.controller.Users', {
 *          extend: 'Ext.app.Controller',
 *
 *          refs: [{
 *              ref: 'list',
 *              selector: 'grid'
 *          }],
 *
 *          control: {
 *              'button': {
 *                  click: 'refreshGrid'
 *              }
 *          },
 *
 *          refreshGrid: function() {
 *              this.getList().store.load();
 *          }
 *      });
 *
 * This example assumes the existence of a {@link Ext.grid.Panel Grid} on the page, which
 * contains a single button to refresh the Grid when clicked. In our refs array, we set up
 * a reference to the grid. There are two parts to this - the 'selector', which is a
 * {@link Ext.ComponentQuery ComponentQuery} selector which finds any grid on the page and
 * assigns it to the reference 'list'.
 *
 * By giving the reference a name, we get a number of things for free. The first is the
 * getList function that we use in the refreshGrid method above. This is generated
 * automatically by the Controller based on the name of our ref, which was capitalized and
 * prepended with get to go from 'list' to 'getList'.
 *
 * The way this works is that the first time getList is called by your code, the
 * ComponentQuery selector is run and the first component that matches the selector
 * ('grid' in this case) will be returned. All future calls to getList will use a cached
 * reference to that grid. Usually it is advised to use a specific ComponentQuery selector
 * that will only match a single View in your application (in the case above our selector
 * will match any grid on the page).
 *
 * Bringing it all together, we configure control
 * to listen to any click on a {@link Ext.button.Button button} and call our refreshGrid
 * function (again, this will match any button on the page so we advise a more specific
 * selector than just 'button', but have left it this way for simplicity). When the button
 * is clicked we use out getList function to refresh the grid.
 *
 * You can create any number of refs and control any number of components this way, simply
 * adding more functions to your Controller as you go. For an example of real-world usage
 * of Controllers see the Feed Viewer example in the examples/app/feed-viewer folder in
 * the SDK download.
 *
 * ## Generated getter methods
 *
 * Refs aren't the only thing that generate convenient getter methods. Controllers often
 * have to deal with Models and Stores so the framework offers a couple of easy ways to
 * get access to those too. Let's look at another example:
 *
 *      Ext.define('MyApp.controller.Users', {
 *          extend: 'Ext.app.Controller',
 *
 *          models: ['User'],
 *          stores: ['AllUsers', 'AdminUsers'],
 *
 *          init: function() {
 *              var User, allUsers, ed;
 *
 *              User = this.getUserModel();
 *              allUsers = this.getAllUsersStore();
 *
 *              ed = new User({ name: 'Ed' });
 *              allUsers.add(ed);
 *          }
 *      });
 *
 * By specifying Models and Stores that the Controller cares about, it again dynamically
 * loads them from the appropriate locations (app/model/User.js, app/store/AllUsers.js and
 * app/store/AdminUsers.js in this case) and creates getter functions for them all. The
 * example above will create a new User model instance and add it to the AllUsers Store.
 * Of course, you could do anything in this function but in this case we just did
 * something simple to demonstrate the functionality.
 *
 * ## Further Reading
 *
 * For more information about writing Ext JS 5 applications, please see the
 * [Application Architecture](../../../application_architecture/application_architecture.html).
 * Also see the {@link Ext.app.Application} documentation.
 */
Ext.define('Ext.app.Controller', {
    extend: 'Ext.app.BaseController',
    requires: [
        'Ext.app.Util',
        'Ext.data.StoreManager',
        'Ext.ComponentManager',
        'Ext.app.domain.Component',
        'Ext.app.domain.Store'
    ],
    statics: {
        strings: {
            model: {
                getter: 'getModel',
                upper: 'Model'
            },
            view: {
                getter: 'getView',
                upper: 'View'
            },
            controller: {
                getter: 'getController',
                upper: 'Controller'
            },
            store: {
                getter: 'getStore',
                upper: 'Store'
            },
            profile: {
                getter: 'getProfile',
                upper: 'Profiles'
            }
        },
        controllerRegex: /^(.*)\.controller\./,
        profileRegex: /^(.*)\.profile\./,
        createGetter: function(baseGetter, name) {
            return function() {
                return this[baseGetter](name);
            };
        },
        getGetterName: function(name, kindUpper) {
            var fn = 'get',
                parts = name.split('.'),
                numParts = parts.length,
                index;
            // Handle namespaced class names. E.g. feed.Add becomes getFeedAddView etc.
            for (index = 0; index < numParts; index++) {
                fn += Ext.String.capitalize(parts[index]);
            }
            fn += kindUpper;
            return fn;
        },
        resolveNamespace: function(cls, data) {
            var Controller = Ext.app.Controller,
                namespaceRe, className, namespace, match;
            namespaceRe = cls.prototype.isProfile ? Controller.profileRegex : Controller.controllerRegex;
            /*
             * Namespace resolution is tricky business: we should know what namespace
             * this Controller descendant belongs to, or model/store/view dependency
             * resolution will be either ambiguous or plainly not possible. To avoid
             * guessing games we try to look for a forward hint ($namespace) that
             * Application class sets when its onClassExtended gets processed; if that
             * fails we try to deduce namespace from class name.
             *
             * Note that for Ext.app.Application, Controller.onClassExtended gets executed
             * *before* Application.onClassExtended so we have to delay namespace handling
             * until after Application.onClassExtended kicks in, hence it is done in this hook.
             */
            className = Ext.getClassName(cls);
            namespace = data.$namespace || data.namespace || Ext.app.getNamespace(className) || ((match = namespaceRe.exec(className)) && match[1]);
            if (!namespace) {
                Ext.log.warn("Missing namespace for " + className + ", please define it " + "in namespaces property of your Application class.");
            }
            return namespace;
        },
        /**
         * This method is called like so:
         *
         *      Ext.app.Controller.processDependencies(proto, requiresArray, 'MyApp', 'model', [
         *          'User',
         *          'Item',
         *          'Foo@Common.model',
         *          'Bar.Baz@Common.model'
         *      ]);
         *
         * Required dependencies are added to requiresArray.
         *
         * @private
         */
        processDependencies: function(cls, requires, namespace, kind, names, profileName) {
            if (!names || !names.length) {
                return;
            }
            /* eslint-disable-next-line vars-on-top */
            var me = this,
                strings = me.strings[kind],
                o, absoluteName, shortName, name, j, subLn, getterName, getter;
            if (!Ext.isArray(names)) {
                names = [
                    names
                ];
            }
            for (j = 0 , subLn = names.length; j < subLn; j++) {
                name = names[j];
                o = me.getFullName(name, kind, namespace, profileName);
                // Update the name in the array to be the absolute name
                names[j] = absoluteName = o.absoluteName;
                shortName = o.shortName;
                requires.push(absoluteName);
                getterName = me.getGetterName(shortName, strings.upper);
                if (!cls[getterName]) {
                    cls[getterName] = getter = me.createGetter(strings.getter, name);
                } else if (getterName === 'getMainView') {
                    Ext.log.warn('Cannot have a view named \'Main\' - getter conflicts ' + 'with mainView config.');
                }
                // Application class will init the controller getters
                if (getter && kind !== 'controller') {
                    // This marker allows the constructor to easily/cheaply identify the
                    // generated getter methods since they all need to be called to get
                    // things initialized. We use a property name that deliberately does
                    // not work with dot-access to reduce any chance of collision.
                    getter['Ext.app.getter'] = true;
                }
            }
        },
        getFullName: function(name, kind, namespace, profileName) {
            var shortName = name,
                sep, absoluteName;
            if ((sep = name.indexOf('@')) > 0) {
                // The unambiguous syntax is Model@Name.space (or "space.Model@Name")
                // which contains both the short name ("Model" or "space.Model") and
                // the full name (Name.space.Model).
                //
                shortName = name.substring(0, sep);
                // "Model"
                absoluteName = name.substring(sep + 1) + '.' + shortName;
            }
            //  ex: "Name.space.Model"
            // Deciding if a class name must be qualified:
            //
            // 1 - if the name doesn't contain a dot, we must qualify it
            //
            // 2 - the name may be a qualified name of a known class, but:
            //
            // 2.1 - in runtime, the loader may not know the class - specially in
            //       production - so we must check the class manager
            //
            // 2.2 - in build time, the class manager may not know the class, but
            //       the loader does, so we check the second one (the loader check
            //       assures it's really a class, and not a namespace, so we can
            //       have 'Books.controller.Books', and requesting a controller
            //       called Books will not be underqualified)
            //
            else if (name.indexOf('.') > 0 && (Ext.ClassManager.isCreated(name) || this.hasRegisteredPrefix(name))) {
                absoluteName = name;
                shortName = name.replace(namespace + '.' + kind + '.', '');
            } else {
                if (!namespace) {
                    Ext.log.warn("Cannot find namespace for " + kind + " " + name + ", " + "assuming it is fully qualified class name");
                }
                if (namespace) {
                    absoluteName = namespace + '.' + kind + '.' + (profileName ? profileName + '.' + name : name);
                    shortName = name;
                } else {
                    absoluteName = name;
                }
            }
            return {
                absoluteName: absoluteName,
                shortName: shortName
            };
        },
        hasRegisteredPrefix: function(className) {
            var inventory = Ext.ClassManager,
                prefix = inventory.getPrefix(className);
            // It's a class if className is not equal to any known namespace
            return prefix && prefix !== className;
        }
    },
    // @cmd-auto-dependency {aliasPrefix : "model.", mvc : true, blame: "all"}
    /**
     * @cfg {String/String[]} models
     * Array of models to require from AppName.model namespace. For example:
     *
     *      Ext.define("MyApp.controller.Foo", {
     *          extend: "Ext.app.Controller",
     *          models: ['User', 'Vehicle']
     *      });
     *
     * This is equivalent to:
     *
     *      Ext.define("MyApp.controller.Foo", {
     *          extend: "Ext.app.Controller",
     *          requires: ['MyApp.model.User', 'MyApp.model.Vehicle'],
     *
     *          getUserModel: function() {
     *              return this.getModel("User");
     *          },
     *
     *          getVehicleModel: function() {
     *              return this.getModel("Vehicle");
     *          }
     *      });
     *
     * **Note:** If the model has a different namespace than that of the
     * application you will need to specify the full class name as well as define a path
     * in the {@link Ext.Loader#cfg-paths Loader's paths} config or
     * {@link Ext.Loader#method-setPath setPath} method.
     */
    models: null,
    // @cmd-auto-dependency {aliasPrefix: "view.", mvc: true, blame: "all"}
    /**
     * @cfg {String/String[]} views
     * Array of views to require from AppName.view namespace and to generate getter methods for.
     * For example:
     *
     *      Ext.define("MyApp.controller.Foo", {
     *          extend: "Ext.app.Controller",
     *          views: ['List', 'Detail']
     *      });
     *
     * This is equivalent to:
     *
     *      Ext.define("MyApp.controller.Foo", {
     *          extend: "Ext.app.Controller",
     *          requires: ['MyApp.view.List', 'MyApp.view.Detail'],
     *
     *          getListView: function() {
     *              return this.getView("List");
     *          },
     *
     *          getDetailView: function() {
     *              return this.getView("Detail");
     *          }
     *      });
     *
     * **Note:** If the view has a different namespace than that of the
     * application you will need to specify the full class name as well as define a path
     * in the {@link Ext.Loader#cfg-paths Loader's paths} config or
     * {@link Ext.Loader#method-setPath setPath} method.
     */
    views: null,
    // @cmd-auto-dependency {aliasPrefix: "store.", mvc: true, blame: "all"}
    /**
     * @cfg {String/String[]} stores
     * Array of stores to require from AppName.store namespace and to generate getter methods for.
     * For example:
     *
     *      Ext.define("MyApp.controller.Foo", {
     *          extend: "Ext.app.Controller",
     *          stores: ['Users', 'Vehicles']
     *      });
     *
     * This is equivalent to:
     *
     *      Ext.define("MyApp.controller.Foo", {
     *          extend: "Ext.app.Controller",
     *
     *          requires: [
     *              'MyApp.store.Users',
     *              'MyApp.store.Vehicles'
     *          ]
     *
     *          getUsersStore: function() {
     *              return this.getStore("Users");
     *          },
     *
     *          getVehiclesStore: function() {
     *              return this.getStore("Vehicles");
     *          }
     *      });
     *
     * **Note:** If the store has a different namespace than that of the
     * application you will need to specify the full class name as well as define a path
     * in the {@link Ext.Loader#cfg-paths Loader's paths} config or
     * {@link Ext.Loader#method-setPath setPath} method.
     */
    stores: null,
    // @cmd-auto-dependency {aliasPrefix: "controller.", mvc: true, blame: "all"}
    controllers: null,
    config: {
        /**
         * @cfg {Ext.app.Application} application
         * The {@link Ext.app.Application} for this controller accessible via the
         * getApplication method.
         * @accessor
         * @readonly
         */
        application: null,
        /**
         * @cfg {Object/Object[]} refs
         * @accessor
         *
         * The refs config creates a getter method on the controller that internally
         * uses Ext.ComponentQuery to fetch the component instance using the configured
         * selector.  The following example will add the `getList` method to
         * the controller and will return the first component in the application
         * hierarchy with an xtype of "grid".  By default, *undefined* will be returned
         * when the query does not locate the target component.
         *
         *     Ext.define('MyApp.controller.Foo', {
         *         extend: 'Ext.app.Controller',
         *
         *         refs: [{
         *             ref: 'list',
         *             selector: 'grid'
         *         }]
         *     });
         *
         * The following fields may be used in the ref definition:
         *
         * - `ref` - name of the reference.
         * - `selector` - Ext.ComponentQuery selector to access the component.
         * - `autoCreate` - True to create the component automatically if not found on
         * page.
         * - `forceCreate` - True to force the creation of the component every time
         * reference is accessed (when `get<REFNAME>` is called).
         * - `xtype` - Used to create the component by its xtype with `autoCreate` or
         * `forceCreate`. If you don't provide `xtype`, an Ext.Component instance will
         * be created.
         *
         * The following example will create a `getList` and `getUser` method on the
         * controller.
         *
         *     Ext.define('MyApp.controller.Foo', {
         *         extend: 'Ext.app.Controller',
         *
         *         refs: [{
         *             list: 'grid',
         *             user: {
         *                 autoCreate: true,
         *                 selector: 'form',
         *                 xtype: 'form'
         *             }
         *         }]
         *     });
         */
        refs: null,
        active: true,
        /**
         * @private
         */
        moduleClassName: null
    },
    onClassExtended: function(cls, data, hooks) {
        var onBeforeClassCreated = hooks.onBeforeCreated;
        hooks.onBeforeCreated = function(cls, data) {
            var Controller = Ext.app.Controller,
                requires = [],
                namespace, proto;
            proto = cls.prototype;
            namespace = Controller.resolveNamespace(cls, data);
            if (namespace) {
                proto.$namespace = namespace;
            }
            Controller.processDependencies(proto, requires, namespace, 'model', data.models);
            Controller.processDependencies(proto, requires, namespace, 'view', data.views);
            Controller.processDependencies(proto, requires, namespace, 'store', data.stores);
            Controller.processDependencies(proto, requires, namespace, 'controller', data.controllers);
            Ext.require(requires, Ext.Function.pass(onBeforeClassCreated, arguments, this));
        };
    },
    /**
     * Creates new Controller.
     *
     * @param {Object} [config] Configuration object.
     */
    constructor: function(config) {
        this.initAutoGetters();
        this.callParent([
            config
        ]);
    },
    /**
     * @private
     * Takes either an object and transforms it into an array. The following are valid
     * refs values:
     *
     *     refs: {
     *         myComponent: 'container'
     *     }
     *
     *     refs: {
     *         myComponent: {
     *             selector: 'container'
     *         }
     *     }
     *
     *     refs: [
     *         {
     *             ref: 'myComponent',
     *             selector: 'container'
     *         }
     *     ]
     *
     * @param {Array|Object} refs The refs to normalize
     * @return {Array} The normalized array of refs
     */
    normalizeRefs: function(refs) {
        var me = this,
            newRefs = [];
        if (refs) {
            if (Ext.isObject(refs)) {
                Ext.Object.each(refs, function(key, value) {
                    if (Ext.isString(value)) {
                        value = {
                            selector: value
                        };
                    }
                    value.ref = key;
                    newRefs.push(value);
                });
            } else if (Ext.isArray(refs)) {
                newRefs = Ext.Array.merge(newRefs, refs);
            }
        }
        refs = me.refs;
        if (refs) {
            me.refs = null;
            refs = me.normalizeRefs(refs);
            if (refs) {
                newRefs = Ext.Array.merge(newRefs, refs);
            }
        }
        return newRefs;
    },
    /**
     * Returns a map of reference names to selectors
     * @private
     */
    getRefMap: function() {
        var me = this,
            refMap = me._refMap,
            refs, ref, ln, i;
        if (!refMap) {
            refs = me.getRefs();
            refMap = me._refMap = {};
            if (refs) {
                for (i = 0 , ln = refs.length; i < ln; i++) {
                    ref = refs[i];
                    refMap[ref.ref] = ref.selector;
                }
            }
        }
        return refMap;
    },
    applyId: function(id) {
        return id || Ext.app.Controller.getFullName(this.$className, 'controller', this.$namespace).shortName;
    },
    applyRefs: function(refs) {
        return this.normalizeRefs(Ext.clone(refs));
    },
    /**
     * @param {Object} refs The refs to pass to the {@link #ref} method.
     * @private
     */
    updateRefs: function(refs) {
        if (refs) {
            this.ref(refs);
        }
    },
    initAutoGetters: function() {
        var proto = this.self.prototype,
            prop, fn;
        for (prop in proto) {
            fn = proto[prop];
            // Look for the marker placed on the getters by processDependencies so that
            // we can know what to call cheaply:
            if (fn && fn['Ext.app.getter']) {
                fn.call(this);
            }
        }
    },
    doInit: function(app) {
        var me = this;
        if (!me._initialized) {
            me.init(app);
            me._initialized = true;
        }
    },
    finishInit: function(app) {
        var me = this,
            controllers = me.controllers,
            controller, i, l;
        if (me._initialized && controllers && controllers.length) {
            for (i = 0 , l = controllers.length; i < l; i++) {
                controller = me.getController(controllers[i]);
                controller.finishInit(app);
            }
        }
    },
    /**
     * @method
     *
     * A template method that is called when your application boots. It is called before the
     * {@link Ext.app.Application Application}'s launch function is executed so gives a hook point
     * to run any code before your Viewport is created.
     *
     * @param {Ext.app.Application} application
     *
     * @template
     */
    init: Ext.emptyFn,
    /**
     * @method
     *
     * A template method like {@link #init}, but called after the viewport is created.
     * This is called after the {@link Ext.app.Application#launch launch} method of Application
     * is executed.
     *
     * @param {Ext.app.Application} application
     *
     * @template
     */
    onLaunch: Ext.emptyFn,
    /**
     * Allow the controller to resume receiving events from the event bus.
     * Routes will also be able to begin firing on this controller.
     * Also see {@link #deactivate}.
     */
    activate: function() {
        this.setActive(true);
    },
    /**
     * Prevent this controller from receiving events from the event bus.
     * Routes will also not be triggered on inactive controllers unless
     * the {@link Ext.route.Route#allowInactive} flag is set.
     * Also see {@link #activate}.
     */
    deactivate: function() {
        this.setActive(false);
    },
    /**
     * Checks if this controller is active. See {@link #activate} &
     * {@link #deactivate}.
     * @return {Boolean} `true` if this controller is active.
     */
    isActive: function() {
        return this.getActive();
    },
    ref: function(refs) {
        var me = this,
            i = 0,
            length = refs.length,
            info, ref, fn;
        refs = Ext.Array.from(refs);
        me.references = me.references || [];
        for (; i < length; i++) {
            info = refs[i];
            ref = info.ref;
            fn = 'get' + Ext.String.capitalize(ref);
            if (!me[fn]) {
                me[fn] = Ext.Function.pass(me.getRef, [
                    ref,
                    info
                ], me);
            }
            me.references.push(ref.toLowerCase());
        }
    },
    /**
     * Registers one or more {@link #refs references}.
     *
     * @param {Object/Object[]} refs
     */
    addRef: function(refs) {
        this.ref(refs);
    },
    getRef: function(ref, info, config) {
        var me = this,
            refCache = me.refCache || (me.refCache = {}),
            cached = refCache[ref];
        info = info || {};
        config = config || {};
        Ext.apply(info, config);
        if (info.forceCreate) {
            return Ext.ComponentManager.create(info, 'component');
        }
        if (!cached) {
            if (info.selector) {
                refCache[ref] = cached = Ext.ComponentQuery.query(info.selector)[0];
            }
            if (!cached && info.autoCreate) {
                refCache[ref] = cached = Ext.ComponentManager.create(info, 'component');
            }
            if (cached) {
                cached.on('destroy', function() {
                    refCache[ref] = null;
                });
            }
        }
        return cached;
    },
    /**
     * Returns `true` if a {@link #refs reference} is registered.
     *
     * @param {String} ref The name of the ref to check for.
     * @return {Boolean}
     */
    hasRef: function(ref) {
        var references = this.references;
        return references && Ext.Array.indexOf(references, ref.toLowerCase()) !== -1;
    },
    /**
     * Returns instance of a {@link Ext.app.Controller Controller} with the given id.
     * When controller doesn't exist yet, it's created. Note that this method depends
     * on Application instance and will return undefined when Application is not
     * accessible. The only exception is when this Controller instance's id is requested;
     * in that case we always return the instance even if Application is no available.
     *
     * @param {String} id
     *
     * @return {Ext.app.Controller} controller instance or undefined.
     */
    getController: function(id) {
        var app = this.getApplication();
        if (id === this.getId()) {
            return this;
        }
        return app && app.getController(id);
    },
    /**
     * Returns instance of a {@link Ext.data.Store Store} with the given name.
     * When store doesn't exist yet, it's created.
     *
     * @param {String} name
     *
     * @return {Ext.data.Store} a store instance.
     */
    getStore: function(name) {
        var storeId, store;
        storeId = (name.indexOf('@') === -1) ? name : name.split('@')[0];
        store = Ext.StoreManager.get(storeId);
        if (!store) {
            name = Ext.app.Controller.getFullName(name, 'store', this.$namespace);
            if (name) {
                store = Ext.create(name.absoluteName, {
                    // Use id here. If the store has a configured storeId,
                    // that will take precedence
                    id: storeId
                });
            }
        }
        return store;
    },
    /**
     * Returns a {@link Ext.data.Model Model} class with the given name.
     *
     * @param {String} modelName
     * @return {Ext.Class} A class ultimately derived from `Ext.data.Model`.
     */
    getModel: function(modelName) {
        var name = Ext.app.Controller.getFullName(modelName, 'model', this.$namespace),
            ret = Ext.ClassManager.get(name.absoluteName);
        if (!ret) {
            ret = Ext.data.schema.Schema.lookupEntity(modelName);
        }
        return ret;
    },
    /**
     * Returns instance of a {@link Ext.app.Profile Profile} with the given name.
     *
     * @param {String} name
     *
     * @return {String} a profile instance.
     */
    getProfile: function(name) {
        name = Ext.app.Controller.getFullName(name, 'profile', this.$namespace);
        return name;
    },
    /**
     * Returns a View class with the given name.  To create an instance of the view,
     * you can use it like it's used by Application to create the Viewport:
     *
     *     this.getView('Viewport').create();
     *
     * @param {String} view
     *
     * @return {Ext.Base} a view class.
     */
    getView: function(view) {
        var name = Ext.app.Controller.getFullName(view, 'view', this.$namespace);
        return name && Ext.ClassManager.get(name.absoluteName);
    },
    /**
     * @method destroy
     * @inheritdoc
     * @param destroyRefs (private)
     * @param fromApp (private)
     */
    destroy: function(destroyRefs, fromApp) {
        var me = this,
            app = me.application,
            refCache, ref;
        if (!fromApp && app) {
            app.unregister(me);
        }
        me.application = null;
        if (destroyRefs) {
            // Possible destroy stores here too?
            refCache = me.refCache;
            for (ref in refCache) {
                if (refCache.hasOwnProperty(ref)) {
                    Ext.destroy(refCache[ref]);
                }
            }
        }
        me.callParent();
    }
});

/**
 * Represents an Ext JS application, which is typically a single page app using a
 * {@link Ext.container.Viewport Viewport}.
 *
 * An application consists of one or more Views. The behavior of a View is managed by its
 * corresponding {@link Ext.app.ViewController ViewController} and {@link Ext.app.ViewModel
 * ViewModel}.
 *
 * Global activities are coordinated by {@link Ext.app.Controller Controllers} which are
 * ultimately instantiated by an instance of this (or a derived) class.
 *
 *     Ext.application({
 *         name: 'MyApp',
 *
 *         // The name of the initial view to create. With the classic toolkit this class
 *         // will gain a "viewport" plugin if it does not extend Ext.Viewport. With the
 *         // modern toolkit, the main view will be added to the Viewport.
 *         //
 *         //mainView: 'Main.view.main.Main'
 *     });
 *
 * This does several things. First it creates a global variable called 'MyApp' - all of
 * your Application's classes (such as its Models, Views and Controllers) will reside under
 * this single namespace, which drastically lowers the chances of colliding global variables.
 *
 * The MyApp global will also have a getApplication method to get a reference to the current
 * application:
 *
 *     var app = MyApp.getApplication();
 *
 * # Telling Application about the rest of the app
 *
 * Because an Ext.app.Application represents an entire app, we should tell it about the other
 * parts of the app - namely the Models, Views and Controllers that are bundled with the
 * application. Let's say we have a blog management app; we might have Models and Controllers for
 * Posts and Comments, and Views for listing, adding and editing Posts and Comments. Here's how
 * we'd tell our Application about all these things:
 *
 *     Ext.application({
 *         name: 'Blog',
 *
 *         models: ['Post', 'Comment'],
 *
 *         controllers: ['Posts', 'Comments'],
 *
 *         launch: function() {
 *             ...
 *         }
 *     });
 *
 * Note that we didn't actually list the Views directly in the Application itself. This is because
 * Views are managed by Controllers, so it makes sense to keep those dependencies there.
 * The Application will load each of the specified Controllers using the pathing conventions
 * laid out in the [application architecture guide][1]
 * - in this case expecting the controllers to reside in app/controller/Posts.js and
 * app/controller/Comments.js. In turn, each Controller simply needs to list the Views it uses
 * and they will be automatically loaded. Here's how our Posts controller like be defined:
 *
 *     Ext.define('MyApp.controller.Posts', {
 *         extend: 'Ext.app.Controller',
 *         views: ['posts.List', 'posts.Edit'],
 *
 *         // the rest of the Controller here
 *     });
 *
 * Because we told our Application about our Models and Controllers, and our Controllers about
 * their Views, Ext JS will automatically load all of our app files for us. This means we don't
 * have to manually add script tags into our html files whenever we add a new class, but more
 * importantly it enables us to create a minimized build of our entire application using Sencha Cmd.
 *
 * # Deriving from Ext.app.Application
 *
 * Typically, applications do not derive directly from Ext.app.Application. Rather, the
 * configuration passed to `Ext.application` mimics what you might do in a derived class.
 * In some cases, however, it can be desirable to share logic by using a derived class
 * from `Ext.app.Application`.
 *
 * Derivation works as you would expect, but using the derived class should still be the
 * job of the `Ext.application` method.
 *
 *     Ext.define('MyApp.Application', {
 *         extend: 'Ext.app.Application',
 *         name: 'MyApp',
 *         ...
 *     });
 *
 *     Ext.application('MyApp.Application');
 *
 * For more information about writing Ext JS applications, please see
 * the [application architecture guide][1].
 * [1]: ../guides/application_architecture/application_architecture.html
 */
Ext.define('Ext.app.Application', {
    extend: 'Ext.app.Controller',
    requires: [
        'Ext.util.History',
        'Ext.util.MixedCollection'
    ],
    isApplication: true,
    /**
     * @cfg {String} extend A class name to use with the `Ext.application` call. The class must
     * also extend {@link Ext.app.Application}.
     *
     *     Ext.define('MyApp.Application', {
     *         extend: 'Ext.app.Application',
     *
     *         launch: function() {
     *             Ext.direct.Manager.addProvider(Ext.REMOTING_API);
     *         }
     *     });
     *
     *     Ext.application({
     *         extend: 'MyApp.Application'
     *     });
     */
    /**
     * @cfg {String/String[]} controllers
     * Names of {@link Ext.app.Controller controllers} that the app uses.  By default,
     * the framework will look for the controllers in the "controller" folder within the
     * {@link #appFolder}.  Controller classes should be named using the syntax of
     * "{appName}.controller.{ClassName}" with additional sub-folders under the
     * "controller" folder specified within the class name following "controller.".
     *
     *     // by default, the following controller class would be located at:
     *     // app/controller/Main.js
     *     controllers: '.Main' // or 'MyApp.controller.Main'
     *
     *     // while the following would be located at:
     *     // app/controller/customer/Main.js
     *     controllers: 'customer.Main' // or 'MyApp.controller.customer.Main'
     *
     * **Note:** If the controller has a different namespace than that of the
     * application you will need to specify the full class name as well as define a path
     * in the {@link Ext.Loader#cfg-paths Loader's paths} config or
     * {@link Ext.Loader#method-setPath setPath} method.
     */
    /**
     * @cfg {Object} scope
     * The scope to execute the {@link #launch} function in. Defaults to the Application instance.
     */
    scope: undefined,
    /**
     * @cfg {String/String[]} [namespaces]
     *
     * The list of namespace prefixes used in the application to resolve dependencies
     * like Views and Stores:
     *
     *      Ext.application({
     *          name: 'MyApp',
     *
     *          namespaces: ['Common.code'],
     *
     *          controllers: [ 'Common.code.controller.Foo', 'Bar' ]
     *      });
     *
     *      Ext.define('Common.code.controller.Foo', {
     *          extend: 'Ext.app.Controller',
     *
     *          models: ['Foo'],    // Loads Common.code.model.Foo
     *          views:  ['Bar']     // Loads Common.code.view.Bar
     *      });
     *
     *      Ext.define('MyApp.controller.Bar', {
     *          extend: 'Ext.app.Controller',
     *
     *          models: ['Foo'],    // Loads MyApp.model.Foo
     *          views:  ['Bar']     // Loads MyApp.view.Bar
     *      });
     *
     * You don't need to include main namespace (MyApp), it will be added to the list
     * automatically.
     */
    namespaces: [],
    /**
     * @cfg {Object} paths
     * Additional load paths to add to Ext.Loader.
     * See {@link Ext.Loader#paths} config for more details.
     */
    paths: null,
    /**
     * @cfg {String} [appFolder="app"]
     * The path to the directory which contains all application's classes.
     * This path will be registered via {@link Ext.Loader#setPath} for the namespace specified
     * in the {@link #name name} config.
     */
    // NOTE - this config has to be processed by Ext.application
    config: {
        /**
         * @cfg {String} name
         * The name of your application. This will also be the namespace for your views, controllers
         * models and stores. Don't use spaces or special characters in the name. **Application name
         * is mandatory**.
         */
        name: '',
        /**
         * @cfg {String} appProperty
         * The name of a property to be assigned to the main namespace to gain a reference to
         * this application. Can be set to an empty value to prevent the reference from
         * being created
         *
         *     Ext.application({
         *         name: 'MyApp',
         *         appProperty: 'myProp',
         *
         *         launch: function() {
         *             console.log(MyApp.myProp === this);
         *         }
         *     });
         */
        appProperty: 'app',
        // @cmd-auto-dependency { aliasPrefix: "profile.", mvc: true, blame: "all" }
        /**
         * @cfg {String/String[]} profiles
         * Names of the profiles that the app uses.
         */
        profiles: [],
        /**
        * @cfg {Ext.app.Profile}
        */
        currentProfile: null,
        // @cmd-auto-dependency {aliasPrefix: "view.", mvc: true, blame: "all"}
        /**
         * @cfg {String/Object/Ext.Component} mainView
         * The application class to be used as the main viewport view for the
         * application.  The view will be configured with the
         * {@link Ext.plugin.Viewport viewport plugin} to ensure the view takes up all
         * available space in the browser viewport.  The main view will be created after
         * the application's {@link #init} method is called and before the
         * {@link #launch} method.  The main view should be an application class type and
         * not a class from the framework.
         *
         * The main view value may be:
         *  - string representing the full class name of the main view or the partial class name
         * following "AppName.view." (provided your main view class follows that convention).
         *  - config object for the main view
         *  - main view class instance
         *
         *     Ext.define('MyApp.view.main.Main', {
         *         extend: 'Ext.panel.Panel',
         *         xtype: 'mainview',
         *         title: 'Main Viewport View'
         *     });
         *
         *     Ext.application({
         *         name : 'MyApp',
         *
         *         mainView: 'MyApp.view.main.Main'
         *         // mainView: 'main.Main'
         *         // mainView: new MyApp.view.main.Main()
         *         // mainView: { xtype: 'mainview' }
         *     });
         *
         * **Note:** You may also call {@link #method-setMainView} at runtime if you require
         * logic within the application's {@link #launch} method to be processed prior to
         * the creation of the main view.
         */
        mainView: {
            $value: null,
            lazy: true
        },
        /**
         * @cfg {String} [defaultToken=null] The default token to be used at application launch
         * if one is not present. Often this is set to something like `'home'`.
         */
        defaultToken: null,
        /**
         * @cfg {String} glyphFontFamily
         * The glyphFontFamily to use for this application.  Used as the default font-family
         * for all components that support a `glyph` config.
         */
        glyphFontFamily: null,
        // Docs will go in subclasses
        quickTips: true,
        /**
         * @cfg {Object} router
         * A configuration object to apply onto the {@link Ext.route.Router Router}.
         * @since 6.5.0
         */
        router: null
    },
    onClassExtended: function(cls, data, hooks) {
        var Controller = Ext.app.Controller,
            proto = cls.prototype,
            requires = [],
            onBeforeClassCreated, namespace;
        // Ordinary inheritance does not work here so we collect
        // necessary data from current class data and its superclass
        namespace = data.name || cls.superclass.name;
        if (namespace) {
            data.$namespace = namespace;
            Ext.app.addNamespaces(namespace);
        }
        if (data.namespaces) {
            Ext.app.addNamespaces(data.namespaces);
        }
        if (data['paths processed']) {
            delete data['paths processed'];
        } else {
            Ext.app.setupPaths(namespace, ('appFolder' in data) ? data.appFolder : cls.superclass.appFolder, data.paths);
        }
        // Require all profiles
        Controller.processDependencies(proto, requires, namespace, 'profile', data.profiles);
        // This hook is used in the classic toolkit to process other configs that need to
        // require classes (like tooltips and viewport plugin).
        proto.getDependencies(cls, data, requires);
        // Any "requires" also have to be processed before we fire up the App instance.
        if (requires.length) {
            onBeforeClassCreated = hooks.onBeforeCreated;
            hooks.onBeforeCreated = function(cls, data) {
                var args = Ext.Array.clone(arguments);
                // This hook is to allow unit tests to come in and control the
                // requires so we don't have to get into the internals of the Loader.
                // Not intended to be used for any other purpose.
                if (data.__handleRequires) {
                    data.__handleRequires.call(this, requires, Ext.bind(function() {
                        return onBeforeClassCreated.apply(this, args);
                    }, this));
                    return;
                }
                Ext.require(requires, function() {
                    return onBeforeClassCreated.apply(this, args);
                });
            };
        }
    },
    getDependencies: Ext.emptyFn,
    /**
     * Creates new Application.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        var me = this;
        Ext.route.Router.application = me;
        me.callParent([
            config
        ]);
        if (Ext.isEmpty(me.getName())) {
            Ext.raise("[Ext.app.Application] Name property is required");
        }
        me.doInit(me);
        Ext.on('appupdate', me.onAppUpdate, me, {
            single: true
        });
        Ext.Loader.setConfig({
            enabled: true
        });
        this.onProfilesReady();
    },
    applyId: function(id) {
        return id || this.$className;
    },
    updateRouter: function(cfg) {
        if (cfg) {
            Ext.route.Router.setConfig(cfg);
        }
    },
    /**
     * @method
     * @template
     * Called automatically when an update to either the Application Cache or the Local Storage
     * Cache is detected.
     * This is mainly used during production builds.
     * @param {Object} [updateInfo] updateInfo Update information object contains properties for
     * checking which cache triggered the update
     */
    onAppUpdate: Ext.emptyFn,
    onProfilesReady: function() {
        var me = this,
            profiles = me.getProfiles(),
            length = profiles.length,
            current, i, instance;
        for (i = 0; i < length; i++) {
            instance = Ext.create(profiles[i], {
                application: me
            });
            if (instance.isActive() && !current) {
                current = instance;
                me.setCurrentProfile(current);
            }
        }
        if (current) {
            current.init();
        }
        me.initControllers();
        me.onBeforeLaunch();
        me.finishInitControllers();
    },
    doInit: function(app) {
        this.initNamespace(app);
        this.callParent([
            app
        ]);
    },
    initNamespace: function(me) {
        var appProperty = me.getAppProperty(),
            ns = Ext.namespace(me.getName());
        if (ns) {
            ns.getApplication = function() {
                return me;
            };
            if (appProperty) {
                if (!ns[appProperty]) {
                    ns[appProperty] = me;
                } else if (ns[appProperty] !== me) {
                    Ext.log.warn('An existing reference is being overwritten for ' + name + '.' + appProperty + '. See the appProperty config.');
                }
            }
        }
    },
    initControllers: function() {
        var me = this,
            controllers = Ext.Array.from(me.controllers),
            profile = me.getCurrentProfile(),
            i, ln;
        me.controllers = new Ext.util.MixedCollection();
        for (i = 0 , ln = controllers.length; i < ln; i++) {
            me.getController(controllers[i]);
        }
        // Also launch controllers for the active profile (if we have one)
        //
        if (profile) {
            controllers = profile.getControllers();
            for (i = 0 , ln = controllers.length; i < ln; i++) {
                me.getController(controllers[i]);
            }
        }
    },
    finishInitControllers: function() {
        var me = this,
            controllers, i, l;
        controllers = me.controllers.getRange();
        for (i = 0 , l = controllers.length; i < l; i++) {
            controllers[i].finishInit(me);
        }
    },
    /**
     * @method
     * @template
     * Called automatically when the page has completely loaded. This is an empty function that
     * should be overridden by each application that needs to take action on page load.
     * @param {String} profile The detected application profile
     * @return {Boolean} By default, the Application will dispatch to the configured startup
     * controller and action immediately after running the launch function. Return false
     * to prevent this behavior.
     */
    launch: Ext.emptyFn,
    /**
     * @private
     */
    onBeforeLaunch: function() {
        var me = this,
            History = Ext.util.History,
            defaultToken = me.getDefaultToken(),
            currentProfile = me.getCurrentProfile(),
            controllers, c, cLen, controller, token;
        me.initMainView();
        if (currentProfile) {
            currentProfile.launch();
        }
        me.launch.call(me.scope || me);
        me.launched = true;
        me.fireEvent('launch', me);
        controllers = me.controllers.items;
        cLen = controllers.length;
        for (c = 0; c < cLen; c++) {
            controller = controllers[c];
            controller.onLaunch(me);
        }
        if (!History.ready) {
            History.init();
        }
        token = History.getToken();
        if (token || token === defaultToken) {
            Ext.route.Router.onStateChange(token);
        } else if (defaultToken) {
            History.replace(defaultToken);
        }
        // Microloader has detected an Application Cache or LocalStorage Cache update, inform
        // everyone that may have added listeners late.
        if (Ext.Microloader && Ext.Microloader.appUpdate && Ext.Microloader.appUpdate.updated) {
            Ext.Microloader.fireAppUpdate();
        }
        // After launch we may as well cleanup the namespace cache
        if (!me.cnsTimer) {
            me.cnsTimer = Ext.defer(Ext.ClassManager.clearNamespaceCache, 2000, Ext.ClassManager);
        }
    },
    getModuleClassName: function(name, kind) {
        return Ext.app.Controller.getFullName(name, kind, this.getName()).absoluteName;
    },
    initMainView: function() {
        var me = this,
            currentProfile = me.getCurrentProfile(),
            mainView;
        if (currentProfile) {
            mainView = currentProfile.getMainView();
        }
        if (mainView) {
            me.setMainView(mainView);
        } else {
            // since mainView is a lazy config we have to call the getter to initialize it
            me.getMainView();
        }
    },
    applyMainView: function(value) {
        var view = this.getView(value);
        // Ensure the full component stack is available immediately.
        return view.create({
            $initParent: this.viewport
        });
    },
    /**
     * Create an instance of a controller by name.
     * @param {String} name The name of the controller. For a controller with the
     * full class name `MyApp.controller.Foo`, the name parameter should be `Foo`.
     * If the controller already exists, it will be returned.
     *
     * @return {Ext.app.Controller} controller
     */
    createController: function(name) {
        return this.getController(name);
    },
    /**
     * Destroys a controller, any listeners are unbound.
     * @param {String/Ext.app.Controller} controller The controller
     */
    destroyController: function(controller) {
        if (typeof controller === 'string') {
            controller = this.getController(controller, true);
        }
        Ext.destroy(controller);
    },
    /**
     * Get an application's controller based on name or id.  Generally, the controller id will be
     * the same as the name unless otherwise specified.
     * @param {String} name The name or id of the controller you are trying to retrieve
     * @param {Boolean} [preventCreate] (private)
     */
    getController: function(name, preventCreate) {
        var me = this,
            controllers = me.controllers,
            className, controller, len, i, c, all;
        // First check with the passed value if we have an explicit id
        controller = controllers.get(name);
        // In a majority of cases, the controller id will be the same as the name.
        // However, when a controller is manually given an id, it will be keyed
        // in the collection that way. So if we don't find it, we attempt to loop
        // over the existing controllers and find it by classname
        if (!controller) {
            all = controllers.items;
            for (i = 0 , len = all.length; i < len; ++i) {
                c = all[i];
                className = c.getModuleClassName();
                if (className && className === name) {
                    controller = c;
                    break;
                }
            }
        }
        if (!controller && !preventCreate) {
            className = me.getModuleClassName(name, 'controller');
            controller = Ext.create(className, {
                application: me,
                moduleClassName: className
            });
            controllers.add(controller);
            if (me._initialized) {
                controller.doInit(me);
            }
        }
        return controller;
    },
    /**
     * Unregister a controller from the application.
     * @private
     * @param {Ext.app.Controller} controller The controller to unregister
     */
    unregister: function(controller) {
        this.controllers.remove(controller);
    },
    getApplication: function() {
        return this;
    },
    destroy: function(destroyRefs) {
        var me = this,
            controllers = me.controllers,
            ns = Ext.namespace(me.getName()),
            appProp = me.getAppProperty();
        Ext.undefer(me.cnsTimer);
        Ext.un('appupdate', me.onAppUpdate, me);
        Ext.destroy(me.viewport);
        if (controllers) {
            controllers.each(function(controller) {
                controller.destroy(destroyRefs, true);
            });
        }
        me.controllers = null;
        me.callParent([
            destroyRefs,
            true
        ]);
        // Clean up any app reference
        if (ns && ns[appProp] === me) {
            delete ns[appProp];
        }
        if (Ext.route.Router.application === me) {
            Ext.route.Router.application = null;
        }
        if (Ext.app.Application.instance === me) {
            Ext.app.Application.instance = null;
        }
    },
    updateGlyphFontFamily: function(fontFamily) {
        Ext.setGlyphFontFamily(fontFamily);
    },
    /**
     * As a convenience developers can locally qualify profile names (e.g. 'MyProfile' vs
     * 'MyApp.profile.MyProfile'). This just makes sure everything ends up fully qualified.
     * @private
     */
    applyProfiles: function(profiles) {
        var me = this;
        return Ext.Array.map(profiles, function(profile) {
            return me.getModuleClassName(profile, "profile");
        });
    }
}, function() {
    /**
     * @member Ext
     * @method getApplication
     * @return {Ext.app.Application}
     */
    Ext.getApplication = function() {
        return Ext.app.Application.instance;
    };
});

// This is an override because it must be loaded very early, possibly before Ext.app.Application
// in dev mode so that Ext.application() can be called.
// Being an override also ensures that it is only included in a built app if Ext.app.Application
// is present.
//
// @override Ext.app.Application
/**
 * @method application
 * @member Ext
 * Loads Ext.app.Application class and starts it up with given configuration after the
 * page is ready.
 *
 * See `Ext.app.Application` for details.
 *
 * @param {Object/String} config Application config object or name of a class derived
 * from Ext.app.Application.
 */
Ext.application = function(config) {
    var createApp = function(App) {
            // This won't be called until App class has been created.
            Ext.onReady(function() {
                var Viewport = Ext.viewport;
                // eslint-disable-next-line dot-notation
                Viewport = Viewport && Viewport['Viewport'];
                if (Viewport && Viewport.setup) {
                    Viewport.setup(App.prototype.config.viewport);
                }
                Ext.app.Application.instance = new App();
            });
        };
    if (typeof config === "string") {
        Ext.require(config, function() {
            createApp(Ext.ClassManager.get(config));
        });
    } else {
        config = Ext.apply({
            extend: 'Ext.app.Application'
        }, // can be replaced by config!
        config);
        // We have to process "paths" before creating Application class,
        // or `requires` won't work.
        Ext.app.setupPaths(config.name, config.appFolder, config.paths);
        config['paths processed'] = true;
        // Let Ext.define do the hard work but don't assign a class name.
        Ext.define(config.name + ".$application", config, function() {
            createApp(this);
        });
    }
};

/**
 * @class Ext.app.Application
 */
Ext.define('Ext.overrides.app.Application', {
    override: 'Ext.app.Application',
    uses: [
        'Ext.tip.QuickTipManager'
    ],
    // @cmd-auto-dependency {aliasPrefix: "view.", mvc: true, requires: ["Ext.plugin.Viewport"]}
    /**
     * @cfg {Boolean/String} [autoCreateViewport=false]
     * @deprecated 5.1 Instead use {@link #mainView}
     * @member Ext.app.Application
     */
    autoCreateViewport: false,
    config: {
        /**
         * @cfg {Boolean} enableQuickTips
         * @deprecated 6.2.0 Use {@link #quickTips}.
         */
        enableQuickTips: null
    },
    /**
     * @cfg {Boolean} quickTips
     * True to automatically set up Ext.tip.QuickTip support.
     *
     * @since 6.2.0
     */
    quickTips: true,
    updateEnableQuickTips: function(enableQuickTips) {
        this.setQuickTips(enableQuickTips);
    },
    applyMainView: function(mainView) {
        var view, proto, config, protoPlugins, configPlugins;
        if (typeof mainView === 'string') {
            view = this.getView(mainView);
            config = {};
        } else {
            config = mainView;
            view = Ext.ClassManager.getByConfig(mainView);
        }
        proto = view.prototype;
        if (!proto.isViewport) {
            // Need to copy over any plugins defined on the prototype and on the config.
            protoPlugins = Ext.Array.from(proto.plugins);
            configPlugins = Ext.Array.from(config.plugins);
            config = Ext.apply({}, config);
            config.plugins = [
                'viewport'
            ].concat(protoPlugins, configPlugins);
        }
        return view.create(config);
    },
    getDependencies: function(cls, data, requires) {
        var Controller = Ext.app.Controller,
            proto = cls.prototype,
            namespace = data.$namespace,
            viewportClass = data.autoCreateViewport;
        if (viewportClass) {
            if (!namespace) {
                Ext.raise("[Ext.app.Application] Can't resolve namespace for " + data.$className + ", did you forget to specify 'name' property?");
            }
            if (viewportClass === true) {
                viewportClass = 'Viewport';
            } else {
                requires.push('Ext.plugin.Viewport');
            }
            Controller.processDependencies(proto, requires, namespace, 'view', viewportClass);
        }
    },
    onBeforeLaunch: function() {
        var me = this,
            autoCreateViewport = me.autoCreateViewport;
        if (me.getQuickTips()) {
            me.initQuickTips();
        }
        if (autoCreateViewport) {
            me.initViewport();
        }
        this.callParent(arguments);
    },
    getViewportName: function() {
        var name = null,
            autoCreate = this.autoCreateViewport;
        if (autoCreate) {
            name = (autoCreate === true) ? 'Viewport' : autoCreate;
        }
        return name;
    },
    initViewport: function() {
        this.setMainView(this.getViewportName());
    },
    initQuickTips: function() {
        Ext.tip.QuickTipManager.init();
    }
});

/**
 * A Profile represents a range of devices that fall under a common category. For the vast majority
 * of apps that use device profiles, the app defines a Phone profile and a Tablet profile. Doing
 * this enables you to easily customize the experience for the different sized screens offered by
 * those device types.
 *
 * Only one Profile can be active at a time, and each Profile defines a simple {@link #isActive}
 * function that should return either true or false. The first Profile to return true from its
 * isActive function is set as your Application's
 * {@link Ext.app.Application#currentProfile current profile}.
 *
 * A Profile can define any number of {@link #models}, {@link #views}, {@link #controllers} and
 * {@link #stores} which will be loaded if the Profile is activated. It can also define
 * a {@link #launch} function that will be called after all of its dependencies have been loaded,
 * just before the {@link Ext.app.Application#launch application launch} function is called.
 *
 * ## Sample Usage
 *
 * First you need to tell your Application about your Profile(s):
 *
 *     Ext.application({
 *         name: 'MyApp',
 *         profiles: ['Phone', 'Tablet']
 *     });
 *
 * This will load app/profile/Phone.js and app/profile/Tablet.js. Here's how we might define the
 * Phone profile:
 *
 *     Ext.define('MyApp.profile.Phone', {
 *         extend: 'Ext.app.Profile',
 *
 *         views: ['Main'],
 *
 *         isActive: function() {
 *             return Ext.os.is('Phone');
 *         }
 *     });
 *
 * The isActive function returns true if we detect that we are running on a phone device. If that
 * is the case the Application will set this Profile active and load the 'Main' view specified
 * in the Profile's {@link #views} config.
 *
 * ## Class Specializations
 *
 * Because Profiles are specializations of an application, all of the models, views, controllers
 * and stores defined in a Profile are expected to be namespaced under the name of the Profile.
 * Here's an expanded form of the example above:
 *
 *     Ext.define('MyApp.profile.Phone', {
 *         extend: 'Ext.app.Profile',
 *
 *         views: ['Main'],
 *         controllers: ['Signup'],
 *         models: ['MyApp.model.Group'],
 *
 *         isActive: function() {
 *             return Ext.os.is('Phone');
 *         }
 *     });
 *
 * In this case, the Profile is going to load *app/view/phone/Main.js*,
 * *app/controller/phone/Signup.js* and *app/model/Group.js*. Notice that in each of the first
 * two cases the name of the profile ('phone' in this case) was injected into the class names.
 * In the third case we specified the full Model name (for Group) so the Profile name was not
 * injected.
 *
 * For a fuller understanding of the ideas behind Profiles and how best to use them in your app,
 * we suggest you read the [device profiles guide](/touch/2.4/core_concepts/device_profiles.html).
 * 
 */
Ext.define('Ext.app.Profile', {
    mixins: [
        'Ext.mixin.Observable'
    ],
    requires: [
        'Ext.app.Controller'
    ],
    /**
     * @property {Boolean}
     * `true` to identify an object as an instance of `Ext.app.Profile`
     */
    isProfile: true,
    /**
     * @cfg {String} [namespace]
     * The namespace that this Profile's classes can be found in. Defaults to the lowercase
     * Profile {@link #name}, for example a Profile called MyApp.profile.Phone will by default
     * have a 'phone' namespace, which means that this Profile's additional models, stores, views
     * and controllers will be loaded from the MyApp.model.phone.*, MyApp.store.phone.*,
     * MyApp.view.phone.* and MyApp.controller.phone.* namespaces respectively.
     * @accessor
     */
    /**
     * @cfg {String} [name]
     * The name of this Profile. Defaults to the last section of the class name (e.g. a profile
     * called MyApp.profile.Phone will default the name to 'Phone').
     * @accessor
     */
    config: {
        /**
         * @cfg {String} mainView
         */
        mainView: {
            $value: null,
            lazy: true
        },
        /**
         * @cfg {Ext.app.Application} application
         * The {@link Ext.app.Application Application} instance to which this Profile is
         * bound. This is set automatically.
         * @accessor
         * @readonly
         */
        application: null,
        // @cmd-auto-dependency {aliasPrefix: "controller.", profile: true, blame: "all"}
        /**
         * @cfg {String[]} controllers
         * Any additional {@link Ext.app.Controller Controllers} to load for this profile.
         * Note that each item here will be prepended with the Profile namespace when loaded.
         *
         * Example usage:
         *
         *     controllers: [
         *         'Users',
         *         'MyApp.controller.Products'
         *     ]
         *
         * This will load *MyApp.controller.tablet.Users* and *MyApp.controller.Products*.
         * @accessor
         */
        controllers: [],
        // @cmd-auto-dependency {aliasPrefix : "model.", profile: true, blame: "all"}
        /**
         * @cfg {String[]} models
         * Any additional {@link Ext.app.Application#models Models} to load for this profile.
         * Note that each item here will be prepended with the Profile namespace when loaded.
         *
         * Example usage:
         *
         *     models: [
         *         'Group',
         *         'MyApp.model.User'
         *     ]
         *
         * This will load *MyApp.model.tablet.Group* and *MyApp.model.User*.
         * @accessor
         */
        models: [],
        /* eslint-disable-next-line max-len */
        // @cmd-auto-dependency { aliasPrefix: "view.", profile: true, isKeyedObject:true, blame: "all" }
        /**
         * @cfg {Object/String[]} views
         * This config allows the active profile to define a set of `xtypes` and map them
         * to desired classes and default configurations. Normally an `xtype` is statically
         * declared by a {@link Ext.Component component} in its class definition. This
         * mechanism allows the active profile to control a set of these types.
         *
         * Example:
         *
         *      views: {
         *          // The "main" xtype maps to MyApp.view.tablet.Main
         *          //
         *          main: 'MyApp.view.tablet.Main',
         *
         *          // The "inbox" xtype maps to a subclass of MyApp.view.Inbox (created
         *          // by this mechanism) that sets the "mode" config to "compact".
         *          //
         *          inbox: {
         *              xclass: 'MyApp.view.Inbox',
         *              mode: 'compact'
         *          }
         *      }
         *
         * Note that class names used in this form must be full class names, unlike the
         * historical usage of `views`. Further, these views cannot be accessed using the
         * `getView` method but rather via their assigned `xtype`.
         *
         * The historical usage of this config is enabled when an array is passed. In this
         * case, these are simply additional {@link Ext.app.Application#views views} to
         * load for this profile. Note that each item here will be prepended with the
         * Profile namespace when loaded.
         *
         * Example usage:
         *
         *     views: [
         *         'Main',
         *         'MyApp.view.Login'
         *     ]
         *
         * This will load *MyApp.view.tablet.Main* and *MyApp.view.Login*. While supported,
         * this usage is discouraged in favor of `xtype` mapping.
         * @accessor
         */
        views: [],
        // @cmd-auto-dependency {aliasPrefix: "store.", profile: true, blame: "all"}
        /**
         * @cfg {String[]} stores
         * Any additional {@link Ext.app.Application#stores Stores} to load for this profile.
         * Note that each item here will be prepended with the Profile namespace when loaded.
         *
         * Example usage:
         *
         *     stores: [
         *         'Users',
         *         'MyApp.store.Products'
         *     ]
         *
         * This will load *MyApp.store.tablet.Users* and *MyApp.store.Products*.
         * @accessor
         */
        stores: []
    },
    /**
     * Creates a new Profile instance
     */
    constructor: function(config) {
        this.initConfig(config);
        this.mixins.observable.constructor.apply(this, arguments);
    },
    /**
     * Determines whether or not this Profile is active on the device isActive is executed on.
     * Should return true if this profile is meant to be active on this device, false otherwise.
     * Each Profile should implement this function (the default implementation just returns false).
     * @return {Boolean} True if this Profile should be activated on the device it is running on,
     * false otherwise
     */
    isActive: function() {
        return false;
    },
    /**
     * This method is called once the profile is determined to be the active profile. This
     * initialization is performed before controllers are initialized and therefore also
     * before launch.
     * @protected
     * @since 6.0.1
     */
    init: function() {
        var views = this.getViews(),
            xtype;
        if (views && !(views instanceof Array)) {
            for (xtype in views) {
                Ext.ClassManager.setXType(views[xtype], xtype);
            }
        }
    },
    /**
     * @method
     * The launch function is called by the {@link Ext.app.Application Application} if this
     * Profile's {@link #isActive} function returned true. This is typically the best place to run
     * any profile-specific app launch code. Example usage:
     *
     *     launch: function() {
     *         Ext.create('MyApp.view.tablet.Main');
     *     }
     */
    launch: Ext.emptyFn,
    onClassExtended: function(cls, data, hooks) {
        var onBeforeClassCreated = hooks.onBeforeCreated;
        hooks.onBeforeCreated = function(cls, data) {
            var Controller = Ext.app.Controller,
                className = cls.$className,
                requires = [],
                proto = cls.prototype,
                views = data.views,
                name, namespace;
            // Process name and namespace configs here since we need to use the namespace
            // in the dependency calculation
            name = data.name;
            if (name) {
                delete data.name;
            } else {
                name = className.split('.');
                name = name[name.length - 1];
            }
            cls._name = name;
            cls._namespace = name = (data.namespace || name).toLowerCase();
            delete data.namespace;
            namespace = Controller.resolveNamespace(cls, data);
            Controller.processDependencies(proto, requires, namespace, 'model', data.models, name);
            Controller.processDependencies(proto, requires, namespace, 'store', data.stores, name);
            Controller.processDependencies(proto, requires, namespace, 'controller', data.controllers, name);
            if (views) {
                if (views instanceof Array) {
                    Controller.processDependencies(proto, requires, namespace, 'view', views, name);
                } else {
                    Ext.app.Profile.processViews(className, views, requires);
                }
            }
            Ext.require(requires, Ext.Function.pass(onBeforeClassCreated, arguments, this));
        };
    },
    getName: function() {
        // This used to be a Config but is now processed in onClassExtended so we provide
        // the getter for compat.
        return this.self._name;
    },
    getNamespace: function() {
        // This used to be a Config but is now processed in onClassExtended so we provide
        // the getter for compat.
        return this.self._namespace;
    },
    privates: {
        statics: {
            processViews: function(className, views, requires) {
                var body, cls, s, xtype;
                for (xtype in views) {
                    cls = views[xtype];
                    if (typeof cls !== 'string') {
                        s = cls.xclass;
                        if (!s) {
                            Ext.raise('Views must specify an xclass');
                        }
                        body = Ext.apply({
                            extend: s
                        }, cls);
                        delete body.xclass;
                        // Class names will be App.profile.Tablet$inbox for example
                        Ext.define(views[xtype] = className + '$' + xtype, body);
                        cls = s;
                    }
                    requires.push(cls);
                }
            }
        }
    }
});

/**
 * @class Ext.app.domain.View
 */
Ext.define('Ext.app.domain.View', {
    extend: 'Ext.app.EventDomain',
    requires: [
        'Ext.Widget'
    ],
    isInstance: true,
    constructor: function(controller) {
        this.callParent([
            controller
        ]);
        this.controller = controller;
        this.monitoredClasses = [
            Ext.Widget
        ];
    },
    match: function(target, selector, controller) {
        var out = false;
        if (selector === '#') {
            out = controller === target.getController();
        } else {
            out = target.is(selector);
        }
        return out;
    },
    destroy: function() {
        this.controller = null;
        this.callParent();
    }
});

Ext.define('Ext.overrides.app.domain.View', {
    override: 'Ext.app.domain.View',
    requires: [
        'Ext.Component'
    ],
    constructor: function(controller) {
        this.callParent([
            controller
        ]);
        // The base class handles Ext.Widget, which encompasses
        // component for modern, so we only need the override here.
        this.monitoredClasses.push(Ext.Component);
    }
});

/**
 * A view controller is a controller that can be attached to a specific view
 * instance so it can manage the view and its child components. Each instance of the view
 * will have a new view controller, so the instances are isolated.
 * 
 * When a view controller is specified on a view, events and other handlers that use strings as
 * values will be automatically connected with the appropriate methods in the controller's class.
 *
 * Sample usage:
 * 
 *     @example
 *     Ext.define('MyViewController', {
 *         extend : 'Ext.app.ViewController',
 *         alias: 'controller.myview',
 *       
 *         // This method is called as a "handler" for the Add button in our view
 *         onAddClick: function() {
 *             Ext.Msg.alert('Add', 'The Add button was clicked');
 *         }
 *     });
 *   
 *     Ext.define('MyView', {
 *         extend: 'Ext.Panel',
 *         controller: 'myview',
 *
 *         items: [{
 *             xtype: 'button',
 *             text: 'Add',
 *             handler: 'onAddClick',  // calls MyViewController's onAddClick method
 *         }]
 *     });
 *   
 *     Ext.onReady(function() {
 *         new MyView({
 *             renderTo: Ext.getBody(),
 *             width: 400,
 *             height: 200
 *         });
 *     }); 
 */
Ext.define('Ext.app.ViewController', {
    extend: 'Ext.app.BaseController',
    alias: 'controller.controller',
    requires: [
        'Ext.app.domain.View'
    ],
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    isViewController: true,
    /**
     * @property factoryConfig
     * @inheritdoc
     */
    factoryConfig: {
        // configure Factoryable
        type: 'controller'
    },
    config: {
        /**
         * @cfg {Object} bindings
         * A declarative set of bindings to the {@link #getViewModel} for this
         * controller. The key should be the method, the value should be
         * the bind statement:
         *
         *     Ext.define('MyApp.TestController', {
         *         extend: 'Ext.app.ViewController',
         *
         *         bindings: {
         *             onTotalChange: '{total}',
         *             onCoordsChange: '({x}, {y})',
         *             onProductChange: {
         *                 amount: '{qty}',
         *                 rating: '{rating}'
         *             }
         *         },
         *
         *          onTotalChange: function(total) {
         *              console.log(total);
         *          },
         *
         *          onCoordsChange: function(coords) {
         *              console.log('The coordinates are: ', coords);
         *          },
         *
         *          onProductChange: function(productInfo) {
         *              console.log('Amount: ', productInfo.amount,' Rating: ', productInfo.rating);
         *          }
         *     });
         *
         * @since 6.5.0
         */
        bindings: {
            $value: null,
            lazy: true
        },
        closeViewAction: 'destroy'
    },
    view: null,
    constructor: function(config) {
        this.compDomain = new Ext.app.domain.View(this);
        this.callParent([
            config
        ]);
    },
    /**
     * @method beforeInit
     *
     * Called before the view initializes. This is called before the view's
     * initComponent method has been called.
     * @param {Ext.Component} view The view
     * @protected
     */
    beforeInit: Ext.emptyFn,
    /**
     * @method init
     *
     * Called when the view initializes. This is called after the view's initComponent
     * method has been called.
     * @param {Ext.Component} view The view
     * @protected
     */
    init: Ext.emptyFn,
    /**
     * @method initViewModel
     *
     * Called when the view model instance for an attached view is first created.
     * @param {Ext.app.ViewModel} viewModel The ViewModel
     * @protected
     */
    initViewModel: Ext.emptyFn,
    /**
     * Destroy the view controller.
     */
    destroy: function() {
        var me = this,
            domain = me.compDomain,
            bind, b, key;
        if (me.$hasBinds) {
            bind = me.getBindings();
            for (key in bind) {
                b = bind[key];
                if (b) {
                    b.destroy();
                }
            }
        }
        if (domain) {
            domain.unlisten(me);
            domain.destroy();
        }
        me.compDomain = me.view = null;
        me.callParent();
    },
    /**
     * This method closes the associated view. The manner in which this is done (that is,
     * the method called to close the view) is specified by `closeViewAction`.
     *
     * It is common for views to map one or more events to this method to allow the view
     * to be closed.
     */
    closeView: function() {
        var view = this.getView(),
            action;
        if (view) {
            action = this.getCloseViewAction();
            view[action]();
        }
    },
    control: function(selectors, listeners) {
        var obj = selectors;
        if (Ext.isString(selectors)) {
            obj = {};
            obj[selectors] = listeners;
        }
        this.compDomain.listen(obj, this);
    },
    listen: function(to, controller) {
        var component = to.component;
        if (component) {
            to = Ext.apply({}, to);
            delete to.component;
            this.control(component);
        }
        this.callParent([
            to,
            controller
        ]);
    },
    applyId: function(id) {
        if (!id) {
            id = Ext.id(null, 'controller-');
        }
        return id;
    },
    /**
     * @method getReferences
     * @inheritdoc Ext.mixin.Container#method!getReferences
     * @since 5.0.0
     */
    getReferences: function() {
        var view = this.view;
        return view && view.getReferences();
    },
    /**
     * Get the view for this controller.
     * @return {Ext.Component} The view.
     */
    getView: function() {
        return this.view;
    },
    /**
     * Gets a reference to the component with the specified {@link Ext.Component#reference}
     * value.
     *
     * The method is a short-hand for the {@link #lookupReference} method.
     *
     * @param {String} key The name of the reference to lookup.
     * @return {Ext.Component} The component, `null` if the reference doesn't exist.
     * @since 6.0.1
     */
    lookup: function(key) {
        var view = this.view;
        return view && view.lookup(key);
    },
    /**
     * Gets a reference to the component with the specified {@link Ext.Component#reference}
     * value.
     *
     * The {@link #lookup} method is a short-hand version of this method.
     *
     * @param {String} key The name of the reference to lookup.
     * @return {Ext.Component} The component, `null` if the reference doesn't exist.
     * @since 5.0.0
     */
    lookupReference: function(key) {
        return this.lookup(key);
    },
    /**
     * Get a {@link Ext.data.Session} attached to the view for this controller.
     * See {@link Ext.Component#lookupSession}.
     * 
     * @return {Ext.data.Session} The session. `null` if no session is found.
     *
     * @since 5.0.0
     */
    getSession: function() {
        var view = this.view;
        return view && view.lookupSession();
    },
    /**
     * Get a {@link Ext.app.ViewModel} attached to the view for this controller.
     * See {@link Ext.Component#lookupViewModel}.
     * 
     * @return {Ext.app.ViewModel} The ViewModel. `null` if no ViewModel is found.
     *
     * @since 5.0.0
     */
    getViewModel: function() {
        var view = this.view;
        return view && view.lookupViewModel();
    },
    /**
     * Get a {@link Ext.data.Store} attached to the {@link #getViewModel ViewModel} attached to
     * this controller. See {@link Ext.app.ViewModel#getStore}.
     * @param {String} name The name of the store.
     * @return {Ext.data.Store} The store. `null` if no store is found, or there is no 
     * {@link Ext.app.ViewModel} attached to the view for this controller.
     *
     * @since 5.0.0
     */
    getStore: function(name) {
        var viewModel = this.getViewModel();
        return viewModel ? viewModel.getStore(name) : null;
    },
    /**
     * Fires an event on the view. See {@link Ext.Component#fireEvent}.
     * @param {String} eventName The name of the event to fire.
     * @param {Object...} args Variable number of parameters are passed to handlers.
     * @return {Boolean} returns false if any of the handlers return false otherwise it returns
     * true.
     * @protected
     */
    fireViewEvent: function(eventName, args) {
        var view = this.view,
            result = false,
            a = arguments;
        if (view) {
            if (view !== args) {
                a = Ext.Array.slice(a);
                a.splice(1, 0, view);
            }
            // insert view at [1]
            result = view.fireEvent.apply(view, a);
        }
        return result;
    },
    /**
     * @method setBind
     * @hide
     */
    applyBindings: function(bindings) {
        if (!bindings) {
            return null;
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            viewModel = me.getViewModel(),
            getBindTemplateScope = me.getBindTemplateScope(),
            b, fn, descriptor;
        me.$hasBinds = true;
        if (!viewModel) {
            Ext.raise('Cannot use bind config without a viewModel');
        }
        for (fn in bindings) {
            descriptor = bindings[fn];
            b = null;
            if (descriptor) {
                b = viewModel.bind(descriptor, fn, me);
                b.getTemplateScope = getBindTemplateScope;
            }
            bindings[fn] = b;
        }
        return bindings;
    },
    //=========================================================================
    privates: {
        view: null,
        /**
         * Set a reference to a component.
         * @param {Ext.Component} component The component to reference
         * @private
         */
        attachReference: function(component) {
            var view = this.view;
            if (view) {
                view.attachReference(component);
            }
        },
        getBindTemplateScope: function() {
            // This method is called as a method on a Binding instance, so the "this" pointer
            // is that of the Binding. The "scope" of the Binding is the controller.
            return this.scope;
        },
        initBindings: function() {
            // Force bind creation
            this.getBindings();
        },
        /**
         * Sets the view for this controller. To be called by the view
         * when it initializes.
         * @param {Object} view The view.
         *
         * @private
         */
        setView: function(view) {
            this.view = view;
            if (!this.beforeInit.$nullFn) {
                this.beforeInit(view);
            }
        }
    }
});

/**
 * This class provides an **unordered** collection similar to `Ext.util.Collection`. The
 * removal of order maintenance provides a significant performance increase. Further, this
 * class does not provide events or other high-level features. It maintains an array of
 * `items` and a map to quickly find items by their `id`.
 *
 * @private
 * @since 5.1.1
 */
Ext.define('Ext.util.Bag', {
    isBag: true,
    constructor: function() {
        /**
         * @property {Object[]} items
         * An array containing the items.
         * @private
         * @since 5.1.1
         */
        this.items = [];
        /**
         * @property {Object} map
         * An object used as a map to find items based on their key.
         * @private
         * @since 5.1.1
         */
        this.map = {};
    },
    /**
     * @property {Number} generation
     * Mutation counter which is incremented when the collection changes.
     * @readonly
     * @since 5.1.1
     */
    generation: 0,
    /**
     * @property {Number} length
     * The count of items in the collection.
     * @readonly
     * @since 5.1.1
     */
    length: 0,
    beginUpdate: Ext.emptyFn,
    endUpdate: Ext.emptyFn,
    add: function(item) {
        var me = this,
            items = me.items,
            map = me.map,
            n = 1,
            old, i, idx, id, it, ret, was;
        if (Ext.isArray(item)) {
            old = ret = [];
            n = item.length;
        }
        for (i = 0; i < n; i++) {
            id = me.getKey(it = old ? item[i] : item);
            idx = map[id];
            if (idx === undefined) {
                items.push(it);
                map[id] = me.length++;
                if (old) {
                    old.push(it);
                } else {
                    ret = it;
                }
            } else {
                was = items[idx];
                if (old) {
                    old.push(was);
                } else {
                    ret = was;
                }
                items[idx] = it;
            }
        }
        ++me.generation;
        return ret;
    },
    clear: function() {
        var me = this,
            needsClear = me.generation || me.length,
            ret = needsClear ? me.items : [];
        if (needsClear) {
            me.items = [];
            me.length = 0;
            me.map = {};
            ++me.generation;
        }
        return ret;
    },
    clone: function() {
        var me = this,
            ret = new me.self(),
            len = me.length;
        if (len) {
            Ext.apply(ret.map, me.map);
            ret.items = me.items.slice();
            ret.length = me.length;
        }
        return ret;
    },
    contains: function(item) {
        var ret = false,
            map = this.map,
            key;
        if (item != null) {
            key = this.getKey(item);
            if (key in map) {
                ret = this.items[map[key]] === item;
            }
        }
        return ret;
    },
    containsKey: function(key) {
        return key in this.map;
    },
    destroy: function() {
        this.items = this.map = null;
        this.callParent();
    },
    each: function(fn, scope) {
        var items = this.items,
            len = items.length,
            i, ret;
        if (len) {
            scope = scope || this;
            items = items.slice(0);
            // safe for re-entrant calls
            for (i = 0; i < len; i++) {
                ret = fn.call(scope, items[i], i, len);
                if (ret === false) {
                    break;
                }
            }
        }
        return ret;
    },
    getAt: function(index) {
        var out = null;
        if (index < this.length) {
            out = this.items[index];
        }
        return out;
    },
    get: function(key) {
        return this.getByKey(key);
    },
    getByKey: function(key) {
        var map = this.map,
            ret = (key in map) ? this.items[map[key]] : null;
        return ret;
    },
    indexOfKey: function(key) {
        var map = this.map,
            ret = (key in map) ? map[key] : -1;
        return ret;
    },
    last: function() {
        return this.items[this.length - 1];
    },
    updateKey: function(item, oldKey) {
        var me = this,
            map = me.map,
            newKey;
        if (!item || !oldKey) {
            return;
        }
        if ((newKey = me.getKey(item)) !== oldKey) {
            if (me.getAt(map[oldKey]) === item && !(newKey in map)) {
                me.generation++;
                map[newKey] = map[oldKey];
                delete map[oldKey];
            }
        } else {
            // It may be that the item is (somehow) already in the map using the
            // newKey or that there is no item in the map with the oldKey. These
            // are not errors.
            if (newKey in map && me.getAt(map[newKey]) !== item) {
                // There is a different item in the map with the newKey which is an
                // error. To properly handle this, add the item instead.
                Ext.raise('Duplicate newKey "' + newKey + '" for item with oldKey "' + oldKey + '"');
            }
            if (oldKey in map && me.getAt(map[oldKey]) !== item) {
                // There is a different item in the map with the oldKey which is also
                // an error. Do not call this method for items that are not part of
                // the collection.
                Ext.raise('Incorrect oldKey "' + oldKey + '" for item with newKey "' + newKey + '"');
            }
        }
    },
    getCount: function() {
        return this.length;
    },
    getKey: function(item) {
        return item.id || item.getId();
    },
    getRange: function(begin, end) {
        var items = this.items,
            length = items.length,
            range;
        if (!length) {
            range = [];
        } else {
            range = Ext.Number.clipIndices(length, [
                begin,
                end
            ]);
            range = items.slice(range[0], range[1]);
        }
        return range;
    },
    remove: function(item) {
        var me = this,
            map = me.map,
            items = me.items,
            ret = null,
            n = 1,
            changed, old, i, idx, id, last, was;
        if (Ext.isArray(item)) {
            n = item.length;
            old = ret = [];
        }
        if (me.length) {
            for (i = 0; i < n; i++) {
                idx = map[id = me.getKey(old ? item[i] : item)];
                if (idx !== undefined) {
                    delete map[id];
                    was = items[idx];
                    if (old) {
                        old.push(was);
                    } else {
                        ret = was;
                    }
                    last = items.pop();
                    if (idx < --me.length) {
                        items[idx] = last;
                        map[me.getKey(last)] = idx;
                    }
                    changed = true;
                }
            }
            if (changed) {
                ++me.generation;
            }
        }
        return ret;
    },
    removeByKey: function(key) {
        var item = this.getByKey(key);
        if (item) {
            this.remove(item);
        }
        return item || null;
    },
    replace: function(item) {
        this.add(item);
        return item;
    },
    sort: function(fn) {
        var me = this,
            items = me.items,
            n = items.length,
            item;
        if (n) {
            Ext.Array.sort(items, fn);
            me.map = {};
            while (n-- > 0) {
                item = items[n];
                me.map[me.getKey(item)] = n;
            }
            ++me.generation;
        }
    }
});

/**
 * This class is used to bulk schedule a set of `Ext.util.Schedulable` items. The items
 * in the scheduler request time by calling their `schedule` method and when the time has
 * arrived its `react` method is called.
 *
 * The `react` methods are called in dependency order as determined by the sorting process.
 * The sorting process relies on each item to implement its own `sort` method.
 *
 * @private
 */
Ext.define('Ext.util.Scheduler', {
    mixins: [
        'Ext.mixin.Observable'
    ],
    requires: [
        'Ext.util.Bag'
    ],
    busyCounter: 0,
    lastBusyCounter: 0,
    destroyed: false,
    firing: null,
    notifyIndex: -1,
    nextId: 0,
    orderedItems: null,
    passes: 0,
    scheduledCount: 0,
    validIdRe: null,
    config: {
        /**
         * @cfg {Number} cycleLimit
         * The maximum number of iterations to make over the items in one `notify` call.
         * This is used to prevent run away logic from looping infinitely. If this limit
         * is exceeded, an error is thrown (in development builds).
         * @private
         */
        cycleLimit: 5,
        /**
         * @cfg {String/Function} preSort
         * If provided the `Schedulable` items will be pre-sorted by this function or
         * property value before the dependency sort.
         */
        preSort: null,
        /**
         * @cfg {Number} tickDelay
         * The number of milliseconds to delay notification after the first `schedule`
         * request.
         */
        tickDelay: 5
    },
    /**
     * @property {Boolean} suspendOnNotify
     * `true` to suspend layouts when the scheduler is triggering bindings. Setting this to `false`
     * may mean multiple layout runs on a single bind call which could affect performance.
     */
    suspendOnNotify: true,
    constructor: function(config) {
        if (Ext.util.Scheduler.instances) {
            Ext.util.Scheduler.instances.push(this);
        } else {
            Ext.util.Scheduler.instances = [
                this
            ];
        }
        this.id = Ext.util.Scheduler.count = (Ext.util.Scheduler.count || 0) + 1;
        this.mixins.observable.constructor.call(this, config);
        this.items = new Ext.util.Bag();
    },
    destroy: function() {
        var me = this,
            timer = me.timer;
        if (timer) {
            window.clearTimeout(timer);
            me.timer = null;
        }
        me.items.destroy();
        me.items = me.orderedItems = null;
        me.callParent();
        Ext.Array.remove(Ext.util.Scheduler.instances, this);
    },
    /**
     * Adds an item to the scheduler. This is called internally by the `constructor` of
     * `{@link Ext.util.Schedulable}`.
     *
     * @param {Object} item The item to add.
     * @private
     * @since 5.0.0
     */
    add: function(item) {
        var me = this,
            items = me.items;
        if (items === me.firing) {
            me.items = items = items.clone();
        }
        item.id = item.id || ++me.nextId;
        item.scheduler = me;
        items.add(item);
        if (!me.sortMap) {
            // If we are sorting we don't want to invalidate this... we will pick up the
            // new items just fine.
            me.orderedItems = null;
        }
    },
    /**
     * Removes an item to the scheduler. This is called internally by the `destroy` method
     * of `{@link Ext.util.Schedulable}`.
     *
     * @param {Object} item The item to remove.
     * @private
     * @since 5.0.0
     */
    remove: function(item) {
        var me = this,
            items = me.items;
        if (me.destroyed) {
            return;
        }
        if (me.sortMap) {
            Ext.raise('Items cannot be removed during sort');
        }
        if (items === me.firing) {
            me.items = items = items.clone();
        }
        if (item.scheduled) {
            me.unscheduleItem(item);
            item.scheduled = false;
        }
        items.remove(item);
        me.orderedItems = null;
    },
    /**
     * This method is called internally as needed to sort or resort the items in their
     * proper dependency order.
     *
     * @private
     * @since 5.0.0
     */
    sort: function() {
        var me = this,
            items = me.items,
            sortMap = {},
            preSort = me.getPreSort(),
            i, item;
        me.orderedItems = [];
        me.sortMap = sortMap;
        me.sortStack = [];
        if (preSort) {
            items.sort(preSort);
        }
        items = items.items;
        // grab the items array
        // We reference items.length since items can be added during this loop
        for (i = 0; i < items.length; ++i) {
            item = items[i];
            if (!sortMap[item.id]) {
                me.sortItem(item);
            }
        }
        me.sortMap = null;
        me.sortStack = null;
    },
    /**
     * Adds one item to the sorted items array. This can be called by the `sort` method of
     * `{@link Ext.util.Sortable sortable}` objects to add an item on which it depends.
     *
     * @param {Object} item The item to add.
     * @return {Ext.util.Scheduler} This instance.
     * @since 5.0.0
     */
    sortItem: function(item) {
        var me = this,
            sortMap = me.sortMap,
            orderedItems = me.orderedItems,
            itemId;
        if (!item.scheduler) {
            me.add(item);
        }
        itemId = item.id;
        if (item.scheduler !== me) {
            Ext.raise('Item ' + itemId + ' belongs to another Scheduler');
        }
        me.sortStack.push(item);
        if (sortMap[itemId] === 0) {
            // eslint-disable-next-line vars-on-top
            for (var cycle = [],
                i = 0; i < me.sortStack.length; ++i) {
                cycle[i] = me.sortStack[i].getFullName();
            }
            Ext.raise('Dependency cycle detected: ' + cycle.join('\n --> '));
        }
        if (!(itemId in sortMap)) {
            // In production builds the above "if" will kick out the items that have
            // already been added (which it must) but also those that are being added
            // and have created a cycle (by virtue of the setting to 0). This check
            // should not be needed if cycles were all detected and removed in dev but
            // this is better than infinite recursion.
            sortMap[itemId] = 0;
            if (!item.sort.$nullFn) {
                item.sort();
            }
            sortMap[itemId] = 1;
            item.order = me.orderedItems.length;
            orderedItems.push(item);
        }
        me.sortStack.pop();
        return me;
    },
    /**
     * Adds multiple items to the sorted items array. This can be called by the `sort`
     * method of `{@link Ext.util.Sortable sortable}` objects to add items on which it
     * depends.
     *
     * @param {Object/Object[]} items The items to add. If this is an object, the values
     * are considered the items and the keys are ignored.
     * @return {Ext.util.Scheduler} This instance.
     * @since 5.0.0
     */
    sortItems: function(items) {
        var me = this,
            sortItem = me.sortItem;
        if (items) {
            if (items instanceof Array) {
                Ext.each(items, sortItem, me);
            } else {
                Ext.Object.eachValue(items, sortItem, me);
            }
        }
        return me;
    },
    applyPreSort: function(preSort) {
        if (typeof preSort === 'function') {
            return preSort;
        }
        // eslint-disable-next-line vars-on-top
        var parts = preSort.split(','),
            direction = [],
            length = parts.length,
            c, i, s;
        for (i = 0; i < length; ++i) {
            direction[i] = 1;
            s = parts[i];
            if ((c = s.charAt(0)) === '-') {
                direction[i] = -1;
            } else if (c !== '+') {
                c = 0;
            }
            if (c) {
                parts[i] = s.substring(1);
            }
        }
        return function(lhs, rhs) {
            var ret = 0,
                i, prop, v1, v2;
            for (i = 0; !ret && i < length; ++i) {
                prop = parts[i];
                v1 = lhs[prop];
                v2 = rhs[prop];
                ret = direction[i] * ((v1 < v2) ? -1 : ((v2 < v1) ? 1 : 0));
            }
            return ret;
        };
    },
    //-------------------------------------------------------------------------
    // Callback scheduling
    // <editor-fold>
    /**
     * This method can be called to force the delivery of any scheduled items. This is
     * called automatically on a timer when items request service.
     *
     * @since 5.0.0
     */
    notify: function() {
        var me = this,
            timer = me.timer,
            cyclesLeft = me.getCycleLimit(),
            globalEvents = Ext.GlobalEvents,
            suspend = me.suspendOnNotify,
            busyCounter, i, item, len, queue, firedEvent;
        if (timer) {
            window.clearTimeout(timer);
            me.timer = null;
        }
        // Only process the queue if we are not already notifying
        // and there are notifications to deliver.
        if (!me.firing && me.scheduledCount) {
            if (suspend) {
                Ext.suspendLayouts();
            }
            while (me.scheduledCount) {
                if (cyclesLeft) {
                    --cyclesLeft;
                } else {
                    me.firing = null;
                    if (me.onCycleLimitExceeded) {
                        me.onCycleLimitExceeded();
                    }
                    break;
                }
                if (!firedEvent) {
                    firedEvent = true;
                    if (globalEvents.hasListeners.beforebindnotify) {
                        globalEvents.fireEvent('beforebindnotify', me);
                    }
                }
                ++me.passes;
                // We need to sort before we start firing because items can be added as we
                // loop.
                if (!(queue = me.orderedItems)) {
                    me.sort();
                    queue = me.orderedItems;
                }
                len = queue.length;
                if (len) {
                    me.firing = me.items;
                    for (i = 0; i < len; ++i) {
                        item = queue[i];
                        if (item.scheduled) {
                            item.scheduled = false;
                            --me.scheduledCount;
                            me.notifyIndex = i;
                            // Ext.log('React: ' + item.getFullName());
                            // This sequence allows the reaction to schedule items further
                            // down the queue without a second pass but also to schedule an
                            // item that is "upstream" or even itself.
                            item.react();
                            if (!me.scheduledCount) {
                                break;
                            }
                        }
                    }
                }
            }
            me.firing = null;
            me.notifyIndex = -1;
            if (suspend) {
                Ext.resumeLayouts(true);
            }
        }
        // The last thing we do is check for idle state transition (now that whatever
        // else that was queued up has been dispatched):
        if ((busyCounter = me.busyCounter) !== me.lastBusyCounter) {
            if (!(me.lastBusyCounter = busyCounter)) {
                // Since the counters are not equal, we were busy and are not anymore,
                // so we can fire the idle event:
                me.fireEvent('idle', me);
            }
        }
    },
    /**
     * The method called by the timer. This cleans up the state and calls `notify`.
     * @private
     * @since 5.0.0
     */
    onTick: function() {
        this.timer = null;
        this.notify();
    },
    /**
     * Called to indicate that an item needs to be scheduled. This should not be called
     * directly. Call the item's `{@link Ext.util.Schedulable#schedule schedule}` method
     * instead.
     * @param {Object} item
     * @private
     * @since 5.0.0
     */
    scheduleItem: function(item) {
        var me = this;
        ++me.scheduledCount;
        // Ext.log('Schedule: ' + item.getFullName());
        if (!me.timer && !me.firing) {
            me.scheduleTick();
        }
    },
    /**
     * This method starts the timer that will execute the next `notify`.
     * @param {Object} item
     * @private
     * @since 5.0.0
     */
    scheduleTick: function() {
        var me = this;
        if (!me.destroyed && !me.timer) {
            me.timer = Ext.defer(me.onTick, me.getTickDelay(), me);
        }
    },
    /**
     * Called to indicate that an item needs to be removed from the schedule. This should
     * not be called directly. Call the item's `{@link Ext.util.Schedulable#unschedule unschedule}`
     * method instead.
     * @param {Object} item
     * @private
     * @since 5.0.0
     */
    unscheduleItem: function(item) {
        if (this.scheduledCount) {
            --this.scheduledCount;
        }
    },
    // </editor-fold>
    //-------------------------------------------------------------------------
    // Busy/Idle state tracking
    // <editor-fold>
    /**
     * This method should be called when items become busy or idle. These changes are
     * useful outside to do things like update modal masks or status indicators. The
     * changes are delivered as `busy` and `idle` events.
     *
     * @param {Number} adjustment Should be `1` or `-1` only to indicate transition to
     * busy state or from busy state, respectively.
     * @since 5.0.0
     */
    adjustBusy: function(adjustment) {
        var me = this,
            busyCounter = me.busyCounter + adjustment;
        me.busyCounter = busyCounter;
        if (busyCounter) {
            // If we are now busy but were not previously, fire the busy event immediately
            // and update lastBusyCounter.
            if (!me.lastBusyCounter) {
                me.lastBusyCounter = busyCounter;
                me.fireEvent('busy', me);
            }
        } else if (me.lastBusyCounter && !me.timer) {
            // If we are now not busy but were previously, defer this to make sure that
            // we don't quickly start with some other activity.
            me.scheduleTick();
        }
    },
    /**
     * Returns `true` if this object contains one or more busy items.
     * @return {Boolean}
     * @since 5.0.0
     */
    isBusy: function() {
        return !this.isIdle();
    },
    /**
     * Returns `true` if this object contains no busy items.
     * @return {Boolean}
     * @since 5.0.0
     */
    isIdle: function() {
        return !(this.busyCounter + this.lastBusyCounter);
    },
    // </editor-fold>
    debugHooks: {
        $enabled: false,
        // Disable by default
        onCycleLimitExceeded: function() {
            Ext.raise('Exceeded cycleLimit ' + this.getCycleLimit());
        },
        scheduleItem: function(item) {
            if (!item) {
                Ext.raise('scheduleItem: Invalid argument');
            }
            Ext.log('Schedule item: ' + item.getFullName() + ' - ' + (this.scheduledCount + 1));
            if (item.order <= this.notifyIndex) {
                Ext.log.warn('Suboptimal order: ' + item.order + ' < ' + this.notifyIndex);
            }
            this.callParent([
                item
            ]);
        },
        unscheduleItem: function(item) {
            if (!this.scheduledCount) {
                Ext.raise('Invalid scheduleCount');
            }
            this.callParent([
                item
            ]);
            Ext.log('Unschedule item: ' + item.getFullName() + ' - ' + this.scheduledCount);
        }
    }
});

/**
 * Provides a mechanism to run one or more {@link Ext.data.operation.Operation operations}
 * in a given order. Fires the `operationcomplete` event after the completion of each
 * Operation, and the `complete` event when all Operations have been successfully executed.
 * Fires an `exception` event if any of the Operations encounter an exception.
 *
 * Usually these are only used internally by {@link Ext.data.proxy.Proxy} classes.
 */
Ext.define('Ext.data.Batch', {
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
    config: {
        /**
        * @cfg {Boolean} pauseOnException
        * True to pause the execution of the batch if any operation encounters an exception
        * (defaults to false). If you set this to true you are responsible for implementing
        * the appropriate handling logic and restarting or discarding the batch as needed.
        * There are different ways you could  do this, e.g. by handling the batch's
        * {@link #event-exception} event directly, or perhaps by overriding
        * {@link Ext.data.ProxyStore#onBatchException onBatchException} at the store level.
        * If you do pause and attempt to handle the exception you can call {@link #retry} to
        * process the same operation again. 
        * 
        * Note that {@link Ext.data.operation.Operation operations} are atomic, so any operations
        * that may have succeeded prior to an exception (and up until pausing the batch) will be
        * finalized at the server level and will not be automatically reversible. Any transactional
        * / rollback behavior that might be desired would have to be implemented at the application
        * level. Pausing on exception will likely be most beneficial when used in coordination with
        * such a scheme, where an exception might actually affect subsequent operations in the same
        * batch and so should be handled before continuing with the next operation.
        * 
        * If you have not implemented transactional operation handling then this option should
        * typically be left  to the default of false (e.g. process as many operations as possible,
        * and handle any exceptions  asynchronously without holding up the rest of the batch).
        */
        pauseOnException: false
    },
    /**
     * @property {Number} current
     * The index of the current operation being executed.
     * @private
     */
    current: -1,
    /**
     * @property {Number} total
     * The total number of operations in this batch.
     * @private
     */
    total: 0,
    /**
     * @property {Boolean} running
     * True if the batch is currently running.
     * @private
     */
    running: false,
    /**
     * @property {Boolean} complete
     * True if this batch has been executed completely.
     * @private
     */
    complete: false,
    /**
     * @property {Boolean} exception
     * True if this batch has encountered an exception. This is cleared at the start of each
     * operation.
     * @private
     */
    exception: false,
    /**
     * Creates new Batch object.
     * @param {Object} [config] Config object
     */
    constructor: function(config) {
        var me = this;
        me.mixins.observable.constructor.call(me, config);
        /**
         * @event complete
         * Fired when all operations of this batch have been completed
         * @param {Ext.data.Batch} batch The batch object
         * @param {Object} operation The last operation that was executed
         */
        /**
         * @event exception
         * Fired when a operation encountered an exception
         * @param {Ext.data.Batch} batch The batch object
         * @param {Object} operation The operation that encountered the exception
         */
        /**
         * @event operationcomplete
         * Fired when each operation of the batch completes
         * @param {Ext.data.Batch} batch The batch object
         * @param {Object} operation The operation that just completed
         */
        /**
         * Ordered array of operations that will be executed by this batch
         * @property {Ext.data.operation.Operation[]} operations
         * @private
         */
        me.operations = [];
        /**
         * Ordered array of operations that raised an exception during the most recent
         * batch execution and did not successfully complete
         * @property {Ext.data.operation.Operation[]} exceptions
         */
        me.exceptions = [];
    },
    /**
     * Adds a new operation to this batch at the end of the {@link #operations} array
     * @param {Ext.data.operation.Operation/Ext.data.operation.Operation[]} operation 
     * The {@link Ext.data.operation.Operation Operation} object or an array of operations.
     * @return {Ext.data.Batch} this
     */
    add: function(operation) {
        var me = this,
            i, len;
        if (Ext.isArray(operation)) {
            for (i = 0 , len = operation.length; i < len; ++i) {
                me.add(operation[i]);
            }
        } else {
            me.total++;
            operation.setBatch(me);
            me.operations.push(operation);
        }
        return me;
    },
    /**
     * Sorts the `{@link Ext.data.operation.Operation operations}` based on their type and
     * the foreign key dependencies of the entities. Consider a simple Parent and Child
     * case where the Child has a "parentId" field. If this batch contains two `create`
     * operations, one of a Parent and one for its Child, the server must receive and
     * process the `create` of the Parent before the Child can be created.
     *
     * In the case of `destroy` operations this order is reversed. The Child entity must be
     * destroyed before the Parent to avoid any foreign key constraints (a Child with an
     * invalid parentId field).
     *
     * Further, `create` operations must all occur before `update` operations to ensure
     * that all entities exist that might be now referenced by the updates. The created
     * entities can safely reference already existing entities.
     *
     * Finally, `destroy` operations are sorted after `update` operations to allow those
     * updates to remove references to the soon-to-be-deleted entities.
     */
    sort: function() {
        this.operations.sort(this.sortFn);
    },
    sortFn: function(operation1, operation2) {
        var ret = operation1.order - operation2.order;
        if (ret) {
            return ret;
        }
        /* eslint-disable-next-line vars-on-top, one-var */
        var entityType1 = operation1.entityType,
            entityType2 = operation2.entityType,
            rank;
        // Since the orders are equal, the operations are the same type. Read operations
        // have no records, so report equality.
        if (!entityType1 || !entityType2) {
            return 0;
        }
        // Otherwise, determine the entity rank for the entities involved in the two
        // operations.
        if (!(rank = entityType1.rank)) {
            // Time to perform the topo-sort based on foreign-key references.
            entityType1.schema.rankEntities();
            // Now the rank is available for all entities.
            rank = entityType1.rank;
        }
        return (rank - entityType2.rank) * operation1.foreignKeyDirection;
    },
    /**
     * Kicks off execution of the batch, continuing from the next operation if the previous
     * operation encountered an exception, or if execution was paused. Use this method to start
     * the batch for the first time or to restart a paused batch by skipping the current
     * unsuccessful operation.
     * 
     * To retry processing the current operation before continuing to the rest of the batch (e.g.
     * because you explicitly handled the operation's exception), call {@link #retry} instead.
     * 
     * Note that if the batch is already running any call to start will be ignored.
     * @param {Number} [index] (private)
     * @return {Ext.data.Batch} this
     */
    start: function(index) {
        var me = this;
        if (me.destroyed || !me.operations.length || me.running) {
            return me;
        }
        me.exceptions.length = 0;
        me.exception = false;
        me.running = true;
        return me.runOperation(Ext.isDefined(index) ? index : me.current + 1);
    },
    abort: function() {
        var me = this,
            op;
        if (me.running) {
            op = me.getCurrent();
            if (!op.destroyed) {
                op.abort();
            }
        }
        me.running = false;
        me.aborted = true;
        me.current = undefined;
    },
    /**
     * Kicks off execution of the batch, continuing from the current operation. This is intended
     * for restarting a {@link #pause paused} batch after an exception, and the operation that
     * raised the exception will now be retried. The batch will then continue with its normal
     * processing until all operations are complete or another exception is encountered.
     * 
     * Note that if the batch is already running any call to retry will be ignored.
     * 
     * @return {Ext.data.Batch} this
     */
    retry: function() {
        return this.start(this.current);
    },
    /**
     * @private
     * Runs the next operation, relative to this.current.
     * @return {Ext.data.Batch} this
     */
    runNextOperation: function() {
        var me = this;
        if (me.running) {
            me.runOperation(me.current + 1);
        }
        return me;
    },
    /**
     * Pauses execution of the batch, but does not cancel the current operation
     * @return {Ext.data.Batch} this
     */
    pause: function() {
        this.running = false;
        return this;
    },
    /**
     * Gets the operations for this batch.
     * @return {Ext.data.operation.Operation[]} The operations.
     */
    getOperations: function() {
        return this.operations;
    },
    /**
     * Gets any operations that have returned without success in this batch.
     * @return {Ext.data.operation.Operation[]} The exceptions
     */
    getExceptions: function() {
        return this.exceptions;
    },
    /**
     * Gets the currently running operation. Will return null if the batch has
     * not started or is completed.
     * @return {Ext.data.operation.Operation} The operation
     */
    getCurrent: function() {
        var out = null,
            current = this.current;
        if (!(current === -1 || this.complete)) {
            out = this.operations[current];
        }
        return out;
    },
    /**
     * Gets the total number of operations in this batch.
     * @return {Number} The total
     */
    getTotal: function() {
        return this.total;
    },
    /**
     * Checks if this batch is running.
     * @return {Boolean} `true` if this batch is running.
     */
    isRunning: function() {
        return this.running;
    },
    /**
     * Checks if this batch is complete.
     * @return {Boolean} `true` if this batch is complete.
     */
    isComplete: function() {
        return this.complete;
    },
    /**
     * Checks if this batch has any exceptions.
     * @return {Boolean} `true` if this batch has any exceptions.
     */
    hasException: function() {
        return this.exception;
    },
    /**
     * Executes an operation by its numeric index in the {@link #operations} array
     * @param {Number} index The operation index to run
     * @return {Ext.data.Batch} this
     * 
     * @private
     */
    runOperation: function(index) {
        var me = this,
            operations = me.operations,
            operation = operations[index];
        if (operation === undefined) {
            me.running = false;
            me.complete = true;
            me.fireEvent('complete', me, operations[operations.length - 1]);
        } else {
            me.current = index;
            operation.setInternalCallback(me.onOperationComplete);
            operation.setInternalScope(me);
            operation.execute();
        }
        return me;
    },
    onOperationComplete: function(operation) {
        var me = this,
            exception = operation.hasException();
        if (exception) {
            me.exception = true;
            me.exceptions.push(operation);
            me.fireEvent('exception', me, operation);
        }
        if (exception && me.getPauseOnException()) {
            me.pause();
        } else {
            me.fireEvent('operationcomplete', me, operation);
            me.runNextOperation();
        }
    },
    destroy: function() {
        var me = this,
            operations = me.operations,
            op, i, len;
        if (me.running) {
            me.abort();
        }
        for (i = 0 , len = me.operations.length; i < len; i++) {
            op = operations[i];
            if (op) {
                if (!op.destroyed && !op.$destroyOwner) {
                    op.destroy();
                }
                op[i] = null;
            }
        }
        // Global cleanup can be turned off
        me.operations = me.exceptions = null;
        me.callParent();
    }
});

/**
 * This class manages one side of a `Matrix`.
 * @private
 */
Ext.define('Ext.data.matrix.Slice', {
    constructor: function(side, id) {
        /**
         * @property {String/Number} id
         * The id of the interested entity. Based on whether this slice is on the "left"
         * or "right" of the matrix, this id identities the respective entity.
         * @readonly
         */
        this.id = id;
        /**
         * @property {Ext.data.matrix.Side} side
         * The side of the matrix to which this slice belongs.
         */
        this.side = side;
        /**
         * 
         */
        this.members = {};
    },
    attach: function(store) {
        var me = this;
        Ext.Assert.falsey(me.store, 'Store is already attached');
        me.store = store;
        store.matrix = me;
        store.on('load', me.onStoreLoad, me, {
            single: true
        });
    },
    commit: function() {
        var members = this.members,
            id;
        for (id in members) {
            members[id][2] = 0;
        }
    },
    onStoreLoad: function(store) {
        this.update(store.getData().items, 0);
    },
    update: function(recordsOrIds, state) {
        if (!(recordsOrIds instanceof Array)) {
            Ext.raise('Only array of records or record ids are supported');
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            MatrixSlice = Ext.data.matrix.Slice,
            side = me.side,
            assocIndex = side.index,
            length = recordsOrIds.length,
            id = me.id,
            members = me.members,
            otherSide = side.inverse,
            otherSlices = otherSide.slices,
            assoc, call, i, item, otherId, otherSlice, record;
        // eslint-disable-line no-unused-vars
        for (i = 0; i < length; ++i) {
            call = record = null;
            item = recordsOrIds[i];
            otherId = item.isEntity ? (record = item).id : item;
            assoc = members[otherId];
            // If we're in a created state and we're asking to remove it, don't move it to
            // removed state, just blow it away completely.
            if (state < 0 && assoc && assoc[2] === 1) {
                delete members[otherId];
                otherSlice = otherSlices[otherId];
                if (otherSlice) {
                    delete otherSlice.members[id];
                }
                call = 1;
            } else {
                if (!assoc) {
                    // Note - when we create a new matrix tuple we must catalog it on both
                    // sides of the matrix or risk losing it on only one side. To gather all
                    // of the tuples we need only visit one side.
                    assoc = [
                        otherId,
                        otherId,
                        state
                    ];
                    assoc[assocIndex] = id;
                    members[otherId] = assoc;
                    otherSlice = otherSlices[otherId];
                    if (!otherSlice) {
                        otherSlices[otherId] = otherSlice = new MatrixSlice(otherSide, otherId);
                    }
                    otherSlice.members[id] = assoc;
                    call = 1;
                } else if (state !== assoc[2] && state !== 0 && !(state === 1 && assoc[2] === 0)) {
                    // If they aren't equal and we're setting it to 0, favour the current state,
                    // except in the case where it's trying to mark as added when we already have
                    // it as present
                    assoc[2] = state;
                    otherSlice = otherSlices[otherId];
                    // because the assoc exists the other side will have a slice
                    call = 1;
                }
            }
            if (call) {
                if (me.notify) {
                    me.notify.call(me.scope, me, otherId, state);
                }
                if (otherSlice && otherSlice.notify) {
                    otherSlice.notify.call(otherSlice.scope, otherSlice, id, state);
                }
            }
        }
    },
    updateId: function(newId) {
        var me = this,
            oldId = me.id,
            side = me.side,
            slices = side.slices,
            slice = slices[oldId],
            members = slice.members,
            index = side.index,
            otherSlices = side.inverse.slices,
            assoc, otherId, otherMembers;
        me.id = newId;
        slices[newId] = slice;
        delete slices[oldId];
        for (otherId in members) {
            assoc = members[otherId];
            assoc[index] = newId;
            otherMembers = otherSlices[otherId].members;
            otherMembers[newId] = otherMembers[oldId];
            delete otherMembers[oldId];
        }
    },
    destroy: function() {
        var me = this,
            store = me.store;
        if (store) {
            store.matrix = null;
            store.un('load', me.onStoreLoad, me);
        }
        me.notify = me.scope = me.store = me.side = me.members = null;
        me.callParent();
    }
});

/**
 * This class manages one side of a `Matrix`.
 * @private
 */
Ext.define('Ext.data.matrix.Side', {
    requires: [
        'Ext.data.matrix.Slice'
    ],
    /**
     * @property {Ext.data.matrix.Side} inverse
     * Reference to the opposite side of the matrix.
     * @readonly
     */
    constructor: function(matrix, index, role) {
        var me = this;
        /**
         * @property {Ext.data.matrix.Matrix} matrix
         * @readonly
         */
        me.matrix = matrix;
        /**
         * @property {Number} index
         * Either 0 or 1 which is the index of our id value in an association entry.
         * @readonly
         */
        me.index = index;
        /**
         * @property {Ext.data.schema.Role} role
         * The role for this side of the matrix.
         * @readonly
         */
        me.role = role;
        /**
         * @property {Object} slices
         * Keyed by the id for this side of the matrix to yield a `Slice`.
         * @readonly
         */
        me.slices = {};
    },
    commit: function() {
        var slices = this.slices,
            id;
        for (id in slices) {
            slices[id].commit();
        }
    },
    get: function(id1, id2) {
        var me = this,
            slices = me.slices,
            slice = slices[id1] || (slices[id1] = new Ext.data.matrix.Slice(me, id1));
        return (id2 || id2 === 0) ? slice.members[id2] : slice;
    },
    update: function(id1, id2, state) {
        var slice = this.get(id1);
        return slice.update(id2, state);
    },
    updateId: function(oldId, newId) {
        var slice = this.get(oldId);
        if (slice) {
            slice.updateId(newId);
        }
    },
    destroy: function() {
        var me = this,
            slices = me.slices,
            id;
        for (id in slices) {
            slices[id].destroy();
        }
        me.inverse = me.matrix = me.role = me.slices = null;
        me.callParent();
    }
});

/**
 * This class manages a many-to-many matrix for a `Session`.
 * @private
 */
Ext.define('Ext.data.matrix.Matrix', {
    requires: [
        'Ext.data.matrix.Side'
    ],
    /**
     * @property {Ext.data.schema.ManyToMany} association
     * The `ManyToMany` association for this matrix.
     * @readonly
     */
    /**
     * @property {Ext.data.Session} session
     * The `Session` owning this matrix.
     * @readonly
     */
    /*
     *      data: [
     *          [ leftId, rightId, -1/0/1 ],   // === DELETED/UNMODIFIED/ADDED
     *          ...
     *      ],
     *      
     *      left: new Ext.data.matrix.Side({
     *          matrix: me,
     *          index: 0,
     *          inverse: right,
     *          slices: {
     *              leftId: new Ext.data.matrix.Slice({
     *                  id: leftId,
     *                  side: left,
     *                  members: {
     *                      rightId: data[0]
     *                  }
     *              })
     *          }
     *      },
     *      
     *      right: new Ext.data.matrix.Side({
     *          matrix: me,
     *          index: 1,
     *          inverse: left,
     *          slices: {
     *              rightId: new Ext.data.matrix.Slice({
     *                  id: rightId,
     *                  side: right,
     *                  members: {
     *                      leftId: data[0]
     *                  }
     *              })
     *          })
     *      }
     */
    constructor: function(session, matrix) {
        var me = this,
            association = matrix.isManyToMany ? matrix : session.getSchema().getAssociation(matrix),
            Side = Ext.data.matrix.Side,
            left = new Side(me, 0, association.left),
            right = new Side(me, 1, association.right);
        Ext.Assert.truthy(association.isManyToMany, 'Association is not many-to-many');
        me.association = association;
        me.session = session;
        me.left = left;
        me.right = right;
        left.inverse = right;
        right.inverse = left;
    },
    commit: function() {
        this.left.commit();
        this.right.commit();
    },
    update: function(id1, id2, state) {
        return this.left.update(id1, id2, state);
    },
    updateId: function(record, oldId, newId) {
        var Type = record.self,
            left = this.left,
            right = this.right,
            matchSide;
        // Are we interested in this record? Check types
        if (Type === left.role.cls) {
            matchSide = left;
        }
        if (Type === right.role.cls) {
            matchSide = right;
        }
        if (matchSide) {
            matchSide.updateId(oldId, newId);
        }
    },
    destroy: function() {
        var me = this;
        me.left.destroy();
        me.right.destroy();
        me.association = me.session = me.left = me.right = null;
        me.callParent();
    }
});

/**
 * This class is used internally by `{@link Ext.data.Session#getChanges}` to build
 * up an object describing changes in the session. It is not intended for public use but
 * can be used as an example of the visitor `{@link Ext.data.Session#visitData}`
 * requires.
 * @protected
 * @since 5.0.0
 */
Ext.define('Ext.data.session.ChangesVisitor', {
    constructor: function(session) {
        var me = this,
            crud;
        me.session = session;
        crud = session.getCrudProperties();
        me.result = null;
        /* eslint-disable max-len */
        me.writerOptions = {};
        /*
             * Keyed by the $className of a Model, e.g. "Foo", and to cache data from
             * Foo.getProxy().getWriter (called "writer" in the pseudo code below):
             *
             *  Foo: {
             *      drop: {
             *          all: writer.getWriteAllFields(),
             *      },
             *      allDataOptions: Ext.apply(Ext.Object.chain(writer.getAllDataOptions()), {
             *          serialize: true
             *      }),
             *      partialDataOptions: Ext.apply(Ext.Object.chain(writer.getPartialDataOptions()), {
             *          serialize: true
             *      })
             *  }
             */
        /* eslint-enable max-len */
        me.createKey = crud.create;
        me.readKey = crud.read;
        me.updateKey = crud.update;
        me.dropKey = crud.drop;
    },
    onDirtyRecord: function(record) {
        var me = this,
            crud = me.crud,
            created = record.phantom,
            dropped = record.dropped,
            type = record.$className,
            prop = (created || dropped) ? 'allDataOptions' : 'partialDataOptions',
            writerOptions = me.writerOptions,
            name = record.entityName,
            options, bucket, entry, result;
        if (created && dropped) {
            return false;
        }
        crud = created ? me.createKey : (dropped ? me.dropKey : me.updateKey);
        writerOptions = writerOptions[type] || (writerOptions[type] = {});
        if (dropped) {
            // If the Writer says "writeAllFields" then we want to use allDataOptions
            // for the prop (set already). Otherwise we just want to encode the id.
            if (!(options = writerOptions.drop)) {
                writerOptions.drop = options = {
                    all: record.getProxy().getWriter().getWriteAllFields()
                };
            }
            if (!options.all) {
                entry = record.id;
            }
        }
        // else entry is unset so we'll ask for the prop and call record.getData
        if (!entry) {
            // Consult the Writer for the entity to determine its preferences for writing
            // complete or partial data. We rely on the serialization of the record's
            // getData method whereas the Writer has its own ideas on the matter.
            if (!(options = writerOptions[prop])) {
                options = record.getProxy().getWriter().getConfig(prop);
                writerOptions[prop] = options = Ext.Object.chain(options);
                me.setupOptions(options);
            }
            entry = record.getData(options);
        }
        //  User: {
        //      C: [
        //          { id: 20, name: 'Don' }
        //      ],
        //      U: [
        //          { id: 30, name: 'Don' }
        //      ],
        //      D: [ 40, 50 ]
        //  }
        result = me.result || (me.result = {});
        bucket = result[name] || (result[name] = {});
        bucket = bucket[crud] || (bucket[crud] = []);
        bucket.push(entry);
    },
    setupOptions: function(options) {
        options.serialize = true;
    },
    onMatrixChange: function(association, id1, id2, state) {
        var me = this,
            name = association.left.type,
            // e.g., "User"
            assocName = association.right.role,
            // e.g., "groups"
            operation = state < 0 ? me.dropKey : me.createKey,
            bucket, result;
        //  User: {
        //      groups: {
        //          C: {
        //              20: [ 30, 40 ]  // associate User 20 w/Groups 30 & 40
        //          },
        //          D: {
        //              10: [ 50 ]  // disassociate User 10 w/Group 50
        //          }
        //      }
        //  }
        result = me.result || (me.result = {});
        bucket = result[name] || (result[name] = {});
        // User
        bucket = bucket[assocName] || (bucket[assocName] = {});
        // groups
        bucket = bucket[operation] || (bucket[operation] = {});
        // C or D
        bucket = bucket[id1] || (bucket[id1] = []);
        bucket.push(id2);
    }
});

/**
 * This visitor class adds extra capability to consider changes as 
 * they would be considered for a parent session.
 * @protected
 * @since 5.0.0
 */
Ext.define('Ext.data.session.ChildChangesVisitor', {
    extend: 'Ext.data.session.ChangesVisitor',
    constructor: function() {
        this.seen = {};
        this.callParent(arguments);
    },
    setupOptions: function(options) {
        this.callParent([
            options
        ]);
        options.serialize = false;
    },
    onDirtyRecord: function(record) {
        if (this.callParent(arguments) !== false) {
            // We have a record that we have updated in ourselves, but not in the parent.
            // We need to read it in
            if (!record.$source && (record.dropped || !record.phantom)) {
                this.readEntity(record);
            }
        }
    },
    readEntity: function(record) {
        var me = this,
            readKey = me.readKey,
            name = record.entityName,
            id = record.id,
            seen = me.seen,
            seenKey = name + id,
            result, bucket;
        // Already read it, jump out
        if (seen[seenKey]) {
            return;
        }
        seen[seenKey] = true;
        result = me.result || (me.result = {});
        bucket = result[name] || (result[name] = {});
        bucket = bucket[readKey] || (bucket[readKey] = []);
        bucket.push(Ext.apply({}, record.modified, record.data));
    }
});

/**
 * This class is used internally by `{@link Ext.data.Session#getSaveBatch}` and is
 * not intended for direct use. It can be studied as an example of implementing a visitor
 * to pass to `{@link Ext.data.Session#visitData}`.
 * @protected
 * @since 5.0.0
 */
Ext.define('Ext.data.session.BatchVisitor', {
    map: null,
    constructor: function(batch) {
        this.batch = batch;
    },
    getBatch: function(sort) {
        var map = this.map,
            batch = this.batch,
            bucket, entity, name, operation, operationType, proxy, batchActions, records, len, i;
        if (map) {
            if (!batch) {
                batch = new Ext.data.Batch();
            }
            for (name in map) {
                bucket = map[name];
                entity = bucket.entity;
                // the entity class
                proxy = entity.getProxy();
                batchActions = proxy.getBatchActions();
                delete bucket.entity;
                // so we don't think its an operation
                for (operationType in bucket) {
                    if (batchActions) {
                        operation = proxy.createOperation(operationType, {
                            records: bucket[operationType]
                        });
                        operation.entityType = entity;
                        batch.add(operation);
                    } else {
                        records = bucket[operationType];
                        for (i = 0 , len = records.length; i < len; ++i) {
                            operation = proxy.createOperation(operationType, {
                                records: [
                                    records[i]
                                ]
                            });
                            operation.entityType = entity;
                            batch.add(operation);
                        }
                    }
                }
            }
        }
        if (batch && sort !== false) {
            batch.sort();
        }
        return batch;
    },
    onDirtyRecord: function(record) {
        var me = this,
            operation = record.phantom ? 'create' : (record.dropped ? 'destroy' : 'update'),
            name = record.$className,
            map = (me.map || (me.map = {})),
            bucket = (map[name] || (map[name] = {
                entity: record.self
            }));
        //  User: {
        //      entity: User,
        //      create: [
        //          { id: 20, name: 'Don' }
        //      ]
        //  }
        bucket = bucket[operation] || (bucket[operation] = []);
        bucket.push(record);
    }
});

/**
 * This mixin provides a `dirty` config that tracks the modified state of an object. If
 * the class using this mixin is {@link Ext.mixin.Observable observable}, changes to the
 * `dirty` config will fire the `dirtychange` event.
 * @protected
 * @since 6.2.0
 */
Ext.define('Ext.mixin.Dirty', {
    mixinId: 'dirty',
    /**
     * @event dirtychange
     * Fires when a change in the object's {@link #cfg-dirty} state is detected.
     *
     * **Note:** In order for this event to fire, the class that mixes in this mixin
     * must be `{@link Ext.mixin.Observable Observable}`.
     *
     * @param {Ext.Base} this
     * @param {Boolean} dirty Whether or not the object is now dirty.
     */
    config: {
        /**
         * @cfg {Boolean} dirty
         * This config property describes the modified state of this object. In most
         * cases this config's value is maintained by the object and should be considered
         * readonly. The class implementor should be the only one to call the setter.
         */
        dirty: {
            $value: false,
            lazy: true
        }
    },
    dirty: false,
    // on the prototype as false (not undefined)
    /**
     * @property {Number} _dirtyRecordCount
     * The number of newly created, modified or dropped records.
     * @private
     * @readonly
     */
    _dirtyRecordCount: 0,
    /**
     * @cfg {Boolean} ignoreDirty
     * This config property indicates that the `dirty` state of this object should be
     * ignored. Because this capability is mixed in at a class level, this config can
     * be helpful when some instances do not participate in dirty state tracking.
     *
     * This option should be set at construction time. When set to `true`, the object
     * will always have `dirty` value of `false`.
     */
    ignoreDirty: false,
    /**
     * @cfg {Boolean} recordStateIsDirtyState
     * Set this config at construction time (or on the class body) to automatically set
     * the `dirty` state based on the records passed to `trackRecordState`.
     *
     * This config defaults to `true` but only has an effect when the record tracking
     * methods are called (`trackRecordState`, `untrackRecordState` and `clearRecordStates`).
     * @protected
     */
    recordStateIsDirtyState: true,
    /**
     * Returns `true` if this object is `dirty`.
     */
    isDirty: function() {
        // This alias matches the Ext.form.field.* family.
        return this.getDirty();
    },
    applyDirty: function(dirty) {
        return this.ignoreDirty ? false : dirty;
    },
    updateDirty: function(dirty) {
        var me = this;
        // Store the property directly in case we are used in an "_dirty" world.
        me.dirty = dirty;
        if (me.fireEvent && !me.isDirtyInitializing) {
            me.fireDirtyChange();
        }
    },
    /**
     * Clears all record state tracking. This state is maintained by `trackRecordState`
     * and `untrackRecordState`.
     * @protected
     */
    clearRecordStates: function() {
        var me = this,
            counters = me._crudCounters;
        if (counters) {
            counters.C = counters.U = counters.D = 0;
        }
        me._dirtyRecordCount = 0;
        if (me.recordStateIsDirtyState) {
            me.setDirty(false);
        }
    },
    fireDirtyChange: function() {
        var me = this;
        if (!me.ignoreDirty && me.hasListeners.dirtychange) {
            me.fireEvent('dirtychange', me, me.dirty);
        }
    },
    /**
     * This method is called to track a given record in the total number of dirty records
     * (modified, created or dropped). See `untrackRecordState` and `clearRecordStates`.
     *
     * @param {Ext.data.Model} record The record to track.
     * @param {Boolean} initial Pass `true` the first time a record is introduced.
     * @return {Boolean} Returns `true` if the state of dirty records has changed.
     * @protected
     */
    trackRecordState: function(record, initial) {
        var me = this,
            counters = me._crudCounters || (me._crudCounters = {
                C: 0,
                R: 0,
                U: 0,
                D: 0
            }),
            dirtyRecordCountWas = me._dirtyRecordCount,
            state = record.crudState,
            stateWas = record.crudStateWas,
            changed, dirtyRecordCount;
        if (initial || state !== stateWas) {
            if (!initial && stateWas) {
                --counters[stateWas];
            }
            if (!(record.phantom && state === 'D')) {
                ++counters[state];
            }
            me.checkCounters();
            me._dirtyRecordCount = dirtyRecordCount = counters.C + counters.U + counters.D;
            changed = !dirtyRecordCount !== !dirtyRecordCountWas;
            if (changed && me.recordStateIsDirtyState) {
                me.setDirty(dirtyRecordCount > 0);
            }
        }
        return changed;
    },
    /**
     * This method is called to remove the tracking of a given record from the total number
     * of dirty records (modified, created or dropped). The record passed to this method
     * must have been previously passed to `trackRecordState`.
     *
     * @param {Ext.data.Model} record The record to stop tracking.
     * @return {Boolean} Returns `true` if the state of dirty records has changed.
     * @protected
     */
    untrackRecordState: function(record) {
        var me = this,
            counters = me._crudCounters,
            dirtyRecordCountWas = me._dirtyRecordCount,
            state = record.crudState,
            changed, dirtyRecordCount;
        // If it's erased and dropped, it will have already been tracked
        if (counters && state !== 'D' && !record.erased) {
            --counters[state];
            me.checkCounters();
            me._dirtyRecordCount = dirtyRecordCount = counters.C + counters.U + counters.D;
            changed = !dirtyRecordCount !== !dirtyRecordCountWas;
            if (changed && me.recordStateIsDirtyState) {
                me.setDirty(dirtyRecordCount > 0);
            }
        }
        return changed;
    },
    checkCounters: function() {
        // eslint-disable-line comma-style
        var counters = this._crudCounters,
            key;
        for (key in counters) {
            if (counters[key] < 0) {
                Ext.raise('Invalid state for ' + key);
            }
        }
    }
});

/**
 * This class manages models and their associations. Instances of `Session` are typically
 * associated with some `Component` (perhaps the Viewport or a Window) and then used by
 * their `{@link Ext.app.ViewModel view models}` to enable data binding.
 *
 * The primary job of a Session is to manage a collection of records of many different
 * types and their associations. This often starts by loading records when requested (via
 * bind - see below) and culminates when it is time to save to the server.
 *
 * Because the Session tracks all records it loads, it ensures that for any given type of
 * model, only one record exists with a given `id`. This means that all edits of that
 * record are properly targeted at that one instance.
 *
 * Similarly, when associations are loaded, the `Ext.data.Store` created to hold the
 * associated records is tracked by the Session. So all requests for the "OrderItems" of
 * a particular Order id will result in the same Store. Adding and removing items from
 * that Order then is sure to remain consistent.
 *
 * # Data
 *
 * Since the Session is managing all this data, there are several methods it provides
 * to give convenient access to that data. The most important of these is `update` and
 * `getChanges`.
 *
 * The `update` and `getChanges` methods both operate on object that contains a summary
 * of records and associations and different CRUD operations.
 *
 * ## Saving
 *
 * There are two basic ways to save the contents of a Session: `getChanges` and
 * `getSaveBatch`. We've already seen `getChanges`. The data contained in the CRUD object
 * can be translated into whatever shape is needed by the server.
 *
 * To leverage the `{@link Ext.data.Model#proxy proxy}` facilities defined by each Model
 * class, there is the `getSaveBatch` method. That method returns an `Ext.data.Batch`
 * object populated with the necessary `create`, `update` and `destroy` operations to
 * save all of the changes in the Session.
 *
 * ## Conflicts
 *
 * If data is loaded from the server (for example a store load) and there is an existing record,
 * the {@link Ext.data.Model#method-mergeData `mergeData`} method will be called to resolve
 * the conflict.
 * 
 * @since 5.0.0
 */
Ext.define('Ext.data.Session', {
    requires: [
        'Ext.data.schema.Schema',
        'Ext.data.Batch',
        'Ext.data.matrix.Matrix',
        'Ext.data.session.ChangesVisitor',
        'Ext.data.session.ChildChangesVisitor',
        'Ext.data.session.BatchVisitor'
    ],
    mixins: [
        'Ext.mixin.Dirty',
        'Ext.mixin.Observable'
    ],
    isSession: true,
    config: {
        /**
         * @cfg {String/Ext.data.schema.Schema} schema
         */
        schema: 'default',
        /**
         * @cfg {Ext.data.Session} parent
         * The parent session for this session.
         */
        parent: null,
        /**
         * @cfg {Boolean} autoDestroy
         * `true` to automatically destroy this session when a component it is attached
         * to is destroyed. This should be set to false if the session is intended to be
         * used across multiple root level components.
         *
         * @since 5.0.1
         */
        autoDestroy: true,
        crudProperties: {
            create: 'C',
            read: 'R',
            update: 'U',
            drop: 'D'
        }
    },
    crudOperations: [
        {
            type: 'R',
            entityMethod: 'readEntities'
        },
        {
            type: 'C',
            entityMethod: 'createEntities'
        },
        {
            type: 'U',
            entityMethod: 'updateEntities'
        },
        {
            type: 'D',
            entityMethod: 'dropEntities'
        }
    ],
    crudKeys: {
        C: 1,
        R: 1,
        U: 1,
        D: 1
    },
    statics: {
        nextId: 1
    },
    constructor: function(config) {
        var me = this;
        /*
         * {
         *     User: {
         *         1: {
         *             record: user1Instance,
         *             refs: {
         *                 posts: {
         *                     101: post101Instance,
         *                     102: post202Instance
         *                 }
         *             }
         *         }
         *     }
         * }
         */
        me.data = {};
        /*
         *  {
         *      UserGroups: new Ext.data.matrix.Matrix({
         *          association: UserGroups
         *      })
         *  }
         */
        me.matrices = {};
        me.id = Ext.data.Session.nextId++;
        me.identifierCache = {};
        // Bind ourselves so we're always called in our own scope.
        me.recordCreator = me.recordCreator.bind(me);
        me.mixins.observable.constructor.call(me, config);
    },
    destroy: function() {
        var me = this,
            matrices = me.matrices,
            data = me.data,
            entityName, entities, record, id;
        for (id in matrices) {
            matrices[id].destroy();
        }
        for (entityName in data) {
            entities = data[entityName];
            for (id in entities) {
                record = entities[id].record;
                if (record) {
                    // Clear up any source if we pushed one on, remove
                    // the session reference
                    record.$source = null;
                    // While we don't actually call join() for the session, we need to
                    // tell the records that they are being detached from the session in
                    // some way.
                    record.unjoin(me);
                }
            }
        }
        // see also evict()
        me.identifierCache = me.recordCreator = me.matrices = me.data = null;
        me.setSchema(null);
        me.callParent();
    },
    /**
     * Adds an existing record instance to the session. The record
     * may not belong to another session. The record cannot be a phantom record, instead
     * use {@link #createRecord}.
     * @param {Ext.data.Model} record The record to adopt.
     */
    adopt: function(record) {
        var me = this,
            associations = record.associations,
            roleName;
        me.checkModelType(record.self);
        if (record.session && record.session !== me) {
            Ext.raise('Record already belongs to an existing session');
        }
        if (record.session !== me) {
            me.add(record);
            if (associations) {
                for (roleName in associations) {
                    associations[roleName].adoptAssociated(record, me);
                }
            }
        }
    },
    /**
     * Marks the session as "clean" by calling {@link Ext.data.Model#commit} on each record
     * that is known to the session.
     *
     * - Phantom records will no longer be phantom.
     * - Modified records will no longer be dirty.
     * - Dropped records will be erased.
     *
     * @since 5.1.0
     */
    commit: function() {
        var me = this,
            data = me.data,
            matrices = me.matrices,
            dirtyWas = me.getDirty(),
            entityName, entities, id, record;
        me.suspendEvent('dirtychange');
        for (entityName in data) {
            entities = data[entityName];
            for (id in entities) {
                record = entities[id].record;
                if (record) {
                    record.commit();
                }
            }
        }
        for (id in matrices) {
            matrices[id].commit();
        }
        me.clearRecordStates();
        me.resumeEvent('dirtychange');
        if (me.getDirty() !== dirtyWas) {
            me.fireDirtyChange();
        }
    },
    /**
     * Creates a new record and tracks it in this session.
     *
     * @param {String/Ext.Class} type The `entityName` or the actual class of record to create.
     * @param {Object} [data] The data for the record.
     * @param {Boolean} [preventAdd] (private) `true` to prevent the record from being added
     * to the session
     * @return {Ext.data.Model} The new record.
     */
    createRecord: function(type, data, preventAdd) {
        this.checkModelType(type);
        /* eslint-disable-next-line vars-on-top */
        var Model = type.$isClass ? type : this.getSchema().getEntity(type),
            parent = this.getParent(),
            id;
        // If we have no data, we're creating a phantom
        if (data && parent) {
            id = Model.getIdFromData(data);
            if (parent.peekRecord(Model, id)) {
                Ext.raise('A parent session already contains an entry for ' + Model.entityName + ': ' + id);
            }
        }
        // By passing the session to the constructor, it will call session.add()
        return new Model(data, preventAdd ? null : this);
    },
    /**
     * Returns an object describing all of the modified fields, created or dropped records
     * and many-to-many association changes maintained by this session.
     *
     * @return {Object} An object in the CRUD format (see the intro docs). `null` if there are
     * no changes.
     */
    getChanges: function() {
        var visitor = new Ext.data.session.ChangesVisitor(this);
        this.visitData(visitor);
        return visitor.result;
    },
    /**
     * The same functionality as {@link #getChanges}, however we also take into account our
     * parent session.
     * 
     * @return {Object} An object in the CRUD format (see the intro docs). `null` if there are
     * no changes.
     *
     * @protected
     */
    getChangesForParent: function() {
        var visitor = new Ext.data.session.ChildChangesVisitor(this);
        this.visitData(visitor);
        return visitor.result;
    },
    /**
     * Get a cached record from the session. If the record does not exist, it will
     * be created. If the `autoLoad` parameter is not set to `false`, the record will
     * be loaded via the {@link Ext.data.Model#proxy proxy} of the Model. 
     * 
     * If this session is configured with a `{@link #parent}` session, a *copy* of any existing
     * record  in the `parent` will be adopted into this session. If the `parent` does not contain
     * the record, the record will be created and *not* inserted into the parent.
     * 
     * See also {@link #peekRecord}.
     *
     * @param {String/Ext.Class/Ext.data.Model} type The `entityName` or the actual class of record
     * to create. This may also be a record instance, where the type and id will be inferred from
     * the record. If the record is not attached to a session, it will be adopted. If it exists
     * in a parent, an appropriate copy will be made as described.
     * @param {Object} id The id of the record.
     * @param {Boolean/Object} [autoLoad=true] `false` to prevent the record from being loaded if
     * it does not exist. If this parameter is an object, it will be passed to the
     * {@link Ext.data.Model#method!load} call.
     * @return {Ext.data.Model} The record.
     */
    getRecord: function(type, id, autoLoad) {
        var me = this,
            wasInstance = type.isModel,
            record, Model, parent, parentRec;
        if (wasInstance) {
            wasInstance = type;
            id = type.id;
            type = type.self;
        }
        record = me.peekRecord(type, id);
        if (!record) {
            Model = type.$isClass ? type : me.getSchema().getEntity(type);
            parent = me.getParent();
            if (parent) {
                parentRec = parent.peekRecord(Model, id);
            }
            if (parentRec) {
                if (parentRec.isLoading()) {
                    // If the parent is loading, it's as though it doesn't have
                    // the record, so we can't copy it, but we don't want to
                    // adopt it either.
                    wasInstance = false;
                } else {
                    record = parentRec.copy(undefined, me);
                    record.$source = parentRec;
                }
            }
            if (!record) {
                if (wasInstance) {
                    record = wasInstance;
                    me.adopt(record);
                } else {
                    record = Model.createWithId(id, null, me);
                    if (autoLoad !== false) {
                        record.load(Ext.isObject(autoLoad) ? autoLoad : undefined);
                    }
                }
            }
        }
        return record;
    },
    /**
     * Returns an `Ext.data.Batch` containing the `Ext.data.operation.Operation` instances
     * that are needed to save all of the changes in this session. This sorting is based
     * on operation type, associations and foreign keys. Generally speaking the operations
     * in the batch can be committed to a server sequentially and the server will never be
     * sent a request with an invalid (client-generated) id in a foreign key field.
     *
     * @param {Boolean} [sort=true] Pass `false` to disable the batch operation sort.
     * @return {Ext.data.Batch}
     */
    getSaveBatch: function(sort) {
        var visitor = new Ext.data.session.BatchVisitor();
        this.visitData(visitor);
        return visitor.getBatch(sort);
    },
    /**
     * Triggered when an associated item from {@link #update} references a record
     * that does not exist in the session.
     * @param {Ext.Class} entityType The entity type.
     * @param {Object} id The id of the model.
     *
     * @protected
     * @template
     */
    onInvalidAssociationEntity: function(entityType, id) {
        Ext.raise('Unable to read association entity: ' + this.getModelIdentifier(entityType, id));
    },
    /**
     * Triggered when an drop block from {@link #update} tries to create a record
     * that already exists.
     * @param {Ext.Class} entityType The entity type.
     * @param {Object} id The id of the model.
     *
     * @protected
     * @template
     */
    onInvalidEntityCreate: function(entityType, id) {
        Ext.raise('Cannot create, record already not exists: ' + this.getModelIdentifier(entityType, id));
    },
    /**
     * Triggered when an drop block from {@link #update} references a record
     * that does not exist in the session.
     * @param {Ext.Class} entityType The entity type.
     * @param {Object} id The id of the model.
     *
     * @protected
     * @template
     */
    onInvalidEntityDrop: function(entityType, id) {
        Ext.raise('Cannot drop, record does not exist: ' + this.getModelIdentifier(entityType, id));
    },
    /**
     * Triggered when an drop block from {@link #update} tries to create a record
     * that already exists.
     * @param {Ext.Class} entityType The entity type.
     * @param {Object} id The id of the model.
     *
     * @protected
     * @template
     */
    onInvalidEntityRead: function(entityType, id) {
        Ext.raise('Cannot read, record already not exists: ' + this.getModelIdentifier(entityType, id));
    },
    /**
     * Triggered when an update block from {@link #update} references a record
     * that does not exist in the session.
     * @param {Ext.Class} entityType The entity type.
     * @param {Object} id The id of the model.
     * @param {Boolean} dropped `true` if the record was dropped.
     *
     * @protected
     * @template
     */
    onInvalidEntityUpdate: function(entityType, id, dropped) {
        if (dropped) {
            Ext.raise('Cannot update, record dropped: ' + this.getModelIdentifier(entityType, id));
        } else {
            Ext.raise('Cannot update, record does not exist: ' + this.getModelIdentifier(entityType, id));
        }
    },
    /**
     * Gets an existing record from the session. The record will *not* be created if it does
     * not exist.
     *
     * See also: {@link #getRecord}.
     * 
     * @param {String/Ext.Class} type The `entityName` or the actual class of record to create.
     * @param {Object} id The id of the record.
     * @param {Boolean} [deep=false] `true` to consult 
     * @return {Ext.data.Model} The record, `null` if it does not exist.
     */
    peekRecord: function(type, id, deep) {
        // Duplicate some of this logic from getEntry here to prevent the creation
        // of entries when asking for the existence of records. We may not need them
        this.checkModelType(type);
        /* eslint-disable-next-line vars-on-top */
        var entityType = type.$isClass ? type : this.getSchema().getEntity(type),
            entityName = entityType.entityName,
            entry = this.data[entityName],
            ret, parent;
        entry = entry && entry[id];
        ret = entry && entry.record;
        if (!ret && deep) {
            parent = this.getParent();
            ret = parent && parent.peekRecord(type, id, deep);
        }
        return ret || null;
    },
    /**
     * Save any changes in this session to a {@link #parent} session.
     */
    save: function() {
        var me = this,
            parent = me.getParent(),
            visitor;
        if (parent) {
            visitor = new Ext.data.session.ChildChangesVisitor(me);
            me.visitData(visitor);
            parent.update(visitor.result);
            me.commit();
        } else {
            Ext.raise('Cannot commit session, no parent exists');
        }
    },
    /**
     * Create a child session with this session as the {@link #parent}.
     * @return {Ext.data.Session} The copied session.
     */
    spawn: function() {
        return new this.self({
            schema: this.getSchema(),
            parent: this
        });
    },
    /**
     * Complete a bulk update for this session.
     * @param {Object} data Data in the CRUD format (see the intro docs).
     */
    update: function(data) {
        var me = this,
            schema = me.getSchema(),
            crudOperations = me.crudOperations,
            len = crudOperations.length,
            crudKeys = me.crudKeys,
            dirtyWas = me.getDirty(),
            entityName, entityType, entityInfo, i, operation, item, associations, key, role, associationData;
        me.suspendEvent('dirtychange');
        // Force the schema to process any pending drops
        me.getSchema().processKeyChecks(true);
        // Do a first pass to setup all the entities first
        for (entityName in data) {
            entityType = schema.getEntity(entityName);
            if (!entityType) {
                Ext.raise('Invalid entity type: ' + entityName);
            }
            entityInfo = data[entityName];
            for (i = 0; i < len; ++i) {
                operation = crudOperations[i];
                item = entityInfo[operation.type];
                if (item) {
                    me[operation.entityMethod](entityType, item);
                }
            }
        }
        // A second pass to process associations once we have all the entities in place
        for (entityName in data) {
            entityType = schema.getEntity(entityName);
            associations = entityType.associations;
            entityInfo = data[entityName];
            for (key in entityInfo) {
                // Skip over CRUD, just looking for associations here
                if (crudKeys[key]) {
                    
                    continue;
                }
                role = associations[key];
                if (!role) {
                    Ext.raise('Invalid association key for ' + entityName + ', "' + key + '"');
                }
                associationData = entityInfo[role.role];
                role.processUpdate(me, associationData);
            }
        }
        me.resumeEvent('dirtychange');
        if (me.getDirty() !== dirtyWas) {
            me.fireDirtyChange();
        }
    },
    //-------------------------------------------------------------------------
    /**
     * Template method, will be called by Model after a record is committed.
     * @param {Ext.data.Model} record The record.
     *
     * @protected
     * @since 6.2.0
     */
    afterCommit: function(record) {
        this.trackRecordState(record);
    },
    /**
     * Template method, will be called by Model after a record is dropped.
     * @param {Ext.data.Model} record The record.
     *
     * @protected
     * @since 6.2.0
     */
    afterDrop: function(record) {
        this.trackRecordState(record);
    },
    /**
     * Template method, will be called by Model after a record is edited.
     * @param {Ext.data.Model} record The record.
     *
     * @protected
     * @since 6.2.0
     */
    afterEdit: function(record) {
        this.trackRecordState(record);
    },
    /**
     * Template method, will be called by Model after a record is erased (a drop
     * that is committed).
     * @param {Ext.data.Model} record The record.
     *
     * @protected
     */
    afterErase: function(record) {
        this.evict(record);
    },
    /**
     * Template method, will be called by Model after a record is rejected.
     * @param {Ext.data.Model} record The record.
     *
     * @protected
     * @since 6.5.1
     */
    afterReject: function(record) {
        this.trackRecordState(record);
    },
    privates: {
        /**
         * Add a record instance to this session. Called by model.
         * @param {Ext.data.Model} record The record.
         *
         * @private
         */
        add: function(record) {
            var me = this,
                id = record.id,
                entry = me.getEntry(record.self, id),
                associations, roleName;
            if (entry.record) {
                Ext.raise('Duplicate id ' + record.id + ' for ' + record.entityName);
            }
            record.session = me;
            entry.record = record;
            me.trackRecordState(record, true);
            me.registerReferences(record);
            associations = record.associations;
            for (roleName in associations) {
                associations[roleName].checkMembership(me, record);
            }
        },
        /**
         * @private
         */
        applySchema: function(schema) {
            return Ext.data.schema.Schema.get(schema);
        },
        /**
         * Checks if the model type being referenced is valid for this session. That includes
         * checking if the model name is correct & is one used in this {@link #schema} for this
         * session. Will raise an exception if the model type is not correct.
         * @param {String/Ext.Class} name The model name or model type.
         *
         * @private
         */
        checkModelType: function(name) {
            if (name.$isClass) {
                name = name.entityName;
            }
            if (!name) {
                Ext.raise('Unable to use anonymous models in a Session');
            } else if (!this.getSchema().getEntity(name)) {
                Ext.raise('Unknown entity type ' + name);
            }
        },
        /**
         * Process a create block of entities from the {@link #update} method.
         * @param {Ext.Class} entityType The entity type.
         * @param {Object[]} items The data objects to create.
         *
         * @private
         */
        createEntities: function(entityType, items) {
            var me = this,
                len = items.length,
                i, data, rec, id;
            for (i = 0; i < len; ++i) {
                data = items[i];
                id = entityType.getIdFromData(data);
                rec = me.peekRecord(entityType, id);
                if (!rec) {
                    // Wait until after creating the record before adding it to the session,
                    // instead of allowing the Model constructor to call session.add().
                    // This allows us to first initialize the phantom and crudState properties.
                    // so that the session sets its dirty state correctly when add() is called.
                    // The Model constructor usually handles setting phantom/crudState,
                    // but in this case it will not detect the record as phantom because
                    // we are passing an id (generated by the child session) to the Model
                    // constructor.
                    rec = me.createRecord(entityType, data, true);
                    rec.phantom = true;
                    rec.crudState = 'C';
                    me.add(rec);
                    // Be sure to set this after "notifying" the session.
                    rec.crudStateWas = 'C';
                } else {
                    me.onInvalidEntityCreate(entityType, id);
                }
            }
        },
        /**
         * Process a drop block for entities from the {@link #update} method.
         * @param {Ext.Class} entityType The entity type.
         * @param {Object[]} ids The identifiers of the items to drop.
         *
         * @private
         */
        dropEntities: function(entityType, ids) {
            var len = ids.length,
                i, rec, id, extractId;
            if (len) {
                // Handle writeAllFields here, we may not have an array of raw ids
                extractId = Ext.isObject(ids[0]);
            }
            for (i = 0; i < len; ++i) {
                id = ids[i];
                if (extractId) {
                    id = entityType.getIdFromData(id);
                }
                rec = this.peekRecord(entityType, id);
                if (rec) {
                    rec.drop();
                } else {
                    this.onInvalidEntityDrop(entityType, id);
                }
            }
        },
        /**
         * Remove a record and any references from the session.
         * @param {Ext.data.Model} record The record
         *
         * @private
         */
        evict: function(record) {
            var me = this,
                entityName = record.entityName,
                entities = me.data[entityName],
                id = record.id;
            if (entities && entities[id]) {
                me.untrackRecordState(record);
                // While we don't actually call join() for the session, we need to
                // tell the records that they are being detached from the session in
                // some way.
                record.unjoin(me);
                delete entities[id];
            }
        },
        // see also destroy()
        /**
         * Transforms a list of ids into a list of records for a particular type.
         * @param {Ext.Class} entityType The entity type.
         * @param {Object[]} ids The ids to transform.
         * @return {Ext.data.Model[]} The models corresponding to the ids.
         */
        getEntityList: function(entityType, ids) {
            var len = ids.length,
                i, id, rec, invalid;
            for (i = 0; i < len; ++i) {
                id = ids[i];
                rec = this.peekRecord(entityType, id);
                if (rec) {
                    ids[i] = rec;
                } else {
                    invalid = true;
                    ids[i] = null;
                    this.onInvalidAssociationEntity(entityType, id);
                }
            }
            if (invalid) {
                ids = Ext.Array.clean(ids);
            }
            return ids;
        },
        /**
         * Return an entry for the data property for a particular type/id.
         * @param {String/Ext.Class} type The entity name or model type.
         * @param {Object} id The id of the record
         * @return {Object} The data entry.
         *
         * @private
         */
        getEntry: function(type, id) {
            if (type.isModel) {
                id = type.getId();
                type = type.self;
            }
            /* eslint-disable-next-line vars-on-top */
            var entityType = type.$isClass ? type : this.getSchema().getEntity(type),
                entityName = entityType.entityName,
                data = this.data,
                entry;
            entry = data[entityName] || (data[entityName] = {});
            entry = entry[id] || (entry[id] = {});
            return entry;
        },
        getRefs: function(record, role, includeParent) {
            var entry = this.getEntry(record),
                refs = entry && entry.refs && entry.refs[role.role],
                parent = includeParent && this.getParent(),
                parentRefs, id, rec;
            if (parent) {
                parentRefs = parent.getRefs(record, role);
                if (parentRefs) {
                    for (id in parentRefs) {
                        rec = parentRefs[id];
                        if ((!refs || !refs[id])) {
                            // We don't know about this record but the parent does. We need to
                            // pull it down so it may be edited as part of the collection
                            this.getRecord(rec.self, rec.id);
                        }
                    }
                    // Recalculate our refs after we pull down all the required records
                    refs = entry && entry.refs && entry.refs[role.role];
                }
            }
            return refs || null;
        },
        getIdentifier: function(entityType) {
            var parent = this.getParent(),
                cache, identifier, key, ret;
            if (parent) {
                ret = parent.getIdentifier(entityType);
            } else {
                cache = this.identifierCache;
                identifier = entityType.identifier;
                key = identifier.getId() || entityType.entityName;
                ret = cache[key];
                if (!ret) {
                    if (identifier.clone) {
                        ret = identifier.clone({
                            id: null
                        });
                    } else {
                        ret = identifier;
                    }
                    cache[key] = ret;
                }
            }
            return ret;
        },
        getMatrix: function(matrix, preventCreate) {
            var name = matrix.isManyToMany ? matrix.name : matrix,
                matrices = this.matrices,
                ret;
            ret = matrices[name];
            if (!ret && !preventCreate) {
                ret = matrices[name] = new Ext.data.matrix.Matrix(this, matrix);
            }
            return ret || null;
        },
        getMatrixSlice: function(role, id) {
            var matrix = this.getMatrix(role.association),
                side = matrix[role.side];
            return side.get(id);
        },
        /**
         * Gets a user friendly identifier for a Model.
         * @param {Ext.Class} entityType The entity type.
         * @param {Object} id The id of the entity.
         * @return {String} The identifier.
         */
        getModelIdentifier: function(entityType, id) {
            return id + '@' + entityType.entityName;
        },
        onIdChanged: function(record, oldId, newId) {
            var me = this,
                matrices = me.matrices,
                entityName = record.entityName,
                id = record.id,
                bucket = me.data[entityName],
                entry = bucket[oldId],
                associations = record.associations,
                refs = entry.refs,
                setNoRefs = me._setNoRefs,
                association, fieldName, refId, role, roleName, roleRefs, key;
            if (bucket[newId]) {
                Ext.raise('Cannot change ' + entityName + ' id from ' + oldId + ' to ' + newId + ' id already exists');
            }
            delete bucket[oldId];
            bucket[newId] = entry;
            for (key in matrices) {
                matrices[key].updateId(record, oldId, newId);
            }
            if (refs) {
                for (roleName in refs) {
                    roleRefs = refs[roleName];
                    role = associations[roleName];
                    association = role.association;
                    if (!association.isManyToMany) {
                        fieldName = association.field.name;
                        for (refId in roleRefs) {
                            roleRefs[refId].set(fieldName, id, setNoRefs);
                        }
                    }
                }
            }
            me.registerReferences(record, oldId);
        },
        processManyBlock: function(entityType, role, items, processor) {
            var me = this,
                id, record, records, store;
            if (items) {
                for (id in items) {
                    record = me.peekRecord(entityType, id);
                    if (record) {
                        records = me.getEntityList(role.cls, items[id]);
                        store = role.getAssociatedItem(record);
                        me[processor](role, store, record, records);
                    } else {
                        me.onInvalidAssociationEntity(entityType, id);
                    }
                }
            }
        },
        processManyCreate: function(role, store, record, records) {
            if (store) {
                // Will handle any duplicates
                store.add(records);
            } else {
                record[role.getterName](null, null, records);
            }
        },
        processManyDrop: function(role, store, record, records) {
            if (store) {
                store.remove(records);
            }
        },
        processManyRead: function(role, store, record, records) {
            if (store) {
                store.setRecords(records);
            } else {
                // We don't have a store. Create it and add the records.
                record[role.getterName](null, null, records);
            }
        },
        /**
         * Process a read block of entities from the {@link #update} method.
         * @param {Ext.Class} entityType The entity type.
         * @param {Object[]} items The data objects to read.
         *
         * @private
         */
        readEntities: function(entityType, items) {
            var me = this,
                len = items.length,
                i, data, rec, id;
            for (i = 0; i < len; ++i) {
                data = items[i];
                id = entityType.getIdFromData(data);
                rec = me.peekRecord(entityType, id);
                if (!rec) {
                    rec = me.createRecord(entityType, data, true);
                } else {
                    me.onInvalidEntityRead(entityType, id);
                }
                // We've been read from a "server", so we aren't a phantom,
                // regardless of whether or not we have an id
                rec.phantom = false;
                me.add(rec);
            }
        },
        recordCreator: function(data, Model) {
            var me = this,
                id = Model.getIdFromData(data),
                record = me.peekRecord(Model, id, true);
            // It doesn't exist anywhere, create it
            if (!record) {
                // We may have a stub that is loading the record (in fact this may be the
                // call coming from that Reader), but the resolution is simple. By creating
                // the record it is registered in the data[entityName][id] entry anyway
                // and the stub will deal with it onLoad.
                record = new Model(data, me);
            } else {
                record = me.getRecord(Model, id);
                record.mergeData(data);
            }
            return record;
        },
        registerReferences: function(record, oldId) {
            var entityName = record.entityName,
                // eslint-disable-line no-unused-vars
                id = record.id,
                recordData = record.data,
                remove = oldId || oldId === 0,
                entry, i, fk, len, reference, references, refs, roleName;
            // Register this records references to other records
            len = (references = record.references).length;
            for (i = 0; i < len; ++i) {
                reference = references[i];
                // e.g., an orderId field
                fk = recordData[reference.name];
                // the orderId
                if (fk || fk === 0) {
                    reference = reference.reference;
                    // the "order" association role
                    entityName = reference.type;
                    roleName = reference.inverse.role;
                    // Track down the entry for the associated record
                    entry = this.getEntry(reference.cls, fk);
                    refs = entry.refs || (entry.refs = {});
                    refs = refs[roleName] || (refs[roleName] = {});
                    refs[id] = record;
                    if (remove) {
                        delete refs[oldId];
                    }
                }
            }
        },
        /**
         * Process an update block for entities from the {@link #update} method.
         * @param {Ext.Class} entityType The entity type.
         * @param {Object[]} items The data objects to update.
         *
         * @private
         */
        updateEntities: function(entityType, items) {
            var len = items.length,
                i, data, rec, id, modified;
            // eslint-disable-line no-unused-vars
            // Repeating some code here, but we want to optimize this for speed
            if (Ext.isArray(items)) {
                for (i = 0; i < len; ++i) {
                    data = items[i];
                    id = entityType.getIdFromData(data);
                    rec = this.peekRecord(entityType, id);
                    if (rec) {
                        rec.set(data);
                    } else {
                        this.onInvalidEntityUpdate(entityType, id);
                    }
                }
            } else {
                for (id in items) {
                    data = items[id];
                    rec = this.peekRecord(entityType, id);
                    if (rec && !rec.dropped) {
                        modified = rec.set(data);
                    } else {
                        this.onInvalidEntityUpdate(entityType, id, !!rec);
                    }
                }
            }
        },
        updateReference: function(record, field, newValue, oldValue) {
            var reference = field.reference,
                entityName = reference.type,
                roleName = reference.inverse.role,
                id = record.id,
                entry, refs;
            if (oldValue || oldValue === 0) {
                // We must be already in this entry.refs collection
                refs = this.getEntry(entityName, oldValue).refs[roleName];
                delete refs[id];
            }
            if (newValue || newValue === 0) {
                entry = this.getEntry(entityName, newValue);
                refs = entry.refs || (entry.refs = {});
                refs = refs[roleName] || (refs[roleName] = {});
                refs[id] = record;
            }
        },
        /**
         * Walks the internal data tracked by this session and calls methods on the provided
         * `visitor` object. The visitor can then accumulate whatever data it finds important.
         * The visitor object can provide a number of methods, but all are optional.
         *
         * This method does not enumerate associations since these can be traversed given the
         * records that are enumerated. For many-to-many associations, however, this method
         * does enumerate the changes because these changes are not "owned" by either side of
         * such associations.
         *
         * @param {Object} visitor
         * @param {Function} [visitor.onCleanRecord] This method is called to describe a record
         * that is known but unchanged.
         * @param {Ext.data.Model} visitor.onCleanRecord.record The unmodified record.
         * @param {Function} [visitor.onDirtyRecord] This method is called to describe a record
         * that has either been created, dropped or modified.
         * @param {Ext.data.Model} visitor.onDirtyRecord.record The modified record.
         * @param {Function} [visitor.onMatrixChange] This method is called to describe a
         * change in a many-to-many association (a "matrix").
         * @param {Ext.data.schema.Association} visitor.onMatrixChange.association The object
         * describing the many-to-many ("matrix") association.
         * @param {Mixed} visitor.onMatrixChange.leftId The `idProperty` of the record on the
         * "left" of the association.
         * @param {Mixed} visitor.onMatrixChange.rightId The `idProperty` of the record on the
         * "right" of the association.
         * @param {Number} visitor.onMatrixChange.state A negative number if the two records
         * are being disassociated or a positive number if they are being associated. For
         * example, when adding User 10 to Group 20, this would be 1. When removing the User
         * this argument would be -1.
         * @return {Object} The visitor instance
         */
        visitData: function(visitor) {
            var me = this,
                data = me.data,
                matrices = me.matrices,
                all, assoc, id, id2, matrix, members, name, record, slice, slices, state;
            // Force the schema to process any pending drops
            me.getSchema().processKeyChecks(true);
            for (name in data) {
                all = data[name];
                // all entities of type "name"
                for (id in all) {
                    record = all[id].record;
                    if (record) {
                        if (record.phantom || record.dirty || record.dropped) {
                            if (visitor.onDirtyRecord) {
                                visitor.onDirtyRecord(record);
                            }
                        } else if (visitor.onCleanRecord) {
                            visitor.onCleanRecord(record);
                        }
                    }
                }
            }
            if (visitor.onMatrixChange) {
                for (name in matrices) {
                    matrix = matrices[name].left;
                    // e.g., UserGroups.left (Users)
                    slices = matrix.slices;
                    assoc = matrix.role.association;
                    for (id in slices) {
                        slice = slices[id];
                        members = slice.members;
                        for (id2 in members) {
                            state = (record = members[id2])[2];
                            if (state) {
                                visitor.onMatrixChange(assoc, record[0], record[1], state);
                            }
                        }
                    }
                }
            }
            return visitor;
        },
        //---------------------------------------------------------------------
        // Record callbacks called because we are the "session" for the record.
        _setNoRefs: {
            refs: false
        }
    }
});

/**
 * This is a base class for objects that can be managed by `Ext.util.Scheduler`.
 * @private
 */
Ext.define('Ext.util.Schedulable', {
    'abstract': true,
    isSchedulable: true,
    scheduled: false,
    constructor: function() {
        this.getScheduler().add(this);
    },
    destroy: function() {
        var me = this,
            scheduler = me.getScheduler();
        if (scheduler && !scheduler.destroyed) {
            scheduler.remove(me);
        }
        me.scheduler = null;
        me.schedule = me.react = Ext.emptyFn;
        me.callParent();
    },
    getFullName: function() {
        return this.name || this.id;
    },
    privates: {
        /**
         * This method returns the `Scheduler` for this item.
         * @return {Ext.util.Scheduler}
         */
        getScheduler: function() {
            return this.scheduler;
        },
        /**
         * Schedules this item with the associated `Ext.util.Scheduler`.
         */
        schedule: function() {
            var me = this,
                scheduler;
            if (!me.scheduled) {
                scheduler = me.getScheduler();
                if (scheduler) {
                    me.scheduled = true;
                    if (me.onSchedule) {
                        me.onSchedule();
                    }
                    scheduler.scheduleItem(me);
                }
            }
        },
        /**
         * Unschedules this item with the associated `Ext.util.Scheduler`.
         */
        unschedule: function() {
            var me = this,
                scheduler;
            if (me.scheduled) {
                scheduler = me.getScheduler();
                if (scheduler && !scheduler.destroyed) {
                    scheduler.unscheduleItem(me);
                }
                me.scheduled = false;
            }
        },
        /**
         * @method sort
         * This method should be overridden by items that have dependencies to insert. The
         * standard form would be:
         *
         *      sort: function () {
         *          this.getScheduler().sortItems(this.dependencies);
         *      }
         *
         * This example assumes the item has a "dependencies" array to pass to the scheduler.
         */
        // Can't use Ext.emptyFn here to avoid setting $private: true on it
        sort: function() {}
    }
});

/**
 * This class is the base for `Binding` and `MultiBinding`.
 * @private
 */
Ext.define('Ext.app.bind.BaseBinding', {
    extend: 'Ext.util.Schedulable',
    isBinding: true,
    calls: 0,
    kind: 20,
    defaultOptions: {},
    lastValue: undefined,
    /**
     * @cfg {Boolean} [single=false]
     * This option instructs the binding to call its `destroy` method immediately after
     * delivering the initial value.
     * @since 5.0.0
     */
    constructor: function(owner, callback, scope, options) {
        var me = this;
        me.options = options;
        me.owner = owner;
        me.scope = scope;
        me.callback = callback;
        if (!callback) {
            Ext.raise('Callback is required');
        }
        // If given a string callback name, preserve the late binding:
        me.lateBound = Ext.isString(callback);
        if (options && options.deep) {
            me.deep = true;
        }
        me.callParent();
    },
    destroy: function() {
        var me = this,
            owner = me.owner;
        if (owner) {
            owner.onBindDestroy(me);
        }
        me.callParent();
        me.scope = me.callback = me.owner = null;
    },
    isReadOnly: function() {
        return true;
    },
    privates: {
        getScheduler: function() {
            var owner = this.owner;
            return owner && owner.getScheduler();
        },
        getSession: function() {
            var owner = this.owner;
            return owner.isSession ? owner : owner.getSession();
        },
        notify: function(value) {
            var me = this,
                options = me.options || me.defaultOptions,
                previous = me.lastValue;
            // We want to deliver if:
            // 1) We've never been called
            // 2) We're a deep binding, which means that our object reference may not have changed,
            //    but something under us has changed. For example a link stub or a model field
            //    binding
            // 3) If the value has changed
            // 4) If the value is an array. It's difficult to tell if the underlying data changed
            if (!me.calls || me.deep || me.valueChanged(value, previous)) {
                ++me.calls;
                me.lastValue = value;
                if (me.lateBound) {
                    // Interestingly, lateBound-ness may be more efficient since it does
                    // not use the "call" method.
                    me.scope[me.callback](value, previous, me);
                } else {
                    me.callback.call(me.scope, value, previous, me);
                }
                if (options.single) {
                    me.destroy();
                }
            }
        },
        valueChanged: function(value, previous) {
            var ret = true;
            if (previous !== value) {
                if (value && previous && value instanceof Date && previous instanceof Date) {
                    ret = value.getTime() !== previous.getTime();
                }
            } else {
                ret = Ext.isArray(value);
            }
            return ret;
        }
    }
});

/**
 * This class is created to manage a direct bind.  `Ext.app.ViewModel` returns this from 
 * its {@link Ext.app.ViewModel#method-bind bind} method.
 */
Ext.define('Ext.app.bind.Binding', {
    extend: 'Ext.app.bind.BaseBinding',
    /**
     * @cfg {Boolean} [deep=false]
     * Normally a binding is only notified of changes to its bound property, but if that
     * property is an object it is sometimes helpful to be notified of changes to its
     * properties. To receive notifications of changes to all properties of a bound object,
     * set this to `true`.
     * @since 5.0.0
     */
    constructor: function(stub, callback, scope, options) {
        var me = this;
        me.callParent([
            stub.owner,
            callback,
            scope,
            options
        ]);
        me.stub = stub;
        me.depth = stub.depth;
        // We need to announce the current value, so if the stub is available
        // will generate its own announcement to all bindings) then we need to schedule
        if (stub.isAvailable() && !stub.scheduled) {
            me.schedule();
        }
    },
    /**
     * Destroys this binding. No further calls will be made to the callback method. No
     * methods should be called on this binding after calling this method.
     * @param {Boolean} [fromParent] (private)
     * @since 5.0.0
     */
    destroy: function(fromParent) {
        var me = this,
            stub = me.stub;
        if (stub && !fromParent) {
            stub.unbind(me);
            me.stub = null;
        }
        me.callParent();
    },
    /**
     * Binds to the `validation` association for the bound property. For example, when a
     * binding is bound to something like this:
     *
     *      var binding = viewModel.bind('{theUser.name}', ...);
     *
     * The validation status for the "name" property can be requested like so:
     *
     *      var validationBinding = binding.bindValidation(fn, scope);
     *
     * Calling this method in the above example would be equivalent to the following bind:
     *
     *      var validationBinding = viewModel.bind('{theUser.validation.name}', fn, scope);
     *
     * The primary reason to use this method is in cases where the original bind expression
     * is not known.
     *
     * For example, this method is used by `Ext.form.field.Base` when given the
     * `{@link Ext.Component#modelValidation modelValidation}` config is set. As such it
     * not common for users to need to call this method.
     *
     * @param {Function} callback The function to call when the validation changes.
     * @param {Object} [scope] The scope on which to call the `callback`.
     * @return {Ext.app.bind.Binding} A binding to the validation of the bound property.
     * @since 5.0.0
     */
    bindValidation: function(callback, scope) {
        var stub = this.stub;
        return stub && stub.bindValidation(callback, scope);
    },
    /**
     * Bind to a model field for validation
     * @param {Function/String} callback The function to call or the name of the function on the
     * scope
     * @param {Object} scope The scope for the callback
     * @return {Ext.app.bind.Binding} The binding, if available
     *
     * @private
     */
    bindValidationField: function(callback, scope) {
        var stub = this.stub;
        return stub && stub.bindValidationField(callback, scope);
    },
    /**
     * Returns the diagnostic name for this binding.
     * @return {String}
     * @since 5.0.0
     */
    getFullName: function() {
        return this.fullName || (this.fullName = '@(' + this.stub.getFullName() + ')');
    },
    /**
     * Returns the current value of the bound property. If this binding is not 
     * {@link #isAvailable available} the value will be `undefined`.
     * @return {Mixed} The value of the bound property.
     * @since 5.0.0
     */
    getValue: function() {
        var me = this,
            stub = me.stub;
        return stub && stub.getValue();
    },
    /**
     * Returns `true` if the bound property is available. If this returns `false`, 
     * it generally means the value is not reachable because the a parent value is
     * not present.
     * @return {Boolean}
     * @since 5.1.2
     */
    isAvailable: function() {
        var stub = this.stub;
        return stub && stub.isAvailable();
    },
    /**
     * Returns `true` if the bound property is loading. In the general case this means
     * that the value is just not available yet. In specific cases, when the bound property
     * is an `Ext.data.Model` it means that a request to the server is in progress to get
     * the record. For an `Ext.data.Store` it means that
     * `{@link Ext.data.Store#method-load load}` has been called on the store but it is
     * still in progress.
     * @return {Boolean}
     * @since 5.0.0
     */
    isLoading: function() {
        var stub = this.stub;
        return stub && stub.isLoading();
    },
    /**
     * This method returns `true` if this binding can only be read. If this method returns
     * `false` then the binding can be set using `setValue` (meaning this binding can be
     * a two-way binding).
     * @return {Boolean}
     * @since 5.0.0
     */
    isReadOnly: function() {
        var stub = this.stub,
            options = this.options,
            ret = true;
        if (!(options && options.twoWay === false)) {
            if (stub) {
                ret = stub.isReadOnly();
            }
        }
        return ret;
    },
    /**
     * Tells the bound property to refresh itself. This has meaning when the bound property
     * is something like an `Ext.data.Model` and an `Ext.data.Store` but does nothing in
     * most cases.
     * @since 5.0.0
     */
    refresh: function() {},
    // TODO - maybe nothing to do here but entities/stores would have work to do
    /**
     * Sets the value of the bound property. This will throw an error in debug mode if
     * this binding `isReadOnly`. This method will climb to set data on
     * a parent view model of this binding if appropriate. See "Inheriting Data" in the
     * {@link Ext.app.ViewModel} class introduction for more information.
     * @param {Mixed} value The new value.
     * @since 5.0.0
     */
    setValue: function(value) {
        if (this.isReadOnly()) {
            Ext.raise('Cannot setValue on a readonly binding');
        }
        this.stub.set(value);
    },
    privates: {
        getDataObject: function() {
            var stub = this.stub;
            return stub && stub.getDataObject();
        },
        getRawValue: function() {
            var me = this,
                stub = me.stub;
            return stub && stub.getRawValue();
        },
        isDescendantOf: function(item) {
            var stub = this.stub;
            return stub ? (item === stub) || stub.isDescendantOf(item) : false;
        },
        react: function() {
            this.notify(this.getValue());
        },
        schedule: function() {
            // If the parent stub is already scheduled, then we will be
            // called when the stub hits the next tick.
            if (!this.stub.scheduled) {
                this.callParent();
            }
        },
        sort: function() {
            var stub = this.stub;
            stub.scheduler.sortItem(stub);
        }
    }
});
// Schedulable#sort === emptyFn
// me.callParent();

/**
 * This class manages bindings for a `Session` or `ViewModel`.
 * @private
 */
Ext.define('Ext.app.bind.AbstractStub', {
    extend: 'Ext.util.Schedulable',
    requires: [
        'Ext.app.bind.Binding'
    ],
    children: null,
    depth: 0,
    generation: 1,
    kind: 10,
    parent: null,
    constructor: function(owner, name) {
        var me = this;
        /**
         * @property {Ext.data.Session/Ext.app.ViewModel} owner
         * This property is set at creation of ths stub and should not be changed.
         * @readonly
         */
        me.owner = owner;
        me.name = name;
        me.callParent();
    },
    destroy: function() {
        var me = this,
            children = me.children,
            bindings = me.bindings,
            len, i, key;
        if (bindings) {
            for (i = 0 , len = bindings.length; i < len; ++i) {
                bindings[i].destroy(true);
            }
        }
        for (key in children) {
            children[key].destroy();
        }
        if (me.scheduled) {
            me.unschedule();
        }
        me.callParent();
    },
    add: function(child) {
        var me = this;
        (me.children || (me.children = {}))[child.name] = child;
        child.depth = me.depth + 1;
        child.parent = me;
    },
    getChild: function(path) {
        var pathArray = Ext.isString(path) ? path.split('.') : path;
        if (pathArray && pathArray.length) {
            return this.descend(pathArray, 0);
        }
        return this;
    },
    getFullName: function() {
        var me = this,
            name = me.fullName,
            parent = me.parent,
            s;
        if (!name) {
            name = me.name || me.id;
            if (parent && (s = parent.getFullName())) {
                name = ((s.charAt(s.length - 1) !== ':') ? s + '.' : s) + name;
            }
            me.fullName = name;
        }
        return name;
    },
    getSession: function() {
        var owner = this.owner;
        return owner.isSession ? owner : owner.getSession();
    },
    bind: function(callback, scope, options) {
        var me = this,
            binding = new Ext.app.bind.Binding(me, callback, scope, options),
            bindings = (me.bindings || (me.bindings = []));
        binding.depth = me.depth;
        bindings.push(binding);
        return binding;
    },
    getValue: function() {
        return this.isAvailable() ? this.getRawValue() : null;
    },
    graft: function(replacement) {
        var me = this,
            bindings = me.bindings,
            name = me.name,
            i;
        // Clear these so that when we call destroy we won't damage anything:
        me.parent = me.bindings = null;
        me.destroy();
        // we may be scheduled
        replacement.depth = me.depth;
        replacement.bindings = bindings;
        replacement.generation = me.generation + 1;
        replacement.name = name;
        replacement.id = me.id;
        replacement.path = me.path;
        // Now for the fun part...
        if (bindings) {
            for (i = bindings.length; i-- > 0; ) {
                bindings[i].stub = replacement;
            }
        }
        return replacement;
    },
    isDescendantOf: function(item) {
        var parent;
        for (parent = this; parent = parent.parent; ) {
            // eslint-disable-line no-cond-assign
            if (parent === item) {
                return true;
            }
        }
        return false;
    },
    isAvailable: function() {
        return true;
    },
    isLoading: function() {
        return false;
    },
    onSchedule: function() {
        var i, len, binding, bindings, p;
        // When a stub changes, say "foo.bar.baz" we may need to notify bindings on our
        // parents "foo.bar" and "foo", This is true especially when these are targets of
        // links. To economize on this we require that bindings that want to be notified
        // of changes to sub-properties of their target set the "deep" property to true.
        for (p = this.parent; p; p = p.parent) {
            bindings = p.bindings;
            if (bindings) {
                for (i = 0 , len = bindings.length; i < len; ++i) {
                    binding = bindings[i];
                    if (binding.deep && !binding.scheduled) {
                        binding.schedule();
                    }
                }
            }
        }
    },
    react: function() {
        var bindings = this.bindings,
            binding, i, len;
        if (bindings) {
            for (i = 0 , len = bindings.length; i < len; ++i) {
                binding = bindings[i];
                if (!binding.scheduled) {
                    binding.schedule();
                }
            }
        }
    },
    unbind: function(binding) {
        var bindings = this.bindings;
        if (bindings && bindings.length) {
            Ext.Array.remove(bindings, binding);
        }
    },
    privates: {
        collect: function() {
            var children = this.children,
                bindings = this.bindings,
                totalCount = 0,
                count = 0,
                child, key;
            if (children) {
                for (key in children) {
                    child = children[key];
                    count = child.collect();
                    if (count === 0) {
                        // The child (and any deep children) have no bindings,
                        // so we can consider it a dead node.
                        child.destroy();
                        delete children[key];
                    }
                    totalCount += count;
                }
            }
            if (bindings) {
                totalCount += bindings.length;
            }
            return totalCount;
        },
        getScheduler: function() {
            var owner = this.owner;
            return owner && owner.getScheduler();
        },
        sort: function() {
            var parent = this.parent;
            if (parent) {
                // We sort our parent first because if it is something like a link we need
                // it to determine the value of the root-level property before we can dot
                // our way into it. This is especially important for formulas that might
                // throw errors if the links have not published results before they run.
                this.scheduler.sortItem(parent);
            }
        }
    }
});
// Schedulable#sort === emptyFn
// me.callParent();

/**
 * This class and its derived classes are used to manage access to the properties of an
 * object stored in a `Session`.
 * @private
 */
Ext.define('Ext.app.bind.Stub', {
    extend: 'Ext.app.bind.AbstractStub',
    requires: [
        'Ext.app.bind.Binding'
    ],
    isStub: true,
    dirty: true,
    formula: null,
    validationKey: 'validation',
    constructor: function(owner, name, parent) {
        var me = this,
            path = name;
        me.callParent([
            owner,
            name
        ]);
        me.boundValue = null;
        if (parent) {
            parent.add(me);
            if (!parent.isRootStub) {
                path = parent.path + '.' + name;
            }
            me.checkHadValue();
        }
        me.path = path;
    },
    destroy: function() {
        var me = this,
            formula = me.formula,
            storeBinding = me.storeBinding;
        if (formula) {
            formula.destroy();
        }
        if (storeBinding) {
            storeBinding.destroy();
        }
        me.detachBound();
        me.callParent();
    },
    bindValidation: function(callback, scope) {
        var parent = this.parent;
        return parent && parent.descend([
            this.validationKey,
            this.name
        ]).bind(callback, scope);
    },
    bindValidationField: function(callback, scope) {
        var parent = this.parent,
            name = this.name,
            lateBound = typeof callback === 'string',
            ret;
        if (parent) {
            ret = parent.bind(function(value) {
                var field = null;
                if (value && value.isModel) {
                    field = value.getField(name);
                }
                if (lateBound) {
                    scope[callback](field, value, this);
                } else {
                    callback.call(scope, field, value, this);
                }
            });
        }
        return ret || null;
    },
    descend: function(path, index) {
        var me = this,
            children = me.children || (me.children = {}),
            pos = index || 0,
            name = path[pos++],
            ret;
        if (!(ret = children[name])) {
            ret = new Ext.app.bind.Stub(me.owner, name, me);
        }
        if (pos < path.length) {
            ret = ret.descend(path, pos);
        }
        return ret;
    },
    getChildValue: function(parentData) {
        var me = this,
            name = me.name,
            bindMappings = me.bindMappings,
            storeMappings = bindMappings.store,
            modelMappings = bindMappings.model,
            ret;
        if (!parentData && !Ext.isString(parentData)) {
            // since these forms of falsey values (0, false, etc.) are not things we
            // can index into, this child stub must be null.
            ret = me.hadValue ? null : undefined;
        } else {
            ret = me.inspectValue(parentData);
            if (!ret) {
                if (parentData.isEntity) {
                    // If we get here, we know it's not an association
                    if (modelMappings[name]) {
                        ret = parentData[modelMappings[name]]();
                    } else {
                        ret = parentData.data[name];
                    }
                } else if (parentData.isStore && storeMappings[name]) {
                    ret = parentData[storeMappings[name]]();
                } else {
                    ret = parentData[name];
                    if (ret === undefined && me.hadValue) {
                        ret = null;
                    }
                }
            }
        }
        return ret;
    },
    getDataObject: function() {
        var me = this,
            parentData = me.parent.getDataObject(),
            // RootStub does not get here
            name = me.name,
            ret = parentData ? parentData[name] : null,
            storeMappings = me.bindMappings.store,
            associations;
        if (!ret) {
            if (parentData && parentData.isEntity) {
                // Check if the item is an association, if it is, grab it but don't load it.
                associations = parentData.associations;
                if (associations && name in associations) {
                    ret = parentData[associations[name].getterName]();
                }
            }
        } else if (parentData.isStore && name in storeMappings) {
            ret = parentData[storeMappings[name]]();
        }
        if (!ret || !(ret.$className || Ext.isObject(ret))) {
            parentData[name] = ret = {};
            // We're implicitly setting a value on the object here
            me.hadValue = true;
            // If we're creating the parent data object, invalidate the dirty
            // flag on our children.
            me.invalidate(true, true);
        }
        return ret;
    },
    getRawValue: function() {
        // NOTE: The RootStub class does not call here so we will *always* have a parent
        // unless dark energy has won and the laws of physics have broken down.
        return this.getChildValue(this.getParentValue());
    },
    graft: function(replacement) {
        var me = this,
            parent = me.parent,
            children = me.children,
            name = me.name,
            i, ret;
        replacement.parent = parent;
        replacement.children = children;
        if (parent) {
            parent.children[name] = replacement;
        }
        if (children) {
            for (i in children) {
                children[i].parent = replacement;
            }
        }
        me.children = null;
        replacement.checkHadValue();
        ret = me.callParent([
            replacement
        ]);
        ret.invalidate(true, true);
        return ret;
    },
    isAvailable: function() {
        return this.checkAvailability();
    },
    isLoading: function() {
        return !this.checkAvailability(true);
    },
    invalidate: function(deep, dirtyOnly) {
        var me = this,
            children = me.children,
            name;
        me.dirty = true;
        me.checkHadValue();
        if (!dirtyOnly && me.isAvailable()) {
            if (!me.scheduled) {
                // If we have no children, we're a leaf
                me.schedule();
            }
        }
        if (deep && children) {
            for (name in children) {
                children[name].invalidate(deep, dirtyOnly);
            }
        }
    },
    isReadOnly: function() {
        var formula = this.formula;
        return !!(formula && !formula.set);
    },
    set: function(value, preventClimb) {
        var me = this,
            parent = me.parent,
            name = me.name,
            formula = me.formula,
            parentData, associations, association, formulaStub, setterName;
        if (formula && !formula.settingValue && formula.set) {
            formula.setValue(value);
            return;
        } else if (me.isLinkStub) {
            formulaStub = me.getLinkFormulaStub();
            formula = formulaStub ? formulaStub.formula : null;
            if (formula) {
                if (formulaStub.isReadOnly()) {
                    Ext.raise('Cannot setValue on a readonly formula');
                }
                formula.setValue(value);
                return;
            }
        }
        // To set a child property, the parent must be an object...
        parentData = parent.getDataObject();
        if (parentData.isEntity) {
            associations = parentData.associations;
            if (associations && (name in associations)) {
                association = associations[name];
                setterName = association.setterName;
                if (setterName) {
                    parentData[setterName](value);
                }
                // We may be setting a record here, force the value to recalculate
                me.invalidate(true);
            } else {
                // If not an association then it is a data field
                parentData.set(name, value);
            }
        }
        // Setting fields or associated records will fire change notifications so we
        // handle the side effects there
        else if ((value && value.constructor === Object) || !(value === parentData[name] && parentData.hasOwnProperty(name))) {
            // The hasOwnProperty check is important, even though the value might be the same here,
            // that value could exist in a viewmodel above us
            if (preventClimb || !me.setByLink(value)) {
                if (value === undefined) {
                    delete parentData[name];
                } else {
                    parentData[name] = value;
                }
                me.inspectValue(parentData);
                // We have children, but we're overwriting the value with something else, so
                // we need to schedule our children
                me.invalidate(true);
            }
        }
    },
    onStoreDataChanged: function() {
        this.invalidate(true);
    },
    afterLoad: function(record) {
        this.invalidate(true);
    },
    afterCommit: function(record) {
        // Essentially the same as an edit, but we don't know what changed.
        this.afterEdit(record, null);
    },
    afterEdit: function(record, modifiedFieldNames) {
        var children = this.children,
            len = modifiedFieldNames && modifiedFieldNames.length,
            associations = record.associations,
            bindMappings = this.bindMappings.model,
            key, i, child, name, ref;
        // No point checking anything if we don't have children
        if (children) {
            if (len) {
                // We know what changed, check for it and schedule it.
                for (i = 0; i < len; ++i) {
                    name = modifiedFieldNames[i];
                    child = children[name];
                    if (!child) {
                        ref = record.fieldsMap[name];
                        ref = ref && ref.reference;
                        child = ref && children[ref.role];
                    }
                    if (child) {
                        child.invalidate(true);
                    }
                }
            } else {
                // We don't know what changed, so loop over everything.
                // If the child is not an association, then it's a field so we
                // need to trigger them so we can respond to field changes
                for (key in children) {
                    if (!(associations && key in associations)) {
                        children[key].invalidate(true);
                    }
                }
            }
            // Whether we know what changed or not, valid/dirty are meta properties so
            // trigger them regardless
            for (key in bindMappings) {
                child = children[key];
                if (child) {
                    child.invalidate();
                }
            }
        }
        this.invalidate();
    },
    afterReject: function(record) {
        // Essentially the same as an edit, but we don't know what changed.
        this.afterEdit(record, null);
    },
    afterAssociatedRecordSet: function(record, associated, role) {
        var children = this.children,
            key = role.role;
        if (children && key in children) {
            children[key].invalidate(true);
        }
    },
    setByLink: function(value) {
        var me = this,
            n = 0,
            ret = false,
            i, link, path, stub, root, name;
        for (stub = me; stub; stub = stub.parent) {
            if (stub.isLinkStub) {
                link = stub;
                if (n) {
                    for (path = [] , i = 0 , stub = me; stub !== link; stub = stub.parent) {
                        ++i;
                        path[n - i] = stub.name;
                    }
                }
                break;
            }
            ++n;
        }
        stub = null;
        if (link) {
            root = link.parent;
            name = link.name;
            if (!root.shouldClimb(name)) {
                // Write to root, descend to stub
                stub = root.insertChild(name);
            } else {
                stub = link.getTargetStub();
            }
        }
        if (stub) {
            // We are a child of a link stub and that stub links to a Stub, so forward the set
            // call over there. This is needed to fire the bindings on that side of the link
            // and that will also arrive back here since we are a linked to it.
            if (path) {
                stub = stub.descend(path);
            }
            stub.set(value);
            ret = true;
        }
        return ret;
    },
    setFormula: function(formula) {
        var me = this,
            oldFormula = me.formula;
        if (oldFormula) {
            oldFormula.destroy();
        }
        // The new formula will bind to what it needs and that will schedule it (and then
        // us when it sets our value).
        me.formula = new Ext.app.bind.Formula(me, formula);
    },
    react: function() {
        var me = this,
            bound = this.boundValue,
            children = me.children,
            generation;
        if (bound) {
            if (bound.isValidation) {
                bound.refresh();
                generation = bound.generation;
                // Don't react if we haven't changed
                if (me.lastValidationGeneration === generation) {
                    return;
                }
                me.lastValidationGeneration = generation;
            } else if (bound.isModel) {
                // At this point we're guaranteed to have a non-validation model
                // Check if we're interested in it, if so, validate it and let
                // the record fire off any changes
                if (children && children[me.validationKey]) {
                    // Trigger validity checks
                    bound.isValid();
                }
            }
        }
        this.callParent();
    },
    privates: {
        bindMappings: {
            store: {
                count: 'getCount',
                first: 'first',
                last: 'last',
                loading: 'hasPendingLoad',
                totalCount: 'getTotalCount'
            },
            model: {
                dirty: 'isDirty',
                phantom: 'isPhantom',
                valid: 'isValid'
            }
        },
        checkAvailability: function(isLoading) {
            var me = this,
                parent = me.parent,
                bindMappings = me.bindMappings,
                name = me.name,
                available = !!(parent && parent.checkAvailability(isLoading)),
                associations, parentValue, value, availableSet;
            if (available) {
                parentValue = me.getParentValue();
                value = me.inspectValue(parentValue);
                // If we get a value back, it's something we can ask for the loading state
                if (value) {
                    if (isLoading) {
                        available = !value.hasPendingLoad();
                    } else {
                        // If it's a store, it should be always available, even if loading
                        if (value.isStore) {
                            available = true;
                        } else {
                            // If it's a model and it's loading, only available if it's after
                            // the first time
                            available = !value.isLoading() || value.loadCount > 0;
                        }
                    }
                } else {
                    if (parentValue) {
                        if (parentValue.isModel) {
                            if (bindMappings.model[name]) {
                                available = !parent.isLoading();
                                availableSet = true;
                            } else {
                                associations = parentValue.associations;
                                // At this point, we know the value is not a record or a store,
                                // otherwise something would have been returned from inspectValue.
                                // We also check here that we are not a defined association,
                                // because we don't treat it like a field.
                                // Otherwise, we are a field on a model, so we're never in a loading
                                // state.
                                if (!(associations && name in associations)) {
                                    available = true;
                                    availableSet = true;
                                }
                            }
                        } else if (parentValue.isStore && bindMappings.store[name] && name !== 'loading') {
                            available = !parent.isLoading();
                            availableSet = true;
                        }
                    }
                    if (!availableSet) {
                        available = me.hadValue || me.getRawValue() !== undefined;
                    }
                }
            }
            return available;
        },
        checkHadValue: function() {
            if (!this.hadValue) {
                this.hadValue = this.getRawValue() !== undefined;
            }
        },
        collect: function() {
            var me = this,
                result = me.callParent(),
                storeBinding = me.storeBinding ? 1 : 0,
                formula = me.formula ? 1 : 0;
            return result + storeBinding + formula;
        },
        getLinkFormulaStub: function() {
            // Setting the value on a link backed by a formula should set the
            // formula. So we climb the hierarchy until we find the rootStub
            // and set it there if it be a formula.
            var stub = this;
            while (stub.isLinkStub) {
                stub = stub.binding.stub;
            }
            return stub.formula ? stub : null;
        },
        getParentValue: function() {
            var me = this;
            // Cache the value of the parent here. Inside onSchedule we clear the value
            // because it may be invalidated.
            if (me.dirty) {
                me.parentValue = me.parent.getValue();
                me.dirty = false;
            }
            return me.parentValue;
        },
        setStore: function(storeBinding) {
            this.storeBinding = storeBinding;
        },
        inspectValue: function(parentData) {
            var me = this,
                name = me.name,
                current = me.boundValue,
                boundValue = null,
                associations, raw, changed, associatedEntity;
            if (parentData && parentData.isEntity) {
                associations = parentData.associations;
                if (associations && (name in associations)) {
                    boundValue = parentData[associations[name].getterName]();
                } else if (name === me.validationKey) {
                    boundValue = parentData.getValidation();
                    // Binding a new one, reset the generation
                    me.lastValidationGeneration = null;
                }
            } else if (parentData) {
                raw = parentData[name];
                if (raw && (raw.isModel || raw.isStore)) {
                    boundValue = raw;
                }
            }
            // Check if we have a current binding that changed. If so, we need
            // to detach ourselves from it
            changed = current !== boundValue;
            if (changed) {
                if (current) {
                    me.detachBound();
                }
                if (boundValue) {
                    if (boundValue.isModel) {
                        boundValue.join(me);
                    } else {
                        // Only want to trigger automatic loading if we've come from an association.
                        // Otherwise leave the user in charge of that.
                        associatedEntity = boundValue.associatedEntity;
                        if (associatedEntity && boundValue.autoLoad !== false && !boundValue.complete && !boundValue.hasPendingLoad()) {
                            boundValue.load();
                        }
                        // We only want to listen for the first load, since the actual
                        // store object won't change from then on
                        boundValue.on({
                            scope: me,
                            // Capture beginload/load so we can bind to the loading state
                            // of the store. We need load because a load may be unsuccessful
                            // which means datachanged won't fire beginload is used because
                            // it's fired:
                            // a) After we're sure to load (beforeload could be vetoed)
                            // b) After the loading flag is set to true. This is important
                            // because we fire the datachanged handler which needs to check if
                            // the store is available (loading) to publish values.
                            beginload: 'onStoreDataChanged',
                            load: 'onStoreDataChanged',
                            datachanged: 'onStoreDataChanged',
                            destroy: 'onDestroyBound'
                        });
                    }
                }
                me.boundValue = boundValue;
            }
            return boundValue;
        },
        detachBound: function() {
            var me = this,
                current = me.boundValue;
            if (current && !current.destroyed) {
                if (current.isModel) {
                    current.unjoin(me);
                } else {
                    current.un({
                        scope: me,
                        beginload: 'onStoreDataChanged',
                        load: 'onStoreDataChanged',
                        datachanged: 'onStoreDataChanged',
                        destroy: 'onDestroyBound'
                    });
                }
            }
        },
        onDestroyBound: function() {
            if (!this.owner.destroying) {
                this.set(null);
            }
        },
        sort: function() {
            var me = this,
                formula = me.formula,
                scheduler = me.scheduler,
                storeBinding = me.storeBinding;
            me.callParent();
            if (storeBinding) {
                scheduler.sortItem(storeBinding);
            }
            if (formula) {
                // Our formula must run before we do so it can set the value on us. Our
                // bindings in turn depend on us so they will be scheduled as part of the
                // current sweep if the formula produces a different result.
                scheduler.sortItem(formula);
            }
        }
    }
});

/**
 * This class manages stubs associated with `link` requests. These bind to some other
 * descriptor and forward changes from there.
 * @private
 */
Ext.define('Ext.app.bind.LinkStub', {
    extend: 'Ext.app.bind.Stub',
    isLinkStub: true,
    binding: null,
    destroy: function() {
        var me = this,
            binding = me.binding,
            owner = me.owner;
        if (binding) {
            me.binding = null;
            binding.destroy();
            if (owner) {
                delete owner.linkData[me.name];
            }
        }
        me.target = null;
        me.callParent();
    },
    getFullName: function() {
        var me = this;
        return me.fullName || (me.fullName = '(' + me.callParent() + ' -> ' + me.binding.getFullName() + ')');
    },
    getDataObject: function() {
        var binding = this.binding,
            root = this.parent,
            name = this.name,
            rootData, ret;
        if (root.isRootStub && !root.shouldClimb(name)) {
            rootData = root.owner.getData();
            if (!rootData.hasOwnProperty(name)) {
                rootData[name] = ret = {};
            }
        } else {
            ret = binding && binding.getDataObject();
        }
        return ret;
    },
    getRawValue: function() {
        var binding = this.binding;
        return binding && binding.getRawValue();
    },
    getValue: function() {
        var binding = this.binding;
        return binding && binding.getValue();
    },
    getTargetStub: function() {
        var binding = this.binding;
        return binding && binding.stub;
    },
    isAvailable: function() {
        var binding = this.binding;
        return binding ? binding.isAvailable() : false;
    },
    isLoading: function() {
        var binding = this.binding;
        return binding ? binding.isLoading() : false;
    },
    link: function(bindDescriptor, target) {
        var me = this,
            binding = me.binding;
        if (binding) {
            binding.destroy();
        }
        target = me.target = target || me.owner;
        me.linkDescriptor = bindDescriptor;
        me.binding = target.bind(bindDescriptor, me.onChange, me);
        me.binding.deep = true;
    },
    onChange: function() {
        this.invalidate(true);
    },
    react: function() {
        var me = this,
            linkData = me.owner.linkData;
        linkData[me.name] = me.getValue();
        me.callParent();
    },
    privates: {
        collect: function() {
            var me = this,
                result = me.callParent(),
                binding = me.binding ? 1 : 0;
            return result + binding;
        },
        sort: function() {
            var binding = this.binding;
            if (binding) {
                // We want to make sure our binding reacts before we do so that it can provide
                // whatever value we might need first.
                this.scheduler.sortItem(binding);
            }
        }
    }
});

/**
 * This class is the root stub for managing a `ViewModel`.
 * @private
 */
Ext.define('Ext.app.bind.RootStub', {
    extend: 'Ext.app.bind.AbstractStub',
    requires: [
        'Ext.app.bind.LinkStub',
        'Ext.app.bind.Stub'
    ],
    isRootStub: true,
    depth: 0,
    createRootChild: function(name, direct) {
        var me = this,
            owner = me.owner,
            ownerData = owner.getData(),
            children = me.children,
            previous = children && children[name],
            parentStub = previous ? null : me,
            parentVM, stub;
        if (direct || ownerData.hasOwnProperty(name) || !(parentVM = owner.getParent())) {
            stub = new Ext.app.bind.Stub(owner, name, parentStub);
        } else {
            stub = new Ext.app.bind.LinkStub(owner, name, parentStub);
            stub.link('{' + name + '}', parentVM);
        }
        if (previous) {
            previous.graft(stub);
        }
        return stub;
    },
    createStubChild: function(name) {
        return this.createRootChild(name, true);
    },
    descend: function(path, index) {
        var me = this,
            children = me.children,
            pos = index || 0,
            name = path[pos++],
            ret = (children && children[name]) || me.createRootChild(name);
        if (pos < path.length) {
            ret = ret.descend(path, pos);
        }
        return ret;
    },
    getFullName: function() {
        return this.fullName || (this.fullName = this.owner.id + ':');
    },
    // The root Stub is associated with the owner's "data" object
    getDataObject: function() {
        return this.owner.data;
    },
    getRawValue: function() {
        return this.owner.data;
    },
    getValue: function() {
        return this.owner.data;
    },
    isDescendantOf: function() {
        return false;
    },
    set: function(value, preventClimb) {
        if (!value || value.constructor !== Object) {
            Ext.raise('Only an object can be set at the root');
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            children = me.children || (me.children = {}),
            owner = me.owner,
            data = owner.data,
            parentVM = owner.getParent(),
            stub, v, key, setSelf, created;
        for (key in value) {
            if (key.indexOf('.') >= 0) {
                Ext.raise('Value names cannot contain dots');
            }
            // Setting the value.
            // Ensure the Stub exists for the name, and set its value.
            v = value[key];
            if (v !== undefined) {
                stub = children[key];
                setSelf = preventClimb || !me.shouldClimb(key);
                if (!stub) {
                    stub = me.createRootChild(key, setSelf);
                    created = true;
                } else if (setSelf && stub.isLinkStub && !stub.getLinkFormulaStub()) {
                    stub = me.insertChild(key);
                }
                if (!created || !data.hasOwnProperty(value)) {
                    owner.invalidateChildLinks(key);
                }
                stub.set(v, setSelf);
            }
            // Clearing the value. Delete the data item
            // Invalidate the Stub if it exists.
            else if (data.hasOwnProperty(key)) {
                delete data[key];
                stub = children[key];
                if (stub) {
                    if (!stub.isLinkStub && parentVM) {
                        stub = me.createRootChild(key);
                    }
                    owner.invalidateChildLinks(key, true);
                    stub.invalidate(true);
                }
            }
        }
    },
    schedule: Ext.emptyFn,
    unschedule: Ext.emptyFn,
    privates: {
        checkAvailability: function() {
            // Always available
            return true;
        },
        insertChild: function(name) {
            return this.createRootChild(name, true);
        },
        invalidateChildLink: function(name, clear) {
            var children = this.children,
                stub = children && children[name];
            if (stub && stub.isLinkStub && !stub.getLinkFormulaStub()) {
                stub = this.createRootChild(name);
                if (clear) {
                    stub.invalidate(true);
                }
                this.owner.invalidateChildLinks(name, clear);
            }
        },
        shouldClimb: function(name) {
            var parent = this.owner.getParent();
            while (parent) {
                if (parent.getData().hasOwnProperty(name)) {
                    return true;
                }
                parent = parent.getParent();
            }
            return false;
        }
    }
});

/**
 * This class is created to manage a multi-bind against a `ViewModel`.
 */
Ext.define('Ext.app.bind.Multi', {
    extend: 'Ext.app.bind.BaseBinding',
    isMultiBinding: true,
    missing: 1,
    // Multi binds have to be deep. We construct a single object/array and we only
    // ever fire by notifying with that value which will never change. As such, we
    // need to notify any child bindings so they can check if their individual
    // bindings have changed.
    deep: true,
    /**
     * @cfg {Boolean} trackStatics
     * This option tracks for static branches of the root object which can be pruned using
     * {@link #pruneStaticKeys}. This can be useful to only get the dynamic parts of a multi bind:
     *
     *      {
     *          a: 1,
     *          b: '{someBind}',
     *          c: ['a', 'b', 'c'],
     *          d: ['a', 'b', '{someBind}'],
     *          e: {
     *              y: 1,
     *              z: 2
     *          },
     *          f: {
     *              y: 1,
     *              z: '{someBind}'
     *          }
     *      }
     *
     *      // Will produce
     *      {
     *          b: value,
     *          d: ['a', 'b', value],
     *          f: {
     *              y: 1,
     *              z: value
     *          }
     *      }
     * @private
     * @since 5.1.0
     */
    constructor: function(descriptor, owner, callback, scope, options) {
        var me = this,
            trackStatics = options && options.trackStatics;
        me.callParent([
            owner,
            callback,
            scope,
            options
        ]);
        me.bindings = [];
        me.literal = descriptor.$literal;
        if (descriptor.constructor === Object) {
            if (trackStatics) {
                me.staticKeys = [];
            }
            me.addObject(descriptor, me.lastValue = {}, me.staticKeys);
        } else {
            me.addArray(descriptor, me.lastValue = []);
        }
        // We started at missing == 1 so that no immediate callbacks would hit 0 before
        // adding all bindings... so now we decrement by 1 to balance things and see if
        // we are at 0.
        if (!--me.missing && !me.scheduled) {
            me.schedule();
        }
    },
    destroy: function() {
        var me = this;
        me.bindings = Ext.destroy(me.bindings);
        me.callParent();
    },
    add: function(descriptor, data, property) {
        var me = this,
            owner = me.owner,
            bindings = me.bindings,
            method = me.literal ? (descriptor.reference ? 'bindEntity' : 'bindExpression') : 'bind',
            binding, depth;
        ++me.missing;
        /* eslint-disable indent */
        binding = owner[method](descriptor, function(value) {
            data[property] = value;
            if (binding.calls === 1) {
                --me.missing;
            }
            if (!me.missing && !me.scheduled) {
                me.schedule();
            }
        }, // TODO - split bind options between us and the sub-binds (pass null for now)
        me, null);
        /* eslint-enable indent */
        depth = binding.depth;
        if (!bindings.length || depth < me.depth) {
            me.depth = depth;
        }
        bindings.push(binding);
        return !this.isBindingStatic(binding);
    },
    addArray: function(multiBindDescr, array) {
        var me = this,
            n = multiBindDescr.length,
            hasDynamic = false,
            dynamic, b, i;
        for (i = 0; i < n; ++i) {
            b = multiBindDescr[i];
            if (b && (b.reference || Ext.isString(b))) {
                dynamic = me.add(b, array, i);
            } else if (Ext.isArray(b)) {
                dynamic = me.addArray(b, array[i] = []);
            } else if (b && b.constructor === Object) {
                dynamic = me.addObject(b, array[i] = {});
            } else {
                array[i] = b;
                dynamic = false;
            }
            hasDynamic = hasDynamic || dynamic;
        }
        return hasDynamic;
    },
    addObject: function(multiBindDescr, object, staticKeys) {
        var me = this,
            hasDynamic = false,
            dynamic, b, name;
        for (name in multiBindDescr) {
            b = multiBindDescr[name];
            if (b && (b.reference || Ext.isString(b))) {
                dynamic = me.add(b, object, name);
            } else if (Ext.isArray(b)) {
                dynamic = me.addArray(b, object[name] = []);
            } else if (b && b.constructor === Object) {
                dynamic = me.addObject(b, object[name] = {});
            } else {
                object[name] = b;
                dynamic = false;
            }
            if (staticKeys && !dynamic) {
                staticKeys.push(name);
            }
            hasDynamic = hasDynamic || dynamic;
        }
        return hasDynamic;
    },
    getFullName: function() {
        var me = this,
            fullName = me.fullName,
            bindings = me.bindings,
            length = bindings.length,
            i;
        if (!fullName) {
            fullName = '@[';
            for (i = 0; i < length; ++i) {
                if (i) {
                    fullName += ',';
                }
                fullName += bindings[i].getFullName();
            }
            fullName += ']';
            me.fullName = fullName;
        }
        return fullName;
    },
    getRawValue: function() {
        return this.lastValue;
    },
    isDescendantOf: function() {
        return false;
    },
    isLoading: function() {
        var bindings = this.bindings,
            n = bindings.length;
        for (; n-- > 0; ) {
            if (bindings[n].isLoading()) {
                return true;
            }
        }
        return false;
    },
    isAvailable: function() {
        var bindings = this.bindings,
            n = bindings.length;
        for (; n-- > 0; ) {
            if (bindings[n].isAvailable()) {
                return true;
            }
        }
        return false;
    },
    isBindingStatic: function(binding) {
        return binding.isTemplateBinding && binding.isStatic;
    },
    isStatic: function() {
        var bindings = this.bindings,
            len = bindings.length,
            i, binding;
        for (i = 0; i < len; ++i) {
            binding = bindings[i];
            if (!this.isBindingStatic(binding)) {
                return false;
            }
        }
        return true;
    },
    pruneStaticKeys: function() {
        var value = Ext.apply({}, this.lastValue),
            keys = this.staticKeys,
            len = keys.length,
            i;
        for (i = 0; i < len; ++i) {
            delete value[keys[i]];
        }
        return value;
    },
    react: function() {
        this.notify(this.lastValue);
    },
    refresh: function() {},
    // @TODO
    privates: {
        sort: function() {
            this.scheduler.sortItems(this.bindings);
        }
    }
});
// Schedulable#sort === emptyFn
// me.callParent();

/**
 * This class manages a formula defined for an `Ext.app.ViewModel`.
 *
 * ## Formula Basics
 *
 * Formulas in a `ViewModel` can be defined as simply as just a function:
 *
 *      formulas: {
 *          xy: function (get) { return get('x') * get('y'); }
 *      }
 *
 * When you need to be more explicit, "xy" can become an object. The following means the
 * same thing as above:
 *
 *      formulas: {
 *          xy: {
 *              get: function (get) { return get('x') * get('y'); }
 *          }
 *      }
 *
 * ### Data Dependencies
 *
 * One of the important aspects of a `ViewModel` is notification of change. In order to
 * manage this, a `ViewModel` *must* know the dependencies between data. In the above case
 * this is accomplished by **parsing the text of the function**. While this is convenient
 * and reduces the maintenance/risk that would come from explicitly listing dependencies
 * separately, there are some rules to be aware of:
 *
 *   * All dependencies are resolved by matching the binding statements in the getter function.
 *   * If you need to use these values in other ways, cache them as a `var` (following
 *     the first rule to capture the value) and use that `var`.
 *
 * In the above formulas, the "xy" formula depends on "x" and "y" in the `ViewModel`. As
 * these values change, the formula is called to produce the correct value for "xy". This
 * in turn can be used by other formulas. For example:
 *
 *      formulas: {
 *          xy: function (get) {  // "get" is arbitrary but a good convention
 *              return get('x') * get('y');
 *          },
 *
 *          xyz: function (get) {
 *              return get('xy') * get('z');
 *          }
 *      }
 *
 * In the above, "xyz" depends on "xy" and "z" values in the `ViewModel`.
 *
 * ### The Getter Method
 *
 * The argument passed to the formula is a function that allows you to retrieve
 * the matched bind statements.
 *
 *      formulas: {
 *          foo: function (get) {
 *              return get('theUser.address.city');
 *          }
 *      }
 *
 * In the above, the dependency is resolved to `theUser.address.city`. The formula will not
 * be triggered until the value for `city` is present.
 *
 * ### Capturing Values
 *
 * If values need to be used repeatedly, you can use a `var` as long as the Rules are not
 * broken.
 *
 *      formulas: {
 *          x2y2: function (get) {
 *              // These are still "visible" as "get('x')" and "get('y')" so this is OK:
 *              var x = get('x'),
 *                  y = get('y');
 *
 *              return x * x * y * y;
 *          }
 *      }
 *
 * ## Explicit Binding
 *
 * While function parsing is convenient, there are times it is not the best solution. In
 * these cases, an explicit `bind` can be given. To revisit the previous example with an
 * explicit binding:
 *
 *      formulas: {
 *          zip: {
 *              bind: '{foo.bar.zip}',
 *
 *              get: function (zip) {
 *                  // NOTE: the only thing we get is what our bind produces.
 *                  return zip * 2;
 *              }
 *          }
 *      }
 *
 * In this case we have given the formula an explicit `bind` value so it will no longer
 * parse the `get` function. Instead, it will call `{@link Ext.app.ViewModel#bind}` with
 * the value of the `bind` property and pass the produced value to `get` whenever it
 * changes.
 *
 * ## Settable Formulas
 *
 * When a formula is "reversible" it can be given a `set` method to allow it to participate
 * in two-way binding. For example:
 *
 *      formulas: {
 *             fullName: {
 *                 get: function (get) {
 *                     var ret = get('firstName') || '';
 *
 *                     if (get('lastName')) {
 *                         ret += ' ' +  get('lastName');
 *                     }
 *
 *                     return ret;
 *                 },
 *
 *                 set: function (value) {
 *                     var space = value.indexOf(' '),
 *                         split = (space < 0) ? value.length : space;
 *
 *                     this.set({
 *                         firstName: value.substring(0, split),
 *                         lastName: value.substring(split + 1)
 *                     });
 *                 }
 *             }
 *         }
 *
 * When the `set` method is called the `this` reference is the `Ext.app.ViewModel` so it
 * just calls its `{@link Ext.app.ViewModel#method-set set method}`.
 *
 * ## Single Run Formulas
 *
 * If a formula only needs to produce an initial value, it can be marked as `single`.
 *
 *      formulas: {
 *          xy: {
 *              single: true,
 *
 *              get: function (get) {
 *                  return get('x') * get('y');
 *              }
 *          }
 *      }
 *
 * This formulas `get` method will be called with `x` and `y` once and then its binding
 * to these properties will be destroyed. This means the `get` method (and hence the value
 * of `xy`) will only be executed/calculated once.
 */
Ext.define('Ext.app.bind.Formula', {
    extend: 'Ext.util.Schedulable',
    requires: [
        'Ext.util.LruCache'
    ],
    statics: {
        getFormulaParser: function(name) {
            var cache = this.formulaCache,
                parser, s;
            if (!cache) {
                cache = this.formulaCache = new Ext.util.LruCache({
                    maxSize: 20
                });
            }
            parser = cache.get(name);
            if (!parser) {
                // Unescaped: [^\.a-z0-9_]NAMEHERE\(\s*(['"])(.*?)\1\s*\)
                s = '[^\\.a-z0-9_]' + Ext.String.escapeRegex(name) + '\\(\\s*([\'"])(.*?)\\1\\s*\\)';
                parser = new RegExp(s, 'gi');
                cache.add(name, parser);
            }
            return parser;
        }
    },
    isFormula: true,
    calculation: null,
    explicit: false,
    /**
     * @cfg {Object} [bind]
     * An explicit bind request to produce data to provide the `get` function. If this is
     * specified, the result of this bind is the first argument to `get`. If not given,
     * then `get` receives a getter function that can retrieve bind expressions. For details
     * on what can be specified for this property see `{@link Ext.app.ViewModel#bind}`.
     * @since 5.0.0
     */
    /**
     * @cfg {Function} get
     * The function to call to calculate the formula's value. The `get` method executes
     * with a `this` pointer of the `ViewModel` and receives a getter function or the result of
     * a configured `bind`.
     * @since 5.0.0
     */
    /**
     * @cfg {Function} [set]
     * If provided this method allows a formula to be set. This method is typically called
     * when `{@link Ext.app.bind.Binding#setValue}` is called. The `set` method executes
     * with a `this` pointer of the `ViewModel`. Whatever values need to be updated can
     * be set by calling `{@link Ext.app.ViewModel#set}`.
     * @since 5.0.0
     */
    set: null,
    /**
     * @cfg {Boolean} [single=false]
     * This option instructs the binding to call its `destroy` method immediately after
     * delivering the initial value.
     * @since 5.0.0
     */
    single: false,
    /* eslint-disable-next-line no-useless-escape */
    fnKeywordArgumentNamesRe: /^function\s*[^\(]*\(\s*([^,\)\s]+)/,
    fnKeywordRe: /^\s*function/,
    /* eslint-disable-next-line no-useless-escape */
    replaceParenRe: /[\(\)]/g,
    constructor: function(stub, formula) {
        var me = this,
            owner = stub.owner,
            bindTo, expressions, getter, options;
        me.owner = owner;
        me.stub = stub;
        me.callParent();
        if (formula instanceof Function) {
            me.get = getter = formula;
        } else {
            me.get = getter = formula.get;
            me.set = formula.set;
            expressions = formula.bind;
            if (formula.single) {
                me.single = formula.single;
            }
            if (expressions) {
                bindTo = expressions.bindTo;
                if (bindTo) {
                    options = Ext.apply({}, expressions);
                    delete options.bindTo;
                    expressions = bindTo;
                }
            }
        }
        if (!getter) {
            Ext.raise('Must specify a getter method for a formula');
        }
        if (expressions) {
            me.explicit = true;
        } else {
            expressions = getter.$expressions || me.parseFormula(getter);
        }
        me.binding = owner.bind(expressions, me.onChange, me, options);
    },
    destroy: function() {
        var me = this,
            binding = me.binding,
            stub = me.stub;
        if (binding) {
            binding.destroy();
            me.binding = null;
        }
        if (stub) {
            stub.formula = null;
        }
        me.callParent();
        // Save for last because this is used to remove us from the Scheduler
        me.getterFn = me.owner = null;
    },
    getFullName: function() {
        return this.fullName || (this.fullName = this.stub.getFullName() + '=' + this.callParent() + ')');
    },
    getRawValue: function() {
        return this.calculation;
    },
    onChange: function() {
        if (!this.scheduled) {
            this.schedule();
        }
    },
    parseFormula: function(formula) {
        var str = Ext.Function.toCode(formula),
            defaultProp = 'get',
            expressions = {
                $literal: true
            },
            match, getterProp, formulaRe, expr;
        if (this.fnKeywordRe.test(str)) {
            match = this.fnKeywordArgumentNamesRe.exec(str);
            if (match) {
                getterProp = match[1];
            }
        } else {
            match = str.split('=>')[0];
            if (match) {
                match = Ext.String.trim(match.replace(this.replaceParenRe, '')).split(',');
                getterProp = match[0];
            }
        }
        getterProp = getterProp || defaultProp;
        formulaRe = Ext.app.bind.Formula.getFormulaParser(getterProp);
        while ((match = formulaRe.exec(str))) {
            expr = match[2];
            expressions[expr] = expr;
        }
        expressions.$literal = true;
        // We store the parse results on the function object because we might reuse the
        // formula function (typically when a ViewModel class is created a 2nd+ time).
        formula.$expressions = expressions;
        return expressions;
    },
    react: function() {
        var me = this,
            owner = me.owner,
            data = me.binding.lastValue,
            arg;
        if (me.explicit) {
            arg = data;
        } else {
            arg = owner.getFormulaFn(data);
        }
        me.settingValue = true;
        me.stub.set(me.calculation = me.get.call(owner, arg));
        me.settingValue = false;
        if (me.single) {
            me.destroy();
        }
    },
    setValue: function(value) {
        this.set.call(this.stub.owner, value);
    },
    privates: {
        getScheduler: function() {
            var owner = this.owner;
            return owner && owner.getScheduler();
        },
        sort: function() {
            var me = this,
                binding = me.binding;
            // Our binding may be single:true
            if (!binding.destroyed) {
                me.scheduler.sortItem(binding);
            }
        }
    }
});
// Schedulable#sort === emptyFn
// me.callParent();

/**
 * This class is a base for classes that want to provide a `fly` static method.
 *
 * For example:
 *
 *      Ext.define('Foo.util.Thing', {
 *          extend: 'Ext.util.Fly',
 *
 *          // useful stuff
 *      });
 *
 *      var thing = Ext.util.Thing.fly(42);  // passes 42 to the reset method
 *
 *      // use "thing"
 *
 *      thing.release();   // return to the pool for future reuse
 *
 * @private
 */
Ext.define('Ext.util.Fly', {
    inheritableStatics: {
        flyPoolSize: 2,
        /**
         * @method
         * Returns a flyweight instance. These instances should be returned when no
         * longer needed by calling `release`.
         *
         * Additional arguments passed to this method will be passed on to the `reset`
         * method.
         *
         * @return {Ext.util.Fly} the flyweight instance
         */
        fly: function() {
            var T = this,
                flyweights = T.flyweights || (T.flyweights = []),
                instance = flyweights.length ? flyweights.pop() : new T();
            instance.reset.apply(instance, arguments);
            return instance;
        }
    },
    /**
     * This method should be called when a flyweight instance is no longer needed and
     * should be returned to the flyweight pool.
     */
    release: function() {
        var me = this,
            T = me.self,
            flyweights = T.flyweights || (T.flyweights = []);
        me.reset();
        if (flyweights.length < T.flyPoolSize) {
            flyweights.push(me);
        }
    },
    /**
     * Resets this instance to prepare for use. Derived classes may accept additional
     * arguments.
     *
     * When called with no arguments, the class should relinquish any resources it can
     * and prepare to wait for potential reuse.
     *
     * @method reset
     * @chainable
     * @return {Ext.util.Fly} this
     */
    reset: Ext.emptyFn
});

/**
 * This class is used to parse a string into a series of tokens. The syntax of the string
 * is JavaScript-like. This class is useful for creating higher-level parsers to allow
 * them to assemble tokens into a meaningful language (such as bind properties).
 *
 * The following set of punctuation characters are supported:
 *
 *      + - * / ! , : [ ] { } ( )
 *
 * This class does not currently separate the dot operator but instead includes it in a
 * single "ident" token. Whitespace between tokens is skipped.
 *
 * Tokens are parsed on-demand when `next` or `peek` are called. As much as possible,
 * the returned tokens are reused (e.g., to represent tokens like ":" the same object is
 * always returned). For tokens that contain values, a new object must be created to
 * return the value. Even so, the `is` property that describes the data is a reused object
 * in all cases.
 *
 *      var tokenizer;  // see below for getting instance
 *
 *      for (;;) {
 *          if (!(token = tokenizer.next())) {
 *              // When null is returned, there are no more tokens
 *
 *              break;
 *          }
 *
 *          var is = token.is;  // the token's classification object
 *
 *          if (is.error) {
 *              // Once an error is encountered, it will always be returned by
 *              // peek or next. The error is cleared by calling reset().
 *
 *              console.log('Syntax error', token.message);
 *              break;
 *          }
 *
 *          if (is.ident) {
 *              // an identifier...
 *              // use token.value to access the name or dot-path
 *
 *              var t = tokenizer.peek();  // don't consume next token (yet)
 *
 *              if (t && t.is.parenOpen) {
 *                  tokenizer.next();  // we'll take this one
 *
 *                  parseThingsInParens();
 *
 *                  t = tokenizer.next();
 *
 *                  mustBeCloseParen(t);
 *              }
 *          }
 *          else if (is.literal) {
 *              // a literal value (null, true/false, string, number)
 *              // use token.value to access the value
 *          }
 *          else if (is.at) {
 *              // @
 *          }
 *      }
 *
 * For details on the returned token see the `peek` method.
 *
 * There is a pool of flyweight instances to reduce memory allocation.
 *
 *      var tokenizer = Ext.parse.Tokenizer.fly('some.thing:foo()');
 *
 *      // use tokenizer (see above)
 *
 *      tokenizer.release();  // returns the fly to the flyweigt pool
 *
 * The `release` method returns the flyweight to the pool for later reuse. Failure to call
 * `release` will leave the flyweight empty which simply forces the `fly` method to always
 * create new instances on each call.
 *
 * A tokenizer can also be reused by calling its `reset` method and giving it new text to
 * tokenize.
 *
 *      this.tokenizer = new Ext.parse.Tokenizer();
 *
 *      // Later...
 *
 *      this.tokenizer.reset('some.thing:foo()');
 *
 *      // use tokenizer (see above)
 *
 *      this.tokenizer.reset();
 *
 * The final call to `reset` is optional but will avoid holding large text strings or
 * parsed results that rae no longer needed.
 *
 * @private
 */
Ext.define('Ext.parse.Tokenizer', function(Tokenizer) {
    var flyweights = (Tokenizer.flyweights = []),
        BOOLEAN = {
            literal: true,
            "boolean": true,
            type: 'boolean'
        },
        ERROR = {
            error: true
        },
        IDENT = {
            ident: true
        },
        LITERAL = {
            literal: true
        },
        NULL = {
            literal: true,
            nil: true
        },
        NUMBER = {
            literal: true,
            number: true,
            type: 'number'
        },
        STRING = {
            literal: true,
            string: true,
            type: 'string'
        };
    /* eslint-disable indent */
    return {
        extend: 'Ext.util.Fly',
        isTokenizer: true,
        statics: {
            BOOLEAN: BOOLEAN,
            ERROR: ERROR,
            IDENT: IDENT,
            LITERAL: LITERAL,
            NULL: NULL,
            NUMBER: NUMBER,
            STRING: STRING
        },
        config: {
            /**
         * @cfg {Object} keywords
         * A map of keywords that should be mapped to other token types. By default the
         * `null`, `true` and `false` keywords are mapped to their respective literal
         * value tokens.
         */
            keywords: {
                'null': {
                    type: 'literal',
                    is: NULL,
                    value: null
                },
                'false': {
                    type: 'literal',
                    is: BOOLEAN,
                    value: false
                },
                'true': {
                    type: 'literal',
                    is: BOOLEAN,
                    value: true
                }
            },
            /**
         * @cfg {Object} operators
         * A map of operators and their names. The keys are the operator text and the
         * name (the values) are placed in the token's `is` object as `true`.
         */
            operators: {
                '+': 'plus',
                '-': 'minus',
                '*': 'multiply',
                '/': 'divide',
                '!': 'not',
                ',': 'comma',
                ':': 'colon',
                '[': 'arrayOpen',
                ']': 'arrayClose',
                '{': 'curlyOpen',
                '}': 'curlyClose',
                '(': 'parenOpen',
                ')': 'parenClose'
            },
            patterns: null
        },
        /**
     * This property is set to an `Error` instance if the parser encounters a syntax
     * error.
     * @property {Object} error
     * @readonly
     */
        error: null,
        /**
     * This property is set to the character index of the current token. This value can
     * be captured immediately after calling the `peek` or `next` method to know the
     * index of the returned token. This value is not included in the returned token to
     * allow those tokens that could otherwise be immutable to be reused.
     * @property {Number} index
     * @readonly
     */
        index: -1,
        constructor: function(config) {
            this.operators = {};
            this.patterns = [];
            this.initConfig(config);
        },
        /**
     * Advance the token stream and return the next token. See `{@link #peek}` for a
     * description of the returned token.
     *
     * After calling this method, the next call to it or `peek` will not return the same
     * token but instead the token that follows the one returned.
     *
     * @return {Object} The next token in the stream (now consumed).
     */
        next: function() {
            var token = this.peek();
            this.head = undefined;
            // indicates that more parsing is needed (see peek)
            return token;
        },
        /**
     * Peeks at the next token stream and returns it. The token remains as the next token
     * and will be returned again by the next call to this method or `next`.
     *
     * At the end of the token stream, the token returned will be `null`.
     *
     * If a syntax error is encountered, the returned token will be an `Error` object. It
     * has the standard `message` property and also additional properties to make it more
     * like a standard token: `error: true`, `type: 'error'` and `at` (the index in the
     * string where the syntax error started.
     *
     * @return {Object} The next token in the stream (not yet consumed).
     *
     * @return {String} return.type The type of the token. This will be one of the
     * following values: `ident`, `literal` and `error` or the text of a operator
     * (i.e., "@", "!", ",", ":", "[", "]", "{", "}", "(" or ")").
     *
     * @return {String} return.value The value of a `"literal"` token.
     *
     * @return {Object} return.is An object containing boolean properties based on type.
     * @return {Boolean} return.is.literal True if the token is a literal value.
     * @return {Boolean} return.is.boolean True if the token is a literal boolean value.
     * @return {Boolean} return.is.error True if the token is an error.
     * @return {Boolean} return.is.ident True if the token is an identifier.
     * @return {Boolean} return.is.nil True if the token is the `null` keyword.
     * @return {Boolean} return.is.number True if the token is a number literal.
     * @return {Boolean} return.is.string True if the token is a string literal.
     * @return {Boolean} return.is.operator True if the token is a operator (i.e.,
     * "@!,:[]{}()"). operators will also have one of these boolean proprieties, in
     * the respective order: `at`, `not`, `comma`, `colon`, `arrayOpen`, `arrayClose`,
     * `curlyOpen`, `curlyClose`, `parentOpen` and `parenClose`).
     */
        peek: function() {
            var me = this,
                error = me.error,
                token = me.head;
            if (error) {
                return error;
            }
            if (token === undefined) {
                me.head = token = me.advance();
            }
            return token;
        },
        /**
     * Returns this flyweight instance to the flyweight pool for reuse.
     */
        release: function() {
            this.reset();
            if (flyweights.length < Tokenizer.flyPoolSize) {
                flyweights.push(this);
            }
        },
        /**
     * Resets the tokenizer for a new string at a given offset (defaults to 0).
     *
     * @param {String} text The text to tokenize.
     * @param {Number} [pos=0] The character position at which to start.
     * @param {Number} [end] The index of the first character beyond the token range.
     * @returns {Ext.parse.Tokenizer}
     */
        reset: function(text, pos, end) {
            var me = this;
            me.error = null;
            me.head = undefined;
            me.index = -1;
            me.text = text || null;
            me.pos = pos || 0;
            me.end = (text && end == null) ? text.length : end;
            return me;
        },
        privates: {
            digitRe: /[0-9]/,
            identFirstRe: /[a-z_$]/i,
            identRe: /[0-9a-z_$]/i,
            spaceRe: /[ \t]/,
            /**
         * The index one beyond the last character of the input text. This defaults to
         * the `text.length`.
         * @property {Number} end
         * @readonly
         */
            end: 0,
            /**
         * The current token at the head of the token stream. This will be `undefined`
         * if the next token must be parsed from `text`. It is `null` if there are no
         * more tokens.
         * @property {Object} head
         * @readonly
         */
            head: undefined,
            /**
         * The current character position in the `text` from which the next token will
         * be parsed.
         * @property {Number} pos
         * @readonly
         */
            pos: 0,
            /**
         * The text to be tokenized.
         * @property {String} text
         * @readonly
         */
            text: null,
            applyOperators: function(ops) {
                var operators = this.operators,
                    block, c, def, i, len, name, op;
                /*
             Builds a map one character at a time (i.e., a "trie"):

                operators: {
                    '=': {
                        '=': {
                            token: // the "==" token
                        },

                        token:  // the "=" token
                    }
                }
             */
                for (op in ops) {
                    block = operators;
                    name = ops[op];
                    len = op.length;
                    for (i = 0; i < len; ++i) {
                        c = op.charAt(i);
                        block = block[c] || (block[c] = {});
                    }
                    if (name) {
                        block.token = def = {
                            type: 'operator',
                            name: name,
                            value: op,
                            is: {
                                operator: true
                            }
                        };
                        def.is[name] = true;
                    } else {
                        block.token = null;
                    }
                }
            },
            applyPatterns: function(pat) {
                var patterns = this.patterns,
                    def, extract, name, re;
                for (name in pat) {
                    def = pat[name];
                    extract = def.extract;
                    re = def.re;
                    delete def.extract;
                    delete def.re;
                    patterns.push({
                        name: name,
                        re: re,
                        extract: extract,
                        token: def
                    });
                }
            },
            /**
         * Parses and returns the next token from `text` starting at `pos`.
         * @return {Object} The next token
         */
            advance: function() {
                var me = this,
                    spaceRe = me.spaceRe,
                    text = me.text,
                    length = me.end,
                    c;
                while (me.pos < length) {
                    c = text.charAt(me.pos);
                    if (spaceRe.test(c)) {
                        ++me.pos;
                        // consume the whitespace
                        
                        continue;
                    }
                    me.index = me.pos;
                    return me.parse(c);
                }
                return null;
            },
            /**
         * Parses the current token that starts with the provided character `c` and
         * located at the current `pos` in the `text`.
         * @param {String} c The current character.
         * @return {Object} The next token
         */
            parse: function(c) {
                var me = this,
                    digitRe = me.digitRe,
                    text = me.text,
                    length = me.end,
                    patterns = me.patterns,
                    i, match, pat, ret;
                // Handle ".123"
                if (c === '.' && me.pos + 1 < length) {
                    if (digitRe.test(text.charAt(me.pos + 1))) {
                        ret = me.parseNumber();
                    }
                }
                if (!ret) {
                    for (i = 0; i < patterns.length; ++i) {
                        pat = patterns[i];
                        pat.re.lastIndex = me.pos;
                        match = pat.re.exec(text);
                        if (match && match.index === me.pos) {
                            ret = Ext.apply({
                                value: pat.extract ? pat.extract(match) : match[0]
                            }, pat.token);
                            me.pos += match[0].length;
                            break;
                        }
                    }
                }
                if (!ret && me.operators[c]) {
                    ret = me.parseOperator(c);
                }
                if (!ret) {
                    if (c === '"' || c === "'") {
                        ret = me.parseString();
                    } else if (digitRe.test(c)) {
                        ret = me.parseNumber();
                    } else if (me.identFirstRe.test(c)) {
                        ret = me.parseIdent();
                    } else {
                        ret = me.syntaxError('Unexpected character');
                    }
                }
                return ret;
            },
            /**
         * Parses the next identifier token.
         * @return {Object} The next token.
         */
            parseIdent: function() {
                var me = this,
                    identRe = me.identRe,
                    keywords = me.getKeywords(),
                    includeDots = !me.operators['.'],
                    text = me.text,
                    start = me.pos,
                    end = start,
                    length = me.end,
                    prev = 0,
                    c, value;
                while (end < length) {
                    c = text.charAt(end);
                    if (includeDots && c === '.') {
                        if (prev === '.') {
                            return me.syntaxError(end, 'Unexpected dot operator');
                        }
                        ++end;
                    } else if (identRe.test(c)) {
                        ++end;
                    } else {
                        break;
                    }
                    prev = c;
                }
                if (prev === '.') {
                    return me.syntaxError(end - 1, 'Unexpected dot operator');
                }
                value = text.substring(start, me.pos = end);
                return (keywords && keywords[value]) || {
                    type: 'ident',
                    is: IDENT,
                    value: value
                };
            },
            /**
         * Parses the next number literal token.
         * @return {Object} The next token.
         */
            parseNumber: function() {
                var me = this,
                    digitRe = me.digitRe,
                    text = me.text,
                    start = me.pos,
                    length = me.end,
                    c, decimal, exp, token;
                while (me.pos < length) {
                    c = text.charAt(me.pos);
                    if (c === '-' || c === '+') {
                        if (me.pos !== start) {
                            break;
                        }
                        ++me.pos;
                    } else if (c === '.') {
                        if (decimal) {
                            break;
                        }
                        decimal = true;
                        ++me.pos;
                    } else if (c === 'e' || c === 'E') {
                        if (exp) {
                            break;
                        }
                        decimal = exp = true;
                        // exp from here on, no decimal allowed
                        c = text.charAt(++me.pos);
                        // consume E and peek ahead
                        if (c === '-' || c === '+') {
                            ++me.pos;
                        }
                    }
                    // keep the exp sign
                    else if (digitRe.test(c)) {
                        ++me.pos;
                    } else {
                        break;
                    }
                }
                token = {
                    type: 'literal',
                    is: NUMBER,
                    // Beware parseFloat as it will stop parsing and return what it could
                    // parse. For example parseFloat('1x') == 1 whereas +'1x' == NaN.
                    value: +text.substring(start, me.pos)
                };
                if (!isFinite(token.value)) {
                    token = me.syntaxError(start, 'Invalid number');
                }
                return token;
            },
            parseOperator: function(c) {
                var me = this,
                    block = me.operators,
                    text = me.text,
                    length = me.end,
                    end = me.pos,
                    match, matchEnd, token;
                while (block[c]) {
                    block = block[c];
                    token = block.token;
                    ++end;
                    if (token) {
                        match = token;
                        matchEnd = end;
                    }
                    if (end < length) {
                        c = text.charAt(end);
                    } else {
                        break;
                    }
                }
                if (match) {
                    me.pos = matchEnd;
                }
                return match;
            },
            /**
         * Parses the next string literal token.
         * @return {Object} The next token.
         */
            parseString: function() {
                var me = this,
                    text = me.text,
                    pos = me.pos,
                    start = pos,
                    length = me.end,
                    str = '',
                    c, closed, quote;
                quote = text.charAt(pos++);
                while (pos < length) {
                    c = text.charAt(pos++);
                    if (c === quote) {
                        closed = true;
                        break;
                    }
                    if (c === '\\' && pos < length) {
                        c = text.charAt(pos++);
                    }
                    // Processing escapes means we cannot use substring() to pick up the
                    // text as a single chunk...
                    str += c;
                }
                me.pos = pos;
                if (!closed) {
                    return me.syntaxError(start, 'Unterminated string');
                }
                return {
                    type: 'literal',
                    is: STRING,
                    value: str
                };
            },
            /**
         * This method is called when a syntax error is encountered. It updates `error`
         * and returns the error token.
         * @param {Number} at The index of the syntax error (optional).
         * @param {String} message The error message.
         * @return {Object} The error token.
         */
            syntaxError: function(at, message) {
                if (typeof at === 'string') {
                    message = at;
                    at = this.pos;
                }
                // eslint-disable-next-line vars-on-top
                var suffix = (at == null) ? '' : (' (at index ' + at + ')'),
                    error = new Error(message + suffix);
                error.type = 'error';
                error.is = ERROR;
                if (suffix) {
                    error.at = at;
                }
                return this.error = error;
            }
        }
    };
});

/**
 * This class represents a symbol in the parser.
 * @private
 */
Ext.define('Ext.parse.Symbol', {
    priority: 0,
    /**
     * This property holds the name of the property to update when a config provided is
     * not an object (just a value).
     * @property {String} defaultProperty
     */
    constructor: function(id, config) {
        var me = this,
            defaultProperty = me.defaultProperty;
        if (config && typeof config === 'object') {
            Ext.apply(me, config);
        } else if (config !== undefined && defaultProperty) {
            me[defaultProperty] = config;
        }
        me.id = id;
    },
    dump: function() {
        var me = this,
            ret = {
                at: me.at,
                arity: me.arity
            },
            i;
        if ('value' in me) {
            ret.value = me.value;
        }
        if (me.lhs) {
            ret.lhs = me.lhs.dump();
            ret.rhs = me.rhs.dump();
        }
        if (me.operand) {
            ret.operand = me.operand.dump();
        }
        if (me.args) {
            ret.args = [];
            for (i = 0; i < me.args.length; ++i) {
                ret.args.push(me.args[i].dump());
            }
        }
        return ret;
    },
    /**
     * This abstract method is implemented by operators that follow their operand (like
     * a binary operator). When the symbol is encountered in an expression this method
     * is called. The name "led" stands for "left denotation".
     *
     * @param {Ext.parse.Symbol} left
     */
    led: function() {
        this.parser.syntaxError(this.at, 'Missing operator');
    },
    /**
     * This abstract method is implemented by operators that precede their operand (like
     * a unary operator). When the symbol is encountered in an expression this method
     * is called. The name "nud" stands for "null denotation".
     */
    nud: function() {
        this.parser.syntaxError(this.at, 'Undefined');
    },
    /**
     * This method updates this symbol given an additional config object. This is used
     * when a symbol is placed in multiple categories (such `infix` and `prefix`). The
     * `priority` is the most likely value to update, but also a `led` or `nud` method
     * may be provided to complete the symbol.
     *
     * @param {Object} config
     */
    update: function(config) {
        if (config && typeof config === 'object') {
            // eslint-disable-next-line vars-on-top
            var me = this,
                priority = config.priority,
                led = config.led,
                nud = config.nud;
            if (me.priority <= priority) {
                me.priority = priority;
            }
            if (led) {
                me.led = led;
            }
            if (nud) {
                me.nud = nud;
            }
        }
    }
});

/**
 * This class represents a constant in the parser.
 * @private
 */
Ext.define('Ext.parse.symbol.Constant', {
    extend: 'Ext.parse.Symbol',
    arity: 'literal',
    isLiteral: true,
    defaultProperty: 'value',
    constructor: function(id, config) {
        this.callParent([
            id,
            config
        ]);
        this._value = this.value;
    },
    nud: function() {
        var me = this;
        // The value property gets smashed by the parser so restore it.
        me.value = me._value;
        // the next line is here in case this symbol already exists in the symbols table
        // and this function overrides that symbol
        me.arity = 'literal';
        me.isLiteral = true;
        return me;
    }
});

/**
 * This class represents an infix (binary) operator.
 * @private
 */
Ext.define('Ext.parse.symbol.Infix', {
    extend: 'Ext.parse.Symbol',
    arity: 'binary',
    isBinary: true,
    defaultProperty: 'priority',
    led: function(left) {
        var me = this;
        me.lhs = left;
        me.rhs = me.parser.parseExpression(me.priority);
        // the next line is here in case this symbol already exists in the symbols table
        // and this function overrides that symbol
        me.arity = 'binary';
        me.isBinary = true;
        return me;
    }
});

/**
 * This class represents an right-associative, infix (binary) operator.
 * @private
 */
Ext.define('Ext.parse.symbol.InfixRight', {
    extend: 'Ext.parse.symbol.Infix',
    led: function(left) {
        var me = this;
        me.lhs = left;
        me.rhs = me.parser.parseExpression(me.priority - 1);
        // the next line is here in case this symbol already exists in the symbols table
        // and this function overrides that symbol
        me.arity = 'binary';
        me.isBinary = true;
        return me;
    }
});

/**
 * This class implements the parenthesis operator.
 * @private
 */
Ext.define('Ext.parse.symbol.Paren', {
    extend: 'Ext.parse.Symbol',
    arity: 'binary',
    isBinary: true,
    priority: 80,
    led: function(left) {
        // Handles function call operator
        var me = this,
            args = [],
            parser = me.parser,
            id = left.id,
            type = left.arity;
        if (id !== '.' && id !== '[') {
            if ((type !== "unary" || id !== "function") && type !== "ident" && id !== "(" && id !== "&&" && id !== "||" && id !== "?") {
                parser.syntaxError(left.at, "Expected a variable name.");
            }
        }
        me.arity = 'invoke';
        me.isInvoke = true;
        me.operand = left;
        me.args = args;
        while (parser.token.id !== ')') {
            if (args.length) {
                parser.advance(',');
            }
            args.push(parser.parseExpression());
        }
        parser.advance(')');
        return me;
    },
    nud: function() {
        // Handles parenthesized expressions
        var parser = this.parser,
            ret = parser.parseExpression();
        parser.advance(")");
        return ret;
    }
});

/**
 * This class represents a prefix (unary) operator.
 * @private
 */
Ext.define('Ext.parse.symbol.Prefix', {
    extend: 'Ext.parse.Symbol',
    arity: 'unary',
    isUnary: true,
    priority: 70,
    nud: function() {
        var me = this;
        me.operand = me.parser.parseExpression(me.priority);
        // the next line is here in case this symbol already exists in the symbols table
        // and this function overrides that symbol
        me.arity = 'unary';
        me.isUnary = true;
        return me;
    }
});

/**
 * This class parses simple expressions. The parser can be enhanced by providing any of
 * the following configs:
 *
 *  * `constants`
 *  * `infix`
 *  * `infixRight`
 *  * `postfix`
 *  * `symbols`
 *
 * The parser requires a `{@link Ext.parse.Tokenizer tokenizer}` which can be configured
 * using the `tokenizer` config. The parser keeps the tokenizer instance and recycles it
 * as it is itself reused.
 *
 * See http://javascript.crockford.com/tdop/tdop.html for background on the techniques
 * used in this parser.
 * @private
 */
Ext.define('Ext.parse.Parser', function() {
    var ITSELF = function() {
            return this;
        };
    /* eslint-disable indent */
    return {
        extend: 'Ext.util.Fly',
        requires: [
            'Ext.parse.Tokenizer',
            'Ext.parse.symbol.Constant',
            'Ext.parse.symbol.InfixRight',
            'Ext.parse.symbol.Paren',
            'Ext.parse.symbol.Prefix'
        ],
        isParser: true,
        config: {
            /**
         * @cfg {Object} constants
         * A map of identifiers that should be converted to literal value tokens. The
         * key in this object is the name of the constant and the value is the constant
         * value.
         *
         * If the value of a key is an object, it is a config object for the
         * `{@link Ext.parse.symbol.Constant constant}`.
         */
            constants: {
                'null': null,
                'false': false,
                'true': true
            },
            /**
         * @cfg {Object} infix
         * A map of binary operators and their associated precedence (or binding priority).
         * These binary operators are left-associative.
         *
         * If the value of a key is an object, it is a config object for the
         * `{@link Ext.parse.symbol.Infix operator}`.
         */
            infix: {
                '===': 40,
                '!==': 40,
                '==': 40,
                '!=': 40,
                '<': 40,
                '<=': 40,
                '>': 40,
                '>=': 40,
                '+': 50,
                '-': 50,
                '*': 60,
                '/': 60
            },
            /**
         * @cfg {Object} infixRight
         * A map of binary operators and their associated precedence (or binding priority).
         * These binary operators are right-associative.
         *
         * If the value of a key is an object, it is a config object for the
         * `{@link Ext.parse.symbol.InfixRight operator}`.
         */
            infixRight: {
                '&&': 30,
                '||': 30
            },
            /**
         * @cfg {Object} prefix
         * A map of unary operators. Typically no value is needed, so `0` is used.
         *
         * If the value of a key is an object, it is a config object for the
         * `{@link Ext.parse.symbol.Prefix operator}`.
         */
            prefix: {
                '!': 0,
                '-': 0,
                '+': 0
            },
            /**
         * @cfg {Object} symbols
         * General language symbols. The values in this object are used as config objects
         * to configure the associated `{@link Ext.parse.Symbol symbol}`. If there is no
         * configuration, use `0` for the value.
         */
            symbols: {
                ':': 0,
                ',': 0,
                ')': 0,
                '[': 0,
                ']': 0,
                '{': 0,
                '}': 0,
                '(end)': 0,
                '(ident)': {
                    arity: 'ident',
                    isIdent: true,
                    nud: ITSELF
                },
                '(literal)': {
                    arity: 'literal',
                    isLiteral: true,
                    nud: ITSELF
                },
                '(': {
                    xclass: 'Ext.parse.symbol.Paren'
                }
            },
            /**
         * @cfg {Object/Ext.parse.Tokenizer} tokenizer
         * The tokenizer or a config object used to create one.
         */
            tokenizer: {
                keywords: null
            }
        },
        // we'll handle keywords here
        /**
     * @cfg {Ext.parse.Symbol} token
     * The current token. These tokens extend this base class and contain additional
     * properties such as:
     *
     *   * `at` - The index of the token in the text.
     *   * `value` - The value of the token (e.g., the name of an identifier).
     *
     * @readonly
     */
        token: null,
        constructor: function(config) {
            this.symbols = {};
            this.initConfig(config);
        },
        /**
     * Advances the token stream and returns the next `token`.
     * @param {String} [expected] The type of symbol that is expected to follow.
     * @return {Ext.parse.Symbol}
     */
        advance: function(expected) {
            var me = this,
                tokenizer = me.tokenizer,
                token = tokenizer.peek(),
                symbols = me.symbols,
                index = tokenizer.index,
                is, name, symbol, value;
            if (me.error) {
                throw me.error;
            }
            if (expected) {
                me.expect(expected);
            }
            if (!token) {
                return me.token = symbols['(end)'];
            }
            tokenizer.next();
            is = token.is;
            value = token.value;
            if (is.ident) {
                symbol = symbols[value] || symbols['(ident)'];
            } else if (is.operator) {
                if (!(symbol = symbols[value])) {
                    me.syntaxError(token.at, 'Unknown operator "' + value + '"');
                }
                name = token.name;
            } else if (is.literal) {
                symbol = symbols['(literal)'];
            } else {
                me.syntaxError(token.at, 'Unexpected token');
            }
            me.token = symbol = Ext.Object.chain(symbol);
            symbol.at = index;
            symbol.is = is;
            symbol.value = value;
            if (!symbol.arity) {
                symbol.arity = token.type;
            }
            if (name) {
                symbol.name = name;
            }
            return symbol;
        },
        expect: function(expected) {
            var token = this.token;
            if (expected !== token.id) {
                this.syntaxError(token.at, 'Expected "' + expected + '"');
            }
            return this;
        },
        /**
     *
     * @param {Number} [rightPriority=0] The precedence of the current operator.
     * @return {Ext.parse.Symbol} The parsed expression tree.
     */
        parseExpression: function(rightPriority) {
            var me = this,
                token = me.token,
                left;
            rightPriority = rightPriority || 0;
            me.advance();
            left = token.nud();
            while (rightPriority < (token = me.token).priority) {
                me.advance();
                left = token.led(left);
            }
            return left;
        },
        /**
     * Resets this parser given the text to parse or a `Tokenizer`.
     * @param {String} text
     * @param {Number} [pos=0] The character position at which to start.
     * @param {Number} [end] The index of the first character beyond the token range.
     * @return {Ext.parse.Parser}
     */
        reset: function(text, pos, end) {
            var me = this;
            me.error = me.token = null;
            me.tokenizer.reset(text, pos, end);
            me.advance();
            // kick start this.token
            return me;
        },
        /**
     * This method is called when a syntax error is encountered. It updates `error`
     * and returns the error token.
     * @param {Number} at The index of the syntax error (optional).
     * @param {String} message The error message.
     * @return {Object} The error token.
     */
        syntaxError: function(at, message) {
            if (typeof at === 'string') {
                message = at;
                at = this.pos;
            }
            // eslint-disable-next-line vars-on-top
            var suffix = (at == null) ? '' : (' (at index ' + at + ')'),
                error = new Error(message + suffix);
            error.type = 'error';
            if (suffix) {
                error.at = at;
            }
            throw this.error = error;
        },
        privates: {
            /**
         * This property is set to an `Error` instance if the parser encounters a syntax
         * error.
         * @property {Object} error
         * @readonly
         */
            error: null,
            addSymbol: function(id, config, type, update) {
                var symbols = this.symbols,
                    symbol = symbols[id],
                    cfg, length, i;
                if (symbol) {
                    // If the symbol was already defined then we need to update it
                    // we either use the config provided in the symbol definition
                    // or we use the `update` param to build a config object.
                    // We usually need to update either `led` or `nud` function
                    if (typeof config === 'object') {
                        cfg = config;
                    } else if (update && type) {
                        update = Ext.Array.from(update);
                        length = update.length;
                        cfg = {};
                        for (i = 0; i < length; i++) {
                            cfg[update[i]] = type.prototype[update[i]];
                        }
                    } else {
                        return symbol;
                    }
                    symbol.update(cfg);
                } else {
                    if (config && config.xclass) {
                        type = Ext.ClassManager.get(config.xclass);
                    } else {
                        type = type || Ext.parse.Symbol;
                    }
                    symbols[id] = symbol = new type(id, config);
                    symbol.parser = this;
                }
                return symbol;
            },
            addSymbols: function(symbols, type, update) {
                var id;
                for (id in symbols) {
                    this.addSymbol(id, symbols[id], type, update);
                }
            },
            applyConstants: function(constants) {
                this.addSymbols(constants, Ext.parse.symbol.Constant, 'nud');
            },
            applyInfix: function(operators) {
                this.addSymbols(operators, Ext.parse.symbol.Infix, 'led');
            },
            applyInfixRight: function(operators) {
                this.addSymbols(operators, Ext.parse.symbol.InfixRight, 'led');
            },
            applyPrefix: function(operators) {
                this.addSymbols(operators, Ext.parse.symbol.Prefix, 'nud');
            },
            applySymbols: function(symbols) {
                this.addSymbols(symbols);
            },
            applyTokenizer: function(config) {
                var ret = config;
                if (config && !config.isTokenizer) {
                    ret = new Ext.parse.Tokenizer(config);
                }
                this.tokenizer = ret;
            }
        }
    };
});

/**
 * This class parses bind template format expressions.
 * @private
 */
Ext.define('Ext.app.bind.Parser', {
    extend: 'Ext.parse.Parser',
    requires: [
        'Ext.util.Format'
    ],
    infix: {
        ':': {
            priority: 70,
            // bind tighter than multiplication
            dump: function() {
                var me = this,
                    ret = {
                        at: me.at,
                        arity: me.arity,
                        value: me.value,
                        operand: me.operand.dump(),
                        fmt: []
                    },
                    fmt = me.fmt,
                    i;
                for (i = 0; i < fmt.length; ++i) {
                    ret.fmt.push(fmt[i].dump());
                }
                return ret;
            },
            led: function(left) {
                // We parse a sequence of ":" separated formatter expressions (like a
                // traditional "," operator) and gather the sequence in our "fmt" array
                var me = this;
                me.arity = 'formatter';
                me.operand = left;
                me.fmt = me.parser.parseFmt();
                return me;
            }
        },
        '?': {
            priority: 20,
            led: function(left) {
                var me = this,
                    parser = me.parser,
                    symbol = parser.symbols[':'],
                    temp;
                me.condition = left;
                // temporarily set priority of `:` symbol to 0
                temp = symbol.priority;
                symbol.priority = 0;
                me.tv = parser.parseExpression(0);
                me.parser.advance(':');
                // restore priority of `:`
                symbol.priority = temp;
                me.fv = parser.parseExpression(0);
                me.arity = 'ternary';
                return me;
            }
        }
    },
    symbols: {
        '(': {
            nud: function() {
                // Handles parenthesized expressions
                var parser = this.parser,
                    symbol = parser.symbols[':'],
                    ret, temp;
                // temporarily set priority of `:` symbol to 70 to correctly extract formatters 
                // inside parens
                temp = symbol.priority;
                symbol.priority = 70;
                ret = parser.parseExpression();
                parser.advance(")");
                // restore priority of `:`
                symbol.priority = temp;
                return ret;
            }
        }
    },
    prefix: {
        '@': 0
    },
    tokenizer: {
        operators: {
            '@': 'at',
            '?': 'qmark',
            '===': 'feq',
            '!==': 'fneq',
            '==': 'eq',
            '!=': 'neq',
            '<': 'lt',
            '<=': 'lte',
            '>': 'gt',
            '>=': 'gte',
            '&&': 'and',
            '||': 'or'
        }
    },
    /**
     * Parses the expression from the current position and compiles it as a function.
     * The expression tokens are stored in the provided arguments.
     *
     * Called by Ext.app.bind.Template.
     *
     * @param {Array} tokens
     * @param {Object} tokensMaps
     * @return {Function}
     */
    compileExpression: function(tokens, tokensMaps) {
        var me = this,
            debug, fn;
        me.tokens = tokens;
        me.tokensMap = tokensMaps;
        debug = me.token.value === '@' && me.tokenizer.peek();
        if (debug) {
            debug = debug.value === 'debugger';
            if (debug) {
                me.advance();
                me.advance();
            }
        }
        fn = me.parseSlot(me.parseExpression(), debug);
        me.tokens = me.tokensMap = null;
        return fn;
    },
    /**
     * Parses the chained format functions and compiles them as a function.
     *
     * Called by the grid column formatter.
     *
     * @return {Function}
     */
    compileFormat: function() {
        var me = this,
            fn;
        try {
            fn = me.parseSlot({
                arity: 'formatter',
                fmt: me.parseFmt(),
                operand: {
                    arity: 'ident',
                    value: 'dummy'
                }
            });
            me.expect('(end)');
        } catch (e) {
            Ext.raise('Invalid format expression: "' + me.tokenizer.text + '"');
        }
        return fn;
    },
    privates: {
        // Chrome really likes "new Function" to realize the code block (as in it is
        // 2x-3x faster to call it than using eval), but Firefox chokes on it badly.
        // IE and Opera are also fine with the "new Function" technique.
        useEval: Ext.isGecko,
        escapeRe: /(["'\\])/g,
        /**
         * Parses a series of ":" delimited format expressions.
         * @return {Ext.parse.Symbol[]}
         * @private
         */
        parseFmt: function() {
            // We parse a sequence of ":" separated formatter expressions (like a
            // traditional "," operator)
            var me = this,
                fmt = [],
                priority = me.symbols[':'].priority,
                expr;
            do {
                if (fmt.length) {
                    me.advance();
                }
                expr = me.parseExpression(priority);
                if (expr.isIdent || expr.isInvoke) {
                    fmt.push(expr);
                } else {
                    me.syntaxError(expr.at, 'Expected formatter name');
                }
            } while (me.token.id === ':');
            return fmt;
        },
        /**
         * Parses the expression tree and compiles it as a function
         *
         * @param expr
         * @param {Boolean} debug
         * @return {Function}
         * @private
         */
        parseSlot: function(expr, debug) {
            var me = this,
                defs = [],
                body = [],
                tokens = me.tokens || [],
                fn, code, i, length, temp;
            me.definitions = defs;
            me.body = body;
            body.push('return ' + me.compile(expr) + ';');
            // now we have the tokens
            length = tokens.length;
            code = 'var fm = Ext.util.Format,\nme,';
            temp = 'var a = Ext.Array.from(values);\nme = scope;\n';
            if (tokens.length) {
                for (i = 0; i < length; i++) {
                    code += 'v' + i + ((i === length - 1) ? ';' : ',');
                    temp += 'v' + i + ' = a[' + i + ']; ';
                }
            } else {
                code += 'v0;';
                temp += 'v0 = a[0];';
            }
            defs = Ext.Array.insert(defs, 0, [
                code
            ]);
            body = Ext.Array.insert(body, 0, [
                temp
            ]);
            body = body.join('\n');
            if (debug) {
                body = 'debugger;\n' + body;
            }
            defs.push((me.useEval ? '$=' : 'return') + ' function (values, scope) {', body, '}');
            code = defs.join('\n');
            fn = me.useEval ? me.evalFn(code) : (new Function('Ext', code))(Ext);
            me.definitions = me.body = null;
            return fn;
        },
        /**
         * Compiles the specified symbol
         *
         * @param expr
         * @return {String}
         * @private
         */
        compile: function(expr) {
            var me = this,
                v;
            switch (expr.arity) {
                case 'ident':
                    // identifiers are our expression's tokens
                    return me.addToken(expr.value);
                case 'literal':
                    v = expr.value;
                    // strings need to be escaped before adding them to formula
                    return (typeof v === 'string') ? '"' + String(v).replace(me.escapeRe, '\\$1') + '"' : v;
                case 'unary':
                    return me.compileUnary(expr);
                case 'binary':
                    return me.compileBinary(expr);
                case 'ternary':
                    return me.compileTernary(expr);
                case 'formatter':
                    return me.compileFormatter(expr);
            }
            return this.syntaxError(expr.at, 'Compile error! Unknown symbol');
        },
        /**
         * Compiles unary symbol
         *
         * @param expr
         * @return {String}
         * @private
         */
        compileUnary: function(expr) {
            var v = expr.value,
                op = expr.operand;
            if (v === '!' || v === '-' || v === '+') {
                return v + '(' + this.compile(op) + ')';
            } else if (v === '@') {
                // @ should be used to prefix global identifiers and nothing else
                if (!op.isIdent) {
                    return this.syntaxError(expr.at, 'Compile error! Unexpected symbol');
                }
                return op.value;
            }
            return '';
        },
        /**
         * Compiles binary symbol
         *
         * @param expr
         * @return {String}
         * @private
         */
        compileBinary: function(expr) {
            return '(' + this.compile(expr.lhs) + ' ' + expr.value + ' ' + this.compile(expr.rhs) + ')';
        },
        /**
         * Compiles ternary symbol
         *
         * @param expr
         * @return {String}
         * @private
         */
        compileTernary: function(expr) {
            return '(' + this.compile(expr.condition) + ' ? ' + this.compile(expr.tv) + ' : ' + this.compile(expr.fv) + ')';
        },
        /**
         * Compiles formatter symbol
         *
         * @param expr
         * @return {String}
         * @private
         */
        compileFormatter: function(expr) {
            var me = this,
                fmt = expr.fmt,
                length = fmt.length,
                body = [
                    'var ret;'
                ],
                i;
            if (fmt.length) {
                body.push('ret = ' + me.compileFormatFn(fmt[0], me.compile(expr.operand)) + ';');
                for (i = 1; i < length; i++) {
                    body.push('ret = ' + me.compileFormatFn(fmt[i], 'ret') + ';');
                }
            }
            body.push('return ret;');
            return me.addFn(body.join('\n'));
        },
        /**
         * Compiles a single format symbol using `value` as the first argument
         *
         * @param expr
         * @param value
         * @return {String}
         * @private
         */
        compileFormatFn: function(expr, value) {
            var fmt,
                args = [],
                code = '',
                length, i;
            if (expr.isIdent) {
                // the function has no arguments
                fmt = expr.value;
            } else if (expr.isInvoke) {
                fmt = expr.operand.value;
                args = expr.args;
            }
            if (fmt.substring(0, 5) === 'this.') {
                fmt = 'me.' + fmt.substring(5);
            } else {
                if (!(fmt in Ext.util.Format)) {
                    return this.syntaxError(expr.at, 'Compile error! Invalid format specified "' + fmt + '"');
                }
                fmt = 'fm.' + fmt;
            }
            code += value;
            length = args.length;
            for (i = 0; i < length; i++) {
                code += ', ' + this.compile(args[i]);
            }
            return fmt + '(' + code + ')';
        },
        /**
         * Adds a new function to the final compiled function
         * @param body
         * @return {string} Name of the function
         * @private
         */
        addFn: function(body) {
            var defs = this.definitions,
                name = 'f' + defs.length;
            defs.push('function ' + name + '() {', body, '}');
            return name + '()';
        },
        /**
         * Evaluates a function
         * @param $
         * @return {Function}
         * @private
         */
        evalFn: function($) {
            eval($);
            return $;
        },
        /**
         * Adds the specified expression token to the internal tokens
         * @param token
         * @return {string} Name of the variable assigned for this token in the compiled function
         * @private
         */
        addToken: function(token) {
            var tokensMap = this.tokensMap,
                tokens = this.tokens,
                pos = 0;
            // token can be ignored when this function is called via `compileFormatFn`
            if (tokensMap && tokens) {
                if (token in tokensMap) {
                    pos = tokensMap[token];
                } else {
                    tokensMap[token] = pos = tokens.length;
                    tokens.push(token);
                }
            }
            return 'v' + pos;
        }
    }
});

/**
 * This class holds the parsed text for a bind template. The syntax is that of a normal
 * `Ext.Template` except that substitution tokens can contain dots to reference property
 * names.
 *
 * The template is parsed and stored in a representation like this:
 *
 *      me.text = 'Hey {foo.bar}! Test {bar} and {foo.bar} with {abc} over {bar:number}'
 *
 *      me.tokens = [ 'foo.bar', 'bar', 'abc' ]
 *
 *      me.buffer = [           me.slots = [
 *          'Hey ',                 undefined,
 *          undefined,              { token: 'foo.bar', pos: 0 },
 *          '! Test ',              undefined,
 *          undefined,              { token: 'bar', pos: 1 },
 *          ' and ',                undefined,
 *          undefined,              { token: 'foo.bar', pos: 0 },
 *          ' with ',               undefined,
 *          undefined,              { token: 'abc', pos: 2 },
 *          ' over ',               undefined,
 *          undefined               { token: 'bar', fmt: 'number', pos: 1 }
 *      ]                       ]
 *
 * @private
 * @since 5.0.0
 */
Ext.define('Ext.app.bind.Template', {
    requires: [
        'Ext.util.Format',
        'Ext.app.bind.Parser'
    ],
    /**
     * @cfg {Boolean} escapes
     * Set to `true` to process escape characters as part of bind expressions.
     *
     * The `'\'` character is used to escape the next character, treating it
     * as a literal character even if it is a `'{'` or other escape.
     *
     * The `'~~'` sequence will treat any subsequent characters as a verbatim,
     * literal expression and no extra processing will take place. This includes
     * escapes and replacement tokens.
     *
     * @since 6.5.2
     * @private
     */
    escapes: false,
    /**
     * @property {String[]} buffer
     * Initially this is just the array of string fragments with `null` between each
     * to hold the place of a substitution token. On first use these slots are filled
     * with the token's value and this array is joined to form the output.
     * @private
     */
    buffer: null,
    /**
     * @property {Object[]} slots
     * The elements of this array line up with those of `buffer`. This array holds
     * the parsed information for the substitution token that fills a given slot in
     * the generated string. Indices that correspond to literal text are `null`.
     *
     * Consider the following substitution token:
     *
     *      {foo:this.fmt(2,4)}
     *
     * The object in this array has the following properties to describe this token:
     *
     *   * `fmt` The name of the formatting function ("fmt") or `null` if none.
     *   * `index` The numeric index if this is not a named substitution or `null`.
     *   * `not` True if the token has a logical not ("!") at the front.
     *   * `token` The name of the token ("foo") if not an `index`.
     *   * `pos` The position of this token in the `tokens` array.
     *   * `scope` A reference to the object on which the `fmt` method exists. This
     *    will be `Ext.util.Format` if no "this." is present or `null` if it is (or
     *    if there is no `fmt`). In the above example, this is `null` to indicate the
     *    scope is unknown.
     *   * `args` An array of arguments to `fmt` if the arguments are simple enough
     *    to parse directly. Otherwise this is `null` and `fn` is used.
     *   * `fn` A generated function to use to evaluate the arguments to the `fmt`. In
     *    rare cases these arguments can reference global variables so the expression
     *    must be evaluated on each call.
     *   * `format` The method to call to perform the format. This method accepts the
     *    scope (in case `scope` is unknown) and the value. This function is `null` if
     *    there is no `fmt`.
     *
     * @private
     */
    slots: null,
    /**
     * @property {String[]} tokens
     * The distinct set of tokens used in the template excluding formatting. This is
     * used to ensure that only one bind is performed per unique token. This array is
     * passed to {@link Ext.app.ViewModel#bind} to perform a "multi-bind". The result
     * is an array of values corresponding these tokens. Each entry in `slots` then
     * knows its `pos` in this array from which to pick up its value, apply formats
     * and place in `buffer`.
     * @private
     */
    tokens: null,
    /**
     * @param {String} text The text of the template.
     */
    constructor: function(text) {
        var me = this,
            initters = me._initters,
            name;
        me.text = text;
        for (name in initters) {
            me[name] = initters[name];
        }
    },
    /**
     * @property {Object} _initters
     * Each of the methods contained on this object are placed in new instances to lazily
     * parse the template text.
     * @private
     * @since 5.0.0
     */
    _initters: {
        apply: function(values, scope) {
            return this.parse().apply(values, scope);
        },
        getTokens: function() {
            return this.parse().getTokens();
        }
    },
    /**
     * Applies this template to the given `values`. The `values` must correspond to the
     * `tokens` returned by `getTokens`.
     *
     * @param {Array} values The values of the `tokens`.
     * @param {Object} scope The object instance to use for "this." formatter calls in the
     * template.
     * @return {String}
     * @since 5.0.0
     */
    apply: function(values, scope) {
        var me = this,
            slots = me.slots,
            buffer = me.buffer,
            length = slots.length,
            i, slot;
        for (i = 0; i < length; ++i) {
            slot = slots[i];
            if (slot) {
                buffer[i] = slot(values, scope);
            }
        }
        // If we have only one component and it is a slot (a {} component), then we
        // want to evaluate to whatever that expression generated.
        if (slot && me.single) {
            return buffer[0];
        }
        return buffer.join('');
    },
    getText: function() {
        return this.buffer.join('');
    },
    /**
     * Returns the distinct set of binding tokens for this template.
     * @return {String[]} The `tokens` for this template.
     */
    getTokens: function() {
        return this.tokens;
    },
    /**
     * Returns true if the expression is static, meaning it has no
     * tokens or slots that need to be evaluated.
     *
     * @private
     */
    isStatic: function() {
        var tokens = this.getTokens(),
            slots = this.slots;
        return (tokens.length === 0 && slots.length === 0);
    },
    privates: {
        literalChar: '~',
        escapeChar: '\\',
        /**
         * Parses the template text into `buffer`, `slots` and `tokens`. This method is called
         * automatically when the template is first used.
         * @return {Ext.app.bind.Template} this
         * @private
         */
        parse: function() {
            // NOTE: The particulars of what is stored here, while private, are likely to be
            // important to Sencha Architect so changes need to be coordinated.
            var me = this,
                text = me.text,
                parser = Ext.app.bind.Parser.fly(),
                buffer = (me.buffer = []),
                slots = (me.slots = []),
                length = text.length,
                pos = 0,
                escapes = me.escapes,
                current = '',
                i = 0,
                esc = me.escapeChar,
                lit = me.literalChar,
                escaped, lastEscaped, c, prev, key;
            // Remove the initters so that we don't get called here again.
            for (key in me._initters) {
                delete me[key];
            }
            me.tokens = [];
            me.tokensMap = {};
            // text = 'Hello {foo:this.fmt(2,4)} World {bar} - {1}'
            while (i < length) {
                c = text[i];
                lastEscaped = escaped;
                escaped = escapes && c === esc;
                if (escaped) {
                    c = text[i + 1];
                    ++i;
                } else if (c === lit && prev === lit && !lastEscaped) {
                    current = current.slice(0, -1);
                    current += text.substring(i + 1);
                    break;
                } else if (c === '{') {
                    if (current) {
                        buffer[pos++] = current;
                        current = '';
                    }
                    // parse expression
                    parser.reset(text, i + 1);
                    i = me.parseExpression(parser, pos);
                    ++pos;
                    
                    continue;
                }
                current += c;
                ++i;
                prev = c;
            }
            if (current) {
                buffer[pos] = current;
            }
            parser.release();
            me.single = buffer.length === 0 && slots.length === 1;
            return me;
        },
        parseExpression: function(parser, pos) {
            var i;
            this.slots[pos] = parser.compileExpression(this.tokens, this.tokensMap);
            i = parser.token.at + 1;
            // skip over the "}" token
            parser.expect('}');
            // ensure the next token is "}"
            return i;
        }
    }
});

/**
 * This class is created to manage a template against a `ViewModel`. A binding of this
 * type uses `{@link Ext.app.bind.Template}` to process the template text so see that
 * class for details on template syntax.
 *
 * The bindings to provide the data needed by the template are managed here.
 */
Ext.define('Ext.app.bind.TemplateBinding', {
    extend: 'Ext.app.bind.BaseBinding',
    requires: [
        'Ext.app.bind.Multi',
        'Ext.app.bind.Template'
    ],
    isTemplateBinding: true,
    lastValue: undefined,
    value: undefined,
    constructor: function(template, owner, callback, scope, options) {
        var me = this,
            tpl = new Ext.app.bind.Template(template),
            tokens = tpl.getTokens();
        me.callParent([
            owner,
            callback,
            scope,
            options
        ]);
        me.tpl = tpl;
        me.tokens = tokens;
        tokens.$literal = true;
        // If we don't have any tokens, then we've just got a static string.
        if (!tpl.isStatic()) {
            me.multiBinding = new Ext.app.bind.Multi(tokens, owner, me.onBindData, me);
        } else {
            me.isStatic = true;
            me.onData(tpl.getText());
        }
    },
    destroy: function() {
        var me = this;
        Ext.destroy(me.multiBinding);
        me.tpl = me.multiBinding = null;
        me.callParent();
    },
    getFullName: function() {
        var multi = this.multiBinding;
        return this.fullName || (this.fullName = '$' + (multi ? multi.getFullName() : this.callParent()));
    },
    getRawValue: function() {
        return this.value;
    },
    getTemplateScope: function() {
        return null;
    },
    isAvailable: function() {
        var multi = this.multiBinding;
        return multi ? multi.isAvailable() : false;
    },
    isDescendantOf: function() {
        return false;
    },
    isLoading: function() {
        var multi = this.multiBinding;
        return multi ? multi.isLoading() : false;
    },
    onBindData: function(data) {
        this.onData(this.tpl.apply(data, this.getTemplateScope()));
    },
    onData: function(value) {
        var me = this,
            lastValue = me.value;
        if (lastValue !== (me.value = value)) {
            me.lastValue = lastValue;
            me.schedule();
        }
    },
    react: function() {
        this.notify(this.value);
    },
    refresh: function() {
        var multi = this.multiBinding;
        if (multi) {
            multi.refresh();
        }
    },
    privates: {
        sort: function() {
            var multi = this.multiBinding;
            if (multi) {
                this.scheduler.sortItem(multi);
            }
        }
    }
});
// Schedulable#sort === emptyFn
// me.callParent();

/**
 * A chained store is a store that is a "view" of an existing store. The data comes from the
 * {@link #source}, however this view of the store may be sorted & filtered independently without
 * having any impact on the {@link #source} store.
 */
Ext.define('Ext.data.ChainedStore', {
    extend: 'Ext.data.AbstractStore',
    alias: 'store.chained',
    mixins: [
        'Ext.data.LocalStore'
    ],
    /**
     * @property {Boolean} isChainedStore
     * `true` in this class to identify an object as an instantiated ChainedStore, or subclass
     * thereof.
     */
    isChainedStore: true,
    config: {
        /**
         * @cfg {Ext.data.Store/String} source
         * The backing data source for this chained store. Either a store instance
         * or the id of an existing store.
         */
        source: null,
        remoteFilter: false,
        remoteSort: false
    },
    syncSourceGrouping: false,
    updateRemoteFilter: function(remoteFilter, oldRemoteFilter) {
        if (remoteFilter) {
            Ext.raise('Remote filtering cannot be used with chained stores.');
        }
        this.callParent([
            remoteFilter,
            oldRemoteFilter
        ]);
    },
    updateRemoteSort: function(remoteSort, oldRemoteSort) {
        if (remoteSort) {
            Ext.raise('Remote sorting cannot be used with chained stores.');
        }
        this.callParent([
            remoteSort,
            oldRemoteSort
        ]);
    },
    remove: function() {
        var source = this.getSource();
        if (!source) {
            Ext.raise('Cannot remove records with no source.');
        }
        return source.remove.apply(source, arguments);
    },
    removeAll: function() {
        var source = this.getSource();
        if (!source) {
            Ext.raise('Cannot remove records with no source.');
        }
        return source.removeAll();
    },
    getData: function() {
        var me = this,
            data = me.data;
        if (!data) {
            me.data = data = me.constructDataCollection();
        }
        return data;
    },
    getTotalCount: function() {
        return this.getCount();
    },
    getSession: function() {
        return this.getSourceValue('getSession', null);
    },
    applySource: function(source) {
        if (source) {
            /* eslint-disable-next-line vars-on-top */
            var original = source,
                s;
            source = Ext.data.StoreManager.lookup(source);
            if (!source) {
                s = 'Invalid source {0}specified for Ext.data.ChainedStore';
                s = Ext.String.format(s, typeof original === 'string' ? '"' + original + '" ' : '');
                Ext.raise(s);
            }
        }
        return source;
    },
    updateSource: function(source, oldSource) {
        var me = this,
            data;
        if (oldSource && !oldSource.destroyed) {
            oldSource.removeObserver(me);
        }
        if (source) {
            data = me.getData();
            data.setSource(source.getData());
            if (me.syncSourceGrouping) {
                me.setGrouper(source.getGrouper());
            }
            if (!me.isInitializing) {
                me.fireEvent('refresh', me);
                me.fireEvent('datachanged', me);
            }
            source.addObserver(me);
        }
    },
    /**
     * Get the model used for this store.
     * @return {Ext.data.Model} The model
     */
    getModel: function() {
        return this.getSourceValue('getModel', null);
    },
    getProxy: function() {
        return null;
    },
    onCollectionAdd: function(collection, info) {
        var me = this,
            records = info.items,
            lastChunk = !info.next;
        if (me.ignoreCollectionAdd) {
            return;
        }
        // Collection add changes the items reference of the collection, and that array
        // object if directly referenced by Ranges. The ranges have to refresh themselves
        // upon add.
        if (me.activeRanges) {
            me.syncActiveRanges();
        }
        me.fireEvent('add', me, records, info.at);
        // If there is a next property, that means there is another range that needs
        // to be removed after this. Wait until everything is gone before firign datachanged
        // since it should be a bulk operation
        if (lastChunk) {
            me.fireEvent('datachanged', me);
        }
    },
    // Our collection tells us that an item has changed
    onCollectionItemChange: function(collection, info) {
        var me = this,
            record = info.item,
            modifiedFieldNames = info.modified || null,
            type = info.meta;
        if (me.activeRanges && info.newIndex !== info.oldIndex) {
            me.syncActiveRanges();
        }
        // Inform any interested parties that a record has been mutated.
        // This will be invoked on TreeStores in which the invoking record
        // is an descendant of a collapsed node, and so *will not be contained by this store
        me.onUpdate(record, type, modifiedFieldNames, info);
        me.fireEvent('update', me, record, type, modifiedFieldNames, info);
        me.fireEvent('datachanged', me);
    },
    onCollectionUpdateKey: function(source, details) {
        // Must react to upstream Collection key update by firing idchanged event
        this.fireEvent('idchanged', this, details.item, details.oldKey, details.newKey);
    },
    onUpdate: Ext.emptyFn,
    lastCollectionRefesh: null,
    onCollectionRefresh: function(collection) {
        var me = this,
            gen = collection.generation;
        if (!me.isConfiguring && me.lastCollectionRefesh !== gen) {
            me.lastCollectionRefesh = gen;
            me.fireEvent('datachanged', me);
            me.fireEvent('refresh', me);
        }
    },
    onCollectionRemove: function(collection, info) {
        var me = this,
            records = info.items,
            lastChunk = !info.next;
        if (me.ignoreCollectionRemove) {
            return;
        }
        me.fireEvent('remove', me, records, info.at, false);
        // If there is a next property, that means there is another range that needs
        // to be removed after this. Wait until everything is gone before firign datachanged
        // since it should be a bulk operation
        if (lastChunk) {
            me.fireEvent('datachanged', me);
        }
    },
    onSourceBeforeLoad: function(source, operation) {
        this.fireEvent('beforeload', this, operation);
        this.callObservers('BeforeLoad', [
            operation
        ]);
    },
    onSourceAfterLoad: function(source, records, successful, operation) {
        this.fireEvent('load', this, records, successful, operation);
        this.callObservers('AfterLoad', [
            records,
            successful,
            operation
        ]);
    },
    onFilterEndUpdate: function() {
        var me = this;
        if (me.getData().generation === me.lastCollectionRefesh && !me.getRemoteFilter()) {
            me.suppressNextFilter = true;
        }
        me.callParent(arguments);
        me.callObservers('Filter');
    },
    onSourceBeforePopulate: function() {
        this.ignoreCollectionAdd = true;
        this.callObservers('BeforePopulate');
    },
    onSourceAfterPopulate: function() {
        var me = this;
        me.ignoreCollectionAdd = false;
        me.fireEvent('datachanged', me);
        me.fireEvent('refresh', me);
        this.callObservers('AfterPopulate');
    },
    onSourceBeforeClear: function() {
        this.ignoreCollectionRemove = true;
        this.callObservers('BeforeClear');
    },
    onSourceAfterClear: function() {
        this.ignoreCollectionRemove = false;
        this.callObservers('AfterClear');
    },
    onSourceBeforeRemoveAll: function() {
        this.ignoreCollectionRemove = true;
        this.callObservers('BeforeRemoveAll');
    },
    onSourceAfterRemoveAll: function(source, silent) {
        var me = this;
        me.ignoreCollectionRemove = false;
        if (!silent) {
            me.fireEvent('clear', me);
            me.fireEvent('datachanged', me);
        }
        this.callObservers('AfterRemoveAll', [
            silent
        ]);
    },
    onSourceFilter: function() {
        var me = this,
            gen = me.getData().generation;
        if (me.lastCollectionRefesh !== gen) {
            me.lastCollectionRefesh = gen;
            me.fireEvent('refresh', me);
            me.fireEvent('datachanged', me);
        }
    },
    onSourceGrouperChange: function(source, grouper) {
        if (this.syncSourceGrouping) {
            this.setGrouper(grouper);
        }
    },
    hasPendingLoad: function() {
        return this.getSourceValue('hasPendingLoad', false);
    },
    isLoaded: function() {
        return this.getSourceValue('isLoaded', false);
    },
    isLoading: function() {
        return this.getSourceValue('isLoading', false);
    },
    doDestroy: function() {
        var me = this;
        me.observers = null;
        me.setSource(null);
        me.getData().destroy(true);
        me.data = null;
        me.callParent();
    },
    privates: {
        getSourceValue: function(method, defaultValue) {
            var source = this.getSource(),
                val = defaultValue;
            if (source && !source.destroyed) {
                val = source[method]();
            }
            return val;
        },
        isMoving: function() {
            var source = this.getSource();
            return source.isMoving ? source.isMoving.apply(source, arguments) : false;
        },
        loadsSynchronously: function() {
            return this.getSource().loadsSynchronously();
        }
    }
});
// Provides docs from the mixin
/**
     * @method add
     * @inheritdoc Ext.data.LocalStore#add
     */
/**
     * @method each
     * @inheritdoc Ext.data.LocalStore#each
     */
/**
     * @method collect
     * @inheritdoc Ext.data.LocalStore#collect
     */
/**
     * @method getById
     * @inheritdoc Ext.data.LocalStore#getById
     */
/**
     * @method getByInternalId
     * @inheritdoc Ext.data.LocalStore#getByInternalId
     */
/**
     * @method indexOf
     * @inheritdoc Ext.data.LocalStore#indexOf
     */
/**
     * @method indexOfId
     * @inheritdoc Ext.data.LocalStore#indexOfId
     */
/**
     * @method insert
     * @inheritdoc Ext.data.LocalStore#insert
     */
/**
     * @method queryBy
     * @inheritdoc Ext.data.LocalStore#queryBy
     */
/**
     * @method query
     * @inheritdoc Ext.data.LocalStore#query
     */
/**
     * @method first
     * @inheritdoc Ext.data.LocalStore#first
     */
/**
     * @method last
     * @inheritdoc Ext.data.LocalStore#last
     */
/**
     * @method sum
     * @inheritdoc Ext.data.LocalStore#sum
     */
/**
     * @method count
     * @inheritdoc Ext.data.LocalStore#count
     */
/**
     * @method min
     * @inheritdoc Ext.data.LocalStore#min
     */
/**
     * @method max
     * @inheritdoc Ext.data.LocalStore#max
     */
/**
     * @method average
     * @inheritdoc Ext.data.LocalStore#average
     */
/**
     * @method aggregate
     * @inheritdoc Ext.data.LocalStore#aggregate
     */

/**
 * This class manages arbitrary data and its relationship to data models. Instances of
 * `ViewModel` are associated with some `Component` and then used by their child items
 * for the purposes of Data Binding.
 * 
 * # Binding
 * 
 * The most commonly used aspect of a `ViewModel` is the `bind` method. This method takes
 * a "bind descriptor" (see below) and a callback to call when the data indicated by the
 * bind descriptor either becomes available or changes.
 *
 * The `bind` method, based on the bind descriptor given, will return different types of
 * "binding" objects. These objects maintain the connection between the requested data and
 * the callback. Bindings ultimately derive from `{@link Ext.app.bind.BaseBinding}`
 * which provides several methods to help manage the binding.
 *
 * Perhaps the most important method is `destroy`. When the binding is no longer needed
 * it is important to remember to `destroy` it. Leaking bindings can cause performance
 * problems or worse when callbacks are called at unexpected times.
 *
 * The types of bindings produced by `bind` are:
 *
 *   * `{@link Ext.app.bind.Binding}`
 *   * `{@link Ext.app.bind.Multi}`
 *   * `{@link Ext.app.bind.TemplateBinding}`
 *
 * ## Bind Descriptors
 * 
 * A "bind descriptor" is a value (a String, an Object or an array of these) that describe
 * the desired data. Any piece of data in the `ViewModel` can be described by a bind
 * descriptor.
 * 
 * ### Textual Bind Descriptors
 * 
 * The simplest and most common form of bind descriptors are strings that look like an
 * `Ext.Template` containing text and tokens surrounded by "{}" with dot notation inside
 * to traverse objects and their properties.
 * 
 * For example:
 * 
 *   * `'Hello {user.name}!'`
 *   * `'You have selected "{selectedItem.text}".'`
 *   * `'{!isDisabled}'`
 *   * `'{a > b ? "Bigger" : "Smaller"}'`
 *   * `'{user.groups}'`
 *
 * All except the last are `{@link Ext.app.bind.TemplateBinding template bindings}`
 * which use the familiar `Ext.Template` syntax with some slight differences. For more on
 * templates see `{@link Ext.app.bind.Template}`.
 *
 * The last descriptor is called a "direct bind descriptor". This special form of
 * bind maps one-to-one to some piece of data in the `ViewModel` and is managed by the
 * `{@link Ext.app.bind.Binding}` class.
 *
 * #### Two-Way Descriptors
 *
 * A direct bind descriptor may be able to write back a value to the `ViewModel` as well
 * as retrieve one. When this is the case, they are said to be "two-way". For example:
 *
 *      var binding = viewModel.bind('{s}', function(s) { console.log('s=' + s); });
 *
 *      binding.setValue('abc');
 *
 * Direct use of `ViewModel` in this way is not commonly needed because `Ext.Component`
 * automates this process. For example, a `textfield` component understands when it is
 * given a "two-way" binding and automatically synchronizes its value bidirectionally using
 * the above technique. For example:
 *
 *      Ext.widget({
 *          items: [{
 *              xtype: 'textfield',
 *              bind: '{s}'  // a two-way / direct bind descriptor
 *          }]
 *      });
 *
 * ### Object and Array Descriptors / Multi-Bind
 *
 * With two exceptions (see below) an Object is interpreted as a "shape" to produce by
 * treating each of its properties as individual bind descriptors. An object of the same
 * shape is passed as the value of the bind except that each property is populated with
 * the appropriate value. Of course, this definition is recursive, so these properties
 * may also be objects.
 *
 * For example:
 *
 *      viewModel.bind({
 *              x: '{x}',
 *              foo: {
 *                  bar: 'Hello {foo.bar}'
 *              }
 *          },
 *          function (obj) {
 *              //  obj = {
 *              //      x: 42,
 *              //      foo: {
 *              //          bar: 'Hello foobar'
 *              //      }
 *              //  }
 *          });
 *
 * Arrays are handled in the same way. Each element of the array is considered a bind
 * descriptor (recursively) and the value produced for the binding is an array with each
 * element set to the bound property.
 *
 * ### Bind Options
 *
 * One exception to the "object is a multi-bind" rule is when that object contains a
 * `bindTo` property. When an object contains a `bindTo` property the object is understood
 * to contain bind options and the value of `bindTo` is considered the actual bind
 * descriptor.
 *
 * For example:
 *
 *      viewModel.bind({
 *              bindTo: '{x}',
 *              single: true
 *          },
 *          function (x) {
 *              console.log('x: ' + x); // only called once
 *          });
 *
 * The available bind options depend on the type of binding, but since all bindings
 * derive from `{@link Ext.app.bind.BaseBinding}` its options are always applicable.
 * For a list of the other types of bindings, see above.
 *
 * #### Deep Binding
 *
 * When a direct bind is made and the bound property is an object, by default the binding
 * callback is only called when that reference changes. This is the most efficient way to
 * understand a bind of this type, but sometimes you may need to be notified if any of the
 * properties of that object change.
 *
 * To do this, we create a "deep bind":
 *
 *      viewModel.bind({
 *              bindTo: '{someObject}',
 *              deep: true
 *          },
 *          function (someObject) {
 *              // called when reference changes or *any* property changes
 *          });
 *
 * #### Binding Timings
 *
 * The `ViewModel` has a {@link #scheduler} attached that is used to coordinate the firing of
 * bindings.
 * It serves 2 main purposes:
 * - To coordinate dependencies between bindings. This means bindings will be fired in an order
 * such that the any dependencies for a binding are fired before the binding itself.
 * - To batch binding firings. The scheduler runs on a short timer, so the following code will
 * only trigger a single binding (the last), the changes in between will never be triggered.
 *
 * Example:
 *
 *     viewModel.bind('{val}', function(v) {
 *         console.log(v);
 *     });
 *     viewModel.set('val', 1);
 *     viewModel.set('val', 2);
 *     viewModel.set('val', 3);
 *     viewModel.set('val', 4);
 *
 * The `ViewModel` can be forced to process by calling `{@link #notify}`, which will force the
 * scheduler to run immediately in the current state.
 * 
 *     viewModel.bind('{val}', function(v) {
 *         console.log(v);
 *     });
 *     viewModel.set('val', 1);
 *     viewModel.notify();
 *     viewModel.set('val', 2);
 *     viewModel.notify();
 *     viewModel.set('val', 3);
 *     viewModel.notify();
 *     viewModel.set('val', 4);
 *     viewModel.notify();
 *  
 *
 * #### Models, Stores and Associations
 *
 * A {@link Ext.data.Session Session} manages model instances and their associations.
 * The `ViewModel` may be used with or without a `Session`. When a `Session` is attached, the
 * `ViewModel` will always consult the `Session` to ask about records and stores. The `Session`
 * ensures that only a single instance of each model Type/Id combination is created. This is 
 * important when tracking changes in models so that we always have the same reference.
 *
 * A `ViewModel` provides functionality to easily consume the built in data package types
 * {@link Ext.data.Model} and {@link Ext.data.Store}, as well as their associations.
 *
 * ### Model Links
 *
 * A model can be described declaratively using {@link #links}. In the example code below,
 * We ask the `ViewModel` to construct a record of type `User` with `id: 17`. The model will be
 * loaded from the server and the bindings will trigger once the load has completed. Similarly,
 * we could also attach a model instance to the `ViewModel` data directly.
 *
 *     Ext.define('MyApp.model.User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['name']
 *     });
 *     
 *     var rec = new MyApp.model.User({
 *         id: 12,
 *         name: 'Foo'
 *     });
 *     
 *     var viewModel = new Ext.app.ViewModel({
 *         links: {
 *             theUser: {
 *                 type: 'User',
 *                 id: 17
 *             }
 *         },
 *         data: {
 *             otherUser: rec
 *         }
 *     });
 *     viewModel.bind('{theUser.name}', function(v) {
 *         console.log(v);
 *     });
 *     viewModel.bind('{otherUser.name}', function(v) {
 *         console.log(v);
 *     });
 *
 * ### Model Fields
 *
 * Bindings have the functionality to inspect the parent values and resolve the underlying
 * value dynamically. This behavior allows model fields to be interrogated as part of a binding.
 *
 *     Ext.define('MyApp.model.User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['name', 'age']
 *     });
 *
 *     var viewModel = new Ext.app.ViewModel({
 *         links: {
 *             theUser: {
 *                 type: 'User',
 *                 id: 22
 *             }
 *         }
 *     });
 *
 *     // Server responds with:
 *     {
 *         "id": 22,
 *         "name": "Foo",
 *         "age": 100
 *     }
 *
 *     viewModel.bind('Hello {name}, you are {age} years old', function(v) {
 *         console.log(v);
 *     });
 *
 * ### Record Properties
 *
 * It is possible to bind to the certain state properties of a record. The available options are:
 * - `{@link Ext.data.Model#property-dirty dirty}`
 * - `{@link Ext.data.Model#property-phantom phantom}`
 * - `{@link Ext.data.Model#method-isValid valid}`
 *
 * Example usage:
 *
 *     Ext.define('MyApp.model.User', {
 *         extend: 'Ext.data.Model',
 *         fields: [{
 *             name: 'name',
 *             validators: 'presence'
 *         }, {
 *             name: 'age',
 *             validators: {
 *                type: 'range',
 *                 min: 0
 *              }
 *         }]
 *     });
 *
 *     var rec = new MyApp.model.User();
 *
 *     var viewModel = new Ext.app.ViewModel({
 *         data: {
 *             theUser: rec
 *         }
 *     });
 *
 *     viewModel.bind({
 *         dirty: '{theUser.dirty}',
 *         phantom: '{theUser.phantom}',
 *         valid: '{theUser.valid}'
 *     }, function(v) {
 *         console.log(v.dirty, v.valid);
 *     });
 *
 *     rec.set('name', 'Foo');
 *     viewModel.notify(); // dirty, not valid
 *     rec.set('age', 20);
 *     viewModel.notify(); // dirty, valid
 *     rec.reject();
 *     viewModel.notify(); // not dirty, not valid
 *
 * ### Advanced Record Binding
 *
 * For accessing other record information that is not exposed by the binding API, formulas
 * can be used to achieve more advanced operations:
 *
 *     Ext.define('MyApp.model.User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['name', 'age']
 *     });
 *
 *     var rec = new MyApp.model.User();
 *
 *     var viewModel = new Ext.app.ViewModel({
 *         formulas: {
 *             isNameModified: {
 *                 bind: {
 *                     bindTo: '{theUser}',
 *                     deep: true
 *                 },
 *                 get: function(rec) {
 *                     return rec.isModified('name');
 *                 }
 *             }
 *         },
 *         data: {
 *             theUser: rec
 *         }
 *     });
 *
 *     viewModel.bind('{isNameModified}', function(modified) {
 *         console.log(modified);
 *     });
 *     rec.set('name', 'Foo');
 *
 * ### Associations
 *
 * In the same way as fields, the bindings can also traverse associations in a bind statement.
 * The `ViewModel` will handle the asynchronous loading of data and only present the value once
 * the full path has been loaded. For more information on associations see
 * {@link Ext.data.schema.OneToOne OneToOne} and {@link Ext.data.schema.ManyToOne ManyToOne}
 * associations.
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['name']
 *     });
 *
 *     Ext.define('Order', {
 *         extend: 'Ext.data.Model',
 *         fields: ['date', {
 *             name: 'userId',
 *             reference: 'User'
 *         }]
 *     });
 *
 *     Ext.define('OrderItem', {
 *         extend: 'Ext.data.Model',
 *         fields: ['price', 'qty', {
 *             name: 'orderId',
 *             reference: 'Order'
 *         }]
 *     });
 *
 *     var viewModel = new Ext.app.ViewModel({
 *         links: {
 *             orderItem: {
 *                 type: 'OrderItem',
 *                 id: 13
 *             }
 *         }
 *     });
 *     // The viewmodel will handle both ways of loading the data:
 *     // a) If the data is loaded inline in a nested fashion it will
 *     //    not make requests for extra data
 *     // b) Only loading a single model at a time. So the Order will be loaded once
 *     //    the OrderItem returns. The User will be loaded once the Order loads.
 *     viewModel.bind('{orderItem.order.user.name}', function(name) {
 *         console.log(name);
 *     });
 *
 * ### Stores
 *
 * Stores can be created as part of the `ViewModel` definition. The definitions are processed
 * like bindings which allows for very powerful dynamic functionality.
 *
 * It is important to ensure that you name viewModel's data keys uniquely. If data is not named  
 * uniquely, binds and formulas may receive information from an unintended data source.  
 * This applies to keys in the viewModel's data block, stores, and links configs.
 *
 *     var viewModel = new Ext.app.ViewModel({
 *         stores: {
 *             users: {
 *                 model: 'User',
 *                 autoLoad: true,
 *                 filters: [{
 *                     property: 'createdDate',
 *                     value: '{createdFilter}',
 *                     operator: '>'
 *                 }]
 *             }
 *         }
 *     });
 *     // Later on in our code, we set the date so that the store is created.
 *     viewModel.set('createdFilter', Ext.Date.subtract(new Date(), Ext.Date.DAY, 7));
 *
 * See {@link #stores} for more detail.
 *
 * ### Store Properties
 *
 * It is possible to bind to the certain state properties of the store. The available options are:
 * - `{@link Ext.data.Store#method-getCount count}`
 * - `{@link Ext.data.Store#method-first}`
 * - `{@link Ext.data.Store#method-last}`
 * - `{@link Ext.data.Store#method-hasPendingLoad loading}`
 * - `{@link Ext.data.Store#method-getTotalCount totalCount}`
 *
 * Example:
 *
 *     Ext.define('MyApp.model.User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['name']
 *     });
 *
 *     var viewModel = new Ext.app.ViewModel({
 *         stores: {
 *             users: {
 *                 model: 'MyApp.model.User',
 *                 data: [{
 *                     name: 'Foo'
 *                 }, {
 *                     name: 'Bar'
 *                 }]
 *             }
 *         }
 *     });
 *
 *     viewModel.bind('{users.first}', function(first) {
 *         console.log(first ? first.get('name') : 'Nobody');
 *     });
 *
 *     var timer = Ext.interval(function() {
 *         var store = viewModel.getStore('users');
 *         if (store.getCount()) {
 *             store.removeAt(0);
 *         } else {
 *             Ext.uninterval(timer);
 *         }
 *     }, 100);
 *
 * ### Advanced Store Binding
 *
 * For accessing other store information that is not exposed by the binding API, formulas
 * can be used to achieve more advanced operations:
 *
 *     Ext.define('MyApp.model.User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['name', 'score']
 *     });
 *
 *     var viewModel = new Ext.app.ViewModel({
 *         stores: {
 *             users: {
 *                 model: 'MyApp.model.User',
 *                 data: [{
 *                     name: 'Foo',
 *                     score: 100
 *                 }, {
 *                     name: 'Bar',
 *                     score: 350
 *                 }]
 *             }
 *         },
 *         formulas: {
 *             totalScore: {
 *                 bind: {
 *                     bindTo: '{users}',
 *                     deep: true
 *                 },
 *                 get: function(store) {
 *                     return store.sum('score');
 *                 }
 *             }
 *         }
 *     });
 *
 *     viewModel.bind('{totalScore}', function(score) {
 *         console.log(score);
 *     });
 *
 *     viewModel.notify();
 *     viewModel.getStore('users').removeAll();
 *
 * #### Formulas
 *
 * Formulas allow for calculated `ViewModel` data values. The dependencies for these formulas
 * are automatically determined so that the formula will not be processed until the required
 * data is present.
 *
 *     var viewModel = new Ext.app.ViewModel({
 *         formulas: {
 *             fullName: function(get) {
 *                 return get('firstName') + ' ' + get('lastName');
 *             }
 *         },
 *         data: {firstName: 'John', lastName: 'Smith'}
 *     });
 *
 *     viewModel.bind('{fullName}', function(v) {
 *         console.log(v);
 *     });
 *
 * See {@link #formulas} for more detail.
 *
 * #### Inheriting Data With Nesting
 *
 * ViewModels can have a {@link #parent} which allows values to be consumed from
 * a shared base. These values that are available from the {@link #parent} are not copied,
 * rather they are "inherited" in a similar fashion to a javascript closure scope chain. 
 * This is demonstrated in the example below:
 *
 *     var parent = new Ext.app.ViewModel({
 *         data: {
 *             foo: 3
 *         }
 *     });
 *     var child = new Ext.app.ViewModel({
 *         parent: parent
 *     });
 *
 * This is analogous to the following javascript closure:
 *
 *     var foo = 3;
 *     Ext.Ajax.request({
 *         success: function() {
 *             // foo is available here
 *         }
 *     });
 *
 * ### Climbing/Inheriting
 *
 * In line with the above, the default behaviour when setting the value of a child ViewModel
 * (either) through {@link #set} or {@link Ext.app.bind.Binding#method-setValue} is to climb to
 * where the value  is "owned" and set the value there:
 *
 *     var parent = new Ext.app.ViewModel({
 *         data: {
 *             foo: 3
 *         }
 *     });
 *     var child = new Ext.app.ViewModel({
 *         parent: parent
 *     });
 *     
 *     child.set('foo', 100); // Climbs to set the value on parent
 *     console.log(parent.get('foo')); // 100
 *     parent.set('foo', 200);
 *     console.log(child.get('foo')); // 200, inherited from the parent
 *
 * Any subsequent sets are also inherited in the same fashion. The inheriting/climbing behavior 
 * occurs for any arbitrary depth, climbing/inherting can owned by a parent at any level above.
 *
 *     function log() {
 *         console.log([a, b, c, d, e].map(function(vm) {
 *             return vm.get('foo');
 *         }));
 *     }
 *
 *     var a = new Ext.app.ViewModel({data: {foo: 3}}),
 *         b = new Ext.app.ViewModel({parent: a}),
 *         c = new Ext.app.ViewModel({parent: b}),
 *         d = new Ext.app.ViewModel({parent: c}),
 *         e = new Ext.app.ViewModel({parent: d});
 *
 *     log(); // [3, 3, 3, 3, 3]
 *
 *     e.set('foo', 100);
 *     log(); // [100, 100, 100, 100, 100]
 *
 * This same climbing behavior applies when setting a value on a binding. The climbing begins from
 * the ViewModel where the binding was attached:
 *
 *     function log() {
 *         console.log([a, b, c].map(function(vm) {
 *             return vm.get('foo');
 *         }));
 *     }
 *
 *     var a = new Ext.app.ViewModel({data: {foo: 3}}),
 *         b = new Ext.app.ViewModel({parent: a}),
 *         c = new Ext.app.ViewModel({parent: b});
 *
 *     var bind = c.bind('{foo}', function() {});
 *
 *     bind.setValue(100);
 *     log(); // [100, 100, 100]
 *
 * The exception to this rule is when there is nothing above to climb to. If a value is set and
 * there is no parent above to hold it, then the value is set where it was called:
 *
 *     function log() {
 *         console.log([a, b, c].map(function(vm) {
 *             return vm.get('foo');
 *         }));
 *     }
 *
 *     var a = new Ext.app.ViewModel(),
 *         b = new Ext.app.ViewModel({parent: a}),
 *         c = new Ext.app.ViewModel({parent: b});
 *
 *     c.set('foo', 3);
 *     log(); // [null, null, 3]
 *
 *     b.set('foo', 2);
 *     log(); // [null, 2, 3]
 *
 *     a.set('foo', 1);
 *     log(); // [1, 2, 3]
 *
 * These values are called local values, which are discussed below.
 *
 * ### Local Values
 *
 * If the child ViewModel is declared with top level data that also exists in the parent, then that
 * child is considered to own that local value, so no value is inherited from the parent, nor does
 * the climbing behaviour occur.
 *
 *     var parent = new Ext.app.ViewModel({
 *         data: {
 *             foo: 3
 *         }
 *     });
 *     var child = new Ext.app.ViewModel({
 *         parent: parent,
 *         data: {
 *             foo: 5
 *         }
 *     });
 *
 *     console.log(parent.get('foo'), child.get('foo')); // 3, 5
 *     child.set('foo', 100);
 *     console.log(parent.get('foo'), child.get('foo')); // 3, 100
 *     parent.set('foo', 200);
 *     console.log(parent.get('foo'), child.get('foo')); // 200, 100
 *
 * The inheriting/climbing behavior is limited to local values:
 *
 *     function log() {
 *         console.log([a, b, c, d, e].map(function(vm) {
 *             return vm.get('foo');
 *         }));
 *     }
 *
 *     var a = new Ext.app.ViewModel({data: {foo: 1}}),
 *         b = new Ext.app.ViewModel({parent: a}),
 *         c = new Ext.app.ViewModel({parent: b, data: {foo: 2}}),
 *         d = new Ext.app.ViewModel({parent: c}),
 *         e = new Ext.app.ViewModel({parent: d, data: {foo: 3}});
 *
 *     log(); // [1, 1, 2, 2, 3]
 *
 *     e.set('foo', 100);
 *     log(); // [1, 1, 2, 2, 100]
 *
 *     d.set('foo', 200);
 *     log(); // [1, 1, 200, 200, 100]
 *
 *     c.set('foo', 201);
 *     log(); // [1, 1, 201, 201, 100]
 *
 *     b.set('foo', 300);
 *     log(); // [300, 300, 201, 201, 100]
 *
 *     a.set('foo', 301);
 *     log(); // [301, 301, 201, 201, 100]
 *
 * ### Attaching/Clearing Local Values Dynamically
 *
 * To bypass the climbing behaviour and push a value into a particular point
 * in the hierarchy, the {@link #setData} method should be used. Once a local value
 * is set, it will be used as such in the future.
 *
 *     function log() {
 *         console.log([a, b, c, d, e].map(function(vm) {
 *             return vm.get('foo');
 *         }));
 *     }
 *
 *     var a = new Ext.app.ViewModel({data: {foo: 3}}),
 *         b = new Ext.app.ViewModel({parent: a}),
 *         c = new Ext.app.ViewModel({parent: b}),
 *         d = new Ext.app.ViewModel({parent: c}),
 *         e = new Ext.app.ViewModel({parent: d});
 *
 *     log(); // [3, 3, 3, 3, 3]
 *
 *     c.setData({
 *         foo: 100
 *     });
 *
 *     log(); // [3, 3, 100, 100, 100]
 *
 *     d.set('foo', 200); // Climbs to new local value
 *     log(); // [3, 3, 200, 200, 200]
 *
 * Similarly, data can be cleared from being a local value by setting the value to undefined:
 *
 *     function log() {
 *         console.log([a, b, c, d].map(function(vm) {
 *             return vm.get('foo');
 *         }));
 *     }
 *
 *     var a = new Ext.app.ViewModel({data: {foo: 3}}),
 *         b = new Ext.app.ViewModel({parent: a}),
 *         c = new Ext.app.ViewModel({parent: b, data: {foo: 100}}),
 *         d = new Ext.app.ViewModel({parent: c});
 *
 *     log(); // [3, 3, 100, 100]
 *
 *     c.setData({
 *         foo: undefined
 *     });
 *     log([3, 3, 3, 3]);
 *
 */
Ext.define('Ext.app.ViewModel', {
    mixins: [
        'Ext.mixin.Factoryable',
        'Ext.mixin.Identifiable'
    ],
    requires: [
        'Ext.util.Scheduler',
        'Ext.data.Session',
        'Ext.app.bind.RootStub',
        'Ext.app.bind.LinkStub',
        'Ext.app.bind.Multi',
        'Ext.app.bind.Formula',
        'Ext.app.bind.TemplateBinding',
        // TODO: this is an injected dependency in onStoreBind, need to define so 
        // cmd can detect it
        'Ext.data.ChainedStore'
    ],
    alias: 'viewmodel.default',
    // also configures Factoryable
    isViewModel: true,
    factoryConfig: {
        name: 'viewModel'
    },
    collectTimeout: 100,
    expressionRe: /^(?:\{(?:(\d+)|([a-z_][\w.]*))\})$/i,
    statics: {
        /**
         * Escape bind strings so they are treated as literals.
         * 
         * @param {Object/String} value The value to escape. If the value is
         * an object, any strings will be recursively escaped.
         * @return {Object/String} The escaped value. Matches the type of the
         * passed value.
         *
         * @since 6.5.2
         * @private
         */
        escape: function(value) {
            var ret = value,
                key;
            if (typeof value === 'string') {
                ret = '~~' + value;
            } else if (value && value.constructor === Object) {
                ret = {};
                for (key in value) {
                    ret[key] = this.escape(value[key]);
                }
            }
            return ret;
        }
    },
    $configStrict: false,
    // allow "formulas" to be specified on derived class body
    config: {
        /**
         * @cfg {Object} data
         * This object holds the arbitrary data that populates the `ViewModel` and is
         * then available for binding.
         * @since 5.0.0
         */
        data: true,
        /**
         * @cfg {Object} formulas
         * An object that defines named values whose value is managed by function calls.
         * The names of the properties of this object are assigned as values in the
         * ViewModel.
         *
         * For example:
         *
         *      formulas: {
         *          xy: function (get) { return get('x') * get('y'); }
         *      }
         *
         * For more details about defining a formula, see `{@link Ext.app.bind.Formula}`.
         * @since 5.0.0
         */
        formulas: {
            $value: null,
            merge: function(newValue, currentValue, target, mixinClass) {
                return this.mergeNew(newValue, currentValue, target, mixinClass);
            }
        },
        /**
         * @cfg {Object} links
         * Links provide a way to assign a simple name to a more complex bind. The primary
         * use for this is to assign names to records in the data model.
         *
         *      links: {
         *          theUser: {
         *              type: 'User',
         *              id: 12
         *          }
         *      }
         *
         * It is also possible to force a new phantom record to be created by not specifying an
         * id but passing `create: true` as part of the descriptor. This is often useful when
         * creating a new record for a child session.
         *
         *     links: {
         *         newUser: {
         *             type: 'User',
         *             create: true
         *         }
         *     } 
         *
         * `create` can also be an object containing initial data for the record.
         *
         *     links: {
         *         newUser: {
         *             type: 'User',
         *             create: {
         *                 firstName: 'John',
         *                 lastName: 'Smith'
         *             }
         *         }
         *     } 
         *
         * While that is the typical use, the value of each property in `links` may also be
         * a bind descriptor (see `{@link #method-bind}` for the various forms of bind
         * descriptors).
         * @since 5.0.0
         */
        links: null,
        /**
         * @cfg {Ext.app.ViewModel} parent
         * The parent `ViewModel` of this `ViewModel`. Once set, this cannot be changed.
         * @readonly
         * @since 5.0.0
         */
        parent: null,
        /**
         * @cfg {Ext.app.bind.RootStub} root
         * A reference to the root "stub" (an object that manages bindings).
         * @private
         * @since 5.0.0
         */
        root: true,
        /**
         * @cfg {Ext.util.Scheduler} scheduler
         * The scheduler used to schedule and manage the delivery of notifications for
         * all connections to this `ViewModel` and any other attached to it. The normal
         * process to initialize the `scheduler` is to get the scheduler used by the
         * `parent` or `session` and failing either of those, create one.
         * @readonly
         * @private
         * @since 5.0.0
         */
        scheduler: null,
        /**
         * @cfg {String/Ext.data.schema.Schema} schema
         * The schema to use for getting information about entities.
         */
        schema: 'default',
        /**
         * @cfg {Ext.data.Session} session
         * The session used to manage the data model (records and stores).
         * @since 5.0.0
         */
        session: null,
        // @cmd-auto-dependency {isKeyedObject: true, aliasPrefix: "store.", defaultType: "store"}
        /**
         * @cfg {Object} stores
         * A declaration of `Ext.data.Store` configurations that are first processed as
         * binds to produce an effective store configuration.
         *
         * A simple store definition. We can reference this in our bind statements using the
         * `{users}` as we would with other data values.
         *
         *     new Ext.app.ViewModel({
         *         stores: {
         *             users: {
         *                 model: 'User',
         *                 autoLoad: true
         *             }
         *         }
         *     });
         *
         * This store definition contains a dynamic binding. The store will not be created until
         * the initial value for groupId is set. Once that occurs, the store is created with the
         * appropriate filter configuration. Subsequently, once we change the group value, the old
         * filter will be overwritten with the new value.
         *
         *     var viewModel = new Ext.app.ViewModel({
         *         stores: {
         *             users: {
         *                 model: 'User',
         *                 filters: [{
         *                     property: 'groupId',
         *                     value: '{groupId}'
         *                 }]
         *             }
         *         }
         *     });
         *     viewModel.set('groupId', 1); // This will trigger the store creation with the filter.
         *     viewModel.set('groupId', 2); // The filter value will be changed.
         *
         * This store uses {@link Ext.data.ChainedStore store chaining} to create a store backed by
         * the data in another store. By specifying a string as the store, it will bind our creation
         * and backing to the other store. This functionality is especially useful when wanting to
         * display a different "view" of a store, for example a different sort order or different
         * filters.
         *
         *     var viewModel = new Ext.app.ViewModel({
         *         stores: {
         *             allUsers: {
         *                 model: 'User',
         *                 autoLoad: true
         *             },
         *             children: {
         *                 source: '{allUsers}',
         *                 filters: [{
         *                     property: 'age',
         *                     value: 18,
         *                     operator: '<'
         *                 }]
         *             }
         *         }
         *     });
         *
         * @since 5.0.0
         */
        stores: null,
        /**
         * @cfg {Ext.container.Container} view
         * The Container that owns this `ViewModel` instance.
         * @since 5.0.0
         */
        view: null
    },
    constructor: function(config) {
        // Used to track non-stub bindings
        this.bindings = {};
        /*
         *  me.data = {
         *      foo: {
         *      },
         *          
         *      selectedUser: {
         *          name: null
         *      },
         *  }
         *
         *  me.root = new Ext.app.bind.RootStub({
         *      children: {
         *          foo: new Ext.app.bind.Stub(),
         *          selectedUser: new Ext.app.bind.LinkStub({
         *              binding: session.bind(...),
         *              children: {
         *                  name: : new Ext.app.bind.Stub()
         *              }
         *          }),
         *      }
         *  })
         */
        this.initConfig(config);
    },
    destroy: function() {
        var me = this,
            scheduler = me._scheduler,
            stores = me.storeInfo,
            parent = me.getParent(),
            task = me.collectTask,
            children = me.children,
            bindings = me.bindings,
            key, store, autoDestroy, storeBinding;
        me.destroying = true;
        if (task) {
            task.cancel();
            me.collectTask = null;
        }
        // When used with components, they are destroyed bottom up
        // so this scenario is only likely to happen in the case where
        // we're using the VM without any component attachment, in which case
        // we need to clean up here.
        if (children) {
            for (key in children) {
                children[key].destroy();
            }
        }
        if (stores) {
            for (key in stores) {
                store = stores[key];
                // Cache this property in case store is destroyed;
                // Properties are cleared on destroy
                storeBinding = store.$binding;
                autoDestroy = store.autoDestroy;
                if (autoDestroy || (!store.$wasInstance && autoDestroy !== false)) {
                    store.destroy();
                }
                Ext.destroy(storeBinding);
            }
        }
        if (parent) {
            parent.unregisterChild(me);
        }
        me.getRoot().destroy();
        for (key in bindings) {
            bindings[key].destroy();
        }
        if (scheduler && scheduler.$owner === me) {
            scheduler.$owner = null;
            scheduler.destroy();
        }
        me.children = me.storeInfo = me._session = me._view = me._scheduler = me.bindings = me._root = me._parent = me.formulaFn = me.$formulaData = null;
        // This just makes it hard to ask "was destroy() called?":
        // me.destroying = false; // removed in 7.0
        me.callParent();
    },
    /**
     * This method requests that data in this `ViewModel` be delivered to the specified
     * `callback`. The data desired is given in a "bind descriptor" which is the first
     * argument.
     *
     * A simple call might look like this:
     *
     *     var binding = vm.bind('{foo}', this.onFoo, this);
     * 
     *     binding.destroy();  // when done with the binding
     *
     * Options for the binding can be provided in the last argument:
     *
     *     var binding = vm.bind('{foo}', this.onFoo, this, {
     *         deep: true
     *     });
     * 
     * Alternatively, bind options can be combined with the bind descriptor using only
     * the first argument:
     *
     *     var binding = vm.bind({
     *         bindTo: '{foo}',  // the presence of bindTo identifies this form
     *         deep: true
     *     }, this.onFoo, this);
     * 
     * See the class documentation for more details on Bind Descriptors and options.
     *
     * @param {String/Object/Array} descriptor The bind descriptor. See class description
     * for details.
     * @param {Function} callback The function to call with the value of the bound property.
     * @param {Object} [scope] The scope (`this` pointer) for the `callback`.
     * @param {Object} [options] Additional options to configure the
     * {@link Ext.app.bind.Binding binding}. If this parameter is provided, the `bindTo` form
     * of combining options and bind descriptor is not recognized.
     * @return {Ext.app.bind.BaseBinding/Ext.app.bind.Binding} The binding.
     */
    bind: function(descriptor, callback, scope, options) {
        var me = this,
            track = true,
            binding;
        scope = scope || me;
        if (!options && descriptor.bindTo !== undefined && !Ext.isString(descriptor)) {
            options = descriptor;
            descriptor = options.bindTo;
        }
        if (!Ext.isString(descriptor)) {
            binding = new Ext.app.bind.Multi(descriptor, me, callback, scope, options);
        } else if (me.expressionRe.test(descriptor)) {
            // If we have '{foo}' alone it is a literal
            descriptor = descriptor.substring(1, descriptor.length - 1);
            binding = me.bindExpression(descriptor, callback, scope, options);
            track = false;
        } else {
            binding = new Ext.app.bind.TemplateBinding(descriptor, me, callback, scope, options);
        }
        if (track) {
            me.bindings[binding.id] = binding;
        }
        return binding;
    },
    /**
     * Gets the session attached to this (or a parent) ViewModel. See the {@link #session}
     * configuration.
     * @return {Ext.data.Session} The session. `null` if no session exists.
     */
    getSession: function() {
        var me = this,
            session = me._session,
            parent;
        if (!session && (parent = me.getParent())) {
            me.setSession(session = parent.getSession());
        }
        return session || null;
    },
    /**
     * Gets a store configured via the {@link #stores} configuration.
     * @param {String} key The name of the store.
     * @return {Ext.data.Store} The store. `null` if no store exists.
     */
    getStore: function(key) {
        var storeInfo = this.storeInfo,
            store;
        if (storeInfo) {
            store = storeInfo[key];
        }
        return store || null;
    },
    /**
     * @method getStores
     * @hide
     */
    /**
     * Create a link to a reference. See the {@link #links} configuration.
     * @param {String} key The name for the link.
     * @param {Object} reference The reference descriptor.
     */
    linkTo: function(key, reference) {
        var me = this,
            stub, create, id, modelType, linkStub, rec;
        if (key.indexOf('.') > -1) {
            Ext.raise('Links can only be at the top-level: "' + key + '"');
        }
        if (reference.isModel) {
            reference = {
                type: reference.entityName,
                id: reference.id
            };
        }
        // reference is backwards compat, type is preferred.
        modelType = reference.type || reference.reference;
        create = reference.create;
        if (modelType) {
            // It's a record
            id = reference.id;
            if (!reference.create && Ext.isEmpty(id)) {
                Ext.raise('No id specified. To create a phantom model, specify "create: true" ' + 'as part of the reference.');
            }
            if (create) {
                id = undefined;
            }
            rec = me.getRecord(modelType, id);
            if (Ext.isObject(create)) {
                rec.set(create);
                rec.commit();
                rec.phantom = true;
            }
            // Force creation at the root level. If an existing stub is there
            // it will be grafted in place here.
            stub = me.getRoot().createStubChild(key);
            stub.set(rec);
        } else {
            stub = me.getStub(key);
            if (!stub.isLinkStub) {
                // Pass parent=null since we will graft in this new stub to replace us:
                linkStub = new Ext.app.bind.LinkStub(me, stub.name);
                stub.graft(linkStub);
                stub = linkStub;
            }
            stub.link(reference);
        }
    },
    /**
     * Forces all bindings in this ViewModel hierarchy to evaluate immediately. Use this to do
     * a synchronous flush of all bindings.
     */
    notify: function() {
        var scheduler = this.getScheduler();
        if (!scheduler.firing) {
            scheduler.notify();
        }
    },
    /**
     * Get a value from the data for this viewmodel.
     * @param {String} path The path of the data to retrieve.
     *
     *     var value = vm.get('theUser.address.city');
     *
     * @return {Object} The data stored at the passed path.
     */
    get: function(path) {
        return this.getStub(path).getValue();
    },
    /**
     * Set a value in the data for this viewmodel. This method will climb to set data on
     * a parent view model if appropriate. See "Inheriting Data" in the class introduction for
     * more information.
     * 
     * @param {Object/String} path The path of the value to set, or an object literal to set
     * at the root of the viewmodel.
     * @param {Object} value The data to set at the value. If the value is an object literal,
     * any required paths will be created.
     *
     *     // Set a single property at the root level
     *     viewModel.set('expiry', Ext.Date.add(new Date(), Ext.Date.DAY, 7));
     *     console.log(viewModel.get('expiry'));
     *     // Sets a single property in user.address, does not overwrite any hierarchy.
     *     viewModel.set('user.address.city', 'London');
     *     console.log(viewModel.get('user.address.city'));
     *     // Sets 2 properties of "user". Overwrites any existing hierarchy.
     *     viewModel.set('user', {firstName: 'Foo', lastName: 'Bar'});
     *     console.log(viewModel.get('user.firstName'));
     *     // Sets a single property at the root level. Overwrites any existing hierarchy.
     *     viewModel.set({rootKey: 1});
     *     console.log(viewModel.get('rootKey'));
     */
    set: function(path, value) {
        var me = this,
            obj, stub;
        // Force data creation
        me.getData();
        if (value === undefined && path && path.constructor === Object) {
            stub = me.getRoot();
            value = path;
        } else if (path && path.indexOf('.') < 0) {
            obj = {};
            obj[path] = value;
            value = obj;
            stub = me.getRoot();
        } else {
            stub = me.getStub(path);
        }
        stub.set(value);
    },
    /**
     * Sets data directly at the level of this viewmodel. This method does not climb
     * to set data on parent view models. Passing `undefined` will clear the value
     * in this viewmodel, which means that this viewmodel is free to inherit data
     * from a parent. See "Inheriting Data" in the class introduction for more information.
     * @param {Object} data The new data to set.
     * @method setData
     */
    //=========================================================================
    privates: {
        registerChild: function(child) {
            var children = this.children;
            if (!children) {
                this.children = children = {};
            }
            children[child.getId()] = child;
        },
        unregisterChild: function(child) {
            var children = this.children;
            // If we're destroying we'll be wiping this collection shortly, so
            // just ignore it here
            if (!this.destroying && children) {
                delete children[child.getId()];
            }
        },
        /**
         * Get a record instance given a reference descriptor. Will ask
         * the session if one exists.
         * @param {String/Ext.Class} type The model type.
         * @param {Object} id The model id.
         * @return {Ext.data.Model} The model instance.
         * @private
         */
        getRecord: function(type, id) {
            var session = this.getSession(),
                Model = type,
                hasId = id !== undefined,
                record;
            if (session) {
                if (hasId) {
                    record = session.getRecord(type, id);
                } else {
                    record = session.createRecord(type);
                }
            } else {
                if (!Model.$isClass) {
                    Model = this.getSchema().getEntity(Model);
                    if (!Model) {
                        Ext.raise('Invalid model name: ' + type);
                    }
                }
                if (hasId) {
                    record = Model.createWithId(id);
                    record.load();
                } else {
                    record = new Model();
                }
            }
            return record;
        },
        bindExpression: function(descriptor, callback, scope, options) {
            var stub = this.getStub(descriptor);
            return stub.bind(callback, scope, options);
        },
        applyScheduler: function(scheduler) {
            if (scheduler && !scheduler.isInstance) {
                if (scheduler === true) {
                    scheduler = {};
                }
                if (!('preSort' in scheduler)) {
                    scheduler = Ext.apply({
                        preSort: 'kind,-depth'
                    }, scheduler);
                }
                scheduler = new Ext.util.Scheduler(scheduler);
                scheduler.$owner = this;
            }
            return scheduler;
        },
        getScheduler: function() {
            var me = this,
                scheduler = me._scheduler,
                parent;
            if (!scheduler) {
                if (!(parent = me.getParent())) {
                    scheduler = new Ext.util.Scheduler({
                        // See Session#scheduler
                        preSort: 'kind,-depth'
                    });
                    scheduler.$owner = me;
                } else {
                    scheduler = parent.getScheduler();
                }
                me.setScheduler(scheduler);
            }
            return scheduler;
        },
        /**
         * This method looks up the `Stub` for a single bind descriptor.
         * @param {String/Object} bindDescr The bind descriptor.
         * @return {Ext.app.bind.AbstractStub} The `Stub` associated to the bind descriptor.
         * @private
         */
        getStub: function(bindDescr) {
            var root = this.getRoot();
            return bindDescr ? root.getChild(bindDescr) : root;
        },
        collect: function() {
            var me = this,
                parent = me.getParent(),
                task = me.collectTask;
            if (parent) {
                parent.collect();
                return;
            }
            if (!task) {
                task = me.collectTask = new Ext.util.DelayedTask(me.doCollect, me);
            }
            // Useful for testing
            if (me.collectTimeout === 0) {
                me.doCollect();
            } else {
                task.delay(me.collectTimeout);
            }
        },
        doCollect: function() {
            var children = this.children,
                key;
            // We need to loop over the children first, since they may have link stubs
            // that create bindings inside our VM. Attempt to clean them up first.
            if (children) {
                for (key in children) {
                    children[key].doCollect();
                }
            }
            this.getRoot().collect();
        },
        invalidateChildLinks: function(name, clear) {
            var children = this.children,
                key;
            if (children) {
                for (key in children) {
                    children[key].getRoot().invalidateChildLink(name, clear);
                }
            }
        },
        onBindDestroy: function(binding, fromChild) {
            var me = this,
                parent;
            if (me.destroying) {
                return;
            }
            if (!fromChild) {
                delete me.bindings[binding.id];
            }
            parent = me.getParent();
            if (parent) {
                parent.onBindDestroy(binding, true);
            } else {
                me.collect();
            }
        },
        //-------------------------------------------------------------------------
        // Config
        // <editor-fold>
        applyData: function(newData, data) {
            var me = this,
                linkData, parent;
            // Force any session to be invoked so we can access it
            me.getSession();
            if (!data) {
                parent = me.getParent();
                /**
                 * @property {Object} linkData
                 * This object is used to hold the result of a linked value. This is done
                 * so that the data object hasOwnProperty equates to whether or not this
                 * property is owned by this instance or inherited.
                 * @private
                 * @readonly
                 * @since 5.0.0
                 */
                me.linkData = linkData = parent ? Ext.Object.chain(parent.getData()) : {};
                /**
                 * @property {Object} data
                 * This object holds all of the properties of this `ViewModel`. It is
                 * prototype chained to the `linkData` which is, in turn, prototype chained
                 * to (if present) the `data` object of the parent `ViewModel`.
                 * @private
                 * @readonly
                 * @since 5.0.0
                 */
                me.data = me._data = Ext.Object.chain(linkData);
            }
            if (newData && newData.constructor === Object) {
                me.getRoot().set(newData, true);
            }
        },
        applyParent: function(parent) {
            if (parent) {
                parent.registerChild(this);
            }
            return parent;
        },
        applyStores: function(stores) {
            var me = this,
                root = me.getRoot(),
                key, cfg, storeBind, stub, listeners;
            me.storeInfo = {};
            me.listenerScopeFn = function() {
                return me.getView().getInheritedConfig('defaultListenerScope');
            };
            for (key in stores) {
                cfg = stores[key];
                if (cfg.isStore) {
                    cfg.$wasInstance = true;
                    me.setupStore(cfg, key);
                    
                    continue;
                } else if (Ext.isString(cfg)) {
                    cfg = {
                        source: cfg
                    };
                } else {
                    cfg = Ext.apply({}, cfg);
                }
                // Get rid of listeners so they don't get considered as a bind
                listeners = cfg.listeners;
                delete cfg.listeners;
                storeBind = me.bind(cfg, me.onStoreBind, me, {
                    trackStatics: true
                });
                if (storeBind.isStatic()) {
                    // Everything is static, we don't need to wait, so remove the
                    // binding because it will only fire the first time.
                    storeBind.destroy();
                    me.createStore(key, cfg, listeners);
                } else {
                    storeBind.$storeKey = key;
                    storeBind.$listeners = listeners;
                    stub = root.createStubChild(key);
                    stub.setStore(storeBind);
                }
            }
        },
        onStoreBind: function(cfg, oldValue, binding) {
            var info = this.storeInfo,
                key = binding.$storeKey,
                store = info[key],
                proxy;
            if (!store) {
                this.createStore(key, cfg, binding.$listeners, binding);
            } else {
                cfg = Ext.merge({}, binding.pruneStaticKeys());
                proxy = cfg.proxy;
                delete cfg.type;
                delete cfg.model;
                delete cfg.fields;
                delete cfg.proxy;
                delete cfg.listeners;
                // TODO: possibly optimize this so we can figure out what has changed
                // instead of smashing the whole lot
                if (proxy) {
                    delete proxy.reader;
                    delete proxy.writer;
                    store.getProxy().setConfig(proxy);
                }
                store.setConfig(cfg);
            }
        },
        createStore: function(key, cfg, listeners, binding) {
            var session = this.getSession(),
                store;
            cfg = Ext.apply({}, cfg);
            if (cfg.session) {
                cfg.session = session;
            }
            if (cfg.source) {
                cfg.type = cfg.type || 'chained';
            }
            // Restore the listeners from applyStores here
            cfg.listeners = listeners;
            // Ensure events fired by ctor can find their target:
            cfg.resolveListenerScope = this.listenerScopeFn;
            store = Ext.Factory.store(cfg);
            store.$binding = binding;
            this.setupStore(store, key);
        },
        setupStore: function(store, key) {
            var me = this,
                obj = {};
            // Force data object creation
            me.getData();
            // May have been given a store instance
            store.resolveListenerScope = me.listenerScopeFn;
            me.storeInfo[key] = store;
            obj[key] = store;
            me.setData(obj);
        },
        applyFormulas: function(formulas) {
            var me = this,
                root = me.getRoot(),
                name, stub;
            me.getData();
            // make sure our data is setup first
            for (name in formulas) {
                if (name.indexOf('.') >= 0) {
                    Ext.raise('Formula names cannot contain dots: ' + name);
                }
                // Force a stub to be created
                root.createStubChild(name);
                stub = me.getStub(name);
                stub.setFormula(formulas[name]);
            }
            return formulas;
        },
        applyLinks: function(links) {
            var link;
            for (link in links) {
                this.linkTo(link, links[link]);
            }
        },
        applySchema: function(schema) {
            return Ext.data.schema.Schema.get(schema);
        },
        applyRoot: function() {
            var root = new Ext.app.bind.RootStub(this),
                parent = this.getParent();
            if (parent) {
                // We are assigning the root of a child VM such that its bindings will be
                // pre-sorted after the bindings of the parent VM.
                root.depth = parent.getRoot().depth - 1000;
            }
            return root;
        },
        getFormulaFn: function(data) {
            var me = this,
                fn = me.formulaFn;
            if (!fn) {
                fn = me.formulaFn = function(name) {
                    // Note that the `this` pointer here is the view model because
                    // the VM calls it in the VM scope.
                    return me.$formulaData[name];
                };
            }
            me.$formulaData = data;
            return fn;
        }
    }
});
// </editor-fold>

/**
 * This class implements the controller event domain. All classes extending from
 * {@link Ext.app.Controller} are included in this domain. The selectors are simply id, 
 * alias, or the wildcard "*" to match any controller.
 * 
 * @private
 */
Ext.define('Ext.app.domain.Controller', {
    extend: 'Ext.app.EventDomain',
    singleton: true,
    requires: [
        'Ext.app.Controller'
    ],
    type: 'controller',
    prefix: 'controller.',
    idMatchRe: /^#/,
    constructor: function() {
        var me = this;
        me.callParent();
        me.monitor(Ext.app.BaseController);
    },
    match: function(target, selector) {
        var result = false,
            alias = target.alias;
        if (selector === '*') {
            result = true;
        } else if (selector === '#') {
            result = !!target.isApplication;
        } else if (this.idMatchRe.test(selector)) {
            result = target.getId() === selector.substring(1);
        } else if (alias) {
            result = Ext.Array.indexOf(alias, this.prefix + selector) > -1;
        }
        return result;
    }
});

/**
 * Ext Direct aims to streamline communication between the client and server
 * by providing a single interface that reduces the amount of common code
 * typically required to validate data and handle returned data packets
 * (reading data, error conditions, etc).
 *
 * The `Ext.direct` namespace includes several classes for a closer integration
 * with the server-side. The Ext.data namespace also includes classes for working
 * with Ext.data.Stores which are backed by data from an Ext Direct method.
 *
 * # Specification
 *
 * For additional information consult the [Ext Direct Specification][1].
 *
 * # Providers
 *
 * Ext Direct uses a provider architecture, where one or more providers are used
 * to transport data to and from the server. There are several providers that exist
 * in the core at the moment:
 *
 * - {@link Ext.direct.JsonProvider JsonProvider} for simple JSON operations
 * - {@link Ext.direct.PollingProvider PollingProvider} for repeated requests
 * - {@link Ext.direct.RemotingProvider RemotingProvider} exposes server side to the client.
 *
 * A provider does not need to be invoked directly, providers are added via
 * {@link Ext.direct.Manager #addProvider}. RemotingProviders' API declarations
 * can also be loaded with {@link Ext.direct.Manager #loadProvider}, with
 * Provider instance created automatically after successful retrieval.
 *
 * # Router
 *
 * Ext Direct RemotingProviders utilize a "router" on the server to direct
 * requests from the client to the appropriate server-side method. Because
 * the Ext Direct API is platform-agnostic, you could completely swap out
 * a Java based server solution and replace it with one that uses C#
 * without changing the client side JavaScript at all, or vice versa.
 *
 * # Server side events
 *
 * Custom events from the server may be handled by the client by adding listeners, for example:
 *
 *     {"type":"event","name":"message","data":"Successfully polled at: 11:19:30 am"}
 *
 *     // add a handler for a 'message' event sent by the server
 *     Ext.direct.Manager.on('message', function(e){
 *         out.append(String.format('<p><i>{0}</i></p>', e.data));
 *         out.el.scrollTo('t', 100000, true);
 *     });
 *
 *    [1]: http://sencha.com/products/extjs/extdirect
 *
 * @singleton
 * @alternateClassName Ext.Direct
 */
Ext.define('Ext.direct.Manager', {
    singleton: true,
    requires: [
        'Ext.util.MixedCollection'
    ],
    mixins: [
        'Ext.mixin.Observable'
    ],
    /**
     * Exception types.
     */
    exceptions: {
        TRANSPORT: 'xhr',
        PARSE: 'parse',
        DATA: 'data',
        LOGIN: 'login',
        SERVER: 'exception'
    },
    // Classes of Providers available to the application
    providerClasses: {},
    // Remoting Methods registered with the Manager
    remotingMethods: {},
    config: {
        /**
         * @cfg {String} [varName="Ext.REMOTING_API"]
         * Default variable name to use for Ext Direct API declaration.
         */
        varName: 'Ext.REMOTING_API'
    },
    apiNotFoundError: 'Ext Direct API was not found at {0}',
    /**
     * @event event
     *
     * Fires after an event.
     *
     * @param {Ext.direct.Event} event The {@link Ext.direct.Event Event} that occurred.
     * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}
     * that provided the event.
     */
    /**
     * @event exception
     *
     * Fires after an event exception.
     *
     * @param {Ext.direct.Event} event The {@link Ext.direct.Event Event} that occurred.
     * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}
     * that provided the event.
     */
    /**
     * @event providerload
     *
     * Fired by {@link #loadProvider} after successfully loading RemotingProvider API
     * declaration and creating a new Provider instance.
     *
     * @param {String} url The URL used to retrieve remoting API.
     * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}
     * instance that was created.
     */
    /**
     * @event providerloaderror
     * 
     * Fired by {@link #loadProvider} when remoting API could not be loaded, or
     * Provider instance could not be created.
     *
     * @param {String} url The URL used to retrieve remoting API.
     * @param {String} error The error that occured.
     */
    /**
     * @private
     */
    constructor: function() {
        var me = this;
        me.mixins.observable.constructor.call(me);
        me.transactions = new Ext.util.MixedCollection();
        me.providers = new Ext.util.MixedCollection();
    },
    /**
     * Adds an Ext Direct Provider and creates the proxy or stub methods to execute
     * server-side methods for RemotingProviders. If the provider is not already connected,
     * it will auto-connect.
     *
     *      var pollProv = new Ext.direct.PollingProvider({
     *          id: 'polling1',
     *          url: 'php/poll2.php'
     *      });
     *
     *      Ext.direct.Manager.addProvider({
     *          id: 'remoting1',
     *          type: 'remoting',           // create a Ext.direct.RemotingProvider
     *          url:  'php/router.php',     // url to connect to the Ext Direct server-side router.
     *          actions: {                  // each property in actions object represents an Action
     *              TestAction: [{          // array of Methods within each server side Action
     *                  name: 'doEcho',     // name of method
     *                  len:  1
     *              }, {
     *                  name: 'multiply',
     *                  len:  1
     *              }, {
     *                  name: 'doForm',
     *                  formHandler: true   // handle form on server with Ext.direct.Transaction
     *              }]
     *          },
     *          namespace: 'myApplication', // namespace to create the Remoting Provider in
     *      }, {
     *          id:   'polling2',
     *          type: 'polling',            // create an Ext.direct.PollingProvider
     *          url:  'php/poll.php'
     *      },
     *      pollProv);                      // reference to previously created instance
     *
     * @param {Ext.direct.Provider/Object...} provider
     *
     * Accepts any number of Provider descriptions (an instance or config object for
     * a Provider). Each Provider description instructs Ext Direct how to create
     * client-side stub methods.
     */
    addProvider: function(provider) {
        var me = this,
            args = arguments,
            relayers = me.relayers || (me.relayers = {}),
            i, len;
        if (args.length > 1) {
            for (i = 0 , len = args.length; i < len; ++i) {
                me.addProvider(args[i]);
            }
            return;
        }
        // if provider has not already been instantiated
        if (!provider.isProvider) {
            provider = Ext.create('direct.' + provider.type + 'provider', provider);
        }
        me.providers.add(provider);
        provider.on('data', me.onProviderData, me);
        if (provider.relayedEvents) {
            relayers[provider.id] = me.relayEvents(provider, provider.relayedEvents);
        }
        if (!provider.isConnected()) {
            provider.connect();
        }
        return provider;
    },
    /**
     * Load Ext Direct Provider API declaration from the server and construct
     * a new Provider instance. The new Provider will then auto-connect and
     * create stub functions for the methods exposed by the server side. See 
     * {@link #addProvider}.
     * 
     *      Ext.direct.Manager.loadProvider({
     *          url: 'php/api.php',
     *          varName: 'MY_REMOTING_API' // defaults to 'Ext.REMOTING_API'
     *      });
     *
     * @param {Object} config Remoting API configuration.
     * @param {String} config.url URL to retrieve remoting API declaration from.
     * @param {String} config.varName Name of the variable that will hold 
     * RemotingProvider configuration block, including its Actions.
     * @param {Function} [callback] Optional callback to execute when
     * Provider is created, or when an error has occured.
     * @param {Object} [scope] Optional scope to execute callback function in.
     *
     * For additional information see the [Ext Direct specification][1].
     */
    loadProvider: function(config, callback, scope) {
        var me = this,
            classes = me.providerClasses,
            type, url, varName, provider, i, len;
        if (Ext.isArray(config)) {
            for (i = 0 , len = config.length; i < len; i++) {
                me.loadProvider(config[i], callback, scope);
            }
            return;
        }
        // We may have been passed config object containing enough
        // information to create a Provider without further ado.
        type = config.type;
        url = config.url;
        if (classes[type] && classes[type].checkConfig(config)) {
            provider = me.addProvider(config);
            me.fireEventArgs('providerload', [
                url,
                provider
            ]);
            Ext.callback(callback, scope, [
                url,
                provider
            ]);
            // We're deliberately not returning the provider here
            // to make way for the future Promises based implementation
            // that should be consistent with the remote API declaration
            // retrieval below.
            return;
        }
        // For remote API declaration retrieval we need to know the
        // service discovery URL and variable name, at the minimum.
        // We have a default for the variable name but not for URL.
        varName = config.varName || me.getVarName();
        delete config.varName;
        if (!url) {
            Ext.raise("Need API discovery URL to load a Remoting provider!");
        }
        // The URL we are requesting API from is not the same as the
        // service URL, and we don't need them to mix.
        delete config.url;
        // Have to use closures here as Loader does not allow passing
        // options object from caller to callback.
        Ext.Loader.loadScript({
            url: url,
            scope: me,
            onLoad: function() {
                this.onApiLoadSuccess({
                    url: url,
                    varName: varName,
                    config: config,
                    callback: callback,
                    scope: scope
                });
            },
            onError: function() {
                this.onApiLoadFailure({
                    url: url,
                    callback: callback,
                    scope: scope
                });
            }
        });
    },
    /**
     * Retrieves a {@link Ext.direct.Provider provider} by the id specified when the
     * provider is added.
     *
     * @param {String/Ext.direct.Provider} id The id of the provider, or the provider instance.
     */
    getProvider: function(id) {
        return id.isProvider ? id : this.providers.get(id);
    },
    /**
     * Removes the provider.
     *
     * @param {String/Ext.direct.Provider} provider The provider instance or the id of the provider.
     *
     * @return {Ext.direct.Provider} The provider, null if not found.
     */
    removeProvider: function(provider) {
        var me = this,
            providers = me.providers,
            relayers = me.relayers,
            id;
        provider = provider.isProvider ? provider : providers.get(provider);
        if (provider) {
            provider.un('data', me.onProviderData, me);
            id = provider.id;
            if (relayers[id]) {
                relayers[id].destroy();
                delete relayers[id];
            }
            providers.remove(provider);
            return provider;
        }
        return null;
    },
    /**
     * Adds a transaction to the manager.
     *
     * @param {Ext.direct.Transaction} transaction The transaction to add
     *
     * @return {Ext.direct.Transaction} transaction
     *
     * @private
     */
    addTransaction: function(transaction) {
        this.transactions.add(transaction);
        return transaction;
    },
    /**
     * Removes a transaction from the manager.
     *
     * @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction
     * to remove
     *
     * @return {Ext.direct.Transaction} transaction
     *
     * @private
     */
    removeTransaction: function(transaction) {
        var me = this;
        transaction = me.getTransaction(transaction);
        me.transactions.remove(transaction);
        return transaction;
    },
    /**
     * Gets a transaction
     *
     * @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction to get
     *
     * @return {Ext.direct.Transaction}
     *
     * @private
     */
    getTransaction: function(transaction) {
        return typeof transaction === 'object' ? transaction : this.transactions.get(transaction);
    },
    onProviderData: function(provider, event) {
        var me = this,
            i, len;
        if (Ext.isArray(event)) {
            for (i = 0 , len = event.length; i < len; ++i) {
                me.onProviderData(provider, event[i]);
            }
            return;
        }
        if (event.name && event.name !== 'event' && event.name !== 'exception') {
            me.fireEvent(event.name, event);
        } else if (event.status === false) {
            me.fireEvent('exception', event);
        }
        me.fireEvent('event', event, provider);
    },
    /**
     * Parses a direct function. It may be passed in a string format, for example:
     * "MyApp.Person.read".
     *
     * @param {String/Function} fn The direct function
     *
     * @return {Function} The function to use in the direct call. Null if not found
     */
    parseMethod: function(fn) {
        var current = Ext.global,
            i = 0,
            resolved, parts, len;
        if (Ext.isFunction(fn)) {
            resolved = fn;
        } else if (Ext.isString(fn)) {
            resolved = this.remotingMethods[fn];
            // Support legacy resolution as top-down lookup
            // from the window scope
            if (!resolved) {
                parts = fn.split('.');
                len = parts.length;
                while (current && i < len) {
                    current = current[parts[i]];
                    ++i;
                }
                resolved = Ext.isFunction(current) ? current : null;
            }
        }
        return resolved || null;
    },
    /**
     * @private
     * Iterate over an API object containing function names and resolve Direct functions.
     *
     * @param {Object} api API object
     * @param {Ext.Base} caller The object calling
     *
     * @return {Object} The API object with each property resolved to a Direct function.
     */
    resolveApi: function(api, caller) {
        var prefix, action, method, fullName, fn;
        prefix = api && api.prefix;
        if (prefix && prefix.substr(prefix.length - 1) !== '.') {
            prefix += '.';
        }
        for (action in api) {
            method = api[action];
            if (action !== 'prefix' && typeof method !== 'function') {
                fullName = (prefix || '') + method;
                fn = this.parseMethod(fullName);
                if (typeof fn === 'function') {
                    api[action] = fn;
                } else {
                    Ext.raise("Cannot resolve Direct API method '" + fullName + "' for " + action + " action in " + caller.$className + " instance with id: " + (caller.id != null ? caller.id : 'unknown'));
                }
            }
        }
        return api;
    },
    privates: {
        addProviderClass: function(type, cls) {
            this.providerClasses[type] = cls;
        },
        onApiLoadSuccess: function(options) {
            var me = this,
                url = options.url,
                varName = options.varName,
                api, provider, error;
            try {
                // Variable name could be nested (default is Ext.REMOTING_API),
                // so we use eval() to get the actual value.
                api = Ext.apply(options.config, eval(varName));
                provider = me.addProvider(api);
            } catch (e) {
                error = e + '';
            }
            if (error) {
                me.fireEventArgs('providerloaderror', [
                    url,
                    error
                ]);
                Ext.callback(options.callback, options.scope, [
                    url,
                    error
                ]);
            } else {
                me.fireEventArgs('providerload', [
                    url,
                    provider
                ]);
                Ext.callback(options.callback, options.scope, [
                    url,
                    provider
                ]);
            }
        },
        onApiLoadFailure: function(options) {
            var url = options.url,
                error;
            error = Ext.String.format(this.apiNotFoundError, url);
            this.fireEventArgs('providerloaderror', [
                url,
                error
            ]);
            Ext.callback(options.callback, options.scope, [
                url,
                error
            ]);
        },
        registerMethod: function(name, method) {
            this.remotingMethods[name] = method;
        },
        // Used for testing
        clearAllMethods: function() {
            this.remotingMethods = {};
        }
    }
}, function() {
    // Backwards compatibility
    Ext.Direct = Ext.direct.Manager;
});

/**
 * Ext.direct.Provider is an abstract class meant to be extended.
 *
 * For example Ext JS implements the following subclasses:
 *
 *     Provider
 *     |
 *     +---JsonProvider
 *         |
 *         +---PollingProvider
 *         |
 *         +---RemotingProvider
 *
 * @abstract
 */
Ext.define('Ext.direct.Provider', {
    alias: 'direct.provider',
    mixins: [
        'Ext.mixin.Observable'
    ],
    requires: [
        'Ext.direct.Manager'
    ],
    isProvider: true,
    $configPrefixed: false,
    $configStrict: false,
    /* eslint-disable max-len */
    /**
     * @cfg {String} id
     * The unique id of the provider (defaults to an {@link Ext#id auto-assigned id}).
     * You should assign an id if you need to be able to access the provider later and you do
     * not have an object reference available, for example:
     *
     *      Ext.direct.Manager.addProvider({
     *          type: 'polling',
     *          url:  'php/poll.php',
     *          id:   'poll-provider'
     *      });
     *      
     *      var p = {@link Ext.direct.Manager}.{@link Ext.direct.Manager#getProvider getProvider}('poll-provider');
     *      
     *      p.disconnect();
     *
     */
    /* eslint-enable max-len */
    /**
     * @cfg {String[]} relayedEvents
     * List of Provider events that should be relayed by {@link Ext.direct.Manager}.
     * 'data' event is always relayed.
     */
    config: {
        /**
         * @cfg {Object} [headers]
         * An object containing default headers for every Ajax request made by this Provider.
         */
        headers: undefined
    },
    /**
     * @event connect
     * Fires when the Provider connects to the server-side
     *
     * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
     */
    /**
     * @event disconnect
     * Fires when the Provider disconnects from the server-side
     *
     * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
     */
    /**
     * @event data
     * Fires when the Provider receives data from the server-side. This event is fired
     * for valid responses as well as for exceptions.
     *
     * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider} instance.
     * @param {Ext.direct.Event} e The {@link Ext.direct.Event} that occurred.
     */
    /**
     * @event exception
     * Fires when the Provider receives an exception from the server-side. This event is *not*
     * fired for valid responses.
     *
     * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider} instance.
     * @param {Ext.direct.Event} e The {@link Ext.direct.Event Exception event} that occured.
     */
    subscribers: 0,
    constructor: function(config) {
        var me = this;
        me.mixins.observable.constructor.call(me, config);
        me.requests = {};
        if (me.id == null) {
            me.id = Ext.id(null, 'provider-');
        }
    },
    destroy: function() {
        var me = this;
        me.disconnect(true);
        me.callParent();
    },
    /**
     * Returns whether or not the server-side is currently connected.
     */
    isConnected: function() {
        return this.subscribers > 0;
    },
    /**
     * Connect the provider and start its service.
     * Provider will fire `connect` event upon successful connection.
     */
    connect: function() {
        var me = this;
        if (me.subscribers === 0) {
            me.doConnect();
            me.fireEventArgs('connect', [
                me
            ]);
        }
        me.subscribers++;
    },
    /**
     * @method
     *
     * Do connection setup. This is a template method.
     * @template
     * @protected
     */
    doConnect: Ext.emptyFn,
    /**
     * Disconnect the provider and stop its service.
     * Provider will fire `disconnect` event upon successful disconnect.
     */
    disconnect: function(/* */
    force) {
        var me = this;
        if (me.subscribers > 0 || force) {
            if (force) {
                me.subscribers = 0;
            } else {
                me.subscribers--;
            }
            if (me.subscribers === 0) {
                me.doDisconnect();
                me.fireEventArgs('disconnect', [
                    me
                ]);
            }
        }
    },
    /**
     * @method
     *
     * Do connection teardown. This is a template method.
     * @template
     * @protected
     */
    doDisconnect: function() {
        var requests = this.requests,
            request, id;
        for (id in requests) {
            request = requests[id];
            request.abort();
        }
        this.requests = {};
    },
    /**
     * Send the Ajax request
     *
     * @param {Object} params Ajax request parameters
     *
     * @private
     */
    sendAjaxRequest: function(params) {
        var request = Ext.Ajax.request(params);
        if (request && request.id) {
            this.requests[request.id] = request;
        }
        return request;
    },
    /**
     * Ajax request callback
     *
     * @private
     */
    onData: function(options, success, response) {
        if (response && response.request) {
            delete this.requests[response.request.id];
        }
    },
    inheritableStatics: {
        /**
         * @method
         *
         * Check if the passed configuration object contains enough
         * information to construct a Provider.
         *
         * @param {Object} config
         *
         * @return {Boolean} `true` if config is sufficient, `false` otherwise.
         * @static
         * @inheritable
         */
        checkConfig: Ext.returnFalse
    },
    onClassExtended: function(cls, data, hooks) {
        if (data.type) {
            Ext.direct.Manager.addProviderClass(data.type, cls);
        }
    }
});

/**
 * This class implements the Ext Direct event domain. All classes extending from
 * {@link Ext.direct.Provider} are included in this domain. The selectors are simply provider
 * id's or the wildcard "*" to match any provider.
 *
 * @private
 */
Ext.define('Ext.app.domain.Direct', {
    extend: 'Ext.app.EventDomain',
    singleton: true,
    requires: [
        'Ext.direct.Provider'
    ],
    type: 'direct',
    idProperty: 'id',
    constructor: function() {
        var me = this;
        me.callParent();
        me.monitor(Ext.direct.Provider);
    }
});

/**
 * @class Ext.data.PageMap
 * @extends Ext.util.LruCache
 * Private class for use by only Store when configured `buffered: true`.
 * @private
 */
Ext.define('Ext.data.PageMap', {
    extend: 'Ext.util.LruCache',
    config: {
        store: null,
        /**
         * @cfg {Number} pageSize
         * The size of pages in this map.
         */
        pageSize: 0,
        /**
         * @cfg {String} rootProperty
         * The root property to use for aggregation, filtering and sorting. By default
         * this is `null` but when containing things like {@link Ext.data.Model records}
         * this config would likely be set to "data" so that property names are applied
         * to the fields of each record.
         */
        rootProperty: ''
    },
    // Maintain a generation counter, so that the Store can reject incoming pages destined
    // for the previous generation
    clear: function(initial) {
        var me = this;
        me.pageMapGeneration = (me.pageMapGeneration || 0) + 1;
        // Map of internalId to recordIndex
        me.indexMap = {};
        me.callParent([
            initial
        ]);
    },
    updatePageSize: function(value, oldValue) {
        if (oldValue != null) {
            throw "pageMap page size may not be changed";
        }
    },
    forEach: function(fn, scope) {
        var me = this,
            pageNumbers = Ext.Object.getKeys(me.map),
            pageCount = pageNumbers.length,
            pageSize = me.getPageSize(),
            pageNumber, page, i, j, len;
        for (i = 0; i < pageCount; i++) {
            pageNumbers[i] = +pageNumbers[i];
        }
        Ext.Array.sort(pageNumbers, Ext.Array.numericSortFn);
        scope = scope || me;
        for (i = 0; i < pageCount; i++) {
            pageNumber = pageNumbers[i];
            page = me.getPage(pageNumber);
            len = page.length;
            for (j = 0; j < len; j++) {
                if (fn.call(scope, page[j], (pageNumber - 1) * pageSize + j) === false) {
                    return;
                }
            }
        }
    },
    /**
    * Returns the first record in this page map which elicits a true return value from the
    * passed selection function.
    *
    * **IMPORTANT**
    *
    * **This can ONLY find records which happen to be cached in the page cache. This will be parts
    * of the dataset around the currently visible zone, or recently visited zones if the pages
    * have not yet been purged from the cache.**
    * 
    * **This CAN NOT find records which have not been loaded into the cache.**
    *
    * If full client side searching is required, do not use a buffered store, instead use a regular,
    * fully loaded store and use the {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer}
    * plugin to minimize DOM footprint.
    * @param {Function} fn The selection function to execute for each item.
    *  @param {Mixed} fn.rec The record.
    *  @param {Mixed} fn.index The index in the total dataset of the record.
    * @param {Object} [scope] The scope (`this` reference) in which the function is executed.
    * Defaults to this PageMap.
    * @return {Object} The first record in this page map which returned true from the selection
    * function, or null if none was found.
    */
    findBy: function(fn, scope) {
        var me = this,
            result = null;
        scope = scope || me;
        me.forEach(function(rec, index) {
            if (fn.call(scope, rec, index)) {
                result = rec;
                return false;
            }
        });
        return result;
    },
    /**
    * Returns the index *in the whole dataset* of the first record in this page map which elicits
    * a true return value from the passed selection function.
    *
    * **IMPORTANT**
    *
    * **This can ONLY find records which happen to be cached in the page cache. This will be parts
    * of the dataset around the currently visible zone, or recently visited zones if the pages
    * have not yet been purged from the cache.**
    * 
    * **This CAN NOT find records which have not been loaded into the cache.**
    *
    * If full client side searching is required, do not use a buffered store, instead use a regular,
    * fully loaded store and use the {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer}
    * plugin to minimize DOM footprint.
    * @param {Function} fn The selection function to execute for each item.
    *  @param {Mixed} fn.rec The record.
    *  @param {Mixed} fn.index The index in the total dataset of the record.
    * @param {Object} [scope] The scope (`this` reference) in which the function is executed.
    * Defaults to this PageMap.
    * @return {Number} The index first record in this page map which returned true from the
    * selection function, or -1 if none was found.
    */
    findIndexBy: function(fn, scope) {
        var me = this,
            result = -1;
        scope = scope || me;
        me.forEach(function(rec, index) {
            if (fn.call(scope, rec)) {
                result = index;
                return false;
            }
        });
        return result;
    },
    find: function(property, value, start, startsWith, endsWith, ignoreCase) {
        if (Ext.isEmpty(value, false)) {
            return null;
        }
        /* eslint-disable-next-line vars-on-top */
        var regex = Ext.String.createRegex(value, startsWith, endsWith, ignoreCase),
            root = this.getRootProperty();
        return this.findBy(function(item) {
            return item && regex.test((root ? item[root] : item)[property]);
        }, null, start);
    },
    findIndex: function(property, value, start, startsWith, endsWith, ignoreCase) {
        if (Ext.isEmpty(value, false)) {
            return null;
        }
        /* eslint-disable-next-line vars-on-top */
        var regex = Ext.String.createRegex(value, startsWith, endsWith, ignoreCase),
            root = this.getRootProperty();
        return this.findIndexBy(function(item) {
            return item && regex.test((root ? item[root] : item)[property]);
        }, null, start);
    },
    getPageFromRecordIndex: function(index) {
        return Math.floor(index / this.getPageSize()) + 1;
    },
    addAll: function(records) {
        if (this.getCount()) {
            Ext.raise('Cannot addAll to a non-empty PageMap');
        }
        this.addPage(1, records);
    },
    addPage: function(pageNumber, records) {
        var me = this,
            pageSize = me.getPageSize(),
            lastPage = pageNumber + Math.floor((records.length - 1) / pageSize),
            storeIndex = (pageNumber - 1) * pageSize,
            indexMap = me.indexMap,
            page, i, len, startIdx;
        // Account for being handed a block of records spanning several pages.
        // This can happen when loading from a MemoryProxy before a viewSize has been determined.
        for (startIdx = 0; pageNumber <= lastPage; pageNumber++ , startIdx += pageSize) {
            page = Ext.Array.slice(records, startIdx, startIdx + pageSize);
            // Maintain the indexMap so that we can implement indexOf(record)
            for (i = 0 , len = page.length; i < len; i++) {
                indexMap[page[i].internalId] = storeIndex++;
            }
            me.add(pageNumber, page);
            me.fireEvent('pageadd', me, pageNumber, page);
        }
    },
    getCount: function() {
        var result = this.callParent();
        if (result) {
            result = (result - 1) * this.getPageSize() + this.last.value.length;
        }
        return result;
    },
    getByInternalId: function(internalId) {
        var index = this.indexMap[internalId];
        if (index != null) {
            return this.getAt(index);
        }
    },
    indexOf: function(record) {
        var result = -1;
        if (record) {
            result = this.indexMap[record.internalId];
            if (result == null) {
                result = -1;
            }
        }
        return result;
    },
    insert: function() {
        Ext.raise('insert operation not suppported into buffered Store');
    },
    remove: function() {
        Ext.raise('remove operation not suppported from buffered Store');
    },
    removeAt: function() {
        Ext.raise('removeAt operation not suppported from buffered Store');
    },
    removeAtKey: function(page) {
        // Allow observers to veto
        var me = this,
            thePage = me.getPage(page),
            result, i, len;
        if (thePage) {
            if (me.fireEvent('beforepageremove', me, page, thePage) !== false) {
                len = thePage.length;
                for (i = 0; i < len; i++) {
                    delete me.indexMap[thePage[i].internalId];
                }
                result = me.callParent(arguments);
                me.fireEvent('pageremove', me, page, thePage);
                // Empty the page array *after* informing observers that the records have exited.
                thePage.length = 0;
            }
        }
        return result;
    },
    getPage: function(pageNumber) {
        return this.get(pageNumber);
    },
    hasRange: function(start, end) {
        var me = this,
            pageNumber = me.getPageFromRecordIndex(start),
            endPageNumber = me.getPageFromRecordIndex(end);
        for (; pageNumber <= endPageNumber; pageNumber++) {
            if (!me.hasPage(pageNumber)) {
                return false;
            }
        }
        // Check that the last page is filled enough to encapsulate the range.
        return (endPageNumber - 1) * me._pageSize + me.getPage(endPageNumber).length > end;
    },
    hasPage: function(pageNumber) {
        // We must use this.get to trigger an access so that the page which is checked for presence
        // is not eligible for pruning
        return !!this.get(pageNumber);
    },
    peekPage: function(pageNumber) {
        return this.map[pageNumber];
    },
    getAt: function(index) {
        return this.getRange(index, index + 1)[0];
    },
    getRange: function(start, end) {
        // Store's backing Collection now uses EXCLUSIVE endIndex
        // So store will always pass the endIndex+1 unless it's 0
        if (end) {
            end--;
        }
        if (!this.hasRange(start, end)) {
            Ext.raise('PageMap asked for range which it does not have');
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            Array = Ext.Array,
            pageSize = me.getPageSize(),
            startPageNumber = me.getPageFromRecordIndex(start),
            endPageNumber = me.getPageFromRecordIndex(end),
            dataStart = (startPageNumber - 1) * pageSize,
            dataEnd = (endPageNumber * pageSize) - 1,
            pageNumber = startPageNumber,
            result = [],
            sliceBegin, sliceEnd, doSlice;
        for (; pageNumber <= endPageNumber; pageNumber++) {
            // First and last pages *may* need slicing to cut into the actual wanted records
            if (pageNumber === startPageNumber) {
                sliceBegin = start - dataStart;
                doSlice = sliceBegin > 0;
            } else {
                sliceBegin = 0;
                doSlice = false;
            }
            if (pageNumber === endPageNumber) {
                sliceEnd = pageSize - (dataEnd - end);
                doSlice = doSlice || sliceEnd < pageSize;
            }
            // First and last pages will need slicing
            if (doSlice) {
                Array.push(result, Array.slice(me.getPage(pageNumber), sliceBegin, sliceEnd));
            } else {
                Array.push(result, me.getPage(pageNumber));
            }
        }
        return result;
    }
});

/**
 * A BufferedStore maintains a sparsely populated map of pages corresponding to an extremely large
 * server-side dataset.
 *
 * **Note:** Buffered Stores are not available in the modern toolkit. Instead use
 * `Ext.data.virtual.Store`.
 *
 * Use a BufferedStore when the dataset size is so large that the database and network latency,
 * and client memory requirements preclude caching the entire dataset in a regular
 * {@link Ext.data.Store Store}.
 *
 * When using a BufferedStore *not all of the dataset is present in the client*. Only pages which
 * have been requested by the UI (usually a {@link Ext.grid.Panel GridPanel}) and surrounding pages
 * will be present. Retention of viewed pages in the BufferedStore after they have been scrolled
 * out of view is configurable. See {@link #leadingBufferZone}, {@link #trailingBufferZone} and
 * {@link #purgePageCount}.
 *
 * To use a BufferedStore, initiate the loading process by loading the first page. The number of
 * rows rendered are determined automatically, and the range of pages needed to keep the cache
 * primed for scrolling is requested and cached.
 * Example:
 *
 *     myBufferedStore.loadPage(1); // Load page 1
 *
 * A {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer} is instantiated which will monitor
 * the scrolling in the grid, and refresh the view's rows from the page cache as needed. It will
 * also pull new data into the page cache when scrolling of the view draws upon data near either
 * end of the prefetched data.
 *
 * The margins which trigger view refreshing from the prefetched data are
 * {@link Ext.grid.plugin.BufferedRenderer#numFromEdge},
 * {@link Ext.grid.plugin.BufferedRenderer#leadingBufferZone}, and
 * {@link Ext.grid.plugin.BufferedRenderer#trailingBufferZone}.
 *
 * The margins which trigger loading more data into the page cache are, {@link #leadingBufferZone}
 * and {@link #trailingBufferZone}.
 *
 * By default, only 5 pages of data (in addition to the pages which over the visible region) are
 * cached in the page cache, with old pages being evicted from the cache as the view moves down
 * through the dataset. This is controlled by the {@link #purgePageCount} setting.
 *
 * Setting this value to zero means that no pages are *ever* scrolled out of the page cache, and
 * that eventually the whole dataset may become present in the page cache. This is sometimes
 * desirable as long as datasets do not reach astronomical proportions.
 *
 * Selection state may be maintained across page boundaries by configuring the SelectionModel
 * not to discard records from its collection when those Records cycle out of the Store's primary
 * collection. This is done by configuring the SelectionModel like this:
 *
 *     selModel: {
 *         pruneRemoved: false
 *     }
 *
 */
Ext.define('Ext.data.BufferedStore', {
    extend: 'Ext.data.ProxyStore',
    alias: 'store.buffered',
    requires: [
        'Ext.data.PageMap',
        'Ext.util.Filter',
        'Ext.util.Sorter',
        'Ext.util.Grouper'
    ],
    uses: [
        'Ext.util.SorterCollection',
        'Ext.util.FilterCollection',
        'Ext.util.GroupCollection'
    ],
    /**
     * @property {Boolean} isBufferedStore
     * `true` in this class to identify an object as an instantiated BufferedStore, or subclass
     * thereof.
     */
    isBufferedStore: true,
    // For backward compatibility with user code.
    buffered: true,
    config: {
        data: 0,
        pageSize: 25,
        /**
         * @cfg remoteSort
         * @inheritdoc
         */
        remoteSort: true,
        /**
         * @cfg remoteFilter
         * @inheritdoc
         */
        remoteFilter: true,
        /**
         * @cfg sortOnLoad
         * @inheritdoc
         */
        sortOnLoad: false,
        /**
         * @cfg {Number} purgePageCount
         *
         * The number of pages *in addition to twice the required buffered range* to keep
         * in the prefetch cache before purging least recently used records.
         *
         * For example, if the height of the view area and the configured
         * {@link #trailingBufferZone} and {@link #leadingBufferZone} require that there
         * are three pages in the cache, then a `purgePageCount` of 5 ensures that up to
         * 11 pages can be in the page cache any any one time. This is enough to allow
         * the user to scroll rapidly between different areas of the dataset without
         * evicting pages which are still needed.
         *
         * A value of 0 indicates to never purge the prefetched data.
         */
        purgePageCount: 5,
        /**
         * @cfg {Number} trailingBufferZone
         * The number of extra records to keep cached on the trailing side of scrolling
         * buffer as scrolling proceeds. A larger number means fewer replenishments from
         * the server.
         */
        trailingBufferZone: 25,
        /**
         * @cfg {Number} leadingBufferZone
         * The number of extra rows to keep cached on the leading side of scrolling
         * buffer as scrolling proceeds. A larger number means fewer replenishments from
         * the server.
         */
        leadingBufferZone: 200,
        /**
         * @cfg {Number} defaultViewSize
         * The default view size to use until the
         * {@link #viewSize} has been configured.
         * @private
         */
        defaultViewSize: 100,
        /**
         * @cfg {Number} viewSize The view size needed to fill the current view. Defaults
         * to the {@link #defaultViewSize}.
         * This will typically be set by the underlying view.
         * @private
         */
        viewSize: 0,
        /**
         * @cfg {Boolean} trackRemoved
         * The {@link Ext.data.ProxyStore#cfg!trackRemoved trackRemoved} config is not
         * supported by buffered stores.
         */
        trackRemoved: false
    },
    /**
     * We are using applyData so that we can return nothing and prevent the `this.data`
     * property to be overridden.
     * @param {Array/Object} data
     */
    applyData: function(data) {
        var dataCollection = this.data || (this.data = this.createDataCollection());
        if (data && data !== true) {
            Ext.raise('Cannot load a buffered store with local data - the store is a map ' + 'of remote data');
        }
        return dataCollection;
    },
    applyProxy: function(proxy) {
        proxy = this.callParent([
            proxy
        ]);
        // This store asks for pages.
        // If used with a MemoryProxy, it must work
        if (proxy && proxy.setEnablePaging) {
            proxy.setEnablePaging(true);
        }
        return proxy;
    },
    applyAutoSort: function() {},
    // Return undefined so that applier does not run.
    // BufferedStore/PageMap cannot sort.
    createFiltersCollection: function() {
        return new Ext.util.FilterCollection();
    },
    createSortersCollection: function() {
        return new Ext.util.SorterCollection();
    },
    updateRemoteFilter: function(remoteFilter, oldRemoteFilter) {
        if (remoteFilter === false) {
            Ext.raise('Buffered stores are always remotely filtered.');
        }
        this.callParent([
            remoteFilter,
            oldRemoteFilter
        ]);
    },
    updateRemoteSort: function(remoteSort, oldRemoteSort) {
        if (remoteSort === false) {
            Ext.raise('Buffered stores are always remotely sorted.');
        }
        this.callParent([
            remoteSort,
            oldRemoteSort
        ]);
    },
    updateTrackRemoved: function(value) {
        if (value !== false) {
            Ext.raise('Cannot use trackRemoved with a buffered store.');
        }
        this.callParent(arguments);
    },
    updateGroupField: function(field) {
        this.group(field);
    },
    getGrouper: function() {
        return this.grouper;
    },
    isGrouped: function() {
        return !!this.grouper;
    },
    createDataCollection: function() {
        var me = this,
            result = new Ext.data.PageMap({
                store: me,
                rootProperty: 'data',
                pageSize: me.getPageSize(),
                maxSize: me.getPurgePageCount(),
                listeners: {
                    // Whenever PageMap gets cleared, it means we re no longer interested in 
                    // any outstanding page prefetches, so cancel them all
                    clear: me.onPageMapClear,
                    scope: me
                }
            });
        // Allow view to veto prune if the old page is still in use by the view
        me.relayEvents(result, [
            'beforepageremove',
            'pageadd',
            'pageremove'
        ]);
        me.pageRequests = {};
        return result;
    },
    add: function() {
        Ext.raise('add method may not be called on a buffered store - the store is a map ' + 'of remote data');
    },
    insert: function() {
        Ext.raise('insert method may not be called on a buffered store - the store is a map ' + 'of remote data');
    },
    removeAll: function(silent) {
        var me = this,
            data = me.getData();
        if (data) {
            if (silent) {
                me.suspendEvent('clear');
            }
            data.clear();
            if (silent) {
                me.resumeEvent('clear');
            }
        }
    },
    flushLoad: function() {
        var me = this,
            options = me.pendingLoadOptions;
        // If it gets called programmatically, the listener will need cancelling
        me.clearLoadTask();
        if (!options) {
            return;
        }
        // Buffered stores, a load operation means kick off a clean load from page 1
        // unless it's specified to preserve the options
        if (!options.preserveOnFlush) {
            me.getData().clear();
            options.page = 1;
            options.start = 0;
            options.limit = me.getViewSize() || me.getDefaultViewSize();
        }
        // If we're prefetching, the arguments on the callback for getting the range is different
        // So we indicate that we need to fire a special "load" style callback
        options.loadCallback = options.callback;
        // options might be chained, with callback on a prototype; delete won't clear it.
        options.callback = null;
        return me.loadToPrefetch(options);
    },
    reload: function(options) {
        var me = this,
            data = me.getData(),
            // If we don't have a known totalCount, use a huge value
            lastTotal = Number.MAX_VALUE,
            startIdx, endIdx, startPage, endPage, i, waitForReload, bufferZone, records;
        if (!options) {
            options = {};
        }
        // Prevent re-entering the load process if we are already in a wait state for a batch
        // of pages.
        if (me.loading || me.fireEvent('beforeload', me, options) === false) {
            return;
        }
        waitForReload = function() {
            var newCount = me.totalCount,
                oldRequestSize = endIdx - startIdx;
            // If the dataset has now shrunk leaving the calculated request zone unavailable,
            // re-evaluate the request zone. Start as close to the end as possible.
            if (endIdx >= newCount) {
                endIdx = newCount - 1;
                startIdx = Math.max(endIdx - oldRequestSize, 0);
            }
            if (me.rangeCached(startIdx, endIdx, false)) {
                me.loadCount = (me.loadCount || 0) + 1;
                me.loading = false;
                data.un('pageadd', waitForReload);
                records = data.getRange(startIdx, endIdx);
                me.fireEvent('refresh', me);
                me.fireEvent('load', me, records, true);
            }
        };
        bufferZone = Math.ceil((me.getLeadingBufferZone() + me.getTrailingBufferZone()) / 2);
        // Decide what reload means.
        // If the View was configured preserveScrollOnReload, then it will
        // inject that setting here. This means that reload means
        // load the last requested range.
        if (me.lastRequestStart && me.preserveScrollOnReload) {
            startIdx = me.lastRequestStart;
            endIdx = me.lastRequestEnd;
            lastTotal = me.getTotalCount();
        } else // Otherwise, reload means start from page 1
        {
            startIdx = options.start || 0;
            endIdx = startIdx + (options.count || me.getPageSize()) - 1;
        }
        // Clear page cache
        data.clear(true);
        // So that prefetchPage does not consider the store to be fully loaded if the local count
        // is equal to the total count
        delete me.totalCount;
        // Calculate a page range which encompasses the Store's loaded range plus both buffer zones
        startIdx = Math.max(startIdx - bufferZone, 0);
        endIdx = Math.min(endIdx + bufferZone, lastTotal);
        // We must wait for a slightly wider range to be cached.
        // This is to allow grouping features to peek at the two surrounding records
        // when rendering a *range* of records to see whether the start of the range
        // really is a group start and the end of the range really is a group end.
        startIdx = startIdx === 0 ? 0 : startIdx - 1;
        endIdx = endIdx === lastTotal ? endIdx : endIdx + 1;
        startPage = me.getPageFromRecordIndex(startIdx);
        endPage = me.getPageFromRecordIndex(endIdx);
        me.loading = true;
        options.waitForReload = waitForReload;
        // Wait for the requested range to become available in the page map
        // Load the range as soon as the whole range is available
        data.on('pageadd', waitForReload);
        // Recache the page range which encapsulates our visible records
        for (i = startPage; i <= endPage; i++) {
            me.prefetchPage(i, options);
        }
    },
    filter: function() {
        if (!this.getRemoteFilter()) {
            Ext.raise('Local filtering may not be used on a buffered store - the store is a map ' + 'of remote data');
        }
        // Remote filtering forces a load. load clears the store's contents.
        this.callParent(arguments);
    },
    filterBy: function(fn, scope) {
        Ext.raise('Local filtering may not be used on a buffered store - the store is a map ' + 'of remote data');
    },
    loadData: function(data, append) {
        Ext.raise('LoadData may not be used on a buffered store - the store is a map ' + 'of remote data');
    },
    loadPage: function(page, options) {
        var me = this;
        options = options || {};
        options.page = me.currentPage = page;
        options.start = (page - 1) * me.getPageSize();
        options.limit = me.getViewSize() || me.getDefaultViewSize();
        options.loadCallback = options.callback;
        // options might be chained, with callback on a prototype; delete won't clear it.
        options.callback = null;
        // Since we're deferring these to flushLoad, we need to preserve the options.
        // Otherwise, they will be reset.
        options.preserveOnFlush = true;
        return me.load(options);
    },
    clearData: function(isLoad) {
        var me = this,
            data = me.getData();
        if (data) {
            data.clear();
        }
    },
    /**
     * @private
     * A BufferedStore always reports that it contains the full dataset.
     * The number of records that happen to be cached at any one time is never useful.
     */
    getCount: function() {
        return this.totalCount || 0;
    },
    getRange: function(start, end, options) {
        var me = this,
            maxIndex = me.totalCount - 1,
            lastRequestStart = me.lastRequestStart,
            result = [],
            data = me.getData(),
            pageAddHandler, requiredStart, requiredEnd, requiredStartPage, requiredEndPage;
        options = Ext.apply({
            prefetchStart: start,
            prefetchEnd: end
        }, options);
        // Sanity check end point to be within dataset range
        end = (end >= me.totalCount) ? maxIndex : end;
        // If this is being called in the default manner, to fetch data 
        // for rendering, then we must wait for a slightly wider range to be cached.
        // This is to allow grouping features to peek at the two surrounding records
        // when rendering a *range* of records to see whether the start of the range
        // really is a group start and the end of the range really is a group end.
        if (options.forRender !== false) {
            requiredStart = start === 0 ? 0 : start - 1;
            requiredEnd = end === maxIndex ? end : end + 1;
        } else {
            requiredStart = start;
            requiredEnd = end;
        }
        // Keep track of range we are being asked for so we can track direction of movement
        // through the dataset
        me.lastRequestStart = start;
        me.lastRequestEnd = end;
        // If data request can be satisfied from the page cache
        if (me.rangeCached(start, end, options.forRender)) {
            me.onRangeAvailable(options);
            result = data.getRange(start, end + 1);
        } else // At least some of the requested range needs loading from server
        {
            // Private event used by the LoadMask class to perform masking when the range
            // required for rendering is not found in the cache
            me.fireEvent('cachemiss', me, start, end);
            requiredStartPage = me.getPageFromRecordIndex(requiredStart);
            requiredEndPage = me.getPageFromRecordIndex(requiredEnd);
            // Add a pageadd listener, and as soon as the requested range is loaded, call
            // onRangeAvailable to call the callback.
            pageAddHandler = function(pageMap, page, records) {
                if (page >= requiredStartPage && page <= requiredEndPage && me.rangeCached(start, end)) {
                    // Private event used by the LoadMask class to unmask when the range
                    // required for rendering has been loaded into the cache
                    me.fireEvent('cachefilled', me, start, end);
                    data.un('pageadd', pageAddHandler);
                    me.onRangeAvailable(options);
                }
            };
            data.on('pageadd', pageAddHandler);
            // Prioritize the request for the *exact range that the UI is asking for*.
            // When a page request is in flight, it will not be requested again by checking
            // the me.pageRequests hash, so the request after this will only request the
            // *remaining* unrequested pages.
            me.prefetchRange(start, end);
        }
        // Load the pages around the requested range required by the leadingBufferZone
        // and trailingBufferZone.
        me.primeCache(start, end, start < lastRequestStart ? -1 : 1);
        return result;
    },
    /**
     * Get the Record with the specified id.
     *
     * This method is not affected by filtering, lookup will be performed from all records
     * inside the store, filtered or not.
     *
     * @param {Mixed} id The id of the Record to find.
     * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.
     */
    getById: function(id) {
        var result = this.data.findBy(function(record) {
                return record.getId() === id;
            });
        return result;
    },
    /**
     * @method getAt
     * @inheritdoc
     */
    getAt: function(index) {
        var data = this.getData();
        if (data.hasRange(index, index)) {
            return data.getAt(index);
        }
    },
    /**
     * @private
     * Get the Record with the specified internalId.
     *
     * This method is not effected by filtering, lookup will be performed from all records
     * inside the store, filtered or not.
     *
     * @param {Mixed} internalId The id of the Record to find.
     * @return {Ext.data.Model} The Record with the passed internalId. Returns null if not found.
     */
    getByInternalId: function(internalId) {
        return this.data.getByInternalId(internalId);
    },
    /**
     * @method contains
     * @inheritdoc
     */
    contains: function(record) {
        return this.indexOf(record) > -1;
    },
    /**
     * Get the index of the record within the store.
     *
     * When store is filtered, records outside of filter will not be found.
     *
     * @param {Ext.data.Model} record The Ext.data.Model object to find.
     * @return {Number} The index of the passed Record. Returns -1 if not found.
     */
    indexOf: function(record) {
        return this.getData().indexOf(record);
    },
    /**
     * Get the index within the store of the Record with the passed id.
     *
     * Like #indexOf, this method is effected by filtering.
     *
     * @param {String} id The id of the Record to find.
     * @return {Number} The index of the Record. Returns -1 if not found.
     */
    indexOfId: function(id) {
        return this.indexOf(this.getById(id));
    },
    group: function(grouper, direction) {
        var me = this,
            oldGrouper;
        if (grouper && typeof grouper === 'string') {
            oldGrouper = me.grouper;
            if (oldGrouper && direction !== undefined) {
                oldGrouper.setDirection(direction);
            } else {
                me.grouper = new Ext.util.Grouper({
                    property: grouper,
                    direction: direction || 'ASC',
                    root: 'data'
                });
            }
        } else {
            me.grouper = grouper ? me.getSorters().decodeSorter(grouper, Ext.util.Grouper) : null;
        }
        me.getData().clear();
        me.loadPage(1, {
            callback: function() {
                me.fireEvent('groupchange', me, me.getGrouper());
            }
        });
    },
    /**
     * Determines the page from a record index
     * @param {Number} index The record index
     * @return {Number} The page the record belongs to
     */
    getPageFromRecordIndex: function(index) {
        return Math.floor(index / this.getPageSize()) + 1;
    },
    calculatePageCacheSize: function(rangeSizeRequested) {
        var me = this,
            purgePageCount = me.getPurgePageCount();
        // Calculate the number of pages that the cache will keep before purging  as follows:
        // TWO full rendering zones (in case of rapid teleporting by dragging the scroller)
        // plus configured purgePageCount.
        // Ensure we never reduce the count. It always uses the largest requested block
        // as the basis for the calculated size.
        /* eslint-disable-next-line max-len */
        return purgePageCount ? Math.max(me.getData().getMaxSize() || 0, Math.ceil((rangeSizeRequested + me.getTrailingBufferZone() + me.getLeadingBufferZone()) / me.getPageSize()) * 2 + purgePageCount) : 0;
    },
    /* eslint-disable max-len */
    loadToPrefetch: function(options) {
        var me = this,
            prefetchOptions = options,
            i, records, dataSetSize,
            // Get the requested record index range in the dataset
            startIdx = options.start,
            endIdx = options.start + options.limit - 1,
            rangeSizeRequested = (me.getViewSize() || options.limit),
            // The end index to load into the store's live record collection
            loadEndIdx = Math.min(endIdx, options.start + rangeSizeRequested - 1),
            // Calculate a page range which encompasses the requested range plus both buffer zones.
            // The endPage will be adjusted to be in the dataset size range as soon as the first
            // data block returns.
            startPage = me.getPageFromRecordIndex(Math.max(startIdx - me.getTrailingBufferZone(), 0)),
            endPage = me.getPageFromRecordIndex(endIdx + me.getLeadingBufferZone()),
            data = me.getData(),
            callbackFn = function() {
                // See comments in load() for why we need this.
                records = records || [];
                if (options.loadCallback) {
                    options.loadCallback.call(options.scope || me, records, operation, true);
                }
                if (options.callback) {
                    options.callback.call(options.scope || me, records, startIdx || 0, endIdx || 0, options);
                }
            },
            fireEventsFn = function() {
                me.loadCount = (me.loadCount || 0) + 1;
                me.fireEvent('datachanged', me);
                me.fireEvent('refresh', me);
                me.fireEvent('load', me, records, true);
            },
            // Wait for the viewable range to be available.
            waitForRequestedRange = function() {
                if (me.rangeCached(startIdx, loadEndIdx)) {
                    me.loading = false;
                    records = data.getRange(startIdx, loadEndIdx + 1);
                    data.un('pageadd', waitForRequestedRange);
                    // If there is a listener for guaranteedrange then fire that event
                    if (me.hasListeners.guaranteedrange) {
                        me.guaranteeRange(startIdx, loadEndIdx, options.callback, options.scope);
                    }
                    callbackFn();
                    fireEventsFn();
                }
            },
            operation;
        if (isNaN(me.pageSize) || !me.pageSize) {
            Ext.raise('Buffered store configured without a pageSize', me);
        }
        // Ensure that the purgePageCount allows enough pages to be kept cached to cover the
        // requested range. If the pageSize is very small we might need a lot of pages.
        data.setMaxSize(me.calculatePageCacheSize(rangeSizeRequested));
        if (me.fireEvent('beforeload', me, options) !== false) {
            // So that prefetchPage does not consider the store to be fully loaded if the local
            // count is equal to the total count
            delete me.totalCount;
            me.loading = true;
            // Any configured callback is handled in waitForRequestedRange above.
            // It should not be processed by onProxyPrefetch.
            if (options.callback) {
                prefetchOptions = Ext.apply({}, options);
                delete prefetchOptions.callback;
            }
            // Load the first page in the range, which will give us the initial total count.
            // Once it is loaded, go ahead and prefetch any subsequent pages, if necessary.
            // The prefetchPage has a check to prevent us loading more than the totalCount,
            // so we don't want to blindly load up <n> pages where it isn't required.
            me.on('prefetch', function(store, records, successful, op) {
                // Capture operation here so it can be used in the loadCallback above
                operation = op;
                if (successful) {
                    // If there is data in the dataset, we can go ahead and add the pageadd
                    // listener which waits for the visible range and we can also issue the
                    // requests to fill the surrounding buffer zones.
                    if ((dataSetSize = me.getTotalCount())) {
                        // Wait for the requested range to become available in the page map
                        data.on('pageadd', waitForRequestedRange);
                        // As soon as we have the size of the dataset, ensure we are not waiting
                        // for more than can ever arrive,
                        loadEndIdx = Math.min(loadEndIdx, dataSetSize - 1);
                        // And make sure we never ask for pages beyond the end of the dataset.
                        endPage = me.getPageFromRecordIndex(Math.min(loadEndIdx + me.getLeadingBufferZone(), dataSetSize - 1));
                        for (i = startPage + 1; i <= endPage; ++i) {
                            me.prefetchPage(i, prefetchOptions);
                        }
                    } else {
                        callbackFn();
                        fireEventsFn();
                    }
                } else // Unsuccessful prefetch: fire a load event with success false.
                {
                    me.loading = false;
                    callbackFn();
                    me.fireEvent('load', me, records, false);
                }
            }, null, {
                single: true
            });
            me.prefetchPage(startPage, prefetchOptions);
        }
    },
    /* eslint-enable max-len */
    // Buffering
    /**
     * Prefetches data into the store using its configured {@link #proxy}.
     * @param {Object} options (Optional) config object, passed into the
     * Ext.data.operation.Operation object before loading.
     * See {@link #method-load}
     */
    prefetch: function(options) {
        var me = this,
            pageSize = me.getPageSize(),
            data = me.getData(),
            operation, existingPageRequest;
        // Check pageSize has not been tampered with. That would break page caching
        if (pageSize) {
            if (me.lastPageSize && pageSize !== me.lastPageSize) {
                Ext.raise("pageSize cannot be dynamically altered");
            }
            if (!data.getPageSize()) {
                data.setPageSize(pageSize);
            }
        } else // Allow first prefetch call to imply the required page size.
        {
            me.pageSize = data.setPageSize(pageSize = options.limit);
        }
        // So that we can check for tampering next time through
        me.lastPageSize = pageSize;
        // Always get whole pages.
        if (!options.page) {
            options.page = me.getPageFromRecordIndex(options.start);
            options.start = (options.page - 1) * pageSize;
            options.limit = Math.ceil(options.limit / pageSize) * pageSize;
        }
        // Currently not requesting this page, or the request was for the last
        // generation of the data cache (clearing it changes generations)
        // then request it...
        existingPageRequest = me.pageRequests[options.page];
        if (!existingPageRequest || existingPageRequest.getOperation().pageMapGeneration !== data.pageMapGeneration) {
            // Copy options into a new object so as not to mutate passed in objects
            options = Ext.apply({
                action: 'read',
                filters: me.getFilters().items,
                sorters: me.getSorters().items,
                grouper: me.getGrouper(),
                internalCallback: me.onProxyPrefetch,
                internalScope: me
            }, options);
            operation = me.createOperation('read', options);
            // Generation # of the page map to which the requested records belong.
            // If page map is cleared while this request is in flight, the pageMapGeneration
            // will increment and the payload will be rejected
            operation.pageMapGeneration = data.pageMapGeneration;
            if (me.fireEvent('beforeprefetch', me, operation) !== false) {
                me.pageRequests[options.page] = operation.execute();
                if (me.getProxy().isSynchronous) {
                    delete me.pageRequests[options.page];
                }
            }
        }
        return me;
    },
    /**
     * @private
     * Cancels all pending prefetch requests.
     *
     * This is called when the page map is cleared.
     *
     * Any requests which still make it through will be for the previous pageMapGeneration
     * (pageMapGeneration is incremented upon clear), and so will be rejected upon arrival.
     */
    onPageMapClear: function() {
        var me = this,
            loadingFlag = me.wasLoading,
            reqs = me.pageRequests,
            data = me.getData(),
            page;
        // If any requests return, we no longer respond to them.
        data.clearListeners();
        // replace the listeners we need.
        data.on('clear', me.onPageMapClear, me);
        me.relayEvents(data, [
            'beforepageremove',
            'pageadd',
            'pageremove'
        ]);
        // If the page cache gets cleared it's because a full reload is in progress.
        // Setting the loading flag prevents linked Views from displaying the empty text
        // during a load... we don't know whether their dataset is empty or not.
        me.loading = true;
        me.totalCount = 0;
        // Abort all outstanding requests.
        // onProxyPrefetch will reject them as being for the previous data generation
        // anyway, if they do return.
        // because of the pageMapGeneration mismatch.
        for (page in reqs) {
            if (reqs.hasOwnProperty(page)) {
                reqs[page].getOperation().abort();
            }
        }
        // This will update any views. 
        me.fireEvent('clear', me);
        // Restore loading flag. The beforeload event could still veto the process.
        // The flag does not get set for real until we pass the beforeload event.
        me.loading = loadingFlag;
    },
    /**
     * Prefetches a page of data.
     * @param {Number} page The page to prefetch
     * @param {Object} options (Optional) config object, passed into the
     * Ext.data.operation.Operation object before loading.
     * See {@link #method-load}
     */
    prefetchPage: function(page, options) {
        var me = this,
            pageSize = me.getPageSize(),
            start = (page - 1) * pageSize,
            total = me.totalCount;
        // No more data to prefetch.
        if (total !== undefined && me.data.getCount() === total) {
            return;
        }
        // Copy options into a new object so as not to mutate passed in objects
        me.prefetch(Ext.applyIf({
            page: page,
            start: start,
            limit: pageSize
        }, options));
    },
    /**
     * Called after the configured proxy completes a prefetch operation.
     * @private
     * @param {Ext.data.operation.Operation} operation The operation that completed
     */
    onProxyPrefetch: function(operation) {
        if (this.destroying || this.destroyed) {
            return;
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            resultSet = operation.getResultSet(),
            records = operation.getRecords(),
            successful = operation.wasSuccessful(),
            page = operation.getPage(),
            waitForReload = operation.waitForReload,
            oldTotal = me.totalCount,
            requests = me.pageRequests,
            key, op;
        // Only cache the data if the operation was invoked for the current pageMapGeneration.
        // If the pageMapGeneration has changed since the request was fired off, it will have been
        // cancelled.
        if (operation.pageMapGeneration === me.getData().pageMapGeneration) {
            if (resultSet) {
                me.totalCount = resultSet.getTotal();
                if (me.totalCount !== oldTotal) {
                    me.fireEvent('totalcountchange', me.totalCount);
                }
            }
            // Remove the loaded page from the outstanding pages hash
            if (page !== undefined) {
                delete me.pageRequests[page];
            }
            // Prefetch is broadcast before the page is cached
            me.loading = false;
            me.fireEvent('prefetch', me, records, successful, operation);
            // Add the page into the page map.
            // pageadd event may trigger the onRangeAvailable
            if (successful) {
                if (me.totalCount === 0) {
                    if (waitForReload) {
                        for (key in requests) {
                            op = requests[key].getOperation();
                            // Created in the same batch, clear the waitForReload so this
                            // won't be run again
                            if (op.waitForReload === waitForReload) {
                                delete op.waitForReload;
                            }
                        }
                        me.getData().un('pageadd', waitForReload);
                        me.fireEvent('refresh', me);
                        me.fireEvent('load', me, [], true);
                    }
                } else {
                    me.cachePage(records, operation.getPage());
                }
            }
            // this is a callback that would have been passed to the 'read' function and is optional
            Ext.callback(operation.getCallback(), operation.getScope() || me, [
                records,
                operation,
                successful
            ]);
        }
    },
    /**
     * Caches the records in the prefetch and stripes them with their server-side
     * index.
     * @private
     * @param {Ext.data.Model[]} records The records to cache
     * @param {Ext.data.operation.Operation} page The associated operation
     */
    cachePage: function(records, page) {
        var me = this,
            len = records.length,
            i;
        if (!Ext.isDefined(me.totalCount)) {
            me.totalCount = records.length;
            me.fireEvent('totalcountchange', me.totalCount);
        }
        // Add the fetched page into the pageCache
        for (i = 0; i < len; i++) {
            records[i].join(me);
        }
        me.getData().addPage(page, records);
    },
    /**
     * Determines if the passed range is available in the page cache.
     * @private
     * @param {Number} start The start index
     * @param {Number} end The end index in the range
     * @param {Boolean} [forRender] (private) Passed by the BufferedRenderer to
     * indicate that it's going to need extra rows to peek at to determine
     * group start/end status for the rendered block.
     */
    rangeCached: function(start, end, forRender) {
        var requiredStart = start,
            requiredEnd = end;
        // If this is for getting data to render, we must wait for a slightly wider range
        // to be cached. This is to allow grouping features to peek at the two surrounding records
        // when rendering a *range* of records to see whether the start of the range
        // really is a group start and the end of the range really is a group end.
        if (forRender !== false) {
            requiredStart = start === 0 ? 0 : start - 1;
            requiredEnd = end === this.totalCount - 1 ? end : end + 1;
        }
        return this.getData().hasRange(requiredStart, requiredEnd);
    },
    /**
     * Determines if the passed page is available in the page cache.
     * @private
     * @param {Number} page The page to find in the page cache.
     */
    pageCached: function(page) {
        return this.getData().hasPage(page);
    },
    /**
     * Determines if a request for a page is currently running
     * @private
     * @param {Number} page The page to check for
     */
    pagePending: function(page) {
        return !!this.pageRequests[page];
    },
    /**
     * Determines if the passed range is available in the page cache.
     * @private
     * @deprecated 4.1.0 use {@link #rangeCached} instead
     * @param {Number} start The start index
     * @param {Number} end The end index in the range
     * @return {Boolean}
     */
    rangeSatisfied: function(start, end) {
        return this.rangeCached(start, end);
    },
    /**
     * Handles the availability of a requested range that was not previously available
     * @private
     */
    onRangeAvailable: function(options) {
        var me = this,
            totalCount = me.getTotalCount(),
            start = options.prefetchStart,
            end = (options.prefetchEnd > totalCount - 1) ? totalCount - 1 : options.prefetchEnd,
            range;
        end = Math.max(0, end);
        if (start > end) {
            Ext.log({
                level: 'warn',
                msg: 'Start (' + start + ') was greater than end (' + end + ') for the range of records requested (' + start + '-' + options.prefetchEnd + ')' + (this.storeId ? ' from store "' + this.storeId + '"' : '')
            });
        }
        range = me.getData().getRange(start, end + 1);
        if (options.fireEvent !== false) {
            me.fireEvent('guaranteedrange', range, start, end, options);
        }
        if (options.callback) {
            options.callback.call(options.scope || me, range, start, end, options);
        }
    },
    /**
     * Guarantee a specific range, this will load the store with a range (that
     * must be the `pageSize` or smaller) and take care of any loading that may
     * be necessary.
     * @param {Number} start The starting index. Defaults to zero.
     * @param {Number} end The ending index. Defaults to the last record. The end index
     * **is included**.
     * @param {Function} callback Function to call with the results
     * @param {Object} [scope] The `this` pointer for the `callback`.
     * @param {Object} [options] (private)
     * @deprecated 4.2 Use {@link #getRange}
     */
    guaranteeRange: function(start, end, callback, scope, options) {
        options = Ext.apply({
            callback: callback,
            scope: scope
        }, options);
        this.getRange(start, end + 1, options);
    },
    /**
     * Ensures that the specified range of rows is present in the cache.
     *
     * Converts the row range to a page range and then only load pages which are not already
     * present in the page cache.
     */
    prefetchRange: function(start, end) {
        var me = this,
            startPage, endPage, page,
            data = me.getData();
        if (!me.rangeCached(start, end)) {
            startPage = me.getPageFromRecordIndex(start);
            endPage = me.getPageFromRecordIndex(end);
            // Ensure that the page cache's max size is correct.
            // Our purgePageCount is the number of additional pages *outside of the required range*
            // which may be kept in the cache. A purgePageCount of zero means unlimited.
            data.setMaxSize(me.calculatePageCacheSize(end - start + 1));
            // We have the range, but ensure that we have a "buffer" of pages around it.
            for (page = startPage; page <= endPage; page++) {
                if (!me.pageCached(page)) {
                    me.prefetchPage(page);
                }
            }
        }
    },
    primeCache: function(start, end, direction) {
        var me = this,
            leadingBufferZone = me.getLeadingBufferZone(),
            trailingBufferZone = me.getTrailingBufferZone(),
            pageSize = me.getPageSize(),
            totalCount = me.totalCount;
        // Scrolling up
        if (direction === -1) {
            start = Math.max(start - leadingBufferZone, 0);
            end = Math.min(end + trailingBufferZone, totalCount - 1);
        }
        // Scrolling down
        else if (direction === 1) {
            start = Math.max(Math.min(start - trailingBufferZone, totalCount - pageSize), 0);
            end = Math.min(end + leadingBufferZone, totalCount - 1);
        } else // Teleporting
        {
            /* eslint-disable max-len */
            start = Math.min(Math.max(Math.floor(start - ((leadingBufferZone + trailingBufferZone) / 2)), 0), totalCount - me.pageSize);
            end = Math.min(Math.max(Math.ceil(end + ((leadingBufferZone + trailingBufferZone) / 2)), 0), totalCount - 1);
        }
        /* eslint-enable max-len */
        me.prefetchRange(start, end);
    },
    sort: function(field, direction, mode) {
        if (arguments.length === 0) {
            this.clearAndLoad();
        } else {
            this.getSorters().addSort(field, direction, mode);
        }
    },
    onSorterEndUpdate: function() {
        var me = this,
            sorters = me.getSorters().getRange();
        // Only load or sort if there are sorters
        if (sorters.length) {
            me.fireEvent('beforesort', me, sorters);
            me.clearAndLoad({
                callback: function() {
                    me.fireEvent('sort', me, sorters);
                }
            });
        } else {
            // Sort event must fire when sorters collection is updated to empty.
            me.fireEvent('sort', me, sorters);
        }
    },
    clearAndLoad: function(options) {
        var me = this;
        me.clearing = true;
        me.getData().clear();
        me.clearing = false;
        me.loadPage(1, options);
    },
    privates: {
        isLast: function(record) {
            return this.indexOf(record) === this.getTotalCount() - 1;
        },
        isMoving: function() {
            return false;
        }
    }
});

/**
 * This class is used to store data on the client in a specified `storage` (either 'local'
 * or 'session'). Unlike the {@link Ext.data.proxy.LocalStorage localStorage proxy}, this
 * store uses a single `storageKey` to hold the entire contents of the store. This helps
 * reduce key overhead but also requires that all records and field be saved for any change.
 */
Ext.define('Ext.data.ClientStore', {
    extend: 'Ext.data.Store',
    alias: 'store.clientstorage',
    requires: [
        'Ext.data.proxy.Memory'
    ],
    config: {
        /**
         * @cfg {"local"/"session"} storage
         * Specify 'local' to use `localStorage` and 'session' to use `sessionStorage`.
         */
        storage: 'local',
        /**
         * @cfg {String} storageKey (required)
         * The key to use for saving the content of this store.
         */
        storageKey: null
    },
    trackRemoved: false,
    proxy: {
        type: 'memory',
        clearOnRead: true
    },
    sync: function(options) {
        var me = this,
            key = me._getKey(),
            storage = me.getStorage(),
            source = me.getDataSource(),
            proxy = me.getProxy(),
            writer = proxy.getWriter(),
            writeAll = writer.getWriteAllFields(),
            data = [];
        try {
            me.suspendAutoSync();
            writer.setWriteAllFields(true);
            source.each(function(rec) {
                if (rec.phantom) {
                    rec.setId(me.nextId());
                }
                data.push(writer.getRecordData(rec));
                rec.commit();
            });
            if (data.length) {
                data = JSON.stringify(data);
                data = storage.setItem(key, data);
            } else {
                storage.removeItem(key);
            }
            if (options && options.success) {
                Ext.callback(options.success, options.scope || proxy, [
                    null,
                    options
                ]);
            }
        } catch (e) {
            if (options && options.failure) {
                Ext.callback(options.failure, options.scope || proxy, [
                    null,
                    options
                ]);
            }
        } finally {
            me.resumeAutoSync();
            writer.setWriteAllFields(writeAll);
        }
        if (options && options.callback) {
            Ext.callback(options.callback, options.scope || proxy, [
                null,
                options
            ]);
        }
        return me;
    },
    applyStorage: function(storage) {
        var ret = Ext.global[storage + 'Storage'];
        if (!ret || !ret.getItem || !ret.setItem) {
            Ext.raise('Invalid storage config "' + storage + '"; ' + 'expected "local" or "session"');
        }
        return ret;
    },
    updateProxy: function(proxy, oldProxy) {
        var me = this,
            key = me._getKey(),
            storage = me.getStorage(),
            data;
        me.callParent([
            proxy,
            oldProxy
        ]);
        data = storage.getItem(key);
        if (data) {
            proxy.setData(JSON.parse(data));
        }
    },
    privates: {
        _getKey: function() {
            var key = this.getStorageKey();
            if (!key) {
                Ext.raise('ClientStore requires a storageKey');
            }
            return key;
        },
        nextId: function() {
            var source = this.getDataSource(),
                id = 1;
            while (source.containsKey(id)) {
                ++id;
            }
            return id;
        }
    }
});

/**
 * This class is used to send requests to the server using {@link Ext.direct.Manager Ext Direct}.
 * When a request is made, the transport mechanism is handed off to the appropriate
 * {@link Ext.direct.RemotingProvider Provider} to complete the call.
 *
 * # Specifying the functions
 *
 * This proxy expects Direct remoting method to be passed in order to be able to complete requests,
 * one Direct function per CRUD method. This is done via {@link #api} configuration:
 *
 *      api: {
 *          read: 'MyApp.readRecords',
 *          create: 'MyApp.createRecords',
 *          update: 'MyApp.updateRecords',
 *          destroy: 'MyApp.destroyRecords'
 *      }
 *
 * You can also use a `prefix` config to avoid duplicating full namespaces for Direct functions:
 *
 *      api: {
 *          prefix: 'MyApp',
 *          read: 'readRecords',
 *          create: 'createRecords',
 *          update: 'updateRecords',
 *          destroy: 'destroyRecords'
 *      }
 *
 * The preferred way is to specify function names to allow late resolution, however you can
 * pass function references instead if desired:
 *
 *      api: {
 *          read: MyApp.readRecords,
 *          create: MyApp.createRecords,
 *          update: MyApp.updateRecords,
 *          destroy: MyApp.destroyRecords
 *      }
 *
 * This method of configuring API is not recommended because this way the Direct functions
 * need to be created very early in the application lifecycle, long before
 * {@link Ext.app.Application} instance is initialized.
 *
 * You can also use the {@link #directFn} configuration instead of {@link #api}. This will use
 * the same Direct function for all types of requests.
 *
 * # Server API
 *
 * The server side methods are expected to conform to the following calling conventions:
 *
 * ## `read`
 *
 * Accept one argument which is either named arguments in an object (default), or an array
 * of values depending on the {@link #paramsAsHash} configuration. Return an array of records
 * or an object with format recognizable by the configured {@link Ext.data.reader.Reader}
 * instance.
 *
 * Example {@link Ext.direct.RemotingProvider#cfg-actions Direct API declaration}:
 *
 *      actions: {
 *          MyApp: [{
 *              name: 'readRecords',
 *              params: [],
 *              strict: false
 *          }]
 *      }
 *
 * Example function invocation:
 *
 *      MyApp.readRecords(
 *          {
 *              start: 0,
 *              limit: 10
 *          },
 *          // Results are passed to the callback function
 *          function(records) {
 *              console.log(records);
 *              // Logs:  [{ id: 'r0', text: 'foo' }, { id: 'r1', text: 'bar' }]
 *          }
 *      );
 *
 * ## `create`
 *
 * Accept one ordered argument which is either an object with data for the new record,
 * or an array of objects for multiple records. Return an array of identifiers for actually
 * created records. See {@link Ext.data.Model#clientIdProperty} for more information.
 *
 * Example {@link Ext.direct.RemotingProvider#cfg-actions Direct API declaration}:
 *
 *      actions: [
 *          MyApp: [{
 *              name: 'createRecords',
 *              len: 1
 *          }]
 *      }
 *
 * Example function invocation:
 *
 *      MyApp.createRecords(
 *          [
 *              { id: 0, text: 'foo' },
 *              { id: 1, text: 'bar' }
 *          ],
 *          // Results are passed to the callback function
 *          function(records) {
 *              console.log(records);
 *              // Logs: [{ clientId: 0, id: 'r0' }, { clientId: 1, id: 'r1' }]
 *          }
 *      );
 *
 * ## `update`
 *
 * Accept one ordered argument which is either an object with updated data and valid
 * record identifier, or an array of objects for multiple records. Return an array of
 * objects with updated record data.
 *
 * Example {@link Ext.direct.RemotingProvider#cfg-actions Direct API declaration}:
 *
 *      actions: [
 *          MyApp: [{
 *              name: 'updateRecords',
 *              len: 1
 *          }]
 *      }
 *
 * Example function invocation:
 *
 *      MyApp.updateRecords(
 *          [
 *              { id: 'r0', text: 'blerg' },
 *              { id: 'r1', text: 'throbbe' }
 *          ],
 *          // Results are passed to the callback function
 *          function(records) {
 *              console.log(records);
 *              // Logs: [{ id: 'r0', text: 'blerg' }, { id: 'r1', text: 'throbbe }]
 *          }
 *      );
 *
 * ## `destroy`
 *
 * Accept one ordered argument which is an array of record identifiers to be deleted.
 * Return an object with at least one {@link Ext.data.reader.Json#successProperty}
 * property set to `true` or `false`, with more optional properties recognizable by configured
 * {@link Ext.data.reader.Reader} instance.
 *
 * Example {@link Ext.direct.RemotingProvider#cfg-actions Direct API declaration}:
 *
 *      actions: [
 *          MyApp: [{
 *              name: 'destroyRecords',
 *              len: 1
 *          }]
 *      }
 *
 * Example function invocation:
 *
 *      MyApp.destroyRecords(
 *          [
 *              { id: 'r0' },
 *              { id: 'r1' }
 *          ],
 *          // Results are passed to the callback function
 *          function(result) {
 *              // Default successProperty is `success`
 *              if (!result.success) {
 *                  // Handle the error
 *              }
 *          }
 *      );
 *
 * ## Read method parameters
 *
 * Direct proxy provides options to help configure which parameters will be sent to the server
 * for Read operations. By setting the {@link #paramsAsHash} option to `true`, the proxy will
 * send an object literal containing each of the passed parameters. This is the default. When
 * {@link #paramsAsHash} is set to `false`, Proxy will pass the Read function an array of values
 * instead of an object, with the order determined by {@link #paramOrder} value.
 *
 * Setting {@link #paramOrder} to any value other than `undefined` will automatically reset
 * {@link #paramsAsHash} to `false`.
 *
 * # Example Usage
 *
 *      Ext.define('User', {
 *          extend: 'Ext.data.Model',
 *          fields: ['firstName', 'lastName']
 *      });
 *      
 *      Ext.define('Users', {
 *          extend: 'Ext.data.Store',
 *          model: 'User',
 *          proxy: {
 *              type: 'direct',
 *              directFn: 'MyApp.getUsers',
 *              // Tells the proxy to pass `start` and `limit` as two by-position arguments:
 *              paramOrder: 'start,limit'
 *          }
 *      });
 *      
 *      var store = new Users();
 *      store.load();
 */
Ext.define('Ext.data.proxy.Direct', {
    /* Begin Definitions */
    extend: 'Ext.data.proxy.Server',
    alternateClassName: 'Ext.data.DirectProxy',
    alias: 'proxy.direct',
    requires: [
        'Ext.direct.Manager'
    ],
    /* End Definitions */
    /**
     * @cfg url
     * @hide
     */
    config: {
        /**
         * @cfg {String/String[]} paramOrder
         * A list of params to be passed to server side Read function. Specify the params
         * in the order in which they must be executed on the server-side as either (1)
         * an Array of String values, or (2) a String of params delimited by either
         * whitespace, comma, or pipe. For example, any of the following would be
         * acceptable:
         *
         *     paramOrder: ['param1','param2','param3']
         *     paramOrder: 'param1 param2 param3'
         *     paramOrder: 'param1,param2,param3'
         *     paramOrder: 'param1|param2|param'
         */
        paramOrder: undefined,
        /**
         * @cfg {Boolean} paramsAsHash
         * Send Read function parameters as a collection of named arguments. Providing a
         * {@link #paramOrder} nullifies this configuration.
         */
        paramsAsHash: true,
        /**
         * @cfg {Function/String} directFn
         * Function to call when executing a request. `directFn` is a simple alternative
         * to defining the api configuration parameter for Stores which will not
         * implement a full CRUD api. The `directFn` may also be a string reference to
         * the fully qualified name of the function, for example:
         * `'MyApp.company.GetProfile'`. This can be useful when using dynamic loading.
         * The string will be resolved before calling the function for the first time.
         */
        directFn: undefined,
        /**
         * @cfg {Object} api
         * The same as {@link Ext.data.proxy.Server#api}, however instead of providing
         * urls you should provide a Direct function name for each CRUD method.
         *
         * Instead of providing fully qualified names for each function, you can use
         * `prefix` property to provide a common prefix for all functions:
         *
         *   api: {
         *       prefix: 'MyApp',
         *       read: 'readRecords',
         *       create: 'createRecords',
         *       update: 'updateRecords',
         *       destroy: 'destroyRecords'
         *   }
         *
         * This way function names will be resolved to `'MyApp.readRecords'`, 
         * `'MyApp.createRecords'`, etc. Note that using `prefix` and fully qualified
         * function names is **not** supported, and prefix will be used for every
         * function name when configured.
         *
         * See also {@link #directFn}.
         */
        api: undefined,
        /**
         * @cfg {Object/Array} metadata
         * Optional set of fixed parameters to send with every Proxy request, similar to
         * {@link #extraParams} but available with all CRUD requests. Also unlike
         * {@link #extraParams}, metadata is not mixed with the ordinary data but sent
         * separately in the data packet.
         * You may need to update your server side Ext Direct stack to use this feature.
         */
        metadata: undefined
    },
    /**
     * @private
     */
    paramOrderRe: /[\s,|]/,
    constructor: function(config) {
        this.callParent([
            config
        ]);
        this.canceledOperations = {};
    },
    applyParamOrder: function(paramOrder) {
        if (Ext.isString(paramOrder)) {
            paramOrder = paramOrder.split(this.paramOrderRe);
        }
        return paramOrder;
    },
    updateApi: function() {
        this.methodsResolved = false;
    },
    updateDirectFn: function() {
        this.methodsResolved = false;
    },
    resolveMethods: function() {
        var me = this,
            fn = me.getDirectFn(),
            api = me.getApi(),
            method;
        if (fn) {
            me.setDirectFn(method = Ext.direct.Manager.parseMethod(fn));
            if (!Ext.isFunction(method)) {
                Ext.raise('Cannot resolve directFn ' + fn);
            }
        }
        if (api) {
            api = Ext.direct.Manager.resolveApi(api, me);
            me.setApi(api);
        }
        me.methodsResolved = true;
    },
    doRequest: function(operation) {
        var me = this,
            writer, request, action, params, args, api, fn;
        if (!me.methodsResolved) {
            me.resolveMethods();
        }
        request = me.buildRequest(operation);
        action = request.getAction();
        api = me.getApi();
        if (api) {
            fn = api[action];
        }
        fn = fn || me.getDirectFn();
        if (!fn || !fn.directCfg) {
            Ext.raise({
                msg: 'No Ext Direct function specified for Direct proxy "' + action + '" operation',
                proxy: me
            });
        }
        // This might lead to exceptions so bail out early
        if (!me.paramOrder && fn.directCfg.method.len > 1) {
            Ext.raise({
                msg: 'Incorrect parameters for Direct proxy "' + action + '" operation',
                proxy: me
            });
        }
        writer = me.getWriter();
        if (writer && operation.allowWrite()) {
            request = writer.write(request);
        }
        // The weird construct below is due to historical way of handling extraParams;
        // they were mixed in with request data in ServerProxy.buildRequest() and were
        // inseparable after that point. This does not work well with CUD operations
        // so instead of using potentially poisoned request params we took the raw
        // JSON data as Direct function argument payload (but only for CUD!). A side
        // effect of that was that the request metadata (extraParams) was only available
        // for read operations.
        // We keep this craziness for backwards compatibility.
        if (action === 'read') {
            params = request.getParams();
        } else {
            params = request.getJsonData();
        }
        args = fn.directCfg.method.getArgs({
            params: params,
            allowSingle: writer.getAllowSingle(),
            paramOrder: me.getParamOrder(),
            paramsAsHash: me.getParamsAsHash(),
            paramsAsArray: true,
            metadata: me.getMetadata(),
            callback: me.createRequestCallback(request, operation),
            scope: me
        });
        request.setConfig({
            args: args,
            directFn: fn
        });
        fn.apply(window, args);
        // Store expects us to return something to indicate that the request
        // is pending; not doing so will make a buffered Store repeat the
        // requests over and over.
        return request;
    },
    /**
     * Aborts a running request by operation.
     *
     * @param {Ext.data.operation.Operation} operation The operation to abort. This parameter
     * is mandatory.
     */
    abort: function(operation) {
        var id;
        // Assume this can be called with request instead of operation, a la Ajax proxy
        if (operation && operation.isDataRequest) {
            operation = operation.getOperation();
        }
        // Check definedness again, the above could have returned null
        if (operation && operation.isOperation) {
            id = operation.id;
        }
        // We cannot abort a running request but we can ignore the data when it comes back.
        if (id != null) {
            this.canceledOperations[id] = true;
        }
    },
    /**
     * @method applyEncoding
     * @inheritdoc
     */
    applyEncoding: Ext.identityFn,
    createRequestCallback: function(request, operation) {
        var me = this;
        return function(data, event) {
            if (!me.canceledOperations[operation.id]) {
                me.processResponse(event.status, operation, request, event);
            }
            delete me.canceledOperations[operation.id];
        };
    },
    /**
     * @method extractResponseData
     * @inheritdoc
     */
    extractResponseData: function(response) {
        return Ext.isDefined(response.result) ? response.result : response.data;
    },
    /**
     * @method setException
     * @inheritdoc
     */
    setException: function(operation, response) {
        operation.setException(response.message);
    },
    /**
     * @method buildUrl
     * @inheritdoc
     */
    buildUrl: function() {
        return '';
    }
});

/**
 * Small helper class to create an {@link Ext.data.Store} configured with an
 * {@link Ext.data.proxy.Direct} and {@link Ext.data.reader.Json} to make interacting with an
 * {@link Ext.direct.Manager} server-side {@link Ext.direct.Provider Provider} easier. To create
 * a different proxy/reader combination create a basic {@link Ext.data.Store} configured as needed.
 *
 * **Note:** Although they are not listed, this class inherits all of the config options of:
 *
 * - **{@link Ext.data.Store Store}**
 *
 * - **{@link Ext.data.reader.Json JsonReader}**
 *
 *   - **{@link Ext.data.reader.Json#cfg-rootProperty rootProperty}**
 *   - **{@link Ext.data.reader.Json#totalProperty totalProperty}**
 *
 * - **{@link Ext.data.proxy.Direct DirectProxy}**
 *
 *   - **{@link Ext.data.proxy.Direct#directFn directFn}**
 *   - **{@link Ext.data.proxy.Direct#paramOrder paramOrder}**
 *   - **{@link Ext.data.proxy.Direct#paramsAsHash paramsAsHash}**
 *
 */
Ext.define('Ext.data.DirectStore', {
    /* Begin Definitions */
    extend: 'Ext.data.Store',
    alias: 'store.direct',
    requires: [
        'Ext.data.proxy.Direct'
    ],
    /* End Definitions */
    constructor: function(config) {
        var proxy;
        config = Ext.apply({}, config);
        if (!config.proxy) {
            proxy = {
                type: 'direct',
                reader: {
                    type: 'json'
                }
            };
            /* eslint-disable-next-line max-len */
            Ext.copyTo(proxy, config, 'paramOrder,paramsAsHash,directFn,api,simpleSortMode,extraParams');
            Ext.copyTo(proxy.reader, config, 'totalProperty,root,rootProperty,idProperty');
            config.proxy = proxy;
        }
        this.callParent([
            config
        ]);
    }
});

/**
 * This class is used to create JSONP requests. JSONP is a mechanism that allows for making
 * requests for data cross domain. JSONP is basically a `<script>` node with the source of the url
 * executing a function that was created by Ext.data.JsonP. Once the resource has loaded, the
 * `<script>` node will be destroyed.
 *
 * If you have a request such as:
 *
 *     Ext.data.JsonP.request({
 *         url : 'foo.php'
 *     });
 *
 * Ext.data.JsonP will create a `<script>` node in the `<head>` with the `src` attribute pointing
 * to `foo.php?callback=Ext.data.JsonP.callback1`. The `foo.php` script will have to detect the
 * `callback` URL parameter and return valid JavaScript:
 *
 *     Ext.data.JsonP.callback1({"foo":"bar"});
 *
 * A simple PHP example would look like:
 *
 *     <?php
 *
 *     $data = array('foo' => 'bar');
 *
 *     if (!empty($_REQUEST['callback'])) {
 *         header('Content-Type: application/javascript');
 *         echo $_REQUEST['callback'] . '(';
 *     }
 *
 *     echo json_encode($data);
 *
 *     if (!empty($_REQUEST['callback']) {
 *         echo ');';
 *     }
 *
 *     ?>
 *
 * More information is available <a href="http://en.wikipedia.org/wiki/JSONP">here</a>. You can
 * also use <a href="http://www.jsonplint.com">JSONPLint</a> to test your JSONP.
 */
Ext.define('Ext.data.JsonP', {
    singleton: true,
    /**
     * Number of requests done so far.
     * @private
     */
    requestCount: 0,
    /**
     * Hash of pending requests.
     * @private
     */
    requests: {},
    /**
     * @property timeout
     * @type Number
     * A default timeout for any JsonP requests. If the request has not completed in this time the
     * failure callback will be fired. The timeout is in ms. Defaults to <tt>30000</tt>.
     */
    timeout: 30000,
    /**
     * @property disableCaching
     * @type Boolean
     * True to add a unique cache-buster param to requests. Defaults to <tt>true</tt>.
     */
    disableCaching: true,
    /**
     * @property disableCachingParam
     * @type String
     * Change the parameter which is sent went disabling caching through a cache buster. Defaults
     * to <tt>'_dc'</tt>.
     */
    disableCachingParam: '_dc',
    /**
     * @property callbackKey
     * @type String
     * Specifies the GET parameter that will be sent to the server containing the function name to
     * be executed when the request completes. Defaults to <tt>callback</tt>. Thus, a common
     * request will be in the form of url?callback=Ext.data.JsonP.callback1
     */
    callbackKey: 'callback',
    /**
     * Makes a JSONP request.
     * @param {Object} options An object which may contain the following properties. Note that
     * options will take priority over any defaults that are specified in the class.
     * @param {String} options.url The URL to request.
     * @param {Object} options.params (optional) An object containing a series of key value pairs
     * that will be sent along with the request.
     * @param {Number} options.timeout (optional) See {@link #timeout}
     * @param {String} options.callbackKey (optional) See {@link #callbackKey}
     * @param {String} options.callbackName (optional) The function name to use for this request. By
     * default this name will be auto-generated: Ext.data.JsonP.callback1, Ext.data.JsonP.callback2,
     * etc. Setting this option to "my_name" will force the function name to be
     * Ext.data.JsonP.my_name. Use this if you want deterministic behavior, but be careful - the
     * callbackName should be different in each JsonP request that you make.
     * @param {Boolean} options.disableCaching (optional) See {@link #disableCaching}
     * @param {String} options.disableCachingParam (optional) See {@link #disableCachingParam}
     * @param {Function} options.success (optional) A function to execute if the request succeeds.
     * @param {Function} options.failure (optional) A function to execute if the request fails.
     * @param {Function} options.callback (optional) A function to execute when the request
     * completes, whether it is a success or failure.
     * @param {Object} options.scope (optional) The scope in which to execute the callbacks:
     * The "this" object for the callback function. Defaults to the browser window.
     *
     * @return {Object} request An object containing the request details.
     */
    request: function(options) {
        options = Ext.apply({}, options);
        if (!options.url) {
            Ext.raise('A url must be specified for a JSONP request.');
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            /* eslint-disable-next-line max-len */
            disableCaching = Ext.isDefined(options.disableCaching) ? options.disableCaching : me.disableCaching,
            cacheParam = options.disableCachingParam || me.disableCachingParam,
            id = ++me.requestCount,
            callbackName = options.callbackName || 'callback' + id,
            callbackKey = options.callbackKey || me.callbackKey,
            timeout = Ext.isDefined(options.timeout) ? options.timeout : me.timeout,
            params = Ext.apply({}, options.params),
            url = options.url,
            name = Ext.name,
            request, script;
        // Add cachebuster param unless it has already been done
        if (disableCaching && !params[cacheParam]) {
            params[cacheParam] = Ext.Date.now();
        }
        options.params = params;
        params[callbackKey] = name + '.data.JsonP.' + callbackName;
        script = me.createScript(url, params, options);
        me.requests[id] = request = {
            url: url,
            params: params,
            script: script,
            id: id,
            scope: options.scope,
            success: options.success,
            failure: options.failure,
            callback: options.callback,
            callbackKey: callbackKey,
            callbackName: callbackName
        };
        if (timeout > 0) {
            request.timeout = Ext.defer(me.handleTimeout, timeout, me, [
                request
            ]);
        }
        me.setupErrorHandling(request);
        me[callbackName] = me.bindResponse(request);
        me.loadScript(request);
        return request;
    },
    bindResponse: function(request) {
        var me = this;
        return function(result) {
            Ext.elevate(function() {
                me.handleResponse(result, request);
            });
        };
    },
    /**
     * Abort a request. If the request parameter is not specified all open requests will
     * be aborted.
     * @param {Object/String} request (Optional) The request to abort
     */
    abort: function(request) {
        var me = this,
            requests = me.requests,
            key;
        if (request) {
            if (!request.id) {
                request = requests[request];
            }
            me.handleAbort(request);
        } else {
            for (key in requests) {
                if (requests.hasOwnProperty(key)) {
                    me.abort(requests[key]);
                }
            }
        }
    },
    /**
     * Sets up error handling for the script
     * @private
     * @param {Object} request The request
     */
    setupErrorHandling: function(request) {
        request.script.onerror = Ext.bind(this.handleError, this, [
            request
        ]);
    },
    /**
     * Handles any aborts when loading the script
     * @private
     * @param {Object} request The request
     */
    handleAbort: function(request) {
        request.errorType = 'abort';
        this.handleResponse(null, request);
    },
    /**
     * Handles any script errors when loading the script
     * @private
     * @param {Object} request The request
     */
    handleError: function(request) {
        request.errorType = 'error';
        this.handleResponse(null, request);
    },
    /**
     * Cleans up anu script handling errors
     * @private
     * @param {Object} request The request
     */
    cleanupErrorHandling: function(request) {
        request.script.onerror = null;
    },
    /**
     * Handle any script timeouts
     * @private
     * @param {Object} request The request
     */
    handleTimeout: function(request) {
        request.errorType = 'timeout';
        this.handleResponse(null, request);
    },
    /**
     * Handle a successful response
     * @private
     * @param {Object} result The result from the request
     * @param {Object} request The request
     */
    handleResponse: function(result, request) {
        var success = true;
        Ext.undefer(request.timeout);
        delete this[request.callbackName];
        delete this.requests[request.id];
        this.cleanupErrorHandling(request);
        Ext.fly(request.script).destroy();
        if (request.errorType) {
            success = false;
            Ext.callback(request.failure, request.scope, [
                request.errorType
            ]);
        } else {
            Ext.callback(request.success, request.scope, [
                result
            ]);
        }
        Ext.callback(request.callback, request.scope, [
            success,
            result,
            request.errorType
        ]);
    },
    /**
     * Create the script tag given the specified url, params and options. The options
     * parameter is passed to allow an override to access it.
     * @private
     * @param {String} url The url of the request
     * @param {Object} params Any extra params to be sent
     * @param {Object} options The object passed to {@link #request}.
     */
    createScript: function(url, params, options) {
        var script = document.createElement('script');
        script.setAttribute("src", Ext.urlAppend(url, Ext.Object.toQueryString(params)));
        script.setAttribute("async", true);
        script.setAttribute("type", "text/javascript");
        return script;
    },
    /**
     * Loads the script for the given request by appending it to the HEAD element. This is
     * its own method so that users can override it (as well as {@link #createScript}).
     * @private
     * @param request The request object.
     */
    loadScript: function(request) {
        Ext.getHead().appendChild(request.script);
    }
});

/**
 * The JsonP proxy is useful when you need to load data from a domain other than the one your
 * application is running on. If your application is running on http://domainA.com it cannot use
 * {@link Ext.data.proxy.Ajax Ajax} to load its data from http://domainB.com because cross-domain
 * Ajax requests are prohibited by the browser.
 *
 * We can get around this using a JsonP proxy. JsonP proxy injects a `<script>` tag into the DOM
 * whenever an AJAX request would usually be made. Let's say we want to load data from
 * http://domainB.com/users - the script tag that would be injected might look like this:
 *
 *     <script src="http://domainB.com/users?callback=someCallback"></script>
 *
 * When we inject the tag above, the browser makes a request to that url and includes the response
 * as if it was any other type of JavaScript include. By passing a callback in the url above,
 * we're telling domainB's server that we want to be notified when the result comes in and that it
 * should call our callback function with the data it sends back. So long as the server formats
 * the response to look like this, everything will work:
 *
 *     someCallback({
 *         users: [
 *             {
 *                 id: 1,
 *                 name: "Ed Spencer",
 *                 email: "ed@sencha.com"
 *             }
 *         ]
 *     });
 *
 * As soon as the script finishes loading, the 'someCallback' function that we passed in the url
 * is called with the JSON object that the server returned.
 *
 * JsonP proxy takes care of all of this automatically. It formats the url you pass, adding
 * the callback parameter automatically. It even creates a temporary callback function, waits for it
 * to be called and then puts the data into the Proxy making it look just like you loaded it
 * through a normal {@link Ext.data.proxy.Ajax AjaxProxy}. Here's how we might set that up:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name', 'email']
 *     });
 *
 *     var store = Ext.create('Ext.data.Store', {
 *         model: 'User',
 *         proxy: {
 *             type: 'jsonp',
 *             url: 'http://domainB.com/users'
 *         }
 *     });
 *
 *     store.load();
 *
 * That's all we need to do - JsonP proxy takes care of the rest. In this case the Proxy will have
 * injected a script tag like this:
 *
 *     <script src="http://domainB.com/users?callback=callback1"></script>
 *
 * # Customization
 *
 * This script tag can be customized using the {@link #callbackKey} configuration. For example:
 *
 *     var store = Ext.create('Ext.data.Store', {
 *         model: 'User',
 *         proxy: {
 *             type: 'jsonp',
 *             url : 'http://domainB.com/users',
 *             callbackKey: 'theCallbackFunction'
 *         }
 *     });
 *
 *     store.load();
 *
 * Would inject a script tag like this:
 *
 *     <script src="http://domainB.com/users?theCallbackFunction=callback1"></script>
 *
 * # Implementing on the server side
 *
 * The remote server side needs to be configured to return data in this format. Here are suggestions
 * for how you might achieve this using Java, PHP and ASP.net:
 *
 * Java:
 *
 *     boolean jsonP = false;
 *     String cb = request.getParameter("callback");
 *     if (cb != null) {
 *         jsonP = true;
 *         response.setContentType("text/javascript");
 *     } else {
 *         response.setContentType("application/x-json");
 *     }
 *     Writer out = response.getWriter();
 *     if (jsonP) {
 *         out.write(cb + "(");
 *     }
 *     out.print(dataBlock.toJsonString());
 *     if (jsonP) {
 *         out.write(");");
 *     }
 *
 * PHP:
 *
 *     $callback = $_REQUEST['callback'];
 *
 *     // Create the output object.
 *     $output = array('a' => 'Apple', 'b' => 'Banana');
 *
 *     //start output
 *     if ($callback) {
 *         header('Content-Type: text/javascript');
 *         echo $callback . '(' . json_encode($output) . ');';
 *     } else {
 *         header('Content-Type: application/x-json');
 *         echo json_encode($output);
 *     }
 *
 * ASP.net:
 *
 *     String jsonString = "{"success": true}";
 *     String cb = Request.Params.Get("callback");
 *     String responseString = "";
 *     if (!String.IsNullOrEmpty(cb)) {
 *         responseString = cb + "(" + jsonString + ")";
 *     } else {
 *         responseString = jsonString;
 *     }
 *     Response.Write(responseString);
 */
Ext.define('Ext.data.proxy.JsonP', {
    extend: 'Ext.data.proxy.Server',
    alternateClassName: 'Ext.data.ScriptTagProxy',
    alias: [
        'proxy.jsonp',
        'proxy.scripttag'
    ],
    requires: [
        'Ext.data.JsonP'
    ],
    config: {
        /**
         * @cfg {String} callbackKey
         * See {@link Ext.data.JsonP#callbackKey}.
         */
        callbackKey: 'callback',
        /**
        * @cfg {String} [recordParam]
        * The HTTP parameter name to use when passing records to the server and the
        * {@link #writer Json writer} is not configured to
        * {@link Ext.data.writer.Json#encode encode} records into a parameter.
        * 
        * The {@link #encodeRecords} method is used to encode the records to create this parameter's
        * value.
        */
        recordParam: 'records',
        /**
        * @cfg {Boolean} autoAppendParams
        * True to automatically append the request's params to the generated url. Defaults to true
        */
        autoAppendParams: true
    },
    /**
     * @private
     * Performs the read request to the remote domain. JsonP proxy does not actually create an Ajax
     * request, instead we write out a `<script>` tag based on the configuration of the internal
     * Ext.data.Request object
     * @param {Ext.data.operation.Operation} operation The
     * {@link Ext.data.operation.Operation Operation} object to execute
     * @param {Function} operation.callback A callback function to execute when the Operation has
     * been completed
     * @param {Object} operation.scope The scope to execute the callback in
     */
    doRequest: function(operation) {
        // generate the unique IDs for this request
        var me = this,
            request = me.buildRequest(operation),
            params = request.getParams();
        // apply JsonP proxy-specific attributes to the Request
        request.setConfig({
            callbackKey: me.callbackKey,
            timeout: me.timeout,
            scope: me,
            disableCaching: false,
            // handled by the proxy
            callback: me.createRequestCallback(request, operation)
        });
        // If we are responsible for appending the params to the URL, clear them now so that
        // The Ext.data.JsonP singleton does not append them.
        if (me.getAutoAppendParams()) {
            request.setParams({});
        }
        request.setRawRequest(Ext.data.JsonP.request(request.getCurrentConfig()));
        // Set the params back once we have made the request though
        request.setParams(params);
        me.lastRequest = request;
        return request;
    },
    /**
     * @private
     * Creates and returns the function that is called when the request has completed. The returned
     * function should accept a Response object, which contains the response to be read by the
     * configured Reader. The third argument is the callback that should be called after the
     * request has been completed and the Reader has decoded the response. This callback will
     * typically be the callback passed by a store, e.g. in
     * proxy.read(operation, theCallback, scope) theCallback refers to the callback argument
     * received by this function.
     * See {@link #doRequest} for details.
     * @param {Ext.data.Request} request The Request object
     * @param {Ext.data.operation.Operation} operation The Operation being executed
     * @param {Function} operation.callback The callback function to be called when the request
     * completes. This is usually the callback passed to doRequest
     * @param {Object} operation.scope The scope in which to execute the callback function
     * @return {Function} The callback function
     */
    createRequestCallback: function(request, operation) {
        var me = this;
        return function(success, response, errorType) {
            if (request === me.lastRequest) {
                me.lastRequest = null;
            }
            me.processResponse(success, operation, request, response);
        };
    },
    setException: function(operation, response) {
        operation.setException(operation.getRequest().getRawRequest().errorType);
    },
    /**
     * Generates a url based on a given Ext.data.Request object. Adds the params and callback
     * function name to the url
     * @param {Ext.data.Request} request The request object
     * @return {String} The url
     */
    buildUrl: function(request) {
        var me = this,
            url = me.callParent(arguments),
            records = request.getRecords(),
            writer = me.getWriter(),
            params, filters, filter, i, v;
        // In the JsonP proxy, params may only go into the URL.
        // So params created by the Writer get applied to the request's params here
        if (writer && request.getOperation().allowWrite()) {
            request = writer.write(request);
        }
        // Encode filters into the URL via params
        params = request.getParams();
        filters = params.filters;
        delete params.filters;
        if (filters && filters.length) {
            for (i = 0; i < filters.length; i++) {
                filter = filters[i];
                v = filter.getValue();
                if (v) {
                    params[filter.getProperty()] = v;
                }
            }
        }
        // If there's no writer, or the writer is not configured to encode the records into
        // a parameter, then we have to do it here.
        if (Ext.isArray(records) && records.length > 0 && (!writer || !writer.getEncode())) {
            params[me.getRecordParam()] = me.encodeRecords(records);
        }
        // If we are responsible for appending the params to the URL, do it now.
        // The params are cleared in doRequest so that the Ext.data.JsonP singleton does not
        // add them.
        if (me.getAutoAppendParams()) {
            url = Ext.urlAppend(url, Ext.Object.toQueryString(params));
        }
        return url;
    },
    /**
     * Aborts a server request. If no request is passed, the most recent request
     * will be aborted.
     * @param {Ext.data.Request} [request] The request to abort.
     */
    abort: function(request) {
        request = request || this.lastRequest;
        if (request) {
            Ext.data.JsonP.abort(request.getRawRequest());
        }
    },
    /**
     * Encodes an array of records into a value suitable to be added to the request `params`
     * as the {@link #recordParam} parameter. This is broken out into its own function so that
     * it can be easily overridden.
     * 
     * The default implementation 
     * @param {Ext.data.Model[]} records The records array
     * @return {Array} An array of record data objects
     */
    encodeRecords: function(records) {
        var encoded = [],
            i = 0,
            len = records.length;
        for (; i < len; i++) {
            encoded.push(Ext.encode(records[i].getData()));
        }
        return encoded;
    }
});

/**
 * Small helper class to make creating {@link Ext.data.Store}s from different domain JSON
 * data easier.
 *
 * A JsonPStore will be automatically configured with a {@link Ext.data.reader.Json} and a
 * {@link Ext.data.proxy.JsonP JsonPProxy}.
 *
 * A store configuration would be something like:
 *
 *     var store = new Ext.data.JsonPStore({
 *         // store configs
 *         storeId: 'myStore',
 *
 *         // proxy configs
 *         url: 'get-images.php',
 *
 *         // reader configs
 *         root: 'images',
 *         fields: ['name', 'url', {name: 'size', type: 'float'}, {name: 'lastmod', type: 'date'}]
 *     });
 *
 *
 * This store is configured to consume a returned object of the form:
 *
 *     stcCallback({
 *         images: [{
 *             name: 'Image one',
 *             url: '/GetImage.php?id=1',
 *             size: 46.5,
 *             lastmod: new Date(2007, 10, 29)
 *         }, {
 *             name: 'Image Two',
 *             url: '/GetImage.php?id=2',
 *             size: 43.2,
 *             lastmod: new Date(2007, 10, 30)
 *         }]
 *     })
 *
 * Where stcCallback is the callback name passed in the request to the remote domain. See
 * {@link Ext.data.proxy.JsonP JsonPProxy} for details of how this works.
 *
 * An object literal of this form could also be used as the {@link #cfg-data} config option.
 */
Ext.define('Ext.data.JsonPStore', {
    extend: 'Ext.data.Store',
    alias: 'store.jsonp',
    requires: [
        'Ext.data.proxy.JsonP',
        'Ext.data.reader.Json'
    ],
    constructor: function(config) {
        config = Ext.apply({
            proxy: {
                type: 'jsonp',
                reader: 'json'
            }
        }, config);
        this.callParent([
            config
        ]);
    }
});

/**
 * Small helper class to make creating {@link Ext.data.Store}s from JSON data easier.
 * A JsonStore will be automatically configured with a {@link Ext.data.reader.Json}.
 *
 * A store configuration would be something like:
 *
 *     var store = new Ext.data.JsonStore({
 *         // store configs
 *         storeId: 'myStore',
 *
 *         proxy: {
 *             type: 'ajax',
 *             url: 'get-images.php',
 *             reader: {
 *                 type: 'json',
 *                 rootProperty: 'images'
 *             }
 *         },
 *
 *         //alternatively, a Model name can be given (see Ext.data.Store for an example)
 *         fields: ['name', 'url', {name: 'size', type: 'float'}, {name: 'lastmod', type: 'date'}]
 *     });
 *
 * This store is configured to consume a returned object of the form:
 *
 *     {
 *         images: [{
 *             name: 'Image one',
 *             url: '/GetImage.php?id=1',
 *             size: 46.5,
 *             lastmod: new Date(2007, 10, 29)
 *         }, {
 *             name: 'Image Two',
 *             url: '/GetImage.php?id=2',
 *             size: 43.2,
 *             lastmod: new Date(2007, 10, 30)
 *         }]
 *     }
 *
 * An object literal of this form could also be used as the {@link #cfg-data} config option.
 */
Ext.define('Ext.data.JsonStore', {
    extend: 'Ext.data.Store',
    alias: 'store.json',
    requires: [
        'Ext.data.proxy.Ajax',
        'Ext.data.reader.Json',
        'Ext.data.writer.Json'
    ],
    constructor: function(config) {
        config = Ext.apply({
            proxy: {
                type: 'ajax',
                reader: 'json',
                writer: 'json'
            }
        }, config);
        this.callParent([
            config
        ]);
    }
});

/**
 * This class has been deprecated. Use `Ext.data.schema.Schema` instead.
 */
Ext.define('Ext.data.ModelManager', {
    alternateClassName: 'Ext.ModelMgr',
    requires: [
        'Ext.data.schema.Schema'
    ],
    singleton: true,
    deprecated: {
        5: {
            methods: {
                clear: null,
                create: function(data, name, id) {
                    var T = name;
                    if (!T.isEntity) {
                        T = this.getModel(name || data.name);
                    }
                    return T.createWithId(id, data);
                },
                each: function(fn, scope) {
                    Ext.data.Model.schema.eachEntity(fn, scope);
                },
                get: function(name) {
                    return this.getModel(name);
                },
                getCount: function() {
                    return Ext.data.Model.schema.entityCount;
                },
                /**
                 * @method getModel
                 * Returns the {@link Ext.data.Model} class for a given model name
                 * @param {String/Object} id The classname of the model or the model class itself.
                 * @return {Ext.data.Model} a model class.
                 * @deprecated 5.0 Use {@link Ext.data.schema.Schema#lookupEntity} instead.
                 */
                getModel: function(id) {
                    return Ext.data.schema.Schema.lookupEntity(id);
                },
                isRegistered: function(name) {
                    return !!this.getModel(name);
                }
            }
        }
    }
});

/** 
 * This class is used as a set of methods that are applied to the prototype of a
 * {@link Ext.data.Model Model} to decorate it with a Node API. This means that models
 * used in conjunction with a tree will have all of the tree related methods available
 * on the model. In general, this class will not be used directly by the developer.
 *
 * This class also creates extra {@link Ext.data.Field fields} on the model, if they do
 * not exist, to help maintain the tree state and UI. These fields are documented as
 * config options.
 *
 * The data fields used to render a tree node are: {@link #text}, {@link #leaf},
 * {@link #children}, and {@link #expanded}.  Once a node is loaded to the tree store
 * you can use {@link Ext.data.Model#get get()} to fetch the value of a given field
 * name (provided there is not a convenience accessor on the Node for that field).
 *
 *     @example
 *     Ext.tip.QuickTipManager.init(); // not required when using Ext.application()
 *
 *     var root = {
 *         expanded: true,
 *         children: [{
 *             text: "Leaf node (<i>no folder/arrow icon</i>)",
 *             leaf: true,
 *             qtitle: 'Sample Tip Title',
 *             qtip: 'Tip body'
 *         }, {
 *             text: "Parent node expanded",
 *             expanded: true,
 *             children: [{
 *                 text: "Expanded leaf node 1",
 *                 leaf: true
 *             }, {
 *                 text: "Expanded leaf node 2",
 *                 leaf: true
 *             }]
 *         }, {
 *             text: "Parent node collapsed",
 *             children: [{
 *                 text: "Collapsed leaf node 1",
 *                 leaf: true
 *             }, {
 *                 text: "Collapsed leaf node 2",
 *                 leaf: true
 *             }]
 *         }]
 *     };
 *
 *     var tree = Ext.create('Ext.tree.Panel', {
 *         title: 'TreePanel',
 *         width: 260,
 *         height: 200,
 *         root: root,
 *         rootVisible: false,
 *         renderTo: document.body,
 *         bbar: ['The first node ', {
 *             text: 'is a leaf?',
 *             handler: function() {
 *                 var firstChild = tree.getRootNode().getChildAt(0);
 *                 Ext.Msg.alert('Is Leaf?', firstChild.isLeaf());
 *             }
 *         }, {
 *             text: 'has text?',
 *             handler: function() {
 *                 var firstChild = tree.getRootNode().getChildAt(0);
 *                 Ext.Msg.alert('Has Text:', firstChild.get('text'));
 *             }
 *         }]
 *     });
 *
 * The following configs have methods used to set the value / state of the node at
 * runtime:
 *
 * **{@link #children} / {@link #leaf}**
 *
 *  - {@link #appendChild}
 *  - {@link #hasChildNodes}
 *  - {@link #insertBefore}
 *  - {@link #insertChild}
 *  - {@link #method-remove}
 *  - {@link #removeAll}
 *  - {@link #removeChild}
 *  - {@link #replaceChild}
 *
 * **{@link #expanded}**
 *
 *  - {@link #method-expand}
 *  - {@link #expandChildren}
 *  - {@link #method-collapse}
 *  - {@link #collapseChildren}
 *
 * The remaining configs may be set using {@link Ext.data.Model#method-set set()}.
 *
 *     node.set('text', 'Changed Text'); // example showing how to change the node label
 *
 * The {@link #qtip}, {@link #qtitle}, and {@link #qshowDelay} use QuickTips and
 * requires initializing {@link Ext.tip.QuickTipManager} unless the application is
 * created using {@link Ext#method-application}.
 *
 *     Ext.tip.QuickTipManager.init();
 *
 * For additional information and examples see the description for
 * {@link Ext.tree.Panel}.
 */
Ext.define('Ext.data.NodeInterface', {
    requires: [
        'Ext.data.field.Boolean',
        'Ext.data.field.Integer',
        'Ext.data.field.String',
        'Ext.data.writer.Json',
        'Ext.mixin.Observable'
    ],
    /**
     * @cfg {Boolean} [expanded=false]
     * True if the node is expanded.
     *
     * When the tree is asynchronously remote loaded, expanding a collapsed node loads
     * the children of that node (if the node has not already been loaded previously).
     *
     * See also: {@link #isExpanded}.
     */
    /**
     * @cfg {Boolean} [expandable=true]
     * False to prevent expanding/collapsing of this node.
     *
     * See also: {@link #isExpandable}.
     */
    /**
     * @cfg {Boolean} [checked=null]
     * Set to true or false to show a checkbox alongside this node.
     *
     * To fetch an array of checked nodes use {@link Ext.tree.Panel#method-getChecked
     * getChecked()}.
     */
    /**
     * @cfg {Boolean} [leaf=false]
     * Set to true to indicate that this child can have no children. The expand icon/arrow will
     * then not be rendered for this node.
     *
     * See also: {@link #isLeaf}.
     */
    /**
     * @cfg {String} cls
     * CSS class to apply to this node.
     */
    /**
     * @cfg iconCls
     * @inheritdoc Ext.panel.Header#cfg-iconCls
     * @localdoc Use {@link #icon} to set the icon src path directly.
     */
    /**
     * @cfg icon
     * @inheritdoc Ext.panel.Header#cfg-icon
     */
    /**
     * @cfg {Number/String} glyph
     *
     * A numeric unicode character code to use as the icon.  The default font-family 
     * for glyphs can be set globally using 
     * {@link Ext.app.Application#glyphFontFamily glyphFontFamily} application 
     * config or the {@link Ext#setGlyphFontFamily Ext.setGlyphFontFamily()} method.
     * It is initially set to `'Pictos'`.
     * 
     * The following shows how to set the glyph using the font icons provided in the 
     * SDK (assuming the font-family has been configured globally):
     *
     *     // assumes the glyphFontFamily is "Pictos"
     *     glyph: 'x48'       // the "home" icon (H character)
     *
     *     // assumes the glyphFontFamily is "Pictos"
     *     glyph: 72          // The "home" icon (H character)
     *
     *     // assumes the glyphFontFamily is "Pictos"
     *     glyph: 'H'         // the "home" icon
     * 
     * Alternatively, this config option accepts a string with the charCode and 
     * font-family separated by the `@` symbol.
     * 
     *     // using Font Awesome
     *     glyph: 'xf015@FontAwesome'     // the "home" icon
     * 
     *     // using Pictos
     *     glyph: 'H@Pictos'              // the "home" icon
     *
     * Depending on the theme you're using, you may need include the font icon 
     * packages in your application in order to use the icons included in the 
     * SDK.  For more information see:
     * 
     *  - [Font Awesome icons](http://fortawesome.github.io/Font-Awesome/cheatsheet/)
     *  - [Pictos icons](../guides/core_concepts/font_ext.html)
     *  - [Theming Guide](../guides/core_concepts/theming.html)
     * @since 6.2.0
     */
    /**
     * @cfg {Boolean} [allowDrop=true]
     * Set to false to deny dropping on this node.
     *
     * Applicable when using the {@link Ext.tree.plugin.TreeViewDragDrop
     * TreeViewDragDrop} plugin.
     */
    /**
     * @cfg {Boolean} [allowDrag=true]
     * Set to false to deny dragging of this node.
     *
     * Applicable when using the {@link Ext.tree.plugin.TreeViewDragDrop
     * TreeViewDragDrop} plugin.
     */
    /**
     * @cfg {String} href
     * A URL for a link that's created when this config is specified.
     *
     * See also {@link #hrefTarget}.
     */
    /**
     * @cfg {String} hrefTarget
     * Target for link. Only applicable when {@link #href} is also specified.
     */
    /**
     * @cfg {String} qtip
     * Tooltip text to show on this node.
     *
     * See also {@link #qtitle}.
     * See also {@link #qshowDelay}.
     */
    /**
     * @cfg {String} qtitle
     * Tooltip title.
     *
     * See also {@link #qtip}.
     * See also {@link #qshowDelay}.
     */
    /**
     * @cfg {Number} qshowDelay
     * Tooltip showDelay.
     *
     * See also {@link #qtip}.
     * See also {@link #qtitle}.
     */
    /**
     * @cfg {String} text
     * The text to show on node label (_html tags are accepted_).
     * The default text for the root node is `ROOT`.  All other nodes default to ''.
     *
     * **Note:** By default the node label is `text`, but can be set using the tree's
     * {@link Ext.tree.Panel#cfg-displayField displayField} config.
     */
    /**
     * @cfg {Ext.data.NodeInterface[]} children
     * Array of child nodes.
     *
     * **Note:** By default the child nodes root is `children`, but can be set using the
     * reader {@link Ext.data.reader.Reader#cfg-rootProperty rootProperty} config on the
     * {@link Ext.data.TreeStore TreeStore's} {@link Ext.data.TreeStore#cfg-proxy proxy}.
     */
    /**
     * @cfg {Boolean} [loaded=false]
     * @private
     * True if the node has finished loading.
     *
     * See {@link #isLoaded}.
     */
    /**
     * @cfg {Boolean} [loading=false]
     * @private
     * True if the node is currently loading.
     *
     * See {@link #isLoading}.
     */
    /**
     * @cfg {Boolean} root
     * @private
     * True if this is the root node.
     *
     * See {@link #isRoot}.
     */
    /**
     * @cfg {Boolean} isLast
     * @private
     * True if this is the last node.
     *
     * See {@link #method-isLast}.
     */
    /**
     * @cfg {Boolean} isFirst
     * @private
     * True if this is the first node.
     *
     * See {@link #method-isFirst}.
     */
    /**
     * @cfg {String} parentId
     * @private
     * ID of parent node.
     *
     * See {@link #parentNode}.
     */
    /**
     * @cfg {Number} index
     * @private
     * The position of the node inside its parent. When parent has 4 children and the node is third
     * amongst them, index will be 2.
     *
     * See {@link #indexOf} and {@link #indexOfId}.
     */
    /**
     * @cfg {Number} depth
     * @private
     * The number of parents this node has. A root node has depth 0, a child of it depth 1, and
     * so on...
     *
     * See {@link #getDepth}.
     */
    /**
     * @property {Ext.data.NodeInterface} nextSibling
     * A reference to this node's next sibling node. `null` if this node does not have a next
     * sibling.
     */
    /**
     * @property {Ext.data.NodeInterface} previousSibling
     * A reference to this node's previous sibling node. `null` if this node does not have a
     * previous sibling.
     */
    /**
     * @property {Ext.data.NodeInterface} parentNode
     * A reference to this node's parent node. `null` if this node is the root node.
     */
    /**
     * @property {Ext.data.NodeInterface} lastChild
     * A reference to this node's last child node. `null` if this node has no children.
     */
    /**
     * @property {Ext.data.NodeInterface} firstChild
     * A reference to this node's first child node. `null` if this node has no children.
     */
    /**
     * @property {Ext.data.NodeInterface[]} childNodes
     * An array of this nodes children.  Array will be empty if this node has no children.
     */
    /**
     * @method onRegisterTreeNode
     * Implement this method in a tree record subclass if it needs to track whenever it is
     * registered with a {@link Ext.data.TreeStore TreeStore}.
     * @param {Ext.data.TreeStore} treeStore The TreeStore to which the node is being registered.
     * @template
     */
    /**
     * @method onUnregisterTreeNode
     * Implement this method in a tree record subclass if it needs to track whenever it is
     * unregistered from a {@link Ext.data.TreeStore TreeStore}.
     * @param {Ext.data.TreeStore} treeStore The TreeStore from which the node is being
     * unregistered.
     * @template
     */
    statics: {
        /**
         * This method decorates a Model class such that it implements the interface of
         * a tree node. That is, it adds a set of methods, events, properties and fields
         * on every record.
         * @param {Ext.Class/Ext.data.Model} modelClass The Model class or an instance of
         * the Model class you want to decorate. In either case, this method decorates
         * the class so all instances of that type will have the new capabilities.
         * @static
         */
        decorate: function(modelClass) {
            var model = Ext.data.schema.Schema.lookupEntity(modelClass),
                proto = model.prototype,
                idName, idField, idType;
            if (!model.prototype.isObservable) {
                model.mixin(Ext.mixin.Observable.prototype.mixinId, Ext.mixin.Observable);
            }
            if (proto.isNode) {
                // if (already decorated)
                return;
            }
            idName = proto.idProperty;
            idField = model.getField(idName);
            idType = idField.type;
            model.override(this.getPrototypeBody());
            /* eslint-disable max-len, no-multi-spaces */
            model.addFields([
                {
                    name: 'parentId',
                    type: idType,
                    defaultValue: null,
                    allowNull: idField.allowNull
                },
                {
                    name: 'index',
                    type: 'int',
                    defaultValue: -1,
                    persist: false,
                    convert: null
                },
                {
                    name: 'depth',
                    type: 'int',
                    defaultValue: 0,
                    persist: false,
                    convert: null
                },
                {
                    name: 'expanded',
                    type: 'bool',
                    defaultValue: false,
                    persist: false,
                    convert: null
                },
                {
                    name: 'expandable',
                    type: 'bool',
                    defaultValue: true,
                    persist: false,
                    convert: null
                },
                {
                    name: 'checked',
                    type: 'auto',
                    defaultValue: null,
                    persist: false,
                    convert: null
                },
                {
                    name: 'leaf',
                    type: 'bool',
                    defaultValue: false
                },
                {
                    name: 'cls',
                    type: 'string',
                    defaultValue: '',
                    persist: false,
                    convert: null
                },
                {
                    name: 'iconCls',
                    type: 'string',
                    defaultValue: '',
                    persist: false,
                    convert: null
                },
                {
                    name: 'icon',
                    type: 'string',
                    defaultValue: '',
                    persist: false,
                    convert: null
                },
                {
                    name: 'glyph',
                    type: 'string',
                    defaultValue: '',
                    persist: false,
                    convert: null
                },
                {
                    name: 'root',
                    type: 'boolean',
                    defaultValue: false,
                    persist: false,
                    convert: null
                },
                {
                    name: 'isLast',
                    type: 'boolean',
                    defaultValue: false,
                    persist: false,
                    convert: null
                },
                {
                    name: 'isFirst',
                    type: 'boolean',
                    defaultValue: false,
                    persist: false,
                    convert: null
                },
                {
                    name: 'allowDrop',
                    type: 'boolean',
                    defaultValue: true,
                    persist: false,
                    convert: null
                },
                {
                    name: 'allowDrag',
                    type: 'boolean',
                    defaultValue: true,
                    persist: false,
                    convert: null
                },
                {
                    name: 'loaded',
                    type: 'boolean',
                    defaultValue: false,
                    persist: false,
                    convert: null
                },
                {
                    name: 'loading',
                    type: 'boolean',
                    defaultValue: false,
                    persist: false,
                    convert: null
                },
                {
                    name: 'href',
                    type: 'string',
                    defaultValue: '',
                    persist: false,
                    convert: null
                },
                {
                    name: 'hrefTarget',
                    type: 'string',
                    defaultValue: '',
                    persist: false,
                    convert: null
                },
                {
                    name: 'qtip',
                    type: 'string',
                    defaultValue: '',
                    persist: false,
                    convert: null
                },
                {
                    name: 'qtitle',
                    type: 'string',
                    defaultValue: '',
                    persist: false,
                    convert: null
                },
                {
                    name: 'qshowDelay',
                    type: 'int',
                    defaultValue: 0,
                    persist: false,
                    convert: null
                },
                {
                    name: 'children',
                    type: 'auto',
                    defaultValue: null,
                    persist: false,
                    convert: null
                },
                {
                    name: 'visible',
                    type: 'boolean',
                    defaultValue: true,
                    persist: false
                },
                {
                    name: 'text',
                    type: 'string',
                    persist: false
                }
            ]);
        },
        /* eslint-enable max-len, no-multi-spaces */
        getPrototypeBody: function() {
            var bubbledEvents = {
                    idchanged: true,
                    append: true,
                    remove: true,
                    move: true,
                    insert: true,
                    beforeappend: true,
                    beforeremove: true,
                    beforemove: true,
                    beforeinsert: true,
                    expand: true,
                    collapse: true,
                    beforeexpand: true,
                    beforecollapse: true,
                    sort: true
                },
                silently = {
                    silent: true
                };
            // bulkUpdate usage:
            // This is used in 3 contexts:
            // a) When registering nodes. When bulk updating, we don't want to descend down the tree
            // recursively making calls to register which is redundant. We do need to call it for
            // each node because they need to be findable via id as soon as append events fire,
            // so we only do the minimum needed.
            // b) When setting a data property on the model. We only need to go through set
            // (and the subsequent event chain) so that the UI can update. If we're doing a bulk
            // update, the UI will update regardless.
            // c) triggerUIUpdate. This is because we know "something has changed", but not exactly
            // what, so we allow the UI to redraw itself. It has no purpose as far as data goes, so
            // skip it when we can
            return {
                /**
                 * @property {Boolean} isNode
                 * `true` in this class to identify an object as an instantiated Node, or subclass
                 * thereof.
                 */
                isNode: true,
                firstChild: null,
                lastChild: null,
                parentNode: null,
                previousSibling: null,
                nextSibling: null,
                constructor: function() {
                    var me = this;
                    me.mixins.observable.constructor.call(me);
                    me.callParent(arguments);
                    me.childNodes = [];
                    // These events are fired on this node, and programmatically bubble 
                    // up the parentNode axis, ending up walking off the top and firing 
                    // on the owning Ext.data.TreeStore
                    /**
                     * @event append
                     * Fires when a new child node is appended
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} node The newly appended node
                     * @param {Number} index The index of the newly appended node
                     */
                    /**
                     * @event remove
                     * Fires when a child node is removed
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} node The removed node
                     * @param {Boolean} isMove `true` if the child node is being removed so it can
                     * be moved to another position in the tree.
                     * @param {Object} context An object providing information about where the
                     * removed node came from. It contains the following properties:
                     * @param {Ext.data.NodeInterface} context.parentNode The node from which the
                     * removed node was removed.
                     * @param {Ext.data.NodeInterface} context.previousSibling The removed node's
                     * former previous sibling.
                     * @param {Ext.data.NodeInterface} context.nextSibling The removed node's former
                     * next sibling. (a side effect of calling
                     * {@link Ext.data.NodeInterface#appendChild appendChild} or
                     * {@link Ext.data.NodeInterface#insertBefore insertBefore} with a node that
                     * already has a parentNode)
                     */
                    /**
                     * @event move
                     * Fires when this node is moved to a new location in the tree
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} oldParent The old parent of this node
                     * @param {Ext.data.NodeInterface} newParent The new parent of this node
                     * @param {Number} index The index it was moved to
                     */
                    /**
                     * @event insert
                     * Fires when a new child node is inserted.
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} node The child node inserted
                     * @param {Ext.data.NodeInterface} refNode The child node the node was inserted
                     * before
                     */
                    /**
                     * @event beforeappend
                     * Fires before a new child is appended, return false to cancel the append.
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} node The child node to be appended
                     */
                    /**
                     * @event beforeremove
                     * Fires before a child is removed, return false to cancel the remove.
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} node The child node to be removed
                     * @param {Boolean} isMove `true` if the child node is being removed so it can
                     * be moved to another position in the tree. (a side effect of calling
                     * {@link Ext.data.NodeInterface#appendChild appendChild} or
                     * {@link Ext.data.NodeInterface#insertBefore insertBefore} with a node that
                     * already has a parentNode)
                     */
                    /**
                     * @event beforemove
                     * Fires before this node is moved to a new location in the tree. Return false
                     * to cancel the move.
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} oldParent The parent of this node
                     * @param {Ext.data.NodeInterface} newParent The new parent this node is moving
                     * to
                     * @param {Number} index The index it is being moved to
                     */
                    /**
                     * @event beforeinsert
                     * Fires before a new child is inserted, return false to cancel the insert.
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} node The child node to be inserted
                     * @param {Ext.data.NodeInterface} refNode The child node the node is being
                     * inserted before
                     */
                    /**
                     * @event expand
                     * Fires when this node is expanded.
                     * @param {Ext.data.NodeInterface} this The expanding node
                     */
                    /**
                     * @event collapse
                     * Fires when this node is collapsed.
                     * @param {Ext.data.NodeInterface} this The collapsing node
                     */
                    /**
                     * @event beforeexpand
                     * Fires before this node is expanded.
                     * @param {Ext.data.NodeInterface} this The expanding node
                     */
                    /**
                     * @event beforecollapse
                     * Fires before this node is collapsed.
                     * @param {Ext.data.NodeInterface} this The collapsing node
                     */
                    /**
                     * @event sort
                     * Fires when this node's childNodes are sorted.
                     * @param {Ext.data.NodeInterface} this This node.
                     * @param {Ext.data.NodeInterface[]} childNodes The childNodes of this node.
                     */
                    return me;
                },
                /**
                 * Ensures that the passed object is an instance of a Record with the NodeInterface
                 * applied
                 * @return {Ext.data.NodeInterface}
                 */
                createNode: function(node) {
                    var me = this,
                        childType = me.childType,
                        store, storeReader, nodeProxy, nodeReader, reader, typeProperty,
                        T = me.self;
                    // Passed node's internal data object
                    if (!node.isModel) {
                        // Check this node type's childType configuration
                        if (childType) {
                            T = me.schema.getEntity(childType);
                        } else // See if the reader has a typeProperty and use it if possible
                        {
                            store = me.getTreeStore();
                            storeReader = store && store.getProxy().getReader();
                            nodeProxy = me.getProxy();
                            nodeReader = nodeProxy ? nodeProxy.getReader() : null;
                            // If the node's proxy's reader was configured with a special
                            // typeProperty (property name which defines the child type name)
                            // use that.
                            /* eslint-disable indent */
                            reader = !storeReader || (nodeReader && nodeReader.initialConfig.typeProperty) ? nodeReader : storeReader;
                            /* eslint-enable indent */
                            if (reader) {
                                typeProperty = reader.getTypeProperty();
                                if (typeProperty) {
                                    T = reader.getChildType(me.schema, node, typeProperty);
                                }
                            }
                        }
                        node = new T(node);
                    }
                    // The node may already decorated, but may not have been
                    // so when the model constructor was called. If not,
                    // setup defaults here
                    if (!node.childNodes) {
                        node.firstChild = node.lastChild = node.parentNode = node.previousSibling = node.nextSibling = null;
                        node.childNodes = [];
                    }
                    return node;
                },
                /**
                 * Returns true if this node is a leaf
                 * @return {Boolean}
                 */
                isLeaf: function() {
                    return this.get('leaf') === true;
                },
                /**
                 * Sets the first child of this node
                 * @private
                 * @param {Ext.data.NodeInterface} node
                 */
                setFirstChild: function(node) {
                    this.firstChild = node;
                },
                /**
                 * Sets the last child of this node
                 * @private
                 * @param {Ext.data.NodeInterface} node
                 */
                setLastChild: function(node) {
                    this.lastChild = node;
                },
                /**
                 * Updates general data of this node like isFirst, isLast, depth. This
                 * method is internally called after a node is moved. This shouldn't
                 * have to be called by the developer unless they are creating custom
                 * Tree plugins.
                 * @protected
                 * @param {Boolean} commit
                 * @param {Object} info The info to update. May contain any of the following
                 *  @param {Object} info.isFirst
                 *  @param {Object} info.isLast
                 *  @param {Object} info.index
                 *  @param {Object} info.depth
                 *  @param {Object} info.parentId
                 *  @return {String[]} The names of any persistent fields that were modified.
                 */
                updateInfo: function(commit, info) {
                    var me = this,
                        phantom = me.phantom,
                        result, childInfo, children, childCount, i;
                    commit = {
                        silent: true,
                        commit: commit
                    };
                    // The only way child data can be influenced is if this node has changed level
                    // in this update.
                    if (info.depth != null && info.depth !== me.data.depth) {
                        childInfo = {
                            depth: info.depth + 1
                        };
                        children = me.childNodes;
                        childCount = children.length;
                        for (i = 0; i < childCount; i++) {
                            children[i].updateInfo(commit, childInfo);
                        }
                    }
                    result = me.set(info, commit);
                    // Restore phantom flag which might get cleared by a commit.
                    me.phantom = phantom;
                    return result;
                },
                /**
                 * Returns true if this node is the last child of its parent
                 * @return {Boolean}
                 */
                isLast: function() {
                    return this.get('isLast');
                },
                /**
                 * Returns true if this node is the first child of its parent
                 * @return {Boolean}
                 */
                isFirst: function() {
                    return this.get('isFirst');
                },
                /**
                 * Returns true if this node has one or more child nodes, else false.
                 * @return {Boolean}
                 */
                hasChildNodes: function() {
                    return !this.isLeaf() && this.childNodes.length > 0;
                },
                /**
                 * Returns true if this node has one or more child nodes, or if the `expandable`
                 * node attribute is explicitly specified as true, otherwise returns false.
                 * @return {Boolean}
                 */
                isExpandable: function() {
                    var me = this;
                    if (me.get('expandable')) {
                        return !(me.isLeaf() || (me.isLoaded() && !me.phantom && !me.hasChildNodes()));
                    }
                    return false;
                },
                triggerUIUpdate: function() {
                    // This isn't ideal, however none of the underlying fields have changed
                    // but we still need to update the UI
                    // callJoined calls both the Stores we are joined to, and any TreeStore
                    // of which we may be a descendant.
                    this.callJoined('afterEdit', []);
                },
                /**
                 * Inserts node(s) as the last child node of this node.
                 *
                 * If the node was previously a child node of another parent node, it will be
                 * removed from that node first.
                 *
                 * @param {Ext.data.NodeInterface/Ext.data.NodeInterface[]/Object} node The node
                 * or Array of nodes to append
                 * @param {Boolean} [suppressEvents=false] True to suppress firing of 
                 * events.
                 * @param {Boolean} [commit=false]
                 * @return {Ext.data.NodeInterface} The appended node if single append, or null
                 * if an array was passed
                 */
                appendChild: function(node, suppressEvents, commit) {
                    var me = this,
                        treeStore = me.getTreeStore(),
                        bulkUpdate = treeStore && treeStore.bulkUpdate,
                        childInfo = {
                            isLast: true,
                            parentId: me.getId(),
                            depth: (me.data.depth || 0) + 1
                        },
                        oldParent, previousSibling, modifiedFields, index, result, i, ln;
                    // Coalesce all layouts caused by node append
                    Ext.suspendLayouts();
                    // if passed an array do them one by one
                    if (Ext.isArray(node)) {
                        ln = node.length;
                        result = new Array(ln);
                        // Suspend view updating and data syncing during update
                        me.callTreeStore('beginFill');
                        for (i = 0; i < ln; i++) {
                            result[i] = me.appendChild(node[i], suppressEvents, commit);
                        }
                        // Resume view updating and data syncing after appending all new children.
                        // This will fire the add event to any views (if its the top level append)
                        me.callTreeStore('endFill', [
                            result
                        ]);
                    } else {
                        // Make sure it is a record
                        node = me.createNode(node);
                        /* eslint-disable-next-line max-len */
                        if (suppressEvents !== true && me.fireBubbledEvent('beforeappend', [
                            me,
                            node
                        ]) === false) {
                            Ext.resumeLayouts(true);
                            return false;
                        }
                        index = me.childNodes.length;
                        oldParent = node.parentNode;
                        // it's a move, make sure we move it cleanly
                        if (oldParent) {
                            /* eslint-disable-next-line max-len */
                            if (suppressEvents !== true && node.fireBubbledEvent('beforemove', [
                                node,
                                oldParent,
                                me,
                                index
                            ]) === false) {
                                Ext.resumeLayouts(true);
                                return false;
                            }
                            // Return false if a beforeremove listener vetoed the remove
                            /* eslint-disable-next-line max-len */
                            if (oldParent.removeChild(node, false, suppressEvents, oldParent.getTreeStore() === treeStore) === false) {
                                Ext.resumeLayouts(true);
                                return false;
                            }
                        }
                        // Coalesce sync operations across this operation
                        // Node field setting (loaded, expanded) and node addition both trigger
                        // a sync if autoSync is set.
                        if (treeStore) {
                            treeStore.beginUpdate();
                        }
                        index = me.childNodes.length;
                        if (index === 0) {
                            me.setFirstChild(node);
                        }
                        me.childNodes[index] = node;
                        node.parentNode = me;
                        node.nextSibling = null;
                        me.setLastChild(node);
                        previousSibling = me.childNodes[index - 1];
                        if (previousSibling) {
                            node.previousSibling = previousSibling;
                            previousSibling.nextSibling = node;
                            previousSibling.updateInfo(commit, {
                                isLast: false
                            });
                            // No need to trigger a ui update if we're doing a bulk update
                            if (!bulkUpdate) {
                                previousSibling.triggerUIUpdate();
                            }
                        } else {
                            node.previousSibling = null;
                        }
                        // Update the new child's info passing in info we already know
                        childInfo.isFirst = index === 0;
                        childInfo.index = index;
                        // Integrate the new node into its new position.
                        // It's not in the store yet, so we might need to
                        // inform the store of significant field changes later.
                        modifiedFields = node.updateInfo(commit, childInfo);
                        // We stop being a leaf as soon as a node is appended
                        if (me.isLeaf()) {
                            me.set('leaf', false);
                        }
                        // As soon as we append a child to this node, we are loaded
                        if (!me.isLoaded()) {
                            if (bulkUpdate) {
                                me.data.loaded = true;
                            } else {
                                me.set('loaded', true);
                            }
                        } else if (me.childNodes.length === 1 && !bulkUpdate) {
                            me.triggerUIUpdate();
                        }
                        // Ensure connectors are correct by updating the UI on all intervening
                        // nodes (descendants) between last sibling and new node.
                        if (index && me.childNodes[index - 1].isExpanded() && !bulkUpdate) {
                            me.childNodes[index - 1].cascade(me.triggerUIUpdate);
                        }
                        // We register the subtree before we proceed so relayed events
                        // (like nodeappend) from our TreeStore (if we have one) will be
                        // able to use getNodeById. The node also needs to be added since 
                        // we're passing it in the events below. If we're not bulk updating, it
                        // means we're just appending a node (with possible children), so do it
                        // deeply here to ensure everything is captured.
                        if (treeStore) {
                            treeStore.registerNode(me, !bulkUpdate);
                            if (bulkUpdate) {
                                treeStore.registerNode(node);
                            }
                        }
                        // This node MUST fire its events first, so that if the TreeStore's
                        // onNodeAppend loads and appends local children, the events are still
                        // in order. This node appended this child first, before the descendant
                        // cascade.
                        if (suppressEvents !== true) {
                            me.fireBubbledEvent('append', [
                                me,
                                node,
                                index
                            ]);
                            if (oldParent) {
                                node.fireBubbledEvent('move', [
                                    node,
                                    oldParent,
                                    me,
                                    index
                                ]);
                            }
                        }
                        // Inform the TreeStore so that the node can be inserted
                        // and registered.
                        me.callTreeStore('onNodeAppend', [
                            node,
                            index
                        ]);
                        // Now that the store contains the new node, we cam inform it of field
                        // changes.
                        if (modifiedFields) {
                            node.callJoined('afterEdit', [
                                modifiedFields
                            ]);
                        }
                        result = node;
                        // Coalesce sync operations across this operation
                        // Node field setting (loaded, expanded) and node addition both trigger
                        // a sync if autoSync is set.
                        if (treeStore) {
                            treeStore.endUpdate();
                        }
                    }
                    // Flush layouts caused by updating of the UI
                    Ext.resumeLayouts(true);
                    return result;
                },
                /**
                * Returns the tree this node is in.
                * @return {Ext.tree.Panel} The tree panel which owns this node.
                */
                getOwnerTree: function() {
                    var store = this.getTreeStore();
                    return store && store.ownerTree;
                },
                /**
                 * Returns the {@link Ext.data.TreeStore} which owns this node.
                 * @return {Ext.data.TreeStore} The TreeStore which owns this node.
                 */
                getTreeStore: function() {
                    var root = this;
                    while (root && !root.treeStore) {
                        root = root.parentNode;
                    }
                    return root && root.treeStore;
                },
                /**
                 * Removes a child node from this node.
                 * @param {Ext.data.NodeInterface} node The node to remove
                 * @param {Boolean} [erase=false] True to erase the record using the
                 * configured proxy.
                 * @param {Boolean} [suppressEvents] (private)
                 * @param {Boolean} [isMove] (private)
                 * @return {Ext.data.NodeInterface} The removed node
                 */
                removeChild: function(node, erase, suppressEvents, isMove) {
                    var me = this,
                        index = me.indexOf(node),
                        i, childCount, previousSibling,
                        treeStore = me.getTreeStore(),
                        bulkUpdate = treeStore && treeStore.bulkUpdate,
                        removeContext,
                        removeRange = [];
                    if (index === -1 || (suppressEvents !== true && me.fireBubbledEvent('beforeremove', [
                        me,
                        node,
                        !!isMove
                    ]) === false)) {
                        return false;
                    }
                    // Coalesce all layouts caused by node removal
                    Ext.suspendLayouts();
                    // Coalesce sync operations across this operation
                    if (treeStore) {
                        treeStore.beginUpdate();
                    }
                    // remove it from childNodes collection
                    Ext.Array.erase(me.childNodes, index, 1);
                    // update child refs
                    if (me.firstChild === node) {
                        me.setFirstChild(node.nextSibling);
                    }
                    if (me.lastChild === node) {
                        me.setLastChild(node.previousSibling);
                    }
                    // Update previous sibling to point to its new next.
                    previousSibling = node.previousSibling;
                    if (previousSibling) {
                        node.previousSibling.nextSibling = node.nextSibling;
                    }
                    // Update the next sibling to point to its new previous
                    if (node.nextSibling) {
                        node.nextSibling.previousSibling = node.previousSibling;
                        // And if it's the new first child, let it know
                        if (index === 0) {
                            node.nextSibling.updateInfo(false, {
                                isFirst: true
                            });
                        }
                        // Update subsequent siblings' index values
                        for (i = index , childCount = me.childNodes.length; i < childCount; i++) {
                            me.childNodes[i].updateInfo(false, {
                                index: i
                            });
                        }
                    }
                    // If the removed node had no next sibling, but had a previous,
                    // update the previous sibling so it knows it's the last
                    else if (previousSibling) {
                        previousSibling.updateInfo(false, {
                            isLast: true
                        });
                        // We're removing the last child.
                        // Ensure connectors are correct by updating the UI on all intervening
                        // nodes (descendants) between previous sibling and new node.
                        if (!bulkUpdate) {
                            if (previousSibling.isExpanded()) {
                                previousSibling.cascade(me.triggerUIUpdate);
                            } else // No intervening descendant nodes, just update the previous sibling
                            {
                                previousSibling.triggerUIUpdate();
                            }
                        }
                    }
                    // If this node suddenly doesn't have child nodes anymore, update 
                    // myself
                    if (!me.childNodes.length && !bulkUpdate) {
                        me.triggerUIUpdate();
                    }
                    // Flush layouts caused by updating the UI
                    Ext.resumeLayouts(true);
                    if (suppressEvents !== true) {
                        // Context argument to events.
                        removeContext = {
                            parentNode: node.parentNode,
                            previousSibling: node.previousSibling,
                            nextSibling: node.nextSibling
                        };
                        // Inform the TreeStore so that descendant nodes can be removed.
                        me.callTreeStore('beforeNodeRemove', [
                            [
                                node
                            ],
                            !!isMove,
                            removeRange
                        ]);
                        node.previousSibling = node.nextSibling = node.parentNode = null;
                        me.fireBubbledEvent('remove', [
                            me,
                            node,
                            !!isMove,
                            removeContext
                        ]);
                        // Inform the TreeStore so that the node unregistered and unjoined.
                        me.callTreeStore('onNodeRemove', [
                            [
                                node
                            ],
                            !!isMove,
                            removeRange
                        ]);
                    }
                    // Update removed node's pointers *after* firing event so that listeners
                    // can tell where the removal took place
                    if (erase) {
                        node.erase(true);
                    } else {
                        node.clear();
                    }
                    // Must clear the parentNode silently upon remove from the TreeStore.
                    // Any subsequent append to any node will trigger dirtiness
                    // (It may be added to a different node of the same ID, e.g. "root").
                    // lastParentId still needed for TreeStore's clearRemovedOnLoad functionality
                    // to be able to link nodes in the removed array to nodes under the reloading
                    // node's tree.
                    if (!isMove) {
                        node.set({
                            parentId: null,
                            lastParentId: me.getId()
                        }, silently);
                    }
                    // Coalesce sync operations across this operation
                    if (treeStore) {
                        treeStore.endUpdate();
                    }
                    return node;
                },
                /**
                 * Creates a copy (clone) of this Node.
                 * @param {String} [newId] A new id, defaults to this Node's id.
                 * @param {Ext.data.session.Session} [session] The session to which the
                 * new record belongs.
                 * @param {Boolean} [deep=false] True to recursively copy all child nodes
                 * into the new Node. False to copy without child Nodes.
                 * @return {Ext.data.NodeInterface} A copy of this Node.
                 */
                copy: function(newId, session, deep) {
                    var me = this,
                        result,
                        args = [
                            newId
                        ],
                        len = me.childNodes ? me.childNodes.length : 0,
                        i;
                    // Historical API of NodeInterface#copy was (newId, deep)
                    // We must keep that working if a Session is passed.
                    // Second argument is Session in superclass copy method.
                    if (session && session.isSession) {
                        args.push(session);
                    } else if (arguments.length < 3) {
                        deep = session;
                    }
                    result = me.callParent(args);
                    // Move child nodes across to the copy if required
                    if (deep) {
                        for (i = 0; i < len; i++) {
                            result.appendChild(me.childNodes[i].copy(undefined, true));
                        }
                    }
                    return result;
                },
                /**
                 * Clears the node.
                 * @private
                 * @param {Boolean} [erase=false] True to erase the node using the configured
                 * proxy.
                 * @param {Boolean} [resetChildren=false] True to reset child nodes
                 */
                clear: function(erase, resetChildren) {
                    var me = this;
                    // clear any references from the node
                    me.parentNode = me.previousSibling = me.nextSibling = null;
                    if (erase) {
                        me.firstChild = me.lastChild = me.childNodes = null;
                    }
                    // This is used by TreeStore for clearing root node state on reload
                    if (resetChildren) {
                        me.firstChild = me.lastChild = null;
                        me.childNodes.length = 0;
                        if (me.data) {
                            me.data.children = null;
                        }
                    }
                },
                drop: function() {
                    var me = this,
                        childNodes = me.childNodes,
                        parentNode = me.parentNode,
                        treeStore = me.getTreeStore(),
                        node, i, len;
                    // Ensure Model operations are performed.
                    // Store removal is NOT handled.
                    // TreeStore's afterDrop does nothing.
                    me.callParent();
                    // If called in recursion from here, there'll be no parentNode
                    if (parentNode) {
                        // TreeStore.onNodeRemove also adds invisible descendant nodes to the remove
                        // tracking array.
                        parentNode.removeChild(me);
                    }
                    // If we are the root, there'll be no parent node. It's a special case. We must
                    // update the TreeStore's root with a null node.
                    else if (me.get('root')) {
                        treeStore.setRoot(null);
                    }
                    // Removing a node removes the node and all *VISIBLE* descendant nodes from the
                    // Store and adds them to the remove tracking array.
                    //
                    // After this point, no descendant nodes have a connection to the TreeStore.
                    // Coalesce sync operations across this operation
                    if (treeStore) {
                        treeStore.beginUpdate();
                    }
                    // Recurse down dropping all descendants.
                    // This will NOT remove them from the store's data collection
                    for (i = 0 , len = childNodes ? childNodes.length : 0; i < len; i++) {
                        node = childNodes[i];
                        // Detach descendant nodes so that they do not all attempt to perform
                        // removal from the parent.
                        node.clear();
                        // Drop descendant nodes.
                        node.drop();
                    }
                    // Coalesce sync operations across this operation
                    if (treeStore) {
                        treeStore.endUpdate();
                    }
                },
                /**
                 * Destroys the node.
                 * @param {Boolean} [options] (private)
                 */
                erase: function(options) {
                    var me = this,
                        childNodes = me.childNodes,
                        len = childNodes && childNodes.length,
                        i, node;
                    // This unhooks this node from the tree structure
                    // The UI is updated.
                    // Now to recursively erase.
                    me.remove();
                    // Clear removes linkage, so the erase's call into drop cannot recurse.
                    // this method has to recurse to do all its stuff.
                    me.clear(true);
                    me.callParent([
                        options
                    ]);
                    for (i = 0; i < len; i++) {
                        node = childNodes[i];
                        // The top level in the cascade is already removed.
                        // Prevent the recursive erase calls doing further node removal.
                        node.parentNode = null;
                        node.erase(options);
                    }
                },
                /**
                 * Inserts the first node before the second node in this nodes childNodes
                 * collection.
                 * @param {Ext.data.NodeInterface/Ext.data.NodeInterface[]/Object} node The node
                 * to insert
                 * @param {Ext.data.NodeInterface} refNode The node to insert before (if null the
                 * node is appended)
                 * @param {Boolean} [suppressEvents] (private)
                 * @return {Ext.data.NodeInterface} The inserted node
                 */
                insertBefore: function(node, refNode, suppressEvents) {
                    var me = this,
                        index = me.indexOf(refNode),
                        oldParent = node.parentNode,
                        refIndex = index,
                        childCount, previousSibling, i,
                        treeStore = me.getTreeStore(),
                        bulkUpdate = treeStore && treeStore.bulkUpdate,
                        modifiedFields, sibling, siblingModifiedFields;
                    if (!refNode) {
                        // like standard Dom, refNode can be null for append
                        return me.appendChild(node);
                    }
                    // nothing to do
                    if (node === refNode) {
                        return false;
                    }
                    // Make sure it is a record with the NodeInterface
                    node = me.createNode(node);
                    /* eslint-disable-next-line max-len */
                    if (suppressEvents !== true && me.fireBubbledEvent('beforeinsert', [
                        me,
                        node,
                        refNode
                    ]) === false) {
                        return false;
                    }
                    // when moving internally, indexes will change after remove
                    if (oldParent === me && me.indexOf(node) < index) {
                        refIndex--;
                    }
                    // it's a move, make sure we move it cleanly
                    if (oldParent) {
                        /* eslint-disable-next-line max-len */
                        if (suppressEvents !== true && node.fireBubbledEvent('beforemove', [
                            node,
                            oldParent,
                            me,
                            index,
                            refNode
                        ]) === false) {
                            return false;
                        }
                        // Return false if a beforeremove listener vetoed the remove
                        /* eslint-disable-next-line max-len */
                        if (oldParent.removeChild(node, false, suppressEvents, oldParent.getTreeStore() === treeStore) === false) {
                            return false;
                        }
                    }
                    // Coalesce sync operations across this operation
                    // Node field setting (loaded, expanded) and node addition both trigger a sync
                    // if autoSync is set.
                    // Nodes acquire a treeStore early now by virtue of getting a parentNode, so
                    // set operations on them will arrive to this Store's onCollectionUpdate
                    if (treeStore) {
                        treeStore.beginUpdate();
                    }
                    if (refIndex === 0) {
                        me.setFirstChild(node);
                    }
                    Ext.Array.splice(me.childNodes, refIndex, 0, node);
                    node.parentNode = me;
                    node.nextSibling = refNode;
                    refNode.previousSibling = node;
                    previousSibling = me.childNodes[refIndex - 1];
                    if (previousSibling) {
                        node.previousSibling = previousSibling;
                        previousSibling.nextSibling = node;
                    } else {
                        node.previousSibling = null;
                    }
                    // Integrate the new node into its new position.
                    // It's not in the store yet, so we might need to
                    // inform the store of significant field changes later.
                    modifiedFields = node.updateInfo(false, {
                        parentId: me.getId(),
                        index: refIndex,
                        isFirst: refIndex === 0,
                        isLast: false,
                        depth: (me.data.depth || 0) + 1
                    });
                    // Update the index for all following siblings.
                    for (i = refIndex + 1 , childCount = me.childNodes.length; i < childCount; i++) {
                        sibling = me.childNodes[i];
                        siblingModifiedFields = sibling.updateInfo(false, {
                            index: i
                        });
                        if (siblingModifiedFields) {
                            sibling.callJoined('afterEdit', [
                                siblingModifiedFields
                            ]);
                        }
                    }
                    if (!me.isLoaded()) {
                        if (bulkUpdate) {
                            me.data.loaded = true;
                        } else {
                            me.set('loaded', true);
                        }
                    }
                    // If this node didn't have any child nodes before, update myself
                    else if (me.childNodes.length === 1 && !bulkUpdate) {
                        me.triggerUIUpdate();
                    }
                    // We register the subtree before we proceed so relayed events
                    // (like nodeappend) from our TreeStore (if we have one) will be
                    // able to use getNodeById.
                    if (treeStore) {
                        treeStore.registerNode(me, !bulkUpdate);
                    }
                    // This node MUST fire its events first, so that if the TreeStore's
                    // onNodeInsert loads and appends local children, the events are still in order;
                    // This node appended this child first, before the descendant cascade.
                    if (suppressEvents !== true) {
                        me.fireBubbledEvent('insert', [
                            me,
                            node,
                            refNode
                        ]);
                        if (oldParent) {
                            node.fireBubbledEvent('move', [
                                node,
                                oldParent,
                                me,
                                refIndex,
                                refNode
                            ]);
                        }
                    }
                    // Inform the TreeStore so that the node can be registered and added
                    me.callTreeStore('onNodeInsert', [
                        node,
                        refIndex
                    ]);
                    // Now that the store contains the new record, we cam inform it of
                    // field changes.
                    if (modifiedFields) {
                        node.callJoined('afterEdit', [
                            modifiedFields
                        ]);
                    }
                    // Coalesce sync operations across this operation
                    // Node field setting (loaded, expanded) and node addition both trigger a sync
                    // if autoSync is set.
                    if (treeStore) {
                        treeStore.endUpdate();
                    }
                    return node;
                },
                /**
                 * Inserts a node into this node.
                 * @param {Number} index The zero-based index to insert the node at
                 * @param {Ext.data.NodeInterface/Object} node The node to insert
                 * @return {Ext.data.NodeInterface} The node you just inserted
                 */
                insertChild: function(index, node) {
                    var sibling = this.childNodes[index];
                    if (sibling) {
                        return this.insertBefore(node, sibling);
                    } else {
                        return this.appendChild(node);
                    }
                },
                /**
                 * Used by {@link Ext.tree.Column#initTemplateRendererData} to determine whether
                 * a node is the last *visible*
                 * sibling.
                 *
                 * @private
                 */
                isLastVisible: function() {
                    var me = this,
                        result = me.data.isLast,
                        next = me.nextSibling;
                    // If it is not the true last and the store is filtered
                    // we need to see if any following siblings are visible.
                    // If any are, return false.
                    if (!result && me.getTreeStore().isFiltered()) {
                        while (next) {
                            if (next.data.visible) {
                                return false;
                            }
                            next = next.nextSibling;
                        }
                        return true;
                    }
                    return result;
                },
                /**
                 * Removes this node from its parent.
                 *
                 * **If** the node is not phantom (only added in the client side), then it may be
                 * marked for removal.
                 *
                 * If the owning {@link Ext.data.TreeStore tree store} is set to
                 * {@link Ext.data.ProxyStore#trackRemoved track removed} then the node will be
                 * added to the stack of nodes due to be removed the next time the store is
                 * synced with the server.
                 *
                 * If the owning {@link Ext.data.TreeStore tree store} is set to
                 * {@link Ext.data.ProxyStore#autoSync auto synchronize} then the synchronize
                 * request will be initiated immediately.
                 *
                 * @param {Boolean} [erase=false] True to erase the node using the configured
                 * proxy. This is only needed when the owning {@link Ext.data.TreeStore tree store}
                 * is not taking care of synchronization operations.
                 *
                 * @param {Boolean} [suppressEvents] (private)
                 * @return {Ext.data.NodeInterface} this
                 */
                remove: function(erase, suppressEvents) {
                    var me = this,
                        parentNode = me.parentNode;
                    if (parentNode) {
                        parentNode.removeChild(me, erase, suppressEvents);
                    } else if (erase) {
                        // If we don't have a parent, just erase it
                        me.erase(true);
                    }
                    return me;
                },
                /**
                 * Removes all child nodes from this node.
                 * @param {Boolean} [erase=false] True to erase the node using the configured
                 * proxy.
                 * @param {Boolean} [suppressEvents] (private)
                 * @param {Boolean} [fromParent] (private)
                 * @return {Ext.data.NodeInterface} this
                 */
                removeAll: function(erase, suppressEvents, fromParent) {
                    // This method duplicates logic from removeChild for the sake of
                    // speed since we can make a number of assumptions because we're
                    // getting rid of everything
                    var me = this,
                        childNodes = me.childNodes,
                        len = childNodes.length,
                        node, treeStore, i,
                        removeRange = [];
                    // Avoid all this if nothing to remove
                    if (!len) {
                        return me;
                    }
                    // Inform the TreeStore so that descendant nodes can be removed.
                    if (!fromParent) {
                        treeStore = me.getTreeStore();
                        // Coalesce sync operations across this operation
                        if (treeStore) {
                            treeStore.beginUpdate();
                            // The remove of visible descendants is handled by the top level
                            // call to onNodeRemove, so suspend firing the remove event so
                            // that every descendant remove does not update the UI.
                            treeStore.suspendEvent('remove');
                            me.callTreeStore('beforeNodeRemove', [
                                childNodes,
                                false,
                                removeRange
                            ]);
                        }
                    }
                    for (i = 0; i < len; ++i) {
                        node = childNodes[i];
                        node.previousSibling = node.nextSibling = node.parentNode = null;
                        me.fireBubbledEvent('remove', [
                            me,
                            node,
                            false
                        ]);
                        if (erase) {
                            node.erase(true);
                        } else // Otherwise.... apparently, removeAll is always recursive.
                        {
                            node.removeAll(false, suppressEvents, true);
                        }
                    }
                    // Inform the TreeStore so that all descendants are unregistered and unjoined.
                    if (!fromParent && treeStore) {
                        treeStore.resumeEvent('remove');
                        me.callTreeStore('onNodeRemove', [
                            childNodes,
                            false,
                            removeRange
                        ]);
                        // Coalesce sync operations across this operation
                        treeStore.endUpdate();
                    }
                    me.firstChild = me.lastChild = null;
                    childNodes.length = 0;
                    if (!fromParent) {
                        me.triggerUIUpdate();
                    }
                    return me;
                },
                /**
                 * Returns the child node at the specified index.
                 * @param {Number} index
                 * @return {Ext.data.NodeInterface}
                 */
                getChildAt: function(index) {
                    return this.childNodes[index];
                },
                /**
                 * Replaces one child node in this node with another.
                 * @param {Ext.data.NodeInterface} newChild The replacement node
                 * @param {Ext.data.NodeInterface} oldChild The node to replace
                 * @param {Boolean} [suppressEvents] (private)
                 * @return {Ext.data.NodeInterface} The replaced node
                 */
                replaceChild: function(newChild, oldChild, suppressEvents) {
                    var s = oldChild ? oldChild.nextSibling : null;
                    this.removeChild(oldChild, false, suppressEvents);
                    this.insertBefore(newChild, s, suppressEvents);
                    return oldChild;
                },
                /**
                 * Returns the index of a child node
                 * @param {Ext.data.NodeInterface} child
                 * @return {Number} The index of the child node or -1 if it was not found.
                 */
                indexOf: function(child) {
                    return Ext.Array.indexOf(this.childNodes, child);
                },
                /**
                 * Returns the index of a child node that matches the id
                 * @param {String} id The id of the node to find
                 * @return {Number} The index of the node or -1 if it was not found
                 */
                indexOfId: function(id) {
                    var childNodes = this.childNodes,
                        len = childNodes.length,
                        i = 0;
                    for (; i < len; ++i) {
                        if (childNodes[i].getId() === id) {
                            return i;
                        }
                    }
                    return -1;
                },
                /**
                 * Gets the hierarchical path from the root of the current node.
                 * @param {String} [field] The field to construct the path from. Defaults to the
                 * model idProperty.
                 * @param {String} [separator='/'] A separator to use.
                 * @return {String} The node path
                 */
                getPath: function(field, separator) {
                    field = field || this.idProperty;
                    separator = separator || '/';
                    /* eslint-disable-next-line vars-on-top */
                    var path = [
                            this.get(field)
                        ],
                        parent = this.parentNode;
                    while (parent) {
                        path.unshift(parent.get(field));
                        parent = parent.parentNode;
                    }
                    return separator + path.join(separator);
                },
                /**
                 * Returns depth of this node (the root node has a depth of 0)
                 * @return {Number}
                 */
                getDepth: function() {
                    return this.get('depth');
                },
                /**
                 * Bubbles up the tree from this node, calling the specified function with each
                 * node. The arguments to the function will be the args provided or the current
                 * node. If the function returns false at any point, the bubble is stopped.
                 * @param {Function} fn The function to call
                 * @param {Object} [scope] The scope (this reference) in which the function
                 * is executed. Defaults to the current Node.
                 * @param {Array} [args] The args to call the function with. Defaults to passing
                 * the current Node.
                 */
                bubble: function(fn, scope, args) {
                    var p = this;
                    while (p) {
                        if (fn.apply(scope || p, args || [
                            p
                        ]) === false) {
                            break;
                        }
                        p = p.parentNode;
                    }
                },
                /**
                 * Cascades down the tree from this node, calling the specified functions with each
                 * node. The arguments to the function will be the args provided or the current
                 * node. If the `before` function returns false at any point, the cascade is
                 * stopped on that branch.
                 *
                 * Note that the 3 argument form passing `fn, scope, args` is still supported.
                 * The `fn` function is as before, called *before* cascading down into child nodes.
                 * If it returns `false`, the child nodes are not traversed.
                 *
                 * @param {Object/Function} spec An object containing `before` and `after`
                 * functions, scope and an argument list or simply the `before` function.
                 * @param {Function} [spec.before] A function to call on a node *before*
                 * cascading down into child nodes. If it returns `false`, the child nodes
                 * are not traversed.
                 * @param {Function} [spec.after] A function to call on a node *after*
                 * cascading down into child nodes.
                 * @param {Object} [spec.scope] The scope (this reference) in which the
                 * functions are executed. Defaults to the current Node.
                 * @param {Array} [spec.args] The args to call the function with. Defaults
                 * to passing the current Node.
                 * @param {Object} [scope] If `spec` is the `before` function instead of
                 * an object, this argument is the `this` pointer.
                 * @param {Array} [args] If `spec` is the `before` function instead of
                 * an object, this argument is the `args` to pass.
                 * @param {Function} [after] If `spec` is the `before` function instead of
                 * an object, this argument is the `after` function to call.
                 */
                cascade: function(spec, scope, args, after) {
                    var me = this,
                        before = spec,
                        childNodes, length, i;
                    if (arguments.length === 1 && !Ext.isFunction(spec)) {
                        after = spec.after;
                        scope = spec.scope;
                        args = spec.args;
                        before = spec.before;
                    }
                    if (!before || before.apply(scope || me, args || [
                        me
                    ]) !== false) {
                        childNodes = me.childNodes;
                        for (i = 0 , length = childNodes.length; i < length; i++) {
                            childNodes[i].cascade.call(childNodes[i], before, scope, args, after);
                        }
                        if (after) {
                            after.apply(scope || me, args || [
                                me
                            ]);
                        }
                    }
                },
                cascadeBy: function() {
                    return this.cascade.apply(this, arguments);
                },
                /**
                 * Iterates the child nodes of this node, calling the specified function 
                 * with each node. The arguments to the function will be the args 
                 * provided or the current node. If the function returns false at any 
                 * point, the iteration stops.
                 * @param {Function} fn The function to call
                 * @param {Object} [scope] The scope (_this_ reference) in which the 
                 * function is executed. Defaults to the Node on which eachChild is 
                 * called.
                 * @param {Array} [args] The args to call the function with. Defaults to 
                 * passing the current Node.
                 */
                eachChild: function(fn, scope, args) {
                    var childNodes = this.childNodes,
                        length = childNodes.length,
                        i;
                    for (i = 0; i < length; i++) {
                        if (fn.apply(scope || this, args || [
                            childNodes[i]
                        ]) === false) {
                            break;
                        }
                    }
                },
                /**
                 * Finds the first child that has the attribute with the specified value.
                 * @param {String} attribute The attribute name
                 * @param {Object} value The value to search for
                 * @param {Boolean} [deep=false] True to search through nodes deeper than the
                 * immediate children
                 * @return {Ext.data.NodeInterface} The found child or null if none was found
                 */
                findChild: function(attribute, value, deep) {
                    return this.findChildBy(function() {
                        return this.get(attribute) == value;
                    }, // eslint-disable-line eqeqeq
                    null, deep);
                },
                /**
                 * Finds the first child by a custom function. The child matches if the function
                 * passed returns true.
                 * @param {Function} fn A function which must return true if the passed Node is the
                 * required Node.
                 * @param {Object} [scope] The scope (this reference) in which the function is
                 * executed. Defaults to the Node being tested.
                 * @param {Boolean} [deep=false] True to search through nodes deeper than the
                 * immediate children
                 * @return {Ext.data.NodeInterface} The found child or null if none was found
                 */
                findChildBy: function(fn, scope, deep) {
                    var cs = this.childNodes,
                        i, len, n, res;
                    for (i = 0 , len = cs.length; i < len; i++) {
                        n = cs[i];
                        if (fn.call(scope || n, n) === true) {
                            return n;
                        } else if (deep) {
                            res = n.findChildBy(fn, scope, deep);
                            if (res !== null) {
                                return res;
                            }
                        }
                    }
                    return null;
                },
                /**
                 * Returns true if this node is an ancestor (at any point) of the passed node.
                 * @param {Ext.data.NodeInterface} node
                 * @return {Boolean}
                 */
                contains: function(node) {
                    return node.isAncestor(this);
                },
                /**
                 * Returns true if the passed node is an ancestor (at any point) of this node.
                 * @param {Ext.data.NodeInterface} node
                 * @return {Boolean}
                 */
                isAncestor: function(node) {
                    var p = this.parentNode;
                    while (p) {
                        if (p === node) {
                            return true;
                        }
                        p = p.parentNode;
                    }
                    return false;
                },
                /**
                 * Sorts this nodes children using the supplied sort function.
                 * @param {Function} [sortFn] A function which, when passed two Nodes, returns -1,
                 * 0 or 1 depending upon required sort order.
                 *
                 * It omitted, the node is sorted according to the existing sorters in the owning
                 * {@link Ext.data.TreeStore TreeStore}.
                 * @param {Boolean} [recursive=false] True to apply this sort recursively
                 * @param {Boolean} [suppressEvent=false] True to not fire a sort event.
                 */
                sort: function(sortFn, recursive, suppressEvent) {
                    var me = this,
                        childNodes = me.childNodes,
                        ln = childNodes.length,
                        info = {
                            isFirst: true
                        },
                        i, n;
                    if (ln > 0) {
                        if (!sortFn) {
                            sortFn = me.getTreeStore().getSortFn();
                        }
                        Ext.Array.sort(childNodes, sortFn);
                        me.setFirstChild(childNodes[0]);
                        me.setLastChild(childNodes[ln - 1]);
                        for (i = 0; i < ln; i++) {
                            n = childNodes[i];
                            n.previousSibling = childNodes[i - 1];
                            n.nextSibling = childNodes[i + 1];
                            // Update the index and first/last status of children
                            info.isLast = (i === ln - 1);
                            info.index = i;
                            n.updateInfo(false, info);
                            info.isFirst = false;
                            if (recursive && !n.isLeaf()) {
                                n.sort(sortFn, true, true);
                            }
                        }
                        // The suppressEvent flag is basically used to indicate a recursive sort
                        if (suppressEvent !== true) {
                            me.fireBubbledEvent('sort', [
                                me,
                                childNodes
                            ]);
                            // Inform the TreeStore that this node is sorted
                            me.callTreeStore('onNodeSort', [
                                childNodes
                            ]);
                        }
                    }
                },
                /**
                 * Returns `true` if this node is expanded.
                 * @return {Boolean}
                 */
                isExpanded: function() {
                    return this.get('expanded');
                },
                /**
                 * Returns true if this node is loaded
                 * @return {Boolean}
                 */
                isLoaded: function() {
                    return this.get('loaded');
                },
                /**
                 * Returns true if this node is a branch node, and the entire branch is fully
                 * loaded.
                 *
                 * Using this method, it is possible to ascertain whether an
                 * `expandAll()` call (_classic toolkit TreePanel method_) will have 
                 * access to all descendant nodes without incurring a store load.
                 * @return {Boolean}
                 */
                isBranchLoaded: function() {
                    var isBranchLoaded = !this.isLeaf() && this.isLoaded();
                    if (isBranchLoaded) {
                        this.cascade(function(node) {
                            if (!node.isLeaf()) {
                                isBranchLoaded = isBranchLoaded || node.isBranchLoaded();
                            }
                            return isBranchLoaded;
                        });
                    }
                    return isBranchLoaded;
                },
                /**
                 * Returns true if this node is loading
                 * @return {Boolean}
                 */
                isLoading: function() {
                    return this.get('loading');
                },
                /**
                 * Returns true if this node is the root node
                 * @return {Boolean}
                 */
                isRoot: function() {
                    return !this.parentNode;
                },
                /**
                 * Returns true if this node is visible. Note that visibility refers to
                 * the structure of the tree, the {@link Ext.tree.Panel#rootVisible}
                 * configuration is not taken into account here. If this method is called
                 * on the root node, it will always be visible.
                 * @return {Boolean}
                 */
                isVisible: function() {
                    var parent = this.parentNode;
                    while (parent) {
                        if (!parent.isExpanded()) {
                            return false;
                        }
                        parent = parent.parentNode;
                    }
                    return true;
                },
                /**
                 * Expand this node.
                 * @param {Boolean} [recursive=false] True to recursively expand all the children
                 * @param {Function} [callback] The function to execute once the expand completes
                 * @param {Object} [scope] The scope to run the callback in
                 */
                expand: function(recursive, callback, scope) {
                    var me = this,
                        treeStore, resumeAddEvent;
                    // all paths must call the callback (eventually) or things like
                    // selectPath fail
                    // First we start by checking if this node is a parent
                    if (!me.isLeaf()) {
                        // If it's loading, wait until it loads before proceeding
                        if (me.isLoading()) {
                            me.on('expand', function() {
                                me.expand(recursive, callback, scope);
                            }, me, {
                                single: true
                            });
                        } else {
                            // Now we check if this record is already expanding or expanded
                            if (!me.isExpanded()) {
                                if (me.fireBubbledEvent('beforeexpand', [
                                    me
                                ]) !== false) {
                                    // Here we are testing if all the descendant nodes required
                                    // by a recursive expansion are available without an
                                    // asynchronous store load.
                                    //
                                    // That is either all branch nodes are loaded, or the store
                                    // loads synchronously.
                                    //
                                    // If that is the case, then we do not want the TreeStore to
                                    // fire add events and update the UI (and layout) for every
                                    // batch of child nodes inserted.
                                    // Instead, we suspend the add event, and at the end, fire
                                    // a data refresh so that the UI gets only one update. It will
                                    // be a view refresh, but will still be more efficient.
                                    if (recursive) {
                                        // Only the topmost node in a recursive expand should
                                        // suspend the add event and fire the refresh event, so if
                                        // our parent is synchronously, recursively expanding,
                                        // we just flag that we are doing likewise.
                                        if (me.parentNode && me.parentNode.isSynchronousRecursiveExpand) {
                                            me.isSynchronousRecursiveExpand = true;
                                        } else {
                                            treeStore = me.getTreeStore();
                                            if (treeStore.getProxy().isSynchronous || me.isBranchLoaded()) {
                                                me.isSynchronousRecursiveExpand = true;
                                                treeStore.suspendEvent('add', 'datachanged');
                                                resumeAddEvent = true;
                                            }
                                        }
                                    }
                                    // Inform the TreeStore that we intend to expand, and that
                                    // it should call onChildNodesAvailable when the child nodes
                                    // are available
                                    /* eslint-disable-next-line max-len */
                                    me.callTreeStore('onBeforeNodeExpand', [
                                        me.onChildNodesAvailable,
                                        me,
                                        [
                                            recursive,
                                            callback,
                                            scope
                                        ]
                                    ]);
                                    // If we suspended the add event so that all additions of
                                    // descendant nodes did not update the UI, then resume the
                                    // event here, and refresh the data
                                    if (resumeAddEvent) {
                                        treeStore.resumeEvent('add', 'datachanged');
                                        // Fire the generic datachanged event in addition to the
                                        // refresh event
                                        treeStore.fireEvent('datachanged', treeStore);
                                        treeStore.fireEvent('refresh', treeStore);
                                    }
                                    me.isSynchronousRecursiveExpand = false;
                                }
                            } else if (recursive) {
                                // If it is is already expanded but we want to recursively expand
                                // then call expandChildren
                                me.expandChildren(true, callback, scope);
                            } else {
                                Ext.callback(callback, scope || me, [
                                    me.childNodes
                                ]);
                            }
                        }
                    } else {
                        // If it's not then we fire the callback right away
                        Ext.callback(callback, scope || me);
                    }
                },
                // leaf = no childNodes
                /**
                 * @private
                 * Called as a callback from the {@link Ext.data.TreeStore#onBeforeNodeExpand} when
                 * the child nodes needed by {@link #method-expand} have been loaded and appended.
                 */
                onChildNodesAvailable: function(records, recursive, callback, scope) {
                    var me = this,
                        treeStore = me.getTreeStore(),
                        bulkUpdate = treeStore && treeStore.bulkUpdate,
                        ancestor, collapsedAncestors, i;
                    // Bracket expansion with layout suspension.
                    // In optimum case, when recursive, child node data are loaded and expansion is
                    // synchronous within the suspension.
                    Ext.suspendLayouts();
                    // Collect collapsed ancestors.
                    // We are going to expand the topmost one while ensuring that
                    // any intervening collapsed nodes have their expanded state as true.
                    for (ancestor = me.parentNode; ancestor; ancestor = ancestor.parentNode) {
                        if (!ancestor.isExpanded()) {
                            (collapsedAncestors || (collapsedAncestors = [])).unshift(ancestor);
                        }
                    }
                    // Not structural. The TreeView's onUpdate listener just updates the [+] icon
                    // to [-] in response.
                    if (bulkUpdate || !treeStore.isVisible(me)) {
                        me.data.expanded = true;
                    } else {
                        me.set('expanded', true);
                    }
                    // Set the intervening collapsed nodes to expanded state, then expand the
                    // topmost.
                    // The whole descendant tree will be inserted into the collection below the
                    // topmost ancestor.
                    if (collapsedAncestors) {
                        // Ensure intervening collapsed nodes have their status set to expanded
                        // Not structural. The TreeView's onUpdate listener just updates the
                        // [+] icon to [-] in response.
                        for (i = 1; i < collapsedAncestors.length; i++) {
                            ancestor = collapsedAncestors[i];
                            if (bulkUpdate || !treeStore.isVisible(ancestor)) {
                                ancestor.data.expanded = true;
                            } else {
                                ancestor.set('expanded', true);
                            }
                        }
                        // Expand the topmost collapsed one.
                        // The correctly set expanded states all the way down will ensure that
                        // All nodes needed are inserted into the Store.
                        collapsedAncestors[0].expand();
                        // Fire the expand event on all those intervening collapsed nodes
                        for (i = 1; i < collapsedAncestors.length; i++) {
                            ancestor = collapsedAncestors[i];
                            ancestor.fireBubbledEvent('expand', [
                                ancestor,
                                ancestor.childNodes
                            ]);
                        }
                    } else {
                        // TreeStore's onNodeExpand inserts the child nodes below the parent
                        me.callTreeStore('onNodeExpand', [
                            records,
                            false
                        ]);
                    }
                    me.fireBubbledEvent('expand', [
                        me,
                        records
                    ]);
                    // Call the expandChildren method if recursive was set to true
                    if (recursive) {
                        me.expandChildren(true, callback, scope);
                    } else {
                        Ext.callback(callback, scope || me, [
                            me.childNodes
                        ]);
                    }
                    Ext.resumeLayouts(true);
                },
                /**
                 * Expand all the children of this node.
                 * @param {Boolean} [recursive=false] True to recursively expand all the children
                 * @param {Function} [callback] The function to execute once all the children are
                 * expanded
                 * @param {Object} [scope] The `this` pointer for the callback.
                 * @param {Boolean} [singleExpand] (private)
                 */
                expandChildren: function(recursive, callback, scope, singleExpand) {
                    var me = this,
                        origCallback, i, allNodes, expandNodes, ln, node, treeStore;
                    // Ext 4.2.0 broke the API for this method by adding a singleExpand argument
                    // at index 1. As of 4.2.3 The method signature has been reverted back
                    // to its original pre-4.2.0 state, however, we must check to see if
                    // the 4.2.0 version is being used for compatibility reasons.
                    if (Ext.isBoolean(callback)) {
                        origCallback = callback;
                        callback = scope;
                        scope = singleExpand;
                        singleExpand = origCallback;
                    }
                    if (singleExpand === undefined) {
                        treeStore = me.getTreeStore();
                        singleExpand = treeStore && treeStore.singleExpand;
                    }
                    allNodes = me.childNodes;
                    expandNodes = [];
                    ln = singleExpand ? Math.min(allNodes.length, 1) : allNodes.length;
                    for (i = 0; i < ln; ++i) {
                        node = allNodes[i];
                        if (!node.isLeaf()) {
                            expandNodes[expandNodes.length] = node;
                        }
                    }
                    ln = expandNodes.length;
                    for (i = 0; i < ln; ++i) {
                        expandNodes[i].expand(recursive);
                    }
                    if (callback) {
                        Ext.callback(callback, scope || me, [
                            me.childNodes
                        ]);
                    }
                },
                /**
                 * Collapse this node.
                 * @param {Boolean} [recursive=false] True to recursively collapse all the children
                 * @param {Function} [callback] The function to execute once the collapse completes
                 * @param {Object} [scope] The scope to run the callback in
                 */
                collapse: function(recursive, callback, scope) {
                    var me = this,
                        expanded = me.isExpanded(),
                        treeStore = me.getTreeStore(),
                        bulkUpdate = treeStore && treeStore.bulkUpdate,
                        len = me.childNodes.length,
                        i, collapseChildren;
                    // If this is a parent and
                    //      already collapsed but the recursive flag is passed to target child nodes
                    //   or
                    //      the collapse is not vetoed by a listener
                    if (!me.isLeaf() && ((!expanded && recursive) || me.fireBubbledEvent('beforecollapse', [
                        me
                    ]) !== false)) {
                        // Bracket collapsing with layout suspension.
                        // Collapsing is synchronous within the suspension.
                        Ext.suspendLayouts();
                        // Inform listeners of a collapse event if we are still expanded.
                        if (me.isExpanded()) {
                            // Set up the callback to set non-leaf descendants to collapsed
                            // if necessary. If recursive, we just need to set all non-leaf
                            // descendants to collapsed state.
                            // We *DO NOT* call collapse on them. That would attempt to remove
                            // their descendants from the UI, and that is done: THIS node
                            // is collapsed - ALL descendants are removed from the UI.
                            // Descendant non-leaves just silently change state.
                            if (recursive) {
                                collapseChildren = function() {
                                    for (i = 0; i < len; i++) {
                                        me.childNodes[i].setCollapsed(true);
                                    }
                                };
                                if (callback) {
                                    /* eslint-disable-next-line max-len */
                                    callback = Ext.Function.createSequence(collapseChildren, Ext.Function.bind(callback, scope, [
                                        me.childNodes
                                    ]));
                                } else {
                                    callback = collapseChildren;
                                }
                            } else if (callback) {
                                callback = Ext.Function.bind(callback, scope, [
                                    me.childNodes
                                ]);
                            }
                            // Not structural. The TreeView's onUpdate listener just updates the
                            // [+] icon to [-] in response.
                            if (bulkUpdate || !treeStore.contains(me)) {
                                me.data.expanded = false;
                            } else {
                                me.set('expanded', false);
                            }
                            // Call the TreeStore's onNodeCollapse which removes all descendant
                            // nodes to achieve UI collapse and passes callback on in its
                            // beforecollapse event which is poked into the animWrap for
                            // final calling in the animation callback.
                            me.callTreeStore('onNodeCollapse', [
                                me.childNodes,
                                callback,
                                scope
                            ]);
                            me.fireBubbledEvent('collapse', [
                                me,
                                me.childNodes
                            ]);
                            // So that it's not called at the end
                            callback = null;
                        }
                        // If recursive, we just need to set all non-leaf descendants to collapsed
                        // state. We *DO NOT* call collapse on them. That would attempt to remove
                        // their descendants from the UI, and that is done: THIS node is collapsed -
                        // ALL descendants are removed from the UI.
                        // Descendant non-leaves just silently change state.
                        else if (recursive) {
                            for (i = 0; i < len; i++) {
                                me.childNodes[i].setCollapsed(true);
                            }
                        }
                        Ext.resumeLayouts(true);
                    }
                    // Call the passed callback
                    Ext.callback(callback, scope || me, [
                        me.childNodes
                    ]);
                },
                /**
                 * @private
                 *
                 * Sets the node into the collapsed state without affecting the UI.
                 *
                 * This is called when a node is collapsed with the recursive flag. All the
                 * descendant nodes will have been removed from the store, but descendant non-leaf
                 * nodes still need to be set to the collapsed state without affecting the UI.
                 */
                setCollapsed: function(recursive) {
                    var me = this,
                        len = me.childNodes.length,
                        i;
                    // Only if we are not a leaf node and the collapse was not vetoed by a listener.
                    if (!me.isLeaf() && me.fireBubbledEvent('beforecollapse', [
                        me
                    ]) !== false) {
                        // Update the state directly.
                        me.data.expanded = false;
                        // Listened for by NodeStore.onNodeCollapse, but will do nothing except
                        // pass on the documented events because the records have already been
                        // removed from the store when the ancestor node was collapsed.
                        me.fireBubbledEvent('collapse', [
                            me,
                            me.childNodes
                        ]);
                        if (recursive) {
                            for (i = 0; i < len; i++) {
                                me.childNodes[i].setCollapsed(true);
                            }
                        }
                    }
                },
                /**
                 * Collapse all the children of this node.
                 * @param {Function} [recursive=false] True to recursively collapse all the children
                 * @param {Function} [callback] The function to execute once all the children are
                 * collapsed
                 * @param {Object} [scope] The scope to run the callback in
                 */
                collapseChildren: function(recursive, callback, scope) {
                    var me = this,
                        allNodes = me.childNodes,
                        ln = allNodes.length,
                        collapseNodes = [],
                        node, i;
                    // Only bother with loaded, expanded, non-leaf nodes
                    for (i = 0; i < ln; ++i) {
                        node = allNodes[i];
                        if (!node.isLeaf() && node.isLoaded() && node.isExpanded()) {
                            collapseNodes.push(node);
                        }
                    }
                    ln = collapseNodes.length;
                    if (ln) {
                        // Collapse the collapsible children.
                        // Pass our callback to the last one.
                        for (i = 0; i < ln; ++i) {
                            node = collapseNodes[i];
                            if (i === ln - 1) {
                                node.collapse(recursive, callback, scope);
                            } else {
                                node.collapse(recursive);
                            }
                        }
                    } else {
                        // Nothing to collapse, so fire the callback
                        Ext.callback(callback, scope);
                    }
                },
                /**
                 * Fires the specified event with the passed parameters (minus the event name,
                 * plus the `options` object passed to
                 * {@link Ext.mixin.Observable#addListener addListener}).
                 *
                 * An event may be set to bubble up an Observable parent hierarchy (See
                 * {@link Ext.Component#getBubbleTarget}) by
                 * calling {@link Ext.mixin.Observable#enableBubble enableBubble}.
                 *
                 * @param {String} eventName The name of the event to fire.
                 * @param {Object...} args Variable number of parameters are passed to handlers.
                 * @return {Boolean} returns `false` if any of the handlers return `false`
                 * otherwise it returns `true`.
                 */
                fireEvent: function(eventName) {
                    return this.fireBubbledEvent(eventName, Ext.Array.slice(arguments, 1));
                },
                // Node events always bubble, but events which bubble are always created,
                // so bubble in a loop and only fire when there are listeners at each level.
                // bubbled events always fire because they cannot tell if there is a listener
                // at each level.
                fireBubbledEvent: function(eventName, args) {
                    var result, eventSource, topNode;
                    // The event bubbles (all native NodeInterface events do)...
                    if (bubbledEvents[eventName]) {
                        /* eslint-disable-next-line max-len */
                        for (eventSource = this; result !== false && eventSource; eventSource = (topNode = eventSource).parentNode) {
                            result = eventSource.fireEventArgs.call(eventSource, eventName, args);
                        }
                        // We hit the topmost node in the loop above.
                        // Fire the event on its TreeStore if any (might be a disembodied
                        // tree fragment with no TreeStore)
                        if (result !== false) {
                            eventSource = topNode.getTreeStore();
                            if (eventSource && eventSource.hasListeners && eventSource.hasListeners[eventName = 'node' + eventName]) {
                                result = eventSource.fireEventArgs(eventName, args);
                            }
                        }
                        return result;
                    } else // Event does not bubble.
                    {
                        return this.fireEventArgs.apply(this, arguments);
                    }
                },
                /**
                 * Creates an object representation of this node including its children.
                 */
                serialize: function(writerParam) {
                    var writer = writerParam || new Ext.data.writer.Json({
                            writeAllFields: true
                        }),
                        result = writer.getRecordData(this),
                        childNodes = this.childNodes,
                        len = childNodes.length,
                        children, i;
                    if (len > 0) {
                        result.children = children = [];
                        for (i = 0; i < len; i++) {
                            children.push(childNodes[i].serialize(writer));
                        }
                    }
                    return result;
                },
                // Used to inform the TreeStore that we belong to about some event which requires
                // its participation.
                callTreeStore: function(funcName, args) {
                    var me = this,
                        target = me.getTreeStore(),
                        fn = target && target[funcName];
                    if (target && fn) {
                        args = args || [];
                        if (args[0] !== me) {
                            args.unshift(me);
                        }
                        fn.apply(target, args);
                    }
                },
                addCls: function(cls) {
                    this.replaceCls(null, cls);
                },
                removeCls: function(cls) {
                    this.replaceCls(cls);
                },
                replaceCls: function(oldCls, newCls) {
                    var pieces = this._parseCls(this.data.cls),
                        parts = this._parseCls(oldCls);
                    if (parts.length) {
                        pieces = Ext.Array.difference(pieces, parts);
                    }
                    parts = this._parseCls(newCls);
                    if (parts.length) {
                        pieces = Ext.Array.unique(pieces.concat(parts));
                    }
                    this.set('cls', pieces.join(' '));
                },
                toggleCls: function(cls, state) {
                    var pieces, parts, len, i, p;
                    if (state === undefined) {
                        pieces = this._parseCls(this.data.cls);
                        parts = this._parseCls(cls);
                        for (i = 0 , len = parts.length; i < len; ++i) {
                            p = parts[i];
                            if (Ext.Array.contains(pieces, p)) {
                                Ext.Array.remove(pieces, p);
                            } else {
                                pieces.push(p);
                            }
                        }
                        this.set('cls', pieces.join(' '));
                    } else if (state) {
                        this.addCls(cls);
                    } else {
                        this.removeCls(cls);
                    }
                },
                // Override private methods from Model superclass
                privates: {
                    _noCls: [],
                    spacesRe: /\s+/,
                    join: function(store) {
                        // Only the root node is linked to the TreeStore
                        if (store.isTreeStore) {
                            if (this.isRoot()) {
                                this.treeStore = this.store = store;
                            }
                        } else // Other stores are always joined.
                        // So a tree node could also be used by a flat store linked to a DataView
                        {
                            this.callParent([
                                store
                            ]);
                        }
                    },
                    // Used by Model base class methods to inform all interested Stores that the
                    // record has been mutated.
                    callJoined: function(funcName, args) {
                        this.callParent([
                            funcName,
                            args
                        ]);
                        this.callTreeStore(funcName, args);
                    },
                    _parseCls: function(cls) {
                        if (!cls) {
                            return this._noCls;
                        }
                        if (typeof cls === 'string') {
                            return cls.split(this.spacesRe);
                        }
                        return cls;
                    }
                }
            };
        }
    }
});

/**
 * @private
 * A mixin for providing query related methods for {@link Ext.ComponentQuery} for classes that
 * implement getRefItems.
 */
Ext.define('Ext.mixin.Queryable', {
    mixinId: 'queryable',
    isQueryable: true,
    /**
     * Retrieves all descendant components which match the passed selector.
     * Executes an Ext.ComponentQuery.query using this container as its root.
     * @param {String} [selector] Selector complying to an Ext.ComponentQuery selector.
     * If no selector is specified all items will be returned.
     * @return {Ext.Component[]} Components which matched the selector
     */
    query: function(selector) {
        selector = selector || '*';
        return Ext.ComponentQuery.query(selector, this.getQueryRoot());
    },
    /**
     * Retrieves all descendant components which match the passed function.
     * The function should return false for components that are to be
     * excluded from the selection.
     * @param {Function} fn The matcher function. It will be called with a single argument,
     * the component being tested.
     * @param {Object} [scope] The scope in which to run the function. If not specified,
     * it will default to the active component.
     * @return {Ext.Component[]} Components matched by the passed function
     */
    queryBy: function(fn, scope) {
        var out = [],
            items = this.getQueryRoot().getRefItems(true),
            i = 0,
            len = items.length,
            item;
        for (; i < len; ++i) {
            item = items[i];
            if (fn.call(scope || item, item) !== false) {
                out.push(item);
            }
        }
        return out;
    },
    /**
     * Finds a component at any level under this container matching the id/itemId.
     * This is a shorthand for calling ct.down('#' + id);
     * @param {String} id The id to find
     * @return {Ext.Component} The matching id, null if not found
     */
    queryById: function(id) {
        return this.down(Ext.makeIdSelector(id));
    },
    /**
     * Retrieves the first direct child of this container which matches the passed selector
     * or component. The passed in selector must comply with an Ext.ComponentQuery selector,
     * or it can be an actual Ext.Component.
     * @param {String/Ext.Component} [selector] An Ext.ComponentQuery selector. If no selector is
     * specified, the first child will be returned.
     * @return {Ext.Component} The matching child Ext.Component (or `null` if no match was found).
     */
    child: function(selector) {
        var children = this.getQueryRoot().getRefItems();
        if (selector && selector.isComponent) {
            return this.matchById(children, selector.getItemId());
        }
        // Filter children array to only matches.
        if (selector) {
            children = Ext.ComponentQuery.query(selector, children);
        }
        // Return first match
        if (children.length) {
            return children[0];
        }
        return null;
    },
    /**
     * Retrieves the first descendant of this container which matches the passed selector.
     * The passed in selector must comply with an Ext.ComponentQuery selector, or it can be
     * an actual Ext.Component.
     * @param {String/Ext.Component} [selector] An Ext.ComponentQuery selector or Ext.Component.
     * If no selector is
     * specified, the first child will be returned.
     * @return {Ext.Component} The matching descendant Ext.Component (or `null` if no match
     * was found).
     */
    down: function(selector) {
        if (selector && selector.isComponent) {
            return this.matchById(this.getRefItems(true), selector.getItemId());
        }
        selector = selector || '';
        return this.query(selector)[0] || null;
    },
    /**
     * Traverses the tree rooted at this node in pre-order mode, calling the passed function
     * on the nodes at each level. That is the function is called upon each node **before**
     * being called on its children).
     *
     * This method is used at each level down the cascade. Currently
     * {@link Ext.Component Component}s and {@link Ext.data.TreeModel TreeModel}s are queryable.
     *
     * If you have tree-structured data, you can make your nodes queryable, and use ComponentQuery
     * on them.
     *
     * @param {Object} selector A ComponentQuery selector used to filter candidate nodes before
     * calling the function. An empty string matches any node.
     * @param {Function} fn The function to call. Return `false` to abort the traverse.
     * @param {Object} fn.node The node being visited.
     * @param {Object} [scope] The context (`this` reference) in which the function is executed.
     * @param {Array} [extraArgs] A set of arguments to be appended to the function's argument list
     * to pass down extra data known to the caller **after** the node being visited.
     */
    visitPreOrder: function(selector, fn, scope, extraArgs) {
        Ext.ComponentQuery._visit(true, selector, this.getQueryRoot(), fn, scope, extraArgs);
    },
    /**
     * Traverses the tree rooted at this node in post-order mode, calling the passed function
     * on the nodes at each level. That is the function is called upon each node **after** being
     * called on its children).
     *
     * This method is used at each level down the cascade. Currently
     * {@link Ext.Component Component}s and {@link Ext.data.TreeModel TreeModel}s are queryable.
     *
     * If you have tree-structured data, you can make your nodes queryable, and use ComponentQuery
     * on them.
     *
     * @param {Object} selector A ComponentQuery selector used to filter candidate nodes before
     * calling the function. An empty string matches any node.
     * @param {Function} fn The function to call. Return `false` to abort the traverse.
     * @param {Object} fn.node The node being visited.
     * @param {Object} [scope] The context (`this` reference) in which the function is executed.
     * @param {Array} [extraArgs] A set of arguments to be appended to the function's argument list
     * to pass down extra data known to the caller **after** the node being visited.
     */
    visitPostOrder: function(selector, fn, scope, extraArgs) {
        Ext.ComponentQuery._visit(false, selector, this.getQueryRoot(), fn, scope, extraArgs);
    },
    getRefItems: function() {
        return [];
    },
    getQueryRoot: function() {
        return this;
    },
    privates: {
        matchById: function(items, id) {
            var len = items.length,
                i, item;
            for (i = 0; i < len; ++i) {
                item = items[i];
                if (item.getItemId() === id) {
                    return item;
                }
            }
            return null;
        }
    }
});

/**
 * This class is used as a base class from which to derive Models used in Trees.
 */
Ext.define('Ext.data.TreeModel', {
    extend: 'Ext.data.Model',
    requires: [
        'Ext.data.NodeInterface'
    ],
    mixins: [
        'Ext.mixin.Queryable'
    ],
    /**
     * @cfg {String} [childType]
     * The class name of child nodes to create when reading child nodes from
     * raw data. By default the type configured into the TreeStore is used.
     *
     * This is one way of creating heterogeneous nodes in a tree.
     *
     * To do this through data types passed from the server, use the
     * {@link Ext.data.reader.Reader#typeProperty}.
     *
     * for example in the case of a hidden root node, you'd use the default type at level zero.
     * See {@link Ext.tree.Panel TreePanel}'s documentation for an example.
     *
     * *Important*
     * If you are using this declaration on your tree models, and have a
     * {@link Ext.tree.Panel#rootVisible hidden root node}, you MUST create a special root model
     * definition which declares the type of its children.
     *
     * If you allow the TreeStore to create a root node of the same type as the first level of
     * *visible* nodes then the reader will atempt to read the wrong type of child node for the
     * root.
     *
     * Example:
     *
     *     Ext.define('myApp.World', {
     *         childType: 'Territory'
     *     });
     *
     *     ...
     *
     *     store: {
     *         id: 'myTreeStore',
     *         model: 'myApp.World' // The hidden root will know to create 'Territory' type children
     *     }
     *
     * If the root is hidden, and the first level of visible nodes are going to be the
     * `myApp.Territory` class, then the hidden root must not be of the `myApp.Territory` class.
     * Otherwise, it would try to read in the territory data as its childType - most likely
     * 'Country'.
     *
     */
    getRefItems: function() {
        return this.childNodes;
    },
    getRefOwner: function() {
        return this.parentNode;
    },
    statics: {
        defaultProxy: 'memory'
    }
}, function() {
    Ext.data.NodeInterface.decorate(this);
});

/**
 * Node Store
 * @private
 */
Ext.define('Ext.data.NodeStore', {
    extend: 'Ext.data.Store',
    alias: 'store.node',
    requires: [
        'Ext.data.TreeModel',
        'Ext.data.NodeInterface'
    ],
    /**
     * @property {Boolean} isNodeStore
     * `true` in this class to identify an object as an instantiated NodeStore, or subclass thereof.
     */
    isNodeStore: true,
    config: {
        /**
         * @cfg {Ext.data.Model} node The Record you want to bind this Store to. Note that
         * this record will be decorated with the {@link Ext.data.NodeInterface} if this is not the
         * case yet.
         * @accessor
         */
        node: null,
        /**
         * @cfg {Boolean} recursive Set this to `true` if you want this NodeStore to represent
         * all the descendants of the node in its flat data collection. This is useful for
         * rendering a tree structure to a DataView and is being used internally by
         * the TreeView. Any records that are moved, removed, inserted or appended to the
         * node at any depth below the node this store is bound to will be automatically
         * updated in this Store's internal flat data structure.
         * @accessor
         */
        recursive: false,
        /**
         * @cfg {Boolean} rootVisible `false` to not include the root node in this Stores
         * collection.
         * @accessor
         */
        rootVisible: false,
        /**
         * @cfg {Boolean} folderSort
         * Set to `true` to automatically prepend a leaf sorter.
         */
        folderSort: false
    },
    implicitModel: 'Ext.data.TreeModel',
    // NodeStores are never buffered or paged. They are loaded from the TreeStore to reflect all
    // visible nodes.
    // BufferedRenderer always asks for the *total* count, so this must return the count.
    getTotalCount: function() {
        return this.getCount();
    },
    updateFolderSort: function(folderSort) {
        var data = this.getData();
        data.setTrackGroups(false);
        if (folderSort) {
            data.setGrouper({
                groupFn: this.folderSortFn
            });
        } else {
            data.setGrouper(null);
        }
    },
    folderSortFn: function(node) {
        return node.data.leaf ? 1 : 0;
    },
    afterReject: function(record) {
        var me = this;
        // Must pass the 5th param (modifiedFieldNames) as null, otherwise the
        // event firing machinery appends the listeners "options" object to the arg list
        // which may get used as the modified fields array by a handler.
        // This array is used for selective grid cell updating by Grid View.
        // Null will be treated as though all cells need updating.
        if (me.contains(record)) {
            me.onUpdate(record, Ext.data.Model.REJECT, null);
            me.fireEvent('update', me, record, Ext.data.Model.REJECT, null);
        }
    },
    afterCommit: function(record, modifiedFieldNames) {
        var me = this;
        if (!modifiedFieldNames) {
            modifiedFieldNames = null;
        }
        if (me.contains(record)) {
            me.onUpdate(record, Ext.data.Model.COMMIT, modifiedFieldNames);
            me.fireEvent('update', me, record, Ext.data.Model.COMMIT, modifiedFieldNames);
        }
    },
    onNodeAppend: function(parent, node) {
        if (parent === this.getNode()) {
            this.add([
                node
            ].concat(this.retrieveChildNodes(node)));
        }
    },
    onNodeInsert: function(parent, node, refNode) {
        var me = this;
        if (parent === me.getNode()) {
            me.insert(0, [
                node
            ].concat(me.retrieveChildNodes(node)));
        }
    },
    onNodeRemove: function(parent, node) {
        if (parent === this.getNode()) {
            this.remove([
                node
            ].concat(this.retrieveChildNodes(node)));
        }
    },
    onNodeExpand: function(parent, records) {
        if (parent === this.getNode()) {
            this.loadRecords(records);
        }
    },
    applyNode: function(node) {
        if (node) {
            if (!node.isModel) {
                node = new (this.getModel())(node);
            }
            if (!node.isNode) {
                Ext.data.NodeInterface.decorate(node);
            }
        }
        return node;
    },
    updateNode: function(node, oldNode) {
        var me = this,
            data;
        if (oldNode && !oldNode.destroyed) {
            oldNode.un({
                append: 'onNodeAppend',
                insert: 'onNodeInsert',
                remove: 'onNodeRemove',
                scope: me
            });
            oldNode.unjoin(me);
        }
        if (node) {
            node.on({
                scope: me,
                append: 'onNodeAppend',
                insert: 'onNodeInsert',
                remove: 'onNodeRemove'
            });
            node.join(me);
            data = [];
            if (node.childNodes.length) {
                data = data.concat(me.retrieveChildNodes(node));
            }
            if (me.getRootVisible()) {
                data.push(node);
            } else if (node.isLoaded() || node.isLoading()) {
                node.set('expanded', true);
            }
            me.getData().clear();
            me.fireEvent('clear', me);
            me.suspendEvents();
            if (me.isInitializing) {
                me.inlineData = data;
            } else {
                me.add(data);
            }
            me.resumeEvents();
            if (data.length === 0) {
                me.loaded = node.loaded = true;
            }
            me.fireEvent('refresh', me, me.data);
        }
    },
    /**
     * @param {Object} node
     * @return {Boolean}
     */
    isVisible: function(node) {
        var parent = node.parentNode;
        if (!this.getRecursive() && parent !== this.getNode()) {
            return false;
        }
        while (parent) {
            if (!parent.isExpanded()) {
                return false;
            }
            // we need to check this because for a nodestore the node is not likely to be the root
            // so we stop going up the chain when we hit the original node as we don't care about
            // any ancestors above the configured node
            if (parent === this.getNode()) {
                break;
            }
            parent = parent.parentNode;
        }
        return true;
    },
    privates: {
        /**
         * Private method used to deeply retrieve the children of a record without recursion.
         * @private
         * @param {Ext.data.NodeInterface} root
         * @return {Ext.data.NodeInterface[]}
         */
        retrieveChildNodes: function(root) {
            var node = this.getNode(),
                recursive = this.getRecursive(),
                added = [],
                child = root;
            if (!root.childNodes.length || (!recursive && root !== node)) {
                return added;
            }
            if (!recursive) {
                return root.childNodes;
            }
            while (child) {
                if (child._added) {
                    delete child._added;
                    if (child === root) {
                        break;
                    } else {
                        child = child.nextSibling || child.parentNode;
                    }
                } else {
                    if (child !== root) {
                        added.push(child);
                    }
                    if (child.firstChild) {
                        child._added = true;
                        child = child.firstChild;
                    } else {
                        child = child.nextSibling || child.parentNode;
                    }
                }
            }
            return added;
        }
    }
});

/**
 * This singleton provides the ability to convert the parse tree of an `Ext.data.Query`
 * (that is, its `ast` or Abstract Syntax Tree) into a `Function` that accepts an object
 * or {@link Ext.data.Model record} and returns a `Boolean` if the query matches it.
 * @private
 * @since 6.7.0
 */
Ext.define('Ext.data.query.Compiler', {
    compile: function() {
        /*
            O = Operation helpers
            F = Functions

            function create (Ext, O, F) {
                return function (item) {
                    return (
                        4
                        && (

                        )
                    );
                };
            }
        */
        var me = this,
            ast = me.ast,
            body, factory, vars;
        me.error = null;
        if (!ast) {
            me.fn = Ext.returnTrue;
        } else {
            body = [
                'return function (item) {',
                '\tvar rec = item.isEntity && item;',
                '\treturn '
            ];
            vars = [];
            me.query = me;
            me.assemble(body, vars, '\t', ast);
            body.push('}');
            body = vars.concat(body).join('\n');
            try {
                factory = new Function('Ext', 'O', 'F', body);
                me.fn = factory(Ext, me.operators, me.getFunctions());
                me.fn.generation = me.generation;
            } catch (e) {
                me.error = e;
                e.message = 'Failed to compile: ' + e.message;
                throw e;
            } finally {
                me.query = null;
            }
        }
    },
    privates: {
        asmOps: {
            '>': 'gt',
            '<': 'lt',
            '==': 'eq',
            '>=': 'ge',
            '<=': 'le',
            '!=': 'ne'
        },
        assemblers: {
            binary: function(me, body, vars, indent, node, last, childIndent) {
                var op = me.operatorTypeMap[node.type][1],
                    asmOp = me.asmOps[op],
                    operands = node.on,
                    close = '',
                    i;
                if (asmOp) {
                    body[last] += 'O.' + asmOp + '(';
                    op = ', ';
                    close = ')';
                } else {
                    op = ' ' + op + ' ';
                }
                body[last] += '(';
                for (i = 0; i < operands.length; ++i) {
                    if (i) {
                        body.push(indent + ')' + op + '(');
                    }
                    body.push(childIndent);
                    me.assemble(body, vars, childIndent, operands[i]);
                }
                body.push(indent + ')' + close);
            },
            between: function(me, body, vars, indent, node, last, childIndent) {
                var operands = node.on,
                    i;
                body[last] += 'O.between(';
                for (i = 0; i < 3; ++i) {
                    if (i) {
                        last = body.length - 1;
                        body[last] += ', ';
                    }
                    me.assemble(body, vars, childIndent, operands[i]);
                }
                body.push(indent + ')');
            },
            fn: function(me, body, vars, indent, node, last, childIndent) {
                var fn = node.fn.toLowerCase(),
                    func = me.query.getFunctions(),
                    exprs, i;
                if (!func[fn]) {
                    Ext.raise('Unsupported function "' + node.fn + '"');
                }
                func = func[fn];
                if (func.vargs) {
                    body[last] += 'F.' + fn + '.fn([';
                } else {
                    body[last] += 'F.' + fn + '.fn(';
                }
                exprs = node.args;
                for (i = 0; i < exprs.length; ++i) {
                    if (i) {
                        last = body.length - 1;
                        body[last] += ', ';
                    }
                    body.push(childIndent);
                    me.assemble(body, vars, childIndent, exprs[i]);
                }
                if (func.vargs) {
                    body.push(indent + '])');
                } else {
                    body.push(indent + ')');
                }
            },
            id: function(me, body, vars, indent, node, last, childIndent) {
                var v = node.value,
                    exprs = v.split('.');
                if (exprs.length === 1) {
                    body[last] += 'rec ? rec.interpret(' + Ext.JSON.encode(v) + ') : item.' + v;
                } else {
                    v = 'p' + vars.length;
                    vars.push('var ' + v + ' = ' + Ext.JSON.encode(exprs) + ';');
                    body[last] += 'O.dots(item, ' + v + ')';
                }
            },
            'in': function(me, body, vars, indent, node, last, childIndent) {
                var operands = node.on;
                body[last] += 'O.in(';
                me.assemble(body, vars, childIndent, operands[0]);
                last = body.length - 1;
                body[last] += ', ';
                me.assemble(body, vars, childIndent, operands[1]);
                body.push(indent + ')');
            },
            like: function(me, body, vars, indent, node, last, childIndent) {
                var operands = node.on,
                    rhs;
                body[last] += 'O.like(';
                me.assemble(body, vars, childIndent, operands[0]);
                last = body.length - 1;
                body[last] += ', ';
                rhs = operands[1];
                if (rhs.re) {
                    rhs = {
                        type: 'regexp',
                        value: rhs.re,
                        flags: rhs.flags
                    };
                }
                me.assemble(body, vars, childIndent, rhs);
                last = body.length - 1;
                body[last] += ') ';
            },
            list: function(me, body, vars, indent, node, last, childIndent) {
                body[last] += '[';
                // eslint-disable-next-line vars-on-top
                for (var i = 0,
                    exprs = node.value; i < exprs.length; ++i) {
                    if (i) {
                        last = body.length - 1;
                        body[last] += ', ';
                    }
                    body.push(childIndent);
                    me.assemble(body, vars, childIndent, exprs[i]);
                }
                body.push(indent + ']');
            },
            string: 'regexp',
            regexp: function(me, body, vars, indent, node, last) {
                var re = 're' + vars.length;
                vars.push('var ' + re + ' = /' + (node.re || node.value) + '/' + (node.flags || '') + ';');
                body[last] += re;
            },
            unary: function(me, body, vars, indent, node, last, childIndent) {
                var op = me.operatorTypeMap[node.type][1],
                    operands = node.on;
                body[last] += op + '(';
                body.push(childIndent);
                me.assemble(body, vars, childIndent, operands);
                body.push(indent + ')');
            }
        },
        /**
         * These methods are used by the compiled function to process certain operators.
         * @private
         */
        operators: {
            between: function(val, lo, hi) {
                return lo <= val && val <= hi;
            },
            dots: function(item, names) {
                var i, ret;
                if (item.isEntity) {
                    for (ret = item , i = 0; i < names.length; ++i) {
                        if (!ret || !ret.interpret) {
                            ret = undefined;
                            // don't return '', 0 or false
                            break;
                        }
                        ret = ret.interpret(names[i]);
                    }
                } else {
                    for (ret = item , i = 0; i < names.length; ++i) {
                        if (!ret) {
                            ret = undefined;
                            break;
                        }
                        ret = ret[names[i]];
                    }
                }
                return ret;
            },
            'in': function(val, values) {
                return Ext.Array.contains(values, val);
            },
            like: function(val, pat) {
                val = String(val);
                if (typeof pat === 'string') {
                    return !!val && val.toLowerCase().indexOf(pat.toLowerCase()) > -1;
                }
                return pat.test(val);
            },
            //--------------------------------
            eq: function(lhs, rhs) {
                if (lhs && rhs && (lhs instanceof Date || rhs instanceof Date)) {
                    return !Ext.Date.compare(lhs, rhs);
                }
                return lhs == rhs;
            },
            // eslint-disable-line eqeqeq
            ge: function(lhs, rhs) {
                if (lhs && rhs && (lhs instanceof Date || rhs instanceof Date)) {
                    return Ext.Date.compare(lhs, rhs) >= 0;
                }
                return lhs >= rhs;
            },
            gt: function(lhs, rhs) {
                if (lhs && rhs && (lhs instanceof Date || rhs instanceof Date)) {
                    return Ext.Date.compare(lhs, rhs) > 0;
                }
                return lhs > rhs;
            },
            le: function(lhs, rhs) {
                if (lhs && rhs && (lhs instanceof Date || rhs instanceof Date)) {
                    return Ext.Date.compare(lhs, rhs) <= 0;
                }
                return lhs <= rhs;
            },
            lt: function(lhs, rhs) {
                if (lhs && rhs && (lhs instanceof Date || rhs instanceof Date)) {
                    return Ext.Date.compare(lhs, rhs) < 0;
                }
                return lhs < rhs;
            },
            ne: function(lhs, rhs) {
                if (lhs && rhs && (lhs instanceof Date || rhs instanceof Date)) {
                    return !!Ext.Date.compare(lhs, rhs);
                }
                return lhs != rhs;
            }
        },
        // eslint-disable-line eqeqeq
        assemble: function(body, vars, indent, node) {
            var me = this,
                assemblers = me.assemblers,
                t = typeof node,
                last = body.length - 1,
                childIndent = indent + '\t',
                type = node.type,
                arity, asm;
            if (t === 'boolean' || t === 'number') {
                body[last] += node;
            } else if (t === 'string') {
                body[last] += Ext.JSON.encode(node);
            } else {
                arity = me.operatorTypeMap[type];
                asm = assemblers[type] || (arity && assemblers[arity[0]]);
                if (typeof asm === 'string') {
                    asm = assemblers[asm];
                }
                asm(me, body, vars, indent, node, body.length - 1, childIndent);
            }
        }
    }
});

/**
 * This mixin provides the ability to convert the parse tree of an `Ext.data.Query`
 * (that is, its `ast` or Abstract Syntax Tree) into an array of serialized
 * {@link Ext.util.Filter filters}. This singleton can also produce an `ast` from such an
 * array.
 * @private
 * @since 6.7.0
 */
Ext.define('Ext.data.query.Converter', {
    /**
     * Returns the array of serialized {@link Ext.util.Filter filter} objects equivalent
     * to this query if possible. If this query is empty, this method returns `null`. If
     * the query cannot be converted without loss into a filters array, this method will
     * return `undefined`.
     * @return {Object[]}
     */
    getFilters: function() {
        var me = this,
            ast = me.ast,
            exprToFilter = me.exprToFilter,
            operatorTypeMap = me.operatorTypeMap,
            fn = me.fn,
            on = ast && ast.on,
            expr, filter, filters, i, ident, n, op, ret, value, xlat;
        if (ast) {
            // We cache the filters on the fn since they are valid for as long as the
            // fn is valid.
            if (fn.hasOwnProperty('$filters')) {
                ret = fn.$filters;
            } else {
                if (ast.type === 'and' && on) {
                    filters = [];
                    for (i = 0 , n = on.length; i < n; ++i) {
                        expr = on[i];
                        ident = expr.on;
                        if (!ident || ident.length !== 2) {
                            break;
                        }
                        value = ident[1];
                        ident = ident[0];
                        if (ident.type !== 'id') {
                            break;
                        }
                        if (!(xlat = exprToFilter[expr.type])) {
                            op = operatorTypeMap[expr.type];
                            if (!op || !(xlat = exprToFilter[op[0]])) {
                                break;
                            }
                        }
                        if (!(filter = xlat(expr, ident.value, value, op))) {
                            break;
                        }
                        filters.push(filter);
                    }
                    if (i === n) {
                        ret = filters;
                    }
                }
                fn.$filters = ret;
            }
        } else {
            ret = null;
        }
        return ret;
    },
    setFilters: function(filters) {
        var me = this,
            ast = null,
            n = filters && filters.length,
            expr, filter, i, op, xlat;
        if (n) {
            ast = {
                type: 'and',
                on: []
            };
            for (i = 0; i < n; ++i) {
                filter = filters[i];
                if (!(xlat = me.filterToExpr[op = filter.operator])) {
                    expr = {
                        type: me.getOperatorType(op),
                        on: [
                            {
                                type: 'id',
                                value: filter.property
                            },
                            filter.value
                        ]
                    };
                } else {
                    expr = xlat(filter);
                }
                ast.on.push(expr);
            }
        }
        me.ast = ast;
        me.refresh();
    },
    privates: {
        exprToFilter: {
            binary: function(expr, ident, value, info) {
                return Ext.isPrimitive(value) && {
                    property: ident,
                    operator: info[1],
                    value: value
                };
            },
            'in': function(expr, ident, value) {
                var i = 0,
                    list = value.value;
                if (value.type === 'list' && Ext.isArray(list)) {
                    for (i = list.length; i-- > 0; ) /* empty */
                    {
                        if (!Ext.isPrimitive(list[i])) {
                            break;
                        }
                    }
                }
                return (i < 0) && {
                    property: ident,
                    operator: 'in',
                    value: list
                };
            },
            like: function(expr, ident, value) {
                if (value.type === 'regexp') {
                    return {
                        property: ident,
                        operator: '/=',
                        value: value.value
                    };
                }
                return (value.type === 'string') && {
                    property: ident,
                    operator: 'like',
                    value: value.value
                };
            }
        },
        filterToExpr: {
            '/=': function(filter) {
                return {
                    type: 'like',
                    on: [
                        {
                            type: 'id',
                            value: filter.property
                        },
                        {
                            type: 'regexp',
                            value: filter.value
                        }
                    ]
                };
            },
            'in': function(filter) {
                return {
                    type: 'in',
                    on: [
                        {
                            type: 'id',
                            value: filter.property
                        },
                        {
                            type: 'list',
                            value: filter.value
                        }
                    ]
                };
            },
            like: function(filter) {
                return {
                    type: 'like',
                    on: [
                        {
                            type: 'id',
                            value: filter.property
                        },
                        {
                            type: 'string',
                            value: filter.value,
                            re: filter.value,
                            flags: 'i'
                        }
                    ]
                };
            }
        }
    }
});

/**
 * This singleton provides the ability to convert the parse tree of an `Ext.data.Query`
 * (that is, its `ast` or Abstract Syntax Tree) into a `String`.
 * @private
 * @since 6.7.0
 */
Ext.define('Ext.data.query.Stringifier', {
    stringify: function(node) {
        var me = this,
            t = typeof node,
            type = node.type,
            operatorTypeMap = me.operatorTypeMap,
            priority = me.getPriority(node),
            stringifiers = me.stringifiers,
            op, stringifier;
        if (t === 'boolean' || t === 'number') {
            return String(node);
        }
        if (t === 'string') {
            return Ext.JSON.encode(node);
        }
        stringifier = stringifiers[type];
        if (!stringifier && type in operatorTypeMap) {
            op = operatorTypeMap[type];
            stringifier = stringifiers[op[0]];
            op = op[2] || op[1];
        }
        if (typeof stringifier === 'string') {
            stringifier = stringifiers[stringifier];
        }
        return stringifier(me, node, priority, op);
    },
    privates: {
        getPriority: function(node) {
            var symbols = this.symbols,
                operatorTypeMap = this.operatorTypeMap,
                type = node.type,
                ret = 1000000000,
                op;
            if (type === 'between') {
                ret = 0;
                ret = symbols[type].priority;
            } else if (type === 'and' || type === 'or' || type === 'in' || type === 'like') {
                ret = symbols[type].priority;
            } else if (type in operatorTypeMap) {
                op = operatorTypeMap[type];
                ret = symbols[op[1]].priority;
            }
            return ret;
        },
        stringifiers: {
            and: 'or',
            or: function(me, node, priority) {
                var op = (node.type === 'or') ? ' or ' : ' and ',
                    s = '',
                    on = node.on,
                    i, lhs, parenL;
                for (i = 0; i < on.length; ++i) {
                    if (s) {
                        s += op;
                    }
                    lhs = on[i];
                    parenL = me.getPriority(lhs) < priority;
                    lhs = me.stringify(lhs);
                    if (parenL) {
                        lhs = '(' + lhs + ')';
                    }
                    s += lhs;
                }
                return s;
            },
            between: function(me, node, priority) {
                var on = node.on,
                    lhs = on[0],
                    parenL = me.getPriority(lhs) < priority,
                    i, parenR, rhs, s;
                lhs = me.stringify(lhs);
                if (parenL) {
                    lhs = '(' + lhs + ')';
                }
                s = lhs + ' between ';
                priority = me.symbols.and.priority;
                for (i = 0; i < 2; ++i) {
                    if (i) {
                        s += ' and ';
                    }
                    rhs = on[i + 1];
                    parenR = i ? (rhs.type !== 'id' && !Ext.isPrimitive(rhs)) : (me.getPriority(rhs) < priority);
                    rhs = me.stringify(rhs);
                    if (parenR) {
                        rhs = '(' + rhs + ')';
                    }
                    s += rhs;
                }
                return s;
            },
            binary: function(me, node, priority, op) {
                var on = node.on,
                    lhs = on[0],
                    rhs = on[1],
                    parenL = me.getPriority(lhs) < priority,
                    parenR = me.getPriority(rhs) < priority;
                lhs = me.stringify(lhs);
                rhs = me.stringify(rhs);
                if (parenL) {
                    lhs = '(' + lhs + ')';
                }
                if (parenR) {
                    rhs = '(' + rhs + ')';
                }
                return lhs + ' ' + op + ' ' + rhs;
            },
            fn: function(me, node) {
                return node.fn + '(' + me.stringifyArray(node.args) + ')';
            },
            id: function(me, node) {
                return node.value;
            },
            list: function(me, node) {
                return '(' + me.stringifyArray(node.value) + ')';
            },
            regexp: function(me, node) {
                return '/' + node.value + '/' + (node.flags || '');
            },
            string: function(me, node) {
                return Ext.JSON.encode(node.value);
            },
            unary: function(me, node, priority, op) {
                var on = node.on,
                    rhs = me.stringify(on),
                    t = on.type;
                if (t !== 'fn' && t !== 'id' && t !== 'unary') {
                    rhs = '(' + rhs + ')';
                }
                return op + rhs;
            }
        },
        stringifyArray: function(array) {
            var s = '',
                i, expr;
            for (i = 0; i < array.length; ++i) {
                if (s) {
                    s += ', ';
                }
                expr = array[i];
                expr = this.stringify(expr);
                s += expr;
            }
            return s;
        }
    }
});

/**
 * This class provides the ability to parse a `String` to produce the `ast` of an
 * `Ext.data.Query` (that is, its Abstract Syntax Tree).
 * @private
 * @since 6.7.0
 */
Ext.define('Ext.data.query.Parser', function(QueryParser) {
    // eslint-disable-line brace-style
    var LIST = {
            list: true,
            literal: true,
            type: 'list'
        };
    return {
        extend: 'Ext.parse.Parser',
        tokenizer: {
            keywords: {
                and: {
                    type: 'operator',
                    name: 'and',
                    value: '&&',
                    is: {
                        operator: true
                    }
                },
                or: {
                    type: 'operator',
                    name: 'or',
                    value: '||',
                    is: {
                        operator: true
                    }
                },
                not: {
                    type: 'operator',
                    name: 'not',
                    value: '!',
                    is: {
                        operator: true
                    }
                },
                between: {
                    type: 'operator',
                    name: 'between',
                    value: 'between',
                    is: {
                        operator: true
                    }
                },
                like: {
                    type: 'operator',
                    name: 'like',
                    value: 'like',
                    is: {
                        operator: true
                    }
                },
                'in': {
                    type: 'operator',
                    name: 'in',
                    value: 'in',
                    is: {
                        operator: true
                    }
                }
            },
            /* eslint-disable key-spacing */
            operators: {
                '=': 'eq',
                '==': 'seq',
                '===': 'seq',
                '!==': 'sne',
                '!=': 'neq',
                '<>': 'neq',
                '<': 'lt',
                '<=': 'lte',
                '>': 'gt',
                '>=': 'gte',
                '&&': 'and',
                '||': 'or',
                ',': 'comma'
            },
            /* eslint-enable key-spacing */
            patterns: {
                regex: {
                    type: 'literal',
                    is: {
                        literal: true,
                        regexp: true,
                        type: 'regexp'
                    },
                    re: /\/(?!\/)((?:\[.+?]|\\.|[^/\\\r\n])+)\/([gimyu]{0,5})/g,
                    extract: function(match) {
                        var body = match[1],
                            flags = match[2];
                        return flags ? [
                            body,
                            flags
                        ] : body;
                    }
                }
            }
        },
        infix: {
            '=': 40,
            '<>': 40,
            like: 40,
            // TODO '**': 90,  // exponent
            between: {
                priority: 70,
                led: function(left) {
                    var me = this,
                        parser = me.parser;
                    me.arity = 'between';
                    me.operand = left;
                    me.low = parser.parseExpression(parser.symbols.and.priority);
                    parser.advance('&&');
                    me.high = parser.parseExpression(80);
                    return me;
                }
            },
            'in': {
                priority: 40,
                led: function(left) {
                    var me = this,
                        parser = me.parser;
                    parser.advance('(');
                    me.arity = 'binary';
                    me.lhs = left;
                    me.rhs = {
                        arity: 'literal',
                        value: parser.parseList(),
                        is: LIST
                    };
                    parser.advance(')');
                    return me;
                }
            }
        },
        infixRight: {
            'and': 30,
            'or': 30
        },
        prefix: {
            not: 0
        },
        parse: function() {
            var expr = this.parseExpression();
            return this.convert(expr);
        },
        privates: {
            opCodes: {
                binary: {
                    '=': 'eq',
                    '>': 'gt',
                    '<': 'lt',
                    '>=': 'ge',
                    '<=': 'le',
                    '!=': 'ne',
                    '<>': 'ne',
                    '+': 'add',
                    '/': 'div',
                    '*': 'mul',
                    '-': 'sub'
                },
                unary: {
                    '-': 'neg',
                    '!': 'not'
                }
            },
            convert: function(node) {
                var me = this,
                    arity = node.arity,
                    is = node.is,
                    name = node.name,
                    opCodes = me.opCodes,
                    value = node.value,
                    exprs, lhs, rhs, ret;
                switch (arity) {
                    case 'between':
                        ret = {
                            type: 'between',
                            on: [
                                me.convert(node.operand),
                                me.convert(node.low),
                                me.convert(node.high)
                            ]
                        };
                        break;
                    case 'ident':
                        ret = {
                            type: 'id',
                            value: value
                        };
                        break;
                    case 'invoke':
                        ret = {
                            type: 'fn',
                            fn: node.operand.value,
                            args: me.convertArray(node.args)
                        };
                        break;
                    case 'unary':
                        ret = {
                            type: opCodes.unary[value],
                            on: me.convert(node.operand)
                        };
                        break;
                    case 'binary':
                        if (name === 'and' || name === 'or') {
                            lhs = me.convert(node.lhs);
                            rhs = me.convert(node.rhs);
                            if (rhs.type === name) {
                                exprs = rhs.on;
                                exprs.unshift(lhs);
                            } else {
                                exprs = [
                                    lhs,
                                    rhs
                                ];
                            }
                            ret = {
                                type: name,
                                on: exprs
                            };
                        } else {
                            if (value === 'or') {
                                value = '||';
                            }
                            ret = {
                                type: opCodes.binary[value] || name,
                                on: [
                                    me.convert(node.lhs),
                                    me.convert(node.rhs)
                                ]
                            };
                            if (name === 'like') {
                                ret.on[1] = me.likeToRe(ret.on[1], node.rhs.at);
                            }
                        };
                        break;
                    case 'literal':
                        if (is.string || is.number || is.boolean) {
                            ret = value;
                        } else {
                            ret = {
                                type: is.type,
                                value: value
                            };
                            if (is.list) {
                                ret.value = me.convertArray(value);
                            } else if (is.regexp && typeof value !== 'string') {
                                ret.value = value[0];
                                ret.flags = value[1];
                            }
                        };
                        break;
                }
                if (ret && typeof ret === 'object' && !ret.type) {
                    ret.type = arity;
                }
                return ret;
            },
            convertArray: function(array) {
                var ret = [],
                    i = array.length;
                for (; i-- > 0; ) /* empty */
                {
                    ret[i] = this.convert(array[i]);
                }
                return ret;
            },
            likeToRe: function(node, at) {
                if (typeof node === 'string') {
                    node = {
                        type: 'string',
                        value: node
                    };
                } else if (node.type === 'regexp') {
                    return node;
                }
                // eslint-disable-next-line vars-on-top
                var specialChars = this.specialChars || (QueryParser.prototype.specialChars = Ext.Array.toMap('.+*?^$=!|:-<>[](){}\\'.split(''))),
                    like = node.value,
                    n = like.length,
                    re = '',
                    simple = true,
                    escape, c, i, start;
                outer: for (i = 0; i < n; ++i) {
                    c = like[i];
                    if (!escape) {
                        if (c === '\\') {
                            escape = c;
                            
                            continue;
                        }
                        if (c === '*' || c === '%') {
                            re += '.*';
                            simple = false;
                            
                            continue;
                        }
                        if (c === '?' || c === '_') {
                            re += '.';
                            simple = false;
                            
                            continue;
                        }
                        // Some SQL-dialects (TSQL) support charsets: name like '[Bb]ob'
                        if (c === '[') {
                            re += c;
                            simple = false;
                            start = i;
                            while (++i < n) {
                                c = like[i];
                                if (escape) {
                                    re += escape + c;
                                    escape = 0;
                                } else if (c === '\\') {
                                    escape = c;
                                } else {
                                    re += c;
                                    if (c === ']') {
                                        
                                        continue outer;
                                    }
                                }
                            }
                            // If we fall out of the while loop we never found the close
                            // of the charset... so throw a parse error
                            this.syntaxError(start + (node.at || at || 0), 'Incomplete character set');
                        }
                    }
                    escape = 0;
                    if (specialChars[c]) {
                        re += '\\';
                    }
                    re += c;
                }
                node.re = re || '.*';
                // Assume most users (at least those that don't include
                // a SQL wildcard) don't know about SQL wildcards in LIKE
                // operators... If there are no wildcards present, assume
                // the user wants a case-insensitive, substring match.
                if (simple) {
                    node.flags = 'i';
                } else {
                    node.re = '^' + re + '$';
                }
                return node;
            },
            parseList: function() {
                var me = this,
                    list = [];
                do {
                    if (list.length) {
                        me.advance();
                    }
                    list.push(me.parseExpression());
                } while (// the ','
                me.token.id === ',');
                return list;
            }
        }
    };
});

/**
 * This class is a filter that compiles from an SQL-like expression. For example:
 *
 *      store.addFilter(new Ext.data.Query('name like "Bob" or age < 20'));
 *
 * Queries can also be assigned an `id`:
 *
 *      store.addFilter(new Ext.data.Query({
 *          id: 'myquery',
 *          source: 'name like "Bob" or age < 20'
 *      ));
 *
 * ## Query Syntax
 *
 * The syntax for a query is SQL-like. The goal of the query syntax is to be as natural
 * to end-users as possible and therefore does not exactly match JavaScript.
 *
 * ### Keyword Operators
 *
 *  - `and` or `&&` - Logical AND
 *  - `or` or `||` - Logical OR
 *  - `like` - String containment or regex match
 *  - `in` - Set membership
 *  - `not` or `!` - Logical negation
 *  - `between` - Bounds check a value (`age between 18 and 99`)
 *
 * #### The `like` Operator
 *
 * There are several forms of `like`. The first uses a simple string on the right-side:
 *
 *      name like "Bob"
 *
 * This expression evaluates as `true` if the `name` contains the substring `'Bob'`
 * (ignoring case).
 *
 * The second form will be more typical of those familiar with SQL. It is when the
 * right-side uses the SQL `%` or `_` wildcards (or the shell `*` or `?` wildcards) and/or
 * character sets (such as `'[a-f]'` and `'[^abc]'`):
 *
 *      name like "[BR]ob%"
 *
 * If any wildcards are used, the typical SQL meaning is assumed (strict match, including
 * case).
 *
 * The right-side can also use shell wildcards `'*'` or `'?'` instead of SQL wildcards.
 *
 * These wildcards can be escaped with a backslash (`\`) character (the `escape` keyword
 * is not supported).
 *
 *      text like 'To be or not to be\?'
 *
 * The final form of `like` is when the right-side is a regular expression:
 *
 *      name like /^Bob/i
 *
 * This form uses the `test()` method of the `RegExp` to match the value of `name`.
 *
 * #### The `in` Operator
 *
 * This operator accepts a parenthesized list of values and evaluates to `true` if the
 * left-side value matches an item in the right-side list:
 *
 *      name in ("Bob", 'Robert')
 *
 * ### Relational Operators
 *
 *  - `<`
 *  - `<=`
 *  - `>`
 *  - `>=`
 *
 * ### Equality and Inequality
 *
 *  - `=` - Equality after conversion (like `==` in JavaScript)
 *  - `==` or `===` - Strict equality (like `===` in JavaScript)
 *  - `!=` or `<>` - Inequality after conversion (like `!=` in JavaScript)
 *  - `!==` - Strict inequality (like `!==` in JavaScript)
 *
 * ### Helper Functions
 *
 * The following functions can be used in a query:
 *
 *  - `abs(x)` - Absolute value of `x`
 *  - `avg(...)` - The average of all parameters.
 *  - `date(d)` - Converts the argument into a date.
 *  - `lower(s)` - The lower-case conversion of the given string.
 *  - `max(...)` - The maximum value of all parameters.
 *  - `min(...)` - The minimum value of all parameters.
 *  - `sum(...)` - The sum of all parameters.
 *  - `upper(s)` - The upper-case conversion of the given string.
 *
 * These functions are used as needed in queries, such as:
 *
 *      upper(name) = 'BOB'
 *
 * @since 6.7.0
 */
Ext.define('Ext.data.Query', {
    extend: 'Ext.util.BasicFilter',
    mixins: [
        'Ext.mixin.Factoryable',
        'Ext.data.query.Compiler',
        'Ext.data.query.Converter',
        'Ext.data.query.Stringifier'
    ],
    alias: 'query.default',
    requires: [
        'Ext.data.query.Parser'
    ],
    config: {
        /**
         * @cfg {"ast"/"filters"/"query"} format
         */
        format: 'ast',
        /**
         * @cfg {Object} functions
         * This config contains the methods that will be made available to queries. To
         * add a custom function:
         *
         *      Ext.define('MyQuery', {
         *          extend: 'Ext.data.Query',
         *
         *          functions: {
         *              round: function (x) {
         *                  return Math.round(x);
         *              },
         *
         *              // When a function name ends with "..." it is called
         *              // with the arguments as an array.
         *              //
         *              'concat...': function (args) {
         *                  return args.join('');
         *              }
         *          }
         *      });
         */
        functions: {
            cached: true,
            $value: {
                abs: function(arg) {
                    return Math.abs(arg);
                },
                'avg...': function(args) {
                    var count = 0,
                        sum = 0,
                        i = args.length,
                        v;
                    for (; i-- > 0; ) /* empty */
                    {
                        v = args[i];
                        if (v != null) {
                            sum += v;
                            ++count;
                        }
                    }
                    return count ? sum / count : 0;
                },
                date: function(arg) {
                    return (arg instanceof Date) ? arg : Ext.Date.parse(arg);
                },
                lower: function(arg) {
                    return (arg == null) ? '' : String(arg).toLowerCase();
                },
                'max...': function(args) {
                    var ret = null,
                        i = args.length,
                        v;
                    for (; i-- > 0; ) /* empty */
                    {
                        v = args[i];
                        if (v != null) {
                            ret = (ret === null) ? v : (ret < v ? v : ret);
                        }
                    }
                    return ret;
                },
                'min...': function(args) {
                    var ret = null,
                        i = args.length,
                        v;
                    for (; i-- > 0; ) /* empty */
                    {
                        v = args[i];
                        if (v != null) {
                            ret = (ret === null) ? v : (ret < v ? ret : v);
                        }
                    }
                    return ret;
                },
                'sum...': function(args) {
                    var ret = null,
                        i = args.length,
                        v;
                    for (; i-- > 0; ) /* empty */
                    {
                        v = args[i];
                        if (v != null) {
                            ret = (ret === null) ? v : (ret + v);
                        }
                    }
                    return ret === null ? 0 : ret;
                },
                upper: function(arg) {
                    return (arg == null) ? '' : String(arg).toUpperCase();
                }
            }
        },
        /**
         * @cfg {String} source
         * The source text of this query. See {@link Ext.data.Query class documentation}
         * for syntax details.
         */
        source: ''
    },
    ast: null,
    error: null,
    generation: 0,
    constructor: function(config) {
        if (typeof config === 'string') {
            config = {
                source: config
            };
        }
        // eslint-disable-next-line vars-on-top
        var parser = Ext.data.query.Parser.fly();
        this.symbols = parser.symbols;
        parser.release();
        this.callParent([
            config
        ]);
    },
    filter: function(item) {
        var me = this,
            error = me.error;
        if (error) {
            throw error;
        }
        return !!me.fn(item);
    },
    /**
     * This method should be called if the `ast` has been manipulated directly.
     */
    refresh: function() {
        ++this.generation;
        this.compile();
    },
    // assigns me.fn
    serialize: function() {
        var me = this,
            format = me.getFormat(),
            serializer = me.getSerializer(),
            ret, serialized;
        switch (format) {
            case 'ast':
                ret = me.ast;
                if (serializer) {
                    ret = Ext.clone(ret);
                };
                break;
            case 'filters':
                ret = me.getFilters() || null;
                break;
            case 'query':
                ret = me.toString();
                break;
        }
        if (ret && serializer) {
            serialized = serializer.call(this, ret);
            if (serialized) {
                ret = serialized;
            }
        }
        return ret;
    },
    serializeTo: function(out) {
        var filters = this.serialize(),
            ret;
        if (filters && filters.length) {
            out.push.apply(out, filters);
            ret = true;
        }
        return ret;
    },
    sync: function() {
        var me = this,
            fn = me.fn;
        if (!fn || fn.generation !== me.generation) {
            me.compile();
        }
    },
    toString: function() {
        var ast = this.ast;
        return ast ? this.stringify(ast) : '';
    },
    //------------------------------------------------------------------------
    // Configs
    // format
    validFormatsRe: /^(ast|filters|query)$/,
    applyFormat: function(format) {
        if (!this.validFormatsRe.test(format)) {
            Ext.raise('Invalid query format');
        }
        return format;
    },
    // functions
    applyFunctions: function(funcs) {
        var ret = {},
            vargsRe = this.vargsRe,
            def, key, name;
        for (key in funcs) {
            def = {
                fn: funcs[name = key],
                vargs: vargsRe.test(key)
            };
            if (def.vargs) {
                name = key.substr(0, key.length - 3);
            }
            // remove '...'
            ret[name.toLowerCase()] = def;
        }
        return ret;
    },
    // source
    applySource: function(source) {
        if (source) {
            return source;
        }
        ++this.generation;
        this.ast = null;
        this.compile();
    },
    // assigns me.fn
    updateSource: function(source) {
        var me = this,
            parser = Ext.data.query.Parser.fly(source);
        ++me.generation;
        try {
            me.error = me.fn = null;
            me.ast = parser.parse();
        } catch (e) {
            me.error = e;
            e.message = 'Failed to parse: ' + e.message;
            throw e;
        } finally {
            parser.release();
        }
        me.compile();
    },
    // assigns me.fn
    //-------------------------------------------------------------------------
    privates: {
        operatorTypeMap: {
            /* eslint-disable no-multi-spaces */
            /* eslint-disable key-spacing */
            //    [ arity,      JS-operator,    Query-operator ]
            and: [
                'binary',
                '&&',
                'and'
            ],
            or: [
                'binary',
                '||',
                'or'
            ],
            eq: [
                'binary',
                '==',
                '='
            ],
            ge: [
                'binary',
                '>=',
                null
            ],
            gt: [
                'binary',
                '>',
                null
            ],
            le: [
                'binary',
                '<=',
                null
            ],
            lt: [
                'binary',
                '<',
                null
            ],
            ne: [
                'binary',
                '!=',
                null
            ],
            add: [
                'binary',
                '+',
                null
            ],
            div: [
                'binary',
                '/',
                null
            ],
            mul: [
                'binary',
                '*',
                null
            ],
            sub: [
                'binary',
                '-',
                null
            ],
            'in': [
                'binary',
                null,
                'in'
            ],
            like: [
                'binary',
                null,
                'like'
            ],
            seq: [
                'binary',
                '===',
                '=='
            ],
            sne: [
                'binary',
                '!==',
                null
            ],
            neg: [
                'unary',
                '-',
                null
            ],
            not: [
                'unary',
                '!',
                null
            ]
        },
        /* eslint-enable no-multi-spaces */
        /* eslint-enable key-spacing */
        vargsRe: /\.\.\.$/,
        getOperatorType: function(op) {
            var map = this.operatorTypeMap,
                key;
            for (key in map) {
                if (map[key][1] === op || map[key][2] === op) {
                    return key;
                }
            }
            Ext.raise('Unrecognized filter operator: "' + op + '"');
            return null;
        }
    }
});

/**
 * Simple class that represents a Request that will be made by any {@link Ext.data.proxy.Server}
 * subclass. All this class does is standardize the representation of a Request as used by any
 * ServerProxy subclass, it does not contain any actual logic or perform the request itself.
 */
Ext.define('Ext.data.Request', {
    isDataRequest: true,
    config: {
        /**
         * @cfg {String} action
         * The name of the action this Request represents. Usually one of 'create', 'read',
         * 'update' or 'destroy'.
         */
        action: undefined,
        /**
         * @cfg {Object} params
         * HTTP request params. The Proxy and its Writer have access to and can modify this object.
         */
        params: undefined,
        /**
         * @cfg {String} method
         * The HTTP method to use on this Request. Should be one of 'GET', 'POST', 'PUT',
         * or 'DELETE'.
         */
        method: 'GET',
        /**
         * @cfg {String} url
         * The url to access on this Request.
         */
        url: null,
        /**
         * @cfg {Ext.data.operation.Operation} operation
         * The operation this request belongs to.
         */
        operation: null,
        /**
         * @cfg {Ext.data.proxy.Proxy} proxy
         * The proxy this request belongs to.
         */
        proxy: null,
        /**
         * @cfg {Boolean} disableCaching
         * Whether or not to disable caching for this request.
         */
        disableCaching: false,
        /**
         * @cfg {Object} headers
         * Some requests (like XMLHttpRequests) want to send additional server headers.
         * This configuration can be set for those types of requests.
         */
        headers: {},
        /**
         * @cfg {String} callbackKey
         * Some requests (like JsonP) want to send an additional key that contains
         * the name of the callback function.
         */
        callbackKey: null,
        /**
         * @cfg {Ext.data.JsonP} rawRequest
         * Set the raw request object (Ajax/JsonP/Other)
         * @private
         */
        rawRequest: null,
        /**
         * @cfg {Object} jsonData
         * This is used by some write actions to attach data to the request without encoding it
         * as a parameter.
         */
        jsonData: undefined,
        /**
         * @cfg {Object} xmlData
         * This is used by some write actions to attach data to the request without encoding it
         * as a parameter, but instead sending it as XML.
         */
        xmlData: undefined,
        /**
         * @cfg {Boolean} withCredentials
         * This field is necessary when using cross-origin resource sharing.
         */
        withCredentials: false,
        /**
         * @cfg {String} username
         * Most oData feeds require basic HTTP authentication. This configuration allows
         * you to specify the username.
         * @accessor
         */
        username: null,
        /**
         * @cfg {String} password
         * Most oData feeds require basic HTTP authentication. This configuration allows
         * you to specify the password.
         * @accessor
         */
        password: null,
        /**
        * @cfg {Boolean} binary
        * True to request binary data from the server.  This feature requires
        * the use of a binary reader such as {@link Ext.data.amf.Reader AMF Reader}
        */
        binary: false,
        callback: null,
        scope: null,
        timeout: 30000,
        records: null,
        // The following two configurations are only used by Ext.data.proxy.Direct and are just
        // for being able to retrieve them after the request comes back from the server.
        directFn: null,
        args: null,
        useDefaultXhrHeader: null,
        responseType: null
    },
    /**
     * Creates the Request object.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        this.initConfig(config);
    },
    /**
     * Gets a single param from the {@link #params}.
     * @param {String} key The key for the param.
     * @return {Object} The value for the param. `undefined` if it does not exist.
     */
    getParam: function(key) {
        var params = this.getParams(),
            val;
        if (params) {
            return params[key];
        }
        return val;
    },
    /**
     * Sets a single param value in the {@link #params}.
     * @param {String} key The key to set.
     * @param {Object} value The value to set.
     */
    setParam: function(key, value) {
        var params = this.getParams() || {};
        params[key] = value;
        this.setParams(params);
    }
});

/**
 * The TreeStore is a store implementation that owns the {@link #cfg-root root node} of
 * a tree, and provides methods to load either local or remote data as child nodes of the root
 * and any descendant non-leaf node.
 *
 * The TreeStore must be used as the store of a {@link Ext.tree.Panel tree panel}.
 *
 * This class also relays many node events from the underlying node structure.
 *
 * # Using Models
 *
 * If no Model is specified, an implicit model will be created that extends
 * {@link Ext.data.TreeModel}. The standard Tree fields will also be copied onto the Model
 * for maintaining their state. These fields are listed in the {@link Ext.data.NodeInterface}
 * documentation.
 *
 * # Reading Nested Data
 *
 * For the tree to read nested data, the {@link Ext.data.reader.Reader} must be configured
 * with a root property, so the reader can find nested data for each node (if a root is not
 * specified, it will default to 'children'). This will tell the tree to look for any nested tree
 * nodes by the same keyword, i.e., 'children'. If a root is specified in the config make sure
 * that any nested nodes with children have the same name.
 * 
 * **Note:** Setting {@link #defaultRootProperty} accomplishes the same thing.
 * 
 * #rootProperty as a Function
 * You can pass a function as the data reader's rootProperty when the tree's dataset has 
 * mixed root properties. Child nodes can then be programmatically determined at read time.  
 * 
 * For example, the child nodes may be passed via the 'children' property 
 * name, though you may have a top-level root property of 'items'.  
 * 
 * See {@link Ext.data.reader.Reader#rootProperty rootProperty} for more information.
 *
 * #Filtering#
 * Filtering of nodes in a TreeStore is hierarchically top down by default. This means that
 * if a non-leaf node does not pass the filter, then it, and all its descendants are filtered *out*
 * of the store.
 *
 * To reverse this, so that any node which passes the filter causes all its ancestors to be visible,
 * configure the `TreeStore` with '{@link #cfg-filterer filterer: 'bottomup'}`
 *
 * You may also programmatically filter individual tree nodes by setting their `'visible'` field.
 *
 * Setting this to `false` filters the node out so that it will not appear in the UI. Setting it
 * to `true` filters the node in.
 *
 * Note that if performing several filter operations, it is best to {@link #method-suspendEvents}
 * on the store first, and when all nodes have been modified, {@link #method-resumeEvents} and fire
 * the {@link #event-refresh} event on the store.
 */
Ext.define('Ext.data.TreeStore', {
    extend: 'Ext.data.Store',
    alias: 'store.tree',
    requires: [
        'Ext.util.Sorter',
        'Ext.data.TreeModel',
        'Ext.data.NodeInterface'
    ],
    /**
     * @property {Boolean} isTreeStore
     * `true` in this class to identify an object as an instantiated TreeStore, or subclass thereof.
     */
    isTreeStore: true,
    config: {
        /**
         * @cfg {Ext.data.TreeModel/Ext.data.NodeInterface/Object} root
         * @accessor
         * The root node for this store. For example:
         *
         *     root: {
         *         expanded: true,
         *         text: "My Root",
         *         children: [
         *             { text: "Child 1", leaf: true },
         *             { text: "Child 2", expanded: true, children: [
         *                 { text: "GrandChild", leaf: true }
         *             ] }
         *         ]
         *     }
         *
         * Setting the `root` config option is the same as calling {@link #setRootNode}.
         *
         * It's important to note that setting expanded to true on the root node will
         * cause the tree store to attempt to load.  This will occur regardless the value
         * of  {@link Ext.data.ProxyStore#autoLoad autoLoad}. If you you do not want the
         * store  to load on instantiation, ensure expanded is false and load the store
         * when you're ready.
         * 
         */
        root: null,
        /**
         * @cfg {Boolean} rootVisible
         * `false` to not include the root node in this Stores collection.
         * @accessor
         */
        rootVisible: false,
        /**
         * @cfg {String} defaultRootProperty
         */
        defaultRootProperty: 'children',
        /**
         * @cfg {String} parentIdProperty
         * This config allows node data to be returned from the server in linear format
         * without having to structure it into `children` arrays.
         *
         * This property specifies which property name in the raw node data yields the id
         * of the parent node.
         *
         * For example the following data would be read into a geographic tree by
         * configuring the TreeStore with `parentIdProperty: 'parentId'`. The node data
         * contains an upward link to a parent node.
         *
         *     data: [{
         *         name: 'North America',
         *         id: 'NA'
         *     }, {
         *         name: 'Unites States',
         *         id: 'USA',
         *         parentId: 'NA'
         *     }, {
         *         name: 'Redwood City',
         *         leaf: true,
         *         parentId: 'USA'
         *     }, {
         *         name: 'Frederick, MD',
         *         leaf: true,
         *         parentId: 'USA'
         *     }]
         *
         */
        parentIdProperty: null,
        /**
         * @cfg {Boolean} clearOnLoad
         * Remove previously existing child nodes before loading.
         */
        clearOnLoad: true,
        /**
         * @cfg {Boolean} clearRemovedOnLoad
         * If `true`, when a node is reloaded, any records in the {@link #removed} record
         * collection that were previously descendants of the node being reloaded will be
         * cleared from the {@link #removed} collection. Only applicable if
         * {@link #clearOnLoad} is `true`.
         */
        clearRemovedOnLoad: true,
        /**
         * @cfg {String} nodeParam
         * The name of the parameter sent to the server which contains the identifier of
         * the node.
         */
        nodeParam: 'node',
        /**
         * @cfg {String} defaultRootId
         * The default root id.
         */
        defaultRootId: 'root',
        /**
         * @cfg {String} defaultRootText
         * The default root text (if not specified)
         */
        defaultRootText: 'Root',
        /**
         * @cfg {Boolean} folderSort
         * Set to true to automatically prepend a leaf sorter.
         */
        folderSort: false,
        /**
         * @cfg {Number} pageSize
         * @hide
         */
        pageSize: null
    },
    // Not valid for TreeStore. Paging parameters must not be passed.
    /**
     * @cfg {String} filterer
     * The order in which to prioritize how filters are applied to nodes.
     *
     * The default, `'topdown'` means that if a parent node does *not* pass the filter,
     * then the branch ends there, and no descendant nodes are filtered in, even if they
     * would pass the filter.
     *
     * By specifying `'bottomup'`, if a leaf node passes the filter, then all its
     * ancestor nodes are filtered in to allow it to be visible.
     * @since 6.0.2
     */
    filterer: 'topdown',
    /**
     * @cfg {Boolean} lazyFill
     * Set to true to prevent child nodes from being loaded until the the node is 
     * expanded or loaded explicitly.
     */
    lazyFill: false,
    fillCount: 0,
    bulkUpdate: 0,
    nodesToUnregister: 0,
    /**
     * @cfg fields
     * @inheritdoc Ext.data.Model#cfg-fields
     * 
     * @localdoc **Note:** If you wish to create a Tree*Grid*, and configure your tree
     * with a  {@link Ext.panel.Table#cfg-columns columns} configuration, it is possible
     * to  define the set of fields you wish to use in the Store instead of configuring
     * the  store with a {@link #cfg-model}.
     *
     * By default, the Store uses an {@link Ext.data.TreeModel}. If you configure 
     * fields, it uses a subclass of {@link Ext.data.TreeModel} defined with the set of 
     * fields that you specify (in addition to the fields which it uses for storing 
     * internal state).
     */
    _silentOptions: {
        silent: true
    },
    /**
     * @property implicitModel
     * @inheritdoc
     */
    implicitModel: 'Ext.data.TreeModel',
    /**
     * @cfg {String} groupField
     * @hide
     */
    groupField: null,
    /**
     * @cfg {String} groupDir
     * @hide
     */
    groupDir: null,
    /**
     * @cfg {Object/Ext.util.Grouper} grouper
     * @hide
     */
    grouper: null,
    constructor: function(config) {
        var me = this;
        me.byIdMap = {};
        me.callParent([
            config
        ]);
        // The following events are fired on this TreeStore by the bubbling
        // from NodeInterface.fireEvent
        /**
         * @event nodeappend
         * @inheritdoc Ext.data.NodeInterface#append
         */
        /**
         * @event noderemove
         * @inheritdoc Ext.data.NodeInterface#remove
         */
        /**
         * @event nodemove
         * @inheritdoc Ext.data.NodeInterface#move
         */
        /**
         * @event nodeinsert
         * @inheritdoc Ext.data.NodeInterface#insert
         */
        /**
         * @event nodebeforeappend
         * @inheritdoc Ext.data.NodeInterface#beforeappend
         */
        /**
         * @event nodebeforeremove
         * @inheritdoc Ext.data.NodeInterface#beforeremove
         */
        /**
         * @event nodebeforemove
         * @inheritdoc Ext.data.NodeInterface#beforemove
         */
        /**
         * @event nodebeforeinsert
         * @inheritdoc Ext.data.NodeInterface#beforeinsert
         */
        /**
         * @event nodeexpand
         * @inheritdoc Ext.data.NodeInterface#expand
         */
        /**
         * @event nodecollapse
         * @inheritdoc Ext.data.NodeInterface#collapse
         */
        /**
         * @event nodebeforeexpand
         * @inheritdoc Ext.data.NodeInterface#beforeexpand
         */
        /**
         * @event nodebeforecollapse
         * @inheritdoc Ext.data.NodeInterface#beforecollapse
         */
        /**
         * @event nodesort
         * @inheritdoc Ext.data.NodeInterface#sort
         */
        if (Ext.isDefined(me.nodeParameter)) {
            if (Ext.isDefined(Ext.global.console)) {
                Ext.global.console.warn('Ext.data.TreeStore: nodeParameter has been deprecated. ' + 'Please use nodeParam instead.');
            }
            me.nodeParam = me.nodeParameter;
            delete me.nodeParameter;
        }
    },
    /**
     * @event rootchange
     * Fires any time the tree's root node changes.
     * @param {Ext.data.TreeModel/Ext.data.NodeInterface} newRoot The new root
     * @param {Ext.data.TreeModel/Ext.data.NodeInterface} oldRoot The old root
     */
    applyFields: function(fields, oldFields) {
        var me = this;
        if (fields) {
            if (me.defaultRootProperty !== me.self.prototype.config.defaultRootProperty) {
                // Use concat. Must not mutate incoming configs
                fields = fields.concat({
                    name: me.defaultRootProperty,
                    type: 'auto',
                    defaultValue: null,
                    persist: false
                });
            }
        }
        me.callParent([
            fields,
            oldFields
        ]);
    },
    applyGroupField: function(field) {
        return null;
    },
    applyGroupDir: function(dir) {
        return null;
    },
    applyGrouper: function(grouper) {
        if (grouper) {
            Ext.raise('You can\'t group a TreeStore');
        }
        return null;
    },
    /**
     * @hide
     */
    group: Ext.emptyFn,
    // TreeStore has to do right things upon SorterCollection update
    onSorterEndUpdate: function() {
        var me = this,
            sorterCollection = me.getSorters(),
            sorters = sorterCollection.getRange(),
            rootNode = me.getRoot(),
            folderSort = me.getFolderSort();
        me.fireEvent('beforesort', me, sorters);
        // Only load or sort if there are sorters
        if (rootNode && (folderSort || sorters.length)) {
            if (me.getRemoteSort()) {
                if (sorters.length) {
                    me.load({
                        callback: function() {
                            me.fireEvent('sort', me, sorters);
                        }
                    });
                }
            } else {
                rootNode.sort(this.getSortFn(), true);
                // Don't fire the event if we have no sorters
                me.fireEvent('datachanged', me);
                me.fireEvent('refresh', me);
                me.fireEvent('sort', me, sorters);
            }
        } else // Sort event must fire when sorters collection is updated to empty.
        {
            me.fireEvent('sort', me, sorters);
        }
    },
    updateFolderSort: function(folderSort) {
        this.needsFolderSort = folderSort;
        this.onSorterEndUpdate();
    },
    getSortFn: function() {
        return this._sortFn || (this._sortFn = this.createSortFn());
    },
    createSortFn: function() {
        var me = this,
            sortersSortFn = this.sorters.getSortFn();
        return function(node1, node2) {
            var node1FolderOrder, node2FolderOrder,
                result = 0;
            if (me.needsFolderSort) {
                // Primary comparator puts Folders before leaves.
                node1FolderOrder = node1.data.leaf ? 1 : 0;
                node2FolderOrder = node2.data.leaf ? 1 : 0;
                result = node1FolderOrder - node2FolderOrder;
            }
            if (me.needsIndexSort && result === 0) {
                result = node1.data.index - node2.data.index;
            }
            return result || sortersSortFn(node1, node2);
        };
    },
    getTotalCount: function() {
        return this.getCount();
    },
    afterEdit: function(node, modifiedFieldNames) {
        var me = this,
            parentNode = node.parentNode,
            rootVisible = me.getRootVisible(),
            isHiddenRoot = !parentNode && !rootVisible,
            prevVisibleNodeIndex,
            isVisible = node.get('visible'),
            toAdd, removeStart;
        // If the node visibility flag is not matched by the Store state, correct it.
        // The hidden root node is a special case. That never appears in the flat store
        // so skip processing for that.
        if (!isHiddenRoot && isVisible !== me.contains(node)) {
            // If we are restoring the node to visibility, then insert
            // at the correct point if this TreeStore considers the node visible
            // (visible flag set, and all ancestors expanded and visible)
            if (isVisible) {
                if (!parentNode || me.isVisible(node)) {
                    toAdd = [
                        node
                    ];
                    // Collect visible descendants. Same operation as expanding
                    if (node.isExpanded()) {
                        me.handleNodeExpand(node, node.childNodes, toAdd);
                    }
                    prevVisibleNodeIndex = node.previousSibling ? me.indexOfPreviousVisibleNode(node.previousSibling) : (parentNode ? me.indexOf(parentNode) : -1);
                    me.insert(prevVisibleNodeIndex + 1, toAdd);
                }
            } else // If we are hiding the node, remove it and all its descendants.
            {
                removeStart = me.indexOf(node);
                me.removeAt(removeStart, me.indexOfNextVisibleNode(node) - removeStart);
            }
        }
        // Modification of other fields must lead to node filtering if we are
        // local filtering. Update the flat store though onFilterEndUpdate.
        // Data modification takes place during initial setup of root node
        // so ignore that.
        else if (me.getRoot() && me.needsLocalFilter()) {
            me.onFilterEndUpdate(me.getFilters());
        }
        me.callParent([
            node,
            modifiedFieldNames
        ]);
    },
    afterReject: function(record) {
        var me = this;
        // Must pass the 5th param (modifiedFieldNames) as null, otherwise the
        // event firing machinery appends the listeners "options" object to the arg list
        // which may get used as the modified fields array by a handler.
        // This array is used for selective grid cell updating by Grid View.
        // Null will be treated as though all cells need updating.
        if (me.contains(record)) {
            me.onUpdate(record, Ext.data.Model.REJECT, null);
            me.fireEvent('update', me, record, Ext.data.Model.REJECT, null);
        }
    },
    afterCommit: function(record, modifiedFieldNames) {
        var me = this;
        if (!modifiedFieldNames) {
            modifiedFieldNames = null;
        }
        if (me.contains(record)) {
            me.onUpdate(record, Ext.data.Model.COMMIT, modifiedFieldNames);
            me.fireEvent('update', me, record, Ext.data.Model.COMMIT, modifiedFieldNames);
        }
    },
    updateRootVisible: function(rootVisible) {
        var rootNode = this.getRoot(),
            data;
        if (rootNode) {
            data = this.getData();
            if (rootVisible) {
                data.insert(0, rootNode);
            } else {
                data.remove(rootNode);
            }
        }
    },
    updateTrackRemoved: function(trackRemoved) {
        this.callParent(arguments);
        this.removedNodes = this.removed;
        this.removed = null;
    },
    onDestroyRecords: function(records, operation, success) {
        if (success) {
            this.removedNodes.length = 0;
        }
    },
    updateProxy: function(proxy) {
        var reader;
        // The proxy sets a parameter to carry the entity ID based upon the Operation's id
        // That parameter name defaults to "id".
        // TreeStore however uses a nodeParam configuration to specify the entity id
        if (proxy) {
            if (proxy.setIdParam) {
                proxy.setIdParam(this.getNodeParam());
            }
            // Readers in a TreeStore's proxy have to use a special rootProperty which defaults
            // to "children"
            reader = proxy.getReader();
            if (Ext.isEmpty(reader.getRootProperty())) {
                reader.setRootProperty(this.getDefaultRootProperty());
            }
        }
    },
    setProxy: function(proxy) {
        this.changingProxy = true;
        this.callParent([
            proxy
        ]);
        this.changingProxy = false;
    },
    updateModel: function(model) {
        var isNode;
        if (model) {
            isNode = model.prototype.isNode;
            // Ensure that the model has the required interface to function as a tree node.
            Ext.data.NodeInterface.decorate(model);
            // If we just had to decorate a raw Model to upgrade it to be a NodeInterface
            // then we need to build new extractor functions on the reader.
            if (!isNode && !this.changingProxy) {
                this.getProxy().getReader().buildExtractors(true);
            }
        }
    },
    onCollectionFilter: Ext.emptyFn,
    // We add listeners to the FilterCollection and do the filtering in a hierarchical
    // way. We are not interested in notifications as an observer on the data collection.
    onFilterEndUpdate: function(filters) {
        var me = this,
            length = filters.length,
            root = me.getRoot(),
            childNodes, childNode, filteredNodes, i;
        if (!me.getRemoteFilter()) {
            if (length) {
                me.doFilter(root);
            } else {
                root.cascade({
                    after: function(node) {
                        // Set visible field silently: do not fire update events to views.
                        // Views will receive refresh event from onNodeFilter.
                        node.set('visible', true, me._silentOptions);
                    }
                });
            }
            if (length) {
                filteredNodes = [];
                childNodes = root.childNodes;
                for (i = 0 , length = childNodes.length; i < length; i++) {
                    childNode = childNodes[i];
                    if (childNode.get('visible')) {
                        filteredNodes.push(childNode);
                    }
                }
            } else {
                filteredNodes = root.childNodes;
            }
            me.onNodeFilter(root, filteredNodes);
            root.fireEvent('filterchange', root, filteredNodes);
            // Inhibit AbstractStore's implementation from firing the refresh event.
            // We fire it in the onNodeFilter.
            me.suppressNextFilter = true;
            me.callParent([
                filters
            ]);
            me.suppressNextFilter = false;
        } else {
            me.callParent([
                filters
            ]);
        }
    },
    /**
     * @private
     *
     * Called from filter/clearFilter. Refreshes the view based upon
     * the new filter setting.
     */
    onNodeFilter: function(root, childNodes) {
        var me = this,
            data = me.getData(),
            toAdd = [];
        // Honour rootVisible.
        if (me.getRootVisible() && root.get('visible')) {
            toAdd.push(root);
        }
        me.handleNodeExpand(root, childNodes, toAdd);
        // Do not relay the splicing's add&remove events.
        // We inform interested parties about filtering through a refresh event.
        me.suspendEvents();
        data.splice(0, data.getCount(), toAdd);
        me.resumeEvents();
        if (!me.suppressNextFilter) {
            me.fireEvent('datachanged', me);
            me.fireEvent('refresh', me);
        }
    },
    /**
     * Called from a node's expand method to ensure that child nodes are available.
     *
     * This ensures that the child nodes are available before calling the passed callback.
     * @private
     * @param {Ext.data.NodeInterface} node The node being expanded.
     * @param {Function} callback The function to run after the expand finishes
     * @param {Object} scope The scope in which to run the callback function
     * @param {Array} args The extra args to pass to the callback after the new child nodes
     */
    onBeforeNodeExpand: function(node, callback, scope, args) {
        var me = this,
            storeReader, nodeProxy, nodeReader, reader, children, callbackArgs;
        // childNodes are loaded: go ahead with expand
        // This will also expand phantom nodes with childNodes.
        if (node.isLoaded()) {
            callbackArgs = [
                node.childNodes
            ];
            if (args) {
                callbackArgs.push.apply(callbackArgs, args);
            }
            Ext.callback(callback, scope || node, callbackArgs);
        }
        // The node is loading
        else if (node.isLoading()) {
            me.on('load', function() {
                callbackArgs = [
                    node.childNodes
                ];
                if (args) {
                    callbackArgs.push.apply(callbackArgs, args);
                }
                Ext.callback(callback, scope || node, callbackArgs);
            }, me, {
                single: true,
                priority: 1001
            });
        } else // There are unloaded child nodes in the raw data because of the lazy configuration,
        // load them, then call back.
        {
            // With heterogeneous nodes, different levels may require differently configured
            // readers to extract children. For example a "Disk" node type may configure its proxy
            // reader with root: 'folders', while a "Folder" node type might configure its proxy
            // reader with root: 'files'. Or the root property could be a configured-in accessor.
            storeReader = me.getProxy().getReader();
            nodeProxy = node.getProxy();
            nodeReader = nodeProxy ? nodeProxy.getReader() : null;
            // If the node's reader was configured with a special root (property name which defines
            // the children array) use that.
            reader = nodeReader && nodeReader.initialConfig.rootProperty ? nodeReader : storeReader;
            // 1. If the raw data read in for the node contains a root (children array), then
            //    read it.
            // 2. If a phantom w/o any children, it should still be processed if expanded so check
            //    for that here as well. See EXTJS-13509.
            children = reader.getRoot(node.raw || node.data);
            // Load locally if there are local children, or it's a phantom (client side only) node.
            // Ensure that programmatically added new root nodes which could be phantom are able to
            // kick off remote requests.
            if (children || (node.phantom && !node.isRoot())) {
                // If node is expanded when children are not set, pass an empty array.
                // See EXTJS-26086.
                // Extract records from the raw data. Allow the node being expanded to dictate
                // its child type
                me.fillNode(node, reader.extractData(children || [], {
                    model: node.childType,
                    recordCreator: me.recordCreator
                }));
                callbackArgs = [
                    node.childNodes
                ];
                if (args) {
                    callbackArgs.push.apply(callbackArgs, args);
                }
                Ext.callback(callback, scope || node, callbackArgs);
            } else // Node needs loading
            {
                me.read({
                    node: node,
                    // We use onChildNodesAvailable here because we want trigger to
                    // the loading event after we've loaded children
                    onChildNodesAvailable: function() {
                        // Clear the callback, since if we're introducing a custom one,
                        // it may be re-used on reload
                        delete me.lastOptions.onChildNodesAvailable;
                        callbackArgs = [
                            node.childNodes
                        ];
                        if (args) {
                            callbackArgs.push.apply(callbackArgs, args);
                        }
                        Ext.callback(callback, scope || node, callbackArgs);
                    }
                });
                // Requests for node expansion must be immediate
                me.flushLoad();
            }
        }
    },
    // Called from a node's onChildNodesAvailable method to
    // insert the newly available child nodes below the parent.
    onNodeExpand: function(parent, records) {
        var me = this,
            insertIndex = me.indexOf(parent) + 1,
            toAdd = [];
        me.handleNodeExpand(parent, records, toAdd);
        // If a hidden root is being expanded for the first time, it's not an insert operation
        if (!me.refreshCounter && parent.isRoot() && !parent.get('visible')) {
            me.loadRecords(toAdd);
        } else // The add event from this insertion is handled by TreeView.onAdd.
        // That implementation calls parent and then ensures the previous sibling's joining lines
        // are correct. Apart from the above code path, TreeStores, do not get a load event. A root
        // node may or may not be inserted, and from then on, chld nodes are added and removed.
        // Views interrogate loadCount so, TreeStores with any data must appear to be loaded.
        {
            ++me.loadCount;
            me.insert(insertIndex, toAdd);
        }
    },
    // Collects child nodes to remove into the passed toRemove array.
    // When available, all descendant nodes are pushed into that array using recursion.
    handleNodeExpand: function(parent, records, toAdd) {
        var me = this,
            ln = records ? records.length : 0,
            i, record;
        // If parent is not visible, nothing to do (unless parent is the root)
        if (parent !== this.getRoot() && !me.isVisible(parent)) {
            return;
        }
        if (ln) {
            // The view items corresponding to these are rendered.
            // Loop through and expand any of the non-leaf nodes which are expanded
            for (i = 0; i < ln; i++) {
                record = records[i];
                // If the TreePanel has not set its visible flag to false, add to new node array
                if (record.get('visible')) {
                    // Add to array being collected by recursion when child nodes are loaded.
                    // Must be done here in loop so that child nodes are inserted into the stream
                    // in place in recursive calls.
                    toAdd.push(record);
                    if (record.isExpanded()) {
                        if (record.isLoaded()) {
                            // Take a shortcut - appends to toAdd array
                            me.handleNodeExpand(record, record.childNodes, toAdd);
                        } else {
                            // Might be asynchronous if child nodes are not immediately available
                            // MUST set expanded silently, otherwise we will receive notification
                            // and attempt to update the UI.
                            record.set('expanded', false, {
                                silent: true
                            });
                            record.expand();
                        }
                    }
                }
            }
        }
    },
    /**
     * @private
     * Called from a node's collapse method
     */
    onNodeCollapse: function(parent, records, callback, scope) {
        var me = this,
            collapseIndex = me.indexOf(parent) + 1,
            lastNodeIndexPlus;
        // Only remove what is visible and therefore in the collection side of this store
        if (me.needsLocalFilter()) {
            records = Ext.Array.filter(records, me.filterVisible);
        }
        // Only attempt to remove the records if they are there.
        // Collapsing an ancestor node *immediately removes from the view, ALL its descendant nodes
        // at all levels*.
        // But if the collapse was recursive, all descendant root nodes will still fire their
        // events. But we must ignore those events here - we have nothing to do.
        if (records.length && me.isVisible(parent)) {
            // Calculate the index *one beyond* the last node we are going to remove.
            lastNodeIndexPlus = me.indexOfNextVisibleNode(parent);
            // Remove the whole collapsed node set.
            me.removeAt(collapseIndex, lastNodeIndexPlus - collapseIndex);
        }
        Ext.callback(callback, scope);
    },
    /**
     * @private
     * Gets the index of next visible node at either the same sibling level or a higher level.
     *
     * This is to facilitate bulk removal of visible descendant nodes. eg in the following case
     * TreeStore.indexOfNextVisibleNode(bletch) must return indexOf(belch) - the next sibling.
     *
     * But TreeStore.indexOfNextVisibleNode(blivit) and TreeStore.indexOfNextVisibleNode(screeble)
     * and TreeStore.indexOfNextVisibleNode(poot) must also return return indexOf(belch)
     *
     *      foo
     *      ├ bar
     *      ├ bletch
     *      │ ├ zarg
     *      │ └ blivit
     *      │   ├ ik
     *      │   └ screeble
     *      │     ├ raz
     *      │     └ poot
     *      ├ belch
     *      apresfoo
     *
     * This is so that removal of nodes at full depth can be optimized into one 
     * removeAt(start, length) call.
     */
    indexOfNextVisibleNode: function(node) {
        var result;
        while (node.parentNode) {
            // Find the next visible sibling (filtering may have knocked out intervening nodes)
            /* eslint-disable-next-line max-len */
            for (result = node.nextSibling; result && !result.get('visible'); result = result.nextSibling) {}
            // This block is intentionally left blank
            // If found, we're done.
            if (result) {
                return this.indexOf(result);
            }
            // If there is no next sibling, we try to find the parent node's next visible sibling.
            node = node.parentNode;
        }
        // No subsequent visible siblings
        return this.getCount();
    },
    /**
     * @private
     * Gets the index of previous visible node at either the same sibling level or a higher level
     * *inclusive* of passed node.
     *
     * This is to facilitate insertion of nodes in a filtered tree. eg in the following case
     * TreeStore.indexOfPreviousVisibleNode(bletch) must return indexOf(bar) - the previous sibling.
     *
     * But TreeStore.indexOfPreviousVisibleNode(belch) must return indexOf(raz) because
     * poot is filtered out of visibility.
     *
     *      foo
     *      ├ bar
     *      ├ bletch
     *      │ ├ zarg
     *      │ └ blivit
     *      │   ├ ik
     *      │   │ ├screeble
     *      │   │ └ raz
     *      │   └ poot<filtered out>
     *      ├ belch
     *      apresfoo
     *
     */
    indexOfPreviousVisibleNode: function(node) {
        var result;
        // Find the previous visible sibling (filtering may have knocked out intervening nodes)
        for (result = node; result && !result.get('visible'); result = result.previousSibling) {}
        // This block is intentionally left blank
        // If found, and there are child nodes, do the same operation on the last child
        if (result) {
            if (result.isExpanded() && result.lastChild) {
                return this.indexOfPreviousVisibleNode(result.lastChild);
            }
        } else // If there is no previous visible sibling, we use the parent node.
        // We only even ATTEMPT to insert into the flat store children of visible nodes.
        {
            result = node.parentNode;
        }
        return this.indexOf(result);
    },
    /**
     * @private
     * Filter function for new records.
     */
    filterNew: function(item) {
        // Root nodes are always generated on the client side, and therefore phantom.
        // But they should never be included in the new records list.
        return !item.get('root') && this.callParent([
            item
        ]);
    },
    /**
     * @private
     * Filter function for rejected records.
     */
    filterRejects: function(item) {
        // Root nodes are always generated on the client side, and therefore phantom.
        // But they should never be included in the rejects list.
        return !item.get('root') && this.callParent([
            item
        ]);
    },
    getNewRecords: function() {
        return Ext.Array.filter(Ext.Object.getValues(this.byIdMap), this.filterNew, this);
    },
    getRejectRecords: function() {
        return Ext.Array.filter(Ext.Object.getValues(this.byIdMap), this.filterRejects, this);
    },
    getUpdatedRecords: function() {
        return Ext.Array.filter(Ext.Object.getValues(this.byIdMap), this.filterUpdated);
    },
    // Called from a node's removeChild & removeAll methods *before* the node(s) is/are unhooked
    // from siblings and parent. We calculate the range of visible nodes affected by the removal.
    // For example in the tree below, if the "bletch" node was being removed, we would have to
    // remove bletch, zarg, blivit, ik, screeble, razz and poot.
    //
    //      foo
    //      ├ bar
    //      ├ bletch
    //      │ ├ zarg
    //      │ └ blivit
    //      │   ├ ik
    //      │   └ screeble
    //      │     ├ raz
    //      │     └ poot
    //      ├ belch
    //      apresfoo
    //
    // If there are expanded nodes, descendants will be in this store and need removing too.
    // These values are used in onNodeRemove below, after the node has been unhooked from its
    // siblings and parent. The [start, length] range parameter list for the flat store removeAt
    // call is calculated and returned before the calling NodeInterface method removes child nodes.
    beforeNodeRemove: function(parentNode, childNodes, isMove, removeRange) {
        if (!Ext.isArray(childNodes)) {
            childNodes = [
                childNodes
            ];
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            len = childNodes.length,
            // Must use class-specific removedNodes property.
            // Regular Stores add to the "removed" property on CollectionRemove.
            // TreeStores are having records removed all the time; node collapse removes.
            // TreeStores add to the "removedNodes" property onNodeRemove
            removed = me.removedNodes,
            startNode, i;
        // Skip to the first visible node.
        for (i = 0; !startNode && i < len; i++) {
            if (childNodes[i].get('visible')) {
                startNode = childNodes[i];
            }
        }
        // Calculate the range of contiguous *VISIBLE* nodes that the childNodes array represents.
        // This is used by the calling code AFTER it has detached the tree structure.
        if (startNode) {
            removeRange[0] = me.indexOf(childNodes[0]);
            removeRange[1] = me.indexOfNextVisibleNode(childNodes[childNodes.length - 1]) - removeRange[0];
        } else {
            removeRange[0] = -1;
            removeRange[1] = 0;
        }
        // The code above calculated the range of nodes that are below expanded parents and not
        // filtered out. That will be used to removed the block from the flat store, thereby
        // updating any dependent UIs.
        // 
        // We now have to walk the descendant tree for nodes which were not in the Store due to not
        // being visible. This means either below a collapsed parent, or filtered out (visible
        // property false)
        //
        // For example, in the tree below, imagine "bletch" is being removed, "zarg" is filtered out
        // of visibility and the "blivit" node is collasped.
        //
        //  foo
        //  ├ bar
        //  ├ bletch
        //  │ ├ zarg   <- this is filtered out and therefore not visible
        //  │ └ blivit <- this is collapsed. ik, screeble, raz and poot are NOT in the Collection
        //  │   ├ ik
        //  │   └ screeble
        //  │     ├ raz
        //  │     └ poot
        //  ├ belch
        //  apresfoo
        //
        // the above code would only collect "bletch" and "blivit".
        // We now have to collect bletch, zarg, blivit, uk, screeble, raz and poot.
        for (i = 0; i < len; i++) {
            childNodes[i].cascade(function(node) {
                // We have to unregister all descendant nodes.
                me.unregisterNode(node, true);
                // We also have to ensure that all descendant nodes that were NOT removed above
                // (ones that were not in the store collection due to invisibility are added to the
                // remove tracking array...
                // IF we are tracking, and is the remove is not for moving elsewhere in the tree.
                if (removed && !isMove) {
                    // Don't push internally moving, or phantom (client side only), or erasing
                    // (informing server through its own proxy) records onto removed or which
                    // have been through a drop operation which will already have registered as to
                    // remove.
                    if (!node.phantom && !node.erasing && !me.loading) {
                        // Store the index the record was removed from so that rejectChanges can
                        // re-insert at the correct place. The record's index property won't do,
                        // as that is the index in the overall dataset when Store is buffered.
                        node.removedFrom = me.indexOf(node);
                        removed.push(node);
                        // Removal of a non-phantom record which is NOT erasing (informing
                        // the server through its own proxy) requires that the store be synced
                        // at some point.
                        me.needsSync = true;
                    }
                }
            });
        }
    },
    // The drop operation of a Model calls afterDrop on attached stores which removes that model
    // from the store's collection, and the store reacts to that.
    // The drop operation on a tree NodeInterface object must not affect the Store. It must
    // callParent to ensure associations are dropped too, but presence in a TreeStore is handled
    // between the NodeInterface object and the TreeStore persona of the store, NOT its Store
    // persona.
    afterDrop: Ext.emptyFn,
    // Called from a node's removeChild & removeAll methods *after* the node is unhooked from
    // siblings and parent. Remove the visible descendant nodes that we calculated in
    // beforeRemoveNode above.
    onNodeRemove: function(parentNode, childNodes, isMove, removeRange) {
        var me = this;
        // Prevent the me.removeAt call which removes *VISIBLE* nodes when this store has
        // a UI attached from syncing. We sync at the end.
        me.suspendAutoSync();
        // Remove all visible descendants from store.
        // Only visible nodes are present in the store.
        // Superclass's onCollectionRemove will handle unjoining.
        // That will not add to removed list. TreeStores keep a different list and we add to it
        // below. Set removeIsMove flag correctly for onCollectionRemove to do the right thing.
        if (removeRange[0] !== -1) {
            me.removeIsMove = isMove;
            me.removeAt.apply(me, removeRange);
            me.removeIsMove = false;
        }
        me.resumeAutoSync();
    },
    /**
     * @private
     *
     * Called from a node's appendChild method.
     */
    onNodeAppend: function(parent, node, index) {
        this.onNodeInsert(parent, node, index);
    },
    /**
     * @private
     *
     * Called from a node's insertBefore method.
     */
    onNodeInsert: function(parent, node, index) {
        var me = this,
            data = node.raw || node.data,
            // Must use class-specific removedNodes property.
            // Regular Stores add to the "removed" property on CollectionRemove.
            // TreeStores are having records removed all the time; node collapse removes.
            // TreeStores add to the "removedNodes" property onNodeRemove
            removed = me.removedNodes,
            storeReader, nodeProxy, nodeReader, reader, dataRoot, storeInsertionPoint;
        if (parent && me.needsLocalFilter()) {
            me.doFilter(parent);
        }
        me.beginUpdate();
        // Only react to a node append if it is to a node which is expanded.
        if (me.isVisible(node)) {
            // Calculate the insertion point into the flat store.
            // If the new node is the first, then it goes after the parent node.
            if (index === 0 || !node.previousSibling) {
                storeInsertionPoint = me.indexOf(parent);
            } else // Otherwise it has to go after the previous visible node which has
            // to be calculated. See indexOfPreviousVisibleNode for explanation.
            {
                storeInsertionPoint = me.indexOfPreviousVisibleNode(node.previousSibling);
            }
            // The reaction to collection add joins the node to this Store
            me.insert(storeInsertionPoint + 1, node);
            if (!node.isLeaf() && node.isExpanded()) {
                if (node.isLoaded()) {
                    // Take a shortcut
                    me.onNodeExpand(node, node.childNodes);
                } else if (!me.fillCount) {
                    // If the node has been marked as expanded, it means the children
                    // should be provided as part of the raw data. If we're filling the nodes,
                    // the children may not have been loaded yet, so only do this if we're
                    // not in the middle of populating the nodes.
                    node.set('expanded', false);
                    node.expand();
                }
            }
        }
        // In case the node was removed and added to the removed nodes list.
        Ext.Array.remove(removed, node);
        // New nodes mean we need a sync if those nodes are phantom or dirty (have client-side only
        // information)
        me.needsSync = me.needsSync || node.phantom || node.dirty;
        if (!node.isLeaf() && !node.isLoaded() && !me.lazyFill) {
            // With heterogeneous nodes, different levels may require differently configured readers
            // to extract children. For example a "Disk" node type may configure it's proxy reader
            // with root: 'folders', while a "Folder" node type might configure its proxy reader
            // with root: 'files'. Or the root property could be a configured-in accessor.
            storeReader = me.getProxy().getReader();
            nodeProxy = node.getProxy();
            nodeReader = nodeProxy ? nodeProxy.getReader() : null;
            // If the node's reader was configured with a special root (property name which defines
            // the children array) use that.
            reader = nodeReader && nodeReader.initialConfig.rootProperty ? nodeReader : storeReader;
            dataRoot = reader.getRoot(data);
            if (dataRoot) {
                me.fillNode(node, reader.extractData(dataRoot, {
                    model: node.childType,
                    recordCreator: me.recordCreator
                }));
            }
        }
        me.endUpdate();
    },
    /**
     * Registers a node so that it can be looked up by ID.
     * @private
     * @param {Ext.data.NodeInterface} node The node to register
     * @param {Boolean} [includeChildren] True to unregister any child nodes
     */
    registerNode: function(node, includeChildren) {
        var me = this,
            was = me.byIdMap[node.id],
            children, length, i;
        // Key the node hash by the node's IDs
        me.byIdMap[node.id] = node;
        // If the node requires to be informed upon register, and is not already
        // registered, keep it informed.
        if (node.onRegisterTreeNode && node !== was) {
            node.onRegisterTreeNode(me);
        }
        // Keep a count of nodes which require to be informed upon unregister.
        // If we are destroyed, or change root nodes, a cascade will be
        // necessary if this is non-zero.
        if (node.onUnregisterTreeNode) {
            me.nodesToUnregister++;
        }
        if (includeChildren === true) {
            children = node.childNodes;
            length = children.length;
            for (i = 0; i < length; i++) {
                me.registerNode(children[i], true);
            }
        }
    },
    /**
     * Unregisters a node.
     * @private
     * @param {Ext.data.NodeInterface} node The node to unregister
     * @param {Boolean} [includeChildren] True to unregister any child nodes
     */
    unregisterNode: function(node, includeChildren) {
        var me = this,
            was = me.byIdMap[node.id],
            children, length, i;
        delete me.byIdMap[node.id];
        if (includeChildren === true) {
            children = node.childNodes;
            length = children.length;
            for (i = 0; i < length; i++) {
                me.unregisterNode(children[i], true);
            }
        }
        // If the node requires to be informed upon unregister, and it was
        // registered, keep it informed.
        if (node.onUnregisterTreeNode && node === was) {
            node.onUnregisterTreeNode(me);
            me.nodesToUnregister--;
        }
    },
    onNodeSort: function(node, childNodes) {
        var me = this;
        // The onNodeCollapse and onNodeExpand should not sync.
        // Should be one coalesced sync if autoSync.
        me.suspendAutoSync();
        // Collapse, then expand the node to refresh the displayed node set if the node
        // is expanded, or it's the root, but the root is not visible (so cannot be expanded
        // by the UI)
        /* eslint-disable-next-line max-len */
        if ((me.indexOf(node) !== -1 && node.isExpanded()) || (node === me.getRoot() && !me.getRootVisible())) {
            Ext.suspendLayouts();
            me.onNodeCollapse(node, childNodes);
            me.onNodeExpand(node, childNodes);
            Ext.resumeLayouts(true);
        }
        // Lift suspension. This will execute a sync if the suspension count has gone to zero
        // and this store is configured to autoSync
        me.resumeAutoSync(me.autoSync);
    },
    applyRoot: function(newRoot) {
        var me = this,
            Model = me.getModel(),
            idProperty = Model.prototype.idProperty,
            defaultRootId = me.getDefaultRootId();
        // Convert to a node. Even if they are passing a normal Model, the Model will not yet
        // have been decorated with the constructor which initializes properties, so we always
        // have to construct a new node if the passed root is not a Node.
        if (newRoot && !newRoot.isNode) {
            // create a default rootNode and create internal data struct.
            newRoot = Ext.apply({
                text: me.getDefaultRootText(),
                root: true,
                isFirst: true,
                isLast: true,
                depth: 0,
                index: 0,
                parentId: null,
                allowDrag: false
            }, newRoot);
            // Ensure the root has the default root id if it has no id.
            if (defaultRootId && newRoot[idProperty] === undefined) {
                newRoot[idProperty] = defaultRootId;
            }
            // Specify that the data object is raw, and converters will need to be called
            newRoot = new Model(newRoot);
        }
        return newRoot;
    },
    updateRoot: function(newRoot, oldRoot) {
        var me = this,
            removeRange = [],
            initial = me.isConfiguring,
            oldOwner, toRemove;
        // Ensure that the removedNodes array is correct, and that the base class's removed array
        // is null
        me.getTrackRemoved();
        // We do not want an add event to fire. This is a refresh operation.
        // A refresh will be fired after the new root is set.
        me.suspendEvent('add', 'remove');
        if (initial) {
            me.suspendEvent('refresh', 'datachanged');
        }
        // Ensure that the old root is unjoined, visible children are removed from Collection,
        // and descendants added to removed list if tracking removed.
        if (oldRoot && oldRoot.isModel) {
            // root will be in flat store only if rootVisible is false
            if (me.getRootVisible()) {
                toRemove = [
                    oldRoot
                ];
            } else {
                toRemove = oldRoot.childNodes;
            }
            me.beforeNodeRemove(null, toRemove, false, removeRange);
            oldRoot.set('root', false);
            me.onNodeRemove(null, toRemove, false, removeRange);
            oldRoot.fireEvent('remove', null, oldRoot, false);
            oldRoot.fireEvent('rootchange', null);
            oldRoot.clearListeners();
            oldRoot.store = oldRoot.treeStore = null;
            // If rootVisible is false, the root will not have been unregistered by
            // the beforeNodeRemove call which calls a recursive unregister on all
            // *visible* nodes being removed from the flat store.
            me.unregisterNode(oldRoot);
        }
        me.getData().clear();
        // Nulling the root node is essentially clearing the store.
        // TreeStore.removeAll updates the root node to null.
        if (newRoot) {
            // Fire beforeappend, to allow veto of new root setting
            if (newRoot.fireEventArgs('beforeappend', [
                null,
                newRoot
            ]) === false) {
                newRoot = null;
            } else {
                // The passed node was a childNode somewhere else; remove it from there.
                oldOwner = newRoot.parentNode;
                if (oldOwner) {
                    // The removeChild operation can be vetoed by beforeremove event handler,
                    // and returns false if so.
                    // Important: That last boolean test is informing the remove whether or not it's
                    // just a move operation within the same TreeStore
                    /* eslint-disable-next-line max-len */
                    if (!oldOwner.removeChild(newRoot, false, false, oldOwner.getTreeStore() === me)) {
                        return;
                    }
                }
                // If the passed root was previously the rootNode of another TreeStore,
                // it must be removed from that store
                /* eslint-disable-next-line max-len */
                else if ((oldOwner = newRoot.getTreeStore()) && oldOwner !== me && newRoot === oldOwner.getRoot()) {
                    oldOwner.setRoot(null);
                }
                // The root node is the only node bound to the TreeStore by a reference.
                // All descendant nodes acquire a reference to their TreeStore by interrogating
                // the parentNode axis. The rootNode never joins this store.
                newRoot.store = newRoot.treeStore = me;
                newRoot.set('root', true);
                // root node should never be phantom or dirty, so commit it
                newRoot.updateInfo(true, {
                    isFirst: true,
                    isLast: true,
                    depth: 0,
                    index: 0,
                    parentId: null
                });
                // We register the subtree before we proceed so relayed events (like
                // nodeappend) will be able to use getNodeById.
                me.registerNode(newRoot, true);
                // The new root fires the append and rootchange events
                newRoot.fireEvent('append', null, newRoot, false);
                newRoot.fireEvent('rootchange', newRoot);
                // Ensure the root node is filtered, registered and joined.
                me.onNodeAppend(null, newRoot, 0);
                // Because of the application of an ID, this client-created root
                // will not be phantom. Ensure it is correctly flagged as a phantom.
                // AFTER being registered and joined, otherwise onNodeInsert will set
                // the needsSync flag.
                newRoot.phantom = true;
            }
        }
        if (!initial) {
            me.fireEvent('rootchange', newRoot, oldRoot);
        }
        // If root configure to start expanded, or we are autoLoad, we want the root's nodes
        // in the Store.
        if (newRoot && (me.getAutoLoad() || newRoot.isExpanded())) {
            // If it was configured with inline children, it will be loaded, so skip ahead
            // to the onNodeExpand callback.
            if (newRoot.isLoaded()) {
                me.onNodeExpand(newRoot, newRoot.childNodes);
                if (!initial) {
                    me.fireEvent('datachanged', me);
                    me.fireEvent('refresh', me);
                }
            } else // Root is not loaded; go through the expand mechanism to force a load
            {
                newRoot.data.expanded = false;
                newRoot.expand(false);
                // If it's already loaded, but the store is not synchronous, then it was loaded
                // from a local children array, so we need to refresh the data.
                // A store based load goes through onProxyLoad which will fire a refresh.
                if (newRoot.isLoaded && !me.getProxy().isSynchronous && !initial) {
                    me.fireEvent('datachanged', me);
                    me.fireEvent('refresh', me);
                }
            }
        } else if (!initial) {
            me.fireEvent('datachanged', me);
            me.fireEvent('refresh', me);
        }
        // Inform views that the entire structure has changed.
        me.resumeEvent('add', 'remove');
        if (initial) {
            me.resumeEvent('refresh', 'datachanged');
        }
    },
    doDestroy: function() {
        var me = this,
            root = me.getRoot();
        // If we contain some nodes which require to be informed upon unregister
        // then we must cascade the whole tree and inform any that require it.
        // The cascade method calls the passed function on the topmost node.
        if (root && me.nodesToUnregister) {
            root.cascade(function(node) {
                if (node.onUnregisterTreeNode) {
                    node.onUnregisterTreeNode(me);
                }
            });
        }
        me.callParent();
    },
    /**
     * @method getById
     * @inheritdoc Ext.data.LocalStore
     * @localdoc **NOTE:** TreeStore's getById method will only search nodes that 
     * are expanded (all ancestor nodes are {@link Ext.data.NodeInterface#expanded 
     * expanded}: true -- {@link Ext.data.NodeInterface#isExpanded isExpanded})
     * 
     * See also {@link #getNodeById}
     */
    /**
     * Calls the specified function for each {@link Ext.data.NodeInterface node} in the store.
     *
     * When store is filtered, only loops over the filtered records unless the `bypassFilters`
     * parameter is `true`.
     *
     * @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed
     * as the first parameter. Returning `false` aborts and exits the iteration.
     * @param {Object} [scope] The scope (`this` reference) in which the function is executed.
     * Defaults to the current {@link Ext.data.NodeInterface node} in the iteration.
     * @param {Object/Boolean} [includeOptions] An object which contains options which
     * modify how the store is traversed. Alternatively, this parameter can be just the
     * `filtered` option.
     * @param {Boolean} [includeOptions.filtered] Pass `true` to include filtered out
     * nodes in the iteration.
     * @param {Boolean} [includeOptions.collapsed] Pass `true` to include nodes which are
     * descendants of collapsed nodes.
     */
    each: function(fn, scope, includeOptions) {
        var i = 0,
            filtered = includeOptions,
            includeCollapsed;
        if (includeOptions && typeof includeOptions === 'object') {
            includeCollapsed = includeOptions.collapsed;
            filtered = includeOptions.filtered;
        }
        if (includeCollapsed) {
            this.getRoot().cascade(function(node) {
                if (filtered === true || node.get('visible')) {
                    return fn.call(scope || node, node, i++);
                }
            });
        } else {
            return this.callParent([
                fn,
                scope,
                filtered
            ]);
        }
    },
    /**
     * Collects unique values for a particular dataIndex from this store.
     *
     * @param {String} dataIndex The property to collect
     * @param {Object/Boolean} [options] An object which contains options which modify how
     * the store is traversed. Or just the `allowNull` option.
     * @param {Boolean} [options.allowNull] Pass true to allow null, undefined or empty
     * string values.
     * @param {Boolean} [options.filtered] Pass `true` to collect from all records, even
     * ones which are filtered.
     * @param {Boolean} [options.collapsed] Pass `true` to include nodes which are
     * descendants of collapsed nodes.
     *
     * @param {Boolean} [filtered] If previous parameter (`options`) is just the
     * `allowNull` value, this parameter is the `filtered` option.
     *
     * @return {Object[]} An array of the unique values
     */
    collect: function(dataIndex, options, filtered) {
        var includeCollapsed,
            map = {},
            result = [],
            allowNull = options,
            strValue, value;
        if (options && typeof options === 'object') {
            includeCollapsed = options.collapsed;
            filtered = options.filtered;
            allowNull = options.allowNull;
        }
        if (includeCollapsed || filtered) {
            this.getRoot().cascade(function(node) {
                if (filtered === true || node.get('visible')) {
                    value = node.get(dataIndex);
                    strValue = String(value);
                    if ((allowNull || !Ext.isEmpty(value)) && !map[strValue]) {
                        map[strValue] = 1;
                        result.push(value);
                    }
                }
                // Do not traverse into children if this is collapsed
                // and they are not asking to include collapsed
                if (!includeCollapsed && !node.isExpanded()) {
                    return false;
                }
            });
        } else {
            result = this.callParent([
                dataIndex,
                allowNull,
                filtered
            ]);
        }
        return result;
    },
    /**
     * Returns the record node by id regardless of visibility due to collapsed states;
     * all nodes present in the tree structure are available.
     * @param {String} id The id of the node to get.
     * @return {Ext.data.NodeInterface}
     */
    getNodeById: function(id) {
        return this.byIdMap[id] || null;
    },
    /**
     * Finds the first matching node in the tree by a specific field value regardless of visibility
     * due to collapsed states; all nodes present in the tree structure are searched.
     *
     * @param {String} fieldName The name of the Record field to test.
     * @param {String/RegExp} value Either a string that the field value
     * should begin with, or a RegExp to test against the field.
     * @param {Boolean} [startsWith=true] Pass `false` to allow a match to start
     * anywhere in the string. By default the `value` will match only at the start
     * of the string.
     * @param {Boolean} [endsWith=true] Pass `false` to allow the match to end before
     * the end of the string. By default the `value` will match only at the end of the
     * string.
     * @param {Boolean} [ignoreCase=true] Pass `false` to make the `RegExp` case
     * sensitive (removes the 'i' flag).
     * @return {Ext.data.NodeInterface} The matched node or null
     */
    findNode: function(fieldName, value, startsWith, endsWith, ignoreCase) {
        var result = null,
            regex;
        if (Ext.isEmpty(value, false)) {
            return result;
        }
        // If they are looking up by the idProperty, do it the fast way.
        if (fieldName === this.model.idProperty && arguments.length < 3) {
            return this.byIdMap[value];
        }
        regex = Ext.String.createRegex(value, startsWith, endsWith, ignoreCase);
        Ext.Object.eachValue(this.byIdMap, function(node) {
            if (node && regex.test(node.get(fieldName))) {
                result = node;
                return false;
            }
        });
        return result;
    },
    /**
     * Marks this store as needing a load. When the current executing event handler exits,
     * this store will send a request to load using its configured {@link #proxy}.
     *
     * **Be aware that it is not usually valid for a developer to call this method on a TreeStore.**
     *
     * TreeStore loads are triggered by a load request from an existing
     * {@link Ext.data.NodeInterface tree node}, when the node is expanding, and it has no
     * locally defined children in its data.
     *
     * *Note:* Even for synchronous Proxy types such as {@link Ext.data.proxy.Memory memory proxy},
     * the result will *NOT* be available in the following line of code. You must use a callback
     * in the load options, or a {@link #event-load load listener}.
     *
     * @param {Object} [options] This is passed into the
     * {@link Ext.data.operation.Operation Operation} object that is created and then sent to the
     * proxy's {@link Ext.data.proxy.Proxy#read} function. The options can also contain a node,
     * which indicates which node is to be loaded. If not specified, it will default to the
     * root node.
     * @param {Ext.data.NodeInterface} [options.node] The tree node to load. Defaults to the store's
     * {@link #cfg-root root node}
     * @param {Function} [options.callback] A function which is called when the response arrives.
     * @param {Ext.data.Model[]} options.callback.records Array of records.
     * @param {Ext.data.operation.Operation} options.callback.operation The Operation itself.
     * @param {Boolean} options.callback.success `true` when operation completed successfully.
     * @method load
     */
    load: function(options) {
        var node = options && options.node;
        // If there is not a node it means the user hasn't defined a root node yet. In this case
        // let's just create one for them. The expanded: true will cause a load operation,
        // so return.
        if (!node && !(node = this.getRoot())) {
            node = this.setRoot({
                expanded: true,
                autoRoot: true
            });
            return;
        }
        // If the node kicked off its own load, then don't schedule another load.
        if (node.isLoading()) {
            return;
        }
        return this.callParent([
            options
        ]);
    },
    /**
     * Reloads the root node of this store.
     */
    reload: function(options) {
        var o = Ext.apply({}, options, this.lastOptions);
        // A reload implies root load
        o.node = this.getRoot();
        return this.load(o);
    },
    /**
     * Called when the event handler which called the {@link #method-load} method exits.
     */
    flushLoad: function() {
        var me = this,
            options = me.pendingLoadOptions,
            clearOnLoad = me.getClearOnLoad(),
            node, callback, scope, isRootLoad, operation, doClear;
        // If it gets called programmatically before the timer fired, the listener
        // will need cancelling.
        me.clearLoadTask();
        if (!options) {
            return;
        }
        node = options.node || me.getRoot();
        // If we are loading the root, then it is a whole store reload.
        // This is handled efficiently in onProxyLoad by firing the refresh event
        // which will completely refresh any dependent views as would be expected
        // from a load() call.
        isRootLoad = node && node.isRoot();
        callback = options.callback;
        scope = options.scope;
        options.params = options.params || {};
        // If this is not a root reload.
        // If the node we are loading was expanded, we have to expand it after the load
        if (node.data.expanded && !isRootLoad) {
            node.data.loaded = false;
            // Must set expanded to false otherwise the onProxyLoad->fillNode->appendChild
            // calls will update the view. We ned to update the view in the callback below.
            if (clearOnLoad) {
                node.data.expanded = false;
            }
            options.callback = function(loadedNodes, operation, success) {
                // If newly loaded nodes are to be added to the existing child node set, then
                // we have to collapse first so that they get removed from the NodeStore,
                // and the subsequent expand will reveal the newly augmented child node set.
                if (!clearOnLoad) {
                    node.collapse();
                }
                node.expand();
                // Call the original callback (if any)
                Ext.callback(callback, scope, [
                    loadedNodes,
                    operation,
                    success
                ]);
            };
        }
        // Assign the ID of the Operation so that a ServerProxy can set its idParam parameter,
        // or a REST proxy can create the correct URL
        options.id = node.getId();
        // Only add filtering and sorting options if those options are remote
        me.setLoadOptions(options);
        if (me.getRemoteSort() && options.sorters) {
            me.fireEvent('beforesort', me, options.sorters);
        }
        options = Ext.apply({
            node: options.node || node,
            internalScope: me,
            internalCallback: me.onProxyLoad
        }, options);
        me.lastOptions = Ext.apply({}, options);
        // Must not be copied into lastOptions, otherwise it overrides next call.
        options.isRootLoad = isRootLoad;
        operation = me.createOperation('read', options);
        if (me.fireEvent('beforeload', me, operation) !== false) {
            // Set the loading flag early
            // Used by onNodeRemove to NOT add the removed nodes to the removed collection
            me.loading = true;
            // If this is a full root load, clear the root node and the flat data.
            if (isRootLoad) {
                if (me.getClearRemovedOnLoad()) {
                    me.removedNodes.length = 0;
                }
                if (clearOnLoad) {
                    // Unregister all descendants, but immediately re-register the root
                    // It is NOT being tipped out by this load operation
                    me.unregisterNode(node, true);
                    node.clear(false, true);
                    me.registerNode(node);
                    doClear = true;
                }
            } else // Load a non-root
            {
                if (me.loading) {
                    // set loaded: false so that the eventual response will trigger the UI update
                    node.data.loaded = false;
                }
                if (me.getTrackRemoved() && me.getClearRemovedOnLoad()) {
                    // clear from the removed array any nodes that were descendants of the node
                    // being reloaded so that they do not get saved on next sync.
                    me.clearRemoved(node);
                }
                if (clearOnLoad) {
                    node.removeAll(false);
                }
            }
            // Silently set loading state if the node doesn't already exist so the UI displays
            // the load icon but doesn't attempt other UI updates
            if (me.loading && node) {
                /* eslint-disable-next-line max-len */
                node.set('loading', true, {
                    silent: !(me.contains(node) || node === me.getRoot())
                });
            }
            if (doClear) {
                me.clearData(true);
                // Read the root we just cleared, since we're reloading it
                if (me.getRootVisible()) {
                    me.suspendEvents();
                    me.add(node);
                    me.resumeEvents();
                }
            }
            operation.execute();
        }
        return me;
    },
    onProxyLoad: function(operation) {
        var me = this,
            options = operation.initialConfig,
            successful = operation.wasSuccessful(),
            records = operation.getRecords(),
            node = options.node,
            isRootLoad = options.isRootLoad,
            scope = operation.getScope() || me,
            args = [
                records,
                operation,
                successful
            ];
        if (me.destroyed) {
            return;
        }
        me.loading = false;
        node.set('loading', false);
        if (successful) {
            ++me.loadCount;
            if (!me.getClearOnLoad()) {
                records = me.cleanRecords(node, records);
            }
            // Nodes are in linear form, linked to the parent using a parentId property
            if (me.getParentIdProperty()) {
                records = me.treeify(node, records);
            }
            if (isRootLoad) {
                me.suspendEvent('add', 'update');
            }
            records = me.fillNode(node, records);
        }
        // The load event has an extra node parameter
        // (differing from the load event described in AbstractStore)
        /**
         * @event load
         * Fires whenever the store reads data from a remote data source.
         * @param {Ext.data.TreeStore} this
         * @param {Ext.data.TreeModel[]} records An array of records.
         * @param {Boolean} successful True if the operation was successful.
         * @param {Ext.data.operation.Operation} operation The operation that triggered this load.
         * @param {Ext.data.NodeInterface} node The node that was loaded.
         */
        Ext.callback(options.onChildNodesAvailable, scope, args);
        if (isRootLoad) {
            me.resumeEvent('add', 'update');
            me.callObservers('BeforePopulate');
            me.fireEvent('datachanged', me);
            me.fireEvent('refresh', me);
            me.callObservers('AfterPopulate');
        }
        me.fireEvent('load', me, records, successful, operation, node);
    },
    /**
     * Removes all records that used to be descendants of the passed node from the removed array
     * @private
     * @param {Ext.data.NodeInterface} node
     */
    clearRemoved: function(node) {
        var me = this,
            removed = me.removedNodes,
            id = node.getId(),
            removedLength = removed.length,
            i = removedLength,
            recordsToClear = {},
            newRemoved = [],
            removedHash = {},
            removedNode, targetNode, targetId;
        if (node === me.getRoot()) {
            // if the passed node is the root node, just reset the removed array
            me.removedNodes.length = 0;
            return;
        }
        // add removed records to a hash so they can be easily retrieved by id later
        for (; i--; ) {
            removedNode = removed[i];
            removedHash[removedNode.getId()] = removedNode;
        }
        for (i = removedLength; i--; ) {
            removedNode = removed[i];
            targetNode = removedNode;
            while (targetNode && targetNode.getId() !== id) {
                // walk up the parent hierarchy until we find the passed node or until
                // we get to the root node lastParentId is set in nodes which have been removed.
                targetId = targetNode.get('parentId') || targetNode.get('lastParentId');
                targetNode = targetNode.parentNode || me.getNodeById(targetId) || removedHash[targetId];
            }
            if (targetNode) {
                // removed node was previously a descendant of the passed node - add it
                // to the records to clear from "removed" later
                recordsToClear[removedNode.getId()] = removedNode;
            }
        }
        // create a new removed array containing only the records that are not in recordsToClear
        for (i = 0; i < removedLength; i++) {
            removedNode = removed[i];
            if (!recordsToClear[removedNode.getId()]) {
                newRemoved.push(removedNode);
            }
        }
        me.removedNodes = newRemoved;
    },
    /**
     * Fills a node with a series of child records.
     * @private
     * @param {Ext.data.NodeInterface} node The node to fill
     * @param {Ext.data.TreeModel[]} newNodes The records to add
     */
    fillNode: function(node, newNodes) {
        var me = this,
            newNodeCount = newNodes ? newNodes.length : 0;
        // If we're filling, increment the counter so nodes can react without doing
        // expensive operations
        if (++me.bulkUpdate === 1) {
            me.suspendEvent('datachanged');
        }
        if (newNodeCount) {
            me.setupNodes(newNodes);
            node.appendChild(newNodes, undefined, true);
        } else // only set loaded if there are no newNodes;
        // appendChild already handles updating the loaded status, 
        // and will do it *after* the child nodes have been added
        {
            if (me.bulkUpdate === 1) {
                node.set('loaded', true);
            } else {
                node.data.loaded = true;
            }
        }
        if (!--me.bulkUpdate) {
            me.resumeEvent('datachanged');
        }
        // No need to call registerNode here, because each child will register itself as it joins
        return newNodes;
    },
    setupNodes: function(newNodes) {
        var me = this,
            sorters = me.getSorters(),
            needsIndexSort = false,
            newNodeCount = newNodes.length,
            /* eslint-disable-next-line max-len */
            performLocalSort = me.sortOnLoad && newNodeCount > 1 && !me.getRemoteSort() && me.getFolderSort() || sorters.length,
            performLocalFilter = me.needsLocalFilter(),
            node1, node2, i;
        // Apply any local filter to the nodes as we fill
        if (performLocalFilter) {
            me.doFilter(newNodes[0]);
        }
        // See if there are any differing index values in the new nodes. If not, then we do not
        // have to sortByIndex
        for (i = 1; i < newNodeCount; i++) {
            node1 = newNodes[i];
            node2 = newNodes[i - 1];
            // Apply any filter to the nodes as we fill
            if (performLocalFilter) {
                me.doFilter(node1);
            }
            needsIndexSort = node1.data.index !== node2.data.index;
        }
        // If there is a set of local sorters defined.
        if (performLocalSort) {
            // If sorting by index is needed, sort by index first
            me.needsIndexSort = true;
            Ext.Array.sort(newNodes, me.getSortFn());
            me.needsIndexSort = false;
        } else if (needsIndexSort) {
            Ext.Array.sort(newNodes, me.sortByIndex);
        }
    },
    // Called by a node which is appending children to itself
    beginFill: function() {
        var me = this;
        if (!me.fillCount++) {
            // jshint ignore:line
            me.beginUpdate();
            me.suspendEvent('add', 'update');
            me.suspendAutoSync();
            me.fillArray = [];
        }
    },
    // resume view updating and data syncing after a node fill
    endFill: function(parent, nodes) {
        var me = this,
            fillArray = me.fillArray,
            i, len, index;
        // Keep every block of records added during the fill
        fillArray.push(nodes);
        if (!--me.fillCount) {
            me.resumeAutoSync();
            me.resumeEvent('add', 'update');
            // Add all blocks of records from nested beginFill calls.
            // appendChild can load local child data and recursively call appendChild.
            // This coalesces all add operations into a layout suspension
            for (i = 0 , len = fillArray.length; i < len; i++) {
                index = me.indexOf(fillArray[i][0]);
                // Only inform views if the blocks appended actually made it into the linear store
                // (are visible)
                if (index !== -1) {
                    me.fireEvent('add', me, fillArray[i], index);
                }
            }
            me.fillArray = null;
            me.endUpdate();
        }
    },
    /**
     * Sorter function for sorting records in index order
     * @private
     * @param {Ext.data.NodeInterface} node1
     * @param {Ext.data.NodeInterface} node2
     * @return {Number}
     */
    sortByIndex: function(node1, node2) {
        return node1.data.index - node2.data.index;
    },
    onIdChanged: function(node, oldId, newId) {
        var childNodes = node.childNodes,
            len = childNodes && childNodes.length,
            i;
        this.callParent(arguments);
        delete this.byIdMap[oldId];
        this.byIdMap[newId] = node;
        // Ensure all child nodes know their parent's new ID
        for (i = 0; i < len; i++) {
            childNodes[i].set('parentId', newId);
        }
    },
    /**
     * @private
     * Converts a flat array of nodes into a tree structure.
     * Returns an array which is the childNodes array of the rootNode.
     */
    treeify: function(parentNode, records) {
        var me = this,
            loadParentNodeId = parentNode.getId(),
            parentIdProperty = me.getParentIdProperty(),
            len = records.length,
            result = [],
            nodeMap = {},
            i, node, parentId, parent, id, children;
        // Collect all nodes keyed by ID, so that regardless of order, they can all be
        // linked to a parent.
        for (i = 0; i < len; i++) {
            node = records[i];
            node.data.depth = 1;
            nodeMap[node.id] = node;
        }
        // Link child nodes up to their parents
        for (i = 0; i < len; i++) {
            node = records[i];
            parentId = node.data[parentIdProperty];
            if (!(parentId || parentId === 0) || parentId === loadParentNodeId) {
                result.push(node);
            } else {
                if (!nodeMap[parentId]) {
                    Ext.raise('Ext.data.TreeStore, Invalid parentId "' + parentId + '"');
                }
                parent = nodeMap[parentId];
                parent.$children = parent.$children || [];
                parent.$children.push(node);
                node.data.depth = parent.data.depth + 1;
            }
        }
        for (id in nodeMap) {
            node = nodeMap[id];
            children = node.$children;
            if (children) {
                delete node.$children;
                me.setupNodes(children);
                node.appendChild(children);
            }
            me.registerNode(node);
        }
        me.setupNodes(result);
        return result;
    },
    cleanRecords: function(node, records) {
        var nodeHash = {},
            childNodes = node.childNodes,
            i = 0,
            len = childNodes.length,
            out = [],
            rec;
        // build a hash of all the childNodes under the current node for performance
        for (; i < len; ++i) {
            nodeHash[childNodes[i].getId()] = true;
        }
        for (i = 0 , len = records.length; i < len; ++i) {
            rec = records[i];
            if (!nodeHash[rec.getId()]) {
                out.push(rec);
            }
        }
        return out;
    },
    removeAll: function() {
        var me = this,
            root = me.getRoot(),
            ln = me.getData().length;
        me.suspendEvents();
        me.setRoot(null);
        me.resumeEvents();
        me.callParent();
        if (ln) {
            me.fireEvent('clear', me);
            me.fireEvent('rootchange', root, null);
        }
    },
    doSort: function(sorterFn) {
        var me = this;
        if (me.getRemoteSort()) {
            // the load function will pick up the new sorters and request the sorted data
            // from the proxy
            me.load();
        } else {
            me.tree.sort(sorterFn, true);
            me.fireEvent('datachanged', me);
            me.fireEvent('refresh', me);
        }
        me.fireEvent('sort', me, me.sorters.getRange());
    },
    filterVisible: function(node) {
        return node.get('visible');
    },
    /**
     * @method isVisible
     * @inheritdoc Ext.data.NodeStore#method-isVisible
     */
    isVisible: function(node) {
        var parentNode = node.parentNode,
            visible = node.data.visible,
            root = this.getRoot();
        while (visible && parentNode) {
            visible = parentNode.data.expanded && parentNode.data.visible;
            parentNode = parentNode.parentNode;
        }
        // The passed node is visible if we ended up at the root node, and it is visible.
        // UNLESS it's the root node, and we are configured with rootVisible:false
        return visible && !(node === root && !this.getRootVisible());
    },
    commitChanges: function() {
        var removed = this.removedNodes;
        if (removed) {
            removed.length = 0;
        }
        this.callParent();
    },
    /**
     * Returns the root node for this tree.
     * @return {Ext.data.NodeInterface}
     * @deprecated 5.0 Use {@link #getRoot} instead
     */
    getRootNode: function() {
        return this.getRoot();
    },
    /**
     * Sets the root node for this store.  See also the {@link #root} config option.
     * @param {Ext.data.TreeModel/Ext.data.NodeInterface/Object} root
     * @return {Ext.data.NodeInterface} The new root
     * @deprecated 5.0 Use {@link #setRoot} instead
     */
    setRootNode: function(root) {
        this.setRoot(root);
        return this.getRoot();
    },
    privates: {
        fireChangeEvent: function(record) {
            return !!this.byIdMap[record.id];
        },
        /**
         * @private
         * Returns the array of nodes which have been removed since the last time this store
         * was synced.
         *
         * This is used internally, when purging removed records after a successful sync.
         * This is overridden by TreeStore because TreeStore accumulates deleted records on removal
         * of child nodes from their parent, *not* on removal of records from its collection.
         * The collection has records added on expand, and removed on collapse.
         */
        getRawRemovedRecords: function() {
            return this.removedNodes;
        },
        createOperation: function(type, options) {
            // Use the proxy assigned to the node by preference so that in a heterogeneous tree,
            // different node types can use different proxies and data providers.
            var me = this,
                node = options.node,
                proxy;
            // Use the node's proxy by preference if we acquired our proxy
            // from the configured Model (See ProxyStore#applyProxy)
            if (me.useModelProxy && node && node !== me.getRootNode()) {
                proxy = node.getProxy();
            }
            // If the node has a proxy which is different from this Store's proxy
            // then we must use that to create and manage its I/O operations.
            if (proxy && proxy !== me.getProxy()) {
                return proxy.createOperation(type, options);
            } else {
                return me.callParent([
                    type,
                    options
                ]);
            }
        },
        /**
         * @private
         * A function passed into {@link Ext.data.reader.Reader#extractData} which is used
         * as a record creation function.
         * @param {Object} data Raw data to transform into a record
         * @param {Function} Model Model constructor to create a record from the passed data.
         * @return {Ext.data.Model} The resulting new Model instance.
         */
        recordCreator: function(data, Model) {
            return new Model(data);
        },
        doFilter: function(node) {
            this.filterNodes(node, this.getFilters().getFilterFn(), true);
        },
        /**
         * @private
         * Filters the passed node according to the passed function.
         *
         * If this TreeStore is configured with {@link #cfg-filterer filterer: 'bottomup'}, leaf
         * nodes are tested according to the function. Additionally, parent nodes are filtered in
         * if any descendant leaf nodes have passed the filter test.
         *
         * Otherwise a parent node which fails the test will terminate the branch and
         * descendant nodes which pass the filter test will be filtered out.
         */
        filterNodes: function(node, filterFn, parentVisible) {
            var me = this,
                bottomUpFiltering = me.filterer === 'bottomup',
                // MUST call filterFn first to avoid shortcutting if parentVisible is false.
                // filterFn may have side effects, so must be called on all nodes.
                match = filterFn(node) && parentVisible || (node.isRoot() && !me.getRootVisible()),
                childNodes = node.childNodes,
                len = childNodes && childNodes.length,
                i, matchingChildren;
            if (len) {
                for (i = 0; i < len; ++i) {
                    // MUST call method first to avoid shortcutting boolean expression
                    // if matchingChildren is true
                    /* eslint-disable-next-line max-len */
                    matchingChildren = me.filterNodes(childNodes[i], filterFn, match || bottomUpFiltering) || matchingChildren;
                }
                if (bottomUpFiltering) {
                    match = matchingChildren || match;
                }
            }
            node.set("visible", match, me._silentOptions);
            return match;
        },
        needsLocalFilter: function() {
            return !this.getRemoteFilter() && this.getFilters().length;
        },
        onRemoteFilterSet: function(filters, remoteFilter) {
            // Filtering is done at the Store level for TreeStores.
            // It has to be done on a hierarchical basis.
            // The onFilterEndUpdate signal has to be passed into the root node which filters
            // its children and cascades the filter instruction downwards.
            var data = this.getData();
            data.setFilters(null);
            if (filters) {
                filters.on('endupdate', this.onFilterEndUpdate, this);
            }
        },
        onRemoteSortSet: function(sorters, remoteSort) {
            // Sorting is done at the Store level for TreeStores.
            // It has to be done on a hierarchical basis.
            // The onSorterEndUpdate signal has to be passed root node which sorts its children
            // and cascades the sort instruction downwards.
            var data = this.getData();
            data.setSorters(null);
            if (sorters) {
                sorters.on('endupdate', this.onSorterEndUpdate, this);
            }
        }
    },
    deprecated: {
        5: {
            properties: {
                tree: null
            }
        }
    }
});

/**
 * @deprecated 5.0 Please use {@link Ext.data.field.Field field types} instead.
 */
Ext.define('Ext.data.Types', {
    singleton: true,
    requires: [
        'Ext.data.SortTypes'
    ]
}, function(Types) {
    var SortTypes = Ext.data.SortTypes;
    Ext.apply(Types, {
        /**
         * @property {RegExp} stripRe
         * A regular expression for stripping non-numeric characters from a numeric value.
         * This should be overridden for localization.
         */
        stripRe: /[$,%]/g,
        /**
         * @property {Object} AUTO
         * This data type means that no conversion is applied to the raw data before it is placed
         * into a Record.
         */
        AUTO: {
            sortType: SortTypes.none,
            type: 'auto'
        },
        /**
         * @property {Object} STRING
         * This data type means that the raw data is converted into a String before it is placed
         * into a Record.
         */
        STRING: {
            convert: function(v) {
                var defaultValue = this.getAllowNull() ? null : '';
                return (v === undefined || v === null) ? defaultValue : String(v);
            },
            sortType: SortTypes.asUCString,
            type: 'string'
        },
        /**
         * @property {Object} INT
         * This data type means that the raw data is converted into an integer before it is placed
         * into a Record.
         *
         * The synonym `INTEGER` is equivalent.
         */
        INT: {
            convert: function(v) {
                // Handle values which are already numbers.
                // Value truncation behaviour of parseInt is historic and must be maintained.
                // parseInt(35.9)  and parseInt("35.9") returns 35
                if (typeof v === 'number') {
                    return parseInt(v, 10);
                }
                return v !== undefined && v !== null && v !== '' ? parseInt(String(v).replace(Types.stripRe, ''), 10) : (this.getAllowNull() ? null : 0);
            },
            sortType: SortTypes.none,
            type: 'int'
        },
        /**
         * @property {Object} FLOAT
         * This data type means that the raw data is converted into a number before it is placed
         * into a Record.
         *
         * The synonym `NUMBER` is equivalent.
         */
        FLOAT: {
            convert: function(v) {
                if (typeof v === 'number') {
                    return v;
                }
                return v !== undefined && v !== null && v !== '' ? parseFloat(String(v).replace(Types.stripRe, ''), 10) : (this.getAllowNull() ? null : 0);
            },
            sortType: SortTypes.none,
            type: 'float'
        },
        /**
         * @property {Object} BOOL
         * This data type means that the raw data is converted into a boolean before it is placed
         * into a Record. The string "true" and the number 1 are converted to boolean true.
         *
         * The synonym `BOOLEAN` is equivalent.
         */
        BOOL: {
            convert: function(v) {
                if (typeof v === 'boolean') {
                    return v;
                }
                if (this.getAllowNull() && (v === undefined || v === null || v === '')) {
                    return null;
                }
                return v === 'true' || v == 1;
            },
            // eslint-disable-line eqeqeq
            sortType: SortTypes.none,
            type: 'bool'
        },
        /**
         * @property {Object} DATE
         * This data type means that the raw data is converted into a Date before it is placed
         * into a Record. The date format is specified in the constructor of the
         * {@link Ext.data.Field} to which this type is being applied.
         */
        DATE: {
            convert: function(v) {
                var df = this.getDateReadFormat() || this.getDateFormat(),
                    parsed;
                if (!v) {
                    return null;
                }
                // instanceof check ~10 times faster than Ext.isDate. Values here will not
                // be cross-document objects
                if (v instanceof Date) {
                    return v;
                }
                if (df) {
                    return Ext.Date.parse(v, df);
                }
                parsed = Date.parse(v);
                return parsed ? new Date(parsed) : null;
            },
            sortType: SortTypes.asDate,
            type: 'date'
        }
    });
    /**
     * @member Ext.data.Types
     * @property {Object} BOOLEAN
     * This data type means that the raw data is converted into a boolean before it is placed into
     * a Record. The string "true" and the number 1 are converted to boolean `true`.
     *
     * The synonym `BOOL` is equivalent.
     */
    Types.BOOLEAN = Types.BOOL;
    /**
     * @member Ext.data.Types
     * @property {Object} INTEGER
     * This data type means that the raw data is converted into an integer before it is placed
     * into a Record.
     *
     * The synonym `INT` is equivalent.
     */
    Types.INTEGER = Types.INT;
    /**
     * @member Ext.data.Types
     * @property {Object} NUMBER
     * This data type means that the raw data is converted into a number before it is placed
     * into a Record.
     *
     * The synonym `FLOAT` is equivalent.
     */
    Types.NUMBER = Types.FLOAT;
});

/**
 * This class is used to hold validation errors for a record. The results of the record's
 * `{@link Ext.data.Model#validators validators}` are stored as the field values of this
 * record. The first failed validation is all that is stored per field unless the Model
 * class has defined a `validationSeparator` config.
 * @since 5.0.0
 */
Ext.define('Ext.data.Validation', {
    extend: 'Ext.data.Model',
    isValidation: true,
    /**
     * @property {Number} syncGeneration
     * This is a capture of the `{@link Ext.data.Model#generation}` value from the last
     * time the validation state was refreshed. This is used to determine if this object
     * is potentially out-of-date with its associated `record`.
     * @private
     * @readonly
     * @since 5.0.0
     */
    syncGeneration: 0,
    // Model generation starts at 1 so we start out-of-sync
    /**
     * Attaches this instance to its associated `record`.
     * @param {Ext.data.Model} record The associated record.
     * @private
     * @since 5.0.0
     */
    attach: function(record) {
        /**
         * @property {Ext.data.Model} record
         * The associated record for this validation instance.
         * @readonly
         * @since 5.0.0
         */
        this.record = record;
        this.isBase = record.self === Ext.data.Model;
        // We need to remove the id property from our data as that is not meaningful for
        // a Validation pseudo-record.
        delete this.data.id;
    },
    getValidation: function() {
        return null;
    },
    /**
     * Returns true if the associated record (not this one) is valid.
     * @return {Boolean}
     */
    isValid: function() {
        var me = this;
        if (me.syncGeneration !== me.record.generation) {
            me.refresh();
        }
        return !me.dirty;
    },
    /**
     * This method updates the state of this record against its associated `record`. This
     * method should not need to be called directly as it is internally called when this
     * record is returned by `{@link Ext.data.Model#getValidation}`.
     * @param {Boolean} [force=false] Pass `true` to force an update of validation state.
     * @private
     * @since 5.0.0
     */
    refresh: function(force) {
        // If it's an Ext.data.Model instance directly, we can't
        // validate it because there can be no fields/validators.
        if (this.isBase) {
            return;
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            data = me.data,
            record = me.record,
            fields = record.fields,
            generation = record.generation,
            recordData = record.data,
            sep = record.validationSeparator,
            values = null,
            defaultMessage, currentValue, error, field, i, len, msg, val, name;
        if (force || me.syncGeneration !== generation) {
            me.syncGeneration = generation;
            for (i = 0 , len = fields.length; i < len; ++i) {
                field = fields[i];
                name = field.name;
                val = recordData[name];
                defaultMessage = field.defaultInvalidMessage;
                error = 0;
                if (!(name in data)) {
                    // Now is the cheapest time to populate our data object with "true"
                    // for all validated fields. This ensures that our non-dirty state
                    // equates to isValid.
                    data[name] = currentValue = true;
                } else // true === valid
                {
                    currentValue = data[name];
                }
                if (field.validate !== Ext.emptyFn) {
                    msg = field.validate(val, sep, null, record);
                    if (msg !== true) {
                        error = msg || defaultMessage;
                    }
                }
                if (!error) {
                    error = true;
                }
                // valid state is stored as true
                if (error !== currentValue) {
                    (values || (values = {}))[name] = error;
                }
            }
            if (values) {
                // only need to do this if something changed...
                me.set(values);
            }
        }
    }
});

/**
 * @alternateClassName Ext.DomHelper
 * @alternateClassName Ext.core.DomHelper
 *
 * @singleton
 *
 * The DomHelper class provides a layer of abstraction from DOM and transparently supports creating
 * elements via DOM or using HTML fragments. It also has the ability to create HTML fragment
 * templates from your DOM building code.
 *
 * ## DomHelper element specification object
 *
 * A specification object is used when creating elements. Attributes of this object are assumed
 * to be element attributes, except for 4 special attributes:
 *
 * * **tag**: The tag name of the element
 * * **children (or cn)**: An array of the same kind of element definition objects to be created
 * and appended. These can be nested as deep as you want.
 * * **cls**: The class attribute of the element. This will end up being either the "class"
 * attribute on a HTML fragment or className for a DOM node, depending on whether DomHelper is using
 * fragments or DOM.
 * * **html**: The innerHTML for the element
 *
 * ## Insertion methods
 *
 * Commonly used insertion methods:
 *
 * * {@link #append}
 * * {@link #insertBefore}
 * * {@link #insertAfter}
 * * {@link #overwrite}
 * * {@link #insertHtml}
 *
 * ## Example
 *
 * This is an example, where an unordered list with 3 children items is appended to an existing
 * element with id 'my-div':
 *
 *     var dh = Ext.DomHelper; // create shorthand alias
 *     // specification object
 *     var spec = {
 *         id: 'my-ul',
 *         tag: 'ul',
 *         cls: 'my-list',
 *         // append children after creating
 *         children: [     // may also specify 'cn' instead of 'children'
 *             {tag: 'li', id: 'item0', html: 'List Item 0'},
 *             {tag: 'li', id: 'item1', html: 'List Item 1'},
 *             {tag: 'li', id: 'item2', html: 'List Item 2'}
 *         ]
 *     };
 *     var list = dh.append(
 *         'my-div', // the context element 'my-div' can either be the id or the actual node
 *         spec      // the specification object
 *     );
 *
 * Element creation specification parameters in this class may also be passed as an Array
 * of specification objects. This can be used to insert multiple sibling nodes into an existing
 * container very efficiently. For example, to add more list items to the example above:
 *
 *     dh.append('my-ul', [
 *         {tag: 'li', id: 'item3', html: 'List Item 3'},
 *         {tag: 'li', id: 'item4', html: 'List Item 4'}
 *     ]);
 *
 * ## Templating
 *
 * The real power is in the built-in templating. Instead of creating or appending any elements,
 * createTemplate returns a Template object which can be used over and over to insert new elements.
 * Revisiting the example above, we could utilize templating this time:
 *
 *     // create the node
 *     var list = dh.append('my-div', {tag: 'ul', cls: 'my-list'});
 *     // get template
 *     var tpl = dh.createTemplate({tag: 'li', id: 'item{0}', html: 'List Item {0}'});
 *
 *     for(var i = 0; i < 5; i++){
 *         tpl.append(list, i); // use template to append to the actual node
 *     }
 *
 * An example using a template:
 *
 *     var html = '"{0}" href="{1}" class="nav">{2}';
 *
 *     var tpl = new Ext.DomHelper.createTemplate(html);
 *     tpl.append('blog-roll', ['link1', 'http://www.foxmulder.com/', "Fox's Site"]);
 *     tpl.append('blog-roll', ['link2', 'http://www.danascully.org/', "Scully's Site"]);
 *
 * The same example using named parameters:
 *
 *     var html = '"{id}" href="{url}" class="nav">{text}';
 *
 *     var tpl = new Ext.DomHelper.createTemplate(html);
 *     tpl.append('blog-roll', {
 *         id: 'link1',
 *         url: 'http://www.danascully.org/',
 *         text: "Scully's Site"
 *     });
 *     tpl.append('blog-roll', {
 *         id: 'link2',
 *         url: 'http://www.foxmulder.com/',
 *         text: "Fox's Site"
 *     });
 *
 * ## Compiling Templates
 *
 * Templates are applied using regular expressions. The performance is great, but if you are adding
 * a bunch of DOM elements using the same template, you can increase performance even further by
 * "compiling" the template. The way "compile()" works is the template is parsed and broken up
 * at the different variable points and a dynamic function is created and eval'ed. The generated
 * function performs string concatenation of these parts and the passed variables instead of using
 * regular expressions.
 *
 *     var html = '"{id}" href="{url}" class="nav">{text}';
 *
 *     var tpl = new Ext.DomHelper.createTemplate(html);
 *     tpl.compile();
 *
 *     // ... use template like normal
 *
 * ## Performance Boost
 *
 * DomHelper will transparently create HTML fragments when it can. Using HTML fragments instead of
 * DOM can significantly boost performance.
 *
 * Element creation specification parameters may also be strings which are used as innerHTML.
 */
Ext.define('Ext.dom.Helper', function() {
    var afterbegin = 'afterbegin',
        afterend = 'afterend',
        beforebegin = 'beforebegin',
        beforeend = 'beforeend',
        bbValues = [
            'BeforeBegin',
            'previousSibling'
        ],
        aeValues = [
            'AfterEnd',
            'nextSibling'
        ],
        bb_ae_PositionHash = {
            beforebegin: bbValues,
            afterend: aeValues
        },
        fullPositionHash = {
            beforebegin: bbValues,
            afterend: aeValues,
            afterbegin: [
                'AfterBegin',
                'firstChild'
            ],
            beforeend: [
                'BeforeEnd',
                'lastChild'
            ]
        };
    return {
        singleton: true,
        alternateClassName: [
            'Ext.DomHelper',
            'Ext.core.DomHelper'
        ],
        emptyTags: /^(?:br|frame|hr|img|input|link|meta|range|spacer|wbr|area|param|col)$/i,
        confRe: /^(?:tag|children|cn|html|tpl|tplData)$/i,
        endRe: /end/i,
        // Since cls & for are reserved words, we need to transform them
        attributeTransform: {
            cls: 'class',
            htmlFor: 'for'
        },
        closeTags: {},
        detachedDiv: document.createElement('div'),
        decamelizeName: function() {
            var camelCaseRe = /([a-z])([A-Z])/g,
                cache = {};
            function decamel(match, p1, p2) {
                return p1 + '-' + p2.toLowerCase();
            }
            return function(s) {
                return cache[s] || (cache[s] = s.replace(camelCaseRe, decamel));
            };
        }(),
        generateMarkup: function(spec, buffer) {
            var me = this,
                specType = typeof spec,
                attr, val, tag, i, closeTags;
            if (specType === "string" || specType === "number") {
                buffer.push(spec);
            } else if (Ext.isArray(spec)) {
                for (i = 0; i < spec.length; i++) {
                    if (spec[i]) {
                        me.generateMarkup(spec[i], buffer);
                    }
                }
            } else {
                tag = spec.tag || 'div';
                buffer.push('<', tag);
                for (attr in spec) {
                    if (spec.hasOwnProperty(attr)) {
                        val = spec[attr];
                        if (val !== undefined && !me.confRe.test(attr)) {
                            if (val && val.join) {
                                val = val.join(' ');
                            }
                            if (typeof val === "object") {
                                buffer.push(' ', attr, '="');
                                me.generateStyles(val, buffer, true).push('"');
                            } else {
                                buffer.push(' ', me.attributeTransform[attr] || attr, '="', val, '"');
                            }
                        }
                    }
                }
                // Now either just close the tag or try to add children and close the tag.
                if (me.emptyTags.test(tag)) {
                    buffer.push('/>');
                } else {
                    buffer.push('>');
                    // Apply the tpl html, and cn specifications
                    if ((val = spec.tpl)) {
                        val.applyOut(spec.tplData, buffer);
                    }
                    if ((val = spec.html)) {
                        buffer.push(val);
                    }
                    if ((val = spec.cn || spec.children)) {
                        me.generateMarkup(val, buffer);
                    }
                    // we generate a lot of close tags, so cache them rather than push 3 parts
                    closeTags = me.closeTags;
                    buffer.push(closeTags[tag] || (closeTags[tag] = '</' + tag + '>'));
                }
            }
            return buffer;
        },
        /**
         * Converts the styles from the given object to text. The styles are CSS style names
         * with their associated value.
         * 
         * The basic form of this method returns a string:
         * 
         *      var s = Ext.DomHelper.generateStyles({
         *          backgroundColor: 'red'
         *      });
         *      
         *      // s = 'background-color:red;'
         *
         * Alternatively, this method can append to an output array.
         * 
         *      var buf = [];
         *
         *      ...
         *
         *      Ext.DomHelper.generateStyles({
         *          backgroundColor: 'red'
         *      }, buf);
         *
         * In this case, the style text is pushed on to the array and the array is returned.
         * 
         * @param {Object} styles The object describing the styles.
         * @param {String[]} [buffer] The output buffer.
         * @param {Boolean} [encode] `true` to {@link Ext.String#htmlEncode} property values if they
         * are going to be inserted as HTML attributes.
         * @return {String/String[]} If buffer is passed, it is returned. Otherwise the style
         * string is returned.
         */
        generateStyles: function(styles, buffer, encode) {
            var a = buffer || [],
                name, val;
            for (name in styles) {
                if (styles.hasOwnProperty(name)) {
                    val = styles[name];
                    // Since a majority of attributes won't have html characters (basically
                    // restricted to fonts), we'll check first before we try and encode it
                    // because it's less expensive and this method gets called a lot.
                    name = this.decamelizeName(name);
                    if (encode && Ext.String.hasHtmlCharacters(val)) {
                        val = Ext.String.htmlEncode(val);
                    }
                    a.push(name, ':', val, ';');
                }
            }
            return buffer || a.join('');
        },
        /**
         * Returns the markup for the passed Element(s) config.
         * @param {Object} spec The DOM object spec (and children).
         * @return {String}
         */
        markup: function(spec) {
            var buf;
            if (typeof spec === "string") {
                return spec;
            }
            buf = this.generateMarkup(spec, []);
            return buf.join('');
        },
        /**
         * Applies a style specification to an element.
         * 
         * Styles in object form should be a valid DOM element style property.  
         * [Valid style property names](http://www.w3schools.com/jsref/dom_obj_style.asp) 
         * (_along with the supported CSS version for each_)
         * 
         *     // <div id="my-el">Phineas Flynn</div>
         *     
         *     var el = Ext.get('my-el'),
         *         dh = Ext.dom.Helper;
         *     
         *     dh.applyStyles(el, 'color: white;');
         *     
         *     dh.applyStyles(el, {
         *         fontWeight: 'bold',
         *         backgroundColor: 'gray',
         *         padding: '10px'
         *     });
         *     
         *     dh.applyStyles(el, function () {
         *         if (name.initialConfig.html === 'Phineas Flynn') {
         *             return 'font-style: italic;';
         *             // OR return { fontStyle: 'italic' };
         *         }
         *     });
         * 
         * @param {String/HTMLElement/Ext.dom.Element} el The element to apply styles to
         * @param {String/Object/Function} styles A style specification string e.g. 'width:100px',
         * or object in the form {width:'100px'}, or a function which returns such a specification.
         */
        applyStyles: function(el, styles) {
            Ext.fly(el).applyStyles(styles);
        },
        /**
         * @private
         * Fix for browsers which do not support createContextualFragment
         */
        createContextualFragment: function(html) {
            var div = this.detachedDiv,
                fragment = document.createDocumentFragment(),
                length, childNodes;
            div.innerHTML = html;
            childNodes = div.childNodes;
            length = childNodes.length;
            // Move nodes into fragment, don't clone: http://jsperf.com/create-fragment
            while (length--) {
                fragment.appendChild(childNodes[0]);
            }
            return fragment;
        },
        /**
         * Creates new DOM element(s) without inserting them to the document.
         * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
         * @return {HTMLElement} The new un-inserted node
         */
        createDom: function(o) {
            var me = this,
                markup = me.markup(o),
                div = me.detachedDiv,
                child;
            div.innerHTML = markup;
            child = div.firstChild;
            // Important to clone the node here, IE8 & 9 have an issue where the markup
            // in the first element will be lost.
            // var ct = document.createElement('div'),
            //     a, b;
            //     ct.innerHTML = '<div>markup1</div>';
            //     a = ct.firstChild;
            //     ct.innerHTML = '<div>markup2</div>';
            //     b = ct.firstChild;
            //     console.log(a.innerHTML, b.innerHTML);
            return Ext.supports.ChildContentClearedWhenSettingInnerHTML ? child.cloneNode(true) : child;
        },
        /**
         * Inserts an HTML fragment into the DOM.
         * @param {String} where Where to insert the html in relation to el - beforeBegin,
         * afterBegin, beforeEnd, afterEnd.
         *
         * For example take the following HTML: `<div>Contents</div>`
         *
         * Using different `where` values inserts element to the following places:
         *
         * - beforeBegin: `<HERE><div>Contents</div>`
         * - afterBegin: `<div><HERE>Contents</div>`
         * - beforeEnd: `<div>Contents<HERE></div>`
         * - afterEnd: `<div>Contents</div><HERE>`
         *
         * @param {HTMLElement/TextNode} el The context element
         * @param {String} html The HTML fragment
         * @return {HTMLElement} The new node
         */
        insertHtml: function(where, el, html) {
            var me = this,
                hashVal, range, rangeEl, setStart, frag;
            where = where.toLowerCase();
            // Has fast HTML insertion into existing DOM: http://www.w3.org/TR/html5/apis-in-html-documents.html#insertadjacenthtml
            if (el.insertAdjacentHTML) {
                if (me.ieInsertHtml) {
                    // hook for IE table hack - impl in ext package override
                    frag = me.ieInsertHtml(where, el, html);
                    if (frag) {
                        return frag;
                    }
                }
                hashVal = fullPositionHash[where];
                if (hashVal) {
                    el.insertAdjacentHTML(hashVal[0], html);
                    return el[hashVal[1]];
                }
            } else // if (not IE and context element is an HTMLElement) or TextNode
            {
                // we cannot insert anything inside a textnode so...
                if (el.nodeType === 3) {
                    where = where === afterbegin ? beforebegin : where;
                    where = where === beforeend ? afterend : where;
                }
                range = Ext.supports.CreateContextualFragment ? el.ownerDocument.createRange() : undefined;
                setStart = 'setStart' + (this.endRe.test(where) ? 'After' : 'Before');
                if (bb_ae_PositionHash[where]) {
                    if (range) {
                        range[setStart](el);
                        frag = range.createContextualFragment(html);
                    } else {
                        frag = this.createContextualFragment(html);
                    }
                    el.parentNode.insertBefore(frag, where === beforebegin ? el : el.nextSibling);
                    return el[(where === beforebegin ? 'previous' : 'next') + 'Sibling'];
                } else {
                    rangeEl = (where === afterbegin ? 'first' : 'last') + 'Child';
                    if (el.firstChild) {
                        if (range) {
                            // Creating ranges on a hidden element throws an error, checking for
                            // visibility is expensive, so we'll catch the error and fall back to
                            // using the full fragment
                            try {
                                range[setStart](el[rangeEl]);
                                frag = range.createContextualFragment(html);
                            } catch (e) {
                                frag = this.createContextualFragment(html);
                            }
                        } else {
                            frag = this.createContextualFragment(html);
                        }
                        if (where === afterbegin) {
                            el.insertBefore(frag, el.firstChild);
                        } else {
                            el.appendChild(frag);
                        }
                    } else {
                        el.innerHTML = html;
                    }
                    return el[rangeEl];
                }
            }
            Ext.raise({
                sourceClass: 'Ext.DomHelper',
                sourceMethod: 'insertHtml',
                htmlToInsert: html,
                targetElement: el,
                msg: 'Illegal insertion point reached: "' + where + '"'
            });
        },
        /**
         * Creates new DOM element(s) and inserts them before el.
         * @param {String/HTMLElement/Ext.dom.Element} el The context element
         * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
         * @param {Boolean} [returnElement] true to return a Ext.Element
         * @return {HTMLElement/Ext.dom.Element} The new node
         */
        insertBefore: function(el, o, returnElement) {
            return this.doInsert(el, o, returnElement, beforebegin);
        },
        /**
         * Creates new DOM element(s) and inserts them after el.
         * @param {String/HTMLElement/Ext.dom.Element} el The context element
         * @param {Object} o The DOM object spec (and children)
         * @param {Boolean} [returnElement] true to return a Ext.Element
         * @return {HTMLElement/Ext.dom.Element} The new node
         */
        insertAfter: function(el, o, returnElement) {
            return this.doInsert(el, o, returnElement, afterend);
        },
        /**
         * Creates new DOM element(s) and inserts them as the first child of el.
         * @param {String/HTMLElement/Ext.dom.Element} el The context element
         * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
         * @param {Boolean} [returnElement] true to return a Ext.Element
         * @return {HTMLElement/Ext.dom.Element} The new node
         */
        insertFirst: function(el, o, returnElement) {
            return this.doInsert(el, o, returnElement, afterbegin);
        },
        /**
         * Creates new DOM element(s) and appends them to el.
         * @param {String/HTMLElement/Ext.dom.Element} el The context element
         * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
         * @param {Boolean} [returnElement] true to return a Ext.Element
         * @return {HTMLElement/Ext.dom.Element} The new node
         */
        append: function(el, o, returnElement) {
            return this.doInsert(el, o, returnElement, beforeend);
        },
        /**
         * Creates new DOM element(s) and overwrites the contents of el with them.
         * @param {String/HTMLElement/Ext.dom.Element} el The context element
         * @param {Object/String} html The DOM object spec (and children) or raw HTML blob
         * @param {Boolean} [returnElement=false] true to return an Ext.Element
         * @return {HTMLElement/Ext.dom.Element} The new node
         */
        overwrite: function(el, html, returnElement) {
            var me = this,
                newNode;
            el = Ext.getDom(el);
            html = me.markup(html);
            if (me.ieOverwrite) {
                // hook for IE table hack - impl in ext package override
                newNode = me.ieOverwrite(el, html);
            }
            if (!newNode) {
                el.innerHTML = html;
                newNode = el.firstChild;
            }
            return returnElement ? Ext.get(newNode) : newNode;
        },
        doInsert: function(el, o, returnElement, where) {
            var me = this,
                newNode;
            el = el.dom || Ext.getDom(el);
            if ('innerHTML' in el) {
                // regular dom node
                // For standard HTMLElements, we insert as innerHTML instead of
                // createElement/appenChild because it is much faster in all versions of
                // IE: https://fiddle.sencha.com/#fiddle/tj
                newNode = me.insertHtml(where, el, me.markup(o));
            } else {
                // document fragment does not support innerHTML
                newNode = me.createDom(o, null);
                // we cannot insert anything inside a textnode so...
                if (el.nodeType === 3) {
                    where = where === afterbegin ? beforebegin : where;
                    where = where === beforeend ? afterend : where;
                }
                if (bb_ae_PositionHash[where]) {
                    el.parentNode.insertBefore(newNode, where === beforebegin ? el : el.nextSibling);
                }
                // eslint-disable-line max-len
                else if (el.firstChild && where === afterbegin) {
                    el.insertBefore(newNode, el.firstChild);
                } else {
                    el.appendChild(newNode);
                }
            }
            return returnElement ? Ext.get(newNode) : newNode;
        },
        /**
         * Creates a new Ext.Template from the DOM object spec.
         * @param {Object} o The DOM object spec (and children)
         * @return {Ext.Template} The new template
         */
        createTemplate: function(o) {
            var html = this.markup(o);
            return new Ext.Template(html);
        },
        /**
         * @method createHtml
         * Alias for {@link #markup}.
         * @deprecated 5.0.0 Please use {@link #markup} instead.
         */
        createHtml: function(spec) {
            return this.markup(spec);
        }
    };
});

/**
 * @class Ext.dom.Helper
 */
Ext.define('Ext.overrides.dom.Helper', (function() {
    var tableRe = /^(?:table|thead|tbody|tr|td)$/i,
        tableElRe = /td|tr|tbody|thead/i,
        ts = '<table>',
        te = '</table>',
        tbs = ts + '<tbody>',
        tbe = '</tbody>' + te,
        trs = tbs + '<tr>',
        tre = '</tr>' + tbe;
    return {
        override: 'Ext.dom.Helper',
        ieInsertHtml: function(where, el, html) {
            var frag = null;
            // IE's incomplete table implementation: http://www.ericvasilik.com/2006/07/code-karma.html
            if (Ext.isIE9m && tableRe.test(el.tagName)) {
                frag = this.insertIntoTable(el.tagName.toLowerCase(), where, el, html);
            }
            return frag;
        },
        ieOverwrite: function(el, html) {
            // IE Inserting HTML into a table/tbody/tr requires extra processing:
            // http://www.ericvasilik.com/2006/07/code-karma.html
            if (Ext.isIE9m && tableRe.test(el.tagName)) {
                // Clearing table elements requires removal of all elements.
                while (el.firstChild) {
                    el.removeChild(el.firstChild);
                }
                if (html) {
                    return this.insertHtml('afterbegin', el, html);
                }
            }
        },
        ieTable: function(depth, openingTags, htmlContent, closingTags) {
            var i = -1,
                el = this.detachedDiv,
                ns, nx;
            el.innerHTML = [
                openingTags,
                htmlContent,
                closingTags
            ].join('');
            while (++i < depth) {
                el = el.firstChild;
            }
            // If the result is multiple siblings, then encapsulate them into one fragment.
            ns = el.nextSibling;
            if (ns) {
                ns = el;
                el = document.createDocumentFragment();
                while (ns) {
                    nx = ns.nextSibling;
                    el.appendChild(ns);
                    ns = nx;
                }
            }
            return el;
        },
        /**
         * @private
         * @method insertIntoTable
         * @member Ext.dom.Helper
         * workaround for broken table implementation in IE9m
         * http://www.ericvasilik.com/2006/07/code-karma.html
         */
        insertIntoTable: function(tag, where, destinationEl, html) {
            var node, before,
                bb = where === 'beforebegin',
                ab = where === 'afterbegin',
                be = where === 'beforeend',
                ae = where === 'afterend';
            if (tag === 'td' && (ab || be) || !tableElRe.test(tag) && (bb || ae)) {
                return null;
            }
            /* eslint-disable indent, multiline-ternary, no-multi-spaces */
            before = bb ? destinationEl : ae ? destinationEl.nextSibling : ab ? destinationEl.firstChild : null;
            /* eslint-enable indent, multiline-ternary, no-multi-spaces */
            if (bb || ae) {
                destinationEl = destinationEl.parentNode;
            }
            if (tag === 'td' || (tag === 'tr' && (be || ab))) {
                node = this.ieTable(4, trs, html, tre);
            } else if (((tag === 'tbody' || tag === 'thead') && (be || ab)) || (tag === 'tr' && (bb || ae))) {
                node = this.ieTable(3, tbs, html, tbe);
            } else {
                node = this.ieTable(2, ts, html, te);
            }
            destinationEl.insertBefore(node, before);
            return node;
        }
    };
})());

/**
 * @class Ext.dom.Query
 * @alternateClassName Ext.DomQuery
 * @alternateClassName Ext.core.DomQuery
 * @singleton
 *
 * Provides high performance selector/xpath processing by compiling queries 
 * into reusable functions. New pseudo classes and matchers can be plugged. It works 
 * on HTML and XML documents (if a content node is passed in).
 *
 * DomQuery supports most of the [CSS3 selectors spec][1], along with some custom selectors 
 * and basic XPath.
 *
 * All selectors, attribute filters and pseudos below can be combined infinitely in any order. 
 * For example `div.foo:nth-child(odd)[@foo=bar].bar:first` would be a perfectly valid selector. 
 * Node filters are processed in the order in which they appear, which allows you to optimize 
 * your queries for your document structure.
 * 
 * ## Simple Selectors
 * 
 * For performance reasons, some query methods accept selectors that are termed as 
 * **simple selectors**. A simple selector is a selector that does not include contextual
 * information about any parent/sibling elements.
 * 
 * Some examples of valid simple selectors:
 * 
 *     var simple = '.foo'; // Only asking for the class name on the element
 *     var simple = 'div.bar'; // Only asking for the tag/class name on the element
 *     var simple = '[href];' // Asking for an attribute on the element.
 *     var simple = ':not(.foo)'; // Only asking for the non-matches against the class name
 *     var simple = 'span:first-child'; // Doesn't require any contextual information about 
 *                                      // the parent node
 * 
 * Simple examples of invalid simple selectors:
 * 
 *     var notSimple = 'div.foo div.bar'; // Requires matching a parent node by class name
 *     var notSimple = 'span + div'; //  Requires matching a sibling by tag name
 *
 * ## Element Selectors:
 *
 *   - **`*`** any element
 *   - **`E`** an element with the tag E
 *   - **`E F`** All descendent elements of E that have the tag F
 *   - **`E > F`** or **E/F** all direct children elements of E that have the tag F
 *   - **`E + F`** all elements with the tag F that are immediately preceded by an element 
 *                 with the tag E
 *   - **`E ~ F`** all elements with the tag F that are preceded by a sibling element with the tag E
 *
 * ## Attribute Selectors:
 *
 * The use of `@` and quotes are optional. For example, `div[@foo='bar']` is also a valid attribute 
 * selector.
 *
 *   - **`E[foo]`** has an attribute "foo"
 *   - **`E[foo=bar]`** has an attribute "foo" that equals "bar"
 *   - **`E[foo^=bar]`** has an attribute "foo" that starts with "bar"
 *   - **`E[foo$=bar]`** has an attribute "foo" that ends with "bar"
 *   - **`E[foo*=bar]`** has an attribute "foo" that contains the substring "bar"
 *   - **`E[foo%=2]`** has an attribute "foo" that is evenly divisible by 2
 *   - **`E[foo!=bar]`** attribute "foo" does not equal "bar"
 *
 * ## Pseudo Classes:
 *
 *   - **`E:first-child`** E is the first child of its parent
 *   - **`E:last-child`** E is the last child of its parent
 *   - **`E:nth-child(_n_)`** E is the _n_th child of its parent (1 based as per the spec)
 *   - **`E:nth-child(odd)`** E is an odd child of its parent
 *   - **`E:nth-child(even)`** E is an even child of its parent
 *   - **`E:only-child`** E is the only child of its parent
 *   - **`E:checked`** E is an element that is has a checked attribute that is true 
 *                     (e.g. a radio or checkbox)
 *   - **`E:first`** the first E in the resultset
 *   - **`E:last`** the last E in the resultset
 *   - **`E:nth(_n_)`** the _n_th E in the resultset (1 based)
 *   - **`E:odd`** shortcut for :nth-child(odd)
 *   - **`E:even`** shortcut for :nth-child(even)
 *   - **`E:contains(foo)`** E's innerHTML contains the substring "foo"
 *   - **`E:nodeValue(foo)`** E contains a textNode with a nodeValue that equals "foo"
 *   - **`E:not(S)`** an E element that does not match simple selector S
 *   - **`E:has(S)`** an E element that has a descendent that matches simple selector S
 *   - **`E:next(S)`** an E element whose next sibling matches simple selector S
 *   - **`E:prev(S)`** an E element whose previous sibling matches simple selector S
 *   - **`E:any(S1|S2|S2)`** an E element which matches any of the simple selectors S1, S2 or S3
 *   - **`E:visible(true)`** an E element which is deeply visible according to 
 *                           {@link Ext.dom.Element#isVisible}
 *
 * ## CSS Value Selectors:
 *
 *   - **`E{display=none}`** css value "display" that equals "none"
 *   - **`E{display^=none}`** css value "display" that starts with "none"
 *   - **`E{display$=none}`** css value "display" that ends with "none"
 *   - **`E{display*=none}`** css value "display" that contains the substring "none"
 *   - **`E{display%=2}`** css value "display" that is evenly divisible by 2
 *   - **`E{display!=none}`** css value "display" that does not equal "none"
 * 
 * ## XML Namespaces:
 *   - **`ns|E`** an element with tag E and namespace prefix ns
 *
 * [1]: http://www.w3.org/TR/2005/WD-css3-selectors-20051215/#selectors
 *
 */
Ext.define('Ext.dom.Query', function() {
    /* eslint-disable eqeqeq, no-undef, no-unused-vars, no-cond-assign, no-useless-escape */
    /* eslint-disable vars-on-top */
    var DQ,
        doc = document,
        cache, simpleCache, valueCache,
        useClassList = !!doc.documentElement.classList,
        useElementPointer = !!doc.documentElement.firstElementChild,
        useChildrenCollection = (function() {
            var d = doc.createElement('div');
            d.innerHTML = '<!-- -->text<!-- -->';
            return d.children && (d.children.length === 0);
        })(),
        nonSpace = /\S/,
        trimRe = /^\s+|\s+$/g,
        tplRe = /\{(\d+)\}/g,
        modeRe = /^(\s?[\/>+~]\s?|\s|$)/,
        tagTokenRe = /^(#)?([\w\-\*\|\\]+)/,
        nthRe = /(\d*)n\+?(\d*)/,
        nthRe2 = /\D/,
        startIdRe = /^\s*#/,
        // This is for IE MSXML which does not support expandos.
        // IE runs the same speed using setAttribute, however FF slows way down
        // and Safari completely fails so they need to continue to use expandos.
        isIE = window.ActiveXObject ? true : false,
        key = 30803,
        longHex = /\\([0-9a-fA-F]{6})/g,
        shortHex = /\\([0-9a-fA-F]{1,6})\s{0,1}/g,
        nonHex = /\\([^0-9a-fA-F]{1})/g,
        escapes = /\\/g,
        num, hasEscapes,
        // True if the browser supports the following syntax:
        // document.getElementsByTagName('namespacePrefix:tagName')
        supportsColonNsSeparator = (function() {
            var xmlDoc,
                xmlString = '<r><a:b xmlns:a="n"></a:b></r>';
            if (window.DOMParser) {
                xmlDoc = (new DOMParser()).parseFromString(xmlString, "application/xml");
            } else {
                xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
                xmlDoc.loadXML(xmlString);
            }
            return !!xmlDoc.getElementsByTagName('a:b').length;
        })(),
        // replaces a long hex regex match group with the appropriate ascii value
        // $args indicate regex match pos
        longHexToChar = function($0, $1) {
            return String.fromCharCode(parseInt($1, 16));
        },
        // converts a shortHex regex match to the long form
        shortToLongHex = function($0, $1) {
            while ($1.length < 6) {
                $1 = '0' + $1;
            }
            return '\\' + $1;
        },
        // converts a single char escape to long escape form
        charToLongHex = function($0, $1) {
            num = $1.charCodeAt(0).toString(16);
            if (num.length === 1) {
                num = '0' + num;
            }
            return '\\0000' + num;
        },
        // Un-escapes an input selector string.  Assumes all escape sequences have been
        // normalized to the css '\\0000##' 6-hex-digit style escape sequence :
        // will not handle any other escape formats
        unescapeCssSelector = function(selector) {
            return (hasEscapes) ? selector.replace(longHex, longHexToChar) : selector;
        },
        // checks if the path has escaping & does any appropriate replacements
        setupEscapes = function(path) {
            hasEscapes = (path.indexOf('\\') > -1);
            if (hasEscapes) {
                path = path.replace(shortHex, shortToLongHex).replace(nonHex, charToLongHex).replace(escapes, '\\\\');
            }
            // double the '\' for js compilation
            return path;
        };
    // this eval is stop the compressor from
    // renaming the variable to something shorter
    eval("var batch = 30803, child, next, prev, byClassName;");
    // Retrieve the child node from a particular
    // parent at the specified index.
    child = useChildrenCollection ? function child(parent, index) {
        return parent.children[index];
    } : function child(parent, index) {
        var i = 0,
            n = parent.firstChild;
        while (n) {
            if (n.nodeType == 1) {
                if (++i == index) {
                    return n;
                }
            }
            n = n.nextSibling;
        }
        return null;
    };
    // retrieve the next element node
    next = useElementPointer ? function(n) {
        return n.nextElementSibling;
    } : function(n) {
        while ((n = n.nextSibling) && n.nodeType != 1) {}
        // This block is intentionally left blank
        return n;
    };
    // retrieve the previous element node
    prev = useElementPointer ? function(n) {
        return n.previousElementSibling;
    } : function(n) {
        while ((n = n.previousSibling) && n.nodeType != 1) {}
        // This block is intentionally left blank
        return n;
    };
    // Mark each child node with a nodeIndex skipping and
    // removing empty text nodes.
    function children(parent) {
        var n = parent.firstChild,
            nodeIndex = -1,
            nextNode;
        while (n) {
            nextNode = n.nextSibling;
            // clean worthless empty nodes.
            if (n.nodeType == 3 && !nonSpace.test(n.nodeValue)) {
                parent.removeChild(n);
            } else {
                // add an expando nodeIndex
                n.nodeIndex = ++nodeIndex;
            }
            n = nextNode;
        }
        return this;
    }
    // nodeSet - array of nodes
    // cls - CSS Class
    // Use classList API where available: http://jsperf.com/classlist-vs-old-school-check/
    byClassName = useClassList ? function(nodeSet, cls) {
        cls = unescapeCssSelector(cls);
        if (!cls) {
            return nodeSet;
        }
        var result = [],
            ri = -1,
            i, ci, classList;
        for (i = 0; ci = nodeSet[i]; i++) {
            classList = ci.classList;
            if (classList) {
                if (classList.contains(cls)) {
                    result[++ri] = ci;
                }
            } else if ((' ' + ci.className + ' ').indexOf(cls) !== -1) {
                // Some elements types (SVG) may not always have a classList
                // in some browsers, so fallback to the old style here
                result[++ri] = ci;
            }
        }
        return result;
    } : function(nodeSet, cls) {
        cls = unescapeCssSelector(cls);
        if (!cls) {
            return nodeSet;
        }
        var result = [],
            ri = -1,
            i, ci;
        for (i = 0; ci = nodeSet[i]; i++) {
            if ((' ' + ci.className + ' ').indexOf(cls) !== -1) {
                result[++ri] = ci;
            }
        }
        return result;
    };
    function attrValue(n, attr) {
        // if its an array, use the first node.
        if (!n.tagName && typeof n.length != "undefined") {
            n = n[0];
        }
        if (!n) {
            return null;
        }
        if (attr == "for") {
            return n.htmlFor;
        }
        if (attr == "class" || attr == "className") {
            return n.className;
        }
        return n.getAttribute(attr) || n[attr];
    }
    // ns - nodes
    // mode - false, /, >, +, ~
    // tagName - defaults to "*"
    function getNodes(ns, mode, tagName) {
        var result = [],
            ri = -1,
            cs, i, ni, j, ci, cn, utag, n, cj;
        if (!ns) {
            return result;
        }
        tagName = tagName.replace('|', ':') || "*";
        // convert to array
        if (typeof ns.getElementsByTagName != "undefined") {
            ns = [
                ns
            ];
        }
        // no mode specified, grab all elements by tagName
        // at any depth
        if (!mode) {
            tagName = unescapeCssSelector(tagName);
            if (!supportsColonNsSeparator && DQ.isXml(ns[0]) && tagName.indexOf(':') !== -1) {
                // Some browsers (e.g. WebKit and Opera do not support the following syntax
                // in xml documents: getElementsByTagName('ns:tagName'). To work around
                // this, we remove the namespace prefix from the tagName, get the elements
                // by tag name only, and then compare each element's tagName property to
                // the tagName with namespace prefix attached to ensure that the tag is in
                // the proper namespace.
                for (i = 0; ni = ns[i]; i++) {
                    cs = ni.getElementsByTagName(tagName.split(':').pop());
                    for (j = 0; ci = cs[j]; j++) {
                        if (ci.tagName === tagName) {
                            result[++ri] = ci;
                        }
                    }
                }
            } else {
                for (i = 0; ni = ns[i]; i++) {
                    cs = ni.getElementsByTagName(tagName);
                    for (j = 0; ci = cs[j]; j++) {
                        result[++ri] = ci;
                    }
                }
            }
        }
        // Direct Child mode (/ or >)
        // E > F or E/F all direct children elements of E that have the tag
        else if (mode == "/" || mode == ">") {
            utag = tagName.toUpperCase();
            for (i = 0; ni = ns[i]; i++) {
                cn = ni.childNodes;
                for (j = 0; cj = cn[j]; j++) {
                    if (cj.nodeName == utag || cj.nodeName == tagName || tagName == '*') {
                        result[++ri] = cj;
                    }
                }
            }
        }
        // Immediately Preceding mode (+)
        // E + F all elements with the tag F that are immediately preceded by an element
        // with the tag E
        else if (mode == "+") {
            utag = tagName.toUpperCase();
            for (i = 0; n = ns[i]; i++) {
                while ((n = n.nextSibling) && n.nodeType != 1) {}
                // This block is intentionally left blank
                if (n && (n.nodeName == utag || n.nodeName == tagName || tagName == '*')) {
                    result[++ri] = n;
                }
            }
        }
        // Sibling mode (~)
        // E ~ F all elements with the tag F that are preceded by a sibling element
        // with the tag E
        else if (mode == "~") {
            utag = tagName.toUpperCase();
            for (i = 0; n = ns[i]; i++) {
                while ((n = n.nextSibling)) {
                    if (n.nodeName == utag || n.nodeName == tagName || tagName == '*') {
                        result[++ri] = n;
                    }
                }
            }
        }
        return result;
    }
    function concat(a, b) {
        a.push.apply(a, b);
        return a;
    }
    function byTag(cs, tagName) {
        if (cs.tagName || cs === doc) {
            cs = [
                cs
            ];
        }
        if (!tagName) {
            return cs;
        }
        var result = [],
            ri = -1,
            i, ci;
        tagName = tagName.toLowerCase();
        for (i = 0; ci = cs[i]; i++) {
            if (ci.nodeType == 1 && ci.tagName.toLowerCase() == tagName) {
                result[++ri] = ci;
            }
        }
        return result;
    }
    function byId(cs, id) {
        id = unescapeCssSelector(id);
        if (cs.tagName || cs === doc) {
            cs = [
                cs
            ];
        }
        if (!id) {
            return cs;
        }
        var result = [],
            ri = -1,
            i, ci;
        for (i = 0; ci = cs[i]; i++) {
            if (ci && ci.id == id) {
                result[++ri] = ci;
                return result;
            }
        }
        return result;
    }
    // operators are =, !=, ^=, $=, *=, %=, |= and ~=
    // custom can be "{"
    function byAttribute(cs, attr, value, op, custom) {
        var result = [],
            ri = -1,
            useGetStyle = custom == "{",
            fn = DQ.operators[op],
            a, xml, hasXml, i, ci;
        value = unescapeCssSelector(value);
        for (i = 0; ci = cs[i]; i++) {
            // skip non-element nodes.
            if (ci.nodeType === 1) {
                // only need to do this for the first node
                if (!hasXml) {
                    xml = DQ.isXml(ci);
                    hasXml = true;
                }
                // we only need to change the property names if we're dealing with html nodes,
                // not XML
                if (!xml) {
                    if (useGetStyle) {
                        a = DQ.getStyle(ci, attr);
                    } else if (attr == "class" || attr == "className") {
                        a = ci.className;
                    } else if (attr == "for") {
                        a = ci.htmlFor;
                    } else if (attr == "href") {
                        // getAttribute href bug
                        // http://www.glennjones.net/Post/809/getAttributehrefbug.htm
                        a = ci.getAttribute("href", 2);
                    } else {
                        a = ci.getAttribute(attr);
                    }
                } else {
                    a = ci.getAttribute(attr);
                }
                if ((fn && fn(a, value)) || (!fn && a)) {
                    result[++ri] = ci;
                }
            }
        }
        return result;
    }
    function byPseudo(cs, name, value) {
        value = unescapeCssSelector(value);
        return DQ.pseudos[name](cs, value);
    }
    function nodupIEXml(cs) {
        var d = ++key,
            r, i, len, c;
        cs[0].setAttribute("_nodup", d);
        r = [
            cs[0]
        ];
        for (i = 1 , len = cs.length; i < len; i++) {
            c = cs[i];
            if (!c.getAttribute("_nodup") != d) {
                c.setAttribute("_nodup", d);
                r[r.length] = c;
            }
        }
        for (i = 0 , len = cs.length; i < len; i++) {
            cs[i].removeAttribute("_nodup");
        }
        return r;
    }
    function nodup(cs) {
        if (!cs) {
            return [];
        }
        var len = cs.length,
            r = cs,
            ri = -1,
            c, cj, i, d, j;
        if (!len || typeof cs.nodeType != "undefined" || len == 1) {
            return cs;
        }
        if (isIE && typeof cs[0].selectSingleNode != "undefined") {
            return nodupIEXml(cs);
        }
        d = ++key;
        cs[0]._nodup = d;
        for (i = 1; c = cs[i]; i++) {
            if (c._nodup != d) {
                c._nodup = d;
            } else {
                r = [];
                for (j = 0; j < i; j++) {
                    r[++ri] = cs[j];
                }
                for (j = i + 1; cj = cs[j]; j++) {
                    if (cj._nodup != d) {
                        cj._nodup = d;
                        r[++ri] = cj;
                    }
                }
                return r;
            }
        }
        return r;
    }
    function quickDiffIEXml(c1, c2) {
        var d = ++key,
            r = [],
            i, len;
        for (i = 0 , len = c1.length; i < len; i++) {
            c1[i].setAttribute("_qdiff", d);
        }
        for (i = 0 , len = c2.length; i < len; i++) {
            if (c2[i].getAttribute("_qdiff") != d) {
                r[r.length] = c2[i];
            }
        }
        for (i = 0 , len = c1.length; i < len; i++) {
            c1[i].removeAttribute("_qdiff");
        }
        return r;
    }
    function quickDiff(c1, c2) {
        var len1 = c1.length,
            d = ++key,
            r = [],
            i, len;
        if (!len1) {
            return c2;
        }
        if (isIE && typeof c1[0].selectSingleNode != "undefined") {
            return quickDiffIEXml(c1, c2);
        }
        for (i = 0; i < len1; i++) {
            c1[i]._qdiff = d;
        }
        for (i = 0 , len = c2.length; i < len; i++) {
            if (c2[i]._qdiff != d) {
                r[r.length] = c2[i];
            }
        }
        return r;
    }
    function quickId(ns, mode, root, id) {
        var d;
        if (ns == root) {
            id = unescapeCssSelector(id);
            d = root.ownerDocument || root;
            return d.getElementById(id);
        }
        ns = getNodes(ns, mode, "*");
        return byId(ns, id);
    }
    return {
        singleton: true,
        alternateClassName: [
            'Ext.core.DomQuery',
            'Ext.DomQuery'
        ],
        requires: [
            'Ext.dom.Helper',
            'Ext.util.Operators'
        ],
        _init: function() {
            DQ = this;
            DQ.operators = Ext.Object.chain(Ext.util.Operators);
            // can capture now
            DQ._cache = cache = new Ext.util.LruCache({
                maxSize: 200
            });
            DQ._valueCache = valueCache = new Ext.util.LruCache({
                maxSize: 200
            });
            DQ._simpleCache = simpleCache = new Ext.util.LruCache({
                maxSize: 200
            });
        },
        clearCache: function() {
            cache.clear();
            valueCache.clear();
            simpleCache.clear();
        },
        getStyle: function(el, name) {
            return Ext.fly(el, '_DomQuery').getStyle(name);
        },
        /**
         * Compiles a selector/xpath query into a reusable function. The returned function
         * takes one parameter "root" (optional), which is the context node from where the query
         * should start.
         * @param {String} path The selector/xpath query
         * @param {String} [type="select"] Either "select" or "simple" for a simple selector match
         * @return {Function}
         */
        compile: function(path, type) {
            type = type || "select";
            // setup fn preamble
            var fn = [
                    "var f = function(root) {\n var mode; ++batch; var n = root || document;\n"
                ],
                lastPath,
                matchers = DQ.matchers,
                matchersLn = matchers.length,
                // accept leading mode switch
                lmode = path.match(modeRe),
                modeMatch, tokenMatch, matched, j, t, m;
            path = setupEscapes(path);
            if (lmode && lmode[1]) {
                fn[fn.length] = 'mode="' + lmode[1].replace(trimRe, "") + '";';
                path = path.replace(lmode[1], "");
            }
            // strip leading slashes
            while (path.substr(0, 1) == "/") {
                path = path.substr(1);
            }
            while (path && lastPath != path) {
                lastPath = path;
                tokenMatch = path.match(tagTokenRe);
                if (type == "select") {
                    if (tokenMatch) {
                        // ID Selector
                        if (tokenMatch[1] == "#") {
                            fn[fn.length] = 'n = quickId(n, mode, root, "' + tokenMatch[2] + '");';
                        } else {
                            fn[fn.length] = 'n = getNodes(n, mode, "' + tokenMatch[2] + '");';
                        }
                        path = path.replace(tokenMatch[0], "");
                    } else if (path.substr(0, 1) != '@') {
                        fn[fn.length] = 'n = getNodes(n, mode, "*");';
                    }
                } else // type of "simple"
                {
                    if (tokenMatch) {
                        if (tokenMatch[1] == "#") {
                            fn[fn.length] = 'n = byId(n, "' + tokenMatch[2] + '");';
                        } else {
                            fn[fn.length] = 'n = byTag(n, "' + tokenMatch[2] + '");';
                        }
                        path = path.replace(tokenMatch[0], "");
                    }
                }
                while (!(modeMatch = path.match(modeRe))) {
                    matched = false;
                    for (j = 0; j < matchersLn; j++) {
                        t = matchers[j];
                        m = path.match(t.re);
                        if (m) {
                            fn[fn.length] = t.select.replace(tplRe, function(x, i) {
                                return m[i];
                            });
                            path = path.replace(m[0], "");
                            matched = true;
                            break;
                        }
                    }
                    // prevent infinite loop on bad selector
                    if (!matched) {
                        Ext.raise({
                            sourceClass: 'Ext.DomQuery',
                            sourceMethod: 'compile',
                            msg: 'Error parsing selector. Parsing failed at "' + path + '"'
                        });
                    }
                }
                if (modeMatch[1]) {
                    fn[fn.length] = 'mode="' + modeMatch[1].replace(trimRe, "") + '";';
                    path = path.replace(modeMatch[1], "");
                }
            }
            // close fn out
            fn[fn.length] = "return nodup(n);\n}";
            // eval fn and return it
            eval(fn.join(""));
            return f;
        },
        /**
         * Selects an array of DOM nodes using JavaScript-only implementation.
         *
         * Use {@link #select} to take advantage of browsers built-in support for CSS selectors.
         * @param {String} path The selector/xpath query (can be a comma separated list
         * of selectors)
         * @param {HTMLElement/String} [root=document] The start of the query.
         * @param type
         * @return {HTMLElement[]} An Array of DOM elements which match the selector. If there are
         * no matches, and empty Array is returned.
         */
        jsSelect: function(path, root, type) {
            // set root to doc if not specified.
            root = root || doc;
            if (typeof root == "string") {
                root = doc.getElementById(root);
            }
            var paths = Ext.splitAndUnescape(path, ","),
                results = [],
                query, i, len, subPath, result;
            // loop over each selector
            for (i = 0 , len = paths.length; i < len; i++) {
                subPath = paths[i].replace(trimRe, "");
                // compile and place in cache
                query = cache.get(subPath);
                if (!query) {
                    // When we compile, escaping is handled inside the compile method
                    query = DQ.compile(subPath, type);
                    if (!query) {
                        Ext.raise({
                            sourceClass: 'Ext.DomQuery',
                            sourceMethod: 'jsSelect',
                            msg: subPath + ' is not a valid selector'
                        });
                    }
                    cache.add(subPath, query);
                } else {
                    // If we've already compiled, we still need to check if the
                    // selector has escaping and setup the appropriate flags
                    setupEscapes(subPath);
                }
                result = query(root);
                if (result && result !== doc) {
                    results = results.concat(result);
                }
            }
            // if there were multiple selectors, make sure dups
            // are eliminated
            if (paths.length > 1) {
                return nodup(results);
            }
            return results;
        },
        isXml: function(el) {
            var docEl = (el ? el.ownerDocument || el : 0).documentElement;
            return docEl ? docEl.nodeName !== "HTML" : false;
        },
        /**
         * Selects an array of DOM nodes by CSS/XPath selector.
         *
         * Uses [document.querySelectorAll][0] if browser supports that, otherwise falls back to
         * {@link Ext.dom.Query#jsSelect} to do the work.
         *
         * [0]: https://developer.mozilla.org/en/DOM/document.querySelectorAll
         *
         * @param {String} path The selector/xpath query
         * @param {HTMLElement} [root=document] The start of the query.
         * @return {HTMLElement[]} An array of DOM elements (not a NodeList as returned by
         * `querySelectorAll`).
         * @param {String} [type="select"] Either "select" or "simple" for a simple selector match
         * (only valid when used when the call is deferred to the jsSelect method)
         * @param {Boolean} [single] Pass `true` to select only the first matching node using
         * `document.querySelector` (where available)
         * @method
         */
        select: doc.querySelectorAll ? function(path, root, type, single) {
            root = root || doc;
            if (!DQ.isXml(root)) {
                try {
                    /*
                         * This checking here is to "fix" the behaviour of querySelectorAll
                         * for non root document queries. The way qsa works is intentional,
                         * however it's definitely not the expected way it should work.
                         * When descendant selectors are used, only the lowest selector must be
                         * inside the root!
                         * More info: http://ejohn.org/blog/thoughts-on-queryselectorall/
                         * So we create a descendant selector by prepending the root's ID,
                         * and query the parent node.
                         * UNLESS the root has no parent in which qsa will work perfectly.
                         *
                         * We only modify the path for single selectors (ie, no multiples),
                         * without a full parser it makes it difficult to do this correctly.
                         */
                    if (root.parentNode && (root.nodeType !== 9) && path.indexOf(',') === -1 && !startIdRe.test(path)) {
                        // eslint-disable-line max-len
                        path = Ext.makeIdSelector(Ext.id(root)) + ' ' + path;
                        root = root.parentNode;
                    }
                    return single ? [
                        root.querySelector(path)
                    ] : Ext.Array.toArray(root.querySelectorAll(path));
                } catch (e) {}
            }
            // This block is intentionally left blank
            return DQ.jsSelect.call(this, path, root, type);
        } : function(path, root, type) {
            return DQ.jsSelect.call(this, path, root, type);
        },
        /**
         * Selects a single element.
         * @param {String} path The selector/xpath query
         * @param {HTMLElement} [root=document] The start of the query.
         * @return {HTMLElement} The DOM element which matched the selector.
         */
        selectNode: function(path, root) {
            return Ext.DomQuery.select(path, root, null, true)[0];
        },
        /**
         * Selects the value of a node, optionally replacing null with the defaultValue.
         * @param {String} path The selector/xpath query
         * @param {HTMLElement} [root=document] The start of the query.
         * @param {String} [defaultValue] When specified, this is return as empty value.
         * @return {String}
         */
        selectValue: function(path, root, defaultValue) {
            path = path.replace(trimRe, "");
            var query = valueCache.get(path),
                n;
            if (!query) {
                query = DQ.compile(path, "select");
                valueCache.add(path, query);
            } else {
                setupEscapes(path);
            }
            n = query(root);
            return DQ.getNodeValue(n[0] || n, defaultValue);
        },
        /**
         * Get the text value for a node, optionally replacing null with the defaultValue.
         * @param {Object} node The node
         * @param {String} [defaultValue] When specified, this is return as empty value.
         * @return {String} The value
         */
        getNodeValue: function(node, defaultValue) {
            // overcome a limitation of maximum textnode size
            // Rumored to potentially crash IE6 but has not been confirmed.
            // http://reference.sitepoint.com/javascript/Node/normalize
            // https://developer.mozilla.org/En/DOM/Node.normalize
            if (typeof node.normalize == 'function') {
                node.normalize();
            }
            var firstChild = node && node.firstChild,
                v = firstChild ? firstChild.nodeValue : null;
            // If we have a defaultValue, and v is null, undefined or '', use that
            // value instead.
            //
            if (defaultValue !== undefined && (v == null || v === '')) {
                v = defaultValue;
            }
            return v;
        },
        /**
         * Selects the value of a node, parsing integers and floats.
         * Returns the defaultValue, or 0 if none is specified.
         * @param {String} path The selector/xpath query
         * @param {HTMLElement} [root=document] The start of the query.
         * @param {Number} [defaultValue] When specified, this is return as empty value.
         * @return {Number}
         */
        selectNumber: function(path, root, defaultValue) {
            var v = DQ.selectValue(path, root, defaultValue || 0);
            return parseFloat(v);
        },
        /**
         * Returns true if the passed element(s) match the passed simple selector
         * @param {String/HTMLElement/HTMLElement[]} el An element id, element or array of elements
         * @param {String} ss The simple selector to test
         * @return {Boolean}
         */
        is: function(el, ss) {
            if (typeof el == "string") {
                el = doc.getElementById(el);
            }
            var isArray = Ext.isArray(el),
                result = DQ.filter(isArray ? el : [
                    el
                ], ss);
            return isArray ? (result.length == el.length) : (result.length > 0);
        },
        /**
         * Filters an array of elements to only include matches of a simple selector
         * @param {HTMLElement[]} els An array of elements to filter
         * @param {String} ss The simple selector to test
         * @param {Boolean} nonMatches If true, it returns the elements that DON'T match
         * the selector instead of the ones that match
         * @return {HTMLElement[]} An Array of DOM elements which match the selector. If there are
         * no matches, and empty Array is returned.
         */
        filter: function(els, ss, nonMatches) {
            ss = ss.replace(trimRe, "");
            var query = simpleCache.get(ss),
                result;
            if (!query) {
                query = DQ.compile(ss, "simple");
                simpleCache.add(ss, query);
            } else {
                setupEscapes(ss);
            }
            result = query(els);
            return nonMatches ? quickDiff(result, els) : result;
        },
        /**
         * Collection of matching regular expressions and code snippets.
         * Each capture group within `()` will be replace the `{}` in the select
         * statement as specified by their index.
         */
        matchers: [
            {
                re: /^\.([\w\-\\]+)/,
                select: useClassList ? 'n = byClassName(n, "{1}");' : 'n = byClassName(n, " {1} ");'
            },
            {
                re: /^\:([\w\-]+)(?:\(((?:[^\s>\/]*|.*?))\))?/,
                select: 'n = byPseudo(n, "{1}", "{2}");'
            },
            {
                re: /^(?:([\[\{])(?:@)?([\w\-]+)\s?(?:(=|.=)\s?['"]?(.*?)["']?)?[\]\}])/,
                select: 'n = byAttribute(n, "{2}", "{4}", "{3}", "{1}");'
            },
            {
                re: /^#([\w\-\\]+)/,
                select: 'n = byId(n, "{1}");'
            },
            {
                re: /^@([\w\-\.]+)/,
                select: 'return {firstChild:{nodeValue:attrValue(n, "{1}")}};'
            }
        ],
        /**
         * Collection of operator comparison functions.
         * The default operators are `=`, `!=`, `^=`, `$=`, `*=`, `%=`, `|=` and `~=`.
         *
         * New operators can be added as long as the match the format *c*`=` where *c*
         * is any character other than space, `>`, or `<`.
         *
         * Operator functions are passed the following parameters:
         *
         * * `propValue` : The property value to test.
         * * `compareTo` : The value to compare to.
         *
         * @property {Object} operators
         */
        /**
         * Object hash of "pseudo class" filter functions which are used when filtering selections.
         * Each function is passed two parameters:
         *
         * - **c** : Array
         *     An Array of DOM elements to filter.
         *
         * - **v** : String
         *     The argument (if any) supplied in the selector.
         *
         * A filter function returns an Array of DOM elements which conform to the pseudo class.
         * In addition to the provided pseudo classes listed above such as `first-child` and
         * `nth-child`, developers may add additional, custom psuedo class filters to select
         * elements according to application-specific requirements.
         *
         * For example, to filter `a` elements to only return links to __external__ resources:
         *
         *     Ext.DomQuery.pseudos.external = function(c, v) {
         *         var r = [], ri = -1;
         *         for(var i = 0, ci; ci = c[i]; i++) {
         *             // Include in result set only if it's a link to an external resource
         *             if (ci.hostname != location.hostname) {
         *                 r[++ri] = ci;
         *             }
         *         }
         *         return r;
         *     };
         *
         * Then external links could be gathered with the following statement:
         *
         *     var externalLinks = Ext.select("a:external");
         */
        pseudos: {
            "first-child": function(c) {
                var r = [],
                    ri = -1,
                    n, i, ci;
                for (i = 0; (ci = n = c[i]); i++) {
                    while ((n = n.previousSibling) && n.nodeType != 1) {}
                    // This block is intentionally left blank
                    if (!n) {
                        r[++ri] = ci;
                    }
                }
                return r;
            },
            "last-child": function(c) {
                var r = [],
                    ri = -1,
                    n, i, ci;
                for (i = 0; (ci = n = c[i]); i++) {
                    while ((n = n.nextSibling) && n.nodeType != 1) {}
                    // This block is intentionally left blank
                    if (!n) {
                        r[++ri] = ci;
                    }
                }
                return r;
            },
            "nth-child": function(c, a) {
                var r = [],
                    ri = -1,
                    m = nthRe.exec(a == "even" && "2n" || a == "odd" && "2n+1" || !nthRe2.test(a) && "n+" + a || a),
                    // eslint-disable-line max-len
                    f = (m[1] || 1) - 0,
                    l = m[2] - 0,
                    i, n, j, cn, pn;
                for (i = 0; n = c[i]; i++) {
                    pn = n.parentNode;
                    if (batch != pn._batch) {
                        j = 0;
                        for (cn = pn.firstChild; cn; cn = cn.nextSibling) {
                            if (cn.nodeType == 1) {
                                cn.nodeIndex = ++j;
                            }
                        }
                        pn._batch = batch;
                    }
                    if (f == 1) {
                        if (l === 0 || n.nodeIndex == l) {
                            r[++ri] = n;
                        }
                    } else if ((n.nodeIndex + l) % f === 0) {
                        r[++ri] = n;
                    }
                }
                return r;
            },
            "only-child": function(c) {
                var r = [],
                    ri = -1,
                    i, ci;
                for (i = 0; ci = c[i]; i++) {
                    if (!prev(ci) && !next(ci)) {
                        r[++ri] = ci;
                    }
                }
                return r;
            },
            "empty": function(c) {
                var r = [],
                    ri = -1,
                    i, ci, cns, j, cn, empty;
                for (i = 0; ci = c[i]; i++) {
                    cns = ci.childNodes;
                    j = 0;
                    empty = true;
                    while (cn = cns[j]) {
                        ++j;
                        if (cn.nodeType == 1 || cn.nodeType == 3) {
                            empty = false;
                            break;
                        }
                    }
                    if (empty) {
                        r[++ri] = ci;
                    }
                }
                return r;
            },
            "contains": function(c, v) {
                var r = [],
                    ri = -1,
                    i, ci;
                for (i = 0; ci = c[i]; i++) {
                    if ((ci.textContent || ci.innerText || ci.text || '').indexOf(v) != -1) {
                        r[++ri] = ci;
                    }
                }
                return r;
            },
            "nodeValue": function(c, v) {
                var r = [],
                    ri = -1,
                    i, ci;
                for (i = 0; ci = c[i]; i++) {
                    if (ci.firstChild && ci.firstChild.nodeValue == v) {
                        r[++ri] = ci;
                    }
                }
                return r;
            },
            "checked": function(c) {
                var r = [],
                    ri = -1,
                    i, ci;
                for (i = 0; ci = c[i]; i++) {
                    if (ci.checked === true) {
                        r[++ri] = ci;
                    }
                }
                return r;
            },
            "not": function(c, ss) {
                return DQ.filter(c, ss, true);
            },
            "any": function(c, selectors) {
                var ss = selectors.split('|'),
                    r = [],
                    ri = -1,
                    s, i, ci, j;
                for (i = 0; ci = c[i]; i++) {
                    for (j = 0; s = ss[j]; j++) {
                        if (DQ.is(ci, s)) {
                            r[++ri] = ci;
                            break;
                        }
                    }
                }
                return r;
            },
            "odd": function(c) {
                return this["nth-child"](c, "odd");
            },
            "even": function(c) {
                return this["nth-child"](c, "even");
            },
            "nth": function(c, a) {
                return c[a - 1] || [];
            },
            "first": function(c) {
                return c[0] || [];
            },
            "last": function(c) {
                return c[c.length - 1] || [];
            },
            "has": function(c, ss) {
                var s = DQ.select,
                    r = [],
                    ri = -1,
                    i, ci;
                for (i = 0; ci = c[i]; i++) {
                    if (s(ss, ci).length > 0) {
                        r[++ri] = ci;
                    }
                }
                return r;
            },
            "next": function(c, ss) {
                var is = DQ.is,
                    r = [],
                    ri = -1,
                    i, ci, n;
                for (i = 0; ci = c[i]; i++) {
                    n = next(ci);
                    if (n && is(n, ss)) {
                        r[++ri] = ci;
                    }
                }
                return r;
            },
            "prev": function(c, ss) {
                var is = DQ.is,
                    r = [],
                    ri = -1,
                    i, ci, n;
                for (i = 0; ci = c[i]; i++) {
                    n = prev(ci);
                    if (n && is(n, ss)) {
                        r[++ri] = ci;
                    }
                }
                return r;
            },
            focusable: function(candidates) {
                var len = candidates.length,
                    results = [],
                    i = 0,
                    c;
                for (; i < len; i++) {
                    c = candidates[i];
                    if (Ext.fly(c, '_DomQuery').isFocusable()) {
                        results.push(c);
                    }
                }
                return results;
            },
            visible: function(candidates, deep) {
                var len = candidates.length,
                    results = [],
                    i = 0,
                    c;
                for (; i < len; i++) {
                    c = candidates[i];
                    if (Ext.fly(c, '_DomQuery').isVisible(deep)) {
                        results.push(c);
                    }
                }
                return results;
            },
            isScrolled: function(c) {
                var r = [],
                    ri = -1,
                    i, ci, s;
                for (i = 0; ci = c[i]; i++) {
                    s = Ext.fly(ci, '_DomQuery').getScroll();
                    if (s.top > 0 || s.left > 0) {
                        r[++ri] = ci;
                    }
                }
                return r;
            }
        }
    };
}, function() {
    this._init();
});

/**
 * The XML Reader is used by a Proxy to read a server response that is sent back in XML format.
 * This usually happens as a result of loading a Store - for example we might create something
 * like this:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name', 'email']
 *     });
 *
 *     var store = Ext.create('Ext.data.Store', {
 *         model: 'User',
 *         proxy: {
 *             type: 'ajax',
 *             url: 'users.xml',
 *             reader: {
 *                 type: 'xml',
 *                 record: 'user',
 *                 rootProperty: 'users'
 *             }
 *         }
 *     });
 *
 * The example above creates a 'User' model. Models are explained in the
 * {@link Ext.data.Model Model} docs if you're not already familiar with them.
 *
 * We created the simplest type of XML Reader possible by simply telling our
 * {@link Ext.data.Store Store}'s {@link Ext.data.proxy.Proxy Proxy} that we want a XML Reader.
 * The Store automatically passes the configured model to the Store, so it is as if we passed
 * this instead:
 *
 *     reader: {
 *         type: 'xml',
 *         model: 'User',
 *         record: 'user',
 *         rootProperty: 'users'
 *     }
 *
 * The reader we set up is ready to read data from our server - at the moment it will accept
 * a response like this:
 *
 *     <?xml version="1.0" encoding="UTF-8"?>
 *     <users>
 *         <user>
 *             <id>1</id>
 *             <name>Ed Spencer</name>
 *             <email>ed@sencha.com</email>
 *         </user>
 *         <user>
 *             <id>2</id>
 *             <name>Abe Elias</name>
 *             <email>abe@sencha.com</email>
 *         </user>
 *     </users>
 *
 * First off there's {@link #rootProperty} option to define the root node `<users>` (there should be
 * only one in a well-formed XML document). Then the XML Reader uses the configured {@link #record}
 * option to pull out the data for each record - in this case we set record to 'user', so each
 * `<user>` above will be converted into a User model.
 *
 * Note that XmlReader doesn't care whether your {@link #rootProperty} and {@link #record} elements
 * are nested deep inside a larger structure, so a response like this will still work:
 *
 *     <?xml version="1.0" encoding="UTF-8"?>
 *     <deeply>
 *         <nested>
 *             <xml>
 *                 <users>
 *                     <user>
 *                         <id>1</id>
 *                         <name>Ed Spencer</name>
 *                         <email>ed@sencha.com</email>
 *                     </user>
 *                     <user>
 *                         <id>2</id>
 *                         <name>Abe Elias</name>
 *                         <email>abe@sencha.com</email>
 *                     </user>
 *                 </users>
 *             </xml>
 *         </nested>
 *     </deeply>
 *
 * If this Reader is being used by a {@link Ext.data.TreeStore TreeStore} to read tree-structured
 * data in which records are nested as descendant nodes of other records, then this lenient
 * behaviour must be overridden by using a more specific child node selector as your {@link #record}
 * selector which will not select all descendants, such as:
 *
 *     record: '>user'
 *
 * # Response metadata
 *
 * The server can return additional data in its response, such as the {@link #totalProperty total
 * number of records} and the {@link #successProperty success status of the response}. These are
 * typically included in the XML response like this:
 *
 *     <?xml version="1.0" encoding="UTF-8"?>
 *     <users>
 *         <total>100</total>
 *         <success>true</success>
 *         <user>
 *             <id>1</id>
 *             <name>Ed Spencer</name>
 *             <email>ed@sencha.com</email>
 *         </user>
 *         <user>
 *             <id>2</id>
 *             <name>Abe Elias</name>
 *             <email>abe@sencha.com</email>
 *         </user>
 *     </users>
 *
 * If these properties are present in the XML response they can be parsed out by the XmlReader
 * and used by the Store that loaded it. We can set up the names of these properties by specifying
 * a final pair of configuration options:
 *
 *     reader: {
 *         type: 'xml',
 *         rootProperty: 'users',
 *         totalProperty: 'total',
 *         successProperty: 'success'
 *     }
 *
 * These final options are not necessary to make the Reader work, but can be useful when the server
 * needs to report an error or if it needs to indicate that there is a lot of data available
 * of which only a subset is currently being returned.
 *
 * # Response format
 *
 * **Note:** in order for the browser to parse a returned XML document, the Content-Type header
 * in the HTTP response must be set to "text/xml" or "application/xml". This is very important -
 * the XmlReader will not work correctly otherwise.
 */
Ext.define('Ext.data.reader.Xml', {
    extend: 'Ext.data.reader.Reader',
    alternateClassName: 'Ext.data.XmlReader',
    alias: 'reader.xml',
    requires: [
        'Ext.dom.Query'
    ],
    config: {
        /**
        * @cfg {String} record (required)
        * The DomQuery path to the repeated element which contains record information.
        *
        * By default, the elements which match the selector may be nested at any level
        * below the {@link #rootProperty}
        *
        * If this Reader is being used by a {@link Ext.data.TreeStore TreeStore} to read
        * tree-structured data, then only first generation child nodes of the root element must be
        * selected, so the record selector must be specified with a more specific selector which
        * will not select all descendants. For example:
        *
        *     record: '>node'
        *
        */
        record: '',
        /**
        * @cfg {String} namespace
        * A namespace prefix that will be prepended to the field name when reading a
        * field from an XML node.  Take, for example, the following Model:
        * 
        *     Ext.define('Foo', {
        *         extend: 'Ext.data.Model',
        *         fields: ['bar', 'baz']
        *     });
        *     
        * The reader would need to be configured with a namespace of 'n' in order to read XML
        * data in the following format:
        * 
        *     <foo>
        *         <n:bar>bar</n:bar>
        *         <n:baz>baz</n:baz>
        *     </foo>
        */
        namespace: ''
    },
    /**
     * @private
     */
    responseType: 'document',
    /**
     * Creates a function to return some particular key of data from a response. The
     * `totalProperty` and `successProperty` are treated as special cases for type
     * casting, everything else is just a simple selector.
     * @param {String} expr
     * @return {Function}
     * @private
     */
    createAccessor: function(expr) {
        if (Ext.isEmpty(expr)) {
            return Ext.emptyFn;
        }
        if (Ext.isFunction(expr)) {
            return expr;
        }
        return function(root) {
            return this.getNodeValue(Ext.DomQuery.selectNode(expr, root));
        };
    },
    getNodeValue: function(node) {
        if (node) {
            // overcome a limitation of maximum textnode size
            // http://reference.sitepoint.com/javascript/Node/normalize
            // https://developer.mozilla.org/En/DOM/Node.normalize
            if (typeof node.normalize === 'function') {
                node.normalize();
            }
            node = node.firstChild;
            if (node) {
                return node.nodeValue;
            }
        }
        return undefined;
    },
    getResponseData: function(response) {
        var xml = response.responseXML,
            error = 'XML data not found in the response';
        if (!xml) {
            Ext.Logger.warn(error);
            return this.createReadError(error);
        }
        return xml;
    },
    /**
     * Normalizes the data object.
     * @param {Object} data The raw data object
     * @return {Object} The documentElement property of the data object if present, or the same
     * object if not.
     */
    getData: function(data) {
        return data.documentElement || data;
    },
    /**
     * @private
     * Given an XML object, returns the Element that represents the root as configured by the
     * Reader's meta data.
     * @param {Object} data The XML data object
     * @return {XMLElement} The root node element
     */
    getRoot: function(data) {
        return this.getRootValue(data, this.getRootProperty());
    },
    /**
     * @private
     * We're just preparing the data for the superclass by pulling out the record nodes we want.
     * @param {XMLElement} root The XML root node
     * @param {Object} [readOptions] See {@link #read} for details.
     * @return {Ext.data.Model[]} The records
     */
    extractData: function(root, readOptions) {
        var recordName = this.getRecord();
        if (!recordName) {
            Ext.raise('Record is a required parameter');
        }
        if (recordName !== root.nodeName) {
            root = Ext.DomQuery.select(recordName, root);
        } else {
            root = [
                root
            ];
        }
        return this.callParent([
            root,
            readOptions
        ]);
    },
    /**
     * Parses an XML document and returns a ResultSet containing the model instances.
     * @param {Object} doc Parsed XML document
     * @param {Object} [readOptions] See {@link #read} for details.
     * @param {Object} [internalReadOptions] (private)
     * @return {Ext.data.ResultSet} The parsed result set
     */
    readRecords: function(doc, readOptions, internalReadOptions) {
        // it's possible that we get passed an array here by associations.
        // Make sure we strip that out (see Ext.data.reader.Reader#readAssociated)
        if (Ext.isArray(doc)) {
            doc = doc[0];
        }
        return this.callParent([
            doc,
            readOptions,
            internalReadOptions
        ]);
    },
    /**
     * @private
     * Returns an accessor function for the passed Field from an XML element using either the
     * Field's mapping, or its ordinal position in the fields collection as the index.
     * This is used by buildExtractors to create optimized on extractor function which converts
     * raw data into model instances.
     */
    createFieldAccessor: function(field) {
        var namespace = this.getNamespace(),
            selector, autoMapping, result;
        if (field.mapping) {
            selector = field.mapping;
        } else {
            selector = (namespace ? namespace + '|' : '') + field.name;
            autoMapping = true;
        }
        if (typeof selector === 'function') {
            result = function(raw, self) {
                return field.mapping(raw, self);
            };
        } else {
            // The generated field accessor is a *very* hot code path in XML reader,
            // so we try hard to optimize away any checks and lessen run time penalty.
            // We also try hard to use native code where possible, since Ext.DomQuery
            // is slow and very CPU intensive.
            // querySelector and getNodeValue break on namespaces so we can't use them
            if (autoMapping && !namespace && Ext.supports.XmlQuerySelector) {
                result = function(raw, self) {
                    return self.getNodeValue(raw.querySelector(selector));
                };
            }
            if (!result) {
                result = function(raw, self) {
                    return self.getNodeValue(Ext.DomQuery.selectNode(selector, raw));
                };
            }
        }
        return result;
    },
    privates: {
        getGroupRoot: function(data) {
            return this.getRootValue(data, this.getGroupRootProperty());
        },
        getRootValue: function(data, prop) {
            var nodeName = data.nodeName;
            if (!prop || (nodeName && nodeName === prop)) {
                return data;
            } else if (typeof prop === 'function') {
                return prop(data);
            } else if (Ext.DomQuery.isXml(data)) {
                // This fix ensures we have XML data
                // Related to TreeStore calling getRoot with the root node, which isn't XML
                // Probably should be resolved in TreeStore at some point
                return Ext.DomQuery.selectNode(prop, data);
            }
        },
        getSummaryRoot: function(data) {
            return this.getRootValue(data, this.getSummaryRootProperty());
        }
    },
    deprecated: {
        '5.1.1': {
            properties: {
                /**
                 * @property {Object} xmlData
                 * Copy of {@link #rawData}.
                 * @deprecated 5.1.1 Removed in Ext JS 5.0. Use {@link #rawData} instead.
                 */
                xmlData: null
            }
        }
    }
});

/**
 * @class Ext.data.writer.Xml
 * This class is used to write {@link Ext.data.Model} data to the server in an XML format.
 * The {@link #documentRoot} property is used to specify the root element in the XML document.
 * The {@link #record} option is used to specify the element name for each record that will
 * make up the XML document.
 */
Ext.define('Ext.data.writer.Xml', {
    extend: 'Ext.data.writer.Writer',
    alternateClassName: 'Ext.data.XmlWriter',
    alias: 'writer.xml',
    config: {
        /**
         * @cfg {String} documentRoot The name of the root element of the document. Defaults to
         * `'xmlData'`. If there is more than 1 record and the root is not specified, the default
         * document root will still be used to ensure a valid XML document is created.
         *
         * If the {@link #record} mapping includes a root element name, eg: "SystemInfo>Operation",
         * and the selector includes the root element name, then you must configure this as `false`
         */
        documentRoot: 'xmlData',
        /**
         * @cfg {String} defaultDocumentRoot The root to be used if {@link #documentRoot} is empty
         * and a root is required to form a valid XML document.
         */
        defaultDocumentRoot: 'xmlData',
        /**
         * @cfg {String} header A header to use in the XML document (such as setting the encoding
         * or version). Defaults to `''`.
         */
        header: '',
        /**
         * @cfg {String} record The name of the node to use for each record. Defaults to
         * the owning {@link Ext.data.proxy.Proxy Proxy}'s {@link Ext.data.reader.Xml Reader}'s
         * {@link Ext.data.reader.Xml#record} setting, or `'record'`.
         */
        record: 'record'
    },
    // To break simple XPath selectors like "SystemInfo>SystemName"
    // into ["SystemInfo", "SystemName"]
    selectorRe: /[^>\s]+/g,
    writeRecords: function(request, data) {
        var me = this,
            xml = [],
            i = 0,
            len = data.length,
            root = me.getDocumentRoot(),
            recordName = me.getRecord(),
            // Convert eg 'Items>Item' into ['Items', 'Item']
            record = recordName.match(this.selectorRe),
            recLen = record.length,
            // Need a containing element if there are multiple data records and
            // it's not a compound record selector
            needsRoot = data.length !== 1 && recLen === 1,
            transform;
        transform = this.getTransform();
        if (transform) {
            data = transform(data, request);
        }
        // may not exist
        xml.push(me.getHeader() || '');
        if (!root && needsRoot) {
            root = me.getDefaultDocumentRoot();
        }
        // May not exist if configured as false, and the record selector is rooted, eg "Items>Item"
        if (root) {
            xml.push('<', root, '>');
        }
        // Output record nodes' wrapping, eg "<Items>" from record "Items>Item"->["Items", "Item"]
        for (i = 0; i < recLen - 1; i++) {
            xml.push('<', record[i], '>');
        }
        recordName = record[i];
        for (i = 0; i < len; ++i) {
            this.objectToElement(recordName, data[i], xml);
        }
        // Close record nodes' wrapping, eg "</Items>" from record "Items>Item"->["Items", "Item"]
        for (i = recLen - 2; i > -1; i--) {
            xml.push('</', record[i], '>');
        }
        if (root) {
            xml.push('</', root, '>');
        }
        request.setXmlData(xml.join(''));
        return request;
    },
    /**
     * Serializes an object to XML.
     * Properties will be serialized as child elements unless their first character is `'@'`
     *
     * For example:
     *
     *     myWriter.objectToElement('SystemComponent', {
     *         "@SystemNumber": '10118795',
     *         "SystemInfo>SystemName": 'Phase Noise Measurement System',
     *         AssetId: 'DE3208',
     *         AgilentModel: 'E5505A',
     *         SerialNumber: 'US44101357',
     *     }, []).join('');
     *
     * becomes
     *
     *     <SystemComponent SystemNumber="10118795">
     *       <SystemInfo>
     *           <SystemName>Phase Noise Measurement System</SystemName>
     *       </SystemInfo>
     *       <AssetId>DE3208</AssetId>
     *       <AgilentModel>E5505A</AgilentModel>
     *       <SerialNumber>US44101357</SerialNumber>
     *     </SystemComponent>
     *    
     * @param {String} name The element name for the object.
     * @param {Object} o The object to serialize.
     * @param {Array} [output] The array into which to serialize the object.
     * @return {undefined}
     */
    objectToElement: function(name, o, output) {
        var subOutput = [],
            key, datum, subKeys, subKeyLen, subObject, subObjects, lastObject, lastKey, i;
        if (!output) {
            output = [];
        }
        // Open the record node, eg "<Item"
        // Stop there because some child properties may be attributes.
        output.push('<', name);
        for (key in o) {
            datum = o[key];
            // Attribute node
            if (key[0] === '@') {
                output.push(' ', key.substr(1), '="', datum, '"');
            } else // Child element node
            {
                // Object properties become child elements
                if (typeof datum === 'object') {
                    this.objectToElement(key, datum, subOutput);
                } else {
                    // Is it a selector?
                    subKeys = key.match(this.selectorRe);
                    // key was eg "foo > bar".
                    // Ensure looks like contains {foo: {bar: {}}}
                    if ((subKeyLen = subKeys.length) > 1) {
                        subObjects = subObjects || {};
                        for (subObject = subObjects , i = 0; i < subKeyLen; i++) {
                            lastObject = subObject;
                            lastKey = subKeys[i];
                            subObject = subObject[lastKey] || (subObject[lastKey] = {});
                        }
                        // lastObject is now the bar property in the above example
                        lastObject[lastKey] = datum;
                    } else {
                        subOutput.push('<', key, '>', datum, '</', key, '>');
                    }
                }
            }
        }
        output.push('>');
        output.push.apply(output, subOutput);
        // Output any embedded nodes that were specified by child element mappings
        // like "SystemInfo>SystemName"
        if (subObjects) {
            for (key in subObjects) {
                datum = subObjects[key];
                this.objectToElement(key, datum, output);
            }
        }
        // Close the record node, eg "</Item>"
        output.push('</', name, '>');
        return output;
    }
});

/**
 * Small helper class to make creating {@link Ext.data.Store}s from XML data easier.
 * An XmlStore will be automatically configured with a {@link Ext.data.reader.Xml}.
 *
 * A store configuration would be something like:
 *
 *     var store = new Ext.data.XmlStore({
 *         // store configs
 *         storeId: 'myStore',
 *         url: 'sheldon.xml', // automatically configures a HttpProxy
 *
 *         // reader configs
 *         record: 'Item', // records will have an "Item" tag
 *         idPath: 'ASIN',
 *         totalRecords: '@TotalResults'
 *
 *         fields: [
 *             // set up the fields mapping into the xml doc
 *             // The first needs mapping, the others are very basic
 *             {name: 'Author', mapping: 'ItemAttributes > Author'},
 *             'Title', 'Manufacturer', 'ProductGroup'
 *         ]
 *     });
 *
 * This store is configured to consume a returned object of the form:
 *
 *      <?xml version="1.0" encoding="UTF-8"?>
 *      <ItemSearchResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2009-05-15">
 *          <Items>
 *              <Request>
 *                  <IsValid>True</IsValid>
 *                  <ItemSearchRequest>
 *                      <Author>Sidney Sheldon</Author>
 *                      <SearchIndex>Books</SearchIndex>
 *                  </ItemSearchRequest>
 *              </Request>
 *              <TotalResults>203</TotalResults>
 *              <TotalPages>21</TotalPages>
 *              <Item>
 *                  <ASIN>0446355453</ASIN>
 *                  <DetailPageURL>
 *                      http://www.amazon.com/
 *                  </DetailPageURL>
 *                  <ItemAttributes>
 *                      <Author>Sidney Sheldon</Author>
 *                      <Manufacturer>Warner Books</Manufacturer>
 *                      <ProductGroup>Book</ProductGroup>
 *                      <Title>Master of the Game</Title>
 *                  </ItemAttributes>
 *              </Item>
 *          </Items>
 *      </ItemSearchResponse>
 *
 * An object literal of this form could also be used as the {@link #cfg-data} config option.
 * **Note:** This class accepts all of the configuration options of
 * {@link Ext.data.reader.Xml XmlReader}.
 */
Ext.define('Ext.data.XmlStore', {
    extend: 'Ext.data.Store',
    alias: 'store.xml',
    requires: [
        'Ext.data.proxy.Ajax',
        'Ext.data.reader.Xml',
        'Ext.data.writer.Xml'
    ],
    constructor: function(config) {
        config = Ext.apply({
            proxy: {
                type: 'ajax',
                reader: 'xml',
                writer: 'xml'
            }
        }, config);
        this.callParent([
            config
        ]);
    }
});

/**
 * This class id generator produces successive negative numbers for id's. That is, -1, -2,
 * etc..
 *
 * The advantage of this type of `identifier` is that these are seldom valid server-side
 * id values (which typically start at 1 and increase from there) but are of the same
 * data type (integer). This means that these values can typically be deserialized by a
 * server and then recognized as provisionally generated.
 */
Ext.define('Ext.data.identifier.Negative', {
    extend: 'Ext.data.identifier.Sequential',
    alias: 'data.identifier.negative',
    config: {
        increment: -1,
        seed: -1
    }
});

/**
 * This class generates UUID's according to RFC 4122. This class has a default id property.
 * This means that a single instance is shared unless the id property is overridden. Thus,
 * two {@link Ext.data.Model} instances configured like the following share one generator:
 *
 *     Ext.define('MyApp.data.MyModelX', {
 *         extend: 'Ext.data.Model',
 *         identifier: 'uuid'
 *     });
 *
 *     Ext.define('MyApp.data.MyModelY', {
 *         extend: 'Ext.data.Model',
 *         identifier: 'uuid'
 *     });
 *
 * This allows all models using this class to share a commonly configured instance.
 *
 * # Using Version 1 ("Sequential") UUID's
 *
 * If a server can provide a proper timestamp and a "cryptographic quality random number"
 * (as described in RFC 4122), the shared instance can be configured as follows:
 *
 *     Ext.data.identifier.Uuid.Global.reconfigure({
 *         version: 1,
 *         clockSeq: clock, // 14 random bits
 *         salt: salt,      // 48 secure random bits (the Node field)
 *         timestamp: ts    // timestamp per Section 4.1.4
 *     });
 *
 *     // or these values can be split into 32-bit chunks:
 *
 *     Ext.data.identifier.Uuid.Global.reconfigure({
 *         version: 1,
 *         clockSeq: clock,
 *         salt: { lo: saltLow32, hi: saltHigh32 },
 *         timestamp: { lo: timestampLow32, hi: timestamptHigh32 }
 *     });
 *
 * This approach improves the generator's uniqueness by providing a valid timestamp and
 * higher quality random data. Version 1 UUID's should not be used unless this information
 * can be provided by a server and care should be taken to avoid caching of this data.
 *
 * See [http://www.ietf.org/rfc/rfc4122.txt](http://www.ietf.org/rfc/rfc4122.txt) for details.
 */
Ext.define('Ext.data.identifier.Uuid', {
    extend: 'Ext.data.identifier.Generator',
    alias: 'data.identifier.uuid',
    /**
     * Provides a way to determine if this identifier supports creating unique IDs. Proxies like
     * {@link Ext.data.proxy.LocalStorage} need the identifier to create unique IDs and will check
     * this property.
     * @property isUnique
     * @type Boolean
     * @private
     */
    isUnique: true,
    config: {
        /**
         * @cfg {String} id
         * The id for this generator instance. By default all model instances share the same
         * UUID generator instance. By specifying an id other than 'uuid', a unique generator
         * instance will be created for the Model.
         */
        id: null
    },
    /**
     * @cfg {Number} [version=4]
     * The Version of UUID. Supported values are:
     *
     *  * 1 : Time-based, "sequential" UUID. To use this type of generator, you must also
     *  specify the `salt`, `timestamp` and `clock` properties. For details on the values
     *  and how a server should produce them, see RFC 4122. Use of this type of generator
     *  produces values that are easier to read since they are sequential, but requires
     *  some care to initialize properly and still ensure their uniqueness.
     *
     *  * 4 : Pseudo-random UUID. This is the simplest form and requires no configuration
     *  and hence is the default type.
     */
    /**
     * @cfg {Number/Object} [salt]
     * This value is a 48-bit number. This can be a number or an object with `hi` and `lo`
     * properties where `lo` is the low 32-bits and `hi` is the upper 16 bits.
     *
     * Only applicable when `version` is set to `1`.
     */
    /**
     * @cfg {Number/Object} [timestamp]
     * When created, this value is a 60-bit number. This can be a number or an object with
     * `hi` and `lo` properties where `lo` is the low 32-bits and `hi` is the upper 28 bits.
     *
     * Only applicable when `version` is set to `1`.
     */
    /**
     * @cfg {Number} [clockSeq]
     * A clock value to help avoid duplicates.
     *
     * Only applicable when `version` is set to `1`.
     */
    constructor: function(config) {
        this.callParent([
            config
        ]);
        this.reconfigure(config);
    },
    /**
     * Reconfigures this generator given new config properties. The only values that this
     * changes are `version` and, if `version` is 1, its related config properties.
     */
    reconfigure: function(config) {
        var cls = this.self;
        this.generate = (config && config.version === 1) ? cls.createSequential(config.salt, config.timestamp, config.clockSeq) : cls.createRandom();
    },
    clone: null,
    statics: {
        createRandom: function() {
            var pattern = 'xxxxxxxx-xxxx-4xxx-Rxxx-xMxxxxxxxxxx'.split(''),
                hex = '0123456789abcdef'.split(''),
                length = pattern.length,
                parts = [];
            return function() {
                var r, c, i;
                for (i = 0; i < length; ++i) {
                    c = pattern[i];
                    if (c !== '-' && c !== '4') {
                        r = Math.random() * 16;
                        r = (c === 'R') ? (r & 3 | 8) : (r | ((c === 'M') ? 1 : 0));
                        c = hex[r];
                    }
                    // above floors r so always 0-15
                    parts[i] = c;
                }
                return parts.join('');
            };
        },
        createSequential: function(salt, time, clockSeq) {
            var parts = [],
                twoPow32 = Math.pow(2, 32),
                saltLo = salt.lo,
                saltHi = salt.hi,
                timeLo = time.lo,
                timeHi = time.hi,
                toHex;
            toHex = function(value, length) {
                var ret = value.toString(16).toLowerCase();
                if (ret.length > length) {
                    ret = ret.substring(ret.length - length);
                }
                // right-most digits
                else if (ret.length < length) {
                    ret = Ext.String.leftPad(ret, length, '0');
                }
                return ret;
            };
            if (typeof salt === 'number') {
                saltHi = Math.floor(salt / twoPow32);
                saltLo = Math.floor(salt - saltHi * twoPow32);
            }
            if (typeof time === 'number') {
                timeHi = Math.floor(time / twoPow32);
                timeLo = Math.floor(time - timeHi * twoPow32);
            }
            // Set multicast bit: "the least significant bit of the first octet of the
            // node ID" (nodeId = salt for this implementation):
            saltHi |= 256;
            parts[3] = toHex(128 | ((clockSeq >>> 8) & 63), 2) + toHex(clockSeq & 255, 2);
            parts[4] = toHex(saltHi, 4) + toHex(saltLo, 8);
            /*
               The magic decoder ring (derived from RFC 4122 Section 4.2.2):

               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
               |                          time_low                             |
               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
               |           time_mid            |  ver  |        time_hi        |
               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
               |res|  clock_hi |   clock_low   |    salt 0   |M|     salt 1    |
               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
               |                         salt (2-5)                            |
               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

                         time_mid      clock_hi (low 6 bits)
                time_low     | time_hi |clock_lo
                    |        |     |   || salt[0]
                    |        |     |   ||   | salt[1..5]
                    v        v     v   vv   v v
                    0badf00d-aced-1def-b123-dfad0badbeef
                                  ^    ^     ^
                            version    |     multicast (low bit)
                                       |
                                    reserved (upper 2 bits)
            */
            return function() {
                parts[0] = toHex(timeLo, 8);
                parts[1] = toHex(timeHi & 65535, 4);
                parts[2] = toHex(((timeHi >>> 16) & 4095) | (1 << 12), 4);
                // sequentially increment the timestamp...
                ++timeLo;
                if (timeLo >= twoPow32) {
                    // if (overflow)
                    timeLo = 0;
                    ++timeHi;
                }
                return parts.join('-');
            };
        }
    }
}, function() {
    this.Global = new this({
        id: 'uuid'
    });
});

/**
 * WebStorageProxy is simply a superclass for the {@link Ext.data.proxy.LocalStorage LocalStorage}
 * and {@link Ext.data.proxy.SessionStorage SessionStorage} proxies. It uses the new HTML5
 * key/value client-side storage objects to save {@link Ext.data.Model model instances} for
 * offline use.
 * @private
 */
Ext.define('Ext.data.proxy.WebStorage', {
    extend: 'Ext.data.proxy.Client',
    alternateClassName: 'Ext.data.WebStorageProxy',
    requires: [
        'Ext.data.identifier.Sequential'
    ],
    config: {
        /**
         * @cfg {String} id
         * The unique ID used as the key in which all record data are stored in the local
         * storage object.
         */
        id: undefined
    },
    /**
     * @cfg {Object} reader
     * Not used by web storage proxy.
     * @hide
     */
    /**
     * @cfg {Object} writer
     * Not used by web storage proxy.
     * @hide
     */
    /**
     * Creates the proxy, throws an error if local storage is not supported in the current browser.
     * @param {Object} config (optional) Config object.
     */
    constructor: function(config) {
        this.callParent(arguments);
        /**
         * @property {Object} cache
         * Cached map of records already retrieved by this Proxy. Ensures that the same instance is
         * always retrieved.
         */
        this.cache = {};
        if (this.getStorageObject() === undefined) {
            Ext.raise("Local Storage is not supported in this browser, please use another type " + "of data proxy");
        }
        if (this.getId() === undefined) {
            Ext.raise("No unique id was provided to the local storage proxy. " + "See Ext.data.proxy.LocalStorage documentation for details");
        }
        this.initialize();
    },
    /**
     * @method create
     * @inheritdoc
     */
    create: function(operation) {
        var me = this,
            records = operation.getRecords(),
            length = records.length,
            ids = me.getIds(),
            id, record, i, identifier;
        if (me.isHierarchical === undefined) {
            // if the storage object does not yet contain any data, this is the first point
            // at which we can determine whether or not this proxy deals with hierarchical data.
            // it cannot be determined during initialization because the Model is not decorated
            // with NodeInterface until it is used in a TreeStore
            me.isHierarchical = !!records[0].isNode;
            if (me.isHierarchical) {
                me.getStorageObject().setItem(me.getTreeKey(), true);
            }
        }
        for (i = 0; i < length; i++) {
            record = records[i];
            if (record.phantom) {
                identifier = record.identifier;
                if (identifier && identifier.isUnique) {
                    id = record.getId();
                } else {
                    id = me.getNextId();
                }
            } else {
                id = record.getId();
            }
            me.setRecord(record, id);
            record.commit();
            ids.push(id);
        }
        me.setIds(ids);
        operation.setSuccessful(true);
    },
    /**
     * @method read
     * @inheritdoc
     */
    read: function(operation) {
        var me = this,
            allRecords,
            records = [],
            success = true,
            Model = me.getModel(),
            validCount = 0,
            recordCreator = operation.getRecordCreator(),
            filters, sorters, limit, filterLen, valid, record, ids, length, data, id, i, j;
        if (me.isHierarchical) {
            records = me.getTreeData();
        } else {
            ids = me.getIds();
            length = ids.length;
            id = operation.getId();
            // read a single record
            if (id) {
                data = me.getRecord(id);
                if (data !== null) {
                    record = recordCreator ? recordCreator(data, Model) : new Model(data);
                }
                if (record) {
                    records.push(record);
                } else {
                    success = false;
                }
            } else {
                sorters = operation.getSorters();
                filters = operation.getFilters();
                limit = operation.getLimit();
                allRecords = [];
                // build an array of all records first first so we can sort them before
                // applying filters or limit.  These are Model instances instead of raw
                // data objects so that the sorter and filter Fn can use the Model API
                for (i = 0; i < length; i++) {
                    data = me.getRecord(ids[i]);
                    record = recordCreator ? recordCreator(data, Model) : new Model(data);
                    allRecords.push(record);
                }
                if (sorters) {
                    Ext.Array.sort(allRecords, Ext.util.Sorter.createComparator(sorters));
                }
                for (i = operation.getStart() || 0; i < length; i++) {
                    record = allRecords[i];
                    valid = true;
                    if (filters) {
                        for (j = 0 , filterLen = filters.length; j < filterLen; j++) {
                            valid = filters[j].filter(record);
                        }
                    }
                    if (valid) {
                        records.push(record);
                        validCount++;
                    }
                    if (limit && validCount === limit) {
                        break;
                    }
                }
            }
        }
        if (success) {
            operation.setResultSet(new Ext.data.ResultSet({
                records: records,
                total: records.length,
                loaded: true
            }));
            operation.setSuccessful(true);
        } else {
            operation.setException('Unable to load records');
        }
    },
    /**
     * @method update
     * @inheritdoc
     */
    update: function(operation) {
        var records = operation.getRecords(),
            length = records.length,
            ids = this.getIds(),
            record, id, i;
        for (i = 0; i < length; i++) {
            record = records[i];
            this.setRecord(record);
            record.commit();
            // we need to update the set of ids here because it's possible that
            // a non-phantom record was added to this proxy - in which case the record's
            // id would never have been added via the normal 'create' call
            id = record.getId();
            if (id !== undefined && Ext.Array.indexOf(ids, id) === -1) {
                ids.push(id);
            }
        }
        this.setIds(ids);
        operation.setSuccessful(true);
    },
    /**
     * @method erase
     * @inheritdoc
     */
    erase: function(operation) {
        var me = this,
            records = operation.getRecords(),
            ids = me.getIds(),
            idLength = ids.length,
            newIds = [],
            removedHash = {},
            i = records.length,
            id;
        for (; i--; ) {
            Ext.apply(removedHash, me.removeRecord(records[i]));
        }
        for (i = 0; i < idLength; i++) {
            id = ids[i];
            if (!removedHash[id]) {
                newIds.push(id);
            }
        }
        me.setIds(newIds);
        operation.setSuccessful(true);
    },
    /**
     * @private
     * Fetches record data from the Proxy by ID.
     * @param {String} id The record's unique ID
     * @return {Object} The record data
     */
    getRecord: function(id) {
        var me = this,
            cache = me.cache,
            data;
        data = !cache[id] ? Ext.decode(me.getStorageObject().getItem(me.getRecordKey(id))) : cache[id];
        if (!data) {
            return null;
        }
        cache[id] = data;
        data[me.getModel().prototype.idProperty] = id;
        // In order to preserve the cache, we MUST copy it here because
        // Models use the incoming raw data as their data object and convert/default values
        // into that object
        return Ext.merge({}, data);
    },
    /**
     * Saves the given record in the Proxy.
     * @param {Ext.data.Model} record The model instance
     * @param {String} [id] The id to save the record under (defaults to the value of the
     * record's getId() function)
     */
    setRecord: function(record, id) {
        if (id) {
            record.set('id', id, {
                commit: true
            });
        } else {
            id = record.getId();
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            rawData = record.getData(),
            data = {},
            model = me.getModel(),
            fields = model.getFields(),
            length = fields.length,
            i = 0,
            field, name, obj, key, value;
        for (; i < length; i++) {
            field = fields[i];
            name = field.name;
            if (field.persist) {
                value = rawData[name];
                if (field.isDateField && field.dateFormat && Ext.isDate(value)) {
                    value = Ext.Date.format(value, field.dateFormat);
                } else if (field.serialize) {
                    value = field.serialize(value, record);
                }
                data[name] = value;
            }
        }
        // no need to store the id in the data, since it is already stored in the record key
        delete data[model.prototype.idProperty];
        // if the record is a tree node and it's a direct child of the root node, do not store
        // the parentId
        if (record.isNode && record.get('depth') === 1) {
            delete data.parentId;
        }
        obj = me.getStorageObject();
        key = me.getRecordKey(id);
        // keep the cache up to date
        me.cache[id] = data;
        // iPad bug requires that we remove the item before setting it
        obj.removeItem(key);
        obj.setItem(key, Ext.encode(data));
    },
    /**
     * @private
     * Physically removes a given record from the local storage and recursively removes children
     * if the record is a tree node. Used internally by {@link #destroy}.
     * @param {Ext.data.Model} record The record to remove
     * @return {Object} a hash with the ids of the records that were removed as keys and the
     * records that were removed as values
     */
    removeRecord: function(record) {
        var me = this,
            id = record.getId(),
            records = {},
            i, childNodes;
        records[id] = record;
        me.getStorageObject().removeItem(me.getRecordKey(id));
        delete me.cache[id];
        if (record.childNodes) {
            childNodes = record.childNodes;
            for (i = childNodes.length; i--; ) {
                Ext.apply(records, me.removeRecord(childNodes[i]));
            }
        }
        return records;
    },
    /**
     * @private
     * Given the id of a record, returns a unique string based on that id and the id of this proxy.
     * This is used when storing data in the local storage object and should prevent naming
     * collisions.
     * @param {String/Number/Ext.data.Model} id The record id, or a Model instance
     * @return {String} The unique key for this record
     */
    getRecordKey: function(id) {
        if (id.isModel) {
            id = id.getId();
        }
        return Ext.String.format("{0}-{1}", this.getId(), id);
    },
    /**
     * @private
     * Returns the unique key used to store the current record counter for this proxy. This is used
     * internally when realizing models (creating them when they used to be phantoms), in order to
     * give each model instance a unique id.
     * @return {String} The counter key
     */
    getRecordCounterKey: function() {
        return Ext.String.format("{0}-counter", this.getId());
    },
    /**
     * @private
     * Returns the unique key used to store the tree indicator. This is used internally to
     * determine if the stored data is hierarchical
     * @return {String} The counter key
     */
    getTreeKey: function() {
        return Ext.String.format("{0}-tree", this.getId());
    },
    /**
     * @private
     * Returns the array of record IDs stored in this Proxy
     * @return {Number[]} The record IDs. Each is cast as a Number
     */
    getIds: function() {
        var me = this,
            ids = (me.getStorageObject().getItem(me.getId()) || "").split(","),
            length = ids.length,
            isString = this.getIdField().isStringField,
            i;
        if (length === 1 && ids[0] === "") {
            ids = [];
        } else {
            for (i = 0; i < length; i++) {
                ids[i] = isString ? ids[i] : +ids[i];
            }
        }
        return ids;
    },
    getIdField: function() {
        return this.getModel().prototype.idField;
    },
    /**
     * @private
     * Saves the array of ids representing the set of all records in the Proxy
     * @param {Number[]} ids The ids to set
     */
    setIds: function(ids) {
        var obj = this.getStorageObject(),
            str = ids.join(","),
            id = this.getId();
        obj.removeItem(id);
        if (!Ext.isEmpty(str)) {
            obj.setItem(id, str);
        }
    },
    /**
     * @private
     * Returns the next numerical ID that can be used when realizing a model instance
     * (see getRecordCounterKey). Increments the counter.
     * @return {Number} The id
     */
    getNextId: function() {
        var me = this,
            obj = me.getStorageObject(),
            key = me.getRecordCounterKey(),
            isString = me.getIdField().isStringField,
            id;
        id = me.idGenerator.generate();
        obj.setItem(key, id);
        if (isString) {
            id = id + '';
        }
        return id;
    },
    /**
     * Gets tree data and transforms it from key value pairs into a hierarchical structure.
     * @private
     * @return {Ext.data.NodeInterface[]}
     */
    getTreeData: function() {
        var me = this,
            ids = me.getIds(),
            length = ids.length,
            records = [],
            recordHash = {},
            root = [],
            i = 0,
            Model = me.getModel(),
            idProperty = Model.prototype.idProperty,
            rootLength, record, parent, parentId, children, id;
        for (; i < length; i++) {
            id = ids[i];
            // get the record for each id
            record = me.getRecord(id);
            // push the record into the records array
            records.push(record);
            // add the record to the record hash so it can be easily retrieved by id later
            recordHash[id] = record;
            if (!record.parentId) {
                // push records that are at the root level (those with no parent id) into the
                // "root" array
                root.push(record);
            }
        }
        rootLength = root.length;
        // sort the records by parent id for greater efficiency, so that each parent record only
        // has to be found once for all of its children
        Ext.Array.sort(records, me.sortByParentId);
        // append each record to its parent, starting after the root node(s), since root nodes
        // do not need to be attached to a parent
        for (i = rootLength; i < length; i++) {
            record = records[i];
            parentId = record.parentId;
            if (!parent || parent[idProperty] !== parentId) {
                // if this record has a different parent id from the previous record, we need to
                // look up the parent by id.
                parent = recordHash[parentId];
                parent.children = children = [];
            }
            // push the record onto its parent's children array
            children.push(record);
        }
        for (i = length; i--; ) {
            record = records[i];
            if (!record.children && !record.leaf) {
                // set non-leaf nodes with no children to loaded so the proxy won't try to
                // dynamically load their contents when they are expanded
                record.loaded = true;
            }
        }
        // Create model instances out of all the "root-level" nodes.
        for (i = rootLength; i--; ) {
            record = root[i];
            root[i] = new Model(record);
        }
        return root;
    },
    /**
     * Sorter function for sorting records by parentId
     * @private
     * @param {Object} node1
     * @param {Object} node2
     * @return {Number}
     */
    sortByParentId: function(node1, node2) {
        return (node1.parentId || 0) - (node2.parentId || 0);
    },
    /**
     * @private
     * Sets up the Proxy by claiming the key in the storage object that corresponds to the unique
     * id of this Proxy. Called automatically by the constructor, this should not need to be called
     * again unless {@link #clear} has been called.
     */
    initialize: function() {
        var me = this,
            storageObject = me.getStorageObject(),
            lastId = +storageObject.getItem(me.getRecordCounterKey()),
            id = me.getId();
        storageObject.setItem(id, storageObject.getItem(id) || "");
        if (storageObject.getItem(me.getTreeKey())) {
            me.isHierarchical = true;
        }
        me.idGenerator = new Ext.data.identifier.Sequential({
            seed: lastId ? lastId + 1 : 1
        });
    },
    /**
     * Destroys all records stored in the proxy and removes all keys and values used to support
     * the proxy from the storage object.
     */
    clear: function() {
        var me = this,
            obj = me.getStorageObject(),
            ids = me.getIds(),
            len = ids.length,
            i;
        // remove all the records
        for (i = 0; i < len; i++) {
            obj.removeItem(me.getRecordKey(ids[i]));
        }
        // remove the supporting objects
        obj.removeItem(me.getRecordCounterKey());
        obj.removeItem(me.getTreeKey());
        obj.removeItem(me.getId());
        // clear the cache
        me.cache = {};
    },
    /**
     * @private
     * Abstract function which should return the storage object that data will be saved to.
     * This must be implemented in each subclass.
     * @return {Object} The storage object
     */
    getStorageObject: function() {
        Ext.raise("The getStorageObject function has not been defined in your " + "Ext.data.proxy.WebStorage subclass");
    }
});

/**
 * The LocalStorageProxy uses the new HTML5 localStorage API to save {@link Ext.data.Model Model}
 * data locally on the client browser. HTML5 localStorage is a key-value store (e.g. cannot save
 * complex objects like JSON), so LocalStorageProxy automatically serializes and deserializes data
 * when saving and retrieving it.
 *
 * localStorage is extremely useful for saving user-specific information without needing to build
 * server-side infrastructure to support it. Let's imagine we're writing a Twitter search
 * application and want to save the user's searches locally so they can easily perform a saved
 * search again later. We'd start by creating a Search model:
 *
 *     Ext.define('Search', {
 *         fields: ['id', 'query'],
 *         extend: 'Ext.data.Model',
 *         proxy: {
 *             type: 'localstorage',
 *             id: 'twitter-Searches'
 *         }
 *     });
 *
 * Our Search model contains just two fields - id and query - plus a Proxy definition. The only
 * configuration we need to pass to the LocalStorage proxy is an {@link #id}. This is important
 * as it separates the Model data in this Proxy from all others. The localStorage API puts all data
 * into a single shared namespace, so by setting an id we enable LocalStorageProxy to manage
 * the saved Search data.
 *
 * Saving our data into localStorage is easy and would usually be done with a
 * {@link Ext.data.Store Store}:
 *
 *     // our Store automatically picks up the LocalStorageProxy defined on the Search model
 *     var store = Ext.create('Ext.data.Store', {
 *         model: "Search"
 *     });
 *
 *     // loads any existing Search data from localStorage
 *     store.load();
 *
 *     // now add some Searches
 *     store.add({ query: 'Sencha Touch' });
 *     store.add({ query: 'Ext JS' });
 *
 *     // finally, save our Search data to localStorage
 *     store.sync();
 *
 * The LocalStorageProxy automatically gives our new Searches an id when we call store.sync().
 * It encodes the Model data and places it into localStorage. We can also save directly
 * to localStorage, bypassing the Store altogether:
 *
 *     var search = Ext.create('Search', { query: 'Sencha Animator' });
 *
 *     // uses the configured LocalStorageProxy to save the new Search to localStorage
 *     search.save();
 *
 * # Limitations
 *
 * If this proxy is used in a browser where local storage is not supported, the constructor
 * will throw an error. A local storage proxy requires a unique ID which is used as a key in which
 * all record data are stored in the local storage object.
 *
 * It's important to supply this unique ID as it cannot be reliably determined otherwise. If no id
 * is provided but the attached store has a storeId, the storeId will be used. If neither option
 * is presented the proxy will throw an error.
 */
Ext.define('Ext.data.proxy.LocalStorage', {
    extend: 'Ext.data.proxy.WebStorage',
    alias: 'proxy.localstorage',
    alternateClassName: 'Ext.data.LocalStorageProxy',
    getStorageObject: function() {
        return window.localStorage;
    }
});

/**
 * The Rest proxy is a specialization of the {@link Ext.data.proxy.Ajax AjaxProxy} which simply maps
 * the four actions (create, read, update and destroy) to RESTful HTTP verbs. For example, let's
 * set up a {@link Ext.data.Model Model} with an inline Rest proxy
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name', 'email'],
 *
 *         proxy: {
 *             type: 'rest',
 *             url: '/users'
 *         }
 *     });
 *
 * Now we can create a new User instance and save it via the Rest proxy. Doing this will cause
 * the Proxy to send a POST request to '/users':
 *
 *     var user = Ext.create('User', { name: 'Ed Spencer', email: 'ed@sencha.com' });
 *
 *     user.save(); // POST /users
 *
 * Let's expand this a little and provide a callback for the {@link Ext.data.Model#save} call
 * to update the Model once it has been created. We'll assume the creation went successfully
 * and that the server gave this user an ID of 123:
 *
 *     user.save({
 *         success: function(user) {
 *             user.set('name', 'Khan Noonien Singh');
 *
 *             user.save(); // PUT /users/123
 *         }
 *     });
 *
 * Now that we're no longer creating a new Model instance, the request method is changed to an
 * HTTP PUT, targeting the relevant url for that user. Now let's delete this user, which will use
 * the DELETE method:
 *
 *         user.erase(); // DELETE /users/123
 *
 * Finally, when we perform a load of a Model or Store, Rest proxy will use the GET method:
 *
 *     // 1. Load via Store
 *
 *     // the Store automatically picks up the Proxy from the User model
 *     var store = Ext.create('Ext.data.Store', {
 *         model: 'User'
 *     });
 *
 *     store.load(); // GET /users
 *
 *     // 2. Load directly from the Model
 *
 *     // GET /users/123
 *     User.load(123, {
 *         success: function(user) {
 *             console.log(user.getId()); // outputs 123
 *         }
 *     });
 *
 * # Url generation
 *
 * The Rest proxy is able to automatically generate the urls above based on two configuration
 * options - {@link #appendId} and {@link #format}. If appendId is true (it is by default) then
 * Rest proxy will automatically append the ID of the Model instance in question to the configured
 * url, resulting in the '/users/123' that we saw above.
 *
 * If the request is not for a specific Model instance (e.g. loading a Store), the url is not
 * appended with an id. The Rest proxy will automatically insert a '/' before the ID if one is not
 * already present.
 *
 *     new Ext.data.proxy.Rest({
 *         url: '/users',
 *         appendId: true // default
 *     });
 *
 *     // Collection url: /users
 *     // Instance url: /users/123
 *
 * The Rest proxy can also optionally append a format string to the end of any generated url:
 *
 *     new Ext.data.proxy.Rest({
 *         url: '/users',
 *         format: 'json'
 *     });
 *
 *     // Collection url: /users.json
 *     // Instance url: /users/123.json
 *
 * If further customization is needed, simply implement the {@link #buildUrl} method and add your
 * custom generated url onto the {@link Ext.data.Request Request} object that is passed to buildUrl.
 * See [Rest proxy's implementation][1] for an example of how to achieve this.
 *
 * Note that Rest proxy inherits from {@link Ext.data.proxy.Ajax AjaxProxy}, which already injects
 * all of the sorter, filter, group and paging options into the generated url. See the
 * {@link Ext.data.proxy.Ajax AjaxProxy docs} for more details.
 *
 * [1]: source/Rest.html#Ext-data-proxy-Rest-method-buildUrl
 */
Ext.define('Ext.data.proxy.Rest', {
    extend: 'Ext.data.proxy.Ajax',
    alternateClassName: 'Ext.data.RestProxy',
    alias: 'proxy.rest',
    /**
     * @property {Object} actionMethods
     * Mapping of action name to HTTP request method. These default to RESTful conventions for the
     * 'create', 'read', 'update' and 'destroy' actions (which map to 'POST', 'GET', 'PUT', and
     * 'DELETE' respectively). This object should not be changed except globally via
     * {@link Ext#override Ext.override} - the {@link #getMethod} function can be overridden
     * instead.
     */
    defaultActionMethods: {
        create: 'POST',
        read: 'GET',
        update: 'PUT',
        destroy: 'DELETE'
    },
    slashRe: /\/$/,
    periodRe: /\.$/,
    config: {
        /**
         * @cfg {Boolean} appendId
         * True to automatically append the ID of a Model instance when performing a
         * request based on that single instance. See Rest proxy intro docs for more
         * details.
         */
        appendId: true,
        /**
         * @cfg {String} format
         * Optional data format to send to the server when making any request (e.g.
         * 'json'). See the Rest proxy intro docs for full details.
         */
        format: null,
        /**
         * @cfg {Boolean} batchActions
         * True to batch actions of a particular type when synchronizing the store.
         */
        batchActions: false,
        /**
         * @cfg actionMethods
         * @inheritdoc
         */
        actionMethods: {
            create: 'POST',
            read: 'GET',
            update: 'PUT',
            destroy: 'DELETE'
        }
    },
    /**
     * Specialized version of buildUrl that incorporates the {@link #appendId} and 
     * {@link #format} options into the generated url. Override this to provide further
     * customizations, but remember to call the superclass buildUrl so that additional
     * parameters like the cache buster string are appended.
     * @param {Object} request
     */
    buildUrl: function(request) {
        var me = this,
            operation = request.getOperation(),
            records = operation.getRecords(),
            record = records ? records[0] : null,
            format = me.getFormat(),
            url = me.getUrl(request),
            id, params;
        if (record && !record.phantom) {
            id = record.getId();
        } else {
            id = operation.getId();
        }
        if (me.getAppendId() && me.isValidId(id)) {
            if (!url.match(me.slashRe)) {
                url += '/';
            }
            url += encodeURIComponent(id);
            params = request.getParams();
            if (params) {
                delete params[me.getIdParam()];
            }
        }
        if (format) {
            if (!url.match(me.periodRe)) {
                url += '.';
            }
            url += format;
        }
        request.setUrl(url);
        return me.callParent([
            request
        ]);
    },
    isValidId: function(id) {
        return id || id === 0;
    }
});

/**
 * Proxy which uses HTML5 session storage as its data storage/retrieval mechanism. If this proxy
 * is used in a browser where session storage is not supported, the constructor will throw an error.
 * A session storage proxy requires a unique ID which is used as a key in which all record data
 * are stored in the session storage object.
 *
 * It's important to supply this unique ID as it cannot be reliably determined otherwise. If no id
 * is provided but the attached store has a storeId, the storeId will be used. If neither option
 * is presented the proxy will throw an error.
 *
 * Proxies are almost always used with a {@link Ext.data.Store store}:
 *
 *     new Ext.data.Store({
 *         proxy: {
 *             type: 'sessionstorage',
 *             id: 'myProxyKey'
 *         }
 *     });
 *
 * Alternatively you can instantiate the Proxy directly:
 *
 *     new Ext.data.proxy.SessionStorage({
 *         id: 'myOtherProxyKey'
 *     });
 *
 * Note that session storage is different to local storage (see
 * {@link Ext.data.proxy.LocalStorage}) - if a browser session is ended (e.g. by closing
 * the browser) then all data in a SessionStorageProxy are lost. Browser restarts
 * don't affect the {@link Ext.data.proxy.LocalStorage} - the data are preserved.
 */
Ext.define('Ext.data.proxy.SessionStorage', {
    extend: 'Ext.data.proxy.WebStorage',
    alias: 'proxy.sessionstorage',
    alternateClassName: 'Ext.data.SessionStorageProxy',
    getStorageObject: function() {
        return window.sessionStorage;
    }
});

/**
 * @class Ext.data.schema.BelongsTo
 *
 * **This is not a real JavaScript class and cannot be created.  This is for documentation
 * purposes only.**
 *
 * This class provides for creating a key-less `Ext.data.schema.ManyToOne` association.
 * This declaration should be on "many" entity. See `Ext.data.schema.ManyToOne` and
 * `Ext.data.schema.Association` for more information.
 */
/**
 * @cfg {String} name An alias for {@link Ext.data.schema.Reference#role role}.
 */
/**
 * @cfg {String} associatedName An alias for {@link Ext.data.schema.Reference#role role}.
 */
/**
 * @cfg {String} model An alias for {@link Ext.data.schema.Reference#type type}.
 */
/**
 * @cfg {String} [foreignKey] The key field to use in this association
 * @deprecated 6.2.0 Use the `{@link Ext.data.field.Field#cfg!reference reference}`
 * configuration on a data field to created a keyed association.
 */

/**
 * @class Ext.data.schema.HasMany
 *
 * **This is not a real JavaScript class and cannot be created.  This is for documentation
 * purposes only.**
 *
 * This class provides for creating a keyless `Ext.data.schema.ManyToOne` association. This
 * declaration should be on "one" entity. See `Ext.data.schema.ManyToOne` and
 * `Ext.data.schema.Association` for more information.
 */
/**
 * @cfg {String} name An alias for {@link Ext.data.schema.Reference#role role}.
 */
/**
 * @cfg {String} associatedName An alias for {@link Ext.data.schema.Reference#role role}.
 */
/**
 * @cfg {String} model An alias for {@link Ext.data.schema.Reference#type type}.
 */
/**
 * @cfg {String} [foreignKey] The key field to use in this association
 * @deprecated 6.2.0 Use the `{@link Ext.data.field.Field#cfg!reference reference}`
 * configuration on a data field to created a keyed association.
 */

/**
 * @class Ext.data.schema.HasOne
 *
 * **This is not a real JavaScript class and cannot be created.  This is for documentation
 * purposes only.**
 *
 * This class provides for creating a keyless `Ext.data.schema.OneToOne` association. This
 * declaration should be on "owner" entity. See `Ext.data.schema.OneToOne` and
 * `Ext.data.schema.Association` for more information.
 */
/**
 * @cfg {String} name An alias for {@link Ext.data.schema.Reference#role role}.
 */
/**
 * @cfg {String} associatedName An alias for {@link Ext.data.schema.Reference#role role}.
 */
/**
 * @cfg {String} model An alias for {@link Ext.data.schema.Reference#type type}.
 */
/**
 * @cfg {String} [foreignKey] The key field to use in this association
 * @deprecated 6.2.0 Use the `{@link Ext.data.field.Field#cfg!reference reference}`
 * configuration on a data field to created a keyed association.
 */

/**
 * @class Ext.data.schema.Reference
 *
 * **This is not a real JavaScript class and cannot be created. This is for documentation purposes
 * only.**
 *
 * This documentation is for:
 *
 * + {@link Ext.data.field.Field#reference reference config}
 *
 * The {@link Ext.data.Model#entityName name} of the entity referenced by this field.
 * In most databases, this relationship is represented by a "foreign key". That is, a
 * value for such a field matches the value of the {@link Ext.data.Model#idProperty id}
 * for an entity of this type.
 *
 *      Ext.define('MyApp.models.Organization', {
 *          extend: 'Ext.data.Model',
 *          ...
 *      });
 *
 *      Ext.define('MyApp.models.User', {
 *          extend: 'Ext.data.Model',
 *
 *          fields: [
 *              { name: 'organizationId', reference: 'Organization' }
 *          ],
 *          ...
 *      });
 *
 * If the name of the generated {@link Ext.data.schema.Association association} or other aspects
 * need to be specified, the `reference` can be an object. The following usage shows
 * what would be generated by default given the above examples using the string form.
 *
 *      Ext.define('MyApp.models.User', {
 *          extend: 'Ext.data.Model',
 *
 *          fields: [{
 *              name: 'organizationId',
 *              reference: {
 *                  type: 'Organization',
 *                  association: 'UsersByOrganization',
 *                  role: 'organization',
 *                  inverse: 'users'
 *              }
 *          }],
 *          ...
 *      });
 *
 * Finally, a `reference` can also describe ownership between the entities. By default,
 * no ownership relationship is assumed. If, however, the User entities are owned by
 * their Organization, we could say this:
 *
 *      Ext.define('MyApp.models.User', {
 *          extend: 'Ext.data.Model',
 *
 *          fields: [{
 *              name: 'organizationId',
 *              reference: {
 *                  parent: 'Organization' // Organization is the parent of User
 *              }
 *          }],
 *          ...
 *      });
 */
/**
 * @cfg {String} type
 * The type which this field references. This is the value set by the string form of
 * `reference`. If the referenced entity has an ownership relationship this field
 * should be omitted and `reference.parent` or `reference.child` should be specified
 * instead.
 */
/**
 * @cfg {String} association
 * The name of the association. By default, the name of the association is the
 * capitalized `inverse` plus "By" plus the capitalized `role`.
 */
/**
 * @cfg {String} child
 * Set this property instead of `reference.type` to indicate that the referenced entity
 * is an owned child of this entity. That is, the `reference` entity should be deleted
 * when this entity is deleted.
 */
/**
 * @cfg {String} parent
 * Set this property instead of `reference.type` to indicate that the referenced entity
 * is the owning parent of this entity. That is, this entity should be deleted when the
 * `reference` entity is deleted.
 *
 */
/**
 * @cfg {String} role
 * The name of the role played by the referenced entity. By default, this is the field
 * name (minus its "Id" suffix if present).
 */
/**
 * @cfg {String} [getterName] The name of the getter function. Typically this is generated
 * based on the {@link #role}.
 */
/**
 * @cfg {String} [setterName] The name of the setter function (if required). Typically this is
 * generated based on the {@link #role}.
 */
/**
 * @cfg {Object} [reader] A custom {@link Ext.data.reader.Reader} configuration for reading
 * nested data.
 */
/**
 * @cfg {Boolean} [unique] `true` True for this reference to create a `Ext.data.schema.OneToOne`
 * relationship. If not specified, the default is `Ext.data.schema.ManyToOne`.
 */
/**
 * @cfg {String/Object} inverse
 * The name of the inverse role (of this entity with respect to the `reference`
 * entity). By default, this is the {@link Ext.util.Inflector#pluralize pluralized}
 * name of this entity, unless this `reference` is `unique`, in which case the default
 * name is the {@link Ext.util.Inflector#singularize singularized} name of this entity.
 *
 * This config may also be an object containing a role, getter, or setter.
 */

/**
 * The base class for calculating data summaries. The summary is calculated using the 
 * {@link #method!calculate} method. This is overridden in subclasses.
 *
 * @since 6.5.0
 */
Ext.define('Ext.data.summary.Base', {
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    alias: 'data.summary.base',
    // also configures Factoryable
    isAggregator: true,
    factoryConfig: {
        defaultType: 'base',
        cacheable: true
    },
    constructor: function(config) {
        var calculate = config && config.calculate;
        if (calculate) {
            config = Ext.apply({}, config);
            delete config.calculate;
            this.calculate = calculate;
        }
        this.initConfig(config);
    },
    /**
     * This method calculates the summary value of the given records.
     * @param {Ext.data.Model[]/Object[]} records The records to aggregate.
     * @param {String} property The property to aggregate on.
     * @param {String} root The root to extra the data from.
     * @param {Number} begin The starting index to calculate from.
     * @param {Number} end The index at which to stop calculating. The item at this
     * index will *not* be included in the calculation.
     *
     * @return {Object} The calculated summary value.
     * @method calculate
     */
    /**
     * Extract the underlying value from the data object.
     * @param {Ext.data.Model} record The record.
     * @param {String} property The property to extract.
     * @param {String} root The root on the data object.
     * @return {Object} The value.
     *
     * @protected
     */
    extractValue: function(record, property, root) {
        var ret;
        if (record) {
            if (root) {
                record = record[root];
            }
            ret = record[property];
        }
        return ret;
    }
}, function() {
    Ext.Factory.on('dataSummary', function(factory, config) {
        if (typeof config === 'function') {
            return factory({
                calculate: config
            });
        }
    });
});
// by not returning anything, the normal factory logic is applied

/**
 * Calculates the sum for a set of data.
 * @since 6.5.0
 */
Ext.define('Ext.data.summary.Sum', {
    extend: 'Ext.data.summary.Base',
    alias: 'data.summary.sum',
    calculate: function(records, property, root, begin, end) {
        var n = end - begin,
            i, sum, v;
        for (i = 0; i < n; ++i) {
            v = this.extractValue(records[begin + i], property, root);
            sum = i ? sum + v : v;
        }
        return sum;
    }
});

/**
 * Calculates the average for a set of data.
 * @since 6.5.0
 */
Ext.define('Ext.data.summary.Average', {
    extend: 'Ext.data.summary.Sum',
    alias: 'data.summary.average',
    calculate: function(records, property, root, begin, end) {
        var len = end - begin,
            value;
        if (len > 0) {
            value = this.callParent([
                records,
                property,
                root,
                begin,
                end
            ]) / len;
        }
        return value;
    }
});

/**
 * Calculates the count for a set of data.
 * @since 6.5.0
 */
Ext.define('Ext.data.summary.Count', {
    extend: 'Ext.data.summary.Base',
    alias: 'data.summary.count',
    calculate: function(records, property, root, begin, end) {
        return end - begin;
    }
});

/**
 * Calculates the maximum for a set of data.
 * @since 6.5.0
 */
Ext.define('Ext.data.summary.Max', {
    extend: 'Ext.data.summary.Base',
    alias: 'data.summary.max',
    calculate: function(records, property, root, begin, end) {
        var max = this.extractValue(records[begin], property, root),
            i, v;
        for (i = begin; i < end; ++i) {
            v = this.extractValue(records[i], property, root);
            if (v > max) {
                max = v;
            }
        }
        return max;
    }
});

/**
 * Calculates the minimum for a set of data.
 * @since 6.5.0
 */
Ext.define('Ext.data.summary.Min', {
    extend: 'Ext.data.summary.Base',
    alias: 'data.summary.min',
    calculate: function(records, property, root, begin, end) {
        var min = this.extractValue(records[begin], property, root),
            i, v;
        for (i = begin; i < end; ++i) {
            v = this.extractValue(records[i], property, root);
            if (v < min) {
                min = v;
            }
        }
        return min;
    }
});

/**
 * Produces no value for a summary. This would be used for fields like `id` or other
 * value that does not aggregate.
 * @since 7.0
 */
Ext.define('Ext.data.summary.None', {
    extend: 'Ext.data.summary.Base',
    alias: 'data.summary.none',
    config: {
        /**
         * @cfg {Mixed} value
         * The value to report for the summary.
         */
        value: null
    },
    calculate: function() {
        return this.getValue();
    }
});

/**
 * Base class for date type validators.
 *
 * @abstract
 */
Ext.define('Ext.data.validator.AbstractDate', {
    extend: 'Ext.data.validator.Validator',
    config: {
        /**
         * @cfg {String} message
         * The error message to return when not valid.
         * @locale
         */
        message: null,
        /**
         * @cfg {String/String[]} format
         * The format(s) to allow. See {@link Ext.Date}.
         * @locale
         */
        format: ''
    },
    applyFormat: function(format) {
        if (!format) {
            format = this.getDefaultFormat();
        }
        if (!Ext.isArray(format)) {
            format = [
                format
            ];
        }
        return format;
    },
    parse: function(value) {
        if (Ext.isDate(value)) {
            return value;
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            format = me.getFormat(),
            len = format.length,
            ret = null,
            i;
        for (i = 0; i < len && !ret; ++i) {
            ret = Ext.Date.parse(value, format[i], true);
        }
        return ret;
    },
    validate: function(value) {
        return this.parse(value) ? true : this.getMessage();
    }
});

/**
 * @abstract
 * A superclass for a validator that checks if a value is within a certain range.
 */
Ext.define('Ext.data.validator.Bound', {
    extend: 'Ext.data.validator.Validator',
    alias: 'data.validator.bound',
    type: 'bound',
    config: {
        /**
         * @cfg {Number} min
         * The minimum length value.
         */
        min: undefined,
        /**
         * @cfg {Number} max
         * The maximum length value.
         */
        max: undefined,
        /**
         * @cfg {String} emptyMessage
         * The error message to return when the value is empty.
         * @locale
         */
        emptyMessage: 'Must be present',
        /**
         * @cfg {String} minOnlyMessage
         * The error message to return when the value is less than the minimum
         * and only a minimum is specified.
         * @locale
         */
        minOnlyMessage: 'Value must be greater than {0}',
        /**
         * @cfg {String} maxOnlyMessage
         * The error message to return when the value is more than the maximum
         * and only a maximum is specified.
         * @locale
         */
        maxOnlyMessage: 'Value must be less than {0}',
        /**
         * @cfg {String} bothMessage
         * The error message to return when the value is not in the specified range
         * and both the minimum and maximum are specified.
         * @locale
         */
        bothMessage: 'Value must be between {0} and {1}'
    },
    resetMessages: function() {
        this._bothMsg = this._minMsg = this._maxMsg = null;
    },
    updateMin: function() {
        this.resetMessages();
    },
    updateMax: function() {
        this.resetMessages();
    },
    updateMinOnlyMessage: function() {
        this.resetMessages();
    },
    updateMaxOnlyMessage: function() {
        this.resetMessages();
    },
    updateBothMessage: function() {
        this.resetMessages();
    },
    validate: function(value) {
        var me = this,
            min = me.getMin(),
            max = me.getMax(),
            hasMin = (min != null),
            hasMax = (max != null),
            msg = this.validateValue(value);
        if (msg !== true) {
            return msg;
        }
        value = me.getValue(value);
        if (hasMin && hasMax) {
            if (value < min || value > max) {
                msg = me._bothMsg || (me._bothMsg = Ext.String.format(me.getBothMessage(), min, max));
            }
        } else if (hasMin) {
            if (value < min) {
                msg = me._minMsg || (me._minMsg = Ext.String.format(me.getMinOnlyMessage(), min));
            }
        } else if (hasMax) {
            if (value > max) {
                msg = me._maxMsg || (me._maxMsg = Ext.String.format(me.getMaxOnlyMessage(), max));
            }
        }
        return msg;
    },
    validateValue: function(value) {
        if (value === undefined || value === null) {
            return this.getEmptyMessage();
        }
        return true;
    },
    getValue: Ext.identityFn
});

/**
 * Validates that the passed value matches a specific format specified by a regex.
 * The format is provided by the {@link #matcher} config.
 */
Ext.define('Ext.data.validator.Format', {
    extend: 'Ext.data.validator.Validator',
    alias: 'data.validator.format',
    type: 'format',
    config: {
        /**
         * @cfg {String} message
         * The error message to return when the value does not match the format.
         * @locale
         */
        message: 'Is in the wrong format',
        /**
         * @cfg {RegExp} matcher (required) The matcher regex to test against the value.
         */
        matcher: undefined
    },
    constructor: function() {
        this.callParent(arguments);
        if (!this.getMatcher()) {
            Ext.raise('validator.Format must be configured with a matcher');
        }
    },
    validate: function(value) {
        var matcher = this.getMatcher(),
            result = matcher && matcher.test(value);
        return result ? result : this.getMessage();
    }
});

/**
 * Validates that the value is a valid CIDR block.
 *
 * Works for both IPV4 only.
 */
Ext.define('Ext.data.validator.CIDRv4', {
    extend: 'Ext.data.validator.Format',
    alias: 'data.validator.cidrv4',
    type: 'cidrv4',
    // https://github.com/flipjs/cidr-regex
    /**
     * @cfg {String} message
     * The error message to return when the value is not a valid CIDR block.
     * @locale
     */
    message: 'Is not a valid CIDR block',
    // http://www.regexpal.com/93987
    /**
     * @cfg {RegExp} matcher
     * A matcher to check for valid CIDR block. This may be overridden.
     */
    matcher: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/
});

/**
 * Validates that the value is a valid CIDR block.
 *
 * Works for both IPV6 only.
 */
Ext.define('Ext.data.validator.CIDRv6', {
    extend: 'Ext.data.validator.Format',
    alias: 'data.validator.cidrv6',
    type: 'cidrv6',
    // https://github.com/flipjs/cidr-regex
    /**
     * @cfg {String} message
     * The error message to return when the value is not a valid CIDR block.
     * @locale
     */
    message: 'Is not a valid CIDR block',
    // http://www.regexpal.com/93987
    /**
     * @cfg {RegExp} matcher
     * A matcher to check for valid CIDR block. This may be overridden.
     */
    matcher: /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/
});

/**
 * Validates that the value is a valid number.
 *
 * A valid number may include a leading + or -, comma separators, and a single decimal point.
 */
Ext.define('Ext.data.validator.Number', {
    extend: 'Ext.data.validator.Validator',
    alias: 'data.validator.number',
    type: 'number',
    config: {
        /**
         * @cfg {String} decimalSeparator
         * The decimal separator. Defaults to {@link Ext.util.Format#decimalSeparator}.
         */
        decimalSeparator: undefined,
        /**
         * @cfg {String} message
         * The error message to return when the value is not a valid number.
         * @locale
         */
        message: 'Is not a valid number',
        /**
         * @cfg {String} thousandSeparator
         * The thousand separator. Defaults to {@link Ext.util.Format#thousandSeparator}.
         */
        thousandSeparator: undefined
    },
    constructor: function(config) {
        this.callParent([
            config
        ]);
        this.rebuildMatcher();
    },
    applyDecimalSeparator: function(v) {
        return v === undefined ? Ext.util.Format.decimalSeparator : v;
    },
    updateDecimalSeparator: function() {
        this.rebuildMatcher();
    },
    applyThousandSeparator: function(v) {
        return v === undefined ? Ext.util.Format.thousandSeparator : v;
    },
    updateThousandSeparator: function() {
        this.rebuildMatcher();
    },
    parse: function(v) {
        var sep = this.getDecimalSeparator(),
            N = Ext.Number;
        if (typeof v === 'string') {
            if (!this.matcher.test(v)) {
                return null;
            }
            v = this.parseValue(v);
        }
        return sep ? N.parseFloat(v) : N.parseInt(v);
    },
    validate: function(value) {
        return this.parse(value) === null ? this.getMessage() : true;
    },
    privates: {
        getMatcherText: function(preventSign) {
            var t = this.getThousandSeparator(),
                d = this.getDecimalSeparator(),
                s = '(?:';
            if (t) {
                t = Ext.String.escapeRegex(t);
                s += '(?:\\d{1,3}(' + t + '\\d{3})*)|';
            }
            s += '\\d*)';
            if (d) {
                d = Ext.String.escapeRegex(d);
                s += '(?:' + d + '\\d*)?';
            }
            if (!preventSign) {
                s = this.getSignPart() + s;
            }
            return s;
        },
        getSignPart: function() {
            return '(\\+|\\-)?';
        },
        parseValue: function(v) {
            var thousandMatcher = this.thousandMatcher,
                decimal;
            if (thousandMatcher) {
                v = v.replace(thousandMatcher, '');
            }
            decimal = this.getDecimalSeparator();
            if (decimal && decimal !== '.') {
                v = v.replace(decimal, '.');
            }
            return v;
        },
        rebuildMatcher: function() {
            var me = this,
                sep;
            if (!me.isConfiguring) {
                sep = me.getThousandSeparator();
                me.matcher = new RegExp('^' + me.getMatcherText() + '$');
                if (sep) {
                    me.thousandMatcher = sep ? new RegExp(Ext.String.escapeRegex(sep), 'g') : null;
                }
            }
        }
    }
});

/**
 * Validates that the value is a valid U.S. currency value.
 *
 */
Ext.define('Ext.data.validator.Currency', {
    extend: 'Ext.data.validator.Number',
    alias: 'data.validator.currency',
    type: 'currency',
    config: {
        /**
         * @cfg {Boolean} symbolAtEnd
         * `true` to show the currency symbol after the number.
         * Defaults to {Ext.util.Format#currencyAtEnd}.
         */
        symbolAtEnd: undefined,
        /**
         * @cfg {String} spacer
         * The spacer to show between the number and the currency symbol.
         * Defaults to {Ext.util.Format#currencySpacer}.
         */
        spacer: undefined,
        /**
         * @cfg {String} symbol
         * The symbol used to denote currency.
         * Defaults to {Ext.util.Format#currencySign}.
         */
        symbol: undefined
    },
    /**
     * @cfg {String} message
     * The error message to return when the value is not a valid currency amount.
     * @locale
     */
    message: 'Is not a valid currency amount',
    applySymbolAtEnd: function(value) {
        return value === undefined ? Ext.util.Format.currencyAtEnd : value;
    },
    updateSymbolAtEnd: function() {
        this.rebuildMatcher();
    },
    applySpacer: function(value) {
        return value === undefined ? Ext.util.Format.currencySpacer : value;
    },
    updateSpacer: function() {
        this.rebuildMatcher();
    },
    applySymbol: function(value) {
        return value === undefined ? Ext.util.Format.currencySign : value;
    },
    updateSymbol: function() {
        this.rebuildMatcher();
    },
    privates: {
        getMatcherText: function() {
            var me = this,
                ret = me.callParent([
                    true
                ]),
                symbolPart = me.getSymbolMatcher();
            if (me.getSymbolAtEnd()) {
                ret += symbolPart;
            } else {
                ret = symbolPart + ret;
            }
            return me.getSignPart() + ret;
        },
        getSymbolMatcher: function() {
            var symbol = Ext.String.escapeRegex(this.getSymbol()),
                spacer = Ext.String.escapeRegex(this.getSpacer() || ''),
                s = this.getSymbolAtEnd() ? (spacer + symbol) : (symbol + spacer);
            return '(?:' + s + ')?';
        },
        parseValue: function(v) {
            // If we're at the front, replace -/+$1 with -/+1
            v = v.replace(this.currencyMatcher, this.atEnd ? '' : '$1');
            return this.callParent([
                v
            ]);
        },
        rebuildMatcher: function() {
            var me = this,
                symbolPart, atEnd, sign;
            me.callParent();
            if (!me.isConfiguring) {
                atEnd = me.getSymbolAtEnd();
                symbolPart = me.getSymbolMatcher();
                sign = me.getSignPart();
                me.atEnd = atEnd;
                me.currencyMatcher = new RegExp(atEnd ? (symbolPart + '$') : ('^' + sign + symbolPart));
            }
        }
    }
});
// eslint-disable-line max-len

/**
 * Validates that the value is a valid U.S. currency value.
 *
 */
Ext.define('Ext.data.validator.CurrencyUS', {
    extend: 'Ext.data.validator.Currency',
    alias: 'data.validator.currency-us',
    type: 'currency-us',
    thousandSeparator: ',',
    decimalSeparator: '.',
    symbol: '$',
    spacer: '',
    symbolAtEnd: false
});

/**
 * Validates that the value is a valid date.
 */
Ext.define('Ext.data.validator.Date', {
    extend: 'Ext.data.validator.AbstractDate',
    alias: 'data.validator.date',
    type: 'date',
    isDateValidator: true,
    /**
     * @cfg {String} message
     * The error message to return when the value is not a valid date.
     * @locale
     */
    message: 'Is not a valid date',
    /**
     * @cfg {String/String[]} format
     * The format(s) to allow. See {@link Ext.Date}. Defaults to {@link Ext.Date#defaultFormat}
     * @locale
     */
    privates: {
        getDefaultFormat: function() {
            return [
                Ext.Date.defaultFormat,
                'm/d/Y',
                'n/j/Y',
                'n/j/y',
                'm/j/y',
                'n/d/y',
                'm/j/Y',
                'n/d/Y',
                'm-d-y',
                'n-d-y',
                'm-d-Y',
                'mdy',
                'mdY',
                'Y-m-d'
            ];
        }
    }
});

/**
 * Validates that the value is a valid date with time.
 */
Ext.define('Ext.data.validator.DateTime', {
    extend: 'Ext.data.validator.AbstractDate',
    alias: 'data.validator.datetime',
    type: 'datetime',
    isDateTimeValidator: true,
    /**
     * @cfg {String} message
     * The error message to return when the value is not a valid time.
     * @locale
     */
    message: 'Is not a valid date and time',
    /**
     * @cfg {String/String[]} format
     * The format(s) to allow. See {@link Ext.Date}. Defaults to  the concatenation of
     * the {@link Ext.Date#defaultFormat} and the {@link Ext.Date#defaultTimeFormat}.
     * @locale
     */
    privates: {
        getDefaultFormat: function() {
            var D = Ext.Date;
            return D.defaultFormat + ' ' + D.defaultTimeFormat;
        }
    }
});

/**
 * Validates that the value is a valid email.
 */
Ext.define('Ext.data.validator.Email', {
    extend: 'Ext.data.validator.Format',
    alias: 'data.validator.email',
    type: 'email',
    /**
     * @cfg {String} message
     * The error message to return when the value is not a valid email
     * @locale
     */
    message: 'Is not a valid email address',
    // http://en.wikipedia.org/wiki/Email_address#Local_part
    // http://stackoverflow.com/a/2049510
    // http://isemail.info/
    // http://blog.stevenlevithan.com/archives/capturing-vs-non-capturing-groups
    //
    // 1. Can begin with a double-quote ONLY IF the local part also ends in a double-quote.
    // 2. Can NOT BEGIN with a period.
    // 3. Can NOT END with a period.
    // 4. Can not have MORE THAN ONE period in a row.
    //
    // Let's break this down:
    //
    // ^(")?
    // The local part may begin with double-quotes, but only if it also ends with it.
    // See the back-reference.  Capturing.
    //
    // (?:[^\."])
    // Here we've defined that the local part cannot begin with a period or a double-quote.
    // Non-capturing.
    //
    // (?:(?:[\.])?(?:[\w\-!#$%&'*+/=?^_`{|}~]))*
    // After the first character is matched, the regex ensures that there is not more than one
    // period in a row.  Then, this nested grouping allows for zero or more of the accepted
    // characters. NOTE that this also ensures that any character not defined in the character class
    // is invalid as an ending character for the local part (such as the period).
    //
    // \1@
    // The local part of the address is a backreference to the first (and only) capturing group
    // that allows
    // for a double-quote to wrap the local part of an email address.
    /**
     * @cfg {RegExp} matcher
     * A matcher to check for simple emails. This may be overridden.
     */
    /* eslint-disable-next-line no-useless-escape */
    matcher: /^(")?(?:[^\."])(?:(?:[\.])?(?:[\w\-!#$%&'*+\/=?\^_`{|}~]))*\1@(\w[\-\w]*\.){1,5}([A-Za-z]){2,6}$/
});

/**
 * A superclass for inclusion/exclusion validators.
 * @abstract
 */
Ext.define('Ext.data.validator.List', {
    extend: 'Ext.data.validator.Validator',
    alias: 'data.validator.list',
    type: 'list',
    config: {
        /**
         * @cfg {Array} list (required)
         * The list to check the passed value against.
         */
        list: null,
        message: null
    },
    inclusion: null,
    validate: function(value) {
        var contains = Ext.Array.contains(this.getList(), value),
            inclusion = this.inclusion,
            exclusion = !inclusion,
            result;
        result = (inclusion && contains) || (exclusion && !contains);
        return result || this.getMessage();
    }
});

/**
 * Validates that the value does not exist in a {@link #list} of values.
 */
Ext.define('Ext.data.validator.Exclusion', {
    extend: 'Ext.data.validator.List',
    alias: 'data.validator.exclusion',
    type: 'exclusion',
    /**
     * @cfg {String} message
     * The error message to return when the passed value exists in the
     * specified {@link #list}.
     * @locale
     */
    message: 'Is a value that has been excluded',
    constructor: function() {
        this.callParent(arguments);
        if (!this.getList()) {
            Ext.raise('validator.Exclusion requires a list');
        }
    },
    inclusion: false
});

/**
 * Validates that the value is a valid IP address.
 *
 * Works for both IPV4 and IPV6.
 */
Ext.define('Ext.data.validator.IPAddress', {
    extend: 'Ext.data.validator.Format',
    alias: 'data.validator.ipaddress',
    type: 'ipaddress',
    /**
     * @cfg {String} message
     * The error message to return when the value is not a valid IP address
     * @locale
     */
    message: 'Is not a valid IP address',
    // http://stackoverflow.com/questions/23483855/javascript-regex-to-validate-ipv4-and-ipv6-address-no-hostnames
    /**
     * @cfg {RegExp} matcher
     * A matcher to check for valid IP address. This may be overridden.
     */
    /* eslint-disable max-len */
    matcher: new RegExp('^(' + // ipv4
    '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)' + '|' + // ipv6
    '((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])))' + ')$')
});
/* eslint-enable max-len */

/**
 * Validates that the value exists in a {@link #list} of values.
 */
Ext.define('Ext.data.validator.Inclusion', {
    extend: 'Ext.data.validator.List',
    alias: 'data.validator.inclusion',
    type: 'inclusion',
    /**
     * @cfg {String} message
     * The error message to return when the passed value does not exist
     * in the specified {@link #list}.
     * @locale
     */
    message: 'Is not in the list of acceptable values',
    constructor: function() {
        this.callParent(arguments);
        if (!this.getList()) {
            Ext.raise('validator.Inclusion requires a list');
        }
    },
    inclusion: true
});

/**
 * Validates that the length of the value is between a {@link #min} and {@link #max}.
 */
Ext.define('Ext.data.validator.Length', {
    extend: 'Ext.data.validator.Bound',
    alias: 'data.validator.length',
    type: 'length',
    /**
     * @cfg {Number} min
     * The minimum length value.
     */
    /**
     * @cfg {Number} max
     * The maximum length value.
     */
    /**
     * @cfg {String} minOnlyMessage
     * The error message to return when the value is less than the minimum
     * length and only a minimum is specified.
     * @locale
     */
    minOnlyMessage: 'Length must be at least {0}',
    /**
     * @cfg {String} maxOnlyMessage
     * The error message to return when the value is more than the maximum
     * length and only a maximum is specified.
     * @locale
     */
    maxOnlyMessage: 'Length must be no more than {0}',
    /**
     * @cfg {String} bothMessage
     * The error message to return when the value length is not in the specified
     * range and both the minimum and maximum are specified.
     * @locale
     */
    bothMessage: 'Length must be between {0} and {1}',
    getValue: function(v) {
        return String(v).length;
    }
});

/**
 * Validates that the passed value is not `null` or `undefined` or `''`.
 * An empty string may be allowed with {@link #allowEmpty}.
 */
Ext.define('Ext.data.validator.Presence', {
    extend: 'Ext.data.validator.Validator',
    alias: 'data.validator.presence',
    type: 'presence',
    isPresence: true,
    config: {
        /**
         * @cfg {String} message
         * The error message to return when the value is not specified.
         * @locale
         */
        message: 'Must be present',
        /**
         * @cfg {Boolean} allowEmpty
         * `true` to allow `''` as a valid value.
         */
        allowEmpty: false
    },
    validate: function(value) {
        var valid = !(value === undefined || value === null);
        if (valid && !this.getAllowEmpty()) {
            valid = value !== '';
        }
        return valid ? true : this.getMessage();
    }
});

/**
 * Validates that the passed value is not `null` or `undefined`.
 * @since 6.5.0
 */
Ext.define('Ext.data.validator.NotNull', {
    extend: 'Ext.data.validator.Presence',
    alias: 'data.validator.notnull',
    type: 'notnull',
    allowEmpty: true
});

/**
 * Validates that the value is a valid phone.
 */
Ext.define('Ext.data.validator.Phone', {
    extend: 'Ext.data.validator.Format',
    alias: 'data.validator.phone',
    type: 'phone',
    // https://www.sitepoint.com/community/t/phone-number-regular-expression-validation/2204/2
    //      (modified)
    /**
     * @cfg {String} message
     * The error message to return when the value is not a valid phone.
     * @locale
     */
    message: 'Is not a valid phone number',
    /**
     * @cfg {RegExp} matcher
     * A matcher to check for simple phones. This may be overridden.
     */
    matcher: new RegExp('^ *' + // optional country code
    '(?:' + '\\+?' + // maybe + prefix
    '(\\d{1,3})' + // optional separator
    '[- .]?' + ')?' + // optional area code
    '(?:' + '(?:' + '(\\d{3})' + // without ()
    '|' + '\\((\\d{3})\\)' + // with ()
    ')?' + // optional separator
    '[- .]?' + ')' + // CO code (3 digit prefix)
    '(?:' + '([2-9]\\d{2})' + // optional separator
    '[- .]?' + ')' + // line number (4 digits)
    '(\\d{4})' + // optional extension
    '(?: *(?:e?xt?) *(\\d*))?' + ' *$')
});

/**
 * Validates that the the value is between a {@link #min} and {@link #max}.
 */
Ext.define('Ext.data.validator.Range', {
    extend: 'Ext.data.validator.Bound',
    alias: 'data.validator.range',
    type: 'range',
    /**
     * @cfg {Number} min
     * The minimum value.
     */
    /**
     * @cfg {Number} max
     * The maximum value.
     */
    /**
     * @cfg minOnlyMessage
     * @inheritdoc
     * @locale
     */
    minOnlyMessage: 'Must be at least {0}',
    /**
     * @cfg maxOnlyMessage
     * @inheritdoc
     * @locale
     */
    maxOnlyMessage: 'Must be no more than than {0}',
    /**
     * @cfg bothMessage
     * @inheritdoc
     * @locale
     */
    bothMessage: 'Must be between {0} and {1}',
    config: {
        /**
         * @cfg {String} nanMessage
         * The error message to return when the value is not numeric.
         * @locale
         */
        nanMessage: 'Must be numeric'
    },
    validateValue: function(value) {
        var msg = this.callParent([
                value
            ]);
        if (msg === true && isNaN(value)) {
            msg = this.getNanMessage();
        }
        return msg;
    }
});

/**
 * Validates that the value is a valid time.
 */
Ext.define('Ext.data.validator.Time', {
    extend: 'Ext.data.validator.AbstractDate',
    alias: 'data.validator.time',
    type: 'time',
    isTimeValidator: true,
    /**
     * @cfg {String} message
     * The error message to return when the value is not a valid time.
     * @locale
     */
    message: 'Is not a valid time',
    /**
     * @cfg {String/String[]} format
     * The format(s) to allow. See {@link Ext.Date}. Defaults to {@link Ext.Date#defaultTimeFormat}
     * @locale
     */
    privates: {
        getDefaultFormat: function() {
            return Ext.Date.defaultTimeFormat;
        }
    }
});

/**
 * Validates that the value is a proper URL.
 */
Ext.define('Ext.data.validator.Url', {
    extend: 'Ext.data.validator.Format',
    alias: 'data.validator.url',
    type: 'url',
    /**
     * @cfg {String} message
     * The error message to return when the value is not a valid URL.
     * @locale
     */
    message: 'Is not a valid URL',
    // URL validator that works is non-trivial
    // There are numerous examples online but not all pass rigoruous test:
    //      https://mathiasbynens.be/demo/url-regex
    // The only one that looks to be comprehensive and bulletproof is this one:
    //      https://gist.github.com/dperini/729294
    //      which requires inclusion of a copyright header
    /**
     * @cfg {RegExp} matcher
     * A matcher to check for simple Urls. This may be overridden.
     */
    /* eslint-disable-next-line no-useless-escape */
    matcher: /^(http:\/\/|https:\/\/|ftp:\/\/|\/\/)([-a-zA-Z0-9@:%_\+.~#?&//=])+$/
});

/**
 * This class contains remote group information about virtual stores and provides
 * a similar interface to `Ext.data.Group`.
 * @since 6.5.0
 */
Ext.define('Ext.data.virtual.Group', {
    isVirtualGroup: true,
    firstRecords: null,
    id: '',
    summaryRecord: null,
    constructor: function(key) {
        this.id = key;
        this.firstRecords = [];
    },
    first: function() {
        return this.firstRecords[0] || null;
    },
    getGroupKey: function() {
        return this.id;
    },
    getSummaryRecord: function() {
        return this.summaryRecord;
    }
});

/**
 * This class manages a page of records in a virtual store's `PageMap`. It is created
 * with the page `number` (0-based) and uses the store's `pageSize` to calculate the
 * record span covered by it and stores these as `begin` and `end` properties. These
 * aspects of the `Page` as well as the owning `PageMap` are expected to be immutable
 * throughout the instance's life-cycle.
 *
 * The normal use for a `Page` is by a `Range`. Ranges acquire and `lock` the pages they
 * span. As they move on, they `release` these locks. The act of locking pages schedules
 * them for loading. Unlocking pages allows them to be evicted from the `PageMap` to
 * reclaim memory for other pages.
 *
 * @private
 * @since 6.5.0
 */
Ext.define('Ext.data.virtual.Page', {
    isVirtualPage: true,
    /**
     * @property {Number} begin
     * The start index of the records that this page represents.
     * Inclusive.
     * @readonly
     */
    begin: 0,
    /**
     * @property {Number} end
     * The end index of the records that this page represents.
     * Exclusive.
     * @readonly
     */
    end: 0,
    /**
     * @property {Error} error
     * The error instance if the page load `operation` failed. If this property is set,
     * the `state` will be "error".
     * @readonly
     */
    error: null,
    /**
     * @property {"active"/"prefetch"} locked
     * This property is managed by the `lock` and `release` methods. It is set to `null`
     * if the page is not locked or it will be set to the string describing the type of
     * the current lock.
     *
     * When pages are `locked` for the first time, they are scheduled for loading by the
     * owning `pageMap`.
     *
     * Locked pages are not eligible for removal from the `PageMap`.
     * @readonly
     */
    locked: null,
    /**
     * @cfg {Number} number
     * The 0-based page number of this page.
     * @readonly
     */
    number: 0,
    /**
     * @property {Ext.data.operation.Read} operation
     * The pending read of the records for this page. This property is only set when the
     * page is in the "loading" `state`.
     * @readonly
     */
    operation: null,
    /**
     * @property {Ext.data.virtual.PageMap} pageMap
     * The owning `PageMap` instance.
     * @readonly
     */
    pageMap: null,
    /**
     * @property {Ext.data.Model[]} records
     * The array of records loaded for this page. The `records[0]` item corresponds to
     * the record at index `begin` in the overall result set.
     * @readonly
     */
    records: null,
    /**
     * @property {"loading"/"loaded"/"error"} state
     * This property describes the current life-cycle state for this page. At creation,
     * this property will be `null` for the "initial" state.
     * @readonly
     */
    state: null,
    constructor: function(config) {
        var me = this,
            pageSize;
        Ext.apply(me, config);
        pageSize = me.pageMap.store.getPageSize();
        me.begin = me.number * pageSize;
        me.end = me.begin + pageSize;
        me.locks = {
            active: 0,
            prefetch: 0
        };
    },
    destroy: function() {
        var me = this,
            operation = me.operation;
        me.state = 'destroyed';
        if (operation) {
            operation.abort();
        }
        me.callParent();
    },
    /**
     * Acquires or releases the specified type of lock to this page. If this causes the
     * `locked` property to transition to a new value, the `pageMap` is informed to
     * enqueue or dequeue this page from the loading queues.
     * @param {"active"/"prefetch"} kind The type of lock.
     * @param {Number} delta A value of `1` to lock or `-1` to release.
     */
    adjustLock: function(kind, delta) {
        var me = this,
            locks = me.locks,
            pageMap = me.pageMap,
            locked = null,
            lockedWas = me.locked;
        if (!(kind in locks)) {
            Ext.raise('Bad lock type (expected "active" or "prefetch"): "' + kind + '"');
        }
        if (delta !== 1 && delta !== -1) {
            Ext.raise('Invalid lock count delta (should be 1 or -1): ' + delta);
        }
        locks[kind] += delta;
        if (locks.active) {
            locked = 'active';
        } else if (locks.prefetch) {
            locked = 'prefetch';
        }
        if (locked !== lockedWas) {
            me.locked = locked;
            if (pageMap) {
                pageMap.onPageLockChange(me, locked, lockedWas);
            }
        }
    },
    clearRecords: function(out, by) {
        var me = this,
            begin = me.begin,
            records = me.records,
            i, n;
        // If we don't have records then fillRecords() could not have filled anything
        if (records) {
            n = records.length;
            if (by) {
                for (i = 0; i < n; ++i) {
                    delete out[records[i][by]];
                }
            } else {
                for (i = 0; i < n; ++i) {
                    delete out[begin + i];
                }
            }
        }
    },
    fillRecords: function(out, by, withIndex) {
        var me = this,
            records = me.records,
            begin = me.begin,
            store = me.pageMap.store,
            i, n, record;
        if (records) {
            // The last page will not likely have a full page worth, so always
            // limit our loops by the actually available records...
            n = records.length;
            if (by) {
                for (i = 0; i < n; ++i) {
                    record = records[i];
                    record.join(store);
                    out[record[by]] = withIndex ? begin + i : record;
                }
            } else {
                for (i = 0; i < n; ++i) {
                    records[i].join(store);
                    out[begin + i] = records[i];
                }
            }
        }
    },
    isInitial: function() {
        return this.state === null;
    },
    isLoaded: function() {
        return this.state === 'loaded';
    },
    isLoading: function() {
        return this.state === 'loading';
    },
    load: function() {
        var me = this,
            operation;
        me.state = 'loading';
        operation = me.pageMap.store.loadVirtualPage(me, me.onLoad, me);
        // Memory proxy will synchronously load pages...
        if (me.state === 'loading') {
            me.operation = operation;
        }
    },
    privates: {
        onLoad: function(operation) {
            var me = this;
            me.operation = null;
            if (!me.destroyed) {
                if (!(me.error = operation.getError())) {
                    me.records = operation.getRecords();
                    me.state = 'loaded';
                } else {
                    me.state = 'error';
                }
                // TODO fireEvent or something from the store?
                me.pageMap.onPageLoad(me);
            }
        }
    }
});

/**
 * This class manages a sparse collection of `Page` objects keyed by their page number.
 * Pages are lazily created on request by the `getPage` method.
 *
 * When pages are locked, they are scheduled to be loaded. The loading is prioritized by
 * the type of lock held on the page. Pages with "active" locks are loaded first while
 * those with "prefetch" locks are loaded only when no "active" locked pages are in the
 * queue.
 *
 * The value of the `concurrentLoading` config controls the maximum number of simultaneously
 * pending, page load requests.
 *
 * @private
 * @since 6.5.0
 */
Ext.define('Ext.data.virtual.PageMap', {
    requires: [
        'Ext.data.virtual.Page'
    ],
    isVirtualPageMap: true,
    config: {
        /**
         * @cfg {Number} cacheSize
         * The number of pages to retain in the `cache`.
         */
        cacheSize: 10,
        /**
         * @cfg {Number} concurrentLoading
         * The maximum number of simultaneous load requests that should be made to the
         * server for pages.
         */
        concurrentLoading: 1,
        /**
         * The number of pages in the data set.
         */
        pageCount: null
    },
    generation: 0,
    store: null,
    constructor: function(config) {
        var me = this;
        me.prefetchSortFn = me.prefetchSortFn.bind(me);
        me.initConfig(config);
        me.clear();
    },
    destroy: function() {
        this.clear(true);
        this.callParent();
    },
    canSatisfy: function(range) {
        var end = this.getPageIndex(range.end),
            pageCount = this.getPageCount();
        return pageCount === null || end < pageCount;
    },
    clear: function(destroy) {
        var me = this,
            alive = !destroy || null,
            pages = me.pages,
            pg;
        ++me.generation;
        /**
         * @property {Object} byId
         * A map of records by their `idProperty`.
         */
        me.byId = alive && {};
        /**
         * @property {Object} byInternalId
         * A map of records by their `internalId`.
         */
        me.byInternalId = alive && {};
        /**
         * @property {Ext.data.virtual.Page[]} cache
         * The array of unlocked pages with the oldest at the front and the newest (most
         * recently unlocked) page at the end.
         * @readonly
         */
        me.cache = alive && [];
        /**
         * @property {Object} indexMap
         * A map of record indices by their `internalId`.
         */
        me.indexMap = alive && {};
        /**
         * @property {Object} pages
         * The sparse collection of `Page` objects keyed by their page number.
         * @readonly
         */
        me.pages = alive && {};
        /**
         * @property {Ext.data.virtual.Page[]} loading
         * The array of currently loading pages.
         */
        me.loading = alive && [];
        /**
         * @property {Object} loadQueues
         * A collection of loading queues keyed by the lock state.
         * @property {Ext.data.virtual.Page[]} loadQueues.active The queue of pages to
         * load that have an "active" lock state.
         * @property {Ext.data.virtual.Page[]} loadQueues.prefetch The queue of pages to
         * load that have a "prefetch" lock state.
         */
        me.loadQueues = alive && {
            active: [],
            prefetch: []
        };
        if (pages) {
            for (pg in pages) {
                me.destroyPage(pages[pg]);
            }
        }
    },
    getPage: function(number, autoCreate) {
        var me = this,
            pageCount = me.getPageCount(),
            pages = me.pages,
            page;
        if (pageCount === null || number < pageCount) {
            page = pages[number];
            if (!page && autoCreate !== false) {
                pages[number] = page = new Ext.data.virtual.Page({
                    pageMap: me,
                    number: number
                });
            }
        } else {
            Ext.raise('Invalid page number ' + number + ' when limit is ' + pageCount);
        }
        return page || null;
    },
    getPageIndex: function(index) {
        if (index.isEntity) {
            index = this.indexOf(index);
        }
        return Math.floor(index / this.store.getPageSize());
    },
    getPageOf: function(index, autoCreate) {
        var pageSize = this.store.getPageSize(),
            n = Math.floor(index / pageSize);
        return this.getPage(n, autoCreate);
    },
    getPages: function(begin, end) {
        var pageSize = this.store.getPageSize(),
            // Convert record indices into page numbers:
            first = Math.floor(begin / pageSize),
            last = Math.ceil(end / pageSize),
            ret = {},
            n;
        for (n = first; n < last; ++n) {
            ret[n] = this.getPage(n);
        }
        return ret;
    },
    flushNextLoad: function() {
        var me = this,
            queueTimer = me.queueTimer;
        if (queueTimer) {
            Ext.unasap(queueTimer);
        }
        me.loadNext();
    },
    indexOf: function(record) {
        var ret;
        // return indexMap if record is not null/undefined
        if (record) {
            ret = this.indexMap[record.internalId];
        }
        return (ret || ret === 0) ? ret : -1;
    },
    getByInternalId: function(internalId) {
        var index = this.indexMap[internalId],
            page;
        if (index || index === 0) {
            page = this.pages[Math.floor(index / this.store.getPageSize())];
            if (page) {
                return page.records[index - page.begin];
            }
        }
    },
    updatePageCount: function(pageCount, oldPageCount) {
        var pages = this.pages,
            pageNumber, page;
        if (oldPageCount === null || pageCount < oldPageCount) {
            // Safe to delete during a for in
            for (pageNumber in pages) {
                page = pages[pageNumber];
                if (page.number >= pageCount) {
                    this.clearPage(page);
                    this.destroyPage(page);
                }
            }
        }
    },
    privates: {
        queueTimer: null,
        clearPage: function(page, fromCache) {
            var me = this,
                A = Ext.Array,
                loadQueues = me.loadQueues;
            delete me.pages[page.number];
            page.clearRecords(me.byId, 'id');
            page.clearRecords(me.byInternalId, 'internalId');
            page.clearRecords(me.indexMap, 'internalId');
            A.remove(loadQueues.active, page);
            A.remove(loadQueues.prefetch, page);
            if (!fromCache) {
                Ext.Array.remove(me.cache, page);
            }
        },
        destroyPage: function(page) {
            this.store.onPageDestroy(page);
            page.destroy();
        },
        loadNext: function() {
            var me = this,
                concurrency = me.getConcurrentLoading(),
                loading = me.loading,
                loadQueues = me.loadQueues,
                page;
            me.queueTimer = null;
            // Keep pulling from the queue(s) as long as we have more concurrency
            // allowed...
            while (loading.length < concurrency) {
                if (!(page = loadQueues.active.shift() || loadQueues.prefetch.shift())) {
                    break;
                }
                loading.push(page);
                page.load();
            }
        },
        onPageLoad: function(page) {
            var me = this,
                store = me.store,
                activeRanges = store.activeRanges,
                n = activeRanges.length,
                i;
            Ext.Array.remove(me.loading, page);
            if (!page.error) {
                page.fillRecords(me.byId, 'id');
                page.fillRecords(me.byInternalId, 'internalId');
                page.fillRecords(me.indexMap, 'internalId', true);
                store.onPageDataAcquired(page);
                for (i = 0; i < n; ++i) {
                    activeRanges[i].onPageLoad(page);
                }
            }
            me.flushNextLoad();
        },
        onPageLockChange: function(page, state, oldState) {
            var me = this,
                cache = me.cache,
                loadQueues = me.loadQueues,
                store = me.store,
                cacheSize, concurrency;
            // When a page that has never been loaded becomes locked, we want to put
            // it in the appropriate loadQueue. It is also possible for the lock state
            // to change while waiting in a loadQueue, so we may need to move it around
            // while it waits...
            if (page.isInitial()) {
                if (oldState) {
                    Ext.Array.remove(loadQueues[oldState], page);
                }
                if (state) {
                    loadQueues[state].push(page);
                    concurrency = me.getConcurrentLoading();
                    // Initiating loads immediately can easily cause problems, so wait
                    // for a tick before firing off the loads.
                    if (!me.queueTimer && me.loading.length < concurrency) {
                        me.queueTimer = Ext.asap(me.loadNext, me);
                    }
                }
            }
            if (state) {
                if (!oldState) {
                    // Make sure the page is not in the LRU queue for recycling. If it
                    // was previously not locked (!oldState) then the page is in line
                    // for removal...
                    Ext.Array.remove(cache, page);
                }
            } else {
                cache.push(page);
                // put MRU item at the end
                for (cacheSize = me.getCacheSize(); cache.length > cacheSize; ) {
                    page = cache.shift();
                    me.clearPage(page, true);
                    // remove LRU item
                    store.onPageEvicted(page);
                    me.destroyPage(page);
                }
            }
        },
        prefetchSortFn: function(a, b) {
            a = a.number;
            b = b.number;
            /* eslint-disable-next-line vars-on-top */
            var M = Math,
                firstPage = this.sortFirstPage,
                lastPage = this.sortLastPage,
                direction = this.sortDirection,
                aDir = a < firstPage,
                bDir = b < firstPage,
                ret;
            a = aDir ? M.abs(firstPage - a) : M.abs(lastPage - a);
            b = bDir ? M.abs(firstPage - b) : M.abs(lastPage - b);
            if (a === b) {
                ret = aDir ? direction : -direction;
            } else {
                ret = a - b;
            }
            return ret;
        },
        prioritizePrefetch: function(direction, firstPage, lastPage) {
            var me = this;
            me.sortDirection = direction;
            me.sortFirstPage = firstPage;
            me.sortLastPage = lastPage;
            me.loadQueues.prefetch.sort(me.prefetchSortFn);
        }
    }
});

/**
 *
 * @since 6.5.0
 */
Ext.define('Ext.data.virtual.Range', {
    extend: 'Ext.data.Range',
    isVirtualRange: true,
    /**
     * @cfg {String/Function} callback
     * The callback to call when new records in this range become available.
     */
    callback: null,
    /**
     * @cfg {Number} leadingBufferZone
     * The number of records to fetch beyond the active range in the direction of movement.
     * If the range is advancing forward, the additional records are beyond `end`. If
     * advancing backwards, they are before `begin`.
     */
    /**
     * @cfg {Boolean} prefetch
     * Specify `true` to enable prefetching for this range.
     */
    prefetch: false,
    /**
     * @cfg {Object} scope
     * The object that implements the supplied `callback` method.
     */
    scope: null,
    /**
     * @cfg {Number} trailingBufferZone
     * The number of records to fetch beyond the active trailing the direction of movement.
     * If the range is advancing forward, the additional records are before `begin`. If
     * advancing backwards, they are beyond `end`.
     */
    /**
     * @property {Number} direction
     * This property is set to `1` if the range was last moved forward and `-1` if it
     * was last moved backwards. This value is used to determine the "leading" and
     * "trailing" buffer zones.
     * @private
     */
    direction: 1,
    constructor: function(config) {
        this.adjustingPages = [];
        this.callParent([
            config
        ]);
    },
    reset: function() {
        var me = this;
        me.records = {};
        me.activePages = me.prefetchPages = null;
    },
    privates: {
        adjustPageLocks: function(kind, adjustment) {
            var me = this,
                pages = me.adjustingPages,
                n = pages.length,
                i;
            // Consider:
            //
            //                     -->
            //    --+----------+==========+--------------------------+----
            //  ... | trailing |  active  |          leading         |   ...
            //    --+----------+==========+--------------------------+----
            //
            //      :------:   :------:   :++++++:                   :++++++:
            //
            //    ---------+----------+==========+--------------------------+----
            //  ...        | trailing |  active  |          leading         |   ...
            //    ---------+----------+==========+--------------------------+----
            //
            // The newly released pages (esp the prefetch pages) should be released
            // such that the closest ones are MRU vs the farthest ones. Releasing
            // them in forward order will do that.
            //
            // New consider:
            //
            //                                             <--
            //    ---------+--------------------------+==========+----------+----
            //  ...        |          leading         |  active  | trailing |   ...
            //    ---------+--------------------------+==========+----------+----
            //
            //      :++++++:                   :++++++:   :------:   :------:
            //
            //    --+--------------------------+==========+----------+---------
            //  ... |          leading         |  active  | trailing |   ...
            //    --+--------------------------+==========+----------+---------
            //
            // When going backwards, we want to release the pages backwards (we'll
            // just sort them that way). That way the page with least index will be
            // released last and be MRU than the others.
            if (n > 1) {
                // Since pages are in objects keyed by page number, there is no
                // order during our set operations... so we sort the array now by
                // page number (ordered by our current direction).
                pages.sort(me.direction < 0 ? me.pageSortBackFn : me.pageSortFwdFn);
            }
            for (i = 0; i < n; ++i) {
                pages[i].adjustLock(kind, adjustment);
            }
            pages.length = 0;
        },
        doGoto: function() {
            var me = this,
                begin = me.begin,
                end = me.end,
                prefetch = me.prefetch,
                records = me.records,
                store = me.store,
                pageMap = store.pageMap,
                limit = store.totalCount,
                beginWas = me.lastBegin,
                endWas = me.lastEnd,
                activePagesWas = me.activePages,
                prefetchPagesWas = me.prefetchPages,
                beginBufferZone = me.trailingBufferZone,
                endBufferZone = me.leadingBufferZone,
                adjustingPages = me.adjustingPages,
                activePages, page, pg, direction, prefetchBegin, prefetchEnd, prefetchPages;
            // If store.totalCount is 0: no need to goto any page, just return from here
            if (limit === 0) {
                return;
            }
            adjustingPages.length = 0;
            // Forwards
            //
            // Most likely case:
            //
            //             beginWas          endWas
            //             |=================|
            //             :---:             :+++:
            //                 |=================|
            //                 begin             end
            //
            // Big step forwards:
            //
            //             beginWas          endWas
            //             |=================|
            //             :-----------------:     :+++++++++++++++++:
            //                                     |=================|
            //                                     begin             end
            //
            // Interesting case:
            //
            //             beginWas          endWas
            //             |=================|
            //             :---:         :---:
            //                 |=========|
            //                 begin     end
            // Backwards
            //
            // Most likely case:
            //
            //             beginWas          endWas
            //             |=================|
            //          :++:              :--:
            //          |=================|
            //          begin             end
            //
            // Big step back:
            //                                 beginWas          endWas
            //                                 |=================|
            //          :+++++++++++++++++:    :-----------------:
            //          |=================|
            //          begin             end
            //
            // Interesting case:
            //
            //             beginWas          endWas
            //             |=================|
            //          :++:                 :++:
            //          |=======================|
            //          begin                   end
            // Retain the direction if narrowing or widening the range
            if ((begin > beginWas && end < endWas) || (begin < beginWas && end > endWas)) {
                direction = me.direction;
            } else {
                direction = (begin < beginWas) ? -1 : ((begin > beginWas) ? 1 : me.direction);
            }
            if (direction < 0) {
                // if (backwards)
                pg = beginBufferZone;
                beginBufferZone = endBufferZone;
                endBufferZone = pg;
            }
            me.direction = direction;
            me.activePages = activePages = pageMap.getPages(begin, end);
            if (prefetch) {
                me.prefetchBegin = prefetchBegin = Math.max(0, begin - beginBufferZone);
                // If we don't know the size of the store yet, don't try and limit the pages
                if (limit === null) {
                    limit = Number.MAX_VALUE;
                }
                me.prefetchEnd = prefetchEnd = Math.min(limit, end + endBufferZone);
                me.prefetchPages = prefetchPages = pageMap.getPages(prefetchBegin, prefetchEnd);
            }
            // In set terms we want to do this:
            //
            //      A  = activePages
            //      Aw = activePagesWas
            //      P  = prefetchPages
            //      Pw = prefetchPagesWas
            //
            //      P -= A;    (activePages start out also in prefetchPages)
            //
            //      foreach page p in (A - Aw), p.lock('active') and p.fill(records)
            //
            //      foreach page p in (P - Pw), p.lock('prefetch')
            //
            //      foreach page p in (Aw - A), p.release('active') and p.clear(records)
            //
            //      foreach page p in (Pw - P), p.release('prefetch')
            //
            for (pg in activePages) {
                page = activePages[pg];
                // Any pages that we will be actively locking, we don't want to mark as
                // prefetch:
                if (prefetchPages) {
                    delete prefetchPages[pg];
                }
                if (activePagesWas && pg in activePagesWas) {
                    // We will unlock any activePages we no longer need so remove
                    // those we will be keeping:
                    delete activePagesWas[pg];
                } else {
                    // For pages that weren't previously active, lock them now.
                    page.adjustLock('active', 1);
                    page.fillRecords(records);
                }
            }
            if (prefetchPages) {
                for (pg in prefetchPages) {
                    if (prefetchPagesWas && pg in prefetchPagesWas) {
                        // If page was previously locked for prefetch, we don't want to
                        // release it...
                        delete prefetchPagesWas[pg];
                    } else {
                        prefetchPages[pg].adjustLock('prefetch', 1);
                    }
                }
            }
            // What's left in our "was" maps are those active or prefetch pages that we
            // previously had need of but no longer need them in that same way. Release
            // our previously prefetched pages first in case this is their final lock (we
            // want them to be retained but at a lower priority then previously active
            // pages).
            if (prefetchPagesWas) {
                for (pg in prefetchPagesWas) {
                    adjustingPages.push(prefetchPagesWas[pg]);
                }
                if (adjustingPages.length) {
                    me.adjustPageLocks('prefetch', -1);
                }
            }
            if (activePagesWas) {
                for (pg in activePagesWas) {
                    adjustingPages.push(page = activePagesWas[pg]);
                    page.clearRecords(records);
                }
                if (adjustingPages.length) {
                    me.adjustPageLocks('active', -1);
                }
            }
            if (prefetchPages) {
                pageMap.prioritizePrefetch(direction, pageMap.getPageIndex(begin), pageMap.getPageIndex(end - 1));
            }
            me.lastBegin = begin;
            me.lastEnd = end;
        },
        onPageDestroy: function(page) {
            var n = page.number,
                activePages = this.activePages,
                prefetchPages = this.prefetchPages;
            if (activePages) {
                delete activePages[n];
            }
            if (prefetchPages) {
                delete prefetchPages[n];
            }
        },
        onPageLoad: function(page) {
            var me = this,
                callback = me.callback,
                first, last;
            if (me.activePages[page.number]) {
                page.fillRecords(me.records);
                if (callback) {
                    // Clip the range to our actually active range for the sake of
                    // the user:
                    first = Math.max(me.begin, page.begin);
                    last = Math.min(me.end, page.end);
                    Ext.callback(callback, me.scope, [
                        me,
                        first,
                        last
                    ]);
                }
            }
        },
        pageSortBackFn: function(page1, page2) {
            return page2.number - page1.number;
        },
        pageSortFwdFn: function(page1, page2) {
            return page1.number - page2.number;
        },
        refresh: function() {
            // ... we don't want to reset this.records
            this.records = this.records || {};
        },
        reload: function() {
            var me = this,
                begin = me.begin,
                end = me.end;
            me.begin = me.end = 0;
            me.direction = 1;
            me.prefetchPages = me.activePages = null;
            /* eslint-disable-next-line dot-notation */
            me.goto(begin, end);
        }
    }
});

/**
 * This type of store is a replacement for BufferedStore at least for Modern. The primary
 * use of this store is to create and manage "active ranges" of records.
 *
 * For example:
 *
 *      var range = store.createActiveRange({
 *          begin: 100,
 *          end: 200,
 *          prefetch: true,  // allow prefetching beyond range
 *          callback: 'onRangeUpdate',
 *          scope: this
 *      });
 *
 *      // Navigate to a different range. This will causes pages to load and
 *      // the onRangeUpdate method will be called as the load(s) progress.
 *      // This can change the length or number of records spanned on each
 *      // call.
 *      //
 *      range.goto(300, 400);
 *
 *      onRangeUpdate: function (range, begin, end) {
 *          // Called when records appear in the range...
 *          // We can check if all things are loaded:
 *
 *          // Or we can use range.records (sparsely populated)
 *      }
 *
 * @since 6.5.0
 */
Ext.define('Ext.data.virtual.Store', {
    extend: 'Ext.data.ProxyStore',
    alias: 'store.virtual',
    requires: [
        'Ext.util.SorterCollection',
        'Ext.util.FilterCollection',
        'Ext.data.virtual.PageMap',
        'Ext.data.virtual.Range'
    ],
    uses: [
        'Ext.data.virtual.Group'
    ],
    isVirtualStore: true,
    config: {
        data: null,
        totalCount: null,
        /**
         * @cfg {Number} leadingBufferZone
         * The number of records to fetch beyond the active range in the direction of
         * movement. If the range is advancing forward, the additional records are beyond
         * `end`. If advancing backwards, they are before `begin`.
         */
        leadingBufferZone: 200,
        /**
         * @cfg {Number} trailingBufferZone
         * The number of records to fetch beyond the active trailing the direction of
         * movement. If the range is advancing forward, the additional records are before
         * `begin`. If advancing backwards, they are beyond `end`.
         */
        trailingBufferZone: 50
    },
    /**
     * @cfg remoteSort
     * @inheritdoc
     */
    remoteSort: true,
    /**
     * @cfg remoteFilter
     * @inheritdoc
     */
    remoteFilter: true,
    /**
     * @cfg sortOnLoad
     * @inheritdoc
     */
    sortOnLoad: false,
    /**
     * @cfg trackRemoved
     * @inheritdoc
     */
    trackRemoved: false,
    constructor: function(config) {
        var me = this;
        me.sortByPage = me.sortByPage.bind(me);
        me.activeRanges = [];
        me.pageMap = new Ext.data.virtual.PageMap({
            store: me
        });
        me.callParent([
            config
        ]);
    },
    doDestroy: function() {
        this.pageMap.destroy();
        this.callParent();
    },
    applyGrouper: function(grouper) {
        this.group(grouper);
        return this.grouper;
    },
    //-----------------------------------------------------------------------
    /**
     * @method contains
     * @inheritdoc
     */
    contains: function(record) {
        return this.indexOf(record) > -1;
    },
    /**
     * Create a `Range` instance to access records by their index.
     *
     * @param {Object/Ext.data.virtual.Range} [config]
     * @return {Ext.data.virtual.Range}
     * @since 6.5.0
     */
    createActiveRange: function(config) {
        var range = Ext.apply({
                leadingBufferZone: this.getLeadingBufferZone(),
                trailingBufferZone: this.getTrailingBufferZone(),
                store: this
            }, config);
        return new Ext.data.virtual.Range(range);
    },
    /**
     * @method getAt
     * @inheritdoc
     */
    getAt: function(index) {
        var page = this.pageMap.getPageOf(index, /* autoCreate= */
            false),
            ret;
        if (page && page.records) {
            // if (page is loaded)
            ret = page.records[index - page.begin];
        }
        return ret || null;
    },
    /**
     * Get the Record with the specified id.
     *
     * This method is not affected by filtering, lookup will be performed from all records
     * inside the store, filtered or not.
     *
     * @param {Mixed} id The id of the Record to find.
     * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.
     */
    getById: function(id) {
        return this.pageMap.byId[id] || null;
    },
    getCount: function() {
        return this.totalCount || 0;
    },
    getGrouper: function() {
        return this.grouper;
    },
    getGroups: function() {
        var me = this,
            groups = me.groupCollection;
        if (!groups) {
            me.groupCollection = groups = new Ext.util.Collection();
        }
        return groups;
    },
    getSummaryRecord: function() {
        return this.summaryRecord || null;
    },
    isGrouped: function() {
        return !!this.grouper;
    },
    group: function(grouper, direction) {
        var me = this;
        grouper = grouper || null;
        if (grouper) {
            if (typeof grouper === 'string') {
                grouper = {
                    property: grouper,
                    direction: direction || 'ASC'
                };
            }
            if (!grouper.isGrouper) {
                grouper = new Ext.util.Grouper(grouper);
            }
            grouper.setRoot('data');
            me.getGroups().getSorters().splice(0, 1, {
                property: 'id',
                direction: grouper.getDirection()
            });
        }
        me.grouper = grouper;
        if (!me.isConfiguring) {
            me.reload();
            me.fireGroupChange(grouper);
        }
    },
    getByInternalId: function(internalId) {
        return this.pageMap.getByInternalId(internalId);
    },
    /**
     * Get the index of the record within the virtual store. Because virtual stores only
     * load a partial set of records, not all records in the logically matching set will
     * have been loaded and will therefore return -1.
     *
     * @param {Ext.data.Model} record The record to find.
     * @return {Number} The index of the `record` or -1 if not found.
     */
    indexOf: function(record) {
        return this.pageMap.indexOf(record);
    },
    /**
     * Get the index within the store of the record with the passed id. Because virtual
     * stores only load a partial set of records, not all records in the logically
     * matching set will have been loaded and will therefore return -1.
     *
     * @param {String} id The id of the record to find.
     * @return {Number} The index of the record or -1 if not found.
     */
    indexOfId: function(id) {
        var rec = this.getById(id);
        return rec ? this.indexOf(rec) : -1;
    },
    /**
     * Returns `true` if the store has been loaded.
     * @return {Boolean} `true` if the store has been loaded.
     */
    isLoaded: function() {
        return Ext.isNumber(this.totalCount);
    },
    load: function(options) {
        if (typeof options === 'function') {
            options = {
                callback: options
            };
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            page = (options && options.page) || 1,
            pageSize = me.getPageSize(),
            operation = me.createOperation('read', Ext.apply({
                start: (page - 1) * pageSize,
                limit: pageSize,
                page: page,
                filters: me.getFilters().items,
                sorters: me.getSorters().items,
                grouper: me.getGrouper()
            }, options));
        if (me.fireEvent('beforeload', me, operation) !== false) {
            me.onBeforeLoad(operation);
            operation.execute();
        } else {
            operation.setCompleted();
        }
        return operation;
    },
    reload: function(options) {
        var me = this;
        if (typeof options === 'function') {
            options = {
                callback: options
            };
        }
        if (me.fireEvent('beforereload') === false) {
            return null;
        }
        options = Ext.apply({
            internalScope: me,
            internalCallback: me.handleReload,
            page: 1
        }, options);
        me.pageMap.clear();
        me.getGroups().clear();
        return me.load(options);
    },
    // TODO load?
    // TODO reload?
    removeAll: function() {
        var me = this,
            activeRanges = me.activeRanges,
            i;
        me.pageMap.clear();
        for (i = activeRanges.length; i-- > 0; ) {
            activeRanges[i].reset();
        }
        me.fireEvent('clear', me);
    },
    //---------------------------------------------------------------------
    applyProxy: function(proxy) {
        proxy = this.callParent([
            proxy
        ]);
        // This store asks for pages.
        // If used with a MemoryProxy, it must work
        if (proxy && proxy.setEnablePaging) {
            proxy.setEnablePaging(true);
        }
        return proxy;
    },
    // createDataCollection: function () {
    //     var result = new Ext.data.virtual.Data({
    //             store: this
    //         });
    //
    //     return result;
    // },
    createFiltersCollection: function() {
        return new Ext.util.FilterCollection();
    },
    createSortersCollection: function() {
        return new Ext.util.SorterCollection();
    },
    onFilterEndUpdate: function() {
        var me = this,
            filters = me.getFilters(false);
        // This is not affected by suppressEvent.
        if (!me.isConfiguring) {
            me.reload();
            me.fireEvent('filterchange', me, filters.getRange());
        }
    },
    onSorterEndUpdate: function() {
        var me = this,
            sorters = me.getSorters().getRange(),
            fire = !me.isConfiguring;
        if (fire) {
            me.fireEvent('beforesort', me, sorters);
        }
        if (fire) {
            me.reload();
            me.fireEvent('sort', me, sorters);
        }
    },
    updatePageSize: function(pageSize) {
        var totalCount = this.totalCount;
        if (totalCount !== null) {
            this.pageMap.setPageCount(Math.ceil(totalCount / pageSize));
        }
    },
    updateTotalCount: function(totalCount, oldTotalCount) {
        var me = this,
            pageMap = me.pageMap;
        me.totalCount = totalCount;
        pageMap.setPageCount(Math.ceil(totalCount / me.getPageSize()));
        me.fireEvent('totalcountchange', me, totalCount, oldTotalCount);
    },
    //--------------------------------------------------------
    // Unsupported API's
    add: function() {
        Ext.raise('Virtual stores do not support the add() method');
    },
    insert: function() {
        Ext.raise('Virtual stores do not support the insert() method');
    },
    filter: function() {
        if (!this.getRemoteFilter()) {
            Ext.raise('Virtual stores do not support local filtering');
        }
        // Remote filtering forces a load. load clears the store's contents.
        this.callParent(arguments);
    },
    filterBy: function() {
        Ext.raise('Virtual stores do not support local filtering');
    },
    loadData: function() {
        Ext.raise('Virtual stores do not support the loadData() method');
    },
    applyData: function() {
        Ext.raise('Virtual stores do not support direct data loading');
    },
    updateRemoteFilter: function(remoteFilter, oldRemoteFilter) {
        if (remoteFilter === false) {
            Ext.raise('Virtual stores are always remotely filtered.');
        }
        this.callParent([
            remoteFilter,
            oldRemoteFilter
        ]);
    },
    updateRemoteSort: function(remoteSort, oldRemoteSort) {
        if (remoteSort === false) {
            Ext.raise('Virtual stores are always remotely sorted.');
        }
        this.callParent([
            remoteSort,
            oldRemoteSort
        ]);
    },
    updateTrackRemoved: function(value) {
        if (value !== false) {
            Ext.raise('Virtual stores do not support trackRemoved.');
        }
        this.callParent(arguments);
    },
    afterEdit: function(record, modifiedFieldNames) {
        var me = this;
        me.fireEvent('update', me, record, Ext.data.Model.EDIT, modifiedFieldNames);
        me.fireEvent('datachanged', me);
    },
    privates: {
        attachSummaryData: function(resultSet) {
            var me = this,
                summary = resultSet.getSummaryData(),
                grouper, len, i, data, rec;
            if (summary) {
                me.summaryRecord = summary;
            }
            summary = resultSet.getGroupData();
            if (summary) {
                grouper = me.getGrouper();
                if (grouper) {
                    me.groupSummaryData = data = {};
                    for (i = 0 , len = summary.length; i < len; ++i) {
                        rec = summary[i];
                        data[grouper.getGroupString(rec)] = rec;
                    }
                }
            }
        },
        handleReload: function(op) {
            var me = this,
                activeRanges = me.activeRanges,
                len = activeRanges.length,
                pageMap = me.pageMap,
                resultSet = op.getResultSet(),
                wasSuccessful = op.wasSuccessful(),
                rsRecords = [],
                i, range;
            if (wasSuccessful) {
                me.readTotalCount(resultSet);
                me.fireEvent('reload', me, op);
                for (i = 0; i < len; ++i) {
                    range = activeRanges[i];
                    if (pageMap.canSatisfy(range)) {
                        range.reload();
                    }
                }
            }
            if (resultSet) {
                rsRecords = resultSet.records;
            }
            me.fireEvent('load', me, rsRecords, wasSuccessful, op);
        },
        loadVirtualPage: function(page, callback, scope) {
            var me = this,
                pageMapGeneration = me.pageMap.generation;
            return me.load({
                page: page.number + 1,
                // store loads are 1 based
                internalCallback: function(op) {
                    var resultSet = op.getResultSet(),
                        rsRecords = [];
                    if (pageMapGeneration === me.pageMap.generation) {
                        if (op.wasSuccessful()) {
                            me.readTotalCount(resultSet);
                            me.attachSummaryData(resultSet);
                        }
                        callback.call(scope || page, op);
                        me.groupSummaryData = null;
                        if (resultSet) {
                            rsRecords = resultSet.records;
                        }
                        me.fireEvent('load', me, rsRecords, op.wasSuccessful(), op);
                    }
                }
            });
        },
        lockGroups: function(grouper, page) {
            var groups = this.getGroups(),
                groupInfo = page.groupInfo = {},
                records = page.records,
                len = records.length,
                groupSummaryData = this.groupSummaryData,
                pageMap = this.pageMap,
                n = page.number,
                group, i, groupKey, summaryRec, rec, firstRecords, first;
            for (i = 0; i < len; ++i) {
                rec = records[i];
                groupKey = grouper.getGroupString(rec);
                if (!groupInfo[groupKey]) {
                    groupInfo[groupKey] = rec;
                    group = groups.get(groupKey);
                    if (!group) {
                        group = new Ext.data.virtual.Group(groupKey);
                        groups.add(group);
                    }
                    // We want to track the first known record in the group.
                    // If we have a record that is before the first one we know
                    // about, add it to the front. Otherwise, we don't care about
                    // the order at this point, so just shift it on to the end.
                    firstRecords = group.firstRecords;
                    first = firstRecords[0];
                    if (first && n < pageMap.getPageIndex(first)) {
                        firstRecords.unshift(rec);
                    } else {
                        firstRecords.push(rec);
                    }
                    summaryRec = groupSummaryData && groupSummaryData[groupKey];
                    if (summaryRec) {
                        group.summaryRecord = summaryRec;
                    }
                }
            }
        },
        onPageDataAcquired: function(page) {
            var grouper = this.getGrouper();
            if (grouper) {
                this.lockGroups(grouper, page);
            }
        },
        onPageDestroy: function(page) {
            var ranges = this.activeRanges,
                len = ranges.length,
                i;
            for (i = 0; i < len; ++i) {
                ranges[i].onPageDestroy(page);
            }
        },
        onPageEvicted: function(page) {
            var grouper = this.getGrouper();
            if (grouper) {
                this.releaseGroups(grouper, page);
            }
        },
        readTotalCount: function(resultSet) {
            var total = resultSet.getRemoteTotal();
            if (!isNaN(total)) {
                this.setTotalCount(total);
            }
        },
        releaseGroups: function(grouper, page) {
            var groups = this.getGroups(),
                groupInfo = page.groupInfo,
                first, firstRecords, key, group;
            for (key in groupInfo) {
                first = groupInfo[key];
                group = groups.get(key);
                firstRecords = group.firstRecords;
                // If there is only 1 first record left, this must be it, which
                // means the group no longer has records
                if (firstRecords.length === 1) {
                    groups.remove(group);
                } else if (firstRecords[0] === first) {
                    firstRecords.shift();
                    firstRecords.sort(this.sortByPage);
                } else {
                    Ext.Array.remove(firstRecords, first);
                }
            }
        },
        sortByPage: function(rec1, rec2) {
            // Bound to this instance in the constructor
            var map = this.pageMap;
            return map.getPageIndex(rec1) - map.getPageIndex(rec2);
        }
    }
});

/**
 * Base class for all Ext Direct events. An event is
 * created after some kind of interaction with the server.
 * The event class is essentially just a data structure
 * to hold a Direct response.
 */
Ext.define('Ext.direct.Event', {
    alias: 'direct.event',
    status: true,
    /**
     * Creates new Event.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        Ext.apply(this, config);
    },
    /**
     * Return the name for this event.
     * @return {String} The name of event
     */
    getName: function() {
        return this.name;
    },
    /**
     * Return the raw data for this event.
     * @return {Mixed} The data from the event
     */
    getData: function() {
        return this.data;
    }
});

/**
 * An event that is fired when data is received from a 
 * {@link Ext.direct.RemotingProvider}. Contains a method to the
 * related transaction for the direct request, see {@link #getTransaction}
 */
Ext.define('Ext.direct.RemotingEvent', {
    extend: 'Ext.direct.Event',
    alias: 'direct.rpc',
    /**
     * Get the transaction associated with this event.
     * @return {Ext.direct.Transaction} The transaction
     */
    getTransaction: function() {
        var me = this;
        return me.transaction || Ext.direct.Manager.getTransaction(me.tid);
    }
});

/**
 * An event that is fired when an exception is received from a {@link Ext.direct.RemotingProvider}
 */
Ext.define('Ext.direct.ExceptionEvent', {
    extend: 'Ext.direct.RemotingEvent',
    alias: 'direct.exception',
    status: false
});

/**
 * A base provider for communicating using JSON. This is an abstract class
 * and should not be instanced directly.
 *
 * @abstract
 */
Ext.define('Ext.direct.JsonProvider', {
    extend: 'Ext.direct.Provider',
    alias: 'direct.jsonprovider',
    uses: [
        'Ext.direct.ExceptionEvent',
        'Ext.direct.Manager'
    ],
    /**
    * Parse the JSON response
    * @private
    *
    * @param {Object} response The XHR response object
    *
    * @return {Object} The data in the response.
    */
    parseResponse: function(response) {
        var text = response && response.responseText;
        // Empty string should blow up in JSON decoder
        if (text != null) {
            if (Ext.isObject(text) || Ext.isArray(text)) {
                return text;
            }
            return Ext.decode(text);
        }
        return null;
    },
    /**
     * Creates a set of events based on the XHR response
     *
     * @param {Object} response The XHR response
     *
     * @return {Ext.direct.Event[]} An array of Ext.direct.Event
     */
    createEvents: function(response) {
        var me = this,
            data = null,
            events = [],
            event, i, len;
        try {
            data = me.parseResponse(response);
        } catch (e) {
            event = new Ext.direct.ExceptionEvent({
                parsingError: true,
                data: e,
                xhr: response,
                code: Ext.direct.Manager.exceptions.PARSE,
                message: 'Error parsing json response: \n\n ' + e
            });
            return [
                event
            ];
        }
        if (Ext.isArray(data)) {
            for (i = 0 , len = data.length; i < len; ++i) {
                events.push(me.createEvent(data[i]));
            }
        } else if (Ext.isObject(data)) {
            events.push(me.createEvent(data));
        }
        return events;
    },
    /**
     * Create an event from a response object
     *
     * @param {Object} response Response object
     *
     * @return {Ext.direct.Event} The event
     */
    createEvent: function(response) {
        if (typeof response !== 'object' || !('type' in response)) {
            return new Ext.direct.ExceptionEvent({
                data: response,
                code: Ext.direct.Manager.exceptions.DATA,
                message: 'Invalid data: event type is not specified'
            });
        }
        return Ext.create('direct.' + response.type, response);
    }
});

/**
 * Provides for repetitive polling of the server at distinct {@link #interval intervals}.
 * The initial request for data originates from the client, and then is responded to by the
 * server.
 * 
 * Configuration for the PollingProvider can be generated by the server-side
 * API portion of the Ext Direct stack.
 *
 * An instance of PollingProvider may be created directly via the new keyword or by simply
 * specifying `type = 'polling'`. For example:
 *
 *      var pollA = new Ext.direct.PollingProvider({
 *          type:'polling',
 *          url: 'php/pollA.php',
 *      });
 *      Ext.direct.Manager.addProvider(pollA);
 *      pollA.disconnect();
 *      
 *      Ext.direct.Manager.addProvider({
 *          type:'polling',
 *          url: 'php/pollB.php',
 *          id: 'pollB-provider'
 *      });
 *      var pollB = Ext.direct.Manager.getProvider('pollB-provider');
 *
 */
Ext.define('Ext.direct.PollingProvider', {
    extend: 'Ext.direct.JsonProvider',
    alias: 'direct.pollingprovider',
    requires: [
        'Ext.Ajax',
        'Ext.util.TaskRunner',
        'Ext.direct.ExceptionEvent'
    ],
    type: 'polling',
    /**
     * @cfg {Number} [interval=3000]
     * How often to poll the server-side in milliseconds. Defaults to every 3 seconds.
     */
    interval: 3000,
    /**
     * @cfg {Object} [baseParams]
     * An object containing properties which are to be sent as parameters on every
     * polling request. Note that if baseParams are set and {@link #url} parameter
     * is an URL string, poll requests will use POST method instead of default GET.
     */
    /**
     * @cfg {String/Function} url
     * The url which the PollingProvider should contact with each request. This can also be
     * an imported Ext Direct method which will be passed baseParams as named arguments.
     *
     * @deprecated 5.1.0 Using Function `url` is deprecated, please use {@link #pollFn} instead
     */
    /**
     * @cfg {String/Function} pollFn
     *
     * Ext Direct method to use for polling. If a method name is provided as a string,
     * the actual function will not be resolved until the first time this provider
     * is connected.
     *
     * The method should accept named arguments and will be passed {@link #baseParams}
     * if set.
     */
    /**
     * @cfg {Number} [timeout]
     *
     * The timeout to use for each request.
     */
    /**
     * @event beforepoll
     * @preventable
     * Fired immediately before a poll takes place.
     *
     * @param {Ext.direct.PollingProvider} this
     */
    /**
     * @event poll
     * Fired immediately after a poll takes place.
     *
     * @param {Ext.direct.PollingProvider} this
     */
    constructor: function(config) {
        var me = this;
        me.callParent([
            config
        ]);
        me.pollTask = Ext.TaskManager.newTask({
            run: me.runPoll,
            interval: me.interval,
            scope: me
        });
    },
    destroy: function() {
        this.pollTask.stop(true);
        this.callParent();
    },
    doConnect: function() {
        var me = this,
            url = me.url,
            pollFn = me.pollFn;
        // It is important that pollFn resolution happens at the time when
        // Provider is first connected, and not at construction time. If
        // pollFn is configured as a string, the API stub may not exist yet
        // when PollingProvider is constructed.
        if (pollFn && Ext.isString(pollFn)) {
            var fnName = pollFn;
            // eslint-disable-line vars-on-top, one-var
            me.pollFn = pollFn = Ext.direct.Manager.parseMethod(pollFn);
            if (!Ext.isFunction(pollFn)) {
                Ext.raise("Cannot resolve Ext Direct API method " + fnName + " for PollingProvider");
            }
        } else if (Ext.isFunction(url)) {
            Ext.log.warn('Using a function for url is deprecated, use pollFn instead.');
            me.pollFn = pollFn = url;
            me.url = url = null;
        }
        if (url || pollFn) {
            me.setInterval(me.interval);
            me.pollTask.start();
        }
    },
    doDisconnect: function() {
        if (this.pollTask) {
            this.pollTask.stop();
        }
    },
    getInterval: function() {
        return this.pollTask && this.pollTask.interval;
    },
    setInterval: function(interval) {
        var me = this,
            pollTask = me.pollTask;
        if (interval < 100) {
            Ext.raise("Attempting to configure PollProvider " + me.id + " with interval that is less than 100ms.");
        }
        me.interval = pollTask.interval = interval;
        if (me.isConnected()) {
            pollTask.restart(interval);
        }
    },
    /**
     * @private
     */
    runPoll: function() {
        var me = this,
            url = me.url,
            pollFn = me.pollFn,
            baseParams = me.baseParams,
            args, request;
        if (me.fireEvent('beforepoll', me) !== false) {
            if (pollFn) {
                args = pollFn.directCfg.method.getArgs({
                    params: baseParams !== undefined ? baseParams : {},
                    callback: me.onPollFn,
                    scope: me
                });
                pollFn.apply(window, args);
            } else {
                request = {
                    url: url,
                    callback: me.onData,
                    scope: me,
                    params: baseParams,
                    headers: me.getHeaders()
                };
                if (me.timeout != null) {
                    request.timeout = me.timeout;
                }
                me.sendAjaxRequest(request);
            }
            me.fireEvent('poll', me);
        }
    },
    /**
     * @private
     */
    onData: function(opt, success, response) {
        var me = this,
            i, len, events, event;
        if (success) {
            events = me.createEvents(response);
            for (i = 0 , len = events.length; i < len; ++i) {
                event = events[i];
                me.fireEvent('data', me, event);
                if (!event.status) {
                    me.fireEvent('exception', me, event);
                }
            }
        } else {
            event = new Ext.direct.ExceptionEvent({
                data: null,
                code: Ext.direct.Manager.exceptions.TRANSPORT,
                message: 'Unable to connect to the server.',
                xhr: response
            });
            me.fireEvent('data', me, event);
            me.fireEvent('exception', me, event);
        }
        me.callParent([
            opt,
            success,
            response
        ]);
    },
    /**
     * @private
     */
    onPollFn: function(result, event, success, options) {
        this.onData(null, success, {
            responseText: result
        });
    },
    inheritableStatics: {
        /**
         * @private
         * @static
         * @inheritable
         */
        checkConfig: function(config) {
            // Polling provider needs either URI or pollFn
            return config && config.type === 'polling' && (config.url || config.pollFn);
        }
    }
});

/**
 * @private
 * Small utility class used internally to represent a Direct method.
 */
Ext.define('Ext.direct.RemotingMethod', {
    constructor: function(config) {
        var me = this,
            params = config.params,
            len = config.len,
            metadataCfg = config.metadata,
            metadata = {},
            name, pLen, p, param;
        me.name = config.name;
        me.disableBatching = config.batched != null ? !config.batched : false;
        if (config.formHandler) {
            me.formHandler = config.formHandler;
        } else if (Ext.isNumeric(len)) {
            // given only the number of parameters
            me.len = len;
            me.ordered = true;
        } else {
            /*
             * Given an array of either
             * a) String
             * b) Objects with a name property. We may want to encode extra info in here later
             * c) Empty array signifies no mandatory parameters
             */
            me.named = true;
            me.strict = config.strict !== undefined ? config.strict : true;
            me.params = {};
            // params may not be defined for a formHandler, or named method
            // with no strict checking
            pLen = params && params.length;
            for (p = 0; p < pLen; p++) {
                param = params[p];
                name = Ext.isObject(param) ? param.name : param;
                me.params[name] = true;
            }
        }
        if (metadataCfg) {
            params = metadataCfg.params;
            len = metadataCfg.len;
            if (Ext.isNumeric(len)) {
                if (len === 0) {
                    Ext.raise('metadata.len cannot be 0 ' + 'for Ext Direct method ' + me.name);
                }
                metadata.ordered = true;
                metadata.len = len;
            } else if (Ext.isArray(params)) {
                metadata.named = true;
                metadata.params = {};
                for (p = 0 , pLen = params.length; p < pLen; p++) {
                    param = params[p];
                    metadata.params[param] = true;
                }
                metadata.strict = metadataCfg.strict !== undefined ? metadataCfg.strict : true;
            } else {
                Ext.raise('metadata is neither named nor ordered ' + 'for Ext Direct method ' + me.name);
            }
            me.metadata = metadata;
        }
    },
    /**
     * Prepare Direct function arguments that can be used with getCallData().
     */
    getArgs: function(config) {
        var me = this,
            params = config.params,
            paramOrder = config.paramOrder,
            metadata = config.metadata,
            options = config.options,
            args = [],
            flatten, i, len;
        if (me.ordered) {
            if (me.len > 0) {
                // If a paramOrder was specified, add the params into the argument list
                // in that order.
                if (paramOrder) {
                    // Direct proxy uses this configuration for its CRUD operations.
                    // We only do this kind of thing for ordered Methods that accept 1 argument,
                    // if there's more or less we fall back to default processing.
                    flatten = config.paramsAsArray && me.len === 1 && (paramOrder.length > 1 || Ext.isArray(params));
                    if (flatten) {
                        if (Ext.isArray(params)) {
                            for (i = 0 , len = params.length; i < len; i++) {
                                args.push(me.convertParams(params[i], paramOrder, paramOrder.length, true));
                            }
                        } else {
                            args = me.convertParams(params, paramOrder, paramOrder.length, true);
                        }
                        if (!params.allowSingle || args.length > 1) {
                            args = [
                                args
                            ];
                        }
                    } else {
                        // The number of arguments expected by the Method has priority
                        // over the number of parameters in paramOrder.
                        args = me.convertParams(params, paramOrder, me.len, false);
                    }
                } else {
                    args.push(params);
                }
            }
        } else {
            args.push(params);
        }
        args.push(config.callback, config.scope || window);
        if (options || metadata) {
            options = Ext.apply({}, options);
            if (metadata) {
                // Could be either an object of named arguments,
                // or an array of ordered arguments
                options.metadata = metadata;
            }
            args.push(options);
        }
        return args;
    },
    convertParams: function(params, paramOrder, count, flatten) {
        var ret = [],
            paramName, i, len;
        for (i = 0 , len = count; i < len; i++) {
            paramName = paramOrder[i];
            ret.push(params[paramName]);
        }
        if (flatten) {
            return ret.length === 0 ? undefined : ret.length === 1 ? ret[0] : ret;
        } else {
            return ret;
        }
    },
    /**
     * Takes the arguments for a Direct function and splits the arguments
     * from the scope and the callback.
     *
     * @param {Array} args The arguments passed to the direct call
     *
     * @return {Object} An object with 4 properties: args, callback, scope, and options object.
     */
    getCallData: function(args) {
        var me = this,
            data = null,
            len = me.len,
            params = me.params,
            strict = me.strict,
            form, callback, scope, name, options, metadata;
        // Historically, the presence of required arguments was not checked;
        // another idiosyncrasy is that null is sent to the server side
        // instead of empty array when len === 0
        if (me.ordered) {
            callback = args[len];
            scope = args[len + 1];
            options = args[len + 2];
            if (len !== 0) {
                data = args.slice(0, len);
            }
        } else if (me.formHandler) {
            form = args[0];
            callback = args[1];
            scope = args[2];
            options = args[3];
        } else {
            data = Ext.apply({}, args[0]);
            callback = args[1];
            scope = args[2];
            options = args[3];
            // filter out any non-existent properties unless !strict
            if (strict) {
                for (name in data) {
                    if (data.hasOwnProperty(name) && !params[name]) {
                        delete data[name];
                    }
                }
            }
        }
        if (me.metadata && options && options.metadata) {
            if (me.metadata.ordered) {
                if (!Ext.isArray(options.metadata)) {
                    Ext.raise('options.metadata is not an Array ' + 'for Ext Direct method ' + me.name);
                } else if (options.metadata.length < me.metadata.len) {
                    Ext.raise('Not enough parameters in options.metadata ' + 'for Ext Direct method ' + me.name);
                }
                metadata = options.metadata.slice(0, me.metadata.len);
            } else {
                if (!Ext.isObject(options.metadata)) {
                    Ext.raise('options.metadata is not an Object ' + 'for Ext Direct method ' + me.name);
                }
                metadata = Ext.apply({}, options.metadata);
                if (me.metadata.strict) {
                    for (name in metadata) {
                        if (metadata.hasOwnProperty(name) && !me.metadata.params[name]) {
                            delete metadata[name];
                        }
                    }
                }
                for (name in me.metadata.params) {
                    if (!metadata.hasOwnProperty(name)) {
                        Ext.raise('Named parameter ' + name + ' is missing ' + 'in options.metadata for Ext Direct method ' + me.name);
                    }
                }
            }
            delete options.metadata;
        }
        return {
            form: form,
            data: data,
            metadata: metadata,
            callback: callback,
            scope: scope,
            options: options
        };
    }
});

/**
 * Supporting Class for Ext Direct (not intended to be used directly).
 */
Ext.define('Ext.direct.Transaction', {
    alias: 'direct.transaction',
    statics: {
        TRANSACTION_ID: 0
    },
    /**
     * @cfg {Ext.direct.Provider} provider Provider to use with this Transaction.
     */
    /**
     * Creates new Transaction.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        var me = this;
        Ext.apply(me, config);
        me.id = me.tid = ++me.self.TRANSACTION_ID;
        me.retryCount = 0;
    },
    send: function() {
        var me = this;
        me.provider.queueTransaction(me);
    },
    retry: function() {
        var me = this;
        me.retryCount++;
        me.send();
    },
    getProvider: function() {
        return this.provider;
    }
});

/**
 * The {@link Ext.direct.RemotingProvider RemotingProvider} exposes access to
 * server side methods on the client (a remote procedure call (RPC) type of
 * connection where the client can initiate a procedure on the server).
 * 
 * This allows for code to be organized in a fashion that is maintainable,
 * while providing a clear path between client and server, something that is
 * not always apparent when using URLs.
 * 
 * To accomplish this the server-side needs to describe what classes and methods
 * are available on the client-side. This configuration will typically be
 * outputted by the server-side Ext Direct stack when the API description is built.
 */
Ext.define('Ext.direct.RemotingProvider', {
    extend: 'Ext.direct.JsonProvider',
    alias: 'direct.remotingprovider',
    requires: [
        'Ext.util.MixedCollection',
        'Ext.util.DelayedTask',
        'Ext.direct.Transaction',
        'Ext.direct.RemotingMethod',
        'Ext.direct.Manager'
    ],
    type: 'remoting',
    /**
     * @cfg {Object} actions
     *
     * Object literal defining the server side actions and methods. For example, if
     * the Provider is configured with:
     *
     *      // each property within the 'actions' object represents a server side Class
     *      actions: {
     *          // array of methods in each server side Class to be stubbed out on client
     *          TestAction: [{
     *              name: 'doEcho',   // stub method will be TestAction.doEcho
     *              len:  1,
     *              batched: false    // always send requests immediately for this method
     *          }, {
     *              name: 'multiply', // name of method
     *              len:  2           // The number of parameters that will be used to create an
     *                                // array of data to send to the server side function.
     *          }, {
     *              name: 'doForm',
     *              formHandler: true // tells the client that this method handles form calls
     *          }],
     *          
     *          // These methods will be created in nested namespace TestAction.Foo
     *          'TestAction.Foo': [{
     *              name: 'ordered',  // stub method will be TestAction.Foo.ordered
     *              len:  1
     *          }, {
     *              name: 'noParams', // this method does not accept any parameters
     *              len:  0
     *          }, {
     *              name: 'named',    // stub method will be TestAction.Foo.named
     *              params: ['foo', 'bar']    // parameters are passed by name
     *          }, {
     *              name: 'namedNoStrict',
     *              params: [],       // this method accepts parameters by name
     *              strict: false     // but does not check if they are required
     *                                // and will pass any to the server side
     *          }]
     *      }
     *
     * Note that starting with 4.2, dotted Action names will generate nested objects.
     * If you wish to reverse to previous behavior, set {@link #cfg-disableNestedActions}
     * to `true`.
     *
     * In the following example a *client side* handler is used to call the
     * server side method "multiply" in the server-side "TestAction" Class:
     *
     *      TestAction.multiply(
     *          // pass two arguments to server, so specify len=2
     *          2, 4,
     *          
     *          // callback function after the server is called
     *          //  result: the result returned by the server
     *          //       e: Ext.direct.RemotingEvent object
     *          // success: true or false
     *          // options: options to be applied to method call and passed to callback
     *          function (result, e, success, options) {
     *              var t, action, method;
     *              
     *              t = e.getTransaction();
     *              action = t.action; // server side Class called
     *              method = t.method; // server side method called
     *              
     *              if (e.status) {
     *                  var answer = Ext.encode(result); // 8
     *              }
     *              else {
     *                  var msg = e.message; // failure message
     *              }
     *          },
     *          
     *          // Scope to call the callback in (optional)
     *          window,
     *          
     *          // Options to apply to this method call. This can include
     *          // Ajax.request() options; only `timeout` is supported at this time.
     *          // When timeout is set for a method call, it will be executed immediately
     *          // without buffering.
     *          // The same options object is passed to the callback so it's possible
     *          // to "forward" some data when needed.
     *          {
     *              timeout: 60000, // milliseconds
     *              foo: 'bar'
     *          }
     *      );
     *
     * In the example above, the server side "multiply" function will be passed two
     * arguments (2 and 4). The "multiply" method should return the value 8 which will be
     * available as the `result` in the callback example above. 
     */
    /**
     * @cfg {Boolean} [disableNestedActions=false]
     * In versions prior to 4.2, using dotted Action names was not really meaningful,
     * because it generated flat {@link #cfg-namespace} object with dotted property
     * names. For example, take this API declaration:
     *
     *      {
     *          actions: {
     *              TestAction: [{
     *                  name: 'foo',
     *                  len:  1
     *              }],
     *              'TestAction.Foo' [{
     *                  name: 'bar',
     *                  len: 1
     *              }]
     *          },
     *          namespace: 'MyApp'
     *      }
     *
     * Before 4.2, that would generate the following API object:
     *
     *      window.MyApp = {
     *          TestAction: {
     *              foo: function() { ... }
     *          },
     *          'TestAction.Foo': {
     *              bar: function() { ... }
     *          }
     *      }
     *
     * In Ext JS 4.2, we introduced new namespace handling behavior. Now the same API
     * object will be like this:
     *
     *      window.MyApp = {
     *          TestAction: {
     *              foo: function() { ... },
     *
     *              Foo: {
     *                  bar: function() { ... }
     *              }
     *          }
     *      }
     *
     * Instead of addressing Action methods array-style `MyApp['TestAction.Foo'].bar()`,
     * now it is possible to use object addressing: `MyApp.TestAction.Foo.bar()`.
     *
     * If you find this behavior undesirable, set this config option to `true`.
     */
    /**
     * @cfg {String/Object} namespace
     *
     * Namespace for the Remoting Provider (defaults to `Ext.global`).
     * Explicitly specify the namespace Object, or specify a String to have a
     * {@link Ext#namespace namespace} created implicitly.
     */
    /**
     * @cfg {String} url
     *
     * **Required**. The url to connect to the {@link Ext.direct.Manager} server-side
     * router. 
     */
    /**
     * @cfg {String} [enableUrlEncode=data]
     *
     * Specify which param will hold the arguments for the method.
     */
    /**
     * @cfg {Number/Boolean} enableBuffer
     *
     * `true` or `false` to enable or disable combining of method
     * calls. If a number is specified this is the amount of time in milliseconds
     * to wait before sending a batched request.
     *
     * Calls which are received within the specified timeframe will be
     * concatenated together and sent in a single request, optimizing the
     * application by reducing the amount of round trips that have to be made
     * to the server. To cancel buffering for some particular invocations, pass
     * `timeout` parameter in `options` object for that method call.
     */
    enableBuffer: 10,
    /**
     * @cfg {Number} bufferLimit
     * The maximum number of requests to batch together. By default, an unlimited number
     * of requests will be batched. This option will allow to wait only for a certain
     * number of Direct method calls before dispatching a request to the server, even if
     * {@link #enableBuffer} timeout has not yet expired.
     * 
     * Note that this option does nothing if {@link #enableBuffer} is set to `false`.
     */
    bufferLimit: Number.MAX_VALUE,
    /**
     * @cfg {Number} maxRetries
     *
     * Number of times to re-attempt delivery on failure of a call.
     */
    maxRetries: 1,
    /**
     * @cfg {Number} timeout
     *
     * The timeout to use for each request.
     */
    /**
     * @event beforecall
     * @preventable
     *
     * Fires immediately before the client-side sends off the RPC call. By returning
     * `false` from an event handler you can prevent the call from being made.
     *
     * @param {Ext.direct.RemotingProvider} provider
     * @param {Ext.direct.Transaction} transaction
     * @param {Object} meta The meta data
     */
    /**
     * @event call
     *
     * Fires immediately after the request to the server-side is sent. This does
     * NOT fire after the response has come back from the call.
     *
     * @param {Ext.direct.RemotingProvider} provider
     * @param {Ext.direct.Transaction} transaction
     * @param {Object} meta The meta data
     */
    /**
     * @event beforecallback
     * @preventable
     *
     * Fires before callback function is executed. By returning `false` from an event handler
     * you can prevent the callback from executing.
     *
     * @param {Ext.direct.RemotingProvider} provider The provider instance
     * @param {Ext.direct.Event} event Event associated with the callback invocation
     * @param {Ext.direct.Transaction} transaction Transaction for which the callback
     * is about to be fired
     */
    constructor: function(config) {
        var me = this;
        me.callParent([
            config
        ]);
        me.namespace = (Ext.isString(me.namespace) ? Ext.ns(me.namespace) : me.namespace) || Ext.global;
        me.callBuffer = [];
    },
    destroy: function() {
        if (this.callTask) {
            this.callTask.cancel();
        }
        this.callParent();
    },
    /**
     * @method connect
     * @inheritdoc
     */
    connect: function() {
        var me = this;
        if (!me.url) {
            Ext.raise('Error initializing RemotingProvider "' + me.id + '", no url configured.');
        }
        me.callParent();
    },
    doConnect: function() {
        if (!this.apiCreated) {
            this.initAPI();
            this.apiCreated = true;
        }
    },
    /**
     * Get nested namespace by property.
     *
     * @private
     */
    getNamespace: function(root, action) {
        var parts, ns, i, len;
        root = root || Ext.global;
        parts = action.toString().split('.');
        for (i = 0 , len = parts.length; i < len; i++) {
            ns = parts[i];
            root = root[ns];
            if (typeof root === 'undefined') {
                return root;
            }
        }
        return root;
    },
    /**
     * Create nested namespaces. Unlike {@link Ext#ns} this method supports
     * nested objects as root of the namespace, not only Ext.global (window).
     *
     * @private
     */
    createNamespaces: function(root, action) {
        var parts, ns, i, len;
        root = root || Ext.global;
        parts = action.toString().split('.');
        for (i = 0 , len = parts.length; i < len; i++) {
            ns = parts[i];
            root[ns] = root[ns] || {};
            root = root[ns];
        }
        return root;
    },
    /**
     * Initialize the API
     *
     * @private
     */
    initAPI: function() {
        var me = this,
            actions = me.actions,
            namespace = me.namespace,
            Manager = Ext.direct.Manager,
            action, cls, methods, i, len, method, handler;
        for (action in actions) {
            if (actions.hasOwnProperty(action)) {
                if (me.disableNestedActions) {
                    cls = namespace[action];
                    if (!cls) {
                        cls = namespace[action] = {};
                    }
                } else {
                    cls = me.getNamespace(namespace, action);
                    if (!cls) {
                        cls = me.createNamespaces(namespace, action);
                    }
                }
                methods = actions[action];
                for (i = 0 , len = methods.length; i < len; ++i) {
                    method = new Ext.direct.RemotingMethod(methods[i]);
                    cls[method.name] = handler = me.createHandler(action, method);
                    Manager.registerMethod(handler.$name, handler);
                }
            }
        }
    },
    /**
     * Create a handler function for a direct call.
     *
     * @param {String} action The action the call is for
     * @param {Object} method The details of the method
     *
     * @return {Function} A JS function that will kick off the call
     *
     * @private
     */
    createHandler: function(action, method) {
        var me = this,
            handler;
        handler = function() {
            me.invokeFunction(action, method, Array.prototype.slice.call(arguments, 0));
        };
        handler.name = handler.$name = action + '.' + method.name;
        handler.$directFn = true;
        handler.directCfg = handler.$directCfg = {
            action: action,
            method: method
        };
        return handler;
    },
    /**
     * Invoke a Direct function call
     *
     * @param {String} action The action being executed
     * @param {Object} method The method being executed
     * @param {Object} args Transaction arguments
     *
     * @private
     */
    invokeFunction: function(action, method, args) {
        var me = this,
            transaction, form, isUpload, postParams;
        transaction = me.configureTransaction(action, method, args);
        if (me.fireEvent('beforecall', me, transaction, method) !== false) {
            Ext.direct.Manager.addTransaction(transaction);
            if (transaction.isForm) {
                form = transaction.form;
                /* eslint-disable-next-line max-len */
                isUpload = String(form.getAttribute("enctype")).toLowerCase() === 'multipart/form-data';
                postParams = {
                    extTID: transaction.id,
                    extAction: action,
                    extMethod: method.name,
                    extType: 'rpc',
                    extUpload: String(isUpload)
                };
                if (transaction.metadata) {
                    postParams.extMetadata = Ext.JSON.encode(transaction.metadata);
                }
                Ext.apply(transaction, {
                    form: form,
                    isUpload: isUpload,
                    params: postParams
                });
            }
            me.queueTransaction(transaction);
            me.fireEvent('call', me, transaction, method);
        }
    },
    /**
     * Configure a transaction for a Direct request
     *
     * @param {String} action The action being executed
     * @param {Object} method The method being executed
     * @param {Array} args Method invocation arguments
     * @param {Boolean} isForm True for a form submit
     *
     * @return {Object} Transaction object
     *
     * @private
     */
    configureTransaction: function(action, method, args, isForm) {
        var data, cb, scope, options, params;
        data = method.getCallData(args);
        cb = data.callback;
        scope = data.scope;
        options = data.options;
        if (cb && !Ext.isFunction(cb)) {
            Ext.raise("Callback argument is not a function " + "for Ext Direct method " + action + "." + method.name);
        }
        // Callback might be unspecified for a notification
        // that does not expect any return value
        cb = cb && scope ? cb.bind(scope) : cb;
        params = Ext.apply({}, {
            provider: this,
            args: args,
            action: action,
            method: method.name,
            form: data.form,
            data: data.data,
            metadata: data.metadata,
            callbackOptions: options,
            callback: cb,
            isForm: !!method.formHandler,
            disableBatching: method.disableBatching
        });
        if (options && options.timeout != null) {
            params.timeout = options.timeout;
        }
        return new Ext.direct.Transaction(params);
    },
    /**
     * Add a new transaction to the queue
     *
     * @param {Ext.direct.Transaction} transaction The transaction
     *
     * @private
     */
    queueTransaction: function(transaction) {
        var me = this,
            callBuffer = me.callBuffer,
            enableBuffer = me.enableBuffer;
        if (transaction.isForm || enableBuffer === false || transaction.disableBatching || transaction.timeout != null) {
            me.sendTransaction(transaction);
            return;
        }
        callBuffer.push(transaction);
        if (enableBuffer && callBuffer.length < me.bufferLimit) {
            if (!me.callTask) {
                me.callTask = new Ext.util.DelayedTask(me.combineAndSend, me);
            }
            me.callTask.delay(Ext.isNumber(enableBuffer) ? enableBuffer : 10);
        } else {
            me.combineAndSend();
        }
    },
    /**
     * Combine any buffered requests and send them off
     *
     * @private
     */
    combineAndSend: function() {
        var me = this,
            buffer = me.callBuffer,
            len = buffer.length;
        if (len > 0) {
            me.sendTransaction(len === 1 ? buffer[0] : buffer);
            me.callBuffer = [];
        }
    },
    /**
     * Create an Ajax request out of transaction and send it to the server
     *
     * @param {Object/Array} transaction The transaction(s) to send
     *
     * @private
     */
    sendTransaction: function(transaction) {
        var me = this,
            request, callData, params,
            enableUrlEncode = me.enableUrlEncode,
            payload, i, len;
        request = {
            url: me.url,
            callback: me.onData,
            scope: me,
            transaction: transaction,
            headers: me.getHeaders()
        };
        // Explicitly specified timeout for Ext Direct call overrides defaults
        if (transaction.timeout != null) {
            request.timeout = transaction.timeout;
        } else if (me.timeout != null) {
            request.timeout = me.timeout;
        }
        if (transaction.isForm) {
            Ext.apply(request, {
                params: transaction.params,
                form: transaction.form,
                isUpload: transaction.isUpload
            });
        } else {
            if (Ext.isArray(transaction)) {
                callData = [];
                for (i = 0 , len = transaction.length; i < len; ++i) {
                    payload = me.getPayload(transaction[i]);
                    callData.push(payload);
                }
            } else {
                callData = me.getPayload(transaction);
            }
            if (enableUrlEncode) {
                params = {};
                /* eslint-disable-next-line max-len */
                params[Ext.isString(enableUrlEncode) ? enableUrlEncode : 'data'] = Ext.encode(callData);
                request.params = params;
            } else {
                request.jsonData = callData;
            }
        }
        return me.sendAjaxRequest(request);
    },
    /**
     * Gets the Ajax call info for a transaction
     *
     * @param {Ext.direct.Transaction} transaction The transaction
     *
     * @return {Object} The call params
     *
     * @private
     */
    getPayload: function(transaction) {
        var result = {
                action: transaction.action,
                method: transaction.method,
                data: transaction.data,
                type: 'rpc',
                tid: transaction.id
            };
        if (transaction.metadata) {
            result.metadata = transaction.metadata;
        }
        return result;
    },
    /**
     * React to the ajax request being completed
     *
     * @private
     */
    onData: function(options, success, response) {
        var me = this,
            i, len, events, event, transaction, transactions;
        if (me.destroying || me.destroyed) {
            return;
        }
        // Success in this context means lack of communication failure,
        // i.e. that we have successfully connected to the server and
        // received a valid HTTP response. This does not imply that
        // the server returned valid JSON data, or that individual
        // function invocations were also successful.
        events = success && me.createEvents(response);
        // Redefine success: if parsing failed, createEvents() will return
        // only one event object, and it will be a parsing error exception.
        success = events && events.length && !events[0].parsingError;
        if (success) {
            for (i = 0 , len = events.length; i < len; ++i) {
                event = events[i];
                me.fireEvent('data', me, event);
                transaction = me.getTransaction(event);
                if (transaction) {
                    if (me.fireEvent('beforecallback', me, event, transaction) !== false) {
                        me.runCallback(transaction, event, true);
                    }
                    Ext.direct.Manager.removeTransaction(transaction);
                }
            }
        } else {
            transactions = [].concat(options.transaction);
            event = events[0] || new Ext.direct.ExceptionEvent({
                data: null,
                transaction: transaction,
                code: Ext.direct.Manager.exceptions.TRANSPORT,
                message: 'Unable to connect to the server.',
                xhr: response
            });
            for (i = 0 , len = transactions.length; i < len; ++i) {
                transaction = me.getTransaction(transactions[i]);
                if (transaction && transaction.retryCount < me.maxRetries) {
                    transaction.retry();
                } else {
                    me.fireEvent('data', me, event);
                    me.fireEvent('exception', me, event);
                    /* eslint-disable-next-line max-len */
                    if (transaction && me.fireEvent('beforecallback', me, event, transaction) !== false) {
                        me.runCallback(transaction, event, false);
                    }
                    Ext.direct.Manager.removeTransaction(transaction);
                }
            }
        }
        me.callParent([
            options,
            success,
            response
        ]);
    },
    /**
     * Get transaction from XHR options
     *
     * @param {Object} options The options sent to the Ajax request
     *
     * @return {Ext.direct.Transaction} The transaction, null if not found
     *
     * @private
     */
    getTransaction: function(options) {
        return options && options.tid ? Ext.direct.Manager.getTransaction(options.tid) : null;
    },
    /**
     * Run any callbacks related to the transaction.
     *
     * @param {Ext.direct.Transaction} transaction The transaction
     * @param {Ext.direct.Event} event The event
     *
     * @private
     */
    runCallback: function(transaction, event) {
        var success = !!event.status,
            funcName = success ? 'success' : 'failure',
            callback, options, result;
        if (transaction && transaction.callback) {
            callback = transaction.callback;
            options = transaction.callbackOptions;
            result = typeof event.result !== 'undefined' ? event.result : event.data;
            if (Ext.isFunction(callback)) {
                callback(result, event, success, options);
            } else {
                Ext.callback(callback[funcName], callback.scope, [
                    result,
                    event,
                    success,
                    options
                ]);
                Ext.callback(callback.callback, callback.scope, [
                    result,
                    event,
                    success,
                    options
                ]);
            }
        }
    },
    inheritableStatics: {
        /**
         * @private
         * @static
         * @inheritable
         */
        checkConfig: function(config) {
            // RemotingProvider needs service URI,
            // type and array of Actions
            return config && config.type === 'remoting' && config.url && Ext.isArray(config.actions);
        }
    }
});

/**
 * Garbage collector for Ext.dom.Element instances.  Automatically cleans up Elements
 * that are no longer in the dom, but were not properly destroyed using
 * {@link Ext.dom.Element#destroy destroy()}.  Recommended practice is for Components to
 * clean up their own elements, but the GarbageCollector runs on regularly scheduled
 * intervals to attempt to clean up orphaned Elements that may have slipped through the cracks.
 * @private
 */
Ext.define('Ext.dom.GarbageCollector', {
    singleton: true,
    /**
     * @property {Number}
     * The interval at which to run Element garbage collection. Set this property directly
     * to tune the interval.
     *
     *     Ext.dom.GarbageCollector.interval = 60000; // run garbage collection every one minute
     */
    interval: 30000,
    constructor: function() {
        var me = this;
        me.lastTime = Ext.now();
        me.onTick = me.onTick.bind(me);
        me.onTick.$skipTimerCheck = true;
        me.resume();
    },
    /**
     * Collects orphaned Ext.dom.Elements by removing their listeners and evicting them
     * from the cache.  Runs on a regularly scheduled {@link #interval} but can be called
     * directly to force garbage collection.
     * @return {String[]} An array containing the IDs of the elements that were garbage
     * collected, prefixed by their tag names.  Only applies in dev mode.  Returns nothing
     * in a production build.
     */
    collect: function() {
        var me = this,
            cache = Ext.cache,
            eid, dom, el, t, isGarbage, tagName;
        var collectedIds = [];
        // eslint-disable-line vars-on-top, one-var
        for (eid in cache) {
            if (!cache.hasOwnProperty(eid)) {
                
                continue;
            }
            el = cache[eid];
            if (el.skipGarbageCollection) {
                
                continue;
            }
            dom = el.dom;
            // Should always have a DOM node
            if (!dom) {
                Ext.raise('Missing DOM node in element garbage collection: ' + eid);
            }
            try {
                // In IE, accessing any properties of the window object of an orphaned iframe
                // can result in a "Permission Denied" error.  The same error also occurs
                // when accessing any properties of orphaned documentElement or body inside
                // of an iframe (documentElement and body become orphaned when the iframe
                // contentWindow is unloaded)
                isGarbage = Ext.isGarbage(dom);
            } catch (e) {
                // if an error was thrown in isGarbage it is most likely because we are
                // dealing with an inaccessible window or documentElement inside an orphaned
                // iframe in IE.  In this case we can't do anything except remove the
                // cache entry.
                delete cache[eid];
                collectedIds.push('#' + el.id);
                
                continue;
            }
            if (isGarbage) {
                isGarbage = false;
                if (el && el.dom) {
                    tagName = el.dom.tagName;
                    el.collect();
                    collectedIds.push((tagName ? tagName : '') + '#' + el.id);
                }
            }
        }
        // Cleanup IE Object leaks
        if (Ext.isIE9m) {
            t = {};
            for (eid in cache) {
                if (cache.hasOwnProperty(eid)) {
                    t[eid] = cache[eid];
                }
            }
            Ext.cache = Ext.dom.Element.cache = t;
        }
        me.lastTime = Ext.now();
        return collectedIds;
    },
    onTick: function() {
        this.timerId = null;
        if (Ext.enableGarbageCollector) {
            this.collect();
        }
        this.resume();
    },
    /**
     * Pauses the timer and stops garbage collection
     */
    pause: function() {
        var timerId = this.timerId;
        if (timerId) {
            this.timerId = null;
            Ext.undefer(timerId);
        }
    },
    /**
     * Resumes garbage collection at the specified {@link #interval}
     */
    resume: function() {
        var me = this,
            lastTime = me.lastTime;
        if (Ext.enableGarbageCollector && (Ext.now() - lastTime) > me.interval) {
            me.collect();
        }
        if (!me.timerId) {
            me.timerId = Ext.defer(me.onTick, me.interval);
        }
    }
});

/**
 * Processes the touch-action css property for an Ext.dom.Element, and provides
 * compatible behavior on devices that do not support pointer events.
 * @private
 */
Ext.define('Ext.dom.TouchAction', {
    singleton: true,
    requires: [
        'Ext.dom.Element',
        'Ext.util.Point'
    ],
    lastTouchStartTime: 0,
    /**
     * @property
     * The minimum distance a touch must move before being cancelled (only applicable
     * on browsers that use touch events).  Allows the direction of movement to be detected
     * so that panX and panY can be separately cancelled.
     * @private
     */
    minMoveDistance: 8,
    spaceRe: /\s+/,
    preventSingle: null,
    preventMulti: null,
    disabledOverflowDom: null,
    panXCls: Ext.baseCSSPrefix + 'touch-action-pan-x',
    panYCls: Ext.baseCSSPrefix + 'touch-action-pan-y',
    cssValues: [
        'none',
        'pan-x',
        'pan-y',
        'pan-x pan-y',
        'pinch-zoom',
        'pan-x pinch-zoom',
        'pan-y pinch-zoom',
        'pan-x pan-y pinch-zoom',
        'double-tap-zoom',
        'pan-x double-tap-zoom',
        'pan-y double-tap-zoom',
        'pan-x pan-y double-tap-zoom',
        'pinch-zoom double-tap-zoom',
        'pan-x pinch-zoom double-tap-zoom',
        'pan-y pinch-zoom double-tap-zoom',
        ''
    ],
    objectValues: [
        {
            panX: false,
            panY: false,
            pinchZoom: false,
            doubleTapZoom: false
        },
        {
            panX: true,
            panY: false,
            pinchZoom: false,
            doubleTapZoom: false
        },
        {
            panX: false,
            panY: true,
            pinchZoom: false,
            doubleTapZoom: false
        },
        {
            panX: true,
            panY: true,
            pinchZoom: false,
            doubleTapZoom: false
        },
        {
            panX: false,
            panY: false,
            pinchZoom: true,
            doubleTapZoom: false
        },
        {
            panX: true,
            panY: false,
            pinchZoom: true,
            doubleTapZoom: false
        },
        {
            panX: false,
            panY: true,
            pinchZoom: true,
            doubleTapZoom: false
        },
        {
            panX: true,
            panY: true,
            pinchZoom: true,
            doubleTapZoom: false
        },
        {
            panX: false,
            panY: false,
            pinchZoom: false,
            doubleTapZoom: true
        },
        {
            panX: true,
            panY: false,
            pinchZoom: false,
            doubleTapZoom: true
        },
        {
            panX: false,
            panY: true,
            pinchZoom: false,
            doubleTapZoom: true
        },
        {
            panX: true,
            panY: true,
            pinchZoom: false,
            doubleTapZoom: true
        },
        {
            panX: false,
            panY: false,
            pinchZoom: true,
            doubleTapZoom: true
        },
        {
            panX: true,
            panY: false,
            pinchZoom: true,
            doubleTapZoom: true
        },
        {
            panX: false,
            panY: true,
            pinchZoom: true,
            doubleTapZoom: true
        },
        {
            panX: true,
            panY: true,
            pinchZoom: true,
            doubleTapZoom: true
        }
    ],
    attributeName: 'data-extTouchAction',
    constructor: function() {
        var me = this,
            supports = Ext.supports;
        if (supports.TouchAction) {
            me.cssProp = 'touch-action';
        } else if (supports.MSPointerEvents) {
            me.cssProp = '-ms-touch-action';
        }
        if (supports.TouchEvents) {
            Ext.getWin().on({
                touchstart: 'onTouchStart',
                touchmove: 'onTouchMove',
                touchend: 'onTouchEnd',
                scope: me,
                delegated: false,
                translate: false,
                capture: true,
                priority: 5000
            });
            Ext.on({
                scroll: 'onScroll',
                scope: me,
                destroyable: true
            });
        }
        if (Ext.isFunction(Object.freeze)) {
            /* eslint-disable-next-line vars-on-top, one-var */
            var objectValues = me.objectValues,
                i, ln;
            for (i = 0 , ln = objectValues.length; i < ln; i++) {
                Object.freeze(objectValues[i]);
            }
        }
    },
    /**
     * Returns true if all of the event's targets are contained within the element
     * @param {HTMLElement} dom
     * @param {Ext.event.Event} e
     * @private
     * @return {Boolean}
     */
    containsTargets: function(dom, e) {
        var contains = true,
            event = e.browserEvent,
            touches = e.type === 'touchend' ? event.changedTouches : event.touches,
            i, ln;
        for (i = 0 , ln = touches.length; i < ln; i++) {
            if (!dom.contains(touches[i].target)) {
                contains = false;
                break;
            }
        }
        return contains;
    },
    /**
     * Forces overflow to 'hidden' on the x or y axis starting with the "el" and ascending
     * upward to all ancestors that have overflow 'auto' or 'scroll' on the given axis.
     * The added classes will remain in place until the end of the current gesture (when
     * the final touchend event is received) at which point they will be removed by invoking
     * {@link #resetOverflow}.
     *
     * This is invoked at the beginning of a gesture when we make the initial determination
     * that we are disabling scrolling on one of the axes (because touch-action contains
     * pan-x or pan-y in the value, but not both).  Dynamically manipulating the overflow
     * in this way vs just adding a static class ensures that the non-touch-scrolling axis
     * can still be scrolled using the mouse.
     *
     * We only do this on browsers that do not have space-consuming scrollbars (e.g. on
     * android, but not on chrome desktop) to avoid a situation where scrollbars disappear
     * during the gesture and re-appear afterwards.
     *
     * We also skip this on iOS because of the following bugs in safari (already filed with apple):
     * 1. Dynamically setting scroll position to hidden on either axis resets visual scroll
     * position to 0:
     * https://gist.github.com/pguerrant/105e8d91e3ffcb1b6e2eed7ecc0571d3
     * 2. Scrolling an element that has overflow set to hidden on either axis causes scroll
     * position to be reset to 0 on the hidden axis:
     * https://gist.github.com/pguerrant/e959c47a6b1d4b841cc3267a61950f33
     *
     * The downside is that on iOS, and on desktop-touch hybrid browsers such as chrome once
     * the user initiates scrolling in an allowed direction, it cannot be disabled in the
     * disallowed direction, This trade-off seems better than the alternatives -
     * vanishing/reappearing scrollbars on desktop, and scroll positions resetting to 0 on iOS.
     *
     * @param {HTMLElement} dom
     * @param {Boolean} [vertical=false] `true` to disable scrolling on the y axis, `false`
     * to disable scrolling on the x axis
     *
     * @private
     */
    disableOverflow: function(dom, vertical) {
        var me = this,
            overflowName = vertical ? 'overflow-y' : 'overflow-x',
            overflowStyle, cls;
        if (!me.disabledOverflowDom && !Ext.isiOS && !Ext.scrollbar.width()) {
            me.disabledOverflowDom = dom;
            cls = vertical ? me.panXCls : me.panYCls;
            while (dom) {
                overflowStyle = Ext.fly(dom).getStyle(overflowName);
                if (overflowStyle === 'auto' || overflowStyle === 'scroll') {
                    Ext.fly(dom).addCls(cls);
                }
                dom = dom.parentNode;
            }
        }
    },
    /**
     * Returns the touch action for the passed HTMLElement
     * @param {HTMLElement} dom
     * @return {Object}
     */
    get: function(dom) {
        var flags = dom.getAttribute(this.attributeName),
            ret = null;
        if (flags != null) {
            ret = this.objectValues[flags];
        }
        return ret;
    },
    /**
     * Accepts a touch action in the object form accepted by
     * {@link Ext.Component}, and converts it to a number representing the desired touch action(s).
     *
     * All touchActions absent from the passed object are defaulted to true.
     *
     * @param {Object} touchAction
     * @returns {Number} A number representing the touch action using the following mapping:
     *
     *     panX            1  "00000001"
     *     panY            2  "00000010"
     *     pinchZoom       4  "00000100"
     *     doubleTapZoom   8  "00001000"
     *
     * 0 represents a css value of "none" and all bits on is the same as "auto"
     * @private
     */
    getFlags: function(touchAction) {
        var flags;
        if (typeof touchAction === 'number') {
            flags = touchAction;
        } else {
            flags = 0;
            if (touchAction.panX !== false) {
                flags |= 1;
            }
            if (touchAction.panY !== false) {
                flags |= 2;
            }
            if (touchAction.pinchZoom !== false) {
                flags |= 4;
            }
            if (touchAction.doubleTapZoom !== false) {
                flags |= 8;
            }
        }
        return flags;
    },
    isScrollable: function(el, vertical, forward) {
        var overflowStyle = Ext.fly(el).getStyle(vertical ? 'overflow-y' : 'overflow-x'),
            isScrollable = (overflowStyle === 'auto' || overflowStyle === 'scroll');
        if (isScrollable) {
            if (vertical) {
                isScrollable = forward ? (el.scrollTop + el.clientHeight) < el.scrollHeight : el.scrollTop > 0;
            } else {
                isScrollable = forward ? (el.scrollLeft + el.clientWidth) < el.scrollWidth : el.scrollLeft > 0;
            }
        }
        return isScrollable;
    },
    lookupFlags: function(dom) {
        return parseInt((dom.getAttribute && dom.getAttribute(this.attributeName)) || 15, 10);
    },
    onScroll: function() {
        // This flag tracks whether or not a scroll has occurred since the last touchstart event
        this.scrollOccurred = true;
        // once scrolling begins we cannot attempt to preventDefault on the touchend event
        // or chrome will issue warnings in the console.
        this.isDoubleTap = false;
    },
    onTouchEnd: function(e) {
        var me = this,
            dom = e.target,
            touchCount, flags, doubleTapZoom;
        touchCount = e.browserEvent.touches.length;
        if (touchCount === 0) {
            if (me.isDoubleTap) {
                while (dom) {
                    flags = me.lookupFlags(dom);
                    if (flags != null) {
                        doubleTapZoom = flags & 8;
                        if (!doubleTapZoom) {
                            e.preventDefault();
                        }
                    }
                    dom = dom.parentNode;
                }
            }
            me.isDoubleTap = false;
            me.preventSingle = null;
            me.preventMulti = null;
            me.resetOverflow();
        }
    },
    onTouchMove: function(e) {
        var me = this,
            prevent = null,
            dom = e.target,
            flags, touchCount, panX, panY, point, startPoint, isVertical, scale, distance, deltaX, deltaY, preventSingle, preventMulti;
        preventSingle = me.preventSingle;
        preventMulti = me.preventMulti;
        touchCount = e.browserEvent.touches.length;
        // Don't check for touchCount here when checking for preventMulti.
        // This ensures that if we determined not to cancel the multi-touch gesture
        // previously we will not attempt to start canceling once touch count is
        // reduced to one (If we do attempt to start canceling at that point chrome
        // will issue warnings in the console because scrolling has already started).
        if ((touchCount === 1 && (preventSingle === false)) || (preventMulti === false)) {
            return;
        }
        if ((touchCount > 1 && (preventMulti === true)) || (touchCount === 1 && (preventSingle === true))) {
            prevent = true;
        } else {
            if (touchCount === 1) {
                point = e.getPoint();
                startPoint = me.startPoint;
                scale = Ext.Element.getViewportScale();
                // account for scale so that move distance is actual screen pixels, not page pixels
                distance = point.getDistanceTo(me.startPoint) * scale;
                deltaX = point.x - startPoint.x;
                deltaY = point.y - startPoint.y;
                isVertical = Math.abs(deltaY) >= Math.abs(deltaX);
            }
            while (dom && (dom.nodeType === 1)) {
                flags = me.lookupFlags(dom);
                if (flags & 0) {
                    // touch-action: none
                    prevent = true;
                } else if (touchCount === 1) {
                    panX = !!(flags & 1);
                    panY = !!(flags & 2);
                    if (panX && panY) {
                        prevent = false;
                    } else if (!panX && !panY) {
                        prevent = true;
                    } else if (distance >= me.minMoveDistance) {
                        prevent = !!((panX && isVertical) || (panY && !isVertical));
                    }
                    // if the element itself is scrollable, and has no touch action
                    // preventing it from scrolling, allow it to scroll - do
                    // not allow an ancestor's touchAction to prevent scrolling
                    if (!prevent && me.isScrollable(dom, isVertical, (isVertical ? deltaY : deltaX) < 0)) {
                        // eslint-disable-line max-len
                        break;
                    }
                } else if (me.containsTargets(dom, e)) {
                    // multi-touch, all targets contained
                    prevent = !(flags & 4);
                } else {
                    // multi-touch and not all targets contained within element
                    prevent = false;
                }
                if (prevent) {
                    break;
                }
                dom = dom.parentNode;
            }
        }
        // In chrome preventing a touchmove event does not prevent the defualt
        // action such as scrolling from taking place on subsequent touchmove
        // events.  Setting these flags tells us to prevent the touchmove event
        // for the remainder of the gesture.
        // explicitly setting these flags to false means do not prevent this gesture
        // going forward. This prevents chrome from complaining because we
        // called preventDefault() after scrolling has already started
        if (touchCount === 1) {
            me.preventSingle = prevent;
        } else if (touchCount > 1) {
            me.preventMulti = prevent;
        }
        if (prevent) {
            e.preventDefault();
        }
    },
    onTouchStart: function(e) {
        var me = this,
            time, flags, dom, panX, panY;
        if (e.browserEvent.touches.length === 1) {
            time = e.time;
            // Use a time of 500ms between touchstart events to detecting a double tap that
            // might possibly cause the screen to zoom.  Although in reality this is usually
            // 300ms iOS can sometimes take a bit longer so 500 seems safe.
            // Can't be a double tap if a scroll occurred in between this touch and the previous
            // one.
            if (!me.scrollOccurred && ((time - me.lastTouchStartTime) <= 500)) {
                me.isDoubleTap = true;
            }
            me.lastTouchStartTime = time;
            me.scrollOccurred = false;
            me.startPoint = e.getPoint();
            dom = e.target;
            while (dom) {
                flags = me.lookupFlags(dom);
                if (flags != null) {
                    panX = !!(flags & 1);
                    panY = !!(flags & 2);
                    if (panX !== panY) {
                        me.disableOverflow(dom, panX);
                        break;
                    }
                }
                dom = dom.parentNode;
            }
        } else {
            // multi touch is never a double tap
            me.isDoubleTap = false;
        }
    },
    /**
     * Removes any classes that were added using {@link #disableOverflow}
     */
    resetOverflow: function() {
        var me = this,
            dom = me.disabledOverflowDom;
        while (dom) {
            Ext.fly(dom).removeCls([
                me.panXCls,
                me.panYCls
            ]);
            dom = dom.parentNode;
        }
        me.disabledOverflowDom = null;
    },
    /**
     * Sets the touch action value for an element
     * @param {HTMLElement} dom The dom element
     * @param {Object/Number} value The touch action as an object with touch action names
     * as keys and boolean values, or as a bit flag (see {@link #getFlags})
     *
     * For example the following two calls are equivalent:
     *
     *     Ext.dom.TouchAction.set(domElement, {
     *         panX: false,
     *         pinchZoom: false
     *     });
     *
     *     Ext.dom.TouchAction.set(domElement, 5);
     *
     * valid touch action names are:
     *
     * - `'panX'`
     * - `'panY'`
     * - `'pinchZoom'`
     * - `'doubleTapZoom'`
     *
     * @private
     */
    set: function(dom, value) {
        var me = this,
            cssProp = me.cssProp,
            flags = me.getFlags(value),
            // We can only set values for CSS touch-action in the dom if they are supported
            // by the browser, otherwise the entire touch-action property is ignored.
            supportedFlags = (flags & Ext.supports.TouchAction),
            attributeName = me.attributeName;
        if (cssProp) {
            Ext.fly(dom).setStyle(cssProp, me.cssValues[supportedFlags]);
        }
        if (flags === 15) {
            dom.removeAttribute(attributeName);
        } else {
            dom.setAttribute(attributeName, flags);
        }
    }
});

/**
 * Provides constraining behavior for a {@link Ext.drag.Source}.
 */
Ext.define('Ext.drag.Constraint', {
    alias: 'drag.constraint.base',
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    factoryConfig: {
        defaultType: 'base',
        type: 'drag.constraint'
    },
    config: {
        /**
         * @cfg {Boolean/String/HTMLElement/Ext.dom.Element} element
         *
         * The element to constrain to:
         * - `true` to constrain to the parent of the {@link Ext.drag.Source#element}.
         * - The id, DOM element or Ext.dom.Element to constrain to.
         */
        element: null,
        /**
         * @cfg {Boolean} horizontal
         * `true` to limit dragging to the horizontal axis.
         */
        horizontal: null,
        /**
         * @cfg {Ext.util.Region} region
         *
         * The region to constrain to.
         */
        region: null,
        /**
         * @cfg {Number/Object} snap
         * The interval to move this drag target during a drag in both dimensions.
         * - `{x: 30}`, snap only x
         * - `{y: 30}`, snap only y
         * - `{x: 30, y: 40}`, snap both
         * - `40`, snap both to `40`.
         *
         * The snap may also be a function to calculate the snap value on each tick.
         *
         *      snap: {
         *          x: function(info, x) {
         *              return x < 300 ? 150 : 500;
         *          }
         *      }
         */
        snap: null,
        /**
         * @cfg {Ext.drag.Source} source
         * The {@link Ext.drag.Source source} for the constraint. This will be
         * set automatically when constructed via the source.
         */
        source: null,
        /**
         * @cfg {Boolean} vertical
         * `true` to limit dragging to the vertical axis.
         */
        vertical: null,
        /**
         * @cfg {Number[]} x
         * The minimum and maximum x position. Use `null` to
         * not set a constraint:
         * - `[100, null]`, constrain only the minimum
         * - `[null, 100]`, constrain only the maximum
         * - `[200, 200]`, constrain both.
         */
        x: null,
        /**
         * @cfg {Number[]} y
         * The minimum and maximum y position. Use `null` to
         * not set a constraint:
         * - `[100, null]`, constrain only the minimum
         * - `[null, 100]`, constrain only the maximum
         * - `[200, 200]`, constrain both.
         */
        y: null
    },
    constructor: function(config) {
        this.initConfig(config);
    },
    applyElement: function(element) {
        if (element && typeof element !== 'boolean') {
            element = Ext.get(element);
        }
        return element || null;
    },
    applySnap: function(snap) {
        if (typeof snap === 'number') {
            snap = {
                x: snap,
                y: snap
            };
        }
        return snap;
    },
    /**
     * Constrain the position of the drag proxy using the configured rules.
     *
     * @param {Number[]} xy The position.
     * @param {Ext.drag.Info} info The drag information.
     *
     * @return {Number[]} The xy position.
     */
    constrain: function(xy, info) {
        var me = this,
            x = xy[0],
            y = xy[1],
            constrainInfo = me.constrainInfo,
            initial = constrainInfo.initial,
            constrainX = constrainInfo.x,
            constrainY = constrainInfo.y,
            snap = constrainInfo.snap,
            min, max;
        if (!constrainInfo.vertical) {
            if (snap && snap.x) {
                if (snap.xFn) {
                    x = snap.x.call(me, info, x);
                } else {
                    x = me.doSnap(x, initial.x, snap.x);
                }
            }
            if (constrainX) {
                min = constrainX[0];
                max = constrainX[1];
                if (min !== null && x < min) {
                    x = min;
                }
                if (max !== null && x > max) {
                    x = max;
                }
            }
        } else {
            x = initial.x;
        }
        if (!constrainInfo.horizontal) {
            if (snap && snap.y) {
                if (snap.yFn) {
                    y = snap.y.call(me, info, y);
                } else {
                    y = me.doSnap(y, initial.y, snap.y);
                }
            }
            if (constrainY) {
                min = constrainY[0];
                max = constrainY[1];
                if (min !== null && y < min) {
                    y = min;
                }
                if (max !== null && y > max) {
                    y = max;
                }
            }
        } else {
            y = initial.y;
        }
        return [
            x,
            y
        ];
    },
    destroy: function() {
        this.setSource(null);
        this.setElement(null);
        this.callParent();
    },
    privates: {
        /**
         * Constrains 2 values, while taking into
         * account nulls.
         * @param {Number} a The first value.
         * @param {Number} b The second value.
         * @param {Function} resolver The function to resolve the value if
         * both are non null.
         * @return {Number} If both values are `null`, `null`. If `a` is null, `b`.
         * If `b` is null, `a`, otherwise the result of the resolver, passing a & b.
         *
         * @private
         */
        constrainValue: function(a, b, resolver) {
            var val = null,
                aNull = a === null,
                bNull = b === null;
            if (!(aNull && bNull)) {
                if (aNull) {
                    val = b;
                } else if (bNull) {
                    val = a;
                } else {
                    val = resolver(a, b);
                }
            }
            return val;
        },
        /**
         * Calculates the position to move the proxy element
         * to when using snapping.
         *
         * @param {Number} position The current mouse position.
         * @param {Number} initial The start position.
         * @param {Number} snap The snap position.
         * @return {Number} The snapped position.
         *
         * @private
         */
        doSnap: function(position, initial, snap) {
            if (!snap) {
                return position;
            }
            /* eslint-disable-next-line vars-on-top */
            var ratio = (position - initial) / snap,
                floor = Math.floor(ratio);
            // Check whether we need to snap less than current, or
            // greater than current position.
            if (ratio - floor <= 0.5) {
                ratio = floor;
            } else {
                ratio = floor + 1;
            }
            return initial + (snap * ratio);
        },
        /**
         * Setup data that is needed during a drag for monitoring constraints.
         * Attempt to merge min/max values with any constrain region.
         *
         * @param {Ext.drag.Info} info The drag info.
         *
         * @private
         */
        onDragStart: function(info) {
            var me = this,
                snap = me.getSnap(),
                vertical = !!me.getVertical(),
                horizontal = !!me.getHorizontal(),
                element = me.getElement(),
                region = me.getRegion(),
                proxy = info.proxy,
                proxyEl = proxy.element,
                x = me.getX(),
                y = me.getY(),
                minX = null,
                maxX = null,
                minY = null,
                maxY = null,
                rminX = null,
                rmaxX = null,
                rminY = null,
                rmaxY = null,
                pos, size;
            if (element) {
                if (typeof element === 'boolean') {
                    element = me.getSource().getElement().parent();
                }
                if (info.local) {
                    pos = element.getStyle('position');
                    if (pos === 'relative' || pos === 'absolute') {
                        size = element.getSize();
                        region = new Ext.util.Region(0, size.width, size.height, 0);
                    } else {
                        region = element.getRegion(true, true);
                    }
                } else {
                    region = element.getRegion(true);
                }
            }
            if (region) {
                if (!vertical) {
                    rminX = region.left;
                    rmaxX = region.right - (proxyEl ? proxy.width : 0);
                }
                if (!horizontal) {
                    rminY = region.top;
                    rmaxY = region.bottom - (proxyEl ? proxy.height : 0);
                }
            }
            // The following piece sets up the numeric values for our constraint.
            // If there is an axis constraint, don't bother calculating the values since
            // it is already explicitly constrained so we can shortcut that portion.
            //
            // Attempt to merge the appropriate min/max values (if needed). With:
            // a) A region and a minimum, the larger value is needed (stricter constraint)
            // b) A region and a maximum, the smaller value is needed (stricter constraint)
            if (!vertical && (region || x)) {
                if (x) {
                    minX = x[0];
                    maxX = x[1];
                }
                if (minX !== null || maxX !== null || rminX !== null || rmaxX !== null) {
                    minX = me.constrainValue(minX, rminX, Math.max);
                    maxX = me.constrainValue(maxX, rmaxX, Math.min);
                    x = [
                        minX,
                        maxX
                    ];
                }
            }
            if (!horizontal && (region || y)) {
                if (y) {
                    minY = y[0];
                    maxY = y[1];
                }
                if (minY !== null || maxY !== null || rminY !== null || rmaxY !== null) {
                    minY = me.constrainValue(minY, rminY, Math.max);
                    maxY = me.constrainValue(maxY, rmaxY, Math.min);
                    y = [
                        minY,
                        maxY
                    ];
                }
            }
            if (snap) {
                snap = {
                    x: snap.x,
                    xFn: typeof snap.x === 'function',
                    y: snap.y,
                    yFn: typeof snap.y === 'function'
                };
            }
            me.constrainInfo = {
                initial: info.element.initial,
                vertical: vertical,
                horizontal: horizontal,
                x: x,
                y: y,
                snap: snap
            };
        }
    }
});

/**
 * This class is used to unify information for a specific drag instance.
 * This object is passed to template methods and events to obtain
 * details about the current operation.
 *
 * It is not expected that this class will be created by user code.
 */
Ext.define('Ext.drag.Info', {
    requires: [
        'Ext.Promise'
    ],
    constructor: function(source, e) {
        // Internally we will call the constructor empty when we want to clone.
        if (!source) {
            return;
        }
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            local = source.getLocal(),
            el, proxyEl, proxy, x, xy, y, pageXY, elPageXY;
        me.source = source;
        me.local = local;
        xy = me.getEventXY(e);
        pageXY = e.getXY();
        el = source.getElement();
        elPageXY = el.getXY();
        xy = local ? el.getLocalXY() : elPageXY;
        x = xy[0];
        y = xy[1];
        me.initialEvent = e;
        me.eventTarget = e.target;
        me.cursor = {
            current: {
                x: x,
                y: y
            },
            delta: {
                x: 0,
                y: 0
            },
            initial: {
                x: pageXY[0],
                y: pageXY[1]
            },
            offset: {
                x: pageXY[0] - elPageXY[0],
                y: pageXY[1] - elPageXY[1]
            }
        };
        me.element = {
            current: {
                x: x,
                y: y
            },
            delta: {
                x: 0,
                y: 0
            },
            initial: {
                x: x,
                y: y
            }
        };
        me.proxy = {
            instance: source.getProxy(),
            current: {
                x: x,
                y: y
            },
            delta: {
                x: 0,
                y: 0
            },
            initial: {
                x: x,
                y: y
            },
            element: el,
            isUnderCursor: false,
            isElement: true
        };
        me.types = [];
        me.data = {};
        source.describe(me);
        proxy = me.proxy;
        proxyEl = proxy.instance.setupElement(me);
        proxy.isElement = proxyEl === source.getElement();
        proxy.element = proxyEl;
        if (proxyEl) {
            proxy.width = proxyEl.getWidth();
            proxy.height = proxyEl.getHeight();
        }
        if (proxy.isElement) {
            // If they are the same we don't need to keep track of both
            el = me.element;
            el.current = proxy.current;
            el.delta = proxy.delta;
        }
        me.needsCursorCheck = proxy.element && source.manager && source.manager.pointerBug;
    },
    /**
     * @property {Object} cursor
     * Information about the cursor position. Not available when
     * {@link #isNative} is `true`.
     * 
     *
     * @property {Object} cursor.current
     * The current cursor position.
     *
     * @property {Number} cursor.current.x
     * The current x position.
     *
     * @property {Number} cursor.current.y
     * The current y position.
     *
     *
     * @property {Object} cursor.delta
     * The change in cursor position.
     *
     * @property {Number} cursor.delta.x
     * The change in x position.
     *
     * @property {Number} cursor.delta.y
     * The change in y position.
     * 
     *
     * @property {Object} cursor.initial
     * The intial cursor position.
     *
     * @property {Number} cursor.initial.x
     * The initial x position.
     *
     * @property {Number} cursor.initial.y
     * The initial y position.
     * 
     *
     * @property {Object} cursor.offset
     * The offset from the cursor to the top/left of
     * the {@link Ext.drag.Source#element element}.
     * 
     * @property {Number} cursor.offset.x
     * The x offset.
     *
     * @property {Number} cursor.offset.y
     * The y offset.
     */
    cursor: null,
    /**
     * @property {Object} element
     * Information about the {@link Ext.drag.Source#element} position. 
     * Not available when {@link #isNative} is `true`.
     * 
     *
     * @property {Object} element.current
     * The current element position.
     *
     * @property {Number} element.current.x
     * The current x position.
     *
     * @property {Number} element.current.y
     * The current y position.
     *
     *
     * @property {Object} element.delta
     * The change in element position.
     *
     * @property {Number} element.delta.x
     * The change in x position.
     *
     * @property {Number} element.delta.y
     * The change in y position.
     * 
     *
     * @property {Object} element.initial
     * The intial element position.
     *
     * @property {Number} element.initial.x
     * The initial x position.
     *
     * @property {Number} element.initial.y
     * The initial y position.
     */
    element: null,
    /**
     * @property {HTMLElement} eventTarget
     * The event target that the drag started on.
     *
     * Not available when {@link #isNative} is `true`.
     */
    eventTarget: null,
    /**
     * @property {FileList} files
     * A list of files included for this drag. See:
     * https://developer.mozilla.org/en/docs/Web/API/FileList
     *
     * Only available when {@link #isNative} is `true`.
     */
    files: null,
    /**
     * @property {Boolean} isNative
     * `true` if the drag is a native drag event, for example
     * a file draggedi nto the browser.
     */
    isNative: false,
    /**
     * @property {Object} proxy
     * Information about the {@link Ext.drag.Source#proxy} position.
     * This may be the actual {@link Ext.drag.Source#element}.
     * Not available when {@link #isNative} is `true`.
     * 
     *
     * @property {Object} proxy.current
     * The current proxy position.
     *
     * @property {Number} proxy.current.x
     * The current x position.
     *
     * @property {Number} proxy.current.y
     * The current y position.
     *
     *
     * @property {Object} proxy.delta
     * The change in proxy position.
     *
     * @property {Number} proxy.delta.x
     * The change in x position.
     *
     * @property {Number} proxy.delta.y
     * The change in y position.
     * 
     *
     * @property {Object} proxy.initial
     * The intial proxy position.
     *
     * @property {Number} proxy.initial.x
     * The initial x position.
     *
     * @property {Number} proxy.initial.y
     * The initial y position.
     *
     * @property {Ext.dom.Element} proxy.element
     * The proxy element.
     *
     * @property {Boolean} proxy.isElement
     * `true` if the proxy is the {@link Ext.drag.Source#element}.
     *
     * @property {Boolean} proxy.isUnderCursor
     * `true` if the alignment causes the proxy to be under the cursor.
     */
    proxy: null,
    /**
     * @property {Ext.drag.Source} source
     * The drag source. Not available when {@link #isNative} is `true`.
     */
    source: null,
    /**
     * @property {Ext.drag.Target} target
     * The active target. `null` if not over a target.
     */
    target: null,
    /**
     * @property {String[]} types
     * The data types this drag provides. Added via {@link #setData}.
     */
    types: null,
    /**
     * @property {Boolean} valid
     * `true` if the {@link #target} is valid. See {@link Ext.drag.Target} for
     * information about validity. `false` if there is no target.
     */
    valid: false,
    /**
     * Clear the data for a particular type.
     * @param {String} type The type.
     */
    clearData: function(type) {
        Ext.Array.remove(this.types, type);
        delete this.data[type];
    },
    /**
     * Create a copy of this object with the current state.
     * @return {Ext.drag.Info} A copy of this object.
     */
    clone: function() {
        var me = this,
            ret = new Ext.drag.Info();
        ret.cursor = Ext.merge({}, me.cursor);
        ret.data = Ext.apply({}, me.data);
        ret.element = Ext.merge({}, me.element);
        ret.eventTarget = me.eventTarget;
        ret.proxy = Ext.merge({}, me.proxy);
        ret.source = me.source;
        ret.target = me.target;
        ret.types = Ext.Array.clone(me.types);
        ret.valid = me.valid;
        return ret;
    },
    /**
     * Get data for this drag. This method may only be called once the drop completes.
     * 
     * @param {String} type The type of data to retrieve. Must be in the {@link #types}.
     * See also {@link #setData}.
     * 
     * @return {Ext.Promise} The data. If the produced data is not a {@link Ext.Promise},
     * it will be wrapped in one.
     */
    getData: function(type) {
        var me = this,
            data = me.data,
            dt = me.dataTransfer,
            ret;
        if (dt) {
            ret = dt.getData(type);
        } else {
            if (!me.finalized) {
                Ext.raise('Unable to call getData until the drop is complete');
            }
            ret = data[type];
            if (typeof ret === 'function') {
                data[type] = ret = ret.call(me.source, me);
            }
            if (!ret && ret !== 0) {
                ret = '';
            }
        }
        return Ext.Promise.resolve(ret);
    },
    /**
     * Set data for this drag. Multiple types may be registered. Each type will be
     * added to {@link #types}.
     * 
     * @param {String} type The type of data being registered.
     * @param {Object/Function} value The value being registered. If a function
     * is provided it will be evaluated if requested when the drop completes. The
     * function should return a value or a {@link Ext.Promise} that will produce a value.
     */
    setData: function(type, value) {
        Ext.Array.include(this.types, type);
        this.data[type] = value;
    },
    destroy: function() {
        var me = this;
        me.eventTarget = me.data = me.proxy = me.targetMap = me.targetMap = me.types = me.elementMap = me.possibleTargets = me.target = null;
        me.callParent();
    },
    privates: {
        /**
         * @property {Object} data
         * The underlying data for this drag. Keyed by type, the value
         * can be a value or a function to return a value.
         *
         * @private
         */
        data: null,
        /**
         * @property {DataTransfer} dataTransfer
         * The browser native dataTransfer object, if available.
         *
         * @private
         */
        dataTransfer: null,
        /**
        * @property {Object} elementMap
        * A map of targets that is keyed by the element 
        * id for each drag target. Only provided in point mode.
        *
        * @private
        */
        elementMap: null,
        /**
        * @property {Object} possibleTargets
        * The map of targets that this drag can interact with, meaning
        * those with not matching groups and disabled items are excluded.
        *
        * @private
        */
        possibleTargets: null,
        /**
        * @property {Object} targetMap
        * A map of targets that is keyed by id when the drag began.
        *
        * @private
        */
        targetMap: null,
        copyNativeData: function(target, e) {
            var dt = e.browserEvent.dataTransfer;
            this.target = target;
            this.dataTransfer = dt;
            this.files = dt.files;
        },
        /**
         * Notify that the drag is finished and final processing can occur.
         *
         * @private
         */
        finalize: function() {
            var me = this,
                target = me.target;
            me.finalized = true;
            if (target) {
                target.info = null;
                target.handleDrop(me);
            }
        },
        /**
         * Calculate the current position of the proxy element, taking
         * into account constraints.
         * @param {Number} x The cursor x position.
         * @param {Number} y The cursor y position.
         *
         * @return {Number[]} The position.
         *
         * @private
         */
        getAlignXY: function(x, y) {
            var me = this,
                source = me.source,
                cursorOffset = me.cursor.offset,
                proxy = source.getProxy(),
                proxyEl = me.proxy.element,
                constrain = source.getConstrain(),
                xy = [
                    x,
                    y
                ];
            if (proxyEl) {
                if (me.proxy.isElement) {
                    xy[0] -= cursorOffset.x;
                    xy[1] -= cursorOffset.y;
                } else {
                    xy = proxy.adjustCursorOffset(me, xy);
                }
                if (constrain) {
                    xy = constrain.constrain(xy, me);
                }
            }
            return xy;
        },
        getEventXY: function(e) {
            var xy = e.getXY(),
                // page coordinates
                source = this.source;
            if (this.local) {
                xy = source.convertToLocalXY(xy);
            }
            return xy;
        },
        onNativeDragEnter: function(target, e) {
            var me = this;
            me.valid = target.accepts(me);
            target.info = me;
            me.copyNativeData(target, e);
        },
        onNativeDragLeave: function(target, e) {
            var me = this;
            // With native events, enter fires before leave, so when the leave fires
            // check that we are the current target, another target may have already
            // taken over here
            if (me.target === target) {
                target.info = null;
                me.valid = false;
                me.target = me.dataTransfer = me.files = null;
            }
        },
        onNativeDragMove: function(target, e) {
            this.copyNativeData(target, e);
        },
        onNativeDrop: function(target, e) {
            this.copyNativeData(target, e);
            target.info = null;
        },
        /**
         * Mark the target as active for the drag.
         * @param {Ext.drag.Target} target The target.
         *
         * @private
         */
        setActive: function(target) {
            var me = this,
                source = me.source,
                current = me.target,
                changed = current !== target;
            if (current && changed) {
                current.handleDragLeave(me);
                current.info = null;
            }
            me.target = target;
            if (target) {
                if (changed) {
                    me.valid = !!me.possibleTargets[target.getId()] && target.accepts(me) !== false;
                    target.handleDragEnter(me);
                    target.info = me;
                }
                target.handleDragMove(me);
            } else {
                me.valid = false;
            }
            if (changed) {
                source.getProxy().update(me);
            }
        },
        /**
         * Update with the current position information.
         * @param {Ext.event.Event} event The event.
         * @param {Boolean} beforeStart `true` if the update is occurring
         * before the drag starts.
         *
         * @private
         */
        update: function(event, beforeStart) {
            var me = this,
                xy = me.getEventXY(event),
                x = xy[0],
                y = xy[1],
                alignXY = me.getAlignXY(x, y),
                alignX = alignXY[0],
                alignY = alignXY[1],
                proxyData = me.proxy,
                cursor = me.cursor,
                current = cursor.current,
                delta = cursor.delta,
                initial = cursor.initial,
                proxy = proxyData.instance;
            current.x = x;
            current.y = y;
            delta.x = x - initial.x;
            delta.y = y - initial.y;
            current = proxyData.current;
            delta = proxyData.delta;
            initial = proxyData.initial;
            current.x = alignX;
            current.y = alignY;
            delta.x = alignX - initial.x;
            delta.y = alignY - initial.y;
            if (me.needsCursorCheck) {
                proxyData.isUnderCursor = !(x < alignX || y < alignY || x > proxyData.width + alignX || y > proxyData.height + alignY);
            }
            // eslint-disable-line max-len
            if (!beforeStart && proxy) {
                proxy.setXY(me, alignXY);
            }
        }
    }
});

/**
 * A base class for draggable and droppable items that wrap a DOM element.
 *
 * @abstract
 */
Ext.define('Ext.drag.Item', {
    mixins: [
        'Ext.mixin.Observable',
        'Ext.mixin.Identifiable'
    ],
    config: {
        /**
         * @cfg {Boolean} autoDestroy
         * `true` to destroy the {@link #element} when this item is destroyed.
         */
        autoDestroy: true,
        /**
         * @cfg {Ext.Component} component
         * The component for this item. This implicity sets the `element` config to be
         * the component's primary `element`. By providing the `component`, drag operations
         * will act upon the component's `x` and `y` configs (if `floated`) or `left` and
         * `top` configs otherwise.
         * @since 6.5.0
         *
         * @private
         */
        component: null,
        /**
         * @cfg {String/HTMLElement/Ext.dom.Element} element
         * The id, dom or Element reference for this item.
         */
        element: null,
        /**
         * @cfg {String/String[]} groups
         * A group controls which {@link Ext.drag.Source sources} and {@link Ext.drag.Target}
         * targets can interact with each other. Only items that have the same (or intersecting)
         * groups will react to each other. Items with no groups will be in the default pool.
         */
        groups: null
    },
    constructor: function(config) {
        this.mixins.observable.constructor.call(this, config);
    },
    /**
     * Checks whether this item is currently disabled.
     * @return {Boolean} `true` if this item is disabled.
     */
    isDisabled: function() {
        return this.disabled;
    },
    /**
     * Disable the current item to disallow it from participating
     * in drag/drop operations.
     */
    disable: function() {
        this.disabled = true;
    },
    /**
     * Enable the current item to allow it to participate in
     * drag/drop operations.
     */
    enable: function() {
        this.disabled = false;
    },
    updateComponent: function(comp, was) {
        var el;
        if (comp) {
            el = comp.el;
        } else if (was && was.el === this.getElement()) {
            el = null;
        } else {
            return;
        }
        this.setElement(el);
    },
    applyElement: function(element) {
        return element ? Ext.get(element) : null;
    },
    updateElement: function() {
        this.setupListeners();
    },
    applyGroups: function(group) {
        if (typeof group === 'string') {
            group = [
                group
            ];
        }
        return group;
    },
    destroy: function() {
        var me = this,
            el = me.getElement();
        me.destroying = true;
        me.setElement(null);
        if (el && me.getAutoDestroy()) {
            el.destroy();
        }
        me.callParent();
    },
    // This just makes it hard to ask "was destroy() called?":
    // me.destroying = false; // removed in 7.0
    privates: {
        /**
        * @property {Boolean} disabled
        * `true` if this item is disabled.
        *
        * @private
        */
        disabled: false,
        convertToLocalXY: function(xy) {
            var c = this.getComponent();
            if (c) {
                xy = c.convertToLocalXY(xy);
            } else {
                xy = this.getElement().translateXY(xy[0], xy[1]);
                xy = [
                    xy.x,
                    xy.y
                ];
            }
            return xy;
        },
        /**
         * @method
         * Gets any listeners to attach for the current element.
         * @return {Object} The listeners for thie element.
         *
         * @private
         */
        getElListeners: Ext.privateFn,
        /**
         * Detach any existing listeners and add new listeners
         * to the element.
         * 
         * @private
         */
        setupListeners: function(element) {
            var me = this,
                elListeners = me.elListeners;
            element = element || me.getElement();
            if (elListeners) {
                elListeners.destroy();
                me.elListeners = null;
            }
            if (element) {
                me.elListeners = element.on(Ext.apply({
                    scope: me,
                    destroyable: true
                }, me.getElListeners()));
            }
        }
    }
});

/**
 * Acts as a mediator between sources and targets. 
 * 
 * Typically this class will not be used in user code.
 *
 * @private
 */
Ext.define('Ext.drag.Manager', {
    singleton: true,
    uses: [
        'Ext.mixin.Inheritable'
    ],
    /**
     * @property {String} dragCls
     * A class added to the body while a drag is active.
     *
     * @private
     */
    dragCls: Ext.baseCSSPrefix + 'drag-body',
    // If we need to use the current mousemove target to find the over el,
    // but pointer-events is not supported, AND the delta position does not place the mouse outside
    // of the dragEl, temporarily move the dragEl away, and fake the mousemove target by using
    // document.elementFromPoint while it's out of the way.
    // The pointer events implementation is bugged in IE9/10 and opera, so fallback even if they
    // report that they support it.
    // IE8m do not support it so they will auto fall back
    pointerBug: Ext.isTouch || (!Ext.supports.CSSPointerEvents || Ext.isIE10m || Ext.isOpera),
    constructor: function() {
        this.targets = {};
        this.nativeTargets = [];
        Ext.onReady(this.init, this);
    },
    init: function() {
        // The purpose of listening for these events is to track when a
        // native drag enters the document so we can create and maintain
        // a single drag.Info object for it. Need to use a "stack-like" mechanism
        // to track while elements are being entered/left, keeping a count is
        // not sufficient because Gecko can fire multiple events for the
        // same element in some instances. So just keep pushing/removing the
        // element from the tracking array. Once we hit 0, the drag is out
        // of the document. On drop, we clear it manually because there is
        // no longer an active drag.
        Ext.getDoc().on({
            scope: this,
            dragenter: {
                capture: true,
                fn: 'onNativeDragEnter'
            },
            dragleave: 'onNativeDragLeave',
            dragover: 'onNativeDragOver',
            drop: 'onNativeDrop'
        });
    },
    destroy: function() {
        var me = this,
            targets = me.targets,
            key;
        me.destroying = true;
        for (key in targets) {
            targets[key].destroy();
        }
        me.targets = null;
        me.callParent();
    },
    // This just makes it hard to ask "was destroy() called?":
    // me.destroying = false; // removed in 7.0
    privates: {
        /**
         * A shim for elementFromPoint to allow RTL behaviour.
         * @param {Number} x The x coordinate.
         * @param {Number} y The y coordinate
         * @return {HTMLElement} The element.
         *
         * @private
         */
        elementFromPoint: function(x, y) {
            if (Ext.rootInheritedState.rtl) {
                x = Ext.Element.getViewportWidth() - x;
            }
            return Ext.dom.Element.fromPagePoint(x, y, true);
        },
        /**
         * Get the matching target (if any) at a particular point.
         * @param {Ext.drag.Info} info The drag info.
         * @return {Ext.drag.Target} The matching target, `null` if not found.
         *
         * @private
         */
        getAtPoint: function(info) {
            var current = info.cursor.current,
                elementMap = info.elementMap,
                isUnderCursor = info.proxy.isUnderCursor,
                proxyEl = this.pointerBug && isUnderCursor ? info.proxy.element.dom : null,
                target, el;
            if (proxyEl) {
                proxyEl.style.visibility = 'hidden';
            }
            el = this.elementFromPoint(current.x, current.y);
            if (proxyEl) {
                proxyEl.style.visibility = 'visible';
            }
            while (el) {
                target = elementMap[el.id];
                if (target) {
                    return target;
                }
                el = el.parentNode;
            }
            return null;
        },
        /**
         * Spins up an info object based on a native drag.
         * @param {Ext.event.Event} e The event.
         * @return {Ext.drag.Info} The info. Cached for a single drag.
         *
         * @private
         */
        getNativeDragInfo: function(e) {
            var info = this.nativeDragInfo;
            if (!info) {
                this.nativeDragInfo = info = new Ext.drag.Info();
                info.isNative = true;
            }
            return info;
        },
        /**
         * Called on drag cancel.
         * @param {Ext.drag.Info} info The drag info.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onDragCancel: function() {
            Ext.getBody().removeCls(this.dragCls);
        },
        /**
         * Called when drag completes.
         * @param {Ext.drag.Info} info The drag info.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onDragEnd: function(info, e) {
            info.finalize();
            Ext.getBody().removeCls(this.dragCls);
        },
        /**
         * Called for each drag movement.
         * @param {Ext.drag.Info} info The drag info.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onDragMove: function(info, e) {
            this.processDrag(info);
        },
        /**
         * Called when drag starts.
         * @param {Ext.drag.Info} info The drag info.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onDragStart: function(info, e) {
            var me = this,
                source = info.source,
                targets = me.targets,
                groups = source.getGroups(),
                targetMap = {},
                possibleTargets = {},
                elementMap = {},
                id, target, targetGroups, groupMap, groupOk, len, i;
            elementMap = {};
            possibleTargets = {};
            if (groups) {
                groupMap = Ext.Array.toMap(groups);
            }
            // Exclude any invalid targets so they don't get used during
            // a drag. This means targets that are locked or have groups that don't
            // match
            for (id in targets) {
                target = targets[id];
                if (!target.isDisabled()) {
                    groupOk = false;
                    targetGroups = target.getGroups();
                    // If neither has groups, proceed. Otherwise, it
                    // can only be correct if both have groups, then we
                    // need to check if they intersect. If one has groups
                    // and not the other it's not possible to intersect.
                    if (!groupMap && !targetGroups) {
                        groupOk = true;
                    } else if (groupMap && targetGroups) {
                        for (i = 0 , len = targetGroups.length; i < len; ++i) {
                            if (groupMap[targetGroups[i]]) {
                                groupOk = true;
                                break;
                            }
                        }
                    }
                    if (groupOk) {
                        possibleTargets[id] = target;
                    }
                }
                targetMap[id] = target;
                elementMap[target.getElement().id] = target;
            }
            info.possibleTargets = possibleTargets;
            info.targetMap = targetMap;
            info.elementMap = elementMap;
            Ext.getBody().addCls(me.dragCls);
            me.processDrag(info);
        },
        /**
         * Handle a native dragenter event from outside the browser.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onNativeDragEnter: function(e) {
            var nativeTargets = this.nativeTargets,
                target = e.target;
            // Need to preventDefault to stop browser navigating to the dropped item.
            e.preventDefault();
            if (nativeTargets[nativeTargets.length - 1] !== target) {
                nativeTargets.push(target);
            }
        },
        /**
         * Handle a native dragleave event.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onNativeDragLeave: function(e) {
            var nativeTargets = this.nativeTargets;
            Ext.Array.remove(nativeTargets, e.target);
            if (nativeTargets.length === 0) {
                this.nativeDragInfo = null;
            }
        },
        /**
         * Handle a native dragover event.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onNativeDragOver: function(e) {
            // Need to preventDefault to stop browser navigating to the dropped item.
            e.preventDefault();
        },
        /**
         * Handle a native drop event.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onNativeDrop: function(e) {
            // Need to preventDefault to stop browser navigating to the dropped item.
            e.preventDefault();
            this.nativeTargets.length = 0;
            this.nativeDragInfo = null;
        },
        /**
         * Process a drag movement.
         * @param {Ext.drag.Info} info The drag info.
         *
         * @private
         */
        processDrag: function(info) {
            info.setActive(this.getAtPoint(info));
        },
        /**
         * Register a target with this group. This is intended to
         * be called by the target.
         * @param {Ext.drag.Target} target The target.
         *
         * @private
         */
        register: function(target) {
            this.targets[target.getId()] = target;
        },
        /**
         * Unregister a target with this group. This is intended to
         * be called by the target.
         * @param {Ext.drag.Target} target The target.
         *
         * @private
         */
        unregister: function(target) {
            var id;
            if (this.destroying) {
                return;
            }
            id = target.getId();
            this.targets[id] = null;
            delete this.targets[id];
        }
    }
});

/**
 * A wrapper around a DOM element that allows it to be dragged.
 *
 * ## Constraining
 *
 * The {@link #constrain} config gives various options for limiting drag, for example:
 * - Vertical or horizontal only
 * - Minimum/maximum x/y values.
 * - Snap to grid
 * - Constrain to an element or region.
 *
 * See {@link Ext.drag.Constraint} for detailed options.
 *
 *
 *      new Ext.drag.Source({
 *          element: dragEl,
 *          constrain: {
 *              // Drag only vertically in 30px increments
 *              vertical: true,
 *              snap: {
 *                  y: 30
 *              }
 *          }
 *      });
 *
 * ## Data
 *
 * Data representing the underlying drag is driven by the {@link #method!describe} method. This
 * method is called once at the beginning of the drag. It should populate the info object with data
 * using the {@link Ext.drag.Info#setData setData} method. It accepts 2 arguments. 
 * 
 * - The `type` is used to indicate to {@link Ext.drag.Target targets} the type(s) of data being
 * provided.  This allows the {@link Ext.drag.Target target} to decide whether it is able to
 * interact with the source.  All types added are available in {@link Ext.drag.Info#types types}.
 * - The value can be a static value, or a function reference. In the latter case, the function
 * is evaluated when the data is requested.
 *
 * The {@link Ext.drag.Info#getData} method may be called once the drop completes. The data for the
 * relevant type is retrieved. All values from this method return a {@link Ext.Promise} to allow
 * for consistency when dealing with synchronous and asynchronous data.
 *
 * ## Proxy
 *
 * A {@link #proxy} is an element that follows the mouse cursor during a drag. This may be the
 * {@link #element}, a newly created element, or none at all (if the purpose is to just track
 * the cursor).
 *
 * See {@link Ext.drag.proxy.None for details}.
 *
 *      var data = [{
 *          id: 1,
 *          name: 'Adam'
 *      }, {
 *          id: 2,
 *          name: 'Barbara'
 *      }, {
 *          id: 3,
 *          name: 'Charlie'
 *      }];
 *
 *      var tpl = new Ext.XTemplate(
 *          '<div class="container">',
 *              '<tpl for=".">',
 *                  '<div class="child" data-id="{id}">{name}</div>',
 *              '</tpl>',
 *          '</div>'
 *      );
 *
 *      var el = tpl.append(Ext.getBody(), data);
 *
 *      new Ext.drag.Source({
 *          element: el,
 *          handle: '.child',
 *          proxy: {
 *              type: 'placeholder',
 *              getElement: function(info) {
 *                  return Ext.getBody().createChild({
 *                      cls: 'foo',
 *                      html: info.eventTarget.innerHTML
 *                  });
 *              }
 *          }
 *      });
 *       
 *
 * ## Handle
 *
 * A {@link #handle} is a CSS selector that allows certain child elements of the {@link #element}
 * to begin a drag. This is useful in 2 case:
 * - Where only a certain part of the element should trigger a drag, but the whole element should
 * move.
 * - When there are several repeated elements that may represent objects. 
 * 
 * In the example below, each child element becomes draggable and
 * the describe method is used to extract the id from the DOM element.
 *
 *
 *      var data = [{
 *          id: 1,
 *          name: 'Adam'
 *      }, {
 *          id: 2,
 *          name: 'Barbara'
 *      }, {
 *          id: 3,
 *          name: 'Charlie'
 *      }];
 *
 *      var tpl = new Ext.XTemplate(
 *          '<div class="container">',
 *              '<tpl for=".">',
 *                  '<div class="child" data-id="{id}">{name}</div>',
 *              '</tpl>',
 *          '</div>'
 *      );
 *
 *      var el = tpl.append(Ext.getBody(), data);
 *
 *      new Ext.drag.Source({
 *          element: el,
 *          handle: '.child',
 *          describe: function(info) {
 *              info.setData('item', Ext.fly(info.eventTarget).getAttribute('data-id'));
 *          }
 *      });
 *  
 */
Ext.define('Ext.drag.Source', {
    extend: 'Ext.drag.Item',
    defaultIdPrefix: 'source-',
    requires: [
        'Ext.GlobalEvents',
        'Ext.drag.Constraint'
    ],
    config: {
        /**
         * @cfg {Boolean/String/String[]} activeOnLongPress
         * `true` to always begin a drag with longpress. `false` to
         * never drag with longpress. If a string (or strings) are passed, it should
         * correspond to the pointer event type that should initiate a a drag on
         * longpress. See {@link Ext.event.Event#pointerType} for available types.
         */
        activateOnLongPress: false,
        /**
         * @cfg {String} activeCls
         * A css class to add to the {@link #element} while dragging is
         * active.
         */
        activeCls: null,
        /**
         * @cfg {Object/Ext.util.Region/Ext.dom.Element} constrain
         *
         * Adds constraining behavior for this drag source. See {@link Ext.drag.Constraint} for
         * configuration options. As a shortcut, a {@link Ext.util.Region Region} 
         * or {@link Ext.dom.Element} may be passed, which will be mapped to the 
         * appropriate configuration on the constraint.
         */
        constrain: null,
        /**
         * @cfg {String} handle
         * A CSS selector to identify child elements of the {@link #element} that will cause
         * a drag to be activated. If this is not specified, the entire {@link #element} will
         * be draggable.
         */
        handle: null,
        local: null,
        // @cmd-auto-dependency {aliasPrefix: "drag.proxy."}
        /**
         * @cfg {String/Object/Ext.drag.proxy.Base} proxy
         * The proxy to show while this element is dragging. This may be
         * the alias, a config, or instance of a proxy.
         *
         * See {@link Ext.drag.proxy.None None}, {@link Ext.drag.proxy.Original Original}, 
         * {@link Ext.drag.proxy.Placeholder Placeholder}.
         */
        proxy: 'original',
        /**
         * @cfg {Boolean/Object} revert
         * `true` (or an animation configuration) to animate the {@link #proxy} (which may be
         * the {@link #element}) back to the original position after drag.
         */
        revert: false
    },
    /**
     * @cfg {Function} describe
     * See {@link #method-describe}.
     */
    /**
     * @event beforedragstart
     * Fires before drag starts on this source. Return `false` to cancel the drag.
     * 
     * @param {Ext.drag.Source} this This source.
     * @param {Ext.drag.Info} info The drag info.
     * @param {Ext.event.Event} event The event.
     */
    /**
     * @event dragstart
     * Fires when the drag starts on this source.
     * 
     * @param {Ext.drag.Source} this This source.
     * @param {Ext.drag.Info} info The drag info.
     * @param {Ext.event.Event} event The event.
     */
    /**
     * @event dragmove
     * Fires continuously as this source is dragged.
     * 
     * @param {Ext.drag.Source} this This source.
     * @param {Ext.drag.Info} info The drag info.
     * @param {Ext.event.Event} event The event.
     */
    /**
     * @event dragend
     * Fires when the drag ends on this source.
     * 
     * @param {Ext.drag.Source} this This source.
     * @param {Ext.drag.Info} info The drag info.
     * @param {Ext.event.Event} event The event.
     */
    /**
     * @event dragcancel
     * Fires when a drag is cancelled.
     *
     * @param {Ext.drag.Source} this This source.
     * @param {Ext.drag.Info} info The drag info.
     * @param {Ext.event.Event} event The event.
     */
    /**
     * @property {Boolean} dragging
     * `true` if this source is currently dragging.
     *
     * @protected
     */
    dragging: false,
    constructor: function(config) {
        var describe = config && config.describe;
        if (describe) {
            this.describe = describe;
            // Don't mutate the object the user passed. Need to do this
            // here otherwise initConfig will complain about writing over
            // the method.
            config = Ext.apply({}, config);
            delete config.describe;
        }
        this.callParent([
            config
        ]);
        // Use bracket syntax to prevent Cmd from creating an
        // auto dependency. Will be pulled in by the target if
        // required.
        this.manager = Ext.drag['Manager'];
    },
    // eslint-disable-line dot-notation
    /**
     * @method
     * Sets up the underlying data that describes the drag. This method
     * is called once at the start of the drag operation.
     *
     * Data should be set on the {@link Ext.drag.Info info} using the 
     * {@link Ext.drag.Info#setData setData} method. See 
     * {@link Ext.drag.Info#setData setData} for more information.
     *
     * This method should not be called by user code.
     * 
     * @param {Ext.drag.Info} info The drag info.
     *
     * @protected
     */
    describe: Ext.emptyFn,
    /**
     * Checks whether this source is actively dragging.
     * @return {Boolean} `true` if this source is dragging.
     */
    isDragging: function() {
        return this.dragging;
    },
    /**
     * @method
     * Called before a drag starts. Return `false` to veto the drag.
     * @param {Ext.drag.Info} The drag info.
     *
     * @return {Boolean} `false` to veto the drag.
     *
     * @protected
     * @template
     */
    beforeDragStart: Ext.emptyFn,
    /**
     * @method
     * Called when a drag is cancelled.
     *
     * @protected
     * @template
     */
    onDragCancel: Ext.emptyFn,
    /**
     * @method
     * Called when a drag ends.
     *
     * @protected
     * @template
     */
    onDragEnd: Ext.emptyFn,
    /**
     * @method
     * Called for each move in a drag.
     *
     * @protected
     * @template
     */
    onDragMove: Ext.emptyFn,
    /**
     * @method
     * Called when a drag starts.
     *
     * @protected
     * @template
     */
    onDragStart: Ext.emptyFn,
    applyActivateOnLongPress: function(activateOnLongPress) {
        if (typeof activateOnLongPress === 'string') {
            activateOnLongPress = [
                activateOnLongPress
            ];
        }
        return activateOnLongPress;
    },
    updateActivateOnLongPress: function(activateOnLongPress) {
        if (!this.isConfiguring) {
            this.setupListeners();
        }
    },
    updateActiveCls: function(cls, oldCls) {
        var el;
        if (this.dragging) {
            el = this.getElement();
            el.replaceCls(oldCls, cls);
        }
    },
    applyConstrain: function(constrain) {
        if (constrain && !constrain.$isClass) {
            if (constrain.isRegion) {
                constrain = {
                    region: constrain
                };
            } else if (constrain.isElement || !Ext.isObject(constrain)) {
                constrain = {
                    element: constrain
                };
            }
            constrain = Ext.apply({
                source: this
            }, constrain);
            constrain = Ext.Factory.dragConstraint(constrain);
        }
        return constrain;
    },
    updateElement: function(element, oldElement) {
        // We can't bind/unbind these listeners with getElListeners because
        // they will conflict with the dragstart gesture event
        if (oldElement && !oldElement.destroyed) {
            oldElement.un('dragstart', 'stopNativeDrag', this);
        }
        if (element && !this.getHandle()) {
            element.setTouchAction({
                panX: false,
                panY: false
            });
            // Suppress translation and delegation for this to avoid event firing on
            // synthetic dragstart published by Gesture from pointermove. We need the
            // native event here.
            element.on('dragstart', 'stopNativeDrag', this, {
                translate: false,
                delegated: false
            });
        }
        this.callParent([
            element,
            oldElement
        ]);
    },
    updateHandle: function() {
        if (!this.isConfiguring) {
            this.setupListeners();
        }
    },
    applyProxy: function(proxy) {
        if (proxy) {
            proxy = Ext.Factory.dragproxy(proxy);
        }
        return proxy;
    },
    updateProxy: function(proxy, oldProxy) {
        if (oldProxy) {
            oldProxy.destroy();
        }
        if (proxy) {
            proxy.setSource(this);
        }
    },
    resolveListenerScope: function() {
        var ownerCmp = this.ownerCmp,
            a = arguments;
        if (ownerCmp) {
            return ownerCmp.resolveListenerScope.apply(ownerCmp, a);
        }
        return this.callParent(a);
    },
    destroy: function() {
        var me = this;
        me.manager = me.initialEvent = null;
        me.setConstrain(null);
        me.setProxy(null);
        me.callParent();
    },
    privates: {
        /**
         * @property {String} draggingCls
         * A class to add while dragging to give a high z-index and
         * disable pointer events.
         *
         * @private
         */
        draggingCls: Ext.baseCSSPrefix + 'drag-dragging',
        /**
         * @property {Ext.drag.Info} info
         * The info. Only available while a drag is active.
         *
         * @private
         */
        info: null,
        /**
         * @property {String} revertCls
         * A class to add to the proxy element while a revert is active.
         *
         * @private
         */
        revertCls: Ext.baseCSSPrefix + 'drag-revert',
        canActivateOnLongPress: function(e) {
            var activate = this.getActivateOnLongPress();
            /* eslint-disable-next-line max-len */
            return !!(activate && (activate === true || Ext.Array.contains(activate, e.pointerType)));
        },
        /**
         * Perform any cleanup after a drag.
         *
         * @private
         */
        dragCleanup: function(info) {
            var me = this,
                cls = me.getActiveCls(),
                proxy = me.getProxy(),
                el = me.getElement(),
                proxyEl = info ? info.proxy.element : null;
            if (cls) {
                el.removeCls(cls);
            }
            if (proxyEl) {
                proxyEl.removeCls(me.draggingCls);
            }
            proxy.cleanup(info);
            me.dragging = false;
            me.initialEvent = me.info = null;
        },
        /**
         * @method getElListeners
         * @inheritdoc
         */
        getElListeners: function() {
            var handle = this.getHandle(),
                o = {
                    touchstart: 'handleTouchStart',
                    dragstart: 'handleDragStart',
                    drag: 'handleDragMove',
                    dragend: 'handleDragEnd',
                    dragcancel: 'handleDragCancel'
                };
            if (handle) {
                o.dragstart = {
                    fn: o.dragstart,
                    delegate: handle
                };
            }
            if (this.getActivateOnLongPress()) {
                o.longpress = 'handleLongPress';
            }
            return o;
        },
        /**
         * Called when a drag is cancelled.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        handleDragCancel: function(e) {
            var me = this,
                info = me.info,
                manager = me.manager;
            if (manager) {
                manager.onDragCancel(info, e);
            }
            me.onDragCancel(info);
            if (me.hasListeners.dragcancel) {
                me.fireEvent('dragcancel', me, info, e);
            }
            Ext.fireEvent('dragcancel', me, info, e);
            me.dragCleanup(info);
        },
        /**
         * Called when a drag is ended.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        handleDragEnd: function(e) {
            if (!this.dragging) {
                return;
            }
            /* eslint-disable-next-line vars-on-top */
            var me = this,
                manager = me.manager,
                revert = me.getRevert(),
                info = me.info,
                proxy = info.proxy;
            info.update(e);
            if (manager) {
                manager.onDragEnd(info, e);
            }
            me.onDragEnd(info);
            if (me.hasListeners.dragend) {
                me.fireEvent('dragend', me, info, e);
            }
            Ext.fireEvent('dragend', me, info, e);
            proxy = proxy.instance;
            if (revert && proxy) {
                proxy.dragRevert(info, me.revertCls, revert, function() {
                    me.dragCleanup(info);
                });
            } else {
                me.dragCleanup(info);
            }
        },
        /**
         * Called for each drag movement.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        handleDragMove: function(e) {
            var me = this,
                info = me.info,
                manager = me.manager;
            if (!me.dragging) {
                return;
            }
            e.stopPropagation();
            e.claimGesture();
            info.update(e);
            if (manager) {
                manager.onDragMove(info, e);
            }
            me.onDragMove(info);
            if (me.hasListeners.dragmove) {
                me.fireEvent('dragmove', me, info, e);
            }
        },
        /**
         * Called when a drag is started.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        handleDragStart: function(e) {
            var me = this,
                hasListeners = me.hasListeners,
                manager = me.manager,
                constrain = me.getConstrain(),
                initialEvent = me.initialEvent,
                el, cls, info, cancel, proxyEl;
            if (me.preventStart(e)) {
                return false;
            }
            if (hasListeners.initdragconstraints) {
                // This (private) event allows drag constraints to be adjusted "JIT"
                // (used by modern sliders)
                me.fireEvent('initdragconstraints', me, e);
            }
            me.info = info = new Ext.drag.Info(me, initialEvent);
            me.setup(info);
            if (constrain) {
                constrain.onDragStart(info);
            }
            info.update(e, true);
            cancel = me.beforeDragStart(info) === false;
            if (!cancel && hasListeners.beforedragstart) {
                cancel = me.fireEvent('beforedragstart', me, info, e) === false;
            }
            if (cancel) {
                me.dragCleanup();
                return false;
            }
            e.claimGesture();
            me.dragging = true;
            cls = me.getActiveCls();
            el = me.getElement();
            if (cls) {
                el.addCls(cls);
            }
            proxyEl = info.proxy.element;
            if (proxyEl) {
                proxyEl.addCls(me.draggingCls);
            }
            info.update(e);
            if (manager) {
                manager.onDragStart(info, e);
            }
            me.onDragStart(info);
            if (hasListeners.dragstart) {
                me.fireEvent('dragstart', me, info, e);
            }
            Ext.fireEvent('dragstart', me, info, e);
        },
        /**
         * Called when a longpress is started on this target (which may lead to a drag)
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        handleLongPress: function(e) {
            if (!this.isDisabled() && this.canActivateOnLongPress(e)) {
                this.initialEvent = e;
                e.startDrag();
            }
        },
        /**
         * Called when a touch starts on this target (which may lead to a drag).
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        handleTouchStart: function(e) {
            if (!this.isDisabled()) {
                this.initialEvent = e;
            }
        },
        preventStart: function(e) {
            return this.isDisabled() || (!e.longpress && this.canActivateOnLongPress(e));
        },
        /**
         * Allow for any setup as soon as the info object is created.
         *
         * @private
         */
        setup: Ext.privateFn,
        stopNativeDrag: function(e) {
            e.preventDefault();
        }
    }
});

/**
 * A wrapper around a DOM element that allows it to receive drops.
 *
 * ## Validity of drag operations
 *
 * There are certain conditions that govern whether a {@link Ext.drag.Source source}
 * and a target can interact. By default (without configuration), all
 * {@link Ext.drag.Source sources} and targets can interact with each other, the conditions
 * are evaluated in this order:
 *
 * ### {@link #isDisabled Disabled State}
 * If the target is disabled, the {@link Ext.drag.Source source} 
 * cannot interact with it.
 *
 * ### {@link #groups Groups}
 * Both the {@link Ext.drag.Source source} and target can belong to multiple groups. 
 * They may interact if:
 * - Neither has a group
 * - Both have one (or more) of the same group
 *
 * ### {@link #method!accepts Accept}
 * This method is called each time a {@link Ext.drag.Source source} enters this
 * target. If the method returns `false`, the drag is not considered valid.
 *
 * ## Asynchronous drop processing
 *
 *  When the drop completes, the {@link #drop} event will fire, however the underlying data
 * may not be ready to be consumed. By returning a {@link Ext.Promise Promise} from the data, 
 * it allows either:
 * - The data to be fetched (either from a remote source or generated if expensive).
 * - Any validation to take place before the drop is finalized.
 *
 * Once the promise is {@link Ext.Promise#resolve resolved} or {@link Ext.Promise#resolve rejected},
 * further processing can be completed.
 *
 * Validation example:
 *
 * 
 *      var confirmSource = new Ext.drag.Source({
 *          element: dragEl,
 *          describe: function(info) {
 *              // Provide the data up front
 *              info.setData('records', theRecords);
 *          }
 *      });  
 *
 *      var confirmTarget = new Ext.drag.Target({
 *          element: dropEl,
 *          listeners: {
 *              drop: function(target, info) {
 *                  Ext.MessageBox.confirm('Really', 'Are you sure?', function(btn) {
 *                      if (btn === 'yes') {
 *                          info.getData('records').then(function(data) {
 *                              // Process the data
 *                          });
 *                      }
 *                  });
 *              }
 *          }
 *      });
 *
 *
 * Remote data example:
 *
 *      var fetchSource = new Ext.drag.Source({
 *          element: dragEl,
 *          // The resulting drag data will be a binary blob
 *          // of image data, we don't want to fetch it up front, so
 *          // pass a callback to be executed when data is requested.
 *          describe: function(info) {
 *              info.setData('image', function() {
 *                  return Ext.Ajax.request({
 *                      url: 'data.json'
 *                      // some options
 *                  }).then(function(result) {
 *                      var imageData;
 *                      // Do some post-processing
 *                      return imageData;
 *                  }, function() {
 *                      return Ext.Promise.reject('Something went wrong!');
 *                  });
 *              });
 *          }
 *      });
 *
 *      var fetchTarget = new Ext.drag.Target({
 *          element: dropEl,
 *          accepts: function(info) {
 *              return info.types.indexOf('image') > -1;
 *          },
 *          listeners: {
 *              drop: function(target, info) {
 *                  info.getData('image').then(function() {
 *                      // All good, show the image
 *                  }, function() {
 *                      // Handle failure case
 *                  });
 *              }
 *          }
 *      });
 * 
 */
Ext.define('Ext.drag.Target', {
    extend: 'Ext.drag.Item',
    requires: [
        'Ext.drag.Manager'
    ],
    defaultIdPrefix: 'target-',
    config: {
        /**
         * @cfg {String} invalidCls
         * A class to add to the {@link #element} when an
         * invalid drag is over this target.
         */
        invalidCls: '',
        /**
         * @cfg {String} validCls
         * A class to add to the {@link #element} when an
         * invalid drag is over this target.
         */
        validCls: ''
    },
    /**
     * @cfg {Function} accepts
     * See {@link #method-accepts}.
     */
    /**
     * @event beforedrop
     * Fires before a valid drop occurs. Return `false` to prevent the drop from
     * completing.
     *
     * @param {Ext.drag.Target} this This target.
     * @param {Ext.drag.Info} info The drag info.
     */
    /**
     * @event drop
     * Fires when a valid drop occurs.
     *
     * @param {Ext.drag.Target} this This target.
     * @param {Ext.drag.Info} info The drag info.
     */
    /**
     * @event dragenter
     * Fires when a drag enters this target.
     *
     * @param {Ext.drag.Target} this This target.
     * @param {Ext.drag.Info} info The drag info.
     */
    /**
     * @event dragleave
     * Fires when a source leaves this target.
     *
     * @param {Ext.drag.Target} this This target.
     * @param {Ext.drag.Info} info The drag info.
     */
    /**
     * @event dragmove
     * Fires when a drag moves while inside this target.
     *
     * @param {Ext.drag.Target} this This target.
     * @param {Ext.drag.Info} info The drag info.
     */
    constructor: function(config) {
        var me = this,
            accepts = config && config.accepts;
        if (accepts) {
            me.accepts = accepts;
            // Don't mutate the object the user passed. Need to do this
            // here otherwise initConfig will complain about writing over
            // the method.
            config = Ext.apply({}, config);
            delete config.accepts;
        }
        me.callParent([
            config
        ]);
        Ext.drag.Manager.register(me);
    },
    /**
     * Called each time a {@link Ext.drag.Source source} enters this target.
     * Allows this target to indicate whether it will interact with
     * the given drag. Determined after {@link #isDisabled} and 
     * {@link #groups} checks. If either of the aforementioned conditions
     * means the target is not valid, this will not be called.
     *
     * Defaults to returning `true`.
     * 
     * @param {Ext.drag.Info} info The drag info.
     * @return {Boolean} `true` if the drag is valid for this target.
     *
     * @protected
     */
    accepts: function(info) {
        return true;
    },
    /**
     * @method disable
     * @inheritdoc
     */
    disable: function() {
        this.callParent();
        this.setupListeners(null);
    },
    /**
     * @method enable
     * @inheritdoc
     */
    enable: function() {
        this.callParent();
        this.setupListeners();
    },
    /**
     * @method
     * Called before a drag finishes on this target. Return `false` to veto
     * the drop.
     * @param {Ext.drag.Info} info The drag info.
     * @return {Boolean} `false` to veto the drop.
     *
     * @protected
     * @template
     */
    beforeDrop: Ext.emptyFn,
    /**
     * @method
     * Called when a drag is dropped on this target.
     * @param {Ext.drag.Info} info The drag info.
     *
     * @protected
     * @template
     */
    onDrop: Ext.emptyFn,
    /**
     * @method
     * Called when a drag enters this target.
     * @param {Ext.drag.Info} info The drag info.
     *
     * @protected
     * @template
     */
    onDragEnter: Ext.emptyFn,
    /**
     * @method
     * Called when a source leaves this target.
     * @param {Ext.drag.Info} info The drag info.
     *
     * @protected
     * @template
     */
    onDragLeave: Ext.emptyFn,
    /**
     * @method
     * Called when a drag is moved while inside this target.
     * @param {Ext.drag.Info} info The drag info.
     *
     * @protected
     * @template
     */
    onDragMove: Ext.emptyFn,
    updateInvalidCls: function(invalidCls, oldInvalidCls) {
        var info = this.info;
        this.doUpdateCls(info && !info.valid, invalidCls, oldInvalidCls);
    },
    updateValidCls: function(validCls, oldValidCls) {
        var info = this.info;
        this.doUpdateCls(info && info.valid, validCls, oldValidCls);
    },
    destroy: function() {
        Ext.drag.Manager.unregister(this);
        this.callParent();
    },
    privates: {
        /**
         * Removes a class and replaces it with a new one, if the old class
         * was already on the element.
         *
         * @param {Boolean} needsAdd `true` if the new class needs adding.
         * @param {String} cls The new class to add.
         * @param {String} oldCls The old class to remove.
         *
         * @private
         */
        doUpdateCls: function(needsAdd, cls, oldCls) {
            var el = this.getElement();
            if (oldCls) {
                el.removeCls(oldCls);
            }
            if (cls && needsAdd) {
                el.addCls(cls);
            }
        },
        /**
         * @method getElListeners
         * @inheritdoc
         */
        getElListeners: function() {
            return {
                dragenter: 'handleNativeDragEnter',
                dragleave: 'handleNativeDragLeave',
                dragover: 'handleNativeDragMove',
                drop: 'handleNativeDrop'
            };
        },
        /**
         * Called when a drag is dropped on this target.
         * @param {Ext.drag.Info} info The drag info.
         *
         * @private
         */
        handleDrop: function(info) {
            var me = this,
                hasListeners = me.hasListeners,
                valid = info.valid;
            me.getElement().removeCls([
                me.getInvalidCls(),
                me.getValidCls()
            ]);
            if (valid && me.beforeDrop(info) !== false) {
                if (hasListeners.beforedrop && me.fireEvent('beforedrop', me, info) === false) {
                    return false;
                }
                me.onDrop(info);
                if (hasListeners.drop) {
                    me.fireEvent('drop', me, info);
                }
            } else {
                return false;
            }
        },
        /**
         * Called when a drag enters this target.
         * @param {Ext.drag.Info} info The drag info.
         *
         * @private
         */
        handleDragEnter: function(info) {
            var me = this,
                cls = info.valid ? me.getValidCls() : me.getInvalidCls();
            if (cls) {
                me.getElement().addCls(cls);
            }
            me.onDragEnter(info);
            if (me.hasListeners.dragenter) {
                me.fireEvent('dragenter', me, info);
            }
        },
        /**
         * Called when a source leaves this target.
         * @param {Ext.drag.Info} info The drag info.
         *
         * @private
         */
        handleDragLeave: function(info) {
            var me = this;
            me.getElement().removeCls([
                me.getInvalidCls(),
                me.getValidCls()
            ]);
            me.onDragLeave(info);
            if (me.hasListeners.dragleave) {
                me.fireEvent('dragleave', me, info);
            }
        },
        /**
         * Called when a drag is moved while inside this target.
         * @param {Ext.drag.Info} info The drag info.
         *
         * @private
         */
        handleDragMove: function(info) {
            var me = this;
            me.onDragMove(info);
            if (me.hasListeners.dragmove) {
                me.fireEvent('dragmove', me, info);
            }
        },
        /**
         * Handle a native drag enter.
         * @param {Ext.event.Event} e The event.
         * 
         * @private
         */
        handleNativeDragEnter: function(e) {
            var me = this,
                info = Ext.drag.Manager.getNativeDragInfo(e);
            info.onNativeDragEnter(me, e);
            if (me.hasListeners.dragenter) {
                me.fireEvent('dragenter', me, info);
            }
        },
        /**
         * Handle a native drag leave.
         * @param {Ext.event.Event} e The event.
         * 
         * @private
         */
        handleNativeDragLeave: function(e) {
            var me = this,
                info = Ext.drag.Manager.getNativeDragInfo(e);
            info.onNativeDragLeave(me, e);
            if (me.hasListeners.dragleave) {
                me.fireEvent('dragleave', me, info);
            }
        },
        /**
         * Handle a native drag move.
         * @param {Ext.event.Event} e The event.
         * 
         * @private
         */
        handleNativeDragMove: function(e) {
            var me = this,
                info = Ext.drag.Manager.getNativeDragInfo(e);
            info.onNativeDragMove(me, e);
            if (me.hasListeners.dragmove) {
                me.fireEvent('dragmove', me, info);
            }
        },
        /**
         * Handle a native drop.
         * @param {Ext.event.Event} e The event.
         * 
         * @private
         */
        handleNativeDrop: function(e) {
            var me = this,
                hasListeners = me.hasListeners,
                info = Ext.drag.Manager.getNativeDragInfo(e),
                valid = info.valid;
            info.onNativeDrop(me, e);
            if (valid) {
                if (hasListeners.beforedrop && me.fireEvent('beforedrop', me, info) === false) {
                    return;
                }
                if (hasListeners.drop) {
                    me.fireEvent('drop', me, info);
                }
            }
        }
    }
});

/**
 * A base class for drag proxies that are shown to represent the
 * dragged item during a drag.
 *
 * Default implementations are:
 * - {@link Ext.drag.proxy.Original}: Moves the original element.
 * - {@link Ext.drag.proxy.Placeholder}: Creates a new element each drag.
 *
 * This implementation does not provide a proxy element, so it can be used
 * for cursor tracking only.
 */
Ext.define('Ext.drag.proxy.None', {
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    alias: 'drag.proxy.none',
    factoryConfig: {
        aliasPrefix: 'drag.proxy.',
        type: 'dragproxy'
    },
    config: {
        source: null
    },
    constructor: function(config) {
        var getElement = config && config.getElement;
        if (getElement) {
            // Don't mutate the object the user passed. Need to do this
            // here otherwise initConfig will complain about writing over
            // the method.
            this.getElement = getElement;
            config = Ext.apply({}, config);
            delete config.getElement;
        }
        this.initConfig(config);
    },
    /**
     * @method
     * Perform any cleanup required. This is called as the drag ends.
     *
     * @template
     * @protected
     */
    cleanup: Ext.emptyFn,
    dragRevert: function(info, revertCls, options, callback) {
        var positionable = this.getPositionable(info),
            initial = info.proxy.initial;
        positionable.addCls(revertCls);
        positionable.setXY([
            initial.x,
            initial.y
        ], Ext.apply({
            callback: function() {
                positionable.removeCls(revertCls);
                callback();
            }
        }, options));
    },
    /**
     * Get the proxy element for the drag source. This is called as
     * the drag starts. This element may be cached on the instance and
     * reused.
     *
     * @param {Ext.drag.Info} info Drag info
     *
     * @return {Ext.dom.Element} The element.
     *
     * @template
     * @protected
     */
    getElement: function() {
        return null;
    },
    getPositionable: function() {
        return this.element;
    },
    setXY: function(info, xy, animation) {
        var positionable = this.getPositionable(info);
        if (positionable) {
            positionable.setXY(xy, animation);
        }
    },
    /**
     * @method
     * Called when the target changes for the active drag. This may
     * mean the target is now `null`.
     *
     * @param {Ext.drag.Info} info Drag info
     *
     * @template
     * @protected
     */
    update: Ext.emptyFn,
    privates: {
        setupElement: function(info) {
            return (this.element = this.getElement(info));
        },
        /**
         * Adjust the xy position based on any cursor offset.
         * @param {Ext.drag.Info} info The drag info.
         * @param {Number[]} pos The xy position.
         * @return {Number[]} The adjusted position.
         *
         * @private
         */
        adjustCursorOffset: function(info, pos) {
            return pos;
        }
    }
});

/**
 * A drag proxy that uses the {@link Ext.drag.Source#element}.
 */
Ext.define('Ext.drag.proxy.Original', {
    extend: 'Ext.drag.proxy.None',
    alias: 'drag.proxy.original',
    getElement: function(info) {
        return info.source.getElement();
    },
    getPositionable: function(info) {
        var source = info.source;
        return source.getComponent() || source.getElement();
    }
});

/**
 * A drag proxy that creates a new element to follow the cursor.
 */
Ext.define('Ext.drag.proxy.Placeholder', {
    extend: 'Ext.drag.proxy.None',
    alias: 'drag.proxy.placeholder',
    config: {
        /**
         * @cfg {String} cls
         * A class for this proxy.
         */
        cls: '',
        /**
         * @cfg {Number[]} cursorOffset
         * Determines the position of the proxy in relation
         * to the cursor.
         */
        cursorOffset: [
            12,
            20
        ],
        /**
         * @cfg {String} html
         * The html for this proxy.
         */
        html: null,
        /**
         * @cfg {String} invalidCls
         * A class to add to this proxy when over an
         * invalid {@link Ext.drag.Target target}.
         */
        invalidCls: '',
        /**
         * @cfg {String} validCls
         * A class to add to this proxy when over a
         * valid {@link Ext.drag.Target target}.
         */
        validCls: ''
    },
    placeholderCls: Ext.baseCSSPrefix + 'drag-proxy-placeholder',
    /**
     * @method cleanup
     * @inheritdoc
     */
    cleanup: function() {
        this.element = Ext.destroy(this.element);
    },
    /**
     * @method getElement
     * @inheritdoc
     */
    getElement: function() {
        var el = Ext.getBody().createChild({
                cls: this.getCls(),
                html: this.getHtml()
            });
        el.addCls(this.placeholderCls);
        el.setTouchAction({
            panX: false,
            panY: false
        });
        return el;
    },
    /**
     * @method update
     * @inheritdoc
     */
    update: function(info) {
        var el = this.element,
            invalidCls = this.getInvalidCls(),
            validCls = this.getValidCls(),
            valid = info.valid;
        if (info.target) {
            // If we are valid, replace the invalidCls with the validCls.
            // Otherwise do the reverse
            el.replaceCls(valid ? invalidCls : validCls, valid ? validCls : invalidCls);
        } else {
            el.removeCls([
                invalidCls,
                validCls
            ]);
        }
    },
    updateCls: function(cls, oldCls) {
        var el = this.element;
        if (el) {
            if (oldCls) {
                el.removeCls(oldCls);
            }
            if (cls) {
                el.addCls(cls);
            }
        }
    },
    updateHtml: function(html) {
        var el = this.element;
        if (el) {
            el.setHtml(html || '');
        }
    },
    updateInvalidCls: function(invalidCls, oldInvalidCls) {
        this.doUpdateCls(invalidCls, oldInvalidCls);
    },
    updateValidCls: function(validCls, oldValidCls) {
        this.doUpdateCls(validCls, oldValidCls);
    },
    destroy: function() {
        this.element = Ext.destroy(this.element);
        this.callParent();
    },
    privates: {
        /**
         * @method adjustCursorOffset
         * @inheritdoc
         */
        adjustCursorOffset: function(info, xy) {
            var offset = this.getCursorOffset();
            if (offset) {
                xy[0] += (offset[0] || 0);
                xy[1] += (offset[1] || 0);
            }
            return xy;
        },
        /**
         * Removes a class and replaces it with a new one, if the old class
         * was already on the element.
         * 
         * @param {String} cls The new class to add.
         * @param {String} oldCls The old class to remove.
         *
         * @private
         */
        doUpdateCls: function(cls, oldCls) {
            var el = this.element,
                hasCls;
            if (el) {
                if (oldCls) {
                    hasCls = cls && el.hasCls(oldCls);
                    el.removeCls(oldCls);
                }
                if (cls && hasCls) {
                    el.addCls(cls);
                }
            }
        }
    }
});

/**
 * A base class for all gesture recognizers.
 *
 * The following gestures are enabled by default in both Ext JS and Sencha Touch:
 *
 * * {@link Ext.event.gesture.Tap}
 * * {@link Ext.event.gesture.DoubleTap}
 * * {@link Ext.event.gesture.LongPress}
 * * {@link Ext.event.gesture.Drag}
 * * {@link Ext.event.gesture.Swipe}
 * * {@link Ext.event.gesture.Pinch}
 * * {@link Ext.event.gesture.Rotate}
 * * {@link Ext.event.gesture.EdgeSwipe}
 *
 * @abstract
 * @private
 */
Ext.define('Ext.event.gesture.Recognizer', {
    requires: [
        'Ext.event.publisher.Gesture'
    ],
    mixins: [
        'Ext.mixin.Identifiable'
    ],
    /**
     * @property {Number}
     * The priority of the recognizer. Determines the order in which it recognizes gestures
     * relative to other recognizers.  The default recognizers use the following priorities:
     *
     * - Ext.event.gesture.Drag: 100
     * - Ext.event.gesture.Tap: 200
     * - Ext.event.gesture.DoubleTap: 300
     * - Ext.event.gesture.LongPress: 400
     * - Ext.event.gesture.EdgeSwipe: 500
     * - Ext.event.gesture.Swipe: 600
     * - Ext.event.gesture.Pinch: 700
     * - Ext.event.gesture.Rotate: 800
     */
    priority: 0,
    handledEvents: [],
    isStarted: false,
    config: {
        onRecognized: Ext.emptyFn,
        callbackScope: null
    },
    constructor: function(config) {
        this.initConfig(config);
        Ext.event.publisher.Gesture.instance.registerRecognizer(this);
    },
    onStart: Ext.emptyFn,
    onEnd: Ext.emptyFn,
    onTouchStart: Ext.emptyFn,
    onTouchMove: Ext.emptyFn,
    onTouchEnd: function() {
        return this.reset();
    },
    onTouchCancel: function(e) {
        return this.cancel(e);
    },
    fire: function(eventName, e, info, isCancel) {
        this.getOnRecognized().call(this.getCallbackScope(), this, eventName, e, info, isCancel);
    },
    cancel: function(e) {
        if (this.isStarted) {
            // If the recognizer is started, that is to say, it has already begun publishing
            // events for the current gesture, then we need to make sure it fires a "cancel"
            // event (implementation determined by subclasses).
            this.onCancel(e);
        }
        return this.reset();
    },
    onCancel: Ext.emptyFn,
    reset: function() {
        this.isStarted = false;
        return false;
    }
});

/**
 * A base class for gesture recognizers that are only concerned with a single point of
 * contact between the screen and the input-device.
 * @abstract
 * @private
 */
Ext.define('Ext.event.gesture.SingleTouch', {
    extend: 'Ext.event.gesture.Recognizer',
    isSingleTouch: true,
    onTouchStart: function(e) {
        if (e.touches.length > 1) {
            return this.cancel(e);
        }
    }
});

/**
 * A simple event recognizer which knows when you double tap.
 */
Ext.define('Ext.event.gesture.DoubleTap', {
    extend: 'Ext.event.gesture.SingleTouch',
    priority: 300,
    config: {
        /**
         * @cfg {Number}
         * The maximum distance a touch can move without canceling recognition
         */
        moveDistance: 8,
        /**
         * @cfg {Number}
         * The minimum distance the second tap can occur from the first tap and still
         * be considered a doubletap
         */
        tapDistance: 24,
        maxDuration: 300
    },
    handledEvents: [
        'singletap',
        'doubletap'
    ],
    /**
     * @member Ext.dom.Element
     * @event singletap
     * Fires when there is a single tap.
     * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
     * @param {HTMLElement} node The target of the event.
     * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
     */
    /**
     * @member Ext.dom.Element
     * @event doubletap
     * Fires when there is a double tap.
     * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
     * @param {HTMLElement} node The target of the event.
     * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
     */
    singleTapTimer: null,
    startTime: 0,
    lastTapTime: 0,
    onTouchStart: function(e) {
        var me = this,
            ret = me.callParent([
                e
            ]),
            lastStartPoint;
        if (ret !== false) {
            me.isStarted = true;
            // the start point of the last touch that occurred.
            lastStartPoint = me.lastStartPoint = e.changedTouches[0].point;
            // the start point of the "first" touch in this gesture
            me.startPoint = me.startPoint || lastStartPoint;
            me.startTime = e.time;
            Ext.undefer(me.singleTapTimer);
        }
        return ret;
    },
    onTouchMove: function(e) {
        var me = this,
            point = e.changedTouches[0].point,
            scale = Ext.Element.getViewportScale(),
            // account for scale so that move distance is actual screen pixels, not page pixels
            distance = Math.round(Math.abs(point.getDistanceTo(me.lastStartPoint) * scale));
        if (distance >= me.getMoveDistance()) {
            return me.cancel(e);
        }
    },
    onTouchEnd: function(e) {
        var me = this,
            maxDuration = me.getMaxDuration(),
            time = e.time,
            target = e.target,
            lastTapTime = me.lastTapTime,
            lastTarget = me.lastTarget,
            point = e.changedTouches[0].point,
            duration, scale, distance;
        me.lastTapTime = time;
        me.lastTarget = target;
        if (lastTapTime) {
            duration = time - lastTapTime;
            if (duration <= maxDuration) {
                scale = Ext.Element.getViewportScale();
                // account for scale so that move distance is actual screen pixels, not page pixels
                distance = Math.round(Math.abs(point.getDistanceTo(me.startPoint) * scale));
                if (distance <= me.getTapDistance()) {
                    if (target !== lastTarget) {
                        return me.cancel(e);
                    }
                    me.lastTarget = null;
                    me.lastTapTime = 0;
                    me.fire('doubletap', e, {
                        touch: e.changedTouches[0],
                        duration: duration
                    });
                    return me.callParent([
                        e
                    ]);
                }
            }
        }
        if (time - me.startTime > maxDuration) {
            me.fire('singletap', e);
            me.reset();
        } else {
            me.setSingleTapTimer(e);
        }
    },
    setSingleTapTimer: function(e) {
        var me = this;
        me.singleTapTimer = Ext.defer(function() {
            me.fire('singletap', e);
            me.reset();
        }, me.getMaxDuration());
    },
    reset: function() {
        var me = this;
        Ext.undefer(me.singleTapTimer);
        me.startTime = me.lastTapTime = 0;
        me.lastStartPoint = me.startPoint = me.singleTapTimer = null;
        return me.callParent();
    }
}, function(DoubleTap) {
    var gestures = Ext.manifest.gestures;
    DoubleTap.instance = new DoubleTap(gestures && gestures.doubleTap);
});

/**
 *
 */
Ext.define('Ext.event.gesture.Drag', {
    extend: 'Ext.event.gesture.SingleTouch',
    priority: 100,
    startPoint: null,
    previousPoint: null,
    lastPoint: null,
    handledEvents: [
        'dragstart',
        'drag',
        'dragend',
        'dragcancel'
    ],
    config: {
        /**
         * @cfg {Number} minDistance
         * The minimum distance of pixels before a touch event becomes a drag event.
         */
        minDistance: 8
    },
    constructor: function() {
        this.callParent(arguments);
        this.initInfo();
    },
    initInfo: function() {
        this.info = {
            touch: null,
            previous: {
                x: 0,
                y: 0
            },
            x: 0,
            y: 0,
            delta: {
                x: 0,
                y: 0
            },
            absDelta: {
                x: 0,
                y: 0
            },
            flick: {
                velocity: {
                    x: 0,
                    y: 0
                }
            },
            direction: {
                x: 0,
                y: 0
            },
            time: 0,
            previousTime: {
                x: 0,
                y: 0
            },
            longpress: false
        };
    },
    onTouchStart: function(e) {
        var me = this,
            ret = me.callParent([
                e
            ]);
        if (ret !== false) {
            me.startTime = e.time;
            me.startPoint = e.changedTouches[0].point;
        }
        return ret;
    },
    tryDragStart: function(e) {
        var me = this,
            point = e.changedTouches[0].point,
            minDistance = me.getMinDistance(),
            scale = Ext.Element.getViewportScale(),
            // account for scale so that move distance is actual screen pixels, not page pixels
            distance = Math.round(Math.abs(point.getDistanceTo(me.startPoint) * scale));
        if (distance >= minDistance) {
            me.doDragStart(e);
        }
    },
    doDragStart: function(e, isLongPress) {
        var me = this,
            touch = e.changedTouches[0],
            point = touch.point,
            info = me.info,
            time;
        if (isLongPress) {
            time = Ext.now();
            me.startTime = time;
            me.startPoint = point;
            info.longpress = true;
        } else {
            time = e.time;
        }
        me.isStarted = true;
        me.previousPoint = me.lastPoint = point;
        me.resetInfo('x', e, touch);
        me.resetInfo('y', e, touch);
        info.time = time;
        me.fire('dragstart', e, info);
    },
    onTouchMove: function(e) {
        var me = this,
            touch, point;
        if (!me.startPoint) {
            return;
        }
        if (!me.isStarted) {
            me.tryDragStart(e);
        }
        if (!me.isStarted) {
            return;
        }
        touch = e.changedTouches[0];
        point = touch.point;
        if (me.lastPoint) {
            me.previousPoint = me.lastPoint;
        }
        me.lastPoint = point;
        me.lastMoveEvent = e;
        me.updateInfo('x', e, touch);
        me.updateInfo('y', e, touch);
        me.info.time = e.time;
        me.fire('drag', e, me.info);
    },
    onAxisDragEnd: function(axis, info) {
        var duration = info.time - info.previousTime[axis];
        if (duration > 0) {
            info.flick.velocity[axis] = (info[axis] - info.previous[axis]) / duration;
        }
    },
    resetInfo: function(axis, e, touch) {
        var me = this,
            value = me.lastPoint[axis],
            startValue = me.startPoint[axis],
            delta = value - startValue,
            capAxis = axis.toUpperCase(),
            info = me.info;
        info.touch = touch;
        info.delta[axis] = delta;
        info.absDelta[axis] = Math.abs(delta);
        info.previousTime[axis] = me.startTime;
        info.previous[axis] = startValue;
        info[axis] = value;
        info.direction[axis] = 0;
        info['start' + capAxis] = me.startPoint[axis];
        info['previous' + capAxis] = info.previous[axis];
        info['page' + capAxis] = info[axis];
        info['delta' + capAxis] = info.delta[axis];
        info['absDelta' + capAxis] = info.absDelta[axis];
        info['previousDelta' + capAxis] = 0;
        info.startTime = me.startTime;
    },
    updateInfo: function(axis, e, touch) {
        var me = this,
            value = me.lastPoint[axis],
            previousValue = me.previousPoint[axis],
            startValue = me.startPoint[axis],
            delta = value - startValue,
            info = me.info,
            direction = info.direction,
            capAxis = axis.toUpperCase(),
            previousFlick = info.previous[axis];
        info.touch = touch;
        info.delta[axis] = delta;
        info.absDelta[axis] = Math.abs(delta);
        if (value !== previousFlick && value !== info[axis]) {
            info.previous[axis] = info[axis];
            info.previousTime[axis] = info.time;
        }
        info[axis] = value;
        if (value > previousValue) {
            direction[axis] = 1;
        } else if (value < previousValue) {
            direction[axis] = -1;
        }
        info['start' + capAxis] = startValue;
        info['previous' + capAxis] = info.previous[axis];
        info['page' + capAxis] = info[axis];
        info['delta' + capAxis] = info.delta[axis];
        info['absDelta' + capAxis] = info.absDelta[axis];
        info['previousDelta' + capAxis] = info.previous[axis] - startValue;
        info.startTime = me.startTime;
    },
    onTouchEnd: function(e) {
        var me = this,
            touch, point, info;
        if (me.isStarted) {
            touch = e.changedTouches[0];
            point = touch.point;
            info = me.info;
            me.lastPoint = point;
            me.updateInfo('x', e, touch);
            me.updateInfo('y', e, touch);
            info.time = e.time;
            me.onAxisDragEnd('x', info);
            me.onAxisDragEnd('y', info);
            me.fire('dragend', e, info);
        }
        return this.callParent([
            e
        ]);
    },
    onCancel: function(e) {
        var me = this,
            touch = e.changedTouches[0],
            info = me.info;
        // if "e" is a true cancellation event (touchcancel, pointercancel) e.touches.length
        // will be 0.  If length is anything else we can safely assume that this was called
        // because an additional touch was added (see SingleTouch#onTouchStart).  If that
        // is the case we do not want to update the lastPoint because the coordinates should
        // be those of the last single-touch drag, not the new touch.
        if (!e.touches.length) {
            me.lastPoint = touch.point;
        }
        me.updateInfo('x', e, touch);
        me.updateInfo('y', e, touch);
        info.time = e.time;
        me.fire('dragcancel', e, info, true);
    },
    reset: function() {
        var me = this;
        me.lastPoint = me.startPoint = me.previousPoint = me.lastPoint = me.lastMoveEvent = null;
        me.initInfo();
        return me.callParent();
    }
}, function(Drag) {
    var gestures = Ext.manifest.gestures;
    Drag.instance = new Drag(gestures && gestures.drag);
});

/**
 * A gesture recognizer for swipe events
 */
Ext.define('Ext.event.gesture.Swipe', {
    extend: 'Ext.event.gesture.SingleTouch',
    priority: 600,
    handledEvents: [
        'swipestart',
        'swipe',
        'swipecancel'
    ],
    /**
     * @member Ext.dom.Element
     * @event swipe
     * Fires when there is a swipe
     * When listening to this, ensure you know about the {@link Ext.event.Event#direction} property
     * in the `event` object.
     * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
     * @param {HTMLElement} node The target of the event.
     * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
     */
    /**
     * @property {Number} direction
     * The direction of the swipe. Available options are:
     *
     * - up
     * - down
     * - left
     * - right
     *
     * **This is only available when the event type is `swipe`**
     * @member Ext.event.Event
     */
    /**
     * @property {Number} duration
     * The duration of the swipe.
     *
     * **This is only available when the event type is `swipe`**
     * @member Ext.event.Event
     */
    config: {
        minDistance: 80,
        maxOffset: 35,
        maxDuration: 1000
    },
    onTouchStart: function(e) {
        var me = this,
            ret = me.callParent([
                e
            ]),
            touch;
        if (ret !== false) {
            touch = e.changedTouches[0];
            me.startTime = e.time;
            me.isHorizontal = true;
            me.isVertical = true;
            me.startX = touch.pageX;
            me.startY = touch.pageY;
        }
        return ret;
    },
    onTouchMove: function(e) {
        var me = this,
            touch = e.changedTouches[0],
            x = touch.pageX,
            y = touch.pageY,
            deltaX = x - me.startX,
            deltaY = y - me.startY,
            absDeltaX = Math.abs(x - me.startX),
            absDeltaY = Math.abs(y - me.startY),
            duration = e.time - me.startTime,
            minDistance, direction, distance;
        // If delta is 0 on both axes that's not swipe
        if ((absDeltaX === 0 && absDeltaY === 0) || (duration > me.getMaxDuration())) {
            return me.cancel(e);
        }
        if (me.isHorizontal && absDeltaY > me.getMaxOffset()) {
            me.isHorizontal = false;
        }
        if (me.isVertical && absDeltaX > me.getMaxOffset()) {
            me.isVertical = false;
        }
        if (!me.isVertical || !me.isHorizontal) {
            minDistance = me.getMinDistance();
            if (me.isHorizontal && absDeltaX < minDistance) {
                direction = (deltaX < 0) ? 'left' : 'right';
                distance = absDeltaX;
            } else if (me.isVertical && absDeltaY < minDistance) {
                direction = (deltaY < 0) ? 'up' : 'down';
                distance = absDeltaY;
            }
        }
        if (!me.isHorizontal && !me.isVertical) {
            return me.cancel(e);
        }
        if (direction && !me.isStarted) {
            me.isStarted = true;
            me.fire('swipestart', e, {
                touch: touch,
                direction: direction,
                distance: distance,
                duration: duration
            });
        }
    },
    onTouchEnd: function(e) {
        var me = this,
            touch, x, y, deltaX, deltaY, absDeltaX, absDeltaY, minDistance, duration, direction, distance;
        if (me.onTouchMove(e) !== false) {
            touch = e.changedTouches[0];
            x = touch.pageX;
            y = touch.pageY;
            deltaX = x - me.startX;
            deltaY = y - me.startY;
            absDeltaX = Math.abs(deltaX);
            absDeltaY = Math.abs(deltaY);
            minDistance = me.getMinDistance();
            duration = e.time - me.startTime;
            if (me.isVertical && absDeltaY < minDistance) {
                me.isVertical = false;
            }
            if (me.isHorizontal && absDeltaX < minDistance) {
                me.isHorizontal = false;
            }
            if (me.isHorizontal) {
                direction = (deltaX < 0) ? 'left' : 'right';
                distance = absDeltaX;
            } else if (me.isVertical) {
                direction = (deltaY < 0) ? 'up' : 'down';
                distance = absDeltaY;
            }
            me.fire('swipe', e, {
                touch: touch,
                direction: direction,
                distance: distance,
                duration: duration
            });
        }
        return this.callParent([
            e
        ]);
    },
    onCancel: function(e) {
        this.fire('swipecancel', e, null, true);
    },
    reset: function() {
        var me = this;
        me.startTime = me.isHorizontal = me.isVertical = me.startX = me.startY = null;
        return me.callParent();
    }
}, function(Swipe) {
    var gestures = Ext.manifest.gestures;
    Swipe.instance = new Swipe(gestures && gestures.swipe);
});

/**
 * A event recognizer created to recognize swipe movements from the edge of a container.
 */
Ext.define('Ext.event.gesture.EdgeSwipe', {
    extend: 'Ext.event.gesture.Swipe',
    priority: 500,
    handledEvents: [
        'edgeswipe',
        'edgeswipestart',
        'edgeswipeend',
        'edgeswipecancel'
    ],
    config: {
        minDistance: 60
    },
    onTouchStart: function(e) {
        var me = this,
            ret = me.callParent([
                e
            ]),
            touch;
        if (ret !== false) {
            touch = e.changedTouches[0];
            me.direction = null;
            me.isHorizontal = true;
            me.isVertical = true;
            me.startX = touch.pageX;
            me.startY = touch.pageY;
        }
        return ret;
    },
    onTouchMove: function(e) {
        var me = this,
            touch = e.changedTouches[0],
            x = touch.pageX,
            y = touch.pageY,
            deltaX = x - me.startX,
            deltaY = y - me.startY,
            absDeltaY = Math.abs(y - me.startY),
            absDeltaX = Math.abs(x - me.startX),
            minDistance = me.getMinDistance(),
            maxOffset = me.getMaxOffset(),
            duration = e.time - me.startTime,
            elementWidth = Ext.Viewport && Ext.Element.getViewportWidth(),
            elementHeight = Ext.Viewport && Ext.Element.getViewportHeight(),
            direction, distance;
        // Check if the swipe is going off vertical
        if (me.isVertical && absDeltaX > maxOffset) {
            me.isVertical = false;
        }
        // Check if the swipe is going off horizontal
        if (me.isHorizontal && absDeltaY > maxOffset) {
            me.isHorizontal = false;
        }
        // If the swipe is both, determin which one it is from the maximum distance travelled
        if (me.isVertical && me.isHorizontal) {
            if (absDeltaY > absDeltaX) {
                me.isHorizontal = false;
            } else {
                me.isVertical = false;
            }
        }
        // Get the direction of the swipe
        if (me.isHorizontal) {
            direction = (deltaX < 0) ? 'left' : 'right';
            distance = deltaX;
        } else if (me.isVertical) {
            direction = (deltaY < 0) ? 'up' : 'down';
            distance = deltaY;
        }
        direction = me.direction || (me.direction = direction);
        // Invert the distance if we are going up or left so the distance is a positive number
        // FROM the side
        if (direction === 'up') {
            distance = deltaY * -1;
        } else if (direction === 'left') {
            distance = deltaX * -1;
        }
        me.distance = distance;
        if (!distance) {
            return me.cancel(e);
        }
        if (!me.isStarted) {
            if ((direction === 'right' && me.startX > minDistance) || (direction === 'down' && me.startY > minDistance) || (direction === 'left' && (elementWidth - me.startX) > minDistance) || (direction === 'up' && (elementHeight - me.startY) > minDistance)) {
                return me.cancel(e);
            }
            me.isStarted = true;
            me.startTime = e.time;
            me.fire('edgeswipestart', e, {
                touch: touch,
                direction: direction,
                distance: distance,
                duration: duration
            });
        } else {
            me.fire('edgeswipe', e, {
                touch: touch,
                direction: direction,
                distance: distance,
                duration: duration
            });
        }
    },
    onTouchEnd: function(e) {
        var me = this,
            duration;
        if (me.onTouchMove(e) !== false) {
            duration = e.time - me.startTime;
            me.fire('edgeswipeend', e, {
                touch: e.changedTouches[0],
                direction: me.direction,
                distance: me.distance,
                duration: duration
            });
        }
        return this.reset();
    },
    onCancel: function(e) {
        this.fire('edgeswipecancel', e, {
            touch: e.changedTouches[0]
        }, true);
    },
    reset: function() {
        var me = this;
        me.direction = me.isHorizontal = me.isVertical = me.startX = me.startY = me.startTime = me.distance = null;
        return me.callParent();
    }
}, function(EdgeSwipe) {
    var gestures = Ext.manifest.gestures;
    EdgeSwipe.instance = new EdgeSwipe(gestures && gestures.edgeSwipe);
});

/**
 * A event recognizer which knows when you tap and hold for more than 1 second.
 */
Ext.define('Ext.event.gesture.LongPress', {
    extend: 'Ext.event.gesture.SingleTouch',
    priority: 400,
    config: {
        moveDistance: 8,
        minDuration: 1000
    },
    handledEvents: [
        'longpress',
        'taphold'
    ],
    /**
     * @member Ext.dom.Element
     * @event longpress
     * Fires when you touch and hold still for more than 1 second.
     * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
     * @param {HTMLElement} node The target of the event.
     * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
     */
    /**
     * @member Ext.dom.Element
     * @event taphold
     * @inheritdoc Ext.dom.Element#longpress
     */
    onTouchStart: function(e) {
        var me = this,
            ret = me.callParent([
                e
            ]);
        if (ret !== false) {
            me.startPoint = e.changedTouches[0].point;
            me.setLongPressTimer(e);
        }
        return ret;
    },
    setLongPressTimer: function(e) {
        var me = this;
        Ext.undefer(me.timer);
        me.timer = Ext.defer(me.fireLongPress, me.getMinDuration(), me, [
            e
        ]);
    },
    onTouchMove: function(e) {
        var me = this,
            point = e.changedTouches[0].point,
            scale = Ext.Element.getViewportScale(),
            // account for scale so that move distance is actual screen pixels, not page pixels
            distance = Math.round(Math.abs(point.getDistanceTo(me.startPoint) * scale));
        if (distance >= me.getMoveDistance()) {
            return me.cancel(e);
        }
    },
    reset: function() {
        var me = this;
        me.timer = me.startPoint = Ext.undefer(me.timer);
        return me.callParent();
    },
    fireLongPress: function(e) {
        var me = this,
            info = {
                touch: e.changedTouches[0],
                duration: me.getMinDuration(),
                startDrag: me.startDrag
            };
        this.fire('taphold', e, info);
        this.fire('longpress', e, info);
        this.reset();
    },
    /**
     * @member Ext.event.Event
     * @method startDrag
     *
     * Initiates a drag gesture in response to this event
     *
     * Only available when `type` is `'longpress'`.  When invoked a dragstart event
     * will be immediately fired at the coordinates of the longpress event.  Thereafter
     * drag events will fire in response to movement on the screen without regard
     * to the distance moved.
     */
    startDrag: function() {
        // the longpress event object is decorated with this function, the scope object
        // here is the event object, not the recognizer
        var dragRecognizer = Ext.event.gesture.Drag.instance,
            touchStartEvent = this.parentEvent;
        dragRecognizer.doDragStart(touchStartEvent, true);
        Ext.event.publisher.Gesture.instance.claimRecognizer(dragRecognizer, touchStartEvent);
    }
}, function(LongPress) {
    var gestures = Ext.manifest.gestures;
    LongPress.instance = new LongPress(gestures && gestures.longPress);
});

/**
 * A base class for gesture recognizers that involve multiple simultaneous contact points
 * between the screen and the input-device, e.g. 'pinch' and 'rotate'
 * @abstract
 * @private
 */
Ext.define('Ext.event.gesture.MultiTouch', {
    extend: 'Ext.event.gesture.Recognizer',
    requiredTouchesCount: 2,
    isTracking: false,
    isMultiTouch: true,
    onTouchStart: function(e) {
        var me = this,
            requiredTouchesCount = me.requiredTouchesCount,
            touches = e.touches,
            touchesCount = touches.length;
        if (touchesCount === requiredTouchesCount) {
            me.isTracking = true;
        } else if (touchesCount > requiredTouchesCount) {
            return me.cancel(e);
        }
    },
    reset: function() {
        this.isTracking = false;
        return this.callParent();
    }
});

/**
 * A event recognizer which knows when you pinch.
 */
Ext.define('Ext.event.gesture.Pinch', {
    extend: 'Ext.event.gesture.MultiTouch',
    priority: 700,
    handledEvents: [
        'pinchstart',
        'pinch',
        'pinchend',
        'pinchcancel'
    ],
    /**
     * @member Ext.dom.Element
     * @event pinchstart
     * Fired once when a pinch has started.
     * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
     * @param {HTMLElement} node The target of the event.
     * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
     */
    /**
     * @member Ext.dom.Element
     * @event pinch
     * Fires continuously when there is pinching (the touch must move for this to be fired).
     * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
     * @param {HTMLElement} node The target of the event.
     * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
     */
    /**
     * @member Ext.dom.Element
     * @event pinchend
     * Fires when a pinch has ended.
     * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
     * @param {HTMLElement} node The target of the event.
     * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
     */
    /**
     * @property {Number} scale
     * The scape of a pinch event.
     *
     * **This is only available when the event type is `pinch`**
     * @member Ext.event.Event
     */
    startDistance: 0,
    lastTouches: null,
    onTouchMove: function(e) {
        var me = this,
            touches, firstPoint, secondPoint, distance;
        if (me.isTracking) {
            touches = e.touches;
            firstPoint = touches[0].point;
            secondPoint = touches[1].point;
            distance = firstPoint.getDistanceTo(secondPoint);
            if (distance === 0) {
                return;
            }
            if (!me.isStarted) {
                me.isStarted = true;
                me.startDistance = distance;
                me.fire('pinchstart', e, {
                    touches: touches,
                    distance: distance,
                    scale: 1
                });
            } else {
                me.fire('pinch', e, {
                    touches: touches,
                    distance: distance,
                    scale: distance / me.startDistance
                });
            }
        }
    },
    onTouchEnd: function(e) {
        if (this.isStarted) {
            this.fire('pinchend', e);
        }
        return this.callParent([
            e
        ]);
    },
    onCancel: function(e) {
        this.fire('pinchcancel', e, null, true);
    },
    reset: function() {
        this.lastTouches = null;
        this.startDistance = 0;
        return this.callParent();
    }
}, function(Pinch) {
    var gestures = Ext.manifest.gestures;
    Pinch.instance = new Pinch(gestures && gestures.pinch);
});

/**
 * A simple event recognizer which knows when you rotate.
 */
Ext.define('Ext.event.gesture.Rotate', {
    extend: 'Ext.event.gesture.MultiTouch',
    priority: 800,
    handledEvents: [
        'rotatestart',
        'rotate',
        'rotateend',
        'rotatecancel'
    ],
    /**
     * @member Ext.dom.Element
     * @event rotatestart
     * Fired once when a rotation has started.
     * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
     * @param {HTMLElement} node The target of the event.
     * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
     */
    /**
     * @member Ext.dom.Element
     * @event rotate
     * Fires continuously when there is rotation (the touch must move for this to be fired).
     * When listening to this, ensure you know about the {@link Ext.event.Event#angle} and
     * {@link Ext.event.Event#rotation} properties in the `event` object.
     * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
     * @param {HTMLElement} node The target of the event.
     * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
     */
    /**
     * @member Ext.dom.Element
     * @event rotateend
     * Fires when a rotation event has ended.
     * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
     * @param {HTMLElement} node The target of the event.
     * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
     */
    /**
     * @property {Number} angle
     * The angle of the rotation.
     *
     * **This is only available when the event type is `rotate`**
     * @member Ext.event.Event
     */
    /**
     * @property {Number} rotation
     * A amount of rotation, since the start of the event.
     *
     * **This is only available when the event type is `rotate`**
     * @member Ext.event.Event
     */
    startAngle: 0,
    lastTouches: null,
    lastAngle: null,
    onTouchMove: function(e) {
        var me = this,
            touches, lastAngle, firstPoint, secondPoint, angle, nextAngle, previousAngle, diff;
        if (me.isTracking) {
            touches = e.touches;
            lastAngle = me.lastAngle;
            firstPoint = touches[0].point;
            secondPoint = touches[1].point;
            angle = firstPoint.getAngleTo(secondPoint);
            if (lastAngle !== null) {
                diff = Math.abs(lastAngle - angle);
                nextAngle = angle + 360;
                previousAngle = angle - 360;
                if (Math.abs(nextAngle - lastAngle) < diff) {
                    angle = nextAngle;
                } else if (Math.abs(previousAngle - lastAngle) < diff) {
                    angle = previousAngle;
                }
            }
            me.lastAngle = angle;
            if (!me.isStarted) {
                me.isStarted = true;
                me.startAngle = angle;
                me.fire('rotatestart', e, {
                    touches: touches,
                    angle: angle,
                    rotation: 0
                });
            } else {
                me.fire('rotate', e, {
                    touches: touches,
                    angle: angle,
                    rotation: angle - me.startAngle
                });
            }
            me.lastTouches = Ext.Array.clone(touches);
        }
    },
    onTouchEnd: function(e) {
        if (this.isStarted) {
            this.fire('rotateend', e);
        }
        return this.callParent([
            e
        ]);
    },
    onCancel: function(e) {
        this.fire('rotatecancel', e, null, true);
    },
    reset: function() {
        var me = this;
        me.lastTouches = me.lastAngle = me.startAngle = null;
        return this.callParent();
    }
}, function(Rotate) {
    var gestures = Ext.manifest.gestures;
    Rotate.instance = new Rotate(gestures && gestures.rotate);
});

/**
 * A simple event recogniser which knows when you tap.
 */
Ext.define('Ext.event.gesture.Tap', {
    extend: 'Ext.event.gesture.SingleTouch',
    priority: 200,
    handledEvents: [
        'tap',
        'tapcancel'
    ],
    config: {
        /**
         * @cfg {Number} moveDistance
         * The maximimum distance in pixels a touchstart event can travel and still be considered
         * a tap event.
         */
        moveDistance: 8
    },
    onTouchStart: function(e) {
        var me = this,
            ret = me.callParent([
                e
            ]);
        if (ret !== false) {
            me.isStarted = true;
            me.startPoint = e.changedTouches[0].point;
        }
        return ret;
    },
    onTouchMove: function(e) {
        var me = this,
            point = e.changedTouches[0].point,
            scale = Ext.Element.getViewportScale(),
            // account for scale so that move distance is actual screen pixels, not page pixels
            distance = Math.round(Math.abs(point.getDistanceTo(me.startPoint) * scale));
        if (distance >= me.getMoveDistance()) {
            return me.cancel(e);
        }
    },
    onTouchEnd: function(e) {
        this.fire('tap', e, {
            touch: e.changedTouches[0]
        });
        return this.callParent([
            e
        ]);
    },
    onCancel: function(e) {
        this.fire('tapcancel', e, {
            touch: e.changedTouches[0]
        }, true);
    },
    reset: function() {
        this.startPoint = null;
        return this.callParent();
    }
}, function(Tap) {
    var gestures = Ext.manifest.gestures;
    Tap.instance = new Tap(gestures && gestures.tap);
});

/**
 * @private
 */
Ext.define('Ext.event.publisher.Focus', {
    extend: 'Ext.event.publisher.Dom',
    requires: [
        'Ext.dom.Element',
        'Ext.GlobalEvents'
    ],
    type: 'focus',
    handledEvents: [
        'focusenter',
        'focusleave',
        'focusmove'
    ],
    // At this point only Firefox does not support focusin/focusout, see this bug:
    // https://bugzilla.mozilla.org/show_bug.cgi?id=687787
    // TODO: Fix event order: https://github.com/jquery/jquery/issues/3123
    handledDomEvents: [
        'focusin',
        'focusout'
    ],
    publishDelegatedDomEvent: function(e) {
        var me = this,
            relatedTarget = e.relatedTarget;
        if (me.$suppressEvents) {
            return;
        }
        if (e.type === 'focusout') {
            // If focus is departing to the document, there will be no forthcoming focusin event
            // to trigger a focusleave, so fire a focusleave now.
            if (relatedTarget == null) {
                me.processFocusIn(e, e.target, document.body);
            }
        } else {
            // IE reports relatedTarget as either an inaccessible object which coercively
            // equates to null, or just a blank object in the case of focusing from nowhere.
            if (relatedTarget == null || !relatedTarget.tagName) {
                relatedTarget = document.body;
            }
            me.processFocusIn(e, relatedTarget, e.target);
        }
    },
    processFocusIn: function(e, fromElement, toElement) {
        var me = this,
            focusFly = me.focusFly,
            targets = [],
            commonAncestor, node, backwards, event, focusEnterEvent;
        // If we have suspended focus/blur processing due to framework needing to
        // silently manipulate focus position, then return early.
        if ((fromElement && focusFly.attach(fromElement).isFocusSuspended()) || (toElement && focusFly.attach(toElement).isFocusSuspended())) {
            return;
        }
        if (toElement.compareDocumentPosition) {
            // Flag if the fromElement is DOCUMENT_POSITION_FOLLOWING toElement
            backwards = !!(toElement.compareDocumentPosition(fromElement) & 4);
        }
        // Gather targets for focusleave event from the fromElement to the parentNode
        // (not inclusive)
        // eslint-disable-next-line max-len
        for (node = fromElement , commonAncestor = Ext.dom.Element.getCommonAncestor(toElement, fromElement, true); node && node !== commonAncestor; node = node.parentNode) {
            targets.push(node);
        }
        // Publish the focusleave event for the bubble hierarchy
        if (targets.length) {
            event = me.createSyntheticEvent('focusleave', e, fromElement, toElement, fromElement, toElement, backwards);
            me.publish(event, targets);
            if (event.stopped) {
                return;
            }
        }
        // Gather targets for focusenter event from the focus targetElement to the parentNode
        // (not inclusive)
        targets.length = 0;
        for (node = toElement; node && node !== commonAncestor; node = node.parentNode) {
            targets.push(node);
        }
        // We always need this event; this is what we pass to the global focus event
        focusEnterEvent = me.createSyntheticEvent('focusenter', e, toElement, fromElement, fromElement, toElement, backwards);
        // Publish the focusleave event for the bubble hierarchy
        if (targets.length) {
            me.publish(focusEnterEvent, targets);
            if (focusEnterEvent.stopped) {
                return;
            }
        }
        // When focus moves within an element, fire a bubbling focusmove event
        targets = me.getPropagatingTargets(commonAncestor);
        // Publish the focusleave event for the bubble hierarchy
        if (targets.length) {
            event = me.createSyntheticEvent('focusmove', e, toElement, fromElement, fromElement, toElement, backwards);
            me.publish(event, targets);
            if (event.stopped) {
                return;
            }
        }
        if (Ext.GlobalEvents.hasListeners.focus) {
            Ext.GlobalEvents.fireEvent('focus', {
                event: focusEnterEvent,
                toElement: toElement,
                fromElement: fromElement,
                backwards: backwards
            });
        }
    },
    createSyntheticEvent: function(eventName, browserEvent, target, relatedTarget, fromElement, toElement, backwards) {
        var event = new Ext.event.Event(browserEvent);
        event.type = eventName;
        event.relatedTarget = relatedTarget;
        event.target = target;
        event.fromElement = fromElement;
        event.toElement = toElement;
        event.backwards = backwards;
        return event;
    }
}, function(Focus) {
    var focusTimeout;
    Focus.prototype.focusFly = new Ext.dom.Fly();
    Focus.instance = new Focus();
    // At this point only Firefox does not support focusin/focusout, see this bug:
    // https://bugzilla.mozilla.org/show_bug.cgi?id=687787
    if (!Ext.supports.FocusinFocusoutEvents) {
        // When focusin/focusout are not available we capture focus event instead,
        // and fire both focusenter *and* focusleave in the focus handler.
        this.override({
            handledDomEvents: [
                'focus',
                'blur'
            ],
            publishDelegatedDomEvent: function(e) {
                var me = this,
                    targetIsElement;
                me.callSuper([
                    e
                ]);
                // We need to know if event target was an element or (window || document)
                targetIsElement = e.target !== window && e.target !== document;
                // There might be an upcoming focus event, but if none happens
                // within a minimal timeout, then we treat this as a focus of the body
                if (e.type === 'blur') {
                    if (!targetIsElement) {
                        // Apparently when focus goes outside of the document, Firefox
                        // will fire blur on the currently focused element, then on the document,
                        // then on the window. Interestingly enough, both follow-up blur events
                        // will have explicitOriginalTarget pointing at the previously focused
                        // element; when that happens we can be reasonably sure that focus
                        // indeed goes out the window.
                        if (e.explicitOriginalTarget === Focus.previousActiveElement) {
                            // But we want that to fire only once, so process window blur
                            // which happens last.
                            if (e.target === window) {
                                Ext.undefer(focusTimeout);
                                focusTimeout = 0;
                                me.processFocusIn(e, Focus.previousActiveElement, document.body);
                                Focus.previousActiveElement = null;
                            }
                        }
                    } else {
                        // If event target is a valid element, blur could have been caused
                        // by removing previously focused element from the DOM, or some
                        // other happening that doesn't involve <strike>Elvis</strike>focus
                        // completely leaving the building.
                        focusTimeout = Ext.defer(function() {
                            focusTimeout = 0;
                            me.processFocusIn(e, e.target, document.body);
                            Focus.previousActiveElement = null;
                        }, 1);
                        // Store the timer in case the element gets destroyed before
                        // the function above has a chance to fire
                        if (targetIsElement && Ext.cache[e.target.id]) {
                            Ext.cache[e.target.id].focusinTimeout = focusTimeout;
                        }
                    }
                    Focus.previousActiveElement = targetIsElement ? e.target : null;
                } else {
                    Ext.undefer(focusTimeout);
                    focusTimeout = 0;
                    me.processFocusIn(e, Focus.previousActiveElement || document.body, targetIsElement ? e.target : document.body);
                }
            }
        });
        Ext.define(null, {
            override: 'Ext.dom.Element',
            destroy: function() {
                if (this.focusinTimeout) {
                    Ext.undefer(this.focusinTimeout);
                    this.focusinTimeout = null;
                }
                this.callParent();
            }
        });
    }
});

/**
 * Input masks provide a way for developers to define rules that govern user input. This ensures
 * that data is submitted in an expected format and with the appropriate character set.
 *
 * Frequent uses of input masks include:
 *
 * + Zip or postal codes
 * + Times
 * + Dates
 * + Telephone numbers
 *
 * ## Character Sets
 *
 * Input mask characters can be defined by representations of the desired set.  For instance,
 * if you only want to allow numbers, you can use 0 or 9.  Here is the list of default
 * representations:
 *
 * + '*': '[A-Za-z0-9]' // any case letter A-Z, any integer
 * + 'a': '[a-z]'       // any lower case letter a-z
 * + 'A': '[A-Z]'       // any upper case letter A-Z
 * + '0': '[0-9]'       // any integer
 * + '9': '[0-9]'        // any integer
 *
 * So, to create a telephone input mask, you could use:
 *
 * + (000) 000-0000
 *
 * or
 *
 * + (999) 999-9999
 *
 * ## Telephone input mask
 *
 *     @example toolkit=modern
 *     Ext.create({
 *         fullscreen: true,
 *         xtype: 'formpanel',
 *
 *         items: [{
 *             xtype: 'textfield',
 *             label: 'Phone Number',
 *             placeholder: '(xxx) xxx-xxxx',
 *             inputMask: '(999) 999-9999'
 *         }]
 *     });
 */
Ext.define('Ext.field.InputMask', function(InputMask) {
    return {
        // eslint-disable-line brace-style
        requires: [
            'Ext.util.LRU'
        ],
        cachedConfig: {
            blank: '_',
            characters: {
                '*': '[A-Za-z0-9]',
                'a': '[a-z]',
                'A': '[A-Z]',
                '0': '[0-9]',
                '9': '[0-9]'
            },
            ignoreCase: true
        },
        config: {
            /**
         * @cfg {String} pattern (required)
         */
            pattern: null
        },
        _cached: false,
        _lastEditablePos: null,
        _mask: null,
        statics: {
            active: {},
            from: function(value, existing) {
                var active = InputMask.active,
                    ret;
                if (value === null) {
                    ret = null;
                } else if (typeof value !== 'string') {
                    if (existing && !existing._cached) {
                        ret = existing;
                        ret.setConfig(value);
                    } else {
                        ret = new InputMask(value);
                    }
                } else if (!(ret = active[value])) {
                    // No one is currently using this mask, but check the cache of
                    // recently used masks. We remove the mask from the cache and
                    // move it to the active set... if it was there.
                    if (!(ret = InputMask.cache.remove(value))) {
                        ret = new InputMask({
                            pattern: value
                        });
                    }
                    active[value] = ret;
                    ret._cached = 1;
                } else // this is the first user either way
                {
                    // The mask was found in the active set so we can reuse it
                    // (just bump the counter).
                    ++ret._cached;
                }
                return ret;
            }
        },
        constructor: function(config) {
            this.initConfig(config);
        },
        release: function() {
            var me = this,
                cache = InputMask.cache,
                key;
            if (me._cached && !--me._cached) {
                key = me.getPattern();
                if (InputMask.active[key] !== me) {
                    Ext.raise('Invalid call to InputMask#release (not active)');
                }
                if (cache.map[key]) {
                    Ext.raise('Invalid call to InputMask#release (already cached)');
                }
                delete InputMask.active[key];
                cache.add(key, me);
                cache.trim(cache.maxSize);
            } else if (me._cached === 0) {
                Ext.raise('Invalid call to InputMask#release (already released)');
            }
        },
        clearRange: function(value, start, len) {
            var me = this,
                blank = me.getBlank(),
                end = start + len,
                n = value.length,
                s = '',
                i, mask, prefixLen;
            if (!blank) {
                prefixLen = me._prefix.length;
                for (i = 0; i < n; ++i) {
                    if (i < prefixLen || i < start || i >= end) {
                        s += value[i];
                    }
                }
                s = me.formatValue(s);
            } else {
                mask = me.getPattern();
                for (i = 0; i < n; ++i) {
                    if (i < start || i >= end) {
                        s += value[i];
                    } else if (me.isFixedChar(i)) {
                        s += mask[i];
                    } else {
                        s += blank;
                    }
                }
            }
            return s;
        },
        formatValue: function(value) {
            var me = this,
                blank = me.getBlank(),
                i, length, mask, prefix, s;
            if (!blank) {
                prefix = me._prefix;
                length = prefix.length;
                s = this.insertRange('', value, 0);
                for (i = s.length; i > length && me.isFixedChar(i - 1); ) {
                    --i;
                }
                s = (i < length) ? prefix : s.slice(0, i - 1);
            } else if (value) {
                s = me.formatValue('');
                s = me.insertRange(s, value, 0);
            } else {
                mask = me.getPattern();
                s = '';
                for (i = 0 , length = mask.length; i < length; ++i) {
                    if (me.isFixedChar(i)) {
                        s += mask[i];
                    } else {
                        s += blank;
                    }
                }
            }
            return s;
        },
        getEditPosLeft: function(pos) {
            var i;
            for (i = pos; i >= 0; --i) {
                if (!this.isFixedChar(i)) {
                    return i;
                }
            }
            return null;
        },
        getEditPosRight: function(pos) {
            var mask = this._mask,
                len = mask.length,
                i;
            for (i = pos; i < len; ++i) {
                if (!this.isFixedChar(i)) {
                    return i;
                }
            }
            return null;
        },
        getFilledLength: function(value) {
            var me = this,
                blank = me.getBlank(),
                c, i;
            if (!blank) {
                return value.length;
            }
            for (i = value && value.length; i-- > 0; ) {
                c = value[i];
                if (!me.isFixedChar(i) && me.isAllowedChar(c, i)) {
                    break;
                }
            }
            return ++i || me._prefix.length;
        },
        getSubLength: function(value, substr, pos) {
            var me = this,
                mask = me.getPattern(),
                k = 0,
                maskLen = mask.length,
                substrLen = substr.length,
                i;
            for (i = pos; i < maskLen && k < substrLen; ) {
                if (!me.isFixedChar(i) || mask[i] === substr[k]) {
                    if (me.isAllowedChar(substr[k++], i, true)) {
                        ++i;
                    }
                } else {
                    ++i;
                }
            }
            return i - pos;
        },
        insertRange: function(value, substr, pos) {
            var me = this,
                mask = me.getPattern(),
                blank = me.getBlank(),
                filled = me.isFilled(value),
                prefixLen = me._prefix.length,
                maskLen = mask.length,
                substrLen = substr.length,
                s = value,
                ch, fixed, i, k;
            if (!blank && pos > s.length) {
                s += mask.slice(s.length, pos);
            }
            for (i = pos , k = 0; i < maskLen && k < substrLen; ) {
                fixed = me.isFixedChar(i);
                if (!fixed || mask[i] === substr[k]) {
                    ch = substr[k++];
                    if (me.isAllowedChar(ch, i, true)) {
                        if (i < s.length) {
                            if (blank || filled || i < prefixLen) {
                                s = s.slice(0, i) + ch + s.slice(i + 1);
                            } else {
                                s = me.formatValue(s.substr(0, i) + ch + s.substr(i));
                            }
                        } else if (!blank) {
                            s += ch;
                        }
                        ++i;
                    }
                } else {
                    if (!blank && i >= s.length) {
                        s += mask[i];
                    } else if (blank && fixed && substr[k] === blank) {
                        ++k;
                    }
                    ++i;
                }
            }
            return s;
        },
        isAllowedChar: function(character, pos, allowBlankChar) {
            var me = this,
                mask = me.getPattern(),
                c, characters, rule;
            if (me.isFixedChar(pos)) {
                return mask[pos] === character;
            }
            c = mask[pos];
            characters = me.getCharacters();
            rule = characters[c];
            return !rule || rule.test(character || '') || (allowBlankChar && character === me.getBlank());
        },
        isEmpty: function(value) {
            var i, len;
            for (i = 0 , len = value.length; i < len; ++i) {
                if (!this.isFixedChar(i) && this.isAllowedChar(value[i], i)) {
                    return false;
                }
            }
            return true;
        },
        // TODO This function would benefit from optimization
        // Used during validation and range insert
        isFilled: function(value) {
            return this.getFilledLength(value) === this._mask.length;
        },
        isFixedChar: function(pos) {
            return Ext.Array.indexOf(this._fixedCharPositions, pos) > -1;
        },
        setCaretToEnd: function(field, value) {
            var filledLen = this.getFilledLength(value),
                pos = this.getEditPosRight(filledLen);
            if (pos !== null) {
                // Because we are called during a focus event, we have to delay pushing
                // down the new caret position to the next frame or else the browser will
                // position the caret at the end of the text. Note, Ext.asap() does *not*
                // work reliably for this.
                Ext.raf(function() {
                    if (!field.destroyed) {
                        field.setCaretPos(pos);
                        Ext.raf(function() {
                            if (!field.destroyed) {
                                field.setCaretPos(pos);
                            }
                        });
                    }
                });
            }
        },
        //---------------------------------------------------------------------
        // Event Handling
        onBlur: function(field, value) {
            if (field.getAutoHideInputMask() !== false) {
                if (this.isEmpty(value)) {
                    field.maskProcessed = true;
                    field.setValue('');
                }
            }
        },
        onFocus: function(field, value) {
            // On focus we have to show the mask and move caret to the last editable position
            // If field has autoHideInputMask === false, inputMask is always shown so we only
            // move the caret
            if (field.getAutoHideInputMask() !== false) {
                if (!value) {
                    field.maskProcessed = true;
                    field.setValue(this._mask);
                }
            }
            this.setCaretToEnd(field, value);
        },
        onChange: function(field, value, oldValue) {
            var me = this,
                s;
            if (field.maskProcessed || value === oldValue) {
                field.maskProcessed = false;
                return true;
            }
            if (value) {
                s = me.formatValue(value);
                field.maskProcessed = true;
                field.setValue(s);
            }
        },
        processAutocomplete: function(field, value) {
            var me = this,
                s;
            if (value) {
                if (value.length > me._mask.length) {
                    value = value.substr(0, me._mask.length);
                }
                s = me.formatValue(value);
                field.maskProcessed = true;
                field.inputElement.dom.value = s;
                // match DOM
                field.setValue(s);
                this.setCaretToEnd(field, value);
            }
        },
        /**
     * @private
     * @param field
     * @param adjustCaret {Boolean} move caret to the first editable position
     */
        showEmptyMask: function(field, adjustCaret) {
            var s = this.formatValue();
            field.maskProcessed = true;
            field.setValue(s);
            if (adjustCaret) {
                this.setCaretToEnd(field);
            }
        },
        onKeyDown: function(field, value, event) {
            if (event.ctrlKey || event.metaKey) {
                return;
            }
            // eslint-disable-next-line vars-on-top
            var me = this,
                // key = event.key(), // Does not work on mobile
                key = event.keyCode === event.DELETE,
                del = key === 'Delete',
                handled = del || (event.keyCode === event.BACKSPACE),
                s = value,
                caret, editPos, len, prefixLen, textSelection, start;
            if (handled) {
                caret = field.getCaretPos();
                prefixLen = me._prefix.length;
                textSelection = field.getTextSelection();
                start = textSelection[0];
                len = textSelection[1] - start;
                if (len) {
                    s = me.clearRange(value, start, len);
                } else if (caret < prefixLen || (!del && caret === prefixLen)) {
                    caret = prefixLen;
                } else {
                    editPos = del ? me.getEditPosRight(caret) : me.getEditPosLeft(caret - 1);
                    if (editPos !== null) {
                        s = me.clearRange(value, editPos, 1);
                        caret = editPos;
                    }
                }
                if (s !== value) {
                    field.maskProcessed = true;
                    field.setValue(s);
                }
                event.preventDefault();
                field.setCaretPos(caret);
            }
        },
        onKeyPress: function(field, value, event) {
            var me = this,
                key = event.keyCode,
                ch = event.getChar(),
                mask = me.getPattern(),
                prefixLen = me._prefix.length,
                s = value,
                caretPos, pos, start, textSelection;
            if (key === event.ENTER || key === event.TAB || event.ctrlKey || event.metaKey) {
                return;
            }
            // TODO Windows Phone may need to return here
            caretPos = field.getCaretPos();
            textSelection = field.getTextSelection();
            if (me.isFixedChar(caretPos) && mask[caretPos] === ch) {
                s = me.insertRange(s, ch, caretPos);
                ++caretPos;
            } else {
                pos = me.getEditPosRight(caretPos);
                if (pos !== null && me.isAllowedChar(ch, pos)) {
                    start = textSelection[0];
                    s = me.clearRange(s, start, textSelection[1] - start);
                    s = me.insertRange(s, ch, pos);
                    caretPos = pos + 1;
                }
            }
            if (s !== value) {
                field.maskProcessed = true;
                field.setValue(s);
            }
            event.preventDefault();
            if (caretPos < me._lastEditablePos && caretPos > prefixLen) {
                caretPos = me.getEditPosRight(caretPos);
            }
            field.setCaretPos(caretPos);
        },
        onPaste: function(field, value, event) {
            // TODO Android browser issues
            // https://bugs.chromium.org/p/chromium/issues/detail?id=369101
            var text,
                clipdData = event.browserEvent.clipboardData;
            if (clipdData && clipdData.getData) {
                text = clipdData.getData('text/plain');
            } else if (Ext.global.clipboardData && Ext.global.clipboardData.getData) {
                text = Ext.global.clipboardData.getData('Text');
            }
            // IE
            if (text) {
                this.paste(field, value, text, field.getTextSelection());
            }
            event.preventDefault();
        },
        paste: function(field, value, text, selection) {
            var me = this,
                caretPos = selection[0],
                len = selection[1] - caretPos,
                s = len ? me.clearRange(value, caretPos, len) : value,
                textLen = me.getSubLength(s, text, caretPos);
            s = me.insertRange(s, text, caretPos);
            caretPos += textLen;
            caretPos = me.getEditPosRight(caretPos) || caretPos;
            if (s !== value) {
                field.maskProcessed = true;
                field.setValue(s);
            }
            field.setCaretPos(caretPos);
        },
        syncPattern: function(field) {
            var fieldValue = field.getValue(),
                s;
            if (field.getAutoHideInputMask() === false) {
                // show blank input mask if there is no initial value
                if (!fieldValue) {
                    this.showEmptyMask(field);
                } else {
                    // format any value and combine with mask
                    s = this.formatValue(fieldValue);
                    field.maskProcessed = true;
                    field.setValue(s);
                }
            } else {
                // field has auto hide mask, but there might be an initial value
                // don't process empty value as that will set value to match the mask
                if (fieldValue) {
                    s = this.formatValue(fieldValue);
                    field.maskProcessed = true;
                    field.setValue(s);
                }
            }
        },
        //---------------------------------------------------------------------
        // Configs
        applyCharacters: function(map) {
            var ret = {},
                flags = this.getIgnoreCase() ? 'i' : '',
                c, v;
            for (c in map) {
                v = map[c];
                if (typeof v === 'string') {
                    v = new RegExp(v, flags);
                }
                ret[c] = v;
            }
            return ret;
        },
        updatePattern: function(mask) {
            var me = this,
                characters = me.getCharacters(),
                lastEditablePos = 0,
                n = mask && mask.length,
                blank = me.getBlank(),
                fixedPosArr = [],
                prefix = '',
                str = '',
                c, i;
            for (i = 0; i < n; ++i) {
                c = mask[i];
                if (!characters[c]) {
                    fixedPosArr.push(str.length);
                    str += c;
                } else {
                    lastEditablePos = str.length + 1;
                    str += blank;
                }
            }
            me._lastEditablePos = lastEditablePos;
            me._mask = str;
            me._fixedCharPositions = fixedPosArr;
            // Now that _fixedCharPositions are populated, isFixedChar can be used
            for (i = 0; i < str.length && me.isFixedChar(i); ++i) {
                prefix += str[i];
            }
            me._prefix = prefix;
        }
    };
}, function(InputMask) {
    InputMask.cache = new Ext.util.LRU();
    InputMask.cache.maxSize = 100;
});

/**
 * @private
 */
Ext.define('Ext.fx.State', {
    isAnimatable: {
        'background-color': true,
        'background-image': true,
        'background-position': true,
        'border-bottom-color': true,
        'border-bottom-width': true,
        'border-color': true,
        'border-left-color': true,
        'border-left-width': true,
        'border-right-color': true,
        'border-right-width': true,
        'border-spacing': true,
        'border-top-color': true,
        'border-top-width': true,
        'border-width': true,
        'bottom': true,
        'color': true,
        'crop': true,
        'font-size': true,
        'font-weight': true,
        'height': true,
        'left': true,
        'letter-spacing': true,
        'line-height': true,
        'margin-bottom': true,
        'margin-left': true,
        'margin-right': true,
        'margin-top': true,
        'max-height': true,
        'max-width': true,
        'min-height': true,
        'min-width': true,
        'opacity': true,
        'outline-color': true,
        'outline-offset': true,
        'outline-width': true,
        'padding-bottom': true,
        'padding-left': true,
        'padding-right': true,
        'padding-top': true,
        'right': true,
        'text-indent': true,
        'text-shadow': true,
        'top': true,
        'vertical-align': true,
        'visibility': true,
        'width': true,
        'word-spacing': true,
        'z-index': true,
        'zoom': true,
        'transform': true
    },
    constructor: function(data) {
        this.data = {};
        this.set(data);
    },
    setConfig: function(data) {
        this.set(data);
        return this;
    },
    setRaw: function(data) {
        this.data = data;
        return this;
    },
    clear: function() {
        return this.setRaw({});
    },
    setTransform: function(name, value) {
        var data = this.data,
            isArray = Ext.isArray(value),
            transform = data.transform,
            ln, key;
        if (!transform) {
            transform = data.transform = {
                translateX: 0,
                translateY: 0,
                translateZ: 0,
                scaleX: 1,
                scaleY: 1,
                scaleZ: 1,
                rotate: 0,
                rotateX: 0,
                rotateY: 0,
                rotateZ: 0,
                skewX: 0,
                skewY: 0
            };
        }
        if (typeof name === 'string') {
            switch (name) {
                case 'translate':
                    if (isArray) {
                        ln = value.length;
                        if (ln === 0) {
                            break;
                        }
                        transform.translateX = value[0];
                        if (ln === 1) {
                            break;
                        }
                        transform.translateY = value[1];
                        if (ln === 2) {
                            break;
                        }
                        transform.translateZ = value[2];
                    } else {
                        transform.translateX = value;
                    };
                    break;
                case 'rotate':
                    if (isArray) {
                        ln = value.length;
                        if (ln === 0) {
                            break;
                        }
                        transform.rotateX = value[0];
                        if (ln === 1) {
                            break;
                        }
                        transform.rotateY = value[1];
                        if (ln === 2) {
                            break;
                        }
                        transform.rotateZ = value[2];
                    } else {
                        transform.rotate = value;
                    };
                    break;
                case 'scale':
                    if (isArray) {
                        ln = value.length;
                        if (ln === 0) {
                            break;
                        }
                        transform.scaleX = value[0];
                        if (ln === 1) {
                            break;
                        }
                        transform.scaleY = value[1];
                        if (ln === 2) {
                            break;
                        }
                        transform.scaleZ = value[2];
                    } else {
                        transform.scaleX = value;
                        transform.scaleY = value;
                    };
                    break;
                case 'skew':
                    if (isArray) {
                        ln = value.length;
                        if (ln === 0) {
                            break;
                        }
                        transform.skewX = value[0];
                        if (ln === 1) {
                            break;
                        }
                        transform.skewY = value[1];
                    } else {
                        transform.skewX = value;
                    };
                    break;
                default:
                    transform[name] = value;
            }
        } else {
            for (key in name) {
                if (name.hasOwnProperty(key)) {
                    value = name[key];
                    this.setTransform(key, value);
                }
            }
        }
    },
    set: function(name, value) {
        var data = this.data,
            key;
        if (typeof name !== 'string') {
            for (key in name) {
                value = name[key];
                if (key === 'transform') {
                    this.setTransform(value);
                } else {
                    data[key] = value;
                }
            }
        } else {
            if (name === 'transform') {
                this.setTransform(value);
            } else {
                data[name] = value;
            }
        }
        return this;
    },
    unset: function(name) {
        var data = this.data;
        if (data.hasOwnProperty(name)) {
            delete data[name];
        }
        return this;
    },
    getData: function() {
        return this.data;
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.animation.Abstract', {
    extend: 'Ext.Evented',
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    factoryConfig: {
        type: 'animation'
    },
    isAnimation: true,
    requires: [
        'Ext.fx.State'
    ],
    config: {
        name: '',
        element: null,
        /**
         * @cfg {Object} before
         * Before configuration.
         */
        before: null,
        from: {},
        to: {},
        after: null,
        states: {},
        duration: 300,
        /**
         * @cfg {String} easing
         * Easing type.
         */
        easing: 'linear',
        iteration: 1,
        direction: 'normal',
        delay: 0,
        onBeforeStart: null,
        callback: null,
        onEnd: null,
        onBeforeEnd: null,
        scope: null,
        reverse: null,
        preserveEndState: false,
        replacePrevious: true
    },
    STATE_FROM: '0%',
    STATE_TO: '100%',
    DIRECTION_UP: 'up',
    DIRECTION_TOP: 'top',
    DIRECTION_DOWN: 'down',
    DIRECTION_BOTTOM: 'bottom',
    DIRECTION_LEFT: 'left',
    DIRECTION_RIGHT: 'right',
    stateNameRegex: /^(?:[\d.]+)%$/,
    constructor: function() {
        this.states = {};
        this.callParent(arguments);
        return this;
    },
    applyElement: function(element) {
        return Ext.get(element);
    },
    applyBefore: function(before, current) {
        if (before) {
            return Ext.factory(before, Ext.fx.State, current);
        }
    },
    applyAfter: function(after, current) {
        if (after) {
            return Ext.factory(after, Ext.fx.State, current);
        }
    },
    setFrom: function(from) {
        return this.setState(this.STATE_FROM, from);
    },
    setTo: function(to) {
        return this.setState(this.STATE_TO, to);
    },
    getFrom: function() {
        return this.getState(this.STATE_FROM);
    },
    getTo: function() {
        return this.getState(this.STATE_TO);
    },
    setStates: function(states) {
        var validNameRegex = this.stateNameRegex,
            name;
        for (name in states) {
            if (validNameRegex.test(name)) {
                this.setState(name, states[name]);
            }
        }
        return this;
    },
    getStates: function() {
        return this.states;
    },
    updateCallback: function(callback) {
        if (callback) {
            this.setOnEnd(callback);
        }
    },
    end: function() {
        // alias for stop so that the following api is the same between ext/touch:
        // element.getActiveAnimation().end()
        this.stop();
    },
    stop: function() {
        this.fireEvent('stop', this);
    },
    destroy: function() {
        // Event handlers need to know this.
        this.destroying = true;
        this.stop();
        this.callParent();
        this.destroying = false;
        this.destroyed = true;
    },
    setState: function(name, state) {
        var states = this.getStates(),
            stateInstance;
        stateInstance = Ext.factory(state, Ext.fx.State, states[name]);
        if (stateInstance) {
            states[name] = stateInstance;
        } else if (name === this.STATE_TO) {
            Ext.Logger.error("Setting and invalid '100%' / 'to' state of: " + state);
        }
        return this;
    },
    getState: function(name) {
        return this.getStates()[name];
    },
    getData: function() {
        var me = this,
            states = me.getStates(),
            statesData = {},
            before = me.getBefore(),
            after = me.getAfter(),
            from = states[me.STATE_FROM],
            to = states[me.STATE_TO],
            fromData = from.getData(),
            toData = to.getData(),
            data, name, state;
        for (name in states) {
            if (states.hasOwnProperty(name)) {
                state = states[name];
                data = state.getData();
                statesData[name] = data;
            }
        }
        return {
            before: before ? before.getData() : {},
            after: after ? after.getData() : {},
            states: statesData,
            from: fromData,
            to: toData,
            duration: me.getDuration(),
            iteration: me.getIteration(),
            direction: me.getDirection(),
            easing: me.getEasing(),
            delay: me.getDelay(),
            onEnd: me.getOnEnd(),
            onBeforeEnd: me.getOnBeforeEnd(),
            onBeforeStart: me.getOnBeforeStart(),
            scope: me.getScope(),
            preserveEndState: me.getPreserveEndState(),
            replacePrevious: me.getReplacePrevious()
        };
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.animation.Slide', {
    extend: 'Ext.fx.animation.Abstract',
    alternateClassName: 'Ext.fx.animation.SlideIn',
    alias: [
        'animation.slide',
        'animation.slideIn'
    ],
    config: {
        /**
         * @cfg {String} direction
         * The direction of which the slide animates
         * @accessor
         */
        direction: 'left',
        /**
         * @cfg {Boolean} out
         * True if you want to make this animation slide out, instead of slide in.
         * @accessor
         */
        out: false,
        /**
         * @cfg {Number} offset
         * The offset that the animation should go offscreen before entering (or when exiting)
         * @accessor
         */
        offset: 0,
        /**
         * @cfg easing
         * @inheritdoc
         */
        easing: 'auto',
        containerBox: 'auto',
        elementBox: 'auto',
        isElementBoxFit: true,
        useCssTransform: true
    },
    reverseDirectionMap: {
        up: 'down',
        top: 'down',
        down: 'up',
        bottom: 'up',
        left: 'right',
        right: 'left'
    },
    applyEasing: function(easing) {
        if (easing === 'auto') {
            return 'ease-' + ((this.getOut()) ? 'in' : 'out');
        }
        return easing;
    },
    getContainerBox: function() {
        var box = this._containerBox;
        if (box === 'auto') {
            box = this.getElement().getParent().getBox();
        }
        return box;
    },
    getElementBox: function() {
        var box = this._elementBox;
        if (this.getIsElementBoxFit()) {
            return this.getContainerBox();
        }
        if (box === 'auto') {
            box = this.getElement().getBox();
        }
        return box;
    },
    getData: function() {
        var elementBox = this.getElementBox(),
            containerBox = this.getContainerBox(),
            box = elementBox ? elementBox : containerBox,
            from = this.getFrom(),
            to = this.getTo(),
            out = this.getOut(),
            offset = this.getOffset(),
            direction = this.getDirection(),
            useCssTransform = this.getUseCssTransform(),
            reverse = this.getReverse(),
            translateX = 0,
            translateY = 0,
            offsetPct, fromX, fromY, toX, toY;
        if (typeof offset === 'string') {
            offsetPct = true;
            offset = parseFloat(offset);
        }
        if (reverse) {
            direction = this.reverseDirectionMap[direction];
        }
        switch (direction) {
            case this.DIRECTION_UP:
            case this.DIRECTION_TOP:
                if (offsetPct) {
                    offset = box.height * offset / 100;
                };
                if (out) {
                    translateY = containerBox.top - box.top - box.height - offset;
                } else {
                    translateY = containerBox.bottom - box.bottom + box.height + offset;
                };
                break;
            case this.DIRECTION_DOWN:
            case this.DIRECTION_BOTTOM:
                if (offsetPct) {
                    offset = box.height * offset / 100;
                };
                if (out) {
                    translateY = containerBox.bottom - box.bottom + box.height + offset;
                } else {
                    translateY = containerBox.top - box.height - box.top - offset;
                };
                break;
            case this.DIRECTION_RIGHT:
                if (offsetPct) {
                    offset = box.width * offset / 100;
                };
                if (out) {
                    translateX = containerBox.right - box.right + box.width + offset;
                } else {
                    translateX = containerBox.left - box.left - box.width - offset;
                };
                break;
            case this.DIRECTION_LEFT:
                if (offsetPct) {
                    offset = box.width * offset / 100;
                };
                if (out) {
                    translateX = containerBox.left - box.left - box.width - offset;
                } else {
                    translateX = containerBox.right - box.right + box.width + offset;
                };
                break;
        }
        fromX = (out) ? 0 : translateX;
        fromY = (out) ? 0 : translateY;
        if (useCssTransform) {
            from.setTransform({
                translateX: fromX,
                translateY: fromY
            });
        } else {
            from.set('left', fromX);
            from.set('top', fromY);
        }
        toX = (out) ? translateX : 0;
        toY = (out) ? translateY : 0;
        if (useCssTransform) {
            to.setTransform({
                translateX: toX,
                translateY: toY
            });
        } else {
            to.set('left', toX);
            to.set('top', toY);
        }
        return this.callParent(arguments);
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.animation.SlideOut', {
    extend: 'Ext.fx.animation.Slide',
    alias: [
        'animation.slideOut'
    ],
    config: {
        // @hide
        out: true
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.animation.Fade', {
    extend: 'Ext.fx.animation.Abstract',
    alternateClassName: 'Ext.fx.animation.FadeIn',
    alias: [
        'animation.fade',
        'animation.fadeIn'
    ],
    config: {
        /**
         * @cfg {Boolean} out True if you want to make this animation fade out, instead of fade in.
         * @accessor
         */
        out: false,
        // Needs to default to false so updater runs and populates from/to data.
        before: {
            display: null,
            opacity: 0
        },
        after: {
            opacity: null
        },
        reverse: null
    },
    updateOut: function(newOut) {
        var to = this.getTo(),
            from = this.getFrom();
        if (newOut) {
            from.set('opacity', 1);
            to.set('opacity', 0);
        } else {
            from.set('opacity', 0);
            to.set('opacity', 1);
        }
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.animation.FadeOut', {
    extend: 'Ext.fx.animation.Fade',
    alias: 'animation.fadeOut',
    config: {
        // @hide
        out: true,
        before: {}
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.animation.Flip', {
    extend: 'Ext.fx.animation.Abstract',
    alias: 'animation.flip',
    config: {
        easing: 'ease-in',
        /**
         * @cfg {String} direction The direction of which the slide animates
         * @accessor
         */
        direction: 'right',
        half: false,
        out: null
    },
    getData: function() {
        var me = this,
            from = me.getFrom(),
            to = me.getTo(),
            direction = me.getDirection(),
            out = me.getOut(),
            half = me.getHalf(),
            rotate = half ? 90 : 180,
            fromScale = 1,
            toScale = 1,
            fromRotateX = 0,
            fromRotateY = 0,
            toRotateX = 0,
            toRotateY = 0;
        if (out) {
            toScale = 0.8;
        } else {
            fromScale = 0.8;
        }
        switch (direction) {
            case this.DIRECTION_UP:
            case this.DIRECTION_TOP:
                if (out) {
                    toRotateX = rotate;
                } else {
                    fromRotateX = -rotate;
                };
                break;
            case this.DIRECTION_DOWN:
            case this.DIRECTION_BOTTOM:
                if (out) {
                    toRotateX = -rotate;
                } else {
                    fromRotateX = rotate;
                };
                break;
            case this.DIRECTION_RIGHT:
                if (out) {
                    toRotateY = rotate;
                } else {
                    fromRotateY = -rotate;
                };
                break;
            case this.DIRECTION_LEFT:
                if (out) {
                    toRotateY = -rotate;
                } else {
                    fromRotateY = rotate;
                };
                break;
        }
        from.setTransform({
            rotateX: fromRotateX,
            rotateY: fromRotateY,
            scale: fromScale
        });
        to.setTransform({
            rotateX: toRotateX,
            rotateY: toRotateY,
            scale: toScale
        });
        return this.callParent();
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.animation.Pop', {
    extend: 'Ext.fx.animation.Abstract',
    alias: [
        'animation.pop',
        'animation.popIn'
    ],
    alternateClassName: 'Ext.fx.animation.PopIn',
    config: {
        /**
         * @cfg {Boolean} out True if you want to make this animation pop out, instead of pop in.
         * @accessor
         */
        out: false,
        before: {
            display: null,
            opacity: 0
        },
        after: {
            opacity: null
        }
    },
    getData: function() {
        var to = this.getTo(),
            from = this.getFrom(),
            out = this.getOut();
        if (out) {
            from.set('opacity', 1);
            from.setTransform({
                scale: 1
            });
            to.set('opacity', 0);
            to.setTransform({
                scale: 0
            });
        } else {
            from.set('opacity', 0);
            from.setTransform({
                scale: 0
            });
            to.set('opacity', 1);
            to.setTransform({
                scale: 1
            });
        }
        return this.callParent(arguments);
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.animation.PopOut', {
    extend: 'Ext.fx.animation.Pop',
    alias: 'animation.popOut',
    config: {
        // @hide
        out: true,
        before: {}
    }
});

/**
 * @private
 * This class is a factory class that will create and return an animation class based on the
 * {@link #type} configuration.
 */
Ext.define('Ext.fx.Animation', {
    requires: [
        'Ext.fx.animation.Slide',
        'Ext.fx.animation.SlideOut',
        'Ext.fx.animation.Fade',
        'Ext.fx.animation.FadeOut',
        'Ext.fx.animation.Flip',
        'Ext.fx.animation.Pop',
        'Ext.fx.animation.PopOut'
    ],
    /**
     * @cfg {String} type The type of animation to use. The possible values are:
     *
     *  - `fade` - {@link Ext.fx.animation.Fade}
     *  - `fadeOut` - {@link Ext.fx.animation.FadeOut}
     *  - `flip` - {@link Ext.fx.animation.Flip}
     *  - `pop` - {@link Ext.fx.animation.Pop}
     *  - `popOut` - {@link Ext.fx.animation.PopOut}
     *  - `slide` - {@link Ext.fx.animation.Slide}
     *  - `slideOut` - {@link Ext.fx.animation.SlideOut}
     */
    constructor: function(config) {
        var defaultClass = Ext.fx.animation.Abstract,
            type;
        if (typeof config === 'string') {
            type = config;
            config = {};
        } else if (config && config.type) {
            type = config.type;
        }
        if (type) {
            defaultClass = Ext.ClassManager.getByAlias('animation.' + type);
            if (!defaultClass) {
                Ext.Logger.error("Invalid animation type of: '" + type + "'");
            }
        }
        return Ext.factory(config, defaultClass);
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.runner.Css', {
    extend: 'Ext.Evented',
    requires: [
        'Ext.fx.Animation'
    ],
    prefixedProperties: {
        'transform': true,
        'transform-origin': true,
        'perspective': true,
        'transform-style': true,
        'transition': true,
        'transition-property': true,
        'transition-duration': true,
        'transition-timing-function': true,
        'transition-delay': true,
        'animation': true,
        'animation-name': true,
        'animation-duration': true,
        'animation-iteration-count': true,
        'animation-direction': true,
        'animation-timing-function': true,
        'animation-delay': true
    },
    lengthProperties: {
        'top': true,
        'right': true,
        'bottom': true,
        'left': true,
        'width': true,
        'height': true,
        'max-height': true,
        'max-width': true,
        'min-height': true,
        'min-width': true,
        'margin-bottom': true,
        'margin-left': true,
        'margin-right': true,
        'margin-top': true,
        'padding-bottom': true,
        'padding-left': true,
        'padding-right': true,
        'padding-top': true,
        'border-bottom-width': true,
        'border-left-width': true,
        'border-right-width': true,
        'border-spacing': true,
        'border-top-width': true,
        'border-width': true,
        'outline-width': true,
        'letter-spacing': true,
        'line-height': true,
        'text-indent': true,
        'word-spacing': true,
        'font-size': true,
        'translate': true,
        'translateX': true,
        'translateY': true,
        'translateZ': true,
        'translate3d': true,
        'x': true,
        'y': true
    },
    durationProperties: {
        'transition-duration': true,
        'transition-delay': true,
        'animation-duration': true,
        'animation-delay': true
    },
    angleProperties: {
        rotate: true,
        rotateX: true,
        rotateY: true,
        rotateZ: true,
        skew: true,
        skewX: true,
        skewY: true
    },
    DEFAULT_UNIT_LENGTH: 'px',
    DEFAULT_UNIT_ANGLE: 'deg',
    DEFAULT_UNIT_DURATION: 'ms',
    customProperties: {
        x: true,
        y: true
    },
    formattedNameCache: {
        'x': 'left',
        'y': 'top'
    },
    transformMethods3d: [
        'translateX',
        'translateY',
        'translateZ',
        'rotate',
        'rotateX',
        'rotateY',
        'rotateZ',
        'skewX',
        'skewY',
        'scaleX',
        'scaleY',
        'scaleZ'
    ],
    transformMethodsNo3d: [
        'translateX',
        'translateY',
        'rotate',
        'skewX',
        'skewY',
        'scaleX',
        'scaleY'
    ],
    constructor: function() {
        var me = this;
        me.transformMethods = Ext.feature.has.Css3dTransforms ? me.transformMethods3d : me.transformMethodsNo3d;
        me.vendorPrefix = Ext.browser.getStyleDashPrefix();
        me.ruleStylesCache = {};
        me.callParent();
    },
    getStyleSheet: function() {
        var styleSheet = this.styleSheet,
            styleElement, styleSheets;
        if (!styleSheet) {
            styleElement = document.createElement('style');
            styleElement.type = 'text/css';
            (document.head || document.getElementsByTagName('head')[0]).appendChild(styleElement);
            styleSheets = document.styleSheets;
            this.styleSheet = styleSheet = styleSheets[styleSheets.length - 1];
        }
        return styleSheet;
    },
    applyRules: function(selectors) {
        var styleSheet = this.getStyleSheet(),
            ruleStylesCache = this.ruleStylesCache,
            rules = styleSheet.cssRules,
            selector, properties, ruleStyle, ruleStyleCache, rulesLength, name, value;
        for (selector in selectors) {
            properties = selectors[selector];
            ruleStyle = ruleStylesCache[selector];
            if (ruleStyle === undefined) {
                rulesLength = rules.length;
                styleSheet.insertRule(selector + '{}', rulesLength);
                ruleStyle = ruleStylesCache[selector] = rules.item(rulesLength).style;
            }
            ruleStyleCache = ruleStyle.$cache;
            if (!ruleStyleCache) {
                ruleStyleCache = ruleStyle.$cache = {};
            }
            for (name in properties) {
                value = this.formatValue(properties[name], name);
                name = this.formatName(name);
                if (ruleStyleCache[name] !== value) {
                    ruleStyleCache[name] = value;
                    if (value === null) {
                        ruleStyle.removeProperty(name);
                    } else {
                        ruleStyle.setProperty(name, value);
                    }
                }
            }
        }
        return this;
    },
    applyStyles: function(styles) {
        var id, element, elementStyle, properties, name, value;
        for (id in styles) {
            if (styles.hasOwnProperty(id)) {
                this.activeElement = element = document.getElementById(id);
                if (!element) {
                    
                    continue;
                }
                elementStyle = element.style;
                properties = styles[id];
                for (name in properties) {
                    if (properties.hasOwnProperty(name)) {
                        value = this.formatValue(properties[name], name);
                        name = this.formatName(name);
                        if (value === null) {
                            elementStyle.removeProperty(name);
                        } else {
                            elementStyle.setProperty(name, value);
                        }
                    }
                }
            }
        }
        this.activeElement = null;
        return this;
    },
    formatName: function(name) {
        var cache = this.formattedNameCache,
            formattedName = cache[name];
        if (!formattedName) {
            if ((Ext.os.is.Tizen || !Ext.feature.has.CssTransformNoPrefix) && this.prefixedProperties[name]) {
                formattedName = this.vendorPrefix + name;
            } else {
                formattedName = name;
            }
            cache[name] = formattedName;
        }
        return formattedName;
    },
    formatValue: function(value, name) {
        var type = typeof value,
            defaultLengthUnit = this.DEFAULT_UNIT_LENGTH,
            isCustom = this.customProperties[name],
            transformMethods, method, i, ln, transformValues, values;
        if (value === null) {
            return '';
        }
        if (type === 'string') {
            if (this.lengthProperties[name]) {
                if (!Ext.dom.Element.hasUnit(value)) {
                    value = value + defaultLengthUnit;
                    if (isCustom) {
                        value = this.getCustomValue(value, name);
                    }
                }
            }
            return value;
        } else if (type === 'number') {
            if (value === 0) {
                return '0';
            }
            if (this.lengthProperties[name]) {
                value = value + defaultLengthUnit;
                if (isCustom) {
                    value = this.getCustomValue(value, name);
                }
                return value;
            }
            if (this.angleProperties[name]) {
                return value + this.DEFAULT_UNIT_ANGLE;
            }
            if (this.durationProperties[name]) {
                return value + this.DEFAULT_UNIT_DURATION;
            }
        } else if (name === 'transform') {
            transformMethods = this.transformMethods;
            transformValues = [];
            for (i = 0 , ln = transformMethods.length; i < ln; i++) {
                method = transformMethods[i];
                transformValues.push(method + '(' + this.formatValue(value[method], method) + ')');
            }
            return transformValues.join(' ');
        } else if (Ext.isArray(value)) {
            values = [];
            for (i = 0 , ln = value.length; i < ln; i++) {
                values.push(this.formatValue(value[i], name));
            }
            return (values.length > 0) ? values.join(', ') : 'none';
        }
        return value;
    },
    getCustomValue: function(value, name) {
        var el = Ext.fly(this.activeElement);
        if (name === 'x') {
            value = el.translateXY(parseInt(value, 10)).x;
        } else if (name === 'y') {
            value = el.translateXY(null, parseInt(value, 10)).y;
        }
        return value + this.DEFAULT_UNIT_LENGTH;
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.runner.CssTransition', {
    extend: 'Ext.fx.runner.Css',
    requires: [
        'Ext.AnimationQueue'
    ],
    alternateClassName: 'Ext.Animator',
    singleton: true,
    listenersAttached: false,
    constructor: function() {
        this.runningAnimationsData = {};
        // if multiple animations run at the same time then we need to queue them
        // to apply them in the same animation frame (see requestAnimationFrame in "run")
        this.transitionQueue = {
            toData: {},
            transitionData: {}
        };
        return this.callParent(arguments);
    },
    attachListeners: function() {
        // NOTE: Ext.getWin() has been used for many years but it doesn't appear to work
        // in the test runner iframe.
        var target = (top === window) ? Ext.getWin() : Ext.getBody();
        this.listenersAttached = true;
        target.on('transitionend', 'onTransitionEnd', this);
    },
    onTransitionEnd: function(e) {
        var target = e.target,
            id = target.id;
        if (id && this.runningAnimationsData.hasOwnProperty(id)) {
            this.refreshRunningAnimationsData(Ext.get(target), [
                e.browserEvent.propertyName
            ]);
        }
    },
    getElementId: function(element) {
        // usually when the element is destroyed the getId function is nullified
        return element.getId ? element.getId() : element.id;
    },
    onAnimationEnd: function(element, data, animation, isInterrupted, isReplaced) {
        var id = this.getElementId(element),
            runningData = this.runningAnimationsData[id],
            endRules = {},
            endData = {},
            runningNameMap, toPropertyNames, i, ln, name;
        animation.un('stop', 'onAnimationStop', this);
        if (runningData) {
            runningNameMap = runningData.nameMap;
        }
        endRules[id] = endData;
        if (data.onBeforeEnd) {
            data.onBeforeEnd.call(data.scope || this, element, isInterrupted);
        }
        animation.fireEvent('animationbeforeend', animation, element, isInterrupted);
        this.fireEvent('animationbeforeend', this, animation, element, isInterrupted);
        if (isReplaced || (!isInterrupted && !data.preserveEndState)) {
            toPropertyNames = data.toPropertyNames;
            for (i = 0 , ln = toPropertyNames.length; i < ln; i++) {
                name = toPropertyNames[i];
                if (runningNameMap && !runningNameMap.hasOwnProperty(name)) {
                    endData[name] = null;
                }
            }
        }
        if (data.after) {
            Ext.merge(endData, data.after);
        }
        this.applyStyles(endRules);
        if (data.onEnd) {
            data.onEnd.call(data.scope || this, element, isInterrupted);
        }
        animation.fireEvent('animationend', animation, element, isInterrupted);
        this.fireEvent('animationend', this, animation, element, isInterrupted);
        Ext.AnimationQueue.stop(Ext.emptyFn, animation);
    },
    onAllAnimationsEnd: function(element) {
        var id = this.getElementId(element),
            transitionQueue = this.transitionQueue,
            endRules = {};
        delete this.runningAnimationsData[id];
        endRules[id] = {
            'transition-property': null,
            'transition-duration': null,
            'transition-timing-function': null,
            'transition-delay': null
        };
        delete transitionQueue.toData[id];
        delete transitionQueue.transitionData[id];
        this.applyStyles(endRules);
        this.fireEvent('animationallend', this, element);
    },
    hasRunningAnimations: function(element) {
        var id = this.getElementId(element),
            runningAnimationsData = this.runningAnimationsData;
        return runningAnimationsData.hasOwnProperty(id) && runningAnimationsData[id].sessions.length > 0;
    },
    refreshRunningAnimationsData: function(element, propertyNames, interrupt, replace) {
        var id = this.getElementId(element),
            runningAnimationsData = this.runningAnimationsData,
            runningData = runningAnimationsData[id],
            hasCompletedSession = false,
            nameMap, nameList, sessions, name, session, map, list, i, ln, j, subLn;
        if (!runningData) {
            return;
        }
        nameMap = runningData.nameMap;
        nameList = runningData.nameList;
        sessions = runningData.sessions;
        interrupt = Boolean(interrupt);
        replace = Boolean(replace);
        if (!sessions) {
            return this;
        }
        ln = sessions.length;
        if (ln === 0) {
            return this;
        }
        if (replace) {
            runningData.nameMap = {};
            nameList.length = 0;
            for (i = 0; i < ln; i++) {
                session = sessions[i];
                this.onAnimationEnd(element, session.data, session.animation, interrupt, replace);
            }
            sessions.length = 0;
        } else {
            for (i = 0; i < ln; i++) {
                session = sessions[i];
                map = session.map;
                list = session.list;
                for (j = 0 , subLn = propertyNames.length; j < subLn; j++) {
                    name = propertyNames[j];
                    if (map[name]) {
                        delete map[name];
                        Ext.Array.remove(list, name);
                        session.length--;
                        if (--nameMap[name] === 0) {
                            delete nameMap[name];
                            Ext.Array.remove(nameList, name);
                        }
                    }
                }
                if (session.length === 0) {
                    sessions.splice(i, 1);
                    i--;
                    ln--;
                    hasCompletedSession = true;
                    this.onAnimationEnd(element, session.data, session.animation, interrupt);
                }
            }
        }
        if (!replace && !interrupt && sessions.length === 0 && hasCompletedSession) {
            this.onAllAnimationsEnd(element);
        }
    },
    getRunningData: function(id) {
        var runningAnimationsData = this.runningAnimationsData;
        if (!runningAnimationsData.hasOwnProperty(id)) {
            runningAnimationsData[id] = {
                nameMap: {},
                nameList: [],
                sessions: []
            };
        }
        return runningAnimationsData[id];
    },
    getTestElement: function() {
        var me = this,
            testElement = me.testElement,
            iframe = me.iframe,
            iframeDocument, iframeStyle;
        if (testElement) {
            // https://sencha.jira.com/browse/EXTJS-21131
            // Forward navigation in Chrome 50 navigates iframes, and orphans
            // the testElement in a detached document. Reconnect it if this has happened.
            if (testElement.ownerDocument.defaultView !== iframe.contentWindow) {
                iframeDocument = iframe.contentDocument;
                iframeDocument.body.appendChild(testElement);
                // eslint-disable-next-line max-len
                me.testElementComputedStyle = iframeDocument.defaultView.getComputedStyle(testElement);
            }
        } else {
            iframe = me.iframe = document.createElement('iframe');
            // Set an attribute that tells the test runner to ignore this node when checking
            // for dom cleanup
            iframe.setAttribute('data-sticky', true);
            iframe.setAttribute('tabIndex', -1);
            iframeStyle = iframe.style;
            iframeStyle.setProperty('visibility', 'hidden', 'important');
            iframeStyle.setProperty('width', '0px', 'important');
            iframeStyle.setProperty('height', '0px', 'important');
            iframeStyle.setProperty('position', 'absolute', 'important');
            iframeStyle.setProperty('border', '0px', 'important');
            iframeStyle.setProperty('zIndex', '-1000', 'important');
            document.body.appendChild(iframe);
            iframeDocument = iframe.contentDocument;
            iframeDocument.open();
            iframeDocument.writeln('</body>');
            iframeDocument.close();
            me.testElement = testElement = iframeDocument.createElement('div');
            testElement.style.setProperty('position', 'absolute', 'important');
            iframeDocument.body.appendChild(testElement);
            me.testElementComputedStyle = iframeDocument.defaultView.getComputedStyle(testElement);
        }
        return testElement;
    },
    getCssStyleValue: function(name, value) {
        var testElement = this.getTestElement(),
            computedStyle = this.testElementComputedStyle,
            style = testElement.style;
        style.setProperty(name, value);
        if (Ext.browser.is.Firefox) {
            // We force a repaint of the element in Firefox to make sure the computedStyle
            // to be updated
            // eslint-disable-next-line no-unused-expressions
            testElement.offsetHeight;
        }
        value = computedStyle.getPropertyValue(name);
        style.removeProperty(name);
        return value;
    },
    run: function(animations) {
        var me = this,
            ret = [],
            isLengthPropertyMap = me.lengthProperties,
            fromData = {},
            toData = me.transitionQueue.toData,
            data = {},
            transitionData = me.transitionQueue.transitionData,
            element, elementId, from, to, before, fromPropertyNames, toPropertyNames, doApplyTo, message, runningData, elementData, i, j, ln, animation, propertiesLength, sessionNameMap, computedStyle, formattedName, name, toFormattedValue, computedValue, fromFormattedValue, isLengthProperty, runningNameMap, runningNameList, runningSessions, runningSession, messageTimerFn, onBeforeStart;
        if (!me.listenersAttached) {
            me.attachListeners();
        }
        animations = Ext.Array.from(animations);
        for (i = 0 , ln = animations.length; i < ln; i++) {
            animation = animations[i];
            animation = Ext.factory(animation, Ext.fx.Animation);
            ret.push(animation);
            me.activeElement = element = animation.getElement();
            // Empty function to prevent idleTasks from running while we animate.
            Ext.AnimationQueue.start(Ext.emptyFn, animation);
            computedStyle = window.getComputedStyle(element.dom);
            elementId = me.getElementId(element);
            data[elementId] = data = Ext.merge({}, animation.getData());
            onBeforeStart = animation.getOnBeforeStart();
            if (onBeforeStart) {
                onBeforeStart.call(animation.scope || me, element);
            }
            // Allow listeners to mutate animation data
            animation.fireEvent('animationstart', animation, data);
            me.fireEvent('animationstart', me, animation, data);
            before = data.before;
            from = data.from;
            to = data.to;
            data.fromPropertyNames = fromPropertyNames = [];
            data.toPropertyNames = toPropertyNames = [];
            for (name in to) {
                if (to.hasOwnProperty(name)) {
                    to[name] = toFormattedValue = me.formatValue(to[name], name);
                    formattedName = me.formatName(name);
                    isLengthProperty = isLengthPropertyMap.hasOwnProperty(name);
                    if (!isLengthProperty) {
                        toFormattedValue = me.getCssStyleValue(formattedName, toFormattedValue);
                    }
                    if (from.hasOwnProperty(name)) {
                        from[name] = fromFormattedValue = me.formatValue(from[name], name);
                        if (!isLengthProperty) {
                            fromFormattedValue = me.getCssStyleValue(formattedName, fromFormattedValue);
                        }
                        if (toFormattedValue !== fromFormattedValue) {
                            fromPropertyNames.push(formattedName);
                            toPropertyNames.push(formattedName);
                        }
                    } else {
                        computedValue = computedStyle.getPropertyValue(formattedName);
                        if (toFormattedValue !== computedValue) {
                            toPropertyNames.push(formattedName);
                        }
                    }
                }
            }
            propertiesLength = toPropertyNames.length;
            if (propertiesLength === 0) {
                me.onAnimationEnd(element, data, animation);
                
                continue;
            }
            runningData = me.getRunningData(elementId);
            runningSessions = runningData.sessions;
            if (runningSessions.length > 0) {
                me.refreshRunningAnimationsData(element, Ext.Array.merge(fromPropertyNames, toPropertyNames), true, data.replacePrevious);
            }
            runningNameMap = runningData.nameMap;
            runningNameList = runningData.nameList;
            sessionNameMap = {};
            for (j = 0; j < propertiesLength; j++) {
                name = toPropertyNames[j];
                sessionNameMap[name] = true;
                if (!runningNameMap.hasOwnProperty(name)) {
                    runningNameMap[name] = 1;
                    runningNameList.push(name);
                } else {
                    runningNameMap[name]++;
                }
            }
            runningSession = {
                element: element,
                map: sessionNameMap,
                list: toPropertyNames.slice(),
                length: propertiesLength,
                data: data,
                animation: animation
            };
            runningSessions.push(runningSession);
            animation.on('stop', 'onAnimationStop', me);
            elementData = Ext.apply({}, before);
            Ext.apply(elementData, from);
            if (runningNameList.length > 0) {
                fromPropertyNames = Ext.Array.difference(runningNameList, fromPropertyNames);
                toPropertyNames = Ext.Array.merge(fromPropertyNames, toPropertyNames);
                elementData['transition-property'] = fromPropertyNames;
            }
            fromData[elementId] = elementData;
            toData[elementId] = Ext.apply({}, to);
            transitionData[elementId] = {
                'transition-property': toPropertyNames,
                'transition-duration': data.duration,
                'transition-timing-function': data.easing,
                'transition-delay': data.delay
            };
            animation.startTime = Date.now();
        }
        me.activeElement = null;
        message = me.$className;
        me.applyStyles(fromData);
        doApplyTo = function(e) {
            if (e.data === message && e.source === window) {
                window.removeEventListener('message', doApplyTo, false);
                me.applyStyles(me.transitionQueue.toData);
            }
        };
        if (!me.messageTimerId) {
            messageTimerFn = function() {
                var messageFollowupFn;
                me.messageTimerId = null;
                if (Ext.isIE) {
                    // https://sencha.jira.com/browse/EXTJS-22362
                    // In some cases IE will fail to animate if the "to" and "transition" styles
                    // are added simultaneously. That is the reason for the multi-delay below.
                    // The first one defines the transition parameters ('transition-property',
                    // 'transition-delay' etc) and the second delay sets the values of the
                    // animating properties, or, the "to" properties. The second delay
                    // is what actually starts the animation.
                    me.applyStyles(me.transitionQueue.transitionData);
                    if (!me.messageFollowupId) {
                        messageFollowupFn = function() {
                            me.messageFollowupId = null;
                            window.addEventListener('message', doApplyTo, false);
                            window.postMessage(message, '*');
                        };
                        messageFollowupFn.$skipTimerCheck = true;
                        me.messageFollowupId = Ext.raf(messageFollowupFn);
                    }
                } else {
                    // In non-IE browsers the above approach can cause a flicker,
                    // so in these browsers we apply all the styles at the same time.
                    Ext.merge(me.transitionQueue.toData, me.transitionQueue.transitionData);
                    window.addEventListener('message', doApplyTo, false);
                    window.postMessage(message, '*');
                }
            };
            messageTimerFn.$skipTimerCheck = true;
            me.messageTimerId = Ext.raf(messageTimerFn);
        }
        // TODO: This method needs to attach something to the element it is animating
        // we then need to monitor for destruction of that element
        // and clean up any animations that remain.
        return ret;
    },
    onAnimationStop: function(animation) {
        var me = this,
            runningAnimationsData = me.runningAnimationsData,
            activeAnimations = 0,
            stoppedAnimations = 0,
            id, runningData, sessions, i, ln, session;
        for (id in runningAnimationsData) {
            if (runningAnimationsData.hasOwnProperty(id)) {
                runningData = runningAnimationsData[id];
                sessions = runningData.sessions;
                activeAnimations++;
                for (i = 0 , ln = sessions.length; i < ln; i++) {
                    session = sessions[i];
                    if (session.animation === animation) {
                        me.refreshRunningAnimationsData(session.element, session.list.slice(), false);
                        if (animation.destroying) {
                            stoppedAnimations++;
                        }
                    }
                }
            }
        }
        if (activeAnimations === stoppedAnimations) {
            if (me.messageFollowupId) {
                Ext.unraf(me.messageFollowupId);
                me.messageFollowupId = null;
            }
            if (me.messageTimerId) {
                Ext.unraf(me.messageTimerId);
                me.messageTimerId = null;
            }
            Ext.apply(me.transitionQueue, {
                toData: {},
                transitionData: {}
            });
        }
    }
});

/**
 * @class Ext.fx.Runner
 * @private
 */
Ext.define('Ext.fx.Runner', {
    requires: [
        'Ext.fx.runner.CssTransition'
    ],
    //        'Ext.fx.runner.CssAnimation'
    constructor: function() {
        return new Ext.fx.runner.CssTransition();
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.animation.Cube', {
    extend: 'Ext.fx.animation.Abstract',
    alias: 'animation.cube',
    config: {
        /**
         * @cfg before
         * @inheritdoc
         */
        before: {},
        //            'transform-style': 'preserve-3d'
        after: {},
        /**
         * @cfg {String} direction
         * The direction of which the slide animates
         * @accessor
         */
        direction: 'right',
        out: false
    },
    //    getData: function() {
    //        var to = this.getTo(),
    //            from = this.getFrom(),
    //            out  = this.getOut(),
    //            direction  = this.getDirection(),
    //            el = this.getElement(),
    //            elW = el.getWidth(),
    //            elH = el.getHeight(),
    //            halfWidth = (elW / 2),
    //            halfHeight = (elH / 2),
    //            fromTransform = {},
    //            toTransform = {},
    //            originalFromTransform = {
    //                rotateY: 0,
    //                translateX: 0,
    //                translateZ: 0
    //            },
    //            originalToTransform = {
    //                rotateY: 90,
    //                translateX: halfWidth,
    //                translateZ: halfWidth
    //            },
    //            originalVerticalFromTransform = {
    //                rotateX: 0,
    //                translateY: 0,
    //                translateZ: 0
    //            },
    //            originalVerticalToTransform = {
    //                rotateX: 90,
    //                translateY: halfHeight,
    //                translateZ: halfHeight
    //            },
    //            tempTransform;
    //
    //        if (direction == "left" || direction == "right") {
    //            if (out) {
    //                toTransform = originalToTransform;
    //                fromTransform = originalFromTransform;
    //            } else {
    //                toTransform = originalFromTransform;
    //                fromTransform = originalToTransform;
    //                fromTransform.rotateY *= -1;
    //                fromTransform.translateX *= -1;
    //            }
    //
    //            if (direction === 'right') {
    //                tempTransform = fromTransform;
    //                fromTransform = toTransform;
    //                toTransform = tempTransform;
    //            }
    //        }
    //
    //        if (direction == "up" || direction == "down") {
    //            if (out) {
    //                toTransform = originalVerticalFromTransform;
    //                fromTransform = {
    //                    rotateX: -90,
    //                    translateY: halfHeight,
    //                    translateZ: halfHeight
    //                };
    //            } else {
    //                fromTransform = originalVerticalFromTransform;
    //                toTransform = {
    //                    rotateX: 90,
    //                    translateY: -halfHeight,
    //                    translateZ: halfHeight
    //                };
    //            }
    //
    //            if (direction == "up") {
    //                tempTransform = fromTransform;
    //                fromTransform = toTransform;
    //                toTransform = tempTransform;
    //            }
    //        }
    //
    //        from.set('transform', fromTransform);
    //        to.set('transform', toTransform);
    //
    //        return this.callParent(arguments);
    //    },
    getData: function() {
        var to = this.getTo(),
            from = this.getFrom(),
            before = this.getBefore(),
            after = this.getAfter(),
            out = this.getOut(),
            direction = this.getDirection(),
            el = this.getElement(),
            elW = el.getWidth(),
            origin = out ? '100% 100%' : '0% 0%',
            fromOpacity = 1,
            toOpacity = 1,
            transformFrom = {
                rotateY: 0,
                translateZ: 0
            },
            transformTo = {
                rotateY: 0,
                translateZ: 0
            };
        if (direction === "left" || direction === "right") {
            if (out) {
                toOpacity = 0.5;
                transformTo.translateZ = elW;
                transformTo.rotateY = -90;
            } else {
                fromOpacity = 0.5;
                transformFrom.translateZ = elW;
                transformFrom.rotateY = 90;
            }
        }
        before['transform-origin'] = origin;
        after['transform-origin'] = null;
        to.set('transform', transformTo);
        from.set('transform', transformFrom);
        from.set('opacity', fromOpacity);
        to.set('opacity', toOpacity);
        return this.callParent(arguments);
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.animation.Wipe', {
    extend: 'Ext.fx.Animation',
    alternateClassName: 'Ext.fx.animation.WipeIn',
    config: {
        /**
         * @cfg {String}
         * Valid values are 'ease', 'linear', ease-in', 'ease-out', 'ease-in-out',
         * or a cubic-bezier curve as defined by CSS.
         */
        easing: 'ease-out',
        /**
         * @cfg {String} direction The direction of which the slide animates
         * @accessor
         */
        direction: 'right',
        /**
         * @cfg {Boolean} out True if you want to make this animation wipe out, instead of slide in.
         * @accessor
         */
        out: false
    },
    refresh: function() {
        var me = this,
            el = me.getElement(),
            elBox = el.dom.getBoundingClientRect(),
            elWidth = elBox.width,
            elHeight = elBox.height,
            from = me.getFrom(),
            to = me.getTo(),
            out = me.getOut(),
            direction = me.getDirection(),
            maskFromX = 0,
            maskFromY = 0,
            maskToX = 0,
            maskToY = 0,
            mask, tmp;
        /* eslint-disable max-len */
        switch (direction) {
            case 'up':
                if (out) {
                    mask = '-webkit-gradient(linear, left top, left bottom, from(#000), to(transparent), color-stop(33%, #000), color-stop(66%, transparent))';
                    maskFromY = elHeight * 3 + 'px';
                    maskToY = elHeight + 'px';
                } else {
                    mask = '-webkit-gradient(linear, left top, left bottom, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
                    maskFromY = -elHeight * 2 + 'px';
                    maskToY = 0;
                };
                break;
            case 'down':
                if (out) {
                    mask = '-webkit-gradient(linear, left top, left bottom, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
                    maskFromY = -elHeight * 2 + 'px';
                    maskToY = 0;
                } else {
                    mask = '-webkit-gradient(linear, left top, left bottom, from(#000), to(transparent), color-stop(33%, #000), color-stop(66%, transparent))';
                    maskFromY = elHeight * 3 + 'px';
                    maskToY = elHeight + 'px';
                };
                break;
            case 'right':
                if (out) {
                    mask = '-webkit-gradient(linear, right top, left top, from(#000), to(transparent), color-stop(33%, #000), color-stop(66%, transparent))';
                    maskFromX = -elWidth * 2 + 'px';
                    maskToX = 0;
                } else {
                    mask = '-webkit-gradient(linear, right top, left top, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
                    maskToX = -elWidth * 2 + 'px';
                };
                break;
            case 'left':
                if (out) {
                    mask = '-webkit-gradient(linear, right top, left top, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
                    maskToX = -elWidth * 2 + 'px';
                } else {
                    mask = '-webkit-gradient(linear, right top, left top, from(#000), to(transparent), color-stop(33%, #000), color-stop(66%, transparent))';
                    maskFromX = -elWidth * 2 + 'px';
                    maskToX = 0;
                };
                break;
        }
        /* eslint-enable max-len */
        if (!out) {
            tmp = maskFromY;
            maskFromY = maskToY;
            maskToY = tmp;
            tmp = maskFromX;
            maskFromX = maskToX;
            maskToX = tmp;
        }
        from.set('mask-image', mask);
        from.set('mask-size', elWidth * 3 + 'px ' + elHeight * 3 + 'px');
        from.set('mask-position-x', maskFromX);
        from.set('mask-position-y', maskFromY);
        to.set('mask-position-x', maskToX);
        to.set('mask-position-y', maskToY);
    }
});
// me.setEasing(out ? 'ease-in' : 'ease-out');

/**
 * @private
 */
Ext.define('Ext.fx.animation.WipeOut', {
    extend: 'Ext.fx.animation.Wipe',
    config: {
        // @hide
        out: true
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.easing.Bounce', {
    extend: 'Ext.fx.easing.Abstract',
    config: {
        springTension: 0.3,
        acceleration: 30,
        startVelocity: 0
    },
    getValue: function() {
        var deltaTime = Ext.Date.now() - this.getStartTime(),
            theta = (deltaTime / this.getAcceleration()),
            powTime = theta * Math.pow(Math.E, -this.getSpringTension() * theta);
        return this.getStartValue() + (this.getStartVelocity() * powTime);
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.easing.Momentum', {
    extend: 'Ext.fx.easing.Abstract',
    config: {
        acceleration: 30,
        friction: 0,
        startVelocity: 0
    },
    alpha: 0,
    updateFriction: function(friction) {
        var theta = Math.log(1 - (friction / 10));
        this.theta = theta;
        this.alpha = theta / this.getAcceleration();
    },
    updateStartVelocity: function(velocity) {
        this.velocity = velocity * this.getAcceleration();
    },
    updateAcceleration: function(acceleration) {
        this.velocity = this.getStartVelocity() * acceleration;
        this.alpha = this.theta / acceleration;
    },
    getValue: function() {
        return this.getStartValue() - this.velocity * (1 - this.getFrictionFactor()) / this.theta;
    },
    getFrictionFactor: function() {
        var deltaTime = Ext.Date.now() - this.getStartTime();
        return Math.exp(deltaTime * this.alpha);
    },
    getVelocity: function() {
        return this.getFrictionFactor() * this.velocity;
    }
});

/**
 * @private
 *
 * This easing is typically used for {@link Ext.scroll.Scroller}. It's a combination of
 * {@link Ext.fx.easing.Momentum} and {@link Ext.fx.easing.Bounce}, which emulates deceleration
 * when the animated element is still within its boundary, then bouncing back (snapping) when
 * it's out-of-bound.
 */
Ext.define('Ext.fx.easing.BoundMomentum', {
    extend: 'Ext.fx.easing.Abstract',
    requires: [
        'Ext.fx.easing.Momentum',
        'Ext.fx.easing.Bounce'
    ],
    config: {
        /**
         * @cfg {Object} momentum
         * A valid config object for {@link Ext.fx.easing.Momentum}
         * @accessor
         */
        momentum: null,
        /**
         * @cfg {Object} bounce
         * A valid config object for {@link Ext.fx.easing.Bounce}
         * @accessor
         */
        bounce: null,
        minMomentumValue: 0,
        maxMomentumValue: 0,
        /**
         * @cfg {Number} minVelocity
         * The minimum velocity to end this easing
         * @accessor
         */
        minVelocity: 0.01,
        /**
         * @cfg {Number} startVelocity
         * The start velocity
         * @accessor
         */
        startVelocity: 0
    },
    applyMomentum: function(config, currentEasing) {
        return Ext.factory(config, Ext.fx.easing.Momentum, currentEasing);
    },
    applyBounce: function(config, currentEasing) {
        return Ext.factory(config, Ext.fx.easing.Bounce, currentEasing);
    },
    updateStartTime: function(startTime) {
        this.getMomentum().setStartTime(startTime);
        this.callParent(arguments);
    },
    updateStartVelocity: function(startVelocity) {
        this.getMomentum().setStartVelocity(startVelocity);
    },
    updateStartValue: function(startValue) {
        this.getMomentum().setStartValue(startValue);
    },
    reset: function() {
        this.lastValue = null;
        this.isBouncingBack = false;
        this.isOutOfBound = false;
        return this.callParent(arguments);
    },
    getValue: function() {
        var momentum = this.getMomentum(),
            bounce = this.getBounce(),
            startVelocity = momentum.getStartVelocity(),
            direction = startVelocity > 0 ? 1 : -1,
            minValue = this.getMinMomentumValue(),
            maxValue = this.getMaxMomentumValue(),
            boundedValue = (direction === 1) ? maxValue : minValue,
            lastValue = this.lastValue,
            value, velocity;
        if (startVelocity === 0) {
            return this.getStartValue();
        }
        if (!this.isOutOfBound) {
            value = momentum.getValue();
            velocity = momentum.getVelocity();
            if (Math.abs(velocity) < this.getMinVelocity()) {
                this.isEnded = true;
            }
            if (value >= minValue && value <= maxValue) {
                return value;
            }
            this.isOutOfBound = true;
            bounce.setStartTime(Ext.Date.now()).setStartVelocity(velocity).setStartValue(boundedValue);
        }
        value = bounce.getValue();
        if (!this.isEnded) {
            if (!this.isBouncingBack) {
                if (lastValue !== null) {
                    if ((direction === 1 && value < lastValue) || (direction === -1 && value > lastValue)) {
                        this.isBouncingBack = true;
                    }
                }
            } else {
                if (Math.round(value) === boundedValue) {
                    this.isEnded = true;
                }
            }
        }
        this.lastValue = value;
        return value;
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.easing.EaseIn', {
    extend: 'Ext.fx.easing.Linear',
    alias: 'easing.ease-in',
    config: {
        exponent: 4,
        duration: 1500
    },
    getValue: function() {
        var deltaTime = Ext.Date.now() - this.getStartTime(),
            duration = this.getDuration(),
            startValue = this.getStartValue(),
            endValue = this.getEndValue(),
            distance = this.distance,
            theta = deltaTime / duration,
            thetaEnd = Math.pow(theta, this.getExponent()),
            currentValue = startValue + (thetaEnd * distance);
        if (deltaTime >= duration) {
            this.isEnded = true;
            return endValue;
        }
        return currentValue;
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.easing.EaseOut', {
    extend: 'Ext.fx.easing.Linear',
    alias: 'easing.ease-out',
    config: {
        exponent: 4,
        duration: 1500
    },
    getValue: function() {
        var deltaTime = Ext.Date.now() - this.getStartTime(),
            duration = this.getDuration(),
            startValue = this.getStartValue(),
            endValue = this.getEndValue(),
            distance = this.distance,
            theta = deltaTime / duration,
            thetaC = 1 - theta,
            thetaEnd = 1 - Math.pow(thetaC, this.getExponent()),
            currentValue = startValue + (thetaEnd * distance);
        if (deltaTime >= duration) {
            this.isEnded = true;
            return endValue;
        }
        return currentValue;
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.easing.Easing', {
    requires: [
        'Ext.fx.easing.Linear'
    ],
    constructor: function(easing) {
        return Ext.factory(easing, Ext.fx.easing.Linear, null, 'easing');
    }
});

/**
 * @private
 */
Ext.define('Ext.fx.runner.CssAnimation', {
    extend: 'Ext.fx.runner.Css',
    constructor: function() {
        this.runningAnimationsMap = {};
        this.elementEndStates = {};
        this.animationElementMap = {};
        this.keyframesRulesCache = {};
        this.uniqueId = 0;
        return this.callParent(arguments);
    },
    attachListeners: function() {
        this.listenersAttached = true;
        Ext.getWin().on({
            animationstart: 'onAnimationStart',
            animationend: 'onAnimationEnd',
            scope: this
        });
    },
    onAnimationStart: function(e) {
        var name = e.browserEvent.animationName,
            elementId = this.animationElementMap[name],
            animation = this.runningAnimationsMap[elementId][name],
            elementEndStates = this.elementEndStates,
            elementEndState = elementEndStates[elementId],
            data = {};
        // console.log("START============= " + name);
        if (elementEndState) {
            delete elementEndStates[elementId];
            data[elementId] = elementEndState;
            this.applyStyles(data);
        }
        if (animation.before) {
            data[elementId] = animation.before;
            this.applyStyles(data);
        }
    },
    onAnimationEnd: function(e) {
        var element = e.target,
            name = e.browserEvent.animationName,
            animationElementMap = this.animationElementMap,
            elementId = animationElementMap[name],
            runningAnimationsMap = this.runningAnimationsMap,
            runningAnimations = runningAnimationsMap[elementId],
            animation = runningAnimations[name];
        // console.log("END============= " + name);
        if (animation.onBeforeEnd) {
            animation.onBeforeEnd.call(animation.scope || this, element);
        }
        if (animation.onEnd) {
            animation.onEnd.call(animation.scope || this, element);
        }
        delete animationElementMap[name];
        delete runningAnimations[name];
        this.removeKeyframesRule(name);
    },
    generateAnimationId: function() {
        return 'animation-' + (++this.uniqueId);
    },
    run: function(animations) {
        var data = {},
            elementEndStates = this.elementEndStates,
            animationElementMap = this.animationElementMap,
            runningAnimationsMap = this.runningAnimationsMap,
            runningAnimations, states, elementId, animationId, i, ln, animation, name, runningAnimation, names, durations, easings, delays, directions, iterations;
        if (!this.listenersAttached) {
            this.attachListeners();
        }
        animations = Ext.Array.from(animations);
        for (i = 0 , ln = animations.length; i < ln; i++) {
            animation = animations[i];
            animation = Ext.factory(animation, Ext.fx.Animation);
            elementId = animation.getElement().getId();
            animationId = animation.getName() || this.generateAnimationId();
            animationElementMap[animationId] = elementId;
            animation = animation.getData();
            states = animation.states;
            this.addKeyframesRule(animationId, states);
            runningAnimations = runningAnimationsMap[elementId];
            if (!runningAnimations) {
                runningAnimations = runningAnimationsMap[elementId] = {};
            }
            runningAnimations[animationId] = animation;
            names = [];
            durations = [];
            easings = [];
            delays = [];
            directions = [];
            iterations = [];
            for (name in runningAnimations) {
                if (runningAnimations.hasOwnProperty(name)) {
                    runningAnimation = runningAnimations[name];
                    names.push(name);
                    durations.push(runningAnimation.duration);
                    easings.push(runningAnimation.easing);
                    delays.push(runningAnimation.delay);
                    directions.push(runningAnimation.direction);
                    iterations.push(runningAnimation.iteration);
                }
            }
            data[elementId] = {
                'animation-name': names,
                'animation-duration': durations,
                'animation-timing-function': easings,
                'animation-delay': delays,
                'animation-direction': directions,
                'animation-iteration-count': iterations
            };
            //            Ext.apply(data[elementId], animation.origin);
            if (animation.preserveEndState) {
                elementEndStates[elementId] = states['100%'];
            }
        }
        this.applyStyles(data);
    },
    addKeyframesRule: function(name, keyframes) {
        var percentage, properties, keyframesRule, styleSheet, rules, styles, rulesLength, key, value;
        styleSheet = this.getStyleSheet();
        rules = styleSheet.cssRules;
        rulesLength = rules.length;
        styleSheet.insertRule('@' + this.vendorPrefix + 'keyframes ' + name + '{}', rulesLength);
        keyframesRule = rules[rulesLength];
        for (percentage in keyframes) {
            properties = keyframes[percentage];
            rules = keyframesRule.cssRules;
            rulesLength = rules.length;
            styles = [];
            for (key in properties) {
                value = this.formatValue(properties[key], key);
                key = this.formatName(key);
                styles.push(key + ':' + value);
            }
            keyframesRule.insertRule(percentage + '{' + styles.join(';') + '}', rulesLength);
        }
        return this;
    },
    removeKeyframesRule: function(name) {
        var styleSheet = this.getStyleSheet(),
            rules = styleSheet.cssRules,
            i, ln, rule;
        for (i = 0 , ln = rules.length; i < ln; i++) {
            rule = rules[i];
            if (rule.name === name) {
                styleSheet.removeRule(i);
                break;
            }
        }
        return this;
    }
});

/**
 * The base class for all items in the `{@link Ext.list.Tree treelist}`.
 * @since 6.0.0
 */
Ext.define('Ext.list.AbstractTreeItem', {
    extend: 'Ext.Widget',
    isTreeListItem: true,
    /**
     * @method setExpandable
     * @ignore
     */
    /**
     * @method setExpanded
     * @ignore
     */
    /**
     * @method setIconCls
     * @ignore
     */
    /**
     * @method setLeaf
     * @ignore
     */
    /**
     * @method setOwner
     * @ignore
     */
    /**
     * @method setLoading
     * @ignore
     */
    /**
     * @method setNode
     * @ignore
     */
    /**
     * @method setParentItem
     * @ignore
     */
    /**
     * @method setText
     * @ignore
     */
    cachedConfig: {
        /**
         * @cfg {Boolean} expandable
         * `true` if this item is expandable. This value is taken from
         * the underlying {@link #node}.
         */
        expandable: false,
        /**
         * @cfg {Boolean} expanded
         * `true` if this item is expanded. This value is taken from
         * the underlying {@link #node}.
         */
        expanded: false,
        /**
         * @cfg iconCls
         * @inheritdoc Ext.panel.Header#cfg-iconCls
         * @localdoc **Note:** This value is taken from the underlying {@link #node}.
         */
        iconCls: '',
        /**
         * @cfg {Boolean} leaf
         * `true` if this item is a leaf. This value is taken from
         * the underlying {@link #node}.
         */
        leaf: true,
        /**
         * @cfg {Boolean} loading
         * `true` if this item is currently loading data. This value is taken from
         * the underlying {@link #node}.
         */
        loading: false,
        /**
         * @cfg {Boolean} selected
         * `true` if this is the selected item in the tree.
         */
        selected: false,
        /**
         * @cfg {Boolean} selectedParent
         * `true` if this item contains the {@link #selected} item in the tree.
         */
        selectedParent: false
    },
    config: {
        /**
         * @cfg {String} iconClsProperty
         * The property from the {@link #node} to map for the {@link #iconCls} config.
         */
        iconClsProperty: 'iconCls',
        indent: null,
        /**
         * @cfg {Ext.list.Tree} owner
         * The owning list for this container.
         */
        owner: null,
        /**
         * @cfg {Ext.data.TreeModel} node
         * The backing node for this item.
         */
        node: null,
        /**
         * @cfg {Number} over
         * One of three possible values:
         *
         *   - 0 if mouse is not over this item or any of its descendants.
         *   - 1 if mouse is not over this item but is over one of this item's descendants.
         *   - 2 if mouse is directly over this item.
         */
        over: null,
        /**
         * @cfg {Ext.list.AbstractTreeItem} parentItem
         * The parent item for this item. 
         */
        parentItem: null,
        /**
         * @cfg {String} text
         * The text for this item. This value is taken from
         * the underlying {@link #node}.
         */
        text: {
            lazy: true,
            $value: ''
        },
        /**
         * @cfg {String} textProperty
         * The property from the {@link #node} to map for the {@link #text} config.
         */
        textProperty: 'text'
    },
    updateNode: function(node) {
        if (node) {
            // eslint-disable-next-line vars-on-top
            var me = this,
                map = me.itemMap,
                childNodes, owner, len, i, item, child;
            me.element.dom.setAttribute('data-recordId', node.internalId);
            if (!map) {
                childNodes = node.childNodes;
                owner = me.getOwner();
                me.itemMap = map = {};
                for (i = 0 , len = childNodes.length; i < len; ++i) {
                    child = childNodes[i];
                    if (child.data.visible) {
                        item = owner.createItem(child, me);
                        map[child.internalId] = item;
                        me.insertItem(item, null);
                    }
                }
            }
            me.setExpanded(node.isExpanded());
            me.doNodeUpdate(node);
        }
    },
    updateSelected: function(selected) {
        var parent;
        if (!this.isConfiguring) {
            parent = this.getParentItem();
            while (parent && !parent.isRootListItem) {
                parent.setSelectedParent(selected);
                parent = parent.getParentItem();
            }
        }
    },
    /**
     * Collapse this item. Does nothing if already collapsed.
     */
    collapse: function() {
        this.getNode().collapse();
    },
    /**
     * Expand this item. Does nothing if already expanded.
     */
    expand: function() {
        this.getNode().expand();
    },
    /**
     * @method
     * Gets the element to be used for the tree when it is in 
     * {@link Ext.list.Tree#micro micro} mode.
     * @return {Ext.dom.Element} The element.
     *
     * @protected
     * @template
     */
    getToolElement: Ext.emptyFn,
    /**
     * @method
     * Append a new child item to the DOM.
     * @param {Ext.list.AbstractTreeItem} item The item to insert.
     * @param {Ext.list.AbstractTreeItem} refItem The item the node is to
     * be inserted before. `null` if the item is to be added to the end.
     *
     * @protected
     * @template
     */
    insertItem: Ext.emptyFn,
    /**
     * Check if the current item is expanded.
     * @return {Boolean} `true` if this item is expanded.
     */
    isExpanded: function() {
        return this.getExpanded();
    },
    /**
     * @method
     * Checks whether the event is an event that should select this node.
     * @param {Ext.event.Event} e The event object.
     * @return {Boolean} `true` if the event should select this node.
     *
     * @protected
     * @template
     */
    isSelectionEvent: Ext.emptyFn,
    /**
     * @method
     * Checks whether the event is an event that should toggle the expand/collapse state.
     * @param {Ext.event.Event} e The event object.
     * @return {Boolean} `true` if the event should toggle the expand/collapsed state.
     * 
     * @protected
     * @template
     */
    isToggleEvent: Ext.emptyFn,
    /**
     * Handle this node being collapsed.
     * @param {Ext.data.TreeModel} node  The node being collapsed.
     * @param collapsingForExpand
     * @protected
     */
    nodeCollapse: function(node, collapsingForExpand) {
        var me = this,
            owner = me.getOwner(),
            animation = me.preventAnimation ? null : owner.getAnimation();
        me.nodeCollapseBegin(animation, collapsingForExpand);
        if (!animation) {
            me.nodeCollapseEnd(collapsingForExpand);
        }
    },
    nodeCollapseBegin: function(animation, collapsingForExpand) {
        var me = this,
            owner = me.getOwner();
        me.setExpanded(false);
        owner.fireEvent('itemcollapse', owner, me);
    },
    nodeCollapseEnd: function(collapsingForExpand) {
        if (!collapsingForExpand && !this.destroying) {
            this.getOwner().updateLayout();
        }
    },
    /**
     * Handle this node being expanded.
     * @param {Ext.data.TreeModel} node  The node being expanded.
     *
     * @protected
     */
    nodeExpand: function(node) {
        var me = this,
            owner = me.getOwner(),
            floated = me.getFloated(),
            animation = !floated && owner.getAnimation();
        me.nodeExpandBegin(animation);
        if (!animation) {
            me.nodeExpandEnd();
        }
    },
    nodeExpandBegin: function(animation) {
        var me = this,
            owner = me.getOwner();
        me.setExpanded(true);
        owner.fireEvent('itemexpand', owner, me);
    },
    nodeExpandEnd: function() {
        if (!this.destroying) {
            this.getOwner().updateLayout();
        }
    },
    /**
     * Handle a node being inserted as a child of this item.
     * @param {Ext.data.TreeModel} node  The node being inserted.
     * @param {Ext.data.TreeModel} refNode The node that is to be inserted before. `null`
     * if this operation is an append.
     *
     * @protected
     */
    nodeInsert: function(node, refNode) {
        var me = this,
            owner = me.getOwner(),
            map = me.itemMap,
            id = node.internalId,
            item = owner.getItem(node),
            refItem = null,
            oldParent;
        if (item) {
            oldParent = item.getParentItem();
            // May have some kind of custom removal processing, allow it to happen, even if it's us
            oldParent.removeItem(item);
            if (oldParent !== me) {
                oldParent.doUpdateExpandable();
                item.setParentItem(me);
            }
        } else {
            item = me.getOwner().createItem(node, me);
        }
        map[id] = item;
        if (refNode) {
            refItem = map[refNode.internalId];
        }
        me.insertItem(item, refItem);
        me.doUpdateExpandable();
        owner.fireEvent('iteminsert', owner, me, item, refItem);
        owner.updateLayout();
    },
    /**
     * Handle a node being removed as a child of this item.
     * @param {Ext.data.TreeModel} node  The node being removed.
     *
     * @protected
     */
    nodeRemove: function(node) {
        var me = this,
            map = me.itemMap,
            owner = me.getOwner(),
            id = node.internalId,
            item = map[id];
        if (item) {
            delete map[id];
            me.removeItem(item);
            item.destroy();
            me.doUpdateExpandable();
            owner.fireEvent('itemremove', owner, me, item);
            owner.updateLayout();
        }
    },
    /**
     * Handle this node having fields changed.
     * 
     * @param {Ext.data.TreeModel} node The node.
     * @param {String[]} modifiedFieldNames The modified field names, if known.
     *
     * @protected
     */
    nodeUpdate: function(node, modifiedFieldNames) {
        this.doNodeUpdate(node);
    },
    /**
     * Handle a click on this item.
     * @param {Ext.event.Event} e The event
     *
     * @protected
     */
    onClick: function(e) {
        var me = this,
            owner = me.getOwner(),
            node = me.getNode(),
            info = {
                event: e,
                item: me,
                node: node,
                tree: owner,
                select: node.get('selectable') !== false && me.isSelectionEvent(e),
                toggle: me.isToggleEvent(e)
            };
        /**
         * @event itemclick
         * @member Ext.list.Tree
         *
         * @param {Ext.list.Tree} sender The `treelist` that fired this event.
         *
         * @param {Object} info
         * @param {Ext.event.Event} info.event The DOM event that precipitated this
         * event.
         * @param {Ext.list.AbstractTreeItem} info.item The tree node that was clicked.
         * @param {Ext.list.Tree} info.tree The `treelist` that fired this event.
         * @param {Boolean} info.select On input this is value is the result of the
         *   {@link #isSelectionEvent} method. On return from event handlers (assuming a
         *   `false` return does not cancel things) this property is used to determine
         *   if the clicked node should be selected.
         * @param {Boolean} info.toggle On input this is value is the result of the
         *   {@link #isToggleEvent} method. On return from event handlers (assuming a
         *   `false` return does not cancel things) this property is used to determine
         *   if the clicked node's expand/collapse state should be toggled.
         *
         * @since 6.0.1
         */
        if (owner.fireEvent('itemclick', owner, info) !== false) {
            if (info.toggle) {
                me.toggleExpanded();
                e.preventDefault();
            }
            if (info.select) {
                owner.setSelection(me.getNode());
            }
        }
    },
    /**
     * @method
     *
     * Remove a child item from the DOM.
     * @param {Ext.list.AbstractTreeItem} item The item to remove.
     *
     * @protected
     * @template
     */
    removeItem: Ext.emptyFn,
    /**
     * @method destroy
     * @inheritdoc
     */
    destroy: function() {
        var me = this,
            map = me.itemMap,
            owner = me.getOwner(),
            key;
        if (map) {
            for (key in map) {
                map[key].destroy();
            }
            me.itemMap = null;
        }
        if (owner) {
            owner.removeItem(me.getNode());
        }
        me.setNode(null);
        me.setParentItem(null);
        me.setOwner(null);
        me.callParent();
    },
    privates: {
        /**
         * Update properties after a node update.
         *
         * @param {Ext.data.TreeModel} node The node.
         * @param {String[]} modifiedFieldNames The modified field names, if known.
         *
         * @private
         */
        doNodeUpdate: function(node, modifiedFieldNames) {
            var me = this,
                textProperty = this.getTextProperty(),
                iconClsProperty = this.getIconClsProperty();
            if (textProperty) {
                me.setText(node.data[textProperty]);
            }
            if (iconClsProperty) {
                me.setIconCls(node.data[iconClsProperty]);
            }
            me.setLoading(node.isLoading());
            me.setLeaf(node.isLeaf());
            me.doUpdateExpandable();
        },
        doUpdateExpandable: function() {
            var node = this.getNode();
            this.setExpandable(node.isExpandable());
        },
        toggleExpanded: function() {
            if (this.isExpanded()) {
                this.collapse();
            } else {
                this.expand();
            }
        },
        updateIndent: function(value) {
            var items = this.itemMap,
                id;
            for (id in items) {
                items[id].setIndent(value);
            }
        },
        /**
         * Implementation so that the Traversable mixin which manipulates the parent
         * axis can function in a TreeList.
         * @param {Ext.list.Tree/Ext.list.TreeItem} owner The owner of this node.
         * @private
         */
        updateOwner: function(owner) {
            this.parent = owner;
        }
    }
});

/**
 * @class Ext.list.AbstractTreeItem
 */
Ext.define('Ext.overrides.list.AbstractTreeItem', {
    override: 'Ext.list.AbstractTreeItem',
    // This config is used by TreeIten, however to support the generic API (RootItem),
    // we need this up here.
    config: {
        floated: null
    }
});

/**
 * This class implements the top-level node in a `{@link Ext.list.Tree treelist}`. Unlike
 * other nodes, this item is only a container for other items. It does not correspond to
 * a data record.
 *
 * @since 6.0.0
 */
Ext.define('Ext.list.RootTreeItem', {
    extend: 'Ext.list.AbstractTreeItem',
    /**
     * This property is `true` to allow type checking for this or derived class.
     * @property {Boolean} isRootListItem
     * @readonly
     */
    isRootListItem: true,
    element: {
        reference: 'element',
        tag: 'ul',
        cls: Ext.baseCSSPrefix + 'treelist-root-container'
    },
    insertItem: function(item, refItem) {
        if (refItem) {
            item.element.insertBefore(refItem.element);
        } else {
            this.element.appendChild(item.element);
        }
    },
    isToggleEvent: function(e) {
        return false;
    }
});

/**
 * @private
 * @since 6.5.0
 */
Ext.define('Ext.mixin.ItemRippler', {
    mixinId: 'itemrippler',
    config: {
        /**
         * @cfg {Boolean/Object} itemRipple
         * @cfg {String} itemRipple.color The background color of the ripple.
         * Default ripple {@link Ext.Widget#ripple} settings for each item in the data view
         *
         * For complex items, individual elements can suppress ripples by adding the
         * `x-no-ripple` class to disable rippling for a tree of elements.
         * @since 6.5.0
         */
        itemRipple: null
    },
    shouldRippleItem: function(item, e) {
        var itemRipple, ripple;
        if (e.getTarget(this.noItemRippleSelector, this.element)) {
            return false;
        }
        itemRipple = item && this.getItemRipple();
        if (itemRipple && item.isWidget) {
            ripple = item.shouldRipple(e);
            if (ripple) {
                itemRipple = Ext.apply({}, itemRipple, ripple);
            }
        }
        return itemRipple;
    },
    rippleItem: function(item, e) {
        if (!item) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            start = e.type.match(me.rippleStateRe),
            itemRipple = me.shouldRippleItem(item, e),
            release = itemRipple && itemRipple.release,
            isRelease = release === true,
            el = item.isWidget ? item.el : item,
            pos, delta, rs, rippledItems;
        // If this is a release based ripple lets track the start point
        // so we can ignore the ripple if this becomes a drag
        if (itemRipple && start && isRelease) {
            me.$rippleStart = e.getXY();
        }
        // Should we ripple?
        // Do we have a ripple config?
        // Are we in the right event (start or end)?
        // do we have an element to ripple with?
        if (itemRipple && el && ((!start && isRelease) || (start && release !== true))) {
            rippledItems = me.$rippledItems || (me.$rippledItems = []);
            rs = me.$rippleStart;
            if (rs) {
                pos = e.getXY();
                // determine the distance from the start point
                delta = Math.sqrt(Math.pow((pos[0] - rs[0]), 2) + Math.pow((pos[1] - rs[1]), 2));
                if (delta <= 8) {
                    el.ripple(e, itemRipple);
                    rippledItems.push(el);
                }
            } else {
                el.ripple(e, itemRipple);
                rippledItems.push(el);
            }
            me.$rippleStart = null;
        }
    },
    destroyAllRipples: function() {
        var items;
        for (items = this.$rippledItems; items && items.length; ) {
            items.pop().destroyAllRipples();
        }
    },
    privates: {
        noItemRippleSelector: '.' + Ext.baseCSSPrefix + 'item-no-ripple, ' + '.' + Ext.baseCSSPrefix + 'item-no-tap',
        rippleStateRe: /start|down/
    }
});

/**
 * The default implementation of the class used for `{@link Ext.list.Tree}`.
 *
 * This class can only be used in conjunction with {@link Ext.list.Tree}.
 * @since 6.0.0
 */
Ext.define('Ext.list.TreeItem', {
    extend: 'Ext.list.AbstractTreeItem',
    xtype: 'treelistitem',
    collapsedCls: Ext.baseCSSPrefix + 'treelist-item-collapsed',
    expandedCls: Ext.baseCSSPrefix + 'treelist-item-expanded',
    floatedToolCls: Ext.baseCSSPrefix + 'treelist-item-tool-floated',
    leafCls: Ext.baseCSSPrefix + 'treelist-item-leaf',
    expandableCls: Ext.baseCSSPrefix + 'treelist-item-expandable',
    hideIconCls: Ext.baseCSSPrefix + 'treelist-item-hide-icon',
    loadingCls: Ext.baseCSSPrefix + 'treelist-item-loading',
    selectedCls: Ext.baseCSSPrefix + 'treelist-item-selected',
    selectedParentCls: Ext.baseCSSPrefix + 'treelist-item-selected-parent',
    withIconCls: Ext.baseCSSPrefix + 'treelist-item-with-icon',
    hoverCls: Ext.baseCSSPrefix + 'treelist-item-over',
    rowHoverCls: Ext.baseCSSPrefix + 'treelist-row-over',
    /**
     * This property is `true` to allow type checking for this or derived class.
     * @property {Boolean} isTreeListItem
     * @readonly
     */
    isTreeListItem: true,
    config: {
        /**
         * @cfg {String} rowCls
         * One or more CSS classes to add to the tree item's logical "row" element. This
         * element fills from left-to-right of the line.
         * @since 6.0.1
         */
        rowCls: null
    },
    /**
     * @cfg {String} [rowClsProperty="rowCls"]
     * The name of the associated record's field to map to the {@link #rowCls} config.
     * @since 6.0.1
     */
    rowClsProperty: 'rowCls',
    element: {
        reference: 'element',
        tag: 'li',
        cls: Ext.baseCSSPrefix + 'treelist-item',
        children: [
            {
                reference: 'rowElement',
                cls: Ext.baseCSSPrefix + 'treelist-row',
                children: [
                    {
                        reference: 'wrapElement',
                        cls: Ext.baseCSSPrefix + 'treelist-item-wrap',
                        children: [
                            {
                                reference: 'iconElement',
                                cls: Ext.baseCSSPrefix + 'treelist-item-icon'
                            },
                            {
                                reference: 'textElement',
                                cls: Ext.baseCSSPrefix + 'treelist-item-text'
                            },
                            {
                                reference: 'expanderElement',
                                cls: Ext.baseCSSPrefix + 'treelist-item-expander'
                            }
                        ]
                    }
                ]
            },
            {
                reference: 'itemContainer',
                tag: 'ul',
                cls: Ext.baseCSSPrefix + 'treelist-container'
            },
            {
                reference: 'toolElement',
                cls: Ext.baseCSSPrefix + 'treelist-item-tool'
            }
        ]
    },
    constructor: function(config) {
        var toolDom;
        this.callParent([
            config
        ]);
        toolDom = this.toolElement.dom;
        // We don't want the tool in the normal <li> structure but it is simpler to let
        // that process create the toolElement.
        toolDom.parentNode.removeChild(toolDom);
    },
    getToolElement: function() {
        return this.toolElement;
    },
    insertItem: function(item, refItem) {
        if (refItem) {
            item.element.insertBefore(refItem.element);
        } else {
            this.itemContainer.appendChild(item.element);
        }
    },
    isSelectionEvent: function(e) {
        var owner = this.getOwner();
        return (!this.isToggleEvent(e) || !owner.getExpanderOnly() || owner.getSelectOnExpander());
    },
    isToggleEvent: function(e) {
        var isExpand = false;
        if (this.getOwner().getExpanderOnly()) {
            isExpand = e.target === this.expanderElement.dom;
        } else {
            // contains also includes the element itself
            isExpand = !this.itemContainer.contains(e.target);
        }
        return isExpand;
    },
    nodeCollapseBegin: function(animation, collapsingForExpand) {
        var me = this,
            itemContainer = me.itemContainer,
            height;
        if (me.expanding) {
            me.stopAnimation(me.expanding);
        }
        // also calls the nodeExpandDone method
        // Measure before collapse since that hides the element (if animating) but after
        // ending any in progress expand animation.
        height = animation && itemContainer.getHeight();
        me.callParent([
            animation,
            collapsingForExpand
        ]);
        if (animation) {
            // The collapsed state is now in effect, so itemContainer is hidden.
            itemContainer.dom.style.display = 'block';
            me.collapsingForExpand = collapsingForExpand;
            me.collapsing = this.runAnimation(Ext.merge({
                from: {
                    height: height
                },
                to: {
                    height: 0
                },
                callback: me.nodeCollapseDone,
                scope: me
            }, animation));
        }
    },
    nodeCollapseDone: function(animation) {
        var me = this,
            itemContainer = me.itemContainer;
        // stopAnimation is called on destroy, so don't
        // bother continuing if we don't need to
        if (!me.destroying && !me.destroyed) {
            me.collapsing = null;
            itemContainer.dom.style.display = '';
            itemContainer.setHeight(null);
            me.nodeCollapseEnd(me.collapsingForExpand);
        }
    },
    nodeExpandBegin: function(animation) {
        var me = this,
            itemContainer = me.itemContainer,
            height;
        if (me.collapsing) {
            me.stopAnimation(me.collapsing);
        }
        me.callParent([
            animation
        ]);
        if (animation) {
            // The expanded state is in effect, so itemContainer is visible again.
            height = itemContainer.getHeight();
            itemContainer.setHeight(0);
            me.expanding = me.runAnimation(Ext.merge({
                to: {
                    height: height
                },
                callback: me.nodeExpandDone,
                scope: me
            }, animation));
        }
    },
    nodeExpandDone: function() {
        this.expanding = null;
        this.itemContainer.setHeight(null);
        this.nodeExpandEnd();
    },
    removeItem: function(item) {
        this.itemContainer.removeChild(item.element);
    },
    //-------------------------------------------------------------------------
    // Updaters
    updateNode: function(node, oldNode) {
        this.syncIndent();
        this.callParent([
            node,
            oldNode
        ]);
    },
    updateExpandable: function(expandable) {
        this.updateExpandCls();
    },
    // We need not to set the expandable attribute of node here, 
    // Refer to isExapndable() function of the node. 
    // This function may get called on removal of child, and thus setting expandable to false
    // But we may not need to set same to node as isExapndable() will be deciding function 
    // not the 'Exapndable' attribute. Fase 'Exapndable' attribute means node will never
    // be expandable irrespective of the child values
    updateExpanded: function(expanded) {
        var node = this.getNode();
        this.updateExpandCls();
        if (node) {
            node.set('expanded', expanded);
        }
    },
    updateIconCls: function(iconCls, oldIconCls) {
        var me = this,
            el = me.element;
        me.doIconCls(me.iconElement, iconCls, oldIconCls);
        me.doIconCls(me.toolElement, iconCls, oldIconCls);
        el.toggleCls(me.withIconCls, !!iconCls);
        // Blank iconCls leaves room for icon to line up w/sibling items
        el.toggleCls(me.hideIconCls, iconCls === null);
    },
    updateLeaf: function(leaf) {
        this.element.toggleCls(this.leafCls, leaf);
    },
    updateLoading: function(loading) {
        this.element.toggleCls(this.loadingCls, loading);
    },
    updateOver: function(over) {
        var me = this;
        me.element.toggleCls(me.hoverCls, !!over);
        // off if over==0, on otherwise
        me.rowElement.toggleCls(me.rowHoverCls, over > 1);
    },
    // off if over = 0 or 1
    updateRowCls: function(value, oldValue) {
        this.rowElement.replaceCls(oldValue, value);
    },
    updateSelected: function(selected, oldSelected) {
        var me = this,
            cls = me.selectedCls,
            tool = me.getToolElement();
        me.callParent([
            selected,
            oldSelected
        ]);
        me.element.toggleCls(cls, selected);
        if (tool) {
            tool.toggleCls(cls, selected);
        }
    },
    updateSelectedParent: function(selectedParent) {
        var me = this,
            tool;
        me.element.toggleCls(me.selectedParentCls, selectedParent);
        tool = me.getToolElement();
        if (tool) {
            tool.toggleCls(me.selectedCls, selectedParent);
        }
    },
    updateText: function(text) {
        this.textElement.update(text);
    },
    //-------------------------------------------------------------------------
    // Private
    privates: {
        doNodeUpdate: function(node) {
            this.callParent([
                node
            ]);
            this.setRowCls(node && node.data[this.rowClsProperty]);
        },
        doIconCls: function(element, iconCls, oldIconCls) {
            if (oldIconCls) {
                element.removeCls(oldIconCls);
            }
            if (iconCls) {
                element.addCls(iconCls);
            }
        },
        syncIndent: function() {
            var me = this,
                indent = me.getIndent(),
                node = me.getNode(),
                depth;
            if (node) {
                depth = node.data.depth - 1;
                me.wrapElement.dom.style.marginLeft = (depth * indent) + 'px';
            }
        },
        updateExpandCls: function() {
            if (!this.updatingExpandCls) {
                // eslint-disable-next-line vars-on-top
                var me = this,
                    expandable = me.getExpandable(),
                    element = me.element,
                    expanded = me.getExpanded(),
                    expandedCls = me.expandedCls,
                    collapsedCls = me.collapsedCls;
                me.updatingExpandCls = true;
                element.toggleCls(me.expandableCls, expandable);
                if (expandable) {
                    element.toggleCls(expandedCls, expanded);
                    element.toggleCls(collapsedCls, !expanded);
                } else {
                    element.removeCls([
                        expandedCls,
                        collapsedCls
                    ]);
                }
                me.updatingExpandCls = false;
            }
        },
        updateIndent: function(value, oldValue) {
            this.syncIndent();
            this.callParent([
                value,
                oldValue
            ]);
        }
    }
}, function(TreeItem) {
    TreeItem.prototype.floatedCls = [
        Ext.Widget.prototype.floatedCls,
        Ext.baseCSSPrefix + 'treelist-item-floated'
    ];
});

/**
 * @class Ext.list.TreeItem
 */
Ext.define('Ext.overrides.list.TreeItem', {
    override: 'Ext.list.TreeItem',
    // Implement a setter.
    // There *is* no "floated" config in Classic.
    // We're still an inner item, we just get put inside a Container.
    setFloated: function(floated) {
        var me = this,
            el = me.element,
            placeholder = me.placeholder,
            node, wasExpanded;
        if (me.treeItemFloated !== floated) {
            if (floated) {
                placeholder = el.clone(false, true);
                // shallow, asDom
                placeholder.id += '-placeholder';
                // avoid duplicate id
                me.placeholder = Ext.get(placeholder);
                me.wasExpanded = me.getExpanded();
                me.setExpanded(true);
                el.addCls(me.floatedCls);
                el.dom.parentNode.insertBefore(placeholder, el.dom);
                me.floater = me.createFloater();
            }
            // toolkit-specific
            else if (placeholder) {
                wasExpanded = me.wasExpanded;
                node = me.getNode();
                me.setExpanded(wasExpanded);
                if (!wasExpanded && node.isExpanded()) {
                    // If we have been floating and expanded a child, we may have been
                    // expanded as part of the ancestors. Attempt to restore state.
                    me.preventAnimation = true;
                    node.collapse();
                    me.preventAnimation = false;
                }
                me.floater.remove(me, false);
                // don't destroy
                el.removeCls(me.floatedCls);
                placeholder.dom.parentNode.insertBefore(el.dom, placeholder.dom);
                placeholder.destroy();
                me.floater.destroy();
                me.placeholder = me.floater = null;
            }
            // Use an internal property name. We are NOT really floated
            me.treeItemFloated = floated;
        }
    },
    getFloated: function() {
        return this.treeItemFloated;
    },
    runAnimation: function(animation) {
        return this.itemContainer.addAnimation(animation);
    },
    stopAnimation: function(animation) {
        animation.jumpToEnd();
    },
    privates: {
        createFloater: function() {
            var me = this,
                owner = me.getOwner(),
                ownerTree = me.up('treelist'),
                floater,
                toolElement = me.getToolElement(),
                expandedWidth = ownerTree.expandedWidth,
                defaultListWidth = ownerTree.defaultListWidth;
            if (expandedWidth === null) {
                expandedWidth = defaultListWidth;
            }
            me.floater = floater = new Ext.container.Container({
                cls: ownerTree.self.prototype.element.cls + ' ' + ownerTree.uiPrefix + ownerTree.getUi() + ' ' + Ext.baseCSSPrefix + 'treelist-floater',
                floating: true,
                // We do not get element resize events on IE8
                // so fall back to 6.0.1 sizing to 200 wide.
                width: Ext.isIE8 ? defaultListWidth : (expandedWidth - toolElement.getWidth()),
                shadow: false,
                hidden: true,
                renderTo: Ext.getBody(),
                listeners: {
                    element: 'el',
                    click: function(e) {
                        return owner.onClick(e);
                    }
                }
            });
            floater.add(me);
            floater.show();
            floater.el.alignTo(toolElement, 'tr?');
            return floater;
        }
    }
});

/**
 * A lightweight component to display data in a simple tree structure using a
 * {@link Ext.data.TreeStore}.
 *
 * Simple Treelist using inline data:
 *
 *     @example
 *     Ext.create({
 *         xtype: 'treelist',
 *         store: {
 *             root: {
 *             expanded: true,
 *                 children: [{
 *                     text: 'detention',
 *                     leaf: true,
 *                     iconCls: 'x-fa fa-frown-o'
 *                 }, {
 *                     text: 'homework',
 *                     expanded: true,
 *                     iconCls: 'x-fa fa-folder',
 *                     children: [{
 *                         text: 'book report',
 *                         leaf: true,
 *                         iconCls: 'x-fa fa-book'
 *                     }, {
 *                         text: 'algebra',
 *                         leaf: true,
 *                         iconCls: 'x-fa fa-graduation-cap'
 *                     }]
 *                 }, {
 *                     text: 'buy lottery tickets',
 *                     leaf: true,
 *                     iconCls: 'x-fa fa-usd'
 *                 }]
 *             }
 *         },
 *         renderTo: Ext.getBody()
 *     });
 *
 * To collapse the Treelist for use in a smaller navigation view see {@link #micro}.
 * Parent Treelist node expansion may be refined using the {@link #singleExpand} and
 * {@link #expanderOnly} config options.  Treelist nodes will be selected when clicked /
 * tapped excluding clicks on the expander unless {@link #selectOnExpander} is set to
 * `true`.
 *
 * @since 6.0.0
 */
Ext.define('Ext.list.Tree', {
    extend: 'Ext.Gadget',
    xtype: 'treelist',
    mixins: [
        'Ext.mixin.ItemRippler'
    ],
    requires: [
        'Ext.list.RootTreeItem'
    ],
    expanderFirstCls: Ext.baseCSSPrefix + 'treelist-expander-first',
    expanderOnlyCls: Ext.baseCSSPrefix + 'treelist-expander-only',
    highlightPathCls: Ext.baseCSSPrefix + 'treelist-highlight-path',
    microCls: Ext.baseCSSPrefix + 'treelist-micro',
    uiPrefix: Ext.baseCSSPrefix + 'treelist-',
    /**
     * @property element
     * @inheritdoc
     */
    element: {
        reference: 'element',
        cls: Ext.baseCSSPrefix + 'treelist ' + Ext.baseCSSPrefix + 'unselectable',
        listeners: {
            click: 'onClick',
            touchstart: 'onTouchStart',
            touchend: 'onTouchEnd',
            mouseenter: 'onMouseEnter',
            mouseleave: 'onMouseLeave',
            mouseover: 'onMouseOver'
        },
        children: [
            {
                reference: 'toolsElement',
                cls: Ext.baseCSSPrefix + 'treelist-toolstrip',
                listeners: {
                    click: 'onToolStripClick',
                    mouseover: 'onToolStripMouseOver'
                }
            }
        ]
    },
    cachedConfig: {
        animation: {
            duration: 500,
            easing: 'ease'
        },
        /**
         * @cfg {Boolean} expanderFirst
         * `true` to display the expander to the left of the item text.
         * `false` to display the expander to the right of the item text.
         */
        expanderFirst: true,
        /**
         * @cfg {Boolean} expanderOnly
         * `true` to expand only on the click of the expander element. Setting this to
         * `false` will allow expansion on click of any part of the element.
         */
        expanderOnly: true
    },
    config: {
        /**
         * @cfg {Boolean} floatLeafItems
         * `true` to allow the popout to show on leaf items on click/tap. This is the same popout
         * (menu) non-leaf items show their child items in. `false` to prevent the popout
         * from showing for leaf items.
         */
        floatLeafItems: false,
        /**
         * @cfg {Object} [defaults]
         * The default configuration for the widgets created for tree items.
         *
         * @cfg {String} [defaults.xtype="treelistitem"]
         * The type of item to create. By default, items are
         * `{@link Ext.list.TreeItem treelistitem}` instances. This can be customized but this
         * `xtype` must reference a class that ultimately derives from the
         * `{@link Ext.list.AbstractTreeItem}` base class.
         */
        defaults: {
            xtype: 'treelistitem'
        },
        /**
         * @cfg {Boolean}
         * Set as `true` to highlight all items on the path to the currently selected
         * node.
         */
        highlightPath: null,
        iconSize: null,
        /**
         * @cfg {Number} [indent=null]
         * 
         * The number of pixels to offset each level of tree nodes.
         */
        indent: null,
        /**
         * @cfg {Boolean}
         *
         * Set to `true` to collapse the Treelist UI to display only the
         * {@link Ext.data.NodeInterface#cfg-iconCls icons} of the root nodes.  Hovering
         * the cursor (or tapping on a touch-enabled device) shows the child nodes beside
         * the icon.
         */
        micro: false,
        overItem: null,
        /**
         * @cfg {Ext.data.TreeModel/Number/String} selection
         *
         * The current selected node or its ID.
         */
        selection: null,
        /**
         * @cfg {Boolean} selectOnExpander
         * `true` to select the node when clicking the expander.
         */
        selectOnExpander: false,
        /**
         * @cfg {Boolean} [singleExpand=false]
         * `true` if only 1 node per branch may be expanded.
         */
        singleExpand: null,
        /**
         * @cfg {String/Object/Ext.data.TreeStore} store
         * The data source to which this component is bound.
         */
        store: null,
        /**
         * @cfg ui
         * @inheritdoc
         */
        ui: null
    },
    /**
     * @event selectionchange
     * This event fires when {@link Ext.list.Tree#selection} changes
     * @param {Ext.list.Tree} treelist The component firing this event.
     * @param {Ext.data.TreeModel} record The currently selected node.
     */
    /**
     * @cfg twoWayBindable
     * @inheritdoc
     */
    twoWayBindable: {
        selection: 1
    },
    /**
     * @cfg publishes
     * @inheritdoc
     */
    publishes: {
        selection: 1
    },
    /**
     * @property defaultBindProperty
     * @inheritdoc
     */
    defaultBindProperty: 'store',
    constructor: function(config) {
        this.callParent([
            config
        ]);
        // Important to publish the value here, so the vm can
        // will know our intial state.
        this.publishState('selection', this.getSelection());
    },
    destroy: function() {
        var me = this;
        me.unfloatAll();
        me.activeFloater = null;
        me.setSelection(null);
        me.setStore(null);
        me.callParent();
    },
    updateOverItem: function(over, wasOver) {
        var map = {},
            state = 2,
            c, node;
        // Walk up the node hierarchy starting at the "over" item and set their "over"
        // config appropriately (2 when over that row, 1 when over a descendant).
        //
        for (c = over; c; c = this.getItem(node.parentNode)) {
            node = c.getNode();
            map[node.internalId] = true;
            c.setOver(state);
            state = 1;
        }
        // There are some cases, like tree filtering where it's possible that the whole tree
        // gets refreshed on expand, so wasOver may be destroyed. In that case, we have nothing to
        // do since the nodes are in a new state
        if (wasOver && !wasOver.destroyed) {
            // If we wasOver something else previously, walk up that node hierarchy and
            // set their "over" to 0... until we encounter some node that we are still
            // "over" (as determined in previous loop).
            //
            for (c = wasOver; c; c = this.getItem(node.parentNode)) {
                node = c.getNode();
                if (map[node.internalId]) {
                    break;
                }
                c.setOver(0);
            }
        }
    },
    applyMicro: function(micro) {
        return Boolean(micro);
    },
    applySelection: function(selection, oldSelection) {
        var store = this.getStore();
        if (!store) {
            selection = null;
        }
        if (store && selection !== null && !(selection instanceof Ext.data.Model)) {
            selection = store.getNodeById(selection);
        }
        if (selection && selection.get('selectable') === false) {
            selection = oldSelection;
        }
        return selection;
    },
    updateSelection: function(selection, oldSelection) {
        var me = this,
            item, parent;
        if (!me.destroying) {
            // getItem has guards around null, so we don't
            // need to check for oldSelection/selection here
            item = me.getItem(oldSelection);
            if (item) {
                item.setSelected(false);
            }
            item = me.getItem(selection);
            if (item) {
                item.setSelected(true);
                while (parent = item.getParentItem()) {
                    // eslint-disable-line no-cond-assign
                    parent.setExpanded(true);
                    item = parent;
                }
            }
            me.fireEvent('selectionchange', me, selection);
        }
    },
    applyStore: function(store) {
        return store && Ext.StoreManager.lookup(store, 'tree');
    },
    updateStore: function(store, oldStore) {
        var me = this,
            root;
        if (oldStore) {
            // Store could be already destroyed upstream
            if (!oldStore.destroyed) {
                if (oldStore.getAutoDestroy()) {
                    oldStore.destroy();
                } else {
                    me.storeListeners.destroy();
                }
            }
            me.removeRoot();
            me.storeListeners = null;
        }
        if (store) {
            me.storeListeners = store.on({
                destroyable: true,
                scope: me,
                nodeappend: 'onNodeAppend',
                nodecollapse: 'onNodeCollapse',
                nodeexpand: 'onNodeExpand',
                nodeinsert: 'onNodeInsert',
                noderemove: 'onNodeRemove',
                rootchange: 'onRootChange',
                update: 'onNodeUpdate',
                refresh: 'onRefresh'
            });
            root = store.getRoot();
            if (root) {
                me.createRootItem(root);
            }
        }
        if (!me.destroying) {
            me.updateLayout();
        }
    },
    updateExpanderFirst: function(expanderFirst) {
        this.element.toggleCls(this.expanderFirstCls, expanderFirst);
    },
    updateExpanderOnly: function(value) {
        this.element.toggleCls(this.expanderOnlyCls, !value);
    },
    updateHighlightPath: function(updatePath) {
        this.element.toggleCls(this.highlightPathCls, updatePath);
    },
    updateMicro: function(micro) {
        var me = this;
        if (!micro) {
            me.unfloatAll();
            me.activeFloater = null;
        }
        me.element.toggleCls(me.microCls, micro);
    },
    updateUi: function(ui, oldValue) {
        var me = this,
            el = me.element,
            uiPrefix = me.uiPrefix;
        if (oldValue) {
            el.removeCls(uiPrefix + oldValue);
        }
        if (ui) {
            el.addCls(uiPrefix + ui);
        }
        // Ensure that the cached iconSize is read from the style.
        delete me.iconSize;
        me.syncIconSize();
    },
    /**
     * Get a child {@link Ext.list.AbstractTreeItem item} by node.
     * @param {Ext.data.TreeModel} node The node.
     * @return {Ext.list.AbstractTreeItem} The item. `null` if not found.
     */
    getItem: function(node) {
        var map = this.itemMap,
            ret;
        if (node && map) {
            ret = map[node.internalId];
        }
        return ret || null;
    },
    /**
     * This method is called to populate and return a config object for new nodes. This
     * can be overridden by derived classes to manipulate properties or `xtype` of the
     * returned object. Upon return, the object is passed to `{@link Ext#method!create}` and the
     * reference is stored as part of this tree.
     *
     * The base class implementation will apply any configured `{@link #defaults}` to the
     * object it returns.
     *
     * @param {Ext.data.TreeModel} node The node backing the item.
     * @param {Ext.list.AbstractTreeItem} parent The parent item. This is never `null` but
     * may be an instance of `{@link Ext.list.RootTreeItem}`.
     * @return {Object} The config object to pass to `{@link Ext#method!create}` for the item.
     * @template
     */
    getItemConfig: function(node, parent) {
        return Ext.apply({
            parentItem: parent.isRootListItem ? null : parent,
            owner: this,
            node: node,
            indent: this.getIndent()
        }, this.getDefaults());
    },
    privates: {
        checkForOutsideClick: function(e) {
            var floater = this.activeFloater;
            if (!floater.element.contains(e.target)) {
                this.unfloatAll();
            }
        },
        collapsingForExpand: false,
        /**
         * Create a new list item.
         * @param {Ext.data.TreeModel} node The node backing the item.
         * @param {Ext.list.AbstractTreeItem} parent The parent item.
         * @return {Ext.list.AbstractTreeItem} The list item.
         *
         * @private
         */
        createItem: function(node, parent) {
            var me = this,
                item = Ext.create(me.getItemConfig(node, parent)),
                toolsElement = me.toolsElement,
                toolEl, previousSibling;
            if (parent.isRootListItem) {
                toolEl = item.getToolElement();
                if (toolEl) {
                    previousSibling = me.findVisiblePreviousSibling(node);
                    if (!previousSibling) {
                        toolsElement.insertFirst(toolEl);
                    } else {
                        previousSibling = me.getItem(previousSibling);
                        toolEl.insertAfter(previousSibling.getToolElement());
                    }
                    toolEl.dom.setAttribute('data-recordId', node.internalId);
                    toolEl.isTool = true;
                }
            }
            me.itemMap[node.internalId] = item;
            return item;
        },
        /**
         * Create a root item for this list.
         * @param {Ext.data.TreeModel} root The root node.
         *
         * @private
         */
        createRootItem: function(root) {
            var me = this,
                item;
            me.itemMap = {};
            me.rootItem = item = new Ext.list.RootTreeItem({
                indent: me.getIndent(),
                node: root,
                owner: me
            });
            me.element.appendChild(item.element);
            me.itemMap[root.internalId] = item;
        },
        findVisiblePreviousSibling: function(node) {
            var sibling = node.previousSibling;
            while (sibling) {
                if (sibling.data.visible) {
                    return sibling;
                }
                sibling = sibling.previousSibling;
            }
            return null;
        },
        floatItem: function(item, byHover) {
            var me = this,
                floater;
            if (item.getFloated()) {
                return;
            }
            // Cancel any mouseout timer,
            if (me.toolMouseListeners) {
                me.toolMouseListeners.destroy();
                me.floaterMouseListeners.destroy();
                me.floaterMouseListeners = me.toolMouseListeners = null;
            }
            me.unfloatAll();
            if (!byHover && !me.getFloatLeafItems() && item.getNode().isLeaf()) {
                return;
            }
            me.activeFloater = floater = item;
            me.floatedByHover = byHover;
            item.setFloated(true);
            if (byHover) {
                // monitorMouseLeave allows straying out for the specified short time
                me.toolMouseListeners = item.getToolElement().monitorMouseLeave(300, me.checkForMouseLeave, me);
                me.floaterMouseListeners = (item.floater || item).el.monitorMouseLeave(300, me.checkForMouseLeave, me);
                floater.element.on('mouseover', 'onMouseOver', me);
            } else {
                Ext.on('mousedown', 'checkForOutsideClick', me);
            }
        },
        shouldRippleItem: function(item, e) {
            if (item && item.getSelected()) {
                return false;
            }
            return this.mixins.itemrippler.shouldRippleItem.call(this, item, e);
        },
        onTouchStart: function(e) {
            this.doItemRipple(e);
        },
        onTouchEnd: function(e) {
            this.doItemRipple(e);
        },
        doItemRipple: function(e) {
            var me = this,
                item = e.getTarget('[data-recordId]'),
                id;
            if (item) {
                id = item.getAttribute('data-recordId');
                item = me.itemMap[id];
                if (item && me.shouldRippleItem(item, e)) {
                    this.rippleItem(item, e);
                }
            }
        },
        /**
         * Handles when this element is clicked.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onClick: function(e) {
            var item = e.getTarget('[data-recordId]'),
                id;
            if (item) {
                id = item.getAttribute('data-recordId');
                item = this.itemMap[id];
                if (item) {
                    item.onClick(e);
                }
            }
        },
        onMouseEnter: function(e) {
            this.onMouseOver(e);
        },
        onMouseLeave: function() {
            this.setOverItem(null);
        },
        onMouseOver: function(e) {
            var comp = Ext.Component.from(e);
            this.setOverItem(comp && comp.isTreeListItem && comp);
        },
        checkForMouseLeave: function(e) {
            var floater = this.activeFloater,
                relatedTarget = e.getRelatedTarget();
            if (floater) {
                if (relatedTarget !== floater.getToolElement().dom && !floater.element.contains(relatedTarget)) {
                    this.unfloatAll();
                }
            }
        },
        /**
         * Handles a node being appended to a parent.
         * @param {Ext.data.TreeModel} parentNode The parent node.
         * @param {Ext.data.TreeModel} node The appended node.
         *
         * @private
         */
        onNodeAppend: function(parentNode, node) {
            var item;
            // If it's a root we'll handle it on rootchange
            if (parentNode) {
                item = this.itemMap[parentNode.internalId];
                if (item) {
                    item.nodeInsert(node, null);
                }
            }
        },
        /**
         * Handles when a node collapses.
         * @param {Ext.data.TreeModel} node The node.
         *
         * @private
         */
        onNodeCollapse: function(node) {
            var item = this.itemMap[node.internalId];
            if (item) {
                item.nodeCollapse(node, this.collapsingForExpand);
            }
        },
        /**
         * Handles when a node expands.
         * @param {Ext.data.TreeModel} node The node.
         *
         * @private
         */
        onNodeExpand: function(node) {
            var me = this,
                item = me.itemMap[node.internalId],
                childNodes, len, i, parentNode, child;
            if (item) {
                if (!item.isRootItem && me.getSingleExpand()) {
                    me.collapsingForExpand = true;
                    parentNode = (item.getParentItem() || me.rootItem).getNode();
                    childNodes = parentNode.childNodes;
                    for (i = 0 , len = childNodes.length; i < len; ++i) {
                        child = childNodes[i];
                        if (child !== node) {
                            child.collapse();
                        }
                    }
                    me.collapsing = false;
                }
                item.nodeExpand(node);
            }
        },
        /**
         * Handles a node being inserted into a parent.
         * @param {Ext.data.TreeModel} parentNode The parent node.
         * @param {Ext.data.TreeModel} node The inserted node.
         * @param {Ext.data.TreeModel} refNode The node this was inserted before.
         *
         * @private
         */
        onNodeInsert: function(parentNode, node, refNode) {
            var item = this.itemMap[parentNode.internalId];
            if (item) {
                item.nodeInsert(node, refNode);
            }
        },
        /**
         * Handles a node being removed from a parent.
         * @param {Ext.data.TreeModel} parentNode The parent node.
         * @param {Ext.data.TreeModel} node The removed node.
         * @param {Boolean} isMove `true` if this node is moving inside the tree.
         *
         * @private
         */
        onNodeRemove: function(parentNode, node, isMove) {
            var item;
            // If it's a move we don't need to do anything, we won't process it
            // as a removal, the addition will handle it all.
            // Also if the node being removed is the root we'll handle it in rootchange
            if (parentNode && !isMove) {
                item = this.itemMap[parentNode.internalId];
                if (item) {
                    item.nodeRemove(node);
                }
            }
        },
        /**
         * Handles when a node updates.
         * @param {Ext.data.TreeStore} store The store.
         * @param {Ext.data.TreeModel} node The node.
         * @param {String} type The update type.
         * @param {String[]} modifiedFieldNames The modified field names, if known.
         *
         * @private
         */
        onNodeUpdate: function(store, node, type, modifiedFieldNames) {
            var item = this.itemMap[node.internalId];
            if (item) {
                item.nodeUpdate(node, modifiedFieldNames);
            }
        },
        /**
         * Handles before a root node loads
         * @param {Ext.data.TreeStore} store The store.
         * @private
         */
        onRefresh: function(store) {
            // Because the tree can use bottom up or top down filtering (or reload), 
            // don't try and figure out, what changed here
            // just do a global refresh
            this.onRootChange(store.getRoot());
        },
        /**
         * Handles when the root node in the tree changes.
         * @param {Ext.data.TreeModel} root The root.
         *
         * @private
         */
        onRootChange: function(root) {
            var me = this;
            me.removeRoot();
            if (root) {
                me.createRootItem(root);
            }
            me.updateLayout();
            me.fireEvent('refresh', me);
        },
        /**
         * Removes a list item.
         * @param {Ext.data.TreeModel} node The node backing the item.
         *
         * @private
         */
        removeItem: function(node) {
            var map = this.itemMap,
                id = node.internalId,
                item, toolEl;
            if (map) {
                item = map[id];
                // If it's null, it means it's a root level item
                if (item.getParentItem() === null) {
                    toolEl = item.getToolElement();
                    if (toolEl) {
                        this.toolsElement.removeChild(toolEl);
                    }
                }
                delete map[id];
            }
        },
        removeRoot: function() {
            var me = this,
                rootItem = me.rootItem;
            if (rootItem) {
                me.element.removeChild(rootItem.element);
                me.rootItem = me.itemMap = Ext.destroy(rootItem);
            }
        },
        /**
         * Handles when the toolstrip has a click.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onToolStripClick: function(e) {
            var item = e.getTarget('[data-recordId]'),
                id;
            if (item) {
                id = item.getAttribute('data-recordId');
                item = this.itemMap[id];
                if (item) {
                    if (item === this.activeFloater) {
                        this.unfloatAll();
                    } else {
                        this.floatItem(item, false);
                    }
                }
            }
        },
        /**
         * Handles when the toolstrip has a mouseover.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onToolStripMouseOver: function(e) {
            var item = e.getTarget('[data-recordId]'),
                id;
            if (item) {
                id = item.getAttribute('data-recordId');
                item = this.itemMap[id];
                if (item) {
                    this.floatItem(item, true);
                }
            }
        },
        syncIconSize: function() {
            var me = this,
                size = me.iconSize || (me.iconSize = parseInt(me.element.getStyle('background-position'), 10));
            me.setIconSize(size);
        },
        unfloatAll: function() {
            var me = this,
                floater = me.activeFloater;
            if (floater) {
                floater.setFloated(false);
                me.activeFloater = null;
                if (me.floatedByHover) {
                    if (me.toolMouseListeners) {
                        me.toolMouseListeners.destroy();
                        me.floaterMouseListeners.destroy();
                        me.floaterMouseListeners = me.toolMouseListeners = null;
                    }
                    floater.element.un('mouseover', 'onMouseOver', me);
                } else {
                    Ext.un('mousedown', 'checkForOutsideClick', me);
                }
            }
        },
        defaultIconSize: 22,
        updateIconSize: function(value) {
            this.setIndent(value || this.defaultIconSize);
        },
        updateIndent: function(value) {
            var rootItem = this.rootItem;
            if (rootItem) {
                rootItem.setIndent(value);
            }
        }
    }
});

/**
 * @class Ext.list.Tree
 */
Ext.define('Ext.overrides.list.Tree', {
    override: 'Ext.list.Tree',
    canMeasure: true,
    constructor: function(config) {
        this.callParent([
            config
        ]);
        // Track size so that we can track the expanded size
        // for use by the floated state of items when in micro mode.
        // Browsers where this event is not supported, fall back to a width
        // of 200px for floated tree items.
        if (!Ext.isIE8) {
            this.element.on('resize', 'onElResize', this);
        }
    },
    beforeLayout: function() {
        this.syncIconSize();
    },
    onElResize: function(el, details) {
        if (!this.getMicro() && this.canMeasure) {
            this.expandedWidth = details.width;
        }
    },
    privates: {
        defaultListWidth: 200,
        expandedWidth: null
    }
});

/**
 * This mixin allows a class to easily forward (or proxy) configs to other objects. Once
 * mixed in, the using class (and its derived classes) can add a `proxyConfig` object
 * property to their class body that specifies the accessor and configs to manage.
 *
 * For example:
 *
 *      Ext.define('ParentThing', {
 *          mixins: [
 *              'Ext.mixin.ConfigProxy'
 *          ],
 *
 *          config: {
 *              childThing: {
 *                  xtype: 'panel'
 *              }
 *          },
 *
 *          proxyConfig: {
 *              // The keys of this object are themselves configs. Their getters
 *              // are used to identify the target to which the listed configs are
 *              // proxied.
 *
 *              childThing: [
 *                  // This list of config names will be proxied to the object
 *                  // returned by the getter (getChildThing in this case). In
 *                  // addition, each of these will be defined as configs on this
 *                  // class but with a special getter and setter.
 *                  //
 *                  // These configs cannot be previously defined nor can their
 *                  // be getters or setters already present.
 *
 *                  'title'
 *              ]
 *          }
 *      });
 *
 * If the getter for a proxy target returns `null`, the setter for the proxied config
 * will simply discard the value. It is expected that the target will generally always
 * exist.
 *
 * To proxy methods, the array of config names is replaced by an object:
 *
 *      Ext.define('ParentThing', {
 *          mixins: [
 *              'Ext.mixin.ConfigProxy'
 *          ],
 *
 *          config: {
 *              childThing: {
 *                  xtype: 'panel'
 *              }
 *          },
 *
 *          proxyConfig: {
 *              // The keys of this object are themselves configs. Their getters
 *              // are used to identify the target to which the listed configs are
 *              // proxied.
 *
 *              childThing: {
 *                  configs: [
 *                      // same as when "childThing" was just this array...
 *                  ],
 *
 *                  methods: [
 *                      // A list of methods to proxy to the childThing.
 *                      'doStuff'
 *                  ]
 *              ]
 *          }
 *      });
 *
 * @private
 * @since 6.5.0
 */
Ext.define('Ext.mixin.ConfigProxy', function(ConfigProxy) {
    return {
        // eslint-disable-line brace-style, max-len
        extend: 'Ext.Mixin',
        mixinConfig: {
            id: 'configproxy',
            extended: function(baseClass, derivedClass, classBody) {
                var proxyConfig = classBody.proxyConfig;
                derivedClass.$configProxies = Ext.apply({}, derivedClass.superclass.self.$configProxies);
                if (proxyConfig) {
                    delete classBody.proxyConfig;
                    ConfigProxy.processClass(derivedClass, proxyConfig);
                }
            }
        },
        onClassMixedIn: function(targetClass) {
            var prototype = targetClass.prototype,
                proxyConfig = prototype.proxyConfig,
                initConfig = prototype.initConfig;
            prototype.$proxiedConfigs = null;
            // constant shape
            targetClass.$configProxies = {};
            // contents are basically the same as the proxyConfig object.
            prototype.initConfig = function(config) {
                initConfig.apply(this, arguments);
                // ensure future setter calls will pass through to the target:
                this.$proxiedConfigs = null;
                return this;
            };
            if (proxyConfig) {
                delete prototype.proxyConfig;
                ConfigProxy.processClass(targetClass, proxyConfig);
            }
        },
        /**
     * This method returns an object of all proxied config values for a given target. This
     * is only useful during the class initialization phase to avoid passing in "wrong"
     * initial config values for a child object and then proxying down all the configs
     * from the parent.
     *
     * This method is not typically called directly but rather `mergeProxiedConfigs` is
     * more likely.
     * @param {String} name The proxy target config name (in the class example, this would
     * be "childThing").
     * @return {Object}
     * @private
     * @since 6.5.0
     */
        getProxiedConfigs: function(name) {
            var me = this,
                configs = me.config,
                // the merged config set
                configProxies = me.self.$configProxies[name],
                i = configProxies && configProxies.length,
                cfg, proxiedConfigs, ret, s, v;
            if (i && me.isConfiguring) {
                // Lazily create the $proxiedConfigs map to track the config properties
                // we are "stealing" away.
                proxiedConfigs = me.$proxiedConfigs || (me.$proxiedConfigs = {});
                while (i-- > 0) {
                    cfg = configProxies[i];
                    proxiedConfigs[s = cfg.name] = cfg;
                    if ((v = configs[s]) !== undefined) {
                        (ret || (ret = {}))[s] = v;
                    }
                }
            }
            return ret;
        },
        /**
     * This method accepts the normal config object (`itemConfig`) for the child object
     * (`name`) and merges any proxied configs into a new config object. This is useful
     * during the class initialization phase to avoid passing in "wrong" initial config
     * values for a child object and then proxying down the rest of the configs.
     *
     * This method is typically called during an "applier" method for a proxy target. If
     * called at any other time this method simply returns the given `itemConfig`. This
     * makes it safe to code such appliers as follows:
     *
     *      applyChildThing: function(config) {
     *          config = this.mergeProxiedConfigs('childThing', config);
     *
     *          return new ChildThing(config);
     *      }
     *
     * @param {String} name The proxy target config name (in the class example, this would
     * be "childThing").
     * @param {Mixed} itemConfig The default configuration for the child item.
     * @param {Boolean} [alwaysClone] Pass `true` to ensure a new object is returned.
     * @return {Object}
     * @private
     * @since 6.5.0
     */
        mergeProxiedConfigs: function(name, itemConfig, alwaysClone) {
            var me = this,
                ret = itemConfig,
                proxied = me.getProxiedConfigs(name),
                configurator;
            if (proxied) {
                if (!itemConfig) {
                    ret = proxied;
                } else if (itemConfig.constructor === Object) {
                    configurator = me.self.getConfigurator();
                    // First clone() so don't mutate the config:
                    ret = configurator.merge(me, Ext.clone(itemConfig), proxied);
                }
            }
            if (alwaysClone && ret === itemConfig) {
                ret = Ext.clone(ret);
            }
            return ret;
        },
        statics: {
            processClass: function(targetClass, proxyConfig) {
                var ExtConfig = Ext.Config,
                    targetProto = targetClass.prototype,
                    add = {},
                    proxies = targetClass.$configProxies,
                    cfg, configs, itemGetter, i, item, methods, n, name, proxiedConfigs, s;
                for (item in proxyConfig) {
                    itemGetter = ExtConfig.get(item).names.get;
                    configs = proxyConfig[item];
                    if (Ext.isArray(configs)) {
                        methods = null;
                    } else {
                        methods = configs.methods;
                        configs = configs.configs;
                    }
                    if (!(proxiedConfigs = proxies[item])) {
                        proxies[item] = proxiedConfigs = [];
                    } else {
                        // this array comes from the superclass so slice it for this class:
                        proxies[item] = proxiedConfigs = proxiedConfigs.slice();
                    }
                    for (i = 0 , n = methods && methods.length; i < n; ++i) {
                        if (!targetProto[name = methods[i]]) {
                            targetProto[name] = ConfigProxy.wrapFn(itemGetter, name);
                        } else {
                            Ext.raise('Cannot proxy method "' + name + '"');
                        }
                    }
                    for (i = 0 , n = configs && configs.length; i < n; ++i) {
                        cfg = ExtConfig.get(s = configs[i]);
                        if (s in add) {
                            Ext.raise('Duplicate proxy config definitions for "' + s + '"');
                        }
                        if (s in targetProto.config) {
                            Ext.raise('Config "' + s + '" already defined for class ' + targetProto.$className);
                        }
                        add[s] = undefined;
                        // sentinel initial value to avoid smashing
                        proxiedConfigs.push(cfg);
                        if (!targetProto[name = cfg.names.get]) {
                            targetProto[name] = ConfigProxy.wrapGet(itemGetter, name);
                        } else {
                            Ext.raise('Cannot proxy "' + s + '" config getter');
                        }
                        if (!targetProto[name = cfg.names.set]) {
                            targetProto[name] = ConfigProxy.wrapSet(itemGetter, name, s);
                        } else {
                            Ext.raise('Cannot proxy "' + s + '" config setter');
                        }
                    }
                }
                targetClass.addConfig(add);
            },
            wrapFn: function(itemGetter, name) {
                return function() {
                    var item = this[itemGetter]();
                    return item && item[name].apply(item, arguments);
                };
            },
            wrapGet: function(itemGetter, configGetter) {
                return function() {
                    var item = this[itemGetter]();
                    return item && item[configGetter]();
                };
            },
            wrapSet: function(itemGetter, configSetter, itemName) {
                return function(value) {
                    var me = this,
                        item, proxiedConfigs;
                    // We define the proxied configs with "undefined" value so that we can
                    // detect this and not smash them by default.
                    if (!me.isConfiguring || value !== undefined) {
                        // If the item's applier called mergeProxiedConfigs or getProxiedConfigs
                        // then each config is marked as processed in proxiedConfigs (only during
                        // initialization).
                        item = me[itemGetter]();
                        proxiedConfigs = me.$proxiedConfigs;
                        // lazy created by itemGetter
                        if (proxiedConfigs && proxiedConfigs[itemName]) {
                            delete proxiedConfigs[itemName];
                            // drop only the first set call
                            item = null;
                        }
                        if (item) {
                            item[configSetter](value);
                        }
                    }
                    return me;
                };
            }
        }
    };
});

/**
 * @private
 */
Ext.define('Ext.mixin.ConfigState', {
    extend: 'Ext.Mixin',
    mixinConfig: {
        id: 'configstate'
    },
    alternateStateConfig: '',
    toggleConfigState: function(isAlternate) {
        var me = this,
            state = me.capturedConfigState,
            cfg = me.getConfig(me.alternateStateConfig),
            key;
        if (!cfg) {
            return;
        }
        if (isAlternate) {
            state = {};
            for (key in cfg) {
                state[key] = me.getConfig(key);
            }
            me.capturedConfigState = state;
            me.setConfig(cfg);
        }
        // Capture
        else if (!me.isConfiguring && state) {
            me.setConfig(state);
            delete me.capturedConfigState;
        }
    }
});

/**
 * @private
 * Common methods for both classic & modern containers
 */
Ext.define('Ext.mixin.Container', {
    extend: 'Ext.Mixin',
    mixinConfig: {
        id: 'container'
    },
    /**
     * @property {Boolean} isContainer
     * `true` in this class to identify an object as an instantiated Container, or subclass thereof.
     */
    isContainer: true,
    /**
     * @cfg {Boolean} nameHolder
     * When `true` child components are tracked by their `name` property and can be
     * retrieved using the `lookupName` method.
     */
    nameHolder: false,
    /**
     * @cfg {Boolean} referenceHolder
     * If `true`, this container will be marked as being a point in the hierarchy where
     * references to items with a specified `reference` config will be held. The container
     * will automatically become a referenceHolder if a {@link #controller} is specified.
     *
     * See the introductory docs for {@link Ext.container.Container} for more information
     * about references & reference holders.
     */
    referenceHolder: false,
    /**
     * Returns an object holding the descendants of this container keyed by their
     * `name`. This object should not be held past the scope of the function calling this
     * method. It will not be valid if items are added or removed from this or any
     * sub-container.
     *
     * The intended usage is shown here (assume there are 3 components with names of
     * "foo", "bar" and "baz" at some level below this container):
     *
     *      onClick: function () {
     *          var items = this.getNamedItems();
     *
     *          // using "items" we can access any descendant by its "name"
     *
     *          items.foo.getValue() + items.bar.getValue() + items.baz.getValue();
     *      }
     *
     * If `this` component has a `name` assigned to it, it is **not** included in this
     * object. That name is understood to belong to the ancestor container configured
     * as the `nameHolder`.
     *
     * @return {Object} An object with each named child. This will be `null` if this
     * container has no descendants with a `name` specified.
     * @since 6.5.0
     */
    getNamedItems: function() {
        if (Ext.referencesDirty) {
            Ext.fixReferences();
        }
        return this.nameRefs || null;
    },
    /**
     * Returns an object holding the descendants of this view keyed by their
     * `{@link Ext.Component#cfg-reference reference}`. This object should not be held
     * past the scope of the function calling this method. It will not be valid if items
     * are added or removed from this or any sub-container.
     *
     * The intended usage is shown here (assume there are 3 components with reference
     * values of "foo", "bar" and "baz" at some level below this container):
     *
     *      onClick: function () {
     *          var refs = this.getReferences();
     *
     *          // using "refs" we can access any descendant by its "reference"
     *
     *          refs.foo.getValue() + refs.bar.getValue() + refs.baz.getValue();
     *      }
     *
     * If `this` component has a `{@link Ext.Component#cfg-reference reference}` assigned
     * to it, that is **not** included in this object. That reference is understood to
     * belong to the ancestor container configured as the `referenceHolder`.
     *
     * @return {Object} An object with each child reference. This will be `null` if this
     * container has no descendants with a `{@link Ext.Component#cfg-reference reference}`
     * specified.
     * @since 5.0.0
     */
    getReferences: function() {
        if (Ext.referencesDirty) {
            Ext.fixReferences();
        }
        return this.refs || null;
    },
    /**
     * Gets a reference to the component with the specified
     * {@link Ext.Component#cfg-reference reference} value.
     *
     * The method is a short-hand for the {@link #lookupReference} method.
     *
     * @param {String} ref The value of the `reference` to lookup.
     * @return {Ext.Component} The referenced component or `null` if it is not found.
     * @since 6.0.1
     */
    lookup: function(ref) {
        var refs = this.getReferences();
        return (refs && refs[ref]) || null;
    },
    /**
     * Gets a reference to the component with the specified `name` property.
     *
     * @param {String} name The name of the descendant to lookup.
     * @return {Ext.Component} The component or `null` if it is not found.
     * @since 6.5.0
     */
    lookupName: function(name) {
        var items = this.getNamedItems();
        return (items && items[name]) || null;
    },
    /**
     * Gets a reference to the component with the specified {@link #reference} value.
     *
     * The {@link #lookup} method is a short-hand version of this method.
     *
     * @param {String} ref The name of the reference to lookup.
     * @return {Ext.Component} The referenced component or `null` if it is not found.
     * @since 5.0
     */
    lookupReference: function(ref) {
        return this.lookup(ref);
    },
    privates: {
        /**
         * Sets up a component name reference.
         * @param {Ext.Component} component The component to reference.
         * @private
         */
        attachNameRef: function(component) {
            var me = this,
                key = component.name || component._name,
                entry, nameRefs;
            // Cleaning all this up later anyway
            if (key && !me.destroying && !me.destroyed) {
                nameRefs = me.nameRefs || (me.nameRefs = {});
                entry = nameRefs[key];
                if (!entry) {
                    entry = component.shareableName ? [
                        component
                    ] : component;
                } else if (!entry.isInstance) {
                    // Else an existing entry is either a component (which will have false
                    // for shareableName) or an array (all elements of which have true
                    // for their shareableName).
                    entry.push(component);
                } else {
                    Ext.raise('Duplicate name: "' + key + '" on ' + me.id + ' between ' + entry.id + ' and ' + component.id);
                }
                nameRefs[key] = entry;
            }
        },
        /**
         * Sets up a component reference.
         * @param {Ext.Component} component The component to reference.
         * @private
         */
        attachReference: function(component) {
            var me = this,
                key, refs;
            // Cleaning all this up later anyway
            if (!me.destroying && !me.destroyed) {
                refs = me.refs || (me.refs = {});
                key = component.referenceKey;
                if (refs[key] && refs[key] !== component) {
                    Ext.log.warn('Duplicate reference: "' + key + '" on ' + me.id);
                }
                refs[key] = component;
            }
        },
        containerOnAdded: function(component, instanced) {
            // We have been added to a container, we may have child references
            // or be a reference ourselves. At this point we have no way of knowing if
            // our references are correct, so trigger a fix.
            Ext.ComponentManager.markReferencesDirty();
        },
        containerOnRemoved: function(destroying) {
            // If we're destroying this will get cleaned up anyway
            if (!destroying) {
                // Clear any references here, they will be reset after the
                // next call to lookupReference after being marked dirty.
                // It's easier to wipe & re-establish them than attempt to
                // track what changed and prune the collection
                Ext.ComponentManager.markReferencesDirty();
            }
        },
        initContainerInheritedState: function(inheritedState, inheritedStateInner) {
            var me = this,
                controller = me.getController(),
                session = me.getSession(),
                // Don't instantiate it here, we just want to know whether we
                // were configured with a VM
                viewModel = me.getConfig('viewModel', true),
                reference = me.reference,
                referenceHolder = me.referenceHolder;
            if (me.nameHolder) {
                inheritedState.nameHolder = me;
            }
            if (controller) {
                inheritedState.referenceHolder = controller;
                referenceHolder = true;
            } else if (referenceHolder) {
                inheritedState.referenceHolder = me;
            }
            if (referenceHolder) {
                inheritedState.referencePath = '';
            } else if (reference && me.isParentReference) {
                inheritedState.referencePath = me.referenceKey + '.';
            }
            if (session) {
                inheritedState.session = session;
            }
            if (viewModel) {
                inheritedState.viewModelPath = '';
            } else if (reference && me.isParentReference) {
                inheritedState.viewModelPath = me.viewModelKey + '.';
            }
        },
        setupReference: function(reference) {
            var len;
            if (reference && reference.charAt(len = reference.length - 1) === '>') {
                this.isParentReference = true;
                reference = reference.substring(0, len);
            }
            if (reference && !Ext.validIdRe.test(reference)) {
                Ext.Error.raise('Invalid reference "' + reference + '" for ' + this.getId() + ' - not a valid identifier');
            }
            return reference;
        }
    }
});

/**
 * Handles mapping key events to handling functions for an element or a Component.
 * One KeyMap can be used for multiple actions.
 *
 * A KeyMap must be configured with a {@link #target} as an event source which may be
 * an Element or a Component.
 *
 * If the target is an element, then the `keydown` event will trigger the invocation
 * of {@link #binding}s.
 *
 * It is possible to configure the KeyMap with a custom {@link #eventName} to listen for.
 * This may be useful when the {@link #target} is a Component.
 *
 * The KeyMap's event handling requires that the first parameter passed is a key event.
 * So if the Component's event signature is different, specify a {@link #processEvent}
 * configuration which accepts the event's parameters and returns a key event.
 *
 * Functions specified in {@link #binding}s are called with this signature:
 * `(String key, Ext.event.Event e)` (if the match is a multi-key combination
 * the callback will still be called only once). A KeyMap can also handle a string
 * representation of keys. By default KeyMap starts enabled.
 *
 * Usage:
 *
 *      // map one key by key code
 *      var map = new Ext.util.KeyMap({
 *          target: "my-element",
 *          key: 13, // or Ext.event.Event.ENTER
 *          handler: myHandler,
 *          scope: myObject
 *      });
 *
 *      // map multiple keys to one action by string
 *      var map = new Ext.util.KeyMap({
 *          target: "my-element",
 *          key: "a\r\n\t",
 *          handler: myHandler,
 *          scope: myObject
 *      });
 *
 *      // map multiple keys to multiple actions by strings and array of codes
 *      var map = new Ext.util.KeyMap({
 *          target: "my-element",
 *          binding: [{
 *              key: [10, 13],
 *              handler: function() {
 *                  alert("Return was pressed");
 *              }
 *          }, {
 *              key: "abc",
 *              handler: function() {
 *                  alert('a, b or c was pressed');
 *              }
 *          }, {
 *              key: "\t",
 *              ctrl: true,
 *              shift: true,
 *              handler: function() {
 *                  alert('Control + shift + tab was pressed.');
 *              }
 *          }]
 *      });
 *
 * KeyMaps can also bind to Components and process key-based events fired by Components.
 *
 * To bind to a Component, include the Component event name to listen for, and a `processEvent`
 * implementation which returns the key event for further processing by the KeyMap:
 *
 *      var map = new Ext.util.KeyMap({
 *          target: myGridView,
 *          eventName: 'itemkeydown',
 *          processEvent: function(view, record, node, index, event) {
 *              // Load the event with the extra information needed by the mappings
 *              event.view = view;
 *              event.store = view.getStore();
 *              event.record = record;
 *              event.index = index;
 *              return event;
 *          },
 *          binding: {
 *              key: Ext.event.Event.DELETE,
 *              handler: function(keyCode, e) {
 *                  e.store.remove(e.record);
 *                  
 *                  // Attempt to select the record that's now in its place
 *                  e.view.getSelectionModel().select(e.index);
 *              }
 *          }
 *      });
 */
Ext.define('Ext.util.KeyMap', {
    alternateClassName: 'Ext.KeyMap',
    /**
     * @property {Ext.event.Event} lastKeyEvent
     * The last key event that this KeyMap handled.
     */
    /**
     * @cfg {Ext.Component/Ext.dom.Element/HTMLElement/String} target
     * The object on which to listen for the event specified by the {@link #eventName}
     * config option.
     */
    /**
     * @cfg {Object/Object[][]} binding
     * Either a single object describing a handling function for s specified key (or set of keys),
     * or an array of such objects.
     * @cfg {String/String[]} binding.key A single keycode or an array of keycodes to handle, or
     * a RegExp which specifies characters to handle, eg `/[a-z]/`.
     * @cfg {Boolean} binding.shift `true` to handle key only when shift is pressed, `false`
     * to handle the key only when shift is not pressed (defaults to undefined)
     * @cfg {Boolean} binding.ctrl `true` to handle key only when ctrl is pressed, `false`
     * to handle the key only when ctrl is not pressed (defaults to undefined)
     * @cfg {Boolean} binding.alt `true` to handle key only when alt is pressed, `false`
     * to handle the key only when alt is not pressed (defaults to undefined)
     * @cfg {Function} binding.handler The function to call when KeyMap finds the expected
     * key combination
     * @cfg {Function} binding.fn Alias of handler (for backwards-compatibility)
     * @cfg {Object} binding.scope The scope (`this` context) in which the handler function
     * is executed.
     * @cfg {String} binding.defaultEventAction A default action to apply to the event
     * *when the handler returns `true`*. Possible values are: stopEvent, stopPropagation,
     * preventDefault. If no value is set no action is performed.
     */
    /**
     * @cfg {Object} [processEventScope=this]
     * The scope (`this` context) in which the {@link #processEvent} method is executed.
     */
    /**
     * @cfg {Boolean} [ignoreInputFields=false]
     * Configure this as `true` if there are any input fields within the {@link #target},
     * and this KeyNav should not process events from input fields
     * (`<input>`,`<textarea> and elements with `contentEditable="true"`)
     */
    /**
     * @cfg {Number} [priority]
     * The priority to set on this KeyMap's listener. Listeners with a higher priority
     * are fired before those with lower priority.
     */
    /**
     * @cfg {String} eventName
     * The event to listen for to pick up key events.
     */
    eventName: 'keydown',
    constructor: function(config) {
        var me = this;
        // Handle legacy arg list in which the first argument is the target.
        // TODO: Deprecate in V5
        if ((arguments.length !== 1) || (typeof config === 'string') || config.dom || config.tagName || config === document || config.isComponent) {
            Ext.raise("Legacy multi-argument KeyMap constructor is removed. " + "Use a config object instead.");
        }
        Ext.apply(me, config);
        me.bindings = [];
        if (!me.target.isComponent) {
            me.target = Ext.get(me.target);
        }
        if (me.binding) {
            me.addBinding(me.binding);
        } else if (config.key) {
            me.addBinding(config);
        }
        me.enable();
    },
    /**
     * Add a new binding to this KeyMap.
     *
     * Usage:
     *
     *      // Create a KeyMap
     *      var map = new Ext.util.KeyMap({
     *          target: Ext.getDoc(),
     *          key: Ext.event.Event.ENTER,
     *          handler: handleKey
     *      });
     *
     *      // Add a new binding to the existing KeyMap later
     *      map.addBinding({
     *          key: 'abc',
     *          shift: true,
     *          handler: handleKey
     *      });
     *
     * @param {Object/Object[]} binding A single KeyMap config or an array of configs.
     * The following config object properties are supported:
     *
     * @param {String/Array} binding.key A single keycode or an array of keycodes to handle,
     * or a RegExp which specifies characters to handle, eg `/[a-z]/`.
     * @param {Boolean} binding.shift `true` to handle key only when shift is pressed,
     * `false` to handle the key only when shift is not pressed (defaults to undefined).
     * @param {Boolean} binding.ctrl `true` to handle key only when ctrl is pressed,
     * `false` to handle the key only when ctrl is not pressed (defaults to undefined).
     * @param {Boolean} binding.alt `true` to handle key only when alt is pressed,
     * `false` to handle the key only when alt is not pressed (defaults to undefined).
     * @param {Function} binding.handler The function to call when KeyMap finds the
     * expected key combination.
     * @param {Function} binding.fn Alias of handler (for backwards-compatibility).
     * @param {Object} binding.scope The scope (`this` context) in which the handler function
     * is executed.
     * @param {String} binding.defaultEventAction A default action to apply to the event
     * *when the handler returns `true`*. Possible values are: stopEvent, stopPropagation,
     * preventDefault. If no value is set no action is performed.
     */
    addBinding: function(binding) {
        var me = this,
            keyCode = binding.key,
            i, len;
        if (me.processing) {
            me.bindings = me.bindings.slice(0);
        }
        if (Ext.isArray(binding)) {
            for (i = 0 , len = binding.length; i < len; i++) {
                me.addBinding(binding[i]);
            }
            return;
        }
        me.bindings.push(Ext.apply({
            keyCode: me.processKeys(keyCode)
        }, binding));
    },
    /**
     * Remove a binding from this KeyMap.
     * @param {Object} binding See {@link #addBinding for options}
     */
    removeBinding: function(binding) {
        var me = this,
            bindings = me.bindings,
            len = bindings.length,
            i, item, keys;
        if (me.processing) {
            me.bindings = bindings.slice(0);
        }
        keys = me.processKeys(binding.key);
        for (i = 0; i < len; i++) {
            item = bindings[i];
            if ((item.fn || item.handler) === (binding.fn || binding.handler) && item.scope === binding.scope) {
                if (binding.alt === item.alt && binding.ctrl === item.ctrl && binding.shift === item.shift) {
                    if (Ext.Array.equals(item.keyCode, keys)) {
                        Ext.Array.erase(me.bindings, i, 1);
                        return;
                    }
                }
            }
        }
    },
    processKeys: function(keyCode) {
        var processed = false,
            key, keys, keyString, len, i;
        // A RegExp to match typed characters
        if (keyCode.test) {
            return keyCode;
        }
        // A String of characters to match
        if (Ext.isString(keyCode)) {
            keys = [];
            keyString = keyCode.toUpperCase();
            for (i = 0 , len = keyString.length; i < len; i++) {
                keys.push(keyString.charCodeAt(i));
            }
            keyCode = keys;
            processed = true;
        }
        // Numeric key code
        if (!Ext.isArray(keyCode)) {
            keyCode = [
                keyCode
            ];
        }
        if (!processed) {
            for (i = 0 , len = keyCode.length; i < len; i++) {
                key = keyCode[i];
                if (Ext.isString(key)) {
                    keyCode[i] = key.toUpperCase().charCodeAt(0);
                }
            }
        }
        return keyCode;
    },
    /**
     * Process the {@link #eventName event} from the {@link #target}.
     * @private
     * @param {Ext.event.Event} event
     */
    handleTargetEvent: function(event) {
        var me = this,
            bindings, i, len, result;
        if (me.enabled) {
            bindings = me.bindings;
            // Process the event
            event = me.processEvent.apply(me.processEventScope || me, arguments);
            // A custom processEvent implementation may return falsy to stop the KeyMap's processing
            if (event) {
                me.lastKeyEvent = event;
                // Ignore events from input fields if configured to do so
                if (me.ignoreInputFields && Ext.fly(event.target).isInputField()) {
                    return;
                }
                // If the processor does not return a keyEvent, we can't process it.
                // Allow them to return false to cancel processing of the event
                if (!event.getKey) {
                    return event;
                }
                me.processing = true;
                for (i = 0 , len = bindings.length; i < len; i++) {
                    result = me.processBinding(bindings[i], event);
                    if (result === false) {
                        me.processing = false;
                        return result;
                    }
                }
                me.processing = false;
            }
        }
    },
    /**
     * @cfg {Function} processEvent
     * An optional event processor function which accepts the argument list provided by the
     * {@link #eventName configured event} of the {@link #target}, and returns a keyEvent
     * for processing by the KeyMap.
     *
     * This may be useful when the {@link #target} is a Component with a complex event signature,
     * where the event is not the first parameter. Extra information from the event arguments
     * may be injected into the event for use by the handler functions before returning it.
     *
     * If `null` is returned the KeyMap stops processing the event.
     */
    processEvent: Ext.identityFn,
    /**
     * Process a particular binding and fire the handler if necessary.
     * @private
     * @param {Object} binding The binding information
     * @param {Ext.event.Event} event
     */
    processBinding: function(binding, event) {
        if (this.checkModifiers(binding, event)) {
            // eslint-disable-next-line vars-on-top
            var key = event.getKey(),
                handler = binding.fn || binding.handler,
                scope = binding.scope || this,
                keyCode = binding.keyCode,
                defaultEventAction = binding.defaultEventAction,
                i, len, result;
            // keyCode is a regExp specifying acceptable characters. eg /[a-z]/
            if (keyCode.test) {
                if (keyCode.test(String.fromCharCode(event.getCharCode()))) {
                    result = handler.call(scope, key, event);
                    if (result !== true && defaultEventAction) {
                        event[defaultEventAction]();
                    }
                    if (result === false) {
                        return result;
                    }
                }
            }
            // Array of key codes
            else if (keyCode.length) {
                for (i = 0 , len = keyCode.length; i < len; i++) {
                    if (key === keyCode[i]) {
                        result = handler.call(scope, key, event);
                        if (result !== true && defaultEventAction) {
                            event[defaultEventAction]();
                        }
                        if (result === false) {
                            return result;
                        }
                        break;
                    }
                }
            }
        }
    },
    /**
     * Check if the modifiers on the event match those on the binding
     * @private
     * @param {Object} binding
     * @param {Ext.event.Event} event
     * @return {Boolean} True if the event matches the binding
     */
    checkModifiers: function(binding, event) {
        var keys = [
                'shift',
                'ctrl',
                'alt'
            ],
            i, len, val, key;
        for (i = 0 , len = keys.length; i < len; i++) {
            key = keys[i];
            val = binding[key];
            if (!(val === undefined || (val === event[key + 'Key']))) {
                return false;
            }
        }
        return true;
    },
    /**
     * Shorthand for adding a single key listener.
     *
     * @param {Number/Number[]/Object} key Either the numeric key code, array of key codes
     * or an object with the following options: `{key: (number or array), shift: (true/false),
     * ctrl: (true/false), alt: (true/false)}`
     * @param {Function} fn The function to call
     * @param {Object} [scope] The scope (`this` reference) in which the function is executed.
     * Defaults to the browser window.
     */
    on: function(key, fn, scope) {
        var keyCode, shift, ctrl, alt;
        if (Ext.isObject(key) && !Ext.isArray(key)) {
            keyCode = key.key;
            shift = key.shift;
            ctrl = key.ctrl;
            alt = key.alt;
        } else {
            keyCode = key;
        }
        this.addBinding({
            key: keyCode,
            shift: shift,
            ctrl: ctrl,
            alt: alt,
            fn: fn,
            scope: scope
        });
    },
    /**
     * Shorthand for removing a single key listener.
     *
     * @param {Number/Number[]/Object} key Either the numeric key code, array of key codes
     * or an object with the following options: `{key: (number or array), shift: (true/false),
     * ctrl: (true/false), alt: (true/false)}`
     * @param {Function} fn The function to call
     * @param {Object} [scope] The scope (`this` reference) in which the function is executed.
     * Defaults to the browser window.
     */
    un: function(key, fn, scope) {
        var keyCode, shift, ctrl, alt;
        if (Ext.isObject(key) && !Ext.isArray(key)) {
            keyCode = key.key;
            shift = key.shift;
            ctrl = key.ctrl;
            alt = key.alt;
        } else {
            keyCode = key;
        }
        this.removeBinding({
            key: keyCode,
            shift: shift,
            ctrl: ctrl,
            alt: alt,
            fn: fn,
            scope: scope
        });
    },
    /**
     * Returns true if this KeyMap is enabled
     * @return {Boolean}
     */
    isEnabled: function() {
        return !!this.enabled;
    },
    /**
     * Enables this KeyMap
     */
    enable: function() {
        var me = this;
        if (!me.enabled) {
            me.target.on(me.eventName, me.handleTargetEvent, me, {
                capture: me.capture,
                priority: me.priority
            });
            me.enabled = true;
        }
    },
    /**
     * Disable this KeyMap
     */
    disable: function() {
        var me = this;
        if (me.enabled) {
            if (!me.target.destroyed) {
                me.target.removeListener(me.eventName, me.handleTargetEvent, me);
            }
            me.enabled = false;
        }
    },
    /**
     * Convenience function for setting disabled/enabled by boolean.
     * @param {Boolean} disabled
     */
    setDisabled: function(disabled) {
        if (disabled) {
            this.disable();
        } else {
            this.enable();
        }
    },
    /**
     * Destroys the KeyMap instance and removes all handlers.
     * @param {Boolean} removeTarget True to also remove the {@link #target}
     */
    destroy: function(removeTarget) {
        var me = this,
            target = me.target;
        me.bindings = [];
        me.disable();
        me.target = null;
        if (removeTarget) {
            target.destroy();
            Ext.raise("Using removeTarget argument in KeyMap destructor is not supported.");
        }
        me.callParent();
    }
});

/**
 * Provides a convenient wrapper for normalized keyboard navigation. KeyNav allows you to bind
 * navigation keys to function calls that will get called when the keys are pressed, providing
 * an easy way to implement custom navigation schemes for any UI component.
 *
 * The following are all of the possible keys that can be implemented: Enter, Space, Left, Right,
 * Up, Down, Tab, Esc, Page Up, Page Down, Delete, Backspace, Home, End.
 *
 * Usage:
 *
 *      var nav = new Ext.util.KeyNav({
 *          target: "my-element",
 *          
 *          left: function(e) {
 *              this.moveLeft(e.ctrlKey);
 *          },
 *          right: function(e) {
 *              this.moveRight(e.ctrlKey);
 *          },
 *          enter: function(e) {
 *              this.save();
 *          },
 *          
 *          // Binding may be a function specifiying fn, scope and defaultEventAction
 *          esc: {
 *              fn: this.onEsc,
 *              defaultEventAction: false
 *          },
 *          
 *          // Binding may be keyed by a single character
 *          A: {
 *              ctrl: true,
 *              fn: selectAll
 *          },
 *          
 *          // Binding may be keyed by a key code (45 = INSERT)
 *          45: {
 *              fn: doInsert
 *          },
 *          
 *          scope: myObject
 *     });
 */
Ext.define('Ext.util.KeyNav', {
    alternateClassName: 'Ext.KeyNav',
    requires: [
        'Ext.util.KeyMap'
    ],
    /**
     * @cfg {Boolean} disabled
     * True to disable this KeyNav instance.
     */
    disabled: false,
    /**
     * @cfg {String} [defaultEventAction=false]
     * The method to call on the {@link Ext.event.Event} after this KeyNav intercepts a key.
     * Valid values are {@link Ext.event.Event#stopEvent}, {@link Ext.event.Event#preventDefault}
     * and {@link Ext.event.Event#stopPropagation}.
     *
     * If a falsy value is specified, no method is called on the key event.
     */
    defaultEventAction: false,
    /**
     * @cfg {Boolean} forceKeyDown
     *
     * Handle the keydown event instead of keypress. KeyNav automatically does this for IE
     * since IE does not propagate special keys on keypress, but setting this to true will force
     * other browsers to also handle keydown instead of keypress.
     */
    forceKeyDown: false,
    /**
     * @cfg {Ext.Component/Ext.dom.Element/HTMLElement/String} target
     * The object on which to listen for the event specified by the {@link #eventName}
     * config option.
     */
    /**
     * @cfg {String} eventName
     * The event to listen for to pick up key events.
     */
    eventName: 'keypress',
    /**
     * @cfg {Function} processEvent
     * An optional event processor function which accepts the argument list provided by the
     * {@link #eventName configured event} of the {@link #target}, and returns a keyEvent
     * for processing by the KeyMap.
     *
     * This may be useful when the {@link #target} is a Component with s complex event signature.
     * Extra information from the event arguments may be injected into the event for use
     * by the handler functions before returning it.
     */
    /**
     * @cfg {Object} [processEventScope=this]
     * The scope (`this` context) in which the {@link #processEvent} method is executed.
     */
    /**
     * @cfg {Boolean} [ignoreInputFields=false]
     * Configure this as `true` if there are any input fields within the {@link #target}, and this
     * KeyNav should not process events from input fields (`<input>`, `<textarea>` and elements
     * with `contentEditable="true"`)
     */
    /**
     * @cfg {Ext.util.KeyMap} [keyMap]
     * An optional pre-existing {@link Ext.util.KeyMap KeyMap} to use to listen for key events.
     * If not specified, one is created.
     */
    /**
     * @property {Ext.event.Event} lastKeyEvent
     * The last key event that this KeyMap handled.
     */
    /**
     * @cfg {Number} [priority]
     * The priority to set on this KeyNav's listener. Listeners with a higher priority are fired
     * before those with lower priority.
     */
    statics: {
        keyOptions: {
            left: 37,
            right: 39,
            up: 38,
            down: 40,
            space: 32,
            pageUp: 33,
            pageDown: 34,
            del: 46,
            backspace: 8,
            home: 36,
            end: 35,
            enter: 13,
            esc: 27,
            tab: 9
        }
    },
    constructor: function(config) {
        var me = this,
            keymapCfg, map;
        if (arguments.length === 2) {
            Ext.raise("2-argument KeyNav constructor is removed. Use a config object instead.");
        }
        config = config || {};
        keymapCfg = {
            target: config.target,
            ignoreInputFields: config.ignoreInputFields,
            eventName: me.getKeyEvent('forceKeyDown' in config ? config.forceKeyDown : me.forceKeyDown, config.eventName),
            capture: config.capture
        };
        if (me.map) {
            me.map.destroy();
        }
        // Ensure config system configs are set
        me.initConfig(config);
        if (config.processEvent) {
            keymapCfg.processEvent = config.processEvent;
            keymapCfg.processEventScope = config.processEventScope || me;
        }
        if (config.priority) {
            keymapCfg.priority = config.priority;
        }
        // If they specified a KeyMap to use, use it
        if (config.keyMap) {
            map = me.map = config.keyMap;
        } else // Otherwise, create one, and remember to destroy it on destroy
        {
            map = me.map = new Ext.util.KeyMap(keymapCfg);
            me.destroyKeyMap = true;
        }
        me.addBindings(config);
        map.disable();
        if (!config.disabled) {
            map.enable();
        }
    },
    addBindings: function(bindings) {
        var me = this,
            map = me.map,
            keyCodes = Ext.util.KeyNav.keyOptions,
            Event = Ext.event.Event,
            defaultScope = bindings.scope || me,
            binding, keyName, keyCode;
        for (keyName in bindings) {
            binding = bindings[keyName];
            // There is a property named after a key name.
            // It may be a function or an binding spec containing handler, scope and
            // defaultEventAction configs
            // Allow { A: { ctrl: true, handler: onCtrlA } }
            // Allow { 45: doInsert } to use key codes directly
            // Allow { F2: onKeyF2 }
            keyCode = keyName.length === 1 ? keyName.charCodeAt(0) : (keyCodes[keyName] || Event[keyName.toUpperCase()]);
            if (keyCode != null) {
                keyName = keyCode;
            }
            if (binding && (keyName.length === 1 || !isNaN(keyName = parseInt(keyName, 10)))) {
                if (typeof binding === 'function') {
                    binding = {
                        handler: binding,
                        defaultEventAction: (bindings.defaultEventAction !== undefined) ? bindings.defaultEventAction : me.defaultEventAction
                    };
                }
                map.addBinding({
                    key: keyName,
                    ctrl: binding.ctrl,
                    shift: binding.shift,
                    alt: binding.alt,
                    handler: Ext.Function.bind(me.handleEvent, binding.scope || defaultScope, [
                        binding.handler || binding.fn,
                        me
                    ], true),
                    defaultEventAction: (binding.defaultEventAction !== undefined) ? binding.defaultEventAction : me.defaultEventAction
                });
            }
        }
    },
    /**
     * Method for filtering out the map argument
     * @private
     * @param {Number} keyCode
     * @param {Ext.event.Event} event
     * @param {Function} handler The function to call
     * @param {Ext.util.KeyNav} keyNav The owning KeyNav
     */
    handleEvent: function(keyCode, event, handler, keyNav) {
        keyNav.lastKeyEvent = event;
        return handler.call(this, event);
    },
    /**
     * Destroy this KeyNav.
     * @param {Boolean} removeEl Pass `true` to remove the element associated with this KeyNav.
     */
    destroy: function(removeEl) {
        var me = this;
        if (removeEl) {
            Ext.raise("removeEl argument in KeyNav destructor is not supported anymore.");
        }
        if (me.destroyKeyMap) {
            me.map.destroy(removeEl);
        }
        me.map = null;
        me.callParent();
    },
    /**
     * Enables this KeyNav.
     */
    enable: function() {
        // this.map will be removed if destroyed
        if (this.map) {
            this.map.enable();
            this.disabled = false;
        }
    },
    /**
     * Disables this KeyNav.
     */
    disable: function() {
        // this.map will be removed if destroyed
        if (this.map) {
            this.map.disable();
        }
        this.disabled = true;
    },
    /**
     * Convenience function for setting disabled/enabled by boolean.
     * @param {Boolean} disabled
     */
    setDisabled: function(disabled) {
        this.map.setDisabled(disabled);
        this.disabled = disabled;
    },
    isEnabled: function() {
        return !this.disabled;
    },
    /**
     * @private
     * Determines the event to bind to listen for keys. Defaults to the {@link #eventName} value,
     * but may be overridden the {@link #forceKeyDown} setting.
     *
     * @return {String} The type of event to listen for.
     */
    getKeyEvent: function(forceKeyDown, configuredEventName) {
        if (forceKeyDown || (Ext.supports.SpecialKeyDownRepeat && !configuredEventName)) {
            return 'keydown';
        } else {
            return configuredEventName || this.eventName;
        }
    }
});

/**
 * A mixin for groups of Focusable things (Components, Widgets, etc) that
 * should respond to arrow keys to navigate among the peers, but keep only
 * one of the peers tabbable by default (tabIndex=0)
 *
 * Some examples: Toolbars, Tab bars, Panel headers, Menus
 */
Ext.define('Ext.mixin.FocusableContainer', {
    extend: 'Ext.Mixin',
    requires: [
        'Ext.util.KeyNav'
    ],
    mixinConfig: {
        id: 'focusablecontainer'
    },
    isFocusableContainer: true,
    /**
     * @cfg {Boolean} [focusableContainer=false] Enable or disable navigation
     * with arrow keys for this FocusableContainer. This option may be useful
     * with nested FocusableContainers, when only the root container should
     * handle keyboard events.
     */
    focusableContainer: false,
    /**
     * @cfg {Boolean} [resetFocusPosition=false] When `true`, FocusableContainer
     * will reset last focused position whenever focus leaves the container.
     * Subsequent tabbing into the container will always focus the first eligible
     * child item.
     *
     * When `false`, subsequent tabbing into the container will focus the child
     * item that was last focused before.
     */
    resetFocusPosition: false,
    /**
     * @cfg {Number} [activeChildTabIndex=0] DOM tabIndex attribute to set on the
     * active Focusable child of this container when using the "Roaming tabindex"
     * technique.
     */
    activeChildTabIndex: 0,
    /**
     * @cfg {Number} [inactiveChildTabIndex=-1] DOM tabIndex attribute to set on
     * inactive Focusable children of this container when using the "Roaming tabindex"
     * technique. This value rarely needs to be changed from its default.
     */
    inactiveChildTabIndex: -1,
    /**
     * @cfg {Boolean} [allowFocusingDisabledChildren=false] Set this to `true`
     * to enable focusing disabled child items via keyboard.
     */
    allowFocusingDisabledChildren: false,
    /**
     * @property {String/Ext.dom.Element} [focusableContainerEl="el"] The name of the element
     * that FocusableContainer should bind its keyboard handler to. Similar to {@link #ariaEl},
     * this name is resolved to the {@link Ext.dom.Element} instance after rendering.
     */
    focusableContainerEl: 'el',
    privates: {
        initFocusableContainer: function(clearChildren) {
            var items, i, len;
            // Allow nested containers to optionally disable
            // children containers' behavior
            if (this.focusableContainer) {
                clearChildren = clearChildren != null ? clearChildren : true;
                this.doInitFocusableContainer(clearChildren);
            } else // A FocusableContainer instance such as a toolbar could have decided
            // to opt out of FC behavior for some reason; it could have happened
            // after all or almost all child items have been initialized with
            // ownerFocusableContainer reference. We need to clean this up
            // if we're not going to behave like a FocusableContainer after all.
            {
                items = this.getFocusables();
                for (i = 0 , len = items.length; i < len; i++) {
                    items[i].ownerFocusableContainer = null;
                }
            }
        },
        doInitFocusableContainer: function(clearChildren) {
            var me = this,
                el = me.focusableContainerEl,
                child;
            // Resolve property name to the actual Element instance if it hasn't been
            // resolved already; doInitFocusableContainer can be called more than once
            if (!el.isElement) {
                el = me.focusableContainerEl = me[el];
            }
            if (me.initFocusableContainerKeyNav) {
                me.initFocusableContainerKeyNav(el);
            }
            // This flag allows post factum initialization of the focusable container,
            // i.e. when container was empty initially and then some tabbable children
            // were added and we need to clear their tabIndices.
            // This is useful for Panel and Window headers that might have tools
            // added dynamically.
            if (clearChildren) {
                me.clearFocusables();
                if (!me.isDisabled()) {
                    child = me.findNextFocusableChild({
                        step: 1
                    }) || me.findNextFocusableChild({
                        beforeRender: true
                    });
                    if (child) {
                        me.activateFocusable(child);
                    }
                }
            }
            child = me.findNextFocusableChild({
                firstTabbable: true
            });
            // If we have no potentially focusable children we need to disable arrow key
            // handlers so that they don't interfere with event bubbling.
            me.activateFocusableContainer(!!child && !me.isDisabled());
        },
        initFocusableContainerKeyNav: function(el) {
            var me = this;
            if (!me.focusableKeyNav) {
                el = el || me.focusableContainerEl;
                me.focusableKeyNav = new Ext.util.KeyNav({
                    target: el,
                    disabled: true,
                    eventName: 'keydown',
                    // Our event processing filters out events from input fields.
                    processEvent: me.processFocusableContainerKeyEvent,
                    processEventScope: me,
                    scope: me,
                    tab: me.onFocusableContainerTabKey,
                    enter: {
                        handler: me.onFocusableContainerEnterKey,
                        ctrl: false,
                        shift: false,
                        alt: false
                    },
                    space: {
                        handler: me.onFocusableContainerSpaceKey,
                        ctrl: false,
                        shift: false,
                        alt: false
                    },
                    up: {
                        handler: me.onFocusableContainerUpKey,
                        ctrl: false,
                        shift: false,
                        alt: false
                    },
                    down: {
                        handler: me.onFocusableContainerDownKey,
                        ctrl: false,
                        shift: false,
                        alt: false
                    },
                    left: {
                        handler: me.onFocusableContainerLeftKey,
                        ctrl: false,
                        shift: false,
                        alt: false
                    },
                    right: {
                        handler: me.onFocusableContainerRightKey,
                        ctrl: false,
                        shift: false,
                        alt: false
                    }
                });
            }
        },
        destroyFocusableContainer: function() {
            this.focusableKeyNav = Ext.destroy(this.focusableKeyNav);
        },
        activateFocusableContainer: function(enable) {
            var keyNav = this.focusableKeyNav;
            if (keyNav) {
                keyNav.setDisabled(!enable);
            }
        },
        isFocusableContainerActive: function() {
            var keyNav = this.focusableKeyNav;
            return keyNav ? !keyNav.disabled : false;
        },
        // Default FocusableContainer implies a flat list of focusable children
        getFocusables: function() {
            return this.items.items;
        },
        initDefaultFocusable: function() {
            var me = this,
                haveFocusable = false,
                items, item, i, len;
            items = me.getFocusables();
            len = items.length;
            if (!len) {
                return;
            }
            // Check if any child Focusable is already active.
            // Note that we're not determining *which* focusable child
            // to focus here, ONLY that we have some focusables.
            for (i = 0; i < len; i++) {
                item = items[i];
                if (!item.isDisabled() && item.isFocusable()) {
                    haveFocusable = true;
                    // DO NOT return an item here! We want to fall through
                    // to findNextFocusableChild below.
                    break;
                }
            }
            if (!haveFocusable) {
                return;
            }
            item = me.findNextFocusableChild({
                items: items,
                step: true
            });
            if (item) {
                me.activateFocusable(item);
            }
            return item;
        },
        clearFocusables: function(skipFocused) {
            var me = this,
                items = me.getFocusables(),
                len = items.length,
                item, i;
            for (i = 0; i < len; i++) {
                item = items[i];
                if (!item.destroyed && item.focusable && item.isTabbable()) {
                    me.deactivateFocusable(item);
                }
            }
        },
        /**
         * @template
         * An overrideable function which preprocesses all key events from within this
         * FocusableContainer. The base implementation vetoes processing of key events from input
         * fields by returning `undefined`. This may be overridden in subclasses with special
         * requirements.
         * @param {Ext.util.Event} e A keyboard event that is significant to the operation
         * of a FocusableContainer.
         * @returns {Ext.util.Event} The event if it is to be processed as a FocusableContainer
         * navigation keystroke, or `undefined` if it is to be ignore by the FocusableContainer
         * navigation machinery.
         */
        processFocusableContainerKeyEvent: function(e) {
            if (!Ext.fly(e.target).isInputField()) {
                return e;
            }
        },
        activateFocusable: function(child) {
            child.setTabIndex(this.activeChildTabIndex);
        },
        deactivateFocusable: function(child) {
            child.setTabIndex(this.inactiveChildTabIndex);
        },
        onFocusableContainerTabKey: function() {
            return true;
        },
        onFocusableContainerEnterKey: function() {
            return true;
        },
        onFocusableContainerSpaceKey: function() {
            return true;
        },
        onFocusableContainerUpKey: function(e) {
            // Default action is to scroll the nearest vertically scrollable container
            e.preventDefault();
            return this.moveChildFocus(e, false);
        },
        onFocusableContainerDownKey: function(e) {
            // Ditto
            e.preventDefault();
            return this.moveChildFocus(e, true);
        },
        onFocusableContainerLeftKey: function(e) {
            // Default action is to scroll the nearest horizontally scrollable container
            e.preventDefault();
            return this.moveChildFocus(e, false);
        },
        onFocusableContainerRightKey: function(e) {
            // Ditto
            e.preventDefault();
            return this.moveChildFocus(e, true);
        },
        getFocusableFromEvent: function(e) {
            var child = Ext.Component.from(e);
            if (!child) {
                Ext.raise("No focusable child found for keyboard event!");
            }
            return child;
        },
        moveChildFocus: function(e, forward) {
            var child = this.getFocusableFromEvent(e);
            return this.focusChild(child, forward, e);
        },
        focusChild: function(child, forward) {
            var nextChild = this.findNextFocusableChild({
                    child: child,
                    step: forward
                });
            if (nextChild) {
                nextChild.focus();
            }
            return nextChild;
        },
        findNextFocusableChild: function(options) {
            // This method is private, so options should always be provided
            var beforeRender = options.beforeRender,
                firstTabbable = options.firstTabbable,
                items, item, child, step, idx, i, len, allowDisabled;
            items = options.items || this.getFocusables();
            step = options.step != null ? options.step : 1;
            child = options.child;
            // Some containers such as Menus need to support arrowing over disabled children
            allowDisabled = !!this.allowFocusingDisabledChildren;
            // If the child is null or undefined, idx will be -1.
            // The loop below will account for that, trying to find
            // the first focusable child from either end (depending on step)
            idx = Ext.Array.indexOf(items, child);
            // It's often easier to pass a boolean for 1/-1
            step = step === true ? 1 : step === false ? -1 : step;
            len = items.length;
            i = step > 0 ? (idx < len ? idx + step : 0) : (idx > 0 ? idx + step : len - 1);
            for (; ; i += step) {
                // We're looking for the first or last focusable child
                // and we've reached the end of the items, so punt
                if (idx < 0 && (i >= len || i < 0)) {
                    return null;
                }
                // Loop over forward
                else if (i >= len) {
                    i = -1;
                    // Iterator will increase it once more
                    
                    continue;
                }
                // Loop over backward
                else if (i < 0) {
                    i = len;
                    
                    continue;
                }
                // Looped to the same item, give up
                else if (i === idx) {
                    return null;
                }
                item = items[i];
                if (!item || !item.focusable || (item.isDisabled() && !allowDisabled)) {
                    
                    continue;
                }
                if (firstTabbable) {
                    if (item.isTabbable && item.isTabbable()) {
                        return item;
                    }
                }
                // This loop can be run either at FocusableContainer init time,
                // or later when we need to navigate upon pressing an arrow key.
                // When we're navigating, we have to know exactly if the child is
                // focusable or not, hence only rendered children will make the cut.
                // At the init time item.isFocusable() may return false incorrectly
                // just because the item has not been rendered yet and its focusEl
                // is not defined, so we don't bother to call isFocusable and return
                // the first potentially focusable child.
                else if (beforeRender || (item.isFocusable && item.isFocusable())) {
                    return item;
                }
            }
            return null;
        },
        // eslint-disable-line no-unreachable
        onFocusEnter: function(e) {
            var me = this,
                target = e.toComponent,
                child;
            // Some FocusableContainers such as Menus are focusable by themselves;
            // when a container becomes focused we need to switch focus to a child
            // item instead, if there are eligible items.
            // Note that when the target is an actual child item, we will activate
            // it in beforeFocusableChildFocus().
            if (target === me) {
                child = me.initDefaultFocusable();
                if (child) {
                    child.focus();
                }
            }
            // Make sure arrow key handlers are enabled.
            me.activateFocusableContainer(true);
        },
        onFocusLeave: function(e) {
            var me = this;
            if (me.resetFocusPosition) {
                me.clearFocusables();
                me.initDefaultFocusable();
            }
        },
        beforeFocusableChildBlur: Ext.privateFn,
        afterFocusableChildBlur: Ext.privateFn,
        beforeFocusableChildFocus: function(child) {
            var me = this;
            if (!me.focusableContainer || me.destroying || me.destroyed) {
                return;
            }
            // Ensure this child is the only one tabbable
            me.clearFocusables();
            me.activateFocusable(child);
        },
        afterFocusableChildFocus: function(child) {
            var me = this;
            if (!me.focusableContainer || me.destroying || me.destroyed) {
                return;
            }
            me.lastFocusedChild = child;
        },
        onFocusableChildAdd: function(child) {
            var me = this;
            if (child.focusable) {
                child.ownerFocusableContainer = me;
            }
        },
        onFocusableChildRemove: function(child) {
            var me = this,
                next;
            child.ownerFocusableContainer = null;
            // If the focused child is being removed, we need to find another item
            // to make it default focusable.
            if (child === me.lastFocusedChild) {
                me.lastFocusedChild = null;
                next = me.initDefaultFocusable();
                // When currently focused child is about to be removed, it can be
                // hidden or destroyed as well and that could cause focus to be
                // lost to the document body. We don't want that.
                if (child.hasFocus) {
                    next = next || child.findFocusTarget();
                    if (next) {
                        next.focus();
                    }
                }
            }
            child = next || me.findNextFocusableChild({
                step: 1,
                beforeRender: true
            });
            if (!child) {
                me.activateFocusableContainer(false);
            }
        },
        beforeFocusableChildEnable: Ext.privateFn,
        onFocusableChildEnable: function(child) {
            var me = this,
                active;
            if (!me.focusableContainer || me.destroying || me.destroyed) {
                return;
            }
            // Having currently focused child trumps any config
            if (me.containsFocus) {
                active = Ext.ComponentManager.getActiveComponent();
                me.clearFocusables();
                me.activateFocusable(active);
            } else if (me.resetFocusPosition || me.lastFocusedChild == null) {
                me.clearFocusables();
                if (child.hasFocus) {
                    me.activateFocusable(child);
                    active = child;
                }
            } else {
                me.deactivateFocusable(child);
                // Some Components like Buttons do not render tabIndex attribute
                // when they start their lifecycle disabled, or remove tabIndex
                // if they get disabled later. Subsequently, such Components will
                // reset their tabIndex to default configured value upon enabling.
                // We don't want these children to be tabbable so we reset their
                // tabIndex yet again, unless this child is the last focused one.
                if (child === me.lastFocusedChild) {
                    me.clearFocusables();
                    me.activateFocusable(child);
                }
                active = me.findNextFocusableChild({
                    firstTabbable: true
                });
            }
            if (!active) {
                me.initDefaultFocusable();
            }
            // Also make sure arrow key handlers are enabled
            me.activateFocusableContainer(true);
        },
        beforeFocusableChildDisable: function(child) {
            var me = this,
                next;
            if (!me.focusableContainer || me.destroying || me.destroyed) {
                return;
            }
            // When currently focused child is about to be disabled,
            // it may lose the focus as well. For example, Classic buttons
            // will remove tabIndex attribute upon disabling, which
            // in turn will throw focus to the document body and cause
            // onFocusLeave to fire on the FocusableContainer.
            // We're focusing the next sibling to prevent that.
            if (child.hasFocus) {
                next = me.findNextFocusableChild({
                    child: child
                }) || child.findFocusTarget();
                // Note that it is entirely possible not to find the nextTarget,
                // e.g. when we're disabling the last button in a toolbar rendered
                // directly into document body. We don't have a good way to handle
                // such cases at present.
                if (next) {
                    next.focus();
                }
            }
        },
        onFocusableChildDisable: function(child) {
            var me = this,
                next;
            if (!me.focusableContainer || me.destroying || me.destroyed) {
                return;
            }
            // If the disabled child was focused before disabling, we should have
            // switched focus to another child and activated it already.
            next = me.findNextFocusableChild({
                firstTabbable: true
            });
            // If not then activate first found child.
            if (!next) {
                next = me.initDefaultFocusable();
            }
            // It is possible that the disabled child was the last focusable child
            // of this container, in which case we need to deactivate keyboard
            // event handlers.
            if (!next) {
                me.activateFocusableContainer(false);
            }
        },
        beforeFocusableChildHide: function(child) {
            return this.beforeFocusableChildDisable(child);
        },
        onFocusableChildHide: function(child) {
            return this.onFocusableChildDisable(child);
        },
        beforeFocusableChildShow: function(child) {
            return this.beforeFocusableChildEnable(child);
        },
        onFocusableChildShow: function(child) {
            return this.onFocusableChildEnable(child);
        },
        // TODO
        onFocusableChildMasked: Ext.privateFn,
        onFocusableChildDestroy: Ext.privateFn,
        onFocusableChildUpdate: Ext.privateFn
    },
    deprecated: {
        7: {
            configs: {
                enableFocusableContainer: 'focusableContainer'
            }
        }
    }
});

/**
 * @private
 */
Ext.define('Ext.mixin.Hookable', {
    extend: 'Ext.Mixin',
    mixinConfig: {
        id: 'hookable'
    },
    bindHook: function(instance, boundMethod, bindingMethod, preventDefault, extraArgs) {
        instance.afterMethod(boundMethod, bindingMethod || boundMethod, this, preventDefault, extraArgs);
        return this;
    },
    unbindHook: function(instance, boundMethod, bindingMethod) {
        instance.removeMethodListener(boundMethod, bindingMethod || boundMethod, this);
        return this;
    }
});

// @cmd.optimizer.requires.async
/**
 * This mixin allows users to easily require external scripts in their classes. This load
 * process delays application launch (`Ext.onReady`) until all such scripts are loaded
 * ensuring that your class will have access to its required scripts from the start.
 *
 * For example:
 *
 *      Ext.define('Feed', {
 *          mixins: ['Ext.mixin.Mashup'],
 *
 *          requiredScripts: [
 *              '//www.foo.com/code/bar.js' // preserve http/s
 *          ],
 *
 *          // The code in "bar.js" will be available at application launch
 *      });
 *
 * If your required script needs to be dynamic, for example it contains a key for an API, you can
 * use the {@link Ext.Manifest#mashup mashup} object.
 *
 * @since 5.0.0
 */
Ext.define('Ext.mixin.Mashup', function(Mashup) {
    return {
        // eslint-disable-line brace-style
        extend: 'Ext.Mixin',
        mixinConfig: {
            id: 'mashup',
            extended: function(baseClass, derivedClass) {
                Mashup.process(derivedClass);
            }
        },
        /**
     * @cfg {String/String[]} requiredScripts
     * Scripts that are required to be loaded in order for your Mashup to work.
     * These scripts can also use {@link Ext.Template template} syntax if there is data in the
     * {@link Ext.Manifest#mashup mashup} object for the class `xtype`.
     *
     * For example, `Ext.ux.google.Map` needs an API key and so its `requiredScripts` looks
     * like this:
     *
     *      requiredScripts: {
     *          '//maps.googleapis.com/maps/api/js?key={key}'
     *      }
     *
     * The `key` property is provided in `app.json` or directly in `Ext.manifest`:
     *
     *      "mashup": {
     *          "map": {  // the xtype
     *              "key": "[GOOGLE_MAPS_KEY]"
     *          }
     *      }
     *
     * For non-components (classes that have no `xtype`), the `mashupConfig` can be used
     * to supply the configuration key:
     *
     *      mashupConfig: {
     *          key: 'map'
     *      }
     */
        /**
     * @cfg {Object} mashup
     * An object that allows developers to pass values into templates used to populate
     * {@link Ext.Mashup#requiredScripts requiredScripts}.
     *
     * Here is an example of passing a key along to `Ext.ux.google.Map`.
     *
     *      "mashup": {
     *          "map": {  // the xtype
     *              "key": "[GOOGLE_MAPS_KEY]"
     *          }
     *      }
     *
     * @member Ext.Manifest
     */
        statics: {
            process: function(targetClass) {
                var body = targetClass.prototype,
                    requiredScripts = body.requiredScripts,
                    hooks = targetClass._classHooks,
                    onCreated = hooks.onCreated,
                    xtypes = targetClass.prototype.xtypes,
                    mashup = Ext.manifest.mashup || {},
                    options = body.mashupConfig,
                    i, script;
                if (requiredScripts) {
                    delete body.requiredScripts;
                    hooks.onCreated = function() {
                        var me = this,
                            scripts = [],
                            args = Ext.Array.slice(arguments),
                            redirect = mashup.redirect || {};
                        requiredScripts = scripts.concat(requiredScripts);
                        options = options && mashup[options.key];
                        if (xtypes) {
                            for (i = 0; !options && i < xtypes.length; ++i) {
                                options = mashup[xtypes[i]];
                            }
                        }
                        for (i = 0; i < requiredScripts.length; i++) {
                            script = requiredScripts[i];
                            if (redirect[script] === false) {
                                
                                continue;
                            }
                            script = redirect[script] || script;
                            if (script.indexOf('{') > -1) {
                                if (options) {
                                    script = new Ext.Template(script).apply(options);
                                } else {
                                    Ext.log.error('Missing mashup options for ' + body.$className + ' script "' + script + '"');
                                }
                            }
                            scripts.push(script);
                        }
                        if (!scripts.length) {
                            hooks.onCreated = onCreated;
                            hooks.onCreated.call(me, args);
                            return;
                        }
                        Ext.Loader.loadScripts({
                            url: scripts,
                            cache: true,
                            // no cache busting
                            onError: function(opts, error) {
                                targetClass.scriptError = targetClass.prototype.scriptError = error;
                                hooks.onCreated = onCreated;
                                hooks.onCreated.call(me, args);
                            },
                            onLoad: function() {
                                hooks.onCreated = onCreated;
                                hooks.onCreated.call(me, args);
                            }
                        });
                    };
                }
            }
        },
        onClassMixedIn: function(targetClass) {
            Mashup.process(targetClass);
        }
    };
});

/**
 * Tracks what records are currently selected in a databound widget. This class is mixed in to
 * {@link Ext.view.View dataview} and all subclasses.
 * @private
 */
Ext.define('Ext.mixin.Selectable', {
    extend: 'Ext.Mixin',
    mixinConfig: {
        id: 'selectable',
        after: {
            updateStore: 'updateStore'
        }
    },
    /**
     * @event selectionchange
     * Fires when a selection changes.
     * @param {Ext.mixin.Selectable} this
     * @param {Ext.data.Model[]} records The records whose selection has changed.
     */
    config: {
        /**
         * @cfg {Boolean} disableSelection
         * Set to `true` to disable selection.
         * This configuration will lock the selection model that the DataView uses.
         * @accessor
         */
        disableSelection: null,
        /**
         * @cfg {'SINGLE'/'SIMPLE'/'MULTI'} mode
         * Modes of selection.
         * @accessor
         */
        mode: 'SINGLE',
        /**
         * @cfg {Boolean} allowDeselect
         * Allow users to deselect a record in a DataView, List or Grid. Only applicable when
         * the Selectable's `mode` is `'SINGLE'`.
         * @accessor
         */
        allowDeselect: false,
        /**
         * @cfg {Ext.data.Model} lastSelected
         * @private
         * @accessor
         */
        lastSelected: null,
        /**
         * @cfg {Ext.data.Model} lastFocused
         * @private
         * @accessor
         */
        lastFocused: null,
        /**
         * @cfg {Boolean} deselectOnContainerClick
         * Set to `true` to deselect current selection when the container body is clicked.
         * @accessor
         */
        deselectOnContainerClick: true,
        /**
         * @cfg {Ext.util.Collection} selected
         * A {@link Ext.util.Collection} instance, or configuration object used to create
         * the collection of selected records.
         * @readonly
         */
        selected: true,
        /**
         * @cfg {Boolean} pruneRemoved
         * Remove records from the selection when they are removed from the store.
         *
         * **Important:** When using {@link Ext.toolbar.Paging paging} or a
         * {@link Ext.data.BufferedStore}, records which are cached in the Store's
         * {@link Ext.data.Store#property-data data collection} may be removed from the Store
         * when pages change, or when rows are scrolled out of view. For this reason `pruneRemoved`
         * should be set to `false` when using a buffered Store.
         *
         * Also, when previously pruned pages are returned to the cache, the records objects
         * in the page will be *new instances*, and will not match the instances in the selection
         * model's collection. For this reason, you MUST ensure that the Model definition's
         * {@link Ext.data.Model#idProperty idProperty} references a unique key because in this
         * situation, records in the Store have their **IDs** compared to records in the
         * SelectionModel in order to re-select a record which is scrolled back into view.
         */
        pruneRemoved: true,
        /**
         * @cfg {Ext.data.Model} selection
         * The selected record.
         */
        selection: null,
        /**
         * @cfg twoWayBindable
         * @inheritdoc Ext.mixin.Bindable#cfg-twoWayBindable
         */
        twoWayBindable: {
            selection: 1
        },
        /**
         * @cfg publishes
         * @inheritdoc Ext.mixin.Bindable#cfg-publishes
         */
        publishes: {
            selection: 1
        }
    },
    modes: {
        SINGLE: true,
        SIMPLE: true,
        MULTI: true
    },
    onNavigate: function(event) {},
    selectableEventHooks: {
        add: 'onSelectionStoreAdd',
        remove: 'onSelectionStoreRemove',
        update: 'onSelectionStoreUpdate',
        clear: {
            fn: 'onSelectionStoreClear',
            priority: 1000
        },
        load: 'refreshSelection',
        refresh: 'refreshSelection'
    },
    initSelectable: function() {
        this.publishState('selection', this.getSelection());
    },
    applySelected: function(selected) {
        if (!selected.isCollection) {
            selected = new Ext.util.Collection(selected);
        }
        // Add this Selectable as an observer immediately so that we are informed of any
        // mutations which occur in this event run.
        selected.addObserver(this);
        return selected;
    },
    /**
     * @private
     */
    applyMode: function(mode) {
        mode = mode ? mode.toUpperCase() : 'SINGLE';
        // set to mode specified unless it doesnt exist, in that case
        // use single.
        return this.modes[mode] ? mode : 'SINGLE';
    },
    /**
     * @private
     */
    updateStore: function(newStore, oldStore) {
        var me = this,
            bindEvents = Ext.apply({}, me.selectableEventHooks, {
                scope: me
            });
        if (oldStore && Ext.isObject(oldStore) && oldStore.isStore) {
            if (oldStore.autoDestroy) {
                oldStore.destroy();
            } else {
                oldStore.un(bindEvents);
            }
        }
        if (newStore) {
            newStore.on(bindEvents);
            me.refreshSelection();
        }
    },
    /**
     * Selects all records.
     * @param {Boolean} silent `true` to suppress all select events.
     */
    selectAll: function(silent) {
        var me = this,
            selections = me.getStore().getRange();
        me.select(selections, true, silent);
    },
    /**
     * Deselects all records.
     */
    deselectAll: function(supress) {
        var me = this;
        me.deselect(me.getSelected().getRange(), supress);
        me.setLastSelected(null);
        me.setLastFocused(null);
    },
    updateSelection: function(selection) {
        if (this.changingSelection) {
            return;
        }
        if (selection) {
            this.select(selection);
        } else {
            this.deselectAll();
        }
    },
    // Provides differentiation of logic between MULTI, SIMPLE and SINGLE
    // selection modes.
    selectWithEvent: function(record) {
        var me = this,
            isSelected = me.isSelected(record);
        switch (me.getMode()) {
            case 'MULTI':
            case 'SIMPLE':
                if (isSelected) {
                    me.deselect(record);
                } else {
                    me.select(record, true);
                };
                break;
            case 'SINGLE':
                if (me.getAllowDeselect() && isSelected) {
                    // if allowDeselect is on and this record isSelected, deselect it
                    me.deselect(record);
                } else {
                    // select the record and do NOT maintain existing selections
                    me.select(record, false);
                };
                break;
        }
    },
    /**
     * Selects a range of rows if the selection model
     * {@link Ext.mixin.Selectable#getDisableSelection} is not locked.
     * All rows in between `startRecord` and `endRecord` are also selected.
     * @param {Number} startRecord The index of the first row in the range.
     * @param {Number} endRecord The index of the last row in the range.
     * @param {Boolean} [keepExisting] `true` to retain existing selections.
     */
    selectRange: function(startRecord, endRecord, keepExisting) {
        var me = this,
            store = me.getStore(),
            records = [],
            tmp, i;
        if (me.getDisableSelection()) {
            return;
        }
        // swap values
        if (startRecord > endRecord) {
            tmp = endRecord;
            endRecord = startRecord;
            startRecord = tmp;
        }
        for (i = startRecord; i <= endRecord; i++) {
            records.push(store.getAt(i));
        }
        this.doMultiSelect(records, keepExisting);
    },
    /**
     * Adds the given records to the currently selected set.
     * @param {Ext.data.Model/Array/Number} records The records to select.
     * @param {Boolean} keepExisting If `true`, the existing selection will be added to
     * (if not, the old selection is replaced).
     * @param {Boolean} suppressEvent If `true`, the `select` event will not be fired.
     */
    select: function(records, keepExisting, suppressEvent) {
        var me = this,
            record;
        if (me.getDisableSelection()) {
            return;
        }
        if (typeof records === "number") {
            records = [
                me.getStore().getAt(records)
            ];
        }
        if (!records) {
            return;
        }
        if (me.getMode() === "SINGLE" && records) {
            record = records.length ? records[0] : records;
            me.doSingleSelect(record, suppressEvent);
        } else {
            me.doMultiSelect(records, keepExisting, suppressEvent);
        }
    },
    /**
     * Selects a single record.
     * @private
     */
    doSingleSelect: function(record, suppressEvent) {
        this.doMultiSelect([
            record
        ], false, suppressEvent);
    },
    /**
     * Selects a set of multiple records.
     * @private
     */
    doMultiSelect: function(records, keepExisting, suppressEvent) {
        if (records === null || this.getDisableSelection()) {
            return;
        }
        records = !Ext.isArray(records) ? [
            records
        ] : records;
        // eslint-disable-next-line vars-on-top
        var me = this,
            selected = me.getSelected(),
            selectionCount = selected.getCount(),
            store = me.getStore(),
            toRemove = [],
            record, i, len;
        if (!keepExisting && selectionCount) {
            toRemove = selected.getRange();
        }
        // Ensure they are all records
        for (i = 0 , len = records.length; i < len; i++) {
            record = records[i];
            if (typeof record === 'number') {
                records[i] = store.getAt(record);
            }
        }
        // Potentially remove from, then add the selected Collection.
        // We will react to successful removal as an observer.
        // We will need to know at that time whether the event is suppressed.
        selected.suppressEvent = suppressEvent;
        selected.splice(selectionCount, toRemove, records);
        selected.suppressEvent = false;
    },
    /**
     * Deselects the given record(s). If many records are currently selected, it will only deselect
     * those you pass in.
     * @param {Number/Array/Ext.data.Model} records The record(s) to deselect. Can also be a number
     * to reference by index.
     * @param {Boolean} suppressEvent If `true` the `deselect` event will not be fired.
     */
    deselect: function(records, suppressEvent) {
        var me = this,
            selected, store, record, i, len;
        if (me.getDisableSelection()) {
            return;
        }
        records = Ext.isArray(records) ? records : [
            records
        ];
        selected = me.getSelected();
        store = me.getStore();
        // Ensure they are all records
        for (i = 0 , len = records.length; i < len; i++) {
            record = records[i];
            if (typeof record === 'number') {
                records[i] = store.getAt(record);
            }
        }
        // Remove the records from the selected Collection.
        // We will react to successful removal as an observer.
        // We will need to know at that time whether the event is suppressed.
        selected.suppressEvent = suppressEvent;
        selected.remove(records);
        selected.suppressEvent = false;
    },
    /**
     * @private
     * Respond to deselection. Call the onItemDeselect template method
     */
    onCollectionRemove: function(selectedCollection, chunk) {
        var me = this,
            lastSelected = me.getLastSelected(),
            records = chunk.items;
        // Keep lastSelected up to date
        if (lastSelected && !selectedCollection.contains(lastSelected)) {
            me.setLastSelected(selectedCollection.last());
        }
        me.onItemDeselect(records, selectedCollection.suppressEvent);
        if (!selectedCollection.suppressEvent) {
            me.fireSelectionChange(records);
        }
    },
    /**
     * @private
     * Respond to selection. Call the onItemSelect template method
     */
    onCollectionAdd: function(selectedCollection, adds) {
        var me = this,
            records = adds.items;
        // Keep lastSelected up to date
        me.setLastSelected(selectedCollection.last());
        me.onItemSelect(records, selectedCollection.suppressEvent);
        if (!selectedCollection.suppressEvent) {
            me.fireSelectionChange(records);
        }
    },
    // TODO: This is the job of a NavigationModel
    /**
     * Sets a record as the last focused record. This does NOT mean
     * that the record has been selected.
     * @param {Ext.data.Record} newRecord
     * @param {Ext.data.Record} oldRecord
     */
    updateLastFocused: function(newRecord, oldRecord) {
        this.onLastFocusChanged(oldRecord, newRecord);
    },
    fireSelectionChange: function(records) {
        var me = this;
        me.changingSelection = true;
        me.setSelection(me.getLastSelected() || null);
        me.changingSelection = false;
        me.fireAction('selectionchange', [
            me,
            records
        ], 'getSelections');
    },
    /**
     * Returns the currently selected records.
     * @return {Ext.data.Model[]} The selected records.
     */
    getSelections: function() {
        return this.getSelected().getRange();
    },
    /**
     * Returns `true` if the specified row is selected.
     * @param {Ext.data.Model/Number} record The record or index of the record to check.
     * @return {Boolean}
     */
    isSelected: function(record) {
        record = Ext.isNumber(record) ? this.getStore().getAt(record) : record;
        return this.getSelected().indexOf(record) !== -1;
    },
    /**
     * Returns `true` if there is a selected record.
     * @return {Boolean}
     */
    hasSelection: function() {
        return this.getSelected().getCount() > 0;
    },
    /**
     * @private
     */
    refreshSelection: function() {
        var me = this,
            selected = me.getSelected(),
            selections = selected.getRange(),
            selectionLength = selections.length,
            storeCollection = me.getStore().getData(),
            toDeselect = [],
            toReselect = [],
            i, rec, matchingSelection;
        // Build the toDeselect list
        if (me.getPruneRemoved()) {
            // Uncover the unfiltered selection if it's there.
            // We only want to prune from the selection records whhich are
            // *really* no longer in the store.
            storeCollection = storeCollection.getSource() || storeCollection;
            for (i = 0; i < selectionLength; i++) {
                rec = selections[i];
                matchingSelection = storeCollection.get(storeCollection.getKey(rec));
                if (matchingSelection) {
                    if (matchingSelection !== rec) {
                        toDeselect.push(rec);
                        toReselect.push(matchingSelection);
                    }
                } else {
                    toDeselect.push(rec);
                }
            }
        }
        // Update the selected Collection.
        // Records which are no longer present will be in the toDeselect list
        // Records which have the same id which have returned will be in the toSelect list.
        // We will react to successful removal as an observer.
        // We will need to know at that time whether the event is suppressed.
        selected.suppressEvent = true;
        selected.splice(selected.getCount(), toDeselect, toReselect);
        selected.suppressEvent = false;
    },
    // prune records from the SelectionModel if
    // they were selected at the time they were
    // removed.
    onSelectionStoreRemove: function(store, records) {
        var me = this,
            selected = me.getSelected(),
            ln = records.length,
            removed, record, i;
        if (me.getDisableSelection()) {
            return;
        }
        for (i = 0; i < ln; i++) {
            record = records[i];
            if (selected.remove(record)) {
                if (me.getLastSelected() == record) {
                    // eslint-disable-line eqeqeq
                    me.setLastSelected(null);
                }
                if (me.getLastFocused() == record) {
                    // eslint-disable-line eqeqeq
                    me.setLastFocused(null);
                }
                removed = removed || [];
                removed.push(record);
            }
        }
        if (removed) {
            me.fireSelectionChange([
                removed
            ]);
        }
    },
    onSelectionStoreClear: function(store) {
        var records = store.getData().items;
        this.onSelectionStoreRemove(store, records);
    },
    /**
     * Returns the number of selections.
     * @return {Number}
     */
    getSelectionCount: function() {
        return this.getSelected().getCount();
    },
    onSelectionStoreAdd: Ext.emptyFn,
    onSelectionStoreUpdate: Ext.emptyFn,
    onItemSelect: Ext.emptyFn,
    onItemDeselect: Ext.emptyFn,
    onLastFocusChanged: Ext.emptyFn,
    onEditorKey: Ext.emptyFn
}, function() {});
/**
     * Selects a record instance by record instance or index.
     * @member Ext.mixin.Selectable
     * @method doSelect
     * @param {Ext.data.Model/Number} records An array of records or an index.
     * @param {Boolean} keepExisting
     * @param {Boolean} suppressEvent Set to `false` to not fire a select event.
     * @deprecated 2.0.0 Please use {@link #select} instead.
     */
/**
     * Deselects a record instance by record instance or index.
     * @member Ext.mixin.Selectable
     * @method doDeselect
     * @param {Ext.data.Model/Number} records An array of records or an index.
     * @param {Boolean} suppressEvent Set to `false` to not fire a deselect event.
     * @deprecated 2.0.0 Please use {@link #deselect} instead.
     */
/**
     * Returns the selection mode currently used by this Selectable.
     * @member Ext.mixin.Selectable
     * @method getSelectionMode
     * @return {String} The current mode.
     * @deprecated 2.0.0 Please use {@link #getMode} instead.
     */
/**
     * Returns the array of previously selected items.
     * @member Ext.mixin.Selectable
     * @method getLastSelected
     * @return {Array} The previous selection.
     * @deprecated 2.0.0 This method is deprecated.
     */
/**
     * Returns `true` if the Selectable is currently locked.
     * @member Ext.mixin.Selectable
     * @method isLocked
     * @return {Boolean} True if currently locked
     * @deprecated 2.0.0 Please use {@link #getDisableSelection} instead.
     */
/**
     * This was an internal function accidentally exposed in 1.x and now deprecated. Calling it
     * has no effect
     * @member Ext.mixin.Selectable
     * @method setLastFocused
     * @deprecated 2.0.0 This method is deprecated.
     */
/**
     * Deselects any currently selected records and clears all stored selections.
     * @member Ext.mixin.Selectable
     * @method clearSelections
     * @deprecated 2.0.0 Please use {@link #deselectAll} instead.
     */
/**
     * Returns the number of selections.
     * @member Ext.mixin.Selectable
     * @method getCount
     * @return {Number}
     * @deprecated 2.0.0 Please use {@link #getSelectionCount} instead.
     */
/**
     * @cfg locked
     * @inheritdoc Ext.mixin.Selectable#cfg-disableSelection
     * @deprecated 2.0.0 Please use {@link #disableSelection} instead.
     */

/**
 * This mixin is used to track and listen to the `store` of its `owner` component. The
 * component must support a `storechange` event (as do grids and dataviews) as well as
 * a `getStore` method.
 * @since 6.5.0
 */
Ext.define('Ext.mixin.StoreWatcher', {
    mixinId: 'storewatcher',
    config: {
        dataSource: null,
        /**
         * @cfg {Ext.Base} owner
         */
        owner: null,
        /**
         * @cfg {Object} ownerListeners
         * The events and associated handlers to which to listen on the `owner`.
         */
        ownerListeners: {
            destroyable: true,
            storechange: 'onOwnerStoreChange'
        },
        /**
         * @cfg {Object} sourceListeners
         * The events and associated handlers to which to listen on the `source` of the
         * connected `store`. That is, these listeners are attached to the unfiltered
         * collection. When `remoteFilter` is `true` there is no unfiltered collection so
         * these listeners are attached to the only collection that exists (which is
         * filtered by the server).
         */
        sourceListeners: null,
        store: null,
        /**
         * @cfg {Object} storeListeners
         * The events and associated handlers to which to listen on the `store` of the
         * `owner`.
         */
        storeListeners: null
    },
    afterClassMixedIn: function(targetClass) {
        var configurator = this.getConfigurator(),
            prototype = targetClass.prototype,
            config = {},
            prop;
        for (prop in configurator.configs) {
            // For each of our configs, see if the class declared them as well. If so
            // we need to merge their values on top of ours and remove them from the
            // class prototype.
            if (prototype.hasOwnProperty(prop)) {
                config[prop] = prototype[prop];
                delete prototype[prop];
            }
        }
        targetClass.addConfig(config);
    },
    onOwnerStoreChange: function(comp, store) {
        this.setStore(store);
    },
    //---------------------------
    // dataSource
    updateDataSource: function(source) {
        this.syncListeners(source, '$sourceListeners', 'getSourceListeners');
    },
    // owner
    updateOwner: function(owner) {
        var me = this,
            ownerProperty = me.ownerProperty;
        if (ownerProperty) {
            me[ownerProperty] = owner;
        }
        me.syncListeners(owner, '$ownerListeners', 'getOwnerListeners');
        me.setStore(owner ? owner.getStore() : null);
    },
    // store
    applyStore: function(store) {
        return (store && !store.isEmptyStore) ? store : null;
    },
    updateStore: function(store) {
        this.syncListeners(store, '$storeListeners', 'getStoreListeners');
        this.syncDataSource();
    },
    privates: {
        syncDataSource: function() {
            var store = this.getStore(),
                source;
            if (!store) {
                source = null;
            } else if (store.getDataSource) {
                source = store.getDataSource();
            } else {
                source = store.getData();
            }
            this.setDataSource(source);
        },
        syncListeners: function(instance, token, listeners) {
            var me = this,
                old = me[token];
            if (old) {
                me[token] = null;
                old.destroy();
            }
            if (instance) {
                listeners = me[listeners]();
                listeners = Ext.applyIf({
                    destroyable: true,
                    scope: me
                }, listeners);
                me[token] = instance.on(listeners);
            }
        }
    }
});

/**
 * @private
 */
Ext.define('Ext.mixin.StyleCacher', {
    extend: 'Ext.Mixin',
    mixinConfig: {
        id: 'stylecacher'
    },
    getCachedStyle: function(el, style) {
        var cache = this.$styleCache;
        if (!cache) {
            cache = this.$styleCache = {};
        }
        if (!(style in cache)) {
            cache[style] = Ext.fly(el).getStyle(style);
        }
        return cache[style];
    }
});

/**
 * A Traversable mixin.
 * @private
 */
Ext.define('Ext.mixin.Traversable', {
    extend: 'Ext.Mixin',
    mixinConfig: {
        id: 'traversable'
    },
    setParent: function(parent) {
        this.parent = parent;
        return this;
    },
    /**
     * Returns `true` if this component has a parent.
     * @return {Boolean} `true` if this component has a parent.
     */
    hasParent: function() {
        return Boolean(this.getParent());
    },
    /**
     * @template
     * Selector processing function for use by {@link #nextSibling},{@link #previousibling},
     * {@link #nextNode},and {@link #previousNode}, to filter candidate nodes.
     *
     * The base implementation returns true. Classes which mix in `Traversable` may implement
     * their own implementations. `@link{Ext.Widget}` does this to implement
     * {@link Ext.ComponentQuery} based filterability.
     * @returns {boolean}
     */
    is: function() {
        return true;
    },
    /**
     * Returns the parent of this component, if it has one.
     * @return {Ext.Component} The parent of this component.
     */
    getParent: function() {
        return this.parent || this.$initParent;
    },
    getAncestors: function() {
        var ancestors = [],
            parent = this.getParent();
        while (parent) {
            ancestors.push(parent);
            parent = parent.getParent();
        }
        return ancestors;
    },
    getAncestorIds: function() {
        var ancestorIds = [],
            parent = this.getParent();
        while (parent) {
            ancestorIds.push(parent.getId());
            parent = parent.getParent();
        }
        return ancestorIds;
    },
    /**
     * Returns the previous node in the Component tree in tree traversal order.
     *
     * Note that this is not limited to siblings, and if invoked upon a node with no matching
     * siblings, will walk the tree in reverse order to attempt to find a match. Contrast with
     * {@link #previousSibling}.
     * @param {String} [selector] A {@link Ext.ComponentQuery ComponentQuery} selector to filter
     * the preceding nodes.
     * @param includeSelf (private)
     * @return {Ext.Component} The previous node (or the previous node which matches the selector).
     * Returns `null` if there is no matching node.
     */
    previousNode: function(selector, includeSelf) {
        var node = this,
            parent = node.getRefOwner(),
            result, it, i, sibling;
        // If asked to include self, test me
        if (includeSelf && node.is(selector)) {
            return node;
        }
        if (parent) {
            for (it = parent.items.items , i = Ext.Array.indexOf(it, node) - 1; i > -1; i--) {
                sibling = it[i];
                if (sibling.query) {
                    result = sibling.query(selector);
                    result = result[result.length - 1];
                    if (result) {
                        return result;
                    }
                }
                if (!selector || sibling.is(selector)) {
                    return sibling;
                }
            }
            return parent.previousNode(selector, true);
        }
        return null;
    },
    /**
     * Returns the previous sibling of this Component.
     *
     * Optionally selects the previous sibling which matches the passed
     * {@link Ext.ComponentQuery ComponentQuery} selector.
     *
     * May also be referred to as **`prev()`**
     *
     * Note that this is limited to siblings, and if no siblings of the item match, `null`
     * is returned. Contrast with {@link #previousNode}
     * @param {String} [selector] A {@link Ext.ComponentQuery ComponentQuery} selector to filter
     * the preceding items.
     * @return {Ext.Component} The previous sibling (or the previous sibling which matches
     * the selector). Returns `null` if there is no matching sibling.
     */
    previousSibling: function(selector) {
        var parent = this.getRefOwner(),
            it, idx, sibling;
        if (parent) {
            it = parent.items;
            idx = it.indexOf(this);
            if (idx !== -1) {
                if (selector) {
                    for (--idx; idx >= 0; idx--) {
                        if ((sibling = it.getAt(idx)).is(selector)) {
                            return sibling;
                        }
                    }
                } else {
                    if (idx) {
                        return it.getAt(--idx);
                    }
                }
            }
        }
        return null;
    },
    /**
     * Returns the next node in the Component tree in tree traversal order.
     *
     * Note that this is not limited to siblings, and if invoked upon a node with no matching
     * siblings, will walk the tree to attempt to find a match. Contrast with {@link #nextSibling}.
     * @param {String} [selector] A {@link Ext.ComponentQuery ComponentQuery} selector to filter
     * the following nodes.
     * @param includeSelf (private)
     * @return {Ext.Component} The next node (or the next node which matches the selector).
     * Returns `null` if there is no matching node.
     */
    nextNode: function(selector, includeSelf) {
        var node = this,
            parent = node.getRefOwner(),
            result, it, len, i, sibling;
        // If asked to include self, test me
        if (includeSelf && node.is(selector)) {
            return node;
        }
        if (parent) {
            // eslint-disable-next-line max-len
            for (it = parent.items.items , i = Ext.Array.indexOf(it, node) + 1 , len = it.length; i < len; i++) {
                sibling = it[i];
                if (!selector || sibling.is(selector)) {
                    return sibling;
                }
                if (sibling.down) {
                    result = sibling.down(selector);
                    if (result) {
                        return result;
                    }
                }
            }
            return parent.nextNode(selector);
        }
        return null;
    },
    /**
     * Returns the next sibling of this Component.
     *
     * Optionally selects the next sibling which matches the passed
     * {@link Ext.ComponentQuery ComponentQuery} selector.
     *
     * May also be referred to as **`next()`**
     *
     * Note that this is limited to siblings, and if no siblings of the item match, `null`
     * is returned. Contrast with {@link #nextNode}
     * @param {String} [selector] A {@link Ext.ComponentQuery ComponentQuery} selector to filter
     * the following items.
     * @return {Ext.Component} The next sibling (or the next sibling which matches the selector).
     * Returns `null` if there is no matching sibling.
     */
    nextSibling: function(selector) {
        var parent = this.getRefOwner(),
            it, last, idx, sibling;
        if (parent) {
            it = parent.items;
            idx = it.indexOf(this) + 1;
            if (idx) {
                if (selector) {
                    for (last = it.getCount(); idx < last; idx++) {
                        if ((sibling = it.getAt(idx)).is(selector)) {
                            return sibling;
                        }
                    }
                } else {
                    if (idx < it.getCount()) {
                        return it.getAt(idx);
                    }
                }
            }
        }
        return null;
    }
});

/**
 * @private
 */
Ext.define('Ext.perf.Accumulator', function() {
    var currentFrame = null,
        khrome = Ext.global['chrome'],
        // eslint-disable-line dot-notation
        formatTpl,
        // lazy init on first request for timestamp (avoids infobar in IE until needed)
        // Also avoids kicking off Chrome's microsecond timer until first needed
        getTimestamp = function() {
            var interval, toolbox;
            getTimestamp = Ext.now;
            // If Chrome is started with the --enable-benchmarking switch
            if (Ext.isChrome && khrome && khrome.Interval) {
                interval = new khrome.Interval();
                interval.start();
                getTimestamp = function() {
                    return interval.microseconds() / 1000;
                };
            } else if (window.ActiveXObject) {
                try {
                    // the above technique is not very accurate for small intervals...
                    /* eslint-disable-next-line no-undef */
                    toolbox = new ActiveXObject('SenchaToolbox.Toolbox');
                    Ext.senchaToolbox = toolbox;
                    // export for other uses
                    getTimestamp = function() {
                        return toolbox.milliseconds;
                    };
                } catch (e) {}
            }
            // ignore
            Ext.perf.getTimestamp = Ext.perf.Accumulator.getTimestamp = getTimestamp;
            return getTimestamp();
        };
    function adjustSet(set, time) {
        set.sum += time;
        set.min = Math.min(set.min, time);
        set.max = Math.max(set.max, time);
    }
    function leaveFrame(time) {
        var totalTime = time ? time : (getTimestamp() - this.time),
            // do this first
            me = this,
            // me = frame
            accum = me.accum;
        ++accum.count;
        if (!--accum.depth) {
            adjustSet(accum.total, totalTime);
        }
        adjustSet(accum.pure, totalTime - me.childTime);
        currentFrame = me.parent;
        if (currentFrame) {
            ++currentFrame.accum.childCount;
            currentFrame.childTime += totalTime;
        }
    }
    function makeSet() {
        return {
            min: Number.MAX_VALUE,
            max: 0,
            sum: 0
        };
    }
    function makeTap(me, fn) {
        return function() {
            var frame = me.enter(),
                ret = fn.apply(this, arguments);
            frame.leave();
            return ret;
        };
    }
    function setToJSON(count, childCount, calibration, set) {
        var data = {
                avg: 0,
                min: set.min,
                max: set.max,
                sum: 0
            };
        if (count) {
            calibration = calibration || 0;
            data.sum = set.sum - childCount * calibration;
            data.avg = data.sum / count;
        }
        // min and max cannot be easily corrected since we don't know the number of
        // child calls for them.
        return data;
    }
    return {
        requires: [
            'Ext.XTemplate',
            'Ext.ClassManager'
        ],
        constructor: function(name) {
            var me = this;
            me.count = me.childCount = me.depth = me.maxDepth = 0;
            me.pure = makeSet();
            me.total = makeSet();
            me.name = name;
        },
        statics: {
            getTimestamp: getTimestamp
        },
        format: function(calibration) {
            var data;
            if (!formatTpl) {
                /* eslint-disable indent */
                formatTpl = new Ext.XTemplate([
                    '{name} - {count} call(s)',
                    '<tpl if="count">',
                    '<tpl if="childCount">',
                    ' ({childCount} children)',
                    '</tpl>',
                    '<tpl if="depth - 1">',
                    ' ({depth} deep)',
                    '</tpl>',
                    '<tpl for="times">',
                    ', {type}: {[this.time(values.sum)]} msec (',
                    // 'min={[this.time(values.min)]}, ',
                    'avg={[this.time(values.sum / parent.count)]}',
                    // ', max={[this.time(values.max)]}',
                    ')',
                    '</tpl>',
                    '</tpl>'
                ].join(''), {
                    time: function(t) {
                        return Math.round(t * 100) / 100;
                    }
                });
            }
            /* eslint-enable indent */
            data = this.getData(calibration);
            data.name = this.name;
            data.pure.type = 'Pure';
            data.total.type = 'Total';
            data.times = [
                data.pure,
                data.total
            ];
            return formatTpl.apply(data);
        },
        getData: function(calibration) {
            var me = this;
            return {
                count: me.count,
                childCount: me.childCount,
                depth: me.maxDepth,
                pure: setToJSON(me.count, me.childCount, calibration, me.pure),
                total: setToJSON(me.count, me.childCount, calibration, me.total)
            };
        },
        enter: function() {
            var me = this,
                frame = {
                    accum: me,
                    leave: leaveFrame,
                    childTime: 0,
                    parent: currentFrame
                };
            ++me.depth;
            if (me.maxDepth < me.depth) {
                me.maxDepth = me.depth;
            }
            currentFrame = frame;
            frame.time = getTimestamp();
            // do this last
            return frame;
        },
        monitor: function(fn, scope, args) {
            var frame = this.enter();
            if (args) {
                fn.apply(scope, args);
            } else {
                fn.call(scope);
            }
            frame.leave();
        },
        report: function() {
            Ext.log(this.format());
        },
        tap: function(className, methodName) {
            var me = this,
                methods = typeof methodName === 'string' ? [
                    methodName
                ] : methodName,
                klass, statik, i, parts, length, name, src, tapFunc;
            tapFunc = function() {
                if (typeof className === 'string') {
                    klass = Ext.global;
                    parts = className.split('.');
                    for (i = 0 , length = parts.length; i < length; ++i) {
                        klass = klass[parts[i]];
                    }
                } else {
                    klass = className;
                }
                for (i = 0 , length = methods.length; i < length; ++i) {
                    name = methods[i];
                    statik = name.charAt(0) === '!';
                    if (statik) {
                        name = name.substring(1);
                    } else {
                        statik = !(name in klass.prototype);
                    }
                    src = statik ? klass : klass.prototype;
                    src[name] = makeTap(me, src[name]);
                }
            };
            Ext.ClassManager.onCreated(tapFunc, me, className);
            return me;
        }
    };
}, /* eslint-disable indent */
function() {
    Ext.perf.getTimestamp = this.getTimestamp;
});

/**
 * @singleton
 * @private
 */
Ext.define('Ext.perf.Monitor', {
    singleton: true,
    alternateClassName: 'Ext.Perf',
    requires: [
        'Ext.perf.Accumulator'
    ],
    constructor: function() {
        this.accumulators = [];
        this.accumulatorsByName = {};
    },
    calibrate: function() {
        var accum = new Ext.perf.Accumulator('$'),
            total = accum.total,
            getTimestamp = Ext.perf.Accumulator.getTimestamp,
            count = 0,
            frame, endTime, startTime;
        startTime = getTimestamp();
        do {
            frame = accum.enter();
            frame.leave();
            ++count;
        } while (total.sum < 100);
        endTime = getTimestamp();
        return (endTime - startTime) / count;
    },
    get: function(name) {
        var me = this,
            accum = me.accumulatorsByName[name];
        if (!accum) {
            me.accumulatorsByName[name] = accum = new Ext.perf.Accumulator(name);
            me.accumulators.push(accum);
        }
        return accum;
    },
    enter: function(name) {
        return this.get(name).enter();
    },
    monitor: function(name, fn, scope) {
        this.get(name).monitor(fn, scope);
    },
    report: function() {
        var me = this,
            accumulators = me.accumulators,
            calibration = me.calibrate();
        accumulators.sort(function(a, b) {
            return (a.name < b.name) ? -1 : ((b.name < a.name) ? 1 : 0);
        });
        me.updateGC();
        Ext.log('Calibration: ' + Math.round(calibration * 100) / 100 + ' msec/sample');
        Ext.each(accumulators, function(accum) {
            Ext.log(accum.format(calibration));
        });
    },
    getData: function(all) {
        var ret = {},
            accumulators = this.accumulators;
        Ext.each(accumulators, function(accum) {
            if (all || accum.count) {
                ret[accum.name] = accum.getData();
            }
        });
        return ret;
    },
    reset: function() {
        Ext.each(this.accumulators, function(accum) {
            var me = accum;
            me.count = me.childCount = me.depth = me.maxDepth = 0;
            me.pure = {
                min: Number.MAX_VALUE,
                max: 0,
                sum: 0
            };
            me.total = {
                min: Number.MAX_VALUE,
                max: 0,
                sum: 0
            };
        });
    },
    updateGC: function() {
        var accumGC = this.accumulatorsByName.GC,
            toolbox = Ext.senchaToolbox,
            bucket;
        if (accumGC) {
            accumGC.count = toolbox.garbageCollectionCounter || 0;
            if (accumGC.count) {
                bucket = accumGC.pure;
                accumGC.total.sum = bucket.sum = toolbox.garbageCollectionMilliseconds;
                bucket.min = bucket.max = bucket.sum / accumGC.count;
                bucket = accumGC.total;
                bucket.min = bucket.max = bucket.sum / accumGC.count;
            }
        }
    },
    watchGC: function() {
        var toolbox = Ext.senchaToolbox;
        Ext.perf.getTimestamp();
        // initializes SenchaToolbox (if available)
        if (toolbox) {
            this.get("GC");
            toolbox.watchGarbageCollector(false);
        }
    },
    // no logging, just totals
    setup: function(config) {
        var key, prop, accum, className, methods;
        if (!config) {
            config = {
                /* insertHtml: {
                    'Ext.dom.Helper': 'insertHtml'
                }, */
                /* xtplCompile: {
                    'Ext.XTemplateCompiler': 'compile'
                }, */
                //                doInsert: {
                //                    'Ext.Template': 'doInsert'
                //                },
                //                applyOut: {
                //                    'Ext.XTemplate': 'applyOut'
                //                },
                render: {
                    'Ext.Component': 'render'
                },
                //                fnishRender: {
                //                    'Ext.Component': 'finishRender'
                //                },
                //                renderSelectors: {
                //                    'Ext.Component': 'applyRenderSelectors'
                //                },
                //                compAddCls: {
                //                    'Ext.Component': 'addCls'
                //                },
                //                compRemoveCls: {
                //                    'Ext.Component': 'removeCls'
                //                },
                //                getStyle: {
                //                    'Ext.core.Element': 'getStyle'
                //                },
                //                setStyle: {
                //                    'Ext.core.Element': 'setStyle'
                //                },
                //                addCls: {
                //                    'Ext.core.Element': 'addCls'
                //                },
                //                removeCls: {
                //                    'Ext.core.Element': 'removeCls'
                //                },
                //                measure: {
                //                    'Ext.layout.component.Component': 'measureAutoDimensions'
                //                },
                //                moveItem: {
                //                    'Ext.layout.Layout': 'moveItem'
                //                },
                //                layoutFlush: {
                //                    'Ext.layout.Context': 'flush'
                //                },
                layout: {
                    'Ext.layout.Context': 'run'
                }
            };
        }
        this.currentConfig = config;
        for (key in config) {
            if (config.hasOwnProperty(key)) {
                prop = config[key];
                accum = Ext.Perf.get(key);
                for (className in prop) {
                    if (prop.hasOwnProperty(className)) {
                        methods = prop[className];
                        accum.tap(className, methods);
                    }
                }
            }
        }
        this.watchGC();
    },
    // This is a quick hack for now
    setupLog: function(config) {
        var className, cls, methods, method, override;
        for (className in config) {
            if (config.hasOwnProperty(className)) {
                cls = Ext.ClassManager.get(className);
                if (cls) {
                    methods = config[className];
                    override = {};
                    for (method in methods) {
                        override[method] = (function(methodName, idProp) {
                            return function() {
                                var before, diff, id, idHolder, ret;
                                before = +Date.now();
                                ret = this.callParent(arguments);
                                diff = +Date.now() - before;
                                if (window.console && diff > 0) {
                                    /* eslint-disable multiline-ternary, no-multi-spaces, indent */
                                    idHolder = idProp === 'this' ? this : typeof idProp === 'string' ? this[idProp] : typeof idProp === 'number' ? arguments[idProp] : null;
                                    /* eslint-enable */
                                    if (idHolder) {
                                        id = idHolder.id;
                                    }
                                    if (id != null) {
                                        console.log(methodName + ' for ' + id + ': ' + diff + 'ms');
                                    } else {
                                        console.log(methodName + ' for unknown: ' + diff + 'ms');
                                    }
                                    if (console.trace) {
                                        console.trace();
                                    }
                                }
                                return ret;
                            };
                        })(method, methods[method]);
                    }
                    Ext.override(cls, override);
                }
            }
        }
    }
});

/**
 * This base class manages clipboard data transfer for a component. As an abstract class,
 * applications use derived classes such as `{@link Ext.grid.plugin.Clipboard}` instead
 * and seldom use this class directly.
 *
 * ## Operation
 *
 * Components that interact with the clipboard do so in two directions: copy and paste.
 * When copying to the clipboard, a component will often provide multiple data formats.
 * On paste, the consumer of the data can then decide what format it prefers and ignore
 * the others.
 *
 * ### Copy (and Cut)
 *
 * There are two storage locations provided for holding copied data:
 *
 *  * The system clipboard, used to exchange data with other applications running
 *    outside the browser.
 *  * A memory space in the browser page that can hold data for use only by other
 *    components on the page. This allows for richer formats to be transferred.
 *
 * A component can copy (or cut) data in multiple formats as controlled by the
 * `{@link #cfg-memory}` and `{@link #cfg-system}` configs.
 *
 * ### Paste
 *
 * While there may be many formats available, when a component is ready to paste, only
 * one format can ultimately be used. This is specified by the `{@link #cfg-source}`
 * config.
 *
 * ## Browser Limitations
 *
 * At the current time, browsers have only a limited ability to interact with the system
 * clipboard. The only reliable, cross-browser, plugin-in-free technique for doing so is
 * to use invisible elements and focus tricks **during** the processing of clipboard key
 * presses like CTRL+C (on Windows/Linux) or CMD+C (on Mac).
 *
 * @protected
 * @since 5.1.0
 */
Ext.define('Ext.plugin.AbstractClipboard', {
    extend: 'Ext.plugin.Abstract',
    requires: [
        'Ext.util.KeyMap'
    ],
    cachedConfig: {
        /**
         * @cfg {Object} formats
         * This object is keyed by the names of the data formats supported by this plugin.
         * The property values of this object are objects with `get` and `put` properties
         * that name the methods for getting data from (copy) and putting to into (paste)
         * the associated component.
         *
         * For example:
         *
         *      formats: {
         *          html: {
         *              get: 'getHtmlData',
         *              put: 'putHtmlData'
         *          }
         *      }
         *
         * This declares support for the "html" data format and indicates that the
         * `getHtmlData` method should be called to copy HTML data from the component,
         * while the `putHtmlData` should be called to paste HTML data into the component.
         *
         * By default, all derived classes must support a "text" format:
         *
         *      formats: {
         *          text: {
         *              get: 'getTextData',
         *              put: 'putTextData'
         *          }
         *      }
         *
         * To understand the method signatures required to implement a data format, see the
         * documentation for `{@link #getTextData}` and  `{@link #putTextData}`.
         *
         * The format name "system" is not allowed.
         *
         * @protected
         */
        formats: {
            text: {
                get: 'getTextData',
                put: 'putTextData'
            }
        }
    },
    config: {
        /**
         * @cfg {String/String[]} [memory]
         * The data format(s) to copy to the private, memory clipboard. By default, data
         * is not saved to the memory clipboard. Specify `true` to include all formats
         * of data, or a string to copy a single format, or an array of strings to copy
         * multiple formats.
         */
        memory: null,
        /**
         * @cfg {String/String[]} [source="system"]
         * The format or formats in order of preference when pasting data. This list can
         * be any of the valid formats, plus the name "system". When a paste occurs, this
         * config is consulted. The first format specified by this config that has data
         * available in the private memory space is used. If "system" is encountered in
         * the list, whatever data is available on the system clipboard is chosen. At
         * that point, no further source formats will be considered.
         */
        source: 'system',
        /**
         * @cfg {String} [system="text"]
         * The data format to set in the system clipboard. By default, the "text"
         * format is used. Based on the type of derived class, other formats may be
         * possible.
         */
        system: 'text',
        gridListeners: null
    },
    destroy: function() {
        var me = this,
            keyMap = me.keyMap,
            shared = me.shared;
        Ext.destroy(me.destroyListener);
        if (keyMap) {
            // If we have a keyMap then we have incremented the shared usage counter
            // and now need to remove ourselves.
            me.keyMap = Ext.destroy(keyMap);
            if (!--shared.counter) {
                shared.textArea = Ext.destroy(shared.textArea);
            }
        } else {
            // If we don't have a keyMap it is because we are waiting for the render
            // event and haven't connected to the shared context.
            me.renderListener = Ext.destroy(me.renderListener);
        }
        me.callParent();
    },
    init: function(comp) {
        var me = this,
            listeners = me.getGridListeners();
        if (comp.rendered) {
            me.finishInit(comp);
        } else if (listeners) {
            me.renderListener = comp.on(Ext.apply({
                scope: me,
                destroyable: true,
                single: true
            }, listeners));
        }
    },
    onCmpReady: function() {
        this.renderListener = null;
        this.finishInit(this.getCmp());
    },
    /**
     * Returns the element target to listen to copy/paste.
     *
     * @param {Ext.Component} comp The component this plugin is initialized on.
     * @return {Ext.dom.Element} The element target.
     */
    getTarget: function(comp) {
        return comp.el;
    },
    /**
     * This method returns the selected data in text format.
     * @method getTextData
     * @param {String} format The name of the format (i.e., "text").
     * @param {Boolean} erase Pass `true` to erase (cut) the data, `false` to just copy.
     * @return {String} The data in text format.
     */
    /**
     * This method pastes the given text data.
     * @method putTextData
     * @param {Object} data The data in the indicated `format`.
     * @param {String} format The name of the format (i.e., "text").
     */
    privates: {
        /**
         * @property {Object} shared
         * The shared state for all clipboard-enabled components.
         * @property {Number} shared.counter The number of clipboard-enabled components
         * currently using this object.
         * @property {Object} shared.data The clipboard data for intra-page copy/paste. The
         * properties of the object are keyed by format.
         * @property {Ext.dom.Element} shared.textArea The shared textarea used to polyfill the
         * lack of HTML5 clipboard API.
         * @private
         */
        shared: {
            counter: 0,
            data: null,
            textArea: null
        },
        applyMemory: function(value) {
            // Same as "source" config but that allows "system" as a format.
            value = this.applySource(value);
            if (value) {
                for (var i = value.length; i-- > 0; ) {
                    // eslint-disable-line vars-on-top
                    if (value[i] === 'system') {
                        Ext.raise('Invalid clipboard format "' + value[i] + '"');
                    }
                }
            }
            return value;
        },
        applySource: function(value) {
            // Make sure we have a non-empty String[] or null
            if (value) {
                if (Ext.isString(value)) {
                    value = [
                        value
                    ];
                } else if (value.length === 0) {
                    value = null;
                }
            }
            if (value) {
                var formats = this.getFormats(),
                    // eslint-disable-line vars-on-top
                    i;
                for (i = value.length; i-- > 0; ) {
                    if (value[i] !== 'system' && !formats[value[i]]) {
                        Ext.raise('Invalid clipboard format "' + value[i] + '"');
                    }
                }
            }
            return value || null;
        },
        applySystem: function(value) {
            var formats = this.getFormats();
            if (!formats[value]) {
                Ext.raise('Invalid clipboard format "' + value + '"');
            }
            return value;
        },
        doCutCopy: function(event, erase) {
            var me = this,
                formats = me.allFormats || me.syncFormats(),
                data = me.getData(erase, formats),
                memory = me.getMemory(),
                system = me.getSystem(),
                sys;
            if (me.validateAction(event) === false) {
                return;
            }
            me.shared.data = memory && data;
            if (system) {
                sys = data[system];
                if (formats[system] < 3) {
                    delete data[system];
                }
                me.setClipboardData(sys);
            }
        },
        doPaste: function(format, data) {
            var formats = this.getFormats();
            this[formats[format].put](data, format);
        },
        finishInit: function(comp) {
            var me = this;
            me.keyMap = new Ext.util.KeyMap({
                target: me.getTarget(comp),
                ignoreInputFields: true,
                binding: [
                    {
                        ctrl: true,
                        key: 'x',
                        fn: me.onCut,
                        scope: me
                    },
                    {
                        ctrl: true,
                        key: 'c',
                        fn: me.onCopy,
                        scope: me
                    },
                    {
                        ctrl: true,
                        key: 'v',
                        fn: me.onPaste,
                        scope: me
                    }
                ]
            });
            ++me.shared.counter;
            me.destroyListener = comp.on({
                destroyable: true,
                destroy: 'destroy',
                scope: me
            });
        },
        getData: function(erase, format) {
            var me = this,
                formats = me.getFormats(),
                data, i, name, names;
            if (Ext.isString(format)) {
                if (!formats[format]) {
                    Ext.raise('Invalid clipboard format "' + format + '"');
                }
                data = me[formats[format].get](format, erase);
            } else {
                data = {};
                names = [];
                if (format) {
                    for (name in format) {
                        if (!formats[name]) {
                            Ext.raise('Invalid clipboard format "' + name + '"');
                        }
                        names.push(name);
                    }
                } else {
                    names = Ext.Object.getAllKeys(formats);
                }
                for (i = names.length; i-- > 0; ) {
                    data[name] = me[formats[name].get](name, erase && !i);
                }
            }
            return data;
        },
        /**
         * @private
         * @return {Ext.dom.Element}
         */
        getHiddenTextArea: function() {
            var shared = this.shared,
                el;
            el = shared.textArea;
            if (!el) {
                el = shared.textArea = Ext.getBody().createChild({
                    tag: 'textarea',
                    tabIndex: -1,
                    // don't tab through this fellow
                    style: {
                        position: 'absolute',
                        top: '-1000px',
                        width: '1px',
                        height: '1px'
                    }
                });
                // We don't want this element to fire focus events ever
                el.suspendFocusEvents();
            }
            return el;
        },
        onCopy: function(keyCode, event) {
            this.doCutCopy(event, false);
        },
        onCut: function(keyCode, event) {
            this.doCutCopy(event, true);
        },
        onPaste: function(keyCode, event) {
            var me = this,
                sharedData = me.shared.data,
                source = me.getSource(),
                i, n, s;
            if (me.validateAction(event) === false) {
                return;
            }
            if (source) {
                for (i = 0 , n = source.length; i < n; ++i) {
                    s = source[i];
                    if (s === 'system') {
                        // get the format used by the system clipboard.
                        s = me.getSystem();
                        me.pasteClipboardData(s);
                        break;
                    } else if (sharedData && (s in sharedData)) {
                        me.doPaste(s, sharedData[s]);
                        break;
                    }
                }
            }
        },
        pasteClipboardData: function(format) {
            var me = this,
                clippy = window.clipboardData,
                area, focusEl;
            if (clippy && clippy.getData) {
                me.doPaste(format, clippy.getData("text"));
            } else {
                focusEl = Ext.Element.getActiveElement(true);
                area = me.getHiddenTextArea().dom;
                area.value = '';
                // We must not disturb application state by doing this focus
                if (focusEl) {
                    focusEl.suspendFocusEvents();
                }
                area.focus();
                // Between now and the deferred function, the CTRL+V hotkey will have
                // its default action processed which will paste the clipboard content
                // into the textarea.
                Ext.defer(function() {
                    // Focus back to the real destination
                    if (focusEl) {
                        focusEl.focus();
                        // Restore framework focus handling
                        focusEl.resumeFocusEvents();
                    }
                    me.doPaste(format, area.value);
                    area.value = '';
                }, 100, me);
            }
        },
        setClipboardData: function(data) {
            var me = this,
                clippy = window.clipboardData,
                area, focusEl;
            if (clippy && clippy.setData) {
                clippy.setData("text", data);
            } else {
                area = me.getHiddenTextArea().dom;
                focusEl = Ext.Element.getActiveElement(true);
                area.value = data;
                // We must not disturb application state by doing this focus
                if (focusEl) {
                    focusEl.suspendFocusEvents();
                }
                area.focus();
                area.select();
                // Between now and the deferred function, the CTRL+C/X hotkey will have
                // its default action processed which will update the clipboard from the
                // textarea.
                Ext.defer(function() {
                    area.value = '';
                    if (focusEl) {
                        focusEl.focus();
                        // Restore framework focus handling
                        focusEl.resumeFocusEvents();
                    }
                }, 50);
            }
        },
        syncFormats: function() {
            var me = this,
                map = {},
                memory = me.getMemory(),
                system = me.getSystem(),
                i, s;
            if (system) {
                map[system] = 1;
            }
            if (memory) {
                for (i = memory.length; i-- > 0; ) {
                    s = memory[i];
                    map[s] = map[s] ? 3 : 2;
                }
            }
            // 1: memory
            // 2: system
            // 3: both
            return me.allFormats = map;
        },
        // jshint ignore:line
        updateMemory: function() {
            this.allFormats = null;
        },
        updateSystem: function() {
            this.allFormats = null;
        },
        validateAction: Ext.privateFn
    }
});

/**
 * This plugin calls a callback whenever the mouse enters or leaves descendant
 * elements of its host component identified by a {@link Ext.plugin.MouseEnter#delegate delegate}
 * query selector string.
 *
 * This is useful for components which render arbitrary and transient child elements
 * such as DataViews and Charts. It allows notification of mousenter events from 
 * child nodes without having to add  listeners to each child element.
 */
Ext.define('Ext.plugin.MouseEnter', {
    extend: 'Ext.plugin.Abstract',
    alias: 'plugin.mouseenter',
    /**
     * @cfg {Ext.dom.Element/String} [element="el"] The element, or component element reference
     * name to which to add the mouse listener.
     */
    element: 'el',
    /**
     * @cfg {String} delegate A query selector string to identify descendant elements
     * which trigger a call to the handler.
     */
    /**
     * @cfg {String/Function} handler A callback to invoke when a the mouse enters a
     * descendant delegate.
     * @cfg {Ext.event.Event} handler.e The `mouseover` event which triggered the mouse enter.
     * @cfg {HTMLElement} handler.target The delegate element into which the mouse just entered.
     */
    /**
     * @cfg {String/Function} [leaveHandler] A callback to invoke when a the mouse leaves a
     * descendant delegate.
     * @cfg {Ext.event.Event} leaveHandler.e The `mouseover` event which triggered the mouse leave.
     * @cfg {HTMLElement} leaveHandler.target The delegate element which the mouse just left.
     */
    /**
     * @cfg {Object} [scope] The scope (`this` pointer) in which to execute the callback(s).
     */
    /**
     * @cfg {Number} [delay] The time in milliseconds to wait before processing the mouse event.
     * This can prevent unwanted processing when the user swipes the mouse rapidly across
     * the component.
     */
    init: function(component) {
        if (!this.delegate) {
            Ext.raise('mouseenter plugin must be configured with a delegate selector');
        }
        if (!this.handler) {
            Ext.raise('mouseenter plugin must be configured with handler callback');
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            listeners = {
                mouseover: 'onMouseEvent',
                scope: me,
                destroyable: true
            },
            element = me.element;
        // Need the mouseout listener if there's a delay, so that we get an event 
        // in which to cancel the mouseover handling.
        if (me.leaveHandler || me.delay) {
            listeners.mouseout = 'onMouseEvent';
        }
        // Element being a string means a referenced element name in the Component
        if (typeof element === 'string') {
            element = component[me.element];
        }
        // If the component has the element, add the listener.
        // Modern components always will have their elements.
        if (element) {
            me.mouseListener = Ext.get(element).on(listeners);
        } else // For classic, we have to wait until render.
        // destroyable: true does not work on named element listeners on a component
        // https://sencha.jira.com/browse/EXTJS-22866
        {
            component.on({
                render: function() {
                    me.mouseListener = component[me.element].on(listeners);
                },
                single: true
            });
        }
    },
    onMouseEvent: function(e) {
        var me = this,
            delegate = e.getTarget(me.delegate);
        // If we have changed delegates, fire (or schedule, if we are delaying) the handler
        if (delegate && delegate !== e.getRelatedTarget(me.delegate)) {
            if (me.delay) {
                Ext.undefer(me.mouseEventTimer);
                me.mouseEventTimer = Ext.defer(me.handleMouseEvent, me.delay, me, [
                    e,
                    delegate
                ]);
            } else {
                me.handleMouseEvent(e, delegate);
            }
        }
    },
    handleMouseEvent: function(e, delegate) {
        var me = this;
        if (e.type === 'mouseover') {
            Ext.callback(me.handler, null, [
                e,
                delegate
            ], 0, me.cmp, me.scope);
        } else if (me.leaveHandler) {
            Ext.callback(me.leaveHandler, null, [
                e,
                delegate
            ], 0, me.cmp, me.scope);
        }
    },
    destroy: function() {
        Ext.destroy(this.mouseListener);
        this.callParent();
    }
});

/**
 * @class Ext.sparkline.Shape
 * @private
 */
Ext.define('Ext.sparkline.Shape', {
    constructor: function(target, id, type, args) {
        var me = this;
        me.target = target;
        me.id = id;
        me.type = type;
        me.args = args;
    },
    append: function() {
        this.target.appendShape(this);
        return this;
    }
});

/**
 * @class Ext.sparkline.CanvasBase
 * @private
 */
Ext.define('Ext.sparkline.CanvasBase', {
    requires: [
        'Ext.sparkline.Shape'
    ],
    shapeCount: 0,
    _pxregex: /(\d+)(px)?\s*$/i,
    constructor: function(ownerSparkLine) {
        this.owner = ownerSparkLine;
        this.rtl = this.owner.getInherited().rtl;
    },
    setWidth: function(width) {
        this.pixelWidth = width;
    },
    setHeight: function(height) {
        this.pixelHeight = height;
    },
    drawLine: function(x1, y1, x2, y2, lineColor, lineWidth) {
        return this.drawShape([
            [
                x1,
                y1
            ],
            [
                x2,
                y2
            ]
        ], lineColor, lineWidth);
    },
    drawShape: function(path, lineColor, fillColor, lineWidth) {
        return this._genShape('Shape', [
            path,
            lineColor,
            fillColor,
            lineWidth
        ]);
    },
    drawCircle: function(x, y, radius, lineColor, fillColor, lineWidth) {
        return this._genShape('Circle', [
            x,
            y,
            radius,
            lineColor,
            fillColor,
            lineWidth
        ]);
    },
    drawPieSlice: function(x, y, radius, startAngle, endAngle, lineColor, fillColor) {
        return this._genShape('PieSlice', [
            x,
            y,
            radius,
            startAngle,
            endAngle,
            lineColor,
            fillColor
        ]);
    },
    drawRect: function(x, y, width, height, lineColor, fillColor) {
        return this._genShape('Rect', [
            x,
            y,
            width,
            height,
            lineColor,
            fillColor
        ]);
    },
    getElement: function() {
        return this.el;
    },
    /*
     * Return the most recently inserted shape id
     */
    getLastShapeId: function() {
        return this.lastShapeId;
    },
    /*
     * Clear and reset the canvas
     */
    reset: function() {
        Ext.raise('reset not implemented');
    },
    /*
     * Generate a shape object and id for later rendering
     */
    _genShape: function(shapetype, shapeargs) {
        var id = this.shapeCount++;
        shapeargs.unshift(id);
        return new Ext.sparkline.Shape(this, id, shapetype, shapeargs);
    },
    /*
     * Add a shape to the end of the render queue
     */
    appendShape: function(shape) {
        Ext.raise('appendShape not implemented');
    },
    /*
     * Replace one shape with another
     */
    replaceWithShape: function(shapeid, shape) {
        Ext.raise('replaceWithShape not implemented');
    },
    /*
     * Insert one shape after another in the render queue
     */
    insertAfterShape: function(shapeid, shape) {
        Ext.raise('insertAfterShape not implemented');
    },
    /*
     * Remove a shape from the queue
     */
    removeShapeId: function(shapeid) {
        Ext.raise('removeShapeId not implemented');
    },
    /*
     * Find a shape at the specified x/y co-ordinates
     */
    getShapeAt: function(x, y) {
        Ext.raise('getShapeAt not implemented');
    },
    /*
     * Render all queued shapes onto the canvas
     */
    render: function() {
        Ext.raise('render not implemented');
    }
});

/**
 * @private
 */
Ext.define('Ext.sparkline.CanvasCanvas', {
    extend: 'Ext.sparkline.CanvasBase',
    statics: {
        contextOverrides: (function() {
            var ratio = window.devicePixelRatio || 1;
            return {
                moveTo: function(x, y) {
                    // Convert to RTL
                    if (this.rtl) {
                        x = this.canvas.width - x - 1;
                    }
                    this.$moveTo(x * ratio, y * ratio);
                },
                lineTo: function(x, y) {
                    // Convert to RTL
                    if (this.rtl) {
                        x = this.canvas.width - x - 1;
                    }
                    this.$lineTo(x * ratio, y * ratio);
                },
                arc: function(x, y, radius, startAngle, endAngle, counterclockwise) {
                    // Convert to RTL
                    if (this.rtl) {
                        x = this.canvas.width - x - 1;
                    }
                    this.$arc(x * ratio, y * ratio, radius * ratio, startAngle, endAngle, counterclockwise);
                },
                clearRect: function(x, y, width, height) {
                    // Convert to RTL
                    if (this.rtl) {
                        x = this.canvas.width - x - width;
                    }
                    this.$clearRect(x * ratio, y * ratio, width * ratio, height * ratio);
                }
            };
        })()
    },
    setWidth: function(width) {
        this.callParent(arguments);
        this.owner.element.dom.width = width * (window.devicePixelRatio || 1);
    },
    setHeight: function(height) {
        this.callParent(arguments);
        this.owner.element.dom.height = height * (window.devicePixelRatio || 1);
    },
    onOwnerUpdate: function() {
        var me = this;
        me.el = me.owner.element;
        me.interact = !me.owner.initialConfig.disableInteraction;
        me.shapes = {};
        me.shapeseq = [];
        me.currentTargetShapeId = me.lastShapeId = null;
    },
    _getContext: function(lineColor, fillColor, lineWidth) {
        var context = this.context,
            overrides, name;
        if (!context) {
            this.context = context = this.el.dom.getContext('2d');
            overrides = Ext.sparkline.CanvasCanvas.contextOverrides;
            for (name in overrides) {
                context['$' + name] = context[name];
            }
            Ext.apply(context, overrides);
            context.rtl = this.rtl;
        }
        if (lineColor != null) {
            context.strokeStyle = lineColor;
        }
        context.lineWidth = lineWidth || 1;
        if (fillColor != null) {
            context.fillStyle = fillColor;
        }
        return context;
    },
    reset: function() {
        var context = this._getContext();
        context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
        this.shapes = {};
        this.shapeseq = [];
        this.currentTargetShapeId = this.lastShapeId = null;
    },
    _drawShape: function(shapeid, path, lineColor, fillColor, lineWidth) {
        var context = this._getContext(lineColor, fillColor, lineWidth),
            xIncr = this.rtl ? -0.5 : 0.5,
            i, plen;
        context.beginPath();
        context.moveTo(path[0][0] + xIncr, path[0][1] + 0.5);
        for (i = 1 , plen = path.length; i < plen; i++) {
            // the 0.5 offset gives us crisp pixel-width lines
            context.lineTo(path[i][0] + xIncr, path[i][1] + 0.5);
        }
        if (lineColor != null) {
            context.stroke();
        }
        if (fillColor != null) {
            context.fill();
        }
        if (this.targetX != null && this.targetY != null && context.isPointInPath(this.targetX, this.targetY)) {
            this.currentTargetShapeId = shapeid;
        }
    },
    _drawCircle: function(shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
        var context = this._getContext(lineColor, fillColor, lineWidth);
        context.beginPath();
        context.arc(x, y, radius, 0, 2 * Math.PI, false);
        if (this.targetX != null && this.targetY != null && context.isPointInPath(this.targetX, this.targetY)) {
            this.currentTargetShapeId = shapeid;
        }
        if (lineColor != null) {
            context.stroke();
        }
        if (fillColor != null) {
            context.fill();
        }
    },
    _drawPieSlice: function(shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
        var context = this._getContext(lineColor, fillColor);
        context.beginPath();
        context.moveTo(x, y);
        context.arc(x, y, radius, startAngle, endAngle, false);
        context.lineTo(x, y);
        context.closePath();
        if (lineColor != null) {
            context.stroke();
        }
        if (fillColor) {
            context.fill();
        }
        if (this.targetX !== undefined && this.targetY !== undefined && context.isPointInPath(this.targetX, this.targetY)) {
            this.currentTargetShapeId = shapeid;
        }
    },
    _drawRect: function(shapeid, x, y, width, height, lineColor, fillColor) {
        return this._drawShape(shapeid, [
            [
                x,
                y
            ],
            [
                x + width,
                y
            ],
            [
                x + width,
                y + height
            ],
            [
                x,
                y + height
            ],
            [
                x,
                y
            ]
        ], lineColor, fillColor);
    },
    appendShape: function(shape) {
        this.shapes[shape.id] = shape;
        this.shapeseq.push(shape.id);
        this.lastShapeId = shape.id;
        return shape.id;
    },
    replaceWithShape: function(shapeid, shape) {
        var shapeseq = this.shapeseq,
            i;
        this.shapes[shape.id] = shape;
        for (i = shapeseq.length; i--; ) {
            if (shapeseq[i] === shapeid) {
                shapeseq[i] = shape.id;
            }
        }
        delete this.shapes[shapeid];
    },
    replaceWithShapes: function(shapeids, shapes) {
        var shapeseq = this.shapeseq,
            shapemap = {},
            sid, i, first;
        for (i = shapeids.length; i--; ) {
            shapemap[shapeids[i]] = true;
        }
        for (i = shapeseq.length; i--; ) {
            sid = shapeseq[i];
            if (shapemap[sid]) {
                shapeseq.splice(i, 1);
                delete this.shapes[sid];
                first = i;
            }
        }
        for (i = shapes.length; i--; ) {
            shapeseq.splice(first, 0, shapes[i].id);
            this.shapes[shapes[i].id] = shapes[i];
        }
    },
    insertAfterShape: function(shapeid, shape) {
        var shapeseq = this.shapeseq,
            i;
        for (i = shapeseq.length; i--; ) {
            if (shapeseq[i] === shapeid) {
                shapeseq.splice(i + 1, 0, shape.id);
                this.shapes[shape.id] = shape;
                return;
            }
        }
    },
    removeShapeId: function(shapeid) {
        var shapeseq = this.shapeseq,
            i;
        for (i = shapeseq.length; i--; ) {
            if (shapeseq[i] === shapeid) {
                shapeseq.splice(i, 1);
                break;
            }
        }
        delete this.shapes[shapeid];
    },
    getShapeAt: function(x, y) {
        // Convert to RTL
        if (this.rtl) {
            x = this.el.dom.width - x - 1;
        }
        this.targetX = x;
        this.targetY = y;
        this.render();
        return this.currentTargetShapeId;
    },
    render: function() {
        var shapeseq = this.shapeseq,
            shapes = this.shapes,
            shapeCount = shapeseq.length,
            context = this._getContext(),
            shapeid, shape, i;
        context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
        for (i = 0; i < shapeCount; i++) {
            shapeid = shapeseq[i];
            shape = shapes[shapeid];
            this['_draw' + shape.type].apply(this, shape.args);
        }
        if (!this.interact) {
            // not interactive so no need to keep the shapes array
            this.shapes = {};
            this.shapeseq = [];
        }
    }
});

/**
 * @private
 */
Ext.define('Ext.sparkline.VmlCanvas', {
    extend: 'Ext.sparkline.CanvasBase',
    setWidth: function(width) {
        var me = this;
        me.callParent(arguments);
        me.owner.groupEl.dom.coordsize = me.width + ' ' + (me.height || 0);
        me.owner.groupEl.dom.style.width = width + 'px';
    },
    setHeight: function(height) {
        var me = this;
        me.callParent(arguments);
        me.owner.groupEl.dom.coordsize = (me.width || 0) + ' ' + me.height;
        me.owner.groupEl.dom.style.height = height + 'px';
    },
    onOwnerUpdate: function() {
        var me = this;
        me.group = me.owner.groupEl;
        me.el = me.owner.element;
        me.prerender = [];
    },
    _drawShape: function(shapeid, path, lineColor, fillColor, lineWidth) {
        var vpath = [],
            initial, stroke, fill, closed, plen, i;
        for (i = 0 , plen = path.length; i < plen; i++) {
            vpath[i] = (path[i][0]) + ',' + (path[i][1]);
        }
        initial = vpath.splice(0, 1);
        lineWidth = lineWidth == null ? 1 : lineWidth;
        stroke = lineColor == null ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
        fill = fillColor == null ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
        closed = vpath[0] === vpath[vpath.length - 1] ? 'x ' : '';
        return [
            '<svml:shape coordorigin="0 0" coordsize="',
            this.pixelWidth,
            ' ',
            this.pixelHeight,
            '" id="jqsshape',
            shapeid,
            '" ',
            stroke,
            fill,
            ' style="position:absolute;height:',
            this.pixelHeight,
            'px;width:',
            this.pixelWidth,
            'px" ',
            ' path="m ',
            initial,
            ' l ',
            vpath.join(', '),
            ' ',
            closed,
            'e"></svml:shape>'
        ].join('');
    },
    _drawCircle: function(shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
        var circumference = radius * 2,
            stroke, fill;
        x -= radius;
        y -= radius;
        stroke = lineColor == null ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
        fill = fillColor == null ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
        return [
            '<svml:oval id="jqsshape',
            shapeid,
            '" ',
            stroke,
            fill,
            ' style="position:absolute;top:',
            y,
            'px; left:',
            x,
            'px;width:',
            circumference,
            'px;height:',
            circumference,
            'px"></svml:oval>'
        ].join('');
    },
    _drawPieSlice: function(shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
        var vpath,
            width = this.pixelWidth,
            height = this.pixelHeight,
            startx, starty, endx, endy, stroke, fill;
        stroke = lineColor == null ? ' stroked="false" ' : ' strokeWeight="1px" strokeColor="' + lineColor + '" ';
        fill = fillColor == null ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
        // VML cannot handle start & end angle the same.
        if (startAngle === endAngle) {
            return '';
        }
        if ((endAngle - startAngle) === (2 * Math.PI)) {
            // VML seems to have a problem when drawing a full circle that doesn't start 0
            startAngle = 0;
            endAngle = (2 * Math.PI);
        }
        startx = x + Math.round(Math.cos(startAngle) * radius);
        starty = y + Math.round(Math.sin(startAngle) * radius);
        endx = x + Math.round(Math.cos(endAngle) * radius);
        endy = y + Math.round(Math.sin(endAngle) * radius);
        if (startx === endx && starty === endy) {
            if ((endAngle - startAngle) < Math.PI) {
                // Prevent very small slices from being mistaken as a whole pie
                return '';
            }
            // essentially going to be the entire circle, so ignore startAngle
            startx = endx = x + radius;
            starty = endy = y;
        }
        if (startx === endx && starty === endy && (endAngle - startAngle) < Math.PI) {
            return '';
        }
        vpath = [
            x - radius,
            y - radius,
            x + radius,
            y + radius,
            startx,
            starty,
            endx,
            endy
        ];
        return [
            '<svml:shape coordorigin="0 0" coordsize="',
            width,
            ' ',
            height,
            '" id="jqsshape',
            shapeid,
            '" ',
            stroke,
            fill,
            ' style="position:absolute;height:',
            height,
            'px;width:',
            width,
            'px" path="m ',
            x,
            ',',
            y,
            ' wa ',
            vpath.join(', '),
            ' x e"></svml:shape>'
        ].join('');
    },
    _drawRect: function(shapeid, x, y, width, height, lineColor, fillColor) {
        return this._drawShape(shapeid, [
            [
                x,
                y
            ],
            [
                x,
                y + height
            ],
            [
                x + width,
                y + height
            ],
            [
                x + width,
                y
            ],
            [
                x,
                y
            ]
        ], lineColor, fillColor);
    },
    reset: function() {
        Ext.fly(this.group).empty();
    },
    appendShape: function(shape) {
        this.prerender.push(this['_draw' + shape.type].apply(this, shape.args));
        this.lastShapeId = shape.id;
        return shape.id;
    },
    replaceWithShape: function(shapeid, shape) {
        var existing = this.el.getById('jqsshape' + shapeid, true),
            vel = this['_draw' + shape.type].apply(this, shape.args);
        existing.outerHTML = vel;
    },
    replaceWithShapes: function(shapeids, shapes) {
        // replace the first shapeid with all the new shapes then toast the remaining old shapes
        var existing = this.el.getById('jqsshape' + shapeids[0], true),
            replace = '',
            slen = shapes.length,
            i;
        for (i = 0; i < slen; i++) {
            replace += this['_draw' + shapes[i].type].apply(this, shapes[i].args);
        }
        existing.outerHTML = replace;
        for (i = 1; i < shapeids.length; i++) {
            this.el.getById('jqsshape' + shapeids[i]).destroy();
        }
    },
    insertAfterShape: function(shapeid, shape) {
        var existing = this.el.getById('jqsshape' + shapeid, true),
            vel = this['_draw' + shape.type].apply(this, shape.args);
        existing.insertAdjacentHTML('afterEnd', vel);
    },
    removeShapeId: function(shapeid) {
        var existing = this.el.getById('jqsshape' + shapeid, true);
        this.group.removeChild(existing);
    },
    getShapeAt: function(x, y) {
        var shapeid = this.el.id.substr(8);
        return shapeid;
    },
    render: function() {
        this.group.dom.innerHTML = this.prerender.join('');
    }
}, function() {
    Ext.onInternalReady(function() {
        var doc = document;
        if (doc.namespaces && !doc.namespaces.svml) {
            doc.namespaces.add("svml", "urn:schemas-microsoft-com:vml", '#default#VML');
        }
    });
});

/**
 * Represents an RGB color and provides helper functions on it e.g. to get
 * color components in HSL color space.
 */
Ext.define('Ext.util.Color', {
    alternateClassName: 'Ext.draw.Color',
    statics: {
        colorToHexRe: /(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/,
        rgbToHexRe: /\s*rgb\((\d+),\s*(\d+),\s*(\d+)\)/,
        rgbaToHexRe: /\s*rgba\((\d+),\s*(\d+),\s*(\d+),\s*([.\d]+)\)/,
        hexRe: /\s*#([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)\s*/,
        // Note that 'none' ia an invalid color string.
        // When assigned to the fillStyle/strokeStyle/shadowColor properties
        // of a Canvas context, those properties won't change their values.
        NONE: 'none',
        RGBA_NONE: 'rgba(0, 0, 0, 0)'
    },
    isColor: true,
    /**
     * @cfg {Number} lightnessFactor
     *
     * The default factor to compute the lighter or darker color.
     */
    lightnessFactor: 0.2,
    /**
     * @constructor
     * @param {Number} red Red component (0..255)
     * @param {Number} green Green component (0..255)
     * @param {Number} blue Blue component (0..255)
     * @param {Number} [alpha=1] (optional) Alpha component (0..1)
     */
    constructor: function(red, green, blue, alpha) {
        this.setRGB(red, green, blue, alpha);
    },
    clone: function() {
        var me = this;
        return new this.self(me.r, me.g, me.b, me.a);
    },
    setRGB: function(red, green, blue, alpha) {
        var me = this;
        me.r = Math.min(255, Math.max(0, red));
        me.g = Math.min(255, Math.max(0, green));
        me.b = Math.min(255, Math.max(0, blue));
        if (alpha === undefined) {
            me.a = 1;
        } else {
            me.a = Math.min(1, Math.max(0, alpha));
        }
    },
    /**
     * The the brightness of a color as defined by W3C:
     * https://www.w3.org/TR/AERT#color-contrast
     * @return {Number} The brightness, between `0` and `100`.
     */
    getBrightness: function() {
        var r = this.r / 255 * 100,
            g = this.g / 255 * 100,
            b = this.b / 255 * 100;
        return ((r * 299) + (g * 587) + (b * 114)) / 1000;
    },
    /**
     * Returns the gray value (0 to 255) of the color.
     *
     * The gray value is calculated using the formula r*0.3 + g*0.59 + b*0.11.
     *
     * @return {Number}
     */
    getGrayscale: function() {
        // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale
        return this.r * 0.3 + this.g * 0.59 + this.b * 0.11;
    },
    /**
     * Get the equivalent HSL components of the color.
     * @return {Number[]}
     */
    getHSL: function() {
        var me = this,
            r = me.r / 255,
            g = me.g / 255,
            b = me.b / 255,
            max = Math.max(r, g, b),
            min = Math.min(r, g, b),
            delta = max - min,
            h,
            s = 0,
            l = 0.5 * (max + min);
        // min==max means achromatic (hue is undefined)
        if (min !== max) {
            s = (l <= 0.5) ? delta / (max + min) : delta / (2 - max - min);
            if (r === max) {
                h = 60 * (g - b) / delta;
            } else if (g === max) {
                h = 120 + 60 * (b - r) / delta;
            } else {
                h = 240 + 60 * (r - g) / delta;
            }
            if (h < 0) {
                h += 360;
            }
            if (h >= 360) {
                h -= 360;
            }
        }
        return [
            h,
            s,
            l
        ];
    },
    /**
     * Get the equivalent HSV components of the color.
     * @return {Number[]}
     */
    getHSV: function() {
        var me = this,
            r = me.r / 255,
            g = me.g / 255,
            b = me.b / 255,
            max = Math.max(r, g, b),
            min = Math.min(r, g, b),
            C = max - min,
            h,
            s = 0,
            v = max;
        // min == max means achromatic (hue is undefined)
        if (min != max) {
            // eslint-disable-line eqeqeq
            s = v ? C / v : 0;
            if (r === max) {
                h = 60 * (g - b) / C;
            } else if (g === max) {
                h = 60 * (b - r) / C + 120;
            } else {
                h = 60 * (r - g) / C + 240;
            }
            if (h < 0) {
                h += 360;
            }
            if (h >= 360) {
                h -= 360;
            }
        }
        return [
            h,
            s,
            v
        ];
    },
    /**
     * Set current color based on the specified HSL values.
     *
     * @param {Number} h Hue component [0..360)
     * @param {Number} s Saturation component [0..1]
     * @param {Number} l Lightness component [0..1]
     * @return {Ext.util.Color}
     */
    setHSL: function(h, s, l) {
        var me = this,
            abs = Math.abs,
            c, x, m;
        h = (h % 360 + 360) % 360;
        s = s > 1 ? 1 : s < 0 ? 0 : s;
        l = l > 1 ? 1 : l < 0 ? 0 : l;
        if (s === 0 || h === null) {
            l *= 255;
            me.setRGB(l, l, l);
        } else {
            // http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL
            h /= 60;
            c = s * (1 - abs(2 * l - 1));
            // chroma
            x = c * (1 - abs(h % 2 - 1));
            // second largest component
            m = l - c / 2;
            // lightness adjustment
            m *= 255;
            c *= 255;
            x *= 255;
            switch (Math.floor(h)) {
                case 0:
                    me.setRGB(c + m, x + m, m);
                    break;
                case 1:
                    me.setRGB(x + m, c + m, m);
                    break;
                case 2:
                    me.setRGB(m, c + m, x + m);
                    break;
                case 3:
                    me.setRGB(m, x + m, c + m);
                    break;
                case 4:
                    me.setRGB(x + m, m, c + m);
                    break;
                case 5:
                    me.setRGB(c + m, m, x + m);
                    break;
            }
        }
        return me;
    },
    /**
     * Set current color based on the specified HSV values.
     *
     * @param {Number} h Hue component [0..360)
     * @param {Number} s Saturation component [0..1]
     * @param {Number} v Value component [0..1]
     * @return {Ext.util.Color}
     */
    setHSV: function(h, s, v) {
        var me = this,
            c, x, m;
        h = (h % 360 + 360) % 360;
        s = s > 1 ? 1 : s < 0 ? 0 : s;
        v = v > 1 ? 1 : v < 0 ? 0 : v;
        if (s === 0 || h === null) {
            v *= 255;
            me.setRGB(v, v, v);
        } else {
            // http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV
            h /= 60;
            c = v * s;
            // chroma
            x = c * (1 - Math.abs(h % 2 - 1));
            // second largest component
            m = v - c;
            // value adjustment
            m *= 255;
            c *= 255;
            x *= 255;
            switch (Math.floor(h)) {
                case 0:
                    me.setRGB(c + m, x + m, m);
                    break;
                case 1:
                    me.setRGB(x + m, c + m, m);
                    break;
                case 2:
                    me.setRGB(m, c + m, x + m);
                    break;
                case 3:
                    me.setRGB(m, x + m, c + m);
                    break;
                case 4:
                    me.setRGB(x + m, m, c + m);
                    break;
                case 5:
                    me.setRGB(c + m, m, x + m);
                    break;
            }
        }
        return me;
    },
    /**
     * Returns a new color that is lighter than this color in the HSL color space.
     * @param {Number} [factor=0.2] Lighter factor (0..1).
     * @return {Ext.util.Color}
     */
    createLighter: function(factor) {
        var color = this.clone();
        color.lighten(factor);
        return color;
    },
    /**
     * Lighten this color in the HSL color space.
     * @param {Number} [factor=0.2] Lighten factor (0..1).
     */
    lighten: function(factor) {
        var hsl;
        if (!factor && factor !== 0) {
            factor = this.lightnessFactor;
        }
        hsl = this.getHSL();
        this.setHSL(hsl[0], hsl[1], Ext.Number.constrain(hsl[2] + factor, 0, 1));
    },
    /**
     * Returns a new color that is darker than this color in the HSL color space.
     * @param {Number} [factor=0.2] Darker factor (0..1).
     * @return {Ext.util.Color}
     */
    createDarker: function(factor) {
        var color = this.clone();
        color.darken(factor);
        return color;
    },
    /**
     * Darken this color in the HSL color space.
     * @param {Number} [factor=0.2] Darken factor (0..1).
     */
    darken: function(factor) {
        if (!factor && factor !== 0) {
            factor = this.lightnessFactor;
        }
        return this.lighten(-factor);
    },
    /**
     * toString() returns a color in hex format ('#rrggbb') if the alpha is 1. If the 
     * alpha is less than one, toString() returns the color in RGBA format ('rgba(255,0,0,0.3)').
     * 
     * @return {String}
     */
    toString: function() {
        var me = this,
            round = Math.round,
            r, g, b;
        if (me.a === 1) {
            r = round(me.r).toString(16);
            g = round(me.g).toString(16);
            b = round(me.b).toString(16);
            r = (r.length === 1) ? '0' + r : r;
            g = (g.length === 1) ? '0' + g : g;
            b = (b.length === 1) ? '0' + b : b;
            return [
                '#',
                r,
                g,
                b
            ].join('');
        } else {
            return 'rgba(' + [
                round(me.r),
                round(me.g),
                round(me.b),
                me.a === 0 ? 0 : me.a.toFixed(15)
            ].join(', ') + ')';
        }
    },
    // Even though things like 'rgba(0,0,0,0)' will probably get converted to
    // 'rgba(0, 0, 0, 0)' when assigned to ctx.fillStyle or ctx.strokeStyle,
    // we can't be sure this is the case for every browser, so for consistency
    // with the Ext.draw.Color.RGBA_NONE (which is used a lot for checks)
    // we join using the ', ' and not ',' here.
    /**
     * Get this color in hexadecimal format.
     * @return {String} The color in hexadecimal format.
     */
    toHex: function(color) {
        var r = this.r,
            g = this.g,
            b = this.b,
            rgb = b | (g << 8) | (r << 16);
        return '#' + ('000000' + rgb.toString(16)).slice(-6);
    },
    /**
     * Parse the string and set the current color.
     *
     * Supported formats: 
     * 
     * + '#rrggbb'
     * + '#rgb', 'rgb(r,g,b)'
     * + 'rgba(r,g,b,a)'
     * + supported CSS color names (e.g., 'black', 'white', etc).
     *
     * If the string is not recognized, setFromString returns rgba(0,0,0,0).
     *
     * @param {String} str Color as string.
     * @return this
     */
    setFromString: function(str) {
        var values, r, g, b,
            a = 1,
            parse = parseInt;
        if (str === Ext.util.Color.NONE) {
            this.r = this.g = this.b = this.a = 0;
            return this;
        }
        if ((str.length === 4 || str.length === 7) && str.substr(0, 1) === '#') {
            values = str.match(Ext.util.Color.hexRe);
            if (values) {
                r = parse(values[1], 16) >> 0;
                g = parse(values[2], 16) >> 0;
                b = parse(values[3], 16) >> 0;
                if (str.length === 4) {
                    r += (r * 16);
                    g += (g * 16);
                    b += (b * 16);
                }
            }
        } else if ((values = str.match(Ext.util.Color.rgbToHexRe))) {
            r = +values[1];
            g = +values[2];
            b = +values[3];
        } else if ((values = str.match(Ext.util.Color.rgbaToHexRe))) {
            r = +values[1];
            g = +values[2];
            b = +values[3];
            a = +values[4];
        } else {
            if (Ext.util.Color.ColorList.hasOwnProperty(str.toLowerCase())) {
                return this.setFromString(Ext.util.Color.ColorList[str.toLowerCase()]);
            }
        }
        if (typeof r === 'undefined') {
            return this;
        }
        this.r = r;
        this.g = g;
        this.b = b;
        this.a = a;
        return this;
    }
}, function() {
    var flyColor = new this();
    this.addStatics({
        /**
         * Returns a flyweight instance of Ext.util.Color.
         *
         * Can be called with either a CSS color string or with separate
         * arguments for red, green, blue, alpha.
         *
         * @param {Number/String} red Red component (0..255) or CSS color string.
         * @param {Number} [green] Green component (0..255)
         * @param {Number} [blue] Blue component (0..255)
         * @param {Number} [alpha=1] Alpha component (0..1)
         * @return {Ext.util.Color}
         * @static
         */
        fly: function(red, green, blue, alpha) {
            switch (arguments.length) {
                case 1:
                    flyColor.setFromString(red);
                    break;
                case 3:
                case 4:
                    flyColor.setRGB(red, green, blue, alpha);
                    break;
                default:
                    return null;
            }
            return flyColor;
        },
        ColorList: {
            aliceblue: '#f0f8ff',
            antiquewhite: '#faebd7',
            aqua: '#00ffff',
            aquamarine: '#7fffd4',
            azure: '#f0ffff',
            beige: '#f5f5dc',
            bisque: '#ffe4c4',
            black: '#000000',
            blanchedalmond: '#ffebcd',
            blue: '#0000ff',
            blueviolet: '#8a2be2',
            brown: '#a52a2a',
            burlywood: '#deb887',
            cadetblue: '#5f9ea0',
            chartreuse: '#7fff00',
            chocolate: '#d2691e',
            coral: '#ff7f50',
            cornflowerblue: '#6495ed',
            cornsilk: '#fff8dc',
            crimson: '#dc143c',
            cyan: '#00ffff',
            darkblue: '#00008b',
            darkcyan: '#008b8b',
            darkgoldenrod: '#b8860b',
            darkgray: '#a9a9a9',
            darkgreen: '#006400',
            darkkhaki: '#bdb76b',
            darkmagenta: '#8b008b',
            darkolivegreen: '#556b2f',
            darkorange: '#ff8c00',
            darkorchid: '#9932cc',
            darkred: '#8b0000',
            darksalmon: '#e9967a',
            darkseagreen: '#8fbc8f',
            darkslateblue: '#483d8b',
            darkslategray: '#2f4f4f',
            darkturquoise: '#00ced1',
            darkviolet: '#9400d3',
            deeppink: '#ff1493',
            deepskyblue: '#00bfff',
            dimgray: '#696969',
            dodgerblue: '#1e90ff',
            firebrick: '#b22222',
            floralwhite: '#fffaf0',
            forestgreen: '#228b22',
            fuchsia: '#ff00ff',
            gainsboro: '#dcdcdc',
            ghostwhite: '#f8f8ff',
            gold: '#ffd700',
            goldenrod: '#daa520',
            gray: '#808080',
            green: '#008000',
            greenyellow: '#adff2f',
            honeydew: '#f0fff0',
            hotpink: '#ff69b4',
            indianred: '#cd5c5c',
            indigo: '#4b0082',
            ivory: '#fffff0',
            khaki: '#f0e68c',
            lavender: '#e6e6fa',
            lavenderblush: '#fff0f5',
            lawngreen: '#7cfc00',
            lemonchiffon: '#fffacd',
            lightblue: '#add8e6',
            lightcoral: '#f08080',
            lightcyan: '#e0ffff',
            lightgoldenrodyellow: '#fafad2',
            lightgray: '#d3d3d3',
            lightgrey: '#d3d3d3',
            lightgreen: '#90ee90',
            lightpink: '#ffb6c1',
            lightsalmon: '#ffa07a',
            lightseagreen: '#20b2aa',
            lightskyblue: '#87cefa',
            lightslategray: '#778899',
            lightsteelblue: '#b0c4de',
            lightyellow: '#ffffe0',
            lime: '#00ff00',
            limegreen: '#32cd32',
            linen: '#faf0e6',
            magenta: '#ff00ff',
            maroon: '#800000',
            mediumaquamarine: '#66cdaa',
            mediumblue: '#0000cd',
            mediumorchid: '#ba55d3',
            mediumpurple: '#9370d8',
            mediumseagreen: '#3cb371',
            mediumslateblue: '#7b68ee',
            mediumspringgreen: '#00fa9a',
            mediumturquoise: '#48d1cc',
            mediumvioletred: '#c71585',
            midnightblue: '#191970',
            mintcream: '#f5fffa',
            mistyrose: '#ffe4e1',
            moccasin: '#ffe4b5',
            navajowhite: '#ffdead',
            navy: '#000080',
            oldlace: '#fdf5e6',
            olive: '#808000',
            olivedrab: '#6b8e23',
            orange: '#ffa500',
            orangered: '#ff4500',
            orchid: '#da70d6',
            palegoldenrod: '#eee8aa',
            palegreen: '#98fb98',
            paleturquoise: '#afeeee',
            palevioletred: '#d87093',
            papayawhip: '#ffefd5',
            peachpuff: '#ffdab9',
            peru: '#cd853f',
            pink: '#ffc0cb',
            plum: '#dda0dd',
            powderblue: '#b0e0e6',
            purple: '#800080',
            red: '#ff0000',
            rosybrown: '#bc8f8f',
            royalblue: '#4169e1',
            saddlebrown: '#8b4513',
            salmon: '#fa8072',
            sandybrown: '#f4a460',
            seagreen: '#2e8b57',
            seashell: '#fff5ee',
            sienna: '#a0522d',
            silver: '#c0c0c0',
            skyblue: '#87ceeb',
            slateblue: '#6a5acd',
            slategray: '#708090',
            snow: '#fffafa',
            springgreen: '#00ff7f',
            steelblue: '#4682b4',
            tan: '#d2b48c',
            teal: '#008080',
            thistle: '#d8bfd8',
            tomato: '#ff6347',
            turquoise: '#40e0d0',
            violet: '#ee82ee',
            wheat: '#f5deb3',
            white: '#ffffff',
            whitesmoke: '#f5f5f5',
            yellow: '#ffff00',
            yellowgreen: '#9acd32'
        },
        /**
         * Create a new color based on the specified HSL values.
         *
         * @param {Number} h Hue component [0..360)
         * @param {Number} s Saturation component [0..1]
         * @param {Number} l Lightness component [0..1]
         * @return {Ext.util.Color}
         * @static
         */
        fromHSL: function(h, s, l) {
            return (new this(0, 0, 0, 0)).setHSL(h, s, l);
        },
        /**
         * Create a new color based on the specified HSV values.
         *
         * @param {Number} h Hue component [0..360)
         * @param {Number} s Saturation component [0..1]
         * @param {Number} v Value component [0..1]
         * @return {Ext.util.Color}
         * @static
         */
        fromHSV: function(h, s, v) {
            return (new this(0, 0, 0, 0)).setHSL(h, s, v);
        },
        /**
         * Parse the string and create a new color.
         *
         * Supported formats: 
         * 
         * + '#rrggbb'
         * + '#rgb', 'rgb(r,g,b)'
         * + 'rgba(r,g,b,a)'
         * + supported CSS color names (e.g., 'black', 'white', etc).
         *
         * If the string is not recognized, fromString returns rgba(0,0,0,0).
         *
         * @param {String} color Color as string.
         * @return {Ext.util.Color}
         * @static
         */
        fromString: function(color) {
            return (new this(0, 0, 0, 0)).setFromString(color);
        },
        /**
         * Convenience method for creating a color.
         *
         * Can be called with several different combinations of arguments:
         *
         *     // Ext.util.Color is returned unchanged.
         *     Ext.util.Color.create(new Ext.util.color(255, 0, 0, 0));
         *
         *     // CSS color string.
         *     Ext.util.Color.create("red");
         *
         *     // Array of red, green, blue, alpha
         *     Ext.util.Color.create([255, 0, 0, 0]);
         *
         *     // Separate arguments of red, green, blue, alpha
         *     Ext.util.Color.create(255, 0, 0, 0);
         *
         *     // Returns black when no arguments given.
         *     Ext.util.Color.create();
         *
         * @param {Array} arg
         * @param {Ext.util.Color/String/Number[]/Number} [arg.red] Red component (0..255),
         * CSS color string or array of all components.
         * @param {Number} [arg.green] Green component (0..255)
         * @param {Number} [arg.blue] Blue component (0..255)
         * @param {Number} [arg.alpha=1] Alpha component (0..1)
         * @return {Ext.util.Color}
         * @static
         */
        create: function(arg) {
            if (arg instanceof this) {
                return arg;
            } else if (Ext.isArray(arg)) {
                return new Ext.util.Color(arg[0], arg[1], arg[2], arg[3]);
            } else if (Ext.isString(arg)) {
                return Ext.util.Color.fromString(arg);
            } else if (arguments.length > 2) {
                return new Ext.util.Color(arguments[0], arguments[1], arguments[2], arguments[3]);
            } else {
                return new Ext.util.Color(0, 0, 0, 0);
            }
        }
    });
});

/**
 * @class Ext.sparkline.Base
 *
 * The base class for ExtJS SparkLines. SparkLines are small, inline graphs used to visually
 * display small amounts of data. For large datasets, use the
 * {@link Ext.chart.AbstractChart chart package}.
 *
 * The SparkLine subclasses accept an {@link #values array of values}, and present the data
 * in different visualizations.
 *
 *     @example
 *     new Ext.Panel({
 *         height: 300,
 *         width: 600,
 *         frame: true,
 *         title: 'Test Sparklines',
 *         renderTo:document.body,
 *         bodyPadding: 10,
 *
 *         // Named listeners will resolve to methods in this Panel
 *         defaultListenerScope: true,
 *
 *         // Named references will be collected, and can be access from this Panel
 *         referenceHolder: true,
 *
 *         items: [{
 *             reference: 'values',
 *             xtype: 'textfield',
 *             fieldLabel: 'Values',
 *             validator: function(v) {
 *                 var result = [];
 *
 *                 v = v.replace(/\s/g, '');
 *                 v = v.replace(/,$/, '');
 *                 v = v.split(',');
 *                 for (var i = 0; i < v.length; i++) {
 *                     if (!Ext.isNumeric(v[i])) {
 *                         return 'Value must be a comma separated array of numbers';
 *                     }
 *                     result.push(parseInt(v[i], 10));
 *                 }
 *                 this.values = result;
 *                 return true;
 *             },
 *             listeners: {
 *                 change: 'onTypeChange',
 *                 buffer: 500,
 *                 afterrender: {
 *                     fn: 'afterTypeRender',
 *                     single: true
 *                 }
 *             }
 *         }, {
 *             reference: 'type',
 *             xtype: 'combobox',
 *             fieldLabel: 'Type',
 *             store: [
 *                 ['sparklineline',     'Line'],
 *                 ['sparklinebox',      'Box'],
 *                 ['sparklinebullet',   'Bullet'],
 *                 ['sparklinediscrete', 'Discrete'],
 *                 ['sparklinepie',      'Pie'],
 *                 ['sparklinetristate', 'TriState']
 *             ],
 *             value: 'sparklineline',
 *             listeners: {
 *                 change: 'onTypeChange',
 *                 buffer: 500
 *             }
 *         }],
 *
 *         // Start with a line plot. 
 *         afterTypeRender: function(typeField) {
 *             typeField.setValue('6,10,4,-3,7,2');
 *         },
 *
 *         onTypeChange: function() {
 *             var me = this,
 *                 refs = me.getReferences(),
 *                 config;
 *
 *             if (me.sparkLine) {
 *                 me.remove(me.sparkLine, true);
 *             }
 *             config = {
 *                 xtype: refs.type.getValue(),
 *                 values: refs.values.values,
 *                 height: 25,
 *                 width: 100                    
 *             };
 *	           me.sparkLine = Ext.create(config);
 *             me.add(me.sparkLine);
 *
 *             // Put under fields
 *             me.sparkLine.el.dom.style.marginLeft = refs.type.labelEl.getWidth() + 'px';
 *         }
 *     });
 *
 */
Ext.define('Ext.sparkline.Base', {
    extend: 'Ext.Gadget',
    xtype: 'sparkline',
    requires: [
        'Ext.XTemplate',
        'Ext.sparkline.CanvasCanvas',
        'Ext.sparkline.VmlCanvas',
        'Ext.util.Color'
    ],
    cachedConfig: {
        /**
         * @cfg {String} lineColor
         * The hex value for line colors in graphs which
         * display lines ({@link Ext.sparkline.Box Box},
         * {@link Ext.sparkline.Discrete Discrete} and {@link Ext.sparkline.Line Line}).
         */
        lineColor: '#157fcc',
        defaultPixelsPerValue: 3,
        tagValuesAttribute: 'values',
        enableTagOptions: false,
        enableHighlight: true,
        /**
         * @cfg {String} [highlightColor=null]
         * The hex value for the highlight color to use when mouseing over a graph segment.
         */
        highlightColor: null,
        /**
         * @cfg {Number} [highlightLighten]
         * How much to lighten the highlight color by when mouseing over a graph segment.
         */
        highlightLighten: 0.1,
        /**
         * @cfg {Boolean} [tooltipSkipNull=true]
         * Null values will not have a tooltip displayed.
         */
        tooltipSkipNull: true,
        /**
         * @cfg {String} [tooltipPrefix]
         * A string to prepend to each field displayed in a tooltip.
         */
        tooltipPrefix: '',
        /**
         * @cfg {String} [tooltipSuffix]
         * A string to append to each field displayed in a tooltip.
         */
        tooltipSuffix: '',
        /**
         * @cfg {Boolean} [disableTooltips=false]
         * Set to `true` to disable mouseover tooltips.
         */
        disableTooltips: false,
        disableInteraction: false,
        /**
         * @cfg {String/Ext.XTemplate} [tipTpl]
         * An XTemplate used to display the value or values in a tooltip when hovering
         * over a Sparkline.
         *
         * The implemented subclases all define their own `tipTpl`, but it can be overridden.
         */
        tipTpl: null
    },
    config: {
        /**
         * @cfg {Number[]} values An array of numbers which define the chart.
         */
        values: null
    },
    baseCls: Ext.baseCSSPrefix + 'sparkline',
    element: {
        tag: 'canvas',
        reference: 'element',
        style: {
            display: 'inline-block',
            verticalAlign: 'top'
        },
        listeners: {
            mouseenter: 'onMouseEnter',
            mouseleave: 'onMouseLeave',
            mousemove: 'onMouseMove'
        },
        // Create canvas zero sized so that it does not affect the containing element's
        // initial layout
        // https://sencha.jira.com/browse/EXTJSIV-10145
        width: 0,
        height: 0
    },
    defaultBindProperty: 'values',
    // When any config is changed, the canvas needs to be redrawn.
    // This is done at the next animation frame when this queue is traversed.
    redrawQueue: {},
    inheritableStatics: {
        /**
         * @private
         * @static
         * @inheritable
         */
        onClassCreated: function(cls) {
            var configUpdater = cls.prototype.updateConfigChange,
                proto = cls.prototype,
                configs = cls.getConfigurator().configs,
                config, updaterName;
            // Set up an applier for all local configs which kicks off a request to redraw
            // on the next animation frame
            for (config in configs) {
                // tipTpl not included in this scheme
                if (config !== 'tipTpl') {
                    updaterName = Ext.Config.get(config).names.update;
                    if (proto[updaterName]) {
                        proto[updaterName] = Ext.Function.createSequence(proto[updaterName], configUpdater);
                    } else {
                        proto[updaterName] = configUpdater;
                    }
                }
            }
        }
    },
    constructor: function(config) {
        var me = this,
            ns = Ext.sparkline;
        // The canvas sets our element config
        me.canvas = Ext.supports.Canvas ? new ns.CanvasCanvas(me) : new ns.VmlCanvas(me);
        me.callParent([
            config
        ]);
    },
    // determine if all values of an array match a value
    // returns true if the array is empty
    all: function(val, arr, ignoreNull) {
        var i;
        for (i = arr.length; i--; ) {
            if (ignoreNull && arr[i] === null) {
                
                continue;
            }
            if (arr[i] !== val) {
                return false;
            }
        }
        return true;
    },
    // generic config value updater.
    // Redraws the graph, unless we are configured to buffer redraws
    // in wehich case it adds this to the queue to do a redraw on the next animation frame.
    updateConfigChange: function(newValue) {
        var me = this;
        // If we are buffering until animation frame, or we have not been sized, then
        // queue a redraw flush.
        if (me.bufferRedraw || !me.height || !me.width) {
            me.redrawQueue[me.getId()] = me;
            // Ensure that there is a single timer to handle all queued redraws.
            if (!me.redrawTimer) {
                Ext.sparkline.Base.prototype.redrawTimer = Ext.raf(me.processRedrawQueue);
            }
        } else {
            me.redraw();
        }
        return newValue;
    },
    // Appliers convert an incoming config value.
    // Ensure the tipTpl is an XTemplate
    applyTipTpl: function(tipTpl) {
        if (tipTpl && !tipTpl.isTemplate) {
            tipTpl = new Ext.XTemplate(tipTpl);
        }
        return tipTpl;
    },
    normalizeValue: function(val) {
        var nf;
        switch (val) {
            case 'undefined':
                val = undefined;
                break;
            case 'null':
                val = null;
                break;
            case 'true':
                val = true;
                break;
            case 'false':
                val = false;
                break;
            default:
                nf = parseFloat(val);
                if (val == nf) {
                    // eslint-disable-line eqeqeq
                    val = nf;
                };
        }
        return val;
    },
    normalizeValues: function(vals) {
        var i,
            result = [];
        for (i = vals.length; i--; ) {
            result[i] = this.normalizeValue(vals[i]);
        }
        return result;
    },
    updateWidth: function(width, oldWidth) {
        var me = this,
            dom = me.element.dom,
            measurer = me.measurer;
        me.callParent([
            width,
            oldWidth
        ]);
        me.canvas.setWidth(width);
        me.width = width;
        if (me.height == null && measurer) {
            me.setHeight(parseInt(measurer.getCachedStyle(dom.parentNode, 'line-height'), 10));
        }
    },
    updateHeight: function(height, oldHeight) {
        var me = this;
        me.callParent([
            height,
            oldHeight
        ]);
        me.canvas.setHeight(height);
        me.height = height;
    },
    setValues: function(values) {
        var me = this,
            oldValues = me.getValues();
        // the values config is expected to be an array
        values = values == null ? [] : Ext.Array.from(values);
        me.values = values;
        me.callParent([
            values
        ]);
        // If it's physically the same Array object, we need to invoke the updater
        // because though the array is the same, it may have been mutated,
        // and the config system setter will reject the change and not invoke the updater.
        // This is how the Stock Ticker example works. It shuffles values down
        // a static array.
        if (values === oldValues) {
            me.updateValues([
                values,
                oldValues
            ]);
        }
    },
    redraw: function() {
        var me = this;
        if (!me.destroyed) {
            me.canvas.onOwnerUpdate();
            me.canvas.reset();
            if (me.getValues()) {
                me.onUpdate();
                me.renderGraph();
            }
        }
    },
    onUpdate: Ext.emptyFn,
    /*
     * Render the chart to the canvas
     */
    renderGraph: function() {
        var ret = true;
        if (this.disabled) {
            this.canvas.reset();
            ret = false;
        }
        return ret;
    },
    onMouseEnter: function(e) {
        this.onMouseMove(e);
    },
    onMouseMove: function(e) {
        var me = this;
        // In IE/Edge, the mousemove event fires before mouseenter
        // This is correct according to the spec
        // https://www.w3.org/TR/uievents/#events-mouseevent-event-order
        me.canvasRegion = me.canvasRegion || me.canvas.el.getRegion();
        me.currentPageXY = e.getPoint();
        me.redraw();
    },
    onMouseLeave: function() {
        var me = this;
        // mouseleave is guaranteed to fire last, so clear region here
        me.canvasRegion = me.currentPageXY = me.targetX = me.targetY = null;
        me.redraw();
        me.hideTip();
    },
    updateDisplay: function() {
        var me = this,
            values = me.getValues(),
            tipHtml, region;
        // To work out the position of currentPageXY within the canvas, we must account
        // for the fact that while document Y values as represented in the currentPageXY
        // are based from the top of the document, canvas Y values begin from the bottom
        // of the canvas element.
        if (values && values.length && me.currentPageXY && me.canvasRegion.contains(me.currentPageXY)) {
            region = me.getRegion(me.currentPageXY[0] - me.canvasRegion.left, (me.canvasRegion.bottom - 1) - me.currentPageXY[1]);
            if (region != null && me.isValidRegion(region, values)) {
                if (!me.disableHighlight) {
                    me.renderHighlight(region);
                }
                if (!me.getDisableTooltips()) {
                    tipHtml = me.getRegionTooltip(region);
                }
            }
            if (me.hasListeners.sparklineregionchange) {
                me.fireEvent('sparklineregionchange', me);
            }
            if (tipHtml) {
                me.getSharedTooltip().setHtml(tipHtml);
                me.showTip();
            }
        }
        // No tip content; ensure it's hidden
        if (!tipHtml) {
            me.hideTip();
        }
    },
    /**
     * @method
     * Return a region id for a given x/y co-ordinate
     */
    getRegion: Ext.emptyFn,
    /**
     * Fetch the HTML to display as a tooltip
     */
    getRegionTooltip: function(region) {
        var me = this,
            entries = [],
            tipTpl = me.getTipTpl(),
            fields, showFields, showFieldsKey, newFields, fv, formatter, fieldlen, i, j;
        fields = me.getRegionFields(region);
        formatter = me.tooltipFormatter;
        if (formatter) {
            return formatter(me, me, fields);
        }
        if (!tipTpl) {
            return '';
        }
        if (!Ext.isArray(fields)) {
            fields = [
                fields
            ];
        }
        showFields = me.tooltipFormatFieldlist;
        showFieldsKey = me.tooltipFormatFieldlistKey;
        if (showFields && showFieldsKey) {
            // user-selected ordering of fields
            newFields = [];
            for (i = fields.length; i--; ) {
                fv = fields[i][showFieldsKey];
                if ((j = Ext.Array.indexOf(fv, showFields)) !== -1) {
                    newFields[j] = fields[i];
                }
            }
            fields = newFields;
        }
        fieldlen = fields.length;
        for (j = 0; j < fieldlen; j++) {
            if (!fields[j].isNull || !me.getTooltipSkipNull()) {
                Ext.apply(fields[j], {
                    prefix: me.getTooltipPrefix(),
                    suffix: me.getTooltipSuffix()
                });
                entries.push(tipTpl.apply(fields[j]));
            }
        }
        if (entries.length) {
            return entries.join('<br>');
        }
        return '';
    },
    getRegionFields: Ext.emptyFn,
    calcHighlightColor: function(color) {
        var me = this,
            highlightColor = me.getHighlightColor(),
            lighten = me.getHighlightLighten(),
            o;
        if (highlightColor) {
            return highlightColor;
        }
        if (lighten) {
            o = Ext.util.Color.fromString(color);
            if (o) {
                o.lighten(lighten);
                color = o.toHex();
            }
        }
        return color;
    },
    destroy: function() {
        delete this.redrawQueue[this.getId()];
        this.callParent();
    },
    privates: {
        hideTip: Ext.privateFn,
        isValidRegion: function(region, values) {
            return region < values.length;
        },
        showTip: Ext.privateFn
    }
}, function(SparklineBase) {
    var proto = SparklineBase.prototype;
    proto.getSharedTooltip = function() {
        var me = this,
            tooltip = me.tooltip;
        if (!tooltip) {
            proto.tooltip = tooltip = SparklineBase.constructTip();
        }
        return tooltip;
    };
    SparklineBase.onClassCreated(SparklineBase);
    proto.processRedrawQueue = function() {
        var redrawQueue = proto.redrawQueue,
            id;
        for (id in redrawQueue) {
            redrawQueue[id].redraw();
        }
        proto.redrawQueue = {};
        proto.redrawTimer = 0;
    };
});

/**
 * @class Ext.sparkline.Base
 */
Ext.define('Ext.override.sparkline.Base', {
    override: 'Ext.sparkline.Base',
    statics: {
        constructTip: function() {
            // eslint-disable-next-line dot-notation
            return new Ext.tip['ToolTip']({
                id: 'sparklines-tooltip',
                showDelay: 0,
                dismissDelay: 0,
                hideDelay: 400
            });
        }
    },
    onMouseMove: function(e) {
        this.getSharedTooltip().triggerEvent = e;
        this.callParent([
            e
        ]);
    },
    onMouseLeave: function(e) {
        this.callParent([
            e
        ]);
        this.getSharedTooltip().target = null;
    },
    privates: {
        hideTip: function() {
            var tip = this.getSharedTooltip();
            tip.target = null;
            tip.hide();
        },
        showTip: function() {
            var tip = this.getSharedTooltip();
            tip.target = this.el;
            tip.onTargetOver(tip.triggerEvent);
        }
    }
}, function(Cls) {
    // If we are on a VML platform (IE8 - TODO: remove this when that retires)...
    if (!Ext.supports.Canvas) {
        Cls.prototype.element = {
            tag: 'span',
            reference: 'element',
            listeners: {
                mouseenter: 'onMouseEnter',
                mouseleave: 'onMouseLeave',
                mousemove: 'onMouseMove'
            },
            style: {
                display: 'inline-block',
                position: 'relative',
                overflow: 'hidden',
                margin: '0px',
                padding: '0px',
                verticalAlign: 'top',
                cursor: 'default'
            },
            children: [
                {
                    tag: 'svml:group',
                    reference: 'groupEl',
                    coordorigin: '0 0',
                    coordsize: '0 0',
                    style: 'position:absolute;width:0;height:0;pointer-events:none'
                }
            ]
        };
    }
});

/**
 * Base class for bar highlighting
 */
Ext.define('Ext.sparkline.BarBase', {
    extend: 'Ext.sparkline.Base',
    renderHighlight: function(region) {
        this.renderRegion(region, true);
    },
    renderGraph: function() {
        var me = this,
            values = me.values,
            canvas = me.canvas,
            regionShapes = me.regionShapes || (me.regionShapes = {}),
            shapes, ids, i, j;
        if (!me.callParent()) {
            return;
        }
        for (i = values.length; i--; ) {
            shapes = me.renderRegion(i);
            if (shapes) {
                if (Ext.isArray(shapes)) {
                    ids = [];
                    for (j = shapes.length; j--; ) {
                        shapes[j].append();
                        ids.push(shapes[j].id);
                    }
                    regionShapes[i] = ids;
                } else {
                    shapes.append();
                    regionShapes[i] = shapes.id;
                }
            } else // store just the shapeid
            {
                // null value
                regionShapes[i] = null;
            }
        }
        // If mouse is over, add the highlight sprite
        if (me.currentPageXY) {
            me.currentRegion = null;
            me.updateDisplay();
        }
        canvas.render();
    }
});

/**
 * Base class for Range Map
 */
Ext.define('Ext.sparkline.RangeMap', {
    constructor: function(map) {
        var key, range,
            rangelist = [];
        for (key in map) {
            if (map.hasOwnProperty(key) && typeof key === 'string' && key.indexOf(':') > -1) {
                range = key.split(':');
                range[0] = range[0].length === 0 ? -Infinity : parseFloat(range[0]);
                range[1] = range[1].length === 0 ? Infinity : parseFloat(range[1]);
                range[2] = map[key];
                rangelist.push(range);
            }
        }
        this.map = map;
        this.rangelist = rangelist || false;
    },
    get: function(value) {
        var rangelist = this.rangelist,
            i, range, result;
        if ((result = this.map[value]) !== undefined) {
            return result;
        }
        if (rangelist) {
            for (i = rangelist.length; i--; ) {
                range = rangelist[i];
                if (range[0] <= value && range[1] >= value) {
                    return range[2];
                }
            }
        }
    }
});

/**
 * @class Ext.sparkline.Bar
 *
 * Plots a bar chart of the values in the passed {@link #values} array.
 *
 * See {@link Ext.sparkline.Base the base class} for a simple example.
 */
Ext.define('Ext.sparkline.Bar', {
    extend: 'Ext.sparkline.BarBase',
    requires: [
        'Ext.sparkline.RangeMap'
    ],
    alias: 'widget.sparklinebar',
    config: {
        /**
         * @cfg {String} barColor
         * The bar color for positive values.
         */
        barColor: '#3366cc',
        /**
         * @cfg {String} negBarColor
         * The bar color for negative values.
         */
        negBarColor: '#f44',
        /**
         * @cfg {String[]} stackedBarColor
         * An array of colours to use for stacked bar charts. The first series will use
         * the first value in the array, the second series will use the second, etc. 
         */
        stackedBarColor: [
            '#3366cc',
            '#dc3912',
            '#ff9900',
            '#109618',
            '#66aa00',
            '#dd4477',
            '#0099c6',
            '#990099'
        ],
        /**
         * @cfg {String} zeroColor
         * The bar color for zero values.
         */
        zeroColor: null,
        /**
         * @cfg {String} nullColor
         * The bar color for null values. Usually null values are omitted and not
         * plotted. Setting this config causes a very thin bar to be plotted with the
         * special color in the case that null is a meaningful value in the series.
         */
        nullColor: null,
        /**
         * @cfg {Boolean} zeroAxis
         * Centers the Y axis at zero by default.
         */
        zeroAxis: true,
        /**
         * @cfg {Number} barWidth
         * The pixel width of bars.
         */
        barWidth: 4,
        /**
         * @cfg {Number} barSpacing
         * The pixel spacing between bars.
         */
        barSpacing: 1,
        /**
         * @cfg {Number} chartRangeMin
         * The minimum value to use for the range of Y values of the chart - Defaults to
         * the minimum value supplied.
         */
        chartRangeMin: null,
        /**
         * @cfg {Number} chartRangeMax The maximum value to use for the range of Y values
         * of the chart - Defaults to the minimum value supplied.
         */
        chartRangeMax: null,
        /**
         * @cfg {Boolean} chartRangeClip
         * If true then the y values supplied to plot will be clipped to fall between
         * {@link #chartRangeMin} and {@link #chartRangeMax} - By default
         * chartRangeMin/Max just ensure that the chart spans at least that range of
         * values, but does not constrain it.
         */
        chartRangeClip: false,
        /**
         * @cfg colorMap
         * @inheritdoc Ext.sparkline.TriState#cfg-colorMap
         */
        colorMap: null
    },
    /**
     * @cfg tipTpl
     * @inheritdoc
     */
    tipTpl: '&#9679; {prefix}{value}{suffix}',
    remove: function(vals, filter) {
        var result = [],
            i, vl;
        for (i = 0 , vl = vals.length; i < vl; i++) {
            if (vals[i] !== filter) {
                result.push(vals[i]);
            }
        }
        return result;
    },
    // determine if all values of an array match a value
    // returns true if the array is empty
    all: function(arr, val, ignoreNull) {
        var i;
        for (i = arr.length; i--; ) {
            if (ignoreNull && arr[i] === null) {
                
                continue;
            }
            if (arr[i] !== val) {
                return false;
            }
        }
        return true;
    },
    applyColorMap: function(colorMap) {
        var me = this;
        if (Ext.isArray(colorMap)) {
            me.colorMapByIndex = colorMap;
            me.colorMapByValue = null;
        } else {
            me.colorMapByIndex = null;
            me.colorMapByValue = colorMap;
            if (me.colorMapByValue && me.colorMapByValue.get == null) {
                me.colorMapByValue = new Ext.sparkline.RangeMap(colorMap);
            }
        }
        me.updateConfigChange();
        return colorMap;
    },
    onUpdate: function() {
        var me = this,
            values = me.values,
            barWidth = me.getBarWidth(),
            barSpacing = me.getBarSpacing(),
            chartRangeMin = me.getChartRangeMin(),
            chartRangeMax = me.getChartRangeMax(),
            chartRangeClip = me.getChartRangeClip(),
            stackMin = Infinity,
            stackMax = -Infinity,
            zeroAxis = me.getZeroAxis(),
            stackTotals = [],
            stackRangesNeg = [],
            isStackString, groupMin, groupMax, stackRanges, numValues, i, vlen, range, xAxisOffset, min, max, clipMin, clipMax, stacked, vlist, j, slen, svals, val, yoffset, yMaxCalc;
        // scan values to determine whether to stack bars
        for (i = 0 , vlen = values.length; i < vlen; i++) {
            val = values[i];
            isStackString = typeof (val) === 'string' && val.indexOf(':') > -1;
            if (isStackString || Ext.isArray(val)) {
                stacked = true;
                if (isStackString) {
                    val = values[i] = me.normalizeValues(val.split(':'));
                }
                val = me.remove(val, null);
                // min/max will treat null as zero
                groupMin = Math.min.apply(Math, val);
                groupMax = Math.max.apply(Math, val);
                if (groupMin < stackMin) {
                    stackMin = groupMin;
                }
                if (groupMax > stackMax) {
                    stackMax = groupMax;
                }
            }
        }
        me.stacked = stacked;
        me.regionShapes = {};
        me.totalBarWidth = barWidth + barSpacing;
        // set width only while expanding the widget in grouped grid REF#EXTJS-26216
        if (values.length) {
            me.width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
        }
        if (chartRangeClip) {
            clipMin = chartRangeMin == null ? -Infinity : chartRangeMin;
            clipMax = chartRangeMax == null ? Infinity : chartRangeMax;
        }
        numValues = [];
        stackRanges = stacked ? [] : numValues;
        for (i = 0 , vlen = values.length; i < vlen; i++) {
            if (stacked) {
                vlist = values[i];
                values[i] = svals = [];
                stackTotals[i] = 0;
                stackRanges[i] = stackRangesNeg[i] = 0;
                for (j = 0 , slen = vlist.length; j < slen; j++) {
                    val = svals[j] = chartRangeClip ? Ext.Number.constrain(vlist[j], clipMin, clipMax) : vlist[j];
                    if (val !== null) {
                        if (val > 0) {
                            stackTotals[i] += val;
                        }
                        if (stackMin < 0 && stackMax > 0) {
                            if (val < 0) {
                                stackRangesNeg[i] += Math.abs(val);
                            } else {
                                stackRanges[i] += val;
                            }
                        } else {
                            stackRanges[i] += Math.abs(val - (val < 0 ? stackMax : stackMin));
                        }
                        numValues.push(val);
                    }
                }
            } else {
                val = chartRangeClip ? Ext.Number.constrain(values[i], clipMin, clipMax) : values[i];
                val = values[i] = me.normalizeValue(val);
                if (val !== null) {
                    numValues.push(val);
                }
            }
        }
        me.max = max = Math.max.apply(Math, numValues);
        me.min = min = Math.min.apply(Math, numValues);
        me.stackMax = stackMax = stacked ? Math.max.apply(Math, stackTotals) : max;
        me.stackMin = stackMin = stacked ? Math.min.apply(Math, numValues) : min;
        if (chartRangeMin != null && (chartRangeClip || chartRangeMin < min)) {
            min = chartRangeMin;
        }
        if (chartRangeMax != null && (chartRangeClip || chartRangeMax > max)) {
            max = chartRangeMax;
        }
        if (min <= 0 && max >= 0 && zeroAxis) {
            xAxisOffset = 0;
        } else if (!zeroAxis) {
            xAxisOffset = min;
        } else if (min > 0) {
            xAxisOffset = min;
        } else {
            xAxisOffset = max;
        }
        me.xAxisOffset = xAxisOffset;
        range = stacked ? (Math.max.apply(Math, stackRanges) + Math.max.apply(Math, stackRangesNeg)) : max - min;
        // as we plot zero/min values a single pixel line, we add a pixel to all other
        // values - Reduce the effective canvas size to suit
        me.canvasHeightEf = (zeroAxis && min < 0) ? me.getHeight() - 2 : me.getHeight() - 1;
        if (min < xAxisOffset) {
            yMaxCalc = (stacked && max >= 0) ? stackMax : max;
            yoffset = (yMaxCalc - xAxisOffset) / range * me.getHeight();
            if (yoffset !== Math.ceil(yoffset)) {
                me.canvasHeightEf -= 2;
                yoffset = Math.ceil(yoffset);
            }
        } else {
            yoffset = me.getHeight();
        }
        me.yoffset = yoffset;
        me.range = range;
    },
    getRegion: function(x, y) {
        var result = Math.floor(x / this.totalBarWidth);
        return (result < 0 || result >= this.values.length) ? undefined : result;
    },
    getRegionFields: function(region) {
        var values = Ext.Array.from(this.values[region]),
            result = [],
            value, i;
        for (i = values.length; i--; ) {
            value = values[i];
            result.push({
                isNull: value === null,
                value: value,
                color: this.calcColor(i, value, region),
                offset: region
            });
        }
        return result;
    },
    calcColor: function(stacknum, value, valuenum) {
        var me = this,
            colorMapByIndex = me.colorMapByIndex,
            colorMapByValue = me.colorMapByValue,
            color, newColor,
            zeroColor = me.getZeroColor();
        if (this.stacked) {
            color = me.getStackedBarColor();
        } else {
            color = (value < 0) ? me.getNegBarColor() : me.getBarColor();
        }
        if (value === 0 && zeroColor != null) {
            color = zeroColor;
        }
        if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
            color = newColor;
        } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
            color = colorMapByIndex[valuenum];
        }
        return Ext.isArray(color) ? color[stacknum % color.length] : color;
    },
    /*
     * Render bar(s) for a region
     */
    renderRegion: function(valuenum, highlight) {
        var me = this,
            vals = me.values[valuenum],
            xaxisOffset = me.xAxisOffset,
            range = me.range,
            stacked = me.stacked,
            canvas = me.canvas,
            barWidth = me.getBarWidth(),
            x = valuenum * me.totalBarWidth,
            canvasHeightEf = me.canvasHeightEf,
            yoffset = me.yoffset,
            y, height, color, isNull, yoffsetNeg, i, valcount, val, minPlotted, allMin,
            nullColor = me.getNullColor();
        vals = Ext.isArray(vals) ? vals : [
            vals
        ];
        valcount = vals.length;
        val = vals[0];
        isNull = me.all(vals, null);
        allMin = me.all(vals, xaxisOffset, true);
        if (isNull) {
            if (nullColor) {
                color = highlight ? nullColor : me.calcHighlightColor(nullColor, me);
                y = (yoffset > 0) ? yoffset - 1 : yoffset;
                canvas.drawRect(x, y, barWidth - 1, 0, color, color).append();
            }
            return;
        }
        yoffsetNeg = yoffset;
        for (i = 0; i < valcount; i++) {
            val = vals[i];
            if (stacked && val === xaxisOffset) {
                if (!allMin || minPlotted) {
                    
                    continue;
                }
                minPlotted = true;
            }
            if (range > 0) {
                height = Math.floor(canvasHeightEf * ((Math.abs(val - xaxisOffset) / range))) + 1;
            } else {
                height = 1;
            }
            if (val < xaxisOffset || (val === xaxisOffset && yoffset === 0)) {
                y = yoffsetNeg;
                yoffsetNeg += height;
            } else {
                y = yoffset - height;
                yoffset -= height;
            }
            color = me.calcColor(i, val, valuenum);
            if (highlight) {
                color = me.calcHighlightColor(color, me);
            }
            canvas.drawRect(x, y, barWidth - 1, height - 1, color, color).append();
        }
    }
}, function(cls) {
    cls.onClassCreated(cls);
});

/**
 * @class Ext.sparkline.Box
 * Generates a box plot graph from the provided {@link #values} array.
 *
 * See <a href="http://en.wikipedia.org/wiki/Box_plot">Wikipedia Box Plots</a>
 *
 * See {@link Ext.sparkline.Base the base class} for a simple example.
 */
Ext.define('Ext.sparkline.Box', {
    extend: 'Ext.sparkline.Base',
    alias: 'widget.sparklinebox',
    config: {
        /**
         * @cfg {Boolean} [raw=false] By default the points are calculated from the
         * input values array. Set this to true to pass the pre-calculated points
         * in the values config.
         */
        raw: false,
        /**
         * @cfg {String} [boxLineColor=#000] The color of the box outline.
         */
        boxLineColor: '#000',
        /**
         * @cfg {String} [boxFillColor=#cdf] The color of the box fill.
         */
        boxFillColor: '#cdf',
        /**
         * @cfg {String} [whiskerColor=#000] The color of the whiskers.
         */
        whiskerColor: '#000',
        /**
         * @cfg {String} [outlierLineColor=#333] The color of the outlier circles' outline.
         */
        outlierLineColor: '#333',
        /**
         * @cfg {String} [outlierFillColor=#fff] The fill color of the outlier circles.
         */
        outlierFillColor: '#fff',
        /**
         * @cfg {String} [medianColor=#f00] The color of the median line.
         */
        medianColor: '#f00',
        /**
         * @cfg {Boolean} [showOutliers=true] Configure as `false` to not show outlier circles.
         */
        showOutliers: true,
        /**
         * @cfg {Number} [outlierIQR=1.5] The inter-quartile range multiplier used to calculate
         * values that qualify as an outlier.
         */
        outlierIQR: 1.5,
        /**
         * @cfg {Number} [spotRadius=1.5] The radius of the outlier circles.
         */
        spotRadius: 1.5,
        /**
         * @cfg {Number} [target] If set, a crosshair will be drawn at the specified value point.
         */
        target: null,
        /**
         * @cfg {String} [targetColor=#4a2] The color of the crosshair drawn at the point
         * specified by {@link #target}.
         */
        targetColor: '#4a2',
        /**
         * @cfg {Number} [chartRangeMin] The minimum value to use for the range of Y values
         * of the chart - Defaults to the minimum value supplied.
         */
        chartRangeMin: null,
        /**
         * @cfg {Number} [chartRangeMax] The maximum value to use for the range of Y values
         * of the chart - Defaults to the minimum value supplied.
         */
        chartRangeMax: null
    },
    tipTpl: [
        '{field:this.fields}: {value}',
        {
            fields: function(v) {
                var fields = {
                        lq: 'Lower Quartile',
                        med: 'Median',
                        uq: 'Upper Quartile',
                        lo: 'Left Outlier',
                        ro: 'Right Outlier',
                        lw: 'Left Whisker',
                        rw: 'Right Whisker'
                    };
                return fields[v];
            }
        }
    ],
    tooltipFormatFieldlistKey: 'field',
    quartile: function(values, q) {
        var vl;
        if (q === 2) {
            vl = Math.floor(values.length / 2);
            return values.length % 2 ? values[vl] : (values[vl - 1] + values[vl]) / 2;
        } else {
            if (values.length % 2) {
                // odd
                vl = (values.length * q + q) / 4;
                return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl - 1];
            } else {
                // even
                vl = (values.length * q + 2) / 4;
                return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl - 1];
            }
        }
    },
    // Ensure values is an array of numbers
    applyValues: function(newValues) {
        newValues = Ext.Array.map(Ext.Array.from(newValues), Number);
        // If we are calculating the values, they need to be sorted
        if (!this.raw) {
            newValues.sort(function(a, b) {
                return a - b;
            });
        }
        this.disabled = !(newValues && newValues.length);
        this.updateConfigChange();
        return newValues;
    },
    /*
     * Simulate a single region
     */
    getRegion: function() {
        return 1;
    },
    getRegionFields: function() {
        var result = [
                {
                    field: 'lq',
                    value: this.quartiles[0]
                },
                {
                    field: 'med',
                    value: this.quartiles[1]
                },
                {
                    field: 'uq',
                    value: this.quartiles[2]
                }
            ];
        if (this.loutlier !== undefined) {
            result.push({
                field: 'lo',
                value: this.loutlier
            });
        }
        if (this.routlier !== undefined) {
            result.push({
                field: 'ro',
                value: this.routlier
            });
        }
        if (this.lwhisker !== undefined) {
            result.push({
                field: 'lw',
                value: this.lwhisker
            });
        }
        if (this.rwhisker !== undefined) {
            result.push({
                field: 'rw',
                value: this.rwhisker
            });
        }
        return result;
    },
    renderHighlight: Ext.emptyFn,
    renderGraph: function() {
        var me = this,
            canvas = me.canvas,
            values = me.values,
            vlen = values.length,
            canvasWidth = me.getWidth(),
            canvasHeight = me.getHeight(),
            chartRangeMin = me.getChartRangeMin(),
            chartRangeMax = me.getChartRangeMax(),
            minValue = chartRangeMin == null ? Math.min.apply(Math, values) : chartRangeMin,
            maxValue = chartRangeMax == null ? Math.max.apply(Math, values) : chartRangeMax,
            canvasLeft = 0,
            lwhisker, loutlier, iqr, q1, q2, q3, rwhisker, routlier, i, size, unitSize,
            spotRadius = me.getSpotRadius(),
            outlierLineColor = me.getOutlierLineColor(),
            outlierFillColor = me.getOutlierFillColor(),
            showOutliers = me.getShowOutliers(),
            outlierIQR = me.getOutlierIQR(),
            lineColor = me.getLineColor(),
            whiskerColor = me.getWhiskerColor(),
            targetColor = me.getTargetColor();
        if (!me.callParent()) {
            return;
        }
        if (me.raw) {
            if (showOutliers && values.length > 5) {
                loutlier = values[0];
                lwhisker = values[1];
                q1 = values[2];
                q2 = values[3];
                q3 = values[4];
                rwhisker = values[5];
                routlier = values[6];
            } else {
                lwhisker = values[0];
                q1 = values[1];
                q2 = values[2];
                q3 = values[3];
                rwhisker = values[4];
            }
        } else {
            q1 = me.quartile(values, 1);
            q2 = me.quartile(values, 2);
            q3 = me.quartile(values, 3);
            iqr = q3 - q1;
            if (showOutliers) {
                lwhisker = rwhisker = null;
                for (i = 0; i < vlen; i++) {
                    if (lwhisker == null && values[i] > q1 - (iqr * outlierIQR)) {
                        lwhisker = values[i];
                    }
                    if (values[i] < q3 + (iqr * outlierIQR)) {
                        rwhisker = values[i];
                    }
                }
                loutlier = values[0];
                routlier = values[vlen - 1];
            } else {
                lwhisker = values[0];
                rwhisker = values[vlen - 1];
            }
        }
        me.quartiles = [
            q1,
            q2,
            q3
        ];
        me.lwhisker = lwhisker;
        me.rwhisker = rwhisker;
        me.loutlier = loutlier;
        me.routlier = routlier;
        unitSize = canvasWidth / (maxValue - minValue + 1);
        if (showOutliers) {
            canvasLeft = Math.ceil(spotRadius);
            canvasWidth -= 2 * Math.ceil(spotRadius);
            unitSize = canvasWidth / (maxValue - minValue + 1);
            if (loutlier < lwhisker) {
                canvas.drawCircle((loutlier - minValue) * unitSize + canvasLeft, canvasHeight / 2, spotRadius, outlierLineColor, outlierFillColor).append();
            }
            if (routlier > rwhisker) {
                canvas.drawCircle((routlier - minValue) * unitSize + canvasLeft, canvasHeight / 2, spotRadius, outlierLineColor, outlierFillColor).append();
            }
        }
        // box
        canvas.drawRect(Math.round((q1 - minValue) * unitSize + canvasLeft), Math.round(canvasHeight * 0.1), Math.round((q3 - q1) * unitSize), Math.round(canvasHeight * 0.8), me.getBoxLineColor(), me.getBoxFillColor()).append();
        // left whisker
        canvas.drawLine(Math.round((lwhisker - minValue) * unitSize + canvasLeft), Math.round(canvasHeight / 2), Math.round((q1 - minValue) * unitSize + canvasLeft), Math.round(canvasHeight / 2), lineColor).append();
        canvas.drawLine(Math.round((lwhisker - minValue) * unitSize + canvasLeft), Math.round(canvasHeight / 4), Math.round((lwhisker - minValue) * unitSize + canvasLeft), Math.round(canvasHeight - canvasHeight / 4), whiskerColor).append();
        // right whisker
        canvas.drawLine(Math.round((rwhisker - minValue) * unitSize + canvasLeft), Math.round(canvasHeight / 2), Math.round((q3 - minValue) * unitSize + canvasLeft), Math.round(canvasHeight / 2), lineColor).append();
        canvas.drawLine(Math.round((rwhisker - minValue) * unitSize + canvasLeft), Math.round(canvasHeight / 4), Math.round((rwhisker - minValue) * unitSize + canvasLeft), Math.round(canvasHeight - canvasHeight / 4), whiskerColor).append();
        // median line
        canvas.drawLine(Math.round((q2 - minValue) * unitSize + canvasLeft), Math.round(canvasHeight * 0.1), Math.round((q2 - minValue) * unitSize + canvasLeft), Math.round(canvasHeight * 0.9), me.getMedianColor()).append();
        if (me.target) {
            size = Math.ceil(me.spotRadius);
            canvas.drawLine(Math.round((me.target - minValue) * unitSize + canvasLeft), Math.round((canvasHeight / 2) - size), Math.round((me.target - minValue) * unitSize + canvasLeft), Math.round((canvasHeight / 2) + size), targetColor).append();
            canvas.drawLine(Math.round((me.target - minValue) * unitSize + canvasLeft - size), Math.round(canvasHeight / 2), Math.round((me.target - minValue) * unitSize + canvasLeft + size), Math.round(canvasHeight / 2), targetColor).append();
        }
        // If mouse is over, re-apply the highlight
        if (me.currentPageXY && me.canvasRegion.contains(me.currentPageXY)) {
            me.currentRegion = null;
            me.updateDisplay();
        }
        canvas.render();
    }
});

/**
 * @class Ext.sparkline.Bullet
 *
 * Plots a bullet graph based upon the input {@link #values} array.
 *
 * See <a href="http://en.wikipedia.org/wiki/Bullet_graph">Bullet graphs Wikipedia Page</a>
 * for more information.
 *
 * The first value should be the target value. If there is no target value, it should be `null`.
 * The second value should be the performance value. If there is no performance value, it should be
 * specified as `null`.
 *
 * An example value:
 *
 *     // Target 10
 *     // Performance 12
 *     // Ranges 12,9,7
 *     [10, 12, 12, 9, 7]
 *
 * See {@link Ext.sparkline.Base the base class} for a simple example.
 */
Ext.define('Ext.sparkline.Bullet', {
    extend: 'Ext.sparkline.Base',
    alias: 'widget.sparklinebullet',
    config: {
        /**
         * @cfg {String} [targetColor=#f33] The colour of the vertical target marker.
         */
        targetColor: '#f33',
        /**
         * @cfg {Number} [targetWidth=3] Width of the target bar in pixels.
         */
        targetWidth: 3,
        /**
         * @cfg {String} [performanceColor=#33f] The color of the performance measure
         * horizontal bar.
         */
        performanceColor: '#33f',
        /**
         * @cfg {String[]} [rangeColors] An array of colors to use for each qualitative range
         * background color.
         */
        rangeColors: [
            '#d3dafe',
            '#a8b6ff',
            '#7f94ff'
        ],
        /**
         * @cfg {Number} [base] Set this to a number to change the base start number.
         */
        base: null
    },
    tipTpl: [
        '{fieldkey:this.fields} - {value}',
        {
            fields: function(v) {
                if (v === 'r') {
                    return 'Range';
                }
                if (v === 'p') {
                    return 'Performance';
                }
                if (v === 't') {
                    return 'Target';
                }
            }
        }
    ],
    // Ensure values is an array of normalized values
    applyValues: function(newValues) {
        newValues = Ext.Array.map(Ext.Array.from(newValues), this.normalizeValue);
        this.disabled = !(newValues && newValues.length);
        this.updateConfigChange();
        return newValues;
    },
    onUpdate: function() {
        var me = this,
            values = me.values,
            min, max, vals,
            base = me.getBase();
        me.callParent(arguments);
        // target or performance could be null
        vals = values.slice();
        vals[0] = vals[0] === null ? vals[2] : vals[0];
        vals[1] = values[1] === null ? vals[2] : vals[1];
        min = Math.min.apply(Math, values);
        max = Math.max.apply(Math, values);
        if (base == null) {
            min = min < 0 ? min : 0;
        } else {
            min = base;
        }
        me.min = min;
        me.max = max;
        me.range = max - min;
        me.shapes = {};
        me.valueShapes = {};
        me.regiondata = {};
        if (!values.length) {
            me.disabled = true;
        }
    },
    getRegion: function(x, y) {
        var shapeid = this.canvas.getShapeAt(x, y);
        return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
    },
    getRegionFields: function(region) {
        return {
            fieldkey: region.substr(0, 1),
            value: this.values[parseInt(region.substr(1), 10)],
            region: region
        };
    },
    renderHighlight: function(region) {
        var me = this,
            valueShapes = me.valueShapes,
            shapes = me.shapes,
            shapeId = valueShapes[region],
            shape;
        delete shapes[shapeId];
        switch (region.substr(0, 1)) {
            case 'r':
                shape = me.renderRange(parseInt(region.substr(1), 10), true);
                break;
            case 'p':
                shape = me.renderPerformance(true);
                break;
            case 't':
                shape = me.renderTarget(true);
                break;
        }
        valueShapes[region] = shape.id;
        shapes[shape.id] = region;
        me.canvas.replaceWithShape(shapeId, shape);
    },
    renderRange: function(region, highlight) {
        var me = this,
            rangeval = me.values[region],
            rangewidth = Math.round(me.getWidth() * ((rangeval - me.min) / me.range)),
            colors = me.getRangeColors(),
            color = colors[Math.min(region - 2, colors.length - 1)];
        if (highlight) {
            color = me.calcHighlightColor(color);
        }
        return me.canvas.drawRect(0, 0, rangewidth - 1, me.getHeight() - 1, color, color);
    },
    renderPerformance: function(highlight) {
        var perfval = this.values[1],
            perfwidth = Math.round(this.getWidth() * ((perfval - this.min) / this.range)),
            color = this.getPerformanceColor();
        if (highlight) {
            color = this.calcHighlightColor(color);
        }
        return this.canvas.drawRect(0, Math.round(this.getHeight() * 0.3), perfwidth - 1, Math.round(this.getHeight() * 0.4) - 1, color, color);
    },
    renderTarget: function(highlight) {
        var targetval = this.values[0],
            targetWidth = this.getTargetWidth(),
            x = Math.round(this.getWidth() * ((targetval - this.min) / this.range) - (targetWidth / 2)),
            // eslint-disable-line max-len
            targettop = Math.round(this.getHeight() * 0.1),
            targetheight = this.getHeight() - (targettop * 2),
            color = this.getTargetColor();
        if (highlight) {
            color = this.calcHighlightColor(color);
        }
        return this.canvas.drawRect(x, targettop, targetWidth - 1, targetheight - 1, color, color);
    },
    renderGraph: function() {
        var me = this,
            vlen = me.values.length,
            canvas = me.canvas,
            i, shape,
            shapes = me.shapes || (me.shapes = {}),
            valueShapes = me.valueShapes || (me.valueShapes = {});
        if (!me.callParent()) {
            return;
        }
        for (i = 2; i < vlen; i++) {
            shape = me.renderRange(i).append();
            shapes[shape.id] = 'r' + i;
            valueShapes['r' + i] = shape.id;
        }
        if (me.values[1] !== null) {
            shape = me.renderPerformance().append();
            shapes[shape.id] = 'p1';
            valueShapes.p1 = shape.id;
        }
        if (me.values[0] !== null) {
            shape = this.renderTarget().append();
            shapes[shape.id] = 't0';
            valueShapes.t0 = shape.id;
        }
        // If mouse is over, apply the highlight
        if (me.currentPageXY && me.canvasRegion.contains(me.currentPageXY)) {
            me.updateDisplay();
        }
        canvas.render();
    },
    privates: {
        isValidRegion: function(region, values) {
            return parseInt(region.substr(1), 10) < values.length;
        }
    }
});

/**
 * @class Ext.sparkline.Discrete
 *
 * Plots a series of thin vertical lines based upon the input {@link #values} array.
 *
 * See {@link Ext.sparkline.Base the base class} for a simple example.
 */
Ext.define('Ext.sparkline.Discrete', {
    extend: 'Ext.sparkline.BarBase',
    alias: 'widget.sparklinediscrete',
    config: {
        /**
         * @cfg {Number} lineHeight Height of each line in pixels - Defaults to 30% of the
         * graph height.
         */
        lineHeight: 'auto',
        /**
         * @cfg {String} thresholdColor Colour to use in combination with {@link #thresholdValue}
         */
        thresholdColor: null,
        /**
         * @cfg {Number} thresholdValue Values less than this value will be drawn using
         * {@link #thresholdColor} instead of lineColor
         */
        thresholdValue: 0,
        /**
         * @cfg {Number} [chartRangeMax] The maximum value to use for the range of Y values
         * of the chart - Defaults to the maximum value supplied.
         */
        chartRangeMax: null,
        /**
         * @cfg {Number} [chartRangeMin] The minimum value to use for the range of Y values
         * of the chart - Defaults to the minimum value supplied.
         */
        chartRangeMin: null,
        /**
         * @cfg {Boolean} chartRangeClip If true then the y values supplied to plot will be clipped
         * to fall between {@link #chartRangeMin} and {@link #chartRangeMax} - By default
         * chartRangeMin/Max just ensure that the chart spans at least that range of values,
         * but does not constrain it.
         */
        chartRangeClip: false
    },
    tipTpl: '{prefix}{value}{suffix}',
    // Ensure values is an array of numbers
    applyValues: function(newValues) {
        newValues = Ext.Array.map(Ext.Array.from(newValues), Number);
        this.disabled = !(newValues && newValues.length);
        this.updateConfigChange();
        return newValues;
    },
    onUpdate: function() {
        var me = this,
            values = me.values,
            chartRangeMin = me.getChartRangeMin(),
            chartRangeMax = me.getChartRangeMax(),
            chartRangeClip = me.getChartRangeClip();
        me.callParent(arguments);
        me.regionShapes = {};
        me.min = Math.min.apply(Math, values);
        me.max = Math.max.apply(Math, values);
        me.range = me.max - me.min;
        me.width = me.getWidth();
        me.interval = Math.floor(me.width / values.length);
        me.itemWidth = me.width / values.length;
        if (chartRangeMin != null && (chartRangeClip || chartRangeMin < me.min)) {
            me.min = chartRangeMin;
        }
        if (chartRangeMax != null && (chartRangeClip || chartRangeMax > me.max)) {
            me.max = chartRangeMax;
        }
        if (me.canvas) {
            if (me.getLineHeight() === 'auto') {
                me.setLineHeight(Math.round(me.getHeight() * 0.3));
            }
        }
    },
    getRegion: function(x, y) {
        return Math.floor(x / this.itemWidth);
    },
    getRegionFields: function(region) {
        return {
            isNull: this.values[region] === undefined,
            value: this.values[region],
            offset: region
        };
    },
    renderRegion: function(valuenum, highlight) {
        var me = this,
            values = me.values,
            min = me.min,
            max = me.max,
            range = me.range,
            interval = me.interval,
            canvas = me.canvas,
            canvasHeight = me.getHeight(),
            lineHeight = me.getLineHeight(),
            pheight = canvasHeight - lineHeight,
            ytop, val, color, x,
            thresholdColor = me.getThresholdColor();
        val = Ext.Number.constrain(values[valuenum], min, max);
        x = valuenum * interval;
        ytop = Math.round(pheight - pheight * ((val - min) / range));
        color = (thresholdColor && val < me.getThresholdValue()) ? thresholdColor : me.getLineColor();
        if (highlight) {
            color = me.calcHighlightColor(color);
        }
        canvas.drawLine(x, ytop, x, ytop + lineHeight, color).append();
    }
});

/**
 * @class Ext.sparkline.Line
 *
 * Plots a line graph based upon the input {@link #values} array.
 *
 * See {@link Ext.sparkline.Base the base class} for a simple example.
 */
Ext.define('Ext.sparkline.Line', {
    extend: 'Ext.sparkline.Base',
    requires: [
        'Ext.sparkline.RangeMap'
    ],
    alias: 'widget.sparklineline',
    config: {
        /**
         * @cfg {String} [fillColor=#def] The hex value for fill color.
         */
        fillColor: '#def',
        /**
         * @cfg {String} [spotColor=#f80] The colour of the final value marker. Set to false
         * or an empty string to hide it.
         */
        spotColor: '#f80',
        /**
         * @cfg {String} [highlightSpotColor=#5f5] The colour of value marker spots
         * when mouseovered.
         */
        highlightSpotColor: '#5f5',
        /**
         * @cfg {String} [highlightLineColor=#f22] The colour of value line shown when the graph
         * is mouseovered.
         */
        highlightLineColor: '#f22',
        /**
         * @cfg {Number} [spotRadius=1.5] The pixel radius of min, max and final value dots.
         */
        spotRadius: 1.5,
        /**
         * @cfg {String} [minSpotColor=#f80] The colour of the mimimum value marker. Set to false
         * or an empty string to hide it.
         */
        minSpotColor: '#f80',
        /**
         * @cfg {String} [maxSpotColor=#f80] The colour of the maximum value marker. Set to false
         * or an empty string to hide it.
         */
        maxSpotColor: '#f80',
        /**
         * @cfg {Number} [lineWidth=1] The pixel width of the line plotted.
         */
        lineWidth: 1,
        /**
         * @cfg {Number} [normalRangeMin] See {@link #normalRangeMax} The minimum value to overlay
         * a "normal range bar" over the graph using the {@link #normalRangeColor}.
         */
        normalRangeMin: null,
        /**
         * @cfg {Number} [normalRangeMax] See {@link #normalRangeMin} The maximum value to overlay
         * a "normal range bar" over the graph using the {@link #normalRangeColor}.
         */
        normalRangeMax: null,
        /**
         * @cfg {String} [normalRangeColor=#ccc] See {@link #normalRangeMin} and
         * {@link #normalRangeMax} The color of the undererlayed "normal range bar".
         */
        normalRangeColor: '#ccc',
        /**
         * @cfg {Boolean} [drawNormalOnTop=false] Configure as `true` to draw the normal range
         * overlaying the chart.
         */
        drawNormalOnTop: false,
        /**
         * @cfg {Number} [chartRangeMin] The minimum value to use for the range of Y values
         * of the chart - Defaults to the minimum value supplied.
         */
        chartRangeMin: null,
        /**
         * @cfg {Number} [chartRangeMax] The maximum value to use for the range of Y values
         * of the chart - Defaults to the minimum value supplied.
         */
        chartRangeMax: null,
        /**
         * @cfg {Number} [chartRangeMinX] The minimum value to use for the X value of the chart.
         */
        chartRangeMinX: null,
        /**
         * @cfg {Number} [chartRangeMaxX] The maximum value to use for the X value of the chart.
         */
        chartRangeMaxX: null,
        /**
         * @cfg {Object} [valueSpots] An object which uses range specifiers as keys to indicate
         * spot color values for range of values. A range specifier is of the form
         * `[number]:[number]` indicating start and end range. Omitting aither means an open ended
         * range. For example to render green spots on all values less than 50 and red on values
         * higher than 50 use:
         *
         *     {
         *         // Open ended range, with max value 49
         *         ":49": "green",
         *
         *         // Open ended range, with min value 50
         *         "50:": "red"
         *     }
         */
        valueSpots: null
    },
    tipTpl: '&#9679; {prefix}{y}{suffix}',
    applyValueSpots: function(valueSpots) {
        if (valueSpots && !valueSpots.get) {
            valueSpots = new Ext.sparkline.RangeMap(valueSpots);
        }
        this.updateConfigChange();
        return valueSpots;
    },
    onUpdate: function() {
        this.vertices = [];
        this.regionMap = [];
        this.xvalues = [];
        this.yvalues = [];
        this.yminmax = [];
    },
    getRegion: function(x, y) {
        var i,
            regionMap = this.regionMap;
        // maps regions to value positions
        for (i = regionMap.length; i--; ) {
            if (regionMap[i] !== null && x >= regionMap[i][0] && x <= regionMap[i][1]) {
                return regionMap[i][2];
            }
        }
        return undefined;
    },
    getRegionFields: function(region) {
        return {
            isNull: this.yvalues[region] === null,
            x: this.xvalues[region],
            y: this.yvalues[region],
            color: this.getLineColor(),
            fillColor: this.getFillColor(),
            offset: region
        };
    },
    renderHighlight: function(region) {
        var me = this,
            canvas = me.canvas,
            vertex = me.vertices[region],
            spotRadius = me.getSpotRadius(),
            highlightSpotColor = me.getHighlightSpotColor(),
            highlightLineColor = me.getHighlightLineColor();
        if (!vertex) {
            return;
        }
        if (spotRadius && highlightSpotColor) {
            canvas.drawCircle(vertex[0], vertex[1], spotRadius, null, highlightSpotColor).append();
        }
        if (highlightLineColor) {
            canvas.drawLine(vertex[0], me.canvasTop, vertex[0], me.canvasTop + me.getHeight(), highlightLineColor).append();
        }
    },
    scanValues: function() {
        var me = this,
            values = me.values,
            valcount = values.length,
            xvalues = me.xvalues,
            yvalues = me.yvalues,
            yminmax = me.yminmax,
            i, val;
        for (i = 0; i < valcount; i++) {
            val = values[i];
            if (typeof val === 'string') {
                val = val.split(':');
            }
            // Array. Use first two values as X and Y
            if (val && val.length === 2) {
                xvalues.push(Number(val[0]));
                yvalues.push(val = Number(val[1]));
                yminmax.push(val);
            } else // Single value. Use i as X, the value as Y
            {
                xvalues.push(i);
                if (val == null || val === 'null') {
                    yvalues.push(null);
                } else {
                    yvalues.push(val = Number(val));
                    yminmax.push(val);
                }
            }
        }
        if (me.xvalues) {
            xvalues = me.xvalues;
        }
        me.maxy = me.maxyorg = Math.max.apply(Math, yminmax);
        me.miny = me.minyorg = Math.min.apply(Math, yminmax);
        me.maxx = Math.max.apply(Math, xvalues);
        me.minx = Math.min.apply(Math, xvalues);
        me.xvalues = xvalues;
        me.yvalues = yvalues;
        me.yminmax = yminmax;
    },
    processRangeOptions: function() {
        var me = this,
            normalRangeMin = me.getNormalRangeMin(),
            normalRangeMax = me.getNormalRangeMax(),
            chartRangeMin = me.getChartRangeMin(),
            chartRangeMinX = me.getChartRangeMinX(),
            chartRangeMax = me.getChartRangeMax(),
            chartRangeMaxX = me.getChartRangeMaxX();
        if (normalRangeMin != null) {
            if (normalRangeMin < me.miny) {
                me.miny = normalRangeMin;
            }
            if (normalRangeMax > me.maxy) {
                me.maxy = normalRangeMax;
            }
        }
        if (chartRangeMin != null && (me.chartRangeClip || chartRangeMin < me.miny)) {
            me.miny = chartRangeMin;
        }
        if (chartRangeMax != null && (me.chartRangeClip || chartRangeMax > me.maxy)) {
            this.maxy = chartRangeMax;
        }
        if (chartRangeMinX != null && (me.chartRangeClipX || chartRangeMinX < me.minx)) {
            me.minx = chartRangeMinX;
        }
        if (chartRangeMaxX != null && (me.chartRangeClipX || chartRangeMaxX > me.maxx)) {
            me.maxx = chartRangeMaxX;
        }
    },
    drawNormalRange: function(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey) {
        var normalRangeMin = this.getNormalRangeMin(),
            normalRangeMax = this.getNormalRangeMax(),
            ytop = canvasTop + Math.round(canvasHeight - (canvasHeight * ((normalRangeMax - this.miny) / rangey))),
            // eslint-disable-line max-len
            height = Math.round((canvasHeight * (normalRangeMax - normalRangeMin)) / rangey);
        this.canvas.drawRect(canvasLeft, ytop, canvasWidth, height, undefined, this.normalRangeColor).append();
    },
    // eslint-disable-line max-len
    renderGraph: function() {
        var me = this,
            canvas = me.canvas,
            canvasWidth = me.getWidth(),
            canvasHeight = me.getHeight(),
            vertices = me.vertices,
            spotRadius = me.getSpotRadius(),
            regionMap = me.regionMap,
            rangeX, Y, yvallast, canvasTop, canvasLeft, vertex, path, paths, x, y, xNext, xPos, xPosNext, last, next, yValCount, lineShapes, fillShapes, plen,
            valueSpots = me.getValueSpots(),
            hlSpotsEnabled, color, xValues, yValues, i,
            spotColor = me.getSpotColor(),
            minSpotColor = me.getMinSpotColor(),
            maxSpotColor = me.getMaxSpotColor(),
            normalRangeMin = me.getNormalRangeMin(),
            drawNormalOnTop = me.getDrawNormalOnTop();
        if (!me.callParent()) {
            return;
        }
        me.scanValues();
        me.processRangeOptions();
        xValues = me.xvalues;
        yValues = me.yvalues;
        if (!me.yminmax.length || me.yvalues.length < 2) {
            // empty or all null valuess
            return;
        }
        canvasTop = canvasLeft = 0;
        rangeX = me.maxx - me.minx === 0 ? 1 : me.maxx - me.minx;
        Y = me.maxy - me.miny === 0 ? 1 : me.maxy - me.miny;
        yvallast = me.yvalues.length - 1;
        if (spotRadius && (canvasWidth < (spotRadius * 4) || canvasHeight < (spotRadius * 4))) {
            spotRadius = 0;
        }
        if (spotRadius) {
            // adjust the canvas size as required so that spots will fit
            hlSpotsEnabled = me.getHighlightSpotColor() && !me.disableInteraction;
            if (hlSpotsEnabled || minSpotColor || (spotColor && yValues[yvallast] === me.miny)) {
                canvasHeight -= Math.ceil(spotRadius);
            }
            if (hlSpotsEnabled || maxSpotColor || (spotColor && yValues[yvallast] === me.maxy)) {
                canvasHeight -= Math.ceil(spotRadius);
                canvasTop += Math.ceil(spotRadius);
            }
            // eslint-disable-next-line max-len
            if (hlSpotsEnabled || ((minSpotColor || maxSpotColor) && (yValues[0] === me.miny || yValues[0] === me.maxy))) {
                canvasLeft += Math.ceil(spotRadius);
                canvasWidth -= Math.ceil(spotRadius);
            }
            // eslint-disable-next-line max-len
            if (hlSpotsEnabled || spotColor || (minSpotColor || maxSpotColor && (yValues[yvallast] === me.miny || yValues[yvallast] === me.maxy))) {
                canvasWidth -= Math.ceil(spotRadius);
            }
        }
        canvasHeight--;
        if (normalRangeMin != null && !drawNormalOnTop) {
            me.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, Y);
        }
        path = [];
        paths = [
            path
        ];
        last = next = null;
        yValCount = yValues.length;
        for (i = 0; i < yValCount; i++) {
            x = xValues[i];
            xNext = xValues[i + 1];
            y = yValues[i];
            xPos = canvasLeft + Math.round((x - me.minx) * (canvasWidth / rangeX));
            xPosNext = i < yValCount - 1 ? canvasLeft + Math.round((xNext - me.minx) * (canvasWidth / rangeX)) : canvasWidth;
            next = xPos + ((xPosNext - xPos) / 2);
            regionMap[i] = [
                last || 0,
                next,
                i
            ];
            last = next;
            if (y === null) {
                if (i) {
                    if (yValues[i - 1] !== null) {
                        path = [];
                        paths.push(path);
                    }
                    vertices.push(null);
                }
            } else {
                if (y < me.miny) {
                    y = me.miny;
                }
                if (y > me.maxy) {
                    y = me.maxy;
                }
                if (!path.length) {
                    // previous value was null
                    path.push([
                        xPos,
                        canvasTop + canvasHeight
                    ]);
                }
                // eslint-disable-next-line max-len
                vertex = [
                    xPos,
                    canvasTop + Math.round(canvasHeight - (canvasHeight * ((y - this.miny) / Y)))
                ];
                path.push(vertex);
                vertices.push(vertex);
            }
        }
        lineShapes = [];
        fillShapes = [];
        plen = paths.length;
        for (i = 0; i < plen; i++) {
            path = paths[i];
            if (path.length) {
                if (me.fillColor) {
                    path.push([
                        path[path.length - 1][0],
                        (canvasTop + canvasHeight)
                    ]);
                    fillShapes.push(path.slice(0));
                    path.pop();
                }
                // if there's only a single point in this path, then we want to display it
                // as a vertical line which means we keep path[0]  as is
                if (path.length > 2) {
                    // else we want the first value
                    path[0] = [
                        path[0][0],
                        path[1][1]
                    ];
                }
                lineShapes.push(path);
            }
        }
        // draw the fill first, then optionally the normal range, then the line on top of that
        plen = fillShapes.length;
        for (i = 0; i < plen; i++) {
            canvas.drawShape(fillShapes[i], me.fillColor, me.fillColor).append();
        }
        if (normalRangeMin != null && drawNormalOnTop) {
            me.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, Y);
        }
        plen = lineShapes.length;
        for (i = 0; i < plen; i++) {
            canvas.drawShape(lineShapes[i], me.getLineColor(), null, me.getLineWidth()).append();
        }
        /* eslint-disable max-len */
        if (spotRadius && valueSpots) {
            if (valueSpots.get == null) {
                valueSpots = new Ext.sparkline.RangeMap(valueSpots);
            }
            for (i = 0; i < yValCount; i++) {
                color = valueSpots.get(yValues[i]);
                if (color) {
                    canvas.drawCircle(canvasLeft + Math.round((xValues[i] - me.minx) * (canvasWidth / rangeX)), canvasTop + Math.round(canvasHeight - (canvasHeight * ((yValues[i] - me.miny) / Y))), spotRadius, null, color).append();
                }
            }
        }
        if (spotRadius && spotColor && yValues[yvallast] != null) {
            canvas.drawCircle(canvasLeft + Math.round((xValues[xValues.length - 1] - me.minx) * (canvasWidth / rangeX)), canvasTop + Math.round(canvasHeight - (canvasHeight * ((yValues[yvallast] - me.miny) / Y))), spotRadius, null, spotColor).append();
        }
        if (me.maxy !== me.minyorg) {
            if (spotRadius && minSpotColor) {
                x = xValues[Ext.Array.indexOf(yValues, me.minyorg)];
                canvas.drawCircle(canvasLeft + Math.round((x - me.minx) * (canvasWidth / rangeX)), canvasTop + Math.round(canvasHeight - (canvasHeight * ((me.minyorg - me.miny) / Y))), spotRadius, null, minSpotColor).append();
            }
            if (spotRadius && maxSpotColor) {
                x = xValues[Ext.Array.indexOf(yValues, me.maxyorg)];
                canvas.drawCircle(canvasLeft + Math.round((x - me.minx) * (canvasWidth / rangeX)), canvasTop + Math.round(canvasHeight - (canvasHeight * ((me.maxyorg - me.miny) / Y))), spotRadius, null, maxSpotColor).append();
            }
        }
        /* eslint-enable max-len */
        me.canvasTop = canvasTop;
        // If mouse is over, apply the highlight
        if (me.currentPageXY && me.canvasRegion.contains(me.currentPageXY)) {
            me.updateDisplay();
        }
        canvas.render();
    }
});

/**
 * @class Ext.sparkline.Pie
 *
 * Plots a pie chart based upon the input {#values} array.
 *
 * See {@link Ext.sparkline.Base the base class} for a simple example.
 */
Ext.define('Ext.sparkline.Pie', {
    extend: 'Ext.sparkline.Base',
    alias: 'widget.sparklinepie',
    config: {
        /**
         * @cfg {Number} [offset] Angle in degrees to offset the first slice.
         */
        offset: 0,
        /**
         * @cfg {String[]} [sliceColors] An array of CSS colro values to apply to the chart slices.
         */
        sliceColors: [
            '#3366cc',
            '#dc3912',
            '#ff9900',
            '#109618',
            '#66aa00',
            '#dd4477',
            '#0099c6',
            '#990099'
        ],
        /**
         * @cfg {Number} [borderWidth=0] Border width in pixels of line round slices.
         */
        borderWidth: 0,
        /**
         * @cfg {String} [borderColor=#000] Border color of line round slices.
         */
        borderColor: '#000'
    },
    tipTpl: '&#9679; {value} ({percent:number("0.0")}%)',
    // Ensure values is an array of numbers
    applyValues: function(newValues) {
        newValues = Ext.Array.map(Ext.Array.from(newValues), Number);
        this.disabled = !(newValues && newValues.length);
        this.updateConfigChange();
        return newValues;
    },
    onUpdate: function() {
        var me = this,
            values = me.values,
            total = 0,
            i;
        me.callParent(arguments);
        me.shapes = {};
        // map shape ids to value offsets
        me.valueShapes = {};
        // maps value offsets to shape ids
        if (values.length > 0) {
            for (i = values.length; i--; ) {
                total += values[i];
            }
        }
        me.total = total;
        me.radius = Math.floor(Math.min(me.getWidth(), me.getHeight()) / 2);
    },
    getRegion: function(x, y) {
        var ratio = window.devicePixelRatio || 1,
            shapeid = this.canvas.getShapeAt(x * ratio, y * ratio);
        return (shapeid != null && this.shapes[shapeid] != null) ? this.shapes[shapeid] : null;
    },
    getRegionFields: function(region) {
        var sliceColors = this.getSliceColors();
        return {
            isNull: this.values[region] == null,
            value: this.values[region],
            percent: this.values[region] / this.total * 100,
            color: sliceColors[region % sliceColors.length],
            offset: region
        };
    },
    renderHighlight: function(region) {
        this.renderSlice(region, true).append();
    },
    renderSlice: function(valuenum, highlight) {
        var me = this,
            canvas = me.canvas,
            radius = me.radius,
            borderWidth = me.getBorderWidth(),
            offset = me.getOffset(),
            circle = 2 * Math.PI,
            values = me.values,
            total = me.total,
            next = offset ? (2 * Math.PI) * (offset / 360) : 0,
            start, end, i, vlen, color,
            sliceColors = this.getSliceColors();
        vlen = values.length;
        for (i = 0; i < vlen; i++) {
            start = next;
            end = next;
            if (total > 0) {
                // avoid divide by zero
                end = next + (circle * (values[i] / total));
            }
            if (valuenum === i) {
                color = sliceColors[i % sliceColors.length];
                if (highlight) {
                    color = me.calcHighlightColor(color);
                }
                return canvas.drawPieSlice(radius, radius, radius - borderWidth, start, end, null, color);
            }
            next = end;
        }
    },
    renderGraph: function() {
        var me = this,
            canvas = me.canvas,
            values = me.values,
            radius = me.radius,
            borderWidth = me.getBorderWidth(),
            shape, i,
            shapes = me.shapes || (me.shapes = {}),
            valueShapes = me.valueShapes || (me.valueShapes = {});
        if (!me.callParent()) {
            return;
        }
        if (borderWidth) {
            canvas.drawCircle(radius, radius, Math.floor(radius - (borderWidth / 2)), me.getBorderColor(), null, borderWidth).append();
        }
        for (i = values.length; i--; ) {
            if (values[i]) {
                // don't render zero values
                shape = me.renderSlice(i).append();
                valueShapes[i] = shape.id;
                // store just the shapeid
                shapes[shape.id] = i;
            }
        }
        // If mouse is over, re-apply the highlight
        if (me.currentPageXY && me.canvasRegion.contains(me.currentPageXY)) {
            me.currentRegion = null;
            me.updateDisplay();
        }
        canvas.render();
    }
});

/**
 * @class Ext.sparkline.TriState
 *
 * Plots bars based upon "win"/"draw" or "lose" status of the input {@link #values} array.
 * Positive values mean a win, zero a draw, and negative a lose. 
 *
 * See {@link Ext.sparkline.Base the base class} for a simple example.
 */
Ext.define('Ext.sparkline.TriState', {
    extend: 'Ext.sparkline.BarBase',
    requires: [
        'Ext.sparkline.RangeMap'
    ],
    alias: 'widget.sparklinetristate',
    config: {
        /**
         * @cfg {Number} barWidth
         * The pixel width of each bar.
         */
        barWidth: 4,
        /**
         * @cfg {Number} barSpacing
         * The pixel spacing between each bar.
         */
        barSpacing: 1,
        /**
         * @cfg {String} posBarColor
         * The color for positive value bars.
         */
        posBarColor: '#6f6',
        /**
         * @cfg {String} negBarColor
         * The color for negative value bars.
         */
        negBarColor: '#f44',
        /**
         * @cfg {String} zeroBarColor
         * The color for zero value bars.
         */
        zeroBarColor: '#999',
        /**
         * @cfg {Object} colorMap
         * An object that uses range specifiers as keys to
         * indicate bar color values for a range of values. A range specifier is
         * specified in the form `[number]:[number]`, which indicates start and end range.
         * Omitting either means an open ended range.
         *
         * For example, to render green bars on all values less than -1 and red on values
         * higher than 1, you would use:
         *
         *     colorMap: {
         *         // Open ended range, with max value -1
         *         ":-1": "green",
         *
         *         // Open ended range, with min value 1
         *         "1:": "red"
         *     }
         */
        colorMap: {}
    },
    tipTpl: [
        '&#9679; {value:this.states}',
        {
            states: function(v) {
                var value = Number(v);
                if (value === -1) {
                    return 'Loss';
                }
                if (value === 0) {
                    return 'Draw';
                }
                if (value === 1) {
                    return 'Win';
                }
                return v;
            }
        }
    ],
    applyColorMap: function(colorMap) {
        var me = this;
        if (Ext.isArray(colorMap)) {
            me.colorMapByIndex = colorMap;
            me.colorMapByValue = null;
        } else {
            me.colorMapByIndex = null;
            me.colorMapByValue = colorMap;
            if (me.colorMapByValue && me.colorMapByValue.get == null) {
                me.colorMapByValue = new Ext.sparkline.RangeMap(colorMap);
            }
        }
        me.updateConfigChange();
        return colorMap;
    },
    // Ensure values is an array of numbers
    applyValues: function(newValues) {
        newValues = Ext.Array.map(Ext.Array.from(newValues), Number);
        this.disabled = !(newValues && newValues.length);
        this.updateConfigChange();
        return newValues;
    },
    onUpdate: function() {
        this.totalBarWidth = this.getBarWidth() + this.getBarSpacing();
    },
    getBarWidth: function() {
        var values = this.values;
        return this._barWidth || (this.getWidth() - (values.length - 1) * this.getBarSpacing()) / values.length;
    },
    getRegion: function(x, y) {
        return Math.floor(x / this.totalBarWidth);
    },
    getRegionFields: function(region) {
        return {
            isNull: this.values[region] == null,
            value: this.values[region],
            color: this.calcColor(this.values[region], region),
            offset: region
        };
    },
    calcColor: function(value, valuenum) {
        var me = this,
            values = me.values,
            colorMapByIndex = me.colorMapByIndex,
            colorMapByValue = me.colorMapByValue,
            color, newColor;
        if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
            color = newColor;
        } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
            color = colorMapByIndex[valuenum];
        } else if (values[valuenum] < 0) {
            color = me.getNegBarColor();
        } else if (values[valuenum] > 0) {
            color = me.getPosBarColor();
        } else {
            color = me.getZeroBarColor();
        }
        return color;
    },
    renderRegion: function(valuenum, highlight) {
        var me = this,
            values = me.values,
            canvas = me.canvas,
            canvasHeight, height, halfHeight, x, y, color;
        canvasHeight = canvas.pixelHeight;
        halfHeight = Math.round(canvasHeight / 2);
        x = valuenum * me.totalBarWidth;
        if (values[valuenum] < 0) {
            y = halfHeight;
            height = halfHeight - 1;
        } else if (values[valuenum] > 0) {
            y = 0;
            height = halfHeight - 1;
        } else {
            y = halfHeight - 1;
            height = 2;
        }
        color = me.calcColor(values[valuenum], valuenum);
        if (color == null) {
            return;
        }
        if (highlight) {
            color = me.calcHighlightColor(color);
        }
        canvas.drawRect(x, y, me.getBarWidth() - 1, height - 1, color, color).append();
    }
});

/* jshint bitwise:false */
/**
 * @class Ext.util.Base64
 *
 * Base64 is a group of similar binary-to-text encoding schemes that represent binary data
 * in an ASCII string format by translating it into a radix-64 representation.
 *
 * This class is an implementation of base64 encoding and decoding functions and is UTF-8 safe.
 *
 * @singleton
 */
Ext.define('Ext.util.Base64', {
    singleton: true,
    /**
     * @private
     */
    _str: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
    /**
     * Encodes given string in to base64 formatted string
     * @param input
     * @return {string}
     */
    encode: function(input) {
        var me = this,
            output = '',
            i = 0,
            chr1, chr2, chr3, enc1, enc2, enc3, enc4, len;
        input = me._utf8_encode(input);
        len = input.length;
        while (i < len) {
            chr1 = input.charCodeAt(i++);
            chr2 = input.charCodeAt(i++);
            chr3 = input.charCodeAt(i++);
            enc1 = chr1 >> 2;
            enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
            enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
            enc4 = chr3 & 63;
            if (isNaN(chr2)) {
                enc3 = enc4 = 64;
            } else if (isNaN(chr3)) {
                enc4 = 64;
            }
            output = output + me._str.charAt(enc1) + me._str.charAt(enc2) + me._str.charAt(enc3) + me._str.charAt(enc4);
        }
        return output;
    },
    /**
     * Decodes given base64 formatted string
     * @param input
     * @return {string}
     */
    decode: function(input) {
        var me = this,
            output = '',
            i = 0,
            chr1, chr2, chr3, enc1, enc2, enc3, enc4, len;
        input = input.replace(/[^A-Za-z0-9+=\/]/g, "");
        // eslint-disable-line no-useless-escape
        len = input.length;
        while (i < len) {
            enc1 = me._str.indexOf(input.charAt(i++));
            enc2 = me._str.indexOf(input.charAt(i++));
            enc3 = me._str.indexOf(input.charAt(i++));
            enc4 = me._str.indexOf(input.charAt(i++));
            chr1 = (enc1 << 2) | (enc2 >> 4);
            chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
            chr3 = ((enc3 & 3) << 6) | enc4;
            output = output + String.fromCharCode(chr1);
            if (enc3 !== 64) {
                output = output + String.fromCharCode(chr2);
            }
            if (enc4 !== 64) {
                output = output + String.fromCharCode(chr3);
            }
        }
        output = me._utf8_decode(output);
        return output;
    },
    /**
     * @private
     * UTF-8 encoding
     */
    _utf8_encode: function(string) {
        var utftext = '',
            c, n, len;
        string = string.replace(/\r\n/g, "\n");
        for (n = 0 , len = string.length; n < len; n++) {
            c = string.charCodeAt(n);
            if (c < 128) {
                utftext += String.fromCharCode(c);
            } else if ((c > 127) && (c < 2048)) {
                utftext += String.fromCharCode((c >> 6) | 192);
                utftext += String.fromCharCode((c & 63) | 128);
            } else {
                utftext += String.fromCharCode((c >> 12) | 224);
                utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                utftext += String.fromCharCode((c & 63) | 128);
            }
        }
        return utftext;
    },
    /**
     * @private
     * UTF-8 decoding
     */
    _utf8_decode: function(utftext) {
        var string = '',
            i = 0,
            c = 0,
            c3 = 0,
            c2 = 0,
            len = utftext.length;
        while (i < len) {
            c = utftext.charCodeAt(i);
            if (c < 128) {
                string += String.fromCharCode(c);
                i++;
            } else if ((c > 191) && (c < 224)) {
                c2 = utftext.charCodeAt(i + 1);
                string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
                i += 2;
            } else {
                c2 = utftext.charCodeAt(i + 1);
                c3 = utftext.charCodeAt(i + 2);
                string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
                i += 3;
            }
        }
        return string;
    }
});

/**
 * This base class contains utility methods for dealing with formats such as CSV (Comma
 * Separated Values) as specified in <a href="http://tools.ietf.org/html/rfc4180">RFC 4180</a>.
 *
 * The base class implements the mechanics and is governed by these config options:
 *
 *  * `{@link #delimiter}`
 *  * `{@link #lineBreak}`
 *  * `{@link #quote}`
 *
 * These options affect the `{@link #method-encode}` and `{@link #method-decode}` methods.
 * When *decoding*, however, `{@link #lineBreak}` is ignored and instead each line can
 * be separated by any standard line terminator character or character sequence:
 *
 *  * ```\u000a```
 *  * ```\u000d```
 *  * ```\u000d\u000a```
 *
 * Strings which contain the {@link #delimiter} character are quoted using the
 * {@link #quote} character, and any internal {@link #quote} characters are doubled.
 *
 * *Important*
 * While the primary use case is to encode strings, other atomic data types can be encoded
 * as values within a line such as:
 *
 *  * Number
 *  * Boolean
 *  * Date (encoded as an <a href="http://www.iso.org/iso/home/standards/iso8601.htm">ISO 8601</a> date string.)
 *  * null (encoded as an empty string.)
 *  * undefined (encoded as an empty string.)
 *
 * Not that when *decoding*, all data is read as strings. This class does not convert
 * incoming data. To do that, use an {@link Ext.data.reader.Array ArrayReader}.
 *
 * See `{@link Ext.util.CSV}` and  `{@link Ext.util.TSV}` for pre-configured instances.
 *
 * @since 5.1.0
 */
Ext.define('Ext.util.DelimitedValue', {
    /**
     * @cfg {String} dateFormat
     * The {@link Ext.Date#format format} to use for dates
     */
    dateFormat: 'C',
    /**
     * @cfg {String} delimiter
     * The string used to separate the values in a row. Common values for this config
     * are comma (",") and tab ("\t"). See `{@link Ext.util.CSV}` and  `{@link Ext.util.TSV}`
     * for pre-configured instances of these formats.
     */
    delimiter: '\t',
    /**
     * @cfg {String} lineBreak
     * The string used by `{@link #encode}` to separate each row. The `{@link #decode}`
     * method accepts all forms of line break.
     */
    lineBreak: '\n',
    /**
     * @cfg {String} quote
     * The character to use as to quote values that contain the special `delimiter`
     * or `{@link #lineBreak}` characters.
     */
    quote: '"',
    lineBreakRe: /\r?\n/g,
    // match line break at end of input
    lastLineBreakRe: /(\r?\n|\r)$/,
    constructor: function(config) {
        if (config) {
            Ext.apply(this, config);
        }
        this.parseREs = {};
        this.quoteREs = {};
    },
    /* eslint-disable max-len */
    /**
     * Decodes a string of encoded values into an array of rows. Each row is an array of
     * strings.
     *
     * Note that this function does not convert the string values in each column into
     * other data types. To do that, use an {@link Ext.data.reader.Array ArrayReader}.
     *
     * For example:
     *
     *     Ext.util.CSV.decode('"foo ""bar"", bletch",Normal String,2010-01-01T21:45:32.004Z\u000a3.141592653589793,1,false');
     *
     * produces the following array of string arrays:
     *
     *     [
     *         ['foo "bar", bletch','Normal String', '2010-01-01T21:45:32.004Z'],
     *         ['3.141592653589793', '1', 'false']
     *     ]
     *
     * @param {String} input The string to parse.
     *
     * @param {String} [delimiter] The column delimiter to use if the default value
     * of {@link #cfg-delimiter delimiter} is not desired.
     *
     * @param {String} [quoteChar] The character used to quote fields. Defaults to
     * double quote ("). Pass `null` to suppress field quoting.
     *
     * @return {String[][]} An array of rows where each row is an array of Strings.
     */
    decode: function(input, delimiter, quoteChar) {
        /* eslint-enable max-len */
        if (!input) {
            return [];
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            // Check to see if the column delimiter is defined. If not,
            // then default to comma.
            row = [],
            result = [
                row
            ],
            quote = quoteChar !== undefined ? quoteChar : me.quote,
            quoteREs = me.quoteREs,
            parseREs = me.parseREs,
            parseRE, dblQuoteRE, arrMatches, strMatchedDelimiter, strMatchedValue;
        delimiter = delimiter || me.delimiter;
        // Create a regular expression to parse the CSV values unless we already have
        // one for this delimiter/quoteChar.
        parseRE = parseREs[delimiter] || new RegExp(// Delimiters.
        '(\\' + delimiter + '|\\r?\\n|\\r|^)' + // (Optionally) Quoted fields.
        '(?:\\' + quote + '([^\\' + quote + ']*(?:\\' + quote + '\\' + quote + '[^\\' + quote + ']*)*)\\' + quote + '|' + // Standard fields.
        '([^\\' + delimiter + '\\r\\n]*))', 'gi');
        dblQuoteRE = quoteREs[quote] || (quoteREs[quote] = new RegExp('\\' + quote + '\\' + quote, 'g'));
        // Keep looping over the regular expression matches
        // until we can no longer find a match.
        while ((arrMatches = parseRE.exec(input))) {
            strMatchedDelimiter = arrMatches[1];
            // Check to see if the given delimiter has a length
            // (is not the start of string) and if it matches
            // field delimiter. If id does not, then we know
            // that this delimiter is a row delimiter.
            if (strMatchedDelimiter.length && strMatchedDelimiter !== delimiter) {
                // Since we have reached a new row of data,
                // add an empty row to our data array.
                result.push(row = []);
            }
            // we need to account for the first value being empty
            if (!arrMatches.index && arrMatches[0].charAt(0) === delimiter) {
                row.push('');
            }
            // Now that we have our delimiter out of the way,
            // let's check to see which kind of value we
            // captured (quoted or unquoted).
            if (arrMatches[2]) {
                // We found a quoted value. When we capture
                // this value, unescape any double quotes.
                strMatchedValue = arrMatches[2].replace(dblQuoteRE, quote);
            } else {
                // We found a non-quoted value.
                strMatchedValue = arrMatches[3];
            }
            row.push(strMatchedValue);
        }
        return result;
    },
    /* eslint-disable max-len */
    /**
     * Converts a two-dimensional array into an encoded string.
     *
     * For example:
     *
     *     Ext.util.CSV.encode([
     *         ['foo "bar", bletch', 'Normal String', new Date()],
     *         [Math.PI, 1, false]
     *     ]);
     *
     * The above produces the following string:
     *
     *     '"foo ""bar"", bletch",Normal String,2010-01-01T21:45:32.004Z\u000a3.141592653589793,1,false'
     *
     * @param {Mixed[][]} input An array of row data arrays.
     *
     * @param {String} [delimiter] The column delimiter to use if the default value
     * of {@link #cfg-delimiter delimiter} is not desired.
     *
     * @param {String} [quoteChar] The character used to quote fields. Defaults to
     * double quote ("). Pass `null` to suppress field quoting.
     *
     * @return {String} A string in which data items are separated by {@link #delimiter}
     * characters, and rows are separated by {@link #lineBreak} characters.
     */
    encode: function(input, delimiter, quoteChar) {
        /* eslint-enable max-len */
        var me = this,
            delim = delimiter || me.delimiter,
            dateFormat = me.dateFormat,
            quote = quoteChar !== undefined ? quoteChar : me.quote,
            twoQuotes = quote + quote,
            rowIndex = input.length,
            lineBreakRe = me.lineBreakRe,
            result = [],
            outputRow = [],
            col, columnIndex, inputRow;
        while (rowIndex-- > 0) {
            inputRow = input[rowIndex];
            outputRow.length = columnIndex = inputRow.length;
            while (columnIndex-- > 0) {
                col = inputRow[columnIndex];
                if (col == null) {
                    // == null || === undefined
                    col = '';
                } else if (typeof col === 'string') {
                    if (col && quote !== null) {
                        // If the value contains quotes, double them up, and wrap with quotes
                        if (col.indexOf(quote) > -1) {
                            col = quote + col.split(quote).join(twoQuotes) + quote;
                        } else if (col.indexOf(delim) > -1 || lineBreakRe.test(col)) {
                            col = quote + col + quote;
                        }
                    }
                } else if (Ext.isDate(col)) {
                    col = Ext.Date.format(col, dateFormat);
                } else if (col && (isNaN(col) || Ext.isArray(col))) {
                    Ext.raise('Cannot serialize ' + Ext.typeOf(col) + ' into CSV');
                }
                outputRow[columnIndex] = col;
            }
            result[rowIndex] = outputRow.join(delim);
        }
        return result.join(me.lineBreak);
    }
});

/**
 * This class contains utility methods for dealing with CSV (Comma Separated Values) as
 * specified in <a href="http://tools.ietf.org/html/rfc4180">RFC 4180</a>.
 *
 * For details see `{@link Ext.util.DelimitedValue}`.
 *
 * @since 5.1.0
 */
Ext.define('Ext.util.CSV', {
    extend: 'Ext.util.DelimitedValue',
    singleton: true,
    delimiter: ','
});

/**
 * A wrapper class which can be applied to any element. Fires a "click" event while the
 * mouse is pressed. The interval between firings may be specified in the config but
 * defaults to 20 milliseconds.
 *
 * Optionally, a CSS class may be applied to the element during the time it is pressed.
 */
Ext.define('Ext.util.ClickRepeater', {
    alternateClassName: 'Ext.util.TapRepeater',
    mixins: [
        'Ext.mixin.Observable'
    ],
    /**
     * @event mousedown
     * Fires when the mouse button is depressed.
     * @param {Ext.util.ClickRepeater} this
     * @param {Ext.event.Event} e
     */
    /**
     * @event click
     * Fires on a specified interval during the time the element is pressed.
     * @param {Ext.util.ClickRepeater} this
     * @param {Ext.event.Event} e
     */
    /**
     * @event mouseup
     * Fires when the mouse key is released.
     * @param {Ext.util.ClickRepeater} this
     * @param {Ext.event.Event} e
     */
    config: {
        /**
         * @cfg {Ext.dom.Element} [el]
         * The element to listen for clicks/taps on.
         */
        el: null,
        /**
         * @cfg {Ext.Component} [target]
         * The Component who's encapsulating element to listen for clicks/taps on.
         */
        target: null,
        disabled: null
    },
    /**
     * @cfg {String/HTMLElement/Ext.dom.Element} el
     * The element to act as a button.
     */
    /**
     * @cfg {String} pressedCls
     * A CSS class name to be applied to the element while pressed.
     */
    /**
     * @cfg {Boolean} accelerate
     * True if autorepeating should start slowly and accelerate.
     * "interval" and "delay" are ignored.
     */
    /**
     * @cfg {Number} interval
     * The interval between firings of the "click" event (in milliseconds).
     */
    interval: 20,
    /**
     * @cfg {Number} delay
     * The initial delay before the repeating event begins firing.
     * Similar to an autorepeat key delay.
     */
    delay: 250,
    /**
     * @cfg {Boolean} preventDefault
     * True to prevent the default click event
     */
    preventDefault: true,
    /**
     * @cfg {Boolean} stopDefault
     * True to stop the default click event
     */
    stopDefault: false,
    timer: 0,
    /**
    * @cfg {Function/String} handler
    * A function called when the menu item is clicked (can be used instead of {@link #click} event).
    * @cfg {Ext.util.ClickRepeater} handler.clickRepeater This ClickRepeater.
    * @cfg {Ext.event.Event} handler.e The underlying {@link Ext.event.Event}.
    * @controllable
    */
    handler: null,
    /**
     * @cfg {Object} scope
     * The scope (`this` refeence) in which the configured {@link #handler} will be executed,
     * unless the scope is a ViewController method nmame.
     * @accessor
     */
    scope: null,
    /**
     * Creates new ClickRepeater.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        var me = this;
        // Legacy constructor. Element is first parameter
        if (arguments.length === 2) {
            me.setEl(config);
            config = arguments[1];
        }
        me.mixins.observable.constructor.call(this, config);
    },
    destroy: function() {
        this.setEl(null);
        this.callParent();
    },
    privates: {
        fireClick: function(e) {
            var me = this;
            me.fireEvent("click", me, e);
            Ext.callback(me.handler, me.scope, [
                me,
                e
            ], 0, me.getTarget());
        },
        updateDisabled: function(disabled) {
            var me = this;
            if (disabled) {
                me.savedEl = me.getEl();
                me.setEl(null);
            } else if (me.savedEl) {
                me.setEl(me.savedEl);
            }
        },
        updateTarget: function(target) {
            this.setEl(target.el);
        },
        updateEl: function(newEl, oldEl) {
            var me = this,
                elListeners;
            if (oldEl) {
                oldEl.selectable();
                Ext.undefer(me.timer);
                if (me.pressedCls) {
                    oldEl.removeCls(me.pressedCls);
                }
                Ext.getDoc().un('mouseup', me.handleMouseUp, me);
                me.elListeners = Ext.destroy(me.elListeners);
            }
            if (newEl) {
                newEl.unselectable();
                elListeners = {
                    mousedown: me.handleMouseDown,
                    scope: me,
                    destroyable: true
                };
                if (me.preventDefault || me.stopDefault) {
                    elListeners.click = me.eventOptions;
                }
                me.elListeners = newEl.on(elListeners);
            }
        },
        eventOptions: function(e) {
            if (this.preventDefault) {
                e.preventDefault();
            }
            if (this.stopDefault) {
                e.stopEvent();
            }
        },
        handleMouseDown: function(e) {
            var me = this,
                el = me.getEl();
            Ext.undefer(me.timer);
            if (me.pressedCls) {
                el.addCls(me.pressedCls);
            }
            me.mousedownTime = Ext.now();
            if (e.pointerType === 'mouse') {
                el.on("mouseout", me.handleMouseOut, me);
            }
            Ext.getDoc().on("mouseup", me.handleMouseUp, me);
            me.fireEvent("mousedown", me, e);
            me.fireClick(e);
            // Do not honor delay or interval if acceleration wanted.
            if (me.accelerate) {
                me.delay = 400;
            }
            me.timer = Ext.defer(me.click, me.delay || me.interval, me, [
                e
            ]);
            if (me.mousedownPreventDefault) {
                e.preventDefault();
            }
            if (me.mousedownStopEvent) {
                e.stopEvent();
            }
        },
        click: function(e) {
            var me = this;
            me.fireClick(e);
            me.timer = Ext.defer(me.click, me.accelerate ? me.easeOutExpo(Ext.now() - me.mousedownTime, 400, -390, 12000) : me.interval, me, [
                e
            ]);
        },
        easeOutExpo: function(t, b, c, d) {
            return (t === d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
        },
        handleMouseOut: function() {
            var me = this,
                el = me.getEl();
            Ext.undefer(me.timer);
            if (me.pressedCls) {
                el.removeCls(me.pressedCls);
            }
            el.on("mouseover", me.handleMouseReturn, me);
        },
        handleMouseReturn: function(e) {
            var me = this,
                el = me.getEl();
            el.un("mouseover", me.handleMouseReturn, me);
            if (me.pressedCls) {
                el.addCls(me.pressedCls);
            }
            me.click(e);
        },
        handleMouseUp: function(e) {
            var me = this,
                el = me.getEl();
            Ext.undefer(me.timer);
            el.un("mouseover", me.handleMouseReturn, me);
            el.un("mouseout", me.handleMouseOut, me);
            Ext.getDoc().un("mouseup", me.handleMouseUp, me);
            if (me.pressedCls) {
                el.removeCls(me.pressedCls);
            }
            me.fireEvent("mouseup", me, e);
        }
    }
});

/**
 * Utility class for setting/reading values from browser cookies.
 * Values can be written using the {@link #set} method.
 * Values can be read using the {@link #get} method.
 * A cookie can be invalidated on the client machine using the {@link #clear} method.
 */
Ext.define('Ext.util.Cookies', {
    singleton: true,
    /**
     * Creates a cookie with the specified name and value. Additional settings for the cookie
     * may be optionally specified (for example: expiration, access restriction, SSL).
     * @param {String} name The name of the cookie to set.
     * @param {Object} value The value to set for the cookie.
     * @param {Object} [expires] Specify an expiration date the cookie is to persist until.
     * Note that the specified Date object will be converted to Greenwich Mean Time (GMT).
     * @param {String} [path] Setting a path on the cookie restricts access to pages that match
     * that path. Defaults to all pages ('/').
     * @param {String} [domain] Setting a domain restricts access to pages on a given domain
     * (typically used to allow cookie access across subdomains). For example, "sencha.com"
     * will create a cookie that can be accessed from any subdomain of sencha.com, including
     * www.sencha.com, support.sencha.com, etc.
     * @param {Boolean} [secure] Specify true to indicate that the cookie should only be accessible
     * via SSL on a page using the HTTPS protocol. Defaults to false. Note that this will only work
     * if the page calling this code uses the HTTPS protocol, otherwise the cookie will be created
     * with default options.
     */
    set: function(name, value) {
        var argv = arguments,
            argc = argv.length,
            expires = (argc > 2) ? argv[2] : null,
            path = (argc > 3) ? argv[3] : '/',
            domain = (argc > 4) ? argv[4] : null,
            secure = (argc > 5) ? argv[5] : false;
        document.cookie = name + "=" + escape(value) + ((expires === null) ? "" : ("; expires=" + expires.toUTCString())) + ((path === null) ? "" : ("; path=" + path)) + ((domain === null) ? "" : ("; domain=" + domain)) + ((secure === true) ? "; secure" : "");
    },
    /**
     * Retrieves cookies that are accessible by the current page. If a cookie does not exist,
     * `get()` returns null. The following example retrieves the cookie called "valid" and stores
     * the String value in the variable validStatus.
     *
     *     var validStatus = Ext.util.Cookies.get("valid");
     *
     * @param {String} name The name of the cookie to get
     * @return {Object} Returns the cookie value for the specified name;
     * null if the cookie name does not exist.
     */
    get: function(name) {
        var parts = document.cookie.split('; '),
            len = parts.length,
            item, i, ret;
        // In modern browsers, a cookie with an empty string will be stored:
        // MyName=
        // In older versions of IE, it will be stored as:
        // MyName
        // So here we iterate over all the parts in an attempt to match the key.
        for (i = 0; i < len; ++i) {
            item = parts[i].split('=');
            if (item[0] === name) {
                ret = item[1];
                return ret ? unescape(ret) : '';
            }
        }
        return null;
    },
    /**
     * Removes a cookie with the provided name from the browser
     * if found by setting its expiration date to sometime in the past.
     * @param {String} name The name of the cookie to remove
     * @param {String} [path] The path for the cookie.
     * This must be included if you included a path while setting the cookie.
     */
    clear: function(name, path) {
        if (this.get(name)) {
            path = path || '/';
            document.cookie = name + '=' + '; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=' + path;
        }
    }
});

/**
 * @private
 */
Ext.define('Ext.util.ItemCollection', {
    extend: 'Ext.util.MixedCollection',
    alternateClassName: 'Ext.ItemCollection',
    getKey: function(item) {
        return item.getItemId && item.getItemId();
    },
    has: function(item) {
        return this.map.hasOwnProperty(item.getId());
    }
});

/**
 * This class provides a common API to LocalStorage with backwards compatibility for IE.
 * 
 * The primary aspects of this API match the HTML5 standard API except that this class
 * provides a scoping mechanism to isolate property values by instance. This scope is
 * determined from the `id` property. Further, this class does not expose the number of
 * keys in the store as a `length` property as this cannot be maintained reliably without
 * undue cost. Instead there is a `getKeys` method that returns the cached array of keys
 * which is lazily populated on first call.
 * 
 * For example:
 * 
 *      var store = new Ext.util.LocalStorage({
 *              id: 'foo'
 *          });
 * 
 *      store.setItem('bar', 'stuff');
 *      
 *      // Equivalent to:
 *      window.localStorage.setItem('foo-bar', 'stuff');
 * 
 * In all cases, the `id` property is only used by the underlying storage and should not
 * be needed in item access calls or appear when enumerating keys.
 * 
 * To continue with the previous example:
 * 
 *      var keys = store.getKeys();
 *      console.log(keys.length);   // logs 1
 *      console.log(store.key(0));  // logs "bar"
 *
 * ## Sharing Instances
 * 
 * The management of the underlying storage can be broken if multiple instances of this
 * class are created with the same `id` simultaneously. To avoid creating multiple instances
 * with the same `id`, use the `get` method and it will lazily create and share a single
 * instance. When you are done with the shared instance, call `release`.
 * 
 *      var storage = Ext.util.LocalStorage.get('id');
 *      
 *      ...
 *      
 *      storage.release(); // do not call `destroy` as others may be using this object
 *
 * **IMPORTANT:** Do not mix direction instantiation and `get` with the same `id`.
 * 
 * ## Legacy IE
 * 
 * Older IE browsers (specifically IE7 and below) do not support `localStorage` so this
 * class provides equivalent support using the IE proprietary persistence mechanism: the
 * [`userData` behavior](http://msdn.microsoft.com/en-us/library/ms531424(VS.85).aspx). In
 * this mode, the `id` serves as name passed to the `load` and `save` methods and as the
 * suffix on the DOM element added to the `head`.
 * 
 * In this mode, writes to the underlying storage are buffered and delayed for performance
 * reasons. This can be managed using the `flushDelay` config or by directly calling the
 * `save` method.
 *
 * @since 4.2.2
 */
Ext.define('Ext.util.LocalStorage', {
    /**
     * The unique identifier for this store. This config is required to scope this storage
     * distinctly from others. Ultimately, this is used to set a prefix on all keys.
     * @cfg {String} id
     */
    id: null,
    /**
     * @property {Boolean} destroyed
     * This property is set to `true` when the instance's `destroy` method is called.
     * @readonly
     */
    destroyed: false,
    /**
     * @cfg {Boolean} lazyKeys
     * Determines if the keys collection is continuously maintained by this object. By
     * default the keys array is lazily fetched from the underlying store and when keys
     * are removed, the array is discarded. This heuristic tends to be safer than doing
     * the linear removal and array rippling to remove keys from the array on each call
     * to `removeItem`. If the cost of scanning `localStorage` for keys is high enough
     * and if the keys are frequently needed, then this flag can be set to `false` to
     * instruct this class to maintain the keys array once it has been determined.
     */
    lazyKeys: true,
    /**
     * @cfg {String} prefix
     * The prefix to apply to all `localStorage` keys manages by this instance. This does
     * not apply to the legacy IE mechanism but only to the HTML5 `localStorage` keys. If
     * not provided, the `id` property initializes this value with `"id-"`.
     */
    prefix: '',
    /**
     * @cfg {Boolean} session
     * Specify this as `true` to use `sessionStorage` instead of the default `localStoreage`.
     * This option is not supported in legacy IE browsers (IE 6 and 7) and is ignored.
     */
    session: false,
    /**
     * @property {String[]} _keys
     * The array of all key names. This will be `null` if the keys need to be redetermined
     * by the `getKeys` method.
     * @private
     * @readonly
     */
    _keys: null,
    /**
     * @property _store
     * The Storage instance used to store items. This is based on the `session` config.
     * @private
     * @readonly
     */
    _store: null,
    /**
     * @property {Number} _users
     * The number of users that have requested this instance using the `get` method and
     * have not yet called `release`.
     * @private
     * @readonly
     */
    _users: 0,
    statics: {
        cache: {},
        /**
         * Returns a shared instance of the desired local store given its `id`. When you
         * are finished with the returned object call the `release` method:
         * 
         *      var store = Ext.util.LocalStorage.get('foo');
         *      
         *      // .. use store
         *      
         *      store.release();
         * 
         * **NOTE:** Do not mix this call with direct instantiation of the same `id`.
         * @param {String/Object} id The `id` of the desired instance or a config object
         * with an `id` property at a minimum.
         * @return {Ext.util.LocalStorage} The desired instance, created if needed.
         */
        get: function(id) {
            var me = this,
                cache = me.cache,
                config = {
                    _users: 1
                },
                // allow constructor to recognize us as the caller
                instance;
            if (Ext.isString(id)) {
                config.id = id;
            } else {
                Ext.apply(config, id);
            }
            if (!(instance = cache[config.id])) {
                instance = new me(config);
            } else {
                if (instance === true) {
                    Ext.raise('Creating a shared instance of private local store "' + me.id + '".');
                }
                ++instance._users;
            }
            return instance;
        },
        /**
         * This will be `true` if some form of local storage is supported or `false` if not.
         * @property {Boolean} supported
         * @readonly
         */
        supported: true
    },
    constructor: function(config) {
        var me = this;
        Ext.apply(me, config);
        if (!me.hasOwnProperty('id')) {
            Ext.raise("No id was provided to the local store.");
        }
        if (me._users) {
            // When we are created by get() for shared use, the _users property is set to
            // 1... so this means we are being created for shared use: add this instance
            // to the cache.
            Ext.util.LocalStorage.cache[me.id] = me;
        } else {
            // else we are being created directly so check that this id is not also in the
            // cache ... that would be "extraordinarily bad".
            if (Ext.util.LocalStorage.cache[me.id]) {
                Ext.raise('Cannot create duplicate instance of local store "' + me.id + '". Use Ext.util.LocalStorage.get() to share instances.');
            }
            // We put true in the cache to be detectable by get() for diagnostic reasons
            // but no "this" to avoid leaking the store:
            Ext.util.LocalStorage.cache[me.id] = true;
        }
        me.init();
    },
    /**
     * Initializes this instance.
     * @private
     */
    init: function() {
        var me = this,
            id = me.id;
        if (!me.prefix && id) {
            me.prefix = id + '-';
        }
        me._store = (me.session ? window.sessionStorage : window.localStorage);
    },
    /**
     * Destroys this instance and for legacy IE, ensures data is flushed to persistent
     * storage. This method should not be called directly on instances returned by the
     * `get` method. Call `release` instead for such instances.
     * 
     * *NOTE:* For non-legacy IE browsers, there is no harm in failing to call this
     * method. In legacy IE, however, failing to call this method can result in memory
     * leaks.
     */
    destroy: function() {
        var me = this;
        if (me._users) {
            Ext.log.warn('LocalStorage(id=' + me.id + ') destroyed while in use');
        }
        delete Ext.util.LocalStorage.cache[me.id];
        me._store = me._keys = null;
        me.callParent();
    },
    /**
     * Returns the keys for this storage.
     * @return {String[]} The keys for this storage. This array should be considered as
     * readonly.
     */
    getKeys: function() {
        var me = this,
            store = me._store,
            prefix = me.prefix,
            keys = me._keys,
            n = prefix.length,
            i, key;
        if (!keys) {
            me._keys = keys = [];
            for (i = store.length; i--; ) {
                key = store.key(i);
                if (key.length > n) {
                    if (prefix === key.substring(0, n)) {
                        keys.push(key.substring(n));
                    }
                }
            }
        }
        return keys;
    },
    /**
     * Call this method when finished with an instance returned by `get` instead of calling
     * `destroy`. When the last shared use of this instance calls `release`, the `destroy`
     * method is called automatically.
     * 
     * *NOTE:* Failing to call this method will result in memory leaks.
     */
    release: function() {
        if (!--this._users) {
            this.destroy();
        }
    },
    /**
     * @method
     * @static
     * @private
     */
    save: Ext.emptyFn,
    /**
     * Removes all of the keys of this storage.
     * **NOTE:** This method conforms to the standard HTML5 Storage interface.
     */
    clear: function() {
        var me = this,
            store = me._store,
            prefix = me.prefix,
            keys = me._keys || me.getKeys(),
            i;
        for (i = keys.length; i--; ) {
            store.removeItem(prefix + keys[i]);
        }
        keys.length = 0;
    },
    /**
     * Returns the specified key given its `index`. These keys have the scoping prefix
     * removed so they match what was passed to `setItem`.
     * **NOTE:** This method conforms to the standard HTML5 Storage interface.
     * @param {Number} index The index of the desired key.
     * @return {String} The key.
     */
    key: function(index) {
        var keys = this._keys || this.getKeys();
        return (0 <= index && index < keys.length) ? keys[index] : null;
    },
    /**
     * Returns the value associated with the given `key`.
     * **NOTE:** This method conforms to the standard HTML5 Storage interface.
     * @param {String} key The key.
     * @return {String} The value associated with the given `key`.
     */
    getItem: function(key) {
        var k = this.prefix + key;
        return this._store.getItem(k);
    },
    /**
     * Removes the value associated with the given `key`.
     * **NOTE:** This method conforms to the standard HTML5 Storage interface.
     * @param {String} key The key.
     */
    removeItem: function(key) {
        var me = this,
            k = me.prefix + key,
            store = me._store,
            keys = me._keys,
            length = store.length;
        store.removeItem(k);
        if (keys && length !== store.length) {
            if (me.lazyKeys) {
                me._keys = null;
            } else {
                Ext.Array.remove(keys, key);
            }
        }
    },
    /**
     * Sets the value associated with the given `key`.
     * **NOTE:** This method conforms to the standard HTML5 Storage interface.
     * @param {String} key The key.
     * @param {String} value The new associated value for `key`.
     */
    setItem: function(key, value) {
        var me = this,
            k = me.prefix + key,
            store = me._store,
            length = store.length,
            keys = me._keys;
        store.setItem(k, value);
        if (keys && length !== store.length) {
            // Good news here - maintaining the keys collection is easy in this case.
            keys.push(key);
        }
    }
});

/**
 * This class manages a set of numeric spans (2-element arrays marking begin and end
 * points). The method of this class coalesce and split spans as necessary to store
 * the fewest possible pairs needed to represent the covered (one-dimensional) area.
 *
 * @private
 * @since 6.5.0
 */
Ext.define('Ext.util.Spans', {
    isSpans: true,
    constructor: function() {
        this.spans = this.spans || [];
    },
    /**
     * Clears all spans.
     * @return {Ext.util.Spans} This Spans object.
     */
    clear: function() {
        this.spans.length = 0;
        return this;
    },
    /**
     * Adds a new span to the current set of spans. This will coalesce adjacent spans
     * as necessary to store the minimum number of spans possible.
     *
     * @param {Number/Number[]} begin Either the beginning of the span or a 2-element
     * array of `[begin,end]`.
     * @param {Number} [end] If `begin` is just the position, the second argument is
     * the end of the span to add. This value is exclusive of the span, that is it
     * marks the first position beyond the span. This ensures that `end - begin` is
     * the length of the span.
     * @return {Boolean} `true` if the new span changes this object, `false` if the
     * span was already in the set.
     */
    add: function(begin, end) {
        if (end === undefined) {
            if (typeof begin === 'number') {
                end = begin + 1;
            } else {
                end = begin[1];
                begin = begin[0];
            }
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            spans = me.spans,
            b, e, first, last, span;
        first = me.bisect(begin);
        if (first) {
            // if (there is a previous span)
            span = spans[first - 1];
            b = span[0];
            e = span[1];
            if (begin <= e) {
                // This new span touches the previous one, but perhaps this new
                // span is contained inside the previous span...
                if (end <= e) {
                    return false;
                }
                // no change
                // The new begin touches the previous span, but the new end goes
                // beyond it, so we extend it by replacing it.
                begin = b;
                spans.splice(--first, 1);
            }
        }
        // Now there is either no previous span, or if there was one, it does
        // not touch the new one.
        last = me.bisect(end);
        if (last > first) {
            // If we are replacing any spans, make sure the new "end" is at
            // least as large as the end of the last span we are replacing.
            span = spans[last - 1];
            end = Math.max(end, span[1]);
        }
        if (last < spans.length) {
            span = spans[last];
            // The span beyond our new span may be touching the end of the
            // new span, in which case we need to coalesce there as well.
            // Since we are removing it, we need to expand "end" to include
            // this additional span.
            if (end === span[0]) {
                end = span[1];
                ++last;
            }
        }
        spans.splice(first, last - first, [
            begin,
            end
        ]);
        return true;
    },
    /**
     * Returns `true` if the given span is fully in the current set of spans.
     * @param {Number/Number[]} begin Either the beginning of the span or a 2-element
     * array of `[begin,end]`.
     * @param {Number} [end] If `begin` is just the position, the second argument is
     * the end of the span to add. This value is exclusive of the span, that is it
     * marks the first position beyond the span. This ensures that `end - begin` is
     * the length of the span.
     * @return {Boolean}
     */
    contains: function(begin, end) {
        if (end === undefined) {
            if (typeof begin === 'number') {
                end = begin + 1;
            } else {
                end = begin[1];
                begin = begin[0];
            }
        }
        // eslint-disable-next-line vars-on-top
        var spans = this.spans,
            index = this.bisect(begin),
            ret = false,
            e, span;
        if (index && begin < (e = spans[index - 1][1])) {
            ret = end <= e;
        } else if (index < spans.length) {
            span = spans[index];
            ret = span[0] <= begin && end <= span[1];
        }
        return ret;
    },
    /**
     * Calls the passed function for every integer in every span.
     * @param {Function} fn The function to call. Returning `false` will abort the operation.
     * @param {Mixed} scope The scope (`this` reference) in which the function will execute.
     */
    each: function(fn, scope) {
        var spans = this.spans,
            len = spans.length,
            i, span, j;
        for (i = 0; i < len; i++) {
            span = spans[i];
            for (j = span[0]; j < span[1]; j++) {
                if (fn.call(scope || this, i) === false) {
                    return;
                }
            }
        }
    },
    /**
     * Returns `true` if the specified span intersects with the current set of spans.
     *
     * @param {Number/Number[]} begin Either the beginning of the span or a 2-element
     * array of `[begin,end]`.
     * @param {Number} [end] If `begin` is just the position, the second argument is
     * the end of the span to add. This value is exclusive of the span, that is it
     * marks the first position beyond the span. This ensures that `end - begin` is
     * the length of the span.
     * @return {Boolean}
     */
    intersects: function(begin, end) {
        if (end === undefined) {
            if (typeof begin === 'number') {
                end = begin + 1;
            } else {
                end = begin[1];
                begin = begin[0];
            }
        }
        // eslint-disable-next-line vars-on-top
        var spans = this.spans,
            index = this.bisect(begin),
            ret = false;
        if (index && begin < spans[index - 1][1]) {
            ret = true;
        } else if (index < spans.length) {
            ret = spans[index][0] < end;
        }
        return ret;
    },
    /**
     * Removes a span from the current set of spans. This will coalesce adjacent spans
     * as necessary to store the minimum number of spans possible.
     *
     * @param {Number/Number[]} begin Either the beginning of the span or a 2-element
     * array of `[begin,end]`.
     * @param {Number} [end] If `begin` is just the position, the second argument is
     * the end of the span to add. This value is exclusive of the span, that is it
     * marks the first position beyond the span. This ensures that `end - begin` is
     * the length of the span.
     * @return {Boolean} `true` if removing the span changes this object, `false` if the
     * span was not in the set.
     */
    remove: function(begin, end) {
        if (end === undefined) {
            if (typeof begin === 'number') {
                end = begin + 1;
            } else {
                end = begin[1];
                begin = begin[0];
            }
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            spans = me.spans,
            first = me.bisect(begin),
            ret = false,
            last, span, tmp;
        if (first) {
            span = spans[first - 1];
            tmp = span[1];
            if (begin < tmp) {
                span[1] = begin;
                if (end < tmp) {
                    spans.splice(first, 0, [
                        end,
                        tmp
                    ]);
                    return true;
                }
                ret = true;
            }
        }
        last = me.bisect(end);
        if (first < last) {
            ret = true;
            span = spans[last - 1];
            if (end < span[1]) {
                span[0] = end;
                --last;
            }
            last -= first;
            if (last) {
                spans.splice(first, last);
            }
        }
        return ret;
    },
    /**
     * Returns an object that holds the current state and can be passed back later
     * to `unstash` to restore that state.
     * @return {Object}
     */
    stash: function() {
        return this.spans.slice();
    },
    /**
     * Takes an object a state object returned by `stash` and makes that the current
     * state.
     * @return {Ext.util.Spans} This Spans object.
     */
    unstash: function(pickle) {
        this.spans = pickle;
        return this;
    },
    /**
     * @return {Number} the number of integer locations covered by all the spans.
     */
    getCount: function() {
        var spans = this.spans,
            len = spans.length,
            result = 0,
            i, span;
        for (i = 0; i < len; i++) {
            span = spans[i];
            result += span[1] - span[0];
        }
        return result;
    },
    privates: {
        bisect: function(value) {
            return Ext.Number.bisectTuples(this.spans, value, 0);
        }
    }
});

/**
 * This class contains utility methods for dealing with TSV (Tab Separated Values) as
 * specified in 
 * <a href="https://www.iana.org/assignments/media-types/text/tab-separated-values">
 * IANA MIME type for text/tab-separated-values</a>.
 *
 * For details see `{@link Ext.util.DelimitedValue}`.
 *
 * @since 5.1.0
 */
Ext.define('Ext.util.TsvDecoder', {
    extend: 'Ext.util.DelimitedValue',
    alternateClassName: 'Ext.util.TSV',
    delimiter: '\t'
}, function(TSVClass) {
    /*
     * @singleton
     * @class Ext.util.TSV
     * @alternateClassName Ext.util.TsvDecoder
     */
    Ext.util.TSV = new TSVClass();
});

// @tag core
/**
 * A static {@link Ext.util.TaskRunner} instance that can be used to start and stop
 * arbitrary tasks. See {@link Ext.util.TaskRunner} for supported methods and task
 * config properties.
 *
 *     @example
 *     var task, clock;
 *     
 *     clock = Ext.getBody().appendChild({
 *         id: 'clock'
 *     });
 *     
 *     // Start a simple clock task that updates a div once per second 
 *     task = {
 *         run: function() {
 *             clock.setHtml(Ext.Date.format(new Date(), 'g:i:s A'));
 *         },
 *         interval: 1000
 *     };
 *     
 *     Ext.TaskManager.start(task);
 *
 * See the {@link #start} method for details about how to configure a task object.
 */
Ext.define('Ext.util.TaskManager', {
    extend: 'Ext.util.TaskRunner',
    alternateClassName: [
        'Ext.TaskManager'
    ],
    singleton: true
});

/**
 * Provides precise pixel measurements for blocks of text so that you can determine 
 * the exact pixel height and width of a block of text. 
 * 
 * **Note:** The TextMetrics tool should only be utilized to measure plain text. Attempting to 
 * measure text that includes HTML may return inaccurate results.
 *
 * This measurement works by copying the relevant font-related CSS styles from the element  
 * param to the TextMetrics' cached measurement element.  This returns the dimensions of the cached
 * element wrapping the text.  By default, the wrapping element is auto-sized.  
 * You must provide a **fixed width** if the passed text is multi-lined.
 *
 * When multiple measurements are being done with the same element styling, you should 
 * create a single, reusable TextMetrics instance.  This is more efficient than using the 
 * static {@link #measure} method.  The element styles are copied to the cached 
 * TextMetrics element once during instantiation versus repeated copying using 
 * _measure()_.
 *
 * The following example demonstrates the recommended use of TextMetrics where the custom 
 * textfield class sets up a reusable TextMetrics instance used to measure the label 
 * width. This example assumes that all instances of _mytextfield_ have the same 
 * {@link Ext.form.Labelable#labelClsExtra labelClsExtra} and 
 * {@link Ext.form.Labelable#labelStyle labelStyle} configs.
 *
 *     Ext.define('MyApp.view.MyTextField', {
 *         extend: 'Ext.form.field.Text',
 *         xtype: 'mytextfield',
 *     
 *         initComponent: function () {
 *             var me = this,
 *                 tm = me.getTextMetrics();
 *      
 *             me.labelWidth = tm.getWidth(me.fieldLabel + me.labelSeparator);
 *             me.callParent();
 *         },
 *     
 *         getTextMetrics: function () {
 *             var me = this,
 *                 // Using me.self allows labelCls etc. to vary by derived
 *                 // class, but not by instance.
 *                 cls = me.self,
 *                 tm = cls.measurer,
 *                 el;
 *     
 *             if (!tm) {
 *                 el = Ext.getBody().createChild();
 *                 el.addCls(me.labelCls + ' ' + me.labelClsExtra).
 *                     applyStyles(me.labelStyle);
 *     
 *                 cls.measurer = tm = new Ext.util.TextMetrics(el);
 *             }
 *     
 *             return tm;
 *         }
 *     });
 *
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Contact Info',
 *         width: 600,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items: [{
 *             xtype: 'mytextfield',
 *             fieldLabel: 'Name',
 *             labelStyle: 'font-size: 10px;'
 *         }, {
 *             xtype: 'mytextfield',
 *             fieldLabel: 'Email Address',
 *             labelStyle: 'font-size: 10px;'
 *         }]
 *     });
 *
 * While less efficient than the preceding example, this example allows each instance of 
 * _mytextfield2_ to have unique labelClsExtra and labelStyle configs.  Each custom 
 * textfield instance uses the static TextMetrics measure method which will copy the 
 * label styles repeatedly, thus being less efficient but more versatile.
 *
 *     Ext.define('MyApp.view.MyTextField2', {
 *         extend: 'Ext.form.field.Text',
 *         xtype: 'mytextfield2',
 *     
 *         initComponent: function () {
 *             var me = this,
 *                 el = me.getMeasurementEl(),
 *                 tm = Ext.util.TextMetrics;
 *     
 *             me.labelWidth = tm.measure(el, me.fieldLabel + me.labelSeparator).width;
 *             me.callParent();
 *         },
 *        
 *         getMeasurementEl: function () {
 *             var me = this,
 *                 cls = MyApp.view.MyTextField2,
 *                 el = cls.measureEl;
 *     
 *             if (!el) {
 *                 cls.measureEl = el = Ext.getBody().createChild();
 *             }
 *     
 *             el.dom.removeAttribute('style');
 *             el.removeCls(el.dom.className).
 *                 addCls(me.labelCls + ' ' + me.labelClsExtra).
 *                 applyStyles(me.labelStyle);
 *     
 *             return el;
 *         }
 *     });
 *     
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Contact Info',
 *         width: 600,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items: [{
 *             xtype: 'mytextfield2',
 *             fieldLabel: 'Name',
 *             labelStyle: 'font-size: 14px;font-weight: bold;',
 *             labelClsExtra: 'nameLabel'
 *         }, {
 *             xtype: 'mytextfield2',
 *             fieldLabel: 'Email Address',
 *             labelStyle: 'font-size: 10px;',
 *             labelClsExtra: 'emailLabel'
 *         }]
 *     });
 */
Ext.define('Ext.util.TextMetrics', {
    requires: [
        'Ext.dom.Element'
    ],
    statics: {
        shared: null,
        /**
         * Measures the size of the specified text
         * @param {String/HTMLElement} el The element, dom node or id from which to copy existing
         * CSS styles that can affect the size of the rendered text
         * @param {String} text The text to measure
         * @param {Number} [fixedWidth] If the text will be multiline, you have to set a fixed width
         * in order to accurately measure the text height
         * @return {Object} An object containing the text's size
         * `{width: (width), height: (height)}`
         * @static
         */
        measure: function(el, text, fixedWidth) {
            var me = this,
                shared = me.shared || (me.shared = new me(el, fixedWidth));
            shared.bind(el);
            shared.setFixedWidth(fixedWidth || 'auto');
            return shared.getSize(text);
        },
        /**
         * Destroy the TextMetrics instance created by {@link #measure}.
         * @static
         */
        destroy: function() {
            this.shared = Ext.destroy(this.shared);
        }
    },
    /**
     * Creates new TextMetrics.
     * @param {String/HTMLElement/Ext.dom.Element} bindTo The element or its ID to bind to.
     * @param {Number} [fixedWidth] A fixed width to apply to the measuring element.
     */
    constructor: function(bindTo, fixedWidth) {
        var me = this,
            measure = me.measure = Ext.getBody().createChild({
                // tell the spec runner to ignore this element when checking if the dom is clean 
                'data-sticky': true,
                role: 'presentation',
                cls: Ext.baseCSSPrefix + 'textmetrics',
                style: {
                    position: 'absolute',
                    left: '-1000px',
                    top: '-1000px',
                    visibility: 'hidden'
                }
            });
        if (bindTo) {
            me.bind(bindTo);
        }
        if (fixedWidth) {
            measure.setWidth(fixedWidth);
        }
    },
    /**
     * Returns the size of the specified text based on the internal element's style and width
     * properties
     * @param {String} text The text to measure
     * @return {Object} An object containing the text's size `{width: (width), height: (height)}`
     */
    getSize: function(text) {
        var measure = this.measure,
            size;
        measure.setHtml(text);
        size = measure.getSize();
        measure.setHtml('');
        return size;
    },
    /**
     * Binds this TextMetrics instance to a new element
     * @param {String/HTMLElement/Ext.dom.Element} el The element or its ID.
     */
    bind: function(el) {
        this.measure.setStyle(// Create an Ext.dom.Fly instance on our prototype unless we've already been through
        // here. Attach it to the passed HTMLElement/Ext.Element
        (this.el || (this.self.prototype.el = new Ext.dom.Fly())).attach(el).getStyle([
            'font-size',
            'font-size-adjust',
            'font-style',
            'font-weight',
            'font-family',
            'font-kerning',
            'font-stretch',
            'line-height',
            'text-transform',
            'text-decoration',
            'letter-spacing',
            'word-break'
        ]));
    },
    /**
     * Sets a fixed width on the internal measurement element.  If the text will be multiline,
     * you have to set a fixed width in order to accurately measure the text height.
     * @param {Number} width The width to set on the element
     */
    setFixedWidth: function(width) {
        this.measure.setWidth(width);
    },
    /**
      * Returns the measured width of the specified text
      * @param {String} text The text to measure
      * @return {Number} width The width in pixels
      */
    getWidth: function(text) {
        this.measure.dom.style.width = 'auto';
        return this.getSize(text).width;
    },
    /**
      * Returns the measured height of the specified text
      * @param {String} text The text to measure
      * @return {Number} height The height in pixels
      */
    getHeight: function(text) {
        return this.getSize(text).height;
    },
    /**
      * Destroy this instance
      */
    destroy: function() {
        var me = this;
        me.el = me.measure = Ext.destroy(me.measure);
        me.callParent();
    }
}, function() {
    Ext.Element.override({
        /**
         * Returns the width in pixels of the passed text, or the width of the text in this Element.
         * @param {String} text The text to measure. Defaults to the innerHTML of the element.
         * @param {Number} [min] The minumum value to return.
         * @param {Number} [max] The maximum value to return.
         * @return {Number} The text width in pixels.
         * @member Ext.dom.Element
         */
        getTextWidth: function(text, min, max) {
            return Ext.Number.constrain(Ext.util.TextMetrics.measure(this.dom, Ext.valueFrom(text, this.dom.innerHTML, true)).width, min || 0, max || 1000000);
        }
    });
});

/**
 * @private
 */
Ext.define('Ext.util.paintmonitor.OverflowChange', {
    extend: 'Ext.util.paintmonitor.Abstract',
    eventName: Ext.browser.is.Firefox ? 'overflow' : 'overflowchanged',
    monitorClass: 'overflowchange',
    onElementPainted: function(e) {
        this.getCallback().apply(this.getScope(), this.getArgs());
    }
});

/**
 * @private
 */
Ext.define('Ext.util.sizemonitor.OverflowChange', {
    extend: 'Ext.util.sizemonitor.Abstract',
    constructor: function(config) {
        this.onExpand = this.onExpand.bind(this);
        this.onShrink = this.onShrink.bind(this);
        this.callParent(arguments);
    },
    getElementConfig: function() {
        return {
            reference: 'detectorsContainer',
            classList: [
                Ext.baseCSSPrefix + 'size-monitors',
                'overflowchanged'
            ],
            children: [
                {
                    reference: 'expandMonitor',
                    className: 'expand',
                    children: [
                        {
                            reference: 'expandHelper'
                        }
                    ]
                },
                {
                    reference: 'shrinkMonitor',
                    className: 'shrink',
                    children: [
                        {
                            reference: 'shrinkHelper'
                        }
                    ]
                }
            ]
        };
    },
    bindListeners: function(bind) {
        var method = bind ? 'addEventListener' : 'removeEventListener';
        this.expandMonitor[method](Ext.browser.is.Firefox ? 'underflow' : 'overflowchanged', this.onExpand, true);
        this.shrinkMonitor[method](Ext.browser.is.Firefox ? 'overflow' : 'overflowchanged', this.onShrink, true);
    },
    onExpand: function(e) {
        // eslint-disable-next-line max-len
        if (!(this.destroyed || (Ext.browser.is.Webkit && e.horizontalOverflow && e.verticalOverflow))) {
            Ext.TaskQueue.requestRead('refresh', this);
        }
    },
    onShrink: function(e) {
        // eslint-disable-next-line max-len
        if (!(this.destroyed || (Ext.browser.is.Webkit && !e.horizontalOverflow && !e.verticalOverflow))) {
            Ext.TaskQueue.requestRead('refresh', this);
        }
    },
    refreshMonitors: function() {
        if (this.destroying || this.destroyed) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var expandHelper = this.expandHelper,
            shrinkHelper = this.shrinkHelper,
            contentBounds = this.getContentBounds(),
            width = contentBounds.width,
            height = contentBounds.height,
            style;
        if (expandHelper && !expandHelper.destroyed) {
            style = expandHelper.style;
            style.width = (width + 1) + 'px';
            style.height = (height + 1) + 'px';
        }
        if (shrinkHelper && !shrinkHelper.destroyed) {
            style = shrinkHelper.style;
            style.width = width + 'px';
            style.height = height + 'px';
        }
        Ext.TaskQueue.requestRead('refresh', this);
    },
    destroy: function() {
        // These are closures so Base destructor won't null them
        this.onExpand = this.onShrink = null;
        this.callParent();
    }
});

/**
 * @class Ext.util.translatable.CssPosition
 * @private
 */
Ext.define('Ext.util.translatable.CssPosition', {
    extend: 'Ext.util.translatable.Dom',
    alias: 'translatable.cssposition',
    // also configures Factoryable
    doTranslate: function(x, y) {
        var domStyle = this.getElement().dom.style;
        if (typeof x === 'number') {
            domStyle.left = x + 'px';
        }
        if (typeof y === 'number') {
            domStyle.top = y + 'px';
        }
    },
    syncPosition: function() {
        var domStyle = this.getElement().dom.style;
        return [
            this.x = parseFloat(domStyle.left),
            this.y = parseFloat(domStyle.top)
        ];
    },
    destroy: function() {
        var domStyle = this.getElement().dom.style;
        domStyle.left = null;
        domStyle.top = null;
        this.callParent();
    }
});

/**
 * @private
 *
 * CSS Transform implementation
 */
Ext.define('Ext.util.translatable.CssTransform', {
    extend: 'Ext.util.translatable.Dom',
    alias: 'translatable.csstransform',
    // also configures Factoryable
    isCssTransform: true,
    posRegex: /(\d+)px[^\d]*(\d+)px/,
    doTranslate: function(x, y) {
        this.getElement().translate(x, y);
    },
    syncPosition: function() {
        var pos = this.posRegex.exec(this.getElement().dom.style.tranform);
        if (pos) {
            this.x = parseFloat(pos[1]);
            this.y = parseFloat(pos[2]);
        }
        return [
            this.x,
            this.y
        ];
    },
    destroy: function() {
        var element = this.getElement();
        if (element && !element.destroyed) {
            element.dom.style.webkitTransform = null;
        }
        this.callParent();
    }
});

/**
 * @private
 *
 * Translates the element by setting the scroll position of its parent node.
 */
Ext.define('Ext.util.translatable.ScrollParent', {
    extend: 'Ext.util.translatable.Dom',
    alias: 'translatable.scrollparent',
    // also configures Factoryable
    isScrollParent: true,
    applyElement: function(element) {
        var el = Ext.get(element);
        if (el) {
            this.parent = el.parent();
        }
        return el;
    },
    doTranslate: function(x, y) {
        var parent = this.parent;
        parent.setScrollLeft(Math.round(-x));
        parent.setScrollTop(Math.round(-y));
    },
    getPosition: function() {
        var me = this,
            position = me.position,
            parent = me.parent;
        position.x = parent.getScrollLeft();
        position.y = parent.getScrollTop();
        return position;
    }
});

/**
 * @class Ext.app.ViewController
 */
/**
 * @method beforeRender
 * @template
 * Template method called by the owning component's 
 * {@link Ext.Component#method-beforeRender beforeRender} method.
 * @param {Ext.Component} component The owner component attached to the 
 * ViewController
 */
/**
 * @method afterRender
 * @template
 * Template method called by the owning component's 
 * {@link Ext.Component#method-afterRender afterRender} method.
 * @param {Ext.Component} component The owner component attached to the 
 * ViewController
 */
/**
 * @method boxReady
 * @template
 * Template method called by the owning component's 
 * {@link Ext.Component#method-onBoxReady onBoxReady} method.
 * @param {Ext.Component} component The owner component attached to the 
 * ViewController
 */

/**
 * @class Ext.form.field.Checkbox
 */
Ext.define(null, {
    override: 'Ext.form.field.Checkbox',
    compatibility: Ext.isIE8,
    // IE8 does not support change event but it has propertychange which is even better
    changeEventName: 'propertychange',
    onChangeEvent: function(e) {
        // IE8 propertychange fires for *any* property change but we're only interested in checked
        // We also don't want to react to propertychange fired as the result of assigning
        // checked property in setRawValue().
        if (this.duringSetRawValue || e.browserEvent.propertyName !== 'checked') {
            return;
        }
        this.callParent([
            e
        ]);
    },
    updateCheckedCls: function(checked) {
        var me = this,
            displayEl = me.displayEl;
        me.callParent([
            checked
        ]);
        // IE8 has a bug with font icons and pseudo-elements
        if (displayEl && checked !== me.lastValue) {
            displayEl.repaint();
        }
    }
});

/**
 * @class Ext.form.field.Radio
 */
Ext.define(null, {
    override: 'Ext.form.field.Radio',
    compatibility: Ext.isIE8,
    getSubTplData: function(fieldData) {
        var data = this.callParent([
                fieldData
            ]);
        // Rendering a radio button with checked attribute
        // will have a curious side effect in IE8: the DOM
        // node will have checked property set to `true` but
        // radio group (radios with the same name attribute)
        // will behave as if no radio is checked in the group;
        // tabbing into the group will select first or last
        // button instead of the checked one.
        // So instead of rendering the attribute we will set
        // checked value in the DOM after rendering. Apparently
        // such a tiny nudge is enough for the browser to behave.
        delete data.checked;
        return data;
    },
    afterRender: function() {
        this.callParent();
        if (this.checked) {
            this.inputEl.dom.checked = true;
        }
    },
    onChange: function(newValue, oldValue) {
        // We don't need to bother updating other radio buttons in IE8
        // since it will fire propertychange event on any change, not only false -> true.
        // This is unlike standard compliant browsers, see main class.
        this.callSuper([
            newValue,
            oldValue
        ]);
    }
});

/**
 * @class Ext.scroll.Scroller
 */
Ext.define(null, {
    override: 'Ext.scroll.Scroller',
    compatibility: Ext.isIE8,
    privates: {
        // Important note: this code had to be copied as a whole
        // because the scrollLeft assignment trickery only works
        // reliably when it is done within the same function context.
        doScrollTo: function(x, y, animate) {
            var me = this,
                element = me.getScrollElement(),
                maxPosition, dom, to, xInf, yInf, ret, deferred, callback;
            if (element && !element.destroyed) {
                dom = element.dom;
                xInf = (x === Infinity);
                yInf = (y === Infinity);
                if (xInf || yInf) {
                    maxPosition = me.getMaxPosition();
                    if (xInf) {
                        x = maxPosition.x;
                    }
                    if (yInf) {
                        y = maxPosition.y;
                    }
                }
                if (x !== null) {
                    x = me.convertX(x);
                }
                if (animate) {
                    to = {};
                    if (y != null) {
                        to.scrollTop = y;
                    }
                    if (x != null) {
                        to.scrollLeft = x;
                    }
                    animate = Ext.mergeIf({
                        to: {
                            scrollTop: y,
                            scrollLeft: x
                        }
                    }, animate);
                    deferred = new Ext.Deferred();
                    callback = animate.callback;
                    animate.callback = function() {
                        if (callback) {
                            callback.call(animate.scope || Ext.global, arguments);
                        }
                        // The callback will be called if the element is destroyed
                        if (me.destroyed) {
                            deferred.reject();
                        } else {
                            deferred.resolve();
                        }
                    };
                    element.animate(animate);
                    ret = deferred.promise;
                } else {
                    // When we need to assign both scrollTop and scrollLeft,
                    // IE8 might fire scroll event on the first assignment
                    // but not on the second; that behavior is unlike the other
                    // browsers which will wait for the second assignment
                    // to happen before firing the event. This leads to our
                    // scrollstart event firing prematurely, when the scrolling
                    // has not actually finished yet.
                    // To work around that, we ignore the first event and then
                    // force another one by assigning scrollLeft the second time.
                    if ((x != null && x !== 0) && y != null) {
                        me.deferDomScroll = true;
                    }
                    if (y != null) {
                        dom.scrollTop = y;
                    }
                    if (x != null) {
                        dom.scrollLeft = x;
                    }
                    if (me.deferDomScroll) {
                        me.deferDomScroll = false;
                        // Reading the DOM makes sure the second assignment will fire the event.
                        // eslint-disable-next-line no-unused-expressions
                        +dom.scrollLeft;
                        dom.scrollLeft = x;
                        // eslint-disable-next-line no-unused-expressions
                        +dom.scrollTop;
                        dom.scrollTop = y;
                    }
                    ret = Ext.Deferred.getCachedResolved();
                }
                // Our position object will need refreshing before returning.
                me.positionDirty = true;
            } else {
                ret = Ext.Deferred.getCachedRejected();
            }
            return ret;
        },
        onDomScroll: function() {
            var me = this;
            if (me.deferDomScroll) {
                return;
            }
            me.callParent();
        }
    }
});

/**
 * An Action encapsulates a shareable, reusable set of properties which define a "clickable"
 * UI component such as a {@link Ext.button.Button button} or {@link Ext.menu.Item menu item},
 * or {@link Ext.panel.Panel#tools panel header tool}, or an
 * {@link Ext.grid.column.Action ActionColumn item}
 * 
 * Actions let you share handlers, configuration options and UI updates across any components
 * which were created using the Action.
 *
 * You do not have to create Action instances. They can be configured into Views
 * using the {@link Ext.container.Container#actions actions} config.
 *
 * Use a reference to an Action as the config object for any number of UI Components which share
 * the same configuration. The Action not only supplies the configuration, but allows all Components
 * based upon it to have a common set of methods called at once through a single call to the Action.
 *
 * Any Component that is to be configured with an Action may support
 * the following methods:
 *
 * - setText(String)
 * - setIconCls(String)
 * - setGlyph(String)
 * - setDisabled(Boolean)
 * - setVisible(Boolean)
 * - setHandler(String)
 *
 * This allows the Action to control its associated Components. Use a
 * {@link Ext.container.Container#getAction reference to an Action} to control
 * all components created from that action.
 *
 * Example usage:
 *
 *     @example
 *     Ext.define('ActionsExampleController', {
 *         extend: 'Ext.app.ViewController',
 *         alias: 'controller.actionsexample',
 *
 *         onOperationClick: function() {
 *             Ext.Msg.alert('Click', 'Perform the operation');
 *         },
 *
 *         onOperationToggle: function(btn, pressed) {
 *             // The action controls all UI components created from it.
 *             this.view.getAction('operation').setDisabled(pressed);
 *         }
 *     });
 *
 *     Ext.define('ActionsPanel', {
 *         extend: 'Ext.panel.Panel',
 *         controller: 'actionsexample',
 *
 *         title: 'Actions',
 *         width: 500,
 *         height: 300,
 *
 *         // Define the shared Action.  Each Component created from these will
 *         // have the same display text, icon and tooltip, and will invoke the
 *         // same controller method on click.
 *         actions: {
 *             operation: {
 *                 text: 'Do operation',
 *                  handler: 'onOperationClick',
 *                 glyph: 'xf005@FontAwesome',
 *                 tooltip: 'Perform the operation'
 *             },
 *             disableOperation: {
 *                 text: 'Disable operation',
 *                 enableToggle: true,
 *                 toggleHandler: 'onOperationToggle',
 *                 tooltip: 'Disable the operation'
 *             }
 *         },
 *
 *         // Actions are interpreted as Buttons by this view.
 *         // Other descendants such as Menus and Toolbars have
 *         // their own defaults.
 *         defaultActionType: 'button',
 *
 *         tools: [
 *             '@operation'
 *         ],
 *
 *         tbar: [
 *             // Add the Action directly to a toolbar as a menu button
 *             '@operation',
 *             {
 *                 text: 'Action Menu',
 *                 menu: [
 *                     // Add the Action to a menu as a text item
 *                     '@operation'
 *                 ]
 *             }, '@disableOperation'
 *         ],
 *
 *         bodyPadding: 10,
 *         layout: {
 *             type: 'vbox',
 *             align: 'stretch'
 *         },
 *         items: [
 *             // Add the Action to the panel body.
 *             // defaultActionType will ensure it is converted to a Button.
 *             '@operation'
 *         ]
 *     });
 *
 *     Ext.QuickTips.init();
 *     new ActionsPanel({
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.Action', {
    /**
     * @cfg {String} [text='']
     * The text to set for all components configured by this Action.
     */
    /**
     * @cfg glyph
     * @inheritdoc Ext.panel.Header#cfg-glyph
     * @since 6.2.0
     */
    /**
     * @cfg iconCls
     * @localdoc **Note:** The CSS class(es) specifying the background image will apply 
     * to all components configured by this Action.
     * @inheritdoc Ext.panel.Header#cfg-iconCls
     */
    /**
     * @cfg {Boolean} [disabled=false]
     * True to disable all components configured by this Action, false to enable them.
     */
    /**
     * @cfg {Boolean} [hidden=false]
     * True to hide all components configured by this Action, false to show them.
     */
    /**
     * @cfg {String/Function} handler
     * The function that will be invoked by each component tied to this Action
     * when the component's primary event is triggered.
     */
    /**
     * @cfg {String} itemId
     * See {@link Ext.Component}.{@link Ext.Component#itemId itemId}.
     */
    /**
     * @cfg {Object} [scope]
     * The scope (this reference) in which the {@link #handler} is executed
     * if specified as a function instead of a named Controller method.
     * Defaults to the browser window.
     */
    /**
     * Creates new Action.
     * @param {Object} config Config object.
     */
    constructor: function(config) {
        this.initialConfig = config;
        this.itemId = config.itemId = (config.itemId || config.id || Ext.id());
        this.items = [];
    },
    /**
     * @property {Boolean} isAction
     * `true` in this class to identify an object as an instantiated Action, or subclass thereof.
     */
    isAction: true,
    /**
     * Sets the text to be displayed by all components configured by this Action.
     * @param {String} text The text to display
     */
    setText: function(text) {
        this.initialConfig.text = text;
        this.callEach('setText', [
            text
        ]);
    },
    /**
     * Gets the text currently displayed by all components configured by this Action.
     */
    getText: function() {
        return this.initialConfig.text;
    },
    /**
     * Sets the {@link #iconCls icon CSS class} for all components configured by this 
     * Action.  The class should supply a background image that will be used as the icon 
     * image.
     * @param {String} cls The CSS class supplying the icon image
     */
    setIconCls: function(cls) {
        this.initialConfig.iconCls = cls;
        this.callEach('setIconCls', [
            cls
        ]);
    },
    /**
     * Sets the {@link #glyph glyph} for all components configured by this 
     * Action.
     * @param {String} glyph The CSS class supplying the icon image
     */
    setGlyph: function(glyph) {
        this.initialConfig.glyph = glyph;
        this.callEach('setGlyph', [
            glyph
        ]);
    },
    /**
     * Gets the icon CSS class currently used by all components configured by this Action.
     */
    getIconCls: function() {
        return this.initialConfig.iconCls;
    },
    /**
     * Sets the disabled state of all components configured by this Action.  Shortcut method
     * for {@link #enable} and {@link #disable}.
     * @param {Boolean} disabled True to disable the component, false to enable it
     */
    setDisabled: function(disabled) {
        this.initialConfig.disabled = disabled;
        this.callEach('setDisabled', [
            disabled
        ]);
    },
    /**
     * Enables all components configured by this Action.
     */
    enable: function() {
        this.setDisabled(false);
    },
    /**
     * Disables all components configured by this Action.
     */
    disable: function() {
        this.setDisabled(true);
    },
    /**
     * Returns true if the components using this Action are currently disabled, else returns false.
     */
    isDisabled: function() {
        return this.initialConfig.disabled;
    },
    /**
     * Sets the hidden state of all components configured by this Action.  Shortcut method
     * for `{@link #hide}` and `{@link #show}`.
     * @param {Boolean} hidden True to hide the component, false to show it.
     */
    setHidden: function(hidden) {
        this.initialConfig.hidden = hidden;
        this.callEach('setVisible', [
            !hidden
        ]);
    },
    /**
     * Shows all components configured by this Action.
     */
    show: function() {
        this.setHidden(false);
    },
    /**
     * Hides all components configured by this Action.
     */
    hide: function() {
        this.setHidden(true);
    },
    /**
     * Returns true if the components configured by this Action are currently hidden,
     * else returns false.
     */
    isHidden: function() {
        return this.initialConfig.hidden;
    },
    /**
     * Sets the function that will be called by each Component using this action when its
     * primary event (usually a click or tap) is triggered.
     * @param {String/Function} handler The function that will be invoked by the action's components
     * when clicked. See the `handler` config of the target component for the arguments passed.
     * @param {Object} [scope] The scope (this reference) in which the function is executed.
     * Defaults to an encapsulating {@link Ext.app.Controller Controller}, or the Component.
     */
    setHandler: function(handler, scope) {
        this.initialConfig.handler = handler;
        this.initialConfig.scope = scope;
        this.callEach('setHandler', [
            handler,
            scope
        ]);
    },
    /**
     * Executes the specified function once for each Component currently tied to this Action.
     * The function passed in should accept a single argument that will be an object that supports
     * the basic Action config/method interface.
     * @param {Function} fn The function to execute for each component
     * @param {Object} scope The scope (this reference) in which the function is executed.
     * Defaults to the Component.
     */
    each: function(fn, scope) {
        Ext.each(this.items, fn, scope);
    },
    /**
     * @private
     */
    callEach: function(fnName, args) {
        var items = this.items,
            i = 0,
            len = items.length,
            item;
        Ext.suspendLayouts();
        for (; i < len; i++) {
            item = items[i];
            item[fnName].apply(item, args);
        }
        Ext.resumeLayouts(true);
    },
    /**
     * @private
     */
    addComponent: function(comp) {
        this.items.push(comp);
        comp.on('destroy', this.removeComponent, this);
    },
    /**
     * @private
     */
    removeComponent: function(comp) {
        Ext.Array.remove(this.items, comp);
    },
    /**
     * Executes this Action manually using the handler function specified in the original config
     * object or the handler function set with {@link #setHandler}. Any arguments passed to this
     * function will be passed on to the handler function.
     * @param {Object...} args Variable number of arguments passed to the handler function
     */
    execute: function() {
        this.initialConfig.handler.apply(this.initialConfig.scope || Ext.global, arguments);
    }
});

/**
 * A class used to load remote content to an Element. Sample usage:
 *
 *     Ext.get('el').load({
 *         url: 'myPage.php',
 *         scripts: true,
 *         params: {
 *             id: 1
 *         }
 *     });
 *
 * In general this class will not be instanced directly, rather the
 * {@link Ext.dom.Element#method-load} method will be used.
 */
Ext.define('Ext.ElementLoader', {
    mixins: [
        'Ext.util.Observable'
    ],
    uses: [
        'Ext.data.Connection',
        'Ext.Ajax'
    ],
    statics: {
        Renderer: {
            Html: function(loader, response, active) {
                loader.getTarget().setHtml(response.responseText, active.scripts === true, active.callback, active.rendererScope);
                return true;
            }
        }
    },
    /**
     * @cfg {String} url (required)
     * The url to retrieve the content from.
     */
    url: null,
    /**
     * @cfg {Object} params
     * Any params to be attached to the Ajax request. These parameters will
     * be overridden by any params in the load options.
     */
    params: null,
    /**
     * @cfg {Object} baseParams Params that will be attached to every request. These parameters
     * will not be overridden by any params in the load options.
     */
    baseParams: null,
    /**
     * @cfg {Boolean/Object} autoLoad
     * `true` to have the loader make a request as soon as it is created.
     * This argument can also be a set of options that will be passed to {@link #method-load}
     * when it is called.
     */
    autoLoad: false,
    /**
     * @cfg {HTMLElement/Ext.dom.Element/String} target
     * The target element for the loader. It can be the DOM element, the id or an
     * {@link Ext.dom.Element}.
     */
    target: null,
    /**
     * @cfg {Boolean/String} loadMask
     * True or a string to show when the element is loading.
     */
    loadMask: false,
    /**
     * @cfg {Object} ajaxOptions
     * Any additional options to be passed to the request, for example timeout or headers.
     */
    ajaxOptions: null,
    /**
     * @cfg {Boolean} scripts
     * True to parse any inline script tags in the response.
     */
    scripts: false,
    /**
     * @cfg {Function/String} success
     * A function to be called when a load request is successful.
     * Will be called with the following config parameters:
     *
     * - this - The ElementLoader instance.
     * - response - The response object.
     * - options - Ajax options.
     * 
     * @controllable
     */
    /**
     * @cfg {Function/String} failure A function to be called when a load request fails.
     * Will be called with the following config parameters:
     *
     * - this - The ElementLoader instance.
     * - response - The response object.
     * - options - Ajax options.
     * 
     * @controllable
     */
    /**
     * @cfg {Function/String} callback A function to be called when a load request finishes.
     * Will be called with the following config parameters:
     *
     * - this - The ElementLoader instance.
     * - success - True if successful request.
     * - response - The response object.
     * - options - Ajax options.
     * 
     * @controllable
     */
    /**
     * @cfg {Object} scope
     * The scope to execute the {@link #success} and {@link #failure} functions in.
     */
    /**
     * @cfg {Function} renderer
     * A custom function to render the content to the element. The function should
     * return false if the renderer could not be applied. The passed parameters are:
     *
     * - The loader
     * - The response
     * - The active request
     */
    /**
     * @cfg {Object} rendererScope
     * The scope to execute the {@link #renderer} function in.
     */
    /**
     * @property {Boolean} isLoader
     * `true` in this class to identify an object as an instantiated ElementLoader,
     * or subclass thereof.
     */
    isLoader: true,
    /**
     * @event beforeload
     * Fires before a load request is made to the server.
     * Returning false from an event listener can prevent the load
     * from occurring.
     * @param {Ext.ElementLoader} this
     * @param {Object} options The options passed to the request
     */
    /**
     * @event exception
     * Fires after an unsuccessful load.
     * @param {Ext.ElementLoader} this
     * @param {Object} response The response from the server
     * @param {Object} options The options passed to the request
     */
    /**
     * @event load
     * Fires after a successful load.
     * @param {Ext.ElementLoader} this
     * @param {Object} response The response from the server
     * @param {Object} options The options passed to the request
     */
    constructor: function(config) {
        var me = this,
            autoLoad;
        me.callParent([
            config
        ]);
        me.mixins.observable.constructor.call(me, config);
        me.setTarget(me.target);
        if (me.autoLoad) {
            autoLoad = me.autoLoad;
            if (autoLoad === true) {
                autoLoad = null;
            }
            me.load(autoLoad);
        }
    },
    /**
     * Sets an {@link Ext.dom.Element} as the target of this loader.
     * Note that if the target is changed, any active requests will be aborted.
     * @param {String/HTMLElement/Ext.dom.Element} target The element or its ID.
     */
    setTarget: function(target) {
        var me = this;
        target = Ext.get(target);
        if (me.target && me.target !== target) {
            me.abort();
        }
        me.target = target;
    },
    /**
     * Returns the target of this loader.
     * @return {Ext.Component} The target or null if none exists.
     */
    getTarget: function() {
        return this.target || null;
    },
    /**
     * Aborts the active load request
     */
    abort: function() {
        var active = this.active;
        if (active !== undefined) {
            Ext.Ajax.abort(active.request);
            if (active.mask) {
                this.removeMask();
            }
            delete this.active;
        }
    },
    /**
     * Removes the mask on the target
     * @private
     */
    removeMask: function() {
        this.target.unmask();
    },
    /**
     * Adds the mask on the target
     * @private
     * @param {Boolean/Object} mask The mask configuration
     */
    addMask: function(mask) {
        this.target.mask(mask === true ? null : mask);
    },
    /**
     * Loads new data from the server.
     * @param {Object} options The options for the request. They can be any configuration option
     * that can be specified for the class, with the exception of the target option. Note that any
     * options passed to the method will override any class defaults.
     */
    load: function(options) {
        if (this.destroying || this.destroyed) {
            return;
        }
        if (!this.target) {
            Ext.raise('A valid target is required when loading content for ' + this.id);
        }
        options = Ext.apply({}, options);
        // eslint-disable-next-line vars-on-top
        var me = this,
            mask = Ext.isDefined(options.loadMask) ? options.loadMask : me.loadMask,
            params = Ext.apply({}, options.params),
            ajaxOptions = Ext.apply({}, options.ajaxOptions),
            callback = options.callback || me.callback,
            scope = options.scope || me.scope || me;
        Ext.applyIf(ajaxOptions, me.ajaxOptions);
        Ext.applyIf(options, ajaxOptions);
        Ext.applyIf(params, me.params);
        Ext.apply(params, me.baseParams);
        Ext.applyIf(options, {
            url: me.url
        });
        if (!options.url) {
            Ext.raise('You must specify the URL from which content should be loaded');
        }
        Ext.apply(options, {
            scope: me,
            params: params,
            callback: me.onComplete
        });
        if (me.fireEvent('beforeload', me, options) === false) {
            return;
        }
        if (mask) {
            me.addMask(mask);
        }
        me.active = {
            options: options,
            mask: mask,
            scope: scope,
            callback: callback,
            success: options.success || me.success,
            failure: options.failure || me.failure,
            renderer: options.renderer || me.renderer,
            scripts: Ext.isDefined(options.scripts) ? options.scripts : me.scripts
        };
        me.active.request = Ext.Ajax.request(options);
        me.setOptions(me.active, options);
    },
    /**
     * @method
     * Sets any additional options on the active request
     * @private
     * @param {Object} active The active request
     * @param {Object} options The initial options
     */
    setOptions: function(active, options) {
        active.rendererScope = options.rendererScope || this.rendererScope || this;
    },
    /**
     * Parses the response after the request completes
     * @private
     * @param {Object} options Ajax options
     * @param {Boolean} success Success status of the request
     * @param {Object} response The response object
     */
    onComplete: function(options, success, response) {
        var me = this,
            active = me.active,
            rendererScope, scope;
        if (active) {
            scope = active.scope;
            rendererScope = active.rendererScope;
            if (success) {
                // eslint-disable-next-line max-len
                success = me.getRenderer(active.renderer).call(rendererScope, me, response, active) !== false;
            }
            if (success) {
                Ext.callback(active.success, scope, [
                    me,
                    response,
                    options
                ]);
                me.fireEvent('load', me, response, options);
            } else {
                Ext.callback(active.failure, scope, [
                    me,
                    response,
                    options
                ]);
                me.fireEvent('exception', me, response, options);
            }
            Ext.callback(active.callback, scope, [
                me,
                success,
                response,
                options
            ]);
            if (active.mask) {
                me.removeMask();
            }
        }
        delete me.active;
    },
    /**
     * Gets the renderer to use
     * @private
     * @param {String/Function} renderer The renderer to use
     * @return {Function} A rendering function to use.
     */
    getRenderer: function(renderer) {
        if (Ext.isFunction(renderer)) {
            return renderer;
        }
        return this.statics().Renderer.Html;
    },
    /**
     * Automatically refreshes the content over a specified period.
     * @param {Number} interval The interval to refresh in ms.
     * @param {Object} options (optional) The options to pass to the load method.
     * See {@link #method-load}
     */
    startAutoRefresh: function(interval, options) {
        var me = this;
        me.stopAutoRefresh();
        me.autoRefresh = Ext.interval(function() {
            me.load(options);
        }, interval);
    },
    /**
     * Clears any auto refresh. See {@link #startAutoRefresh}.
     */
    stopAutoRefresh: function() {
        Ext.uninterval(this.autoRefresh);
        this.autoRefresh = null;
    },
    /**
     * Checks whether the loader is automatically refreshing. See {@link #startAutoRefresh}.
     * @return {Boolean} True if the loader is automatically refreshing
     */
    isAutoRefreshing: function() {
        return !!this.autoRefresh;
    },
    /**
     * Destroys the loader. Any active requests will be aborted.
     */
    destroy: function() {
        var me = this;
        me.stopAutoRefresh();
        me.abort();
        me.callParent();
    }
});

/**
 * This class is used to load content via Ajax into a {@link Ext.Component}. In general 
 * this class will not be instanced directly, rather a loader configuration will be passed to the
 * constructor of the {@link Ext.Component}.
 *
 * ## HTML Renderer
 *
 * By default, the content loaded will be processed as raw html. The response text
 * from the request is taken and added to the component. This can be used in
 * conjunction with the {@link #scripts} option to execute any inline scripts in
 * the resulting content. Using this renderer has the same effect as passing the
 * {@link Ext.Component#html} configuration option.
 *
 * ## Data Renderer
 *
 * This renderer allows content to be added by using JSON data and a {@link Ext.XTemplate}.
 * The content received from the response is passed to the {@link Ext.Component#update} method.
 * This content is run through the attached {@link Ext.Component#tpl} and the data is added to
 * the Component. Using this renderer has the same effect as using the {@link Ext.Component#data}
 * configuration in conjunction with a {@link Ext.Component#tpl}.
 *
 * ## Component Renderer
 *
 * This renderer can only be used with a {@link Ext.container.Container} and subclasses. It allows
 * for Components to be loaded remotely into a Container. The response is expected to be
 * a single/series of {@link Ext.Component} configuration objects. When the response is received,
 * the data is decoded and then passed to {@link Ext.container.Container#method-add}. Using this
 * renderer has the same effect as specifying the {@link Ext.container.Container#cfg-items}
 * configuration on a Container.
 *
 * ## Custom Renderer
 *
 * A custom function can be passed to handle any other special case, see the {@link #renderer}
 * option.
 *
 * ## Example Usage
 *
 *     var cmp = Ext.create('Ext.Component', {
 *         renderTo: Ext.getBody(),
 *         tpl: '{firstName} - {lastName}',
 *         loader: {
 *             url: 'myPage.php',
 *             renderer: 'data',
 *             params: {
 *                 userId: 1
 *             }
 *         }
 *     });
 *
 *     // call the loader manually (or use autoLoad:true instead)
 *     cmp.getLoader().load();
 */
Ext.define('Ext.ComponentLoader', {
    extend: 'Ext.ElementLoader',
    statics: {
        Renderer: {
            Data: function(loader, response, active) {
                var success = true;
                try {
                    loader.getTarget().update(Ext.decode(response.responseText));
                } catch (e) {
                    success = false;
                }
                return success;
            },
            Component: function(loader, response, active) {
                var success = true,
                    target = loader.getTarget(),
                    items = [];
                if (!target.isContainer) {
                    Ext.raise({
                        target: target,
                        msg: 'Components can only be loaded into a container'
                    });
                }
                try {
                    items = Ext.decode(response.responseText);
                } catch (e) {
                    success = false;
                }
                if (success) {
                    target.suspendLayouts();
                    if (active.removeAll) {
                        target.removeAll();
                    }
                    target.add(items);
                    target.resumeLayouts(true);
                }
                return success;
            }
        }
    },
    /**
     * @cfg {Ext.Component/String} target The target {@link Ext.Component} for the loader.
     * If a string is passed it will be looked up via the id.
     */
    target: null,
    /**
     * @cfg {Boolean/Object} loadOnRender
     * `true` to have the loader make a request when the {@link #target} is rendered. If the target
     * is already rendered, a load will take place immediately.
     * This argument can also be a set of options that will be passed to {@link #method-load} when
     * it is called.
     */
    loadOnRender: false,
    /**
     * @cfg {Boolean/Object} loadMask True or a {@link Ext.LoadMask} configuration to enable masking
     * during loading.
     */
    loadMask: false,
    /**
     * @cfg {Boolean} scripts True to parse any inline script tags in the response. This only used
     * when using the html {@link #renderer}. The scripts will be executed with the target Component
     * as the scope (`this` reference).
     */
    /**
     * @cfg {Object} [rendererScope=`this`]
     * The scope (`this` reference)to execute the {@link #renderer} function in. If the
     * {@link #scripts} option is `true`, *inline* script source (not scripts with a `src`
     * attribute) is also executed in this scope.
     * Defaults to this Component.
     */
    /**
     * @cfg {String/Function} renderer
     *
     * The type of content that is to be loaded into, which can be one of 3 types:
     * 
     * + **html**: Loads raw html content, see {@link Ext.Component#html}
     * + **data**: Loads raw html content, see {@link Ext.Component#data}
     * + **component**: Loads child {Ext.Component} instances. This option is only valid when used
     * with a Container.
     * 
     * Alternatively, you can pass a function which is called with the following parameters.
     * 
     * + loader - Loader instance
     * + response - The server response
     * + active - The active request
     * 
     * The function must return false is loading is not successful. Below is a sample of using
     * a custom renderer:
     *
     *     new Ext.Component({
     *         loader: {
     *             url: 'myPage.php',
     *             renderer: function(loader, response, active) {
     *                 var text = response.responseText;
     *                 loader.getTarget().setHtml('The response is ' + text);
     *                 return true;
     *             }
     *         }
     *     });
     *
     */
    renderer: 'html',
    /**
     * Set a {Ext.Component} as the target of this loader. Note that if the target is changed,
     * any active requests will be aborted.
     * @param {String/Ext.Component} target The component to be the target of this loader.
     * If a string is passed it will be looked up via its id.
     */
    setTarget: function(target) {
        var me = this;
        if (Ext.isString(target)) {
            target = Ext.getCmp(target);
        }
        if (me.target && me.target !== target) {
            me.abort();
        }
        me.target = target;
        if (target && me.loadOnRender) {
            if (target.rendered) {
                me.doLoadOnRender();
            } else {
                me.mon(target, 'render', me.doLoadOnRender, me);
            }
        }
    },
    doLoadOnRender: function() {
        var loadOnRender = this.loadOnRender;
        this.load(Ext.isObject(loadOnRender) ? loadOnRender : null);
    },
    removeMask: function() {
        this.target.setLoading(false);
    },
    /**
     * Add the mask on the target
     * @private
     * @param {Boolean/Object} mask The mask configuration
     */
    addMask: function(mask) {
        this.target.setLoading(mask);
    },
    setOptions: function(active, options) {
        active.removeAll = Ext.isDefined(options.removeAll) ? options.removeAll : this.removeAll;
        active.rendererScope = options.rendererScope || this.rendererScope || this.target;
    },
    /**
     * Gets the renderer to use
     * @private
     * @param {String/Function} renderer The renderer to use
     * @return {Function} A rendering function to use.
     */
    getRenderer: function(renderer) {
        var renderers;
        if (Ext.isFunction(renderer)) {
            return renderer;
        }
        renderers = this.statics().Renderer;
        switch (renderer) {
            case 'component':
                return renderers.Component;
            case 'data':
                return renderers.Data;
            default:
                return Ext.ElementLoader.Renderer.Html;
        }
    }
});

/**
 * This class describes a size determination strategy or algorithm used by the layout
 * system. There are special instances of this class stored as static properties to
 * avoid needless object instantiation. These instances should be treated as readonly.
 * 
 *  * `calculated`
 *  * `configured`
 *  * `constrainedMax`
 *  * `constrainedMin`
 *  * `natural`
 *  * `shrinkWrap`
 *  * `calculatedFromConfigured`
 *  * `calculatedFromNatural`
 *  * `calculatedFromShrinkWrap`
 *
 * Using one of these instances is simply:
 *
 *       var calculated = Ext.layout.SizeModel.calculated;
 *
 * @private
 */
Ext.define('Ext.layout.SizeModel', {
    constructor: function(config) {
        var me = this,
            SizeModel = me.self,
            sizeModelsArray = SizeModel.sizeModelsArray,
            name;
        Ext.apply(me, config);
        me[name = me.name] = true;
        // set the one special flag that matches our name
        me.fixed = !(me.auto = me.natural || me.shrinkWrap);
        /**
         * @property {Number} ordinal
         * The 0-based ordinal for this `SizeModel` instance.
         * @readonly
         */
        sizeModelsArray[me.ordinal = sizeModelsArray.length] = SizeModel[name] = SizeModel.sizeModels[name] = me;
    },
    statics: {
        /**
         * An array of all SizeModel instances.
         * @private
         */
        sizeModelsArray: [],
        /**
         * An object containing all SizeModel instances keyed by `name`.
         * @private
         */
        sizeModels: {}
    },
    /**
     * @property {String} name
     * The name of this size model (e.g., "calculated").
     * @readonly
     */
    /**
     * @property {Boolean} auto
     * True if the size is either `natural` or `shrinkWrap`, otherwise false.
     * @readonly
     */
    /**
     * @property {Boolean} calculated
     * True if the size is calculated by the `ownerLayout`.
     * @readonly
     */
    calculated: false,
    /**
     * @property {Boolean} configured
     * True if the size is configured (e.g., by a `width` or `minWidth`). The names of
     * configuration properties can be found in the {@link #names} property.
     * @readonly
     */
    configured: false,
    /**
     * @property {Boolean} constrainedMax
     * True if the size is constrained by a `maxWidth` or `maxHeight` configuration. This
     * is a flavor of `configured` (since `maxWidth` and `maxHeight` are config options).
     * If true, the {@link #names} property will be defined as well.
     * @readonly
     */
    constrainedMax: false,
    /**
     * @property {Boolean} constrainedMin
     * True if the size is constrained by a `minWidth` or `minHeight` configuration. This
     * is a flavor of `configured` (since `minWidth` and `minHeight` are config options).
     * If true, the {@link #names} property will be defined as well.
     * @readonly
     */
    constrainedMin: false,
    /**
     * @property {Boolean} fixed
     * True if the size is either `calculated` or `configured`, otherwise false.
     * @readonly
     */
    /**
     * @property {Boolean} natural
     * True if the size is determined by CSS and not by content. Such sizes are assumed to
     * be dependent on the container box and measurement occurs on the outer-most element.
     * @readonly
     */
    natural: false,
    /**
     * @property {Boolean} shrinkWrap
     * True if the size is determined by content irrespective of the container box.
     * @readonly
     */
    shrinkWrap: false,
    /**
     * @property {Boolean} calculatedFromConfigured
     * True if the size is calculated by the `ownerLayout` based on a configured size.
     * @readonly
     */
    calculatedFromConfigured: false,
    /**
     * @property {Boolean} calculatedFromNatural
     * True if the size is calculated by the `ownerLayout` based on `natural` size model
     * results.
     * @readonly
     */
    calculatedFromNatural: false,
    /**
     * @property {Boolean} calculatedFromShrinkWrap
     * True if the size is calculated by the `ownerLayout` based on `shrinkWrap` size model
     * results.
     * @readonly
     */
    calculatedFromShrinkWrap: false,
    /**
     * @property {Object} names An object with the config property names that determine the
     * size.
     * @property {String} names.width The width property name (e.g., 'width').
     * @property {String} names.height The height property name (e.g., 'minHeight').
     * @readonly
     */
    names: null
}, function() {
    var SizeModel = this,
        sizeModelsArray = SizeModel.sizeModelsArray,
        i, j, n, pairs, sizeModel;
    //-------------------------------------------------------------------------------
    // These are the 4 fundamental size models.
    new SizeModel({
        name: 'calculated'
    });
    new SizeModel({
        name: 'configured',
        names: {
            width: 'width',
            height: 'height'
        }
    });
    new SizeModel({
        name: 'natural'
    });
    new SizeModel({
        name: 'shrinkWrap'
    });
    //-------------------------------------------------------------------------------
    // These are the size models are flavors of the above but with some extra detail
    // about their dynamic use.
    new SizeModel({
        name: 'calculatedFromConfigured',
        configured: true,
        calculatedFrom: true,
        names: {
            width: 'width',
            height: 'height'
        }
    });
    new SizeModel({
        name: 'calculatedFromNatural',
        natural: true,
        calculatedFrom: true
    });
    new SizeModel({
        name: 'calculatedFromShrinkWrap',
        shrinkWrap: true,
        calculatedFrom: true
    });
    new SizeModel({
        name: 'constrainedMax',
        configured: true,
        constrained: true,
        names: {
            width: 'maxWidth',
            height: 'maxHeight'
        }
    });
    new SizeModel({
        name: 'constrainedMin',
        configured: true,
        constrained: true,
        names: {
            width: 'minWidth',
            height: 'minHeight'
        }
    });
    new SizeModel({
        name: 'constrainedDock',
        configured: true,
        constrained: true,
        constrainedByMin: true,
        names: {
            width: 'dockConstrainedWidth',
            height: 'dockConstrainedHeight'
        }
    });
    for (i = 0 , n = sizeModelsArray.length; i < n; ++i) {
        sizeModel = sizeModelsArray[i];
        /**
         * @member Ext.layout.SizeModel
         * An array of objects indexed by the {@link #ordinal} of a height `SizeModel` on
         * a width `SizeModel` to yield an object describing both height and width size
         * models.
         * 
         * Used like this:
         *
         *      widthModel.pairsByHeightOrdinal[heightModel.ordinal]
         *
         * This provides a reusable object equivalent to the following:
         * 
         *      {
         *          width: widthModel,
         *          height: heightModel
         *      }
         *
         * @property {Object[]} pairsByHeightOrdinal
         * @property {Ext.layout.SizeModel} pairsByHeightOrdinal.width The `SizeModel` for
         * the width.
         * @property {Ext.layout.SizeModel} pairsByHeightOrdinal.height The `SizeModel` for
         * the height.
         */
        sizeModel.pairsByHeightOrdinal = pairs = [];
        for (j = 0; j < n; ++j) {
            pairs.push({
                width: sizeModel,
                height: sizeModelsArray[j],
                ordinal: j
            });
        }
    }
});

/**
 * This class is the base for all layout types: component and container.
 * @protected
 */
Ext.define('Ext.layout.Layout', {
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    requires: [
        'Ext.XTemplate',
        'Ext.layout.SizeModel'
    ],
    uses: [
        'Ext.layout.Context'
    ],
    factoryConfig: {
        type: 'layout',
        defaultType: 'autocontainer',
        instanceProp: 'isLayout'
    },
    /**
     * @property {Boolean} isLayout
     * `true` in this class to identify an object as an instantiated Layout, or subclass thereof.
     * @readonly
     */
    isLayout: true,
    initialized: false,
    running: false,
    /**
     * @private
     * `true` if this layout may need to incorporate the dimensions of individual child
     * items into its layout calculations.  Layouts that handle the size of their children
     * as a group (autocontainer, form) can set this to false for an additional performance
     * optimization.  When `false` the layout system will not recurse into the child
     * items if {@link Ext.layout.container.Container#activeItemCount} is `0`, which will be
     * the case if all child items use "liquid" CSS layout, e.g. form fields.
     * (See Ext.Component#liquidLayout)
     */
    needsItemSize: true,
    /**
     * @private
     * `true` if this layout may set the size of its child items.  Layouts that do not
     * set the size of their child items (autocontainer, form) can set this to false
     * for an additional performance optimization.  When `true` the layout system will
     * not create a context item for children that use liquid layout, because there is
     * no need for a context item if item size is neither read nor set by the owning layout.
     */
    setsItemSize: true,
    autoSizePolicy: {
        readsWidth: 1,
        readsHeight: 1,
        setsWidth: 0,
        setsHeight: 0
    },
    $configPrefixed: false,
    $configStrict: false,
    constructor: function(config) {
        var me = this;
        me.id = Ext.id(null, me.type + '-');
        me.initConfig(config);
        // prevent any "type" that was passed as the alias from overriding the "type"
        // property from the prototype
        delete me.type;
        me.layoutCount = 0;
    },
    /**
     * @property {Boolean} done Used only during a layout run, this value indicates that a
     * layout has finished its calculations. This flag is set to true prior to the call to
     * {@link #calculate} and should be set to false if this layout has more work to do.
     */
    /**
     * Called before any calculation cycles to prepare for layout.
     * 
     * This is a write phase and DOM reads should be strictly avoided when overridding
     * this method.
     * 
     * @param {Ext.layout.ContextItem} ownerContext The context item for the layout's owner
     * component.
     * @method beginLayout
     */
    beginLayout: Ext.emptyFn,
    /**
     * Called before any calculation cycles to reset DOM values and prepare for calculation.
     * 
     * This is a write phase and DOM reads should be strictly avoided when overridding
     * this method.
     * 
     * @param {Ext.layout.ContextItem} ownerContext The context item for the layout's owner
     * component.
     * @method beginLayoutCycle
     */
    beginLayoutCycle: function(ownerContext) {
        var me = this,
            context = me.context,
            changed;
        if (me.lastWidthModel !== ownerContext.widthModel) {
            if (me.lastWidthModel) {
                changed = true;
            }
            me.lastWidthModel = ownerContext.widthModel;
        }
        if (me.lastHeightModel !== ownerContext.heightModel) {
            if (me.lastWidthModel) {
                changed = true;
            }
            me.lastHeightModel = ownerContext.heightModel;
        }
        if (changed) {
            (context = ownerContext.context).clearTriggers(me, false);
            context.clearTriggers(me, true);
            me.triggerCount = 0;
        }
    },
    /**
     * Called to perform the calculations for this layout. This method will be called at
     * least once and may be called repeatedly if the {@link #done} property is cleared
     * before return to indicate that this layout is not yet done. The {@link #done} property
     * is always set to `true` before entering this method.
     * 
     * This is a read phase and DOM writes should be strictly avoided in derived classes.
     * Instead, DOM writes need to be written to {@link Ext.layout.ContextItem} objects to
     *  be flushed at the next opportunity.
     * 
     * @param {Ext.layout.ContextItem} ownerContext The context item for the layout's owner
     * component.
     * @method calculate
     * @abstract
     */
    /**
     * This method (if implemented) is called at the end of the cycle in which this layout
     * completes (by not setting {@link #done} to `false` in {@link #calculate}). It is
     * possible for the layout to complete and yet become invalid before the end of the cycle,
     * in which case, this method will not be called. It is also possible for this method to
     * be called and then later the layout becomes invalidated. This will result in
     * {@link #calculate} being called again, followed by another call to this method.
     * 
     * This is a read phase and DOM writes should be strictly avoided in derived classes.
     * Instead, DOM writes need to be written to {@link Ext.layout.ContextItem} objects to
     * be flushed at the next opportunity.
     * 
     * This method need not be implemented by derived classes and, in fact, should only be
     * implemented when needed.
     * 
     * @param {Ext.layout.ContextItem} ownerContext The context item for the layout's owner
     * component.
     * @method completeLayout
     */
    /**
     * This method (if implemented) is called after all layouts have completed. In most
     * ways this is similar to {@link #completeLayout}. This call can cause this (or any
     * layout) to be become invalid (see {@link Ext.layout.Context#invalidate}), but this
     * is best avoided. This method is intended to be where final reads are made and so it
     * is best to avoid invalidating layouts at this point whenever possible. Even so, this
     * method can be used to perform final checks that may require all other layouts to be
     * complete and then invalidate some results.
     * 
     * This is a read phase and DOM writes should be strictly avoided in derived classes.
     * Instead, DOM writes need to be written to {@link Ext.layout.ContextItem} objects to
     * be flushed at the next opportunity.
     * 
     * This method need not be implemented by derived classes and, in fact, should only be
     * implemented when needed.
     * 
     * @param {Ext.layout.ContextItem} ownerContext The context item for the layout's owner
     * component.
     * @method finalizeLayout
     */
    /**
     * This method is called after all layouts are complete and their calculations flushed
     * to the DOM. No further layouts will be run and this method is only called once per
     * layout run. The base component layout caches `lastComponentSize`.
     * 
     * This is a write phase and DOM reads should be avoided if possible when overridding
     * this method.
     * 
     * This method need not be implemented by derived classes and, in fact, should only be
     * implemented when needed.
     * 
     * @param {Ext.layout.ContextItem} ownerContext The context item for the layout's owner
     * component.
     */
    finishedLayout: function(ownerContext) {
        this.lastWidthModel = ownerContext.widthModel;
        this.lastHeightModel = ownerContext.heightModel;
        this.ownerContext = null;
    },
    /**
     * This method (if implemented) is called after all layouts are finished, and all have
     * a `lastComponentSize` cached. No further layouts will be run and this method is only
     * called once per layout run. It is the bookend to {@link #beginLayout}.
     * 
     * This is a write phase and DOM reads should be avoided if possible when overridding
     * this method. This is the catch-all tail method to a layout and so the rules are more
     * relaxed. Even so, for performance reasons, it is best to avoid reading the DOM. If
     * a read is necessary, consider implementing a {@link #finalizeLayout} method to do the
     * required reads.
     * 
     * This method need not be implemented by derived classes and, in fact, should only be
     * implemented when needed.
     * 
     * @param {Ext.layout.ContextItem} ownerContext The context item for the layout's owner
     * component.
     * @method notifyOwner
     */
    redoLayout: Ext.emptyFn,
    undoLayout: Ext.emptyFn,
    /**
     * @cfg {Object} animatePolicy
     * An object that contains as keys the names of the properties that can be animated
     * by child items as a consequence of a layout. This config is used internally by the
     * {@link Ext.layout.container.Accordion accordion} layout to cause the child panels
     * to animate to their proper size and position after a collapse/expand event.
     * @protected
     * @since 4.1.0
     */
    getAnimatePolicy: function() {
        return this.animatePolicy;
    },
    /**
     * Returns an object describing how this layout manages the size of the given component.
     * This method must be implemented by any layout that manages components.
     *
     * @param {Ext.Component} item
     * @return {Ext.layout.SizePolicy} An object describing the sizing done by the layout
     * for this item.
     * @protected
     */
    getItemSizePolicy: function(item) {
        return this.autoSizePolicy;
    },
    isItemBoxParent: function(itemContext) {
        return false;
    },
    isItemLayoutRoot: function(item) {
        var sizeModel = item.getSizeModel(),
            width = sizeModel.width,
            height = sizeModel.height;
        // If this component has never had a layout and some of its dimensions are set by
        // its ownerLayout, we cannot be the layoutRoot...
        if (!item.componentLayout.lastComponentSize && (width.calculated || height.calculated)) {
            return false;
        }
        // otherwise an ownerCt whose size is not effected by its content is a root
        return !width.shrinkWrap && !height.shrinkWrap;
    },
    isItemShrinkWrap: function(item) {
        return item.shrinkWrap;
    },
    isRunning: function() {
        return !!this.ownerContext;
    },
    //-----------------------------------------------------
    /*
     * Clears any styles which must be cleared before layout can take place.
     * Only DOM WRITES must be performed at this stage.
     *
     * An entry for the owner's element ID must be created in the layoutContext containing
     * a reference to the target which must be sized/positioned/styled by the layout at
     * the flush stage:
     *
     *     {
     *         target: me.owner
     *     }
     *
     * Component layouts should iterate through managed Elements,
     * pushing an entry for each element:
     *
     *     {
     *         target: childElement
     *     }
     */
    //-----------------------------------------------------
    getItemsRenderTree: function(items, renderCfgs) {
        var length = items.length,
            i, item, itemConfig, result;
        if (length) {
            result = [];
            for (i = 0; i < length; ++i) {
                item = items[i];
                // If we are being asked to move an already rendered Component, we must not
                // recalculate its renderTree and rerun its render process.
                // The Layout's isValidParent check will ensure that the DOM is moved into place.
                if (!item.rendered) {
                    // If we've already calculated the item's element config, don't calculate it
                    // again. This may happen if the rendering process mutates the owning
                    // Container's items collection, and Ext.layout.Container#getRenderTree runs
                    // through the collection again.
                    // Note that the config may be null if a beforerender listener vetoed
                    // the operation, so we must compare to undefined.
                    if (renderCfgs && (renderCfgs[item.id] !== undefined)) {
                        itemConfig = renderCfgs[item.id];
                    } else {
                        // Perform layout preprocessing in the bulk render path
                        this.configureItem(item);
                        itemConfig = item.getRenderTree();
                        if (renderCfgs) {
                            renderCfgs[item.id] = itemConfig;
                        }
                    }
                    // itemConfig mey be null if a beforerender listener vetoed the operation.
                    if (itemConfig) {
                        result.push(itemConfig);
                    }
                }
            }
        }
        return result;
    },
    finishRender: Ext.emptyFn,
    finishRenderItems: function(target, items) {
        var length = items.length,
            i, item;
        for (i = 0; i < length; i++) {
            item = items[i];
            // Only postprocess items which are being rendered. deferredRender may mean
            // that only one has been rendered.
            if (item.rendering) {
                // Tell the item at which index in the Container it is
                item.finishRender(i);
            }
        }
    },
    renderChildren: function() {
        var me = this,
            items = me.getLayoutItems(),
            target = me.getRenderTarget();
        me.renderItems(items, target);
    },
    /**
     * Iterates over all passed items, ensuring they are rendered.  If the items
     * are already rendered, also determines if the items are in the proper place in the dom.
     * @protected
     */
    renderItems: function(items, target) {
        var me = this,
            ln = items.length,
            i = 0,
            pos = 0,
            item;
        if (ln) {
            Ext.suspendLayouts();
            for (; i < ln; i++ , pos++) {
                item = items[i];
                if (item && !item.rendered) {
                    me.renderItem(item, target, pos);
                } else if (item.ignoreDomPosition) {
                    --pos;
                } else if (!me.isValidParent(item, target, pos)) {
                    me.moveItem(item, target, pos);
                } else {
                    // still need to configure the item, it may have moved in the container.
                    me.configureItem(item);
                }
            }
            Ext.resumeLayouts(true);
        }
    },
    /**
     * Validates item is in the proper place in the dom.
     * @protected
     */
    isValidParent: function(item, target, position) {
        var targetDom = (target && target.dom) || target,
            itemDom = this.getItemLayoutEl(item);
        // Test DOM nodes for equality using "===" : http://jsperf.com/dom-equality-test
        if (itemDom && targetDom) {
            if (typeof position === 'number') {
                position = this.getPositionOffset(position);
                return itemDom === targetDom.childNodes[position];
            }
            return itemDom.parentNode === targetDom;
        }
        return false;
    },
    /**
     * For a given item, returns the element that participates in the childNodes array
     * of the layout's target element.  This is usually the component's "el", but can
     * also be a wrapper
     * @private
     * @param {Ext.Component} item
     * @return {HTMLElement}
     */
    getItemLayoutEl: function(item) {
        var dom = item.el ? item.el.dom : Ext.getDom(item),
            parentNode = dom.parentNode,
            className;
        if (parentNode) {
            className = parentNode.className;
            if (className && className.indexOf(Ext.baseCSSPrefix + 'resizable-wrap') !== -1) {
                dom = dom.parentNode;
            }
        }
        return dom;
    },
    getPositionOffset: function(position) {
        return position;
    },
    /**
     * Called before an item is rendered to allow the layout to configure the item.
     * @param {Ext.Component} item The item to be configured
     * @protected
     */
    configureItem: function(item) {
        item.ownerLayout = this;
    },
    /**
     * Renders the given Component into the target Element.
     * @param {Ext.Component} item The Component to render
     * @param {Ext.dom.Element} target The target Element
     * @param {Number} position The position within the target to render the item to
     * @private
     */
    renderItem: function(item, target, position) {
        var me = this;
        if (!item.rendered) {
            me.configureItem(item);
            item.render(target, position);
        }
    },
    /**
     * Moves Component to the provided target instead.
     * @private
     */
    moveItem: function(item, target, position) {
        var activeEl = Ext.fly(document.activeElement);
        target = target.dom || target;
        if (typeof position === 'number') {
            position = target.childNodes[position];
        }
        // If the element we are about to move contains focus, ensure we don't
        // disturb application state when it blurs on remove.
        // 
        // That's if it blurs on remove! Some browsers don't, which leaves
        // focus "stranded" with framework and app state set, and rendition
        // set on an element while the element is in fact not focused.
        // By actively restoring focus afterwards we avoid an inconsistent state.
        // Specifically: https://sencha.jira.com/browse/EXTJS-20609
        if (item.el.contains(activeEl)) {
            activeEl.suspendFocusEvents();
        } else {
            activeEl = null;
        }
        target.insertBefore(item.el.dom, position || null);
        item.container = Ext.get(target);
        this.configureItem(item);
        // If we moved the element that contained focus, silently restore it.
        if (activeEl) {
            activeEl.focus();
            activeEl.resumeFocusEvents();
        }
    },
    /**
     * This method is called when a child item changes in some way. By default this calls
     * {@link Ext.Component#method!updateLayout} on this layout's owner.
     * 
     * @param {Ext.Component} child The child item that has changed.
     * @return {Boolean} True if this layout has handled the content change.
     */
    onContentChange: function() {
        this.owner.updateLayout();
        return true;
    },
    /**
     * A one-time initialization method called just before rendering.
     * @protected
     */
    initLayout: function() {
        this.initialized = true;
    },
    /**
     * @private
     * Sets the layout owner
     */
    setOwner: function(owner) {
        this.owner = owner;
    },
    /**
     * Returns the set of items to layout (empty by default).
     * @protected
     */
    getLayoutItems: function() {
        return [];
    },
    onAdd: function(item) {
        item.ownerLayout = this;
    },
    onRemove: Ext.emptyFn,
    onDestroy: Ext.emptyFn,
    /**
     * Removes layout's itemCls and owning Container's itemCls.
     * Clears the managed dimensions flags
     * @protected
     */
    afterRemove: function(item) {
        var me = this,
            el = item.el,
            owner = me.owner,
            removeClasses;
        if (item.rendered) {
            removeClasses = [].concat(me.itemCls || []);
            if (owner.itemCls) {
                removeClasses = Ext.Array.push(removeClasses, owner.itemCls);
            }
            if (removeClasses.length) {
                el.removeCls(removeClasses);
            }
        }
        delete item.ownerLayout;
    },
    /**
     * @private
     * Called by an owning Panel after the Panel finishes its collapse process.
     */
    afterCollapse: function(owner, animated) {
        if (animated) {
            this.onContentChange(owner);
        }
    },
    /**
     * @private
     * Called by an owning Panel after the Panel finishes its expand process.
     */
    afterExpand: function(owner, animated) {
        if (animated) {
            this.onContentChange(owner);
        }
    },
    /**
     * Destroys this layout. This method removes a `targetCls` from the `target`
     * element and calls `onDestroy`.
     * 
     * A derived class can override either this method or `onDestroy` but in all
     * cases must call the base class versions of these methods to allow the base class to
     * perform its cleanup.
     * 
     * This method (or `onDestroy`) are overridden by subclasses most often to purge
     * event handlers or remove unmanged DOM nodes.
     *
     * @protected
     */
    destroy: function() {
        var me = this,
            target;
        if (me.targetCls) {
            target = me.getTarget();
            if (target) {
                target.removeCls(me.targetCls);
            }
        }
        if (!me.onDestroy.$emptyFn) {
            me.onDestroy();
        }
        me.callParent();
    },
    sortWeightedItems: function(items, reverseProp) {
        var i, length;
        for (i = 0 , length = items.length; i < length; ++i) {
            items[i].$i = i;
        }
        Ext.Array.sort(items, function(item1, item2) {
            var ret = item2.weight - item1.weight;
            if (!ret) {
                ret = item1.$i - item2.$i;
                if (item1[reverseProp]) {
                    ret = -ret;
                }
            }
            return ret;
        });
        for (i = 0; i < length; ++i) {
            delete items[i].$i;
        }
    }
}, function() {
    var Layout = this;
    Layout.prototype.sizeModels = Layout.sizeModels = Ext.layout.SizeModel.sizeModels;
});

/**
 * This class is intended to be extended or created via the
 * {@link Ext.container.Container#layout layout} configuration property.
 * See {@link Ext.container.Container#layout} for additional details.
 */
Ext.define('Ext.layout.container.Container', {
    extend: 'Ext.layout.Layout',
    alternateClassName: 'Ext.layout.ContainerLayout',
    alias: 'layout.container',
    mixins: [
        'Ext.util.ElementContainer'
    ],
    requires: [
        'Ext.XTemplate'
    ],
    type: 'container',
    /**
     * @cfg {String} itemCls
     * An optional extra CSS class that will be added to the container. This can be useful for
     * adding customized styles to the container or any of its children using standard CSS
     * rules. See {@link Ext.Component}.{@link Ext.Component#componentCls componentCls} also.
     */
    /**
     * @private
     * Called by an owning Panel before the Panel begins its collapse process.
     * Most layouts will not need to override the default Ext.emptyFn implementation.
     */
    beginCollapse: Ext.emptyFn,
    /**
     * @private
     * Called by an owning Panel before the Panel begins its expand process.
     * Most layouts will not need to override the default Ext.emptyFn implementation.
     */
    beginExpand: Ext.emptyFn,
    /**
     * An object which contains boolean properties specifying which properties are to be
     * animated upon flush of child Component ContextItems. For example, Accordion would
     * have:
     *
     *      {
     *          y: true,
     *          height: true
     *      }
     *
     * @private
     */
    animatePolicy: null,
    /**
     * @private
     * tracks the number of child items that do not use "liquid" CSS layout
     */
    activeItemCount: 0,
    renderTpl: [
        '{%this.renderBody(out,values)%}'
    ],
    usesContainerHeight: true,
    usesContainerWidth: true,
    usesHeight: true,
    usesWidth: true,
    constructor: function() {
        this.callParent(arguments);
        this.mixins.elementCt.constructor.call(this);
    },
    destroy: function() {
        this.mixins.elementCt.destroy.call(this);
        this.callParent();
    },
    /**
     * In addition to work done by our base classes, containers benefit from some extra
     * cached data. The following properties are added to the ownerContext:
     *
     *  - visibleItems: the result of {@link #getVisibleItems}
     *  - childItems: the ContextItem[] for each visible item
     *  - targetContext: the ContextItem for the {@link #getTarget} element
     */
    beginLayout: function(ownerContext) {
        this.callParent(arguments);
        ownerContext.targetContext = ownerContext.paddingContext = ownerContext.getEl('getTarget', this);
        this.cacheChildItems(ownerContext);
    },
    beginLayoutCycle: function(ownerContext, firstCycle) {
        var me = this;
        me.callParent(arguments);
        if (firstCycle) {
            if (me.usesContainerHeight) {
                ++ownerContext.consumersContainerHeight;
            }
            if (me.usesContainerWidth) {
                ++ownerContext.consumersContainerWidth;
            }
        }
    },
    cacheChildItems: function(ownerContext) {
        var me = this,
            context, childItems, items, length, i;
        // if we neither read nor set the size of our items, we can skip creation of
        // the childItems array
        if (me.needsItemSize || me.setsItemSize) {
            context = ownerContext.context;
            childItems = ownerContext.childItems = [];
            items = ownerContext.visibleItems = me.getVisibleItems();
            length = items.length;
            for (i = 0; i < length; ++i) {
                childItems.push(context.getCmp(items[i]));
            }
        }
    },
    cacheElements: function() {
        var owner = this.owner;
        this.attachChildEls(owner.el, owner);
    },
    // from ElementContainer mixin
    calculate: function(ownerContext) {
        var props = ownerContext.props,
            el = ownerContext.el;
        if (ownerContext.widthModel.shrinkWrap && isNaN(props.width)) {
            ownerContext.setContentWidth(el.getWidth());
        }
        if (ownerContext.heightModel.shrinkWrap && isNaN(props.height)) {
            ownerContext.setContentHeight(el.getHeight());
        }
    },
    /**
     * Adds layout's itemCls and owning Container's itemCls
     * @protected
     */
    configureItem: function(item) {
        var me = this,
            itemCls = me.itemCls,
            ownerItemCls = me.owner.itemCls,
            needsCopy, addClasses;
        // Effectively callParent but without the function overhead
        item.ownerLayout = me;
        if (itemCls) {
            // itemCls can be a single class or an array
            if (typeof itemCls === 'string') {
                addClasses = [
                    itemCls
                ];
            } else {
                addClasses = itemCls;
                needsCopy = !!addClasses;
            }
        }
        if (ownerItemCls) {
            // Add some extra logic so we don't clone the array unnecessarily
            if (needsCopy) {
                addClasses = Ext.Array.clone(addClasses);
            }
            addClasses = Ext.Array.push(addClasses || [], ownerItemCls);
        }
        if (addClasses) {
            item.addCls(addClasses);
        }
    },
    doRenderBody: function(out, renderData) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!
        this.renderItems(out, renderData);
        this.renderContent(out, renderData);
    },
    doRenderContainer: function(out, renderData) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!
        var me = renderData.$comp.layout,
            tpl = me.getRenderTpl(),
            data = me.getRenderData();
        tpl.applyOut(data, out);
    },
    doRenderItems: function(out, renderData) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!
        var me = renderData.$layout,
            tree = me.getRenderTree();
        if (tree) {
            Ext.DomHelper.generateMarkup(tree, out);
        }
    },
    doRenderTabGuard: function(out, renderData, position) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!
        var cmp = renderData.$comp,
            tabGuardTpl;
        // Due to framing, we will be called in two different ways: in the frameTpl or in
        // the renderTpl. The frameTpl version enters via doRenderFramingTabGuard which
        // sets "$skipTabGuards" on the renderTpl's renderData.
        //
        if (cmp.tabGuard && !renderData.$skipTabGuards) {
            tabGuardTpl = cmp.lookupTpl('tabGuardTpl');
            if (tabGuardTpl) {
                renderData.tabGuard = position;
                renderData.tabGuardEl = cmp.tabGuardElements[position];
                cmp.addChildEl(renderData.tabGuardEl);
                tabGuardTpl.applyOut(renderData, out);
                delete renderData.tabGuard;
                delete renderData.tabGuardEl;
            }
        }
    },
    finishRender: function() {
        var me = this,
            target, items;
        me.callParent();
        me.cacheElements();
        target = me.getRenderTarget();
        items = me.getLayoutItems();
        me.finishRenderItems(target, items);
    },
    /**
     * @private
     * Called for every layout in the layout context after all the layouts have been finally flushed
     */
    notifyOwner: function() {
        if (!this._hasTargetWarning && this.targetCls && !this.getTarget().hasCls(this.targetCls)) {
            this._hasTargetWarning = true;
            Ext.log.warn('targetCls is missing. This may mean that getTargetEl() ' + 'is being overridden but not applyTargetCls(). ' + this.owner.id);
        }
        this.owner.afterLayout(this);
    },
    /**
     * Returns the container size (that of the target). Only the fixed-sized dimensions can
     * be returned because the shrinkWrap dimensions are based on the contentWidth/Height
     * as determined by the container layout.
     *
     * @param {Ext.layout.ContextItem} ownerContext The owner's context item.
     * @param {Boolean} [inDom=false] True if the container size must be in the DOM.
     * @return {Object} The size
     * @return {Number} return.width The width
     * @return {Number} return.height The height
     * @protected
     */
    getContainerSize: function(ownerContext, inDom) {
        // Subtle But Important:
        //
        // We don't want to call getProp/hasProp et.al. unless we in fact need that value
        // for our results! If we call it and don't need it, the layout manager will think
        // we depend on it and will schedule us again should it change.
        var targetContext = ownerContext.targetContext,
            frameInfo = targetContext.getFrameInfo(),
            padding = ownerContext.paddingContext.getPaddingInfo(),
            got = 0,
            needed = 0,
            gotWidth, gotHeight, width, height;
        // In an shrinkWrap width/height case, we must not ask for any of these dimensions
        // because they will be determined by contentWidth/Height which is calculated by
        // this layout...
        // Fit/Card layouts are able to set just the width of children, allowing child's
        // resulting height to autosize the Container.
        // See examples/tabs/tabs.html for an example of this.
        if (!ownerContext.widthModel.shrinkWrap) {
            ++needed;
            width = inDom ? targetContext.getDomProp('width') : targetContext.getProp('width');
            gotWidth = (typeof width === 'number');
            if (gotWidth) {
                ++got;
                width -= frameInfo.width + padding.width;
                if (width < 0) {
                    width = 0;
                }
            }
        }
        if (!ownerContext.heightModel.shrinkWrap) {
            ++needed;
            height = inDom ? targetContext.getDomProp('height') : targetContext.getProp('height');
            gotHeight = (typeof height === 'number');
            if (gotHeight) {
                ++got;
                height -= frameInfo.height + padding.height;
                if (height < 0) {
                    height = 0;
                }
            }
        }
        return {
            width: width,
            height: height,
            needed: needed,
            got: got,
            gotAll: got === needed,
            gotWidth: gotWidth,
            gotHeight: gotHeight
        };
    },
    // This method is used to offset the DOM position when checking
    // whether the element is a certain child of the target. This is
    // required in cases where the extra elements prepended to the target
    // before any of the items. An example of this is when using labelAlign: 'top'
    // on a field. The label appears first in the DOM before any child items are
    // created, so when we check the position we need to add an extra offset.
    // Containers that create an innerCt are exempt because this new element
    // preserves the order
    getPositionOffset: function(position) {
        var offset;
        if (!this.createsInnerCt) {
            offset = this.owner.itemNodeOffset;
            if (offset) {
                position += offset;
            }
        }
        return position;
    },
    /**
     * Returns an array of child components either for a render phase (Performed in the beforeLayout
     * method of the layout's base class), or the layout phase (onLayout).
     * @return {Ext.Component[]} of child components
     */
    getLayoutItems: function() {
        var owner = this.owner,
            items = owner && owner.items;
        return (items && items.items) || [];
    },
    getRenderData: function() {
        var comp = this.owner;
        return {
            $comp: comp,
            $layout: this,
            ownerId: comp.id
        };
    },
    /**
     * @protected
     * Returns all items that are rendered
     * @return {Array} All matching items
     */
    getRenderedItems: function() {
        var me = this,
            target = me.getRenderTarget(),
            items = me.getLayoutItems(),
            ln = items.length,
            renderedItems = [],
            i, pos, item;
        for (i = 0 , pos = 0; i < ln; i++ , pos++) {
            item = items[i];
            if (item.rendered) {
                if (item.ignoreDomPosition) {
                    --pos;
                } else if (!this.isValidParent(item, target, pos)) {
                    
                    continue;
                }
                renderedItems.push(item);
            }
        }
        return renderedItems;
    },
    /**
     * Returns the element into which rendering must take place. Defaults to the owner Container's
     * target element.
     *
     * May be overridden in layout managers which implement an inner element.
     *
     * @return {Ext.dom.Element}
     */
    getRenderTarget: function() {
        return this.owner.getTargetEl();
    },
    /**
     * Returns the element into which extra functional DOM elements can be inserted.
     * Defaults to the owner Component's encapsulating element.
     *
     * May be overridden in Component layout managers which implement a
     * {@link #getRenderTarget component render target} which must only contain child components.
     * @return {Ext.dom.Element}
     */
    getElementTarget: function() {
        return this.getRenderTarget();
    },
    getRenderTpl: function() {
        var me = this,
            renderTpl = Ext.XTemplate.getTpl(this, 'renderTpl');
        // Make sure all standard callout methods for the owner component are placed on the
        // XTemplate instance (but only once please):
        if (!renderTpl.renderContent) {
            me.owner.setupRenderTpl(renderTpl);
        }
        return renderTpl;
    },
    getRenderTree: function() {
        var result,
            items = this.owner.items,
            itemsGen,
            renderCfgs = {};
        do {
            itemsGen = items.generation;
            result = this.getItemsRenderTree(this.getLayoutItems(), renderCfgs);
        } while (items.generation !== itemsGen);
        return result;
    },
    renderChildren: function() {
        var me = this,
            ownerItems = me.owner.items,
            target = me.getRenderTarget(),
            itemsGen, items;
        // During the render phase, new items may be added. Specifically, a panel will
        // create a placeholder component during render if required, so we need to catch
        // it here so we can render it.
        do {
            itemsGen = ownerItems.generation;
            items = me.getLayoutItems();
            me.renderItems(items, target);
        } while (ownerItems.generation !== itemsGen);
    },
    getScrollbarsNeeded: function(width, height, contentWidth, contentHeight) {
        var scrollbarSize = Ext.scrollbar.size(),
            hasWidth = typeof width === 'number',
            hasHeight = typeof height === 'number',
            needHorz = 0,
            needVert = 0;
        // No space-consuming scrollbars.
        if (!scrollbarSize.width) {
            return 0;
        }
        if (hasHeight && height < contentHeight) {
            needVert = 2;
            width -= scrollbarSize.width;
        }
        if (hasWidth && width < contentWidth) {
            needHorz = 1;
            if (!needVert && hasHeight) {
                height -= scrollbarSize.height;
                if (height < contentHeight) {
                    needVert = 2;
                }
            }
        }
        return needVert + needHorz;
    },
    /**
     * Returns the owner component's resize element.
     * @return {Ext.dom.Element}
     */
    getTarget: function() {
        return this.owner.getTargetEl();
    },
    /**
     * @protected
     * Returns all items that are both rendered and visible
     * @return {Array} All matching items
     */
    getVisibleItems: function() {
        var target = this.getRenderTarget(),
            items = this.getLayoutItems(),
            ln = items.length,
            visibleItems = [],
            i, pos, item;
        for (i = 0 , pos = 0; i < ln; i++ , pos++) {
            item = items[i];
            if (item.rendered && item.hidden !== true && !item.floated) {
                if (item.ignoreDomPosition) {
                    --pos;
                } else if (!this.isValidParent(item, target, pos)) {
                    
                    continue;
                }
                visibleItems.push(item);
            }
        }
        return visibleItems;
    },
    getMoveAfterIndex: function(after) {
        return this.owner.items.indexOf(after) + 1;
    },
    moveItemBefore: function(item, before) {
        var owner = this.owner,
            items = owner.items,
            index = items.indexOf(item),
            toIndex;
        if (item === before) {
            return item;
        }
        if (before) {
            toIndex = items.indexOf(before);
            if (index > -1 && index < toIndex) {
                --toIndex;
            }
        } else {
            toIndex = items.length;
        }
        return owner.insert(toIndex, item);
    },
    setupRenderTpl: function(renderTpl) {
        renderTpl.renderBody = this.doRenderBody;
        renderTpl.renderContainer = this.doRenderContainer;
        renderTpl.renderItems = this.doRenderItems;
        renderTpl.renderTabGuard = this.doRenderTabGuard;
    },
    getContentTarget: function() {
        return this.owner.getDefaultContentTarget();
    },
    onAdd: function(item) {
        if (!item.liquidLayout) {
            ++this.activeItemCount;
        }
        this.callParent([
            item
        ]);
    },
    onRemove: function(item, isDestroying) {
        if (!item.liquidLayout) {
            --this.activeItemCount;
        }
        this.callParent([
            item,
            isDestroying
        ]);
    }
});

/**
 * @class Ext.layout.container.Auto
 *
 * The AutoLayout is the default layout manager delegated by {@link Ext.container.Container} to
 * render any child Components when no `{@link Ext.container.Container#layout layout}`
 * is configured into a `{@link Ext.container.Container Container}.` AutoLayout provides
 * only a passthrough of any layout calls to any child containers.
 *
 *     @example
 *     Ext.create('Ext.Panel', {
 *         width: 500,
 *         height: 280,
 *         title: 'AutoLayout Panel',
 *         layout: 'auto',
 *         renderTo: document.body,
 *         items: [{
 *             xtype: 'panel',
 *             title: 'Top Inner Panel',
 *             width: '75%',
 *             height: 90
 *         }, {
 *             xtype: 'panel',
 *             title: 'Bottom Inner Panel',
 *             width: '75%',
 *             height: 90
 *         }]
 *     });
 */
Ext.define('Ext.layout.container.Auto', {
    extend: 'Ext.layout.container.Container',
    alias: [
        'layout.auto',
        'layout.autocontainer'
    ],
    type: 'autocontainer',
    childEls: [
        'outerCt',
        'innerCt'
    ],
    /**
     * @cfg {Boolean} [reserveScrollbar=false]
     * Set to `true` to leave space for a vertical scrollbar (if the OS shows space-consuming
     * scrollbars) regardless of whether a scrollbar is needed.
     *
     * This is useful if content height changes during application usage, but you do not want
     * the calculated width of child items to change when a scrollbar appears or disappears.
     * The scrollbar will appear in the reserved space, and the calculated width
     * of child Components will not change.
     *
     *     @example
     *     Ext.define('Employee', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *            {name: 'rating', type: 'int'},
     *            {name: 'salary', type: 'float'},
     *            {name: 'name'}
     *         ]
     *     });
     *
     *     function createFakeData(count) {
     *         var firstNames = ['Screech', 'Kelly', 'Zach', 'Jessie', 'Lisa', 'A.C.', 'Richard'],
     *             lastNames = ['Powers', 'Kapowski', 'Morris', 'Spano', 'Turtle', 'Slater',
     *                          'Belding'],
     *             ratings = [1, 2, 3, 4, 5],
     *             salaries = [100, 400, 900, 1500, 1000000],
     *             data = [];
     *
     *         for (var i = 0; i < (count || 25); i++) {
     *             var ratingId = Math.floor(Math.random() * ratings.length),
     *                 salaryId = Math.floor(Math.random() * salaries.length),
     *                 firstNameId = Math.floor(Math.random() * firstNames.length),
     *                 lastNameId = Math.floor(Math.random() * lastNames.length),
     *
     *                 rating = ratings[ratingId],
     *                 salary = salaries[salaryId],
     *                 name = Ext.String.format(
     *                     "{0} {1}", firstNames[firstNameId], lastNames[lastNameId]
     *                 );
     *
     *             data.push({
     *                 rating: rating,
     *                 salary: salary,
     *                 name: name
     *             });
     *         }
     *         store.loadData(data);
     *     }
     *
     *     // create the Data Store
     *     var store = Ext.create('Ext.data.Store', {
     *         id: 'store',
     *         model: 'Employee',
     *         proxy: {
     *             type: 'memory'
     *         }
     *     });
     *     createFakeData(10);
     *
     *     var grid = Ext.create('Ext.grid.Panel', {
     *         title: 'Grid loaded with varying number of records',
     *         anchor: '100%',
     *         store: store,
     *         columns: [{
     *             xtype: 'rownumberer',
     *             width: 40,
     *             sortable: false
     *         },{
     *             text: 'Name',
     *             flex: 1,
     *             sortable: true,
     *             dataIndex: 'name'
     *         },{
     *             text: 'Rating',
     *             width: 125,
     *             sortable: true,
     *             dataIndex: 'rating'
     *         },{
     *             text: 'Salary',
     *             width: 125,
     *             sortable: true,
     *             dataIndex: 'salary',
     *             align: 'right',
     *             renderer: Ext.util.Format.usMoney
     *         }]
     *     });
     *
     *     Ext.create('Ext.panel.Panel', {
     *         renderTo: document.body,
     *         width: 800,
     *         height: 600,
     *         layout: {
     *             type: 'anchor',
     *             reserveScrollbar: true // There will be a gap even when there's no scrollbar
     *         },
     *         scrollable: true,
     *         items: grid,
     *         tbar: {
     *             defaults: {
     *                 handler: function(b) {
     *                     createFakeData(b.count);
     *                 }
     *             },
     *             items: [{
     *                  text: '10 Items',
     *                  count: 10
     *             },{
     *                  text: '100 Items',
     *                  count: 100
     *             },{
     *                  text: '300 Items',
     *                  count: 300
     *             },{
     *                  text: '1000 Items',
     *                  count: 1000
     *             },{
     *                  text: '5000 Items',
     *                  count: 5000
     *             }]
     *         }
     *     });
     *
     */
    reserveScrollbar: false,
    /**
     * @property {Boolean} [managePadding=true]
     * indicates that this layout will correct cross browser padding differences when the
     * container has overflow.
     * 
     * In some browsers the right and/or bottom padding of a container is lost when
     * the container has overflow.  If managePadding is true the layout will apply the
     * padding to an inner wrapping element instead of the container element that has the
     * overflow so that paddding will be included in the scrollable area.
     * Note: padding will not be managed if it is configured on the container using
     * a style config or css class.  In order to be managed, padding must be added to the
     * container using the appropriate {@link Ext.Component#contentPaddingProperty
     * contentPaddingProperty}.  For {@link Ext.panel.Panel Panels} use 
     * {@link Ext.panel.Panel#bodyPadding}, and for
     * {@link Ext.container.Container Containers}, use
     * {@link Ext.Component#padding padding}
     */
    managePadding: true,
    /**
     * @property {Boolean} [manageOverflow=false]
     * true to rerun the layout if scrollbars are needed.
     */
    manageOverflow: false,
    // auto layout does not care about the dimensions of individual child items since
    // it does not size them, and it measures them as a whole when in shrinkWrap mode.
    needsItemSize: false,
    setsItemSize: false,
    // Begin with no previous adjustments
    lastOverflowAdjust: {
        width: 0,
        height: 0
    },
    outerCtCls: Ext.baseCSSPrefix + 'autocontainer-outerCt',
    innerCtCls: Ext.baseCSSPrefix + 'autocontainer-innerCt',
    /* eslint-disable indent, max-len */
    // Auto layout's renderTpl wraps the content in an outerCt which is used to accomplish
    // the following 3 goals:
    // 
    // 1. When the container has a shrink wrapped width and/or height, the outerCt is used
    // to measure the size of the content.
    // 2. When the container has overflow some browsers lose the container's right and/or
    // bottom padding.  To fix this, the padding is rendered to the outerCt instead of
    // the container target element.  This ensures that the padding is included in the 
    // container's scrollWidth/scrollHeight. In Old IE when a table is used, the padding
    // is rendered to the innerCt td element.
    // 3. The outerCt contains the margins of its children, that is to say, it prevents
    // them from collapsing.
    renderTpl: [
        // An outerCt with display:table shrink-wraps contents, and contains child
        // margins. The table-cell innerCt is required in order to support percentage
        // heights on child elements.
        '<div id="{ownerId}-outerCt" data-ref="outerCt" class="{outerCtCls}" role="presentation">',
        '<div id="{ownerId}-innerCt" data-ref="innerCt" style="{%this.renderPadding(out, values)%}" ',
        // If raw HTML is used as the component's content, avoid setting
        // presentation role so as not to mask the content from screen readers
        '<tpl if="!$comp.html">role="presentation"</tpl>',
        'class="{innerCtCls}">',
        '{%this.renderBody(out,values)%}',
        '</div>',
        '</div>'
    ],
    /* eslint-enable indent, max-len */
    beginLayout: function(ownerContext) {
        this.callParent(arguments);
        this.initContextItems(ownerContext);
    },
    beforeLayoutCycle: function(ownerContext) {
        var comp = this.owner,
            inheritedState = comp.inheritedState,
            inheritedStateInner = comp.inheritedStateInner;
        if (!inheritedState || inheritedState.invalid) {
            inheritedState = comp.getInherited();
            // fixes both
            inheritedStateInner = comp.inheritedStateInner;
        }
        if (ownerContext.widthModel.shrinkWrap) {
            inheritedStateInner.inShrinkWrapTable = true;
        } else {
            delete inheritedStateInner.inShrinkWrapTable;
        }
    },
    beginLayoutCycle: function(ownerContext) {
        var me = this,
            outerCt = me.outerCt,
            lastOuterCtWidth = me.lastOuterCtWidth || '',
            lastOuterCtHeight = me.lastOuterCtHeight || '',
            lastOuterCtTableLayout = me.lastOuterCtTableLayout || '',
            state = ownerContext.state,
            overflowXStyle, outerCtWidth, outerCtHeight, outerCtTableLayout, inheritedStateInner;
        me.callParent(arguments);
        // Default to "shrink wrap styles".
        outerCtWidth = outerCtHeight = outerCtTableLayout = '';
        if (!ownerContext.widthModel.shrinkWrap) {
            // if we're not shrink wrapping width, we need to get the innerCt out of the
            // way to avoid any shrink wrapping effect on child items
            // fill the available width within the container
            outerCtWidth = '100%';
            inheritedStateInner = me.owner.inheritedStateInner;
            // expand no further than the available width, even if contents are wider
            // unless there is a potential for horizontal overflow, then allow
            // the outerCt to expand to the width of the contents
            overflowXStyle = me.getOverflowXStyle(ownerContext);
            outerCtTableLayout = (inheritedStateInner.inShrinkWrapTable || overflowXStyle === 'auto' || overflowXStyle === 'scroll') ? '' : 'fixed';
        }
        if (!ownerContext.heightModel.shrinkWrap && !Ext.supports.PercentageHeightOverflowBug) {
            // if we're not shrink wrapping height, we need to get the outerCt out of the
            // way so that percentage height children will be sized correctly.  We do this
            // by giving the outerCt a height of '100%' unless the browser is affected by
            // the "percentage height overflow bug", in which case the outerCt will get a
            // pixel height set during the calculate phase after we know the targetEl size.
            outerCtHeight = '100%';
        }
        // if the outerCt width changed since last time (becuase of a widthModel change)
        // or if we set a pixel width on the outerCt last time to work around a browser-
        // specific bug, we need to set the width of the outerCt
        if ((outerCtWidth !== lastOuterCtWidth) || me.hasOuterCtPxWidth) {
            outerCt.setStyle('width', outerCtWidth);
            me.lastOuterCtWidth = outerCtWidth;
            me.hasOuterCtPxWidth = false;
        }
        // Set the outerCt table-layout property if different from last time.
        if (outerCtTableLayout !== lastOuterCtTableLayout) {
            outerCt.setStyle('table-layout', outerCtTableLayout);
            me.lastOuterCtTableLayout = outerCtTableLayout;
        }
        // if the outerCt height changed since last time (becuase of a heightModel change)
        // or if we set a pixel height on the outerCt last time to work around a browser-
        // specific bug, we need to set the height of the outerCt
        if ((outerCtHeight !== lastOuterCtHeight) || me.hasOuterCtPxHeight) {
            outerCt.setStyle('height', outerCtHeight);
            me.lastOuterCtHeight = outerCtHeight;
            me.hasOuterCtPxHeight = false;
        }
        if (me.hasInnerCtPxHeight) {
            me.innerCt.setStyle('height', '');
            me.hasInnerCtPxHeight = false;
        }
        // Begin with the scrollbar adjustment that we used last time - this is more likely
        // to be correct than beginning with no adjustment at all, but only if it is not
        // already defined - it may have already been set by invalidate()
        state.overflowAdjust = state.overflowAdjust || me.lastOverflowAdjust;
    },
    calculate: function(ownerContext) {
        var me = this,
            state = ownerContext.state,
            containerSize = me.getContainerSize(ownerContext, true),
            calculatedItems;
        // If subclass has a calculateItems method, call it and cache the result
        calculatedItems = state.calculatedItems || (state.calculatedItems = me.calculateItems ? me.calculateItems(ownerContext, containerSize) : true);
        me.setCtSizeIfNeeded(ownerContext, containerSize);
        if (calculatedItems && ownerContext.hasDomProp('containerChildrenSizeDone')) {
            me.calculateContentSize(ownerContext);
            if (containerSize.gotAll) {
                if (me.manageOverflow && !ownerContext.state.secondPass && !me.reserveScrollbar) {
                    me.calculateOverflow(ownerContext, containerSize);
                }
                return;
            }
        }
        me.done = false;
    },
    calculateContentSize: function(ownerContext) {
        var me = this,
            containerDimensions = ((ownerContext.widthModel.shrinkWrap ? 1 : 0) | (ownerContext.heightModel.shrinkWrap ? 2 : 0)),
            calcWidth = (containerDimensions & 1) || undefined,
            calcHeight = (containerDimensions & 2) || undefined,
            needed = 0,
            props = ownerContext.props;
        if (calcWidth) {
            if (isNaN(props.contentWidth)) {
                ++needed;
            } else {
                calcWidth = undefined;
            }
        }
        if (calcHeight) {
            if (isNaN(props.contentHeight)) {
                ++needed;
            } else {
                calcHeight = undefined;
            }
        }
        if (needed) {
            if (calcWidth && !ownerContext.setContentWidth(me.measureContentWidth(ownerContext))) {
                me.done = false;
            }
            // eslint-disable-next-line max-len
            if (calcHeight && !ownerContext.setContentHeight(me.measureContentHeight(ownerContext))) {
                me.done = false;
            }
        }
    },
    // if (me.done) {
    //    var el = ownerContext.targetContext.el.dom;
    //    Ext.log(this.owner.id, '.contentSize: ', contentWidth, 'x', contentHeight,
    //        ' => scrollSize: ', el.scrollWidth, 'x', el.scrollHeight);
    // }
    /**
     * Handles overflow processing for a container.  In addition to the ownerContext
     * passed to the {@link #calculate} method, this method also needs the containerSize
     * (the object returned by {@link #getContainerSize}).
     * @protected
     * 
     * @param {Ext.layout.ContextItem} ownerContext
     */
    calculateOverflow: function(ownerContext) {
        var me = this,
            width, height, scrollbarSize, scrollbars, xauto, yauto, targetEl;
        // Determine the dimensions that have overflow:auto applied. If these come by
        // way of component config, this does not require a DOM read:
        xauto = (me.getOverflowXStyle(ownerContext) === 'auto');
        yauto = (me.getOverflowYStyle(ownerContext) === 'auto');
        if (xauto || yauto) {
            scrollbarSize = Ext.scrollbar.size();
            targetEl = ownerContext.overflowContext.el.dom;
            scrollbars = 0;
            if (targetEl.scrollWidth > targetEl.clientWidth) {
                // has horizontal scrollbar
                scrollbars |= 1;
            }
            if (targetEl.scrollHeight > targetEl.clientHeight) {
                // has vertical scrollbar
                scrollbars |= 2;
            }
            width = (yauto && (scrollbars & 2)) ? scrollbarSize.width : 0;
            height = (xauto && (scrollbars & 1)) ? scrollbarSize.height : 0;
            if (width !== me.lastOverflowAdjust.width || height !== me.lastOverflowAdjust.height) {
                me.done = false;
                // we pass overflowAdjust and overflowState in as state for the next
                // cycle (these are discarded if one of our ownerCt's invalidates):
                ownerContext.invalidate({
                    state: {
                        overflowAdjust: {
                            width: width,
                            height: height
                        },
                        overflowState: scrollbars,
                        secondPass: true
                    }
                });
            }
        }
    },
    completeLayout: function(ownerContext) {
        this.lastOverflowAdjust = ownerContext.state.overflowAdjust;
    },
    doRenderBody: function(out, renderData) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!
        var me = renderData.$layout,
            XTemplate = Ext.XTemplate,
            beforeBodyTpl = me.beforeBodyTpl,
            afterBodyTpl = me.afterBodyTpl;
        if (beforeBodyTpl) {
            XTemplate.getTpl(me, 'beforeBodyTpl').applyOut(renderData, out);
        }
        this.renderItems(out, renderData);
        this.renderContent(out, renderData);
        if (afterBodyTpl) {
            XTemplate.getTpl(me, 'afterBodyTpl').applyOut(renderData, out);
        }
    },
    doRenderPadding: function(out, renderData) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!
        var me = renderData.$layout,
            owner = renderData.$layout.owner,
            padding = owner[owner.contentPaddingProperty];
        if (me.managePadding && padding) {
            out.push('padding:', owner.unitizeBox(padding));
        }
    },
    finishedLayout: function(ownerContext) {
        var innerCt = this.innerCt;
        this.callParent(arguments);
        if (Ext.isIE8) {
            // IE8 needs a repaint to render percentage sized child items.
            innerCt.repaint();
        }
        if (Ext.isOpera) {
            // Opera also needs a repaint to render percentage sized child items. but 
            // the normal repaint() method doesn't seem to do the trick, but tweaking
            // the position property in combination with reading scrollWidth does.
            innerCt.setStyle('position', 'relative');
            // eslint-disable-next-line no-unused-expressions
            innerCt.dom.scrollWidth;
            innerCt.setStyle('position', '');
        }
    },
    /**
     * Returns the container size (that of the target). Only the fixed-sized dimensions can
     * be returned because the shrinkWrap dimensions are based on the contentWidth/Height
     * as determined by the container layout.
     *
     * If the {@link #calculateOverflow} method is used and if {@link #manageOverflow} is
     * true, this will adjust the width/height by the size of scrollbars.
     * 
     * @param {Ext.layout.ContextItem} ownerContext The owner's context item.
     * @param {Boolean} [inDom=false] True if the container size must be in the DOM.
     * @return {Object} The size
     * @return {Number} return.width The width
     * @return {Number} return.height The height
     * @protected
     */
    getContainerSize: function(ownerContext, inDom) {
        // Subtle But Important:
        // 
        // We don't want to call getProp/hasProp et.al. unless we in fact need that value
        // for our results! If we call it and don't need it, the layout manager will think
        // we depend on it and will schedule us again should it change.
        var size = this.callParent(arguments),
            overflowAdjust = ownerContext.state.overflowAdjust;
        if (overflowAdjust) {
            size.width -= overflowAdjust.width;
            size.height -= overflowAdjust.height;
        }
        return size;
    },
    getRenderData: function() {
        var me = this,
            data = me.callParent();
        data.innerCtCls = me.innerCtCls;
        data.outerCtCls = me.outerCtCls;
        return data;
    },
    // Overridden method from Ext.layout.container.Container.
    // Used in the beforeLayout method to render all items into.
    getRenderTarget: function() {
        return this.innerCt;
    },
    // Overridden method from Ext.layout.container.Container.
    // Used by Container classes to insert special DOM elements which must exist
    // in addition to the child components
    getElementTarget: function() {
        return this.innerCt;
    },
    /**
     * Returns the overflow-x style of the render target.
     * Note: If overflow is configured on a container using style or css class this method
     * will read the dom the first time it is called. It is therefore preferable for
     * performance reasons to use the {@link Ext.Component#scrollable scrollable} config when
     * horizontal overflow is desired.
     * @protected
     * @param {Ext.layout.ContextItem} ownerContext
     * @return {String}
     */
    getOverflowXStyle: function(ownerContext) {
        return ownerContext.overflowXStyle || (ownerContext.overflowXStyle = this.owner.scrollFlags.overflowX || ownerContext.overflowContext.getStyle('overflow-x'));
    },
    /**
     * Returns the overflow-y style of the render target.
     * Note: If overflow is configured on a container using style or css class this method
     * will read the dom the first time it is called. It is therefore preferable for
     * performance reasons to use the {@link Ext.Component#scrollable scrollable} config when
     * vertical overflow is desired.
     * @protected
     * @param {Ext.layout.ContextItem} ownerContext
     * @return {String}
     */
    getOverflowYStyle: function(ownerContext) {
        return ownerContext.overflowYStyle || (ownerContext.overflowYStyle = this.owner.scrollFlags.overflowY || ownerContext.overflowContext.getStyle('overflow-y'));
    },
    initContextItems: function(ownerContext) {
        var me = this,
            target = ownerContext.target,
            overflowEl = me.owner.getOverflowEl();
        ownerContext.outerCtContext = ownerContext.getEl('outerCt', me);
        ownerContext.innerCtContext = ownerContext.getEl('innerCt', me);
        ownerContext.overflowContext = (overflowEl === ownerContext.el) ? ownerContext : ownerContext.getEl(overflowEl);
        if (target[target.contentPaddingProperty] !== undefined) {
            // If padding was defined using the contentPaddingProperty, we render the
            // the padding to the innerCt or outerCt (depending on the template that is
            // being used), so we need to set the paddingContext accordingly.
            // Otherwise we leave paddingContext as set by Container layout (defaults to
            // the targetContext)
            ownerContext.paddingContext = ownerContext.innerCtContext;
        }
    },
    initLayout: function() {
        var me = this,
            scrollbarWidth = Ext.scrollbar.width(),
            owner = me.owner;
        me.callParent();
        // Create a default lastOverflowAdjust based upon scrolling configuration.
        // If the Container is to overflow, or we *always* reserve space for a scrollbar
        // then reserve space for a vertical scrollbar
        if (scrollbarWidth && me.manageOverflow && !me.hasOwnProperty('lastOverflowAdjust')) {
            if (owner.scrollable || me.reserveScrollbar) {
                me.lastOverflowAdjust = {
                    width: scrollbarWidth,
                    height: 0
                };
            }
        }
    },
    measureContentHeight: function(ownerContext) {
        // contentHeight includes padding, but not border, framing or margins
        var contentHeight = this.outerCt.getHeight(),
            target = ownerContext.target;
        if (this.managePadding && (target[target.contentPaddingProperty] === undefined)) {
            // if padding was not configured using the appropriate contentPaddingProperty
            // then the padding will not be on the paddingContext, and therfore not included
            // in the outerCt measurement, so we need to read the padding from the
            // targetContext
            contentHeight += ownerContext.targetContext.getPaddingInfo().height;
        }
        return contentHeight;
    },
    measureContentWidth: function(ownerContext) {
        var dom, style, old, contentWidth, target;
        // In the newer Chrome versions, it won't measure the
        // width correctly without repainting the inner
        // cell in some circumstances.
        if (this.chromeCellMeasureBug) {
            dom = this.innerCt.dom;
            style = dom.style;
            old = style.display;
            if (old === 'table-cell') {
                style.display = '';
                // eslint-disable-next-line no-unused-expressions
                dom.offsetWidth;
                style.display = old;
            }
        }
        if (Ext.isSafari) {
            // EXTJS-12041: Safari needs a reflow of the outerCt to measure content width
            // correctly in some cases.  The circumstances which make this happen are
            // very difficult to isolate, so we have to resort to always triggering a
            // reflow before measuring. We switch between table-cell and table in hopes
            // of minimizing the impact of the reflow on surrounding elements
            dom = this.outerCt.dom;
            style = dom.style;
            style.display = 'table-cell';
            // eslint-disable-next-line no-unused-expressions
            dom.offsetWidth;
            dom.style.display = '';
        }
        // contentWidth includes padding, but not border, framing or margins
        contentWidth = this.outerCt.getWidth();
        target = ownerContext.target;
        if (this.managePadding && (target[target.contentPaddingProperty] === undefined)) {
            // if padding was not configured using the appropriate contentPaddingProperty
            // then the padding will not be on the paddingContext, and therfore not included
            // in the outerCt measurement, so we need to read the padding from the
            // targetContext
            contentWidth += ownerContext.targetContext.getPaddingInfo().width;
        }
        return contentWidth;
    },
    /**
     * This method sets the height and/or width of the outerCt/innerCt to adjust for the
     * following browser-specific issues:
     * 
     * 1. In some browsers a percentage-height element ignores the horizontal scrollbar
     * of its parent (see Ext.supports.PercentageHeightOverflowBug).  If the browser is
     * affected by this bug the outerCt needs a pixel height in order to support
     * percentage-height children when not shrink-wrapping height. If the browser is not
     * affected by this bug, a height of 100% is assigned to the outerCt (see
     * beginLayoutCycle).
     * 
     * 2. IE8 mode has a bug with percentage height children.  if the innerCt has
     * a height of 100%, has padding, and has a child item with a percentage height, that
     * child item will be sized as a percentage of the parent's height plus padding height.
     * In other words, a child with height:50% would have its height caclulated thusly:
     * (parentHeight + parentPaddingHeight) * 0.5
     * To fix this, we have to give the innerCt a pixel height.
     * 
     * @protected
     * @param {Ext.layout.ContextItem} ownerContext
     * @param {Object} containerSize
     */
    setCtSizeIfNeeded: function(ownerContext, containerSize) {
        var me = this,
            height = containerSize.height,
            padding = ownerContext.paddingContext.getPaddingInfo(),
            targetEl = me.getTarget(),
            overflowXStyle = me.getOverflowXStyle(ownerContext),
            canOverflowX = (overflowXStyle === 'auto' || overflowXStyle === 'scroll'),
            scrollbarSize = Ext.scrollbar.size(),
            needsOuterHeight, needsInnerHeight;
        if (height && !ownerContext.heightModel.shrinkWrap) {
            if (Ext.supports.PercentageHeightOverflowBug) {
                // set a pixel height on the outerCt if the browser ignores horizontal
                // scrollbar when rendering percentage-height elements
                needsOuterHeight = true;
            }
            if (Ext.isIE8) {
                // When not shrink wrapping, we set a pixel height on the innerCt to
                // support percentage height children in IE8.
                needsInnerHeight = true;
            }
            if ((needsOuterHeight || needsInnerHeight) && canOverflowX && (targetEl.dom.scrollWidth > targetEl.dom.clientWidth)) {
                // adjust the height for scrollbar size since it's not accounted for
                // in the containerSize.
                // IE8 does not tolerate negative sizes
                height = Math.max(height - scrollbarSize.height, 0);
            }
            if (needsOuterHeight) {
                ownerContext.outerCtContext.setProp('height', height + padding.height);
                me.hasOuterCtPxHeight = true;
            }
            if (needsInnerHeight) {
                ownerContext.innerCtContext.setProp('height', height);
                me.hasInnerCtPxHeight = true;
            }
        }
    },
    setupRenderTpl: function(renderTpl) {
        this.callParent(arguments);
        renderTpl.renderPadding = this.doRenderPadding;
    },
    getContentTarget: function() {
        return this.innerCt;
    }
}, function(Cls) {
    var v = Ext.chromeVersion;
    // This was likely fixed much earlier, on the bug tracker marked as fixed on 2014/04/01.
    // 34 was the most recently released version after this date. Google doesn't release older
    // versions to test on so it's not possible to say. However due to the auto update nature it's
    // highly unlikely anyone is running this range anyway.
    Cls.prototype.chromeCellMeasureBug = Ext.isChrome && v >= 26 && v <= 34;
});

/**
 * A class that manages a group of {@link Ext.Component#cfg-floating} Components and 
 * provides z-order management, and Component activation behavior, including masking 
 * below the active (topmost) Component.
 *
 * {@link Ext.Component#cfg-floating Floating} Components which are rendered directly 
 * into the document (such as {@link Ext.window.Window Window}s) which are 
 * {@link Ext.Component#method-show show}n are managed by a
 * {@link Ext.WindowManager global instance}.
 *
 * {@link Ext.Component#cfg-floating Floating} Components which are descendants of 
 * {@link Ext.Component#cfg-floating floating} *Containers* (for example a 
 * {@link Ext.view.BoundList BoundList} within an {@link Ext.window.Window Window},
 * or a {@link Ext.menu.Menu Menu}), are managed by a ZIndexManager owned by that floating
 * Container. Therefore ComboBox dropdowns within Windows will have managed z-indices guaranteed
 * to be correct, relative to the Window.
 */
Ext.define('Ext.ZIndexManager', {
    alternateClassName: 'Ext.WindowGroup',
    requires: [
        'Ext.util.SorterCollection',
        'Ext.util.FilterCollection',
        'Ext.GlobalEvents'
    ],
    statics: {
        zBase: 9000,
        activeCounter: 0
    },
    reflowSuspended: 0,
    /**
     * @private
     */
    constructor: function(container) {
        var me = this;
        me.id = Ext.id(null, 'zindex-mgr-');
        // The stack is a collection sorted on the incrementing activeCounter ascending,
        // so recently active components sort to the top.
        // The component's alwaysOnTop flag takes priority in the sort order and
        // cause the component to gravitate to the correct end of the stack.
        me.zIndexStack = new Ext.util.Collection({
            sorters: {
                sorterFn: function(comp1, comp2) {
                    var ret = (comp1.alwaysOnTop || 0) - (comp2.alwaysOnTop || 0);
                    if (!ret) {
                        ret = comp1.getActiveCounter() - comp2.getActiveCounter();
                    }
                    return ret;
                }
            },
            filters: {
                filterFn: function(comp) {
                    return comp.isVisible();
                }
            }
        });
        // zIndexStack will call into this class on key lifecycle events if methods exist here.
        // Specifically, we implement onCollectionSort which is called by Component's updaters
        // for activeCounter and alwaysOnTop.
        me.zIndexStack.addObserver(me);
        me.front = null;
        me.sortCount = 0;
        // Listen for global component hiding and showing.
        // onComponentShowHide only reacts if we are managing the component.
        me.globalListeners = Ext.GlobalEvents.on({
            // The 'beforehide' global event is a non-vetoable event fired before the component
            // is hidden. We use this to sort the remaining visible components, and unmask
            // if the hiding component is the sole modal.
            beforehide: me.onComponentShowHide,
            show: me.onComponentShowHide,
            scope: me,
            destroyable: true
        });
        if (container) {
            // This is the ZIndexManager for an Ext.container.Container, base its zseed
            // on the zIndex of the Container's element
            if (container.isContainer) {
                me.resizeListeners = container.on({
                    resize: me.onContainerResize,
                    scope: me,
                    destroyable: true
                });
                me.zseed = Ext.Number.from(me.rendered ? container.getEl().getStyle('zIndex') : undefined, me.getNextZSeed());
                // The containing element we will be dealing with (eg masking) is the content target
                me.targetEl = container.getTargetEl();
                me.container = container;
            } else // This is the ZIndexManager for a DOM element
            {
                me.resizeListeners = Ext.on({
                    resize: me.onContainerResize,
                    scope: me,
                    destroyable: true
                });
                me.zseed = me.getNextZSeed();
                me.targetEl = Ext.get(container);
            }
        } else // No container passed means we are the global WindowManager. Our target is the doc body.
        // DOM must be ready to collect that ref.
        {
            me.zseed = me.getNextZSeed();
            Ext.onInternalReady(function() {
                // We need to use lowest possible priority here to give enough time
                // for layouts to run and resize if we're masking a contained component
                me.resizeListeners = Ext.on({
                    resize: me.scheduleContainerResize,
                    scope: me,
                    destroyable: true,
                    priority: -10000
                });
                me.targetEl = Ext.getBody();
            });
        }
    },
    // Required to be an Observer of a Collection
    getId: function() {
        return this.id;
    },
    getNextZSeed: function() {
        return (Ext.ZIndexManager.zBase += 10000);
    },
    setBase: function(baseZIndex) {
        this.zseed = baseZIndex;
        return this.onCollectionSort();
    },
    onCollectionSort: function() {
        var me = this,
            oldFront = me.front,
            zIndex = me.zseed,
            a = me.zIndexStack.getRange(),
            len = a.length,
            i, comp, topModal, topFocusable, topMost,
            doFocus = !oldFront || oldFront.isVisible();
        me.sortCount++;
        for (i = 0; i < len; i++) {
            comp = a[i];
            if (comp.destroying || comp.destroyed) {
                
                continue;
            }
            // Setting the zIndex of a Component returns the topmost zIndex consumed by
            // that Component.
            // If it's just a plain floating Component such as a BoundList, then the
            // return value is the passed value plus 10, ready for the next item.
            // If a floating *Container* has its zIndex set, it re-orders its managed
            // floating children, starting from that new base, and returns a value 10000 above
            // the highest zIndex which it allocates.
            zIndex = comp.setZIndex(zIndex);
            // Only register a new topmost to activate if we find one that is visible
            // Unfiltered panels with hidden:true can end up here during an animated hide process
            // When the hidden flag is set, and the ghost show operation kicks the ZIndexManager's
            // sort.
            if (!comp.hidden) {
                topMost = comp;
                // Track topmost visible modal so we can place the modal mask just below it.
                // Any prior focusable ones just became not focusable - they'll be below our mask.
                if (comp.modal) {
                    topModal = comp;
                    topFocusable = null;
                }
                // Track topmost focusable floater which is above all modals.
                // Unfocusable things like tooltips and toasts may be above it
                // but they do not matter, the topmost *focusable* must be focused.
                if (doFocus && (comp.isFocusable(true) && (comp.modal || comp.focusOnToFront))) {
                    topFocusable = comp;
                }
            }
        }
        // Sort resulted in a different topmost focusable.
        if (topFocusable && topFocusable !== oldFront && !topFocusable.preventFocusOnActivate) {
            topFocusable.onFocusTopmost();
        }
        // If we encountered a modal in our reassigment, ensure our modal mask is just below it.
        if (topModal) {
            // If it's the same topmost, then just ensure the
            // correct z-index and size of mask.
            if (topModal === me.topModal) {
                me.syncModalMask(topModal);
            } else // If it's a new top, we must re-show the mask because of tabbability resets.
            {
                me.showModalMask(topModal);
            }
        } else {
            me.hideModalMask();
        }
        // Inform components of change in to of stack.
        if (topMost !== me.topMost) {
            if (me.topMost) {
                // This one has been bumped from top.
                me.topMost.onZIndexChange(false);
            }
            if (topMost) {
                // This one is now at the top.
                topMost.onZIndexChange(true);
            }
        }
        // Cache the top of the stack
        me.front = topFocusable;
        me.topModal = topModal;
        me.topMost = topMost;
        // Ensure the top-most component is the front
        if (!me.front && me.topMost) {
            me.front = me.topMost;
        }
        return zIndex;
    },
    /**
     * @private
     * Called from {@link Ext.util.Floating} updater methods when a config which affects
     * the stack order is updated in a Component.
     *
     * eg {@link Ext.Component#alwaysOnTop alwaysOnTop} or
     * {@link Ext.Component#activeCounter activeCounter}
     */
    onComponentUpdate: function(comp) {
        if (!this.reflowSuspended && this.zIndexStack.contains(comp)) {
            this.zIndexStack.sort();
        }
    },
    suspendReflow: function() {
        this.reflowSuspended++;
    },
    resumeReflow: function(flush) {
        if (this.reflowSuspended && !--this.reflowSuspended) {
            if (flush) {
                this.zIndexStack.sort();
            }
        }
    },
    onAfterComponentRender: function(comp) {
        if (!this.reflowSuspended && comp.isVisible() && comp.toFrontOnShow) {
            this.zIndexStack.itemChanged(comp, 'hidden');
            this.zIndexStack.sort();
        }
    },
    /**
     * @private
     * Called when the global hide and show events are fired. If it is one of our components,
     * we must re-sort.
     */
    onComponentShowHide: function(comp) {
        var me = this,
            zIndexStack = me.zIndexStack,
            sortCount = me.sortCount;
        // If component has hidden, it will be filtered out, so we have to look in Collection's
        // source if it's there.
        if (comp.isFloating() && !me.hidingAll && (zIndexStack.getSource() || zIndexStack).contains(comp)) {
            if (me.tempHidden) {
                Ext.Array.remove(me.tempHidden, comp);
            }
            zIndexStack.beginUpdate();
            // Showing. If it should go to front on show; nudge the active
            // counter which will cause a stack sort.
            if (comp.isVisible()) {
                if (comp.toFrontOnShow) {
                    zIndexStack.itemChanged(comp, 'hidden');
                    comp.setActiveCounter(++Ext.ZIndexManager.activeCounter);
                }
            } else // Hiding
            {
                zIndexStack.itemChanged(comp, 'hidden');
            }
            // We must update the frontmost according to the new stack order
            // even if there has been no sort (Collection will not autosort if only one member)
            zIndexStack.endUpdate();
            if (me.sortCount === sortCount && !me.reflowSuspended) {
                me.onCollectionSort();
            }
        }
    },
    /**
     * Registers a floating {@link Ext.Component} with this ZIndexManager. This should not
     * need to be called under normal circumstances. Floating Components (such as Windows,
     * BoundLists and Menus) are automatically registered with a
     * {@link Ext.Component#zIndexManager zIndexManager} at render time.
     *
     * Where this may be useful is moving Windows between two ZIndexManagers. For example,
     * to bring the Ext.MessageBox dialog under the same manager as the Desktop's
     * ZIndexManager in the desktop sample app:
     *
     *     MyDesktop.getDesktop().getManager().register(Ext.MessageBox);
     *
     * @param {Ext.Component} comp The Component to register.
     */
    register: function(comp) {
        var me = this;
        if (comp.zIndexManager) {
            comp.zIndexManager.unregister(comp);
        }
        comp.zIndexManager = me;
        if (!comp.rendered) {
            // Checking for rendered as opposed to hide/show is important because
            // it's still possible to render a floating component and have it be visible.
            // Since rendered isn't a global event, we need to react individually on each
            // component and update the state in the collection after render.
            comp.on('afterrender', me.onAfterComponentRender, me, {
                single: true
            });
        }
        me.zIndexStack.add(comp);
    },
    /**
     * Unregisters a {@link Ext.Component} from this ZIndexManager. This should not
     * need to be called. Components are automatically unregistered upon destruction.
     * See {@link #register}.
     * @param {Ext.Component} comp The Component to unregister.
     */
    unregister: function(comp) {
        var me = this;
        delete comp.zIndexManager;
        comp.un('afterrender', me.onAfterComponentRender, me);
        me.zIndexStack.remove(comp);
        me.onCollectionSort();
    },
    /**
     * Gets a registered Component by id.
     * @param {String/Object} id The id of the Component or a {@link Ext.Component} instance
     * @return {Ext.Component}
     */
    get: function(id) {
        return id.isComponent ? id : this.zIndexStack.get(id);
    },
    /**
     * Brings the specified Component to the front of any other active Components in this
     * ZIndexManager.
     * @param {String/Object} comp The id of the Component or a {@link Ext.Component} instance.
     * @param {Boolean} preventFocus Pass `true` to prevent the component being focused when moved
     * to front.
     * @return {Boolean} True if the component was brought to the front, else false
     * if it was already in front, or another component remains at the front due to configuration
     * (eg {@link Ext.util.Floating#alwaysOnTop}, or if the component was not found.
     */
    bringToFront: function(comp, preventFocus) {
        var me = this,
            zIndexStack = me.zIndexStack,
            oldFront = zIndexStack.last(),
            newFront, preventFocusSetting;
        comp = me.get(comp);
        // Refuse to perform this operation if we do not own the passed component.
        if (!comp) {
            return false;
        }
        preventFocusSetting = comp.preventFocusOnActivate;
        // The onCollectionSorted reaction to the setting of activeCounter will focus by default.
        // Prevent it if requested.
        comp.preventFocusOnActivate = preventFocus;
        comp.setActiveCounter(++Ext.ZIndexManager.activeCounter);
        comp.preventFocusOnActivate = preventFocusSetting;
        newFront = zIndexStack.last();
        // Return true if the passed component was moved to the front
        // and was not already at the front
        return (newFront === comp && newFront !== oldFront);
    },
    /**
     * Sends the specified Component to the back of other active Components in this ZIndexManager.
     * @param {String/Object} comp The id of the Component or a {@link Ext.Component} instance
     * @return {Ext.Component} The Component
     */
    sendToBack: function(comp) {
        comp = this.get(comp);
        if (comp) {
            comp.setActiveCounter(0);
        }
        return comp || null;
    },
    /**
     * Hides all Components managed by this ZIndexManager.
     */
    hideAll: function() {
        var me = this,
            all = me.zIndexStack.getRange(),
            len = all.length,
            i;
        me.hidingAll = true;
        for (i = 0; i < len; i++) {
            all[i].hide();
        }
        me.hidingAll = false;
        me.hideModalMask();
        me.front = null;
    },
    /**
     * @private
     * Temporarily hides all currently visible managed Components. This is for when
     * dragging a Window which may manage a set of floating descendants in its ZIndexManager;
     * they should all be hidden just for the duration of the drag.
     */
    hide: function() {
        var me = this,
            activeElement = Ext.Element.getActiveElement(),
            all = me.zIndexStack.getRange(),
            len = all.length,
            comp, i;
        // If any of the components contained focus, we must restore it on show.
        me.focusRestoreElement = null;
        (me.tempHidden || (me.tempHidden = [])).length = 0;
        for (i = 0; i < len; i++) {
            comp = all[i];
            // Only hide currently visible floaters
            if (comp.isVisible()) {
                if (comp.el.contains(activeElement)) {
                    me.focusRestoreElement = activeElement;
                }
                comp.el.hide();
                comp.pendingShow = comp.hidden = true;
                me.tempHidden.push(comp);
            }
        }
    },
    /**
     * @private
     * Restores temporarily hidden managed Components to visibility.
     */
    show: function() {
        var me = this,
            tempHidden = me.tempHidden,
            len = tempHidden ? tempHidden.length : 0,
            comp, i;
        for (i = 0; i < len; i++) {
            comp = tempHidden[i];
            comp.hidden = false;
            if (comp.pendingShow) {
                comp.el.show();
                comp.pendingShow = false;
                comp.setPosition(comp.x, comp.y);
                comp.onFloatShow();
            } else // An attempt at hiding while temp hidden
            // has cleared the pendingShow flag. Hide it
            // properly with full event flow now.
            {
                comp.hide();
            }
        }
        me.tempHidden = null;
        if (me.focusRestoreElement) {
            me.focusRestoreElement.focus();
        }
    },
    /**
     * Gets the currently-active Component in this ZIndexManager.
     * @return {Ext.Component} The active Component
     */
    getActive: function() {
        return this.zIndexStack.last();
    },
    /**
     * Returns zero or more Components in this ZIndexManager using the custom search function passed
     * to this method. The function should accept a single {@link Ext.Component} reference
     * as its only argument and should return true if the Component matches the search criteria,
     * otherwise it should return false.
     * @param {Function} fn The search function
     * @param {Object} [scope] The scope (`this` reference) in which the function is executed.
     * Defaults to the Component being tested. That gets passed to the function if not specified.
     * @return {Array} An array of zero or more matching floating components.
     */
    getBy: function(fn, scope) {
        return this.zIndexStack.filterBy(fn, scope).getRange();
    },
    /**
     * Executes the specified function once for every Component in this ZIndexManager, passing each
     * Component as the only parameter. Returning false from the function will stop the iteration.
     * @param {Function} fn The function to execute for each item
     * @param {Object} [scope] The scope (this reference) in which the function
     * is executed. Defaults to the current Component in the iteration.
     */
    each: function(fn, scope) {
        this.zIndexStack.each(fn, scope);
    },
    /**
     * Executes the specified function once for every Component in this ZIndexManager, passing each
     * Component as the only parameter. Returning false from the function will stop the iteration.
     * The components are passed to the function starting at the bottom and proceeding to the top.
     * @param {Function} fn The function to execute for each item
     * @param {Object} scope (optional) The scope (this reference) in which the function
     * is executed. Defaults to the current Component in the iteration.
     */
    eachBottomUp: function(fn, scope) {
        var stack = this.zIndexStack.getRange(),
            len = stack.length,
            comp, i;
        for (i = 0; i < len; i++) {
            comp = stack[i];
            if (comp.isComponent && fn.call(scope || comp, comp) === false) {
                return;
            }
        }
    },
    /**
     * Executes the specified function once for every Component in this ZIndexManager, passing each
     * Component as the only parameter. Returning false from the function will stop the iteration.
     * The components are passed to the function starting at the top and proceeding to the bottom.
     * @param {Function} fn The function to execute for each item
     * @param {Object} [scope] The scope (this reference) in which the function
     * is executed. Defaults to the current Component in the iteration.
     */
    eachTopDown: function(fn, scope) {
        var stack = this.zIndexStack.getRange(),
            comp, i;
        for (i = stack.length; i-- > 0; ) {
            comp = stack[i];
            if (comp.isComponent && fn.call(scope || comp, comp) === false) {
                return;
            }
        }
    },
    destroy: function() {
        var me = this,
            stack = me.zIndexStack.getRange(),
            len = stack.length,
            i;
        for (i = 0; i < len; i++) {
            Ext.destroy(stack[i]);
        }
        Ext.destroy(me.mask, me.maskShim, me.zIndexStack, me.globalListeners, me.resizeListeners);
        me.callParent();
    },
    privates: {
        getMaskBox: function() {
            var maskTarget = this.mask.maskTarget;
            if (maskTarget.dom === document.body) {
                // If we're masking the body, subtract the border/padding
                // so we don't cause scrollbar.
                return {
                    height: Math.max(document.body.scrollHeight, Ext.dom.Element.getDocumentHeight()),
                    width: Math.max(document.body.scrollWidth, Ext.dom.Element.getDocumentWidth()),
                    x: 0,
                    y: 0
                };
            } else {
                return maskTarget.getBox();
            }
        },
        scheduleContainerResize: function() {
            // The reason we're scheduling resize handler here is to allow Responsive mixin
            // to fire events and run layouts that may affect the size of the modal mask.
            // Responsive will request animation frame on browser window resize event,
            // we do likewise here to minimize flicker.
            if (!this.containerResizeTimer) {
                this.containerResizeTimer = Ext.raf(this.onContainerResize, this);
            }
        },
        onContainerResize: function() {
            var me = this,
                mask = me.mask,
                maskShim = me.maskShim,
                viewSize;
            me.containerResizeTimer = null;
            if (mask && mask.isVisible()) {
                // At the new container size, the mask might be *causing* the scrollbar,
                // so to find the valid client size to mask, we must temporarily unmask
                // the parent node.
                mask.hide();
                if (maskShim) {
                    maskShim.hide();
                }
                viewSize = me.getMaskBox();
                if (maskShim) {
                    maskShim.setSize(viewSize);
                    maskShim.show();
                }
                mask.setSize(viewSize);
                mask.show();
            }
        },
        onMaskMousedown: function(e) {
            // Focus frontmost modal, do not allow mousedown to focus mask.
            if (this.topModal) {
                this.topModal.focus();
                e.preventDefault();
            }
        },
        onMaskClick: function() {
            var front = this.topModal,
                methodName;
            if (front) {
                // Fire a maskclick event. Allow the onward processing by the maskClickAction method
                // to be vetoed by a false return value.
                if (!front.hasListeners.maskclick || front.fireEvent('maskclick', front) !== false) {
                    // We call whatever method 'maskClickAction' points to.
                    // By default, Windows have 'focus'. If we encounter other
                    // classes without that property, default to 'focus'
                    methodName = front.maskClickAction || 'focus';
                    front[methodName]();
                }
            }
        },
        showModalMask: function(comp) {
            var me = this,
                compEl = comp.el,
                maskTarget = comp.floatParent ? comp.floatParent.getEl() : comp.container,
                mask = me.mask;
            if (!mask) {
                // Create the mask at zero size so that it does not affect upcoming
                // target measurements.
                me.mask = mask = Ext.getBody().createChild({
                    // tell the spec runner to ignore this element when checking if the dom is clean
                    'data-sticky': true,
                    role: 'presentation',
                    cls: Ext.baseCSSPrefix + 'mask ' + Ext.baseCSSPrefix + 'border-box',
                    style: 'height:0;width:0'
                });
                mask.setVisibilityMode(Ext.Element.DISPLAY);
                mask.on({
                    mousedown: me.onMaskMousedown,
                    click: me.onMaskClick,
                    scope: me
                });
            } else // If the mask is already shown, hide it before showing again
            // to ensure underlying elements' tabbability is restored
            {
                me.hideModalMask();
            }
            mask.maskTarget = maskTarget;
            // Since there is no fast and reliable way to find elements above or below
            // a given z-index, we just cheat and prevent tabbable elements within the
            // topmost component from being made untabbable.
            maskTarget.saveTabbableState({
                excludeRoot: compEl
            });
            // Size and zIndex stack the mask (and its shim)
            me.syncModalMask(comp);
        },
        syncModalMask: function(comp) {
            var me = this,
                zIndex = comp.el.getZIndex() - 4,
                mask = me.mask,
                shim = me.maskShim,
                viewSize = me.getMaskBox();
            if (shim) {
                shim.setZIndex(zIndex);
                shim.show();
                shim.setBox(viewSize);
            }
            mask.setZIndex(zIndex);
            mask.show();
            mask.setBox(viewSize);
        },
        hideModalMask: function() {
            var mask = this.mask,
                maskShim = this.maskShim;
            if (mask && mask.isVisible()) {
                mask.maskTarget.restoreTabbableState();
                mask.maskTarget = undefined;
                mask.hide();
                if (maskShim) {
                    maskShim.hide();
                }
            }
        }
    }
}, function() {
    /**
     * @class Ext.WindowManager
     * @extends Ext.ZIndexManager
     *
     * The default global floating Component group that is available automatically.
     *
     * This manages instances of floating Components which were rendered programatically without
     * being added to a {@link Ext.container.Container Container}, and for floating Components
     * which were added into non-floating Containers.
     * 
     * *Floating* Containers create their own instance of ZIndexManager, and floating Components
     * added at any depth below there are managed by that ZIndexManager.
     *
     * @singleton
     */
    Ext.WindowManager = Ext.WindowMgr = new this();
});

/**
 * Base class for any Ext.Component that may contain other Components. Containers handle the basic
 * behavior of containing items, namely adding, inserting and removing items.
 *
 * The most commonly used Container classes are Ext.panel.Panel, Ext.window.Window and
 * Ext.tab.Panel. If you do not need the capabilities offered by the aforementioned classes you can
 * create a lightweight Container to be encapsulated by an HTML element to your specifications
 * by using the {@link Ext.Component#autoEl autoEl} config option.
 *
 * The code below illustrates how to explicitly create a Container:
 *
 *     @example
 *     // Explicitly create a Container
 *     Ext.create('Ext.container.Container', {
 *         layout: {
 *             type: 'hbox'
 *         },
 *         width: 400,
 *         renderTo: Ext.getBody(),
 *         border: 1,
 *         style: {borderColor:'#000000', borderStyle:'solid', borderWidth:'1px'},
 *         defaults: {
 *             labelWidth: 80,
 *             // implicitly create Container by specifying xtype
 *             xtype: 'datefield',
 *             flex: 1,
 *             style: {
 *                 padding: '10px'
 *             }
 *         },
 *         items: [{
 *             xtype: 'datefield',
 *             name: 'startDate',
 *             fieldLabel: 'Start date'
 *         },{
 *             xtype: 'datefield',
 *             name: 'endDate',
 *             fieldLabel: 'End date'
 *         }]
 *     });
 *
 * ## Layout
 *
 * Container classes delegate the rendering of child Components to a layout manager class which must
 * be configured into the Container using the `{@link #layout}` configuration property.
 *
 * When either specifying child `{@link #cfg-items}` of a Container, or dynamically
 * {@link #method-add adding} Components to a Container, remember to consider how you wish
 * the Container to arrange those child elements, and whether those child elements need to be sized
 * using one of Ext's built-in `{@link #layout}` schemes. By default, Containers use the
 * {@link Ext.layout.container.Auto Auto} scheme which only renders child components, appending them
 * one after the other inside the Container, and **does not apply any sizing** at all.
 *
 * A common mistake is when a developer neglects to specify a `{@link #layout}` (e.g. GridPanels or
 * TreePanels are added to Containers for which no `{@link #layout}` has been specified).
 * If a Container is left to use the default {@link Ext.layout.container.Auto Auto} scheme,
 * none of its child components will be resized, or changed in any way when the Container
 * is resized.
 *
 * Certain layout managers allow dynamic addition of child components. Those that do include
 * Ext.layout.container.Card, Ext.layout.container.Anchor, Ext.layout.container.VBox,
 * Ext.layout.container.HBox, and Ext.layout.container.Table. For example:
 *
 *     //  Create the GridPanel.
 *     var myNewGrid = Ext.create('Ext.grid.Panel', {
 *         store: myStore,
 *         headers: myHeaders,
 *         title: 'Results', // the title becomes the title of the tab
 *     });
 *
 *     myTabPanel.add(myNewGrid); // Ext.tab.Panel implicitly uses Ext.layout.container.Card
 *     myTabPanel.setActiveTab(myNewGrid);
 *
 * The example above adds a newly created GridPanel to a TabPanel. Note that a TabPanel uses
 * {@link Ext.layout.container.Card} as its layout manager which means all its child items are sized
 * to {@link Ext.layout.container.Fit fit} exactly into its client area.
 *
 * **_Overnesting is a common problem_**. An example of overnesting occurs when a GridPanel is added
 * to a TabPanel by wrapping the GridPanel _inside_ a wrapping Panel (that has no `{@link #layout}`
 * specified) and then add that wrapping Panel to the TabPanel. The point to realize is that
 * a GridPanel **is** a Component which can be added directly to a Container. If the wrapping Panel
 * has no `{@link #layout}` configuration, then the overnested GridPanel will not be sized
 * as expected.
 * 
 * ## {@link Ext.Component#reference References} and {@link #referenceHolder Reference Holders}
 * 
 * Reference holders are used to keep references to child components inside a hierarchy.
 * 
 * This functionality allows the connection of encapsulated references between containers
 * and their child components declaratively. Simple usage:
 * 
 *     Ext.define('Login', {
 *         extend: 'Ext.window.Window',
 *
 *         // This config is not compatible with the more common "controller" config
 *         // used to specify a ViewController for the view. When a ViewController is
 *         // specified it effectively acts as the "reference holder" for the view. In
 *         // this example we simply mark this container as the reference holder for
 *         // demonstration purposes.
 *         referenceHolder: true,
 *
 *         title: 'Login',
 *         items: [{
 *             xtype: 'form',
 *             items: [{
 *                 xtype: 'textfield',
 *                 reference: 'username', // A named reference to be held on the referenceHolder
 *                 name: 'username',
 *                 fieldLabel: 'Username'
 *             }, {
 *                 xtype: 'textfield',
 *                 reference: 'password', // A named reference to be held on the referenceHolder
 *                 name: 'password',
 *                 fieldLabel: 'Password'
 *             }] 
 *         }] 
 *     });
 *     var w = new Login();
 *     console.log(w.lookupReference('password')); // The password field
 * 
 * Reference holders are also encapsulated, so a reference will only be put on the closest
 * reference holder above it in the component hierarchy:
 * 
 *     var ct = new Ext.container.Container({
 *         referenceHolder: true,
 *         items: [{
 *             xtype: 'container',
 *             referenceHolder: true,
 *             reference: 'innerCt1',
 *             items: [{
 *                 xtype: 'component',
 *                 reference: 'a',
 *                 id: 'a1'
 *             }, {
 *                 xtype: 'component',
 *                 reference: 'b',
 *                 id: 'b1'
 *             }]
 *         }, {
 *             xtype: 'container',
 *             referenceHolder: true,
 *             reference: 'innerCt2',
 *             items: [{
 *                 xtype: 'component',
 *                 reference: 'a',
 *                 id: 'a2'
 *             }, {
 *                 xtype: 'component',
 *                 reference: 'b',
 *                 id: 'b2'
 *             }]
 *         }]
 *     });
 *     // The main container will not have references to a/b, each innerCt will
 *     console.log(ct.lookupReference('a'), ct.lookupReference('b'));
 *     var inner1 = ct.lookupReference('innerCt1');
 *     var inner2 = ct.lookupReference('innerCt2');
 * 
 *     console.log(inner1.lookupReference('a').id, inner1.lookupReference('b').id);
 *     console.log(inner2.lookupReference('a').id, inner2.lookupReference('b').id);
 *     
 * If the view has a controller attached, it will automatically become a {@link #referenceHolder}.
 * References will be available in both the view and the controller:
 * 
 *     Ext.define('ProfileController', {
 *         extend: 'Ext.app.ViewController',
 *         alias: 'controller.profile',
 *   
 *         init: function() {
 *             console.log(this.lookupReference('firstName'));
 *         }
 *     });
 *
 *     Ext.define('Profile', {
 *         extend: 'Ext.form.Panel',
 *         controller: 'profile',
 *         items: [{
 *             xtype: 'textfield',
 *             reference: 'firstName',
 *             fieldLabel: 'First Name'
 *         }]
 *     });
 * 
 *     new Profile(); 
 * 
 * ## Events & {@link #defaultListenerScope} ##
 * 
 * Events can use the default listener scope to determine at runtime the appropriate place
 * to fire. This allows for declarative binding of events in a useful way:
 * 
 *     Ext.define('MyView', {
 *         extend: 'Ext.container.Container',
 *         defaultListenerScope: true,
 *         referenceHolder: true,
 *         items: [{
 *             xtype: 'textfield',
 *             reference: 'myfield'
 *         }, {
 *             xtype: 'button',
 *             text: 'Set to A',
 *             listeners: {
 *                 click: 'onButtonAClick'
 *             }
 *         }, {
 *             xtype: 'button',
 *             text: 'Set to B',
 *             listeners: {
 *                 click: 'onButtonBClick'
 *             }
 *         }],
 * 
 *         onButtonAClick: function() {
 *             this.lookupReference('myfield').setValue('A');
 *         },
 * 
 *         onButtonBClick: function() {
 *             this.lookupReference('myfield').setValue('B');
 *         }
 *     });
 *     
 * Like {@link #referenceHolder}, the {@link #defaultListenerScope} is encapsulated, the scope will
 * be resolved at the closest {@link #defaultListenerScope} above it in the component hierarchy:
 * 
 *     var ct = new Ext.container.Container({
 *         defaultListenerScope: true,
 *         onCustomEvent: function() {
 *             console.log('Outer called'); // Will NOT be called
 *         },
 *         items: [{
 *             xtype: 'container',
 *             defaultListenerScope: true,
 *             onCustomEvent: function() {
 *                 console.log('Inner called'); // Will be called
 *             },
 *             items: [{
 *                 xtype: 'component',
 *                 itemId: 'child',
 *                 listeners: {
 *                     customevent: 'onCustomEvent'
 *                 }
 *             }]
 *         }]
 *     });
 *     // The main container will not have references to a/b, each innerCt will
 *     console.log(ct.lookupReference('a'), ct.lookupReference('b'));
 *     var inner1 = ct.lookupReference('innerCt1');
 *     var inner2 = ct.lookupReference('innerCt2');
 * 
 *     console.log(inner1.lookupReference('a').id, inner1.lookupReference('b').id);
 *     console.log(inner2.lookupReference('a').id, inner2.lookupReference('b').id);
 * 
 * Similar to references, if a {@link Ext.app.ViewController} is attached to this view, it becomes
 * the {@link #defaultListenerScope}, which means un-scoped, late bound events will be directed
 * to the controller. This is powerful as it allows views to be totally declarative:
 * 
 *     Ext.define('MyApp.controller.Login', {
 *         extend : 'Ext.app.ViewController',
 *         alias : 'controller.login',
 *   
 *         init: function() {
 *             this.sendCount = 0;    
 *         },
 *
 *         onLoginClick : function(btn) {
 *             this.login();
 *         },
 *
 *         onFieldSpecialKey : function(field, e) {
 *             if (e.getKey() === e.ENTER) {
 *                this.login();
 *             }
 *         },
 *
 *         login : function() {
 *            var form = this.lookupReference('form');
 *             this.lookupReference('error').hide();
 *             if (form.isValid()) {
 *                 console.log('Do the login!');
 *                 // Server responded...
 *                 if (++this.sendCount % 2 === 0) {
 *                     this.onServerSuccess();
 *                 } else {
 *                     this.onServerFailure();
 *                 }
 *             }
 *         },
 *   
 *         onServerSuccess: function() {
 *             // Proceed   
 *             console.log('All good');
 *         },
 *   
 *         onServerFailure: function() {
 *             var error = this.lookupReference('error');
 *             error.update('Invalid username/password');
 *             error.show();
 *         }
 *     });
 *
 *     Ext.define('MyApp.view.Login', {
 *         extend : 'Ext.window.Window',
 *         controller : 'login',
 *         referenceHolder: true,
 *
 *         title : 'Login',
 *         width : 400,
 *
 *         items : [{
 *             xtype : 'form',
 *             reference : 'form',
 *             border : false,
 *             bodyPadding : 10,
 *             defaultType : 'textfield',
 *             defaults : {
 *                 anchor : '90%',
 *                 allowBlank : false,
 *                 enableKeyEvents : true
 *             },
 *             items : [{
 *                 xtype: 'component',
 *                 reference: 'error',
 *                 hidden: true,
 *                 margin: '0 0 10 0',
 *                 style: 'color: red;'
 *             }, {
 *                 name : 'username',
 *                 fieldLabel : 'Username',
 *                 reference : 'username',
 *                 listeners : {
 *                     specialkey : 'onFieldSpecialKey'
 *                 }
 *             }, {
 *                 name : 'password',
 *                 fieldLabel : 'Password',
 *                 reference : 'password',
 *                 inputType : 'password',
 *                 listeners : {
 *                     specialkey : 'onFieldSpecialKey'
 *                 }
 *             }]
 *         }],
 *         buttons : ['->', {
 *             text : 'Login',
 *             listeners : {
 *                 click : 'onLoginClick'
 *             }
 *         }]
 *     });
 *
 * ## Adding via remote configuration
 *
 * A server side script can be used to add Components which are generated dynamically on the server.
 * An example of adding a GridPanel to a TabPanel where the GridPanel is generated by the server
 * based on certain parameters:
 *
 *     // execute an Ajax request to invoke server side script:
 *     Ext.Ajax.request({
 *         url: 'gen-invoice-grid.php',
 *         // send additional parameters to instruct server script
 *         params: {
 *             startDate: Ext.getCmp('start-date').getValue(),
 *             endDate: Ext.getCmp('end-date').getValue()
 *         },
 *         // process the response object to add it to the TabPanel:
 *         success: function(xhr) {
 *             var newComponent = eval(xhr.responseText); // see discussion below
 *             myTabPanel.add(newComponent); // add the component to the TabPanel
 *             myTabPanel.setActiveTab(newComponent);
 *         },
 *         failure: function() {
 *             Ext.Msg.alert("Grid create failed", "Server communication failure");
 *         }
 *     });
 *
 * The server script needs to return a JSON representation of a configuration object, which,
 * when decoded will return a config object with an {@link Ext.Component#xtype xtype}. The server
 * might return the following JSON:
 *
 *     {
 *         "xtype": 'grid',
 *         "title": 'Invoice Report',
 *         "store": {
 *             "model": 'Invoice',
 *             "proxy": {
 *                 "type": 'ajax',
 *                 "url": 'get-invoice-data.php',
 *                 "reader": {
 *                     "type": 'json'
 *                     "record": 'transaction',
 *                     "idProperty": 'id',
 *                     "totalRecords": 'total'
 *                 })
 *             },
 *             "autoLoad": {
 *                 "params": {
 *                     "startDate": '01/01/2008',
 *                     "endDate": '01/31/2008'
 *                 }
 *             }
 *         },
 *         "headers": [
 *             {"header": "Customer", "width": 250, "dataIndex": 'customer', "sortable": true},
 *             {"header": "Invoice Number", "width": 120, "dataIndex": 'invNo', "sortable": true},
 *             {"header": "Invoice Date", "width": 100, "dataIndex": 'date',
 *              "renderer": Ext.util.Format.dateRenderer('M d, y'), "sortable": true},
 *             {"header": "Value", "width": 120, "dataIndex": 'value', "renderer": 'usMoney',
 *              "sortable": true}
 *         ]
 *     }
 *
 * When the above code fragment is passed through the `eval` function in the success handler
 * of the Ajax request, the result will be a config object which, when added to a Container,
 * will cause instantiation of a GridPanel. **Be sure that the Container is configured with a layout
 * which sizes and positions the child items to your requirements.**
 *
 * **Note:** since the code above is _generated_ by a server script, the `autoLoad` params
 * for the Store, the user's preferred date format, the metadata to allow generation of the Model
 * layout, and the ColumnModel can all be generated into the code since these are all known
 * on the server.
 */
Ext.define('Ext.container.Container', {
    extend: 'Ext.Component',
    xtype: 'container',
    alternateClassName: [
        'Ext.Container',
        'Ext.AbstractContainer'
    ],
    requires: [
        'Ext.Action',
        'Ext.util.MixedCollection',
        'Ext.layout.container.Auto',
        'Ext.ZIndexManager',
        'Ext.util.ItemCollection'
    ],
    mixins: [
        'Ext.mixin.Queryable',
        'Ext.mixin.Container',
        'Ext.mixin.FocusableContainer'
    ],
    /**
     * @cfg renderTpl
     * @inheritdoc
     */
    renderTpl: '<tpl if="hasTabGuard">{% this.renderTabGuard(out, values, \'before\'); %}</tpl>' + '{% this.renderContainer(out,values) %}' + '<tpl if="hasTabGuard">{% this.renderTabGuard(out, values, \'after\'); %}</tpl>',
    // <editor-fold desc="Config">
    // ***********************************************************************************
    // Begin Config
    // ***********************************************************************************
    config: {
        /**
         * @cfg {Object} actions
         * An object containing properties which define named {@link Ext.Action actions}
         * for this container and any descendant components.
         *
         * An Action encapsulates a shareable, reusable set of properties which define a
         * "clickable" UI component such as a {@link Ext.button.Button button} or
         * {@link Ext.menu.Item menu item}, or {@link Ext.panel.Panel#tools panel header tool},
         * or an {@link Ext.grid.column.Action ActionColumn item}
         *
         * An Action, or more conveniently, the *name* of an action prefixed with `'@'`
         * may be used as a config object for creating child components which use a `handler`
         * config property to reference a Controller method to invoke when the component is
         * clicked.
         *
         * The property name is the action name, which may then be used as a child item
         * configuration in an {@link Ext.container.Container#cfg!items items} configuration in
         * any descendant component such as a toolbar or a menu, or in a
         * {@link Ext.panel.Panel#tools tools} configuration of a Panel.
         *
         * The property value is a configuration object for any clickable component.
         *
         * See the {@link Ext.Action} class for an example of reusable Actions.
         * @since 6.2.0
         */
        actions: null
    },
    /**
     * @cfg {String/Number} activeItem
     * A string component id or the numeric index of the component that should be
     * initially activated within the container's layout on render.  For example,
     * activeItem: 'item-1' or activeItem: 0 (index 0 = the first item in the
     * container's collection).  activeItem only applies to layout styles that can
     * display items one at a time (like {@link Ext.layout.container.Card} and
     * {@link Ext.layout.container.Fit}).
     *
     * @since 2.3.0
     */
    /**
     * @cfg {Boolean} autoDestroy
     * If true the container will automatically destroy any contained component that is removed
     * from it, else destruction must be handled manually.
     * @since 2.3.0
     */
    autoDestroy: true,
    /**
     * @cfg {String[]} bubbleEvents
     * An array of events that, when fired, should be bubbled to any parent container.
     * See {@link Ext.util.Observable#enableBubble}.
     * @since 3.4.0
     */
    /**
     * @cfg {Object/Function} defaults
     * This option is a means of applying default settings to all added items whether added
     * through the {@link #cfg-items} config or via the {@link #method-add} or {@link #insert}
     * methods.
     *
     * Defaults are applied to both config objects and instantiated components conditionally
     * so as not to override existing properties in the item (see {@link Ext#applyIf}).
     *
     * If the defaults option is specified as a function, then the function will be called
     * using this Container as the scope (`this` reference) and passing the added item as
     * the first parameter. Any resulting object from that call is then applied to the item
     * as default properties.
     *
     * For example, to automatically apply padding to the body of each of a set of
     * contained {@link Ext.panel.Panel} items, you could pass:
     * `defaults: {bodyStyle:'padding:15px'}`.
     *
     * Usage:
     *
     *     defaults: { // defaults are applied to items, not the container
     *         scrollable: true
     *     },
     *     items: [
     *         // default will not be applied here, panel1 will be scrollable: false
     *         {
     *             xtype: 'panel',
     *             id: 'panel1',
     *             scrollable: false
     *         },
     *         // this component will have scrollable: true
     *         new Ext.panel.Panel({
     *             id: 'panel2'
     *         })
     *     ]
     *
     * @since 2.3.0
     */
    /**
     * @cfg {String} defaultType
     * The default {@link Ext.Component xtype} of child Components to create in this Container when
     * a child item is specified as a raw configuration object, rather than as an instantiated
     * Component.
     * @since 2.3.0
     */
    defaultType: 'panel',
    /**
     * @cfg {Boolean} detachOnRemove
     * True to move any component to the {@link Ext#getDetachedBody detachedBody} when the component
     * is removed from this container. This option is only applicable when the component
     * is not destroyed while being removed, see {@link #autoDestroy} and {@link #method-remove}.
     * If this option is set to false, the DOM of the component will remain in the current place
     * until it is explicitly moved.
     */
    detachOnRemove: true,
    // eslint-disable-next-line max-len
    // @cmd-auto-dependency {aliasPrefix: "widget.", typeProperty: "xtype", defaultTypeProperty: "defaultType", defaultsProperty: "defaults"}
    /**
     * @cfg {Object/Object[]} items
     * A single item, or an array of child Components to be added to this container
     *
     * **Unless configured with a {@link #cfg!layout}, a Container simply renders child
     * Components serially into its encapsulating element and performs no sizing or
     * positioning upon them.**
     *
     * Example:
     *
     *     // specifying a single item
     *     items: {...},
     *     layout: 'fit',    // The single items is sized to fit
     *
     *     // specifying multiple items
     *     items: [{...}, {...}],
     *     layout: 'hbox', // The items are arranged horizontally
     *
     * Each item may be:
     *
     * - A {@link Ext.Component Component}
     * - A Component configuration object
     *
     * If a configuration object is specified, the actual type of Component to be
     * instantiated my be indicated by using the {@link Ext.Component#xtype xtype} option.
     *
     * Every Component class has its own {@link Ext.Component#xtype xtype}.
     *
     * If an {@link Ext.Component#xtype xtype} is not explicitly specified, the
     * {@link #cfg-defaultType} for the Container is used, which by default is usually `panel`.
     *
     * # Notes:
     *
     * Ext uses lazy rendering. Child Components will only be rendered
     * should it become necessary. Items are automatically laid out when they are first
     * shown (no sizing is done while hidden), or in response to a {@link #method-updateLayout}
     * call.
     *
     * Do not specify {@link Ext.panel.Panel#contentEl contentEl} or
     * {@link Ext.panel.Panel#html html} with `items`.
     *
     * @since 2.3.0
     */
    items: undefined,
    // Not "null" so that Ext.applyIf(this, {items: []}) works
    // @cmd-auto-dependency { aliasPrefix : "layout." }
    /**
     * @cfg {Ext.enums.Layout/Object} layout
     * **Important**: In order for child items to be correctly sized and
     * positioned, typically a layout manager **must** be specified through
     * the `layout` configuration option.
     *
     * The sizing and positioning of child {@link #cfg-items} is the responsibility of
     * the Container's layout manager which creates and manages the type of layout
     * you have in mind.  For example:
     *
     * If the layout configuration is not explicitly specified for
     * a general purpose container (e.g. Container or Panel) the
     * {@link Ext.layout.container.Auto default layout manager} will be used
     * which does nothing but render child components sequentially into the
     * Container (no sizing or positioning will be performed in this situation).
     *
     * **layout** may be specified as either as an Object or as a String:
     *
     * ## Specify as an Object
     *
     * Example usage:
     *
     *     layout: {
     *         type: 'vbox',
     *         align: 'left'
     *     }
     *
     *   - **type**
     *
     *     The layout type to be used for this container.  If not specified,
     *     a default {@link Ext.layout.container.Auto} will be created and used.
     *
     *     Valid layout <code>type</code> values are listed in {@link Ext.enums.Layout}.
     *
     *   - Layout specific configuration properties
     *
     *     Additional layout specific configuration properties may also be
     *     specified. For complete details regarding the valid config options for
     *     each layout type, see the layout class corresponding to the `type`
     *     specified.
     *
     * ## Specify as a String
     *
     * Example usage:
     *
     *     layout: 'vbox'
     *
     *   - **layout**
     *
     *     The layout `type` to be used for this container (see {@link Ext.enums.Layout}
     *     for list of valid values).
     *
     *     Additional layout specific configuration properties. For complete
     *     details regarding the valid config options for each layout type, see the
     *     layout class corresponding to the `layout` specified.
     *
     * ## Configuring the default layout type
     *
     * If a certain Container class has a default layout (For example a
     * {@link Ext.toolbar.Toolbar Toolbar} with a default `Box` layout), then to simply configure
     * the default layout, use an object, but without the `type` property:
     *
     *
     *     xtype: 'toolbar',
     *     layout: {
     *         pack: 'center'
     *     }
     *
     * @since 2.3.0
     *
     */
    layout: 'auto',
    /**
     * @cfg {Boolean} suspendLayout
     * If true, suspend calls to updateLayout. Useful when batching multiple adds to a container
     * and not passing them as multiple arguments or an array.
     */
    suspendLayout: false,
    /**
     * @cfg {String} defaultFocus
     *
     * Specifies a child Component to receive focus when this Container's {@link #method-focus}
     * method is called. Should be a valid {@link Ext.ComponentQuery query} selector.
     */
    /**
     * When set to `true`, two elements are added to the container's element. These are the
     * `{@link #tabGuardBeforeEl}` and `{@link #tabGuardAfterEl}`.
     * @cfg {Boolean} tabGuard
     * @private
     * @since 6.0.0
     */
    // ***********************************************************************************
    // End Config
    // ***********************************************************************************
    // </editor-fold>
    // <editor-fold desc="Properties">
    // ***********************************************************************************
    // Begin Properties
    // ***********************************************************************************
    /* eslint-disable indent, max-len */
    /**
     * @property {String/String[]/Ext.XTemplate} tabGuardTpl
     * This template is used to generate the `tabGuard` elements. It is used once per
     * element (see `{@link #tabGuardBeforeEl}` and `{@link #tabGuardAfterEl}`).
     * @private
     * @since 6.0.0
     */
    tabGuardTpl: // We use span instead of div because of IE bug/misfeature: it will focus
    // block elements upon clicking or calling node.focus() regardless of
    // tabIndex attribute. It doesn't do that with inline elements, hence span.
    '<span id="{id}-{tabGuardEl}" data-ref="{tabGuardEl}" aria-hidden="true"' + ' class="' + Ext.baseCSSPrefix + 'tab-guard ' + Ext.baseCSSPrefix + 'tab-guard-{tabGuardPosition}"' + ' style="width:0px;height:0px;">' + '</span>',
    /* eslint-enable indent, max-len */
    /**
     * @property {Object} tabGuardElements
     * Read only object containing property names for tab guard elements, keyed by position.
     * @private
     * @since 6.2.0
     */
    tabGuardElements: {
        before: 'tabGuardBeforeEl',
        after: 'tabGuardAfterEl'
    },
    /**
     * @property {Number} tabGuardBeforeIndex The tabIndex attribute value to assign
     * to the "before" tab guard element. Default is `undefined` for automatic detection
     * from the DOM.
     * @private
     * @since 6.2.0
     */
    /**
     * @property {Number} tabGuardAfterIndex The tabIndex attribute value to assign
     * to the "after" tab guard element. Default is `undefined` for automatic detection
     * from the DOM.
     * @private
     * @since 6.2.0
     */
    /**
     * This element reference is generated when `{@link #tabGuard}` is `true`. This element
     * is generated before all `dockedItems` in the DOM.
     * @property {Ext.dom.Element} tabGuardBeforeEl
     * @private
     * @since 6.0.0
     */
    /**
     * This element reference is generated when `{@link #tabGuard}` is `true`. This element
     * is generated after all `dockedItems` in the DOM.
     * @property {Ext.dom.Element} tabGuardAfterEl
     * @private
     * @since 6.0.0
     */
    /**
     * @private
     */
    _applyDefaultsOptions: {
        defaults: true,
        strict: false
    },
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'presentation',
    /**
     * @cfg baseCls
     * @inheritdoc
     */
    baseCls: Ext.baseCSSPrefix + 'container',
    /**
     * @property {Number} layoutCounter
     * The number of container layout calls made on this object.
     * @private
     */
    layoutCounter: 0,
    // ***********************************************************************************
    // End Properties
    // ***********************************************************************************
    // </editor-fold>
    // <editor-fold desc="Events">
    // ***********************************************************************************
    // Begin Events
    // ***********************************************************************************
    /**
     * @event add
     * Fires after any {@link Ext.Component} is added or inserted into the container.
     * @param {Ext.container.Container} this
     * @param {Ext.Component} component The component that was added
     * @param {Number} index The index at which the component was added to the container's items
     * collection
     * @since 2.3.0
     */
    /**
     * @event afterlayout
     * Fires when the components in this container are arranged by the associated layout manager.
     * @param {Ext.container.Container} this
     * @param {Ext.layout.container.Container} layout The ContainerLayout implementation for this
     * container
     * @since 2.3.0
     */
    /**
     * @event beforeadd
     * Fires before any {@link Ext.Component} is added or inserted into the container.
     * A handler can return false to cancel the add.
     * @param {Ext.container.Container} this
     * @param {Ext.Component} component The component being added
     * @param {Number} index The index at which the component will be added to the container's items
     * collection
     * @since 2.3.0
     */
    /**
     * @event beforeremove
     * Fires before any {@link Ext.Component} is removed from the container.  A handler can return
     * false to cancel the remove.
     * @param {Ext.container.Container} this
     * @param {Ext.Component} component The component being removed
     * @since 2.3.0
     */
    /**
     * @event remove
     * Fires after any {@link Ext.Component} is removed from the container.
     * @param {Ext.container.Container} this
     * @param {Ext.Component} component The component that was removed
     * @since 2.3.0
     */
    /**
     * @event childmove
     * Fires after any {@link Ext.Component} has changed its ordinal position within the container.
     * @param {Ext.container.Container} this
     * @param {Ext.Component} component The component that was moved
     * @param {Number} prevIndex The previous ordinal position of the Component
     * @param {Number} newIndex The new ordinal position of the Component
     */
    // ***********************************************************************************
    // End Events
    // ***********************************************************************************
    // </editor-fold>
    // <editor-fold desc="Methods">
    // ***********************************************************************************
    // Begin Methods
    // ***********************************************************************************
    /**
     * Adds {@link Ext.Component Component}(s) to this Container.
     *
     * ## Description:
     *
     * - Fires the {@link #beforeadd} event before adding.
     * - The Container's {@link #defaults default config values} will be applied
     *   accordingly (see `{@link #defaults}` for details).
     * - Fires the `{@link #event-add}` event after the component has been added.
     *
     * ## Notes:
     *
     * If the Container is __already rendered__ when `add`
     * is called, it will render the newly added Component into its content area.
     *
     * **If** the Container was configured with a size-managing {@link #cfg!layout} manager,
     * the Container will recalculate its internal layout at this time too.
     *
     * Note that the default layout manager simply renders child Components sequentially
     * into the content area and thereafter performs no sizing.
     *
     * If adding multiple new child Components, pass them as an array to the `add` method,
     * so that only one layout recalculation is performed.
     *
     *     tb = new Ext.toolbar.Toolbar({
     *         renderTo: document.body
     *     });  // toolbar is rendered
     *     // add multiple items.
     *     // default type for Toolbar is 'button')
     *     tb.add([{text:'Button 1'}, {text:'Button 2'}]);
     *
     * To inject components between existing ones, use the {@link #insert} method.
     *
     * ## Warning:
     *
     * Components directly managed by the BorderLayout layout manager may not be removed
     * or added.  See the Notes for {@link Ext.layout.container.Border BorderLayout} for
     * more details.
     *
     * @param {Ext.Component[]|Object[]/Ext.Component.../Object...} component
     * Either one or more Components to add or an Array of Components to add.
     * See `{@link #cfg-items}` for additional information.
     *
     * @return {Ext.Component[]/Ext.Component} The Components that were added.
     *
     * @since 2.3.0
     */
    add: function() {
        var me = this,
            args = Ext.Array.slice(arguments),
            index = (typeof args[0] === 'number') ? args.shift() : -1,
            layout = me.getLayout(),
            needsLayout = false,
            addingArray, items, i, length, item, pos, ret, instanced;
        if (args.length === 1 && Ext.isArray(args[0])) {
            items = args[0];
            addingArray = true;
        } else {
            items = args;
        }
        if (me.rendered) {
            Ext.suspendLayouts();
        }
        // suspend layouts while adding items...
        ret = items = me.prepareItems(items, true);
        length = items.length;
        if (!addingArray && length === 1) {
            // an array of 1 should still return an array...
            ret = items[0];
        }
        // loop
        for (i = 0; i < length; i++) {
            item = items[i];
            if (!item) {
                Ext.raise("Cannot add null item to Container with itemId/id: " + me.getItemId());
            }
            if (item.destroyed) {
                Ext.raise("Cannot add destroyed item '" + item.getId() + "' to Container '" + me.getId() + "'");
            }
            pos = (index < 0) ? me.items.length : (index + i);
            instanced = !!item.instancedCmp;
            delete item.instancedCmp;
            // Floating Components are not added into the items collection,
            // but to a separate floatingItems collection
            if (item.floating) {
                (me.floatingItems || (me.floatingItems = new Ext.util.ItemCollection())).add(item);
                item.onAdded(me, pos, instanced);
                delete item.$initParent;
                if (me.hasListeners.add) {
                    me.fireEvent('add', me, item, pos);
                }
            }
            // eslint-disable-next-line max-len
            else if ((!me.hasListeners.beforeadd || me.fireEvent('beforeadd', me, item, pos) !== false) && me.onBeforeAdd(item) !== false) {
                me.items.insert(pos, item);
                item.onAdded(me, pos, instanced);
                delete item.$initParent;
                if (me.focusableContainer) {
                    me.onFocusableChildAdd(item);
                }
                me.onAdd(item, pos);
                layout.onAdd(item, pos);
                needsLayout = true;
                if (me.hasListeners.add) {
                    me.fireEvent('add', me, item, pos);
                }
            }
            // This flag may be set by onBeforeAdd to tell the layout system that any remove
            // is temporary and that focus should not be reverted because Ext.layout.Layout#moveItem
            // will be moving things into place soon, and that will handle keeping focus stable.
            item.isLayoutMoving = false;
        }
        // We need to update our layout after adding all passed items
        // Unless we only added floating items.
        if (needsLayout) {
            me.updateLayout();
        }
        if (me.rendered) {
            if (length && me.focusableContainer) {
                me.$initFocusableContainerAfterLayout = true;
            }
            Ext.resumeLayouts(true);
        }
        return ret;
    },
    onAdded: function(container, pos, instanced) {
        this.callParent([
            container,
            pos,
            instanced
        ]);
        this.containerOnAdded(container, instanced);
    },
    /**
     * @method onRemoved
     * @inheritdoc
     */
    onRemoved: function(destroying) {
        this.containerOnRemoved(destroying);
        this.callParent(arguments);
    },
    afterComponentLayout: function() {
        var floaters = this.floatingItems,
            floaterCount, i, floater;
        this.callParent(arguments);
        // Contained, unrendered, autoShow items must be shown upon next layout of the Container
        if (floaters) {
            floaters = floaters.items;
            floaterCount = floaters.length;
            for (i = 0; i < floaterCount; i++) {
                floater = floaters[i];
                if (!floater.rendered && floater.autoShow) {
                    floater.show();
                }
            }
        }
    },
    /**
     * Invoked after the Container has laid out (and rendered if necessary)
     * its child Components.
     *
     * @param {Ext.layout.container.Container} layout
     *
     * @template
     * @protected
     */
    afterLayout: function(layout) {
        var me = this;
        ++me.layoutCounter;
        if (me.hasListeners.afterlayout) {
            me.fireEvent('afterlayout', me, layout);
        }
        // focusableContainer could have changed between setting the flag in add()
        // and actual layout, so check again
        if (me.focusableContainer && me.$initFocusableContainerAfterLayout) {
            me.initFocusableContainer();
        }
        delete me.$initFocusableContainerAfterLayout;
    },
    doDestroy: function() {
        var me = this,
            items = me.items,
            floatingItems = me.floatingItems,
            c;
        if (me.focusableContainer) {
            me.destroyFocusableContainer();
        }
        if (items) {
            while ((c = items.first())) {
                me.doRemove(c, true);
            }
            items.destroy();
            me.items = null;
        }
        if (floatingItems) {
            while ((c = floatingItems.first())) {
                me.doRemove(c, true);
            }
            floatingItems.destroy();
            me.floatingItems = null;
        }
        Ext.destroy(me.layout);
        me.callParent();
    },
    beforeRender: function() {
        var me = this,
            layout = me.getLayout(),
            targetCls;
        // In beforeRender in the parent we call disable to allow onDisable to be applied here.
        me.preventChildDisable = true;
        me.callParent();
        me.preventChildDisable = false;
        if (!layout.initialized) {
            layout.initLayout();
        }
        targetCls = layout.targetCls;
        if (targetCls) {
            me.applyTargetCls(targetCls);
        }
    },
    /**
     * Cascades down the component/container heirarchy from this component (passed in
     * the first call), calling the specified function with each component. The scope
     * (this reference) of the function call will be the scope provided or the current
     * component. The arguments to the function will be the args provided or the current
     * component. If the function returns false at any point, the cascade is stopped on
     * that branch.
     * @param {Function} fn The function to call
     * @param {Object} [scope] The scope of the function(defaults to current component)
     * @param {Array} [origArgs] The args to call the function with. The current component
     * always passed as the last argument.
     * @return {Ext.Container} this
     * @since 2.3.0
     */
    cascade: function(fn, scope, origArgs) {
        var me = this,
            cs = me.items ? me.items.items : [],
            len = cs.length,
            i = 0,
            c,
            args = origArgs ? origArgs.concat(me) : [
                me
            ],
            componentIndex = args.length - 1;
        if (fn.apply(scope || me, args) !== false) {
            for (; i < len; i++) {
                c = cs[i];
                if (c.cascade) {
                    c.cascade(fn, scope, origArgs);
                } else {
                    args[componentIndex] = c;
                    fn.apply(scope || c, args);
                }
            }
        }
        return this;
    },
    /**
     * Determines whether the passed Component is either an immediate child of this Container,
     * or whether it is a descendant.
     *
     * @param {Ext.Component} comp The Component to test.
     * @param {Boolean} [deep=false] Pass `true` to test for the Component being a descendant
     * at any level.
     * @return {Boolean} `true` if the passed Component is contained at the specified level.
     */
    contains: function(comp, deep) {
        var result = false;
        if (deep) {
            this.cascade(function(c) {
                // Only test if the item is a container
                if (c.contains && c.contains(comp)) {
                    result = true;
                    return false;
                }
            });
            return result;
        } else {
            return this.items.contains(comp) || (this.floatingItems && this.floatingItems.contains(comp));
        }
    },
    /**
     * Disables all child input fields and buttons.
     * @param silent
     * @param fromParent (private)
     */
    disable: function(silent, fromParent) {
        var me = this,
            wasDisabled = me.disabled,
            itemsToDisable, len, i;
        me.callParent([
            silent,
            fromParent
        ]);
        if (!fromParent && !me.preventChildDisable && !wasDisabled) {
            itemsToDisable = me.getChildItemsToDisable();
            len = itemsToDisable.length;
            for (i = 0; i < len; i++) {
                itemsToDisable[i].disable(silent, true);
            }
        }
        if (me.focusableContainer) {
            me.activateFocusableContainer(false);
        }
        return me;
    },
    /**
     * Enables all child input fields and buttons.
     * @param silent
     * @param fromParent (private)
     */
    enable: function(silent, fromParent) {
        var me = this,
            wasDisabled = me.disabled,
            itemsToDisable, len, i;
        me.callParent([
            silent,
            fromParent
        ]);
        if (wasDisabled) {
            itemsToDisable = me.getChildItemsToDisable();
            len = itemsToDisable.length;
            for (i = 0; i < len; i++) {
                itemsToDisable[i].enable(silent, true);
            }
        }
        if (me.focusableContainer) {
            me.activateFocusableContainer(true);
        }
        return me;
    },
    /**
     * Return the immediate child Component in which the passed element is located.
     * @param {Ext.dom.Element/HTMLElement/String} el The element to test (or ID of element).
     * @param {Boolean} deep If `true`, returns the deepest descendant Component which contains
     * the passed element.
     * @return {Ext.Component} The child item which contains the passed element.
     */
    getChildByElement: function(el, deep) {
        var item, itemEl,
            i = 0,
            it = this.getRefItems(),
            ln = it.length;
        el = Ext.getDom(el);
        for (; i < ln; i++) {
            item = it[i];
            itemEl = item.getEl();
            if (itemEl && ((itemEl.dom === el) || itemEl.contains(el))) {
                return (deep && item.getChildByElement) ? item.getChildByElement(el, deep) : item;
            }
        }
        return null;
    },
    /**
     * Examines this container's {@link #property-items} **property** and gets a direct child
     * component of this container.
     *
     * @param {String/Number} comp This parameter may be any of the following:
     *
     * - a **String** : representing the {@link Ext.Component#itemId itemId}
     *   or {@link Ext.Component#id id} of the child component.
     * - a **Number** : representing the position of the child component
     *   within the {@link #property-items} **property**
     *
     * For additional information see {@link Ext.util.MixedCollection#get}.
     *
     * @return {Ext.Component} The component (if found).
     *
     * @since 2.3.0
     */
    getComponent: function(comp) {
        if (Ext.isObject(comp)) {
            comp = comp.getItemId();
        }
        // eslint-disable-next-line vars-on-top
        var c = this.items.get(comp),
            floaters = this.floatingItems;
        // Only allow finding by index on the main items container
        if (!c && floaters && typeof comp !== 'number') {
            c = floaters.get(comp);
        }
        return c;
    },
    /**
     * @protected
     * Returns the focus holder element associated with this Container.
     * By default, this is the Container's target element; however if {@link #defaultFocus}
     * is defined, the child component referenced by that property will be found
     * and returned instead.
     *
     * @return {Ext.dom.Element} the focus holding element.
     */
    getFocusEl: function() {
        var delegate = this.getDefaultFocus();
        if (delegate) {
            // DO NOT drill down to delegate's focusEl or return its main el here.
            // Container's getFocusEl() is supposed to return delegates as components,
            // otherwise things break elsewhere.
            return delegate;
        } else if (this.focusable) {
            return this.getTargetEl();
        }
        // Containers that are not focusable should not return a focusEl
        return undefined;
    },
    /**
     * Returns the {@link Ext.layout.container.Container layout} instance currently associated
     * with this Container. If a layout has not been instantiated yet, that is done first
     * @return {Ext.layout.container.Container} The layout
     */
    getLayout: function() {
        var me = this,
            layout = me.layout;
        if (!layout || !layout.isLayout) {
            me.setLayout(layout);
        }
        return me.layout;
    },
    /**
     * @protected
     * Used by {@link Ext.ComponentQuery ComponentQuery}, {@link #child} and {@link #down}
     * to retrieve all of the items which can potentially be considered a child of this Container.
     *
     * This may be overriden by Components which have ownership of Components
     * that are not contained in the {@link #property-items} collection.
     *
     * NOTE: IMPORTANT note for maintainers:
     * Items are returned in tree traversal order. Each item is appended to the result array
     * followed by the results of that child's getRefItems call.
     * Floating child items are appended after internal child items.
     */
    getRefItems: function(deep) {
        var me = this,
            items = me.items.items,
            len = items.length,
            i = 0,
            item,
            result = [];
        for (; i < len; i++) {
            item = items[i];
            result[result.length] = item;
            if (deep && item.getRefItems) {
                result.push.apply(result, item.getRefItems(true));
            }
        }
        // Append floating items to the list.
        if (me.floatingItems) {
            items = me.floatingItems.items;
            len = items.length;
            for (i = 0; i < len; i++) {
                item = items[i];
                result[result.length] = item;
                if (deep && item.getRefItems) {
                    result.push.apply(result, item.getRefItems(true));
                }
            }
        }
        return result;
    },
    /**
     * Finds the configured default focus item. See {@link #defaultFocus}.
     */
    getDefaultFocus: function() {
        var defaultFocus = this.defaultFocus,
            result;
        // This might not work during initConfig
        if (defaultFocus && !this.isConfiguring) {
            result = this.down(defaultFocus);
        }
        // Returning undefined is ok
        return result;
    },
    setDefaultFocus: function(value) {
        this.defaultFocus = value;
    },
    initComponent: function() {
        var me = this;
        me.callParent();
        me.getLayout();
        // Set a flag to say we're constructing children, can be useful
        // to know during construction time to save work
        me.constructing = true;
        me.initItems();
        if (me.disabled) {
            me.disabled = false;
            me.disable(true);
        }
        me.reference = me.setupReference(me.reference);
        delete me.constructing;
    },
    /**
     * This method is called to initialize the `items` collection. A derived class can
     * override this method to do any last minute manipulation of `items` and then call
     * this method using `callParent`. Upon return, the `items` will no longer be a simple
     * array.
     * @protected
     */
    initItems: function() {
        var me = this,
            items = me.items;
        if (!items || !items.isMixedCollection) {
            // allow the items collection to be pre-initialized.
            // (used by Ext.draw.ComponentBase)
            /**
             * The Collection containing all the child items of this container.
             * @property {Ext.util.ItemCollection} items
             * @since 2.3.0
             */
            me.items = new Ext.util.ItemCollection();
            /**
             * The MixedCollection containing all the floating child items of this container.
             * Will be `undefined` if there are no floating child items.
             * @property {Ext.util.MixedCollection} floatingItems
             * @since 4.1.0
            */
            if (items) {
                if (!Ext.isArray(items)) {
                    items = [
                        items
                    ];
                }
                me.$initingItems = true;
                me.add(items);
                delete me.$initingItems;
            }
        }
    },
    /**
     * Called by `getInherited` to initialize the inheritedState the first time it is
     * requested.
     * @protected
     */
    initInheritedState: function(inheritedState, inheritedStateInner) {
        var me = this,
            layout = me.layout;
        me.callParent([
            inheritedState,
            inheritedStateInner
        ]);
        if (me.collapsed) {
            inheritedState.collapsed = true;
        }
        me.initContainerInheritedState(inheritedState, inheritedStateInner);
        if (layout && layout.initInheritedState) {
            layout.initInheritedState(inheritedState, inheritedStateInner);
        }
    },
    /**
     * Inserts a Component into this Container at a specified index. Fires the
     * {@link #beforeadd} event before inserting, then fires the {@link #event-add}
     * event after the Component has been inserted.
     *
     * @param {Number} index The index at which the Component will be inserted
     * into the Container's items collection
     *
     * @param {Ext.Component/Object/Ext.Component[]/Object[]} component The child Component
     * or config object to insert.
     *
     * Ext uses lazy rendering, and will only render the inserted Component should
     * it become necessary.
     *
     * A Component config object may be passed in order to avoid the overhead of
     * constructing a real Component object if lazy rendering might mean that the
     * inserted Component will not be rendered immediately. To take advantage of
     * this 'lazy instantiation', set the {@link Ext.Component#xtype} config
     * property to the registered type of the Component wanted.
     *
     * You can pass an array of Component instances and config objects.
     *
     * For a list of all available xtypes, see {@link Ext.enums.Widget}.
     *
     * @return {Ext.Component} component The Component (or config object) that was
     * inserted with the Container's default config values applied.
     *
     * @since 2.3.0
     */
    insert: function(index, component) {
        var compIdx;
        if (component && component.isComponent) {
            compIdx = this.items.indexOf(component);
            if (compIdx !== -1) {
                return this.move(compIdx, index);
            }
        }
        return this.add(index, component);
    },
    /**
     * @protected
     * Called when a raw config object is added to this container either during initialization
     * of the {@link #cfg-items} config, or when new items are {@link #method-add added},
     * or {@link #method-insert inserted}.
     *
     * This method converts the passed object into an instanced child component.
     *
     * This may be overridden in subclasses when special processing needs to be applied to child
     * creation. 
     *
     * @param {Object} comp The config object being added.
     * @return {Ext.Component} The component to be added.
     */
    lookupComponent: function(comp) {
        var me = this,
            defaultType = me.defaultType,
            wasAction;
        if (!comp.isComponent) {
            if (typeof comp === 'string') {
                // First char '@' means its an Action name.
                // Search this and all ancestors for a matching named Action
                // with which to create the Component.
                if (!(wasAction = (comp[0] === '@'))) {
                    // String used as a global component ID. Deprecated practice!
                    return Ext.ComponentManager.get(comp);
                }
                comp = me.getAction(comp.substr(1));
                defaultType = me.defaultActionType || defaultType;
            }
            comp = Ext.ComponentManager.create(comp, defaultType);
            // We need the inherited state to be invalidated upon add
            // because the create will have been performed outside of the
            // ownership hierarchy. The Action has no owner view (Actions are shareable).
            // This flag indicates to the new item's onAdded->onInheritedAdded machinery
            // that the inherited state must be invalidated.
            if (wasAction) {
                comp.instancedCmp = true;
            }
        }
        return comp;
    },
    /**
     * Moves a Component within the Container. This method does **not** account for things
     * like splitter components added by a layout. To better handle these situations, it
     * is recommended to use `{@link #moveBefore}` or `{@link #moveAfter}` instead.
     *
     * @param {Number/Ext.Component} fromIdx The index/component to move.
     * @param {Number} toIdx The new index for the Component.
     * @return {Ext.Component} component The Component that was moved.
     * @deprecated 5.0 Use `{@link #moveBefore}` or `{@link #moveAfter}` instead.
     */
    move: function(fromIdx, toIdx) {
        var me = this,
            items = me.items,
            item;
        if (fromIdx.isComponent) {
            fromIdx = items.indexOf(fromIdx);
        }
        item = items.getAt(fromIdx);
        if (fromIdx !== toIdx) {
            item = items.removeAt(fromIdx);
            if (item === false) {
                return false;
            }
            toIdx = Math.min(toIdx, items.getCount());
            items.insert(toIdx, item);
            me.onMove(item, fromIdx, toIdx);
            if (me.hasListeners.childmove) {
                me.fireEvent('childmove', me, item, fromIdx, toIdx);
            }
            me.updateLayout();
        }
        return item;
    },
    /**
     * Moves the given `item(s)` into this container in front of `before`. This method 
     * will account for layout-generated components like splitters and should be used 
     * instead of index based `{@link #method-move}`. If `before` is `null` then the 
     * `item` will be the last item in this container.
     * 
     *     var tb = Ext.create({
     *         xtype: 'toolbar',
     *         renderTo: Ext.getBody(),
     *         items: [{
     *             text: 'one'
     *         }, {
     *             text: 'two'
     *         }]
     *     });
     *
     *     // moves the 'two' button before the 'one' button
     *     tb.moveBefore(tb.getComponent(1), tb.getComponent(0));
     * 
     * @param {Ext.Component/Ext.Component[]} item The item to move. May be a component, 
     * component configuration object, or an array of either.
     * @param {Ext.Component} before The reference component. May be `null`.
     * @return {Ext.Component/Ext.Component[]} The moved item(s).
     * @since 5.0.0
     */
    moveBefore: function(item, before) {
        var activeEl, refocusEl;
        if (item !== before) {
            activeEl = Ext.Element.getActiveElement(true);
            // Do not disturb application focus state when moving a focused component.
            // Must test whether we contain the activeEl, not the containsFocus flag
            // because of asynchronous focus events.
            if (item.el && item.el.contains(activeEl)) {
                refocusEl = activeEl;
                refocusEl.suspendFocusEvents();
                item.isLayoutMoving = true;
            }
            item = this.layout.moveItemBefore(item, before);
            if (refocusEl) {
                item.isLayoutMoving = false;
                refocusEl.focus();
                refocusEl.resumeFocusEvents();
            }
        }
        return item;
    },
    /**
     * Moves the given `item(s)` into this container following `after`. This method will
     * account for layout-generated components like splitters and should be used instead
     * of index based `{@link #method-move}`. If `after` is `null` then the `item` will be the
     * first item in this container.
     * 
     *     var tb = Ext.create({
     *         xtype: 'toolbar',
     *         renderTo: Ext.getBody(),
     *         items: [{
     *             text: 'one'
     *         }, {
     *             text: 'two'
     *         }]
     *     });
     *
     *     // moves the 'one' button after the 'two' button
     *     tb.moveAfter(tb.getComponent(0), tb.getComponent(1));
     * 
     * @param {Ext.Component/Ext.Component[]} item The item to move. May be a component, 
     * component configuration object, or an array of either.
     * @param {Ext.Component} after The reference component. May be `null`.
     * @return {Ext.Component/Ext.Component[]} The moved item(s).
     * @since 5.0.0
     */
    moveAfter: function(item, after) {
        var layout = this.layout,
            index;
        if (item !== after) {
            index = after ? layout.getMoveAfterIndex(after) : 0;
            item = this.moveBefore(item, this.items.getAt(index));
        }
        return item;
    },
    /**
     * A method to find a child component after the passed child parameter. If a selector is also
     * provided, the first child component matching the selector will be returned.
     *
     * @param {Ext.Component} child The child to use as a starting point to find the next child.
     * @param {String} [selector] A {@link Ext.ComponentQuery} selector to find the next child.
     * This will return the next child matching this selector. This parameter is optional.
     * @return {Ext.Component} The next child found, `null` if no child found.
     */
    nextChild: function(child, selector) {
        var me = this,
            items = me.items,
            childIndex = items.indexOf(child),
            i = 0,
            len = items.length,
            result;
        if (childIndex !== -1) {
            if (selector) {
                for (; i < len; i++) {
                    result = items.getAt(childIndex + i);
                    if (!result || Ext.ComponentQuery.is(result, selector)) {
                        break;
                    }
                }
            } else {
                result = items.getAt(childIndex + 1);
            }
        }
        return result || null;
    },
    /**
     * @method
     * This method is invoked after a new Component has been added. It
     * is passed the Component which has been added. This method may
     * be used to update any internal structure which may depend upon
     * the state of the child items.
     *
     * @param {Ext.Component} component
     * @param {Number} position
     *
     * @template
     * @protected
     */
    onAdd: Ext.emptyFn,
    /**
     * This method is invoked before adding a new child Component. It
     * is passed the new Component, and may be used to modify the
     * Component, or prepare the Container in some way. Returning
     * false aborts the add operation.
     *
     * @param {Ext.Component} item
     *
     * @template
     * @protected
     */
    onBeforeAdd: function(item) {
        // Remove from current container without detaching it from the DOM if it's not us.
        var owner = item.ownerCt;
        if (item.isDetached) {
            item.reattachToBody();
        }
        if (owner && owner !== this) {
            item.isLayoutMoving = true;
            owner.remove(item, {
                destroy: false,
                detach: false
            });
        }
    },
    onMove: Ext.emptyFn,
    /**
     * @method
     * This method is invoked after a new Component has been
     * removed. It is passed the Component which has been
     * removed. This method may be used to update any internal
     * structure which may depend upon the state of the child items.
     *
     * @param {Ext.Component} component The removed component
     * @param {Boolean} isDestroying `true` if the the component is being destroyed in
     * the remove action
     *
     * @template
     * @protected
     */
    onRemove: Ext.emptyFn,
    onPosition: function() {
        this.callParent(arguments);
        this.repositionFloatingItems();
    },
    /**
     * @method onResize
     * @inheritdoc
     */
    onResize: function() {
        this.callParent(arguments);
        this.repositionFloatingItems();
    },
    /**
     * A method to find a child component before the passed child parameter. If a selector is also
     * provided, the first child component matching the selector will be returned.
     *
     * @param {Ext.Component} child The child to use as a starting point to find the previous child.
     * @param {String} [selector] A {@link Ext.ComponentQuery} selector to find the previous child.
     * This will return the first child matching this selector. This parameter is optional.
     * @return {Ext.Component} The previous child found, `null` if no child found.
     */
    prevChild: function(child, selector) {
        var me = this,
            items = me.items,
            childIndex = items.indexOf(child),
            i = 0,
            len = items.length,
            result;
        if (childIndex !== -1) {
            if (selector) {
                for (; i < len; i++) {
                    result = items.getAt(childIndex - i);
                    if (!result || Ext.ComponentQuery.is(result, selector)) {
                        break;
                    }
                }
            } else {
                result = items.getAt(childIndex - 1);
            }
        }
        return result || null;
    },
    /**
     * Removes a component from this container.  Fires the {@link #beforeremove} event
     * before removing, then fires the {@link #event-remove} event after the component has
     * been removed.
     *
     * @param {Ext.Component/String} component The component instance or id to remove.
     *
     * @param {Object} [autoDestroy] Flags to determine what to do with the removed component.
     * (May also be specified as a boolean `autoDestroy` flag for backward compatibility).
     * @param {Boolean} [autoDestroy.destroy] Defaults to this Container's {@link #autoDestroy}
     * config. Specifies whether to destroy the component being removed.
     * @param [autoDestroy.detach] Defaults to the {@link #detachOnRemove} configuration
     * Specifies whether to remove the component's DOM from the container and into
     * the {@link Ext#getDetachedBody detached body element}
     *
     * @return {Ext.Component} component The Component that was removed.
     * @since 2.3.0
     */
    remove: function(component, autoDestroy) {
        var me = this,
            c;
        // After destroying, items is nulled so we can't proceed
        if (me.destroyed || me.destroying) {
            return;
        }
        c = me.getComponent(component);
        if (!arguments.length) {
            Ext.log.warn("Ext.container.Container: remove takes an argument of the component " + "to remove. cmp.remove() is incorrect usage.");
        }
        if (c && (!me.hasListeners.beforeremove || me.fireEvent('beforeremove', me, c) !== false)) {
            me.doRemove(c, autoDestroy);
            if (me.hasListeners.remove) {
                me.fireEvent('remove', me, c);
            }
            if (!me.destroying && !me.destroyAfterRemoving && !c.floating) {
                me.updateLayout();
            }
            if (me.destroyAfterRemoving) {
                me.destroy();
            }
        }
        return c;
    },
    /**
     * Removes all components from this container.
     * @param {Boolean} [autoDestroy] True to automatically invoke the removed
     * Component's {@link Ext.Component#method-destroy} function.
     * Defaults to the value of this Container's {@link #autoDestroy} config.
     * @return {Ext.Component[]} Array of the removed components
     * @since 2.3.0
     */
    removeAll: function(autoDestroy) {
        var me = this,
            removeItems,
            floaters = me.floatingItems,
            items = [],
            i = 0,
            len, item;
        if (floaters) {
            removeItems = me.items.items.concat(floaters.items);
        } else {
            removeItems = me.items.items.slice();
        }
        len = removeItems.length;
        // Suspend Layouts while we remove multiple items from the container
        Ext.suspendLayouts();
        me.removingAll = true;
        for (; i < len; i++) {
            item = removeItems[i];
            me.remove(item, autoDestroy);
            if (item.ownerCt !== me) {
                items.push(item);
            }
        }
        me.removingAll = false;
        // Resume Layouts now that all items have been removed and do a single layout
        // (if we removed anything!)
        Ext.resumeLayouts(!!len);
        return items;
    },
    /**
     * Reconfigures the initially configured {@link #cfg!layout}.
     *
     * NOTE: this method cannot be used to change the "type" of layout after the component
     * has been rendered to the DOM. After rendering, this method can only modify the
     * existing layout's configuration properties. The reason for this restriction is that
     * many container layouts insert special wrapping elements into the dom, and the
     * framework does not currently support dynamically changing these elements once
     * rendered.
     * @param {Object} configuration object for the layout
     */
    setLayout: function(configuration) {
        var me = this,
            oldLayout = me.layout,
            type;
        if (configuration) {
            if (typeof configuration === 'string') {
                configuration = {
                    type: configuration
                };
            }
            type = configuration.type;
            if (oldLayout) {
                if (oldLayout.isLayout) {
                    // Same layout type requested, or just a reconfigure object.
                    // Either way, we reconfigure the current layout.
                    if (!type || (type === oldLayout.type)) {
                        oldLayout.setConfig(configuration);
                        configuration = oldLayout;
                    } else // Different layout type requested (not allowed after render)
                    // just detach from the current layout.
                    {
                        oldLayout.setOwner(null);
                    }
                } else // Old layout has not yet been instantiated; merge new in.
                {
                    if (typeof oldLayout === 'string') {
                        oldLayout = {
                            type: oldLayout
                        };
                    }
                    configuration = Ext.merge({}, oldLayout, configuration);
                }
            }
            if (!(configuration && configuration.isLayout)) {
                configuration.owner = this;
                configuration = Ext.Factory.layout(configuration);
            }
            configuration.setOwner(this);
        }
        me.layout = configuration;
        if (me.rendered) {
            me.updateLayout();
        }
    },
    /**
     * Sets a component as the active layout item. This only applies when using
     * a {@link Ext.layout.container.Card} layout.
     *
     *     var card1 = Ext.create('Ext.panel.Panel', {itemId: 'card-1'});
     *     var card2 = Ext.create('Ext.panel.Panel', {itemId: 'card-2'});
     *     var panel = Ext.create('Ext.panel.Panel', {
     *         layout: 'card',
     *         items: [card1, card2]
     *     });
     *     // These are all equivalent
     *     panel.getLayout().setActiveItem(card2);
     *     panel.getLayout().setActiveItem('card-2');
     *     panel.getLayout().setActiveItem(1);
     *
     * @param {Ext.Component/Number/String} item  The component, component
     * {@link Ext.Component#id id}, {@link Ext.Component#itemId itemId}, or index of component.
     * @return {Ext.Component} the activated component or false when nothing activated.
     * False is returned also when trying to activate an already active item.
     */
    setActiveItem: function(item) {
        return this.getLayout().setActiveItem(item);
    },
    updateActions: function(actions) {
        var actionName;
        // Convert action configs into Ext.Action instances.
        for (actionName in actions) {
            if (!actions[actionName].isAction) {
                actions[actionName] = new Ext.Action(actions[actionName]);
            }
        }
    },
    /**
     * Retrieves the named {@link Ext.Action Action} from this view or any ancestor which
     * has that named Action. See {@link #actions}
     */
    getAction: function(name) {
        var owner = this;
        for (owner = this; owner; owner = owner.getRefOwner()) {
            if (owner.actions && owner.actions[name]) {
                return owner.actions[name];
            }
        }
    },
    onShowComplete: function(cb, scope) {
        var me = this;
        me.callParent([
            cb,
            scope
        ]);
        if (me.focusableContainer && me.activateFocusableContainer) {
            me.activateFocusableContainer();
        }
    },
    onFocusEnter: function(e) {
        var me = this;
        me.callParent([
            e
        ]);
        // We DO NOT check if `me` is focusable here. The reason is that
        // non-focusable containers need to track focus entering their
        // children so that revertFocus would work if these children
        // become unavailable.
        if (me.focusableContainer && !me.destroying && !me.destroyed) {
            me.mixins.focusablecontainer.onFocusEnter.call(me, e);
        }
    },
    onFocusLeave: function(e) {
        var me = this;
        me.callParent([
            e
        ]);
        // Ditto
        if (me.focusableContainer && !me.destroying && !me.destroyed) {
            me.mixins.focusablecontainer.onFocusLeave.call(me, e);
        }
    },
    // ***********************************************************************************
    // End Methods
    // ***********************************************************************************
    // </editor-fold>
    privates: {
        /**
         * @private
         */
        applyDefaults: function(config) {
            var me = this,
                defaults = me.defaults;
            if (defaults) {
                if (Ext.isFunction(defaults)) {
                    defaults = defaults.call(me, config);
                }
                if (Ext.isString(config)) {
                    config = Ext.ComponentManager.get(config);
                }
                if (config.isComponent) {
                    // If we already have a component instance the best we can do is
                    // conditionally set all the configs in defaults if they have not
                    // yet been set on the component instance.
                    config.setConfig(defaults, null, me._applyDefaultsOptions);
                } else {
                    // If we have a config object (not a component instance) we can do a
                    // full (deep) merge of the config with the defaults object.
                    // Fork the defaults object first so that we don't modify the original
                    config = me.self.getConfigurator().merge(me, Ext.Object.fork(defaults), config);
                }
            }
            return config;
        },
        // The targetCls is a CSS class that the layout needs added to the targetEl. The targetEl
        // is where the container's children are rendered and is usually just the main el.
        // Some containers (e.g. panels) use a body instead.
        //
        // In general, if a class overrides getTargetEl it will also need to override this method.
        // This is necessary to avoid a post-render step to add the targetCls.
        applyTargetCls: function(targetCls) {
            this.layoutTargetCls = targetCls;
        },
        /**
         * @private
         */
        doRemove: function(component, flags) {
            var me = this,
                layout = me.layout,
                hasLayout = layout && me.rendered,
                floating = component.floating,
                doDetach = me.detachOnRemove,
                doDestroy = me.autoDestroy,
                isDestroying;
            // Ensure the flags are set correctly
            if (typeof flags === 'boolean') {
                doDestroy = flags;
            } else if (typeof flags === 'object') {
                if (flags.destroy != null) {
                    doDestroy = flags.destroy;
                }
                if (flags.detach != null) {
                    doDetach = flags.detach;
                }
            }
            // isDestroying flag is true if the removal is taking place as part of destruction,
            // OR if removal is intended to *cause* destruction
            isDestroying = component.destroying || doDestroy;
            if (floating) {
                me.floatingItems.remove(component);
            } else {
                me.items.remove(component);
            }
            // Inform ownerLayout of removal before deleting the ownerLayout & ownerCt references
            // in the onRemoved call
            if (hasLayout && !floating) {
                // Removing a component from a running layout has to cancel the layout
                if (layout.running) {
                    Ext.Component.cancelLayout(component, isDestroying);
                }
                layout.onRemove(component, isDestroying);
            }
            // Can be already destroyed!
            if (!component.destroyed) {
                component.onRemoved(isDestroying);
            }
            if (me.focusableContainer && !me.destroying && !me.destroyed) {
                me.onFocusableChildRemove(component, isDestroying);
            }
            me.onRemove(component, isDestroying);
            // Destroy if we were explicitly told to, or we're defaulting to our autoDestroy
            // configuration. If the component is already destroyed, calling destroy() again
            // won't blow up.
            if (doDestroy) {
                component.destroy();
            }
            // Only have the layout perform remove postprocessing if the Component is not
            // being destroyed, and this container is yet alive (could be destroyed too)
            else if (!me.destroyed) {
                if (hasLayout && !floating) {
                    layout.afterRemove(component);
                }
                if (doDetach && component.rendered) {
                    component.detachFromBody();
                }
            }
        },
        finishRenderChildren: function() {
            var layout;
            this.callParent();
            layout = this.getLayout();
            if (layout) {
                layout.finishRender();
            }
        },
        /**
         * Gets a list of child components to enable/disable when the container is
         * enabled/disabled
         * @private
         * @return {Ext.Component[]} Items to be enabled/disabled
         */
        getChildItemsToDisable: function() {
            return this.query('[isLabelable],[isFocusableContainer],button');
        },
        /**
         * @private
         */
        getContentTarget: function() {
            return this.getLayout().getContentTarget();
        },
        /**
         * @private
         */
        getDefaultContentTarget: function() {
            return this.el;
        },
        /**
         * @private
         */
        prepareItems: function(items, applyDefaults) {
            // Create an Array which does not refer to the passed array.
            // The passed array is a reference to a user's config object and MUST NOT be mutated.
            if (Ext.isArray(items)) {
                items = items.slice();
            } else {
                items = [
                    items
                ];
            }
            // Make sure defaults are applied and item is initialized
            // eslint-disable-next-line vars-on-top
            var me = this,
                i = 0,
                len = items.length,
                item;
            for (; i < len; i++) {
                item = items[i];
                if (item == null) {
                    Ext.Array.erase(items, i, 1);
                    --i;
                    --len;
                } else {
                    if (applyDefaults) {
                        item = this.applyDefaults(item);
                    }
                    // Tell the item we're in a container during construction
                    item.$initParent = me;
                    if (item.isComponent) {
                        // When this was passed to us, it's an already constructed component
                        // This is useful to know because we can make decisions regarding the
                        // state of the component if it's newly created
                        item.instancedCmp = true;
                    }
                    items[i] = me.lookupComponent(item);
                    // delete here because item may have been a config, so we don't
                    // want to mutate it
                    delete item.$initParent;
                }
            }
            return items;
        },
        repositionFloatingItems: function() {
            var floaters = this.floatingItems,
                floaterCount, i, floater;
            // Ensure correct positioning of floated children before calling superclass
            if (floaters) {
                floaters = floaters.items;
                floaterCount = floaters.length;
                for (i = 0; i < floaterCount; i++) {
                    floater = floaters[i];
                    if (floater.el && !floater.hidden) {
                        floater.setPosition(floater.x, floater.y);
                    }
                }
            }
        },
        initTabGuards: function(activate) {
            var me = this,
                beforeGuard = me.tabGuardBeforeEl,
                afterGuard = me.tabGuardAfterEl,
                minTabIndex = me.tabGuardBeforeIndex || 0,
                maxTabIndex = me.tabGuardAfterIndex || 0,
                i, tabIndex, nodes;
            if (!me.rendered || !me.tabGuard) {
                return;
            }
            nodes = me.el.findTabbableElements({
                skipSelf: true
            });
            // Both tab guards may be in the list, disregard them
            if (nodes[0] === beforeGuard.dom) {
                nodes.shift();
            }
            if (nodes[nodes.length - 1] === afterGuard.dom) {
                nodes.pop();
            }
            if (nodes && nodes.length) {
                // In some cases it might be desirable to configure before and after
                // guard elements' tabIndex explicitly but if it is missing we try to
                // infer it from the DOM. If we don't and there are elements with
                // tabIndex > 0 within the container then tab order will be very
                // unintuitive.
                if (minTabIndex == null || maxTabIndex == null) {
                    for (i = 0; i < nodes.length; i++) {
                        // Can't use node.tabIndex property here, IE8 will report 0
                        // even if tabIndex attribute is missing.
                        tabIndex = +nodes[i].getAttribute('tabIndex');
                        if (tabIndex > 0) {
                            minTabIndex = Math.min(minTabIndex, tabIndex);
                            maxTabIndex = Math.max(maxTabIndex, tabIndex);
                        }
                    }
                }
                beforeGuard.dom.setAttribute('tabIndex', minTabIndex);
                afterGuard.dom.setAttribute('tabIndex', maxTabIndex);
            } else {
                // We don't want the guards to participate in tab flow
                // if there are no tabbable children in the container
                beforeGuard.dom.removeAttribute('tabIndex');
                afterGuard.dom.removeAttribute('tabIndex');
            }
            if (me.onTabGuardFocusEnter) {
                if (!beforeGuard.hasListeners.focusenter) {
                    beforeGuard.on('focusenter', me.onTabGuardFocusEnter, me);
                }
                if (!afterGuard.hasListeners.focusenter) {
                    afterGuard.on('focusenter', me.onTabGuardFocusEnter, me);
                }
            }
        },
        _noMargin: {
            'margin-top': '',
            'margin-right': '',
            'margin-bottom': '',
            'margin-left': ''
        },
        // Removes inline margins set by the layout system (see ContextItem#getMarginInfo)
        // TODO: fix EXTJS-13359 and remove this method
        resetItemMargins: function() {
            var items = this.items.items,
                i = items.length,
                noMargin = this._noMargin,
                item;
            while (i--) {
                item = items[i];
                item.margin$ = null;
                item.el.setStyle(noMargin);
            }
        },
        setupRenderTpl: function(renderTpl) {
            this.callParent(arguments);
            this.getLayout().setupRenderTpl(renderTpl);
        }
    }
});
// private

/**
 * Component layout for editors
 * @private
 */
Ext.define('Ext.layout.container.Editor', {
    extend: 'Ext.layout.container.Container',
    alias: 'layout.editor',
    autoSizeDefault: {
        width: 'field',
        height: 'field'
    },
    sizePolicies: {
        // indexed by autoSize.width
        $: {
            // indexed by autoSize.height
            $: {
                readsWidth: 1,
                readsHeight: 1,
                setsWidth: 0,
                setsHeight: 0
            },
            boundEl: {
                readsWidth: 1,
                readsHeight: 0,
                setsWidth: 0,
                setsHeight: 1
            }
        },
        boundEl: {
            // indexed by autoSize.height
            $: {
                readsWidth: 0,
                readsHeight: 1,
                setsWidth: 1,
                setsHeight: 0
            },
            boundEl: {
                readsWidth: 0,
                readsHeight: 0,
                setsWidth: 1,
                setsHeight: 1
            }
        }
    },
    getItemSizePolicy: function(item) {
        var me = this,
            autoSize = me.owner.autoSize,
            key = autoSize && autoSize.width,
            policy = me.sizePolicies;
        policy = policy[key] || policy.$;
        key = autoSize && autoSize.height;
        policy = policy[key] || policy.$;
        return policy;
    },
    calculate: function(ownerContext) {
        var me = this,
            owner = me.owner,
            autoSize = owner.autoSize,
            fieldWidth, fieldHeight;
        if (autoSize === true) {
            autoSize = me.autoSizeDefault;
        }
        // Calculate size of both Editor, and its owned Field
        if (autoSize) {
            fieldWidth = me.getDimension(owner, autoSize.width, 'getWidth', owner.width);
            fieldHeight = me.getDimension(owner, autoSize.height, 'getHeight', owner.height);
        }
        // Set Field size
        ownerContext.childItems[0].setSize(fieldWidth, fieldHeight);
        // Bypass validity checking. Container layouts should not usually set their owner's size.
        ownerContext.setWidth(fieldWidth);
        ownerContext.setHeight(fieldHeight);
        // This is a Container layout, so publish content size
        ownerContext.setContentSize(fieldWidth || owner.field.getWidth(), fieldHeight || owner.field.getHeight());
    },
    getDimension: function(owner, type, getMethod, ownerSize) {
        switch (type) {
            // Size to boundEl's dimension
            case 'boundEl':
                return owner.boundEl[getMethod]();
            // Auto size (shrink wrap the Field's size
            case 'field':
                return undefined;
            // Size to the Editor's configured size
            default:
                return ownerSize;
        }
    }
});

/**
 * The Editor class is used to provide inline editing for elements on the page. The editor
 * is backed by a {@link Ext.form.field.Field} that will be displayed to edit the underlying
 * content. The editor is a floating Component, when the editor is shown it is automatically
 * aligned to display over the top of the bound element it is editing. The Editor contains several
 * options for how to handle key presses:
 *
 * - {@link #completeOnEnter}
 * - {@link #cancelOnEsc}
 * - {@link #swallowKeys}
 *
 * It also has options for how to use the value once the editor has been activated:
 *
 * - {@link #revertInvalid}
 * - {@link #ignoreNoChange}
 * - {@link #updateEl}
 *
 * Sample usage:
 * 
 *     @example
 *     var form = Ext.create('Ext.form.Panel', {
 *         renderTo: Ext.getBody(),
 *         width: 380,
 *         height: 400,
 *         title: 'User Details',
 *         bodyPadding: 10,
 *         items: [{
 *             html: 'Double-Click on the header title, this, or the field label to edit',
 *             height:30
 *         },{
 *             fieldLabel: 'First Name',
 *             name: 'firstname',
 *             xtype: 'textfield'
 *         }]
 *     });
 *     
 *     var editor = new Ext.Editor({
 *         // update the innerHTML of the bound element 
 *         // when editing completes
 *         updateEl: true,
 *         alignment: 'l-l',
 *         autoSize: {
 *             width: 'boundEl'
 *         },
 *         field: {
 *             xtype: 'textfield'
 *         }
 *     });
 *     
 *     form.header.getTitle().textEl.on('dblclick', function(e, t) {
 *         editor.startEdit(t);
 *     });
 *     
 *     form.getTargetEl().on('dblclick', function(e, t) {
 *         editor.startEdit(t);
 *         // Manually focus, since clicking on the label will focus the text field
 *         editor.field.focus(50, true);
 *     });
 *
 * {@img Ext.Editor/Ext.Editor.png Ext.Editor component}
 *
 */
Ext.define('Ext.Editor', {
    extend: 'Ext.container.Container',
    xtype: 'editor',
    requires: [
        'Ext.layout.container.Editor'
    ],
    layout: 'editor',
    /**
    * @cfg {Ext.form.field.Field} field
    * The Field object (or descendant) or config object for field
    */
    /**
     * @cfg {Boolean} allowBlur
     * True to {@link #completeEdit complete the editing process} if in edit mode when focus exits
     * from this Editor's hierarchy.
     */
    allowBlur: true,
    /**
     * @cfg {Boolean/Object} autoSize
     * True for the editor to automatically adopt the size of the underlying field. Otherwise,
     * an object can be passed to indicate where to get each dimension. The available properties
     * are 'boundEl' and 'field'. If a dimension is not specified, it will use the underlying
     * height/width specified on the editor object.
     * Examples:
     *
     *     autoSize: true // The editor will be sized to the height/width of the field
     *
     *     height: 21,
     *     autoSize: {
     *         width: 'boundEl' // The width will be determined by the width of the boundEl,
     *                          // the height from the editor (21)
     *     }
     *
     *     autoSize: {
     *         width: 'field', // Width from the field
     *         height: 'boundEl' // Height from the boundEl
     *     }
     */
    /**
     * @cfg {Boolean} revertInvalid
     * True to automatically revert the field value and cancel the edit when the user completes
     * an edit and the field validation fails
     */
    revertInvalid: true,
    /**
     * @cfg {Boolean} [ignoreNoChange=false]
     * True to skip the edit completion process (no save, no events fired) if the user completes
     * an edit and the value has not changed. Applies only to string values - edits for other
     * data types will never be ignored.
     */
    /**
     * @cfg {Boolean} [hideEl=true]
     * False to keep the bound element visible while the editor is displayed
     */
    /**
     * @cfg {Object} value
     * The data value of the underlying field
     */
    value: '',
    /**
     * @cfg {String} alignment
     * The position to align to (see {@link Ext.util.Positionable#alignTo} for more details).
     */
    alignment: 'c-c?',
    /**
     * @cfg {Number[]} offsets
     * The offsets to use when aligning (see {@link Ext.util.Positionable#alignTo} for more details.
     */
    offsets: [
        0,
        0
    ],
    /**
     * @cfg {Boolean/String} shadow
     * "sides" for sides/bottom only, "frame" for 4-way shadow, and "drop" for bottom-right shadow.
     */
    shadow: 'frame',
    /**
     * @cfg {Boolean} constrain
     * True to constrain the editor to the viewport
     */
    constrain: false,
    /**
     * @cfg {Boolean} swallowKeys
     * Handle the keydown/keypress events so they don't propagate
     */
    swallowKeys: true,
    /**
     * @cfg {Boolean} completeOnEnter
     * True to complete the edit when the enter key is pressed.
     */
    completeOnEnter: true,
    /**
     * @cfg {Boolean} cancelOnEsc
     * True to cancel the edit when the escape key is pressed.
     */
    cancelOnEsc: true,
    /**
     * @cfg {Boolean} updateEl
     * True to update the innerHTML of the bound element when the update completes
     */
    updateEl: false,
    // Do not participate in the ZIndexManager's focus switching operations.
    // When an editor is hidden, the ZIndexManager will not automatically activate
    // the last visible floater on the stack.
    focusOnToFront: false,
    /**
     * @cfg {String/HTMLElement/Ext.dom.Element} [parentEl=document.body]
     * An element to render to.
     */
    baseCls: Ext.baseCSSPrefix + 'editor',
    /**
     * @property {Boolean} editing
     * True if this editor is currently active.
     * @readonly
     */
    editing: false,
    /**
     * @event beforestartedit
     * Fires when editing is initiated, but before the value changes.  Editing can be canceled
     * by returning `false` from the handler of this event.
     * @param {Ext.Editor} this
     * @param {Ext.dom.Element} boundEl The underlying element bound to this editor
     * @param {Object} value The field value being set
     */
    /**
     * @event startedit
     * Fires when this editor is displayed
     * @param {Ext.Editor} this
     * @param {Ext.dom.Element} boundEl The underlying element bound to this editor
     * @param {Object} value The starting field value
     */
    /**
     * @event beforecomplete
     * Fires after a change has been made to the field, but before the change is reflected in the
     * underlying field. Saving the change to the field can be canceled by returning `false`
     * from the handler of this event. Note that if the value has not changed and
     * ignoreNoChange = true, the editing will still end but this event will not fire since no edit
     * actually occurred.
     * @param {Ext.Editor} this
     * @param {Object} value The current field value
     * @param {Object} startValue The original field value
     */
    /**
     * @event complete
     * Fires after editing is complete and any changed value has been written to the underlying
     * field.
     * @param {Ext.Editor} this
     * @param {Object} value The current field value
     * @param {Object} startValue The original field value
     */
    /**
     * @event canceledit
     * Fires after editing has been canceled and the editor's value has been reset.
     * @param {Ext.Editor} this
     * @param {Object} value The user-entered field value that was discarded
     * @param {Object} startValue The original field value that was set back into the editor after
     * cancel
     */
    /**
     * @event specialkey
     * Fires when any key related to navigation (arrows, tab, enter, esc, etc.) is pressed.
     * You can check {@link Ext.event.Event#getKey} to determine which key was pressed.
     * @param {Ext.Editor} this
     * @param {Ext.form.field.Field} field The field attached to this editor
     * @param {Ext.event.Event} event The event object
     */
    preventDefaultAlign: true,
    useBoundValue: true,
    specialKeyDelay: 1,
    initComponent: function() {
        var me = this,
            field = me.field = Ext.ComponentManager.create(me.field || {}, 'textfield');
        field.msgTarget = field.msgTarget || 'qtip';
        me.mon(field, {
            scope: me,
            specialkey: me.onSpecialKey
        });
        if (field.grow) {
            me.mon(field, 'autosize', me.onFieldAutosize, me, {
                delay: 1
            });
        }
        me.floating = {
            constrain: me.constrain
        };
        me.items = field;
        me.callParent();
    },
    onAdded: function(container) {
        // Editors are floaters and shouldn't have an ownerCt, so use ownerCmp as
        // the upward link.
        this.ownerCmp = container;
    },
    /**
     * @private
     */
    onFieldAutosize: function() {
        this.updateLayout();
    },
    afterRender: function(ct, position) {
        var me = this,
            field = me.field,
            inputEl = field.inputEl;
        me.callParent(arguments);
        // Ensure the field doesn't get submitted as part of any form
        if (inputEl) {
            inputEl.dom.name = '';
            if (me.swallowKeys) {
                inputEl.swallowEvent([
                    'keypress',
                    // *** Opera
                    'keydown'
                ]);
            }
        }
    },
    // *** all other browsers
    /**
     * @private
     */
    onSpecialKey: function(field, event) {
        var me = this,
            key = event.getKey(),
            complete = me.completeOnEnter && key === event.ENTER,
            cancel = me.cancelOnEsc && key === event.ESC,
            task = me.specialKeyTask;
        if (complete || cancel) {
            event.stopEvent();
            if (!task) {
                me.specialKeyTask = task = new Ext.util.DelayedTask();
            }
            // Must defer this slightly to prevent exiting edit mode before the field's own
            // key nav can handle the enter key, e.g. selecting an item in a combobox list
            task.delay(me.specialKeyDelay, complete ? me.completeEdit : me.cancelEdit, me);
            // Makes unit testing easier
            if (me.specialKeyDelay === 0) {
                task.cancel();
                if (complete) {
                    me.completeEdit();
                } else {
                    me.cancelEdit();
                }
            }
        }
        me.fireEvent('specialkey', me, field, event);
    },
    /**
     * Starts the editing process and shows the editor.
     * @param {String/HTMLElement/Ext.dom.Element} el The element to edit
     * @param {String} value (optional) A value to initialize the editor with. If a value
     * is not provided, it defaults to the innerHTML of el.
     * @param doFocus (private)
     * @param isResuming (private)
     */
    startEdit: function(el, value, doFocus, isResuming) {
        var me = this,
            field = me.field,
            dom, ownerCt, renderTo;
        if (!isResuming) {
            me.completeEdit(true);
        }
        me.boundEl = Ext.get(el);
        dom = me.boundEl.dom;
        if (me.useBoundValue && !Ext.isDefined(value)) {
            value = Ext.String.trim(dom.textContent || dom.innerText || dom.innerHTML);
        }
        if (me.fireEvent('beforestartedit', me, me.boundEl, value) !== false) {
            if (me.context) {
                // Grab the value again, may have changed in beforestartedit
                value = me.context.value;
            }
            // If NOT configured with a renderTo, render to the ownerCt's element
            // Being floating, we do not need to use the actual layout's target.
            // Indeed, it's better if we do not so that we do not interfere with layout's
            // child management.
            Ext.suspendLayouts();
            if (!me.rendered) {
                ownerCt = me.ownerCt;
                renderTo = me.renderTo || (ownerCt && ownerCt.getEl()) || Ext.getBody();
                Ext.fly(renderTo).position();
                me.renderTo = renderTo;
            }
            me.startValue = value;
            me.show();
            me.realign(true);
            // temporarily suspend events on field to prevent the "change" event from firing
            // when resetOriginalValue() and setValue() are called
            field.suspendEvents();
            field.setValue(value);
            field.resetOriginalValue();
            field.resumeEvents();
            if (doFocus !== false) {
                field.focus(field.selectOnFocus ? true : [
                    Ext.Number.MAX_SAFE_INTEGER
                ]);
            }
            if (field.autoSize) {
                field.autoSize();
            }
            Ext.resumeLayouts(true);
            me.toggleBoundEl(false);
            me.editing = true;
        }
    },
    /**
     * Realigns the editor to the bound field based on the current alignment config value.
     * @param {Boolean} autoSize (optional) True to size the field to the dimensions of the bound
     * element.
     */
    realign: function(autoSize) {
        var me = this;
        if (autoSize === true) {
            me.updateLayout();
        }
        me.alignTo(me.boundEl, me.alignment, me.offsets);
    },
    /**
     * Ends the editing process, persists the changed value to the underlying field, and hides
     * the editor.
     * @param {Boolean} [remainVisible=false] Override the default behavior and keep the editor
     * visible after edit
     */
    completeEdit: function(remainVisible) {
        var me = this,
            field = me.field,
            startValue = me.startValue,
            cancel = me.context && me.context.cancel,
            value;
        if (!me.editing) {
            return;
        }
        // Assert combo values first
        if (field.assertValue) {
            field.assertValue();
        }
        value = me.getValue();
        if (!field.isValid()) {
            if (me.revertInvalid !== false) {
                me.cancelEdit(remainVisible);
            }
            return;
        }
        if (me.ignoreNoChange && !field.didValueChange(value, startValue)) {
            me.onEditComplete(remainVisible);
            return;
        }
        if (me.fireEvent('beforecomplete', me, value, startValue) !== false) {
            // Grab the value again, may have changed in beforecomplete
            value = me.getValue();
            if (me.updateEl && me.boundEl) {
                me.boundEl.setHtml(value);
            }
            me.onEditComplete(remainVisible, cancel);
            me.fireEvent('complete', me, value, startValue);
        }
    },
    onShow: function() {
        var me = this;
        me.callParent(arguments);
        me.fireEvent('startedit', me, me.boundEl, me.startValue);
    },
    /**
     * Cancels the editing process and hides the editor without persisting any changes. The field
     * value will be reverted to the original starting value.
     * @param {Boolean} [remainVisible=false] Override the default behavior and keep the editor
     * visible after cancel
     */
    cancelEdit: function(remainVisible) {
        var me = this,
            startValue = me.startValue,
            field = me.field,
            value;
        if (me.editing) {
            if (field) {
                value = me.editedValue = me.getValue();
                // temporarily suspend events on field to prevent the "change" event from firing
                // when setValue() is called
                field.suspendEvents();
                me.setValue(startValue);
                field.resumeEvents();
            }
            me.onEditComplete(remainVisible, true);
            me.fireEvent('canceledit', me, value, startValue);
            delete me.editedValue;
        }
    },
    /**
     * @private
     */
    onEditComplete: function(remainVisible, canceling) {
        this.editing = false;
        if (remainVisible !== true) {
            this.hide();
            this.toggleBoundEl(true);
        }
    },
    onFocusLeave: function(e) {
        var me = this;
        if (me.allowBlur === true && me.editing) {
            me.completeEdit();
        }
        me.callParent([
            e
        ]);
    },
    onHide: function() {
        var me = this,
            field = me.field;
        if (me.editing) {
            me.completeEdit();
        } else if (field.collapse) {
            field.collapse();
        }
        me.callParent(arguments);
    },
    /**
     * Gets the data value of the editor
     * @return {Object} The data value
     */
    getValue: function() {
        return this.field.getValue();
    },
    /**
     * Sets the data value of the editor
     * @param {Object} value Any valid value supported by the underlying field
     */
    setValue: function(value) {
        this.field.setValue(value);
    },
    toggleBoundEl: function(visible) {
        if (this.hideEl !== false) {
            this.boundEl.setVisible(visible);
        }
    },
    doDestroy: function() {
        var me = this,
            task = me.specialKeyTask;
        if (task) {
            task.cancel();
        }
        Ext.destroy(me.field);
        me.callParent();
    }
});

/**
 * @class Ext.EventManager
 * Registers event handlers on DOM elements.
 * 
 * This class is deprecated.  Please use the Ext.dom.Element api to attach listeners to
 * DOM Elements.  For example:
 * 
 *     var element = Ext.get('myId');
 *     
 *     element.on('click', function(e) {
 *         // event handling logic here
 *     });
 *
 * @singleton
 * @deprecated 5.0.0 Please use the Ext.dom.Element api to attach listeners to DOM Elements
 */
Ext.define('Ext.EventManager', {
    singleton: true,
    mouseLeaveRe: /(mouseout|mouseleave)/,
    mouseEnterRe: /(mouseover|mouseenter)/,
    /**
     * Appends an event handler to an element.  The shorthand version {@link #on} is equivalent.
     * Typically you will use {@link Ext.dom.Element#addListener} directly on an Element in favor of
     * calling this version.
     *
     * {@link Ext.EventManager#on} is an alias for {@link Ext.EventManager#addListener}.
     *
     * @param {String/Ext.dom.Element/HTMLElement/Window} element The html element or id to assign
     * the event handler to.
     *
     * @param {String} eventName The name of the event to listen for.
     * May also be an object who's property names are event names.
     *
     * @param {Function/String} [fn] The handler function the event invokes. A String parameter
     * is assumed to be method name in `scope` object, or Element object if no scope is provided.
     * @param {Ext.event.Event} fn.event The {@link Ext.event.Event EventObject} describing
     * the event.
     * @param {Ext.dom.Element} fn.target The Element which was the target of the event.
     * Note that this may be filtered by using the `delegate` option.
     * @param {Object} fn.options The options object from the addListener call.
     *
     * @param {Object} [scope] The scope (`this` reference) in which the handler function
     * is executed. Defaults to the Element.
     *
     * @param {Object} [options] An object containing handler configuration properties.
     * This may contain any of the following properties (See {@link Ext.dom.Element#addListener}
     * for examples of how to use these options.):
     * @param {Object} options.scope The scope (`this` reference) in which the handler function
     * is executed. Defaults to the Element.
     * @param {String} options.delegate A simple selector to filter the target or look for
     * a descendant of the target. See {@link Ext.dom.Query} for information about simple selectors.
     * @param {Boolean} options.stopEvent True to stop the event. That is stop propagation,
     * and prevent the default action.
     * @param {Boolean} options.preventDefault True to prevent the default action
     * @param {Boolean} options.stopPropagation True to prevent event propagation
     * @param {Boolean} options.normalized False to pass a browser event to the handler function
     * instead of an Ext.event.Event
     * @param {Number} options.delay The number of milliseconds to delay the invocation of the
     * handler after te event fires.
     * @param {Boolean} options.single True to add a handler to handle just the next firing of the
     * event, and then remove itself.
     * @param {Number} options.buffer Causes the handler to be scheduled to run in an
     * {@link Ext.util.DelayedTask} delayed by the specified number of milliseconds. If the event
     * fires again within that time, the original handler is *not* invoked, but the new handler
     * is scheduled in its place.
     * @param {Ext.dom.Element} options.target Only call the handler if the event was fired
     * on the target Element, *not* if the event was bubbled up from a child node.
     * @param {Boolean} options.capture `true` to initiate capture which will fire the listeners
     * on the target Element *before* any descendant Elements. Normal events start with the target
     * element and propagate upward to ancestor elements, whereas captured events propagate
     * from the top of the DOM downward to descendant elements. This option is the same
     * as the useCapture parameter in the javascript addEventListener method.
     */
    addListener: function(element, eventName, fn, scope, options) {
        Ext.log.warn("Ext.EventManager is deprecated. " + "Use Ext.dom.Element#addListener to attach an event listener.");
        Ext.get(element).addListener(eventName, fn, scope, options);
    },
    /**
     * Adds a listener to be notified when the browser window is resized and provides resize event
     * buffering (100 milliseconds), passes new viewport width and height to handlers.
     * @param {Function} fn      The handler function the window resize event invokes.
     * @param {Object}   scope   The scope (<code>this</code> reference) in which the handler
     * function executes. Defaults to the browser window.
     * @param {Boolean}  [options] Options object as passed to {@link Ext.dom.Element#addListener}
     * @deprecated 5.0.0 Use {@link Ext#method-on Ext.on}('resize', fn) to attach a window resize
     * listener.
     */
    onWindowResize: function(fn, scope, options) {
        Ext.log.warn("Ext.EventManager is deprecated. " + "Use Ext.on('resize', fn) to attach a window resize listener.");
        Ext.GlobalEvents.on('resize', fn, scope, options);
    },
    /**
     * Adds a listener to be notified when the browser window is unloaded.
     * @param {Function} fn      The handler function the window unload event invokes.
     * @param {Object}   scope   The scope (<code>this</code> reference) in which the handler
     * function executes. Defaults to the browser window.
     * @param {Boolean}  options Options object as passed to {@link Ext.dom.Element#addListener}
     * @deprecated 5.0.0 This method is deprecated.
     */
    onWindowUnload: function(fn, scope, options) {
        Ext.log.warn("Ext.EventManager is deprecated. " + "Use Ext.getWin().on('unload', fn) to attach a window unload listener.");
        Ext.getWin().on('unload', fn, scope, options);
    },
    /**
     * Recursively removes all previous added listeners from an element and its children.
     * Typically you will use {@link Ext.dom.Element#clearListeners} directly on an Element
     * in favor of calling this method.
     * @param {String/Ext.dom.Element/HTMLElement/Window} element The id or html element from which
     * to remove all event handlers.
     * @param {String} eventName (optional) The name of the event.
     * @deprecated 5.0.0 This method is deprecated.
     */
    purgeElement: function(element, eventName) {
        Ext.log.warn("Ext.EventManager is deprecated. " + "Call clearListeners() on a Ext.dom.Element to remove all listeners.");
        Ext.get(element).clearListeners();
    },
    /**
     * Removes all event handers from an element.  Typically you will use {@link
     * Ext.dom.Element#clearListeners} directly on an Element in favor of calling this method.
     * @param {String/Ext.dom.Element/HTMLElement/Window} element The id or html element from which
     * to remove all event handlers.
     * @deprecated 5.0.0 This method is deprecated.
     */
    removeAll: function(element) {
        Ext.log.warn("Ext.EventManager is deprecated. " + "Call clearListeners() on a Ext.dom.Element to remove all listeners.");
        Ext.get(element).clearListeners();
    },
    /**
     * Removes an event handler from an element.  The shorthand version {@link #un} is equivalent.
     * Typically you will use {@link Ext.dom.Element#removeListener} directly on an Element in favor
     * of calling this version.
     *
     * {@link Ext.EventManager#on} is an alias for {@link Ext.EventManager#addListener}.
     *
     * @param {String/Ext.dom.Element/HTMLElement/Window} element The id or html element from which
     * to remove the listener.
     * @param {String} eventName The name of the event.
     * @param {Function} fn The handler function to remove. **This must be a reference to the
     * function passed into the {@link #addListener} call.**
     * @param {Object} [scope] If a scope (`this` reference) was specified when the listener
     * was added, then this must refer to the same object.
     * @param {Object} [options]
     */
    removeListener: function(element, eventName, fn, scope, options) {
        Ext.log.warn("Ext.EventManager is deprecated. " + "Use Ext.dom.Element#removeListener to remove an event listener.");
        Ext.get(element).removeListener(eventName, fn, scope, options);
    },
    /**
     * Removes the passed window resize listener.
     * @param {Function} fn        The method the event invokes
     * @param {Object}   [scope]  The scope of handler
     * @deprecated 5.0.0 This method is deprecated.
     */
    removeResizeListener: function(fn, scope) {
        Ext.log.warn("Ext.EventManager is deprecated. " + "Use Ext.on('resize', fn) to detach a window resize listener.");
        Ext.GlobalEvents.un('resize', fn, scope);
    },
    /**
     * Removes the passed window unload listener.
     * @param {Function} fn        The method the event invokes
     * @param {Object}   [scope]  The scope of handler
     * @deprecated 5.0.0 This method is deprecated.
     */
    removeUnloadListener: function(fn, scope) {
        Ext.log.warn("Ext.EventManager is deprecated. " + "Use Ext.getWin().un('unload', fn) to detach a window unload listener.");
        Ext.getWin().un('unload', fn, scope);
    },
    /**
     * Stop the event (preventDefault and stopPropagation)
     * @param {Event} event The event to stop
     * @deprecated 5.0.0 This method is deprecated.
     */
    stopEvent: function(event) {
        Ext.log.warn("Ext.EventManager.stopEvent() is deprecated. " + "Call stopEvent() directly on the Ext.event.Event instance instead.");
        this.stopPropagation(event);
        this.preventDefault(event);
    },
    /**
     * Cancels bubbling of the event.
     * @param {Event} event The event to stop bubbling.
     * @deprecated 5.0.0 This method is deprecated.
     */
    stopPropagation: function(event) {
        Ext.log.warn("Ext.EventManager.stopPropagation() is deprecated. " + "Call stopPropagation() directly on the Ext.event.Event instance instead.");
        event = event.browserEvent || event;
        if (event.stopPropagation) {
            event.stopPropagation();
        } else {
            event.cancelBubble = true;
        }
    },
    /**
     * Prevents the browsers default handling of the event.
     * @param {Event} event The event to prevent the default
     * @deprecated 5.0.0 This method is deprecated.
     */
    preventDefault: function(event) {
        Ext.log.warn("Ext.EventManager.preventDefault() is deprecated. " + "Call preventDefault() directly on the Ext.event.Event instance instead.");
        event = event.browserEvent || event;
        if (event.preventDefault) {
            event.preventDefault();
        } else {
            event.returnValue = false;
            // Some keys events require setting the keyCode to -1 to be prevented
            try {
                // all ctrl + X and F1 -> F12
                if (event.ctrlKey || event.keyCode > 111 && event.keyCode < 124) {
                    event.keyCode = -1;
                }
            } catch (e) {}
        }
    },
    // see this outdated document http://support.microsoft.com/kb/934364/en-us
    // for more info
    /**
     * Get the id of the element. If one has not been assigned, automatically assign it.
     * @param {HTMLElement/Ext.dom.Element} element The element to get the id for.
     * @return {String} id
     * @deprecated 5.0.0 This method is deprecated.
     */
    getId: function(element) {
        Ext.log.warn("Ext.EventManager.getId() is deprecated. " + "Call Ext.get() to assign ids to elements.");
        element = Ext.get(element);
        return element.id;
    },
    /**
     * Gets the related target from the event.
     * @param {Object} event The event
     * @return {HTMLElement} The related target.
     * @deprecated 5.0.0 This method is deprecated.
     */
    getRelatedTarget: function(event) {
        var target;
        Ext.log.warn("Ext.EventManager.getRelatedTarget() is deprecated. " + "Call getRelatedTarget() directly on the Ext.event.Event instance instead.");
        event = event.browserEvent || event;
        target = event.relatedTarget;
        if (!target) {
            if (this.mouseLeaveRe.test(event.type)) {
                target = event.toElement;
            } else if (this.mouseEnterRe.test(event.type)) {
                target = event.fromElement;
            }
        }
        return this.resolveTextNode(target);
    },
    /**
     * Gets the x coordinate from the event
     * @param {Object} event The event
     * @return {Number} The x coordinate
     * @deprecated 5.0.0 This method is deprecated.
     */
    getPageX: function(event) {
        Ext.log.warn("Ext.EventManager.getPageX() is deprecated. " + "Call getX() directly on the Ext.event.Event instance instead.");
        return this.getPageXY(event)[0];
    },
    /**
     * Gets the x & y coordinate from the event
     * @param {Object} event The event
     * @return {Number[]} The x/y coordinate
     * @deprecated 5.0.0 This method is deprecated.
     */
    getPageXY: function(event) {
        Ext.log.warn("Ext.EventManager.getPageXY() is deprecated. " + "Call getXY() directly on the Ext.event.Event instance instead.");
        event = event.browserEvent || event;
        // eslint-disable-next-line vars-on-top
        var x = event.pageX,
            y = event.pageY,
            docEl = document.documentElement,
            body = document.body;
        // pageX/pageY not available (undefined, not null), use clientX/clientY instead
        if (!x && x !== 0) {
            x = event.clientX + (docEl && docEl.scrollLeft || body && body.scrollLeft || 0) - (docEl && docEl.clientLeft || body && body.clientLeft || 0);
            y = event.clientY + (docEl && docEl.scrollTop || body && body.scrollTop || 0) - (docEl && docEl.clientTop || body && body.clientTop || 0);
        }
        return [
            x,
            y
        ];
    },
    /**
     * Gets the y coordinate from the event
     * @param {Object} event The event
     * @return {Number} The y coordinate
     * @deprecated 5.0.0 This method is deprecated.
     */
    getPageY: function(event) {
        Ext.log.warn("Ext.EventManager.getPageY() is deprecated. " + "Call getY() directly on the Ext.event.Event instance instead.");
        return this.getPageXY(event)[1];
    },
    /**
     * Gets the target of the event.
     * @param {Object} event The event
     * @return {HTMLElement} target
     * @deprecated 5.0.0 This method is deprecated.
     */
    getTarget: function(event) {
        Ext.log.warn("Ext.EventManager.getTarget() is deprecated. " + "Call getTarget() directly on the Ext.event.Event instance instead.");
        event = event.browserEvent || event;
        return Ext.EventManager.resolveTextNode(event.target || event.srcElement);
    },
    // technically no need to browser sniff this, however it makes
    // no sense to check this every time, for every event, whether
    // the string is equal.
    /**
     * @method
     * Resolve any text nodes accounting for browser differences.
     * @private
     * @param {HTMLElement} node The node
     * @return {HTMLElement} The resolved node
     * @deprecated 5.0.0 This method is deprecated.
     */
    resolveTextNode: Ext.isGecko ? function(node) {
        var s;
        if (node) {
            // work around firefox bug, https://bugzilla.mozilla.org/show_bug.cgi?id=101197
            s = HTMLElement.prototype.toString.call(node);
            if (s !== '[xpconnect wrapped native prototype]' && s !== '[object XULElement]') {
                return node.nodeType === 3 ? node.parentNode : node;
            }
        }
    } : function(node) {
        return node && node.nodeType === 3 ? node.parentNode : node;
    }
}, function(EventManager) {
    /**
     * @method on
     * @member Ext.EventManager
     * @inheritdoc Ext.EventManager#method-addListener
     */
    EventManager.on = EventManager.addListener;
    /**
     * @method un
     * @member Ext.EventManager
     * @inheritdoc Ext.EventManager#method-removeListener
     */
    EventManager.un = EventManager.removeListener;
});

/**
 * @class Ext.Gadget
 * @extend Ext.Widget
 *
 * `Ext.Gadget` is not actually a class but simply an alternate class name for `Ext.Widget`. It
 * is used primarily for writing cross-toolkit widgets.
 *
 * In the modern toolkit, `Ext.Gadget` is an alternate class name for Ext.Component in order
 * to allow gadgets to be used as `items` in an Ext.Container.
 */

/**
 * Simple helper class for easily creating image components. This renders an image tag to
 * the DOM with the configured src.
 *
 * {@img Ext.Img/Ext.Img.png Ext.Img component}
 *
 * ## Example usage:
 *
 *     var changingImage = Ext.create('Ext.Img', {
 *         src: 'http://www.sencha.com/img/20110215-feat-html5.png',
 *         width: 184,
 *         height: 90,
 *         renderTo: Ext.getBody()
 *     });
 *
 *     // change the src of the image programmatically
 *     changingImage.setSrc('http://www.sencha.com/img/20110215-feat-perf.png');
 *
 * By default, only an img element is rendered and that is this component's primary
 * {@link Ext.Component#getEl element}. If the {@link Ext.Component#autoEl} property
 * is other than 'img' (the default), the a child img element will be added to the primary
 * element. This can be used to create a wrapper element around the img.
 *
 * ## Wrapping the img in a div:
 *
 *     var wrappedImage = Ext.create('Ext.Img', {
 *         src: 'http://www.sencha.com/img/20110215-feat-html5.png',
 *         autoEl: 'div', // wrap in a div
 *         renderTo: Ext.getBody()
 *     });
 *
 * ## Using a glyph
 *
 *     var glyphImage = Ext.create('Ext.Img', {
 *         glyph: 'xf015@FontAwesome',     // the "home" icon
 *         renderTo: Ext.getBody()
 *     });
 *
 * ## Image Dimensions
 *
 * You should include height and width dimensions for any image owned by a parent 
 * container.  By omitting dimensions, an owning container will not know how to 
 * size and position the image in the initial layout.
 */
Ext.define('Ext.Img', {
    extend: 'Ext.Component',
    alias: [
        'widget.image',
        'widget.imagecomponent'
    ],
    requires: [
        'Ext.Glyph'
    ],
    /**
     * @cfg autoEl
     * @inheritdoc
     */
    autoEl: 'img',
    /**
     * @cfg baseCls
     * @inheritdoc
     */
    baseCls: Ext.baseCSSPrefix + 'img',
    config: {
        /**
         * @cfg {String} src
         * The source of this image. See {@link Ext#resolveResource} for details on
         * locating application resources.
         * @accessor
         */
        src: null,
        /**
         * @cfg glyphorig
         * @inheritdoc Ext.panel.Header#cfg-glyph
         */
        /**
         * @cfg {Number/String} glyph
         * A numeric unicode character code to serve as the image.  If this option is used
         * The image will be rendered using a div with innerHTML set to the html entity
         * for the given character code.  The default font-family for glyphs can be set
         * globally using {@link Ext#setGlyphFontFamily Ext.setGlyphFontFamily()}. Alternatively,
         * this config option accepts a string with the charCode and font-family separated by
         * the `@` symbol. For example '65@My Font Family'.
         */
        glyph: null
    },
    /**
     * @cfg {String} alt
     * The descriptive text for non-visual UI description.
     */
    alt: '',
    /**
     * @cfg {String} title
     * Specifies additional information about the image.
     */
    title: '',
    /**
     * @cfg {String} imgCls
     * Optional CSS classes to add to the img element.
     */
    imgCls: '',
    /**
     * @property maskOnDisable
     * @inheritdoc
     */
    maskOnDisable: false,
    applySrc: function(src) {
        return src && Ext.resolveResource(src);
    },
    getElConfig: function() {
        var me = this,
            autoEl = me.autoEl,
            config = me.callParent(),
            glyph = me.glyph,
            img;
        // We were configured with a glyph, then this is a div with a single char content
        if (glyph) {
            config.tag = 'div';
            config.html = glyph.character;
            config.style = config.style || {};
            config.style.fontFamily = glyph.fontFamily;
            // A glyph is a graphic which is not an <img> tag so it should have
            // the corresponding role for Accessibility interface to recognize
            config.role = 'img';
        }
        // The default; an img element
        else if (autoEl === 'img' || (Ext.isObject(autoEl) && autoEl.tag === 'img')) {
            img = config;
        } else // It is sometimes helpful (like in a panel header icon) to have the img wrapped
        // by a div. If our autoEl is not 'img' then we just add an img child to the el.
        {
            config.cn = [
                img = {
                    tag: 'img',
                    id: me.id + '-img'
                }
            ];
        }
        if (img) {
            if (me.imgCls) {
                img.cls = (img.cls ? img.cls + ' ' : '') + me.imgCls;
            }
            img.src = me.src || Ext.BLANK_IMAGE_URL;
        }
        if (me.alt) {
            (img || config).alt = me.alt;
        } else {
            // Images that do not have alt attribute can't be properly announced
            // by screen readers. In best case they will be silently skipped;
            // in worst case screen reader will announce data url. Yes, that very long
            // base-64 encoded string. :/
            // That will make the application totally unusable for blind people.
            (img || config).alt = '';
            Ext.log.warn('For WAI-ARIA compliance, IMG elements SHOULD have an alt attribute.');
        }
        if (me.title) {
            (img || config).title = me.title;
        }
        return config;
    },
    onRender: function() {
        var me = this,
            autoEl = me.autoEl,
            el;
        me.callParent(arguments);
        el = me.el;
        if (autoEl === 'img' || (Ext.isObject(autoEl) && autoEl.tag === 'img')) {
            me.imgEl = el;
        } else {
            me.imgEl = el.getById(me.id + '-img');
        }
    },
    doDestroy: function() {
        var me = this,
            imgEl = me.imgEl;
        // Only clean up when the img is a child, otherwise it will get handled
        // by the element destruction in the parent
        if (imgEl && me.el !== imgEl) {
            imgEl.destroy();
        }
        me.imgEl = null;
        me.callParent();
    },
    getTitle: function() {
        return this.title;
    },
    /**
     * Updates the {@link #title} of the image.
     * @param {String} title
     */
    setTitle: function(title) {
        var me = this,
            imgEl = me.imgEl;
        me.title = title || '';
        if (imgEl) {
            imgEl.dom.title = title || '';
        }
    },
    afterComponentLayout: function(width, height, oldWidth, oldHeight) {
        var heightModel = this.getSizeModel().height,
            h;
        // If we have our height set, then size the glyph as requested to make image scalable.
        if ((heightModel.calculated || heightModel.configured) && height && this.glyph) {
            h = height + 'px';
            this.setStyle({
                'line-height': h,
                'font-size': h
            });
        }
        this.callParent([
            width,
            height,
            oldWidth,
            oldHeight
        ]);
    },
    getAlt: function() {
        return this.alt;
    },
    /**
     * Updates the {@link #alt} of the image.
     * @param {String} alt
     */
    setAlt: function(alt) {
        var me = this,
            imgEl = me.imgEl;
        me.alt = alt || '';
        if (imgEl) {
            imgEl.dom.alt = alt || '';
        }
    },
    _naturalSize: null,
    /**
     * Returns the size of the image as an object.
     * @return {Object} The size and aspect ratio of the image.
     * @return {Number} return.aspect The aspect ration of the image (`width / height`).
     * @return {Number} return.height The height of the image.
     * @return {Number} return.width The width of the image.
     * @since 6.2.0
     */
    getNaturalSize: function() {
        var me = this,
            img = me.imgEl,
            naturalSize = me._naturalSize,
            style, w, h;
        if (img && !naturalSize) {
            img = img.dom;
            me._naturalSize = naturalSize = {
                width: w = img.naturalWidth,
                height: img.naturalHeight
            };
            if (!w) {
                style = img.style;
                w = style.width;
                h = style.height;
                // As long as the width/height styles are "auto", the IMG dom element
                // will have "width" and "height" properties that are the natural size.
                style.width = style.height = 'auto';
                naturalSize.width = img.width;
                naturalSize.height = img.height;
                style.width = w;
                style.height = h;
            }
            naturalSize.aspect = naturalSize.width / naturalSize.height;
        }
        return naturalSize;
    },
    updateSrc: function(src) {
        var imgEl = this.imgEl;
        if (imgEl) {
            imgEl.dom.src = src || Ext.BLANK_IMAGE_URL;
        }
    },
    applyGlyph: function(glyph, oldGlyph) {
        if (glyph) {
            if (!glyph.isGlyph) {
                glyph = new Ext.Glyph(glyph);
            }
            if (glyph.isEqual(oldGlyph)) {
                glyph = undefined;
            }
        }
        return glyph;
    },
    updateGlyph: function(glyph, oldGlyph) {
        var el = this.el;
        if (el) {
            el.dom.innerHTML = glyph.character;
            el.setStyle(glyph.getStyle());
        }
    }
});

/**
 * This class is used as a mixin.
 *
 * This class is to be used to provide basic methods for binding/unbinding stores to other
 * classes.
 *
 * This class is not intended for direct use but rather internally by those classes that
 * manage a Store.
 * @private
 */
Ext.define('Ext.util.StoreHolder', {
    requires: [
        'Ext.data.StoreManager'
    ],
    mixinId: 'storeholder',
    /**
     * @property {Boolean} [autoDestroyBoundStore] This property allows the object
     * to destroy bound stores that have {@link Ext.data.AbstractStore#autoDestroy}
     * option set to `true`. 
     */
    autoDestroyBoundStore: false,
    /**
     * Binds a store to this instance.
     * @param {Ext.data.AbstractStore/String} [store] The store to bind or ID of the store.
     * When no store given (or when `null` or `undefined` passed), unbinds the existing store.
     * @param initial
     * @param propertyName
     */
    bindStore: function(store, initial, propertyName) {
        var me = this,
            oldStore;
        // Private params
        // @param {Boolean} [initial=false] True to not remove listeners from existing store.
        // @param {String} [propertyName="store"] The property in this object under which
        // to cache the passed Store.
        propertyName = propertyName || 'store';
        oldStore = initial ? null : me[propertyName];
        if (store !== oldStore) {
            if (oldStore) {
                // Perform implementation-specific unbinding operations *before*
                // possible Store destruction.
                if (!me.onUnbindStore.$emptyFn) {
                    me.onUnbindStore(oldStore, initial, propertyName);
                }
                // autoDestroy is only intended for when it is unbound from a component,
                // and the store could have been already destroyed upstream
                if (!oldStore.destroyed) {
                    if (me.autoDestroyBoundStore && propertyName === 'store' && oldStore.autoDestroy) {
                        oldStore.destroy();
                    } else {
                        me.unbindStoreListeners(oldStore);
                    }
                }
            }
            if (store) {
                me[propertyName] = store = Ext.data.StoreManager.lookup(store);
                me.bindStoreListeners(store);
                if (!me.onBindStore.$emptyFn) {
                    me.onBindStore(store, oldStore, initial);
                }
            } else {
                me[propertyName] = null;
            }
            if (me.fireEvent) {
                me.fireEvent('storechange', me, store, oldStore);
            }
        }
        return me;
    },
    /**
     * Gets the current store instance.
     * @return {Ext.data.AbstractStore} The store, null if one does not exist.
     */
    getStore: function() {
        return this.store;
    },
    /**
     * Sets the store to the specified store.
     * @param store
     * @since 5.0.0
     */
    setStore: function(store) {
        this.bindStore(store);
    },
    /**
     * Unbinds listeners from this component to the store. By default it will remove
     * anything bound by the bindStoreListeners method, however it can be overridden
     * in a subclass to provide any more complicated handling.
     * @protected
     * @param {Ext.data.AbstractStore} store The store to unbind from
     */
    unbindStoreListeners: function(store) {
        // Can be overridden in the subclass for more complex removal
        var listeners = this.storeListeners;
        if (listeners) {
            store.un(listeners);
        }
    },
    /**
     * Binds listeners for this component to the store. By default it will add
     * anything bound by the getStoreListeners method, however it can be overridden
     * in a subclass to provide any more complicated handling.
     * @protected
     * @param {Ext.data.AbstractStore} store The store to bind to
     */
    bindStoreListeners: function(store) {
        // Can be overridden in the subclass for more complex binding
        var listeners = this.getStoreListeners(store);
        if (listeners) {
            listeners = Ext.apply({}, listeners);
            if (!listeners.scope) {
                listeners.scope = this;
            }
            this.storeListeners = listeners;
            store.on(listeners);
        }
    },
    /**
     * @method
     * Gets the listeners to bind to a new store.
     * @protected
     * @param {Ext.data.Store} store The Store which is being bound to for which a listeners object
     * should be returned.
     * @return {Object} The listeners to be bound to the store in object literal form. The scope
     * may be omitted, it is assumed to be the current instance.
     */
    getStoreListeners: Ext.emptyFn,
    /**
     * @method
     * Template method, it is called when an existing store is unbound
     * from the current instance.
     * @protected
     * @param {Ext.data.AbstractStore} store The store being unbound
     * @param {Boolean} initial True if this store is being bound as initialization of the instance.
     */
    onUnbindStore: Ext.emptyFn,
    /**
     * @method
     * Template method, it is called when a new store is bound
     * to the current instance.
     * @protected
     * @param {Ext.data.AbstractStore} store The store being bound
     * @param {Boolean} initial True if this store is being bound as initialization of the instance.
     */
    onBindStore: Ext.emptyFn
});

/**
 * A modal, floating Component which may be shown above a specified {@link Ext.Component Component}
 * while loading data. When shown, the configured owning Component will be covered with a modality
 * mask, and the LoadMask's {@link #msg} will be displayed centered, accompanied by a spinner image.
 *
 * If the {@link #store} config option is specified, the masking will be automatically shown
 * and then hidden synchronized with the Store's loading process.
 *
 * Because this is a floating Component, its z-index will be managed by the global
 * {@link Ext.WindowManager ZIndexManager} object, and upon show, it will place itsef at the top
 * of the hierarchy.
 *
 * Example usage:
 *
 *     @example
 *     var myPanel = new Ext.panel.Panel({
 *         renderTo : document.body,
 *         height   : 100,
 *         width    : 200,
 *         title    : 'Foo'
 *     });
 *
 *     var myMask = new Ext.LoadMask({
 *         msg    : 'Please wait...',
 *         target : myPanel
 *     });
 *
 *     myMask.show();
 */
Ext.define('Ext.LoadMask', {
    extend: 'Ext.Component',
    alias: 'widget.loadmask',
    mixins: [
        'Ext.util.StoreHolder'
    ],
    uses: [
        'Ext.data.StoreManager'
    ],
    /**
     * @property {Boolean} isLoadMask
     * `true` in this class to identify an object as an instantiated LoadMask, or subclass thereof.
     */
    isLoadMask: true,
    /**
     * @cfg {Ext.Component} target The Component you wish to mask. The the mask will be
     * automatically sized upon Component resize, and the message box will be kept centered.
     */
    /**
     * @cfg {Ext.data.Store} store
     * Optional Store to which the mask is bound. The mask is displayed when a load request
     * is issued, and hidden on either load success, or load fail.
     */
    /**
     * @cfg {String} [msg="Loading..."]
     * The text to display in a centered loading message box.
     * @locale
     */
    msg: 'Loading...',
    msgCls: Ext.baseCSSPrefix + 'mask-loading',
    msgWrapCls: Ext.baseCSSPrefix + 'mask-msg',
    /**
     * @cfg {Boolean} [useMsg=true]
     * Whether or not to use a loading message class or simply mask the bound element.
     */
    useMsg: true,
    /**
     * @cfg {Boolean} [useTargetEl=false]
     * True to mask the {@link Ext.Component#getTargetEl targetEl} of the bound Component.
     * By default, the {@link Ext.Component#getEl el} will be masked.
     */
    useTargetEl: false,
    /**
     * @cfg {Boolean} shim `true` to enable an iframe shim for this LoadMask to keep
     * windowed objects from showing through.
     */
    /**
     * @private
     */
    cls: Ext.baseCSSPrefix + 'mask',
    componentCls: Ext.baseCSSPrefix + 'border-box',
    ariaRole: 'progressbar',
    focusable: true,
    tabIndex: 0,
    childEls: [
        'msgWrapEl',
        'msgEl',
        'msgTextEl'
    ],
    /* eslint-disable indent, max-len */
    renderTpl: [
        '<div id="{id}-msgWrapEl" data-ref="msgWrapEl" class="{[values.$comp.msgWrapCls]}" role="presentation">',
        '<div id="{id}-msgEl" data-ref="msgEl" class="{[values.$comp.msgCls]} ',
        Ext.baseCSSPrefix,
        'mask-msg-inner {childElCls}" role="presentation">',
        '<div id="{id}-msgTextEl" data-ref="msgTextEl" class="',
        Ext.baseCSSPrefix,
        'mask-msg-text',
        '{childElCls}" role="presentation">{msg}</div>',
        '</div>',
        '</div>'
    ],
    /* eslint-enable indent, max-len */
    maskOnDisable: false,
    /**
     * @private
     */
    skipLayout: true,
    /**
     * Creates new LoadMask.
     * @param {Object} [config] The config object.
     */
    constructor: function(config) {
        var me = this,
            comp;
        if (arguments.length === 2) {
            if (Ext.isDefined(Ext.global.console)) {
                Ext.global.console.warn('Ext.LoadMask: LoadMask now uses a standard 1 arg constructor: ' + 'use the target config');
            }
            comp = me.target = config;
            config = arguments[1];
        } else {
            comp = config.target;
        }
        if (config.maskCls) {
            Ext.log.warn('Ext.LoadMask property maskCls is deprecated, use msgWrapCls instead');
            config.msgWrapCls = config.msgWrapCls || config.maskCls;
        }
        // Must apply configs early so that renderTo can be calculated correctly.
        me.callParent([
            config
        ]);
        // Target is a Component
        if (comp.isComponent) {
            me.ownerCt = comp;
            me.hidden = true;
            // Ask the component which element should be masked.
            // Most will not have an answer, in which case this returns the document body
            // Ext.view.Table for example returns the el of its owning Panel.
            me.renderTo = me.getMaskTarget();
            me.external = me.renderTo === Ext.getBody();
            me.bindComponent(comp);
        } else // Element support to be deprecated
        {
            if (Ext.isDefined(Ext.global.console)) {
                Ext.global.console.warn('Ext.LoadMask: LoadMask for elements has been deprecated, ' + 'use Ext.dom.Element.mask & Ext.dom.Element.unmask');
            }
            comp = Ext.get(comp);
            me.isElement = true;
            me.renderTo = me.target;
        }
        me.render(me.renderTo);
        if (me.store) {
            me.bindStore(me.store, true);
        }
    },
    initRenderData: function() {
        var result = this.callParent(arguments);
        result.msg = this.msg || '';
        return result;
    },
    onRender: function() {
        this.callParent(arguments);
        // In versions prior to 5.1, maskEl was rendered outside of the
        // LoadMask's main el and had a reference to it; we keep this
        // reference for backwards compatibility.
        this.maskEl = this.el;
    },
    bindComponent: function(comp) {
        var me = this,
            listeners = {
                scope: this,
                resize: me.sizeMask
            };
        if (me.external) {
            listeners.added = me.onComponentAdded;
            listeners.removed = me.onComponentRemoved;
            if (comp.floating) {
                listeners.move = me.sizeMask;
                me.activeOwner = comp;
            } else if (comp.ownerCt) {
                me.onComponentAdded(comp.ownerCt);
            }
        }
        me.mon(comp, listeners);
        // Subscribe to the observer that manages the hierarchy
        // Only needed if we had to be rendered outside of the target
        if (me.external) {
            me.mon(Ext.GlobalEvents, {
                show: me.onContainerShow,
                hide: me.onContainerHide,
                expand: me.onContainerExpand,
                collapse: me.onContainerCollapse,
                scope: me
            });
        }
    },
    onComponentAdded: function(owner) {
        var me = this;
        delete me.activeOwner;
        me.floatParent = owner;
        if (!owner.floating) {
            owner = owner.up('[floating]');
        }
        if (owner) {
            me.activeOwner = owner;
            me.mon(owner, 'move', me.sizeMask, me);
            me.mon(owner, 'tofront', me.onOwnerToFront, me);
        } else {
            me.preventBringToFront = true;
        }
        owner = me.floatParent.ownerCt;
        if (me.rendered && me.isVisible() && owner) {
            me.floatOwner = owner;
            me.mon(owner, 'afterlayout', me.sizeMask, me, {
                single: true
            });
        }
    },
    onComponentRemoved: function(owner) {
        var me = this,
            activeOwner = me.activeOwner,
            floatOwner = me.floatOwner;
        if (activeOwner) {
            me.mun(activeOwner, 'move', me.sizeMask, me);
            me.mun(activeOwner, 'tofront', me.onOwnerToFront, me);
        }
        if (floatOwner) {
            me.mun(floatOwner, 'afterlayout', me.sizeMask, me);
        }
        delete me.activeOwner;
        delete me.floatOwner;
    },
    afterRender: function() {
        var me = this;
        me.callParent(arguments);
        // In IE8-11, clicking on an inner msgEl will focus it, despite
        // it having no tabindex attribute and thus being canonically
        // non-focusable. Placing unselectable="on" attribute will make
        // it unfocusable but will also prevent clicks from focusing
        // the parent element. We want clicks within the mask's main el
        // to focus it, hence the workaround.
        if (Ext.isIE) {
            me.el.on('mousedown', me.onMouseDown, me);
        }
        // This LoadMask shares the DOM and may be tipped out by the use of innerHTML
        // Ensure the element does not get garbage collected from under us.
        this.el.skipGarbageCollection = true;
    },
    onMouseDown: function(e) {
        var el = this.el;
        if (e.within(el)) {
            e.preventDefault();
            el.focus();
        }
    },
    onOwnerToFront: function(owner, zIndex) {
        this.el.setStyle('zIndex', zIndex + 1);
    },
    // Only called if we are rendered external to the target.
    // Best we can do is show.
    onContainerShow: function(container) {
        if (!this.isHierarchicallyHidden()) {
            this.onComponentShow();
        }
    },
    // Only called if we are rendered external to the target.
    // Best we can do is hide.
    onContainerHide: function(container) {
        if (this.isHierarchicallyHidden()) {
            this.onComponentHide();
        }
    },
    // Only called if we are rendered external to the target.
    // Best we can do is show.
    onContainerExpand: function(container) {
        if (!this.isHierarchicallyHidden()) {
            this.onComponentShow();
        }
    },
    // Only called if we are rendered external to the target.
    // Best we can do is hide.
    onContainerCollapse: function(container) {
        if (this.isHierarchicallyHidden()) {
            this.onComponentHide();
        }
    },
    onComponentHide: function() {
        var me = this;
        if (me.rendered && me.isVisible()) {
            me.hide();
            me.showNext = true;
        }
    },
    onComponentShow: function() {
        if (this.showNext) {
            this.show();
        }
        delete this.showNext;
    },
    /**
     * @private
     * Called when this LoadMask's Component is resized. The toFront method rebases and resizes
     * the modal mask.
     */
    sizeMask: function() {
        var me = this,
            // Need to use the closest floating component (if it exists) as the basis
            // for our z-index positioning
            target = me.activeOwner || me.target,
            boxTarget = me.external ? me.getOwner().el : me.getMaskTarget(),
            zIndex;
        if (me.rendered && me.isVisible()) {
            // Only need to move and size the message wrap if we are outside of
            // the masked element.
            // If we are inside, it will be left:0;top:0;width:100%;height:100% by default
            if (me.external) {
                if (!me.isElement && target.floating) {
                    zIndex = target.el.getZIndex();
                    if (!isNaN(zIndex)) {
                        me.onOwnerToFront(target, zIndex);
                    }
                }
                me.el.setSize(boxTarget.getSize()).alignTo(boxTarget, 'tl-tl');
            }
            // Always need to center the message wrap
            me.msgWrapEl.center(me.el);
        }
    },
    /**
     * Changes the data store bound to this LoadMask.
     * @param {Ext.data.Store} store The store to bind to this LoadMask
     * @param [initial]
     */
    bindStore: function(store, initial) {
        var me = this;
        // If the server returns a failure, and the proxy fires an exception instead of
        // loading the store, the mask must clear.
        Ext.destroy(me.proxyListeners);
        me.mixins.storeholder.bindStore.apply(me, arguments);
        store = me.store;
        if (store) {
            // Skip ChainedStores to the store that does the loading
            while (store.getSource) {
                store = store.getSource();
            }
            if (!store.loadsSynchronously()) {
                me.proxyListeners = store.getProxy().on({
                    exception: me.onLoad,
                    scope: me,
                    destroyable: true
                });
            }
            if (store.isLoading()) {
                me.onBeforeLoad();
            }
        }
    },
    getStoreListeners: function(store) {
        var onLoad = this.onLoad,
            beforeLoad = this.onBeforeLoad,
            result = {
                // Fired when a range is requested for rendering that is not in the cache
                cachemiss: beforeLoad,
                // Fired when a range for rendering which was previously missing from the cache
                // is loaded. buffer so that scrolling and store filling has settled,
                // and the results have been rendered.
                cachefilled: {
                    fn: onLoad,
                    buffer: 100
                }
            };
        // Only need to mask on load if the proxy is asynchronous - ie: Ajax/JsonP
        if (!store.loadsSynchronously()) {
            result.beforeload = beforeLoad;
            result.load = onLoad;
        }
        return result;
    },
    onDisable: function() {
        this.callParent(arguments);
        if (this.loading) {
            this.onLoad();
        }
    },
    getOwner: function() {
        return this.ownerCt || this.ownerCmp || this.floatParent;
    },
    getMaskTarget: function() {
        var owner = this.getOwner();
        if (this.isElement) {
            return this.target;
        }
        return this.useTargetEl ? owner.getTargetEl() : (owner.getMaskTarget() || Ext.getBody());
    },
    /**
     * @private
     */
    onBeforeLoad: function() {
        var me = this,
            owner = me.getOwner(),
            origin;
        if (!me.disabled) {
            me.loading = true;
            // If the owning Component has not been laid out, defer so that the ZIndexManager
            // gets to read its laid out size when sizing the modal mask
            if (owner.componentLayoutCounter) {
                me.maybeShow();
            } else {
                // The code below is a 'run-once' interceptor.
                origin = owner.afterComponentLayout;
                owner.afterComponentLayout = function() {
                    owner.afterComponentLayout = origin;
                    origin.apply(owner, arguments);
                    me.maybeShow();
                };
            }
        }
    },
    maybeShow: function() {
        var me = this,
            owner = me.getOwner(),
            ownerVisible;
        // Owner could be detached
        ownerVisible = owner.isVisible(true) && (!me.isComponent || owner.el.isVisible(true));
        if (!ownerVisible) {
            me.showNext = true;
        } else if (me.loading && owner.rendered) {
            me.show();
        }
    },
    hide: function() {
        var me = this,
            ownerCt = me.ownerCt;
        me.target.removeCls(Ext.baseCSSPrefix + "masked");
        // Element support to be deprecated
        if (me.isElement) {
            ownerCt.unmask();
            me.fireEvent('hide', this);
            return;
        }
        // Could be already nulled while destroying
        if (ownerCt) {
            ownerCt.updateMaskState(false, me);
        }
        delete me.showNext;
        return me.callParent(arguments);
    },
    show: function() {
        var me = this;
        me.target.addCls(Ext.baseCSSPrefix + "masked");
        // Element support to be deprecated
        if (me.isElement) {
            me.ownerCt.mask(this.useMsg ? this.msg : '', this.msgCls);
            me.fireEvent('show', this);
            return;
        }
        return me.callParent(arguments);
    },
    afterShow: function() {
        var me = this,
            ownerCt = me.ownerCt;
        me.loading = true;
        me.callParent(arguments);
        ownerCt.updateMaskState(true, me);
        // Owner's disabled tabbing will also make the mask
        // untabbable since it is rendered within the target
        me.el.restoreTabbableState();
        me.syncMaskState();
    },
    /**
     * Synchronizes the visible state of the mask with the configuration settings such
     * as {@link #msgWrapCls}, {@link #msg}, sizes the mask to occlude the target element
     * or Component and focuses the mask.
     * @private
     */
    syncMaskState: function() {
        var me = this,
            ownerCt = me.ownerCt,
            el = me.el;
        if (me.isVisible()) {
            // Allow dynamic setting of msgWrapCls
            if (me.hasOwnProperty('msgWrapCls')) {
                el.dom.className = me.msgWrapCls;
            }
            if (me.useMsg) {
                me.msgTextEl.setHtml(me.msg);
                me.ariaEl.dom.setAttribute('aria-valuetext', me.msg);
            } else {
                // Only the mask is visible if useMsg is false
                me.msgWrapEl.hide();
            }
            if (me.shim || Ext.useShims) {
                el.enableShim(null, true);
            } else {
                // Just in case me.shim was changed since last time we were shown (by
                // Component#setLoading())
                el.disableShim();
            }
            // If owner contains focus, focus this.
            // Component level onHide processing takes care of focus reversion on hide.
            if (ownerCt.el.contains(Ext.Element.getActiveElement())) {
                me.focus();
            }
            me.sizeMask();
        }
    },
    /**
     * @private
     */
    onLoad: function() {
        this.loading = false;
        this.hide();
    },
    doDestroy: function() {
        var me = this;
        // We don't have a real ownerCt, so clear it out here to prevent
        // spurious warnings when we are destroyed
        me.ownerCt = null;
        me.bindStore(null);
        if (me.isElement) {
            me.ownerCt.unmask();
        }
        me.callParent();
    }
});

/**
 * This class is intended to be extended or created via the
 * {@link Ext.Component#componentLayout layout} configuration property.
 * See {@link Ext.Component#componentLayout} for additional details.
 * @private
 */
Ext.define('Ext.layout.component.Component', {
    extend: 'Ext.layout.Layout',
    type: 'component',
    isComponentLayout: true,
    nullBox: {},
    usesContentHeight: true,
    usesContentWidth: true,
    usesHeight: true,
    usesWidth: true,
    widthCache: {},
    heightCache: {},
    beginLayoutCycle: function(ownerContext, firstCycle) {
        var me = this,
            owner = me.owner,
            ownerCtContext = ownerContext.ownerCtContext,
            heightModel = ownerContext.heightModel,
            widthModel = ownerContext.widthModel,
            body = owner.el.dom === document.body,
            lastBox = owner.lastBox || me.nullBox,
            lastSize = owner.el.lastBox || me.nullBox,
            dirty = !body,
            isTopLevel = ownerContext.isTopLevel,
            ownerLayout, v, width, height, scroller;
        me.callParent([
            ownerContext,
            firstCycle
        ]);
        if (firstCycle) {
            scroller = owner.getScrollable && owner.getScrollable();
            if (scroller) {
                scroller.flushOnDomScrollEnd();
            }
            if (me.usesContentWidth) {
                ++ownerContext.consumersContentWidth;
            }
            if (me.usesContentHeight) {
                ++ownerContext.consumersContentHeight;
            }
            if (me.usesWidth) {
                ++ownerContext.consumersWidth;
            }
            if (me.usesHeight) {
                ++ownerContext.consumersHeight;
            }
            if (ownerCtContext && !ownerCtContext.hasRawContent) {
                ownerLayout = owner.ownerLayout;
                if (ownerLayout) {
                    if (ownerLayout.usesWidth) {
                        ++ownerContext.consumersWidth;
                    }
                    if (ownerLayout.usesHeight) {
                        ++ownerContext.consumersHeight;
                    }
                }
            }
        }
        // we want to publish configured dimensions as early as possible and since this is
        // a write phase...
        if (widthModel.configured) {
            // If the owner.el is the body, owner.width is not dirty (we don't want to write
            // it to the body el). For other el's, the width may already be correct in the
            // DOM (e.g., it is rendered in the markup initially). If the width is not
            // correct in the DOM, this is only going to be the case on the first cycle.
            width = owner[widthModel.names.width];
            if (isTopLevel && widthModel.calculatedFrom) {
                width = lastBox.width;
            }
            if (!body) {
                dirty = me.setWidthInDom || (firstCycle ? width !== lastSize.width : widthModel.constrained);
            }
            ownerContext.setWidth(width, dirty);
        } else if (isTopLevel) {
            if (widthModel.calculated) {
                v = lastBox.width;
                ownerContext.setWidth(v, /* dirty= */
                v !== lastSize.width);
            } else if (widthModel.calculatedFromNatural) {
                owner.el.dom.style.width = owner.width;
            }
            v = lastBox.x;
            ownerContext.setProp('x', v, /* dirty= */
            v !== lastSize.x);
        }
        if (heightModel.configured) {
            height = owner[heightModel.names.height];
            if (isTopLevel && heightModel.calculatedFrom) {
                height = lastBox.height;
            }
            if (!body) {
                dirty = firstCycle ? height !== lastSize.height : heightModel.constrained;
            }
            ownerContext.setHeight(height, dirty);
        } else if (isTopLevel) {
            if (heightModel.calculated) {
                v = lastBox.height;
                ownerContext.setHeight(v, v !== lastSize.height);
            } else if (heightModel.calculatedFromNatural) {
                owner.el.dom.style.height = owner.height;
            }
            v = lastBox.y;
            ownerContext.setProp('y', v, /* dirty= */
            v !== lastSize.y);
        }
    },
    finishedLayout: function(ownerContext) {
        var me = this,
            elementChildren = ownerContext.children,
            owner = me.owner,
            len, i, elContext, lastBox, props;
        // NOTE: In the code below we cannot use getProp because that will generate
        // a layout dependency
        // Set lastBox on managed child Elements.
        // So that ContextItem.constructor can snag the lastBox for use by its undo method.
        if (elementChildren) {
            len = elementChildren.length;
            for (i = 0; i < len; i++) {
                elContext = elementChildren[i];
                elContext.el.lastBox = elContext.props;
            }
        }
        // Cache the size from which we are changing so that notifyOwner
        // can notify the owningComponent with all essential information
        ownerContext.previousSize = me.lastComponentSize;
        // Cache the currently layed out size
        me.lastComponentSize = owner.el.lastBox = props = ownerContext.props;
        // lastBox is a copy of the defined props to allow save/restore of these (panel
        // collapse needs this)
        lastBox = owner.lastBox || (owner.lastBox = {});
        lastBox.x = props.x;
        lastBox.y = props.y;
        lastBox.width = props.width;
        lastBox.height = props.height;
        lastBox.invalid = false;
        me.callParent([
            ownerContext
        ]);
    },
    notifyOwner: function(ownerContext) {
        var me = this,
            currentSize = me.lastComponentSize,
            prevSize = ownerContext.previousSize;
        me.owner.afterComponentLayout(currentSize.width, currentSize.height, prevSize ? prevSize.width : undefined, prevSize ? prevSize.height : undefined);
    },
    /**
     * Returns the owner component's resize element.
     * @return {Ext.dom.Element}
     */
    getTarget: function() {
        return this.owner.el;
    },
    /**
     * Returns the element into which rendering must take place.
     * Defaults to the owner Component's encapsulating element.
     *
     * May be overridden in Component layout managers which implement an inner element.
     * @return {Ext.dom.Element}
     */
    getRenderTarget: function() {
        return this.owner.el;
    },
    cacheTargetInfo: function(ownerContext) {
        var me = this,
            targetInfo = me.targetInfo,
            target;
        if (!targetInfo) {
            target = ownerContext.getEl('getTarget', me);
            me.targetInfo = targetInfo = {
                padding: target.getPaddingInfo(),
                border: target.getBorderInfo()
            };
        }
        return targetInfo;
    },
    measureAutoDimensions: function(ownerContext, dimensions) {
        // Subtle But Important:
        // 
        // We don't want to call getProp/hasProp et.al. unless we in fact need that value
        // for our results! If we call it and don't need it, the layout manager will think
        // we depend on it and will schedule us again should it change.
        var me = this,
            owner = me.owner,
            containerLayout = owner.layout,
            heightModel = ownerContext.heightModel,
            widthModel = ownerContext.widthModel,
            boxParent = ownerContext.boxParent,
            isBoxParent = ownerContext.isBoxParent,
            target = ownerContext.target,
            props = ownerContext.props,
            isContainer,
            ret = {
                gotWidth: false,
                gotHeight: false,
                isContainer: (isContainer = !ownerContext.hasRawContent)
            },
            hv = dimensions || 3,
            zeroWidth, zeroHeight,
            needed = 0,
            got = 0,
            ready, size, temp, key, cache;
        // Note: this method is called *a lot*, so we have to be careful not to waste any
        // time or make useless calls or, especially, read the DOM when we can avoid it.
        //---------------------------------------------------------------------
        // Width
        if (widthModel.shrinkWrap && ownerContext.consumersContentWidth) {
            ++needed;
            zeroWidth = !(hv & 1);
            if (isContainer) {
                // as a componentLayout for a container, we rely on the container layout to
                // produce contentWidth...
                if (zeroWidth) {
                    ret.contentWidth = 0;
                    ret.gotWidth = true;
                    ++got;
                } else if ((ret.contentWidth = ownerContext.getProp('contentWidth')) !== undefined) {
                    ret.gotWidth = true;
                    ++got;
                }
            } else {
                size = props.contentWidth;
                if (typeof size === 'number') {
                    // if (already determined)
                    ret.contentWidth = size;
                    ret.gotWidth = true;
                    ++got;
                } else {
                    if (zeroWidth) {
                        ready = true;
                    } else if (!ownerContext.hasDomProp('containerChildrenSizeDone')) {
                        ready = false;
                    } else if (isBoxParent || !boxParent || boxParent.widthModel.shrinkWrap) {
                        // if we have no boxParent, we are ready, but a shrinkWrap boxParent
                        // artificially provides width early in the measurement process so
                        // we are ready to go in that case as well...
                        ready = true;
                    } else {
                        // lastly, we have a boxParent that will be given a width, so we
                        // can wait for that width to be set in order to properly measure
                        // whatever is inside...
                        ready = boxParent.hasDomProp('width');
                    }
                    if (ready) {
                        if (zeroWidth) {
                            temp = 0;
                        } else if (containerLayout && containerLayout.measureContentWidth) {
                            // Allow the container layout to do the measurement since it
                            // may have a better idea of how to do it even with no items:
                            temp = containerLayout.measureContentWidth(ownerContext);
                        } else {
                            if (target.cacheWidth) {
                                // if all instances of a given xtype/UI are the same size,
                                // only read the DOM once to measure the first instance.
                                // Thereafter, retrieve the width from the cache.
                                key = target.xtype + '-' + target.ui;
                                cache = me.widthCache;
                                temp = cache[key] || (cache[key] = me.measureContentWidth(ownerContext));
                            } else {
                                temp = me.measureContentWidth(ownerContext);
                            }
                        }
                        if (!isNaN(ret.contentWidth = temp)) {
                            ownerContext.setContentWidth(temp, true);
                            ret.gotWidth = true;
                            ++got;
                        }
                    }
                }
            }
        } else if (widthModel.natural && ownerContext.consumersWidth) {
            ++needed;
            size = props.width;
            // zeroWidth does not apply
            if (typeof size === 'number') {
                // if (already determined)
                ret.width = size;
                ret.gotWidth = true;
                ++got;
            } else {
                if (isBoxParent || !boxParent) {
                    ready = true;
                } else {
                    // lastly, we have a boxParent that will be given a width, so we
                    // can wait for that width to be set in order to properly measure
                    // whatever is inside...
                    ready = boxParent.hasDomProp('width');
                }
                if (ready) {
                    if (!isNaN(ret.width = me.measureOwnerWidth(ownerContext))) {
                        ownerContext.setWidth(ret.width, false);
                        ret.gotWidth = true;
                        ++got;
                    }
                }
            }
        }
        //---------------------------------------------------------------------
        // Height
        if (heightModel.shrinkWrap && ownerContext.consumersContentHeight) {
            ++needed;
            zeroHeight = !(hv & 2);
            if (isContainer) {
                // don't ask unless we need to know...
                if (zeroHeight) {
                    ret.contentHeight = 0;
                    ret.gotHeight = true;
                    ++got;
                }
                // eslint-disable-next-line no-cond-assign, max-len
                else if ((ret.contentHeight = ownerContext.getProp('contentHeight')) !== undefined) {
                    ret.gotHeight = true;
                    ++got;
                }
            } else {
                size = props.contentHeight;
                if (typeof size === 'number') {
                    // if (already determined)
                    ret.contentHeight = size;
                    ret.gotHeight = true;
                    ++got;
                } else {
                    if (zeroHeight) {
                        ready = true;
                    } else if (!ownerContext.hasDomProp('containerChildrenSizeDone')) {
                        ready = false;
                    } else if (owner.noWrap) {
                        ready = true;
                    } else if (!widthModel.shrinkWrap) {
                        // fixed width, so we need the width to determine the height...
                        // eslint-disable-next-line max-len
                        ready = (ownerContext.bodyContext || ownerContext).hasDomProp('width');
                    }
                    // && (!ownerContext.bodyContext || ownerContext.bodyContext.hasDomProp('width'));
                    else if (isBoxParent || !boxParent || boxParent.widthModel.shrinkWrap) {
                        // if we have no boxParent, we are ready, but an autoWidth boxParent
                        // artificially provides width early in the measurement process so
                        // we are ready to go in that case as well...
                        ready = true;
                    } else {
                        // lastly, we have a boxParent that will be given a width, so we
                        // can wait for that width to be set in order to properly measure
                        // whatever is inside...
                        ready = boxParent.hasDomProp('width');
                    }
                    if (ready) {
                        if (zeroHeight) {
                            temp = 0;
                        } else if (containerLayout && containerLayout.measureContentHeight) {
                            // Allow the container layout to do the measurement since it
                            // may have a better idea of how to do it even with no items:
                            temp = containerLayout.measureContentHeight(ownerContext);
                        } else {
                            if (target.cacheHeight) {
                                // if all instances of a given xtype/UI are the same size,
                                // only read the DOM once to measure the first instance.
                                // Thereafter, retrieve the height from the cache.
                                key = target.xtype + '-' + target.ui;
                                cache = me.heightCache;
                                temp = cache[key] || (cache[key] = me.measureContentHeight(ownerContext));
                            } else {
                                temp = me.measureContentHeight(ownerContext);
                            }
                        }
                        if (!isNaN(ret.contentHeight = temp)) {
                            ownerContext.setContentHeight(temp, true);
                            ret.gotHeight = true;
                            ++got;
                        }
                    }
                }
            }
        } else if (heightModel.natural && ownerContext.consumersHeight) {
            ++needed;
            size = props.height;
            // zeroHeight does not apply
            if (typeof size === 'number') {
                // if (already determined)
                ret.height = size;
                ret.gotHeight = true;
                ++got;
            } else {
                if (isBoxParent || !boxParent) {
                    ready = true;
                } else {
                    // lastly, we have a boxParent that will be given a width, so we
                    // can wait for that width to be set in order to properly measure
                    // whatever is inside...
                    ready = boxParent.hasDomProp('width');
                }
                if (ready) {
                    if (!isNaN(ret.height = me.measureOwnerHeight(ownerContext))) {
                        ownerContext.setHeight(ret.height, false);
                        ret.gotHeight = true;
                        ++got;
                    }
                }
            }
        }
        if (boxParent) {
            ownerContext.onBoxMeasured();
        }
        ret.gotAll = got === needed;
        // see if we can avoid calling this method by storing something on ownerContext.
        return ret;
    },
    measureContentWidth: function(ownerContext) {
        // contentWidth includes padding, but not border, framing or margins
        return ownerContext.el.getWidth() - ownerContext.getFrameInfo().width;
    },
    measureContentHeight: function(ownerContext) {
        // contentHeight includes padding, but not border, framing or margins
        return ownerContext.el.getHeight() - ownerContext.getFrameInfo().height;
    },
    measureOwnerHeight: function(ownerContext) {
        return ownerContext.el.getHeight();
    },
    measureOwnerWidth: function(ownerContext) {
        return ownerContext.el.getWidth();
    }
});

/**
 * The class is the default component layout for {@link Ext.Component} when no explicit
 * `{@link Ext.Component#componentLayout componentLayout}` is configured.
 *
 * This class uses template methods to perform the individual aspects of measurement,
 * calculation and publication of results. The methods called depend on the component's
 * {@link Ext.Component#getSizeModel size model}.
 * 
 * ## configured / calculated
 *
 * In either of these size models, the dimension of the outer element is of a known size.
 * The size is found in the `ownerContext` (the {@link Ext.layout.ContextItem} for the owner
 * component) as either "width" or "height". This value, if available, is passed to the
 * `publishInnerWidth` or `publishInnerHeight` method, respectively.
 * 
 * ## shrinkWrap
 *
 * When a dimension uses the `shrinkWrap` size model, that means the content is measured,
 * then the outer (owner) size is calculated and published.
 * 
 * For example, for a shrinkWrap width, the following sequence of calls are made:
 * 
 * - `Ext.layout.component.Component#measureContentWidth`
 * - `publishOwnerWidth`
 *    - `calculateOwnerWidthFromContentWidth`
 *    - `publishInnerWidth` (in the event of hitting a min/maxWidth constraint)
 *
 * ## natural
 *
 * When a dimension uses the `natural` size model, the measurement is made on the outer
 * (owner) element. This size is then used to determine the content area in much the same
 * way as if the outer element had a `configured` or `calculated` size model.
 * 
 * - `Ext.layout.component.Component#measureOwnerWidth`
 * - `publishInnerWidth`
 *
 * @protected
 */
Ext.define('Ext.layout.component.Auto', {
    /* Begin Definitions */
    alias: 'layout.autocomponent',
    extend: 'Ext.layout.component.Component',
    /* End Definitions */
    type: 'autocomponent',
    /**
     * @cfg {Boolean} [setHeightInDom=false]
     * @protected
     * When publishing height of an auto Component, it is usually not written to the DOM.
     * Setting this to `true` overrides this behaviour.
     */
    setHeightInDom: false,
    /**
     * @cfg {Boolean} [setWidthInDom=false]
     * @protected
     * When publishing width of an auto Component, it is usually not written to the DOM.
     * Setting this to `true` overrides this behaviour.
     */
    setWidthInDom: false,
    waitForOuterHeightInDom: false,
    waitForOuterWidthInDom: false,
    beginLayoutCycle: function(ownerContext, firstCycle) {
        var me = this,
            lastWidthModel = me.lastWidthModel,
            lastHeightModel = me.lastHeightModel,
            el = me.owner.el;
        me.callParent(arguments);
        if (lastWidthModel && lastWidthModel.fixed && ownerContext.widthModel.shrinkWrap) {
            el.setWidth(null);
        }
        if (lastHeightModel && lastHeightModel.fixed && ownerContext.heightModel.shrinkWrap) {
            el.setHeight(null);
        }
    },
    calculate: function(ownerContext) {
        var me = this,
            measurement = me.measureAutoDimensions(ownerContext),
            heightModel = ownerContext.heightModel,
            widthModel = ownerContext.widthModel,
            width, height;
        // It is generally important to process widths before heights, since widths can
        // often effect heights...
        if (measurement.gotWidth) {
            if (widthModel.shrinkWrap) {
                me.publishOwnerWidth(ownerContext, measurement.contentWidth);
            } else if (me.publishInnerWidth) {
                me.publishInnerWidth(ownerContext, measurement.width);
            }
        } else if (!widthModel.auto && me.publishInnerWidth) {
            width = me.waitForOuterWidthInDom ? ownerContext.getDomProp('width') : ownerContext.getProp('width');
            if (width === undefined) {
                me.done = false;
            } else {
                me.publishInnerWidth(ownerContext, width);
            }
        }
        if (measurement.gotHeight) {
            if (heightModel.shrinkWrap) {
                me.publishOwnerHeight(ownerContext, measurement.contentHeight);
            } else if (me.publishInnerHeight) {
                me.publishInnerHeight(ownerContext, measurement.height);
            }
        } else if (!heightModel.auto && me.publishInnerHeight) {
            height = me.waitForOuterHeightInDom ? ownerContext.getDomProp('height') : ownerContext.getProp('height');
            if (height === undefined) {
                me.done = false;
            } else {
                me.publishInnerHeight(ownerContext, height);
            }
        }
        if (!measurement.gotAll) {
            me.done = false;
        }
    },
    calculateOwnerHeightFromContentHeight: function(ownerContext, contentHeight) {
        return contentHeight + ownerContext.getFrameInfo().height;
    },
    calculateOwnerWidthFromContentWidth: function(ownerContext, contentWidth) {
        return contentWidth + ownerContext.getFrameInfo().width;
    },
    publishOwnerHeight: function(ownerContext, contentHeight) {
        var me = this,
            owner = me.owner,
            height = me.calculateOwnerHeightFromContentHeight(ownerContext, contentHeight),
            constrainedHeight, dirty, heightModel;
        if (isNaN(height)) {
            me.done = false;
        } else {
            constrainedHeight = Ext.Number.constrain(height, owner.minHeight, owner.maxHeight);
            if (constrainedHeight === height) {
                dirty = me.setHeightInDom;
            } else {
                heightModel = me.sizeModels[(constrainedHeight < height) ? 'constrainedMax' : 'constrainedMin'];
                height = constrainedHeight;
                if (ownerContext.heightModel.calculatedFromShrinkWrap) {
                    // Don't bother to invalidate since that will come soon... but we need
                    // to signal our ownerLayout that we need an invalidate to actually
                    // make good on the determined (constrained) size!
                    ownerContext.heightModel = heightModel;
                } else {
                    ownerContext.invalidate({
                        heightModel: heightModel
                    });
                }
            }
            ownerContext.setHeight(height, dirty);
        }
    },
    publishOwnerWidth: function(ownerContext, contentWidth) {
        var me = this,
            owner = me.owner,
            width = me.calculateOwnerWidthFromContentWidth(ownerContext, contentWidth),
            constrainedWidth, dirty, widthModel;
        if (isNaN(width)) {
            me.done = false;
        } else {
            constrainedWidth = Ext.Number.constrain(width, owner.minWidth, owner.maxWidth);
            if (constrainedWidth === width) {
                dirty = me.setWidthInDom;
            } else {
                widthModel = me.sizeModels[(constrainedWidth < width) ? 'constrainedMax' : 'constrainedMin'];
                width = constrainedWidth;
                if (ownerContext.widthModel.calculatedFromShrinkWrap) {
                    // Don't bother to invalidate since that will come soon... but we need
                    // to signal our ownerLayout that we need an invalidate to actually
                    // make good on the determined (constrained) size!
                    ownerContext.widthModel = widthModel;
                } else {
                    ownerContext.invalidate({
                        widthModel: widthModel
                    });
                }
            }
            ownerContext.setWidth(width, dirty);
        }
    }
});

/**
 * @private
 */
Ext.define('Ext.layout.component.ProgressBar', {
    extend: 'Ext.layout.component.Auto',
    alias: 'layout.progressbar',
    type: 'progressbar',
    beginLayout: function(ownerContext) {
        var me = this,
            i, textEls;
        me.callParent(arguments);
        if (!ownerContext.textEls) {
            textEls = me.owner.textEl;
            // an Ext.Element or CompositeList (raw DOM el's)
            if (textEls.isComposite) {
                ownerContext.textEls = [];
                textEls = textEls.elements;
                for (i = textEls.length; i--; ) {
                    ownerContext.textEls[i] = ownerContext.getEl(Ext.get(textEls[i]));
                }
            } else {
                ownerContext.textEls = [
                    ownerContext.getEl('textEl')
                ];
            }
        }
    },
    calculate: function(ownerContext) {
        var me = this,
            i, textEls, width;
        me.callParent(arguments);
        if (Ext.isNumber(width = ownerContext.getProp('width'))) {
            width -= ownerContext.getBorderInfo().width;
            textEls = ownerContext.textEls;
            for (i = textEls.length; i--; ) {
                textEls[i].setWidth(width);
            }
        } else {
            me.done = false;
        }
    }
});

/**
 * An updateable progress bar component. The progress bar supports two different modes: manual
 * and automatic.
 *
 * In manual mode, you are responsible for showing, updating (via {@link #updateProgress})
 * and clearing the progress bar as needed from your own code. This method is most appropriate
 * when you want to show progress throughout an operation that has predictable points of interest
 * at which you can update the control.
 *
 * In automatic mode, you simply call {@link #wait} and let the progress bar run indefinitely,
 * only clearing it once the operation is complete. You can optionally have the progress bar
 * wait for a specific amount of time and then clear itself. Automatic mode is most appropriate
 * for timed operations or asynchronous operations in which you have no need for indicating
 * intermediate progress.
 *
 *     @example
 *     var p = Ext.create('Ext.ProgressBar', {
 *        renderTo: Ext.getBody(),
 *        width: 300
 *     });
 *
 *     // Wait for 5 seconds, then update the status el (progress bar will auto-reset)
 *     p.wait({
 *         interval: 500, //bar will move fast!
 *         duration: 50000,
 *         increment: 15,
 *         text: 'Updating...',
 *         scope: this,
 *         fn: function(){
 *             p.updateText('Done!');
 *         }
 *     });
 */
Ext.define('Ext.ProgressBar', {
    extend: 'Ext.Component',
    xtype: 'progressbar',
    mixins: [
        'Ext.ProgressBase'
    ],
    requires: [
        'Ext.Template',
        'Ext.CompositeElement',
        'Ext.TaskManager',
        'Ext.layout.component.ProgressBar'
    ],
    uses: [
        'Ext.fx.Anim'
    ],
    /**
     * @cfg {String/HTMLElement/Ext.dom.Element} textEl
     * The element to render the progress text to (defaults to the progress bar's internal
     * text element)
     */
    /**
     * @cfg {String} id
     * The progress bar element's id (defaults to an auto-generated id)
     */
    /**
     * @cfg {String} [baseCls='x-progress']
     * The base CSS class to apply to the progress bar's wrapper element.
     */
    baseCls: Ext.baseCSSPrefix + 'progress',
    /**
     * @cfg {Boolean/Object} animate
     * True to animate the progress bar during transitions, or an animation configuration
     * (see the {@link #method-animate} method for details).
     */
    animate: false,
    /**
     * @cfg {String} text
     * The text shown in the progress bar.
     */
    text: '',
    /**
     * @private
     */
    waitTimer: null,
    childEls: [
        'bar'
    ],
    defaultBindProperty: 'value',
    /* eslint-disable indent, max-len */
    renderTpl: [
        '<tpl if="internalText">',
        '<div class="{baseCls}-text {baseCls}-text-back" role="presentation">{text}</div>',
        '</tpl>',
        '<div id="{id}-bar" data-ref="bar" class="{baseCls}-bar {baseCls}-bar-{ui}" role="presentation" style="width:{percentage}%">',
        '<tpl if="internalText">',
        '<div class="{baseCls}-text" role="presentation">',
        '<div role="presentation">{text}</div>',
        '</div>',
        '</tpl>',
        '</div>'
    ],
    /* eslint-enable indent, max-len */
    componentLayout: 'progressbar',
    ariaRole: 'progressbar',
    focusable: true,
    tabIndex: 0,
    autoEl: {
        'aria-valuemin': '0',
        'aria-valuenow': '0',
        'aria-valuemax': '100'
    },
    /**
     * @event update
     * Fires after each update interval
     * @param {Ext.ProgressBar} this
     * @param {Number} value The current progress value
     * @param {String} text The current progress text
     */
    initRenderData: function() {
        var me = this,
            value = me.value || 0,
            data;
        data = me.callParent();
        return Ext.apply(data, {
            internalText: !me.hasOwnProperty('textEl'),
            text: me.text || Math.round(value * 100) + '%',
            percentage: value * 100
        });
    },
    onRender: function() {
        var me = this;
        me.callParent(arguments);
        // External text display
        if (me.textEl) {
            me.textEl = Ext.get(me.textEl);
            me.updateText(me.text);
        } else // Inline text display
        {
            // This produces a composite w/2 el's (which is why we cannot use childEls or
            // renderSelectors):
            me.textEl = me.el.select('.' + me.baseCls + '-text');
        }
    },
    afterRender: function() {
        var me = this;
        me.callParent(arguments);
        if (me.text) {
            me.ariaEl.dom.setAttribute('aria-valuetext', me.text);
        }
    },
    updateValue: function(value) {
        this.updateProgress(value);
    },
    /**
     * Updates the progress bar value, and optionally its text.
     * 
     * If the text argument is not specified, then the {@link #textTpl} will be used to generate
     * the text. If there is no `textTpl`, any existing text value will be unchanged. To blank out
     * existing text, pass `""`.
     *
     * Note that even if the progress bar value exceeds 1, it will never automatically reset --
     * you are responsible for determining when the progress is complete and
     * calling {@link #reset} to clear and/or hide the control.
     * @param {Number} [value=0] A floating point value between 0 and 1 (e.g., .5)
     * @param {String} [text=''] The string to display in the progress text element
     * @param {Boolean} [animate=false] Whether to animate the transition of the progress bar.
     * If this value is not specified, the default for the class is used
     * @return {Ext.ProgressBar} this
     */
    updateProgress: function(value, text, animate) {
        var me = this,
            oldValue = me.value,
            textTpl = me.getTextTpl();
        value = value || 0;
        // Ensure value is not undefined.
        me.value = value || (value = 0);
        // Empty string (falsy) must blank out the text as per docs.
        if (text != null) {
            me.autoText = false;
            me.updateText(text);
        }
        // Generate text using template and progress values.
        else if (textTpl) {
            me.autoText = false;
            me.updateText(textTpl.apply({
                value: value,
                percent: value * 100
            }));
        } else if (!me.text && me.autoText !== false) {
            me.autoText = true;
            me.updateText(Math.round(value * 100) + '%');
        }
        // Text was set the previous time but not this time. We can't
        // deduce what it should be just from the value so need to
        // reset aria-valuetext because it is no longer valid.
        else if (me.text && me.ariaEl.dom) {
            me.ariaEl.dom.removeAttribute('aria-valuetext');
        }
        if (me.rendered && !me.destroyed) {
            if (animate === true || (animate !== false && me.animate)) {
                me.bar.stopAnimation();
                me.bar.animate(Ext.apply({
                    from: {
                        width: (oldValue * 100) + '%'
                    },
                    to: {
                        width: (value * 100) + '%'
                    }
                }, me.animate));
            } else {
                me.bar.setStyle('width', (value * 100) + '%');
            }
            me.ariaEl.dom.setAttribute('aria-valuenow', Math.round(value * 100));
        }
        me.fireEvent('update', me, value, text);
        return me;
    },
    /**
     * Updates the progress bar text. If specified, textEl will be updated, otherwise
     * the progress bar itself will display the updated text.
     * @param {String} [text=''] The string to display in the progress text element
     * @return {Ext.ProgressBar} this
     */
    updateText: function(text) {
        var me = this;
        if (!me.autoText) {
            me.text = text;
        }
        if (me.rendered) {
            me.textEl.setHtml(text);
            if (!me.autoText) {
                me.ariaEl.dom.setAttribute('aria-valuetext', text);
            } else {
                me.ariaEl.dom.removeAttribute('aria-valuetext');
            }
        }
        return me;
    },
    applyText: function(text) {
        this.updateText(text);
    },
    getText: function() {
        return this.text;
    },
    /**
     * Initiates an auto-updating progress bar. A duration can be specified, in which case
     * the progress bar will automatically reset after a fixed amount of time and optionally call
     * a callback function if specified. If no duration is passed in, then the progress bar will run
     * indefinitely and must be manually cleared by calling {@link #reset}.
     *
     * Example usage:
     *
     *     var p = new Ext.ProgressBar({
     *        renderTo: 'my-el'
     *     });
     *
     *     // Wait for 5 seconds, then update the status el (progress bar will auto-reset)
     *     var p = Ext.create('Ext.ProgressBar', {
     *        renderTo: Ext.getBody(),
     *        width: 300
     *     });
     *
     *     // Wait for 5 seconds, then update the status el (progress bar will auto-reset)
     *     p.wait({
     *        interval: 500, // bar will move fast!
     *        duration: 50000,
     *        increment: 15,
     *        text: 'Updating...',
     *        scope: this,
     *        fn: function() {
     *           p.updateText('Done!');
     *        }
     *     });
     *
     *     // Or update indefinitely until some async action completes, then reset manually
     *     p.wait();
     *     myAction.on('complete', function() {
     *         p.reset();
     *         p.updateText('Done!');
     *     });
     *
     * @param {Object} config (optional) Configuration options
     * @param {Number} config.duration The length of time in milliseconds that the progress bar
     * should run before resetting itself (defaults to undefined, in which case it will run
     * indefinitely until reset is called)
     * @param {Number} config.interval The length of time in milliseconds between each progress
     * update (defaults to 1000 ms)
     * @param {Boolean} config.animate Whether to animate the transition of the progress bar.
     * If this value is not specified, the default for the class is used.
     * @param {Number} config.increment The number of progress update segments to display within
     * the progress bar (defaults to 10).  If the bar reaches the end and is still updating, it will
     * automatically wrap back to the beginning.
     * @param {String} config.text Optional text to display in the progress bar element
     * (defaults to '').
     * @param {Function} config.fn A callback function to execute after the progress bar finishes
     * auto-updating. The function will be called with no arguments. This function will be ignored
     * if duration is not specified since in that case the progress bar can only be stopped
     * programmatically, so any required function should be called by the same code after it resets
     * the progress bar.
     * @param {Object} config.scope The scope that is passed to the callback function (only applies
     * when duration and fn are both passed).
     * @return {Ext.ProgressBar} this
     */
    wait: function(config) {
        var me = this,
            scope;
        if (!me.waitTimer) {
            scope = me;
            config = config || {};
            if (config.text != null) {
                me.autoText = false;
            }
            me.updateText(config.text);
            me.waitTimer = Ext.TaskManager.start({
                run: function(i) {
                    var inc = config.increment || 10;
                    i -= 1;
                    me.updateProgress(((((i + inc) % inc) + 1) * (100 / inc)) * 0.01, null, config.animate);
                },
                interval: config.interval || 1000,
                duration: config.duration,
                onStop: function() {
                    if (config.fn) {
                        config.fn.apply(config.scope || me);
                    }
                    me.reset();
                },
                scope: scope
            });
        }
        return me;
    },
    /**
     * Returns true if the progress bar is currently in a {@link #wait} operation
     * @return {Boolean} True if waiting, else false
     */
    isWaiting: function() {
        return this.waitTimer !== null;
    },
    /**
     * Resets the progress bar value to 0 and text to empty string. If hide = true,
     * the progress bar will also be hidden (using the {@link #hideMode} property internally).
     * @param {Boolean} [hide=false] True to hide the progress bar.
     * @return {Ext.ProgressBar} this
     */
    reset: function(hide) {
        var me = this;
        me.updateProgress(0);
        me.clearTimer();
        if (hide === true) {
            me.hide();
        }
        if (me.rendered) {
            me.ariaEl.dom.removeAttribute('aria-valuetext');
        }
        return me;
    },
    /**
     * @private
     */
    clearTimer: function() {
        var me = this;
        if (me.waitTimer) {
            me.waitTimer.onStop = null;
            // prevent recursion
            Ext.TaskManager.stop(me.waitTimer);
            me.waitTimer = null;
        }
    },
    doDestroy: function() {
        var me = this,
            bar = me.bar,
            nodes, el, i, len;
        me.clearTimer();
        if (me.rendered) {
            if (me.textEl.isComposite) {
                // Clean up Element references created by the layout
                nodes = me.textEl.slice();
                for (i = 0 , len = nodes.length; i < len; i++) {
                    el = Ext.get(nodes[i]);
                    el.destroy();
                }
            }
            Ext.destroyMembers(me, 'textEl', 'progressBar');
            if (bar && me.animate) {
                bar.stopAnimation();
            }
        }
        me.callParent();
    }
});

/**
 * A special Ext.dom.Element used by Buttons.  Since buttons use `display:table` elements
 * for their layout, some special adjustments are needed when the width switches from
 * auto to fixed width and back.
 *
 * When the button has a width other than `auto`, and a right-aligned arrow, long button
 * text will cause the arrow to disappear off the right edge of the button if the btnWrap
 * element has table-layout:auto.  To prevent this, we need to set table-layout:fixed
 * on the btnWrap when the button has a width, however, when the button is shrinkwrap
 * width the btnWrap cannot have table-layout:fixed because its width:100% style will
 * cause the button to expand outward.
 *
 * Additionally, in shrinkWrap height mode, the button css sets a height on the btnEl
 * element, but if the height is being stretched, the btnEl's height will cause the contents
 * to be incorrectly vertically centered, so we dynamically set the btnEl's height to
 * "auto" in fixed-height mode.
 *
 * @private
 */
Ext.define('Ext.dom.ButtonElement', {
    extend: 'Ext.dom.Element',
    setSize: function(width, height, animate) {
        var me = this,
            component = me.component;
        me.callParent([
            width,
            height,
            animate
        ]);
        component.btnWrap.setStyle('table-layout', (!width || width === 'auto') ? '' : 'fixed');
        component.btnEl.setStyle('height', (!height || height === 'auto') ? '' : 'auto');
        return me;
    },
    setStyle: function(prop, value) {
        var me = this,
            component = me.component,
            width, height;
        me.callParent([
            prop,
            value
        ]);
        if (prop) {
            if (prop === 'width' || (typeof prop !== 'string' && 'width' in prop)) {
                width = value || prop.width;
                component.btnWrap.setStyle('table-layout', (!width || width === 'auto') ? '' : 'fixed');
            }
            if (prop === 'height' || (typeof prop !== 'string' && 'height' in prop)) {
                height = value || prop.height;
                component.btnEl.setStyle('height', (!height || height === 'auto') ? '' : 'auto');
            }
        }
        return me;
    },
    setHeight: function(height, animate) {
        this.callParent([
            height,
            animate
        ]);
        this.component.btnEl.setStyle('height', (!height || height === 'auto') ? '' : 'auto');
        return this;
    },
    setWidth: function(width, animate) {
        this.callParent([
            width,
            animate
        ]);
        this.component.btnWrap.setStyle('table-layout', (!width || width === 'auto') ? '' : 'fixed');
        return this;
    }
});

/**
 * @private
 */
Ext.define('Ext.button.Manager', {
    singleton: true,
    alternateClassName: 'Ext.ButtonToggleManager',
    groups: {},
    pressedButton: null,
    init: function() {
        var me = this;
        if (!me.initialized) {
            Ext.getDoc().on({
                mouseup: me.onDocumentMouseUp,
                scope: me
            });
            me.initialized = true;
        }
    },
    // Called by buton instances.
    // Track the button which was mousedowned upon so that the next *document* mouseup
    // can be delivered to it in case mouse is moved outside of button element.
    onButtonMousedown: function(button, e) {
        var pressed = this.pressedButton;
        if (pressed && !pressed.destroying && !pressed.destroyed) {
            pressed.onMouseUp(e);
        }
        this.pressedButton = button;
    },
    onDocumentMouseUp: function(e) {
        var pressed = this.pressedButton;
        if (pressed && !pressed.destroying && !pressed.destroyed) {
            pressed.onMouseUp(e);
            this.pressedButton = null;
        }
    },
    toggleGroup: function(btn, state) {
        var g, i, length;
        if (state) {
            g = this.groups[btn.toggleGroup];
            for (i = 0 , length = g.length; i < length; i++) {
                if (g[i] !== btn) {
                    g[i].toggle(false);
                }
            }
        }
    },
    register: function(btn) {
        var me = this,
            groups = this.groups,
            group = groups[btn.toggleGroup];
        me.init();
        if (!btn.toggleGroup) {
            return;
        }
        if (!group) {
            group = groups[btn.toggleGroup] = [];
        }
        group.push(btn);
        btn.on('toggle', me.toggleGroup, me);
    },
    unregister: function(btn) {
        if (!btn.toggleGroup) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            group = me.groups[btn.toggleGroup];
        if (group) {
            Ext.Array.remove(group, btn);
            btn.un('toggle', me.toggleGroup, me);
        }
    },
    /**
     * Gets the pressed button in the passed group or null
     * @param {String} groupName
     * @return {Ext.button.Button}
     */
    getPressed: function(groupName) {
        var group = this.groups[groupName],
            i = 0,
            len;
        if (group) {
            for (len = group.length; i < len; i++) {
                if (group[i].pressed === true) {
                    return group[i];
                }
            }
        }
        return null;
    }
});

/**
 * Provides a common registry groups of {@link Ext.menu.CheckItem}s.
 *
 * Since 5.1.0, this class no longer registers all menus in your applications.
 * To access all menus, use {@link Ext.ComponentQuery ComponentQuery}.
 * @singleton
 */
Ext.define('Ext.menu.Manager', {
    singleton: true,
    alternateClassName: 'Ext.menu.MenuMgr',
    uses: [
        'Ext.menu.Menu'
    ],
    groups: {},
    visible: [],
    /**
     * @private
     */
    constructor: function() {
        var me = this;
        // Lazily create the mousedown listener on first menu show
        me.onShow = function() {
            // This is a separate method to allow calling eagerly in unit tests
            me.registerGlobalListeners();
            return me.onShow.apply(me, arguments);
        };
    },
    // do the real thing
    onGlobalScroll: function(scroller) {
        var allMenus = this.visible,
            len = allMenus.length,
            i, menu,
            scrollerEl = scroller.getElement();
        // Scrolling document should not hide menus.
        // The will move along with the document.
        if (len && scroller !== Ext.scroll.Scroller.viewport) {
            // Clone here, we may modify this collection while the loop is active
            allMenus = allMenus.slice();
            for (i = 0; i < len; ++i) {
                menu = allMenus[i];
                // Hide the menu if:
                //      The menu does not own scrolling element
                if (!menu.alignOnScroll && menu.hideOnScroll !== false && !menu.owns(scrollerEl)) {
                    menu.hide();
                }
            }
        }
    },
    checkActiveMenus: function(e) {
        var allMenus = this.visible,
            len = allMenus.length,
            i, menu,
            mousedownCmp = Ext.Component.from(e);
        if (len) {
            // Clone here, we may modify this collection while the loop is active
            allMenus = allMenus.slice();
            for (i = 0; i < len; ++i) {
                menu = allMenus[i];
                // Hide the menu if:
                //      The menu does not own the clicked upon element AND
                //      The menu is not the child menu of a clicked upon MenuItem
                // eslint-disable-next-line max-len
                if (!(menu.owns(e) || (mousedownCmp && mousedownCmp.isMenuItem && mousedownCmp.getMenu() === menu))) {
                    menu.hide();
                }
            }
        }
    },
    /**
     * {@link Ext.menu.Menu#afterShow} adds itself to the visible list here.
     * @private
     */
    onShow: function(menu) {
        if (menu.floating) {
            Ext.Array.include(this.visible, menu);
        }
    },
    /**
     * {@link Ext.menu.Menu#onHide} removes itself from the visible list here.
     * @private
     */
    onHide: function(menu) {
        if (menu.floating) {
            Ext.Array.remove(this.visible, menu);
        }
    },
    /**
     * Hides all floating menus that are currently visible
     * @return {Boolean} success True if any active menus were hidden.
     */
    hideAll: function() {
        var allMenus = this.visible,
            len = allMenus.length,
            result = false,
            i;
        if (len) {
            // Clone here, we may modify this collection while the loop is active
            allMenus = allMenus.slice();
            for (i = 0; i < len; i++) {
                allMenus[i].hide();
                result = true;
            }
        }
        return result;
    },
    /**
     * Returns a {@link Ext.menu.Menu} object
     * @param {String/Object} menu The string menu id, an existing menu object reference,
     * or a Menu config that will be used to generate and return a new Menu this.
     * @param {Object} [config] A configuration to use when creating the menu.
     * @return {Ext.menu.Menu} The specified menu, or null if none are found
     */
    get: function(menu, config) {
        var result;
        if (typeof menu === 'string') {
            // menu id
            result = Ext.getCmp(menu);
            if (result instanceof Ext.menu.Menu) {
                menu = result;
            }
        } else if (Ext.isArray(menu)) {
            // array of menu items
            config = Ext.apply({
                items: menu
            }, config);
            menu = new Ext.menu.Menu(config);
        } else if (!menu.isComponent) {
            // otherwise, must be a config
            config = Ext.apply({}, menu, config);
            menu = Ext.ComponentManager.create(config, 'menu');
        }
        return menu;
    },
    /**
     * @private
     */
    registerCheckable: function(menuItem) {
        var groups = this.groups,
            groupId = menuItem.group;
        if (groupId) {
            if (!groups[groupId]) {
                groups[groupId] = [];
            }
            groups[groupId].push(menuItem);
        }
    },
    /**
     * @private
     */
    unregisterCheckable: function(menuItem) {
        var groups = this.groups,
            groupId = menuItem.group;
        if (groupId) {
            Ext.Array.remove(groups[groupId], menuItem);
        }
    },
    onCheckChange: function(menuItem, state) {
        var groups = this.groups,
            groupId = menuItem.group,
            i = 0,
            group, ln, curr;
        if (groupId && state) {
            group = groups[groupId];
            ln = group.length;
            for (; i < ln; i++) {
                curr = group[i];
                if (curr !== menuItem) {
                    curr.setChecked(false);
                }
            }
        }
    },
    /**
     * @private
     */
    registerGlobalListeners: function() {
        var me = this;
        delete me.onShow;
        // remove the lazy-init hook
        // Use the global mousedown event that gets fired even if propagation is stopped
        Ext.on({
            mousedown: me.checkActiveMenus,
            scroll: me.onGlobalScroll,
            scope: me
        });
    }
});

/**
 * Create simple buttons with this component. Customizations include {@link #iconAlign aligned}
 * {@link #iconCls icons}, {@link #cfg-menu dropdown menus}, {@link #tooltip tooltips}
 * and {@link #scale sizing options}. Specify a {@link #handler handler} to run code when
 * a user clicks the button, or use {@link #listeners listeners} for other events such as
 * {@link #mouseover mouseover}. Example usage:
 *
 *     @example
 *     Ext.create('Ext.Button', {
 *         text: 'Click me',
 *         renderTo: Ext.getBody(),
 *         handler: function() {
 *             alert('You clicked the button!');
 *         }
 *     });
 *
 * The {@link #handler} configuration can also be updated dynamically using the {@link #setHandler}
 * method.  Example usage:
 *
 *     @example
 *     Ext.create('Ext.Button', {
 *         text    : 'Dynamic Handler Button',
 *         renderTo: Ext.getBody(),
 *         handler : function() {
 *             // this button will spit out a different number every time you click it.
 *             // so firstly we must check if that number is already set:
 *             if (this.clickCount) {
 *                 // looks like the property is already set, so lets just add 1 to that number
 *                 // and alert the user
 *                 this.clickCount++;
 *                 alert('You have clicked the button "' + this.clickCount +
 *                       '" times.\n\nTry clicking it again..');
 *             } else {
 *                 // if the clickCount property is not set, we will set it and alert the user
 *                 this.clickCount = 1;
 *                 alert('You just clicked the button for the first time!\n\n' +
 *                       'Try pressing it again..');
 *             }
 *         }
 *     });
 *
 * A button within a container:
 *
 *     @example
 *     Ext.create('Ext.Container', {
 *         renderTo: Ext.getBody(),
 *         items   : [
 *             {
 *                 xtype: 'button',
 *                 text : 'My Button'
 *             }
 *         ]
 *     });
 *
 * A useful option of Button is the {@link #scale} configuration. This configuration has three
 * different options:
 *
 * - `'small'`
 * - `'medium'`
 * - `'large'`
 *
 * Example usage:
 *
 *     @example
 *     Ext.create('Ext.Button', {
 *         renderTo: document.body,
 *         text    : 'Click me',
 *         scale   : 'large'
 *     });
 *
 * Buttons can also be toggled. To enable this, you simple set the {@link #enableToggle} property
 * to `true`.
 * Example usage:
 *
 *     @example
 *     Ext.create('Ext.Button', {
 *         renderTo: Ext.getBody(),
 *         text: 'Click Me',
 *         enableToggle: true
 *     });
 *
 * You can assign a menu to a button by using the {@link #cfg-menu} configuration. This standard
 * configuration can either be a reference to a {@link Ext.menu.Menu menu} object,
 * a {@link Ext.menu.Menu menu} id or a {@link Ext.menu.Menu menu} config blob. When assigning
 * a menu to a button, an arrow is automatically added to the button. You can change the alignment
 * of the arrow using the {@link #arrowAlign} configuration on button.
 * Example usage:
 *
 *     @example
 *     Ext.create('Ext.Button', {
 *         text      : 'Menu button',
 *         renderTo  : Ext.getBody(),
 *         arrowAlign: 'bottom',
 *         menu      : [
 *             {text: 'Item 1'},
 *             {text: 'Item 2'},
 *             {text: 'Item 3'},
 *             {text: 'Item 4'}
 *         ]
 *     });
 *
 * Using listeners, you can easily listen to events fired by any component, using the
 * {@link #listeners} configuration or using the {@link #addListener} method.
 * Button has a variety of different listeners:
 *
 * - `click`
 * - `toggle`
 * - `mouseover`
 * - `mouseout`
 * - `mouseshow`
 * - `menuhide`
 * - `menutriggerover`
 * - `menutriggerout`
 *
 * Example usage:
 *
 *     @example
 *     Ext.create('Ext.Button', {
 *         text     : 'Button',
 *         renderTo : Ext.getBody(),
 *         listeners: {
 *             click: function() {
 *                 // this == the button, as we are in the local scope
 *                 this.setText('I was clicked!');
 *             },
 *             mouseover: function() {
 *                 // set a new config which says we moused over, if not already set
 *                 if (!this.mousedOver) {
 *                     this.mousedOver = true;
 *                     alert('You moused over a button!\n\nI wont do this again.');
 *                 }
 *             }
 *         }
 *     });
 */
Ext.define('Ext.button.Button', {
    /* Begin Definitions */
    alias: 'widget.button',
    extend: 'Ext.Component',
    requires: [
        'Ext.dom.ButtonElement',
        'Ext.button.Manager',
        'Ext.menu.Manager',
        'Ext.util.ClickRepeater',
        'Ext.util.TextMetrics',
        'Ext.Glyph'
    ],
    mixins: [
        'Ext.mixin.Queryable'
    ],
    alternateClassName: 'Ext.Button',
    config: {
        /**
         * @cfg {String} iconAlign
         * The side of the Button box to render the icon. Four values are allowed:
         *
         * - 'top'
         * - 'right'
         * - 'bottom'
         * - 'left'
         */
        iconAlign: 'left',
        /**
         * @cfg {String} text
         * The button text to be used as innerHTML (html tags are accepted).
         */
        text: null,
        /**
         * @cfg {String} textAlign
         * The text alignment for this button (center, left, right).
         */
        textAlign: 'center',
        /**
         * @cfg {Boolean} arrowVisible
         * `false` to hide the button arrow.  Only applicable for {@link Ext.button.Split
         * Split Buttons} and buttons configured with a {@link #cfg-menu}.
         */
        arrowVisible: true,
        /**
         * @cfg glyph
         * @inheritdoc Ext.panel.Header#cfg-glyph
         */
        glyph: null
    },
    /* End Definitions */
    /**
     * @property {Boolean} isButton
     * `true` in this class to identify an object as an instantiated Button, or subclass thereof.
     */
    isButton: true,
    /**
     * @property {Boolean} _syncFrameHeight
     * @private
     * `true` to keep height of the frame's "MC" element in sync.  This is needed in IE8
     * so that the button's inner element(s) can use height:100% to fill the button when
     * it not in shrinkWrap mode
     */
    _syncFrameHeight: true,
    /**
     * @private
     * @readonly
     */
    liquidLayout: true,
    /**
     * @property {Boolean} hidden
     * True if this button is hidden.
     * @readonly
     */
    hidden: false,
    /**
     * @property {Boolean} disabled
     * True if this button is disabled.
     * @readonly
     */
    disabled: false,
    /**
     * @property {Boolean} pressed
     * True if this button is pressed (only if enableToggle = true).
     * @readonly
     */
    pressed: false,
    /**
     * @cfg icon
     * @inheritdoc Ext.panel.Header#cfg-icon
     */
    /**
     * @cfg {Function/String} handler
     * A function called when the button is clicked (can be used instead of click event).
     *
     * See also {@link #clickEvent}
     * @param {Ext.button.Button} button This button.
     * @param {Ext.event.Event} e The click event.
     * @controllable
     */
    /**
     * @cfg {Number} minWidth
     * The minimum width for this button (used to give a set of buttons a common width).
     * See also {@link Ext.panel.Panel}.{@link Ext.panel.Panel#minButtonWidth minButtonWidth}.
     */
    /**
     * @cfg {String/Object} tooltip
     * The tooltip for the button - can be a string to be used as innerHTML (html tags are accepted)
     * or QuickTips config object.
     */
    /**
     * @cfg {Boolean} [hidden=false]
     * True to start hidden.
     */
    /**
     * @cfg {Boolean} [disabled=false]
     * True to start disabled.
     */
    /**
     * @cfg padding
     * @inheritdoc
     * @removed Use the $button-*-padding CSS Vars within a custom theme instead.
     */
    /**
     * @cfg {Boolean} [pressed=false]
     * True to start pressed (only if enableToggle = true)
     */
    /**
     * @cfg {String} toggleGroup
     * The group this toggle button is a member of (only 1 per group can be pressed).
     * If a toggleGroup is specified, the {@link #enableToggle} configuration will automatically
     * be set to true.
     */
    /**
     * @cfg {Boolean/Object} [repeat=false]
     * True to repeat fire the click event while the mouse is down. This can also be a
     * {@link Ext.util.ClickRepeater ClickRepeater} config object.
     */
    /**
     * @cfg {Number} tabIndex
     * Sets a DOM tabIndex for this button. tabIndex may be set to `-1` in order to remove
     * the button from the tab rotation.
     */
    tabIndex: 0,
    /**
     * @cfg {Boolean} [allowDepress=true]
     * False to not allow a pressed Button to be depressed. Only valid when {@link #enableToggle}
     * is true.
     */
    /**
     * @cfg {Boolean} enableToggle
     * True to enable pressed/not pressed toggling. If a {@link #toggleGroup} is specified, this
     * option will be set to true.
     */
    enableToggle: false,
    /**
     * @cfg {Function/String} toggleHandler
     * Function called when a Button with {@link #enableToggle} set to true is clicked.
     * @cfg {Ext.button.Button} toggleHandler.button This button.
     * @cfg {Boolean} toggleHandler.state The next state of the Button, true means pressed.
     * @controllable
     */
    /**
     * @cfg {Ext.menu.Menu/String/Object} menu
     * Standard menu attribute consisting of a reference to a menu object, a menu id
     * or a menu config blob. Note that using menus with handlers or click event listeners
     * violates WAI-ARIA 1.0 requirements for accessible Web applications, and is not
     * recommended.
     */
    /**
     * @cfg {String} menuAlign
     * The position to align the menu to (see {@link Ext.util.Positionable#alignTo} for more
     * details).
     */
    menuAlign: 'tl-bl?',
    /**
     * @cfg {Boolean} showEmptyMenu
     * True to force an attached {@link #cfg-menu} with no items to be shown when clicking
     * this button. By default, the menu will not show if it is empty.
     */
    showEmptyMenu: false,
    /**
     * @cfg {String} overflowText
     * If used in a {@link Ext.toolbar.Toolbar Toolbar}, the text to be used if this item is shown
     * in the overflow menu.
     * See also {@link Ext.toolbar.Item}.`{@link Ext.toolbar.Item#overflowText overflowText}`.
     */
    /**
     * @cfg iconCls
     * @inheritdoc Ext.panel.Header#cfg-iconCls
     */
    /**
     * @cfg {String} clickEvent
     * The DOM event that will fire the handler of the button. This can be any valid event name
     * (dblclick, contextmenu).
     */
    clickEvent: 'click',
    /**
     * @cfg {Boolean} preventDefault
     * Is set to `true` to prevent the default action when the {@link #clickEvent} is processed.
     * This provides focus control for clicks and stops scrolling on some devices when using
     * the keyboard to simulate clicks. Set this to `false` if you need to listen directly
     * to element events (for example, to use `window.open()` in response to a click).
     */
    preventDefault: true,
    /**
     * @cfg {Boolean} handleMouseEvents
     * False to disable visual cues on mouseover, mouseout and mousedown.
     */
    handleMouseEvents: true,
    /**
     * @cfg {String} tooltipType
     * The type of tooltip to use. Either 'qtip' for QuickTips or 'title' for title attribute.
     */
    tooltipType: 'qtip',
    /**
     * @cfg {String} baseCls
     * The base CSS class to add to all buttons.
     */
    baseCls: Ext.baseCSSPrefix + 'btn',
    /**
     * @cfg {String} href
     * The URL to open when the button is clicked. Specifying this config causes the Button to be
     * rendered with the specified URL as the `href` attribute of its `<a>` Element.
     *
     * This is better than specifying a click handler of
     *
     *     function() { window.location = "http://www.sencha.com" }
     *
     * because the UI will provide meaningful hints to the user as to what to expect upon clicking
     * the button, and will also allow the user to open in a new tab or window, bookmark or drag
     * the URL, or directly save the URL stream to disk.
     *
     * See also the {@link #hrefTarget} config.
     */
    /**
     * @cfg {String} hrefTarget
     * The target attribute to use for the underlying anchor. Only used if the {@link #href}
     * property is specified.
     */
    hrefTarget: '_blank',
    /**
     * @cfg {Boolean} destroyMenu
     * Whether or not to destroy any associated menu when this button is destroyed.
     * In addition, a value of `true` for this config will destroy the currently bound menu when
     * a new menu is set in {@link #setMenu} unless overridden by that method's destroyMenu function
     * argument.
     */
    destroyMenu: true,
    /**
     * @cfg {Object} baseParams
     * An object literal of parameters to pass to the url when the {@link #href} property
     * is specified.
     */
    /**
     * @cfg {Object} params
     * An object literal of parameters to pass to the url when the {@link #href} property
     * is specified. Any params override {@link #baseParams}. New params can be set using
     * the {@link #setParams} method.
     */
    /**
     * @cfg {String/Number} value
     * The value of this button.  Only applicable when used as an item of a
     * {@link Ext.button.Segmented Segmented Button}.
     */
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: true,
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'button',
    /**
     * @cfg keyMap
     * @inheritdoc
     */
    keyMap: {
        scope: 'this',
        SPACE: 'onEnterKey',
        ENTER: 'onEnterKey',
        DOWN: 'onDownKey'
    },
    /**
     * @property defaultBindProperty
     * @inheritdoc
     */
    defaultBindProperty: 'text',
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        'btnEl',
        'btnWrap',
        'btnInnerEl',
        'btnIconEl',
        'arrowEl'
    ],
    /**
     * @cfg publishes
     * @inheritdoc
     */
    publishes: {
        pressed: 1
    },
    /**
     * @private
     */
    _btnWrapCls: Ext.baseCSSPrefix + 'btn-wrap',
    _btnCls: Ext.baseCSSPrefix + 'btn-button',
    _baseIconCls: Ext.baseCSSPrefix + 'btn-icon-el',
    _glyphCls: Ext.baseCSSPrefix + 'btn-glyph',
    _innerCls: Ext.baseCSSPrefix + 'btn-inner',
    _textCls: Ext.baseCSSPrefix + 'btn-text',
    _noTextCls: Ext.baseCSSPrefix + 'btn-no-text',
    _hasIconCls: Ext.baseCSSPrefix + 'btn-icon',
    _pressedCls: Ext.baseCSSPrefix + 'btn-pressed',
    /**
     * @cfg overCls
     * @inheritdoc
     */
    overCls: Ext.baseCSSPrefix + 'btn-over',
    _disabledCls: Ext.baseCSSPrefix + 'btn-disabled',
    _menuActiveCls: Ext.baseCSSPrefix + 'btn-menu-active',
    _arrowElCls: Ext.baseCSSPrefix + 'btn-arrow-el',
    _focusCls: Ext.baseCSSPrefix + 'btn-focus',
    _arrowFocusCls: Ext.baseCSSPrefix + 'arrow-focus',
    _arrowOverCls: Ext.baseCSSPrefix + 'arrow-over',
    _arrowPressedCls: Ext.baseCSSPrefix + 'arrow-pressed',
    /* eslint-disable indent, max-len */
    // We have to keep "unselectable" attribute on all elements because it's not inheritable.
    // Without it, clicking anywhere on a button disrupts current selection and cursor position
    // in HtmlEditor.
    /**
     * @cfg renderTpl
     * @inheritdoc
     */
    renderTpl: '<span id="{id}-btnWrap" data-ref="btnWrap" role="presentation" unselectable="on" style="{btnWrapStyle}" ' + 'class="{btnWrapCls} {btnWrapCls}-{ui} {splitCls}{childElCls}">' + '<span id="{id}-btnEl" data-ref="btnEl" role="presentation" unselectable="on" style="{btnElStyle}" ' + 'class="{btnCls} {btnCls}-{ui} {textCls} {noTextCls} {hasIconCls} ' + '{iconAlignCls} {textAlignCls} {btnElAutoHeightCls}{childElCls}">' + '<tpl if="iconBeforeText">{[values.$comp.renderIcon(values)]}</tpl>' + '<span id="{id}-btnInnerEl" data-ref="btnInnerEl" unselectable="on" ' + 'class="{innerCls} {innerCls}-{ui}{childElCls}">{text}</span>' + '<tpl if="!iconBeforeText">{[values.$comp.renderIcon(values)]}</tpl>' + '</span>' + '</span>' + '{[values.$comp.getAfterMarkup ? values.$comp.getAfterMarkup(values) : ""]}' + // if "closable" (tab) add a close element icon
    '<tpl if="closable">' + '<span id="{id}-closeEl" data-ref="closeEl" class="{baseCls}-close-btn">' + '<tpl if="closeText">' + ' {closeText}' + '</tpl>' + '</span>' + '</tpl>' + // Split buttons have additional tab stop for the arrow element
    '<tpl if="split">' + '<span id="{id}-arrowEl" class="{arrowElCls}" data-ref="arrowEl" ' + 'role="button" hidefocus="on" unselectable="on"' + '<tpl if="tabIndex != null"> tabindex="{tabIndex}"</tpl>' + '<tpl foreach="arrowElAttributes"> {$}="{.}"</tpl>' + ' style="{arrowElStyle}"' + '>{arrowElText}</span>' + '</tpl>',
    iconTpl: '<span id="{id}-btnIconEl" data-ref="btnIconEl" role="presentation" unselectable="on" class="{baseIconCls} ' + '{baseIconCls}-{ui} {iconCls} {glyphCls}{childElCls}" style="' + '<tpl if="iconUrl">background-image:url({iconUrl});</tpl>' + '<tpl if="glyph">' + '<tpl if="glyphFontFamily">' + 'font-family:{glyphFontFamily};' + '</tpl>' + '">{glyph}' + '<tpl else>' + '">' + '</tpl>' + '</span>',
    /* eslint-enable indent, max-len */
    /**
     * @cfg {"small"/"medium"/"large"} scale
     * The size of the Button. Three values are allowed:
     *
     * - 'small' - Results in the button element being 16px high.
     * - 'medium' - Results in the button element being 24px high.
     * - 'large' - Results in the button element being 32px high.
     */
    scale: 'small',
    /**
     * @private
     * An array of allowed scales.
     */
    allowedScales: [
        'small',
        'medium',
        'large'
    ],
    /**
     * @cfg {Object} scope
     * The scope (**this** reference) in which the `{@link #handler}` and `{@link #toggleHandler}`
     * is executed. Defaults to this Button.
     */
    /**
     * @cfg {String} arrowAlign
     * The side of the Button box to render the arrow if the button has an associated
     * {@link #cfg-menu}. Two values are allowed:
     *
     * - 'right'
     * - 'bottom'
     */
    arrowAlign: 'right',
    /**
     * @cfg {String} arrowCls
     * The className used for the inner arrow element if the button has a menu.
     */
    arrowCls: 'arrow',
    /**
     * @property {Ext.Template} template
     * A {@link Ext.Template Template} used to create the Button's DOM structure.
     *
     * Instances, or subclasses which need a different DOM structure may provide a different
     * template layout in conjunction with an implementation of {@link #getTemplateArgs}.
     */
    /**
     * @cfg {String} cls
     * A CSS class string to apply to the button's main element.
     */
    /**
     * @property {Ext.menu.Menu} menu
     * The {@link Ext.menu.Menu Menu} object associated with this Button when configured with the
     * {@link #cfg-menu} config option.
     */
    /**
     * @property maskOnDisable
     * @inheritdoc
     */
    maskOnDisable: false,
    /**
     * @cfg shrinkWrap
     * @inheritdoc
     */
    shrinkWrap: 3,
    /**
     * @cfg frame
     * @inheritdoc
     */
    frame: true,
    /**
     * @cfg autoEl
     * @inheritdoc
     */
    autoEl: {
        tag: 'a',
        hidefocus: 'on',
        unselectable: 'on'
    },
    hasFrameTable: function() {
        // Instead of browser sniffing, it's easier to check for the presence of frameTable.
        // If present, we know that it's a browser that doesn't support CSS3BorderRadius.
        return this.href && this.frameTable;
    },
    frameTableListener: function() {
        if (!this.disabled) {
            this.doNavigate();
        }
    },
    doNavigate: function() {
        // Non-HTML5 browsers don't support a block element inside an A tag.
        // http://stackoverflow.com/questions/5682048/putting-a-table-inside-a-hyperlink-not-working-in-ie
        // Note use this.getHref() to append any params to the url.
        if (this.hrefTarget === '_blank') {
            window.open(this.getHref(), this.hrefTarget);
        } else {
            location.href = this.getHref();
        }
    },
    // A reusable object used by getTriggerRegion to avoid excessive object creation.
    _triggerRegion: {},
    /**
     * @event click
     * Fires when this button is clicked, before the configured {@link #handler} is invoked.
     * Execution of the {@link #handler} may be vetoed by returning `false` to this event.
     * @param {Ext.button.Button} this
     * @param {Event} e The click event
     */
    /**
     * @event beforetoggle
     * Fires before the 'pressed' state of this button changes (only if enableToggle = true)
     * If a handler returns `false`, the toggle is vetoed.
     * @param {Ext.button.Button} this
     * @param {Boolean} pressed
     */
    /**
     * @event toggle
     * Fires when the 'pressed' state of this button changes (only if enableToggle = true)
     * @param {Ext.button.Button} this
     * @param {Boolean} pressed
     */
    /**
     * @event mouseover
     * Fires when the mouse hovers over the button
     * @param {Ext.button.Button} this
     * @param {Event} e The event object
     */
    /**
     * @event mouseout
     * Fires when the mouse exits the button
     * @param {Ext.button.Button} this
     * @param {Event} e The event object
     */
    /**
     * @event menushow
     * If this button has a menu, this event fires when it is shown
     * @param {Ext.button.Button} this
     * @param {Ext.menu.Menu} menu
     */
    /**
     * @event menuhide
     * If this button has a menu, this event fires when it is hidden
     * @param {Ext.button.Button} this
     * @param {Ext.menu.Menu} menu
     */
    /**
     * @event menutriggerover
     * If this button has a menu, this event fires when the mouse enters the menu triggering element
     * @param {Ext.button.Button} this
     * @param {Ext.menu.Menu} menu
     * @param {Event} e
     */
    /**
     * @event menutriggerout
     * If this button has a menu, this event fires when the mouse leaves the menu triggering element
     * @param {Ext.button.Button} this
     * @param {Ext.menu.Menu} menu
     * @param {Event} e
     */
    /**
     * @event textchange
     * Fired when the button's text is changed by the {@link #setText} method.
     * @param {Ext.button.Button} this
     * @param {String} oldText
     * @param {String} newText
     */
    /**
     * @event iconchange
     * Fired when the button's icon is changed by the {@link #setIcon} or {@link #setIconCls}
     * methods.
     * @param {Ext.button.Button} this
     * @param {String} oldIcon
     * @param {String} newIcon
     */
    /**
     * @event glyphchange
     * Fired when the button's glyph is changed by the {@link #setGlyph} method.
     * @param {Ext.button.Button} this
     * @param {Number/String} newGlyph
     * @param {Number/String} oldGlyph
     */
    initComponent: function() {
        var me = this;
        // WAI-ARIA spec requires that menu buttons react to Space and Enter keys
        // by showing the menu while leaving focus on the button, and to Down Arrow key
        // by showing the menu and selecting first menu item. This behavior may conflict
        // with historical Ext JS menu button behavior if a handler or a click listener
        // is set on a button; in that case Space or Enter key would activate
        // the handler/click listener, and only Down Arrow key would open the menu.
        // To avoid the ambiguity, we check if the button has both menu *and* handler
        // or click event listener, and warn the developer in that case.
        // Note that this check does not apply to Split buttons because those now have
        // two tab stops and can effectively combine both menu and toggling/href/handler.
        if (!me.isSplitButton && me.menu) {
            if (me.enableToggle || me.toggleGroup) {
                Ext.ariaWarn(me, "According to WAI-ARIA 1.0 Authoring guide " + "(http://www.w3.org/TR/wai-aria-practices/#menubutton), " + "menu button '" + me.id + "' behavior will conflict with " + "toggling.");
            }
            if (me.href) {
                Ext.ariaWarn(me, "According to WAI-ARIA 1.0 Authoring guide " + "(http://www.w3.org/TR/wai-aria-practices/#menubutton), " + "menu button '" + me.id + "' cannot behave as a link.");
            }
            // Only check listeners of the component instance; there could be other
            // listeners on the EventBus inherited via hasListeners prototype.
            if (me.handler || me.hasListeners.hasOwnProperty('click')) {
                Ext.ariaWarn(me, "According to WAI-ARIA 1.0 Authoring guide " + "(http://www.w3.org/TR/wai-aria-practices/#menubutton), " + "menu button '" + me.id + "' should display the menu " + "on SPACE and ENTER keys, which will conflict with the " + "button handler.");
            }
        }
        // Ensure no selection happens
        me.addCls(Ext.baseCSSPrefix + 'unselectable');
        me.callParent();
        if (me.menu) {
            // Flag that we'll have a splitCls
            me.split = true;
            me.setMenu(me.menu, /* destroyMenu */
            false, true);
        }
        // Accept url as a synonym for href
        if (me.url) {
            me.href = me.url;
        }
        // preventDefault defaults to false for links
        me.configuredWithPreventDefault = me.hasOwnProperty('preventDefault');
        if (me.href && !me.configuredWithPreventDefault) {
            me.preventDefault = false;
        }
        if (Ext.isString(me.toggleGroup) && me.toggleGroup !== '') {
            me.enableToggle = true;
        }
        if (me.html && !me.text) {
            me.text = me.html;
            delete me.html;
        }
    },
    getElConfig: function() {
        var me = this,
            config = me.callParent(),
            href = me.getHref(),
            hrefTarget = me.hrefTarget;
        if (config.tag === 'a') {
            if (!me.disabled) {
                config.tabIndex = me.tabIndex;
            }
            if (href) {
                // https://sencha.jira.com/browse/EXTJS-11964
                // Disabled links are clickable on iPad, and right clickable on desktop browsers.
                // The only way to completely disable navigation is removing the href
                if (!me.disabled) {
                    config.href = href;
                    if (hrefTarget) {
                        config.target = hrefTarget;
                    }
                }
            }
        }
        if (!me.ariaStaticRoles[me.ariaRole]) {
            // Split buttons render aria-haspopup into arrowEl
            if (me.menu && !me.isSplitButton) {
                config['aria-haspopup'] = true;
            }
            if (me.enableToggle) {
                config['aria-pressed'] = !!me.pressed;
            }
        }
        return config;
    },
    beforeRender: function() {
        this.callParent();
        if (this.pressed) {
            this.addCls(this._pressedCls);
        }
    },
    initRenderData: function() {
        return Ext.apply(this.callParent(), this.getTemplateArgs());
    },
    /**
     * Get the {@link #cfg-menu} for this button.
     * @return {Ext.menu.Menu} The menu. `null` if no menu is configured.
     */
    getMenu: function() {
        return this.menu || null;
    },
    /**
     * Sets a new menu for this button. Pass a falsy value to unset the current menu.
     * To destroy the previous menu for this button, explicitly pass `false` as the second argument.
     * If this is not set, the destroy will depend on the value of {@link #cfg-destroyMenu}.
     *
     * @param {Ext.menu.Menu/String/Object/null} menu Accepts a menu component, a menu id or a menu
     * config.
     * @param {Boolean} destroyMenu By default, will destroy the previous set menu and remove it
     * from the menu manager. Pass `false` to prevent the destroy.
     * @param {Boolean} [initial] (private)
     */
    setMenu: function(menu, destroyMenu, initial) {
        var me = this,
            oldMenu = me.menu,
            ariaDom = me.isSplitButton ? me.arrowEl && me.arrowEl.dom : me.ariaEl.dom,
            instanced, ariaAttr;
        if (oldMenu && !initial) {
            if (destroyMenu !== false && me.destroyMenu) {
                oldMenu.destroy();
            }
            oldMenu.ownerCmp = null;
        }
        if (menu) {
            instanced = menu.isMenu;
            // Retrieve menu by id or instantiate instance if needed.
            menu = Ext.menu.Manager.get(menu, {
                // Use ownerCmp as the upward link. Menus *must have no ownerCt* - they are
                // global floaters.
                // Upward navigation is done using the up() method.
                ownerCmp: me
            });
            // We need to forcibly set this here because we could be passed an existing menu,
            // which means the config above won't get applied during creation.
            menu.setOwnerCmp(me, instanced);
            // Menu can't reshow within 250ms of being hidden.
            // Likewise, must set here in case an instantiated Menu is passed.
            // This is so that clicking on this button when the menu is visible
            // leaves the menu hidden. Mousedown hides it, and the click caused by
            // mouseup should not reshow.
            menu.menuClickBuffer = 250;
            me.mon(menu, {
                scope: me,
                show: me.onMenuShow,
                hide: me.onMenuHide
            });
            // If the button wasn't initially configured with a menu or has previously been unset
            // then we need to poke the split classes onto the btnWrap dom element.
            if (!oldMenu && me.getArrowVisible()) {
                me.split = true;
                if (me.rendered) {
                    me._addSplitCls();
                    me.updateLayout();
                }
            }
            me.menu = menu;
            // May not be rendered yet
            if (ariaDom) {
                ariaDom.setAttribute('aria-haspopup', true);
                ariaDom.setAttribute('aria-owns', menu.id);
            } else {
                /* eslint-disable */
                // We use me.isSplitButton here because me.split can be set to true
                // for ordinary menu buttons. We only render arrowEl for the true Split buttons.
                ariaAttr = me.isSplitButton ? (me.ariaArrowElAttributes || (me.ariaArrowElAttributes = {})) : (me.ariaRenderAttributes || (me.ariaRenderAttributes = {}));
                /* eslint-enable */
                ariaAttr['aria-haspopup'] = true;
                ariaAttr['aria-owns'] = menu.id;
            }
        } else {
            if (me.rendered) {
                ariaDom.removeAttribute('aria-haspopup');
                ariaDom.removeAttribute('aria-owns');
                me._removeSplitCls();
                me.updateLayout();
            } else {
                ariaAttr = me.isSplitButton ? me.ariaArrowElAttributes : me.ariaRenderAttributes;
                if (ariaAttr) {
                    delete ariaAttr['aria-haspopup'];
                    delete ariaAttr['aria-owns'];
                }
            }
            me.split = false;
            me.menu = null;
        }
    },
    /**
     * @private
     */
    onRender: function() {
        var me = this,
            addOnclick, btn, btnListeners;
        me.callParent(arguments);
        // Set btn as a local variable for easy access
        btn = me.el;
        if (me.tooltip) {
            me.setTooltip(me.tooltip, true);
        }
        // Add the mouse events to the button
        if (me.handleMouseEvents) {
            btnListeners = {
                scope: me,
                mouseover: me.onMouseOver,
                mouseout: me.onMouseOut,
                mousedown: me.onMouseDown
            };
            if (me.split) {
                btnListeners.mousemove = me.onMouseMove;
            }
        } else {
            btnListeners = {
                scope: me
            };
        }
        // Touch start events must be preventDefaulted when in disabled state
        if (Ext.supports.Touch) {
            btnListeners.touchstart = me.onTouchStart;
        }
        // Check if it is a repeat button
        if (me.repeat) {
            me.mon(new Ext.util.ClickRepeater(btn, Ext.isObject(me.repeat) ? me.repeat : {}), 'click', me.onRepeatClick, me);
        } else {
            // If the activation event already has a handler, make a note to add the handler later
            if (btnListeners[me.clickEvent]) {
                addOnclick = true;
            } else {
                btnListeners[me.clickEvent] = me.onClick;
            }
        }
        // Add whatever button listeners we need
        me.mon(btn, btnListeners);
        if (me.hasFrameTable()) {
            me.mon(me.frameTable, 'click', me.frameTableListener, me);
        }
        // If the listeners object had an entry for our clickEvent, add a listener now
        if (addOnclick) {
            me.mon(btn, me.clickEvent, me.onClick, me);
        }
        Ext.button.Manager.register(me);
    },
    onFocusLeave: function(e) {
        this.callParent([
            e
        ]);
        if (this.menu) {
            this.menu.hide();
        }
    },
    /**
     * This method returns an object which provides substitution parameters for the
     * {@link #renderTpl XTemplate} used to create this Button's DOM structure.
     *
     * Instances or subclasses which use a different Template to create a different DOM structure
     * may need to provide their own implementation of this method.
     * @protected
     *
     * @return {Object} Substitution data for a Template. The default implementation which provides
     * data for the default {@link #template} returns an Object containing the following properties:
     * @return {String} return.innerCls A CSS class to apply to the button's text element.
     * @return {String} return.splitCls A CSS class to determine the presence and position
     * of an arrow icon. (`'x-btn-arrow'` or `'x-btn-arrow-bottom'` or `''`)
     * @return {String} return.iconUrl The url for the button icon.
     * @return {String} return.iconCls The CSS class for the button icon.
     * @return {String} return.glyph The glyph to use as the button icon.
     * @return {String} return.glyphCls The CSS class to use for the glyph element.
     * @return {String} return.glyphFontFamily The CSS font-family to use for the glyph element.
     * @return {String} return.text The {@link #text} to display ion the Button.
     */
    getTemplateArgs: function() {
        var me = this,
            btnCls = me._btnCls,
            baseIconCls = me._baseIconCls,
            iconAlign = me.getIconAlign(),
            glyph = me.glyph,
            glyphFontFamily,
            text = me.text,
            hasIcon = me._hasIcon(),
            hasIconCls = me._hasIconCls;
        // Transform Glyph to the useful parts
        if (glyph) {
            glyphFontFamily = glyph.fontFamily;
            glyph = glyph.character;
        }
        return {
            split: me.isSplitButton,
            innerCls: me._innerCls,
            splitCls: me.getArrowVisible() ? me.getSplitCls() : '',
            iconUrl: me.icon,
            iconCls: me.iconCls,
            glyph: glyph,
            glyphCls: glyph ? me._glyphCls : '',
            glyphFontFamily: glyphFontFamily,
            text: text || '&#160;',
            closeText: me.closeText,
            textCls: text ? me._textCls : '',
            noTextCls: text ? '' : me._noTextCls,
            hasIconCls: hasIcon ? hasIconCls : '',
            btnWrapCls: me._btnWrapCls,
            btnWrapStyle: me.width ? 'table-layout:fixed;' : '',
            btnElStyle: me.height ? 'height:auto;' : '',
            btnCls: btnCls,
            baseIconCls: baseIconCls,
            iconBeforeText: iconAlign === 'left' || iconAlign === 'top',
            iconAlignCls: hasIcon ? (hasIconCls + '-' + iconAlign) : '',
            textAlignCls: btnCls + '-' + me.getTextAlign(),
            arrowElCls: me._arrowElCls,
            arrowElStyle: me.arrowVisible ? '' : 'display:none',
            tabIndex: me.tabIndex
        };
    },
    renderIcon: function(values) {
        return this.lookupTpl('iconTpl').apply(values);
    },
    /**
     * Sets the href of the embedded anchor element to the passed URL.
     *
     * Also appends any configured {@link #cfg-baseParams} and parameters set through
     * {@link #setParams}.
     * @param {String} href The URL to set in the anchor element.
     *
     */
    setHref: function(href) {
        var me = this,
            hrefTarget = me.hrefTarget,
            dom;
        me.href = href;
        if (!me.configuredWithPreventDefault) {
            me.preventDefault = !href;
        }
        if (me.rendered) {
            dom = me.el.dom;
            // https://sencha.jira.com/browse/EXTJS-11964
            // Disabled links are clickable on iPad, and right clickable on desktop browsers.
            // The only way to completely disable navigation is removing the href
            if (!href || me.disabled) {
                dom.removeAttribute('href');
                dom.removeAttribute('hrefTarget');
            } else {
                dom.href = me.getHref();
                if (hrefTarget) {
                    dom.target = hrefTarget;
                }
            }
        }
    },
    /**
     * @private
     * If there is a configured href for this Button, returns the href with parameters appended.
     * @return {String/Boolean} The href string with parameters appended.
     */
    getHref: function() {
        var me = this,
            href = me.href;
        return href ? Ext.urlAppend(href, Ext.Object.toQueryString(Ext.apply({}, me.params, me.baseParams))) : false;
    },
    /**
     * Sets the href of the link dynamically according to the params passed, and any
     * {@link #baseParams} configured.
     *
     *     var button = Ext.create('Ext.button.Button', {
     *         renderTo   : document.body,
     *         text       : 'Open',
     *         href       : 'http://www.sencha.com',
     *         baseParams : {
     *             foo : 'bar'
     *         }
     *     });
     *
     *     button.setParams({
     *         company : 'Sencha'
     *     });
     *
     * When clicked, this button will open a new window with the url http://www.sencha.com/?foo=bar&company=Sencha because
     * the button was configured with the {@link #baseParams} to have `foo` = `'bar'`
     * and then used {@link #setParams} to set the `company` parameter to `'Sencha'`.
     *
     * **Only valid if the Button was originally configured with a {@link #href}**
     *
     * @param {Object} params Parameters to use in the href URL.
     */
    setParams: function(params) {
        var me = this,
            dom;
        me.params = params;
        // https://sencha.jira.com/browse/EXTJS-11964
        // Disabled links are clickable on iPad, and right clickable on desktop browsers.
        // The only way to completely disable navigation is removing the href
        if (me.rendered) {
            dom = me.el.dom;
            if (me.disabled) {
                dom.removeAttribute('href');
            } else {
                dom.href = me.getHref() || '';
            }
        }
    },
    getSplitCls: function() {
        var me = this;
        // eslint-disable-next-line max-len
        return me.split ? (me.baseCls + '-' + me.arrowCls) + ' ' + (me.baseCls + '-' + me.arrowCls + '-' + me.arrowAlign) : '';
    },
    /**
     * Sets the background image (inline style) of the button. This method also changes the value
     * of the {@link #icon} config internally.
     * @param {String} icon The path to an image to display in the button
     * @return {Ext.button.Button} this
     */
    setIcon: function(icon) {
        var me = this,
            btnIconEl = me.btnIconEl,
            oldIcon = me.icon || '';
        icon = icon || '';
        // If setIcon is called when we are configured with a glyph, clear the glyph
        if (me.glyph) {
            me.setGlyph(null);
        }
        me.icon = icon;
        if (icon !== oldIcon) {
            if (btnIconEl) {
                btnIconEl.removeCls(me.iconCls);
                btnIconEl.setStyle('background-image', icon ? 'url(' + icon + ')' : '');
                me._syncHasIconCls();
                if (me.didIconStateChange(oldIcon, icon)) {
                    me.updateLayout();
                }
            }
            me.fireEvent('iconchange', me, oldIcon, icon);
        }
        return me;
    },
    /**
     * Sets the CSS class that provides a background image to use as the button's icon. This method
     * also changes the value of the {@link #iconCls} config internally.
     * @param {String} cls The CSS class providing the icon image
     * @return {Ext.button.Button} this
     */
    setIconCls: function(cls) {
        var me = this,
            btnIconEl = me.btnIconEl,
            oldCls = me.iconCls || '';
        cls = cls || '';
        // If setIcon is called when we are configured with a glyph, clear the glyph
        if (me.glyph) {
            me.setGlyph(null);
        }
        me.iconCls = cls;
        if (oldCls !== cls) {
            if (btnIconEl) {
                // In case it had been set to 'none' by a glyph setting.
                btnIconEl.setStyle('background-image', '');
                // Remove the previous iconCls from the button
                btnIconEl.removeCls(oldCls);
                btnIconEl.addCls(cls);
                me._syncHasIconCls();
                if (me.didIconStateChange(oldCls, cls)) {
                    me.updateLayout();
                }
            }
            me.fireEvent('iconchange', me, oldCls, cls);
        }
        return me;
    },
    applyGlyph: function(glyph, oldGlyph) {
        if (glyph) {
            if (!glyph.isGlyph) {
                glyph = new Ext.Glyph(glyph);
            }
            if (glyph.isEqual(oldGlyph)) {
                glyph = undefined;
            }
        }
        return glyph;
    },
    updateGlyph: function(glyph, oldGlyph) {
        var me = this,
            btnIconEl = me.btnIconEl,
            glyphCls = me._glyphCls;
        if (btnIconEl) {
            me.icon = null;
            btnIconEl.setStyle('background-image', '');
            if (glyph) {
                btnIconEl.dom.innerHTML = glyph.character;
                btnIconEl.addCls(glyphCls);
                btnIconEl.setStyle(glyph.getStyle());
            } else {
                btnIconEl.dom.innerHTML = '';
                btnIconEl.removeCls(glyphCls);
            }
            me._syncHasIconCls();
            if (me.didIconStateChange(oldGlyph, glyph)) {
                me.updateLayout();
            }
        }
        me.fireEvent('glyphchange', me, glyph && glyph.glyphConfig, oldGlyph && oldGlyph.glyphConfig);
        return me;
    },
    /**
     * Sets the tooltip for this Button.
     *
     * @param {String/Object} tooltip This may be:
     *
     *   - **String** : A string to be used as innerHTML (html tags are accepted) to show
     *    in a tooltip
     *   - **Object** : A configuration object for {@link Ext.tip.QuickTipManager#register}.
     *
     * @param initial
     * @return {Ext.button.Button} this
     */
    setTooltip: function(tooltip, initial) {
        var me = this;
        if (me.rendered) {
            if (!initial || !tooltip) {
                me.clearTip();
            }
            if (tooltip) {
                if (Ext.quickTipsActive && Ext.isObject(tooltip)) {
                    Ext.tip.QuickTipManager.register(Ext.apply({
                        target: me.el.id
                    }, tooltip));
                    me.tooltip = tooltip;
                } else {
                    me.el.dom.setAttribute(me.getTipAttr(), tooltip);
                }
            }
        } else {
            me.tooltip = tooltip;
        }
        return me;
    },
    updateIconAlign: function(align, oldAlign) {
        var me = this,
            btnEl, btnIconEl, hasIconCls;
        if (me.rendered) {
            btnEl = me.btnEl;
            btnIconEl = me.btnIconEl;
            hasIconCls = me._hasIconCls;
            if (oldAlign) {
                btnEl.removeCls(hasIconCls + '-' + oldAlign);
            }
            btnEl.addCls(hasIconCls + '-' + align);
            // move the iconWrap to the correct position in the dom - before the btnInnerEl
            // for top/left alignments, and after the btnInnerEl for right/bottom
            if (align === 'top' || align === 'left') {
                btnEl.insertFirst(btnIconEl);
            } else {
                btnEl.appendChild(btnIconEl);
            }
            me.updateLayout();
        }
    },
    updateTextAlign: function(align, oldAlign) {
        var me = this,
            btnEl = me.btnEl,
            btnCls = me._btnCls;
        if (me.rendered) {
            btnEl.removeCls(btnCls + '-' + oldAlign);
            btnEl.addCls(btnCls + '-' + align);
        }
    },
    getTipAttr: function() {
        return this.tooltipType === 'qtip' ? 'data-qtip' : 'title';
    },
    /**
     * @private
     */
    getRefItems: function(deep) {
        var menu = this.menu,
            items = [];
        if (menu) {
            if (deep) {
                items = menu.getRefItems(deep);
            }
            items.unshift(menu);
        }
        return items;
    },
    /**
     * @private
     */
    clearTip: function() {
        var me = this,
            el = me.el;
        if (Ext.quickTipsActive && Ext.isObject(me.tooltip)) {
            Ext.tip.QuickTipManager.unregister(el);
        } else {
            el.dom.removeAttribute(me.getTipAttr());
        }
    },
    doDestroy: function() {
        var me = this,
            menu = me.menu;
        if (me.deferFocusTimer) {
            Ext.undefer(me.deferFocusTimer);
            me.deferFocusTimer = null;
        }
        if (me.rendered) {
            me.clearTip();
        }
        Ext.destroy(me.repeater);
        if (menu && me.destroyMenu) {
            me.menu = Ext.destroy(menu);
        }
        Ext.button.Manager.unregister(me);
        me.callParent();
    },
    /**
     * Assigns this Button's click handler
     * @param {Function} handler The function to call when the button is clicked
     * @param {Object} [scope] The scope (`this` reference) in which the handler function
     * is executed. Defaults to this Button.
     * @return {Ext.button.Button} this
     */
    setHandler: function(handler, scope) {
        this.handler = handler;
        if (arguments.length > 1) {
            this.scope = scope;
        }
        return this;
    },
    updateText: function(text, oldText) {
        var me = this,
            btnInnerEl = me.btnInnerEl,
            btnEl = me.btnEl;
        // Coerce to string. Maybe set to a numeric value.
        text = text == null ? '' : String(text);
        oldText = oldText || '';
        if (me.rendered) {
            btnInnerEl.setHtml(text || '&#160;');
            btnEl[text ? 'addCls' : 'removeCls'](me._textCls);
            btnEl[text ? 'removeCls' : 'addCls'](me._noTextCls);
            me.updateLayout();
        }
        me.fireEvent('textchange', me, oldText, text);
    },
    /**
     * Checks if the icon/iconCls changed from being empty to having a value, or having a value
     * to being empty.
     * @private
     * @param {String} old The old icon/iconCls
     * @param {String} current The current icon/iconCls
     * @return {Boolean} True if the icon state changed
     */
    didIconStateChange: function(old, current) {
        var currentEmpty = Ext.isEmpty(current);
        return Ext.isEmpty(old) ? !currentEmpty : currentEmpty;
    },
    /**
     * Programmatically activate the button.
     *
     * @param {Ext.event.Event} [e] Optional event to process.
     */
    click: function(e) {
        return this.onClick(e);
    },
    /**
     * Sets the `pressed` state of this button.
     * @param {Boolean} [pressed=true] Pass `false` to clear the `pressed` state.
     * @return {Ext.button.Button} this
     */
    setPressed: function(pressed) {
        return this.toggle(pressed !== false);
    },
    /**
     * If a state it passed, it becomes the pressed state otherwise the current state is toggled.
     * @param {Boolean} [state] Force a particular state
     * @param {Boolean} [suppressEvent=false] True to stop events being fired when calling
     * this method.
     * @return {Ext.button.Button} this
     */
    toggle: function(state, suppressEvent) {
        var me = this,
            ariaDom = me.ariaEl.dom;
        if (!me.enableToggle) {
            return me;
        }
        state = state === undefined ? !me.pressed : !!state;
        // Allow toggle to be vetoed in case a toggle group needs to enforce a mimimum pressed state
        if (me.fireEvent('beforetoggle', me, state) !== false) {
            if (state !== me.pressed) {
                me[state ? 'addCls' : 'removeCls'](me._pressedCls);
                me.pressed = state;
                if (ariaDom) {
                    ariaDom.setAttribute('aria-pressed', state);
                }
                if (!suppressEvent) {
                    me.fireEvent('toggle', me, state);
                    Ext.callback(me.toggleHandler, me.scope, [
                        me,
                        state
                    ], 0, me);
                    if (me.publishState) {
                        me.publishState('pressed', state);
                    }
                }
            }
        }
        return me;
    },
    maybeShowMenu: function(e) {
        if (this.menu) {
            this.showMenu(e);
        }
    },
    /**
     * Shows this button's menu (if it has one)
     * @param clickEvent (private)
     */
    showMenu: function(clickEvent) {
        var me = this,
            menu = me.menu,
            isPointerEvent = !clickEvent || clickEvent.pointerType;
        if (menu && me.rendered) {
            if (me.tooltip && Ext.quickTipsActive && me.getTipAttr() !== 'title') {
                Ext.tip.QuickTipManager.getQuickTip().cancelShow(me.el);
            }
            if (menu.isVisible()) {
                // Click/tap toggles the menu visibility.
                if (isPointerEvent) {
                    menu.hide();
                } else {
                    menu.focus();
                }
            } else if (!clickEvent || me.showEmptyMenu || menu.items.getCount() > 0) {
                // Pointer-invoked menus do not auto focus, key invoked ones do.
                // Note that this behavior is inconsistent with WAI-ARIA specification
                // requirements, per which only Down Arrow key should activate the menu;
                // pressing Space or Enter key should open the menu but not focus it.
                // However no other accessible framework implements it that way;
                // both Dojo and YUI will activate the menu on either Space, Enter, or
                // Down Arrow keys. Furthermore, testing with JAWS screen reader
                // proved that this non-standard behavior is in fact expected since
                // JAWS will announce a menu button as follows: <name> button menu,
                // Press Space to activate the menu then navigate with arrow keys.
                // So without further ado we choose to keep the existing historical
                // Ext JS behavior which, by coincidence, happens to be congruent
                // with the industry standard. :)
                menu.autoFocus = !isPointerEvent;
                menu.showBy(me.el, me.menuAlign);
            }
        }
        return me;
    },
    /**
     * Hides this button's menu (if it has one)
     */
    hideMenu: function() {
        if (this.hasVisibleMenu()) {
            this.menu.hide();
        }
        return this;
    },
    /**
     * Returns true if the button has a menu and it is visible
     * @return {Boolean}
     */
    hasVisibleMenu: function() {
        var menu = this.menu;
        return menu && menu.rendered && menu.isVisible();
    },
    /**
     * @private
     */
    onRepeatClick: function(repeat, e) {
        this.onClick(e);
    },
    onTouchStart: function(e) {
        if (this.disabled) {
            this.doPreventDefault(e);
        }
    },
    /**
     * @private
     */
    onEnterKey: function(e) {
        if (!this.href) {
            this.onClick(e);
            // Buttons always intercept Space and Enter keys
            e.stopEvent();
            return false;
        }
    },
    /**
     * @private
     */
    onClick: function(e) {
        var me = this;
        // Event is optional if we're called from click()
        if (e) {
            me.doPreventDefault(e);
        }
        // Can be triggered by ENTER or SPACE keydown events which set the button property.
        // Only veto event handling if it's a mouse event with an alternative button.
        // Checking e.button for a truthy value (instead of != 0) also allows touch events
        // (tap) to continue, as they do not have a button property defined.
        if (e && e.type !== 'keydown' && e.button) {
            return;
        }
        if (!me.disabled) {
            me.doToggle();
            me.maybeShowMenu(e);
            me.fireHandler(e);
        }
    },
    doToggle: function() {
        var me = this;
        if (me.allowDepress !== false || !me.pressed) {
            me.toggle();
        }
    },
    doPreventDefault: function(e) {
        if (e && (this.preventDefault || (this.disabled && this.getHref()))) {
            e.preventDefault();
        }
    },
    fireHandler: function(e) {
        var me = this;
        // Click may have destroyed the button
        if (me.fireEvent('click', me, e) !== false && !me.destroyed) {
            Ext.callback(me.handler, me.scope, [
                me,
                e
            ], 0, me);
        }
    },
    /**
     * @private
     * mouseover handler called when a mouseover event occurs anywhere within the encapsulating
     * element. The targets are interrogated to see what is being entered from where.
     * @param e
     */
    onMouseOver: function(e) {
        var me = this;
        if (!me.disabled && !e.within(me.el, true, true)) {
            me.onMouseEnter(e);
        }
    },
    /**
     * @private
     * mouseout handler called when a mouseout event occurs anywhere within the encapsulating
     * element - or the mouse leaves the encapsulating element.
     * The targets are interrogated to see what is being exited to where.
     * @param e
     */
    onMouseOut: function(e) {
        var me = this;
        if (!e.within(me.el, true, true)) {
            if (me.overMenuTrigger) {
                me.onMenuTriggerOut(e);
            }
            me.onMouseLeave(e);
        }
    },
    /**
     * @private
     * mousemove handler called when the mouse moves anywhere within the encapsulating element.
     * The position is checked to determine if the mouse is entering or leaving the trigger area.
     * Using mousemove to check this is more resource intensive than we'd like, but it is necessary
     * because the trigger area does not line up exactly with sub-elements so we don't always get
     * mouseover/out events when needed. In the future we should consider making the trigger
     * a separate element that is absolutely positioned and sized over the trigger area.
     */
    onMouseMove: function(e) {
        var me = this,
            over = me.overMenuTrigger;
        if (me.split) {
            if (me.isWithinTrigger(e)) {
                if (!over) {
                    me.onMenuTriggerOver(e);
                }
            } else if (over) {
                me.onMenuTriggerOut(e);
            }
        }
    },
    /**
     * @protected
     * Returns true if the passed event's x/y coordinates are within the trigger region
     * @param {Ext.event.Event} e
     */
    isWithinTrigger: function(e) {
        var me = this,
            el = me.el,
            overPosition, triggerRegion;
        overPosition = (me.arrowAlign === 'right') ? e.getX() - me.getX() : e.getY() - el.getY();
        triggerRegion = me.getTriggerRegion();
        return overPosition > triggerRegion.begin && overPosition < triggerRegion.end;
    },
    /**
     * @private
     * Returns an object containing `begin` and `end` properties that indicate the
     * left/right bounds of a right trigger or the top/bottom bounds of a bottom trigger.
     * @return {Object}
     */
    getTriggerRegion: function() {
        var me = this,
            region = me._triggerRegion,
            isRight = me.arrowAlign === 'right',
            getEnd = isRight ? 'getRight' : 'getBottom',
            btnSize = isRight ? me.getWidth() : me.getHeight();
        region.begin = btnSize - (me.el[getEnd]() - me.btnEl[getEnd]());
        region.end = btnSize;
        return region;
    },
    /**
     * @private
     * virtual mouseenter handler called when it is detected that the mouseout event
     * signified the mouse entering the encapsulating element.
     * @param e
     */
    onMouseEnter: function(e) {
        // overCls is handled by Component
        this.fireEvent('mouseover', this, e);
    },
    /**
     * @private
     * virtual mouseleave handler called when it is detected that the mouseover event
     * signified the mouse entering the encapsulating element.
     * @param e
     */
    onMouseLeave: function(e) {
        // overCls is handled by Component
        this.fireEvent('mouseout', this, e);
    },
    /**
     * @private
     * virtual mouseenter handler called when it is detected that the mouseover event
     * signified the mouse entering the arrow area of the button - the `<em>`.
     * @param e
     */
    onMenuTriggerOver: function(e) {
        var me = this,
            arrowTip = me.arrowTooltip;
        me.overMenuTrigger = true;
        // We don't have a hoverable arrow element, so we only add the tip attribute if
        // we're over that part of the button
        if (me.split && arrowTip) {
            me.btnWrap.dom.setAttribute(me.getTipAttr(), arrowTip);
        }
        me.fireEvent('menutriggerover', me, me.menu, e);
    },
    /**
     * @private
     * virtual mouseleave handler called when it is detected that the mouseout event
     * signified the mouse leaving the arrow area of the button - the `<em>`.
     * @param e
     */
    onMenuTriggerOut: function(e) {
        var me = this;
        delete me.overMenuTrigger;
        // See onMenuTriggerOver
        if (me.split && me.arrowTooltip) {
            me.btnWrap.dom.setAttribute(me.getTipAttr(), '');
        }
        me.fireEvent('menutriggerout', me, me.menu, e);
    },
    onEnable: function() {
        var me = this,
            href = me.href,
            hrefTarget = me.hrefTarget,
            dom = me.el.dom;
        me.callParent();
        me.removeCls(me._disabledCls);
        me.el.setTabIndex(me.tabIndex);
        // https://sencha.jira.com/browse/EXTJS-11964
        // Disabled links are clickable on iPad, and right clickable on desktop browsers.
        // The only way to completely disable navigation is removing the href
        if (href) {
            dom.href = href;
        }
        if (hrefTarget) {
            dom.target = hrefTarget;
        }
    },
    onDisable: function() {
        var me = this,
            dom = me.el.dom;
        me.callParent();
        me.addCls(me._disabledCls);
        me.removeCls(me.overCls);
        me.el.setTabIndex(null);
        // https://sencha.jira.com/browse/EXTJS-11964
        // Disabled links are clickable on iPad, and right clickable on desktop browsers.
        // The only way to completely disable navigation is clearing the href
        if (me.href) {
            dom.removeAttribute('href');
        }
        if (me.hrefTarget) {
            dom.removeAttribute('target');
        }
    },
    /**
     * Method to change the scale of the button. See {@link #scale} for allowed configurations.
     * @param {String} scale The scale to change to.
     */
    setScale: function(scale) {
        var me = this,
            ui = me.ui.replace('-' + me.scale, '');
        // check if it is an allowed scale
        if (!Ext.Array.contains(me.allowedScales, scale)) {
            throw new Error('#setScale: scale must be an allowed scale (' + me.allowedScales.join(', ') + ')');
        }
        me.scale = scale;
        me.setUI(ui);
    },
    setUI: function(ui) {
        var me = this;
        // we need to append the scale to the UI, if not already done
        if (me.scale && !ui.match(me.scale)) {
            ui = ui + '-' + me.scale;
        }
        me.callParent([
            ui
        ]);
    },
    /**
     * @private
     */
    onMouseDown: function(e) {
        var me = this,
            activeEl;
        if (Ext.isIE || Ext.isEdge || e.pointerType === 'touch') {
            // In IE the use of unselectable on the button's elements causes the element
            // to not receive focus, even when it is directly clicked.
            // On Touch devices, we need to explicitly focus on touchstart.
            if (me.deferFocusTimer) {
                Ext.undefer(me.deferFocusTimer);
            }
            activeEl = Ext.Element.getActiveElement();
            me.deferFocusTimer = Ext.defer(function() {
                var focusEl;
                me.deferFocusTimer = null;
                // We can't proceed if we've been destroyed, or the app has since controlled
                // the focus, or if we are no longer focusable.
                if (me.destroying || me.destroyed || (Ext.Element.getActiveElement() !== activeEl) || !me.canFocus()) {
                    return;
                }
                focusEl = me.getFocusEl();
                // Deferred to give other mousedown handlers the chance to preventDefault
                if (focusEl && !e.defaultPrevented) {
                    focusEl.focus();
                }
            }, 1);
        }
        if (!me.disabled && e.button === 0) {
            Ext.button.Manager.onButtonMousedown(me, e);
            me.removeCls(me._arrowPressedCls);
            me.addCls(me._pressedCls);
        }
    },
    /**
     * @private
     */
    onMouseUp: function(e) {
        var me = this;
        // If the external mouseup listener of the ButtonManager fires after the button
        // has been destroyed, ignore.
        if (!me.destroyed && e.button === 0) {
            if (!me.pressed) {
                me.removeCls(me._pressedCls);
            }
        }
    },
    /**
     * @private
     */
    onMenuShow: function() {
        var me = this;
        me.addCls(me._menuActiveCls);
        me.fireEvent('menushow', me, me.menu);
    },
    /**
     * @private
     */
    onMenuHide: function(e) {
        var me = this;
        me.removeCls(me._menuActiveCls);
        me.fireEvent('menuhide', me, me.menu);
    },
    /**
     * @private
     */
    onDownKey: function(e) {
        var me = this;
        if (me.menu && !me.disabled) {
            me.showMenu(e);
            e.stopEvent();
            return false;
        }
    },
    updateArrowVisible: function(visible) {
        var me = this;
        if (me.rendered) {
            if (visible) {
                if (me.menu || me.isSplitButton) {
                    me.split = true;
                    me._addSplitCls();
                }
            } else {
                me._removeSplitCls();
                me.split = false;
            }
        }
        return visible;
    },
    privates: {
        elClsMap: {
            'btnWrap': '_btnWrapCls',
            'btnEl': '_btnCls',
            'btnIconEl': '_baseIconCls',
            'btnInnerEl': '_innerCls'
        },
        addUIToElement: function() {
            this.callParent();
            this.updateChildElsUICls(true);
        },
        addOverCls: function() {
            if (!this.disabled) {
                this.addCls(this.overCls);
            }
        },
        _addSplitCls: function() {
            var me = this;
            me.btnWrap.addCls(me.getSplitCls());
        },
        /**
         * @private
         * Needed for when widget is rendered into a grid cell. The class to add to the cell
         * element. Override needed to add scale to the mix which is part of the ui name in the
         * mixin and the CSS rule.
         */
        getTdCls: function() {
            return Ext.baseCSSPrefix + 'button-' + this.ui + '-' + this.scale + '-cell';
        },
        /**
         * @private
         * @return {Number/String} The button value, used for segmented button API compatibility
         * with modern.
         */
        getValue: function() {
            return this.value;
        },
        removeUIFromElement: function() {
            this.callParent();
            this.updateChildElsUICls(false);
        },
        removeOverCls: function() {
            this.removeCls(this.overCls);
        },
        _removeSplitCls: function() {
            var me = this;
            me.btnWrap.removeCls(me.getSplitCls());
        },
        _syncHasIconCls: function() {
            var me = this,
                btnEl = me.btnEl,
                hasIconCls = me._hasIconCls;
            if (btnEl) {
                btnEl[me._hasIcon() ? 'addCls' : 'removeCls']([
                    hasIconCls,
                    hasIconCls + '-' + me.iconAlign
                ]);
            }
        },
        /**
         * Returns true if this button has an icon (either icon, iconCls, or glyph)
         * @return {Boolean}
         * @private
         */
        _hasIcon: function() {
            return !!(this.icon || this.iconCls || this.glyph);
        },
        updateChildElsUICls: function(add) {
            var me = this,
                ui = me.ui,
                state = add ? 'addCls' : 'removeCls',
                elClsMap = me.elClsMap,
                key, el, mapCls, cls;
            for (key in elClsMap) {
                el = me[key];
                mapCls = elClsMap[key];
                cls = me[mapCls];
                if (el && cls) {
                    el[state](cls + '-' + ui);
                }
            }
        },
        wrapPrimaryEl: function(dom) {
            this.el = new Ext.dom.ButtonElement(dom);
            this.callParent([
                dom
            ]);
        }
    }
});

/**
 * A split button that provides a built-in dropdown arrow that can fire an event separately
 * from the default click event of the button. Typically this would be used to display a dropdown
 * menu that provides additional options to the primary button action, but any custom handler
 * can provide the arrowclick implementation.
 * Example usage:
 *
 *     @example
 *     // display a dropdown menu:
 *     Ext.create('Ext.button.Split', {
 *         renderTo: Ext.getBody(),
 *         text: 'Options',
 *         // handle a click on the button itself
 *         handler: function() {
 *             alert("The button was clicked");
 *         },
 *         menu: new Ext.menu.Menu({
 *             items: [
 *                 // these will render as dropdown menu items when the arrow is clicked:
 *                 {text: 'Item 1', handler: function(){ alert("Item 1 clicked"); }},
 *                 {text: 'Item 2', handler: function(){ alert("Item 2 clicked"); }}
 *             ]
 *         })
 *     });
 *
 * Instead of showing a menu, you can provide any type of custom functionality you want when
 * the dropdown arrow is clicked:
 *
 *     Ext.create('Ext.button.Split', {
 *         renderTo: 'button-ct',
 *         text: 'Options',
 *         handler: optionsHandler,
 *         arrowHandler: myCustomHandler
 *     });
 *
 */
Ext.define('Ext.button.Split', {
    extend: 'Ext.button.Button',
    alternateClassName: 'Ext.SplitButton',
    alias: 'widget.splitbutton',
    isSplitButton: true,
    /**
     * @cfg {Function/String} arrowHandler
     * A function called when the arrow button is clicked (can be used instead of click event)
     * @cfg {Ext.button.Split} arrowHandler.this
     * @cfg {Event} arrowHandler.e The click event.
     * @controllable
     */
    /**
     * @cfg {String} arrowTooltip
     * The title attribute of the arrow.
     */
    /**
     * @cfg {Boolean} [separateArrowStyling=false] If enabled, arrow element mouseover,
     * click, and focus events will be handled separately from main element and
     * corresponding hover, pressed, and focused states will be added separately
     * to main element and arrow element, respectively.
     *
     * This requires theme support for extended states (see Graphite theme).
     *
     * @since 6.6.0
     * @private
     */
    separateArrowStyling: false,
    /**
     * @private
     */
    arrowCls: 'split',
    split: true,
    componentCls: Ext.baseCSSPrefix + 'split-button',
    /**
     * @event arrowclick
     * Fires when this button's arrow is clicked.
     * @param {Ext.button.Split} this
     * @param {Event} e The click event.
     */
    // It is possible to use both menu and arrowHandler with a Split button, which is confusing
    // and will clash with WAI-ARIA requirements. So we check that and warn if need be.
    // Unfortunately this won't work with arrowclick event that can be subscribed to
    // dynamically but we don't want to run these checks at run time so there's a limit
    // to what we can do about it.
    initComponent: function() {
        var me = this;
        // Don't warn if we're under the slicer. Only check hasListeners of the component
        // instance; there could be listeners on the EventBus inherited via prototype.
        if (me.menu && (me.arrowHandler || me.hasListeners.hasOwnProperty('arrowclick'))) {
            Ext.ariaWarn(me, "Using both menu and arrowHandler config options in Split buttons " + "leads to confusing user experience and conflicts with accessibility " + "best practices. See WAI-ARIA 1.0 Authoring guide: " + "http://www.w3.org/TR/wai-aria-practices/#menubutton");
        }
        me.callParent();
    },
    getTemplateArgs: function() {
        var me = this,
            ariaAttr, data;
        data = me.callParent();
        if (me.disabled) {
            data.tabIndex = null;
        }
        ariaAttr = me.ariaArrowElAttributes || {};
        ariaAttr['aria-hidden'] = !!me.hidden;
        ariaAttr['aria-disabled'] = !!me.disabled;
        if (me.arrowTooltip) {
            ariaAttr['aria-label'] = me.arrowTooltip;
        } else {
            ariaAttr['aria-labelledby'] = me.id;
        }
        data.arrowElAttributes = ariaAttr;
        return data;
    },
    onRender: function() {
        var me = this,
            el;
        me.callParent();
        el = me.getFocusEl();
        if (el) {
            el.on({
                scope: me,
                focus: me.onMainElFocus,
                blur: me.onMainElBlur
            });
        }
        el = me.arrowEl;
        if (el) {
            el.dom.setAttribute('data-componentid', me.id);
            el.setVisibilityMode(Ext.dom.Element.DISPLAY);
            el.on({
                scope: me,
                focus: me.onArrowElFocus,
                blur: me.onArrowElBlur
            });
        }
    },
    /**
     * Sets this button's arrow click handler.
     * @param {Function} handler The function to call when the arrow is clicked.
     * @param {Object} scope (optional) Scope for the function passed above.
     */
    setArrowHandler: function(handler, scope) {
        this.arrowHandler = handler;
        this.scope = scope;
    },
    onMouseDown: function(e) {
        var me = this;
        if (me.separateArrowStyling && !me.disabled && e.button === 0 && me.isWithinTrigger(e)) {
            e.preventDefault();
            me.arrowEl.focus();
            Ext.button.Manager.onButtonMousedown(me, e);
            me.addCls(me._arrowPressedCls);
        } else {
            me.callParent([
                e
            ]);
        }
    },
    onMouseUp: function(e) {
        var me = this;
        if (me.separateArrowStyling && !me.destroyed && e.button === 0 && me.isWithinTrigger(e)) {
            me.removeCls(me._arrowPressedCls);
        } else {
            me.callParent([
                e
            ]);
        }
    },
    onMenuTriggerOver: function(e) {
        var me = this;
        if (me.separateArrowStyling && !me.disabled) {
            me.addCls(me._arrowOverCls);
        }
        me.callParent([
            e
        ]);
    },
    onMenuTriggerOut: function(e) {
        var me = this;
        if (me.separateArrowStyling && !me.disabled) {
            me.removeCls(me._arrowOverCls);
        }
        me.callParent([
            e
        ]);
    },
    /**
     * @private
     */
    onClick: function(e) {
        var me = this,
            arrowKeydown = e.type === 'keydown' && e.target === me.arrowEl.dom;
        me.doPreventDefault(e);
        if (!me.disabled) {
            if (arrowKeydown || me.isWithinTrigger(e)) {
                // Force prevent default here, if we click on the arrow part
                // we want to trigger the menu, not any link if we have it
                e.preventDefault();
                me.maybeShowMenu(e);
                me.fireEvent("arrowclick", me, e);
                if (me.arrowHandler) {
                    me.arrowHandler.call(me.scope || me, me, e);
                }
            } else {
                me.doToggle();
                me.fireHandler(e);
            }
        }
    },
    enable: function(silent) {
        var me = this,
            arrowEl = me.arrowEl;
        me.callParent([
            silent
        ]);
        // May not be rendered yet
        if (arrowEl) {
            arrowEl.dom.setAttribute('tabIndex', me.tabIndex);
            arrowEl.dom.setAttribute('aria-disabled', 'false');
        }
    },
    disable: function(silent) {
        var me = this,
            arrowEl = me.arrowEl;
        me.callParent([
            silent
        ]);
        // May not be rendered yet
        if (arrowEl) {
            arrowEl.dom.removeAttribute('tabIndex');
            arrowEl.dom.setAttribute('aria-disabled', 'true');
        }
    },
    afterHide: function(cb, scope) {
        this.callParent([
            cb,
            scope
        ]);
        this.arrowEl.dom.setAttribute('aria-hidden', 'true');
    },
    afterShow: function(animateTarget, cb, scope) {
        this.callParent([
            animateTarget,
            cb,
            scope
        ]);
        this.arrowEl.dom.setAttribute('aria-hidden', 'false');
    },
    privates: {
        isFocusing: function(e) {
            var me = this,
                from = e.fromElement,
                to = e.toElement,
                focusEl = me.focusEl && me.focusEl.dom,
                arrowEl = me.arrowEl && me.arrowEl.dom;
            if (me.focusable) {
                if (to === focusEl) {
                    return from === arrowEl ? false : true;
                } else if (to === arrowEl) {
                    return from === focusEl ? false : true;
                }
                return true;
            }
            return false;
        },
        isBlurring: function(e) {
            var me = this,
                from = e.fromElement,
                to = e.toElement,
                focusEl = me.focusEl && me.focusEl.dom,
                arrowEl = me.arrowEl && me.arrowEl.dom;
            if (me.focusable) {
                if (from === focusEl) {
                    return to === arrowEl ? false : true;
                } else if (from === arrowEl) {
                    return to === focusEl ? false : true;
                }
                return true;
            }
            return false;
        },
        // We roll our own focus style handling for Split button, see below
        getFocusClsEl: Ext.privateFn,
        onMainElFocus: function(e) {
            this.el.addCls(this._focusCls);
        },
        onMainElBlur: function(e) {
            this.el.removeCls(this._focusCls);
        },
        onArrowElFocus: function(e) {
            this.el.addCls(this._arrowFocusCls);
        },
        onArrowElBlur: function() {
            this.el.removeCls(this._arrowFocusCls);
        },
        setTabIndex: function(newTabIndex) {
            this.callParent([
                newTabIndex
            ]);
            // May not be rendered yet
            if (this.arrowEl) {
                this.arrowEl.set({
                    tabIndex: newTabIndex
                });
            }
        },
        // This and below are called by the setMenu method in the parent class.
        _addSplitCls: function() {
            var arrowEl = this.arrowEl;
            this.callParent();
            arrowEl.dom.setAttribute('tabIndex', this.tabIndex);
            arrowEl.setVisible(true);
        },
        _removeSplitCls: function() {
            var arrowEl = this.arrowEl;
            this.callParent();
            arrowEl.dom.removeAttribute('tabIndex');
            arrowEl.setVisible(false);
        }
    }
});

/**
 * A specialized SplitButton that contains a menu of {@link Ext.menu.CheckItem} elements. The button
 * automatically cycles through each menu item on click, raising the button's {@link #change} event
 * (or calling the button's {@link #changeHandler} function, if supplied) for the active menu item.
 * Clicking on the arrow section of the button displays the dropdown menu just like a normal
 * SplitButton.
 * Example usage:
 *
 *     @example
 *     Ext.create('Ext.button.Cycle', {
 *         showText: true,
 *         prependText: 'View as ',
 *         renderTo: Ext.getBody(),
 *         menu: {
 *             id: 'view-type-menu',
 *             items: [{
 *                 text: 'text only',
 *                 iconCls: 'view-text',
 *                 checked: true
 *             },{
 *                 text: 'HTML',
 *                 iconCls: 'view-html'
 *             }]
 *         },
 *         changeHandler: function(cycleBtn, activeItem) {
 *             Ext.Msg.alert('Change View', activeItem.text);
 *         }
 *     });
 */
Ext.define('Ext.button.Cycle', {
    extend: 'Ext.button.Split',
    alternateClassName: 'Ext.CycleButton',
    alias: 'widget.cycle',
    /**
     * @cfg {Object[]} items
     * An array of {@link Ext.menu.CheckItem} **config** objects to be used when creating
     * the button's menu items (e.g., `{text:'Foo', iconCls:'foo-icon'}`)
     * 
     * @deprecated 4.0 Use the {@link #cfg-menu} config instead. All menu items will be created as
     * {@link Ext.menu.CheckItem CheckItems}.
     */
    /**
     * @cfg {Boolean} [showText=false]
     * True to display the active item's text as the button text. The Button will show its
     * configured {@link #text} if this config is omitted.
     */
    /**
     * @cfg {String} [prependText='']
     * A static string to prepend before the active item's text when displayed as the button's text
     * (only applies when showText = true).
     */
    /**
     * @cfg {Function/String} [changeHandler=undefined]
     * A callback function that will be invoked each time the active menu item in the button's menu
     * has changed. If this callback is not supplied, the SplitButton will instead fire the
     * {@link #change} event on active item change. The changeHandler function will be called with
     * the following argument list: (SplitButton this, Ext.menu.CheckItem item)
     * @controllable
     */
    /**
     * @cfg {String} forceIcon
     * A css class which sets an image to be used as the static icon for this button. This icon will
     * always be displayed regardless of which item is selected in the dropdown list. This overrides
     * the default behavior of changing the button's icon to match the selected item's icon
     * on change.
     */
    /**
     * @cfg {Number/String} forceGlyph
     * The charCode to be used as the static icon for this button.  This icon will always be
     * displayed regardless of which item is selected in the dropdown list. This override
     * the default behavior of changing the button's icon to match the selected item's icon
     * on change. This property expects a format consistent with that of {@link #glyph}
     */
    /**
     * @property {Ext.menu.Menu} menu
     * The {@link Ext.menu.Menu Menu} object used to display the
     * {@link Ext.menu.CheckItem CheckItems} representing the available choices.
     */
    /**
     * @event change
     * Fires after the button's active menu item has changed. Note that if a {@link #changeHandler}
     * function is set on this CycleButton, it will be called instead on active item change and this
     * change event will not be fired.
     * @param {Ext.button.Cycle} this
     * @param {Ext.menu.CheckItem} item The menu item that was selected
     */
    /**
     * @private
     */
    getButtonText: function(item) {
        var me = this,
            text = '';
        if (item && me.showText === true) {
            if (me.prependText) {
                text += me.prependText;
            }
            text += item.text;
            return text;
        }
        return me.text;
    },
    /**
     * Sets the button's active menu item.
     * @param {Ext.menu.CheckItem} item The item to activate
     * @param {Boolean} [suppressEvent=false] True to prevent the {@link #change} event and
     * {@link #changeHandler} from firing.
     */
    setActiveItem: function(item, suppressEvent) {
        var me = this,
            changeHandler = me.changeHandler,
            forceIcon = me.forceIcon,
            forceGlyph = me.forceGlyph;
        me.settingActive = true;
        if (!Ext.isObject(item)) {
            item = me.menu.getComponent(item);
        }
        if (item) {
            me.setText(me.getButtonText(item));
            me.setIconCls(forceIcon ? forceIcon : item.iconCls);
            me.setGlyph(forceGlyph ? forceGlyph : item.glyph);
            me.activeItem = item;
            if (!item.checked) {
                item.setChecked(true, false);
            }
            if (!suppressEvent) {
                if (changeHandler) {
                    Ext.callback(changeHandler, me.scope, [
                        me,
                        item
                    ], 0, me);
                }
                me.fireEvent('change', me, item);
            }
        }
        me.settingActive = false;
    },
    /**
     * Gets the currently active menu item.
     * @return {Ext.menu.CheckItem} The active item
     */
    getActiveItem: function() {
        return this.activeItem;
    },
    /**
     * @private
     */
    initComponent: function() {
        var me = this,
            checked = 0,
            items, i, iLen, item;
        // Ext JS Cycle buttons are implemented in a way that clashes with WAI-ARIA requirements,
        // so we warn the developer about that.
        Ext.ariaWarn(me, "Using Cycle buttons is not recommended in accessible " + "applications, because their behavior conflicts " + "with accessibility best practices. See WAI-ARIA 1.0 " + "Authoring guide: http://www.w3.org/TR/wai-aria-practices/#menubutton");
        // Allow them to specify a menu config which is a standard Button config.
        // Remove direct use of "items" in 5.0.
        items = (me.menu.items || []).concat(me.items || []);
        me.menu = Ext.applyIf({
            cls: Ext.baseCSSPrefix + 'cycle-menu',
            items: []
        }, me.menu);
        iLen = items.length;
        // Convert all items to CheckItems
        for (i = 0; i < iLen; i++) {
            item = items[i];
            item = Ext.applyIf({
                group: me.id,
                itemIndex: i,
                checkHandler: me.checkHandler,
                scope: me,
                checked: item.checked || false
            }, item);
            me.menu.items.push(item);
            if (item.checked) {
                checked = i;
            }
        }
        me.itemCount = me.menu.items.length;
        me.callParent(arguments);
        me.on('click', me.toggleSelected, me);
        me.setActiveItem(checked, true);
    },
    /**
     * @private
     */
    checkHandler: function(item, pressed) {
        if (pressed && !this.settingActive) {
            this.setActiveItem(item);
        }
    },
    /**
     * This is normally called internally on button click, but can be called externally to advance
     * the button's active item programmatically to the next one in the menu. If the current item
     * is the last one in the menu the active item will be set to the first item in the menu.
     */
    toggleSelected: function() {
        var me = this,
            m = me.menu,
            checkItem;
        checkItem = me.activeItem.next(':not([disabled])') || m.items.getAt(0);
        checkItem.setChecked(true);
    }
});

/**
 * This class is used internally to manage the layout of `Ext.button.Segmented`.
 * @private
 */
Ext.define('Ext.layout.container.SegmentedButton', {
    extend: 'Ext.layout.container.Container',
    alias: 'layout.segmentedbutton',
    needsItemSize: false,
    setsItemSize: false,
    _btnRowCls: Ext.baseCSSPrefix + 'segmented-button-row',
    getRenderTree: function() {
        var me = this,
            result = me.callParent(),
            i, ln;
        if (me.owner.getVertical()) {
            for (i = 0 , ln = result.length; i < ln; i++) {
                result[i] = {
                    cls: me._btnRowCls,
                    cn: result[i]
                };
            }
        }
        return result;
    },
    getItemLayoutEl: function(item) {
        var dom = item.el.dom;
        return this.owner.getVertical() ? dom.parentNode : dom;
    },
    onDestroy: function() {
        var targetEl, row;
        // The items of a Segmented Button create an Ext.dom.Element reference
        // to their "container" element (see Ext.util.Renderable#finishRender)
        // for vertical Segmented Buttons this container ends up being the
        // 'segmented-button-row' element, which is not a childEl of either the container
        // or the layout and so it does not get automatically cleaned up upon destruction,
        // leaving the element orphaned, unless we destroy it now.
        if (this.owner.rendered) {
            targetEl = this.getRenderTarget();
            while ((row = targetEl.last())) {
                row.destroy();
            }
        }
    }
});

/**
 * SegmentedButton is a container for a group of {@link Ext.button.Button Button}s.  You 
 * may populate the segmented button's children by adding buttons to the items config.
 * The segmented  button's children enjoy the same customizations as regular buttons, such as 
 * menu, tooltip, etc.  You can see usages of the various configuration
 * possibilities in the example below.
 *
 *     @example preview 
 *     Ext.create('Ext.button.Segmented', {            
 *          renderTo: Ext.getBody(),
 *          allowMultiple: true,
 *          items: [{
 *               text: 'Segment Item 1',
 *               menu: [{
 *                   text: 'Menu Item 1'
 *               }]
 *          },{
 *               text: 'Segment Item 2',
 *               tooltip: 'My custom tooltip'
 *          },{
 *               text: 'Segment Item 3'
 *          }],
 *          listeners: {
 *               toggle: function(container, button, pressed) {
 *                    console.log("User toggled the '" + button.text + "' button: " +
 *                                (pressed ? 'on' : 'off'));
 *               }
 *          }
 *     });
 * 
 */
Ext.define('Ext.button.Segmented', {
    extend: 'Ext.container.Container',
    xtype: 'segmentedbutton',
    requires: [
        'Ext.button.Button',
        'Ext.layout.container.SegmentedButton'
    ],
    config: {
        /**
         * @cfg {Boolean} allowDepress
         * Allow toggling the pressed state of each button.
         * Only applicable when {@link #allowMultiple} is `false`.
         */
        allowDepress: false,
        /**
         * @cfg {Boolean} allowMultiple
         * Allow multiple pressed buttons.
         */
        allowMultiple: false,
        /**
         * @cfg {Boolean} forceSelection
         * If {@link #allowMultiple} is `true`, this config may be set to `true` to indicate that
         * at least one button in the set must remain pressed at all times.
         *
         * If no {@link #value} is configured, and no child buttons are configured `pressed`,
         * the first child button is set `pressed: true`
         */
        forceSelection: false,
        /**
         * @cfg {Boolean} allowToggle
         * True to enable pressed/not pressed toggling.
         */
        allowToggle: true,
        /**
         * @cfg {Boolean} vertical
         * True to align the buttons vertically
         */
        vertical: false,
        /**
         * @cfg {String} defaultUI
         * Default {@link Ext.Component#ui ui} to use for buttons in this segmented button.
         * Buttons can override this default by specifying their own UI
         */
        defaultUI: 'default'
    },
    beforeRenderConfig: {
        /**
         * @cfg {String/Number/String[]/Number[]} value
         * @accessor
         * The value of this button.  When {@link #allowMultiple} is `false`, value is a
         * String or Number.  When {@link #allowMultiple} is `true`, value is an array
         * of values.  A value corresponds to a child button's {@link Ext.button.Button#value
         * value}, or its index if no child button values match the given value.
         *
         * Using the `value` config of the child buttons with single toggle:
         *
         *     @example
         *     var button = Ext.create('Ext.button.Segmented', {
         *         renderTo: Ext.getBody(),
         *         value: 'optTwo', // begin with "Option Two" selected
         *         items: [{
         *             text: 'Option One',
         *             value: 'optOne'
         *         }, {
         *             text: 'Option Two',
         *             value: 'optTwo'
         *         }, {
         *             text: 'Option Three',
         *             value:  'optThree'
         *         }]
         *     });
         *
         *     console.log(button.getValue()); // optTwo
         *
         *     // Sets the value to optOne, and sets the pressed state of the "Option One" button
         *     button.setValue('optOne');
         *
         *     console.log(button.getValue()); // optOne
         *
         * Using multiple toggle, and index-based values:
         *
         *     @example
         *     var button = Ext.create('Ext.button.Segmented', {
         *         renderTo: Ext.getBody(),
         *         allowMultiple: true
         *         value: [1, 2], // begin with "Option Two" and "Option Three" selected
         *         items: [{
         *             text: 'Option One'
         *         }, {
         *             text: 'Option Two'
         *         }, {
         *             text: 'Option Three'
         *         }]
         *     });
         *
         *     // Sets value to [0, 2], and sets pressed state of "Option One" and "Option Three"
         *     button.setValue([0, 2]);
         *
         *     console.log(button.getValue()); // [0, 2]
         *
         *     // Remove all pressed buttons, and set value to null
         *     button.setValue(null);
         */
        value: undefined
    },
    /**
     * @property defaultBindProperty
     * @inheritdoc
     */
    defaultBindProperty: 'value',
    /**
     * @cfg publishes
     * @inheritdoc
     */
    publishes: [
        'value'
    ],
    /**
     * @cfg twoWayBindable
     * @inheritdoc
     */
    twoWayBindable: [
        'value'
    ],
    /**
     * @cfg layout
     * @inheritdoc
     */
    layout: 'segmentedbutton',
    /**
     * @cfg defaultType
     * @inheritdoc
     */
    defaultType: 'button',
    /**
     * @property maskOnDisable
     * @inheritdoc
     */
    maskOnDisable: false,
    isSegmentedButton: true,
    /**
     * @cfg baseCls
     * @inheritdoc
     */
    baseCls: Ext.baseCSSPrefix + 'segmented-button',
    itemCls: Ext.baseCSSPrefix + 'segmented-button-item',
    /**
     * @private
     */
    _firstCls: Ext.baseCSSPrefix + 'segmented-button-first',
    /**
     * @private
     */
    _lastCls: Ext.baseCSSPrefix + 'segmented-button-last',
    /**
     * @private
     */
    _middleCls: Ext.baseCSSPrefix + 'segmented-button-middle',
    /**
     * @event toggle
     * Fires when any child button's pressed state has changed.
     * @param {Ext.button.Segmented} this
     * @param {Ext.button.Button} button The toggled button.
     * @param {Boolean} isPressed `true` to indicate if the button was pressed.
     */
    /**
     * @event change
     * Fires when any child button's pressed state has changed and caused the value to change.
     * @param {Ext.button.Segmented} this
     * @param {Array} newValue The new value.
     * @param {Array} oldValue The old value.
     */
    applyValue: function(value, oldValue) {
        var me = this,
            allowMultiple = me.getAllowMultiple(),
            buttonValue, button, values, oldValues, items, i, ln, hasPressed;
        values = (value instanceof Array) ? value : (value == null) ? [] : [
            value
        ];
        oldValues = (oldValue instanceof Array) ? oldValue : (oldValue == null) ? [] : [
            oldValue
        ];
        // Set a flag to tell our toggle listener not to respond to the buttons' toggle
        // events while we are applying the value.
        me._isApplyingValue = true;
        if (!me.rendered) {
            // first time - add values of buttons with an initial config of pressed:true
            items = me.items.items;
            for (i = items.length - 1; i >= 0; i--) {
                button = items[i];
                // If we've got back to zero with no pressed buttons and have forceSelection,
                // then make button zero pressed.
                if (me.forceSelection && !i && !hasPressed) {
                    button.pressed = true;
                }
                if (button.pressed) {
                    hasPressed = true;
                    buttonValue = button.value;
                    if (buttonValue == null) {
                        buttonValue = me.items.indexOf(button);
                    }
                    // We're looping backwards, unshift the values into the front of the array
                    if (!Ext.Array.contains(values, buttonValue)) {
                        values.unshift(buttonValue);
                    }
                }
            }
        }
        ln = values.length;
        if (ln > 1 && !allowMultiple) {
            Ext.raise('Cannot set multiple values when allowMultiple is false');
        }
        // press all buttons corresponding to the values
        for (i = 0; i < ln; i++) {
            value = values[i];
            button = me._lookupButtonByValue(value);
            if (button) {
                buttonValue = button.value;
                if ((buttonValue != null) && buttonValue !== value) {
                    // button has a value, but it was matched by index.
                    // transform the index into the button value
                    values[i] = buttonValue;
                }
                if (!button.pressed) {
                    button.setPressed(true);
                }
            } else {
                // no matched button. fail.
                Ext.raise("Invalid value '" + value + "' for segmented button: '" + me.id + "'");
            }
        }
        value = allowMultiple ? values : ln ? values[0] : null;
        // unpress buttons for the old values, if they do not exist in the new values array
        for (i = 0 , ln = oldValues.length; i < ln; i++) {
            oldValue = oldValues[i];
            if (!Ext.Array.contains(values, oldValue)) {
                me._lookupButtonByValue(oldValue).setPressed(false);
            }
        }
        me._isApplyingValue = false;
        return value;
    },
    updateValue: function(value, oldValue) {
        var me = this,
            same;
        if (me.hasListeners.change) {
            if (value && oldValue && me.getAllowMultiple()) {
                same = Ext.Array.equals(value, oldValue);
            }
            if (!same) {
                me.fireEvent('change', me, value, oldValue);
            }
        }
    },
    beforeRender: function() {
        var me = this;
        me.addCls(me.baseCls + me._getClsSuffix());
        me._syncItemClasses(true);
        me.callParent();
    },
    onAdd: function(item) {
        var me = this,
            syncItemClasses = '_syncItemClasses';
        // eslint-disable-next-line vars-on-top, one-var
        var items = me.items.items,
            ln = items.length,
            i = 0,
            value, defaultUI;
        if (item.ui === 'default' && !item.hasOwnProperty('ui')) {
            defaultUI = me.getDefaultUI();
            if (defaultUI !== 'default') {
                item.ui = defaultUI;
            }
        }
        for (; i < ln; i++) {
            if (items[i] !== item) {
                value = items[i].value;
                if (value != null && value === item.value) {
                    Ext.raise("Segmented button '" + me.id + "' cannot contain multiple items with value: '" + value + "'");
                }
            }
        }
        me.mon(item, {
            hide: syncItemClasses,
            show: syncItemClasses,
            beforetoggle: '_onBeforeItemToggle',
            toggle: '_onItemToggle',
            scope: me
        });
        if (me.getAllowToggle()) {
            item.enableToggle = true;
            if (!me.getAllowMultiple()) {
                item.toggleGroup = me.getId();
                item.allowDepress = me.getAllowDepress();
            }
        }
        item.addCls(me.itemCls + me._getClsSuffix());
        me._syncItemClasses();
        me.callParent([
            item
        ]);
    },
    onRemove: function(item) {
        var me = this;
        item.removeCls(me.itemCls + me._getClsSuffix());
        me._syncItemClasses();
        me.callParent([
            item
        ]);
    },
    beforeLayout: function() {
        if (Ext.isChrome) {
            // workaround for a chrome bug with table-layout:fixed elements where the element
            // is layed out with 0 width, for example, in the following test case, without
            // this workaround the segmented button has 0 width in chrome:
            //
            //     Ext.create({
            //        renderTo: document.body,
            //            xtype: 'toolbar',
            //            items: [{
            //            xtype: 'segmentedbutton',
            //            items: [{
            //                text: 'Foo'
            //            }]
            //        }]
            //    });
            //
            // reading offsetWidth corrects the issue.
            // eslint-disable-next-line no-unused-expressions
            this.el.dom.offsetWidth;
        }
        this.callParent();
    },
    updateDefaultUI: function(defaultUI) {
        var items = this.items,
            item, i, ln;
        if (this.rendered) {
            Ext.raise("Changing the ui config of a segmented button after render is not supported.");
        } else if (items) {
            if (items.items) {
                // Mixed collection already created
                items = items.items;
            }
            for (i = 0 , ln = items.length; i < ln; i++) {
                item = items[i];
                if (item.ui === 'default' && defaultUI !== 'default' && !item.hasOwnProperty('ui')) {
                    items[i].ui = defaultUI;
                }
            }
        }
    },
    updateAllowDepress: function(newAllowDepress, oldAllowDepress) {
        if (this.rendered && (newAllowDepress !== oldAllowDepress)) {
            Ext.raise("Changing the allowDepress config of a segmented button after render " + "is not supported.");
        }
    },
    updateAllowMultiple: function(newAllowMultiple, oldAllowMultiple) {
        if (this.rendered && (newAllowMultiple !== oldAllowMultiple)) {
            Ext.raise("Changing the allowMultiple config of a segmented button after render " + "is not supported.");
        }
    },
    updateAllowToggle: function(newAllowToggle, oldAllowToggle) {
        if (this.rendered && (newAllowToggle !== oldAllowToggle)) {
            Ext.raise("Changing the allowToggle config of a segmented button after render " + "is not supported.");
        }
    },
    updateVertical: function(newVertical, oldVertical) {
        if (this.rendered && (newVertical !== oldVertical)) {
            Ext.raise("Changing the orientation of a segmented button after render " + "is not supported.");
        }
    },
    privates: {
        _getClsSuffix: function() {
            return this.getVertical() ? '-vertical' : '-horizontal';
        },
        // rtl hook
        _getFirstCls: function() {
            return this._firstCls;
        },
        // rtl hook
        _getLastCls: function() {
            return this._lastCls;
        },
        /**
         * Looks up a child button by its value
         * @private
         * @param {String/Number} value The button's value or index
         * @return {Ext.button.Button}
         */
        _lookupButtonByValue: function(value) {
            var items = this.items.items,
                ln = items.length,
                i = 0,
                button = null,
                buttonValue, btn;
            for (; i < ln; i++) {
                btn = items[i];
                buttonValue = btn.value;
                if ((buttonValue != null) && buttonValue === value) {
                    button = btn;
                    break;
                }
            }
            if (!button && typeof value === 'number') {
                // no button matched by value, assume value is an index
                button = items[value];
            }
            return button;
        },
        _onBeforeItemToggle: function(button, pressed) {
            // If we allow multiple selections, and we are forcing a selection, and we are
            // unpressing and we only have one value, then veto this. we are not allowing
            // the selection length to fall to zero.
            if (this.allowMultiple && this.forceSelection && !pressed && this.getValue().length === 1) {
                return false;
            }
        },
        /**
         * Handles the "toggle" event of the child buttons.
         * @private
         * @param {Ext.button.Button} button
         * @param {Boolean} pressed
         */
        _onItemToggle: function(button, pressed) {
            if (this._isApplyingValue) {
                return;
            }
            // eslint-disable-next-line vars-on-top
            var me = this,
                Array = Ext.Array,
                allowMultiple = me.allowMultiple,
                buttonValue = (button.value != null) ? button.value : me.items.indexOf(button),
                value = me.getValue(),
                valueIndex;
            if (allowMultiple) {
                valueIndex = Array.indexOf(value, buttonValue);
            }
            if (pressed) {
                if (allowMultiple) {
                    if (valueIndex === -1) {
                        // We must not mutate our value property here
                        value = Array.slice(value);
                        value.push(buttonValue);
                    }
                } else {
                    value = buttonValue;
                }
            } else {
                if (allowMultiple) {
                    if (valueIndex > -1) {
                        // We must not mutate our value property here
                        value = Array.slice(value);
                        value.splice(valueIndex, 1);
                    }
                } else if (value === buttonValue) {
                    value = null;
                }
            }
            me.setValue(value);
            me.fireEvent('toggle', me, button, pressed);
        },
        /**
         * Synchronizes the "first", "last", and "middle" css classes when buttons are
         * added, removed, shown, or hidden
         * @private
         * @param {Boolean} force force sync even if not rendered.
         */
        _syncItemClasses: function(force) {
            var me = this,
                firstCls, middleCls, lastCls, items, ln, visibleItems, item, i;
            if (!force && !me.rendered) {
                return;
            }
            firstCls = me._getFirstCls();
            middleCls = me._middleCls;
            lastCls = me._getLastCls();
            items = me.items.items;
            ln = items.length;
            visibleItems = [];
            for (i = 0; i < ln; i++) {
                item = items[i];
                if (!item.hidden) {
                    visibleItems.push(item);
                }
            }
            ln = visibleItems.length;
            // remove all existing classes from visible items
            for (i = 0; i < ln; i++) {
                visibleItems[i].removeCls([
                    firstCls,
                    middleCls,
                    lastCls
                ]);
            }
            // do not add any classes if there is only one item (no border removal needed)
            if (ln > 1) {
                // add firstCls to the first visible button
                visibleItems[0].addCls(firstCls);
                // add middleCls to all visible buttons in between
                for (i = 1; i < ln - 1; i++) {
                    visibleItems[i].addCls(middleCls);
                }
                // add lastCls to the first visible button
                visibleItems[ln - 1].addCls(lastCls);
            }
        }
    }
});

/**
 * Abstract base class for common functionality shared between {@link Ext.panel.Header}
 * and {@link Ext.tab.Bar}
 * @private
 * @abstract
 */
Ext.define('Ext.panel.Bar', {
    extend: 'Ext.container.Container',
    vertical: false,
    _verticalSides: {
        left: 1,
        right: 1
    },
    initComponent: function() {
        var me = this,
            vertical = me.vertical;
        me.dock = me.dock || (vertical ? 'left' : 'top');
        me.layout = Ext.apply(vertical ? {
            type: 'vbox',
            align: 'middle',
            alignRoundingMethod: 'ceil'
        } : {
            type: 'hbox',
            align: 'middle',
            alignRoundingMethod: 'floor'
        }, me.layout);
        this.callParent();
    },
    onAdded: function(container, pos, instanced) {
        this.initOrientation();
        this.callParent([
            container,
            pos,
            instanced
        ]);
    },
    onRemoved: function(destroying) {
        this.removeClsWithUI(this.uiCls);
        this.callParent([
            destroying
        ]);
    },
    beforeRender: function() {
        var me = this;
        if (me.forceOrientation || !me.ownerCt) {
            // allows bars to be rendered directly to body with no owner container
            me.initOrientation();
        }
        me.callParent();
    },
    setDock: function(dock) {
        var me = this,
            layout, vertical;
        if (dock !== me.dock) {
            Ext.suspendLayouts();
            me.clearOrientation();
            me.callParent([
                dock
            ]);
            me.initOrientation();
            vertical = me.vertical;
            layout = me.layout;
            layout.setVertical(vertical);
            layout.setAlignRoundingMethod(vertical ? 'ceil' : 'floor');
            Ext.resumeLayouts(true);
        }
    },
    privates: {
        clearOrientation: function() {
            this.removeClsWithUI([
                this.vertical ? 'vertical' : 'horizontal',
                this.getDockName()
            ]);
        },
        getDockName: function() {
            return this.dock;
        },
        initOrientation: function() {
            var me = this,
                dock = me.dock,
                vertical = (me.vertical = (dock ? dock in me._verticalSides : me.vertical));
            me.addClsWithUI([
                vertical ? 'vertical' : 'horizontal',
                me.getDockName()
            ]);
        }
    }
});

/**
 * A basic title component for a Panel Header
 */
Ext.define('Ext.panel.Title', {
    extend: 'Ext.Component',
    xtype: 'title',
    requires: [
        'Ext.Glyph'
    ],
    isTitle: true,
    // layout system optimization.  Allows autocomponent layout to measure height without
    // having to first know the width.
    noWrap: true,
    // For performance reasons we give the following configs their default values on
    // the class body.  This prevents the updaters from running on initialization in the
    // default configuration scenario
    textAlign: 'left',
    iconAlign: 'left',
    rotation: 0,
    text: '&#160;',
    beforeRenderConfig: {
        /**
         * @cfg [textAlign='left']
         * @inheritdoc Ext.panel.Header#cfg-titleAlign
         * @accessor
         */
        textAlign: null,
        /**
         * @cfg {String} [text='&#160;']
         * The title's text (can contain html tags/entities)
         * @accessor
         */
        text: null,
        /**
         * @cfg glyph
         * @inheritdoc Ext.panel.Header#cfg-glyph
         * @accessor
         */
        glyph: null,
        /**
         * @cfg icon
         * @inheritdoc Ext.panel.Header#cfg-icon
         * @accessor
         */
        icon: null,
        /**
         * @cfg {'top'/'right'/'bottom'/'left'} [iconAlign='left']
         * alignment of the icon
         * @accessor
         */
        iconAlign: null,
        /**
         * @cfg iconCls
         * @inheritdoc Ext.panel.Header#cfg-iconCls
         * @accessor
         */
        iconCls: null,
        /**
         * @cfg [rotation=0]
         * @inheritdoc Ext.panel.Header#cfg-titleRotation
         * @accessor
         */
        rotation: null
    },
    /**
     * @cfg autoEl
     * @inheritdoc
     */
    autoEl: {
        role: 'presentation',
        // Required for Opera
        unselectable: 'on'
    },
    // In most cases the panel header title is purely presentational
    // and does not have any structural significance wrt Assistive Technologies.
    // The only exception is when the panel participates in Accordion layout;
    // in that case the title component has the role of 'tab' and its textEl
    // should not have any role to expose the text as the tab's accessible name.
    // Header component is aware of this participation and will reset textElRole.
    textElRole: 'presentation',
    // By default, panel title is not focusable; this only happens in Accordion layout.
    // This config option is overridable, and it will prime tabIndex to be used
    // without hardcoding it.
    /**
     * @cfg tabIndex
     * @inheritdoc
     */
    tabIndex: 0,
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        'textEl',
        'iconEl',
        'iconWrapEl'
    ],
    /**
     * @cfg renderTpl
     * @inheritdoc
     */
    renderTpl: '<tpl if="iconMarkup && iconBeforeTitle">{iconMarkup}</tpl>' + // unselectable="on" is required for Opera, other browsers
    // inherit unselectability from the header
    '<div id="{id}-textEl" data-ref="textEl"' + ' class="{textCls} {textCls}-{ui} {itemCls}{childElCls}" unselectable="on"' + '<tpl if="textElRole"> role="{textElRole}"</tpl>' + '>' + '{text}' + '</div>' + '<tpl if="iconMarkup && !iconBeforeTitle">{iconMarkup}</tpl>',
    iconTpl: '<div id="{id}-iconWrapEl" data-ref="iconWrapEl" role="presentation" ' + 'class="{iconWrapCls} {iconWrapCls}-{ui} {iconAlignCls} {itemCls}{childElCls}"' + '<tpl if="iconWrapStyle"> style="{iconWrapStyle}"</tpl>>' + '<div id="{id}-iconEl" data-ref="iconEl" role="presentation" unselectable="on" ' + 'class="{baseIconCls} {baseIconCls}-{ui} {iconCls} {glyphCls}" style="' + '<tpl if="iconUrl">background-image:url({iconUrl});</tpl>' + '<tpl if="glyph && glyphFontFamily">font-family:{glyphFontFamily};</tpl>">' + '<tpl if="glyph">{glyph}</tpl>' + '</div>' + '</div>',
    _textAlignClasses: {
        left: Ext.baseCSSPrefix + 'title-align-left',
        center: Ext.baseCSSPrefix + 'title-align-center',
        right: Ext.baseCSSPrefix + 'title-align-right'
    },
    _iconAlignClasses: {
        top: Ext.baseCSSPrefix + 'title-icon-top',
        right: Ext.baseCSSPrefix + 'title-icon-right',
        bottom: Ext.baseCSSPrefix + 'title-icon-bottom',
        left: Ext.baseCSSPrefix + 'title-icon-left'
    },
    _rotationClasses: {
        0: Ext.baseCSSPrefix + 'title-rotate-none',
        1: Ext.baseCSSPrefix + 'title-rotate-right',
        2: Ext.baseCSSPrefix + 'title-rotate-left'
    },
    _rotationAngles: {
        1: 90,
        2: 270
    },
    /**
     * @cfg baseCls
     * @inheritdoc
     */
    baseCls: Ext.baseCSSPrefix + 'title',
    _titleSuffix: '-title',
    _glyphCls: Ext.baseCSSPrefix + 'title-glyph',
    _iconWrapCls: Ext.baseCSSPrefix + 'title-icon-wrap',
    _baseIconCls: Ext.baseCSSPrefix + 'title-icon',
    _itemCls: Ext.baseCSSPrefix + 'title-item',
    _textCls: Ext.baseCSSPrefix + 'title-text',
    afterComponentLayout: function() {
        var me = this,
            rotation = me.getRotation(),
            lastBox, lastX, el;
        if (rotation && !Ext.isIE8) {
            // In IE8  we use a BasicImage filter to rotate the title
            // element 90 degrees.  The result is that what was the bottom left
            // corner is positioned exactly where the top left corner was
            // originally.  Since this is the desired result, no additional
            // positioning is needed in IE8.  In browsers that support CSS3 transform,
            // however, we use transform: rotate(90deg) to rotate the element.
            // CSS3 also provides a way to specify the position the rotated element
            // by changing the axis on which it is rotated using the transform-origin
            // property, but the required transform origin varies based on the
            // elements size, and would require some complex math to calculate.
            // To achieve the desired rotated position in modern browsers we use
            // a transform-origin of "0, 0" which means the top left corner of
            // the element is the rotation axis. After rotating 90 degrees we
            // simply move the element to the right by the same number of pixels
            // as its width.
            el = me.el;
            lastBox = me.lastBox;
            lastX = lastBox.x;
            el.setStyle(me._getVerticalAdjustDirection(), (lastX + ((rotation === 1) ? lastBox.width : -lastBox.height)) + 'px');
        }
        this.callParent();
    },
    onRender: function() {
        var me = this,
            rotation = me.getRotation(),
            el = me.el;
        me.callParent();
        if (rotation) {
            el.setVertical(me._rotationAngles[rotation]);
        }
        if (Ext.supports.FixedTableWidthBug) {
            // Workaround for https://bugs.webkit.org/show_bug.cgi?id=130239 and
            // https://code.google.com/p/chromium/issues/detail?id=377190
            // See styleHooks for more details
            el._needsTableWidthFix = true;
        }
    },
    applyText: function(text) {
        if (!text) {
            text = '&#160;';
        }
        return text;
    },
    beforeRender: function() {
        var me = this;
        me.callParent();
        me.addCls(me._rotationClasses[me.getRotation()]);
        me.addCls(me._textAlignClasses[me.getTextAlign()]);
    },
    getIconMarkup: function() {
        return this.lookupTpl('iconTpl').apply(this.getIconRenderData());
    },
    getIconRenderData: function() {
        var me = this,
            icon = me.getIcon(),
            iconCls = me.getIconCls(),
            glyph = me.getGlyph(),
            glyphFontFamily,
            iconAlign = me.getIconAlign();
        // Transform Glyph to the useful parts
        if (glyph) {
            glyphFontFamily = glyph.fontFamily;
            glyph = glyph.character;
        }
        return {
            id: me.id,
            ui: me.ui,
            itemCls: me._itemCls,
            iconUrl: icon,
            iconCls: iconCls,
            iconWrapCls: me._iconWrapCls,
            baseIconCls: me._baseIconCls,
            iconAlignCls: me._iconAlignClasses[iconAlign],
            glyph: glyph,
            glyphCls: glyph ? me._glyphCls : '',
            glyphFontFamily: glyphFontFamily
        };
    },
    initRenderData: function() {
        var me = this,
            iconAlign, renderData;
        renderData = Ext.apply({
            text: me.getText(),
            textElRole: me.textElRole,
            id: me.id,
            ui: me.ui,
            itemCls: me._itemCls,
            textCls: me._textCls,
            iconMarkup: null,
            iconBeforeTitle: null
        }, me.callParent());
        if (me._hasIcon()) {
            iconAlign = me.getIconAlign();
            renderData.iconMarkup = me.getIconMarkup();
            renderData.iconBeforeTitle = (iconAlign === 'top' || iconAlign === 'left');
        }
        return renderData;
    },
    onAdded: function(container, pos, instanced) {
        var me = this,
            suffix = me._titleSuffix,
            baseCls = container.baseCls;
        me.addCls([
            baseCls + suffix,
            baseCls + suffix + '-' + container.ui
        ]);
        me.callParent([
            container,
            pos,
            instanced
        ]);
    },
    applyGlyph: function(glyph, oldGlyph) {
        if (glyph) {
            if (!glyph.isGlyph) {
                glyph = new Ext.Glyph(glyph);
            }
            if (glyph.isEqual(oldGlyph)) {
                glyph = undefined;
            }
        }
        return glyph;
    },
    updateGlyph: function(glyph, oldGlyph) {
        var me = this,
            glyphCls = me._glyphCls,
            iconEl;
        if (me.rendered) {
            me._syncIconVisibility();
            iconEl = me.iconEl;
            if (glyph) {
                iconEl.dom.innerHTML = glyph.character;
                iconEl.addCls(glyphCls);
                iconEl.setStyle('font-family', glyph.fontFamily);
            } else if (oldGlyph !== glyph) {
                iconEl.dom.innerHTML = '';
                iconEl.removeCls(glyphCls);
            }
            if (me._didIconStateChange(oldGlyph, glyph)) {
                me.updateLayout();
            }
        }
    },
    updateIcon: function(icon, oldIcon) {
        var me = this,
            iconEl;
        icon = icon || '';
        if (me.rendered && icon !== oldIcon) {
            me._syncIconVisibility();
            iconEl = me.iconEl;
            iconEl.setStyle('background-image', icon ? 'url(' + icon + ')' : '');
            if (me._didIconStateChange(oldIcon, icon)) {
                me.updateLayout();
            }
        }
    },
    updateIconAlign: function(align, oldAlign) {
        var me = this,
            iconWrapEl = me.iconWrapEl,
            el, iconAlignClasses;
        if (me.iconWrapEl) {
            el = me.el;
            iconAlignClasses = me._iconAlignClasses;
            if (oldAlign) {
                iconWrapEl.removeCls(iconAlignClasses[oldAlign]);
            }
            iconWrapEl.addCls(iconAlignClasses[align]);
            // here we move the iconWrap to the correct position in the dom - before the
            // title el for top/left alignments, and after the title el for right/bottom
            if (align === 'top' || align === 'left') {
                el.insertFirst(iconWrapEl);
            } else {
                el.appendChild(iconWrapEl);
            }
            me.updateLayout();
        }
    },
    updateIconCls: function(cls, oldCls) {
        var me = this,
            iconEl;
        cls = cls || '';
        if (me.rendered && oldCls !== cls) {
            me._syncIconVisibility();
            iconEl = me.iconEl;
            if (oldCls) {
                iconEl.removeCls(oldCls);
            }
            iconEl.addCls(cls);
            if (me._didIconStateChange(oldCls, cls)) {
                me.updateLayout();
            }
        }
    },
    updateRotation: function(rotation, oldRotation) {
        var me = this,
            el, rotationClasses;
        if (me.rendered) {
            el = me.el;
            rotationClasses = me._rotationClasses;
            me.removeCls(rotationClasses[oldRotation]);
            me.addCls(rotationClasses[rotation]);
            el.setHorizontal();
            if (rotation) {
                el.setVertical(me._rotationAngles[rotation]);
            }
            // reset styles set by adjustTitlePosition (handles both rtl/ltr), and sizing
            // set by last layout run (this prevents parallel size from becoming perpendicular
            // size after rotation)
            el.setStyle({
                right: '',
                left: '',
                top: '',
                height: '',
                width: ''
            });
            me.lastBox = null;
            me.updateLayout();
        }
    },
    updateText: function(text) {
        if (this.rendered) {
            this.textEl.setHtml(text);
            this.updateLayout();
        }
    },
    updateTextAlign: function(align, oldAlign) {
        var me = this,
            textAlignClasses = me._textAlignClasses;
        if (me.rendered) {
            if (oldAlign) {
                me.removeCls(textAlignClasses[oldAlign]);
            }
            me.addCls(textAlignClasses[align]);
            me.updateLayout();
        }
    },
    privates: {
        // rtl hook
        _getVerticalAdjustDirection: function() {
            return 'left';
        },
        _didIconStateChange: function(old, current) {
            var currentEmpty = Ext.isEmpty(current);
            return Ext.isEmpty(old) ? !currentEmpty : currentEmpty;
        },
        _hasIcon: function() {
            return !!(this.getIcon() || this.getIconCls() || this.getGlyph());
        },
        _syncIconVisibility: function() {
            var me = this,
                el = me.el,
                hasIcon = me._hasIcon(),
                iconWrapEl = me.iconWrapEl,
                isBefore, iconAlign;
            if (hasIcon && !iconWrapEl) {
                // if an icon was configured, but we have not yet rendered an icon
                // element, we need to render it now.
                iconAlign = me.iconAlign;
                isBefore = (iconAlign === 'left' || iconAlign === 'top');
                el.dom.insertAdjacentHTML(isBefore ? 'afterbegin' : 'beforeend', me.getIconMarkup());
                iconWrapEl = me.iconWrapEl = el[isBefore ? 'first' : 'last']();
                me.iconEl = iconWrapEl.first();
            }
            if (iconWrapEl) {
                iconWrapEl.setDisplayed(hasIcon);
            }
        }
    }
});

/**
 * This class is used to display small visual icons in the header of a panel.
 *
 * There are a set of 25 icons that can be specified by using the {@link #cfg-type} config.
 * 
 * The {@link #cfg-glyph} config may be used to specify a font icon in any available font.
 *
 * The {@link #cfg-iconCls} config may be used to add a specific class name to the tool element.
 * 
 * The {@link #cfg-callback} config can be used to provide a function that will respond to any
 * click events. In general, this class will not be instantiated directly, rather it will be created
 * by specifying the {@link Ext.panel.Panel#tools} configuration on the Panel itself.
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *         width: 200,
 *         height: 200,
 *         renderTo: document.body,
 *         title: 'A Panel',
 *         tools: [{
 *             type: 'help',
 *             callback: function() {
 *                 // show help here
 *             }
 *         }, {
 *             itemId: 'reply',
 *             glyph: 'xf112@FontAwesome', // Reply icon
 *             hidden: true,
 *             callback: function() {
 *                 // do reply
 *             }
 *         }, {
 *             type: 'search',
 *             callback: function (panel) {
 *                 // do search
 *                 panel.down('#refresh').show();
 *             }
 *         }]
 *     });
 *
 * The `callback` config was added in Ext JS 4.2.1 as an alternative to {@link #handler}
 * to provide a more convenient list of arguments. In Ext JS 4.2.1 it is also possible to
 * pass a method name instead of a direct function:
 * 
 *      tools: [{
 *          type: 'help',
 *          callback: 'onHelp',
 *          scope: this
 *      },
 *      ...
 * 
 * The `callback` (or `handler`) name is looked up on the `scope` which will also be the
 * `this` reference when the method is called.
 */
Ext.define('Ext.panel.Tool', {
    extend: 'Ext.Component',
    uses: [
        'Ext.tip.QuickTipManager'
    ],
    xtype: 'tool',
    requires: [
        'Ext.Glyph'
    ],
    config: {
        /**
         * @cfg glyph
         * @inheritdoc Ext.panel.Header#cfg-glyph
         */
        glyph: null
    },
    /**
     * @property {Boolean} isTool
     * `true` in this class to identify an object as an instantiated Tool, or subclass thereof.
     */
    isTool: true,
    /**
     * @cfg baseCls
     * @inheritdoc
     */
    baseCls: Ext.baseCSSPrefix + 'tool',
    /**
     * @cfg disabledCls
     * @inheritdoc
     */
    disabledCls: Ext.baseCSSPrefix + 'tool-disabled',
    /**
     * @cfg
     * @private
     */
    toolPressedCls: Ext.baseCSSPrefix + 'tool-pressed',
    /**
     * @cfg
     * @private
     */
    toolOverCls: Ext.baseCSSPrefix + 'tool-over',
    /**
     * @cfg iconCls
     * @inheritdoc Ext.panel.Header#cfg-iconCls
     */
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        'toolEl'
    ],
    /* eslint-disable indent, max-len */
    /**
     * @cfg renderTpl
     * @inheritdoc
     */
    renderTpl: [
        '<div id="{id}-toolEl" data-ref="toolEl" class="{className} {childElCls}" role="presentation"' + '<tpl if="glyph">' + '<tpl if="glyphFontFamily">' + ' style="font-family:{glyphFontFamily};">' + '</tpl>' + '{glyph}' + '<tpl else>' + '>' + '</tpl>' + '</div>'
    ],
    /* eslint-enable indent, max-len */
    /**
     * @cfg {Ext.Component} toolOwner
     * The owner to report to the `callback` method. Default is `null` for the `ownerCt`.
     * This is automatically set to the owning `Ext.panel.Panel` when a tool is created as
     * a member of a panel's `tools`.
     * @since 4.2
     */
    toolOwner: null,
    /**
     * @cfg {Function/String} callback
     * A function to execute when the tool is clicked.  The callback is passed the
     * following params:
     * @param {Ext.Component} owner The logical owner of the tool. In a typical
     * `Ext.panel.Panel`, this is set to the owning panel. This value comes from the
     * `toolOwner` config.
     * @param {Ext.panel.Tool} callback.tool The tool that is calling
     * @param {Ext.event.Event} callback.event The click event
     * @since 4.2
     * @controllable
     */
    /**
     * @cfg {Function} handler
     * A function to execute when the tool is clicked. Arguments passed are:
     *
     * - **event** : Ext.event.Event - The click event.
     * - **toolEl** : Ext.Element - The tool Element.
     * - **owner** : Ext.panel.Header - The host panel header.
     * - **tool** : Ext.panel.Tool - The tool object
     *
     * @deprecated 4.2 Use `callback` instead.
     */
    /**
     * @cfg {Object} scope
     * The scope to execute the {@link #callback} or {@link #handler} function. Defaults
     * to the tool.
     */
    /* eslint-disable max-len */
    /**
     * @cfg {String} type
     * The type of tool to render. The following types are available:
     *
     * ##Classic Theme
     * (_desaturated images of the same type used in the gray theme_)
     * 
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76J+y49ns/yZDkO/9/8/k+9Hm/OHz/+b2/9To/tbq/+r5/wAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpDMTgzMjYwNDlBQTAxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpDMTgzMjYwMzlBQTAxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NTg1ZWVlOWMtZTMwNy0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NTg1ZWVlOWMtZTMwNy0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABE8QgECrDXLmvTHm4IQVhUCeZiECjEAITOzCa5LM92uvSD+/PcTKQDTMiobVYfkTLA8rhWI2fUlXi9liq10NBoKvODxYFcTor8oT6mQucEkEADs=" /> close
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76Nns/5+y4+/9/+r5/8/k++Hz/+b2/yZDkNHm/Nbq/9To/gAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpDMTgzMjYwODlBQTAxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpDMTgzMjYwNzlBQTAxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6OGNjYTgyOGYtZTMwNy0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6OGNjYTgyOGYtZTMwNy0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABEcQgECrDXLmvTHm4IQRZGkSIlCsbFukSCzPSHrceH6kRu//hpRCMSgaB8MUg5FoOhPL1GJxNE5Tgqx2K0gRuFuUJ9TJXM6SCAA7" /> minimize
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76DJVstns/+/9/8/k+9Hm/OHz/9To/tbq/+b2/+r5/wAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4RjRENjg0OTlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4RjRENjg0ODlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6OWNkZTA1ZDktZTMwNy0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6OWNkZTA1ZDktZTMwNy0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABEoQgECrDXLmvTHm4IQRZGkSIrAIbNsuqeLOSnoIR67fR1oIhaDwV0gZBIak8mhIIQSIqPSJSCVmrkRqwO16BynC14vyhDqZi1oSAQA7" /> maximize
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76DJVstns/+/9/5+y4+Hz/9To/tbq/+r5/+b2/8/k+9Hm/AAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4RjRENjg0RDlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4RjRENjg0QzlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6YWMyMzhiYzQtZTMwNy0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6YWMyMzhiYzQtZTMwNy0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABE4QgECrDXLmvTHm4IQRZGkSIpCsQusKSarMQmHbgpIavMD/vtTi5RouUozareBipA6Cg1QalaYQAoRWm9WmBsTWYJwijM9olCfUyVzekggAOw==" /> restore
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76Nns/zJVsu/9/9To/tbq/+r5/+b2/+Hz/8/k+9Hm/AAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4RjRENjg1MTlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4RjRENjg1MDlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ZDJkZWU5YjAtZTMwNy0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ZDJkZWU5YjAtZTMwNy0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABEcQgECrDXLmvTHm4IQRZGkSInCsbHukSBwPcpwmeDLseZIqwJ0QqEgtFsLkMVVoOp+FlGFKrRpSgqx2K0gRuFuUJ9TJXM6SCAA7" /> toggle
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPAOYAAP///5m76Nns/+/9/+b2/9bq/+Hz/9To/ur5/8/k+4KTytHm/IOPt3qJu5yw4qS54Wt1nbvS93J9rHqKt3OCuGJtopqt363D6UVNhKvA646g04mZxIOUy6m+6nqItVFYipSl0ZWo3K/F7llikp6x4oaUuXWDv3J/oKS55KG15KK246Kx0HWDsE9YkbXM8ZGj2oud0m55p7bN8o2f1KO453J/sX2Mv6/E7WZukV9ropao0XF/vaC054OUyImZzJ+z46e86AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpBQkE1MDM3QzlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpBQkE1MDM3QjlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6N2E3OTczMjQtZTMwOC0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6N2E3OTczMjQtZTMwOC0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAAB4OAAAABhIWGAYKDiYuLiIiMkIOIA5QDCgollQOSAAgIOhsXMCgeDp6cBAQPLiIyHRo9qZwGBjY3Eyc+NBS0nAkNERksCSApCjgJnAsNOUAqLw4WJiMLnAcHNSQ8PyEzMdecBQUQHxUcLTsS4pwC7QIMGAwr7pwD7vftm46RjYmH/4ICAQA7" /> gear
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76Nns/zJVsu/9/+r5/9bq/9To/ub2/9Hm/OHz/8/k+wAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpBQkE1MDM4MDlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpBQkE1MDM3RjlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ODdlOTI0MDctZTMwOC0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ODdlOTI0MDctZTMwOC0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABEwQgECrDXLmvTHm4IQRZGkSIlCs7MAWKSLLwzAjqaIr9bArqYWwNxAKU4lkoqdMpA5QaC16SBmu2AHWkBJ4v2BBihAGozyhTubClkQAADs=" /> prev
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76Nns/zJVsu/9/+r5/9bq/9To/ub2/9Hm/OHz/8/k+wAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpBQkE1MDM4NDlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpBQkE1MDM4MzlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6OTkzZWQ4NTgtZTMwOC0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6OTkzZWQ4NTgtZTMwOC0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABEwQgECrDXLmvTHm4IQRZGkSIlCsxcCyKSIjwzDLqaIr9bArqYVw0RsMU4lkoqdMpA7QQy0KTRmuhgEWmxJ4v2BBihAGozyhTubClkQAADs=" /> next
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76HGg3dns/9bq/+/9/+b2/9Hm/M/k++Hz/+r5/9To/rPN7QAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCQkE3MEQwNzlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCQkE3MEQwNjlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ZTliYzIzYWMtZTMwOC0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ZTliYzIzYWMtZTMwOC0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABFEQgECrDXLmvTHm4IQVpGCaZCECSisYZ6usRv2etbEmvJCcvMQKQRQgTkTE6sAUHE7Mw2qxOFmpK4JWK2BsCauBWNwdD1YFc1ms8oQ6mYtcEgEAOw==" /> pin
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76FCAsNns/+/9/+r5/9bq/+Hz/+b2/9To/rPN7dHm/M/k+wAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCQkE3MEQwQjlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCQkE3MEQwQTlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ZmE0NmQxMTQtZTMwOC0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ZmE0NmQxMTQtZTMwOC0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABEYQgECrDXLmvTHm4IQRZGkSIlCswuquKSILdC0g6aELes+ntqCAkVIYhYJFKsFEJlKGqCBKjaYG2Kx2kCJstShPqJO5mCURADs=" /> unpin
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76FCAsNns/+/9/+r5/9bq/9Hm/M/k++b2/+Hz/9To/gAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpDODE0QUIyODlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCQkE3MEQwRTlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6MGQ1ODZmOTQtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6MGQ1ODZmOTQtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABE0QgECrDXLmvTHm4IQRZGkSIlAUwsq6RZokgjDXc5IqfN0LvBRiiKgNjamDsrYUKFOLRS06jaYMBgE2uzWkBuCweJAijMUoT6iTubglEQA7" /> right
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76FCAsNns/+/9/+r5/9bq/9Hm/M/k++b2/+Hz/9To/gAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpDODE0QUIyQzlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpDODE0QUIyQjlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6MTg0MDAyYjMtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6MTg0MDAyYjMtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABE0QgECrDXLmvTHm4IQRZGkSIlCshcC6RZrMgkDbSaoo9d7vKUQNIRQQianDoaZkKlOLaE0qiKYMWIMguzWkBuCweJAijMUoT6iTubglEQA7" /> left
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76FCAsNns/+/9/8/k++Hz/9bq/9Hm/Ob2/9To/ur5/wAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpDODE0QUIzMDlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpDODE0QUIyRjlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6MjIyOTY4YWMtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6MjIyOTY4YWMtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABE4QgECrDXLmvTHm4IQRZGkSIrAsgrCy7pImdJvYdGrsRivwhlRhOBQQhykEoqVcCpQphbSloEpTh+zBpz2kBmCwIAxOEcholCfUyVzekggAOw==" /> down
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76FCAsNns/+/9/8/k++r5/9Hm/OHz/+b2/9bq/9To/gAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpEMzNBREI3NzlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpEMzNBREI3NjlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6MzJlYmU5YTUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6MzJlYmU5YTUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABE8QgECrDXLmvTHm4IQRJCmUpAgYrCG8raEm9JvYtIog784LO1VhOBQQh6qD8vASLA+qhfS1oEpVCsUrqxVkVYOweDxQEchjguYTkrAvcEkEADs=" /> up
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76FCAsNns/+/9/+Hz/9Hm/LPN7er5/8/k+9bq/9To/ub2/wAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpEMzNBREI3QjlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpEMzNBREI3QTlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NDFiYTk0NWUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NDFiYTk0NWUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABFEQgECrDXLmvTHm4IQRZCmUhAggLHIcgtCqDAPHeK0WhcD3Pp4qkTgkBMciUWVoxpoGQVO1WOBwh6pKwY29uFzVYDwQkMkqwnmd8oQ6mYtcEgEAOw==" /> refresh
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76Nns/+/9/8PX8s7i+FCAsOb2/+r5/9To/tbq/3Gg3c/k++Hz/9Hm/AAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpEMzNBREI3RjlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpEMzNBREI3RTlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NGQyMDlhMjUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NGQyMDlhMjUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABFQQgECrDXLmvTHm4IQNZGkOIoCsSLEULJIe9FEYRX2kTVP8uF8vxWAsDEjkophyOAhQA5TgTCWuCYIUm0gpvgrCggBWpATotFqQGqzVKE+ok7nYJREAOw==" /> plus
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAP///5m76Nns/+/9/+b2/9bq/+r5/9To/sPX8s7i+FCAsNHm/M/k++Hz/3Gg3QAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFMTU0OEYxMDlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFMTU0OEYwRjlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NTZkNTU2YTAtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NTZkNTU2YTAtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABEoQgECrDXLmvTHm4IQNZGkOImCsbGukRCzPRNo0Sa4nd8owDoVQ6PilFguEcolApg7QqPSQKliv2EJKwO16BanB14vyhDqZi1oSAQA7" /> minus
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPAOYAAP///5m76Nns/+/9/0FNZur5/+b2/9Hm/NTo/tbq/5u5++Hz/8/k+7TS/1ZqlaS251xtlV9xmTlBVEZZgTQ6TVFhh3CDsVdifm+LyYym4UFOby81RDhDXICd25a2/0ZVd8He/2l8p05cfnWLvWd8qHGGtmd6pGt9qIih2Gl9qmZ6pUJPaT9IYLza/259ollwnl5mcWd4opGgydzy/1VliWF0np7A/3CGtlZmik9ce4SVvKa56srl/09fgZmo0hwfKYuk4HqQyLbU/0JJXJCs6k9adJm2+XGFtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFMTU0OEYxNDlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFMTU0OEYxMzlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NjQ5Mjg0ODEtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NjQ5Mjg0ODEtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAAB4mAAAABhIWGAYKDiYuLiIiMkIOIA5QlFgMhLpQDkgAFBTxEOTMiKDufnQYGJkEaQBMdH6qdCw80Ni8YDh4VMgudDCc1PUU+LCscGwydB0cgCjoHMQ1GFwedCCktNzAIKkIjFAidCRAKJAQJOBkRPwmdAgTyBPEEEkMCnQMC/P39nI4iNUp0qKCgQAA7" /> search
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPAOYAAP///5m76Nns/+/9/+Hz/+r5/8/k++b2/9Hm/NTo/tbq/z5qq+bx79f0otf2ov//4IS/VGGLyYWo3Vd+u3OZ0WmQzWCJycfuh8fujIWi3e3183GY0FWDxm2L2pnJceXx7Xmf2+rz83CP4V2IyP//3en05GWNynuc3fr7/ebx7YKk1ubw74yv5G6TzGiL03mg2Xie2FN/wZjJb1F/w/v8/YS/UXee2WyN3ERwsFSBxGGKx3yj23Wa0GFnkF2Ev36l3ejz6vH4/32j3oGn4YCm3Xuh3Pz9/jppqmyLzW6X1HOa1FuGx3mc1fv8/kRtq4Gn4m6K1/r8/Xqg2f39/vf7/2KLyGiOzS5fpnif2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFMTU0OEYxODlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFMTU0OEYxNzlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NzBmM2IxMzUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NzBmM2IxMzUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAAB4+AAAABhIWGAYKDiYuLiIiMkIOIA0kbKlYVPBRVFgOSAAVLAC2CVEEsOgWfByNRTVNGKDQSMQefBCY/RDtSL1gwPgSfBhFPQ0JFIDZKEwafCDkhKQwrHxpMOAifCTNANTIeECUnRwmfChwkFw0OGA8ZCwqfAj0uIjcdUEhOVwKfAwICChToyVGkRokOKRQUCAA7" /> save
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPAMQAAP///5m76Nns/1CAsNTo/tbq/9Hm/O/9/8/k++Hz/+r5/+b2/4Spz4yvzmSQuuHy+FaFtMvi+VmHtbrT6LPO5Mbd9leGtNvu/ICmym6YwsTb9YSpyt/y/srg+Imtz3eexCH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGNEU0NDAyQjlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGNEU0NDAyQTlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ODczY2UxMWUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ODczY2UxMWUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABWMgAARkaQbimK4rirLwiB7Hs1lO89CHDCiKxmA4+AAVvsXCMaBMBhLlwpeoXjiYgaea8CG+CMYg0wH7DGgDBBJJG3yEOEFTkcd9hXxhqM/7BIACDAyBgD4HhYk9LjEtKSeQIiEAOw==" /> help
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPANUAAP///5m76Nns/1F/w+/9/7TS/32j3ur5/+Hz/+b2/9bq/9Hm/GyN3M/k+9To/nCP4YS/VHuh3Hmf22yLzVWDxoGn4m6K14Gn4Vd+u0Rtq1SBxHee2T5qq22L2miL05nJcXOa1ERwsOjz6pjJb4S/UTppqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGNEU0NDAyRjlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGNEU0NDAyRTlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6OWJjNzc2YmUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6OWJjNzc2YmUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABm5AACBALBoDwmFyuUQimdAhkkAYWAcGKlUKOBwGSaz3wE0kwEID1sxFINBhN7cRqFwMEckGhGlwFxpQASELXA4DIiQjHxAFASUOXAoUBZWWARwKXAIGHg8MHRYTGQwCXAQCqaqqBEpPUUKvR7NCQQA7" /> print
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAHGg3c/k+9To/ub2/1F/w////7TS/9ns/+/9/+r5/9Hm/OHz/9bq/wAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowRDYwMjk1MDlBQTIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGNEU0NDAzMjlBQTExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6YWY2NmEwNWUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6YWY2NmEwNWUtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABE2wFECrBXKGzXvAQJCN4xcSQ6qqiBkQ8ADHQ+KiqaEb6r3+i9uMIBhuKAFBUSnYGRQuJZFJjRZhVyLDRGXuBAcX4kAul1sgjxojubglEQA7" /> expand
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPALMAAHGg3c/k++b2/////9Hm/FF/w9ns/+/9/9To/tbq/+Hz/+r5/wAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowRDYwMjk1NDlBQTIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowRDYwMjk1MzlBQTIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6YzFhMDM4NmMtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1IiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6YzFhMDM4NmMtZTMwOS0xMTc3LTk0N2ItYmI5ZTVlNjcwMDI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA8ADwAABEVwDECrBXKGzXvAQJCN4xcWQqqqhxkU6EUtLrre9b0qdUHIoRqB4NkQKa+hcono/S6EhAmhLBIMroNhy+W2QEUPRgK0SCIAOw==" /> collapse
     * 
     * ##Neptune Theme
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQfAGZmZqamptbW1nNzc9nZ2eXl5ampqfv7+2pqanR0dNjY2H5+fnh4eGdnZ2xsbG5ubtfX129vb2hoaObm5qWlpaqqqvr6+qSkpI+Pj21tbY6OjoCAgH19fWlpaX9/f////yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpDRTk3OUEzMjlBQTQxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpDRTk3OUEzMzlBQTQxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkNFOTc5QTMwOUFBNDExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkNFOTc5QTMxOUFBNDExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAHwAsAAAAABAAEAAABYLgJ35TtSCIV03jeAQNIM/NZbnYrM/aIQYARyChSwQcgMCnIAFQPoSBbED4UACSgkHGqCoiEQWUITMsZgPIRyD4QKSyk+7RXj90qHldcJ9lzlNqbG5wABtbAF0fX2FjZUxJUHBUH0BZlUFDRUeRHwc5Ozo9LjChNTctSwYcHRmHBS0hADs=" /> close
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQUAGZmZqampqmpqfv7++Xl5Wpqan5+fmdnZ2hoaObm5o+Pj21tbaSkpI6Ojqqqqvr6+oCAgGlpaX19fX9/f////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0Qzg2QTQzQjlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo0Qzg2QTQzQzlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkNFOTc5QTM0OUFBNDExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjRDODZBNDNBOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAFAAsAAAAABAAEAAABVAgJVKJYxTF5CTjOAQHIM8H87jKrM/NIAa7ICBAISCEOwRBgAwKDM3dSdaqUmQoqnUkW0CjMwgTLBMYyUoKEEykDHLNngsWrN2qS0lkISa0QgA7" /> minimize
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQYAGZmZm9vb6mpqeXl5fv7+6ampt3d3Wpqat7e3n5+fmhoaGdnZ9/f321tbY6OjqqqqoCAgPr6+qSkpObm5o+Pj319fWlpaX9/f////wAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0Qzg2QTQzRjlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo0Qzg2QTQ0MDlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjRDODZBNDNEOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjRDODZBNDNFOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAGAAsAAAAABAAEAAABWogJmLTkxzH9UzjSBQLIM+LFLnUrM8OIRa7IKCAGShmAUYLI1MMBEhAsjUTJGaGgFQpmp1mmKwU0ZWhwGFtgDxrXGUj8XoGgcLj6qrxPkJomwMYQEI6RBgEOYQAPS4wQTU3S08VFg11gSMhADs=" /> maximize
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQWAGZmZvv7++Xl5ampqaamprKysrOzs2pqamdnZ2hoaH5+fvr6+oCAgI+Pj46Ojqqqqubm5m1tbaSkpH19fX9/f2lpaf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0Qzg2QTQ0MzlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo0Qzg2QTQ0NDlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjRDODZBNDQxOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjRDODZBNDQyOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAFgAsAAAAABAAEAAABWCgJVrQoxwH9UDjGBAIIM+ItLjNLBe6E4gEnczCmxEsgoQQICoCEoLBktmcDRTTVvGU1QJQAEPLMo1gw60pQyoTi6aD5My9hFqCc7LwaAnk5kI+LjBLNTdjURMVEWsCLSEAOw==" /> restore
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUkAGZmZmpqaqmpqaampvv7++Xl5WhoaGdnZ35+fubm5qqqqo+Pj6SkpPr6+oCAgH19fW1tbY6Oju7u7re3t39/f/X19cfHx/7+/sjIyG5ubuTk5MDAwHd3d2lpabW1tYGBgZeXl29vb9fX1/b29v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2RUQwMTJEQTlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2RUQwMTJEQjlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjZFRDAxMkQ4OUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjZFRDAxMkQ5OUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAJAAsAAAAABAAEAAABnFAkpCUUCACAYoiMRwSBgeAdHpgNJyLqXYaIQgH2zBgQCoYth/LeWooCLYcCcmzFSC0Ic0QpD1OAyJNIw9/AVIHGE1CCRlSEHcAE4okFxUbUg5vYmECZptabSRgn1JkJARZn11OUGFVV4puDx0QmQVNQQA7" /> toggle
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUqAGZmZvf394mJifX19WhoaGdnZ2tra/b29n9/f4yMjNLS0u/v7+np6Y2NjfDw8KCgoJeXl+bm5m9vb8bGxnd3d/Ly8uLi4mpqapiYmJqamvT09L6+vnh4ePHx8bCwsLi4uOjo6Le3t62trW1tbdPT06+vr3l5ebGxsePj47y8vP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2RUQwMTJERTlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2RUQwMTJERjlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjZFRDAxMkRDOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjZFRDAxMkREOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAKgAsAAAAABAAEAAABoxAlXB4AAAOw6QqoFChPMaSReUIDAMCgsDILTw4AqtqQeGauYSm8MGVQCASbmIIKhgTA+EgYSwwVAMhRgYVSRUGRh8DZg1KKg1nXAKOW1wDKUYEf0MMBEYbeR2IAAgRQhEIRhcLQxhdCAh2RiJDJJ6RXBSsSwImJyNmBhlaYksOKhoTRhN5CsVKRUeOQQA7" /> gear
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUkAGZmZmpqaqmpqaampvv7++Xl5WhoaGdnZ35+fubm5qqqqo+Pj6SkpPr6+oCAgH19fW1tbY6Oju7u7re3t39/f/X19cfHx/7+/sjIyG5ubuTk5MDAwHd3d2lpabW1tYGBgZeXl29vb9fX1/b29v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2RUQwMTJFMjlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4NTk0OEQzQzlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjZFRDAxMkUwOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjZFRDAxMkUxOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAJAAsAAAAABAAEAAABnJAkpCUUFACAYQiMRw2GAeAdHoYEIaEyHQ7XVxJAy7gMNkOSAUDN4AhbQ0FATckEnIFiC1HM+QiAlMfEk1rEFMWTW5bAQ5TBh6EWwhyWyAjJBd3aVwPCRVvBWBiGRtmQlliW15OUGJVX01xDhAdDwKhQ0EAOw==" /> prev
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUkAGZmZmpqaqmpqaampvv7++Xl5WhoaGdnZ35+fubm5qqqqo+Pj6SkpPr6+oCAgH19fW1tbY6Oju7u7re3t39/f/X19cfHx/7+/sjIyG5ubuTk5MDAwHd3d2lpabW1tYGBgZeXl29vb9fX1/b29v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4NTk0OEQzRjlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4NTk0OEQ0MDlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjg1OTQ4RDNEOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjg1OTQ4RDNFOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAJAAsAAAAABAAEAAABnJAkpCUUFACAYQiMRw2GAeAdHoYEIaEyHQ7XVxJg+0kyh2QCoYtCRPgGgoCrlAU4goQcqGGs0W01UMSH1MBEHlDFoQOhyQeaVIIcVsXJCMgdmhbFQkPbgVgWxsZXABmJFmkW15OUKRVX01wDhAdDwKfQ0EAOw==" /> next
     * - <img style="vertical-align:sub;margin-right:4px;" width="10" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhCgAQAMQZAGZmZvT09NHR0XNzc729vW5ubo2NjWpqavX19bCwsL6+vtLS0vz8/Pf395SUlLGxsXJycry8vMnJybi4uG1tbfr6+rKysnx8fH19ff///wAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4NTk0OEQ0MzlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4NTk0OEQ0NDlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjg1OTQ4RDQxOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjg1OTQ4RDQyOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAGQAsAAAAAAoAEAAABVNglgkGRRmCmCVA6yYjcBABQhwACURqNgGGAiDQCwAKg2ERMHDsej+HREZA2HCSCsTlglQyD27LIgoIXQWiKPzqNZLMRi+jaCnmGcYFw8BnFgs9IQA7" /> pin
     * - <img style="vertical-align:sub;margin-right:4px;" width="10" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhCgAQAMQbAGZmZvT09L29vff3942NjWpqanNzc21tbfX19XBwcLi4uJSUlNLS0ry8vNHR0bCwsNDQ0Pv7+3V1dX19fcnJyfz8/Lm5uW5ubr6+vrGxsbKysv///wAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCNTcyRjZBNjlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCNTcyRjZBNzlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjg1OTQ4RDQ1OUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjg1OTQ4RDQ2OUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAGwAsAAAAAAoAEAAABVXgJm4MM57VNFWniAEA1g4GbAynBsPZOCQ7QAK3sQRhlk1EcgRIIhRAQRBACAoAygLQOCkAi1rgFABcLoDxqHwgcL0AgkMqQFixjs2j+RhBCAdtECIhADs=" /> unpin
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUgAGZmZvv7++Xl5ampqaampmhoaGpqan5+fmdnZ+bm5pWVlfr6+pGRkaenp6SkpLu7u5mZmbKyssXFxY+Pj46Ojm1tbaysrICAgPn5+aqqqsPDw7e3t2lpaX19fX9/f7a2tv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCNTcyRjZBQTlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCNTcyRjZBQjlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkI1NzJGNkE4OUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkI1NzJGNkE5OUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAIAAsAAAAABAAEAAABnBAkBCUyHgMhkMmMRwuHAiAdIogBIYBynQ7nVxBhGlB0+ACCCBBYSoRWrgFwWAL+Ua4gwNX8f1sDwZmDBhCD1MGFYKEIIZSBhd7Xxt/c1N1QndbA2psbnACYGJkZmggWWZbXk5QZlVfTXIXFRwdm01BADs=" /> right
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUgAGZmZvv7++Xl5ampqaampmhoaGpqan5+fmdnZ+bm5pWVlfr6+pGRkaenp6SkpLu7u5mZmbKyssXFxY+Pj46Ojm1tbaysrICAgPn5+aqqqsPDw7e3t2lpaX19fX9/f7a2tv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCNTcyRjZBRTlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCNTcyRjZBRjlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkI1NzJGNkFDOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkI1NzJGNkFEOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAIAAsAAAAABAAEAAABnRAkBCUyHgMhkMmMRwuHAiAdIogBIYBynQ7nVxBBC6goSlMCSCBeWsRSqYFwYAbEQYg28Fhu7EruAcGUw9CGAxiBhWDhYdcBhdbH36Ac1t1IHd5alxtIG9ScWBiZGsAaJhaYl1fIE9RXFWsQ3IXFRwdAwJNQQA7" /> left
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUgAGZmZvv7++Xl5ampqaampmhoaGpqan5+fmdnZ+bm5pWVlfr6+pGRkaenp6SkpLu7u5mZmbKyssXFxY+Pj46Ojm1tbaysrICAgPn5+aqqqsPDw7e3t2lpaX19fX9/f7a2tv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpENUNDODdFNzlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpENUNDODdFODlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkI1NzJGNkIwOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkQ1Q0M4N0U2OUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAIAAsAAAAABAAEAAABntAkBAkGHQ4lctAMBwGCAiAdIpwLJyTqXZKCQgJ2zCAQCyIt4WitCCBaCESM2BwkGpAAYVU4dVILwZSDUIBDAxeIA1SFYFSFkIYGEIWUwZ1UxFNEVoHA1sbQh9bS3JTDw9oTGBnU2R4WaxdTlBhVVdNIAkZBwYGHhkJTUEAOw==" /> down
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUgAGZmZvv7++Xl5ampqaampmhoaGpqan5+fmdnZ+bm5pWVlfr6+pGRkaenp6SkpLu7u5mZmbKyssXFxY+Pj46Ojm1tbaysrICAgPn5+aqqqsPDw7e3t2lpaX19fX9/f7a2tv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpENUNDODdFQjlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpENUNDODdFQzlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkQ1Q0M4N0U5OUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkQ1Q0M4N0VBOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAIAAsAAAAABAAEAAABnpAkBCUyBwMBk8mMRwGCAiAdIpwLJyTqXZKCQgJ2zCAABIUto/HtiAYbDfCz3Zw0EaaEe1xahFiMEIWU0hSDUIBDAxeIA1SFXUAGiABClIKXhpSF24ABRIQWhASZwADZmJrAiBgqFNkk1mtXU5QYVVXTWUDHRwVm6pDQQA7" /> up
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUuAGZmZq2trfv7+2dnZ3p6empqamhoaLOzs6ampqmpqX5+fqSkpPr6+ubm5s7OzrKysvj4+Kenp39/f+Xl5bCwsI6Ojq+vr/39/YuLi8zMzIODg+Tk5Pz8/NDQ0GlpadHR0ZOTk4+Pj6qqqqWlpWtra/n5+Xd3d66urr29vX19ff7+/tnZ2aysrMrKyv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpENUNDODdFRjlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpENUNDODdGMDlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkQ1Q0M4N0VEOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkQ1Q0M4N0VFOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAALgAsAAAAABAAEAAABoVAl9DVEEkKBQWiMTwIGSMDYEo1RDgHgEsQmhIODgcKNAAQpq4IYHASDIUfKmAifbyFEIw8YXa/83IAEgAUdy4dAYmKBQAZho+Mjo93CoSTd3wEfoYXKkJ0AHaPDxorQghrFpsuFyxlC0ICFV5gLRYmUxUlQwwLZYEDCwyGDQkpHiRKG29BADs=" /> refresh
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQVAGZmZmhoaKmpqfv7+2pqaqampubm5uXl5X9/f4+Pj6enp6qqqn5+fmdnZ21tbfr6+oCAgI6OjqWlpWlpaX19ff///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGNUEwNjdFMjlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGNUEwNjdFMzlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkY1QTA2N0UwOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkY1QTA2N0UxOUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAFQAsAAAAABAAEAAABVxgJVbGghAEshjj+BQBIM+BMoxDMu9zclcKnohXqBxiu+EucBDwAMqdAPGMzhgEWWtbkaG03JHMwah2eRCn8Cw9rnnMSuG9K1YGkefO55I0ng02XAYCFBMOaSwjIQA7" /> plus
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQVAGZmZmhoaKmpqfv7+2pqaqampubm5uXl5aenp4+Pj39/f4CAgKqqqm1tbX5+fmdnZ/r6+n19faWlpWlpaY6Ojv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGNUEwNjdFNjlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGNUEwNjdFNzlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkY1QTA2N0U0OUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkY1QTA2N0U1OUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAFQAsAAAAABAAEAAABVRgJVYGoxCEwhjjCBUBIM8BMoxDMu9zclcInhBQqBxiw13gIEgKBQonz0GQta4VGcqKHckaDulu0RTLBEfzslIwFysDitTnkjyEDxvWIIhMGmQsIyEAOw==" /> minus
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANU7AGZmZmdnZ8vLy8DAwLS0tGpqavv7+8rKyoCAgJaWlv7+/vX19cHBwbOzs+3t7bm5uWxsbI6OjsLCwpmZmbq6upiYmN3d3eHh4b29vbW1tZ2dnX19faqqqvPz8/39/WlpaYeHh4WFhezs7ImJiYuLi7i4uO/v74ODg+jo6Pj4+Hl5eeXl5e7u7uPj48TExPb29np6euTk5NDQ0I2NjZSUlGhoaLCwsG1tbYaGhnNzc2tra////wAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGNUEwNjdFQTlBQTUxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowRTE4NTU0NDlBQTYxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkY1QTA2N0U4OUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkY1QTA2N0U5OUFBNTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAOwAsAAAAABAAEAAABn7AnVBocCyGSGQroQMAEBJFUigoADaVmTVhSK4KN8HQMQJwkhPAIflSfUxDDwQ23ZUAriELQKvLAA1DKQEgdQwAGEg4NShTEQAXSAcAIR1IFE9TGgA5NgISJE4ABEkKDRCiABEZAaNTCwIPDDFCA62kdUm2AA+5ugEnvkkWIkEAOw==" /> search
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="13" title="" alt="" src="data:image/gif;base64,R0lGODlhEAANANUtAGZmZvv7+/39/XNzc+vr629vb3d3d9/f33Fxcaqqqurq6pWVle7u7ufn52xsbHR0dKioqOPj46ysrPLy8uXl5aKioujo6O/v79fX17q6umdnZ5iYmKCgoIyMjHBwcNra2q+vr3l5eZycnJeXl9vb23x8fH9/f6enp7CwsJqamuLi4rOzs5aWlv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowRTE4NTU0NzlBQTYxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowRTE4NTU0ODlBQTYxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjBFMTg1NTQ1OUFBNjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBFMTg1NTQ2OUFBNjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAALQAsAAAAABAADQAABljAlnBIFAZYn6KyFVgACoflUJACWDtSIccKME2EBOWJW7oIE4gIUcINMYQo66AhXHENYSHorshwHxZFEFwDGnIUSxVcVh4qUgIiXFBZLQIjAA4klEYbGFlBADs=" /> save
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUqAGZmZmhoaKmpqfv7+2pqaqampvr6+uXl5ebm5qenp4+Pj39/f46Ojn5+fqqqqvb29oCAgO7u7m1tbbS0tNvb2/39/WdnZ8/Pz+vr63t7e6Ojo6WlpcjIyLKyss7Ozn19fWlpafT09NnZ2YODg9DQ0Jubm7y8vHBwcIaGht3d3f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowRTE4NTU0QjlBQTYxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowRTE4NTU0QzlBQTYxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjBFMTg1NTQ5OUFBNjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBFMTg1NTRBOUFBNjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAKgAsAAAAABAAEAAABn9AlVCFcCwIhIUDMRwaCgGAdBpIDIYDxXQ7VVxVCarp8eCcpgXVISrtqEQYlYd6EGwjJEBGZdgKFlsaKAAMKhFbDQRcACMhFSVbSIspBgxcEg2LFReLEHaLoAJrXBMTXAEHKgVcFBRcaSoDlqBSXk4bFosWVk1CCAIfIBKeTENBADs=" /> help
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="14" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAOAKIFAGZmZnV1ddPT03R0dHZ2dv///wAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozNTFFNEY3QzlBQTYxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozNTFFNEY3RDlBQTYxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjBFMTg1NTREOUFBNjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBFMTg1NTRFOUFBNjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAABQAsAAAAABAADgAAAyxYCtyusLgJor1RBEqD5twGjhW2TBKXPmVUUSvaiNL1AoMw3DBACC6bCRRJAAA7" /> print
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAIABAGZmZv///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozNTFFNEY4MDlBQTYxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozNTFFNEY4MTlBQTYxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjM1MUU0RjdFOUFBNjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjM1MUU0RjdGOUFBNjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAAQAsAAAAABAAEAAAAh+EHakbh8wcgNHRJW/UvDcPQlNIflTGodP6SaOGwk4BADs=" /> expand
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="14" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAOAIABAGZmZv///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozNTFFNEY4NDlBQTYxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozNTFFNEY4NTlBQTYxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjM1MUU0RjgyOUFBNjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjM1MUU0RjgzOUFBNjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAAQAsAAAAABAADgAAAhyEHakbh8wcAq1SSeGSuvsPhmLkgNyHWWqKll8BADs=" /> collapse
     * 
     * ##Crisp Theme
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQWANXl9eLt9YCw1PH3+iZ4tvT4+8zg7pe/3EuPwleXxpi/3H+w1MDY6mOg3GGf29fn9tHj9YO041iZ2d3q92ukzd7r9////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpERUFFRTEwRDlBQTkxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpERUFFRTEwRTlBQTkxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjM1MUU0Rjg2OUFBNjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkRFQUVFMTBDOUFBOTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAFgAsAAAAABAAEAAABX+gJVrMghAEsjDjGAiJYgyDoSRC4FJH0YqFA0VnERx+v4OAlPBFAC1AxFJIlBQiSOMhejQgIoXJMAI4KhUHVGQ4DVoTiWTSGqDeo/i8niKLzGhqI20LWBZaXBZeYBZiDE0WT1FTVSxGSC1KIgE8Pj9BQy4wMjQ2OEQtJScpKy0hADs=" /> close
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQUAMzg7pe/3H+w1OLt9Zi/3ICw1EuPwsDY6iZ4tvH3+leXxvT4+4y42M7h7mukzcfc7M/h74242Hyu04e11////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpERUFFRTExMTlBQTkxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpERUFFRTExMjlBQTkxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkRFQUVFMTBGOUFBOTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkRFQUVFMTEwOUFBOTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAFAAsAAAAABAAEAAABWkgJVKHYCCIIRzjOBQKASQJQCjF4DrB0oqLgENHKQR+v0CBpPAhRwtFifD8EUyAagtwSlAkjLCYMYlQEijvo8FuNwCQcyqrFXEF1DrlemjWoyxGdUoiAzxOLUFDLjAyNDY4RC0lJykrLSEAOw==" /> minimize
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUnAMzg7n+w1Ji/3PT4+8DY6vH3+oCw1OLt9SZ4tkuPwpe/3FeXxoa144q45Ie25GukzYm35JzD6JK95lWY2IKz46TI6pnB6EeP1Sd8zq7O7Sl9zqjK6/r8/oy55bTR7jSE0ZG85trp94+75nes4JvC6N/s+Pv9/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpERUFFRTExNTlBQTkxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpERUFFRTExNjlBQTkxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkRFQUVFMTEzOUFBOTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkRFQUVFMTE0OUFBOTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAJwAsAAAAABAAEAAABpfAk/BECCQQiESAMBweDAsBoFAACBaGg/OhGDSFA8VDezIovl+FgbjwnkqYkYPBmJwGi6JAmOFsIkMNQgJGAEIfECckFEIMQgBHBUIiFYkSgg5CBUiSJ5kNEicQFSCaSYYnjicUgA0aj0p7J4JCHR4mFoNLbScTdA4OFyFgeWVnaENqQgdcbk1hY05QUlRWWGRNRUdJS01BADs=" /> maximize
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQaAIe25M7h7pi/3H+w1MDY6uLt9Wai3ICw1Mzg7iZ4tvT4+0uPwleXxvH3+pe/3GukzcHZ8bLQ7bjU77zW8Pj7/abJ68vf87nU79Hj9bfT7////wAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowMUU5MTYyNjlBQUExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowMUU5MTYyNzlBQUExMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjAxRTkxNjI0OUFBQTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjAxRTkxNjI1OUFBQTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAGgAsAAAAABAAEAAABYigJmrEsCTJMhDjWByMgDQNIjBH4T6OIgISkcLx0GkOjhbAABk5DiSGbwQIGDJCRknQ0lQphohIYEJ0qyLDRIM4NQDwuBzeQDUqhoAecNHrLXULZhMGPwFdbQNcGhEGFGgtZARSIhIGkFksSCMQmBpPIgU8UwAYQkRGGi8xMzU3OV0kJigqLCMhADs=" /> restore
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQZAGmk3X+w1PH3+vT4+5e/3MDY6pi/3CZ4toCw1FeXxuLt9UuPwszg7mukzbjU7/T4/Mne893q9+zz+3uv4dXl9evz+mai3Mzg88vf8////wAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowMUU5MTYyQTlBQUExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowMUU5MTYyQjlBQUExMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjAxRTkxNjI4OUFBQTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjAxRTkxNjI5OUFBQTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAGQAsAAAAABAAEAAABXtgJmZFsBzHEhTjqCCJwQgCYySI4jbE0IoDQkOXQRB+PwKClPCNHI7WIFEytDAAwKVlMDFGEUulYomMGCeBSAKgiCgAiUiAUj8mkBZk8sjQF19ZgoMAGWgBVkg/XQVNiiNTLEaPIkoiCjxOUkJEGS8xMzU3OUglJykrLSEAOw==" /> toggle
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUnAPD2/K7O7YGy4nOq34665ZS+5/z9/m6n3uvz+mSh3LvW8NHj9djn9r3X8Mzg84W142ym3p7E6crf81eZ2ZnB6Pn7/t3q93uv4fL3/M/i9Ory+mej3F2d2oS042Wh3Gai3IKz43Wr4G2m3mKg2/3+/4+75pC85v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowMUU5MTYyRTlBQUExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFNDJGMTEzMjlBQUExMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjAxRTkxNjJDOUFBQTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjAxRTkxNjJEOUFBQTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAJwAsAAAAABAAEAAABpzAk3AY4XAiw+TJUjgBEotFAnAqWIYMyGBzIAAAhMNmAGEITYOTApFEKE6h5skwaggDj0dA2PCQhBUHGgYXAhISAhcGGiIVJxQeJScBAkkCewQfFBMOGCcdGUkZHScYDhOcnqCipKYTkJKUlpiaJ4GDFyCHIIqMjnN1d3l7Jw0ffydoamxDbnByWVtdX2FjZUNMTlBSVFZKREZISkEAOw==" /> gear
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQZAGmk3X+w1PH3+vT4+5e/3MDY6pi/3CZ4toCw1FeXxuLt9UuPwszg7mukzbjU7/T4/Mne893q9+zz+3uv4dXl9evz+mai3Mzg88vf8////wAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFNDJGMTEzNTlBQUExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFNDJGMTEzNjlBQUExMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkU0MkYxMTMzOUFBQTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkU0MkYxMTM0OUFBQTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAGQAsAAAAABAAEAAABXtgJmZFsBzHEhTjqCCJwQgCYySI4jbE0IoDQkOXQRB+D8CIgCAlfCPJRAlMlAytCABCFRlMjNHFQsl0M4yTQOQAVERnAWqdab/NLfkiLMKQ8SNpAVgjWlwtXwVPLVJdA1ZFRy1JS00ZCjxQLUFDLjAyNDY4RC0lJykrLSEAOw==" /> prev
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQZAGmk3X+w1PH3+vT4+5e/3MDY6pi/3CZ4toCw1FeXxuLt9UuPwszg7mukzbjU7/T4/Mne893q9+zz+3uv4dXl9evz+mai3Mzg88vf8////wAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFNDJGMTEzOTlBQUExMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFNDJGMTEzQTlBQUExMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkU0MkYxMTM3OUFBQTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkU0MkYxMTM4OUFBQTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAGQAsAAAAABAAEAAABXlgJmZFsBzHEhTjqCCJwQgCYySI4jbE0IoDQkOXQRBGgMePgCAlfCLARNIaJEqGFgACiLQMJoY2Q7FgRoyTYJypABwiAWqNFLnhGflCXC9f0CpZSFxeI2AFT0hTVVdFR1FKLUwiCjxQVUJEGS8xMzU3OT8kJigqLCMhADs=" /> next
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANU7AP3+/2+n3vL3/Pj7/aTI6tvp95C85pW/52ym3t7r926n3l+e2+bw+XSr32ej3PX5/ePu+e30++zz+4+75vD2/O/1+7zW8HKp32Gf2+Tv+Wul3YGy4mWh3HCo3prC6Huv4WSh3LLQ7brV71ub2oS046fK68DZ8W2m3oy55V2d2tbm9vP4/IW146nL63yv4Wqk3YO044i35Ja/53Gp3/n7/vr8/lqb2Y265Yu45YKz46XI6////wAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxQUFGMTkzMjlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxQUFGMTkzMzlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkU0MkYxMTNCOUFBQTExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkU0MkYxMTNDOUFBQTExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAOwAsAAAAABAAEAAABo3AnXAnujRcjYtoyNwZOglJNNExNHehAMCDYJ08gECo6WAQPoPd4ENgOJqaHYcypHB2cWZgB3oMHyA7e0wIFQcoQzgHFQhNEwQAGwoxChsAOhNNEBhpBSYFOzQYEFckJU0tJFc7GSMCQwIpGas7BzdDBjK0OysjKjsFC6+7Fi81Mxa7Qzk2MMpDEQsRq0EAOw==" /> pin
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANU7AP3+/2+n3vL3/Pj7/aTI6tvp95C85pW/52ym3t7r926n3l+e2+bw+XSr32ej3PX5/ePu+e30++zz+4+75vD2/O/1+7zW8HKp32Gf2+Tv+Wul3YGy4mWh3HCo3prC6Huv4WSh3LLQ7brV71ub2oS046fK68DZ8W2m3oy55V2d2tbm9vP4/IW146nL63yv4Wqk3YO044i35Ja/53Gp3/n7/vr8/lqb2Y265Yu45YKz46XI6////wAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxQUFGMTkzNjlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxQUFGMTkzNzlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjFBQUYxOTM0OUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjFBQUYxOTM1OUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAOwAsAAAAABAAEAAABorAnXAYWUSGyOQOZssplZZZ7WV5DgWLwk41Wll3MsPwdrBmUoLrKPMktZIlkhKCoe0KJu0AA0lOdAAbCjEKGwAEE0kIFQc4QygHFQhJATsgD0MPIDuVSBo7HBRDFBw7n0gODAQfAzsDHwQMDkkhAQAeJywIHgABIUoGHQkSwwkdYk8iFw0uDRciSUEAOw==" /> unpin
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQZAOLt9czg7pi/3MDY6n+w1ICw1PT4+5e/3EuPwvH3+leXxiZ4tl+e2+ny+mukzWGf27TR7rnU77LQ7e/1+6fK67XS7rbT7uHt+GKg2////wAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxQUFGMTkzQTlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxQUFGMTkzQjlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjFBQUYxOTM4OUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjFBQUYxOTM5OUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAGQAsAAAAABAAEAAABXZgJmYDgSwLQgzjCBSKECRJICgF4DqH0YqGg0OXKRxGFMvvUCApfKILI9IyKEqCX4NRaQlMgV+m8ZCMAqeEODPBQEQJlFrcfmfiiLC2fFZlW1tdI18DTyNSVCNWLEZISi1MIgA8UFVCRBkvMTM1NzliJScpKy0hADs=" /> right
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQZAOLt9czg7pi/3MDY6n+w1ICw1PT4+5e/3EuPwvH3+leXxiZ4tl+e2+ny+mukzWGf27TR7rnU77LQ7e/1+6fK67XS7rbT7uHt+GKg2////wAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozRDQzMDhERDlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozRDQzMDhERTlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjFBQUYxOTNDOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjNENDMwOERDOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAGQAsAAAAABAAEAAABXdgJmYDgSwLQgzjCBSKECRJICgF4DqH0YqGg0OXKRx+GQtFdCiQFL5WhHEBKkqCX4XRaAlMgZbk0W0FTokRBDNBJlBp0br9eyPCo3F5dCZkW1t7GV8DUD9TVRkGV0VHP0pMThkAPFEtQUMuMDI0NjhELSUnKSstIQA7" /> left
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQZAOLt9czg7pi/3MDY6n+w1ICw1PT4+5e/3EuPwvH3+leXxiZ4tl+e2+ny+mukzWGf27TR7rnU77LQ7e/1+6fK67XS7rbT7uHt+GKg2////wAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozRDQzMDhFMTlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozRDQzMDhFMjlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjNENDMwOERGOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjNENDMwOEUwOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAGQAsAAAAABAAEAAABXxgJmYDgSwLQgzjCBSKECRJICgF4DqH0YqGg0OXKRx+v0OBpPAhRwZFSfD8CUyBDOWCvFAygVMiY2E0Wg2GJZNAjTMRsygdEbURWVHlMZk8KiNhBFQjEhgYEi1XA00tEBAtUSxGVSJKIgA8TpFCRBkvMTM1NzlIJScpKy0hADs=" /> down
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQZAOLt9czg7pi/3MDY6n+w1ICw1PT4+5e/3EuPwvH3+leXxiZ4tl+e2+ny+mukzWGf27TR7rnU77LQ7e/1+6fK67XS7rbT7uHt+GKg2////wAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozRDQzMDhFNTlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozRDQzMDhFNjlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjNENDMwOEUzOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjNENDMwOEU0OUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAGQAsAAAAABAAEAAABXpgJmYDgSwLQgzjCBSKECRJICgF4DqH0YqGg0OXKRx+v0OBpPCNIJCWQVEStCQYjKQlMAVGlcdk8qiMAqeEKMJoiBqMiCiBUlvaLbglQ0d8KRdIFxQZaARWSD9dA02JI1MsRo4iSiIAPE5SQkQZLzEzNTc5SCUnKSstIQA7" /> up
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQALMOAECJvvT4++Lt9VeXxoCw1Je/3Ji/3MDY6n+w1EuPwiZ4tvH3+szg7mukzf///wAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1QTU3Q0Y1NDlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1QTU3Q0Y1NTlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjVBNTdDRjUyOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjVBNTdDRjUzOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAADgAsAAAAABAAEAAABFrQSXdQUiqhM4EUxGAwy8IYAyE4ntAUwTQFRSN4RCHvTkEAh0GMNxsAEAbizgBIMIiAqBSgWCh51evOqZ0huw7DRqgNDDg5re/zGspotglIRDKhVLzKJbORRQAAOw==" /> refresh
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUgAOLt9czg7vH3+pe/3MDY6oCw1Ji/3CZ4tleXxn+w1PT4+0uPwsfc7Iy42M7h7om212ukzYi211GTxNro8o+52Xyu04e116LF38jd7HGnz5O825nA3HCmz8/h783g7ou32P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1QTU3Q0Y1ODlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1QTU3Q0Y1OTlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjVBNTdDRjU2OUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjVBNTdDRjU3OUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAIAAsAAAAABAAEAAABoVAkBBESCwOh0WCMBwCCghDQCAIGBAFgBMyUDSFigFECyoMmhFGc1AgIrzDh3qoQBQNX/nXYAx8Gw5fAUcCIBUNDRsSHBoNFhQgAkiFDA4OABkXEw4BHZFJfk0NHoJKeE16TXwEb6hzYHZlZ0MfGGttIABccE1hY05QUlRWWGRNRUdJS01BADs=" /> plus
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAMQTAMzg7s7h7pe/3MDY6vT4+1eXxvH3+uLt9Zi/3ICw1EuPwiZ4tn+w1Iy42GukzYi2132v04u32Mjd7P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1QTU3Q0Y1QzlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2RENDMTQxMjlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjVBNTdDRjVBOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjVBNTdDRjVCOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAEwAsAAAAABAAEAAABWngJE4DoyyLwgzjeCQFAhgGgBTJ4ToC0YoEgUM3SQh+P0GCVPAhR4RCCfH8IUyAagtwMkwgjbC48YhMDCivJMBuBwCBcyqrFXEZ1PrkOmjWoyxGdUoiBzxOLUFDLjAyNDY4RC0lJykrLSEAOw==" /> minus
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUhAOzz+X2v077X6VmYxyt8t5rA3eXv9mCcybfT58vf7u70+TB/ueDs9e30+TWCu+jx9/j7/ery+Cd6tjOBul+cyRlxsfT4+/n7/Sx8uHOp0CR3tcjd7Bxysu/1+Sp7t9ro8ih6t////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2RENDMTQxNTlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2RENDMTQxNjlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjZEQ0MxNDEzOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjZEQ0MxNDE0OUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAIQAsAAAAABAAEAAABmrAUMhQGBAIg4JByBQmDgEBACAIHBLN4QGRDSEOS2Yh0BUGCs2BoBwSDJoEABtAgMvL9PS67G6O2WdNBltdGRwbWU9RU1UOIAsRWURGSEoWHhh3WRcQTQoLEgpsWQ0TFR+jTR0aFKlNDwxBADs=" /> search
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUnAGCcyb7X6eTu9leXxvf6/O30+dTk8FiYx8/h7+Ds9bbS5s7h7qbI4dbm8VOUxbjT52mizIi219Di7/H2+mCdyefw9+ry+N3q9Ony+L3W6XSp0FGTxF6bydfm8vj7/cbc7FSVxa7N5LvV6E2Rw8fc7Ji/3Ojx9////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2RENDMTQxOTlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2RENDMTQxQTlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjZEQ0MxNDE3OUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjZEQ0MxNDE4OUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAJwAsAAAAABAAEAAABkjAk3BILBqPyOSJQFAOCxqD8zShlKYFAGNa4YSGHmQCpCBGRMbOJlM0jAJEhONzvBweQtJgkRQAAhIDDU4WEAcCUycmGImNjkEAOw==" /> save
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANUpAMzg7leXxoCw1MDY6pe/3CZ4tpi/3H+w1PT4+/H3+kuPwuLt9Wukzdrp90qR1lKW2DWE0fj7/Wul3UaP1YKz4+/1+7TR7j+K08vf826n3u30+7/Y8K/O7UyS1sXc8uny+pzD6LfT70mQ1uLt+DiG0rLQ7cne82ij3cLa8f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4RjlFNzQxODlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4RjlFNzQxOTlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjZEQ0MxNDFCOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjZEQ0MxNDFDOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAKQAsAAAAABAAEAAABozAlDA1OCgKBcVhMBwuBAEDIJEAGAKChZNBQDSFCAJDmxIQhBqJQ/RoCAkCYsCbkpwqEYoDHCgahiRuKR8QQwZGAEMmEUIcE0MARwlfKRgQKEMJSJNNIxcWTZoKiU0gD1+RB39NHiFfhwNzTRslTQh9ZWdDGR1NcEILXHS2YmQpT1FTVVdZlEVHSUtNQQA7" /> help
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQANU0AISr0Mrb612Ru3+nzfT3+3ylzHijy4yw0oqv0szc63qkzICoznmjy4Gpzouv0ufv9sna6m2bx5K01YOqz6K/29Lg7sbY6ZO11Zq62IKpzvn7/Za31n6nzVWLvdrm8ezy+I2x05u62LfO43SgyZe411uPv+Lr9E+Gu9bj72OUw3iiy87e7Ias0LnP5L/T5pG01WuZxrPL4XWgybrQ5P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4RjlFNzQxQzlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4RjlFNzQxRDlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjhGOUU3NDFBOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjhGOUU3NDFCOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAANAAsAAAAABAAEAAABp5AGg114hiOBpmBc0IJhRbSc0ojWaaPlOPA7TpSD2FFIpkUzujzRPKqRFyQgHxOh7QiAKr+CcjTNASBgoEaQn00HiUNA4yNDSUeNIcQIIsLlwsDDSAQknkJDAh6CAwJnjQJCiF6IQqmh6kbNAK0AjQbCis0LDAmMSMYehgjMyYRIgwqFBeztTQXFAYMIk8PDnoOYVQfHRkI3wgZHR9PQQA7" /> print
     * - <img style="vertical-align:sub;margin-right:4px;" width="16" height="16" title="" alt="" src="data:image/gif;base64,R0lGODlhEAAQAIABAF6d0P///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4RjlFNzQyMDlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4RjlFNzQyMTlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjhGOUU3NDFFOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjhGOUU3NDFGOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAAQAsAAAAABAAEAAAAiCEHakboM4YbFG+ug7CvPs4feI1llSlhSjptOekgiSXFgA7" /> expand
     * - <img style="vertical-align:sub;margin-right:4px;" width="15" height="15" title="" alt="" src="data:image/gif;base64,R0lGODlhDwAPAIABAF6d0P///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU1NzcyLCAyMDE0LzAxLzEzLTE5OjQ0OjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCQjkxNDlCQjlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCQjkxNDlCQzlBQUIxMUU0OEVCNUNFMTgyMDM3Mzc3MSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjhGOUU3NDIyOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkJCOTE0OUJBOUFBQjExRTQ4RUI1Q0UxODIwMzczNzcxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAAQAsAAAAAA8ADwAAAiCEHakL2gtfaHRalCTMR+8PhuJIllEnSl7kXC5mhd26FAA7" /> collapse
     */
    /* eslint-enable max-len */
    /**
     * @cfg {String/Object} tooltip
     * The tooltip for the tool - can be a string to be used as innerHTML (html tags are accepted)
     * or QuickTips config
     * object
     */
    /**
     * @cfg {String} tooltipType
     * The type of tooltip to use. Either 'qtip' (default) for QuickTips or 'title'
     * for title attribute.
     */
    tooltipType: 'qtip',
    /**
     * @cfg {Boolean} stopEvent
     * Specify as false to allow click event to propagate.
     */
    stopEvent: true,
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'button',
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: true,
    /**
     * @cfg tabIndex
     * @inheritdoc
     */
    tabIndex: 0,
    /**
     * @cfg keyMap
     * @inheritdoc
     */
    keyMap: {
        scope: 'this',
        SPACE: 'onClick',
        ENTER: 'onClick'
    },
    /**
     * @cfg {Boolean} cacheHeight
     * Set to `false` to use a custom tool height and not the cached tool height.
     */
    cacheHeight: true,
    /**
     * @cfg {Boolean} cacheWidth
     * Set to `false` to use a custom tool width and not the cached tool width.
     */
    cacheWidth: true,
    _toolTypes: {
        close: 1,
        collapse: 1,
        down: 1,
        expand: 1,
        gear: 1,
        help: 1,
        left: 1,
        maximize: 1,
        minimize: 1,
        minus: 1,
        // move:1,
        next: 1,
        pin: 1,
        plus: 1,
        prev: 1,
        print: 1,
        refresh: 1,
        // resize:1,
        restore: 1,
        right: 1,
        save: 1,
        search: 1,
        toggle: 1,
        unpin: 1,
        up: 1
    },
    initComponent: function() {
        var me = this;
        if (me.id && me._toolTypes[me.id]) {
            Ext.raise('When specifying a tool you should use the type option, ' + 'the id can conflict now that tool is a Component');
        }
        // alias qtip, should use tooltip since it's what we have in the docs
        me.tooltip = me.tooltip || me.qtip;
        me.callParent();
    },
    initRenderData: function() {
        var me = this,
            data = me.callParent(),
            glyph = me.getGlyph(),
            glyphFontFamily;
        // Transform Glyph to the useful parts
        if (glyph) {
            glyphFontFamily = glyph.fontFamily;
            glyph = glyph.character;
        }
        Ext.applyIf(data, {
            className: me.calculateClassName(),
            glyph: glyph,
            glyphFontFamily: glyphFontFamily
        });
        return data;
    },
    calculateClassName: function() {
        var me = this,
            result = me.baseCls + '-tool-el ';
        if (me.type) {
            result += me.baseCls + '-img ' + me.baseCls + '-' + me.type;
        } else if (me.iconCls) {
            result += me.iconCls;
        }
        // do not add x-tool-img to allow iconCls full sway
        return result;
    },
    afterRender: function() {
        var me = this,
            tip;
        me.callParent();
        if (me.setTypeAfterRender) {
            me.setTypeAfterRender = false;
            me.setType(me.type);
        }
        me.el.on({
            click: me.onClick,
            mousedown: me.onMouseDown,
            mouseover: me.onMouseOver,
            mouseout: me.onMouseOut,
            scope: me
        });
        tip = me.tooltip;
        if (tip) {
            me.setTooltip(tip);
        }
    },
    tipAttrs: {
        qtip: 'data-qtip'
    },
    setTooltip: function(tooltip, type) {
        var me = this,
            oldTip = me.tooltip,
            oldType = me.tooltipType,
            id = me.id,
            el = me.el,
            attr;
        if (oldTip && Ext.quickTipsActive && Ext.isObject(oldTip)) {
            Ext.tip.QuickTipManager.unregister(id);
        }
        me.tooltip = tooltip;
        if (type) {
            me.tooltipType = type;
        }
        if (tooltip) {
            if (Ext.quickTipsActive && Ext.isObject(tooltip)) {
                Ext.tip.QuickTipManager.register(Ext.apply({
                    target: id
                }, tooltip));
            } else if (el) {
                if (type && oldType && type !== oldType) {
                    attr = me.tipAttrs[oldType] || 'title';
                    el.dom.removeAttribute(attr);
                }
                attr = me.tipAttrs[type || oldType] || 'title';
                el.dom.setAttribute(attr, tooltip);
            }
            if (attr !== 'title' && me.ariaRole && me.ariaRole !== 'presentation') {
                if (el) {
                    el.dom.setAttribute('aria-label', tooltip);
                } else {
                    me.ariaRenderAttributes = me.ariaRenderAttributes || {};
                    me.ariaRenderAttributes['aria-label'] = tooltip;
                }
            }
        }
    },
    /**
     * Sets the type of the tool. Allows the icon to be changed.
     * @param {String} type The new type. See the {@link #type} config.
     * @return {Ext.panel.Tool} this
     */
    setType: function(type) {
        var me = this,
            updating = me.updating,
            rendering = me.rendering,
            oldClassName, clear;
        if (!updating) {
            oldClassName = me.calculateClassName();
            if (!rendering) {
                me.updating = clear = true;
            }
            me.setIconCls(null);
            me.setGlyph(null);
        }
        me.type = type;
        if (clear) {
            me.updateToolCls(oldClassName);
            me.updating = false;
        } else if (rendering) {
            me.setTypeAfterRender = true;
        }
        return me;
    },
    /**
     * Sets the icon class. Allows the icon to be changed.
     * @param {String} iconCls The new icon class. See the {@link #type} config.
     * @return {Ext.panel.Tool} this
     */
    setIconCls: function(iconCls) {
        var me = this,
            updating = me.updating,
            oldClassName, clear;
        if (!updating) {
            oldClassName = me.calculateClassName();
            me.updating = clear = true;
            me.setType(null);
            me.setGlyph(null);
        }
        me.iconCls = iconCls;
        if (clear) {
            me.updateToolCls(oldClassName);
            me.updating = false;
        }
        return me;
    },
    doDestroy: function() {
        var me = this;
        me.setTooltip(null);
        delete me.toolOwner;
        me.callParent();
    },
    applyGlyph: function(glyph, oldGlyph) {
        if (glyph) {
            if (!glyph.isGlyph) {
                glyph = new Ext.Glyph(glyph);
            }
            if (glyph.isEqual(oldGlyph)) {
                glyph = undefined;
            }
        }
        return glyph;
    },
    updateGlyph: function(glyph, oldGlyph) {
        var me = this,
            toolEl = me.toolEl,
            updating = me.updating,
            oldClassName, clear;
        if (!updating) {
            oldClassName = me.calculateClassName();
            me.updating = clear = true;
            me.setType(null);
            me.setIconCls(null);
        }
        if (toolEl) {
            if (glyph) {
                toolEl.dom.innerHTML = glyph.character;
                toolEl.setStyle(glyph.getStyle());
            } else {
                toolEl.dom.innerHTML = '';
            }
        }
        if (clear) {
            me.updateToolCls(oldClassName);
            me.updating = false;
        }
    },
    privates: {
        /**
         * Called when the tool element is clicked
         * @private
         * @param {Ext.event.Event} e
         * @param {HTMLElement} target The target element
         */
        onClick: function(e, target) {
            var me = this;
            if (me.disabled) {
                return false;
            }
            // remove the pressed + over class if it was a pointer event
            if (e.type !== 'keydown') {
                me.el.removeCls(me.toolPressedCls + ' ' + me.toolOverCls);
            }
            if (me.stopEvent !== false) {
                e.stopEvent();
            }
            if (me.handler) {
                Ext.callback(me.handler, me.scope, [
                    e,
                    target,
                    me.ownerCt,
                    me
                ], 0, me);
            } else if (me.callback) {
                Ext.callback(me.callback, me.scope, [
                    me.toolOwner || me.ownerCt,
                    me,
                    e
                ], 0, me);
            }
            // The handler could have destroyed the owner, and the Tool instance as well.
            // This is what happens with Close tools in Panels.
            if (me.destroyed) {
                return;
            }
            /**
             * @event click
             * Fires when the tool is clicked
             * @param {Ext.panel.Tool} this
             * @param {Ext.event.Event} e The event object
             * @param {Ext.Component} owner The logical owner of the tool. In a typical
             * `Ext.panel.Panel`, this is set to the owning panel. This value comes from the
             * `toolOwner` config.
             * Note that if the tool handler destroys the tool and/or its owner, the event
             * will not fire.
             * @since 5.0.0
             */
            me.fireEvent('click', me, e, me.toolOwner || me.ownerCt);
            return true;
        },
        /**
         * Called when the user presses their mouse button down on a tool
         * Adds the press class ({@link #toolPressedCls})
         * @private
         */
        onMouseDown: function(e) {
            // We prevent default action on mousedown to avoid focusing the tool.
            // This is consistent with tool behavior in versions prior to 5.5 where
            // tools were pointer-interactive only.
            e.preventDefault();
            if (this.disabled) {
                return false;
            }
            this.el.addCls(this.toolPressedCls);
        },
        /**
         * Called when the user rolls over a tool
         * Adds the over class ({@link #toolOverCls})
         * @private
         */
        onMouseOver: function() {
            if (this.disabled) {
                return false;
            }
            this.el.addCls(this.toolOverCls);
        },
        /**
         * Called when the user rolls out from a tool.
         * Removes the over class ({@link #toolOverCls})
         * @private
         */
        onMouseOut: function() {
            this.el.removeCls(this.toolOverCls);
        },
        updateToolCls: function(oldCls) {
            var toolEl = this.toolEl;
            if (toolEl) {
                toolEl.removeCls(oldCls);
                toolEl.addCls(this.calculateClassName());
            }
        }
    }
});

/**
 * Simple header class which is used for on {@link Ext.panel.Panel} and {@link Ext.window.Window}.
 */
Ext.define('Ext.panel.Header', {
    extend: 'Ext.panel.Bar',
    xtype: 'header',
    requires: [
        'Ext.panel.Title',
        'Ext.panel.Tool'
    ],
    /**
     * @property {Boolean} isHeader
     * `true` in this class to identify an object as an instantiated Header, or subclass thereof.
     */
    isHeader: true,
    defaultType: 'tool',
    indicateDrag: false,
    weight: -1,
    shrinkWrap: 3,
    focusableContainer: true,
    // For performance reasons we give the following configs their default values on
    // the class body.  This prevents the updaters from running on initialization in the
    // default configuration scenario
    iconAlign: 'left',
    titleAlign: 'left',
    titlePosition: 0,
    titleRotation: 'default',
    autoEl: {
        role: 'presentation'
    },
    beforeRenderConfig: {
        /**
         * @cfg {Number/String} glyph
         * @accessor
         * A numeric unicode character code to use as the icon.  The default font-family 
         * for glyphs can be set globally using 
         * {@link Ext.app.Application#glyphFontFamily glyphFontFamily} application 
         * config or the {@link Ext#setGlyphFontFamily Ext.setGlyphFontFamily()} method.
         * It is initially set to `'Pictos'`.
         * 
         * The following shows how to set the glyph using the font icons provided in the 
         * SDK (assuming the font-family has been configured globally):
         *
         *     // assumes the glyphFontFamily is "Pictos"
         *     glyph: 'x48'       // the "home" icon (H character)
         *
         *     // assumes the glyphFontFamily is "Pictos"
         *     glyph: 72          // The "home" icon (H character)
         *
         *     // assumes the glyphFontFamily is "Pictos"
         *     glyph: 'H'         // the "home" icon
         * 
         * Alternatively, this config option accepts a string with the charCode and 
         * font-family separated by the `@` symbol.
         * 
         *     // using Font Awesome
         *     glyph: 'xf015@FontAwesome'     // the "home" icon
         * 
         *     // using Pictos
         *     glyph: 'H@Pictos'              // the "home" icon
         * 
         * Depending on the theme you're using, you may need include the font icon 
         * packages in your application in order to use the icons included in the 
         * SDK.  For more information see:
         * 
         *  - [Font Awesome icons](http://fortawesome.github.io/Font-Awesome/cheatsheet/)
         *  - [Pictos icons](../guides/core_concepts/font_ext.html)
         *  - [Theming Guide](../guides/core_concepts/theming.html)
         */
        glyph: null,
        /**
         * @cfg {String} icon
         * Path to an image to use as an icon.
         *
         * For instructions on how you can use icon fonts including those distributed in 
         * the SDK see {@link #iconCls}.
         * @accessor
         */
        icon: null,
        /**
         * @cfg {String} iconCls
         * @accessor
         * One or more space separated CSS classes to be applied to the icon element.  
         * The CSS rule(s) applied should specify a background image to be used as the 
         * icon.
         *
         * An example of specifying a custom icon class would be something like:
         *
         *     // specify the property in the config for the class:
         *     iconCls: 'my-home-icon'
         *
         *     // css rule specifying the background image to be used as the icon image:
         *     .my-home-icon {
         *         background-image: url(../images/my-home-icon.gif) !important;
         *     }
         * 
         * In addition to specifying your own classes, you can use the font icons 
         * provided in the SDK using the following syntax:
         * 
         *     // using Font Awesome
         *     iconCls: 'x-fa fa-home'
         * 
         *     // using Pictos
         *     iconCls: 'pictos pictos-home'
         * 
         * Depending on the theme you're using, you may need include the font icon 
         * packages in your application in order to use the icons included in the 
         * SDK.  For more information see:
         * 
         *  - [Font Awesome icons](http://fortawesome.github.io/Font-Awesome/cheatsheet/)
         *  - [Pictos icons](../guides/core_concepts/font_ext.html)
         *  - [Theming Guide](../guides/core_concepts/theming.html)
         */
        iconCls: null,
        /**
         * @cfg {'top'/'right'/'bottom'/'left'} [iconAlign='left']
         * The side of the title to render the icon.
         * @accessor
         */
        iconAlign: null,
        /**
         * @cfg {String/Ext.panel.Title}
         * The title text or config object for the {@link Ext.panel.Title Title} component.
         * @accessor
         */
        title: {
            $value: {
                xtype: 'title',
                flex: 1
            },
            merge: function(newValue, oldValue) {
                if (typeof newValue !== 'object') {
                    newValue = {
                        text: newValue
                    };
                }
                return Ext.merge(oldValue ? Ext.Object.chain(oldValue) : {}, newValue);
            }
        },
        /**
         * @cfg {'left'/'center'/'right'} [titleAlign='left']
         * The alignment of the title text within the available space between the
         * icon and the tools.
         * @accessor
         */
        titleAlign: null,
        /**
         * @cfg {Number} [titlePosition=0]
         * The ordinal position among the header items (tools and other components specified
         * using the {@link #cfg-items} config) at which the title component is inserted.
         * See {@link Ext.panel.Panel#cfg-header Panel's header config}.
         *
         * If not specified, the title is inserted after any {@link #cfg-items}, but *before*
         * any {@link Ext.panel.Panel#tools}.
         *
         * Note that if an {@link #icon} or {@link #iconCls} has been configured, then the icon
         * component will be the first item before all specified tools or {@link #cfg-items}.
         * This configuration does not include the icon.
         * @accessor
         */
        titlePosition: null,
        /**
         * @cfg {'default'/0/1/2} [titleRotation='default']
         * @accessor
         * The rotation of the header's title text.  Can be one of the following values:
         *
         * - `'default'` - use the default rotation, depending on the dock position of the header
         * - `0` - no rotation
         * - `1` - rotate 90deg clockwise
         * - `2` - rotate 90deg counter-clockwise
         *
         * The default behavior of this config depends on the dock position of the header:
         *
         * - `'top'` or `'bottom'` - `0`
         * - `'right'` - `1`
         * - `'left'` - `1`
         */
        titleRotation: null
    },
    // a class for styling that is shared between panel and window headers
    headerCls: Ext.baseCSSPrefix + 'header',
    /**
     * @event click
     * Fires when the header is clicked. This event will not be fired
     * if the click was on a {@link Ext.panel.Tool}
     * @param {Ext.panel.Header} this
     * @param {Ext.event.Event} e
     */
    /**
     * @event dblclick
     * Fires when the header is double clicked. This event will not
     * be fired if the click was on a {@link Ext.panel.Tool}
     * @param {Ext.panel.Header} this
     * @param {Ext.event.Event} e
     */
    /**
     * @cfg {Number} [itemPosition]
     * The index at which the any {@link #cfg-items} will be inserted into the Header's
     * items collection.  By default this will effectively be the `1` position
     * placing the items following the panel {@link Ext.panel.Panel#title title}.
     *
     * Set to `0` to have the items {@link #insert inserted} before the panel title.
     *
     *     Ext.create('Ext.panel.Panel', {
     *         title: 'Hello',
     *         width: 200,
     *         html: '<p>World!</p>',
     *         renderTo: Ext.getBody(),
     *         tools: [{
     *             type: 'pin'
     *         }],
     *         header: {
     *             //itemPosition: 0,	// before panel title
     *             //itemPosition: 1, // after panel title
     *             //itemPosition: 2, // after pin tool
     *             items: [{
     *                 xtype: 'button',
     *                 text: 'Header Button'
     *             }]
     *         }
     *     });
     */
    initComponent: function() {
        var me = this,
            items = me.items,
            itemPosition = me.itemPosition,
            cls = [
                me.headerCls
            ];
        me.tools = me.tools || [];
        me.items = items = (items ? items.slice() : []);
        if (itemPosition !== undefined) {
            me._userItems = items.slice();
            me.items = items = [];
        }
        me.indicateDragCls = me.headerCls + '-draggable';
        if (me.indicateDrag) {
            cls.push(me.indicateDragCls);
        }
        me.addCls(cls);
        me.syncNoBorderCls();
        // Add Tools
        Ext.Array.push(items, me.tools);
        // Clear the tools so we can have only the instances.
        // Intentional mutation of passed in array.
        // Owning code in Panel uses this array as its public tools property.
        me.tools.length = 0;
        me.callParent();
        me.on({
            dblclick: me.onDblClick,
            click: me.onClick,
            element: 'el',
            scope: me
        });
    },
    /**
     * Add a tool to the header
     * @param {Object} tool
     */
    addTool: function(tool) {
        var me = this;
        // Even though the defaultType is tool, it may be changed,
        // so let's be safe and forcibly specify tool
        me.add(Ext.ComponentManager.create(tool, 'tool'));
        me.checkFocusableTools();
    },
    afterLayout: function() {
        var me = this,
            frameBR, frameTR, frameTL, xPos;
        if (me.vertical) {
            frameTR = me.frameTR;
            if (frameTR) {
                // The corners sprite currently requires knowledge of the vertical header's
                // width to correctly set the background position of the bottom right corner.
                // TODO: rearrange the sprite so that this can be done with pure css.
                frameBR = me.frameBR;
                frameTL = me.frameTL;
                xPos = (me.getWidth() - frameTR.getPadding('r') - ((frameTL) ? frameTL.getPadding('l') : me.el.getBorderWidth('l'))) + 'px';
                frameBR.setStyle('background-position-x', xPos);
                frameTR.setStyle('background-position-x', xPos);
            }
        }
        this.callParent();
    },
    applyTitle: function(title, oldTitle) {
        var me = this,
            isString, configHasRotation;
        title = title || '';
        isString = Ext.isString(title);
        if (!Ext.isObject(title)) {
            title = {
                text: title.toString()
            };
        }
        if (oldTitle) {
            // several title configs can trigger layouts, so suspend before setting
            // configs in bulk
            Ext.suspendLayouts();
            oldTitle.setConfig(title);
            Ext.resumeLayouts(true);
            title = oldTitle;
        } else {
            if (isString) {
                title.xtype = 'title';
            }
            title.ui = me.ui;
            configHasRotation = ('rotation' in title);
            // Important Panel attribute aria-labelledby depends on title textEl id
            title.id = me.id + '-title';
            if (me.isAccordionHeader) {
                title.ariaRole = 'tab';
                title.textElRole = null;
                title.focusable = true;
            }
            title = Ext.create(title);
            // avoid calling the title's rotation updater on initial startup in the default scenario
            if (!configHasRotation && me.vertical && me.titleRotation === 'default') {
                title.rotation = 1;
            }
        }
        return title;
    },
    applyTitlePosition: function(position) {
        var max = this.items.getCount();
        if (this._titleInItems) {
            --max;
        }
        return Math.max(Math.min(position, max), 0);
    },
    beforeLayout: function() {
        this.callParent();
        this.syncBeforeAfterTitleClasses();
    },
    beforeRender: function() {
        var me = this,
            itemPosition = me.itemPosition;
        me.protoEl.unselectable();
        me.callParent();
        if (itemPosition !== undefined) {
            me.insert(itemPosition, me._userItems);
        }
        me.checkFocusableTools();
    },
    checkFocusableTools: function() {
        var me = this,
            tools = me.tools,
            haveFocusableTool, i, len;
        if (me.isAccordionHeader) {
            me.focusableContainer = false;
            return;
        }
        // We only need to enable FocusableContainer behavior when there are focusable tools.
        // For instance, Windows and Accordion panels can have Close tool that is not focusable,
        // in which case there is no sense in making the header behave like focusable container.
        for (i = 0 , len = tools.length; i < len; i++) {
            if (tools[i].focusable) {
                haveFocusableTool = true;
                break;
            }
        }
        if (haveFocusableTool) {
            if (!me.initialConfig.hasOwnProperty('focusableContainer') || me.focusableContainer) {
                me.ariaRole = 'toolbar';
                me.focusableContainer = true;
                if (me.rendered) {
                    me.ariaEl.dom.setAttribute('role', 'toolbar');
                    me.initFocusableContainer(true);
                }
            }
        } else {
            me.ariaRole = 'presentation';
            me.focusableContainer = false;
            if (me.rendered) {
                me.ariaEl.dom.setAttribute('role', 'presentation');
                me.initFocusableContainer(true);
            }
        }
    },
    /**
     * Gets the tools for this header.
     * @return {Ext.panel.Tool[]} The tools
     */
    getTools: function() {
        return this.tools.slice();
    },
    onAdd: function(component, index) {
        var tools = this.tools;
        this.callParent([
            component,
            index
        ]);
        if (component.isTool) {
            tools.push(component);
            tools[component.type] = component;
        }
    },
    onAdded: function(container, pos, instanced) {
        this.syncNoBorderCls();
        this.callParent([
            container,
            pos,
            instanced
        ]);
    },
    onRemoved: function(container, pos, instanced) {
        this.syncNoBorderCls();
        this.callParent([
            container,
            pos,
            instanced
        ]);
    },
    setDock: function(dock) {
        var me = this,
            title = me.getTitle(),
            rotation = me.getTitleRotation(),
            titleRotation = title.getRotation();
        Ext.suspendLayouts();
        me.callParent([
            dock
        ]);
        if (rotation === 'default') {
            rotation = (me.vertical ? 1 : 0);
            if (rotation !== titleRotation) {
                title.setRotation(rotation);
            }
            if (me.rendered) {
                // remove margins set on items by box layout last time around.
                // TODO: this will no longer be needed when EXTJS-13359 is fixed
                me.resetItemMargins();
            }
        }
        Ext.resumeLayouts(true);
    },
    updateGlyph: function(glyph) {
        this.getTitle().setGlyph(glyph);
    },
    updateIcon: function(icon) {
        this.getTitle().setIcon(icon);
    },
    updateIconAlign: function(align, oldAlign) {
        this.getTitle().setIconAlign(align);
    },
    updateIconCls: function(cls) {
        this.getTitle().setIconCls(cls);
    },
    updateTitle: function(title, oldTitle) {
        if (!oldTitle) {
            this.insert(this.getTitlePosition(), title);
            this._titleInItems = true;
        }
        // for backward compat with 4.x, set titleCmp property
        this.titleCmp = title;
    },
    updateTitleAlign: function(align, oldAlign) {
        this.getTitle().setTextAlign(align);
    },
    updateTitlePosition: function(position) {
        this.insert(position, this.getTitle());
    },
    updateTitleRotation: function(rotation) {
        if (rotation === 'default') {
            rotation = (this.vertical ? 1 : 0);
        }
        this.getTitle().setRotation(rotation);
    },
    privates: {
        fireClickEvent: function(type, e) {
            var toolCls = '.' + Ext.panel.Tool.prototype.baseCls;
            if (!e.getTarget(toolCls)) {
                this.fireEvent(type, this, e);
            }
        },
        getFramingInfoCls: function() {
            var me = this,
                cls = me.callParent(),
                owner = me.ownerCt;
            if (!me.expanding && owner && (owner.collapsed || me.isCollapsedExpander)) {
                cls += '-' + owner.collapsedCls;
            }
            return cls + '-' + me.dock;
        },
        onClick: function(e) {
            this.fireClickEvent('click', e);
        },
        onDblClick: function(e) {
            this.fireClickEvent('dblclick', e);
        },
        syncBeforeAfterTitleClasses: function(force) {
            var me = this,
                items = me.items,
                childItems = items.items,
                titlePosition = me.getTitlePosition(),
                itemCount = childItems.length,
                itemGeneration = items.generation,
                syncGen = me.syncBeforeAfterGen,
                afterCls, beforeCls, i, item;
            if (!force && (syncGen === itemGeneration)) {
                return;
            }
            me.syncBeforeAfterGen = itemGeneration;
            for (i = 0; i < itemCount; ++i) {
                item = childItems[i];
                afterCls = item.afterTitleCls || (item.afterTitleCls = item.baseCls + '-after-title');
                beforeCls = item.beforeTitleCls || (item.beforeTitleCls = item.baseCls + '-before-title');
                if (!me.title || i < titlePosition) {
                    if (syncGen) {
                        item.removeCls(afterCls);
                    }
                    // else first time we won't need to remove anything...
                    item.addCls(beforeCls);
                } else if (i > titlePosition) {
                    if (syncGen) {
                        item.removeCls(beforeCls);
                    }
                    item.addCls(afterCls);
                }
            }
        },
        syncNoBorderCls: function() {
            var me = this,
                ownerCt = this.ownerCt,
                noBorderCls = me.headerCls + '-noborder';
            // test for border === false is needed because undefined is the same as true
            if (ownerCt ? (ownerCt.border === false && !ownerCt.frame) : me.border === false) {
                me.addCls(noBorderCls);
            } else {
                me.removeCls(noBorderCls);
            }
        }
    }
});
// private

/**
 * @private
 * Base class for Box Layout overflow handlers. These specialized classes are invoked
 * when a Box Layout (either an HBox or a VBox) has child items that are either too wide
 * (for HBox) or too tall (for VBox) for its container.
 */
Ext.define('Ext.layout.container.boxOverflow.None', {
    alternateClassName: 'Ext.layout.boxOverflow.None',
    alias: [
        'box.overflow.none',
        'box.overflow.None'
    ],
    // capitalized for 4.x compat
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    factoryConfig: {
        defaultType: 'none'
    },
    isBoxOverflowHandler: true,
    $configPrefixed: false,
    $configStrict: false,
    constructor: function(config) {
        this.initConfig(config);
    },
    handleOverflow: Ext.emptyFn,
    clearOverflow: Ext.emptyFn,
    beginLayout: Ext.emptyFn,
    beginLayoutCycle: Ext.emptyFn,
    calculate: function(ownerContext) {
        var me = this,
            plan = ownerContext.state.boxPlan,
            overflow;
        if (plan && plan.tooNarrow) {
            overflow = me.handleOverflow(ownerContext);
            if (overflow) {
                if (overflow.reservedSpace) {
                    me.layout.publishInnerCtSize(ownerContext, overflow.reservedSpace);
                }
            }
        } else // TODO: If we need to use the code below then we will need to pass along
        // the new targetSize as state and use it calculate somehow...
        //
        // if (overflow.recalculate) {
        //    ownerContext.invalidate({
        //        state: {
        //            overflow: overflow
        //        }
        //    });
        // }
        {
            me.clearOverflow();
        }
    },
    completeLayout: Ext.emptyFn,
    finishedLayout: function(ownerContext) {
        var me = this,
            owner = me.layout.owner,
            hiddens, hiddenCount;
        // Only count hidden children if someone is interested when the overflow state changes
        if (owner.hasListeners.overflowchange) {
            hiddens = owner.query('>[hidden]');
            hiddenCount = hiddens.length;
            if (hiddenCount !== me.lastHiddenCount) {
                owner.fireEvent('overflowchange', me.lastHiddenCount, hiddenCount, hiddens);
                me.lastHiddenCount = hiddenCount;
            }
        }
    },
    onRemove: Ext.emptyFn,
    /**
     * @private
     * Normalizes an item reference, string id or numerical index into a reference to the item
     * @param {Ext.Component/String/Number} item The item reference, id or index
     * @return {Ext.Component} The item
     */
    getItem: function(item) {
        return this.layout.owner.getComponent(item);
    },
    getOwnerType: function(owner) {
        var type;
        if (owner.isToolbar) {
            type = 'toolbar';
        } else if (owner.isTabBar) {
            type = 'tab-bar';
        } else if (owner.isMenu) {
            type = 'menu';
        } else if (owner.isBreadcrumb) {
            type = 'breadcrumb';
        } else {
            type = owner.getXType();
        }
        return type;
    },
    getPrefixConfig: Ext.emptyFn,
    getSuffixConfig: Ext.emptyFn,
    getOverflowCls: function() {
        return '';
    },
    setVertical: function() {
        var me = this,
            layout = me.layout,
            innerCt = layout.innerCt;
        innerCt.removeCls(me.getOverflowCls(layout.oppositeDirection));
        innerCt.addCls(me.getOverflowCls(layout.direction));
    }
});

/**
 * @private
 */
Ext.define('Ext.layout.container.boxOverflow.Scroller', {
    extend: 'Ext.layout.container.boxOverflow.None',
    alternateClassName: 'Ext.layout.boxOverflow.Scroller',
    alias: [
        'box.overflow.scroller',
        'box.overflow.Scroller'
    ],
    // capitalized for 4.x compat
    requires: [
        'Ext.util.ClickRepeater',
        'Ext.Element'
    ],
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
    /**
     * @cfg {Boolean} animateScroll
     * True to animate the scrolling of items within the layout (ignored if enableScroll is false)
     */
    animateScroll: false,
    /**
     * @cfg {Number} scrollIncrement
     * The number of pixels to scroll by on scroller click
     */
    scrollIncrement: 20,
    /**
     * @cfg {Number} wheelIncrement
     * The number of pixels to increment on mouse wheel scrolling.
     */
    wheelIncrement: 10,
    /**
     * @cfg {Number} scrollRepeatInterval
     * Number of milliseconds between each scroll while a scroller button is held down
     */
    scrollRepeatInterval: 60,
    /**
     * @cfg {Number} scrollDuration
     * Number of milliseconds that each scroll animation lasts
     */
    scrollDuration: 400,
    /**
     * @private
     */
    scrollerCls: Ext.baseCSSPrefix + 'box-scroller',
    beforeSuffix: '-before-scroller',
    afterSuffix: '-after-scroller',
    /**
     * @event scroll
     * @param {Ext.layout.container.boxOverflow.Scroller} scroller The layout scroller
     * @param {Number} newPosition The new position of the scroller
     */
    constructor: function(config) {
        var me = this;
        me.mixins.observable.constructor.call(me, config);
        me.layout.owner.on({
            afterrender: me.onOwnerRender,
            scope: me,
            single: true
        });
        me.layout.owner.getOverflowEl = me.ownerGetOverflowImpl;
        me.scrollPosition = 0;
        me.scrollSize = 0;
    },
    onOwnerRender: function(owner) {
        var me = this,
            scrollable = {
                isBoxOverflowScroller: true,
                x: false,
                y: false,
                listeners: {
                    scrollend: this.onScrollEnd,
                    scope: this
                }
            };
        // If no obstrusive scrollbars, allow natural scrolling on mobile touch devices
        if (!Ext.scrollbar.width() && !Ext.platformTags.desktop) {
            scrollable[owner.layout.horizontal ? 'x' : 'y'] = true;
        } else {
            me.wheelListener = me.layout.innerCt.on('mousewheel', me.onMouseWheel, me, {
                destroyable: true
            });
        }
        owner.setScrollable(scrollable);
    },
    getPrefixConfig: function() {
        return {
            role: 'presentation',
            id: this.layout.owner.id + this.beforeSuffix,
            cls: this.createScrollerCls('beforeX'),
            style: 'display:none'
        };
    },
    getSuffixConfig: function() {
        return {
            role: 'presentation',
            id: this.layout.owner.id + this.afterSuffix,
            cls: this.createScrollerCls('afterX'),
            style: 'display:none'
        };
    },
    createScrollerCls: function(xName) {
        var me = this,
            layout = me.layout,
            owner = layout.owner,
            type = me.getOwnerType(owner),
            scrollerCls = me.scrollerCls,
            cls = scrollerCls + ' ' + scrollerCls + '-' + layout.names[xName] + ' ' + scrollerCls + '-' + type + ' ' + scrollerCls + '-' + type + '-' + owner.ui;
        if (owner.plain) {
            // Add plain class for components that need separate "plain" styling (e.g. tab bar)
            cls += ' ' + scrollerCls + '-plain';
        }
        return cls;
    },
    getOverflowCls: function(direction) {
        return this.scrollerCls + '-body-' + direction;
    },
    beginLayout: function(ownerContext) {
        ownerContext.innerCtScrollPos = this.getScrollPosition();
        this.callParent(arguments);
    },
    finishedLayout: function(ownerContext) {
        var me = this,
            plan = ownerContext.state.boxPlan,
            layout = me.layout,
            names = layout.names,
            scrollPos = Math.min(me.getMaxScrollPosition(), ownerContext.innerCtScrollPos),
            lastProps;
        // If there is overflow...
        if (plan && plan.tooNarrow) {
            lastProps = ownerContext.childItems[ownerContext.childItems.length - 1].props;
            // capture this before callParent since it calls handle/clearOverflow:
            me.scrollSize = lastProps[names.x] + lastProps[names.width];
            me.updateScrollButtons();
            // Restore pre layout scroll position
            layout.innerCt[names.setScrollLeft](scrollPos);
        }
        me.callParent([
            ownerContext
        ]);
    },
    handleOverflow: function(ownerContext) {
        var me = this,
            names = me.layout.names,
            getWidth = names.getWidth,
            parallelMargins = names.parallelMargins,
            scrollerWidth, targetPaddingWidth, beforeScroller, afterScroller;
        me.showScrollers();
        beforeScroller = me.getBeforeScroller();
        afterScroller = me.getAfterScroller();
        scrollerWidth = beforeScroller[getWidth]() + afterScroller[getWidth]() + beforeScroller.getMargin(parallelMargins) + afterScroller.getMargin(parallelMargins);
        targetPaddingWidth = ownerContext.targetContext.getPaddingInfo()[names.width];
        return {
            reservedSpace: Math.max(scrollerWidth - targetPaddingWidth, 0)
        };
    },
    /**
     * @private
     * Returns a reference to the "before" scroller element.  Creates click handlers on
     * the first call.
     */
    getBeforeScroller: function() {
        var me = this;
        return me._beforeScroller || (me._beforeScroller = me.createScroller(me.beforeSuffix, 'beforeRepeater', 'scrollLeft'));
    },
    /**
     * @private
     * Returns a reference to the "after" scroller element.  Creates click handlers on
     * the first call.
     */
    getAfterScroller: function() {
        var me = this;
        return me._afterScroller || (me._afterScroller = me.createScroller(me.afterSuffix, 'afterRepeater', 'scrollRight'));
    },
    createScroller: function(suffix, repeaterName, scrollHandler) {
        var me = this,
            owner = me.layout.owner,
            scrollerCls = me.scrollerCls,
            scrollerEl;
        scrollerEl = owner.el.getById(owner.id + suffix);
        scrollerEl.addClsOnOver(scrollerCls + '-hover');
        scrollerEl.addClsOnClick(scrollerCls + '-pressed');
        scrollerEl.setVisibilityMode(Ext.Element.DISPLAY);
        me[repeaterName] = new Ext.util.ClickRepeater(scrollerEl, {
            interval: me.scrollRepeatInterval,
            handler: scrollHandler,
            scope: me
        });
        return scrollerEl;
    },
    onMouseWheel: function(e) {
        var cmp = Ext.Component.from(e.target),
            cmpScroller = cmp.getScrollable && cmp.getScrollable();
        // Only stop the event if we are not scrolling a scrollable component
        // inside this container.
        if (!cmpScroller || (cmpScroller === this.layout.owner.getScrollable())) {
            e.stopEvent();
            this.scrollBy(this.getWheelDelta(e) * this.wheelIncrement * -1, false);
        }
    },
    getWheelDelta: function(e) {
        return e.getWheelDelta();
    },
    /**
     * @private
     */
    clearOverflow: function() {
        this.hideScrollers();
    },
    /**
     * @private
     * Shows the scroller elements. Creates the scrollers first if they are not already present.
     */
    showScrollers: function() {
        var me = this;
        me.getBeforeScroller().show();
        me.getAfterScroller().show();
        me.layout.owner.addClsWithUI(me.layout.direction === 'vertical' ? 'vertical-scroller' : 'scroller');
    },
    // TODO - this may invalidates data in the ContextItem's styleCache
    /**
     * @private
     * Hides the scroller elements.
     */
    hideScrollers: function() {
        var me = this,
            beforeScroller = me.getBeforeScroller(),
            afterScroller = me.getAfterScroller();
        if (beforeScroller) {
            beforeScroller.hide();
            afterScroller.hide();
            me.layout.owner.removeClsWithUI(me.layout.direction === 'vertical' ? 'vertical-scroller' : 'scroller');
        }
    },
    // TODO - this may invalidates data in the ContextItem's styleCache
    destroy: function() {
        Ext.destroyMembers(this, 'beforeRepeater', 'afterRepeater', '_beforeScroller', '_afterScroller', 'wheelListener');
        this.callParent();
    },
    /**
     * @private
     * Scrolls left or right by the number of pixels specified
     * @param {Number} delta Number of pixels to scroll to the right by.
     * Use a negative number to scroll left
     * @param {Boolean} animate
     */
    scrollBy: function(delta, animate) {
        var layout = this.layout,
            scroller = layout.owner.getScrollable(),
            args = [
                0,
                0,
                animate ? this.getScrollAnim() : false
            ];
        args[layout.horizontal ? 0 : 1] = delta;
        scroller.scrollBy.apply(scroller, args);
    },
    /**
     * @private
     * @return {Object} Object passed to scrollTo when scrolling
     */
    getScrollAnim: function() {
        return {
            duration: this.scrollDuration,
            callback: this.updateScrollButtons,
            scope: this
        };
    },
    /**
     * @private
     * Enables or disables each scroller button based on the current scroll position
     */
    updateScrollButtons: function() {
        var me = this,
            beforeScroller = me.getBeforeScroller(),
            afterScroller = me.getAfterScroller(),
            scrollPos = me.getScrollPosition(),
            disabledCls;
        if (!beforeScroller || !afterScroller) {
            return;
        }
        disabledCls = me.scrollerCls + '-disabled';
        beforeScroller[scrollPos ? 'removeCls' : 'addCls'](disabledCls);
        afterScroller[scrollPos >= me.getMaxScrollPosition() ? 'addCls' : 'removeCls'](disabledCls);
    },
    /**
     * @private
     * Scrolls to the left by the configured amount
     */
    scrollLeft: function() {
        this.scrollBy(-this.scrollIncrement, false);
    },
    /**
     * @private
     * Scrolls to the right by the configured amount
     */
    scrollRight: function() {
        this.scrollBy(this.scrollIncrement, false);
    },
    /**
     * Returns the current scroll position of the innerCt element
     * @return {Number} The current scroll position
     */
    getScrollPosition: function() {
        var layout = this.layout;
        return layout.owner.getScrollable().getPosition()[layout.horizontal ? 'x' : 'y'];
    },
    /**
     * @private
     * Returns the maximum value we can scrollTo
     * @return {Number} The max scroll value
     */
    getMaxScrollPosition: function() {
        var layout = this.layout;
        return layout.owner.getScrollable().getMaxPosition()[layout.horizontal ? 'x' : 'y'];
    },
    /**
     * @private
     */
    setVertical: function() {
        var me = this,
            beforeScroller = me.getBeforeScroller(),
            afterScroller = me.getAfterScroller(),
            names = me.layout.names,
            scrollerCls = me.scrollerCls;
        beforeScroller.removeCls(scrollerCls + '-' + names.beforeY);
        afterScroller.removeCls(scrollerCls + '-' + names.afterY);
        beforeScroller.addCls(scrollerCls + '-' + names.beforeX);
        afterScroller.addCls(scrollerCls + '-' + names.afterX);
        me.callParent();
    },
    /**
     * @private
     * Scrolls to the given position. Performs bounds checking.
     * @param {Number} position The position to scroll to. This is constrained.
     * @param {Boolean} animate True to animate. If undefined, falls back to value
     * of this.animateScroll
     */
    scrollTo: function(position, animate) {
        var layout = this.layout,
            scroller = layout.owner.getScrollable(),
            args = [
                0,
                0,
                animate ? this.getScrollAnim() : false
            ];
        args[layout.horizontal ? 0 : 1] = position;
        scroller.scrollTo.apply(scroller, args);
    },
    onScrollEnd: function(scroller, x, y) {
        this.updateScrollButtons();
        this.fireEvent('scroll', this, this.layout.horizontal ? x : y, false);
    },
    /**
     * Scrolls to the given component.
     * @param {String/Number/Ext.Component} item The item to scroll to. Can be a numerical index,
     * component id or a reference to the component itself.
     * @param {Boolean} animate True to animate the scrolling
     */
    scrollToItem: function(item, animate) {
        item = this.getItem(item);
        if (item !== undefined) {
            this.layout.owner.getScrollable().ensureVisible(item.el, {
                animation: animate
            });
        }
    },
    privates: {
        // This is injected into the owner component because the scroller
        // must be applied to the element this this class scrolls
        ownerGetOverflowImpl: function() {
            return this.layout.innerCt;
        }
    }
});

/*
 * This is a derivative of the similarly named class in the YUI Library.
 * The original license:
 * Copyright (c) 2006, Yahoo! Inc. All rights reserved.
 * Code licensed under the BSD License:
 * http://developer.yahoo.net/yui/license.txt
 */
/**
 * DragDropManager is a singleton that tracks the element interaction for
 * all DragDrop items in the window.  Generally, you will not call
 * this class directly, but it does have helper methods that could
 * be useful in your DragDrop implementations.
 */
Ext.define('Ext.dd.DragDropManager', {
    singleton: true,
    requires: [
        'Ext.util.Region'
    ],
    uses: [
        'Ext.tip.QuickTipManager'
    ],
    // shorter ClassName, to save bytes and use internally
    alternateClassName: [
        'Ext.dd.DragDropMgr',
        'Ext.dd.DDM'
    ],
    /**
     * @property {String[]} ids
     * Two dimensional Array of registered DragDrop objects.  The first
     * dimension is the DragDrop item group, the second the DragDrop
     * object.
     * @private
     */
    ids: {},
    /**
     * @property {String[]} handleIds
     * Array of element ids defined as drag handles.  Used to determine
     * if the element that generated the mousedown event is actually the
     * handle and not the html element itself.
     * @private
     */
    handleIds: {},
    /**
     * @property {Ext.dd.DragDrop} dragCurrent
     * the DragDrop object that is currently being dragged
     * @private
     */
    dragCurrent: null,
    /**
     * @property {Ext.dd.DragDrop[]} dragOvers
     * the DragDrop object(s) that are being hovered over
     * @private
     */
    dragOvers: {},
    /**
     * @property {Number} deltaX
     * the X distance between the cursor and the object being dragged
     * @private
     */
    deltaX: 0,
    /**
     * @property {Number} deltaY
     * the Y distance between the cursor and the object being dragged
     * @private
     */
    deltaY: 0,
    /**
     * @property {Boolean} preventDefault
     * `true` to invoke `preventDefault()` on all events during a drag (may be
     * mouse, touch, or pointer events depending on the platform).
     *
     * @deprecated 6.2.0 Use {@link Ext.Component#touchAction touchAction} or
     * {@link Ext.dom.Element#setTouchAction setTouchAction} to prevent specific
     * default actions during drag
     */
    preventDefault: true,
    /**
     * @property {Boolean} stopPropagation
     * `true` to invoke `stopPropagation()` on all events during a drag (may be
     * mouse, touch, or pointer events depending on the platform).
     *
     * @deprecated 6.2.0 This property is deprecated
     */
    stopPropagation: false,
    /**
     * Internal flag that is set to true when drag and drop has been
     * initialized
     * @property initialized
     * @private
     */
    initialized: false,
    /**
     * All drag and drop can be disabled.
     * @property locked
     * @private
     */
    locked: false,
    /**
     * Called the first time an element is registered.
     * @private
     */
    init: function() {
        this.initialized = true;
    },
    /**
     * @property {Number} POINT
     * In point mode, drag and drop interaction is defined by the
     * location of the cursor during the drag/drop
     */
    POINT: 0,
    /**
     * @property {Number} INTERSECT
     * In intersect mode, drag and drop interaction is defined by the
     * overlap of two or more drag and drop objects.
     */
    INTERSECT: 1,
    /**
     * @property {Number} mode
     * The current drag and drop mode.  Default: POINT
     */
    mode: 0,
    /**
     * @property {Boolean} [notifyOccluded=false]
     * This config is only provided to provide old, usually unwanted drag/drop behaviour.
     *
     * From ExtJS 4.1.0 onwards, when drop targets are contained in floating, absolutely positioned
     * elements such as in {@link Ext.window.Window Windows}, which may overlap each other,
     * `over` and `drop` events are only delivered to the topmost drop target at the mouse position.
     *
     * If all targets below that in zIndex order should also receive notifications, set
     * `notifyOccluded` to `true`.
     */
    notifyOccluded: false,
    /**
     * @property {String} dragCls
     * @readonly
     * Class to add to the {@link Ext.dd.DragDrop#getDragEl dragged element} of a DragDrop instance.
     */
    dragCls: Ext.baseCSSPrefix + 'dd-drag-current',
    currentPoint: new Ext.util.Point(),
    /**
     * Runs method on all drag and drop objects
     * @private
     */
    _execOnAll: function(sMethod, args) {
        var ids = this.ids,
            i, j, oDD, item;
        for (i in ids) {
            if (ids.hasOwnProperty(i)) {
                item = ids[i];
                for (j in item) {
                    if (item.hasOwnProperty(j)) {
                        oDD = item[j];
                        if (!this.isTypeOfDD(oDD)) {
                            
                            continue;
                        }
                        oDD[sMethod].apply(oDD, args);
                    }
                }
            }
        }
    },
    /**
     * Drag and drop initialization.  Sets up the global event handlers
     * @private
     */
    addListeners: function() {
        var me = this;
        me.init();
        Ext.getWin().on({
            unload: me._onUnload,
            resize: me._onResize,
            scope: me
        });
    },
    /**
     * Reset constraints on all drag and drop objs
     * @private
     */
    _onResize: function(e) {
        this._execOnAll("resetConstraints", []);
    },
    /**
     * Lock all drag and drop functionality
     */
    lock: function() {
        this.locked = true;
    },
    /**
     * Unlock all drag and drop functionality
     */
    unlock: function() {
        this.locked = false;
    },
    /**
     * Is drag and drop locked?
     * @return {Boolean} True if drag and drop is locked, false otherwise.
     */
    isLocked: function() {
        return this.locked;
    },
    /**
     * @property {Object} locationCache
     * Location cache that is set for all drag drop objects when a drag is
     * initiated, cleared when the drag is finished.
     * @private
     */
    locationCache: {},
    /**
     * @property {Boolean} useCache
     * Set useCache to false if you want to force object the lookup of each
     * drag and drop linked element constantly during a drag.
     */
    useCache: true,
    /**
     * @property {Number} clickPixelThresh
     * The number of pixels that the mouse needs to move after the
     * mousedown before the drag is initiated.  Default=8;
     * defaults to the same value used in the LongPress gesture so that drag cannot be
     * initiated if there is a possible pending longpress
     */
    clickPixelThresh: 8,
    /**
     * @property {Boolean} dragThreshMet
     * Flag that indicates that either the drag pixel threshold or the
     * mousdown time threshold has been met
     * @private
     */
    dragThreshMet: false,
    /**
     * @property {Object} clickTimeout
     * Timeout used for the click time threshold
     * @private
     */
    clickTimeout: null,
    /**
     * @property {Number} startX
     * The X position of the mousedown event stored for later use when a
     * drag threshold is met.
     * @private
     */
    startX: 0,
    /**
     * @property {Number} startY
     * The Y position of the mousedown event stored for later use when a
     * drag threshold is met.
     * @private
     */
    startY: 0,
    /**
     * Each DragDrop instance must be registered with the DragDropManager.
     * This is executed in DragDrop.init()
     * @param {Ext.dd.DragDrop} oDD the DragDrop object to register
     * @param {String} sGroup the name of the group this element belongs to
     */
    regDragDrop: function(oDD, sGroup) {
        if (!this.initialized) {
            this.init();
        }
        if (!this.ids[sGroup]) {
            this.ids[sGroup] = {};
        }
        this.ids[sGroup][oDD.id] = oDD;
    },
    /**
     * Removes the supplied dd instance from the supplied group. Executed
     * by DragDrop.removeFromGroup, so don't call this function directly.
     * @private
     */
    removeDDFromGroup: function(oDD, sGroup) {
        var obj;
        if (!this.ids[sGroup]) {
            this.ids[sGroup] = {};
        }
        obj = this.ids[sGroup];
        if (obj && obj[oDD.id]) {
            delete obj[oDD.id];
        }
    },
    /**
     * Unregisters a drag and drop item.  This is executed in
     * DragDrop.unreg, use that method instead of calling this directly.
     * @private
     */
    _remove: function(oDD, clearGroup) {
        var me = this,
            ids = me.ids,
            groups = oDD.groups,
            g;
        // If we're clearing everything, we'll just end up wiping
        // this.ids & this.handleIds
        if (me.clearingAll) {
            return;
        }
        if (me.dragCurrent === oDD) {
            me.dragCurrent = null;
        }
        for (g in groups) {
            if (groups.hasOwnProperty(g)) {
                if (clearGroup) {
                    delete ids[g];
                } else if (ids[g]) {
                    delete ids[g][oDD.id];
                }
            }
        }
        delete me.handleIds[oDD.id];
        delete me.locationCache[oDD.id];
    },
    /**
     * Each DragDrop handle element must be registered.  This is done
     * automatically when executing DragDrop.setHandleElId()
     * @param {String} sDDId the DragDrop id this element is a handle for
     * @param {String} sHandleId the id of the element that is the drag
     * handle
     */
    regHandle: function(sDDId, sHandleId) {
        if (!this.handleIds[sDDId]) {
            this.handleIds[sDDId] = {};
        }
        this.handleIds[sDDId][sHandleId] = sHandleId;
    },
    /**
     * Utility function to determine if a given element has been
     * registered as a drag drop item.
     * @param {String} id the element id to check
     * @return {Boolean} true if this element is a DragDrop item,
     * false otherwise
     */
    isDragDrop: function(id) {
        return (this.getDDById(id)) ? true : false;
    },
    /**
     * Returns the drag and drop instances that are in all groups the
     * passed in instance belongs to.
     * @param {Ext.dd.DragDrop} p_oDD the obj to get related data for
     * @param {Boolean} bTargetsOnly if true, only return targetable objs
     * @return {Ext.dd.DragDrop[]} the related instances
     */
    getRelated: function(p_oDD, bTargetsOnly) {
        var oDDs = [],
            i, j, dd;
        for (i in p_oDD.groups) {
            for (j in this.ids[i]) {
                dd = this.ids[i][j];
                if (!this.isTypeOfDD(dd)) {
                    
                    continue;
                }
                if (!bTargetsOnly || dd.isTarget) {
                    oDDs[oDDs.length] = dd;
                }
            }
        }
        return oDDs;
    },
    /**
     * Returns true if the specified dd target is a legal target for
     * the specifice drag obj
     * @param {Ext.dd.DragDrop} oDD the drag obj
     * @param {Ext.dd.DragDrop} oTargetDD the target
     * @return {Boolean} true if the target is a legal target for the
     * dd obj
     */
    isLegalTarget: function(oDD, oTargetDD) {
        var targets = this.getRelated(oDD, true),
            i, len;
        for (i = 0 , len = targets.length; i < len; ++i) {
            if (targets[i].id === oTargetDD.id) {
                return true;
            }
        }
        return false;
    },
    /**
     * My goal is to be able to transparently determine if an object is
     * typeof DragDrop, and the exact subclass of DragDrop.  typeof
     * returns "object", oDD.constructor.toString() always returns
     * "DragDrop" and not the name of the subclass.  So for now it just
     * evaluates a well-known variable in DragDrop.
     * @param {Object} oDD The object to evaluate
     * @return {Boolean} true if typeof oDD = DragDrop
     */
    isTypeOfDD: function(oDD) {
        return (oDD && oDD.__ygDragDrop);
    },
    /**
     * Utility function to determine if a given element has been
     * registered as a drag drop handle for the given Drag Drop object.
     * @param {String} sDDId the element id to check
     * @param {String} sHandleId
     * @return {Boolean} true if this element is a DragDrop handle, false
     * otherwise
     */
    isHandle: function(sDDId, sHandleId) {
        return (this.handleIds[sDDId] && this.handleIds[sDDId][sHandleId]);
    },
    /**
     * Returns the DragDrop instance for a given id
     * @param {String} id the id of the DragDrop object
     * @param {Boolean} force
     * @return {Ext.dd.DragDrop} the drag drop object, null if it is not found
     */
    getDDById: function(id, force) {
        var i, dd;
        for (i in this.ids) {
            dd = this.ids[i][id];
            if (dd instanceof Ext.dd.DDTarget || force) {
                return dd;
            }
        }
        return null;
    },
    /**
     * Fired after a registered DragDrop object gets the mousedown event.
     * Sets up the events required to track the object being dragged
     * @param {Event} e the event
     * @param {Ext.dd.DragDrop} oDD the DragDrop object being dragged
     * @private
     */
    handleMouseDown: function(e, oDD) {
        var me = this,
            xy = e.getXY(),
            el = oDD.getEl(),
            // let other mouseup events occur before us
            pointerup = {
                translate: false,
                fn: me.handleMouseUp,
                capture: false,
                priority: -1000
            },
            // Mousemove events do not need to be captured because they do not contend
            // with scroll events - they're only processed when a drag has begun.
            // Capturing was causing https://sencha.jira.com/browse/EXTJS-13952
            pointermove = {
                translate: false,
                fn: me.handleMouseMove,
                capture: false
            },
            listeners = {
                capture: true,
                destroyable: true,
                scope: me
            },
            DomEventType = e.browserEvent.type;
        // On devices that support multi-touch the second touch terminates drag
        listeners.touchstart = me.handleMouseUp;
        // Listen for the right kind of events depending on how
        // the drag was initiated.
        // Pointer events standard
        if (DomEventType === 'pointerdown') {
            listeners.pointerup = pointerup;
            listeners.pointermove = pointermove;
        }
        // IE10 pointer event
        else if (DomEventType === 'MSPointerDown') {
            listeners.MSPointerUp = pointerup;
            listeners.MSPointerMove = pointermove;
        }
        // Real mouse event
        else if (DomEventType === 'mousedown') {
            listeners.mouseup = pointerup;
            listeners.mousemove = pointermove;
        } else // Touch start
        {
            listeners.touchend = pointerup;
            listeners.touchmove = pointermove;
        }
        me.pointerMoveListeners = Ext.getDoc().on(listeners);
        me.isMouseDown = true;
        me.currentPoint.setPosition(xy);
        if (me.dragCurrent) {
            // the original browser mouseup wasn't handled (e.g. outside FF browser window)
            // so clean up first to avoid breaking the next drag
            me.handleMouseUp(e);
        }
        me.mousedownEvent = e;
        me.currentTarget = e.getTarget();
        me.dragCurrent = oDD;
        // We use this to handle an issue where a mouseup will not be detected
        // if the mouseup event happens outside of the browser window. When the
        // mouse comes back, any drag will still be active
        // http://msdn.microsoft.com/en-us/library/ms537630(VS.85).aspx
        Ext.fly(el).setCapture();
        // track start position
        me.startX = xy[0];
        me.startY = xy[1];
        // Track the distance moved.
        me.offsetX = me.offsetY = 0;
        me.deltaX = me.startX - el.offsetLeft;
        me.deltaY = me.startY - el.offsetTop;
        me.dragThreshMet = false;
    },
    /**
     * Fired when either the drag pixel threshold or the mousedown hold
     * time threshold has been met.
     * @param {Number} x the X position of the original mousedown
     * @param {Number} y the Y position of the original mousedown
     */
    startDrag: function(x, y) {
        var me = this,
            current = me.dragCurrent,
            dragEl;
        Ext.undefer(me.clickTimeout);
        if (Ext.quickTipsActive) {
            Ext.tip.QuickTipManager.ddDisable();
        }
        if (current) {
            current.b4StartDrag(x, y);
            current.startDrag(x, y);
            dragEl = Ext.fly(current.getDragEl());
            // Add current drag class to dragged element
            if (dragEl) {
                dragEl.addCls(me.dragCls);
                // This will allow pointer events to bubble through the shim iframe
                // to the parent document
                if (dragEl.shim && dragEl.shim.el) {
                    dragEl.shim.el.addCls(me.dragCls);
                }
            }
        }
        me.dragThreshMet = true;
    },
    /**
     * Internal function to handle the mouseup event.  Will be invoked
     * from the context of the document.
     * @param {Event} e the event
     * @private
     */
    handleMouseUp: function(e) {
        var me = this;
        // We only listen for pointermove after a trigger event
        me.pointerMoveListeners.destroy();
        me.isMouseDown = false;
        if (!me.dragCurrent) {
            return;
        }
        // See setCapture call in handleMouseDown
        if (Ext.isIE && document.releaseCapture) {
            document.releaseCapture();
        }
        Ext.undefer(me.clickTimeout);
        if (me.dragThreshMet) {
            me.fireEvents(e, true);
        }
        me.stopDrag(e);
        if (me.dragThreshMet) {
            me.stopEvent(e);
        }
        me.mousedownEvent = me.currentTarget = null;
    },
    /**
     * Utility to stop event propagation and event default, if these
     * features are turned on.
     * @param {Event} e the event as returned by this.getEvent()
     */
    stopEvent: function(e) {
        if (this.stopPropagation) {
            e.stopPropagation();
        }
        if (this.preventDefault && e.pointerType === 'touch') {
            e.preventDefault();
        }
    },
    /**
     * Internal function to clean up event handlers after the drag
     * operation is complete
     * @param {Event} e the event
     * @private
     */
    stopDrag: function(e) {
        var me = this,
            current = me.dragCurrent,
            dragEl;
        if (Ext.quickTipsActive) {
            Ext.tip.QuickTipManager.ddEnable();
        }
        // Fire the drag end event for the item that was dragged
        if (current) {
            if (me.dragThreshMet) {
                // Remove current drag class from dragged element
                dragEl = Ext.fly(current.getDragEl());
                if (dragEl) {
                    dragEl.removeCls(me.dragCls);
                    if (dragEl.shim && dragEl.shim.el) {
                        dragEl.shim.el.removeCls(me.dragCls);
                    }
                }
                current.b4EndDrag(e);
                current.endDrag(e);
            }
            me.dragCurrent.onMouseUp(e);
        }
        me.dragCurrent = null;
        me.dragOvers = {};
    },
    /**
     * Internal function to handle the mousemove event.  Will be invoked
     * from the context of the html element.
     *
     * TODO: figure out what we can do about mouse events lost when the
     * user drags objects beyond the window boundary.  Currently we can
     * detect this in internet explorer by verifying that the mouse is
     * down during the mousemove event.  Firefox doesn't give us the
     * button state on the mousemove event.
     *
     * @param {Event} e the event
     * @private
     */
    handleMouseMove: function(e) {
        var me = this,
            current = me.dragCurrent,
            point = e.getXY(),
            currentX = point[0],
            currentY = point[1],
            diffX, diffY;
        me.offsetX = currentX - me.startX;
        me.offsetY = currentY - me.startY;
        me.currentPoint.setPosition(point);
        if (!current) {
            return true;
        }
        if (!me.dragThreshMet) {
            diffX = Math.abs(me.offsetX);
            diffY = Math.abs(me.offsetY);
            if (diffX > me.clickPixelThresh || diffY > me.clickPixelThresh) {
                e.claimGesture();
                me.startDrag(me.startX, me.startY);
            }
        }
        if (me.dragThreshMet) {
            current.b4Drag(e);
            current.onDrag(e);
            if (!current.moveOnly) {
                me.fireEvents(e, false);
            }
        }
        me.stopEvent(e);
        return true;
    },
    /**
     * Iterates over all of the DragDrop elements to find ones we are
     * hovering over or dropping on
     * @param {Event} e the event
     * @param {Boolean} isDrop is this a drop op or a mouseover op?
     * @private
     */
    fireEvents: function(e, isDrop) {
        var me = this,
            isTouch = Ext.supports.Touch,
            dragCurrent = me.dragCurrent,
            mousePoint = me.currentPoint,
            allTargets = [],
            oldOvers = [],
            // cache the previous dragOver array
            outEvts = [],
            overEvts = [],
            dropEvts = [],
            enterEvts = [],
            dragEl, overTarget, overTargetEl, needsSort, i, len, sGroup, overDragEl;
        // If the user did the mouse up outside of the window, we could
        // get here even though we have ended the drag.
        if (!dragCurrent || dragCurrent.isLocked()) {
            return;
        }
        // Touch's delegated event system means that the mousemove (which will be a touchmove
        // really) target will be the element that the listener was requested for, NOT the actual
        // lowest level target. So we have to use elementFromPoint to find the target which we are
        // currently over.
        //
        // If we need to use the current mousemove target to find the over el, but pointer-events
        // is not supported, AND the delta position does not place the mouse outside of the dragEl,
        // temporarily move the dragEl away, and fake the mousemove target by using
        // document.elementFromPoint while it's out of the way.
        // The pointer events implementation is bugged in IE9/10 and opera, so fallback even if they
        // report that they support it. IE8m do not support it so they will auto fall back.
        overDragEl = !(dragCurrent.deltaX < 0 || dragCurrent.deltaY < 0);
        if (isTouch || (!me.notifyOccluded && (!Ext.supports.CSSPointerEvents || Ext.isIE10m || Ext.isOpera) && overDragEl)) {
            dragEl = dragCurrent.getDragEl();
            // Temporarily hide the dragEl instead of moving it off the page. Moving the el
            // off the page can cause problems when in an iframe with IE8 standards.
            // See EXTJSIV-11728.
            if (overDragEl) {
                dragEl.style.visibility = 'hidden';
            }
            // In Win10, dragging outside the browser window will cause elementFromPoint to
            // return null. In these cases, default to the document.
            // We are about to change the event target so that it behaves like a mouse
            // event, not a touch event.  We first need to prototype chain a new object
            // to the original event, to avoid modifying the original.
            e = e.chain({
                target: me.elementFromPoint(e.clientX, e.clientY) || document.documentElement
            });
            if (overDragEl) {
                dragEl.style.visibility = 'visible';
            }
        }
        // Check to see if the object(s) we were hovering over is no longer
        // being hovered over so we can fire the onDragOut event
        for (i in me.dragOvers) {
            overTarget = me.dragOvers[i];
            delete me.dragOvers[i];
            // Check to make sure that the component hasn't been destroyed in the middle
            // of a drag operation.
            if (!me.isTypeOfDD(overTarget) || overTarget.destroyed) {
                
                continue;
            }
            // On mouseup/pointerup/touchend, we destroy our
            // pointerMoveListeners, (see handleMouseUp). So will will recieve no
            // further move notifications to cause the terminating "out"
            // events, so create the out events now.
            if (isDrop) {
                outEvts.push(overTarget);
            } else {
                // If notifyOccluded set, we use mouse position
                if (me.notifyOccluded) {
                    if (!this.isOverTarget(mousePoint, overTarget, me.mode)) {
                        outEvts.push(overTarget);
                    }
                } else // Otherwise we use event source of the mousemove event
                {
                    if (!e.within(overTarget.getEl())) {
                        outEvts.push(overTarget);
                    }
                }
            }
            oldOvers[i] = true;
        }
        // Collect all targets which are members of the same ddGoups that the dragCurrent
        // is a member of, and which may receive mouseover and drop notifications.
        // This is preparatory to seeing which one(s) we are currently over
        // Begin by iterating through the ddGroups of which the dragCurrent is a member
        for (sGroup in dragCurrent.groups) {
            if ("string" !== typeof sGroup) {
                
                continue;
            }
            // Loop over the registered members of each group, testing each as a potential target
            for (i in me.ids[sGroup]) {
                overTarget = me.ids[sGroup][i];
                // The target is valid if it is a DD type
                // And it's got a DOM element
                // And it's configured to be a drop target
                // And it's not locked
                // And the DOM element is fully visible with no hidden ancestors
                // And it's either not the dragCurrent, or, if it is, tha dragCurrent is configured
                // to not ignore itself.
                if (me.isTypeOfDD(overTarget) && (overTargetEl = overTarget.getEl()) && (overTarget.isTarget) && (!overTarget.isLocked()) && (Ext.fly(overTargetEl).isVisible(true)) && ((overTarget !== dragCurrent) || (dragCurrent.ignoreSelf === false))) {
                    // If notifyOccluded set, we use mouse position
                    if (me.notifyOccluded) {
                        // Only sort by zIndex if there were some which had a floating zIndex value
                        if ((overTarget.zIndex = me.getZIndex(overTargetEl)) !== -1) {
                            needsSort = true;
                        }
                        allTargets.push(overTarget);
                    } else // Otherwise we use event source of the mousemove event
                    {
                        if (e.within(overTargetEl)) {
                            allTargets.push(overTarget);
                            break;
                        }
                    }
                }
            }
        }
        // If there were floating targets, sort the highest zIndex to the top
        if (needsSort) {
            Ext.Array.sort(allTargets, me.byZIndex);
        }
        // Loop through possible targets, notifying the one(s) we are over.
        // Usually we only deliver events to the topmost.
        for (i = 0 , len = allTargets.length; i < len; i++) {
            overTarget = allTargets[i];
            // If we are over the overTarget, queue it up to recieve an event of whatever type
            // we are handling
            if (me.isOverTarget(mousePoint, overTarget, me.mode)) {
                // look for drop interactions
                if (isDrop) {
                    dropEvts.push(overTarget);
                } else // look for drag enter and drag over interactions
                {
                    // initial drag over: dragEnter fires
                    if (!oldOvers[overTarget.id]) {
                        enterEvts.push(overTarget);
                    } else // subsequent drag overs: dragOver fires
                    {
                        overEvts.push(overTarget);
                    }
                    me.dragOvers[overTarget.id] = overTarget;
                }
                // Unless this DragDropManager has been explicitly configured to deliver events
                // to multiple targets, then we are done.
                if (!me.notifyOccluded) {
                    break;
                }
            }
        }
        if (me.mode) {
            if (enterEvts.length) {
                dragCurrent.onDragEnter(e, enterEvts);
            }
            if (overEvts.length) {
                dragCurrent.b4DragOver(e, overEvts);
                dragCurrent.onDragOver(e, overEvts);
            }
            if (dropEvts.length) {
                dragCurrent.b4DragDrop(e, dropEvts);
                dragCurrent.onDragDrop(e, dropEvts);
            }
            // fire dragout events.
            // These are fires on mouseup/pointerup/touchend
            // in addition to the dropEvt, so must happen *after* the drop
            if (outEvts.length) {
                dragCurrent.b4DragOut(e, outEvts);
                dragCurrent.onDragOut(e, outEvts);
            }
        } else {
            // fire enter events
            for (i = 0 , len = enterEvts.length; i < len; ++i) {
                // dc.b4DragEnter(e, oDD.id);
                dragCurrent.onDragEnter(e, enterEvts[i].id);
            }
            // fire over events
            for (i = 0 , len = overEvts.length; i < len; ++i) {
                dragCurrent.b4DragOver(e, overEvts[i].id);
                dragCurrent.onDragOver(e, overEvts[i].id);
            }
            // fire drop events
            for (i = 0 , len = dropEvts.length; i < len; ++i) {
                dragCurrent.b4DragDrop(e, dropEvts[i].id);
                dragCurrent.onDragDrop(e, dropEvts[i].id);
            }
            // fire dragout events.
            // These are fires on mouseup/pointerup/touchend
            // in addition to the dropEvt, so must happen *after* the drop
            for (i = 0 , len = outEvts.length; i < len; ++i) {
                dragCurrent.b4DragOut(e, outEvts[i].id);
                dragCurrent.onDragOut(e, outEvts[i].id);
            }
        }
        // notify about a drop that did not find a target
        if (isDrop && !dropEvts.length) {
            dragCurrent.onInvalidDrop(e);
        }
    },
    /**
     * @private
     * Wrap Ext.Element.fromPagePoint.
     *
     * This is because in RTL mode we need to reverse any RTLification of the X coordinate
     * because document.elementFromPoint uses LTR.
     */
    elementFromPoint: function(x, y) {
        if (Ext.rootInheritedState.rtl) {
            x = Ext.Element.getViewportWidth() - x;
        }
        return Ext.Element.fromPoint(x, y, true);
    },
    /**
     * @private
     * Collects the z-index of the passed element, looking up the parentNode axis to find
     * an absolutely positioned ancestor which is able to yield a z-index. If found to be not
     * absolutely positioned, returns -1.
     *
     * This is used when sorting potential drop targets into z-index order so that only the topmost
     * receives `over` and `drop` events.
     *
     * @return {Number} The z-index of the element, or of its topmost absolutely positioned
     * ancestor. Returns -1 if the element is not absolutely positioned.
     */
    getZIndex: function(element) {
        var body = document.body,
            z,
            zIndex = -1;
        element = Ext.getDom(element);
        while (element !== body) {
            if (!isNaN(z = Number(Ext.fly(element).getStyle('zIndex')))) {
                zIndex = z;
            }
            element = element.parentNode;
        }
        return zIndex;
    },
    /**
     * @private
     * Utility method to pass to {@link Ext.Array#sort} when sorting potential drop targets
     * by z-index.
     */
    byZIndex: function(d1, d2) {
        return d1.zIndex < d2.zIndex;
    },
    /**
     * Helper function for getting the best match from the list of drag
     * and drop objects returned by the drag and drop events when we are
     * in INTERSECT mode.  It returns either the first object that the
     * cursor is over, or the object that has the greatest overlap with
     * the dragged element.
     * @param  {Ext.dd.DragDrop[]} dds The array of drag and drop objects
     * targeted
     * @return {Ext.dd.DragDrop}       The best single match
     */
    getBestMatch: function(dds) {
        var winner = null,
            len = dds.length,
            i, dd;
        if (len === 1) {
            winner = dds[0];
        } else {
            // Loop through the targeted items
            for (i = 0; i < len; ++i) {
                dd = dds[i];
                // If the cursor is over the object, it wins.  If the
                // cursor is over multiple matches, the first one we come
                // to wins.
                if (dd.cursorIsOver) {
                    winner = dd;
                    break;
                } else // Otherwise the object with the most overlap wins
                {
                    if (!winner || winner.overlap.getArea() < dd.overlap.getArea()) {
                        winner = dd;
                    }
                }
            }
        }
        return winner;
    },
    /**
     * Refreshes the cache of the top-left and bottom-right points of the
     * drag and drop objects in the specified group(s).  This is in the
     * format that is stored in the drag and drop instance, so typical
     * usage is:
     *
     *     Ext.dd.DragDropManager.refreshCache(ddinstance.groups);
     *
     * Alternatively:
     *
     *     Ext.dd.DragDropManager.refreshCache({group1:true, group2:true});
     *
     * TODO: this really should be an indexed array.  Alternatively this
     * method could accept both.
     *
     * @param {Object} groups an associative array of groups to refresh
     */
    refreshCache: function(groups) {
        var sGroup, i, oDD, loc;
        for (sGroup in groups) {
            if ("string" !== typeof sGroup) {
                
                continue;
            }
            for (i in this.ids[sGroup]) {
                oDD = this.ids[sGroup][i];
                if (this.isTypeOfDD(oDD)) {
                    // if (this.isTypeOfDD(oDD) && oDD.isTarget) {
                    loc = this.getLocation(oDD);
                    if (loc) {
                        this.locationCache[oDD.id] = loc;
                    } else {
                        delete this.locationCache[oDD.id];
                    }
                }
            }
        }
    },
    // this will unregister the drag and drop object if
    // the element is not in a usable state
    // oDD.unreg();
    /**
     * This checks to make sure an element exists and is in the DOM.  The
     * main purpose is to handle cases where innerHTML is used to remove
     * drag and drop objects from the DOM.
     * @param {HTMLElement} el the element to check
     * @return {Boolean} true if the element looks usable
     */
    verifyEl: function(el) {
        return Ext.getBody().contains(el);
    },
    /**
     * Returns a Region object containing the drag and drop element's position
     * and size, including the padding configured for it
     * @param {Ext.dd.DragDrop} oDD the drag and drop object to get the location for.
     * @return {Ext.util.Region} a Region object representing the total area
     * the element occupies, including any padding
     * the instance is configured for.
     */
    getLocation: function(oDD) {
        var el, pos, x1, x2, y1, y2, t, r, b, l;
        if (!this.isTypeOfDD(oDD)) {
            return null;
        }
        // delegate getLocation method to the
        // drag and drop target.
        if (oDD.getRegion) {
            return oDD.getRegion();
        }
        el = oDD.getEl();
        try {
            pos = Ext.fly(el).getXY();
        } catch (e) {}
        // ignore
        if (!pos) {
            return null;
        }
        x1 = pos[0];
        x2 = x1 + el.offsetWidth;
        y1 = pos[1];
        y2 = y1 + el.offsetHeight;
        t = y1 - oDD.padding[0];
        r = x2 + oDD.padding[1];
        b = y2 + oDD.padding[2];
        l = x1 - oDD.padding[3];
        return new Ext.util.Region(t, r, b, l);
    },
    /**
     * Checks the cursor location to see if it over the target
     * @param {Ext.util.Point} pt The point to evaluate
     * @param {Ext.dd.DragDrop} oTarget the DragDrop object we are inspecting
     * @param intersect
     * @return {Boolean} true if the mouse is over the target
     * @private
     */
    isOverTarget: function(pt, oTarget, intersect) {
        // use cache if available
        var loc = this.locationCache[oTarget.id],
            dc, pos, el, curRegion, overlap;
        if (!loc || !this.useCache) {
            loc = this.getLocation(oTarget);
            this.locationCache[oTarget.id] = loc;
        }
        if (!loc) {
            return false;
        }
        oTarget.cursorIsOver = loc.contains(pt);
        // DragDrop is using this as a sanity check for the initial mousedown
        // in this case we are done.  In POINT mode, if the drag obj has no
        // contraints, we are also done. Otherwise we need to evaluate the
        // location of the target as related to the actual location of the
        // dragged element.
        dc = this.dragCurrent;
        if (!dc || !dc.getTargetCoord || (!intersect && !dc.constrainX && !dc.constrainY)) {
            return oTarget.cursorIsOver;
        }
        oTarget.overlap = null;
        // Get the current location of the drag element, this is the
        // location of the mouse event less the delta that represents
        // where the original mousedown happened on the element.  We
        // need to consider constraints and ticks as well.
        pos = dc.getTargetCoord(pt.x, pt.y);
        el = dc.getDragEl();
        curRegion = new Ext.util.Region(pos.y, pos.x + el.offsetWidth, pos.y + el.offsetHeight, pos.x);
        overlap = curRegion.intersect(loc);
        if (overlap) {
            oTarget.overlap = overlap;
            return (intersect) ? true : oTarget.cursorIsOver;
        } else {
            return false;
        }
    },
    /**
     * unload event handler
     * @private
     */
    _onUnload: function(e, me) {
        Ext.dd.DragDropManager.unregAll();
    },
    /**
     * Cleans up the drag and drop events and objects.
     * @private
     */
    unregAll: function() {
        var me = this,
            cache = me.elementCache,
            i;
        if (me.dragCurrent) {
            me.stopDrag();
            me.dragCurrent = null;
        }
        me.clearingAll = true;
        me._execOnAll("unreg", []);
        delete me.clearingAll;
        for (i in cache) {
            delete cache[i];
        }
        me.elementCache = {};
        me.ids = {};
        me.handleIds = {};
    },
    /**
     * @property {Object} elementCache
     * A cache of DOM elements
     * @private
     */
    elementCache: {},
    /**
     * Get the wrapper for the DOM element specified
     * @param {String} id the id of the element to get
     * @return {Ext.dd.DragDropManager.ElementWrapper} the wrapped element
     * @private
     * @deprecated 6.5.0 This method is deprecated.
     */
    getElWrapper: function(id) {
        var oWrapper = this.elementCache[id];
        if (!oWrapper || !oWrapper.el) {
            oWrapper = this.elementCache[id] = new this.ElementWrapper(Ext.getDom(id));
        }
        return oWrapper;
    },
    /**
     * Returns the actual DOM element
     * @param {String} id the id of the elment to get
     * @return {Object} The element
     * @deprecated 6.5.0 Use Ext.lib.Ext.getDom() instead.
     */
    getElement: function(id) {
        return Ext.getDom(id);
    },
    /**
     * Returns the style property for the DOM element (i.e.,
     * document.getElById(id).style)
     * @param {String} id the id of the element to get
     * @return {Object} The style property of the element
     */
    getCss: function(id) {
        var el = Ext.getDom(id);
        return (el) ? el.style : null;
    },
    /**
     * @class Ext.dd.DragDropManager.ElementWrapper
     * Deprecated inner class for cached elements.
     * @private
     * @deprecated 6.5.0 This class is deprecated
     */
    ElementWrapper: function(el) {
        /** The element */
        this.el = el || null;
        /** The element id */
        this.id = this.el && el.id;
        /** A reference to the style property */
        this.css = this.el && el.style;
    },
    // Continue class docs
    /** @class Ext.dd.DragDropElement */
    /**
     * Returns the X position of an html element
     * @param {HTMLElement} el the element for which to get the position
     * @return {Number} the X coordinate
     */
    getPosX: function(el) {
        return Ext.fly(el).getX();
    },
    /**
     * Returns the Y position of an html element
     * @param {HTMLElement} el the element for which to get the position
     * @return {Number} the Y coordinate
     */
    getPosY: function(el) {
        return Ext.fly(el).getY();
    },
    /**
     * Swap two nodes.  In IE, we use the native method, for others we
     * emulate the IE behavior
     * @param {HTMLElement} n1 the first node to swap
     * @param {HTMLElement} n2 the other node to swap
     */
    swapNode: function(n1, n2) {
        var p, s;
        if (n1.swapNode) {
            n1.swapNode(n2);
        } else {
            p = n2.parentNode;
            s = n2.nextSibling;
            if (s === n1) {
                p.insertBefore(n1, n2);
            } else if (n2 === n1.nextSibling) {
                p.insertBefore(n2, n1);
            } else {
                n1.parentNode.replaceChild(n2, n1);
                p.insertBefore(n1, s);
            }
        }
    },
    /**
     * Returns the current scroll position
     * @private
     */
    getScroll: function() {
        var doc = window.document,
            docEl = doc.documentElement,
            body = doc.body,
            top = 0,
            left = 0;
        if (docEl && (docEl.scrollTop || docEl.scrollLeft)) {
            top = docEl.scrollTop;
            left = docEl.scrollLeft;
        } else if (body) {
            top = body.scrollTop;
            left = body.scrollLeft;
        }
        return {
            top: top,
            left: left
        };
    },
    /**
     * Returns the specified element style property
     * @param {HTMLElement} el          the element
     * @param {String}      styleProp   the style property
     * @return {String} The value of the style property
     */
    getStyle: function(el, styleProp) {
        return Ext.fly(el).getStyle(styleProp);
    },
    /**
     * Gets the scrollTop
     * @return {Number} the document's scrollTop
     */
    getScrollTop: function() {
        return this.getScroll().top;
    },
    /**
     * Gets the scrollLeft
     * @return {Number} the document's scrollTop
     */
    getScrollLeft: function() {
        return this.getScroll().left;
    },
    /**
     * Sets the x/y position of an element to the location of the
     * target element.
     * @param {HTMLElement} moveEl      The element to move
     * @param {HTMLElement} targetEl    The position reference element
     */
    moveToEl: function(moveEl, targetEl) {
        var aCoord = Ext.fly(targetEl).getXY();
        Ext.fly(moveEl).setXY(aCoord);
    },
    /**
     * Numeric array sort function
     * @param {Number} a
     * @param {Number} b
     * @return {Number} positive, negative or 0
     */
    numericSort: function(a, b) {
        return (a - b);
    },
    /**
     * Recursively searches the immediate parent and all child nodes for
     * the handle element in order to determine whether or not it was
     * clicked.
     * @param {HTMLElement} node the html element to inspect
     * @param {string} id
     */
    handleWasClicked: function(node, id) {
        var p;
        if (this.isHandle(id, node.id)) {
            return true;
        } else {
            // check to see if this is a text node child of the one we want
            p = node.parentNode;
            while (p) {
                if (this.isHandle(id, p.id)) {
                    return true;
                } else {
                    p = p.parentNode;
                }
            }
        }
        return false;
    }
}, function(DragDropManager) {
    Ext.onInternalReady(function() {
        DragDropManager.addListeners();
    });
});

/**
 * This class functions **between siblings of a {@link Ext.layout.container.VBox VBox} or
 * {@link Ext.layout.container.HBox HBox} layout** to resize both immediate siblings.
 *
 * A Splitter will preserve the flex ratio of any flexed siblings it is required to resize.
 * It does this by setting the `flex` property of *all* flexed siblings to equal their pixel size.
 * The actual numerical `flex` property in the Components will change, but the **ratio**
 * to the total flex value will be preserved.
 *
 * A Splitter may be configured to show a centered mini-collapse tool orientated to collapse
 * the {@link #collapseTarget}. The Splitter will then call that sibling Panel's
 * {@link Ext.panel.Panel#method-collapse collapse} or {@link Ext.panel.Panel#method-expand expand}
 * method to perform the appropriate operation (depending on the sibling collapse state).
 * To create the mini-collapse tool but take care of collapsing yourself, configure the splitter
 * with `{@link #performCollapse}: false`.
 */
Ext.define('Ext.resizer.Splitter', {
    extend: 'Ext.Component',
    xtype: 'splitter',
    requires: [
        'Ext.XTemplate'
    ],
    uses: [
        'Ext.resizer.SplitterTracker'
    ],
    childEls: [
        'collapseEl'
    ],
    /* eslint-disable indent, max-len */
    renderTpl: [
        '<tpl if="collapsible===true">',
        '<div id="{id}-collapseEl" data-ref="collapseEl" role="presentation" class="',
        Ext.baseCSSPrefix,
        'collapse-el ',
        Ext.baseCSSPrefix,
        'layout-split-{collapseDir}{childElCls}">',
        '</div>',
        '</tpl>'
    ],
    /* eslint-enable indent, max-len */
    isSplitter: true,
    baseCls: Ext.baseCSSPrefix + 'splitter',
    collapsedClsInternal: Ext.baseCSSPrefix + 'splitter-collapsed',
    // Default to tree, allow internal classes to disable resizing
    canResize: true,
    /**
     * @cfg {Boolean} [collapsible]
     * True to show a mini-collapse tool in the Splitter to toggle expand and collapse
     * on the {@link #collapseTarget} Panel.
     * Defaults to the {@link Ext.panel.Panel#collapsible collapsible} setting of the Panel.
     */
    collapsible: null,
    /**
     * @cfg {Boolean} performCollapse
     * Set to false to prevent this Splitter's mini-collapse tool from managing the collapse
     * state of the {@link #collapseTarget}.
     */
    /**
     * @cfg {Boolean} collapseOnDblClick
     * True to enable dblclick to toggle expand and collapse on the {@link #collapseTarget} Panel.
     */
    collapseOnDblClick: true,
    /**
     * @cfg {Number} defaultSplitMin
     * Provides a default minimum width or height for the two components
     * that the splitter is between.
     */
    defaultSplitMin: 40,
    /**
     * @cfg {Number} defaultSplitMax
     * Provides a default maximum width or height for the two components
     * that the splitter is between.
     */
    defaultSplitMax: 1000,
    /**
     * @cfg {String} collapsedCls
     * A class to add to the splitter when it is collapsed. See {@link #collapsible}.
     */
    /**
     * @cfg {String/Ext.panel.Panel} collapseTarget
     * A string describing the relative position of the immediate sibling Panel to collapse.
     * May be 'prev' or 'next'.
     *
     * Or the immediate sibling Panel to collapse.
     *
     * The orientation of the mini-collapse tool will be inferred from this setting.
     *
     * **Note that only Panels may be collapsed.**
     */
    collapseTarget: 'next',
    /**
     * @property {String} orientation
     * Orientation of this Splitter. `'vertical'` when used in an hbox layout, `'horizontal'`
     * when used in a vbox layout.
     */
    horizontal: false,
    vertical: false,
    touchAction: undefined,
    // so applier/updater always run
    /**
     * @cfg {Number} size
     * The size of the splitter. This becomes the height for vertical splitters and 
     * width for horizontal splitters.
     */
    size: 5,
    /**
     * @cfg {Object} [tracker]
     * Any configuration options to be passed to the underlying {@link Ext.resizer.SplitterTracker}.
     */
    tracker: null,
    ariaRole: 'separator',
    focusable: true,
    tabIndex: 0,
    applyTouchAction: function(touchAction, oldTouchAction) {
        if (touchAction === undefined) {
            touchAction = this.vertical ? {
                panX: false
            } : {
                panY: false
            };
        }
        return this.callParent([
            touchAction,
            oldTouchAction
        ]);
    },
    /**
     * Returns the config object (with an `xclass` property) for the splitter tracker. This
     * is overridden by {@link Ext.resizer.BorderSplitter BorderSplitter} to create a
     * {@link Ext.resizer.BorderSplitterTracker BorderSplitterTracker}.
     * @protected
     */
    getTrackerConfig: function() {
        return Ext.apply({
            xclass: 'Ext.resizer.SplitterTracker',
            el: this.el,
            splitter: this
        }, this.tracker);
    },
    beforeRender: function() {
        var me = this,
            target = me.getCollapseTarget(),
            collapsible = me.collapsible;
        me.callParent();
        if (target.collapsed) {
            me.addCls(me.collapsedClsInternal);
        }
        if (!me.canResize) {
            me.addCls(me.baseCls + '-noresize');
        }
        Ext.applyIf(me.renderData, {
            collapseDir: me.getCollapseDirection(),
            collapsible: (collapsible !== null) ? collapsible : target.collapsible
        });
        me.ariaRenderAttributes = me.ariaRenderAttributes || {};
        // Calling getCollapseDirection() above will set the orientation property
        me.ariaRenderAttributes['aria-orientation'] = me.orientation;
        me.protoEl.unselectable();
    },
    onRender: function() {
        var me = this,
            target, collapseEl;
        me.callParent(arguments);
        // Add listeners on the mini-collapse tool unless performCollapse is set to false
        if (me.performCollapse !== false) {
            if (me.renderData.collapsible) {
                me.mon(me.collapseEl, 'click', me.toggleTargetCmp, me);
            }
            if (me.collapseOnDblClick) {
                me.mon(me.el, 'dblclick', me.toggleTargetCmp, me);
            }
        }
        // Ensure the mini collapse icon is set to the correct direction
        // when the target is collapsed/expanded by any means.
        // Make sure we're only listening to collapse/expand events on Panels!
        target = me.getCollapseTarget();
        if (target && target.isPanel) {
            target.on({
                collapse: me.onTargetCollapse,
                expand: me.onTargetExpand,
                beforeexpand: me.onBeforeTargetExpand,
                beforecollapse: me.onBeforeTargetCollapse,
                scope: me
            });
        }
        if (me.canResize) {
            me.tracker = Ext.create(me.getTrackerConfig());
            // Relay the most important events to our owner (could open wider later):
            me.relayEvents(me.tracker, [
                'beforedragstart',
                'dragstart',
                'dragend'
            ]);
        }
        collapseEl = me.collapseEl;
        if (collapseEl) {
            collapseEl.lastCollapseDirCls = me.collapseDirProps[me.collapseDirection].cls;
        }
    },
    getCollapseDirection: function() {
        var me = this,
            dir = me.collapseDirection,
            collapseTarget, idx, items, type;
        if (!dir) {
            collapseTarget = me.collapseTarget;
            if (collapseTarget.isComponent) {
                dir = collapseTarget.collapseDirection;
            }
            if (!dir) {
                // Avoid duplication of string tests.
                // Create a two bit truth table of the configuration of the Splitter:
                // Collapse Target | orientation
                //        0              0             = next, horizontal
                //        0              1             = next, vertical
                //        1              0             = prev, horizontal
                //        1              1             = prev, vertical
                type = me.ownerCt.layout.type;
                if (collapseTarget.isComponent) {
                    items = me.ownerCt.items;
                    idx = Number(items.indexOf(collapseTarget) === items.indexOf(me) - 1) << 1 | Number(type === 'hbox');
                } else {
                    idx = Number(me.collapseTarget === 'prev') << 1 | Number(type === 'hbox');
                }
                // Read the data out the truth table
                dir = [
                    'bottom',
                    'right',
                    'top',
                    'left'
                ][idx];
            }
            me.collapseDirection = dir;
        }
        me.setOrientation((dir === 'top' || dir === 'bottom') ? 'horizontal' : 'vertical');
        return dir;
    },
    getCollapseTarget: function() {
        var me = this;
        return me.collapseTarget.isComponent ? me.collapseTarget : me.collapseTarget === 'prev' ? me.previousSibling() : me.nextSibling();
    },
    setCollapseEl: function(display) {
        var el = this.collapseEl;
        if (el) {
            el.setDisplayed(display);
        }
    },
    onBeforeTargetExpand: function(target) {
        this.setCollapseEl('none');
    },
    onBeforeTargetCollapse: function() {
        this.setCollapseEl('none');
    },
    onTargetCollapse: function(target) {
        var me = this;
        // Only add the collapsed class if the collapse was from our target
        // (not bubbled from below as in a Dashboard Column) and was in the dimension
        // which this Splitter controls.
        if (target === me.getCollapseTarget() && target[me.orientation === 'vertical' ? 'collapsedHorizontal' : 'collapsedVertical']()) {
            me.el.addCls(me.collapsedClsInternal + ' ' + (me.collapsedCls || ''));
        }
        me.setCollapseEl('');
    },
    onTargetExpand: function(target) {
        var me = this;
        me.el.removeCls(me.collapsedClsInternal + ' ' + (me.collapsedCls || ''));
        me.setCollapseEl('');
    },
    collapseDirProps: {
        top: {
            cls: Ext.baseCSSPrefix + 'layout-split-top'
        },
        right: {
            cls: Ext.baseCSSPrefix + 'layout-split-right'
        },
        bottom: {
            cls: Ext.baseCSSPrefix + 'layout-split-bottom'
        },
        left: {
            cls: Ext.baseCSSPrefix + 'layout-split-left'
        }
    },
    orientationProps: {
        horizontal: {
            opposite: 'vertical',
            fixedAxis: 'height',
            stretchedAxis: 'width'
        },
        vertical: {
            opposite: 'horizontal',
            fixedAxis: 'width',
            stretchedAxis: 'height'
        }
    },
    applyCollapseDirection: function() {
        var me = this,
            collapseEl = me.collapseEl,
            collapseDirProps = me.collapseDirProps[me.collapseDirection],
            cls;
        if (collapseEl) {
            cls = collapseEl.lastCollapseDirCls;
            if (cls) {
                collapseEl.removeCls(cls);
            }
            collapseEl.addCls(collapseEl.lastCollapseDirCls = collapseDirProps.cls);
        }
    },
    applyOrientation: function() {
        var me = this,
            orientation = me.orientation,
            orientationProps = me.orientationProps[orientation],
            defaultSize = me.size,
            fixedSizeProp = orientationProps.fixedAxis,
            stretchSizeProp = orientationProps.stretchedAxis,
            cls = me.baseCls + '-';
        me[orientation] = true;
        me[orientationProps.opposite] = false;
        if (!me.hasOwnProperty(fixedSizeProp) || me[fixedSizeProp] === '100%') {
            me[fixedSizeProp] = defaultSize;
        }
        if (!me.hasOwnProperty(stretchSizeProp) || me[stretchSizeProp] === defaultSize) {
            me[stretchSizeProp] = '100%';
        }
        me.removeCls(cls + orientationProps.opposite);
        me.addCls(cls + orientation);
    },
    setOrientation: function(orientation) {
        var me = this;
        if (me.orientation !== orientation) {
            me.orientation = orientation;
            me.applyOrientation();
        }
    },
    updateOrientation: function() {
        delete this.collapseDirection;
        // recompute
        this.getCollapseDirection();
        this.applyCollapseDirection();
    },
    toggleTargetCmp: function(e, t) {
        var cmp = this.getCollapseTarget(),
            placeholder = cmp.placeholder,
            toggle;
        // We can only toggle the target if it offers the expand/collapse API
        if (Ext.isFunction(cmp.expand) && Ext.isFunction(cmp.collapse)) {
            if (placeholder && !placeholder.hidden) {
                toggle = true;
            } else {
                toggle = !cmp.hidden;
            }
            if (toggle) {
                if (cmp.collapsed || cmp.floatedFromCollapse) {
                    cmp.expand();
                } else if (cmp.collapseDirection) {
                    cmp.collapse();
                } else {
                    cmp.collapse(this.renderData.collapseDir);
                }
            }
        }
    },
    /*
     * Work around IE bug. %age margins do not get recalculated on element resize
     * unless repaint called.
     */
    setSize: function() {
        var me = this;
        me.callParent(arguments);
        if (Ext.isIE && me.el) {
            me.el.repaint();
        }
    },
    doDestroy: function() {
        Ext.destroy(this.tracker);
        this.callParent();
    }
});

/**
 * Base Class for HBoxLayout and VBoxLayout Classes. Generally it should not need to be used
 * directly.
 */
Ext.define('Ext.layout.container.Box', {
    extend: 'Ext.layout.container.Container',
    alternateClassName: 'Ext.layout.BoxLayout',
    alias: 'layout.box',
    requires: [
        'Ext.layout.container.boxOverflow.None',
        'Ext.layout.container.boxOverflow.Scroller',
        'Ext.util.Format',
        'Ext.dd.DragDropManager',
        'Ext.resizer.Splitter'
    ],
    type: 'box',
    config: {
        /**
         * @cfg {String} [align="begin"]
         * Controls how the child items of the container are aligned. The value is used to
         * position items "perpendicularly". That is, for horizontal boxes (where `vertical`
         * is `false`), then this will position items vertically. Otherwise, this will position
         * items horizontally. The acceptable values for this property are best explained in
         * context with the value of `vertical`.
         *
         * If `vertical` is `false` then this layout is behaving as an `hbox` and this config
         * operates as follows:
         *
         * - **begin** : Child items are aligned vertically at the top of the container.
         * - **middle** : Child items are vertically centered in the container.
         * - **end** : Child items are aligned vertically at the bottom of the container.
         * - **stretch** : Child items are stretched vertically to fill the height of the container.
         * - **stretchmax** : Child items are stretched vertically to the height of the largest
         * item.
         *
         * If `vertical` is `true` then this layout is behaving as an `vbox` and this config
         * operates as follows:
         *
         * - **begin** : Child items are aligned horizontally at the left side of the container.
         * - **middle** : Child items are horizontally centered in the container.
         * - **end** : Child items are aligned horizontally at the right of the container.
         * - **stretch** : Child items are stretched horizontally to fill the width of the
         * container.
         * - **stretchmax** : Child items are stretched horizontally to the size of the largest
         * item.
         *
         * For backwards compatibility, the following values are also recognized:
         *
         * - **left** : Same as **begin**.
         * - **top** : Same as **begin**.
         * - **center** : Same as **middle**.
         * - **right** : Same as **end**.
         * - **bottom** : Same as **end**.
         */
        align: 'begin',
        // end, middle, stretch, strechmax
        /**
         * @cfg {Boolean} constrainAlign
         * Limits the size of {@link #align aligned} components to the size of the container
         * under certain circumstances. Firstly, the container's height (for `hbox`) or width
         * (for `vbox`) must not be determined by the size of the child components. Secondly,
         * the child components must have {@link Ext.AbstractComponent#shrinkWrap shrinkwrap}
         * enabled for this dimension.
         */
        constrainAlign: false,
        /**
         * @cfg {Boolean} [enableSplitters=true]
         * This flag can be set to `false` to ignore the `split` config on box items. This is
         * set to `false` by `Ext.layout.container.Accordion`.
         */
        enableSplitters: true,
        // @cmd-auto-dependency { aliasPrefix: 'box.overflow.' }
        /**
         * @cfg {String/Ext.layout.container.boxOverflow.None}
         * An overflow handler or config object for an overflow handler.  This is typically
         * specified as one of the following strings:
         *
         * - `scroller` - Scroller buttons are rendered before and after the content.
         * - `menu` - Overflowing items are rendered into a menu, and a button is rendered
         *    after the items, which shows the menu when clicked.
         *
         * NOTE: This config is currently only supported when box layout is used by the
         * following components:
         *
         * - {@link Ext.toolbar.Toolbar}
         * - {@link Ext.menu.Menu}
         * - {@link Ext.toolbar.Breadcrumb}
         * - {@link Ext.tab.Bar}
         *
         * Components where `overflowHandler` is not supported should use
         * `{@link Ext.Component#scrollable scrollable}:true` if they have overflowing
         * content.
         */
        overflowHandler: {
            $value: null,
            merge: function(newValue, oldValue) {
                if (typeof newValue === 'string') {
                    newValue = {
                        type: newValue
                    };
                }
                return Ext.merge(oldValue ? Ext.Object.chain(oldValue) : {}, newValue);
            }
        },
        /**
         * @cfg {String} padding
         * Sets the padding to be applied to all child items managed by this layout.
         *
         * This property must be specified as a string containing space-separated, numeric
         * padding values. The order of the sides associated with each value matches the
         * way CSS processes padding values:
         *
         *   - If there is only one value, it applies to all sides.
         *   - If there are two values, the top and bottom borders are set to the first
         *     value and the right and left are set to the second.
         *   - If there are three values, the top is set to the first value, the left and
         *     right are set to the second, and the bottom is set to the third.
         *   - If there are four values, they apply to the top, right, bottom, and left,
         *     respectively.
         */
        padding: 0,
        /**
         * @cfg {String} pack
         * Controls how the child items of the container are packed together. Acceptable
         * configuration values for this property are:
         *
         *   - **start** - child items are packed together at **left** (HBox) or **top**
         *     (VBox) side of container (*default**)
         *   - **center** - child items are packed together at **mid-width** (HBox) or
         *     **mid-height** (VBox) of container
         *   - **end** - child items are packed together at **right** (HBox) or **bottom**
         *     (VBox) side of container
         */
        pack: 'start',
        /**
         * @cfg {String/Ext.Component} stretchMaxPartner
         * Allows stretchMax calculation to take into account the max perpendicular size
         * (height for HBox layout and width for VBox layout) of another Box layout when
         * calculating its maximum perpendicular child size.
         *
         * If specified as a string, this may be either a known Container ID, or a
         * ComponentQuery selector which is rooted at this layout's Container (ie, to find
         * a sibling, use `"^>#siblingItemId`).
         */
        stretchMaxPartner: undefined,
        /**
         * @cfg {Boolean} [vertical=false]
         * Set to `true` to switch the layout to `vbox`.
         */
        vertical: false,
        /**
         * @cfg {"round"/"floor"/"ceil"} [alignRoundingMethod='round'] The Math method 
         * to use for rounding fractional pixels when `{@link #align}:middle` is used.  
         * The possible values are:
         * 
         *  - [round](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round)
         *  - [floor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor)
         *  - [ceil](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil)
         */
        alignRoundingMethod: 'round'
    },
    itemCls: Ext.baseCSSPrefix + 'box-item',
    targetCls: Ext.baseCSSPrefix + 'box-layout-ct',
    targetElCls: Ext.baseCSSPrefix + 'box-target',
    innerCls: Ext.baseCSSPrefix + 'box-inner',
    manageMargins: true,
    createsInnerCt: true,
    childEls: [
        'innerCt',
        'targetEl'
    ],
    /* eslint-disable indent, max-len */
    renderTpl: [
        '{%var oc,l=values.$comp.layout,oh=l.overflowHandler;' + 'if (oh && oh.getPrefixConfig!==Ext.emptyFn) {' + 'if(oc=oh.getPrefixConfig())dh.generateMarkup(oc, out)' + '}%}' + '<div id="{ownerId}-innerCt" data-ref="innerCt" role="presentation" class="{[l.innerCls]}' + '{[oh ? (" " + oh.getOverflowCls(l.direction)) : ""]}">' + '<div id="{ownerId}-targetEl" data-ref="targetEl" class="{targetElCls}" role="presentation">' + '{%this.renderBody(out, values)%}' + '</div>' + '</div>' + '{%if (oh && oh.getSuffixConfig!==Ext.emptyFn) {' + 'if(oc=oh.getSuffixConfig())dh.generateMarkup(oc, out)' + '}%}',
        {
            disableFormats: true,
            definitions: 'var dh=Ext.DomHelper;'
        }
    ],
    /* eslint-enable indent, max-len */
    constructor: function(config) {
        var me = this,
            type;
        me.callParent([
            config
        ]);
        me.setVertical(me.vertical);
        // The sort function needs access to properties in this, so must be bound.
        me.flexSortFn = me.flexSort.bind(me);
        type = typeof me.padding;
        if (type === 'string' || type === 'number') {
            me.padding = Ext.util.Format.parseBox(me.padding);
            me.padding.height = me.padding.top + me.padding.bottom;
            me.padding.width = me.padding.left + me.padding.right;
        }
    },
    _beginRe: /^(?:begin|left|top)$/,
    _centerRe: /^(?:center|middle)$/,
    _endRe: /^(?:end|right|bottom)$/,
    // Matches: `<spaces>digits[.digits]<spaces>%<spaces>`
    // Captures: `digits[.digits]`
    _percentageRe: /^\s*(\d+(?:\.\d*)?)\s*[%]\s*$/,
    getItemSizePolicy: function(item, ownerSizeModel) {
        var me = this,
            policy = me.sizePolicy,
            align = me.align,
            flex = item.flex,
            key = align,
            names = me.names,
            heightName = names.height,
            widthName = names.width,
            width = item[widthName],
            height = item[heightName],
            percentageRe = me._percentageRe,
            percentageWidth = percentageRe.test(width),
            isStretch = (align === 'stretch'),
            isStretchMax = (align === 'stretchmax'),
            constrain = me.constrainAlign;
        // Getting the size model is expensive, so we only want to do so if we really need it
        if (!ownerSizeModel && (isStretch || flex || percentageWidth || (constrain && !isStretchMax))) {
            ownerSizeModel = me.owner.getSizeModel();
        }
        if (isStretch) {
            // If we are height.shrinkWrap, we behave as if we were stretchmax (for more
            // details, see beginLayoutCycle)...
            if (!percentageRe.test(height) && ownerSizeModel[heightName].shrinkWrap) {
                key = 'stretchmax';
            }
        }
        // We leave %age height as stretch since it will not participate in the
        // stretchmax size calculation. This avoid running such a child in its
        // shrinkWrap mode prior to supplying the calculated size.
        else if (!isStretchMax) {
            if (percentageRe.test(height)) {
                // Height %ages are calculated based on container size, so they are the
                // same as align=stretch for this purpose...
                key = 'stretch';
            } else if (constrain && !ownerSizeModel[heightName].shrinkWrap) {
                // Same functionality as stretchmax, only the max is going to be the size
                // of the container, not the largest item
                key = 'stretchmax';
            } else {
                key = '';
            }
        }
        if (flex || percentageWidth) {
            // If we are width.shrinkWrap, we won't be flexing since that requires a
            // container width...
            if (!ownerSizeModel[widthName].shrinkWrap) {
                policy = policy.flex;
            }
        }
        // both flex and %age width are calculated
        return policy[key];
    },
    flexSort: function(a, b) {
        // We need to sort the flexed items to ensure that we have
        // the items with max/min width first since when we set the
        // values we may have the value constrained, so we need to
        // react accordingly. Precedence is given from the largest
        // value through to the smallest value
        var maxWidthName = this.names.maxWidth,
            minWidthName = this.names.minWidth,
            infiniteValue = Infinity,
            aTarget = a.target,
            bTarget = b.target,
            aFlex = aTarget.flex,
            bFlex = bTarget.flex,
            result = 0,
            aMin, bMin, aMax, bMax, hasMin, hasMax;
        aMax = aTarget[maxWidthName] || infiniteValue;
        bMax = bTarget[maxWidthName] || infiniteValue;
        aMin = aTarget[minWidthName] || 0;
        bMin = bTarget[minWidthName] || 0;
        hasMin = isFinite(aMin) || isFinite(bMin);
        hasMax = isFinite(aMax) || isFinite(bMax);
        if (hasMin || hasMax) {
            if (hasMax) {
                result = aMax - bMax;
            }
            // If the result is 0, it means either
            // a) hasMax was false
            // b) The max values were the same
            if (result === 0 && hasMin) {
                result = bMin - aMin;
            }
            // If 0, it means either the max and/or minimum was the same
            if (result === 0) {
                if (hasMax) {
                    result = bFlex - aFlex;
                } else {
                    result = aFlex - bFlex;
                }
            }
        }
        return result;
    },
    isItemBoxParent: function(itemContext) {
        return true;
    },
    isItemShrinkWrap: function(item) {
        return true;
    },
    roundFlex: function(width) {
        return Math.floor(width);
    },
    /**
     * @private
     * Called by an owning Panel before the Panel begins its collapse process.
     * Most layouts will not need to override the default Ext.emptyFn implementation.
     */
    beginCollapse: function(child) {
        var me = this;
        if (me.direction === 'vertical' && child.collapsedVertical()) {
            child.collapseMemento.capture([
                'flex'
            ]);
            delete child.flex;
        } else if (me.direction === 'horizontal' && child.collapsedHorizontal()) {
            child.collapseMemento.capture([
                'flex'
            ]);
            delete child.flex;
        }
    },
    /**
     * @private
     * Called by an owning Panel before the Panel begins its expand process.
     * Most layouts will not need to override the default Ext.emptyFn implementation.
     */
    beginExpand: function(child) {
        // Restores the flex if we used to be flexed before
        child.collapseMemento.restore([
            'flex'
        ]);
    },
    beginLayout: function(ownerContext) {
        var me = this,
            owner = me.owner,
            smp = owner.stretchMaxPartner,
            style = me.innerCt.dom.style,
            names = me.names,
            overflowHandler = me.overflowHandler;
        ownerContext.boxNames = names;
        // this must happen before callParent to allow the overflow handler to do its work
        // that can effect the childItems collection...
        if (overflowHandler) {
            overflowHandler.beginLayout(ownerContext);
        }
        // get the contextItem for our stretchMax buddy:
        if (typeof smp === 'string') {
            smp = Ext.getCmp(smp) || owner.query(smp)[0];
        }
        ownerContext.stretchMaxPartner = smp && ownerContext.context.getCmp(smp);
        me.callParent([
            ownerContext
        ]);
        ownerContext.innerCtContext = ownerContext.getEl('innerCt', me);
        ownerContext.targetElContext = ownerContext.getEl('targetEl', me);
        ownerContext.ownerScrollable = owner.getScrollable();
        // Don't allow sizes burned on to the innerCt to influence measurements.
        style.width = style.height = '';
    },
    beginLayoutCycle: function(ownerContext, firstCycle) {
        var me = this,
            state = ownerContext.state,
            scrollable = ownerContext.ownerScrollable,
            align = me.align,
            names = ownerContext.boxNames,
            pack = me.pack,
            centerRe = me._centerRe,
            overflowHandler = me.overflowHandler,
            canScroll = ownerContext.state.canScroll,
            widthModel, heightModel;
        // this must happen before callParent to allow the overflow handler to do its work
        // that can effect the childItems collection...
        if (overflowHandler) {
            overflowHandler.beginLayoutCycle(ownerContext, firstCycle);
        }
        me.callParent([
            ownerContext,
            firstCycle
        ]);
        // Cache several of our string concat/compare results (since width/heightModel can
        // change if we are invalidated, we cannot do this in beginLayout)
        ownerContext.parallelSizeModel = widthModel = ownerContext[names.widthModel];
        ownerContext.perpendicularSizeModel = heightModel = ownerContext[names.heightModel];
        ownerContext.boxOptions = {
            align: align = {
                stretch: align === 'stretch',
                stretchmax: align === 'stretchmax',
                center: centerRe.test(align),
                bottom: me._endRe.test(align)
            },
            pack: pack = {
                center: centerRe.test(pack),
                end: pack === 'end'
            }
        };
        // Scrolling can occur if:
        // a) The owner is configured to scroll - not if there's a boxOverflow scroller.
        // We must not handle scroll if its this layout's innerCt which is scrolling
        // which can be set by  a Ext.layout.container.boxOverflow.Scroller.
        // b) We're not shrink wrapping. If we shrink wrap, we should always size around the content
        if (scrollable && !scrollable.isBoxOverflowScroller) {
            if (!canScroll) {
                // Use getX/getY here to indicate whether we will show visible scrollbars
                // in that direction, we may have a scrollable and can scroll in that direction
                // without having a visible scrollbar.
                state.canScroll = {
                    parallel: !widthModel.shrinkWrap && scrollable[names.getX](),
                    perpendicular: !heightModel.shrinkWrap && scrollable[names.getY]()
                };
            }
            if (!state.actualScroll) {
                // Store the final calculated state for this cycle in here
                state.actualScroll = {
                    parallel: false,
                    perpendicular: false
                };
            }
        }
        // Consider an hbox w/stretch which means "assign all items the container's height".
        // The spirit of this request is make all items the same height, but when shrinkWrap
        // height is also requested, the height of the tallest item determines the height.
        // This is exactly what the stretchmax option does, so we jiggle the flags here to
        // act as if stretchmax were requested.
        if (align.stretch && heightModel.shrinkWrap) {
            align.stretchmax = true;
            align.stretch = false;
        }
        // This is handy for knowing that we might need to apply height %ages
        align.nostretch = !(align.stretch || align.stretchmax);
        // In our example hbox, packing items to the right (end) or center can only work if
        // there is a container width. So, if we are shrinkWrap, we just turn off the pack
        // options for the run.
        if (widthModel.shrinkWrap) {
            pack.center = pack.end = false;
        }
        me.cacheFlexes(ownerContext);
        // We set the width of the target el equal to the width of the innerCt
        // when the layout cycle is finished, so we need to clear the width here
        // to prevent the children from being crushed.
        // IE needs it because of its scrollIntoView bug: https://sencha.jira.com/browse/EXTJSIV-6520
        // Webkit needs it because of its mouse drag bug: https://sencha.jira.com/browse/EXTJSIV-5962
        // FF needs it because of a vertical tab bug: https://sencha.jira.com/browse/EXTJSIV-8614
        me.targetEl.setWidth(20000);
    },
    /**
     * This method is called to (re)cache our understanding of flexes.
     * This happens during beginLayoutCycle and may need to be called again
     * if the flexes are changed during the layout (e.g., like ColumnLayout).
     * @param {Object} ownerContext
     * @protected
     */
    cacheFlexes: function(ownerContext) {
        var me = this,
            names = ownerContext.boxNames,
            widthModelName = names.widthModel,
            heightModelName = names.heightModel,
            nostretch = ownerContext.boxOptions.align.nostretch,
            totalFlex = 0,
            childItems = ownerContext.childItems,
            i = childItems.length,
            flexedItems = [],
            minWidth = 0,
            smallestHeight = 0,
            smallestWidth = 0,
            minWidthName = names.minWidth,
            minHeightName = names.minHeight,
            percentageRe = me._percentageRe,
            percentageWidths = 0,
            percentageHeights = 0,
            child, childContext, flex, match, heightModel, widthModel, width, height;
        while (i--) {
            childContext = childItems[i];
            child = childContext.target;
            widthModel = childContext[widthModelName];
            // check widthModel to see if we are the sizing layout. If so, copy the flex
            // from the item to the contextItem and add it to totalFlex
            //
            if (widthModel.calculated) {
                childContext.flex = flex = child.flex;
                if (flex) {
                    totalFlex += flex;
                    flexedItems.push(childContext);
                    minWidth += child[minWidthName] || 0;
                }
                // a %age width. Note that we are testing the value that is
                // assigned to match.
                else if ((match = percentageRe.exec(child[names.width]))) {
                    childContext.percentageParallel = parseFloat(match[1]) / 100;
                    ++percentageWidths;
                }
            }
            // the above means that "childContext.flex" is properly truthy/falsy, which is
            // often times quite convenient...
            if (widthModel.configured) {
                width = child[names.width];
            } else {
                width = child[minWidthName] || 0;
            }
            smallestWidth += width;
            heightModel = childContext[heightModelName];
            if (nostretch && heightModel.calculated) {
                // the only reason we would be calculated height in this case is due to a
                // height %age...
                match = percentageRe.exec(child[names.height]);
                childContext.percentagePerpendicular = parseFloat(match[1]) / 100;
                ++percentageHeights;
            }
            if (heightModel.configured) {
                height = child[names.height];
            } else {
                height = child[minHeightName] || 0;
            }
            if (height > smallestHeight) {
                smallestHeight = height;
            }
        }
        ownerContext.flexedItems = flexedItems;
        ownerContext.flexedMinWidth = minWidth;
        // These dimensions are the smallest possible dimensions (using known sizes) for
        // the innerCt on each axis
        ownerContext.smallestWidth = smallestWidth;
        ownerContext.smallestHeight = smallestHeight;
        ownerContext.totalFlex = totalFlex;
        ownerContext.percentageWidths = percentageWidths;
        ownerContext.percentageHeights = percentageHeights;
        // The flexed boxes need to be sorted in ascending order of maxSize to work properly
        // so that unallocated space caused by maxWidth being less than flexed width can be
        // reallocated to subsequent flexed boxes.
        Ext.Array.sort(flexedItems, me.flexSortFn);
    },
    calculate: function(ownerContext) {
        var me = this,
            names = ownerContext.boxNames,
            state = ownerContext.state,
            actualScroll = state.actualScroll,
            needsScroll = state.needsScroll,
            canScroll = state.canScroll,
            plan = state.boxPlan || (state.boxPlan = {}),
            overflowHandler = me.overflowHandler;
        plan.targetSize = me.getContainerSize(ownerContext);
        if (canScroll && !needsScroll) {
            state.needsScroll = needsScroll = {
                // Attempt to figure out early on if we need to scroll in the parallel direction.
                // If the perpendicular is done and we need to scroll, we need to invalidate
                // because it may need recalculation.
                parallel: canScroll.parallel && plan.targetSize[names.width] < ownerContext.smallestWidth,
                perpendicular: canScroll.perpendicular && plan.targetSize[names.height] < ownerContext.smallestHeight
            };
        }
        if (!state.parallelDone) {
            state.parallelDone = me.calculateParallel(ownerContext, names, plan);
        }
        if (!state.perpendicularDone) {
            state.perpendicularDone = me.calculatePerpendicular(ownerContext, names, plan);
        }
        if (state.parallelDone && state.perpendicularDone) {
            if (canScroll && !state.scrollPass) {
                if (needsScroll.parallel !== actualScroll.parallel || needsScroll.perpendicular !== actualScroll.perpendicular) {
                    ownerContext.invalidate({
                        state: {
                            scrollPass: true,
                            canScroll: canScroll,
                            needsScroll: actualScroll
                        }
                    });
                    me.done = false;
                    return;
                }
            }
            me.publishInnerCtSize(ownerContext);
            // We always need to run calculateStretchMax, when relevant since we may 
            // have hit a constraint in an earlier calculation.
            if (me.done && ownerContext.boxOptions.align.stretchmax && !state.stretchMaxDone) {
                me.calculateStretchMax(ownerContext, names, plan);
                state.stretchMaxDone = true;
            }
            if (overflowHandler) {
                overflowHandler.calculate(ownerContext);
            }
        } else {
            me.done = false;
        }
    },
    calculateParallel: function(ownerContext, names, plan) {
        var me = this,
            widthShrinkWrap = ownerContext.parallelSizeModel.shrinkWrap,
            widthName = names.width,
            childItems = ownerContext.childItems,
            beforeXName = names.beforeX,
            afterXName = names.afterX,
            setWidthName = names.setWidth,
            childItemsLength = childItems.length,
            flexedItems = ownerContext.flexedItems,
            flexedItemsLength = flexedItems.length,
            pack = ownerContext.boxOptions.pack,
            padding = me.padding,
            targetSize = plan.targetSize,
            containerWidth = targetSize[widthName],
            state = ownerContext.state,
            needsScroll = state.needsScroll,
            canScroll = state.canScroll,
            totalMargin = 0,
            left = padding[beforeXName],
            nonFlexWidth = left + padding[afterXName],
            scrollbarSize = Ext.scrollbar.size(),
            scrollbarWidth = scrollbarSize[names.width],
            scrollbarHeight = scrollbarSize[names.height],
            i, childMargins, remainingWidth, remainingFlex, childContext, flex, flexedWidth, contentWidth, childWidth, percentageSpace, availableSpace;
        // If we are not widthModel.shrinkWrap, we need the width before we can lay out boxes.
        // This check belongs here so it does not prevent the perpendicular from attempting to
        // calculate. It may have a dependency on the width, but it may be able to achieve
        // the correct size without the width.
        if (!widthShrinkWrap && !targetSize[names.gotWidth]) {
            return false;
        }
        // Gather the total size taken up by non-flexed items:
        for (i = 0; i < childItemsLength; ++i) {
            childContext = childItems[i];
            childMargins = childContext.marginInfo || childContext.getMarginInfo();
            totalMargin += childMargins[widthName];
            if (!childContext[names.widthModel].calculated) {
                childWidth = childContext.getProp(widthName);
                nonFlexWidth += childWidth;
                // min/maxWidth safe
                if (isNaN(nonFlexWidth)) {
                    return false;
                }
            }
        }
        nonFlexWidth += totalMargin;
        if (ownerContext.percentageWidths) {
            percentageSpace = containerWidth - totalMargin;
            if (isNaN(percentageSpace)) {
                return false;
            }
            for (i = 0; i < childItemsLength; ++i) {
                childContext = childItems[i];
                if (childContext.percentageParallel) {
                    childWidth = Math.ceil(percentageSpace * childContext.percentageParallel);
                    childWidth = childContext[setWidthName](childWidth);
                    nonFlexWidth += childWidth;
                }
            }
        }
        // if we get here, we have all the childWidths for non-flexed items...
        if (widthShrinkWrap) {
            availableSpace = 0;
            plan.tooNarrow = false;
        } else {
            availableSpace = containerWidth - nonFlexWidth;
            if (needsScroll && needsScroll.perpendicular) {
                availableSpace -= scrollbarHeight;
            }
            plan.tooNarrow = availableSpace < ownerContext.flexedMinWidth;
            if (plan.tooNarrow && canScroll && canScroll.parallel) {
                state.actualScroll.parallel = true;
            }
        }
        contentWidth = nonFlexWidth;
        remainingWidth = availableSpace;
        remainingFlex = ownerContext.totalFlex;
        // Calculate flexed item sizes:
        for (i = 0; i < flexedItemsLength; i++) {
            childContext = flexedItems[i];
            flex = childContext.flex;
            flexedWidth = me.roundFlex((flex / remainingFlex) * remainingWidth);
            flexedWidth = childContext[setWidthName](flexedWidth);
            // constrained
            // due to minWidth constraints, it may be that flexedWidth > remainingWidth
            contentWidth += flexedWidth;
            // Remaining space has already had margins subtracted, so just subtract size
            remainingWidth = Math.max(0, remainingWidth - flexedWidth);
            // no negatives!
            remainingFlex -= flex;
        }
        if (pack.center) {
            left += remainingWidth / 2;
            // If content is too wide to pack to center, do not allow the centering calculation
            // to place it off the left edge.
            if (left < 0) {
                left = 0;
            }
        } else if (pack.end) {
            left += remainingWidth;
        }
        // Assign parallel position for the boxes:
        for (i = 0; i < childItemsLength; ++i) {
            childContext = childItems[i];
            childMargins = childContext.marginInfo;
            // already cached by first loop
            left += childMargins[beforeXName];
            childContext.setProp(names.x, left);
            // We can read directly from "props.width" because we have already properly
            // requested it in the calculation of nonFlexedWidths or we calculated it.
            // We cannot call getProp because that would be inappropriate for flexed items
            // and we don't need any extra function call overhead:
            left += childMargins[afterXName] + childContext.props[widthName];
        }
        contentWidth += ownerContext.targetContext.getPaddingInfo()[widthName];
        ownerContext.state.contentWidth = contentWidth;
        // if there is perpendicular overflow, the published parallel content size includes
        // the size of the perpendicular scrollbar.
        if (needsScroll && needsScroll.perpendicular) {
            if (widthShrinkWrap) {
                contentWidth += scrollbarWidth;
            }
            ownerContext[names.hasOverflowY] = true;
            // tell the component layout to set the parallel size in the dom
            ownerContext.target.componentLayout[names.setWidthInDom] = true;
            // IE8 will not create a scrollbar if there is just the *exactly correct*
            // spare space created for it. We have to force that to happen once all the
            // styles have been flushed to the DOM (see completeLayout):
            ownerContext[names.invalidateScrollY] = Ext.isIE8;
        }
        ownerContext[names.setContentWidth](contentWidth);
        return true;
    },
    calculatePerpendicular: function(ownerContext, names, plan) {
        var me = this,
            state = ownerContext.state,
            needsScroll = state.needsScroll,
            canScroll = state.canScroll,
            heightShrinkWrap = ownerContext.perpendicularSizeModel.shrinkWrap,
            targetSize = plan.targetSize,
            childItems = ownerContext.childItems,
            childItemsLength = childItems.length,
            mmax = Math.max,
            heightName = names.height,
            setHeightName = names.setHeight,
            beforeYName = names.beforeY,
            topPositionName = names.y,
            padding = me.padding,
            top = padding[beforeYName],
            availHeight = targetSize[heightName] - top - padding[names.afterY],
            align = ownerContext.boxOptions.align,
            isStretch = align.stretch,
            // never true if heightShrinkWrap (see beginLayoutCycle)
            isStretchMax = align.stretchmax,
            isCenter = align.center,
            isBottom = align.bottom,
            constrain = me.constrainAlign,
            maxHeight = 0,
            hasPercentageSizes = 0,
            onBeforeInvalidateChild = me.onBeforeConstrainInvalidateChild,
            onAfterInvalidateChild = me.onAfterConstrainInvalidateChild,
            scrollbarHeight = Ext.scrollbar.height(),
            childTop, i, childHeight, childMargins, diff, height, childContext, stretchMaxPartner, stretchMaxChildren, shrinkWrapParallelOverflow, percentagePerpendicular;
        if (!heightShrinkWrap && !targetSize[names.gotHeight]) {
            return false;
        }
        if (isStretch || ((isCenter || isBottom) && !heightShrinkWrap)) {
            if (isNaN(availHeight)) {
                return false;
            }
        }
        // If the intention is to horizontally scroll child components, but the container
        // is too narrow, then:
        //     if we are shrinkwrapping height:
        //         Set a flag because we are going to expand the height taken by the perpendicular
        //         dimension to accommodate the scrollbar
        //     else
        //         We must allow for the parallel scrollbar to intrude into the height
        if (needsScroll && needsScroll.parallel) {
            if (heightShrinkWrap) {
                shrinkWrapParallelOverflow = true;
            } else {
                availHeight -= scrollbarHeight;
                plan.targetSize[heightName] -= scrollbarHeight;
            }
        }
        if (isStretch) {
            height = availHeight;
            // never heightShrinkWrap...
            maxHeight = mmax(height, ownerContext.smallestHeight);
        } else {
            for (i = 0; i < childItemsLength; i++) {
                childContext = childItems[i];
                childMargins = (childContext.marginInfo || childContext.getMarginInfo())[heightName];
                if (!(percentagePerpendicular = childContext.percentagePerpendicular)) {
                    childHeight = childContext.getProp(heightName);
                } else {
                    ++hasPercentageSizes;
                    if (heightShrinkWrap) {
                        // height %age items cannot contribute to maxHeight... they are going
                        // to be a %age of that maxHeight!
                        
                        continue;
                    } else {
                        childHeight = percentagePerpendicular * availHeight - childMargins;
                        childHeight = childContext[setHeightName](childHeight);
                    }
                }
                // Summary:
                // 1) Not shrink wrapping height, so the height is not determined by the children
                // 2) Constrain is set
                // 3) The child item is shrink wrapping
                // 4) It exceeds the max
                if (!heightShrinkWrap && constrain && childContext[names.heightModel].shrinkWrap && childHeight > availHeight) {
                    childContext.invalidate({
                        before: onBeforeInvalidateChild,
                        after: onAfterInvalidateChild,
                        layout: me,
                        childHeight: availHeight,
                        names: names
                    });
                    // By invalidating the height, it could mean the width can change, so we need
                    // to recalculate in the parallel direction.
                    ownerContext.state.parallelDone = false;
                }
                // Max perpendicular measurement (used for stretchmax) must take the min
                // perpendicular size of each child into account in case any fall short.
                if (isNaN(maxHeight = mmax(maxHeight, childHeight + childMargins, childContext.target[names.minHeight] || 0))) {
                    return false;
                }
            }
        }
        // heightShrinkWrap || isCenter || isStretchMax ??
        // If there is going to be a parallel scrollbar maxHeight must include it
        // to the outside world.
        // ie: a stretchmaxPartner, and the setContentHeight
        if (shrinkWrapParallelOverflow) {
            maxHeight += scrollbarHeight;
            ownerContext[names.hasOverflowX] = true;
            // tell the component layout to set the perpendicular size in the dom
            ownerContext.target.componentLayout[names.setHeightInDom] = true;
            // IE8 will not create a scrollbar if there is just the *exactly correct*
            // spare space created for it. We have to force that to happen once all
            // the styles have been flushed to the DOM (see completeLayout):
            ownerContext[names.invalidateScrollX] = Ext.isIE8;
        }
        // If we are associated with another box layout, grab its maxChildHeight
        // This must happen before we calculate and publish our contentHeight
        stretchMaxPartner = ownerContext.stretchMaxPartner;
        if (stretchMaxPartner) {
            // Publish maxChildHeight as soon as it has been calculated for our partner:
            ownerContext.setProp('maxChildHeight', maxHeight);
            stretchMaxChildren = stretchMaxPartner.childItems;
            // Only wait for maxChildHeight if our partner has visible items:
            if (stretchMaxChildren && stretchMaxChildren.length) {
                maxHeight = mmax(maxHeight, stretchMaxPartner.getProp('maxChildHeight'));
                if (isNaN(maxHeight)) {
                    return false;
                }
            }
        }
        ownerContext[names.setContentHeight](maxHeight + me.padding[heightName] + ownerContext.targetContext.getPaddingInfo()[heightName]);
        // We have to publish the contentHeight with the additional scrollbarHeight
        // to encourage our container to accommodate it, but we must remove the height
        // of the scrollbar as we go to sizing or centering the children.
        if (shrinkWrapParallelOverflow) {
            maxHeight -= scrollbarHeight;
        }
        if (maxHeight > targetSize[heightName] && canScroll && canScroll.perpendicular) {
            state.actualScroll.perpendicular = true;
        }
        plan.maxSize = maxHeight;
        if (isStretchMax) {
            height = maxHeight;
        } else if (isCenter || isBottom || hasPercentageSizes) {
            if (constrain) {
                height = heightShrinkWrap ? maxHeight : availHeight;
            } else {
                height = heightShrinkWrap ? maxHeight : mmax(availHeight, maxHeight);
            }
            // When calculating a centered position within the content box of the innerCt,
            // the width of the borders must be subtracted from the size to yield the
            // space available to center within. The publishInnerCtSize method explicitly
            // adds the border widths to the set size of the innerCt.
            height -= ownerContext.innerCtContext.getBorderInfo()[heightName];
        }
        for (i = 0; i < childItemsLength; i++) {
            childContext = childItems[i];
            childMargins = childContext.marginInfo || childContext.getMarginInfo();
            childTop = top + childMargins[beforeYName];
            if (isStretch) {
                childContext[setHeightName](height - childMargins[heightName]);
            } else {
                percentagePerpendicular = childContext.percentagePerpendicular;
                if (heightShrinkWrap && percentagePerpendicular) {
                    childMargins = childContext.marginInfo || childContext.getMarginInfo();
                    childHeight = percentagePerpendicular * height - childMargins[heightName];
                    childHeight = childContext[setHeightName](childHeight);
                }
                if (isCenter) {
                    diff = height - childContext.props[heightName];
                    if (diff > 0) {
                        childTop = top + Math[me.alignRoundingMethod](diff / 2);
                    }
                } else if (isBottom) {
                    childTop = mmax(0, height - childTop - childContext.props[heightName]);
                }
            }
            childContext.setProp(topPositionName, childTop);
        }
        return true;
    },
    onBeforeConstrainInvalidateChild: function(childContext, options) {
        // NOTE: No "this" pointer in here...
        var heightModelName = options.names.heightModel;
        if (!childContext[heightModelName].constrainedMin) {
            // if the child hit a min constraint, it needs to be at its configured size, so
            // we leave the sizeModel alone
            childContext[heightModelName] = Ext.layout.SizeModel.calculated;
        }
    },
    onAfterConstrainInvalidateChild: function(childContext, options) {
        // NOTE: No "this" pointer in here...
        var names = options.names;
        // We use 0 here because we know the size exceeds the available size.
        // This was chosen on purpose, even for align: 'bottom', because it doesn't
        // make practical sense to place the item at the bottom and then have it overflow
        // over the top of the container, since it's not possible to scroll to it. As such,
        // we always put the component at the top to follow normal document flow.
        childContext.setProp(names.beforeY, 0);
        if (childContext[names.heightModel].calculated) {
            childContext[names.setHeight](options.childHeight);
        }
    },
    calculateStretchMax: function(ownerContext, names, plan) {
        var me = this,
            heightName = names.height,
            widthName = names.width,
            childItems = ownerContext.childItems,
            length = childItems.length,
            height = plan.maxSize,
            onBeforeStretchMaxInvalidateChild = me.onBeforeStretchMaxInvalidateChild,
            onAfterStretchMaxInvalidateChild = me.onAfterStretchMaxInvalidateChild,
            childContext, props, i, childHeight;
        for (i = 0; i < length; ++i) {
            childContext = childItems[i];
            props = childContext.props;
            childHeight = height - childContext.getMarginInfo()[heightName];
            if (childHeight !== props[heightName] || // if (wrong height ...
            childContext[names.heightModel].constrained) {
                // ...or needs invalidation)
                // When we invalidate a child, since we won't be around to size or position
                // it, we include an after callback that will be run after the invalidate
                // that will (re)do that work. The good news here is that we can read the
                // results of all that from the childContext props.
                //
                // We also include a before callback to change the sizeModel to calculated
                // prior to the layout being invoked.
                childContext.invalidate({
                    before: onBeforeStretchMaxInvalidateChild,
                    after: onAfterStretchMaxInvalidateChild,
                    layout: me,
                    // passing this data avoids a 'scope' and its Function.bind
                    childWidth: props[widthName],
                    // subtract margins from the maximum value
                    childHeight: childHeight,
                    childX: props.x,
                    childY: props.y,
                    names: names
                });
            }
        }
    },
    onBeforeStretchMaxInvalidateChild: function(childContext, options) {
        // NOTE: No "this" pointer in here...
        var heightModelName = options.names.heightModel;
        // Change the childItem to calculated (i.e., "set by ownerCt"). The component layout
        // of the child can course-correct (like dock layout does for a collapsed panel),
        // so we must make these changes here before that layout's beginLayoutCycle is
        // called.
        if (!childContext[heightModelName].constrainedMax) {
            // if the child hit a max constraint, it needs to be at its configured size, so
            // we leave the sizeModel alone...
            childContext[heightModelName] = Ext.layout.SizeModel.calculated;
        }
    },
    onAfterStretchMaxInvalidateChild: function(childContext, options) {
        // NOTE: No "this" pointer in here...
        var names = options.names,
            childHeight = options.childHeight,
            childWidth = options.childWidth;
        childContext.setProp('x', options.childX);
        childContext.setProp('y', options.childY);
        if (childContext[names.heightModel].calculated) {
            // We need to respect a child that is still not calculated (such as a collapsed
            // panel)...
            childContext[names.setHeight](childHeight);
        }
        if (childContext[names.widthModel].calculated) {
            childContext[names.setWidth](childWidth);
        }
    },
    completeLayout: function(ownerContext) {
        var me = this,
            invalidateScrollX = ownerContext.invalidateScrollX,
            invalidateScrollY = ownerContext.invalidateScrollY,
            overflowHandler = me.overflowHandler,
            dom, el, overflowX, overflowY, styles;
        if (overflowHandler) {
            overflowHandler.completeLayout(ownerContext);
        }
        if (invalidateScrollX || invalidateScrollY) {
            el = me.getTarget();
            dom = el.dom;
            styles = dom.style;
            if (invalidateScrollX) {
                // get computed style to see if we are 'auto'
                overflowX = el.getStyle('overflowX');
                if (overflowX === 'auto') {
                    // capture the inline style (if any) so we can restore it later:
                    overflowX = styles.overflowX;
                    styles.overflowX = 'scroll';
                } else // force the scrollbar to appear
                {
                    invalidateScrollX = false;
                }
            }
            // no work really since not 'auto'
            if (invalidateScrollY) {
                // get computed style to see if we are 'auto'
                overflowY = el.getStyle('overflowY');
                if (overflowY === 'auto') {
                    // capture the inline style (if any) so we can restore it later:
                    overflowY = styles.overflowY;
                    styles.overflowY = 'scroll';
                } else // force the scrollbar to appear
                {
                    invalidateScrollY = false;
                }
            }
            // no work really since not 'auto'
            if (invalidateScrollX || invalidateScrollY) {
                // if (some form of 'auto' in play)
                // force a reflow...
                // eslint-disable-next-line no-unused-expressions
                dom.scrollWidth;
                if (invalidateScrollX) {
                    styles.overflowX = overflowX;
                }
                // restore inline style
                if (invalidateScrollY) {
                    styles.overflowY = overflowY;
                }
            }
        }
    },
    // restore inline style
    finishedLayout: function(ownerContext) {
        var overflowHandler = this.overflowHandler;
        if (overflowHandler) {
            overflowHandler.finishedLayout(ownerContext);
        }
        this.callParent([
            ownerContext
        ]);
    },
    getLayoutItems: function() {
        var items = this.callParent(),
            n = items.length,
            lastVisibleItem, hide, i, item, splitAfter, splitBefore, splitter;
        for (i = 0; i < n; ++i) {
            if ((item = items[i]).isSplitter) {
                
                continue;
            }
            splitter = item.splitter;
            if (item.hidden) {
                if (splitter) {
                    // hidden items always need to hide their splitter
                    if (!splitter.hidden) {
                        splitter.hidden = true;
                        if (splitter.el) {
                            splitter.el.hide();
                        }
                    }
                }
                
                continue;
            }
            if (splitter) {
                splitBefore = splitter.collapseTarget === 'next';
            } else {
                // item w/o splitter
                splitBefore = false;
            }
            hide = null;
            if (lastVisibleItem && splitAfter) {
                // the last item had a splitter after it so we can keep it and hide
                // this one if splitBefore
                if (splitAfter.hidden) {
                    splitAfter.hidden = false;
                    if (splitAfter.el) {
                        splitAfter.el.show();
                    }
                }
                if (splitBefore) {
                    hide = true;
                }
            } else if (splitBefore) {
                hide = !lastVisibleItem;
            }
            // else we have no splitter or are !splitBefore, so we defer the fate of this
            // splitter
            if (hide !== null && splitter.hidden !== hide) {
                splitter.hidden = hide;
                if (splitter.el) {
                    splitter.el.setVisible(!hide);
                }
            }
            splitAfter = !splitBefore && splitter;
            lastVisibleItem = item;
        }
        // If we ended with a visible item and a splitAfter, we need to hide the tail
        // splitter
        if (lastVisibleItem && splitAfter && !splitAfter.hidden) {
            splitAfter.hidden = true;
            if (splitAfter.el) {
                splitAfter.el.hide();
            }
        }
        return items;
    },
    /**
     * Inserts the splitter for a given region. A reference to the splitter is also stored
     * on the component as "splitter".
     * @private
     */
    insertSplitter: function(item, index, hidden, splitterCfg) {
        var splitter = {
                xtype: 'splitter',
                id: item.id + '-splitter',
                hidden: hidden,
                splitterFor: item,
                synthetic: true
            },
            // not user-defined
            at = index + ((splitterCfg.collapseTarget === 'prev') ? 1 : 0);
        splitter[this.names.height] = '100%';
        if (splitterCfg) {
            Ext.apply(splitter, splitterCfg);
        }
        item.splitter = this.owner.add(at, splitter);
    },
    publishInnerCtSize: function(ownerContext, widthOffset) {
        var me = this,
            state = ownerContext.state,
            names = ownerContext.boxNames,
            heightName = names.height,
            widthName = names.width,
            align = ownerContext.boxOptions.align,
            padding = me.padding,
            plan = state.boxPlan,
            targetSize = plan.targetSize,
            height = plan.maxSize,
            needsScroll = state.needsScroll,
            innerCtContext = ownerContext.innerCtContext,
            innerCtWidth, innerCtHeight;
        widthOffset = widthOffset || 0;
        // The state.canScroll check is on purpose here, all we want to know is whether we have
        // a scrollable instance, since even if UI scrolling isn't available, we may scroll it
        // programmatically
        if (ownerContext.parallelSizeModel.shrinkWrap || (plan.tooNarrow && state.canScroll)) {
            innerCtWidth = state.contentWidth - ownerContext.targetContext.getPaddingInfo()[widthName];
        } else {
            innerCtWidth = targetSize[widthName];
            if (needsScroll && needsScroll.perpendicular) {
                innerCtWidth -= Ext.scrollbar.size()[widthName];
            }
        }
        innerCtWidth -= widthOffset;
        // Allow the other co-operating objects to know whether the columns overflow
        // the available width.
        me.owner.tooNarrow = plan.tooNarrow;
        if (align.stretch) {
            innerCtHeight = height;
        } else {
            innerCtHeight = plan.maxSize + padding[names.beforeY] + padding[names.afterY] + innerCtContext.getBorderInfo()[heightName];
            if (!ownerContext.perpendicularSizeModel.shrinkWrap && (align.center || align.bottom)) {
                innerCtHeight = Math.max(targetSize[heightName], innerCtHeight);
            }
        }
        innerCtContext[names.setWidth](innerCtWidth);
        innerCtContext[names.setHeight](innerCtHeight);
        // Fix for an obscure webkit bug (EXTJSIV-5962) caused by the targetEl's 20000px
        // width.  We set a very large width on the targetEl at the beginning of the 
        // layout cycle to prevent any "crushing" effect on the child items, however
        // in some cases the very large width makes it possible to scroll the innerCt
        // by dragging on certain child elements. To prevent this from happening we ensure
        // that the targetEl's width is the same as the innerCt.
        // IE needs it because of its scrollIntoView bug: https://sencha.jira.com/browse/EXTJSIV-6520
        // Webkit needs it because of its mouse drag bug: https://sencha.jira.com/browse/EXTJSIV-5962
        // FF needs it because of a vertical tab bug: https://sencha.jira.com/browse/EXTJSIV-8614
        ownerContext.targetElContext.setWidth(ownerContext.innerCtContext.props.width - (me.vertical ? 0 : (widthOffset || 0)));
        // If unable to publish both dimensions, this layout needs to run again
        if (isNaN(innerCtWidth + innerCtHeight)) {
            me.done = false;
        }
    },
    onAdd: function(item, index) {
        var me = this,
            // Buttons have their own concept of "split" config
            split = me.enableSplitters && !item.isButton && item.split;
        me.callParent([
            item,
            index
        ]);
        if (split) {
            if (split === true) {
                split = {
                    collapseTarget: 'next'
                };
            } else if (Ext.isString(split)) {
                split = {
                    collapseTarget: split === 'before' ? 'next' : 'prev'
                };
            } else {
                split = Ext.apply({
                    collapseTarget: split.side === 'before' ? 'next' : 'prev'
                }, split);
            }
            me.insertSplitter(item, index, !!item.hidden, split);
        }
    },
    onRemove: function(comp, isDestroying) {
        var me = this,
            names = me.names,
            owner = me.owner,
            splitter = comp.splitter,
            overflowHandler = me.overflowHandler,
            el;
        me.callParent([
            comp,
            isDestroying
        ]);
        if (splitter && owner.contains(splitter)) {
            owner.doRemove(splitter, true);
            comp.splitter = null;
        }
        if (overflowHandler) {
            overflowHandler.onRemove(comp);
        }
        if (comp.layoutMarginCap === me.id) {
            delete comp.layoutMarginCap;
        }
        if (!owner.destroying && !isDestroying && comp.rendered) {
            // Clear top/left styles
            el = comp.getEl();
            if (el) {
                el.setStyle(names.beforeY, '');
                el.setStyle(names.beforeX, '');
                // Box layout imposes margin:0 on its child items and the layout provides margins
                // using its absolute positioning strategy. This has to be reversed on remove.
                el.setStyle('margin', '');
            }
        }
    },
    applyOverflowHandler: function(overflowHandler, oldOverflowHandler) {
        if (typeof overflowHandler === 'string') {
            overflowHandler = {
                type: overflowHandler
            };
        }
        if (oldOverflowHandler && oldOverflowHandler.type === overflowHandler.type) {
            delete overflowHandler.type;
            oldOverflowHandler.setConfig(overflowHandler);
            return oldOverflowHandler;
        }
        overflowHandler.layout = this;
        return Ext.Factory.boxOverflow(overflowHandler);
    },
    // Overridden method from Ext.layout.container.Container.
    // Used in the beforeLayout method to render all items into.
    getRenderTarget: function() {
        return this.targetEl;
    },
    // Overridden method from Ext.layout.container.Container.
    // Used by Container classes to insert special DOM elements which must exist
    // in addition to the child components
    getElementTarget: function() {
        return this.innerCt;
    },
    calculateChildBox: Ext.deprecated(),
    calculateChildBoxes: Ext.deprecated(),
    updateChildBoxes: Ext.deprecated(),
    destroy: function() {
        var me = this;
        Ext.destroy(me.innerCt, me.overflowHandler);
        me.flexSortFn = me.innerCt = null;
        me.callParent();
    },
    getRenderData: function() {
        var data = this.callParent();
        data.targetElCls = this.targetElCls;
        return data;
    },
    updateVertical: function(vertical) {
        var me = this,
            overflowHandler = me.overflowHandler,
            owner = me.owner,
            props = me._props;
        Ext.apply(me, vertical ? props.vbox : props.hbox);
        if (overflowHandler && owner && owner.rendered) {
            overflowHandler.setVertical(vertical);
        }
    },
    _props: {
        // HBOX - this key is produced by setVertical
        'hbox': {
            direction: 'horizontal',
            oppositeDirection: 'vertical',
            horizontal: true,
            vertical: false,
            names: {
                // parallel
                beforeX: 'left',
                beforeScrollX: 'left',
                leftCap: 'Left',
                afterX: 'right',
                width: 'width',
                contentWidth: 'contentWidth',
                minWidth: 'minWidth',
                maxWidth: 'maxWidth',
                widthCap: 'Width',
                widthModel: 'widthModel',
                widthIndex: 0,
                x: 'x',
                getX: 'getX',
                setX: 'setX',
                scrollLeft: 'scrollLeft',
                overflowX: 'overflowX',
                hasOverflowX: 'hasOverflowX',
                invalidateScrollX: 'invalidateScrollX',
                parallelMargins: 'lr',
                // perpendicular
                center: 'middle',
                beforeY: 'top',
                afterY: 'bottom',
                height: 'height',
                contentHeight: 'contentHeight',
                minHeight: 'minHeight',
                maxHeight: 'maxHeight',
                heightCap: 'Height',
                heightModel: 'heightModel',
                heightIndex: 1,
                y: 'y',
                getY: 'getY',
                setY: 'setY',
                overflowY: 'overflowY',
                hasOverflowY: 'hasOverflowY',
                invalidateScrollY: 'invalidateScrollY',
                perpendicularMargins: 'tb',
                // Methods
                getWidth: 'getWidth',
                getHeight: 'getHeight',
                setWidth: 'setWidth',
                setHeight: 'setHeight',
                gotWidth: 'gotWidth',
                gotHeight: 'gotHeight',
                setContentWidth: 'setContentWidth',
                setContentHeight: 'setContentHeight',
                setWidthInDom: 'setWidthInDom',
                setHeightInDom: 'setHeightInDom',
                getScrollLeft: 'getScrollLeft',
                setScrollLeft: 'setScrollLeft',
                scrollTo: 'scrollTo'
            },
            sizePolicy: {
                flex: {
                    '': {
                        readsWidth: 0,
                        readsHeight: 1,
                        setsWidth: 1,
                        setsHeight: 0
                    },
                    stretch: {
                        readsWidth: 0,
                        readsHeight: 0,
                        setsWidth: 1,
                        setsHeight: 1
                    },
                    stretchmax: {
                        readsWidth: 0,
                        readsHeight: 1,
                        setsWidth: 1,
                        setsHeight: 1
                    }
                },
                '': {
                    readsWidth: 1,
                    readsHeight: 1,
                    setsWidth: 0,
                    setsHeight: 0
                },
                stretch: {
                    readsWidth: 1,
                    readsHeight: 0,
                    setsWidth: 0,
                    setsHeight: 1
                },
                stretchmax: {
                    readsWidth: 1,
                    readsHeight: 1,
                    setsWidth: 0,
                    setsHeight: 1
                }
            }
        },
        // VBOX
        'vbox': {
            direction: 'vertical',
            oppositeDirection: 'horizontal',
            horizontal: false,
            vertical: true,
            names: {
                // parallel
                beforeX: 'top',
                beforeScrollX: 'top',
                leftCap: 'Top',
                afterX: 'bottom',
                width: 'height',
                contentWidth: 'contentHeight',
                minWidth: 'minHeight',
                maxWidth: 'maxHeight',
                widthCap: 'Height',
                widthModel: 'heightModel',
                widthIndex: 1,
                x: 'y',
                getX: 'getY',
                setX: 'setY',
                scrollLeft: 'scrollTop',
                overflowX: 'overflowY',
                hasOverflowX: 'hasOverflowY',
                invalidateScrollX: 'invalidateScrollY',
                parallelMargins: 'tb',
                // perpendicular
                center: 'center',
                beforeY: 'left',
                afterY: 'right',
                height: 'width',
                contentHeight: 'contentWidth',
                minHeight: 'minWidth',
                maxHeight: 'maxWidth',
                heightCap: 'Width',
                heightModel: 'widthModel',
                heightIndex: 0,
                y: 'x',
                getY: 'getX',
                setY: 'setX',
                overflowY: 'overflowX',
                hasOverflowY: 'hasOverflowX',
                invalidateScrollY: 'invalidateScrollX',
                perpendicularMargins: 'lr',
                // Methods
                getWidth: 'getHeight',
                getHeight: 'getWidth',
                setWidth: 'setHeight',
                setHeight: 'setWidth',
                gotWidth: 'gotHeight',
                gotHeight: 'gotWidth',
                setContentWidth: 'setContentHeight',
                setContentHeight: 'setContentWidth',
                setWidthInDom: 'setHeightInDom',
                setHeightInDom: 'setWidthInDom',
                getScrollLeft: 'getScrollTop',
                setScrollLeft: 'setScrollTop',
                scrollTo: 'scrollTo'
            },
            sizePolicy: {
                flex: {
                    '': {
                        readsWidth: 1,
                        readsHeight: 0,
                        setsWidth: 0,
                        setsHeight: 1
                    },
                    stretch: {
                        readsWidth: 0,
                        readsHeight: 0,
                        setsWidth: 1,
                        setsHeight: 1
                    },
                    stretchmax: {
                        readsWidth: 1,
                        readsHeight: 0,
                        setsWidth: 1,
                        setsHeight: 1
                    }
                },
                '': {
                    readsWidth: 1,
                    readsHeight: 1,
                    setsWidth: 0,
                    setsHeight: 0
                },
                stretch: {
                    readsWidth: 0,
                    readsHeight: 1,
                    setsWidth: 1,
                    setsHeight: 0
                },
                stretchmax: {
                    readsWidth: 1,
                    readsHeight: 1,
                    setsWidth: 1,
                    setsHeight: 0
                }
            }
        }
    }
});

/**
 * A layout that arranges items horizontally across a 
 * {@link Ext.container.Container Container}. This layout optionally divides available 
 * horizontal space between child {@link Ext.container.Container#cfg-items items} 
 * containing a numeric {@link Ext.Component#cfg-flex flex} configuration.
 * 
 * This layout may be used to set the heights or vertical position of child items 
 * by configuring it with the {@link #align} option.  The horizontal position of the 
 * child items may be set using the {@link #pack} config.
 * 
 *     @example
 *     Ext.create('Ext.Panel', {
 *         width: 500,
 *         height: 300,
 *         title: "HBoxLayout Panel",
 *         layout: {
 *             type: 'hbox',
 *             align: 'stretch'
 *         },
 *         renderTo: document.body,
 *         items: [{
 *             xtype: 'panel',
 *             title: 'Inner Panel One',
 *             flex: 2
 *         },{
 *             xtype: 'panel',
 *             title: 'Inner Panel Two',
 *             flex: 1
 *         },{
 *             xtype: 'panel',
 *             title: 'Inner Panel Three',
 *             flex: 1
 *         }]
 *     });
 * 
 * The following example may be used to view the outcomes when combining the `align` and 
 * `pack` configs:
 * 
 *     @example
 *     Ext.create({
 *         xtype: 'panel',
 *         renderTo: Ext.getBody(),
 *         height: 400,
 *         width: 520,
 *         defaultListenerScope: true,
 *         layout: 'hbox',
 *         defaultType: 'button',
 *         items: [{
 *             text: 'One'
 *         }, {
 *             text: 'Two'
 *         }, {
 *             text: 'Three'
 *         }],
 *         dockedItems: [{
 *             xtype: 'toolbar',
 *             dock: 'top',
 *             items: [{
 *                 xtype: 'buttongroup',
 *                 title: 'align',
 *                 layout: 'fit',
 *                 items: [{
 *                     xtype: 'segmentedbutton',
 *                     margin: 10,
 *                     allowDepress: true,
 *                     defaults: {
 *                         configType: 'align'  // custom config used in this example
 *                     },
 *                     items: [{
 *                         text: 'begin'
 *                     }, {
 *                         text: 'middle'
 *                     }, {
 *                         text: 'end'
 *                     }, {
 *                         text: 'stretch'
 *                     }, {
 *                         text: 'stretchmax'
 *                     }],
 *                     listeners: {
 *                         toggle: 'onToggle'
 *                     }
 *                 }]
 *             }, '->', {
 *                 xtype: 'buttongroup',
 *                 title: 'pack',
 *     			   layout: 'fit',
 *                 items: [{
 *                     xtype: 'segmentedbutton',
 *                     margin: 10,
 *                     allowDepress: true,
 *                     defaults: {
 *                         configType: 'pack'  // custom config used in this example
 *                     },
 *                     items: [{
 *                         text: 'start'
 *                     }, {
 *                         text: 'center'
 *                     }, {
 *                         text: 'end'
 *                     }],
 *                     listeners: {
 *                         toggle: 'onToggle'
 *                     }
 *                 }]
 *             }]
 *         }],
 *         
 *         onToggle: function (group, button, isPressed) {
 *             var cfg = {};
 *             
 *             cfg[button.configType] = isPressed ? button.getText() : null;
 *             this.setLayout(cfg);
 *         }
 *     });
 */
Ext.define('Ext.layout.container.HBox', {
    extend: 'Ext.layout.container.Box',
    alias: 'layout.hbox',
    alternateClassName: 'Ext.layout.HBoxLayout',
    type: 'hbox',
    vertical: false
});

/**
 * A layout that arranges items vertically down a 
 * {@link Ext.container.Container Container}. This layout optionally divides available 
 * vertical space between child {@link Ext.container.Container#cfg-items items} 
 * containing a numeric {@link Ext.Component#cfg-flex flex} configuration.
 *
 * This layout may also be used to set the widths of child items by configuring it with 
 * the {@link #align} option.  The vertical position of the child items may be set using 
 * the {@link #pack} config.
 *
 *     @example
 *     Ext.create('Ext.Panel', {
 *         width: 500,
 *         height: 400,
 *         title: "VBoxLayout Panel",
 *         layout: {
 *             type: 'vbox',
 *         },
 *         renderTo: document.body,
 *         items: [{
 *             xtype: 'panel',
 *             title: 'Inner Panel One',
 *             width: 250,
 *             flex: 2
 *         },
 *         {
 *             xtype: 'panel',
 *             title: 'Inner Panel Two',
 *             width: 250,
 *             flex: 4
 *         },
 *         {
 *             xtype: 'panel',
 *             title: 'Inner Panel Three',
 *             width: '50%',
 *             flex: 4
 *         }]
 *     });
 * 
 * The following example may be used to view the outcomes when combining the `align` and 
 * `pack` configs:
 * 
 *     @example
 *     Ext.create({
 *         xtype: 'panel',
 *         renderTo: Ext.getBody(),
 *         height: 400,
 *         width: 520,
 *         defaultListenerScope: true,
 *         layout: 'vbox',
 *         defaultType: 'button',
 *         items: [{
 *             text: 'One'
 *         }, {
 *             text: 'Two'
 *         }, {
 *             text: 'Three'
 *         }],
 *         
 *         dockedItems: [{
 *             xtype: 'toolbar',
 *             dock: 'top',
 *             items: [{
 *                 xtype: 'buttongroup',
 *                 title: 'align',
 *                 layout: 'fit',
 *                 items: [{
 *                     xtype: 'segmentedbutton',
 *                     margin: 10,
 *                     allowDepress: true,
 *                     defaults: {
 *                         configType: 'align'  // custom config used in this example
 *                     },
 *                     items: [{
 *                         text: 'begin'
 *                     }, {
 *                         text: 'middle'
 *                     }, {
 *                         text: 'end'
 *                     }, {
 *                         text: 'stretch'
 *                     }, {
 *                         text: 'stretchmax'
 *                     }],
 *                     listeners: {
 *                         toggle: 'onToggle'
 *                     }
 *                 }]
 *             }, '->', {
 *                 xtype: 'buttongroup',
 *                 title: 'pack',
 *                 layout: 'fit',
 *                 items: [{
 *                     xtype: 'segmentedbutton',
 *                     margin: 10,
 *                     allowDepress: true,
 *                     defaults: {
 *                         configType: 'pack'  // custom config used in this example
 *                     },
 *                     items: [{
 *                         text: 'start'
 *                     }, {
 *                         text: 'center'
 *                     }, {
 *                         text: 'end'
 *                     }],
 *                     listeners: {
 *                         toggle: 'onToggle'
 *                     }
 *                 }]
 *             }]
 *         }],
 *         
 *         onToggle: function (group, button, isPressed) {
 *             var cfg = {};
 *             
 *             cfg[button.configType] = isPressed ? button.getText() : null;
 *             this.setLayout(cfg);
 *         }
 *     });
 */
Ext.define('Ext.layout.container.VBox', {
    extend: 'Ext.layout.container.Box',
    alias: 'layout.vbox',
    alternateClassName: 'Ext.layout.VBoxLayout',
    type: 'vbox',
    vertical: true
});

/* eslint-disable max-len */
/**
 * Basic Toolbar class. Although the {@link Ext.container.Container#defaultType defaultType} for
 * Toolbar is {@link Ext.button.Button button}, Toolbar elements (child items for the Toolbar container)
 * may be virtually any type of Component. Toolbar elements can be created explicitly via their
 * constructors, or implicitly via their xtypes, and can be {@link #method-add}ed dynamically.
 *
 * ## Some items have shortcut strings for creation:
 *
 * | Shortcut | xtype         | Class                         | Description
 * |:---------|:--------------|:------------------------------|:---------------------------------------------------
 * | '->'     | `tbfill`      | {@link Ext.toolbar.Fill}      | begin using the right-justified button container
 * | '-'      | `tbseparator` | {@link Ext.toolbar.Separator} | add a vertical separator bar between toolbar items
 * | ' '      | `tbspacer`    | {@link Ext.toolbar.Spacer}    | add horizontal space between elements
 *
 *     @example
 *     Ext.create('Ext.toolbar.Toolbar', {
 *         renderTo: document.body,
 *         width   : 500,
 *         items: [
 *             {
 *                 // xtype: 'button', // default for Toolbars
 *                 text: 'Button'
 *             },
 *             {
 *                 xtype: 'splitbutton',
 *                 text : 'Split Button'
 *             },
 *             // begin using the right-justified button container
 *             '->', // same as { xtype: 'tbfill' }
 *             {
 *                 xtype    : 'textfield',
 *                 name     : 'field1',
 *                 emptyText: 'enter search term'
 *             },
 *             // add a vertical separator bar between toolbar items
 *             '-', // same as {xtype: 'tbseparator'} to create Ext.toolbar.Separator
 *             'text 1', // same as {xtype: 'tbtext', text: 'text1'} to create Ext.toolbar.TextItem
 *             { xtype: 'tbspacer' },// same as ' ' to create Ext.toolbar.Spacer
 *             'text 2',
 *             { xtype: 'tbspacer', width: 50 }, // add a 50px space
 *             'text 3'
 *         ]
 *     });
 *
 * Toolbars have {@link #method-enable} and {@link #method-disable} methods which when called, will
 * enable/disable all items within your toolbar.
 *
 *     @example
 *     Ext.create('Ext.toolbar.Toolbar', {
 *         renderTo: document.body,
 *         width   : 400,
 *         items: [
 *             {
 *                 text: 'Button'
 *             },
 *             {
 *                 xtype: 'splitbutton',
 *                 text : 'Split Button'
 *             },
 *             '->',
 *             {
 *                 xtype    : 'textfield',
 *                 name     : 'field1',
 *                 emptyText: 'enter search term'
 *             }
 *         ]
 *     });
 *
 * Example
 *
 *     @example
 *     var enableBtn = Ext.create('Ext.button.Button', {
 *         text    : 'Enable All Items',
 *         disabled: true,
 *         scope   : this,
 *         handler : function() {
 *             //disable the enable button and enable the disable button
 *             enableBtn.disable();
 *             disableBtn.enable();
 *
 *             //enable the toolbar
 *             toolbar.enable();
 *         }
 *     });
 *
 *     var disableBtn = Ext.create('Ext.button.Button', {
 *         text    : 'Disable All Items',
 *         scope   : this,
 *         handler : function() {
 *             //enable the enable button and disable button
 *             disableBtn.disable();
 *             enableBtn.enable();
 *
 *             //disable the toolbar
 *             toolbar.disable();
 *         }
 *     });
 *
 *     var toolbar = Ext.create('Ext.toolbar.Toolbar', {
 *         renderTo: document.body,
 *         width   : 400,
 *         margin  : '5 0 0 0',
 *         items   : [enableBtn, disableBtn]
 *     });
 *
 * Adding items to and removing items from a toolbar is as simple as calling the {@link #method-add}
 * and {@link #method-remove} methods. There is also a {@link #removeAll} method
 * which remove all items within the toolbar.
 *
 *     @example
 *     var toolbar = Ext.create('Ext.toolbar.Toolbar', {
 *         renderTo: document.body,
 *         width   : 700,
 *         items: [
 *             {
 *                 text: 'Example Button'
 *             }
 *         ]
 *     });
 *
 *     var addedItems = [];
 *
 *     Ext.create('Ext.toolbar.Toolbar', {
 *         renderTo: document.body,
 *         width   : 700,
 *         margin  : '5 0 0 0',
 *         items   : [
 *             {
 *                 text   : 'Add a button',
 *                 scope  : this,
 *                 handler: function() {
 *                     var text = prompt('Please enter the text for your button:');
 *                     addedItems.push(toolbar.add({
 *                         text: text
 *                     }));
 *                 }
 *             },
 *             {
 *                 text   : 'Add a text item',
 *                 scope  : this,
 *                 handler: function() {
 *                     var text = prompt('Please enter the text for your item:');
 *                     addedItems.push(toolbar.add(text));
 *                 }
 *             },
 *             {
 *                 text   : 'Add a toolbar separator',
 *                 scope  : this,
 *                 handler: function() {
 *                     addedItems.push(toolbar.add('-'));
 *                 }
 *             },
 *             {
 *                 text   : 'Add a toolbar spacer',
 *                 scope  : this,
 *                 handler: function() {
 *                     addedItems.push(toolbar.add('->'));
 *                 }
 *             },
 *             '->',
 *             {
 *                 text   : 'Remove last inserted item',
 *                 scope  : this,
 *                 handler: function() {
 *                     if (addedItems.length) {
 *                         toolbar.remove(addedItems.pop());
 *                     } else if (toolbar.items.length) {
 *                         toolbar.remove(toolbar.items.last());
 *                     } else {
 *                         alert('No items in the toolbar');
 *                     }
 *                 }
 *             },
 *             {
 *                 text   : 'Remove all items',
 *                 scope  : this,
 *                 handler: function() {
 *                     toolbar.removeAll();
 *                 }
 *             }
 *         ]
 *     });
 *
 * @constructor
 * Creates a new Toolbar
 * @param {Object/Object[]} config A config object or an array of buttons to {@link #method-add}
 * eslint-enable max-len
 */
Ext.define('Ext.toolbar.Toolbar', {
    extend: 'Ext.container.Container',
    requires: [
        'Ext.layout.container.HBox',
        'Ext.layout.container.VBox'
    ],
    uses: [
        'Ext.toolbar.Fill',
        'Ext.toolbar.Separator',
        'Ext.toolbar.Spacer',
        'Ext.plugin.MouseEnter'
    ],
    alias: 'widget.toolbar',
    alternateClassName: 'Ext.Toolbar',
    /**
     * @property {Boolean} isToolbar
     * `true` in this class to identify an object as an instantiated Toolbar, or subclass thereof.
     */
    isToolbar: true,
    baseCls: Ext.baseCSSPrefix + 'toolbar',
    ariaRole: 'toolbar',
    focusableContainer: true,
    defaultType: 'button',
    /**
     * @cfg {Ext.enums.Layout/Object} layout
     * This class assigns a default layout (`layout: 'hbox'` or `layout: 'vbox'` depending upon orientation).
     *
     * Developers _may_ override this configuration option if another layout is required.
     * See {@link Ext.container.Container#layout} for additional information.
     */
    layout: undefined,
    /**
     * @cfg {Boolean} [vertical=false]
     * Set to `true` to make the toolbar vertical. The layout will become a `vbox`.
     */
    vertical: undefined,
    // @cmd-auto-dependency { directRef: 'Ext.layout.container.boxOverflow.Menu' }
    /**
     * @cfg {Boolean} enableOverflow
     * Configure true to make the toolbar provide a button which activates a dropdown Menu to show
     * items which overflow the Toolbar's width.  Setting this too true is the equivalent
     * of setting `{@link #overflowHandler}:'menu'`.
     */
    enableOverflow: false,
    // @cmd-auto-dependency { aliasPrefix: 'box.overflow.' }
    /**
     * @cfg {String} overflowHandler
     *
     * - `null` - hidden overflow
     * - `'scroller'` to render left/right scroller buttons on either side of the breadcrumb
     * - `'menu'` to render the overflowing buttons as items of an overflow menu.
     */
    overflowHandler: null,
    /**
     * @cfg {String} defaultButtonUI
     * A default {@link Ext.Component#ui ui} to use for {@link Ext.button.Button Button} items. This is a quick and simple
     * way to change the look of all child {@link Ext.button.Button Buttons}.
     *
     * If there is no value for defaultButtonUI, the button's {@link Ext.Component#ui ui} value will get `-toolbar`
     * appended so the {@link Ext.button.Button Button} has a different look when it's a child of a {@link Ext.toolbar.Toolbar Toolbar}.
     * To prevent this and have the same look as buttons outside of a toolbar, you can provide a string value to the defaultButtonUI:
     *
     *     Ext.create('Ext.panel.Panel', {
     *         renderTo    : document.body,
     *         width       : 300,
     *         title       : 'Panel',
     *         html        : 'Some Body',
     *         dockedItems : [
     *             {
     *                 xtype           : 'toolbar',
     *                 dock            : 'top',
     *                 defaultButtonUI : 'default',
     *                 items           : [
     *                     {
     *                         text : 'Save'
     *                     },
     *                     {
     *                         text : 'Remove'
     *                     }
     *                 ]
     *             }
     *         ]
     *     });
     */
    defaultButtonUI: 'default-toolbar',
    /**
     * @cfg {String}
     * Default UI for form field items.
     */
    defaultFieldUI: 'default',
    /**
     * @cfg {String}
     * Default UI for Buttons if the toolbar has a UI of 'footer'
     */
    defaultFooterButtonUI: 'default',
    /**
     * @cfg {String}
     * Default UI for Form Fields if the toolbar has a UI of 'footer'
     */
    defaultFooterFieldUI: 'default',
    /**
     * @cfg {Boolean} [trackMenus=true]
     * If `true`, when a toolbar button has a menu open, then mousing over other
     * toolbar buttons opens their menus.
     */
    trackMenus: true,
    itemCls: Ext.baseCSSPrefix + 'toolbar-item',
    /**
     * @event overflowchange
     * Fires after the overflow state has changed if this toolbar has been configured with
     * an `{@link #overflowHandler}`.
     * @param {Number} lastHiddenCount The number of overflowing items that used to be hidden.
     * @param {Number} hiddenCount The number of overflowing items that are hidden now.
     * @param {Array} hiddenItems The hidden items
     */
    statics: {
        shortcuts: {
            '-': 'tbseparator',
            ' ': 'tbspacer'
        },
        shortcutsHV: {
            // horizontal
            0: {
                '->': {
                    xtype: 'tbfill',
                    height: 0
                }
            },
            // vertical
            1: {
                '->': {
                    xtype: 'tbfill',
                    width: 0
                }
            }
        }
    },
    initComponent: function() {
        var me = this,
            layout = me.layout,
            vertical = me.vertical;
        if (vertical === undefined) {
            me.vertical = vertical = me.dock === 'right' || me.dock === 'left';
        }
        me.layout = layout = Ext.applyIf(Ext.isString(layout) ? {
            type: layout
        } : layout || {}, {
            type: vertical ? 'vbox' : 'hbox',
            align: vertical ? 'stretchmax' : 'middle'
        });
        if (me.overflowHandler) {
            layout.overflowHandler = me.overflowHandler;
        } else if (me.enableOverflow) {
            layout.overflowHandler = 'menu';
        }
        if (vertical) {
            me.addClsWithUI('vertical');
        }
        // @TODO: remove this hack and implement a more general solution
        if (me.ui === 'footer') {
            me.ignoreBorderManagement = true;
        }
        me.callParent();
    },
    afterFirstLayout: function(width, height) {
        var me = this,
            el = me.layout.getRenderTarget().dom;
        me.callParent([
            width,
            height
        ]);
        if (me.trackMenus) {
            me.addPlugin({
                ptype: 'mouseenter',
                element: el,
                delegate: function(e) {
                    return e.parentNode === el;
                },
                handler: me.onItemOver,
                scope: me
            });
        }
    },
    getRefItems: function(deep) {
        var me = this,
            items = me.callParent(arguments),
            layout = me.layout,
            handler;
        if (deep && (me.enableOverflow || (me.overflowHandler === 'menu'))) {
            handler = layout.overflowHandler;
            if (handler && handler.menu) {
                items = items.concat(handler.menu.getRefItems(deep));
            }
        }
        return items;
    },
    /**
     * Adds element(s) to the toolbar -- this function takes a variable number of
     * arguments of mixed type and adds them to the toolbar.
     *
     * **Note**: See the notes within {@link Ext.container.Container#method-add}.
     *
     * @param {Ext.Component.../Object.../String.../HTMLElement...} args The following types
     * of arguments are all valid:
     *
     *  - `{@link Ext.button.Button config}`: A valid button config object
     *  - `HTMLElement`: Any standard HTML element
     *  - `Field`: Any form field
     *  - `Item`: Any subclass of {@link Ext.toolbar.Item}
     *  - `String`: Any generic string (gets wrapped in a {@link Ext.toolbar.TextItem}).
     *
     *    Note that there are a few special strings that are treated differently as explained next:
     *
     *      - `'-'`: Creates a separator element
     *      - `' '`: Creates a spacer element
     *      - `'->'`: Creates a fill element
     *
     * @return {Ext.Component[]/Ext.Component} The Components that were added.
     *
     * @method add
     */
    /**
     * Inserts a Component into this Container at a specified index.
     *
     * @param {Number} index The index at which the Component will be inserted.
     * @param {Ext.Component/Object/String/HTMLElement} component
     * See {@link #method-add} method for overview of possible values.
     * @return {Ext.Component} The component that was inserted.
     * @method insert
     */
    /**
     * @private
     */
    lookupComponent: function(c) {
        var args = arguments,
            shortcut, T;
        if (typeof c === 'string' && c[0] !== '@') {
            T = Ext.toolbar.Toolbar;
            shortcut = T.shortcutsHV[this.vertical ? 1 : 0][c] || T.shortcuts[c];
            if (typeof shortcut === 'string') {
                c = {
                    xtype: shortcut
                };
            } else if (shortcut) {
                c = Ext.apply({}, shortcut);
            } else {
                c = {
                    xtype: 'tbtext',
                    text: c
                };
            }
            this.applyDefaults(c);
            // See: EXTJSIV-7578
            args = [
                c
            ];
        }
        return this.callParent(args);
    },
    onBeforeAdd: function(component) {
        var me = this,
            isFooter = me.ui === 'footer',
            defaultButtonUI = isFooter ? me.defaultFooterButtonUI : me.defaultButtonUI;
        if (component.isSegmentedButton) {
            if (component.getDefaultUI() === 'default' && !component.config.hasOwnProperty('defaultUI')) {
                component.setDefaultUI(defaultButtonUI);
            }
        } else if (component.ui === 'default' && !component.hasOwnProperty('ui')) {
            if (component.isButton) {
                component.ui = defaultButtonUI;
            } else if (component.isFormField) {
                component.ui = isFooter ? me.defaultFooterFieldUI : me.defaultFieldUI;
            }
        }
        // Any separators needs to know if is vertical or not
        if (component instanceof Ext.toolbar.Separator) {
            component.setUI(me.vertical ? 'vertical' : 'horizontal');
        }
        me.callParent(arguments);
    },
    onAdd: function(component) {
        var me = this;
        // If we encounter a child component that needs to handle arrow keys
        // (input fields, sliders) we opt out of FocusableContainer behavior
        // because it becomes highly confusing for the keyboard users. We also
        // change the ARIA role because toolbars are expected to behave the way
        // WAI-ARIA spec defines them, i.e. navigable with arrow keys.
        // A widget that is announced as a toolbar but is *not* navigable
        // with arrow keys is highly confusing to disabled users relying on
        // hearing.
        // Newer edition of WAI-ARIA allows for arrow-processing elements in
        // Toolbars, so let the users force the flag if they wish so:
        // https://www.w3.org/TR/wai-aria-practices/#toolbar
        if (component.needArrowKeys && me.focusableContainer && !me.hasOwnProperty('focusableContainer')) {
            me.focusableContainer = false;
            me.ariaRole = 'group';
        }
        me.callParent(arguments);
        me.trackMenu(component);
    },
    onRemove: function(c) {
        this.callParent(arguments);
        this.trackMenu(c, true);
    },
    privates: {
        /**
         * @private
         */
        applyDefaults: function(c) {
            if (!Ext.isString(c)) {
                c = this.callParent(arguments);
            }
            return c;
        },
        /**
         * @private
         */
        trackMenu: function(item, remove) {
            var me = this;
            if (item.menu) {
                item[remove ? 'un' : 'on']({
                    menushow: me.onButtonMenuShow,
                    menuhide: me.onButtonMenuHide,
                    scope: me
                });
            }
        },
        getChildItemsToDisable: function() {
            return this.items.getRange();
        },
        /**
         * @private
         */
        onItemOver: function(e, target) {
            var btn = Ext.Component.from(target),
                activeMenuBtn = this.activeMenuBtn;
            if (activeMenuBtn && activeMenuBtn !== btn && btn.showMenu && btn.menu) {
                btn.focus();
                btn.showMenu(e);
            }
        },
        /**
         * @private
         */
        onButtonMenuShow: function(btn) {
            this.activeMenuBtn = btn;
        },
        /**
         * @private
         */
        onButtonMenuHide: function(btn) {
            this.activeMenuBtn = null;
        }
    }
});

/*
 * This is a derivative of the similarly named class in the YUI Library.
 * The original license:
 * Copyright (c) 2006, Yahoo! Inc. All rights reserved.
 * Code licensed under the BSD License:
 * http://developer.yahoo.net/yui/license.txt
 */
/**
 * Defines the interface and base operation of items that that can be
 * dragged or can be drop targets.  It was designed to be extended, overriding
 * the event handlers for startDrag, onDrag, onDragOver and onDragOut.
 * Up to three html elements can be associated with a DragDrop instance:
 *
 * - linked element: the element that is passed into the constructor.
 *   This is the element which defines the boundaries for interaction with
 *   other DragDrop objects.
 *
 * - handle element(s): The drag operation only occurs if the element that
 *   was clicked matches a handle element.  By default this is the linked
 *   element, but there are times that you will want only a portion of the
 *   linked element to initiate the drag operation, and the setHandleElId()
 *   method provides a way to define this.
 *
 * - drag element: this represents the element that would be moved along
 *   with the cursor during a drag operation.  By default, this is the linked
 *   element itself as in {@link Ext.dd.DD}.  setDragElId() lets you define
 *   a separate element that would be moved, as in {@link Ext.dd.DDProxy}.
 *
 * This class should not be instantiated until the onload event to ensure that
 * the associated elements are available.
 * The following would define a DragDrop obj that would interact with any
 * other DragDrop obj in the "group1" group:
 *
 *     dd = new Ext.dd.DragDrop("div1", "group1");
 *
 * Since none of the event handlers have been implemented, nothing would
 * actually happen if you were to run the code above.  Normally you would
 * override this class or one of the default implementations, but you can
 * also override the methods you want on an instance of the class...
 *
 *     dd.onDragDrop = function(e, id) {
 *         alert("dd was dropped on " + id);
 *     }
 *
 */
Ext.define('Ext.dd.DragDrop', {
    requires: [
        'Ext.dd.DragDropManager'
    ],
    /**
     * Creates new DragDrop.
     * @param {String} id of the element that is linked to this instance
     * @param {String} sGroup the group of related DragDrop objects
     * @param {Object} config an object containing configurable attributes.
     * Valid properties for DragDrop:
     *
     * - padding
     * - isTarget
     * - maintainOffset
     * - primaryButtonOnly
     */
    constructor: function(id, sGroup, config) {
        if (id) {
            this.init(id, sGroup, config);
        }
    },
    /**
     * @property {Boolean} ignoreSelf
     * Set to false to enable a DragDrop object to fire drag events while dragging
     * over its own Element. Defaults to true - DragDrop objects do not by default
     * fire drag events to themselves.
     */
    /**
     * @property {String} id
     * The id of the element associated with this object.  This is what we
     * refer to as the "linked element" because the size and position of
     * this element is used to determine when the drag and drop objects have
     * interacted.
     */
    id: null,
    /**
     * @property {Object} config
     * Configuration attributes passed into the constructor
     */
    config: null,
    /**
     * @property {String} dragElId
     * The id of the element that will be dragged.  By default this is same
     * as the linked element, but could be changed to another element. Ex:
     * Ext.dd.DDProxy
     * @private
     */
    dragElId: null,
    /**
     * @property {String} handleElId
     * The ID of the element that initiates the drag operation.  By default
     * this is the linked element, but could be changed to be a child of this
     * element.  This lets us do things like only starting the drag when the
     * header element within the linked html element is clicked.
     * @private
     */
    handleElId: null,
    /**
     * @property {Object} invalidHandleTypes
     * An object who's property names identify HTML tags to be considered invalid as drag handles.
     * A non-null property value identifies the tag as invalid. Defaults to the
     * following value which prevents drag operations from being initiated by `<a>` elements:
     *
     *     {
     *         A: "A"
     *     }
     */
    invalidHandleTypes: null,
    /**
     * @property {Object} invalidHandleIds
     * An object who's property names identify the IDs of elements to be considered invalid
     * as drag handles. A non-null property value identifies the ID as invalid. For example,
     * to prevent dragging from being initiated on element ID "foo", use:
     *
     *     {
     *         foo: true
     *     }
     */
    invalidHandleIds: null,
    /**
     * @property {String[]} invalidHandleClasses
     * An Array of CSS class names for elements to be considered in valid as drag handles.
     */
    invalidHandleClasses: null,
    /**
     * @property {Number} startPageX
     * The linked element's absolute X position at the time the drag was
     * started
     * @private
     */
    startPageX: 0,
    /**
     * @property {Number} startPageY
     * The linked element's absolute X position at the time the drag was
     * started
     * @private
     */
    startPageY: 0,
    /**
     * @property {Object} groups
     * The group defines a logical collection of DragDrop objects that are
     * related.  Instances only get events when interacting with other
     * DragDrop object in the same group.  This lets us define multiple
     * groups using a single DragDrop subclass if we want.
     *
     * An object in the format {'group1':true, 'group2':true}
     */
    groups: null,
    /**
     * @property {Boolean} locked
     * Individual drag/drop instances can be locked.  This will prevent
     * onmousedown start drag.
     * @private
     */
    locked: false,
    /**
     * Locks this instance
     */
    lock: function() {
        this.locked = true;
    },
    /**
     * @property {Boolean} moveOnly
     * When set to true, other DD objects in cooperating DDGroups do not receive
     * notification events when this DD object is dragged over them.
     */
    moveOnly: false,
    /**
     * Unlocks this instace
     */
    unlock: function() {
        this.locked = false;
    },
    /**
     * @property {Boolean} isTarget
     * By default, all instances can be a drop target.  This can be disabled by
     * setting isTarget to false.
     */
    isTarget: true,
    /**
     * @property {Number[]} padding
     * The padding configured for this drag and drop object for calculating
     * the drop zone intersection with this object.
     * An array containing the 4 padding values: [top, right, bottom, left]
     */
    padding: null,
    /**
     * @property _domRef
     * Cached reference to the linked element
     * @private
     */
    _domRef: null,
    /**
     * @property __ygDragDrop
     * Internal typeof flag
     * @private
     */
    __ygDragDrop: true,
    /**
     * @property {Boolean} constrainX
     * Set to true when horizontal contraints are applied
     * @private
     */
    constrainX: false,
    /**
     * @property {Boolean} constrainY
     * Set to true when vertical contraints are applied
     * @private
     */
    constrainY: false,
    /**
     * @property {Number} minX
     * The left constraint
     * @private
     */
    minX: 0,
    /**
     * @property {Number} maxX
     * The right constraint
     * @private
     */
    maxX: 0,
    /**
     * @property {Number} minY
     * The up constraint
     * @private
     */
    minY: 0,
    /**
     * @property {Number} maxY
     * The down constraint
     * @private
     */
    maxY: 0,
    /**
     * @property {Boolean} maintainOffset
     * Maintain offsets when we resetconstraints.  Set to true when you want
     * the position of the element relative to its parent to stay the same
     * when the page changes
     */
    maintainOffset: false,
    /**
     * @property {Number[]} xTicks
     * Array of pixel locations the element will snap to if we specified a
     * horizontal graduation/interval.  This array is generated automatically
     * when you define a tick interval.
     */
    xTicks: null,
    /**
     * @property {Number[]} yTicks
     * Array of pixel locations the element will snap to if we specified a
     * vertical graduation/interval.  This array is generated automatically
     * when you define a tick interval.
     */
    yTicks: null,
    /**
     * @property {Boolean} primaryButtonOnly
     * By default the drag and drop instance will only respond to the primary
     * button click (left button for a right-handed mouse).  Set to true to
     * allow drag and drop to start with any mouse click that is propogated
     * by the browser
     */
    primaryButtonOnly: true,
    /**
     * @property {Boolean} available
     * The available property is false until the linked dom element is accessible.
     */
    available: false,
    /**
     * @property {Boolean} hasOuterHandles
     * By default, drags can only be initiated if the mousedown occurs in the
     * region the linked element is.  This is done in part to work around a
     * bug in some browsers that mis-report the mousedown if the previous
     * mouseup happened outside of the window.  This property is set to true
     * if outer handles are defined. Defaults to false.
     */
    hasOuterHandles: false,
    triggerEvent: 'mousedown',
    /**
     * Code that executes immediately before the startDrag event
     * @private
     */
    b4StartDrag: function(x, y) {},
    /**
     * Abstract method called after a drag/drop object is clicked
     * and the drag or mousedown time thresholds have beeen met.
     * @param {Number} x X click location
     * @param {Number} y Y click location
     */
    startDrag: function(x, y) {},
    /* override this */
    /**
     * Code that executes immediately before the onDrag event
     * @private
     */
    b4Drag: function(e) {},
    /**
     * Abstract method called during the onMouseMove event while dragging an
     * object.
     * @param {Event} e the mousemove event
     */
    onDrag: function(e) {},
    /* override this */
    /**
     * Abstract method called when this element fist begins hovering over
     * another DragDrop obj
     * @param {Event} e the mousemove event
     * @param {String/Ext.dd.DragDrop[]} id In POINT mode, the element
     * id this is hovering over.  In INTERSECT mode, an array of one or more
     * dragdrop items being hovered over.
     */
    onDragEnter: function(e, id) {},
    /* override this */
    /**
     * Code that executes immediately before the onDragOver event
     * @private
     */
    b4DragOver: function(e) {},
    /**
     * Abstract method called when this element is hovering over another
     * DragDrop obj
     * @param {Event} e the mousemove event
     * @param {String/Ext.dd.DragDrop[]} id In POINT mode, the element
     * id this is hovering over.  In INTERSECT mode, an array of dd items
     * being hovered over.
     */
    onDragOver: function(e, id) {},
    /* override this */
    /**
     * Code that executes immediately before the onDragOut event
     * @private
     */
    b4DragOut: function(e) {},
    /**
     * Abstract method called when we are no longer hovering over an element
     * @param {Event} e the mousemove event
     * @param {String/Ext.dd.DragDrop[]} id In POINT mode, the element
     * id this was hovering over.  In INTERSECT mode, an array of dd items
     * that the mouse is no longer over.
     */
    onDragOut: function(e, id) {},
    /* override this */
    /**
     * Code that executes immediately before the onDragDrop event
     * @private
     */
    b4DragDrop: function(e) {},
    /**
     * Abstract method called when this item is dropped on another DragDrop
     * obj
     * @param {Event} e the mouseup event
     * @param {String/Ext.dd.DragDrop[]} id In POINT mode, the element
     * id this was dropped on.  In INTERSECT mode, an array of dd items this
     * was dropped on.
     */
    onDragDrop: function(e, id) {},
    /* override this */
    /**
     * Abstract method called when this item is dropped on an area with no
     * drop target
     * @param {Event} e the mouseup event
     */
    onInvalidDrop: function(e) {},
    /* override this */
    /**
     * Code that executes immediately before the endDrag event
     * @private
     */
    b4EndDrag: function(e) {},
    /**
     * Called when we are done dragging the object
     * @param {Event} e the mouseup event
     */
    endDrag: function(e) {},
    /* override this */
    /**
     * Code executed immediately before the onMouseDown event
     * @param {Event} e the mousedown event
     * @private
     */
    b4MouseDown: function(e) {},
    /**
     * Called when a drag/drop obj gets a mousedown
     * @param {Event} e the mousedown event
     */
    onMouseDown: function(e) {},
    /* override this */
    /**
     * Called when a drag/drop obj gets a mouseup
     * @param {Event} e the mouseup event
     */
    onMouseUp: function(e) {},
    /* override this */
    /**
     * Override the onAvailable method to do what is needed after the initial
     * position was determined.
     */
    onAvailable: function() {},
    /**
     * @property {Object} defaultPadding
     * Provides default constraint padding to "constrainTo" elements.
     */
    defaultPadding: {
        left: 0,
        right: 0,
        top: 0,
        bottom: 0
    },
    /**
     * Initializes the drag drop object's constraints to restrict movement to a certain element.
     *
     * Usage:
     *
     *     var dd = new Ext.dd.DDProxy("dragDiv1", "proxytest",
     *                    { dragElId: "existingProxyDiv" });
     *     dd.startDrag = function(){
     *         this.constrainTo("parent-id");
     *     };
     *
     * Or you can initalize it using the {@link Ext.dom.Element} object:
     *
     *     Ext.get("dragDiv1").initDDProxy("proxytest", {dragElId: "existingProxyDiv"}, {
     *         startDrag : function(){
     *             this.constrainTo("parent-id");
     *         }
     *     });
     *
     * @param {String/HTMLElement/Ext.dom.Element} constrainTo The element or element ID
     * to constrain to.
     * @param {Object/Number} pad (optional) Pad provides a way to specify "padding"
     * of the constraints, and can be either a number for symmetrical padding (4 would be equal to
     * `{ left: 4, right: 4, top: 4, bottom: 4 }`) or an object containing the sides to pad.
     * For example: `{ right: 10, bottom: 10 }`
     * @param {Boolean} inContent (optional) Constrain the draggable in the content box
     * of the element (inside padding and borders)
     */
    constrainTo: function(constrainTo, pad, inContent) {
        if (Ext.isNumber(pad)) {
            pad = {
                left: pad,
                right: pad,
                top: pad,
                bottom: pad
            };
        }
        pad = pad || this.defaultPadding;
        // eslint-disable-next-line vars-on-top
        var ddBox = Ext.get(this.getEl()).getBox(),
            constrainEl = Ext.get(constrainTo),
            s = constrainEl.getScroll(),
            constrainDom = constrainEl.dom,
            c, xy, topSpace, leftSpace;
        if (constrainDom === document.body) {
            c = {
                x: s.left,
                y: s.top,
                width: Ext.Element.getViewportWidth(),
                height: Ext.Element.getViewportHeight()
            };
        } else {
            xy = constrainEl.getXY();
            c = {
                x: xy[0],
                y: xy[1],
                width: constrainDom.clientWidth,
                height: constrainDom.clientHeight
            };
        }
        topSpace = ddBox.y - c.y;
        leftSpace = ddBox.x - c.x;
        this.resetConstraints();
        this.setXConstraint(leftSpace - (pad.left || 0), // left
        c.width - leftSpace - ddBox.width - (pad.right || 0), // right
        this.xTickSize);
        this.setYConstraint(topSpace - (pad.top || 0), // top
        c.height - topSpace - ddBox.height - (pad.bottom || 0), // bottom
        this.yTickSize);
    },
    /**
     * Returns a reference to the linked element
     * @return {HTMLElement} the html element
     */
    getEl: function() {
        if (!this._domRef) {
            this._domRef = Ext.getDom(this.id);
        }
        return this._domRef;
    },
    /**
     * Returns a reference to the actual element to drag.  By default this is
     * the same as the html element, but it can be assigned to another
     * element. An example of this can be found in Ext.dd.DDProxy
     * @return {HTMLElement} the html element
     */
    getDragEl: function() {
        return Ext.getDom(this.dragElId);
    },
    /**
     * Sets up the DragDrop object.  Must be called in the constructor of any
     * Ext.dd.DragDrop subclass
     * @param {String} id the id of the linked element
     * @param {String} sGroup the group of related items
     * @param {Object} config configuration attributes
     */
    init: function(id, sGroup, config) {
        var me = this;
        me.el = me.el || Ext.get(id);
        // subclass may have already set "el"
        me.initTarget(id, sGroup, config);
        Ext.get(me.id).on(me.triggerEvent, me.handleMouseDown, me);
        // Longpress fires contextmenu in some touch platforms, so if we are using longpress
        // inhibit the contextmenu on this element
        if (Ext.supports.Touch && me.triggerEvent === 'longpress') {
            Ext.get(me.id).swallowEvent('contextmenu', true);
        }
    },
    /**
     * Initializes Targeting functionality only... the object does not
     * get a mousedown handler.
     * @param {String} id the id of the linked element
     * @param {String} sGroup the group of related items
     * @param {Object} config configuration attributes
     */
    initTarget: function(id, sGroup, config) {
        // configuration attributes
        this.config = config || {};
        // create a local reference to the drag and drop manager
        this.DDMInstance = Ext.dd.DragDropManager;
        // initialize the groups array
        this.groups = {};
        // assume that we have an element reference instead of an id if the
        // parameter is not a string
        if (typeof id !== "string") {
            id = Ext.id(id);
        }
        // set the id
        this.id = id;
        // add to an interaction group
        this.addToGroup((sGroup) ? sGroup : "default");
        // We don't want to register this as the handle with the manager
        // so we just set the id rather than calling the setter.
        this.handleElId = id;
        // the linked element is the element that gets dragged by default
        this.setDragElId(id);
        // by default, clicked anchors will not start drag operations.
        this.invalidHandleTypes = {
            A: "A"
        };
        this.invalidHandleIds = {};
        this.invalidHandleClasses = [];
        this.applyConfig();
        this.handleOnAvailable();
    },
    /**
     * Applies the configuration parameters that were passed into the constructor.
     * This is supposed to happen at each level through the inheritance chain.  So
     * a DDProxy implentation will execute apply config on DDProxy, DD, and
     * DragDrop in order to get all of the parameters that are available in
     * each object.
     */
    applyConfig: function() {
        // configurable properties:
        //    padding, isTarget, maintainOffset, primaryButtonOnly
        this.padding = this.config.padding || [
            0,
            0,
            0,
            0
        ];
        this.isTarget = (this.config.isTarget !== false);
        this.maintainOffset = (this.config.maintainOffset);
        this.primaryButtonOnly = (this.config.primaryButtonOnly !== false);
    },
    /**
     * Executed when the linked element is available
     * @private
     */
    handleOnAvailable: function() {
        this.available = true;
        this.resetConstraints();
        this.onAvailable();
    },
    /**
     * Configures the padding for the target zone in px.  Effectively expands
     * (or reduces) the virtual object size for targeting calculations.
     * Supports css-style shorthand; if only one parameter is passed, all sides
     * will have that padding, and if only two are passed, the top and bottom
     * will have the first param, the left and right the second.
     * @param {Number} iTop    Top pad
     * @param {Number} iRight  Right pad
     * @param {Number} iBot    Bot pad
     * @param {Number} iLeft   Left pad
     */
    setPadding: function(iTop, iRight, iBot, iLeft) {
        // this.padding = [iLeft, iRight, iTop, iBot];
        if (!iRight && 0 !== iRight) {
            this.padding = [
                iTop,
                iTop,
                iTop,
                iTop
            ];
        } else if (!iBot && 0 !== iBot) {
            this.padding = [
                iTop,
                iRight,
                iTop,
                iRight
            ];
        } else {
            this.padding = [
                iTop,
                iRight,
                iBot,
                iLeft
            ];
        }
    },
    /**
     * Stores the initial placement of the linked element.
     * @param {Number} diffX   the X offset, default 0
     * @param {Number} diffY   the Y offset, default 0
     */
    setInitPosition: function(diffX, diffY) {
        var el = this.getEl(),
            dx, dy, p;
        if (!this.DDMInstance.verifyEl(el)) {
            return;
        }
        dx = diffX || 0;
        dy = diffY || 0;
        p = Ext.fly(el).getXY();
        this.initPageX = p[0] - dx;
        this.initPageY = p[1] - dy;
        this.lastPageX = p[0];
        this.lastPageY = p[1];
        this.setStartPosition(p);
    },
    /**
     * Sets the start position of the element.  This is set when the obj
     * is initialized, the reset when a drag is started.
     * @param pos current position (from previous lookup)
     * @private
     */
    setStartPosition: function(pos) {
        var p = pos || Ext.fly(this.getEl()).getXY();
        this.deltaSetXY = null;
        this.startPageX = p[0];
        this.startPageY = p[1];
    },
    /**
     * Adds this instance to a group of related drag/drop objects.  All
     * instances belong to at least one group, and can belong to as many
     * groups as needed.
     * @param {String} sGroup the name of the group
     */
    addToGroup: function(sGroup) {
        this.groups[sGroup] = true;
        this.DDMInstance.regDragDrop(this, sGroup);
    },
    /**
     * Removes this instance from the supplied interaction group
     * @param {String} sGroup  The group to drop
     */
    removeFromGroup: function(sGroup) {
        if (this.groups[sGroup]) {
            delete this.groups[sGroup];
        }
        this.DDMInstance.removeDDFromGroup(this, sGroup);
    },
    /**
     * Allows you to specify that an element other than the linked element
     * will be moved with the cursor during a drag
     * @param {String} id the id of the element that will be used to initiate the drag
     */
    setDragElId: function(id) {
        this.dragElId = id;
    },
    /**
     * Allows you to specify a child of the linked element that should be
     * used to initiate the drag operation.  An example of this would be if
     * you have a content div with text and links.  Clicking anywhere in the
     * content area would normally start the drag operation.  Use this method
     * to specify that an element inside of the content div is the element
     * that starts the drag operation.
     * @param {String} id the id of the element that will be used to
     * initiate the drag.
     */
    setHandleElId: function(id) {
        if (typeof id !== "string") {
            id = Ext.id(id);
        }
        this.handleElId = id;
        this.DDMInstance.regHandle(this.id, id);
    },
    /**
     * Allows you to set an element outside of the linked element as a drag
     * handle
     * @param {String} id the id of the element that will be used to initiate the drag
     */
    setOuterHandleElId: function(id) {
        if (typeof id !== "string") {
            id = Ext.id(id);
        }
        Ext.get(id).on(this.triggerEvent, this.handleMouseDown, this);
        this.setHandleElId(id);
        this.hasOuterHandles = true;
    },
    /**
     * Removes all drag and drop hooks for this element
     */
    unreg: function() {
        var me = this,
            el;
        if (me._domRef) {
            el = Ext.fly(me.id);
            if (el) {
                el.un(me.triggerEvent, me.handleMouseDown, me);
            }
        }
        me._domRef = null;
        me.DDMInstance._remove(me, me.autoGroup);
    },
    destroy: function() {
        this.unreg();
        this.callParent();
    },
    /**
     * Returns true if this instance is locked, or the drag drop mgr is locked
     * (meaning that all drag/drop is disabled on the page.)
     * @return {Boolean} true if this obj or all drag/drop is locked, else
     * false
     */
    isLocked: function() {
        return (this.DDMInstance.isLocked() || this.locked);
    },
    /**
     * Called when this object is clicked
     * @param {Event} e
     * @param {Ext.dd.DragDrop} oDD the clicked dd object (this dd obj)
     * @private
     */
    handleMouseDown: function(e, oDD) {
        var me = this;
        if ((me.primaryButtonOnly && e.button) || me.isLocked()) {
            return;
        }
        me.DDMInstance.refreshCache(me.groups);
        if (me.hasOuterHandles || me.DDMInstance.isOverTarget(e.getPoint(), me)) {
            if (me.clickValidator(e)) {
                // set the initial element position
                me.setStartPosition();
                me.b4MouseDown(e);
                me.onMouseDown(e);
                me.DDMInstance.handleMouseDown(e, me);
                me.DDMInstance.stopEvent(e);
            }
        }
    },
    clickValidator: function(e) {
        var target = e.getTarget();
        return (this.isValidHandleChild(target) && (this.id === this.handleElId || this.DDMInstance.handleWasClicked(target, this.id)));
    },
    /**
     * Allows you to specify a tag name that should not start a drag operation
     * when clicked.  This is designed to facilitate embedding links within a
     * drag handle that do something other than start the drag.
     * @method addInvalidHandleType
     * @param {String} tagName the type of element to exclude
     */
    addInvalidHandleType: function(tagName) {
        var type = tagName.toUpperCase();
        this.invalidHandleTypes[type] = type;
    },
    /**
     * Lets you to specify an element id for a child of a drag handle
     * that should not initiate a drag
     * @method addInvalidHandleId
     * @param {String} id the element id of the element you wish to ignore
     */
    addInvalidHandleId: function(id) {
        if (typeof id !== "string") {
            id = Ext.id(id);
        }
        this.invalidHandleIds[id] = id;
    },
    /**
     * Lets you specify a css class of elements that will not initiate a drag
     * @param {String} cssClass the class of the elements you wish to ignore
     */
    addInvalidHandleClass: function(cssClass) {
        this.invalidHandleClasses.push(cssClass);
    },
    /**
     * Unsets an excluded tag name set by addInvalidHandleType
     * @param {String} tagName the type of element to unexclude
     */
    removeInvalidHandleType: function(tagName) {
        var type = tagName.toUpperCase();
        // this.invalidHandleTypes[type] = null;
        delete this.invalidHandleTypes[type];
    },
    /**
     * Unsets an invalid handle id
     * @param {String} id the id of the element to re-enable
     */
    removeInvalidHandleId: function(id) {
        if (typeof id !== "string") {
            id = Ext.id(id);
        }
        delete this.invalidHandleIds[id];
    },
    /**
     * Unsets an invalid css class
     * @param {String} cssClass the class of the element(s) you wish to
     * re-enable
     */
    removeInvalidHandleClass: function(cssClass) {
        var invalidHandleClasses = this.invalidHandleClasses,
            len = invalidHandleClasses.length,
            i;
        for (i = 0; i < len; ++i) {
            if (invalidHandleClasses[i] === cssClass) {
                delete invalidHandleClasses[i];
            }
        }
    },
    /**
     * Checks the tag exclusion list to see if this click should be ignored
     * @param {HTMLElement} node the HTMLElement to evaluate
     * @return {Boolean} true if this is a valid tag type, false if not
     */
    isValidHandleChild: function(node) {
        var valid = true,
            nodeName, i, len;
        // var n = (node.nodeName == "#text") ? node.parentNode : node;
        try {
            nodeName = node.nodeName.toUpperCase();
        } catch (e) {
            nodeName = node.nodeName;
        }
        valid = valid && !this.invalidHandleTypes[nodeName];
        valid = valid && !this.invalidHandleIds[node.id];
        for (i = 0 , len = this.invalidHandleClasses.length; valid && i < len; ++i) {
            valid = !Ext.fly(node).hasCls(this.invalidHandleClasses[i]);
        }
        return valid;
    },
    /**
     * Creates the array of horizontal tick marks if an interval was specified
     * in setXConstraint().
     * @private
     */
    setXTicks: function(iStartX, iTickSize) {
        var tickMap = {},
            i;
        this.xTicks = [];
        this.xTickSize = iTickSize;
        for (i = this.initPageX; i >= this.minX; i = i - iTickSize) {
            if (!tickMap[i]) {
                this.xTicks[this.xTicks.length] = i;
                tickMap[i] = true;
            }
        }
        for (i = this.initPageX; i <= this.maxX; i = i + iTickSize) {
            if (!tickMap[i]) {
                this.xTicks[this.xTicks.length] = i;
                tickMap[i] = true;
            }
        }
        Ext.Array.sort(this.xTicks, this.DDMInstance.numericSort);
    },
    /**
     * Creates the array of vertical tick marks if an interval was specified in
     * setYConstraint().
     * @private
     */
    setYTicks: function(iStartY, iTickSize) {
        var tickMap = {},
            i;
        this.yTicks = [];
        this.yTickSize = iTickSize;
        for (i = this.initPageY; i >= this.minY; i = i - iTickSize) {
            if (!tickMap[i]) {
                this.yTicks[this.yTicks.length] = i;
                tickMap[i] = true;
            }
        }
        for (i = this.initPageY; i <= this.maxY; i = i + iTickSize) {
            if (!tickMap[i]) {
                this.yTicks[this.yTicks.length] = i;
                tickMap[i] = true;
            }
        }
        Ext.Array.sort(this.yTicks, this.DDMInstance.numericSort);
    },
    /**
     * By default, the element can be dragged any place on the screen.  Use
     * this method to limit the horizontal travel of the element.  Pass in
     * 0,0 for the parameters if you want to lock the drag to the y axis.
     * @param {Number} iLeft the number of pixels the element can move to the left
     * @param {Number} iRight the number of pixels the element can move to the
     * right
     * @param {Number} iTickSize (optional) parameter for specifying that the
     * element should move iTickSize pixels at a time.
     */
    setXConstraint: function(iLeft, iRight, iTickSize) {
        this.leftConstraint = iLeft;
        this.rightConstraint = iRight;
        this.minX = this.initPageX - iLeft;
        this.maxX = this.initPageX + iRight;
        if (iTickSize) {
            this.setXTicks(this.initPageX, iTickSize);
        }
        this.constrainX = true;
    },
    /**
     * Clears any constraints applied to this instance.  Also clears ticks
     * since they can't exist independent of a constraint at this time.
     */
    clearConstraints: function() {
        this.constrainX = false;
        this.constrainY = false;
        this.clearTicks();
    },
    /**
     * Clears any tick interval defined for this instance
     */
    clearTicks: function() {
        this.xTicks = null;
        this.yTicks = null;
        this.xTickSize = 0;
        this.yTickSize = 0;
    },
    /**
     * By default, the element can be dragged any place on the screen.  Set
     * this to limit the vertical travel of the element.  Pass in 0,0 for the
     * parameters if you want to lock the drag to the x axis.
     * @param {Number} iUp the number of pixels the element can move up
     * @param {Number} iDown the number of pixels the element can move down
     * @param {Number} iTickSize (optional) parameter for specifying that the
     * element should move iTickSize pixels at a time.
     */
    setYConstraint: function(iUp, iDown, iTickSize) {
        this.topConstraint = iUp;
        this.bottomConstraint = iDown;
        this.minY = this.initPageY - iUp;
        this.maxY = this.initPageY + iDown;
        if (iTickSize) {
            this.setYTicks(this.initPageY, iTickSize);
        }
        this.constrainY = true;
    },
    /**
     * Must be called if you manually reposition a dd element.
     * @param {Boolean} maintainOffset
     */
    resetConstraints: function() {
        var dx, dy;
        // Maintain offsets if necessary
        if (this.initPageX || this.initPageX === 0) {
            // figure out how much this thing has moved
            dx = (this.maintainOffset) ? this.lastPageX - this.initPageX : 0;
            dy = (this.maintainOffset) ? this.lastPageY - this.initPageY : 0;
            this.setInitPosition(dx, dy);
        } else // This is the first time we have detected the element's position
        {
            this.setInitPosition();
        }
        if (this.constrainX) {
            this.setXConstraint(this.leftConstraint, this.rightConstraint, this.xTickSize);
        }
        if (this.constrainY) {
            this.setYConstraint(this.topConstraint, this.bottomConstraint, this.yTickSize);
        }
    },
    /**
     * Normally the drag element is moved pixel by pixel, but we can specify
     * that it move a number of pixels at a time.  This method resolves the
     * location when we have it set up like this.
     * @param {Number} val where we want to place the object
     * @param {Number[]} tickArray sorted array of valid points
     * @return {Number} the closest tick
     * @private
     */
    getTick: function(val, tickArray) {
        var i, len, next, diff1, diff2;
        if (!tickArray) {
            // If tick interval is not defined, it is effectively 1 pixel,
            // so we return the value passed to us.
            return val;
        } else if (tickArray[0] >= val) {
            // The value is lower than the first tick, so we return the first
            // tick.
            return tickArray[0];
        } else {
            for (i = 0 , len = tickArray.length; i < len; ++i) {
                next = i + 1;
                if (tickArray[next] && tickArray[next] >= val) {
                    diff1 = val - tickArray[i];
                    diff2 = tickArray[next] - val;
                    return (diff2 > diff1) ? tickArray[i] : tickArray[next];
                }
            }
            // The value is larger than the last tick, so we return the last
            // tick.
            return tickArray[tickArray.length - 1];
        }
    },
    /**
     * toString method
     * @return {String} string representation of the dd obj
     */
    toString: function() {
        return ("DragDrop " + this.id);
    }
});

/*
 * This is a derivative of the similarly named class in the YUI Library.
 * The original license:
 * Copyright (c) 2006, Yahoo! Inc. All rights reserved.
 * Code licensed under the BSD License:
 * http://developer.yahoo.net/yui/license.txt
 */
/**
 * A DragDrop implementation where the linked element follows the
 * mouse cursor during a drag.
 */
Ext.define('Ext.dd.DD', {
    extend: 'Ext.dd.DragDrop',
    requires: [
        'Ext.dd.DragDropManager'
    ],
    /**
     * Creates new DD instance.
     * @param {String} id the id of the linked element
     * @param {String} sGroup the group of related DragDrop items
     * @param {Object} config an object containing configurable attributes.
     * Valid properties for DD: scroll
     */
    constructor: function(id, sGroup, config) {
        if (id) {
            this.init(id, sGroup, config);
        }
    },
    /**
     * @property {Boolean} scroll
     * When set to true, the utility automatically tries to scroll the browser
     * window when a drag and drop element is dragged near the viewport boundary.
     */
    scroll: true,
    /**
     * Sets the pointer offset to the distance between the linked element's top
     * left corner and the location the element was clicked.
     * @param {Number} iPageX the X coordinate of the click
     * @param {Number} iPageY the Y coordinate of the click
     */
    autoOffset: function(iPageX, iPageY) {
        var x = iPageX - this.startPageX,
            y = iPageY - this.startPageY;
        this.setDelta(x, y);
    },
    /**
     * Sets the pointer offset.  You can call this directly to force the
     * offset to be in a particular location (e.g., pass in 0,0 to set it
     * to the center of the object)
     * @param {Number} iDeltaX the distance from the left
     * @param {Number} iDeltaY the distance from the top
     */
    setDelta: function(iDeltaX, iDeltaY) {
        this.deltaX = iDeltaX;
        this.deltaY = iDeltaY;
    },
    /**
     * Sets the drag element to the location of the mousedown or click event,
     * maintaining the cursor location relative to the location on the element
     * that was clicked.  Override this if you want to place the element in a
     * location other than where the cursor is.
     * @param {Number} iPageX the X coordinate of the mousedown or drag event
     * @param {Number} iPageY the Y coordinate of the mousedown or drag event
     */
    setDragElPos: function(iPageX, iPageY) {
        // the first time we do this, we are going to check to make sure
        // the element has css positioning
        var el = this.getDragEl();
        this.alignElWithMouse(el, iPageX, iPageY);
    },
    /**
     * Sets the element to the location of the mousedown or click event,
     * maintaining the cursor location relative to the location on the element
     * that was clicked.  Override this if you want to place the element in a
     * location other than where the cursor is.
     * @param {HTMLElement} el the element to move
     * @param {Number} iPageX the X coordinate of the mousedown or drag event
     * @param {Number} iPageY the Y coordinate of the mousedown or drag event
     */
    alignElWithMouse: function(el, iPageX, iPageY) {
        var oCoord = this.getTargetCoord(iPageX, iPageY),
            fly = el.dom ? el : Ext.fly(el, '_dd'),
            elSize = fly.getSize(),
            EL = Ext.Element,
            vpSize, aCoord, newLeft, newTop;
        if (!this.deltaSetXY) {
            vpSize = this.cachedViewportSize = {
                width: EL.getDocumentWidth(),
                height: EL.getDocumentHeight()
            };
            aCoord = [
                Math.max(0, Math.min(oCoord.x, vpSize.width - elSize.width)),
                Math.max(0, Math.min(oCoord.y, vpSize.height - elSize.height))
            ];
            fly.setXY(aCoord);
            newLeft = this.getLocalX(fly);
            newTop = fly.getLocalY();
            this.deltaSetXY = [
                newLeft - oCoord.x,
                newTop - oCoord.y
            ];
        } else {
            vpSize = this.cachedViewportSize;
            this.setLocalXY(fly, Math.max(0, Math.min(oCoord.x + this.deltaSetXY[0], vpSize.width - elSize.width)), Math.max(0, Math.min(oCoord.y + this.deltaSetXY[1], vpSize.height - elSize.height)));
        }
        this.cachePosition(oCoord.x, oCoord.y);
        this.autoScroll(oCoord.x, oCoord.y, el.offsetHeight, el.offsetWidth);
        return oCoord;
    },
    /**
     * Saves the most recent position so that we can reset the constraints and
     * tick marks on-demand.  We need to know this so that we can calculate the
     * number of pixels the element is offset from its original position.
     *
     * @param {Number} [iPageX] the current x position (this just makes it so we
     * don't have to look it up again)
     * @param {Number} [iPageY] the current y position (this just makes it so we
     * don't have to look it up again)
     */
    cachePosition: function(iPageX, iPageY) {
        var aCoord;
        if (iPageX) {
            this.lastPageX = iPageX;
            this.lastPageY = iPageY;
        } else {
            aCoord = Ext.fly(this.getEl()).getXY();
            this.lastPageX = aCoord[0];
            this.lastPageY = aCoord[1];
        }
    },
    /**
     * Auto-scroll the window if the dragged object has been moved beyond the
     * visible window boundary.
     * @param {Number} x the drag element's x position
     * @param {Number} y the drag element's y position
     * @param {Number} h the height of the drag element
     * @param {Number} w the width of the drag element
     * @private
     */
    autoScroll: function(x, y, h, w) {
        var clientH, clientW, st, sl, bot, right, toBot, toRight, thresh, scrAmt;
        if (this.scroll) {
            // The client height
            clientH = Ext.Element.getViewportHeight();
            // The client width
            clientW = Ext.Element.getViewportWidth();
            // The amt scrolled down
            st = this.DDMInstance.getScrollTop();
            // The amt scrolled right
            sl = this.DDMInstance.getScrollLeft();
            // Location of the bottom of the element
            bot = h + y;
            // Location of the right of the element
            right = w + x;
            // The distance from the cursor to the bottom of the visible area,
            // adjusted so that we don't scroll if the cursor is beyond the
            // element drag constraints
            toBot = (clientH + st - y - this.deltaY);
            // The distance from the cursor to the right of the visible area
            toRight = (clientW + sl - x - this.deltaX);
            // How close to the edge the cursor must be before we scroll
            // var thresh = (document.all) ? 100 : 40;
            thresh = 40;
            // How many pixels to scroll per autoscroll op.  This helps to reduce
            // clunky scrolling. IE is more sensitive about this ... it needs this
            // value to be higher.
            scrAmt = (document.all) ? 80 : 30;
            // Scroll down if we are near the bottom of the visible page and the
            // obj extends below the crease
            if (bot > clientH && toBot < thresh) {
                window.scrollTo(sl, st + scrAmt);
            }
            // Scroll up if the window is scrolled down and the top of the object
            // goes above the top border
            if (y < st && st > 0 && y - st < thresh) {
                window.scrollTo(sl, st - scrAmt);
            }
            // Scroll right if the obj is beyond the right border and the cursor is
            // near the border.
            if (right > clientW && toRight < thresh) {
                window.scrollTo(sl + scrAmt, st);
            }
            // Scroll left if the window has been scrolled to the right and the obj
            // extends past the left border
            if (x < sl && sl > 0 && x - sl < thresh) {
                window.scrollTo(sl - scrAmt, st);
            }
        }
    },
    /**
     * Finds the location the element should be placed if we want to move
     * it to where the mouse location less the click offset would place us.
     * @param {Number} iPageX the X coordinate of the click
     * @param {Number} iPageY the Y coordinate of the click
     * @return {Object} An object that contains the coordinates (Object.x and Object.y)
     * @return {Number} return.x
     * @return {Number} return.y
     * @private
     */
    getTargetCoord: function(iPageX, iPageY) {
        var x = iPageX - this.deltaX,
            y = iPageY - this.deltaY;
        if (this.constrainX) {
            if (x < this.minX) {
                x = this.minX;
            }
            if (x > this.maxX) {
                x = this.maxX;
            }
        }
        if (this.constrainY) {
            if (y < this.minY) {
                y = this.minY;
            }
            if (y > this.maxY) {
                y = this.maxY;
            }
        }
        x = this.getTick(x, this.xTicks);
        y = this.getTick(y, this.yTicks);
        return {
            x: x,
            y: y
        };
    },
    /**
     * Sets up config options specific to this class. Overrides
     * Ext.dd.DragDrop, but all versions of this method through the
     * inheritance chain are called
     */
    applyConfig: function() {
        this.callParent();
        this.scroll = (this.config.scroll !== false);
    },
    /**
     * Event that fires prior to the onMouseDown event.  Overrides
     * Ext.dd.DragDrop.
     */
    b4MouseDown: function(e) {
        // this.resetConstraints();
        var xy = e.getXY();
        this.autoOffset(xy[0], xy[1]);
    },
    /**
     * Event that fires prior to the onDrag event.  Overrides
     * Ext.dd.DragDrop.
     */
    b4Drag: function(e) {
        var xy = e.getXY();
        this.setDragElPos(xy[0], xy[1]);
    },
    toString: function() {
        return ("DD " + this.id);
    },
    getLocalX: function(el) {
        return el.getLocalX();
    },
    setLocalXY: function(el, x, y) {
        el.setLocalXY(x, y);
    }
});
// ////////////////////////////////////////////////////////////////////////
// Debugging ygDragDrop events that can be overridden
// ////////////////////////////////////////////////////////////////////////
/*
    startDrag: function(x, y) {
    },

    onDrag: function(e) {
    },

    onDragEnter: function(e, id) {
    },

    onDragOver: function(e, id) {
    },

    onDragOut: function(e, id) {
    },

    onDragDrop: function(e, id) {
    },

    endDrag: function(e) {
    }

    */

/*
 * This is a derivative of the similarly named class in the YUI Library.
 * The original license:
 * Copyright (c) 2006, Yahoo! Inc. All rights reserved.
 * Code licensed under the BSD License:
 * http://developer.yahoo.net/yui/license.txt
 */
/**
 * A DragDrop implementation that inserts an empty, bordered div into
 * the document that follows the cursor during drag operations.  At the time of
 * the click, the frame div is resized to the dimensions of the linked html
 * element, and moved to the exact location of the linked element.
 *
 * References to the "frame" element refer to the single proxy element that
 * was created to be dragged in place of all DDProxy elements on the
 * page.
 */
Ext.define('Ext.dd.DDProxy', {
    extend: 'Ext.dd.DD',
    statics: {
        /**
         * The default drag frame div id
         * @static
         */
        dragElId: "ygddfdiv"
    },
    /**
     * Creates new DDProxy.
     * @param {String} id the id of the linked html element
     * @param {String} sGroup the group of related DragDrop objects
     * @param {Object} config an object containing configurable attributes.
     * Valid properties for DDProxy in addition to those in DragDrop:
     * 
     * - resizeFrame
     * - centerFrame
     * - dragElId
     */
    constructor: function(id, sGroup, config) {
        if (id) {
            this.init(id, sGroup, config);
            this.initFrame();
        }
    },
    /**
     * @property {Boolean} resizeFrame
     * By default we resize the drag frame to be the same size as the element
     * we want to drag (this is to get the frame effect).  We can turn it off
     * if we want a different behavior.
     */
    resizeFrame: true,
    /**
     * @property {Boolean} centerFrame
     * By default the frame is positioned exactly where the drag element is, so
     * we use the cursor offset provided by Ext.dd.DD.  Another option that works only if
     * you do not have constraints on the obj is to have the drag frame centered
     * around the cursor.  Set centerFrame to true for this effect.
     */
    centerFrame: false,
    /**
     * Creates the proxy element if it does not yet exist
     */
    createFrame: function() {
        var self = this,
            body = document.body,
            div, s;
        if (!body || !body.firstChild) {
            Ext.defer(function() {
                self.createFrame();
            }, 50);
            return;
        }
        div = this.getDragEl();
        if (!div) {
            div = document.createElement("div");
            div.id = this.dragElId;
            div.setAttribute('role', 'presentation');
            s = div.style;
            s.position = "absolute";
            s.visibility = "hidden";
            s.cursor = "move";
            s.border = "2px solid #aaa";
            s.zIndex = 999;
            // appendChild can blow up IE if invoked prior to the window load event
            // while rendering a table.  It is possible there are other scenarios
            // that would cause this to happen as well.
            body.insertBefore(div, body.firstChild);
        }
    },
    /**
     * Initialization for the drag frame element.  Must be called in the
     * constructor of all subclasses
     */
    initFrame: function() {
        this.createFrame();
    },
    applyConfig: function() {
        this.callParent();
        this.resizeFrame = (this.config.resizeFrame !== false);
        this.centerFrame = (this.config.centerFrame);
        this.setDragElId(this.config.dragElId || Ext.dd.DDProxy.dragElId);
    },
    /**
     * Resizes the drag frame to the dimensions of the clicked object, positions
     * it over the object, and finally displays it
     * @param {Number} iPageX X click position
     * @param {Number} iPageY Y click position
     * @private
     */
    showFrame: function(iPageX, iPageY) {
        var me = this,
            dragEl = me.getDragEl(),
            s = dragEl.style;
        me._resizeProxy();
        if (me.centerFrame) {
            me.setDelta(Math.round(parseInt(s.width, 10) / 2), Math.round(parseInt(s.height, 10) / 2));
        }
        me.setDragElPos(iPageX, iPageY);
        Ext.fly(dragEl).show();
    },
    /**
     * The proxy is automatically resized to the dimensions of the linked
     * element when a drag is initiated, unless resizeFrame is set to false
     * @private
     */
    _resizeProxy: function() {
        var el;
        if (this.resizeFrame) {
            el = this.getEl();
            Ext.fly(this.getDragEl()).setSize(el.offsetWidth, el.offsetHeight);
        }
    },
    // overrides Ext.dd.DragDrop
    b4MouseDown: function(e) {
        var xy = e.getXY(),
            x = xy[0],
            y = xy[1];
        this.autoOffset(x, y);
        this.setDragElPos(x, y);
    },
    // overrides Ext.dd.DragDrop
    b4StartDrag: function(x, y) {
        // show the drag frame
        this.showFrame(x, y);
    },
    // overrides Ext.dd.DragDrop
    b4EndDrag: function(e) {
        Ext.fly(this.getDragEl()).hide();
    },
    // overrides Ext.dd.DragDrop
    // By default we try to move the element to the last location of the frame.
    // This is so that the default behavior mirrors that of Ext.dd.DD.
    endDrag: function(e) {
        var lel = this.getEl(),
            del = this.getDragEl();
        // Show the drag frame briefly so we can get its position
        del.style.visibility = "";
        this.beforeMove();
        // Hide the linked element before the move to get around a Safari
        // rendering bug.
        lel.style.visibility = "hidden";
        Ext.dd.DDM.moveToEl(lel, del);
        del.style.visibility = "hidden";
        lel.style.visibility = "";
        this.afterDrag();
    },
    beforeMove: function() {},
    afterDrag: function() {},
    toString: function() {
        return ("DDProxy " + this.id);
    }
});

/**
 * A specialized floating Component that supports a drop status icon and auto-repair.
 * This is the default drag proxy used by all Ext.dd components.
 */
Ext.define('Ext.dd.StatusProxy', {
    extend: 'Ext.Component',
    animRepair: false,
    childEls: [
        'ghost'
    ],
    /* eslint-disable indent, max-len */
    renderTpl: [
        '<div class="' + Ext.baseCSSPrefix + 'dd-drop-icon" role="presentation"></div>' + '<div id="{id}-ghost" data-ref="ghost" class="' + Ext.baseCSSPrefix + 'dd-drag-ghost" role="presentation"></div>'
    ],
    /* eslint-enable indent, max-len */
    repairCls: Ext.baseCSSPrefix + 'dd-drag-repair',
    ariaRole: 'presentation',
    skipLayout: true,
    alignOnScroll: false,
    /**
     * Creates new StatusProxy.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        var me = this;
        config = config || {};
        Ext.apply(me, {
            hideMode: 'visibility',
            hidden: true,
            floating: true,
            id: me.id || Ext.id(),
            cls: Ext.baseCSSPrefix + 'dd-drag-proxy ' + this.dropNotAllowed,
            shadow: config.shadow || false,
            renderTo: Ext.getDetachedBody()
        });
        me.callParent(arguments);
        this.dropStatus = this.dropNotAllowed;
    },
    /**
     * @cfg {String} dropAllowed
     * The CSS class to apply to the status element when drop is allowed.
     */
    dropAllowed: Ext.baseCSSPrefix + 'dd-drop-ok',
    /**
     * @cfg {String} dropNotAllowed
     * The CSS class to apply to the status element when drop is not allowed.
     */
    dropNotAllowed: Ext.baseCSSPrefix + 'dd-drop-nodrop',
    /**
     * Updates the proxy's visual element to indicate the status of whether or not drop is allowed
     * over the current target element.
     * @param {String} cssClass The css class for the new drop status indicator image
     */
    setStatus: function(cssClass) {
        cssClass = cssClass || this.dropNotAllowed;
        if (this.dropStatus !== cssClass) {
            this.el.replaceCls(this.dropStatus, cssClass);
            this.dropStatus = cssClass;
        }
    },
    /**
     * Resets the status indicator to the default dropNotAllowed value
     * @param {Boolean} clearGhost True to also remove all content from the ghost,
     * false to preserve it
     */
    reset: function(clearGhost) {
        var me = this,
            clsPrefix = Ext.baseCSSPrefix + 'dd-drag-proxy ';
        me.el.replaceCls(clsPrefix + me.dropAllowed, clsPrefix + me.dropNotAllowed);
        me.dropStatus = me.dropNotAllowed;
        if (clearGhost) {
            me.ghost.setHtml('');
        }
    },
    /**
     * Updates the contents of the ghost element
     * @param {String/HTMLElement} html The html that will replace the current innerHTML
     * of the ghost element, or a DOM node to append as the child of the ghost element
     * (in which case the innerHTML will be cleared first).
     */
    update: function(html) {
        var el;
        if (typeof html === "string") {
            this.ghost.setHtml(html);
        } else {
            this.ghost.setHtml('');
            html.style.margin = "0";
            this.ghost.dom.appendChild(html);
        }
        el = this.ghost.dom.firstChild;
        if (el) {
            Ext.fly(el).setStyle('float', 'none');
        }
    },
    /**
     * Returns the ghost element
     * @return {Ext.dom.Element} el
     */
    getGhost: function() {
        return this.ghost;
    },
    /**
     * Hides the proxy
     * @param {Boolean} clear True to reset the status and clear the ghost contents,
     * false to preserve them
     */
    hide: function(clear) {
        this.callParent();
        if (clear) {
            this.reset(true);
        }
    },
    /**
     * Stops the repair animation if it's currently running
     */
    stop: function() {
        if (this.anim && this.anim.isAnimated && this.anim.isAnimated()) {
            this.anim.stop();
        }
    },
    /**
     * Force the Element to sync its shadow and shim positions
     */
    sync: function() {
        this.el.syncUnderlays();
    },
    /**
     * Causes the proxy to return to its position of origin via an animation.
     * Should be called after an invalid drop operation by the item being dragged.
     * @param {Number[]} xy The XY position of the element ([x, y])
     * @param {Function} callback The function to call after the repair is complete.
     * @param {Object} scope The scope (`this` reference) in which the callback function
     * is executed. Defaults to the browser window.
     */
    repair: function(xy, callback, scope) {
        var me = this;
        me.callback = callback;
        me.scope = scope;
        if (xy && me.animRepair !== false) {
            me.el.addCls(me.repairCls);
            me.el.setUnderlaysVisible(false);
            me.anim = me.el.animate({
                duration: me.repairDuration || 500,
                easing: 'ease-out',
                to: {
                    x: xy[0],
                    y: xy[1]
                },
                stopAnimation: true,
                callback: me.afterRepair,
                scope: me
            });
        } else {
            me.afterRepair();
        }
    },
    /**
     * @private
     */
    afterRepair: function() {
        var me = this;
        me.hide(true);
        me.el.removeCls(me.repairCls);
        if (typeof me.callback === "function") {
            me.callback.call(me.scope || me);
        }
        delete me.callback;
        delete me.scope;
    }
});

/**
 * A simple class that provides the basic implementation needed to make any element draggable.
 */
Ext.define('Ext.dd.DragSource', {
    extend: 'Ext.dd.DDProxy',
    requires: [
        'Ext.dd.StatusProxy',
        'Ext.dd.DragDropManager'
    ],
    /**
     * @cfg {String} ddGroup
     * A named drag drop group to which this object belongs.  If a group is specified, then this
     * object will only interact with other drag drop objects in the same group.
     */
    /**
     * @property {Object} dragData
     * This property contains the data representing the dragged object. This data is set up
     * by the implementation of the {@link #getDragData} method. It must contain a ddel property,
     * but can contain any other data according to the application's needs.
     */
    /**
     * @cfg {String} dropAllowed
     * The CSS class returned to the drag source when drop is allowed.
     */
    dropAllowed: Ext.baseCSSPrefix + 'dd-drop-ok',
    /**
     * @cfg {String} dropNotAllowed
     * The CSS class returned to the drag source when drop is not allowed.
     */
    dropNotAllowed: Ext.baseCSSPrefix + 'dd-drop-nodrop',
    /**
     * @cfg {Boolean} animRepair
     * If true, animates the proxy element back to the position of the handle element used to
     * trigger the drag.
     */
    animRepair: true,
    /**
     * @cfg {String} repairHighlightColor
     * The color to use when visually highlighting the drag source in the afterRepair
     * method after a failed drop (defaults to light blue). The color must be a 6 digit hex value,
     * without a preceding '#'.
     */
    repairHighlightColor: 'c3daf9',
    /**
     * Creates new drag-source.
     * @param {String/HTMLElement/Ext.dom.Element} el The container element or ID of it.
     * @param {Object} config (optional) Config object.
     */
    constructor: function(el, config) {
        this.el = Ext.get(el);
        if (!this.dragData) {
            this.dragData = {};
        }
        Ext.apply(this, config);
        if (!this.proxy) {
            this.proxy = new Ext.dd.StatusProxy({
                id: this.el.id + '-drag-status-proxy',
                animRepair: this.animRepair
            });
        }
        this.callParent([
            this.el.dom,
            this.ddGroup || this.group,
            {
                dragElId: this.proxy.id,
                resizeFrame: false,
                isTarget: false,
                scroll: this.scroll === true
            }
        ]);
        this.dragging = false;
    },
    /**
     * Returns the data object associated with this drag source
     * @return {Object} data An object containing arbitrary data
     */
    getDragData: function(e) {
        return this.dragData;
    },
    /**
     * @private
     */
    onDragEnter: function(e, id) {
        var target = Ext.dd.DragDropManager.getDDById(id),
            status;
        this.cachedTarget = target;
        if (this.beforeDragEnter(target, e, id) !== false) {
            if (target.isNotifyTarget) {
                status = target.notifyEnter(this, e, this.dragData);
                this.proxy.setStatus(status);
            } else {
                this.proxy.setStatus(this.dropAllowed);
            }
            if (this.afterDragEnter) {
                /**
                 * An empty function by default, but provided so that you can perform a custom
                 * action when the dragged item enters the drop target by providing
                 * an implementation.
                 * @param {Ext.dd.DragDrop} target The drop target
                 * @param {Event} e The event object
                 * @param {String} id The id of the dragged element
                 * @method afterDragEnter
                 */
                this.afterDragEnter(target, e, id);
            }
        }
    },
    /**
     * An empty function by default, but provided so that you can perform a custom action
     * before the dragged item enters the drop target and optionally cancel the onDragEnter.
     * @param {Ext.dd.DragDrop} target The drop target
     * @param {Event} e The event object
     * @param {String} id The id of the dragged element
     * @return {Boolean} isValid True if the drag event is valid, else false to cancel
     * @template
     */
    beforeDragEnter: function(target, e, id) {
        return true;
    },
    /**
     * @private
     */
    onDragOver: function(e, id) {
        var target = this.cachedTarget || Ext.dd.DragDropManager.getDDById(id),
            status;
        if (this.beforeDragOver(target, e, id) !== false) {
            if (target.isNotifyTarget) {
                status = target.notifyOver(this, e, this.dragData);
                this.proxy.setStatus(status);
            }
            if (this.afterDragOver) {
                /**
                 * An empty function by default, but provided so that you can perform a custom
                 * action while the dragged item is over the drop target by providing
                 * an implementation.
                 * @param {Ext.dd.DragDrop} target The drop target
                 * @param {Event} e The event object
                 * @param {String} id The id of the dragged element
                 * @method afterDragOver
                 */
                this.afterDragOver(target, e, id);
            }
        }
    },
    /**
     * An empty function by default, but provided so that you can perform a custom action
     * while the dragged item is over the drop target and optionally cancel the onDragOver.
     * @param {Ext.dd.DragDrop} target The drop target
     * @param {Event} e The event object
     * @param {String} id The id of the dragged element
     * @return {Boolean} isValid True if the drag event is valid, else false to cancel
     * @template
     */
    beforeDragOver: function(target, e, id) {
        return true;
    },
    /**
     * @private
     */
    onDragOut: function(e, id) {
        var target = this.cachedTarget || Ext.dd.DragDropManager.getDDById(id);
        if (this.beforeDragOut(target, e, id) !== false) {
            if (target.isNotifyTarget) {
                target.notifyOut(this, e, this.dragData);
            }
            this.proxy.reset();
            if (this.afterDragOut) {
                /**
                 * An empty function by default, but provided so that you can perform a custom
                 * action after the dragged item is dragged out of the target without dropping.
                 * @param {Ext.dd.DragDrop} target The drop target
                 * @param {Event} e The event object
                 * @param {String} id The id of the dragged element
                 * @method afterDragOut
                 */
                this.afterDragOut(target, e, id);
            }
        }
        this.cachedTarget = null;
    },
    /**
     * An empty function by default, but provided so that you can perform a custom action before
     * the dragged item is dragged out of the target without dropping, and optionally cancel
     * the onDragOut.
     * @param {Ext.dd.DragDrop} target The drop target
     * @param {Event} e The event object
     * @param {String} id The id of the dragged element
     * @return {Boolean} isValid True if the drag event is valid, else false to cancel
     * @template
     */
    beforeDragOut: function(target, e, id) {
        return true;
    },
    /**
     * @private
     */
    onDragDrop: function(e, id) {
        var target = this.cachedTarget || Ext.dd.DragDropManager.getDDById(id);
        if (this.beforeDragDrop(target, e, id) !== false) {
            if (target.isNotifyTarget) {
                if (target.notifyDrop(this, e, this.dragData) !== false) {
                    // valid drop?
                    this.onValidDrop(target, e, id);
                } else {
                    this.onInvalidDrop(target, e, id);
                }
            } else {
                this.onValidDrop(target, e, id);
            }
            if (this.afterDragDrop) {
                /**
                 * An empty function by default, but provided so that you can perform a custom
                 * action after a valid drag drop has occurred by providing an implementation.
                 * @param {Ext.dd.DragDrop} target The drop target
                 * @param {Event} e The event object
                 * @param {String} id The id of the dropped element
                 * @method afterDragDrop
                 */
                this.afterDragDrop(target, e, id);
            }
        }
        delete this.cachedTarget;
    },
    /**
     * An empty function by default, but provided so that you can perform a custom action before
     * the dragged item is dropped onto the target and optionally cancel the onDragDrop.
     * @param {Ext.dd.DragDrop} target The drop target
     * @param {Event} e The event object
     * @param {String} id The id of the dragged element
     * @return {Boolean} isValid True if the drag drop event is valid, else false to cancel
     * @template
     */
    beforeDragDrop: function(target, e, id) {
        return true;
    },
    /**
     * @private
     */
    onValidDrop: function(target, e, id) {
        this.hideProxy();
        if (this.afterValidDrop) {
            /**
             * An empty function by default, but provided so that you can perform a custom action
             * after a valid drop has occurred by providing an implementation.
             * @param {Object} target The target DD
             * @param {Event} e The event object
             * @param {String} id The id of the dropped element
             * @method afterValidDrop
             */
            this.afterValidDrop(target, e, id);
        }
    },
    /**
     * @private
     */
    getRepairXY: function(e, data) {
        return this.el.getXY();
    },
    /**
     * @private
     */
    onInvalidDrop: function(target, e, id) {
        // This method may be called by the DragDropManager.
        // To preserve backwards compat, it only passes the event object
        // Here we correct the arguments.
        var me = this;
        if (!e) {
            e = target;
            target = null;
            id = e.getTarget().id;
        }
        if (me.beforeInvalidDrop(target, e, id) !== false) {
            if (me.cachedTarget) {
                if (me.cachedTarget.isNotifyTarget) {
                    me.cachedTarget.notifyOut(me, e, me.dragData);
                }
                me.cacheTarget = null;
            }
            me.proxy.repair(me.getRepairXY(e, me.dragData), me.afterRepair, me);
            if (me.afterInvalidDrop) {
                /**
                * An empty function by default, but provided so that you can perform a custom action
                * after an invalid drop has occurred by providing an implementation.
                * @param {Event} e The event object
                * @param {String} id The id of the dropped element
                * @method afterInvalidDrop
                */
                me.afterInvalidDrop(e, id);
            }
        }
    },
    /**
     * @private
     */
    afterRepair: function() {
        var me = this;
        if (Ext.enableFx) {
            me.el.highlight(me.repairHighlightColor);
        }
        me.dragging = false;
    },
    /**
     * An empty function by default, but provided so that you can perform a custom action after
     * an invalid drop has occurred.
     * @param {Ext.dd.DragDrop} target The drop target
     * @param {Event} e The event object
     * @param {String} id The id of the dragged element
     * @return {Boolean} isValid True if the invalid drop should proceed, else false to cancel
     * @template
     */
    beforeInvalidDrop: function(target, e, id) {
        return true;
    },
    /**
     * @private
     */
    handleMouseDown: function(e) {
        var data;
        if (this.dragging) {
            return;
        }
        data = this.getDragData(e);
        if (data && this.onBeforeDrag(data, e) !== false) {
            this.dragData = data;
            this.proxy.stop();
            this.callParent(arguments);
        }
    },
    /**
     * An empty function by default, but provided so that you can perform a custom action before
     * the initial drag event begins and optionally cancel it.
     * @param {Object} data An object containing arbitrary data to be shared with drop targets
     * @param {Event} e The event object
     * @return {Boolean} isValid True if the drag event is valid, else false to cancel
     * @template
     */
    onBeforeDrag: function(data, e) {
        return true;
    },
    /**
     * An empty function by default, but provided so that you can perform a custom action once
     * the initial drag event has begun.  The drag cannot be canceled from this function.
     * @param {Number} x The x position of the click on the dragged object
     * @param {Number} y The y position of the click on the dragged object
     * @method
     * @template
     */
    onStartDrag: Ext.emptyFn,
    alignElWithMouse: function() {
        this.proxy.ensureAttachedToBody(true);
        return this.callParent(arguments);
    },
    /**
     * @private
     */
    startDrag: function(x, y) {
        this.proxy.reset();
        this.proxy.hidden = false;
        this.dragging = true;
        this.proxy.update("");
        this.onInitDrag(x, y);
        this.proxy.show();
    },
    /**
     * @private
     */
    onInitDrag: function(x, y) {
        var clone = this.el.dom.cloneNode(true);
        clone.id = Ext.id();
        // prevent duplicate ids
        this.proxy.update(clone);
        this.onStartDrag(x, y);
        return true;
    },
    /**
     * Returns the drag source's underlying {@link Ext.dd.StatusProxy}
     * @return {Ext.dd.StatusProxy} proxy The StatusProxy
     */
    getProxy: function() {
        return this.proxy;
    },
    /**
     * Hides the drag source's {@link Ext.dd.StatusProxy}
     */
    hideProxy: function() {
        this.proxy.hide();
        this.proxy.reset(true);
        this.dragging = false;
    },
    /**
     * @private
     */
    triggerCacheRefresh: function() {
        Ext.dd.DDM.refreshCache(this.groups);
    },
    /**
     * @private
     */
    b4EndDrag: function(e) {},
    /**
     * @private
     */
    endDrag: function(e) {
        this.onEndDrag(this.dragData, e);
    },
    /**
     * @private
     */
    onEndDrag: function(data, e) {},
    /**
     * @private
     */
    autoOffset: function(x, y) {
        this.setDelta(-12, -20);
    },
    destroy: function() {
        Ext.destroy(this.proxy);
        this.callParent();
    }
});

/**
 * A custom drag proxy implementation specific to {@link Ext.panel.Panel}s. This class
 * is primarily used internally for the Panel's drag drop implementation, and
 * should never need to be created directly.
 * @private
 */
Ext.define('Ext.panel.Proxy', {
    alternateClassName: 'Ext.dd.PanelProxy',
    /**
     * @cfg {Boolean} [moveOnDrag=true]
     * True to move the panel to the dragged position when dropped
     */
    moveOnDrag: true,
    /**
     * Creates new panel proxy.
     * @param {Ext.panel.Panel} panel The {@link Ext.panel.Panel} to proxy for
     * @param {Object} [config] Config object
     */
    constructor: function(panel, config) {
        var me = this;
        /**
         * @property panel
         * @type Ext.panel.Panel
         */
        me.panel = panel;
        me.id = me.panel.id + '-ddproxy';
        Ext.apply(me, config);
    },
    /**
     * @cfg {Boolean} insertProxy
     * True to insert a placeholder proxy element while dragging the panel, false to drag
     * with no proxy.
     * Most Panels are not absolute positioned and therefore we need to reserve this space.
     */
    insertProxy: true,
    setStatus: Ext.emptyFn,
    reset: Ext.emptyFn,
    update: Ext.emptyFn,
    stop: Ext.emptyFn,
    sync: Ext.emptyFn,
    /**
     * Gets the proxy's element
     * @return {Ext.dom.Element} The proxy's element
     */
    getEl: function() {
        return this.ghost.el;
    },
    /**
     * Gets the proxy's ghost Panel
     * @return {Ext.panel.Panel} The proxy's ghost Panel
     */
    getGhost: function() {
        return this.ghost;
    },
    /**
     * Gets the proxy element. This is the element that represents where the
     * Panel was before we started the drag operation.
     * @return {Ext.dom.Element} The proxy's element
     */
    getProxy: function() {
        return this.proxy;
    },
    /**
     * Hides the proxy
     */
    hide: function() {
        var me = this;
        if (me.ghost) {
            if (me.proxy) {
                me.proxy.destroy();
                delete me.proxy;
            }
            // Unghost the Panel, do not move the Panel to where the ghost was
            me.panel.unghost(null, me.moveOnDrag);
            delete me.ghost;
        }
    },
    /**
     * Shows the proxy
     */
    show: function() {
        var me = this,
            panelSize;
        if (!me.ghost) {
            panelSize = me.panel.getSize();
            me.panel.el.setVisibilityMode(Ext.Element.DISPLAY);
            me.ghost = me.panel.ghost();
            if (me.insertProxy) {
                // bc Panels aren't absolute positioned we need to take up the space
                // of where the panel previously was
                me.proxy = me.panel.el.insertSibling({
                    role: 'presentation',
                    cls: Ext.baseCSSPrefix + 'panel-dd-spacer'
                });
                me.proxy.setSize(panelSize);
            }
        }
    },
    /**
     * @private
     */
    repair: function(xy, callback, scope) {
        this.hide();
        Ext.callback(callback, scope || this);
    },
    /**
     * Moves the proxy to a different position in the DOM.  This is typically
     * called while dragging the Panel to keep the proxy sync'd to the Panel's
     * location.
     * @param {HTMLElement} parentNode The proxy's parent DOM node
     * @param {HTMLElement} [before] The sibling node before which the
     * proxy should be inserted. Defaults to the parent's last child if not
     * specified.
     */
    moveProxy: function(parentNode, before) {
        if (this.proxy) {
            parentNode.insertBefore(this.proxy.dom, before);
        }
    }
});

/**
 * DD implementation for Panels.
 * @private
 */
Ext.define('Ext.panel.DD', {
    extend: 'Ext.dd.DragSource',
    requires: [
        'Ext.panel.Proxy'
    ],
    constructor: function(panel, cfg) {
        var me = this;
        me.panel = panel;
        me.dragData = {
            panel: panel
        };
        me.panelProxy = new Ext.panel.Proxy(panel, cfg);
        me.proxy = me.panelProxy.proxy;
        me.callParent([
            panel.el,
            cfg
        ]);
        me.setupEl(panel);
    },
    setupEl: function(panel) {
        var me = this,
            header = panel.header,
            el = panel.body;
        if (header) {
            me.setHandleElId(header.id);
            el = header.el;
        }
        if (el) {
            el.setStyle('cursor', 'move');
            me.scroll = false;
        } else {
            // boxready fires after first layout, so we'll definitely be rendered
            panel.on('boxready', me.setupEl, me, {
                single: true
            });
        }
    },
    showFrame: Ext.emptyFn,
    startDrag: Ext.emptyFn,
    b4StartDrag: function(x, y) {
        this.panelProxy.show();
    },
    b4MouseDown: function(e) {
        var xy = e.getXY(),
            x = xy[0],
            y = xy[1];
        this.autoOffset(x, y);
    },
    onInitDrag: function(x, y) {
        this.onStartDrag(x, y);
        return true;
    },
    createFrame: Ext.emptyFn,
    getDragEl: function(e) {
        var ghost = this.panelProxy.ghost;
        if (ghost) {
            return ghost.el.dom;
        }
    },
    endDrag: function(e) {
        this.panelProxy.hide();
        this.panel.saveState();
    },
    autoOffset: function(x, y) {
        x -= this.startPageX;
        y -= this.startPageY;
        this.setDelta(x, y);
    },
    // Override this, we don't want to repair on an "invalid" drop, the panel
    // should main it's position
    onInvalidDrop: function(target, e, id) {
        var me = this;
        if (me.beforeInvalidDrop(target, e, id) !== false) {
            if (me.cachedTarget) {
                if (me.cachedTarget.isNotifyTarget) {
                    me.cachedTarget.notifyOut(me, e, me.dragData);
                }
                me.cacheTarget = null;
            }
            if (me.afterInvalidDrop) {
                /**
                * An empty function by default, but provided so that you can perform a custom action
                * after an invalid drop has occurred by providing an implementation.
                * @param {Event} e The event object
                * @param {String} id The id of the dropped element
                * @method afterInvalidDrop
                */
                me.afterInvalidDrop(e, id);
            }
        }
    }
});

/**
 * This ComponentLayout handles docking for Panels. It takes care of panels that are
 * part of a ContainerLayout that sets this Panel's size and Panels that are part of
 * an AutoContainerLayout in which this panel get his height based of the CSS or
 * its content.
 * @private
 */
Ext.define('Ext.layout.component.Dock', {
    extend: 'Ext.layout.component.Component',
    alias: 'layout.dock',
    alternateClassName: 'Ext.layout.component.AbstractDock',
    type: 'dock',
    horzAxisProps: {
        name: 'horz',
        oppositeName: 'vert',
        dockBegin: 'left',
        dockEnd: 'right',
        horizontal: true,
        marginBegin: 'margin-left',
        maxSize: 'maxWidth',
        minSize: 'minWidth',
        pos: 'x',
        setSize: 'setWidth',
        shrinkWrapDock: 'shrinkWrapDockWidth',
        size: 'width',
        sizeModel: 'widthModel'
    },
    vertAxisProps: {
        name: 'vert',
        oppositeName: 'horz',
        dockBegin: 'top',
        dockEnd: 'bottom',
        horizontal: false,
        marginBegin: 'margin-top',
        maxSize: 'maxHeight',
        minSize: 'minHeight',
        pos: 'y',
        setSize: 'setHeight',
        shrinkWrapDock: 'shrinkWrapDockHeight',
        size: 'height',
        sizeModel: 'heightModel'
    },
    initializedBorders: -1,
    horizontalCollapsePolicy: {
        width: true,
        x: true
    },
    verticalCollapsePolicy: {
        height: true,
        y: true
    },
    finishRender: function() {
        var me = this,
            target, items;
        me.callParent();
        target = me.getRenderTarget();
        items = me.getDockedItems();
        me.finishRenderItems(target, items);
    },
    isItemBoxParent: function(itemContext) {
        return true;
    },
    isItemShrinkWrap: function(item) {
        return true;
    },
    noBorderClasses: [
        Ext.baseCSSPrefix + 'docked-noborder-top',
        Ext.baseCSSPrefix + 'docked-noborder-right',
        Ext.baseCSSPrefix + 'docked-noborder-bottom',
        Ext.baseCSSPrefix + 'docked-noborder-left'
    ],
    noBorderClassesSides: {
        top: Ext.baseCSSPrefix + 'docked-noborder-top',
        right: Ext.baseCSSPrefix + 'docked-noborder-right',
        bottom: Ext.baseCSSPrefix + 'docked-noborder-bottom',
        left: Ext.baseCSSPrefix + 'docked-noborder-left'
    },
    borderWidthProps: {
        top: 'border-top-width',
        right: 'border-right-width',
        bottom: 'border-bottom-width',
        left: 'border-left-width'
    },
    _itemCls: Ext.baseCSSPrefix + 'docked',
    handleItemBorders: function() {
        var me = this,
            owner = me.owner,
            borders, docked,
            lastItems = me.lastDockedItems,
            oldBorders = me.borders,
            currentGeneration = owner.dockedItems.generation,
            noBorderClassesSides = me.noBorderClassesSides,
            borderWidthProps = me.borderWidthProps,
            i, ln, item, dock, side,
            collapsed = me.collapsed;
        if (me.initializedBorders === currentGeneration || (owner.border && !owner.manageBodyBorders) || (owner.collapsed && owner.collapseMode === 'mini')) {
            return;
        }
        me.initializedBorders = currentGeneration;
        // Borders have to be calculated using expanded docked item collection.
        me.collapsed = false;
        me.lastDockedItems = docked = me.getLayoutItems();
        me.collapsed = collapsed;
        borders = {
            top: [],
            right: [],
            bottom: [],
            left: []
        };
        for (i = 0 , ln = docked.length; i < ln; i++) {
            item = docked[i];
            dock = item.dock;
            if (item.ignoreBorderManagement) {
                
                continue;
            }
            if (!borders[dock].satisfied) {
                borders[dock].push(item);
                borders[dock].satisfied = true;
            }
            if (!borders.top.satisfied && dock !== 'bottom') {
                borders.top.push(item);
            }
            if (!borders.right.satisfied && dock !== 'left') {
                borders.right.push(item);
            }
            if (!borders.bottom.satisfied && dock !== 'top') {
                borders.bottom.push(item);
            }
            if (!borders.left.satisfied && dock !== 'right') {
                borders.left.push(item);
            }
        }
        if (lastItems) {
            for (i = 0 , ln = lastItems.length; i < ln; i++) {
                item = lastItems[i];
                if (!item.destroyed && !item.ignoreBorderManagement && !owner.manageBodyBorders) {
                    item.removeCls(me.noBorderClasses);
                }
            }
        }
        if (oldBorders) {
            for (side in oldBorders) {
                if (owner.manageBodyBorders && oldBorders[side].satisfied) {
                    owner.setBodyStyle(borderWidthProps[side], '');
                }
            }
        }
        for (side in borders) {
            ln = borders[side].length;
            if (!owner.manageBodyBorders) {
                for (i = 0; i < ln; i++) {
                    borders[side][i].addCls(noBorderClassesSides[side]);
                }
                if ((!borders[side].satisfied && !owner.bodyBorder) || owner.bodyBorder === false) {
                    owner.addBodyCls(noBorderClassesSides[side]);
                } else {
                    owner.removeBodyCls(noBorderClassesSides[side]);
                }
            } else if (borders[side].satisfied) {
                owner.setBodyStyle(borderWidthProps[side], '1px');
            }
        }
        me.borders = borders;
    },
    beforeLayoutCycle: function(ownerContext) {
        var me = this,
            owner = me.owner,
            shrinkWrap = me.sizeModels.shrinkWrap,
            shrinkWrapDock = owner.shrinkWrapDock,
            collapsedHorz, collapsedVert;
        if (owner.collapsed) {
            if (owner.collapsedVertical()) {
                collapsedVert = true;
                ownerContext.measureDimensions = 1;
            } else {
                collapsedHorz = true;
                ownerContext.measureDimensions = 2;
            }
        }
        ownerContext.collapsedVert = collapsedVert;
        ownerContext.collapsedHorz = collapsedHorz;
        // If we are collapsed, we want to auto-layout using the placeholder/expander
        // instead of the normal items/dockedItems. This must be done here since we could
        // be in a box layout w/stretchmax which sets the width/heightModel to allow it to
        // control the size.
        if (collapsedVert) {
            ownerContext.heightModel = shrinkWrap;
        } else if (collapsedHorz) {
            ownerContext.widthModel = shrinkWrap;
        }
        shrinkWrapDock = shrinkWrapDock === true ? 3 : (shrinkWrapDock || 0);
        ownerContext.shrinkWrapDockHeight = (shrinkWrapDock & 1) && ownerContext.heightModel.shrinkWrap;
        ownerContext.shrinkWrapDockWidth = (shrinkWrapDock & 2) && ownerContext.widthModel.shrinkWrap;
    },
    beginLayout: function(ownerContext) {
        var me = this,
            owner = me.owner,
            docked = me.getLayoutItems(),
            layoutContext = ownerContext.context,
            dockedItemCount = docked.length,
            lastCollapsedState = me.lastCollapsedState,
            dockedItems, i, item, itemContext, offsets, collapsed, dock;
        me.callParent(arguments);
        // Cache the children as ContextItems (like a Container). Also setup to handle
        // collapsed state:
        collapsed = owner.getCollapsed();
        if (collapsed !== lastCollapsedState && lastCollapsedState !== undefined) {
            // If we are collapsing...
            if (me.owner.collapsed) {
                ownerContext.isCollapsingOrExpanding = 1;
                // Add the collapsed class now, so that collapsed CSS rules
                // are applied before measurements are taken by the layout.
                owner.addClsWithUI(owner.collapsedCls);
            } else {
                ownerContext.isCollapsingOrExpanding = 2;
                // Remove the collapsed class now, before layout calculations are done.
                owner.removeClsWithUI(owner.collapsedCls);
                ownerContext.lastCollapsedState = me.lastCollapsedState;
            }
        }
        me.lastCollapsedState = collapsed;
        ownerContext.dockedItems = dockedItems = [];
        for (i = 0; i < dockedItemCount; i++) {
            item = docked[i];
            if (item.rendered) {
                dock = item.dock;
                itemContext = layoutContext.getCmp(item);
                itemContext.dockedAt = {
                    x: 0,
                    y: 0
                };
                itemContext.offsets = offsets = Ext.Element.parseBox(item.offsets || 0);
                itemContext.horizontal = dock === 'top' || dock === 'bottom';
                offsets.width = offsets.left + offsets.right;
                offsets.height = offsets.top + offsets.bottom;
                dockedItems.push(itemContext);
            }
        }
        ownerContext.bodyContext = ownerContext.getEl('body');
    },
    beginLayoutCycle: function(ownerContext) {
        var me = this,
            docked = ownerContext.dockedItems,
            len = docked.length,
            owner = me.owner,
            frameBody = owner.frameBody,
            lastHeightModel = me.lastHeightModel,
            i, item, dock;
        me.callParent(arguments);
        if (me.owner.manageHeight) {
            // Reset in case manageHeight gets turned on during lifecycle.
            // See below for why display could be set to non-default value.
            if (me.lastBodyDisplay) {
                owner.body.dom.style.display = me.lastBodyDisplay = '';
            }
        } else {
            // When manageHeight is false, the body stretches the outer el by using wide margins
            // to force it to accommodate the docked items. When overflow is visible
            // (when panel is resizable and has embedded handles), the body must be inline-block
            // so as not to collapse its margins
            if (me.lastBodyDisplay !== 'inline-block') {
                owner.body.dom.style.display = me.lastBodyDisplay = 'inline-block';
            }
            if (lastHeightModel && lastHeightModel.shrinkWrap && !ownerContext.heightModel.shrinkWrap) {
                owner.body.dom.style.marginBottom = '';
            }
        }
        if (ownerContext.widthModel.auto) {
            if (ownerContext.widthModel.shrinkWrap) {
                owner.el.setWidth(null);
            }
            owner.body.setWidth(null);
            if (frameBody) {
                frameBody.setWidth(null);
            }
        }
        if (ownerContext.heightModel.auto) {
            owner.body.setHeight(null);
            // owner.el.setHeight(null); Disable this for now
            if (frameBody) {
                frameBody.setHeight(null);
            }
        }
        // Each time we begin (2nd+ would be due to invalidate) we need to publish the
        // known contentWidth/Height if we are collapsed:
        if (ownerContext.collapsedVert) {
            ownerContext.setContentHeight(0);
        } else if (ownerContext.collapsedHorz) {
            ownerContext.setContentWidth(0);
        }
        // dock: 'right' items, when a panel gets narrower get "squished". Moving them to
        // left:0px avoids this!
        for (i = 0; i < len; i++) {
            item = docked[i].target;
            dock = item.dock;
            if (dock === 'right') {
                item.setLocalX(0);
            } else if (dock !== 'left') {
                
                continue;
            }
        }
    },
    // TODO - clear width/height?
    calculate: function(ownerContext) {
        var me = this,
            measure = me.measureAutoDimensions(ownerContext, ownerContext.measureDimensions),
            state = ownerContext.state,
            horzDone = state.horzDone,
            vertDone = state.vertDone,
            bodyContext = ownerContext.bodyContext,
            framing, horz, vert, forward, backward;
        // make sure we can use these value w/o calling methods to get them
        /* eslint-disable */
        ownerContext.borderInfo || ownerContext.getBorderInfo();
        ownerContext.paddingInfo || ownerContext.getPaddingInfo();
        ownerContext.frameInfo || ownerContext.getFrameInfo();
        bodyContext.borderInfo || bodyContext.getBorderInfo();
        bodyContext.paddingInfo || bodyContext.getPaddingInfo();
        /* eslint-enable */
        // On CSS3 browsers, the border and padding frame the outer el. On non-CSS3
        // browsers, the outer el has no border or padding - all that appears on the
        // framing elements as padding and height. In CSS3, the border size effects the
        // origin of the dockedItems but the padding does not (so that must be added in
        // most of the time). In non-CSS3 mode, the dockedItems are outside the framing:
        //
        //      ... top / left dockedItems ...
        //      <div id="...-ml" style="padding-left: border-radius-left;">
        //          <div id="...-mr" style="padding-right: border-radius-right;">
        //              <div id="...-mc" style="padding: extra;">
        //                  ... body ...
        //              </div>
        //          </div>
        //      </div>
        //      ... bottom / right dockedItems ...
        // 
        // For the sake of sanity, we perform all the calculations in CSS3 mode. We test
        // for the presence of non-CSS3 framing only when necessary.
        //
        if (!ownerContext.frameBorder) {
            if (!(framing = ownerContext.framing)) {
                ownerContext.frameBorder = ownerContext.borderInfo;
                ownerContext.framePadding = ownerContext.paddingInfo;
            } else {
                // These values match what they would have been in CSS3.
                ownerContext.frameBorder = framing.border;
                ownerContext.framePadding = framing.padding;
            }
        }
        // Start the axes so they are ready to proceed inwards (fixed-size) or outwards
        // (shrinkWrap) and stash key property names as well:
        horz = !horzDone && me.createAxis(ownerContext, measure.contentWidth, ownerContext.widthModel, me.horzAxisProps, ownerContext.collapsedHorz);
        vert = !vertDone && me.createAxis(ownerContext, measure.contentHeight, ownerContext.heightModel, me.vertAxisProps, ownerContext.collapsedVert);
        // We iterate forward and backward over the dockedItems at the same time based on
        // whether an axis is shrinkWrap or fixed-size. For a fixed-size axis, the outer box
        // axis is allocated to docked items in forward order and is reduced accordingly.
        // To handle a shrinkWrap axis, the box starts at the inner (body) size and is used to
        // size docked items in backwards order. This is because the last docked item shares
        // an edge with the body. The item size is used to adjust the shrinkWrap axis outwards
        // until the first docked item (at the outermost edge) is processed. This backwards
        // order ensures that docked items never get an incorrect size for any dimension.
        for (forward = 0 , backward = ownerContext.dockedItems.length; backward--; ++forward) {
            if (horz) {
                me.dockChild(ownerContext, horz, backward, forward);
            }
            if (vert) {
                me.dockChild(ownerContext, vert, backward, forward);
            }
        }
        if (horz && me.finishAxis(ownerContext, horz)) {
            state.horzDone = horzDone = horz;
        }
        if (vert && me.finishAxis(ownerContext, vert)) {
            state.vertDone = vertDone = vert;
        }
        // Once all items are docked, the final size of the outer panel or inner body can
        // be determined. If we can determine both width and height, we are done.
        if (horzDone && vertDone && me.finishConstraints(ownerContext, horzDone, vertDone)) {
            // Size information is published as we dock items but position is hard to do
            // that way (while avoiding published multiple times) so we publish all the
            // positions at the end.
            me.finishPositions(ownerContext, horzDone, vertDone);
        } else {
            me.done = false;
        }
    },
    /**
     * Creates an axis object given the particulars. The process starts by placing the
     * dockedItems in an idealized box where this method is called once for each side.
     * The ideal box is defined by the CSS3 border and padding values (which account for
     * the influence of border-radius). The origin (the (0,0) point) of the ideal box is
     * the top-left edge of the border or the border-box. Normal dockedItems are placed
     * inside this box at an offset to clear the border and padding and sit properly in
     * the panel next to the body.
     * 
     * The origin has to be started differently if the axis is in shrinkWrap mode. When
     * shrink-wrapping an axis, the axis starts at the edge of the body and expands
     * outwards as items are docked. This means the ideal (0,0) for shrinkWrap is on the
     * top-left corner of the body.
     * 
     * The following diagram illustrates this using the vertical axis.
     * 
     *      +---------------------------+ 10px (border)
     *      |                           |
     *      |  xxxxxxxxxxxxxxxxxxxxxxx  | 5px (padding)   shrinkWrap    other
     *      |  +=====================+  |                   -50         15
     *      |  |  Header             |  | 30px
     *      |  |                     |  |
     *      |  +=====================+  |
     *      |  +---------------------+  |                   -20         45
     *      |  |  tbar               |  | 20 px
     *      |  +---------------------+  |
     *      |  +---------------------+  |                   0           65
     *      |  |  Body               |  | 100px
     *      |  |                     |  |
     *      |  |                     |  |
     *      |  +---------------------+  |
     *      |  +---------------------+  |                   100         165
     *      |  |  bbar               |  | 15px
     *      |  +---------------------+  |
     *      |  xxxxxxxxxxxxxxxxxxxxxxx  | 5px
     *      |                           |
     *      +---------------------------+ 10px
     *
     * These are sufficient to determine sizes of things, but to finalize this process
     * and assign proper positions, the tentative coordinates have to be adjusted by an
     * amount appropriate for the item. Because dockedItems are position:absolute, they
     * sit inside the border and so must be adjusted for padding. The body is different
     * because it is position:relative and so it naturally sits inside the padding and
     * the padding must not be included in its position.
     * 
     * Headers and footers that use `ignoreParentFrame` interact with this process by
     * moving themselves outside the border and padding. So in the above diagram, the
     * Header would move up by 15px and *everything else* would move up by 10px. When
     * shrinkWrap is taking place, the 10px of border on the top is removed from the
     * height as well.
     * 
     * The bbar behaves slightly different when it is `ignoreParentFrame`. In shrinkWrap
     * mode, it alone would move down by the padding and the bottom border would not be
     * included in the height. Otherwise, the bbar would be moved down 15px (since the
     * edge is fixed) and the next dockedItem would be placed at, or the body would be
     * stretched down to, 5px (padding) pixels above the bbar.
     *
     * @private
     */
    createAxis: function(ownerContext, contentSize, sizeModel, axisProps, collapsedAxis) {
        var me = this,
            begin = 0,
            owner = me.owner,
            maxSize = owner[axisProps.maxSize],
            minSize = owner[axisProps.minSize] || 0,
            dockBegin = axisProps.dockBegin,
            dockEnd = axisProps.dockEnd,
            posProp = axisProps.pos,
            sizeProp = axisProps.size,
            // exactly the same as "maxSize !== null && maxSize !== undefined"
            hasMaxSize = maxSize != null,
            shrinkWrap = sizeModel.shrinkWrap,
            bodyContext, framing, padding, end;
        if (shrinkWrap) {
            // End position before adding docks around the content is content size
            // plus the body borders in this axis.
            // If collapsed in this axis, the body borders will not be shown.
            if (collapsedAxis) {
                end = 0;
            } else {
                bodyContext = ownerContext.bodyContext;
                end = contentSize + bodyContext.borderInfo[sizeProp];
            }
        } else {
            framing = ownerContext.frameBorder;
            padding = ownerContext.framePadding;
            begin = framing[dockBegin] + padding[dockBegin];
            end = ownerContext.getProp(sizeProp) - (framing[dockEnd] + padding[dockEnd]);
        }
        return {
            shrinkWrap: sizeModel.shrinkWrap,
            sizeModel: sizeModel,
            // An axis tracks start and end+1 px positions. eg 0 to 10 for 10px high
            initialBegin: begin,
            begin: begin,
            end: end,
            collapsed: collapsedAxis,
            horizontal: axisProps.horizontal,
            ignoreFrameBegin: null,
            ignoreFrameEnd: null,
            initialSize: end - begin,
            maxChildSize: 0,
            hasMinMaxConstraints: (minSize || hasMaxSize) && sizeModel.shrinkWrap,
            minSize: minSize,
            maxSize: hasMaxSize ? maxSize : 1000000000,
            bodyPosProp: me.owner.manageHeight ? posProp : axisProps.marginBegin,
            dockBegin: dockBegin,
            // 'left' or 'top'
            dockEnd: dockEnd,
            // 'right' or 'end'
            posProp: posProp,
            // 'x' or 'y'
            sizeProp: sizeProp,
            // 'width' or 'height'
            setSize: axisProps.setSize,
            shrinkWrapDock: ownerContext[axisProps.shrinkWrapDock],
            sizeModelName: axisProps.sizeModel,
            dockedPixelsEnd: 0
        };
    },
    /**
     * Docks a child item on the specified axis. This boils down to determining if the item
     * is docked at the "beginning" of the axis ("left" if horizontal, "top" if vertical),
     * the "end" of the axis ("right" if horizontal, "bottom" if vertical) or stretches
     * along the axis ("top" or "bottom" if horizontal, "left" or "right" if vertical). It
     * also has to differentiate between fixed and shrinkWrap sized dimensions.
     * @private
     */
    dockChild: function(ownerContext, axis, backward, forward) {
        var me = this,
            itemContext = ownerContext.dockedItems[axis.shrinkWrap ? backward : forward],
            item = itemContext.target,
            dock = item.dock,
            // left/top/right/bottom
            sizeProp = axis.sizeProp,
            pos, size;
        if (item.ignoreParentFrame && ownerContext.isCollapsingOrExpanding) {
            // collapsed window header margins may differ from expanded window header margins
            // so we need to make sure the old cached values are not used in axis calculations
            itemContext.clearMarginCache();
        }
        if (!itemContext.marginInfo) {
            itemContext.getMarginInfo();
        }
        // get marginInfo ready
        if (dock === axis.dockBegin) {
            if (axis.shrinkWrap) {
                pos = me.dockOutwardBegin(ownerContext, itemContext, item, axis);
            } else {
                pos = me.dockInwardBegin(ownerContext, itemContext, item, axis);
            }
        } else if (dock === axis.dockEnd) {
            if (axis.shrinkWrap) {
                pos = me.dockOutwardEnd(ownerContext, itemContext, item, axis);
            } else {
                pos = me.dockInwardEnd(ownerContext, itemContext, item, axis);
            }
        } else {
            if (axis.shrinkWrapDock) {
                // we are still shrinkwrapping transversely... so we need to include the
                // size of this item in the max calculation
                size = itemContext.getProp(sizeProp) + itemContext.marginInfo[sizeProp];
                axis.maxChildSize = Math.max(axis.maxChildSize, size);
                pos = 0;
            } else {
                pos = me.dockStretch(ownerContext, itemContext, item, axis);
            }
        }
        itemContext.dockedAt[axis.posProp] = pos;
    },
    /**
     * Docks an item on a fixed-size axis at the "beginning". The "beginning" of the horizontal
     * axis is "left" and the vertical is "top". For a fixed-size axis, the size works from
     * the outer element (the panel) towards the body.
     * @private
     */
    dockInwardBegin: function(ownerContext, itemContext, item, axis) {
        var pos = axis.begin,
            sizeProp = axis.sizeProp,
            ignoreParentFrame = item.ignoreParentFrame,
            delta, size, dock;
        if (ignoreParentFrame) {
            axis.ignoreFrameBegin = itemContext;
            dock = item.dock;
            // We need to move everything up by the border-width.
            delta = ownerContext.frameBorder[dock];
            // We need to move the header "up" by the padding as well.
            pos -= delta + ownerContext.framePadding[dock];
        }
        if (!item.overlay) {
            size = itemContext.getProp(sizeProp) + itemContext.marginInfo[sizeProp];
            axis.begin += size;
            if (ignoreParentFrame) {
                axis.begin -= delta;
            }
        }
        return pos;
    },
    /**
     * Docks an item on a fixed-size axis at the "end". The "end" of the horizontal axis is
     * "right" and the vertical is "bottom".
     * @private
     */
    dockInwardEnd: function(ownerContext, itemContext, item, axis) {
        var sizeProp = axis.sizeProp,
            size = itemContext.getProp(sizeProp) + itemContext.marginInfo[sizeProp],
            pos = axis.end - size,
            frameEnd;
        if (!item.overlay) {
            axis.end = pos;
        }
        if (item.ignoreParentFrame) {
            axis.ignoreFrameEnd = itemContext;
            frameEnd = ownerContext.frameBorder[item.dock];
            pos += frameEnd + ownerContext.framePadding[item.dock];
            axis.end += frameEnd;
        }
        return pos;
    },
    /**
     * Docks an item on a shrinkWrap axis at the "beginning". The "beginning" of the horizontal
     * axis is "left" and the vertical is "top". For a shrinkWrap axis, the size works from
     * the body outward to the outermost element (the panel).
     * 
     * During the docking process, coordinates are allowed to be negative. We start with the
     * body at (0,0) so items docked "top" or "left" will simply be assigned negative x/y. In
     * the {@link #finishPositions} method these are corrected and framing is added. This way
     * the correction is applied as a simple translation of delta x/y on all coordinates to
     * bring the origin back to (0,0).
     * @private
     */
    dockOutwardBegin: function(ownerContext, itemContext, item, axis) {
        var pos = axis.begin,
            sizeProp = axis.sizeProp,
            size;
        if (axis.collapsed) {
            axis.ignoreFrameBegin = axis.ignoreFrameEnd = itemContext;
        } else if (item.ignoreParentFrame) {
            axis.ignoreFrameBegin = itemContext;
        }
        // NOTE - When shrinkWrapping an ignoreParentFrame, this must be the last item
        // on the axis. Since that is so, we let finishAxis take this in to account.
        if (!item.overlay) {
            size = itemContext.getProp(sizeProp) + itemContext.marginInfo[sizeProp];
            pos -= size;
            axis.begin = pos;
        }
        return pos;
    },
    /**
     * Docks an item on a shrinkWrap axis at the "end". The "end" of the horizontal axis is
     * "right" and the vertical is "bottom".
     * @private
     */
    dockOutwardEnd: function(ownerContext, itemContext, item, axis) {
        var pos = axis.end,
            sizeProp = axis.sizeProp,
            size;
        size = itemContext.getProp(sizeProp) + itemContext.marginInfo[sizeProp];
        if (axis.collapsed) {
            axis.ignoreFrameBegin = axis.ignoreFrameEnd = itemContext;
        } else if (item.ignoreParentFrame) {
            axis.ignoreFrameEnd = itemContext;
        }
        // NOTE - When shrinkWrapping an ignoreParentFrame, this must be the last item
        // on the axis. Since that is so, we let finishAxis take this in to account.
        if (!item.overlay) {
            axis.end = pos + size;
            axis.dockedPixelsEnd += size;
        }
        return pos;
    },
    /**
     * Docks an item that might stretch across an axis. This is done for dock "top" and
     * "bottom" items on the horizontal axis and dock "left" and "right" on the vertical.
     * @private
     */
    dockStretch: function(ownerContext, itemContext, item, axis) {
        var dock = item.dock,
            // left/top/right/bottom (also used to index padding/border)
            sizeProp = axis.sizeProp,
            // 'width' or 'height'
            horizontal = dock === 'top' || dock === 'bottom',
            border = ownerContext.frameBorder,
            offsets = itemContext.offsets,
            padding = ownerContext.framePadding,
            endProp = horizontal ? 'right' : 'bottom',
            startProp = horizontal ? 'left' : 'top',
            pos = axis.begin + offsets[startProp],
            margin, size;
        if (item.stretch !== false) {
            size = axis.end - pos - offsets[endProp];
            if (item.ignoreParentFrame) {
                // In CSS3, the border and padding need to be ignored specifically. In
                // non-CSS3 / framing mode, the border and padding will be 0 **but** the
                // header is not rendered inside the framing elements and so we do not
                // want to do anything anyway!
                pos -= padding[startProp] + border[startProp];
                size += padding[sizeProp] + border[sizeProp];
            }
            margin = itemContext.marginInfo;
            size -= margin[sizeProp];
            itemContext[axis.setSize](size);
        }
        return pos;
    },
    /**
     * Finishes the calculation of an axis by determining its size. In non-shrink-wrap
     * cases, this is also where we set the body size.
     * @private
     */
    finishAxis: function(ownerContext, axis) {
        // If the maxChildSize is NaN it means at some point we tried to determine
        // The size of a docked item but we couldn't, so just jump out straight
        // away before doing any other processing
        if (isNaN(axis.maxChildSize)) {
            return false;
        }
        // eslint-disable-next-line vars-on-top
        var axisBegin = axis.begin,
            size = axis.end - axisBegin,
            collapsed = axis.collapsed,
            setSizeMethod = axis.setSize,
            beginName = axis.dockBegin,
            // left or top
            endName = axis.dockEnd,
            // right or bottom
            padding = ownerContext.framePadding,
            border = ownerContext.frameBorder,
            borderBegin = border[beginName],
            framing = ownerContext.framing,
            framingBegin = framing && framing[beginName],
            // The padding is in play unless the axis is collapsed.
            paddingBegin = collapsed ? 0 : padding[beginName],
            sizeProp = axis.sizeProp,
            ignoreFrameBegin = axis.ignoreFrameBegin,
            ignoreFrameEnd = axis.ignoreFrameEnd,
            bodyContext = ownerContext.bodyContext,
            extraPaddingBegin = Math.max(borderBegin + paddingBegin - framingBegin, 0),
            bodyPos, bodySize, delta, dirty;
        if (axis.shrinkWrap) {
            // Since items docked left/top on a shrinkWrap axis go into negative coordinates,
            // we apply a delta to all coordinates to adjust their relative origin back to
            // a (0,0) inside the border.
            bodySize = axis.initialSize;
            if (framing) {
                // In CSS3 mode, things are compartively simple because "framing" is just
                // borders and padding. In non-CSS3 mode, however, the framing elements
                // are given a size equal to the max of the border-width and border-radius
                // and this pushes the body down accordingly. Further, the dockedItems are
                // all rendered outside the framing elements, so their origin equals the
                // ideal box origin. To translate this to match CSS3, we have to add on
                // the border-top.
                delta = -axisBegin + borderBegin + paddingBegin;
                bodyPos = delta - framingBegin - extraPaddingBegin;
            } else {
                bodyPos = -axisBegin;
                delta = bodyPos + paddingBegin;
            }
            if (!collapsed) {
                size += padding[sizeProp];
            }
            if (ignoreFrameBegin) {
                // When some component ignores the begin framing, we move everything "up"
                // by that amount of framing. We also do not include that amount of the
                // framing in the shrinkWrap size.
                delta -= borderBegin;
                bodyPos -= borderBegin;
                // The item ignoring the framing must also escape the padding. Since the
                // axis.delta includes the padding and we want to apply this to only the
                // one item, we just poke its dockedAt.x/y property so that when we add
                // axis.begin the padding will cancel out. (Note: when we are collapsed
                // paddingBegin will be 0).
                ignoreFrameBegin.dockedAt[axis.posProp] -= paddingBegin;
            } else {
                size += borderBegin;
            }
            if (collapsed) {}
            // in this case "ignoreFrameBegin === ignoreFrameEnd" so we can take the
            // special cases out of the mix here...
            else if (ignoreFrameEnd) {
                // When a component ignores the end framing, we simply move it further
                // "down" by the end padding and we do not add the end framing to the
                // shrinkWrap size.
                ignoreFrameEnd.dockedAt[axis.posProp] += padding[endName];
            } else {
                size += border[endName];
            }
            axis.size = size;
            // we have to wait for min/maxWidth/Height processing
            if (!axis.horizontal && !this.owner.manageHeight) {
                // the height of the bodyEl will give the proper height to the outerEl so
                // we don't need to set heights in the DOM
                dirty = false;
            }
        } else {
            // For a fixed-size axis, we started at the outer box and already have the
            // proper origin... almost... except for the owner's border.
            if (framing) {
                // since dockedItems are rendered outside the framing, they have the
                // proper origin already:
                delta = 0;
                bodyPos = axisBegin - framingBegin - extraPaddingBegin;
            } else {
                delta = -borderBegin;
                bodyPos = axisBegin - paddingBegin - borderBegin;
            }
            // Body size is remaining space between ends of Axis.
            bodySize = size;
        }
        axis.delta = delta;
        bodyContext[setSizeMethod](bodySize, dirty);
        bodyContext.setProp(axis.bodyPosProp, bodyPos);
        return !isNaN(size);
    },
    beforeInvalidateShrinkWrapDock: function(itemContext, options) {
        var sizeModelName = options.axis.sizeModelName;
        if (!itemContext[sizeModelName].constrainedMin) {
            // if the child hit a min constraint, it needs to be at its configured size, so
            // we leave the sizeModel alone
            itemContext[sizeModelName] = Ext.layout.SizeModel.calculated;
        }
    },
    afterInvalidateShrinkWrapDock: function(itemContext, options) {
        var axis = options.axis,
            me = options.layout,
            pos;
        if (itemContext[axis.sizeModelName].calculated) {
            pos = me.dockStretch(options.ownerContext, itemContext, itemContext.target, axis);
            itemContext.setProp(axis.posProp, axis.delta + pos);
        }
    },
    /**
     * Finishes processing of each axis by applying the min/max size constraints.
     * @private
     */
    finishConstraints: function(ownerContext, horz, vert) {
        var me = this,
            sizeModels = me.sizeModels,
            publishWidth = horz.shrinkWrap,
            publishHeight = vert.shrinkWrap,
            owner = me.owner,
            dirty, height, width, heightModel, widthModel, size, minSize, maxSize, maxChildSize, desiredSize;
        // In these calculations, maxChildSize will only be > 0 in the scenario where
        // we are dock shrink wrapping in that direction, otherwise it is not measured.
        // As such, the additions are done to simplify the logic, even though in most
        // cases, it will have no impact on the overall result.
        if (publishWidth) {
            size = horz.size;
            minSize = horz.collapsed ? 0 : horz.minSize;
            maxSize = horz.maxSize;
            maxChildSize = horz.maxChildSize;
            desiredSize = Math.max(size, maxChildSize);
            if (desiredSize > maxSize) {
                widthModel = sizeModels.constrainedMax;
                width = maxSize;
            } else if (desiredSize < minSize) {
                widthModel = sizeModels.constrainedMin;
                width = minSize;
            } else if (size < maxChildSize) {
                widthModel = sizeModels.constrainedDock;
                owner.dockConstrainedWidth = width = maxChildSize;
            } else {
                width = size;
            }
        }
        if (publishHeight) {
            size = vert.size;
            minSize = vert.collapsed ? 0 : vert.minSize;
            maxSize = vert.maxSize;
            maxChildSize = vert.maxChildSize;
            // For vertical docks, their weighting means the height is affected by top/bottom
            // docked items, so we need to subtract them here
            desiredSize = Math.max(size, maxChildSize + size - vert.initialSize);
            if (desiredSize > maxSize) {
                heightModel = sizeModels.constrainedMax;
                height = maxSize;
            } else if (desiredSize < minSize) {
                heightModel = sizeModels.constrainedMin;
                height = minSize;
            } else if (size < maxChildSize) {
                heightModel = sizeModels.constrainedDock;
                owner.dockConstrainedHeight = height = maxChildSize;
            } else {
                if (!ownerContext.collapsedVert && !owner.manageHeight) {
                    // height of the outerEl is provided by the height (including margins)
                    // of the bodyEl, so this value does not need to be written to the DOM
                    dirty = false;
                    // so long as we set top and bottom margins on the bodyEl!
                    ownerContext.bodyContext.setProp('margin-bottom', vert.dockedPixelsEnd);
                }
                height = size;
            }
        }
        // Handle the constraints...
        if (widthModel || heightModel) {
            // See ContextItem#init for an analysis of why this case is special. Basically,
            // in this case, we only know the width and the height could be anything.
            if (widthModel && heightModel && widthModel.constrainedMax && heightModel.constrainedByMin) {
                ownerContext.invalidate({
                    widthModel: widthModel
                });
                return false;
            }
            // To process a width or height other than that to which we have shrinkWrapped,
            // we need to invalidate our component and carry forward w/these constrains...
            // unless the ownerLayout wants these results and will invalidate us anyway.
            if (!ownerContext.widthModel.calculatedFromShrinkWrap && !ownerContext.heightModel.calculatedFromShrinkWrap) {
                // nope, just us to handle the constraint...
                ownerContext.invalidate({
                    widthModel: widthModel,
                    heightModel: heightModel
                });
                return false;
            }
        } else // We have a constraint to deal with, so we just adjust the size models and
        // allow the ownerLayout to invalidate us with its contribution to our final
        // size...
        {
            // We're not invalidating, the ownerContext, so if we're shrink wrapping we'll need to
            // tell any docked items to invalidate themselves if necessary.'
            me.invalidateAxes(ownerContext, horz, vert);
        }
        // we only publish the sizes if we are not invalidating the result...
        if (publishWidth) {
            ownerContext.setWidth(width);
            if (widthModel) {
                ownerContext.widthModel = widthModel;
            }
        }
        // important to the ownerLayout
        if (publishHeight) {
            ownerContext.setHeight(height, dirty);
            if (heightModel) {
                ownerContext.heightModel = heightModel;
            }
        }
        // important to the ownerLayout
        return true;
    },
    /**
     * 
     * The default weighting of docked items produces this arrangement:
     * 
     *      +--------------------------------------------+
     *      |                    Top 1                   |
     *      +--------------------------------------------+
     *      |                    Top 2                   |
     *      +-----+-----+--------------------+-----+-----+
     *      |     |     |                    |     |     |
     *      |     |     |                    |     |     |
     *      |     |     |                    |  R  |  R  |
     *      |  L  |  L  |                    |  I  |  I  |
     *      |  E  |  E  |                    |  G  |  G  |
     *      |  F  |  F  |                    |  H  |  H  |
     *      |  T  |  T  |                    |  T  |  T  |
     *      |     |     |                    |     |     |
     *      |  2  |  1  |                    |  1  |  2  |
     *      |     |     |                    |     |     |
     *      |     |     |                    |     |     |
     *      +-----+-----+--------------------+-----+-----+
     *      |                  Bottom 1                  |
     *      +--------------------------------------------+
     *      |                  Bottom 2                  |
     *      +--------------------------------------------+
     * 
     * So when we are shrinkWrapDock on the horizontal, the stretch size for top/bottom
     * docked items is the final axis size. For the vertical axis, however, the stretch
     *
     */
    invalidateAxes: function(ownerContext, horz, vert) {
        var before = this.beforeInvalidateShrinkWrapDock,
            after = this.afterInvalidateShrinkWrapDock,
            horzSize = horz.end - horz.begin,
            vertSize = vert.initialSize,
            invalidateHorz = horz.shrinkWrapDock && horz.maxChildSize <= horzSize,
            invalidateVert = vert.shrinkWrapDock && vert.maxChildSize <= vertSize,
            dockedItems, len, i, itemContext, itemSize, isHorz, axis, sizeProp;
        if (invalidateHorz || invalidateVert) {
            if (invalidateVert) {
                // For vertical, we need to reset the initial position because they are affected
                // by the horizontally docked items
                vert.begin = vert.initialBegin;
                vert.end = vert.begin + vert.initialSize;
            }
            dockedItems = ownerContext.dockedItems;
            for (i = 0 , len = dockedItems.length; i < len; ++i) {
                itemContext = dockedItems[i];
                isHorz = itemContext.horizontal;
                axis = null;
                if (invalidateHorz && isHorz) {
                    sizeProp = horz.sizeProp;
                    itemSize = horzSize;
                    axis = horz;
                } else if (invalidateVert && !isHorz) {
                    sizeProp = vert.sizeProp;
                    itemSize = vertSize;
                    axis = vert;
                }
                if (axis) {
                    // subtract any margins
                    itemSize -= itemContext.getMarginInfo()[sizeProp];
                    if (itemSize !== itemContext.props[sizeProp]) {
                        itemContext.invalidate({
                            before: before,
                            after: after,
                            axis: axis,
                            ownerContext: ownerContext,
                            layout: this
                        });
                    }
                }
            }
        }
    },
    /**
     * Finishes the calculation by setting positions on the body and all of the items.
     * @private
     */
    finishPositions: function(ownerContext, horz, vert) {
        var dockedItems = ownerContext.dockedItems,
            length = dockedItems.length,
            deltaX = horz.delta,
            deltaY = vert.delta,
            index, itemContext;
        for (index = 0; index < length; ++index) {
            itemContext = dockedItems[index];
            itemContext.setProp('x', deltaX + itemContext.dockedAt.x);
            itemContext.setProp('y', deltaY + itemContext.dockedAt.y);
        }
    },
    finishedLayout: function(ownerContext) {
        var me = this,
            target = ownerContext.target;
        me.callParent(arguments);
        if (!ownerContext.animatePolicy) {
            if (ownerContext.isCollapsingOrExpanding === 1) {
                target.afterCollapse(false);
            } else if (ownerContext.isCollapsingOrExpanding === 2) {
                target.afterExpand(false);
            }
        }
    },
    getAnimatePolicy: function(ownerContext) {
        var me = this,
            lastCollapsedState, policy;
        if (ownerContext.isCollapsingOrExpanding === 1) {
            lastCollapsedState = me.lastCollapsedState;
        } else if (ownerContext.isCollapsingOrExpanding === 2) {
            lastCollapsedState = ownerContext.lastCollapsedState;
        }
        if (lastCollapsedState === 'left' || lastCollapsedState === 'right') {
            policy = me.horizontalCollapsePolicy;
        } else if (lastCollapsedState === 'top' || lastCollapsedState === 'bottom') {
            policy = me.verticalCollapsePolicy;
        }
        return policy;
    },
    /**
     * Retrieve an ordered and/or filtered array of all docked Components.
     * @param {String} [order='render'] The desired ordering of the items ('render' or 'visual').
     * @param {Boolean} [beforeBody] An optional flag to limit the set of items to only those
     *  before the body (true) or after the body (false). All components are returned by
     *  default.
     * @return {Ext.Component[]} An array of components.
     * @protected
     */
    getDockedItems: function(order, beforeBody) {
        var me = this,
            renderedOnly = (order === 'visual'),
            all = renderedOnly ? Ext.ComponentQuery.query('[rendered]', me.owner.dockedItems.items) : me.owner.dockedItems.items,
            sort = all && all.length && order !== false,
            renderOrder, dock, dockedItems, i, isBefore, length;
        if (beforeBody == null) {
            dockedItems = sort && !renderedOnly ? all.slice() : all;
        } else {
            dockedItems = [];
            for (i = 0 , length = all.length; i < length; ++i) {
                dock = all[i].dock;
                isBefore = (dock === 'top' || dock === 'left');
                if (beforeBody ? isBefore : !isBefore) {
                    dockedItems.push(all[i]);
                }
            }
            sort = sort && dockedItems.length;
        }
        if (sort) {
            renderOrder = (order = order || 'render') === 'render';
            Ext.Array.sort(dockedItems, function(a, b) {
                var aw, bw;
                // If the two items are on opposite sides of the body, they must not be sorted
                // by any weight value:
                // For rendering purposes, left/top *always* sorts before right/bottom
                // eslint-disable-next-line max-len
                if (renderOrder && ((aw = me.owner.dockOrder[a.dock]) !== (bw = me.owner.dockOrder[b.dock]))) {
                    // The two dockOrder values cancel out when two items are on opposite sides.
                    if (!(aw + bw)) {
                        return aw - bw;
                    }
                }
                aw = me.getItemWeight(a, order);
                bw = me.getItemWeight(b, order);
                if ((aw !== undefined) && (bw !== undefined)) {
                    return aw - bw;
                }
                return 0;
            });
        }
        return dockedItems || [];
    },
    getItemWeight: function(item, order) {
        var weight = item.weight || this.owner.defaultDockWeights[item.dock];
        return weight[order] || weight;
    },
    /**
     * @protected
     * Returns an array containing all the **visible** docked items inside this layout's owner Panel
     * @return {Array} An array containing all the **visible** docked items of the Panel
     */
    getLayoutItems: function() {
        var me = this,
            items, itemCount, item, i, result;
        if (me.owner.collapsed) {
            result = me.owner.getCollapsedDockedItems();
        } else {
            items = me.getDockedItems('visual');
            itemCount = items.length;
            result = [];
            for (i = 0; i < itemCount; i++) {
                item = items[i];
                if (!item.hidden) {
                    result.push(item);
                }
            }
        }
        return result;
    },
    // Content size includes padding but not borders, so subtract them off
    measureContentWidth: function(ownerContext) {
        var bodyContext = ownerContext.bodyContext;
        return bodyContext.el.getWidth() - bodyContext.getBorderInfo().width;
    },
    measureContentHeight: function(ownerContext) {
        var bodyContext = ownerContext.bodyContext;
        return bodyContext.el.getHeight() - bodyContext.getBorderInfo().height;
    },
    redoLayout: function(ownerContext) {
        var me = this,
            owner = me.owner;
        // If we are collapsing...
        if (ownerContext.isCollapsingOrExpanding === 1) {
            if (owner.reExpander) {
                owner.reExpander.el.show();
            }
            // Add the collapsed class now, so that collapsed CSS rules
            // are applied before measurements are taken by the layout.
            owner.addClsWithUI(owner.collapsedCls);
            ownerContext.redo(true);
        } else if (ownerContext.isCollapsingOrExpanding === 2) {
            // Remove the collapsed class now, before layout calculations are done.
            owner.removeClsWithUI(owner.collapsedCls);
            ownerContext.bodyContext.redo();
        }
    },
    getRenderTarget: function() {
        return this.owner.bodyWrap;
    },
    /**
     * @private
     * Used to render in the correct order, top/left before bottom/right
     */
    renderChildren: function() {
        var me = this,
            items = me.getDockedItems(),
            target = me.getRenderTarget();
        me.handleItemBorders();
        me.renderItems(items, target);
    },
    /**
     * Render the top and left docked items before any existing DOM nodes in our render
     * target and then render the right and bottom docked items after. This is important,
     * for such things as tab stops and ARIA readers, that the DOM nodes are in a
     * meaningful order.
     *
     * Our collection of docked items will already be ordered via Panel.getDockedItems().
     * @protected
     */
    renderItems: function(items, target) {
        var me = this,
            owner = me.owner,
            dockedItemIds = {},
            dockedItemCount = items.length,
            bodyDom = owner.body.dom,
            bodyWrapDom = owner.bodyWrap.dom,
            hasFrame = !!owner.frameSize,
            bodyContainer = owner.bodyContainer,
            childNodes, childNodeCount, hasDockedToEl, item, dom, elFound, gap, bodyBaseIndex, bodyDockIndex, wrapBaseIndex, wrapDockIndex, i, insertPosition, insertTarget;
        // TODO: This is affected if users provide custom weight values to their
        // docked items, which puts it out of (t,l,r,b) order. Avoiding a second
        // sort operation here, for now, in the name of performance. getDockedItems()
        // needs the sort operation not just for this layout-time rendering, but
        // also for getRefItems() to return a logical ordering (CQ, et al).
        if (dockedItemCount) {
            // Build a correct map of docked item ids to match with the ID of found DOM elements.
            // The "map" property of the dockedItems Collection uses the *itemId* as the key, so 
            // that will never match, and will cause isValidParent to return false, resulting
            // in DOM motion.
            for (i = 0; i < dockedItemCount; i++) {
                item = items[i];
                dockedItemIds[item.id] = item;
                if (item.dockToEl) {
                    hasDockedToEl = true;
                }
            }
            childNodes = me.getRenderTarget().dom.childNodes;
            childNodeCount = childNodes.length;
            gap = 0;
            // Our childNodes may contain non-item elements as well as the bodyEl. We
            // want to find the bodyEl's index and count the number of dockedItems that
            // are already rendered before it.
            //
            for (i = 0; i < childNodeCount; ++i) {
                dom = childNodes[i];
                // Regardless of whether framing is used, bodyEl will be contained
                // in the bodyWrap element.
                elFound = dom === bodyDom;
                if (elFound) {
                    // We want top/left dockedItems to be inserted before the body, so
                    // use the bodyEl's index as the base.
                    bodyBaseIndex = i;
                    break;
                }
                if (dockedItemIds[dom.id]) {
                    ++gap;
                }
            }
            if (!elFound) {
                Ext.log.error('Dock layout error for ' + owner.id + ': bodyEl not found!');
            }
            // Subtract the number of rendered dockedItems from the bodyEl's index to get
            // the actual base.
            //
            bodyBaseIndex -= gap;
            // If we have any items docked to the main el (presently Panel header only),
            // we want to find bodyWrap element base index the same way as we did for
            // the body element.
            //
            if (hasDockedToEl) {
                elFound = false;
                gap = 0;
                childNodes = owner.el.dom.childNodes;
                childNodeCount = childNodes.length;
                for (i = 0; i < childNodeCount; i++) {
                    dom = childNodes[i];
                    // When extra framing elements are used, bodyWrap will be contained
                    // in the frameBody, which in turn might be contained in other
                    // framing elements.
                    if (hasFrame) {
                        elFound = dom === bodyWrapDom || dom === bodyContainer;
                        // Cache the found body container to speed up subsequent layouts
                        if (!elFound && Ext.fly(dom).contains(bodyWrapDom)) {
                            elFound = true;
                            owner.bodyContainer = dom;
                        }
                    } else {
                        elFound = dom === bodyWrapDom;
                    }
                    if (elFound) {
                        wrapBaseIndex = i;
                        break;
                    }
                    if (dockedItemIds[dom.id]) {
                        ++gap;
                    }
                }
                if (!elFound) {
                    Ext.log.error('Dock layout error for ' + owner.id + ': bodyWrapEl not found!');
                }
                // We need to adjust wrapBaseIndex the same way as bodyBaseIndex above,
                // because docked-to-el header may already exist when this method is called.
                wrapBaseIndex -= gap;
            }
            bodyDockIndex = wrapDockIndex = 0;
            // Finally loop over the dockedItems again and ensure that the top/left and
            // bottom/right items are at the proper DOM offset, immediately surrounding
            // the bodyEl or bodyWrap if they're docked to the main el.
            // Presently only Panel header does that.
            //
            for (i = 0; i < dockedItemCount; i++) {
                item = items[i];
                // Header is rendered to the el instead of bodyWrap
                if (item.dockToEl) {
                    insertTarget = owner.el;
                    insertPosition = wrapBaseIndex + wrapDockIndex++;
                    if (item.dock === 'right' || item.dock === 'bottom') {
                        insertPosition++;
                        // skip over the bodyWrap
                        // frameBody consists of 3 elements, wrapBaseIndex points
                        // to the middle one. Skip the bottom one, too.
                        if (hasFrame) {
                            insertPosition++;
                        }
                    } else if (hasFrame && insertPosition > 0) {
                        // Skip top frameBody element
                        insertPosition--;
                    }
                } else {
                    insertTarget = target;
                    insertPosition = bodyBaseIndex + bodyDockIndex++;
                    if (item.dock === 'right' || item.dock === 'bottom') {
                        insertPosition++;
                    }
                }
                // skip over the bodyEl or frameBody
                // Same logic as Layout.renderItems()
                if (!item.rendered) {
                    me.renderItem(item, insertTarget, insertPosition);
                } else if (!me.isValidParent(item, insertTarget, insertPosition)) {
                    me.moveItem(item, insertTarget, insertPosition);
                }
            }
        }
    },
    undoLayout: function(ownerContext) {
        var me = this,
            owner = me.owner;
        // If we are collapsing...
        if (ownerContext.isCollapsingOrExpanding === 1) {
            // We do not want to see the re-expander header until the final collapse is complete
            if (owner.reExpander) {
                owner.reExpander.el.hide();
            }
            // Add the collapsed class now, so that collapsed CSS rules are applied
            // before measurements are taken by the layout.
            owner.removeClsWithUI(owner.collapsedCls);
            ownerContext.undo(true);
        } else if (ownerContext.isCollapsingOrExpanding === 2) {
            // Remove the collapsed class now, before layout calculations are done.
            owner.addClsWithUI(owner.collapsedCls);
            ownerContext.bodyContext.undo();
        }
    },
    sizePolicy: {
        nostretch: {
            setsWidth: 0,
            setsHeight: 0
        },
        horz: {
            // item goes horizontally (top or bottom docked)
            shrinkWrap: {
                // This is how we manage the width of a top/bottom docked item when its
                // shrinkWrapWidth and ours need to be maxed (calculatedFromShrinkWrap)
                setsWidth: 1,
                setsHeight: 0,
                readsWidth: 1
            },
            stretch: {
                setsWidth: 1,
                setsHeight: 0
            }
        },
        vert: {
            // item goes vertically (left or right docked)
            shrinkWrap: {
                setsWidth: 0,
                setsHeight: 1,
                readsHeight: 1
            },
            stretch: {
                setsWidth: 0,
                setsHeight: 1
            }
        },
        stretchV: {
            setsWidth: 0,
            setsHeight: 1
        },
        // Circular dependency with partial auto-sized panels:
        //
        // If we have an autoHeight docked item being stretched horizontally (top/bottom),
        // that stretching will determine its width and its width must be set before its
        // autoHeight can be determined. If that item is docked in an autoWidth panel, the
        // body will need its height set before it can determine its width, but the height
        // of the docked item is needed to subtract from the panel height in order to set
        // the body height.
        //
        // This same pattern occurs with autoHeight panels with autoWidth docked items on
        // left or right. If the panel is fully auto or fully fixed, these problems don't
        // come up because there is no dependency between the dimensions.
        //
        // Cutting the Gordian Knot: In these cases, we have to allow something to measure
        // itself without full context. This is OK as long as the managed dimension doesn't
        // effect the auto-dimension, which is often the case for things like toolbars. The
        // managed dimension only effects overflow handlers and such and does not change the
        // auto-dimension. To encourage the item to measure itself without waiting for the
        // managed dimension, we have to tell it that the layout will also be reading that
        // dimension. This is similar to how stretchmax works.
        autoStretchH: {
            readsWidth: 1,
            setsWidth: 1,
            setsHeight: 0
        },
        autoStretchV: {
            readsHeight: 1,
            setsWidth: 0,
            setsHeight: 1
        }
    },
    getItemSizePolicy: function(item, ownerSizeModel) {
        var me = this,
            policy = me.sizePolicy,
            shrinkWrapDock = me.owner.shrinkWrapDock,
            dock, vertical;
        if (item.stretch === false) {
            return policy.nostretch;
        }
        dock = item.dock;
        vertical = (dock === 'left' || dock === 'right');
        shrinkWrapDock = shrinkWrapDock === true ? 3 : (shrinkWrapDock || 0);
        if (vertical) {
            policy = policy.vert;
            shrinkWrapDock = shrinkWrapDock & 1;
        } else {
            policy = policy.horz;
            shrinkWrapDock = shrinkWrapDock & 2;
        }
        if (shrinkWrapDock) {
            // Getting the size model is expensive, so only do so if we really need it
            if (!ownerSizeModel) {
                ownerSizeModel = me.owner.getSizeModel();
            }
            if (ownerSizeModel[vertical ? 'height' : 'width'].shrinkWrap) {
                return policy.shrinkWrap;
            }
        }
        return policy.stretch;
    },
    /**
     * @protected
     * We are overriding the Ext.layout.Layout configureItem method to also add a class that
     * indicates the position of the docked item. We use the itemCls (x-docked) as a prefix.
     * An example of a class added to a dock: right item is x-docked-right
     * @param {Ext.Component} item The item we are configuring
     * @param pos
     */
    configureItem: function(item, pos) {
        this.callParent(arguments);
        item.addCls(this._itemCls);
        if (!item.ignoreBorderManagement) {
            item.addClsWithUI(this.getDockCls(item.dock));
        }
    },
    /**
     * Get's the css class name for a given dock position.
     * @param {String} dock `top`, `right`, `bottom`, or `left`
     * @return {String}
     * @private 
     */
    getDockCls: function(dock) {
        return 'docked-' + dock;
    },
    afterRemove: function(item) {
        var dom;
        this.callParent(arguments);
        item.removeCls(this._itemCls);
        if (!item.ignoreBorderManagement) {
            item.removeClsWithUI(this.getDockCls(item.dock));
        }
        dom = item.el && item.el.dom;
        if (dom && !item.destroying) {
            dom.parentNode.removeChild(dom);
        }
        this.childrenChanged = true;
    },
    /**
     * This object is indexed by a component's `baseCls` to yield another object which
     * is then indexed by the component's `ui` to produce an array of CSS class names.
     * This array is indexed in the same manner as the `noBorderClassTable` and indicates
     * the a particular edge of a docked item or the body element is actually "collapsed"
     * with the component's outer border.
     * @private
     */
    borderCollapseMap: {},
    /*
        'x-panel': {
            'default': []
        }
        */
    /**
     * Returns the array of class names to add to a docked item or body element when for
     * the edges that should collapse with the outer component border. Basically, the
     * panel's outer border must look visually like a contiguous border but may need to
     * be realized by using the border of docked items and/or the body. This class name
     * allows the border color and width to be controlled accordingly and distinctly from
     * the border of the docked item or body element when it is not having its border
     * collapsed.
     * @private
     */
    getBorderCollapseTable: function() {
        var me = this,
            map = me.borderCollapseMap,
            owner = me.owner,
            baseCls = owner.baseCls,
            ui = owner.ui,
            table;
        map = map[baseCls] || (map[baseCls] = {});
        table = map[ui];
        if (!table) {
            baseCls += '-' + ui + '-outer-border-';
            map[ui] = table = [
                0,
                // TRBL
                baseCls + 'l',
                // 0001 = 1
                baseCls + 'b',
                // 0010 = 2
                baseCls + 'bl',
                // 0011 = 3
                baseCls + 'r',
                // 0100 = 4
                baseCls + 'rl',
                // 0101 = 5
                baseCls + 'rb',
                // 0110 = 6
                baseCls + 'rbl',
                // 0111 = 7
                baseCls + 't',
                // 1000 = 8
                baseCls + 'tl',
                // 1001 = 9
                baseCls + 'tb',
                // 1010 = 10
                baseCls + 'tbl',
                // 1011 = 11
                baseCls + 'tr',
                // 1100 = 12
                baseCls + 'trl',
                // 1101 = 13
                baseCls + 'trb',
                // 1110 = 14
                baseCls + 'trbl'
            ];
        }
        // 1111 = 15
        return table;
    }
});

/**
 * @class Ext.util.Memento
 * This class manages a set of captured properties from an object. These captured properties
 * can later be restored to an object.
 */
Ext.define('Ext.util.Memento', (function() {
    function captureOne(src, target, prop, prefix) {
        src[prefix ? prefix + prop : prop] = target[prop];
    }
    function removeOne(src, target, prop) {
        delete src[prop];
    }
    function restoreOne(src, target, prop, prefix) {
        var name = prefix ? prefix + prop : prop,
            value = src[name];
        if (value || src.hasOwnProperty(name)) {
            restoreValue(target, prop, value);
        }
    }
    function restoreValue(target, prop, value) {
        if (Ext.isDefined(value)) {
            target[prop] = value;
        } else {
            delete target[prop];
        }
    }
    function doMany(doOne, src, target, props, prefix) {
        var p, pLen;
        if (src) {
            if (Ext.isArray(props)) {
                for (p = 0 , pLen = props.length; p < pLen; p++) {
                    doOne(src, target, props[p], prefix);
                }
            } else {
                doOne(src, target, props, prefix);
            }
        }
    }
    return {
        /**
         * @property data
         * The collection of captured properties.
         * @private
         */
        data: null,
        /**
         * @property target
         * The default target object for capture/restore (passed to the constructor).
         */
        target: null,
        /**
         * Creates a new memento and optionally captures properties from the target object.
         * @param {Object} target The target from which to capture properties. If specified in the
         * constructor, this target becomes the default target for all other operations.
         * @param {String/String[]} props The property or array of properties to capture.
         */
        constructor: function(target, props) {
            this.data = {};
            if (target) {
                this.target = target;
                if (props) {
                    this.capture(props);
                }
            }
        },
        /**
         * Captures the specified properties from the target object in this memento.
         * @param {String/String[]} props The property or array of properties to capture.
         * @param {Object} target The object from which to capture properties.
         * @param {String} prefix
         */
        capture: function(props, target, prefix) {
            var me = this;
            doMany(captureOne, me.data || (me.data = {}), target || me.target, props, prefix);
        },
        /**
         * Removes the specified properties from this memento. These properties will not be
         * restored later without re-capturing their values.
         * @param {String/String[]} props The property or array of properties to remove.
         */
        remove: function(props) {
            doMany(removeOne, this.data, null, props);
        },
        /**
         * Restores the specified properties from this memento to the target object.
         * @param {String/String[]} props The property or array of properties to restore.
         * @param {Boolean} clear True to remove the restored properties from this memento or
         * false to keep them (default is true).
         * @param {Object} target The object to which to restore properties.
         * @param {String} prefix
         */
        restore: function(props, clear, target, prefix) {
            doMany(restoreOne, this.data, target || this.target, props, prefix);
            if (clear !== false) {
                this.remove(props);
            }
        },
        /**
         * Restores all captured properties in this memento to the target object.
         * @param {Boolean} clear True to remove the restored properties from this memento or
         * false to keep them (default is true).
         * @param {Object} target The object to which to restore properties.
         */
        restoreAll: function(clear, target) {
            var me = this,
                t = target || this.target,
                data = me.data,
                prop;
            clear = clear !== false;
            for (prop in data) {
                if (data.hasOwnProperty(prop)) {
                    restoreValue(t, prop, data[prop]);
                    if (clear) {
                        delete data[prop];
                    }
                }
            }
        }
    };
}()));

/**
 *
 */
Ext.define('Ext.container.DockingContainer', {
    requires: [
        'Ext.util.MixedCollection',
        'Ext.Element'
    ],
    isDockingContainer: true,
    /**
     * @event dockedadd
     * Fires when any {@link Ext.Component} is added or inserted as a docked item.
     * @param {Ext.panel.Panel} this
     * @param {Ext.Component} component The component being added
     * @param {Number} index The index at which the component will be added docked items collection
     */
    /**
     * @event dockedremove
     * Fires when any {@link Ext.Component} is removed from the docked items.
     * @param {Ext.panel.Panel} this
     * @param {Ext.Component} component The component being removed
     */
    /**
     * @cfg {Object} defaultDockWeights
     * This object holds the default weights applied to dockedItems that have no weight.
     * These start with a weight of 1, to allow negative weights to insert before top items
     * and are odd numbers so that even weights can be used to get between different dock orders.
     *
     * To make default docking order match border layout, do this:
     *
     *      Ext.panel.Panel.prototype.defaultDockWeights = { top: 1, bottom: 3, left: 5, right: 7 };
     *
     * Changing these defaults as above or individually on this object will effect all Panels.
     * To change the defaults on a single panel, you should replace the entire object:
     *
     *      initComponent: function() {
     *          // NOTE: Don't change members of defaultDockWeights since the object is shared.
     *          this.defaultDockWeights = { top: 1, bottom: 3, left: 5, right: 7 };
     *
     *          this.callParent();
     *      }
     *
     * To change only one of the default values, you do this:
     *
     *      initComponent: function() {
     *          // NOTE: Don't change members of defaultDockWeights since the object is shared.
     *          this.defaultDockWeights = Ext.applyIf({ top: 10 }, this.defaultDockWeights);
     * 
     *          this.callParent();
     *      }
     */
    defaultDockWeights: {
        top: {
            render: 1,
            visual: 1
        },
        left: {
            render: 3,
            visual: 5
        },
        right: {
            render: 5,
            visual: 7
        },
        bottom: {
            render: 7,
            visual: 3
        }
    },
    /**
     * @private
     * Values to decide which side of the body element docked items must go
     * This overides any weight. A left/top will *always* sort before a right/bottom
     * regardless of any weight value. Weights sort at either side of the "body" dividing point.
     */
    dockOrder: {
        top: -1,
        left: -1,
        right: 1,
        bottom: 1
    },
    /**
     * @private
     * Number of dock 'left' and 'right' items.
     */
    horizontalDocks: 0,
    /**
     * Adds docked item(s) to the container.
     *
     * @param {Object/Object[]} items The Component or array of components to add. The components
     * must include a 'dock' parameter on each component to indicate where it should be docked
     * ('top', 'right', 'bottom', 'left').
     * @param {Number} [pos] The index at which the Component will be added
     * @return {Ext.Component[]} The added components.
     */
    addDocked: function(items, pos) {
        var me = this,
            rendered = me.rendered,
            i = 0,
            dockedItems = me.dockedItems,
            lastIndex = dockedItems.getCount(),
            index, instanced, item, length;
        items = me.prepareItems(items);
        length = items.length;
        if (rendered) {
            Ext.suspendLayouts();
        }
        if (pos === undefined) {
            pos = lastIndex;
        } else {
            pos = Math.min(pos, lastIndex);
        }
        for (; i < length; i++) {
            item = items[i];
            if (item.isDetached) {
                item.reattachToBody();
            }
            item.dock = item.dock || 'top';
            if (item.dock === 'left' || item.dock === 'right') {
                me.horizontalDocks++;
            }
            index = pos + i;
            dockedItems.insert(index, item);
            instanced = !!item.instancedCmp;
            delete item.instancedCmp;
            item.onAdded(me, index, instanced);
            delete item.$initParent;
            if (me.onDockedAdd !== Ext.emptyFn) {
                me.onDockedAdd(item);
            }
            if (me.hasListeners.dockedadd) {
                me.fireEvent('dockedadd', me, item, index);
            }
        }
        if (me.rendered) {
            me.updateLayout();
            Ext.resumeLayouts(true);
        }
        return items;
    },
    destroyDockedItems: function() {
        var dockedItems = this.dockedItems,
            c;
        if (dockedItems) {
            while ((c = dockedItems.first())) {
                this.removeDocked(c, {
                    destroy: true
                });
            }
        }
    },
    doRenderDockedItems: function(out, renderData, after) {
        // Careful! This method is bolted on to the frameTpl and renderTpl so all we get for
        // context is the renderData! The "this" pointer is either the frameTpl or the
        // renderTpl instance!
        var me = renderData.$comp,
            layout = me.componentLayout,
            // eslint-disable-next-line no-unused-vars
            tabGuard = me.tabGuard && me.lookupTpl('tabGuardTpl'),
            items, tree;
        if (layout.getDockedItems) {
            items = layout.getDockedItems('render', !after);
            tree = items && layout.getItemsRenderTree(items);
            if (tree) {
                Ext.DomHelper.generateMarkup(tree, out);
            }
        }
    },
    /**
     * Finds a docked component by id, itemId or position. Also see {@link #getDockedItems}
     * @param {String/Number} comp The id, itemId or position of the docked component
     * (see {@link Ext.container.Container#getComponent getComponent} for details)
     * @return {Ext.Component} The docked component (if found)
     */
    getDockedComponent: function(comp) {
        if (Ext.isObject(comp)) {
            comp = comp.getItemId();
        }
        return this.dockedItems.get(comp);
    },
    /**
     * Retrieves an array of all currently docked Components.
     *
     * For example to find a toolbar that has been docked at top:
     *
     *     panel.getDockedItems('toolbar[dock="top"]');
     *
     * @param {String} selector A {@link Ext.ComponentQuery ComponentQuery} selector string
     * to filter the returned items.
     * @param {Boolean} beforeBody An optional flag to limit the set of items to only those
     * before the body (true) or after the body (false). All components are returned by default.
     * @return {Ext.Component[]} The array of docked components meeting the specified criteria.
     */
    getDockedItems: function(selector, beforeBody) {
        var dockedItems = this.getComponentLayout().getDockedItems('render', beforeBody);
        if (selector && dockedItems.length) {
            dockedItems = Ext.ComponentQuery.query(selector, dockedItems);
        }
        return dockedItems;
    },
    getDockingRefItems: function(deep, containerItems) {
        // deep fetches the docked items and their descendants using '*' and then '* *'
        var selector = deep && '*,* *',
            // start with only the top/left docked items (and maybe their children)
            dockedItems = this.getDockedItems(selector, true),
            items;
        // push container items (and maybe their children) after top/left docked items:
        dockedItems.push.apply(dockedItems, containerItems);
        // push right/bottom docked items (and maybe their children) after container items:
        items = this.getDockedItems(selector, false);
        dockedItems.push.apply(dockedItems, items);
        return dockedItems;
    },
    /**
     * @protected
     * Initializes an Item Collection for docked items to be inserted into.  If items
     * already exist, they will be added to the new collection.
     */
    initDockingItems: function() {
        var me = this,
            items = me.dockedItems;
        if (!items || !items.isMixedCollection) {
            me.dockedItems = new Ext.util.ItemCollection();
            if (items) {
                me.addDocked(items);
            }
        }
    },
    /**
     * Inserts docked item(s) to the panel at the indicated position.
     * @param {Number} pos The index at which the Component will be inserted
     * @param {Object/Object[]} items The Component or array of components to add. The components
     * must include a 'dock' paramater on each component to indicate where it should be docked
     * ('top', 'right', 'bottom', 'left').
     */
    insertDocked: function(pos, items) {
        this.addDocked(items, pos);
    },
    // Placeholder empty functions
    /**
     * @method
     * Invoked after a docked item is added to the Panel.
     * @param {Ext.Component} component
     * @template
     * @protected
     */
    onDockedAdd: Ext.emptyFn,
    /**
     * @method
     * Invoked after a docked item is removed from the Panel.
     * @param {Ext.Component} component
     * @template
     * @protected
     */
    onDockedRemove: Ext.emptyFn,
    /**
     * Removes the docked item from the panel.
     * @param {Ext.Component} item The Component to remove.
     * @param {Boolean/Object} [flags] Pass `true` to destroy the item or an
     * object with multiple options. If not passed, {@link #cfg!autoDestroy} will
     * determine if the item is destroyed.
     * @param {Boolean} [flags.destroy] Pass `true` to destroy the item.
     * @param {Boolean} [flags.detach] Pass `true` to put the item in the
     * {@link Ext#getDetachedBody detachedBody element}.
     */
    removeDocked: function(item, flags) {
        var me = this,
            layout, hasLayout, doDestroy, doDetach;
        if (!me.dockedItems.contains(item)) {
            return item;
        }
        // Ensure the flags are set correctly 
        if (flags == null) {
            doDestroy = me.autoDestroy;
        } else if (typeof flags === 'boolean') {
            doDestroy = flags;
        } else {
            doDestroy = ('destroy' in flags) && flags.destroy;
            doDetach = ('detach' in flags) && flags.detach;
        }
        if (item.dock === 'left' || item.dock === 'right') {
            me.horizontalDocks--;
        }
        layout = me.componentLayout;
        hasLayout = layout && me.rendered;
        if (hasLayout) {
            layout.onRemove(item);
        }
        me.dockedItems.remove(item);
        // destroying flag is true if the removal is taking place as part of destruction,
        // OR if removal is intended to *cause* destruction
        if (!item.destroyed) {
            item.onRemoved(item.destroying || doDestroy);
        }
        me.onDockedRemove(item);
        if (doDestroy) {
            item.destroy();
        } else if (!me.destroyed) {
            if (hasLayout) {
                // not destroying, make any layout related removals
                layout.afterRemove(item);
            }
            if (doDetach && item.rendered) {
                item.detachFromBody();
            }
        }
        if (me.hasListeners.dockedremove) {
            me.fireEvent('dockedremove', me, item);
        }
        if (!me.destroying) {
            me.updateLayout();
        }
        return item;
    },
    /**
     * Moves a docked item to a different side.
     * @param {Ext.Component} item
     * @param {'top'/'right'/'bottom'/'left'} side
     * @private
     */
    moveDocked: function(item, side) {
        var me = this;
        if (me.rendered) {
            Ext.suspendLayouts();
        }
        me.removeDocked(item, {
            destroy: false,
            detach: true
        });
        item.dock = side;
        me.addDocked(item);
        if (me.rendered) {
            if (item.frame) {
                item.updateFrame();
            }
            Ext.resumeLayouts(true);
        }
    },
    setupDockingRenderTpl: function(renderTpl) {
        renderTpl.renderDockedItems = this.doRenderDockedItems;
    }
});

/**
 * Panel is a container that has specific functionality and structural components that make it
 * the perfect building block for application-oriented user interfaces.
 *
 * Panels are, by virtue of their inheritance from {@link Ext.container.Container}, capable of being
 * configured with a {@link Ext.container.Container#layout layout}, and containing child Components.
 *
 * When either specifying child {@link #cfg-items} of a Panel, or dynamically
 * {@link Ext.container.Container#method-add adding} Components to a Panel, remember to consider
 * how you wish the Panel to arrange those child elements, and whether those child elements
 * need to be sized using one of Ext's built-in `{@link Ext.container.Container#layout layout}`
 * schemes. By default, Panels use the {@link Ext.layout.container.Auto Auto} scheme. This simply
 * renders child components, appending them one after the other inside the Container,
 * and **does not apply any sizing** at all.
 *
 * {@img Ext.panel.Panel/panel.png Panel components}
 *
 * A Panel may also contain {@link #bbar bottom} and {@link #tbar top} toolbars, along with separate
 * {@link Ext.panel.Header header}, {@link #fbar footer} and body sections.
 *
 * Panel also provides built-in {@link #collapsible collapsible, expandable} and {@link #closable}
 * behavior. Panels can be easily dropped into any {@link Ext.container.Container Container}
 * or layout, and the layout and rendering pipeline is
 * {@link Ext.container.Container#method-add completely managed by the framework}.
 *
 * **Note:** By default, the `{@link #closable close}` header tool _destroys_ the Panel resulting
 * in removal of the Panel and the destruction of any descendant Components. This makes the Panel
 * object, and all its descendants **unusable**. To enable the close tool to simply _hide_ a Panel
 * for later re-use, configure the Panel with `{@link #closeAction closeAction}: 'hide'`.
 *
 * Usually, Panels are used as constituents within an application, in which case, they would be used
 * as child items of Containers, and would themselves use Ext.Components as child
 * {@link #cfg-items}. However to illustrate simply rendering a Panel into the document,
 * here's how to do it:
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *         title: 'Hello',
 *         width: 200,
 *         html: '<p>World!</p>',
 *         renderTo: Ext.getBody()
 *     });
 *
 * A more realistic scenario is a Panel created to house input fields which will not be rendered,
 * but used as a constituent part of a Container:
 *
 *     @example
 *     var filterPanel = Ext.create('Ext.panel.Panel', {
 *         bodyPadding: 5,  // Don't want content to crunch against the borders
 *         width: 300,
 *         title: 'Filters',
 *         items: [{
 *             xtype: 'datefield',
 *             fieldLabel: 'Start date'
 *         }, {
 *             xtype: 'datefield',
 *             fieldLabel: 'End date'
 *         }],
 *         renderTo: Ext.getBody()
 *     });
 *
 * Note that the Panel above is configured to render into the document and assigned a size.
 * In a real world scenario, the Panel will often be added inside a Container which will use a
 * {@link #layout} to render, size and position its child Components.
 *
 * Panels will often use specific {@link #layout}s to provide an application with shape
 * and structure by containing and arranging child Components:
 *
 *     @example
 *     var resultsPanel = Ext.create('Ext.panel.Panel', {
 *         title: 'Results',
 *         width: 600,
 *         height: 400,
 *         renderTo: Ext.getBody(),
 *         layout: {
 *             type: 'vbox',       // Arrange child items vertically
 *             align: 'stretch',    // Each takes up full width
 *             padding: 5
 *         },
 *         items: [{
 *             // Results grid specified as a config object with an xtype of 'grid'
 *             xtype: 'grid',
 *             // One header just for show. There's no data
 *             columns: [{header: 'Column One'}],
 *             store: Ext.create('Ext.data.ArrayStore', {}), // A dummy empty data store
 *             // Use 1/3 of Container's height (hint to Box layout)
 *             flex: 1
 *         }, {
 *             xtype: 'splitter'   // A splitter between the two child items
 *         }, {
 *             // Details Panel specified as a config object (no xtype defaults to 'panel').
 *             title: 'Details',
 *             bodyPadding: 5,
 *             items: [{
 *                 fieldLabel: 'Data item',
 *                 xtype: 'textfield'
 *             }], // An array of form fields
 *             flex: 2             // Use 2/3 of Container's height (hint to Box layout)
 *         }]
 *     });
 *
 * The example illustrates one possible method of displaying search results. The Panel contains
 * a grid with the resulting data arranged in rows. Each selected row may be displayed in detail
 * in the Panel below. The {@link Ext.layout.container.VBox vbox} layout is used to arrange
 * the two vertically. It is configured to stretch child items horizontally to full width.
 * Child items may either be configured with a numeric height, or with a `flex` value to distribute
 * available space proportionately.
 *
 * This Panel itself may be a child item of, for example, a {@link Ext.tab.Panel} which 
 * will size its child items to fit within its content area.
 *
 * Using these techniques, as long as the **layout** is chosen and configured correctly,
 * an application may have any level of nested containment, all dynamically sized according to
 * configuration, the user's preference and available
 * browser size.
 */
Ext.define('Ext.panel.Panel', {
    extend: 'Ext.container.Container',
    alias: 'widget.panel',
    alternateClassName: 'Ext.Panel',
    requires: [
        'Ext.panel.Header',
        'Ext.util.MixedCollection',
        'Ext.toolbar.Toolbar',
        'Ext.fx.Anim',
        'Ext.panel.DD',
        'Ext.XTemplate',
        'Ext.layout.component.Dock',
        'Ext.util.Memento'
    ],
    mixins: {
        docking: 'Ext.container.DockingContainer'
    },
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        'bodyWrap',
        'body'
    ],
    /* eslint-disable indent, max-len */
    /**
     * @cfg renderTpl
     * @inheritdoc
     */
    renderTpl: [
        // headingEl can also be inserted in updateHeader
        '<tpl if="headingText">',
        '<div id="{id}-headingEl" data-ref="headingEl" role="heading"',
        ' class="',
        Ext.baseCSSPrefix,
        'hidden-clip" style="height:0">',
        '{headingText}',
        '</div>',
        '</tpl>',
        '<tpl if="hasTabGuard">{% this.renderTabGuard(out, values, \'before\'); %}</tpl>',
        '<div id="{id}-bodyWrap" data-ref="bodyWrap" class="{baseCls}-bodyWrap"',
        '<tpl if="bodyWrapAriaAttributes">',
        '<tpl foreach="bodyWrapAriaAttributes"> {$}="{.}"</tpl>',
        '<tpl else>',
        ' role="presentation"',
        '</tpl>',
        '>',
        // If this Panel is framed, the framing template renders the docked items round the frame
        '{% this.renderDockedItems(out,values,0); %}',
        '<div id="{id}-body" data-ref="body" class="{baseCls}-body<tpl if="bodyCls"> {bodyCls}</tpl>',
        ' {baseCls}-body-{ui}<tpl if="uiCls">',
        '<tpl for="uiCls"> {parent.baseCls}-body-{parent.ui}-{.}</tpl>',
        '</tpl>{childElCls}"',
        '<tpl if="bodyAriaAttributes">',
        '<tpl foreach="bodyAriaAttributes"> {$}="{.}"</tpl>',
        '<tpl else>',
        ' role="presentation"',
        '</tpl>',
        '<tpl if="bodyStyle"> style="{bodyStyle}"</tpl>>',
        '{%this.renderContainer(out,values);%}',
        '</div>',
        '{% this.renderDockedItems(out,values,1); %}',
        '</div>',
        '<tpl if="hasTabGuard">{% this.renderTabGuard(out, values, \'after\'); %}</tpl>'
    ],
    /* eslint-enable indent, max-len */
    // <editor-fold desc="Config">
    // ***********************************************************************************
    // Begin Config
    // ***********************************************************************************
    // For performance reasons we give the following configs their default values on
    // the class body.  This prevents the updaters from running on initialization in the
    // default configuration scenario
    headerPosition: 'top',
    iconAlign: 'left',
    titleAlign: 'left',
    titleRotation: 'default',
    titlePosition: 0,
    headerConfigs: {
        glyph: 1,
        icon: 1,
        iconAlign: 1,
        iconCls: 1,
        title: 1,
        titleAlign: 1,
        titlePosition: 1,
        titleRotation: 1
    },
    beforeRenderConfig: {
        /**
         * @cfg glyph
         * @inheritdoc Ext.panel.Header#cfg-glyph
         * @accessor
         */
        glyph: null,
        /**
         * @cfg {'top'/'bottom'/'left'/'right'} [headerPosition='top']
         * Specify as `'top'`, `'bottom'`, `'left'` or `'right'`.
         * @accessor
         */
        headerPosition: null,
        /**
         * @cfg icon
         * @inheritdoc Ext.panel.Header#cfg-icon
         * @accessor
         */
        icon: null,
        /**
         * @cfg [iconAlign='left']
         * @inheritdoc Ext.panel.Header#cfg-iconAlign
         * @accessor
         */
        iconAlign: null,
        /**
         * @cfg iconCls
         * @inheritdoc Ext.panel.Header#cfg-iconCls
         * @accessor
         */
        iconCls: null,
        /**
         * @cfg title
         * @inheritdoc Ext.panel.Header#cfg-title
         * @localdoc When a `title` is specified, the {@link Ext.panel.Header} will 
         * automatically be created and displayed unless {@link #header} is set to `false`.
         * @accessor
         */
        title: null,
        /**
         * @cfg [titleAlign='left']
         * @inheritdoc Ext.panel.Header#cfg-titleAlign
         * @accessor
         */
        titleAlign: null,
        /**
         * @cfg [titlePosition=0]
         * @inheritdoc Ext.panel.Header#cfg-titlePosition
         * @accessor
         * @since 6.5.1
         */
        titlePosition: null,
        /**
         * @cfg [titleRotation='default']
         * @inheritdoc Ext.panel.Header#cfg-titleRotation
         * @accessor
         */
        titleRotation: null
    },
    /**
     * @cfg {Boolean/Number} animCollapse
     * `true` to animate the transition when the panel is collapsed, `false` to skip the animation
     * (defaults to `true` if the {@link Ext.fx.Anim} class is available, otherwise `false`).
     * May also be specified as the animation duration in milliseconds.
     */
    animCollapse: Ext.enableFx,
    /**
     * @cfg {Boolean} bodyBorder
     * A shortcut to add or remove the border on the body of a panel. In the classic theme
     * this only applies to a panel which has the {@link #frame} configuration set to `true`.
     * @since 2.3.0
     */
    /**
     * @cfg {String/String[]} bodyCls
     * A CSS class, space-delimited string of classes, or array of classes to be applied
     * to the panel's body element. The following examples are all valid:
     *
     *     bodyCls: 'foo'
     *     bodyCls: 'foo bar'
     *     bodyCls: ['foo', 'bar']
     */
    /**
     * @cfg {Number/String} [bodyPadding=undefined]
     * A shortcut for setting a padding style on the body element. The value can either be
     * a number to be applied to all sides, or a normal css string describing padding.
     */
    /**
     * @cfg {String/Object/Function} bodyStyle
     * Custom CSS styles to be applied to the panel's body element, which can be supplied
     * as a valid CSS style string, an object containing style property name/value pairs
     * or a function that returns such a string or object.
     * For example, these two formats are interpreted to be equivalent:
     *
     *     bodyStyle: 'background:#ffc; padding:10px;'
     *
     *     bodyStyle: {
     *         background: '#ffc',
     *         padding: '10px'
     *     }
     *
     * @since 2.3.0
     */
    /**
     * @cfg {Boolean} border
     * Specify as `false` to render the Panel with zero width borders.
     *
     * Leaving the value as `true` uses the selected theme's
     * {@link Ext.panel.Panel#$panel-border-width}
     *
     * Defaults to `false` when using or extending Neptune.
     * 
     * **Note:** is ignored when {@link #frame} is set to **true**.
     */
    border: true,
    /**
     * @cfg {Boolean} closable
     * True to display the 'close' tool button and allow the user to close the window,
     * false to hide the button and disallow closing the window.
     *
     * By default, when close is requested by clicking the close button in the header,
     * the {@link #method-close} method will be called. This will
     * _{@link Ext.Component#method-destroy destroy}_ the Panel and its content
     * meaning that it may not be reused.
     *
     * To make closing a Panel _hide_ the Panel so that it may be reused, set {@link #closeAction}
     * to 'hide'.
     * @accessor
     */
    closable: false,
    /**
     * @cfg {String} closeAction
     * The action to take when the close header tool is clicked:
     *
     * - **`'{@link #method-destroy}'`** :
     *
     *   {@link #method-remove remove} the window from the DOM and
     *   {@link Ext.Component#method-destroy destroy} it and all descendant Components.
     *   The window will **not** be available to be redisplayed via the {@link #method-show} method.
     *
     * - **`'{@link #method-hide}'`** :
     *
     *   {@link #method-hide} the window by setting visibility to hidden and applying negative
     *   offsets. The window will be available to be redisplayed via the {@link #method-show}
     *   method.
     *
     * **Note:** This behavior has changed! setting *does* affect the {@link #method-close} method
     * which will invoke the appropriate closeAction.
     */
    closeAction: 'destroy',
    /**
     * @cfg {String} closeToolText
     * Text to be announced by screen readers when the **close**
     * {@link Ext.panel.Tool tool} is focused.  Will also be set as the close tool's 
     * {@link Ext.panel.Tool#cfg-tooltip tooltip} text.
     * 
     * **Note:** Applicable when the panel is {@link #closable}: true
     * @locale
     */
    closeToolText: 'Close panel',
    /**
     * @cfg {Boolean} collapsed
     * `true` to render the panel collapsed, `false` to render it expanded.
     */
    collapsed: false,
    /**
     * @cfg {String} collapsedCls
     * A CSS class to add to the panel's element after it has been collapsed.
     */
    collapsedCls: 'collapsed',
    /**
     * @cfg {String} collapseDirection
     * The direction to collapse the Panel when the toggle button is clicked.
     *
     * Defaults to the {@link #cfg-headerPosition}
     *
     * **Important: This config is _ignored_ for {@link #collapsible} Panels which are direct child
     * items of a {@link Ext.layout.container.Border border layout}.**
     *
     * Specify as `'top'`, `'bottom'`, `'left'` or `'right'`.
     */
    /**
     * @cfg {Boolean} collapseFirst
     * `true` to make sure the collapse/expand toggle button always renders first (to the left of)
     * any other tools in the panel's title bar, `false` to render it last.
     */
    collapseFirst: true,
    /**
     * @cfg {Boolean} collapsible
     * True to make the panel collapsible and have an expand/collapse toggle Tool added
     * into the header tool button area. False to keep the panel sized either statically,
     * or by an owning layout manager, with no toggle Tool.
     * When a panel is used in a {@link Ext.layout.container.Border border layout}, the
     * {@link #floatable} option can influence the behavior of collapsing.
     * See {@link #collapseMode} and {@link #collapseDirection}
     */
    collapsible: undefined,
    /**
     * @cfg {String} collapseMode
     * **Important: this config is only effective for {@link #collapsible} Panels which are direct
     * child items of a {@link Ext.layout.container.Border border layout}.**
     *
     * When _not_ a direct child item of a {@link Ext.layout.container.Border border layout},
     * then the Panel's header remains visible, and the body is collapsed to zero dimensions.
     * If the Panel has no header, then a new header (orientated correctly depending on the
     * {@link #collapseDirection}) will be inserted to show a the title and a re-expand tool.
     *
     * When a child item of a {@link Ext.layout.container.Border border layout}, this config
     * has three possible values:
     *
     * - `undefined` - When collapsed, a placeholder {@link Ext.panel.Header Header} is injected
     *   into the layout to represent the Panel and to provide a UI with a Tool to allow the user
     *   to re-expand the Panel.
     *
     * - `"header"` - The Panel collapses to leave its header visible as when not inside a
     *   {@link Ext.layout.container.Border border layout}.
     *
     * - `"mini"` - The Panel collapses without a visible header.
     */
    /**
     * @cfg {String} collapseToolText
     * Text to be announced by screen readers when  **collapse**
     * {@link Ext.panel.Tool tool} is focused.  Will also be set as the  collapse tool's
     * {@link Ext.panel.Tool#cfg-tooltip tooltip} text.
     * 
     * **Note:** Applicable when the panel is {@link #collapsible}: true
     * @locale
     */
    collapseToolText: 'Collapse panel',
    /**
     * @cfg {String} expandToolText
     * Text to be announced by screen readers when **expand** {@link Ext.panel.Tool tool}
     * is focused.  Will also be set as the  expand tool's
     * {@link Ext.panel.Tool#cfg-tooltip tooltip} text.
     * 
     * **Note:** Applicable when the panel is {@link #collapsible}: true
     * @locale
     */
    expandToolText: 'Expand panel',
    /**
     * @cfg {Boolean} constrain
     * True to constrain the panel within its containing element, false to allow it to fall outside
     * of its containing element. By default floating components such as Windows will be rendered to
     * `document.body`. To render and constrain the window within another element specify
     * {@link #renderTo}. Optionally the header only can be constrained using
     * {@link #constrainHeader}.
     */
    constrain: false,
    /**
     * @cfg {Boolean} constrainHeader
     * True to constrain the panel header within its containing element (allowing the panel body
     * to fall outside of its containing element) or false to allow the header to fall outside
     * its containing element.
     * Optionally the entire panel can be constrained using {@link #constrain}.
     */
    constrainHeader: false,
    // @cmd-auto-dependency {aliasPrefix: "widget.", typeProperty: "xtype"}
    /**
     * @cfg {Object/Object[]} dockedItems
     * A component or series of components to be added as docked items to this panel. The docked
     * items can be docked to either the top, right, left or bottom of a panel. This is typically
     * used for things like toolbars or tab bars:
     *
     *     var panel = new Ext.panel.Panel({
     *         dockedItems: [{
     *             xtype: 'toolbar',
     *             dock: 'top',
     *             items: [{
     *                 text: 'Docked to the top'
     *             }]
     *         }]
     *     });
     */
    dockedItems: null,
    /**
     * @cfg {String} buttonAlign
     * The alignment of any buttons added to this panel. Valid values are 'right', 'left'
     * and 'center' (defaults to 'right' for buttons/fbar, 'left' for other toolbar types).
     *
     * **NOTE:** The preferred way to specify toolbars is to use the dockedItems config.
     * Instead of buttonAlign you would add the layout: { pack: 'start' | 'center' | 'end' } option
     * to the dockedItem config.
     */
    // @cmd-auto-dependency {aliasPrefix: "widget.", typeProperty: "xtype", defaultType: "toolbar"}
    /**
     * @cfg {Object/Object[]} tbar
     * Convenience config. Short for 'Top Bar'.
     *
     *     tbar: [
     *       { xtype: 'button', text: 'Button 1' }
     *     ]
     *
     * is equivalent to
     *
     *     dockedItems: [{
     *         xtype: 'toolbar',
     *         dock: 'top',
     *         items: [
     *             { xtype: 'button', text: 'Button 1' }
     *         ]
     *     }]
     */
    tbar: null,
    // @cmd-auto-dependency {aliasPrefix: "widget.", typeProperty: "xtype", defaultType: "toolbar"}
    /**
     * @cfg {Object/Object[]} bbar
     * Convenience config. Short for 'Bottom Bar'.
     *
     *     bbar: [
     *       { xtype: 'button', text: 'Button 1' }
     *     ]
     *
     * is equivalent to
     *
     *     dockedItems: [{
     *         xtype: 'toolbar',
     *         dock: 'bottom',
     *         items: [
     *             { xtype: 'button', text: 'Button 1' }
     *         ]
     *     }]
     */
    bbar: null,
    // @cmd-auto-dependency {aliasPrefix: "widget.", typeProperty: "xtype", defaultType: "toolbar"}
    /**
     * @cfg {Object/Object[]} fbar
     * Convenience config used for adding items to the bottom of the panel. Short for Footer Bar.
     *
     *     fbar: [
     *       { type: 'button', text: 'Button 1' }
     *     ]
     *
     * is equivalent to
     *
     *     dockedItems: [{
     *         xtype: 'toolbar',
     *         dock: 'bottom',
     *         ui: 'footer',
     *         defaults: {
     *             minWidth: 200
     *         },
     *         items: [
     *             { xtype: 'component', flex: 1 },
     *             { xtype: 'button', text: 'Button 1' }
     *         ]
     *     }]
     *
     * The {@link #minButtonWidth} is used as the default
     * {@link Ext.button.Button#minWidth minWidth} for each of the buttons in the fbar.
     */
    fbar: null,
    // @cmd-auto-dependency {aliasPrefix: "widget.", typeProperty: "xtype", defaultType: "toolbar"}
    /**
     * @cfg {Object/Object[]} lbar
     * Convenience config. Short for 'Left Bar' (left-docked, vertical toolbar).
     *
     *     lbar: [
     *       { xtype: 'button', text: 'Button 1' }
     *     ]
     *
     * is equivalent to
     *
     *     dockedItems: [{
     *         xtype: 'toolbar',
     *         dock: 'left',
     *         items: [
     *             { xtype: 'button', text: 'Button 1' }
     *         ]
     *     }]
     */
    lbar: null,
    // @cmd-auto-dependency {aliasPrefix: "widget.", typeProperty: "xtype", defaultType: "toolbar"}
    /**
     * @cfg {Object/Object[]} rbar
     * Convenience config. Short for 'Right Bar' (right-docked, vertical toolbar).
     *
     *     rbar: [
     *       { xtype: 'button', text: 'Button 1' }
     *     ]
     *
     * is equivalent to
     *
     *     dockedItems: [{
     *         xtype: 'toolbar',
     *         dock: 'right',
     *         items: [
     *             { xtype: 'button', text: 'Button 1' }
     *         ]
     *     }]
     */
    rbar: null,
    /**
     * @cfg {Object[]} buttons
     * Convenience config used for adding buttons docked to the bottom of the panel. This is a
     * synonym for the {@link #fbar} config.
     *
     *     buttons: [
     *       { text: 'Button 1' }
     *     ]
     *
     * is equivalent to
     *
     *     dockedItems: [{
     *         xtype: 'toolbar',
     *         dock: 'bottom',
     *         ui: 'footer',
     *         defaults: {
     *             minWidth: 200
     *         },
     *         items: [
     *             { xtype: 'component', flex: 1 },
     *             { xtype: 'button', text: 'Button 1' }
     *         ]
     *     }]
     *
     * The {@link #minButtonWidth} is used as the default
     * {@link Ext.button.Button#minWidth minWidth} for each of the buttons in the buttons toolbar.
     */
    buttons: null,
    /**
     * @cfg draggable
     * @inheritdoc
     * @localdoc **NOTE:** The private {@link Ext.panel.DD} class is used instead of 
     * ComponentDragger when {@link #simpleDrag} is false (_default_).  In this case you 
     * may pass a config for {@link Ext.dd.DragSource}.
     * 
     * See also {@link #dd}.
     */
    /**
     * @cfg {Boolean} floatable
     * **Important: This config is only effective for {@link #collapsible} Panels which are direct
     * child items of a {@link Ext.layout.container.Border border layout}.**
     *
     * true to allow clicking a collapsed Panel's {@link #placeholder} to display the Panel floated
     * above the layout, false to force the user to fully expand a collapsed region by clicking
     * the expand button to see it again.
     */
    floatable: true,
    /**
     * @cfg {Boolean} frame
     * True to apply a frame to the panel.
     * 
     * **Note:** `frame: true` overrides {@link #border border:false}
     */
    frame: false,
    /**
     * @cfg {Boolean} frameHeader
     * True to apply a frame to the panel panels header (if 'frame' is true).
     */
    frameHeader: true,
    /**
     * @cfg {Boolean/Object} header
     * Pass as `false` to prevent a Header from being created and shown.
     *
     * Pass as a config object (optionally containing an `xtype`) to custom-configure this Panel's
     * header.
     *
     * See {@link Ext.panel.Header} for all the options that may be specified here.
     *
     * A {@link Ext.panel.Header panel header} is a {@link Ext.container.Container} which contains
     * the Panel's {@link #title} and {@link #tools}. You may also configure the Panel's `header`
     * option with its own child items which go *before* the {@link #tools}
     *
     * By default the panel {@link #title} is inserted after items configured in this config,
     * but before any tools. To insert the title at any point in the full array, specify the
     * {@link Ext.panel.Header#cfg-titlePosition titlePosition} config:
     *
     *     new Ext.panel.Panel({
     *         title: 'Test',
     *         tools: [{
     *             type: 'refresh'
     *         }, {
     *             type: 'help'
     *         }],
     *         titlePosition: 2 // Title will come AFTER the two tools
     *         ...
     *     });
     *
     */
    /**
     * @cfg {String} headerOverCls
     * Optional CSS class to apply to the header element on mouseover
     */
    /**
     * @cfg {Boolean} hideCollapseTool
     * `true` to hide the expand/collapse toggle button when `{@link #collapsible} == true`,
     * `false` to display it.
     */
    hideCollapseTool: false,
    /**
     * @cfg {Boolean} [manageHeight=true] When true, the dock component layout writes
     * height information to the panel's DOM elements based on its shrink wrap height
     * calculation. This ensures that the browser respects the calculated height.
     * When false, the dock component layout will not write heights on the panel or its
     * body element. In some simple layout cases, not writing the heights to the DOM may
     * be desired because this allows the browser to respond to direct DOM manipulations
     * (like animations).
     */
    manageHeight: true,
    /**
     * @cfg {String} maskElement
     *
     * The name of the element property in this Panel to mask when masked by a LoadMask.
     *
     * Defaults to `"el"` to indicate that any LoadMask should be rendered into this Panel's
     * encapsulating element.
     *
     * This could be configured to be `"body"` so that only the body is masked and toolbars
     * and the header are still mouse-accessible.
     */
    maskElement: 'el',
    /**
     * @cfg {Number} minButtonWidth
     * Minimum width of all footer toolbar buttons in pixels. If set, this will be used
     * as the default value for the {@link Ext.button.Button#minWidth} config of each Button
     * added to the **footer toolbar** via the {@link #fbar} or {@link #buttons} configurations.
     * It will be ignored for buttons that have a minWidth configured some other way,
     * e.g. in their own config object or via the {@link Ext.container.Container#defaults defaults}
     * of their parent container.
     */
    minButtonWidth: 75,
    /**
     * @cfg {Boolean} overlapHeader
     * True to overlap the header in a panel over the framing of the panel itself. This is needed
     * when `frame: true` (and is done automatically for you). Otherwise it is undefined.
     * If you manually add rounded corners to a panel header which does not have `frame: true`,
     * this will need to be set to true.
     */
    /**
     * @cfg {Ext.Component/Object} placeholder
     * **Important: This config is only effective for {@link #collapsible} Panels which are direct
     * child items of a {@link Ext.layout.container.Border border layout} when not using
     * the `'header'` {@link #collapseMode}.**
     *
     * **Optional.** A Component (or config object for a Component) to show in place of this Panel
     * when this Panel is collapsed by a {@link Ext.layout.container.Border border layout}.
     * Defaults to a generated {@link Ext.panel.Header Header} containing a
     * {@link Ext.panel.Tool Tool} to re-expand the Panel.
     */
    /**
     * @cfg {Number} [placeholderCollapseHideMode=Ext.Element.VISIBILITY]
     * The {@link Ext.dom.Element#setVisibilityMode mode} for hiding collapsed panels when
     * using {@link #collapseMode} "placeholder".
     */
    // placeholderCollapseHideMode: Ext.Element.VISIBILITY,
    /**
     * @cfg {Boolean} preventHeader
     * @deprecated 4.1.0 Use {@link #header} instead.
     * Prevent a Header from being created and shown.
     */
    preventHeader: false,
    /**
     * @cfg {Boolean} maintainTitlePosition
     * For panels that are collapsed to the left or right, 
     * {@link Ext.panel.Header#titlePosition} may be temporarily changed for UI consistency.
     * Setting this config to true will force the specified titlePosition to be maintained
     * @since 6.5.1
     */
    maintainTitlePosition: false,
    /**
     * @cfg [shrinkWrap=2]
     * @inheritdoc
     * @localdoc ##Panels (subclasses and instances)
     * 
     * By default, when a panel is configured to shrink wrap in a given dimension, only 
     * the panel's "content" (items and html content inside the panel body) contributes 
     * to its size, and the content of docked items is ignored. Optionally you can use 
     * the {@link #shrinkWrapDock} config to allow docked items to contribute to the 
     * panel's size as well. For example, if shrinkWrap and shrinkWrapDock are both set 
     * to true, the width of the panel would be the width of the panel's content and the 
     * panel's header text.
     */
    /**
     * @cfg {Boolean/Number} shrinkWrapDock
     * Allows for this panel to include the {@link #dockedItems} when trying to determine 
     * the overall size of the panel. This option is only applicable when this panel is 
     * also shrink wrapping in the same dimensions. See {@link Ext.Panel#shrinkWrap} for 
     * an explanation of the configuration options.
     */
    shrinkWrapDock: false,
    /**
     * @cfg {Boolean} [simpleDrag=false]
     * When {@link #cfg-draggable} is `true`, Specify this as `true` to  cause the `draggable`
     * config to work the same as it does in {@link Ext.window.Window Window}. This Panel
     * just becomes movable. No DragDrop instances receive any notifications.
     * For example:
     *
     *     @example
     *     var win = Ext.create('widget.window', {
     *         height: 300,
     *         width: 300,
     *         title: 'Constraining Window',
     *         closable: false,
     *         items: {
     *             title: "Floating Panel",
     *             width: 100,
     *             height: 100,
     *             floating: true,
     *             draggable: true,
     *             constrain: true,
     *             simpleDrag: true
     *         }
     *     });
     *     win.show();
     *     // Floating components begin life hidden
     *     win.child('[title=Floating Panel]').show();
     *
     */
    /**
     * @cfg stateEvents
     * @inheritdoc Ext.state.Stateful#cfg-stateEvents
     * @localdoc By default the following stateEvents are added:
     * 
     *  - {@link #event-resize} - _(added by Ext.Component)_
     *  - {@link #event-collapse}
     *  - {@link #event-expand}
     */
    /**
     * @cfg {Boolean} titleCollapse
     * `true` to allow expanding and collapsing the panel (when `{@link #collapsible} = true`)
     * by clicking anywhere in the header bar, `false`) to allow it only by clicking to tool
     * button). When a panel is used in a {@link Ext.layout.container.Border border layout},
     * the {@link #floatable} option can influence the behavior of collapsing.
     */
    titleCollapse: undefined,
    /**
     * @cfg {Object[]/Ext.panel.Tool[]} tools
     * An array of {@link Ext.panel.Tool} configs/instances to be added to the header tool area.
     * The tools are stored as child components of the header container. They can be accessed using
     * {@link #down} and {#query}, as well as the other component methods. The toggle tool
     * is automatically created if {@link #collapsible} is set to true.
     *
     * Note that, apart from the toggle tool which is provided when a panel is collapsible,
     * these tools only provide the visual button. Any required functionality must be provided
     * by adding handlers that implement the necessary behavior.
     *
     * Example usage:
     *
     *     tools: [{
     *         type:'refresh',
     *         tooltip: 'Refresh form Data',
     *         // hidden:true,
     *         handler: function(event, toolEl, panelHeader) {
     *             // refresh logic
     *         }
     *     },
     *     {
     *         type:'help',
     *         tooltip: 'Get Help',
     *         callback: function(panel, tool, event) {
     *             // show help here
     *         }
     *     }]
     *
     * The difference between `handler` and `callback` is the signature. For details on
     * the distinction, see {@link Ext.panel.Tool}.
     */
    /**
     * @cfg {String} defaultButton
     * Reference name of the component to act as the default button for this Panel.
     * Default button is activated by pressing Enter key while focus is contained within
     * the Panel's {@link #defaultButtonTarget}.
     *
     * The most obvious use for `defaultButton` is submitting a form:
     *
     *      var loginWindow = new Ext.window.Window({
     *          autoShow: true,
     *          width: 300,
     *          layout: 'form',
     *          title: 'Enter login information',
     *          referenceHolder: true,
     *          defaultFocus: 'textfield',
     *          defaultButton: 'okButton',
     *          
     *          items: [{
     *              xtype: 'textfield',
     *              fieldLabel: 'User name'
     *          }, {
     *              xtype: 'textfield',
     *              fieldLabel: 'Password'
     *          }],
     *          
     *          buttons: [{
     *              reference: 'okButton',
     *              text: 'Login',
     *              handler: function() {
     *                  Ext.Msg.alert('Submit', 'Your login is being processed');
     *              }
     *          }]
     *      });
     */
    /**
     * @cfg {String} [defaultButtonTarget] Name of the element that will be the target of
     * {@link #defaultButton} keydown listener. The default element is Panel body, which
     * means that pressing Enter key while focus is on docked items will not fire `defaultButton`
     * action.
     *
     * If you want `defaultButton` action to fire in docked items, set this config to `"el"`.
     */
    // ***********************************************************************************
    // End Config
    // ***********************************************************************************
    // </editor-fold>
    // <editor-fold desc="Properties">
    // ***********************************************************************************
    // Begin Properties
    // ***********************************************************************************
    /**
     * @cfg baseCls
     * @inheritdoc
     */
    baseCls: Ext.baseCSSPrefix + 'panel',
    /**
     * @property {Ext.dom.Element} body
     * The Panel's body {@link Ext.dom.Element Element} which may be used to contain HTML content.
     * The content may be specified in the {@link #html} config, or it may be loaded using the
     * {@link #loader} config. Read-only.
     *
     * If this is used to load visible HTML elements in either way, then
     * the Panel may not be used as a Layout for hosting nested Panels.
     *
     * If this Panel is intended to be used as the host of a Layout (See {@link #cfg!layout}
     * then the body Element must not be loaded or changed - it is under the control
     * of the Panel's Layout.
     *
     * @readonly
     */
    bodyPosProps: {
        x: 'x',
        y: 'y'
    },
    /**
     * @cfg componentLayout
     * @inheritdoc
     */
    componentLayout: 'dock',
    /**
     * @property contentPaddingProperty
     * @inheritdoc
     */
    contentPaddingProperty: 'bodyPadding',
    emptyArray: [],
    /**
     * @property {Boolean} isPanel
     * `true` in this class to identify an object as an instantiated Panel, or subclass thereof.
     */
    isPanel: true,
    /**
     * @property defaultBindProperty
     * @inheritdoc
     */
    defaultBindProperty: 'title',
    // ***********************************************************************************
    // End Properties
    // ***********************************************************************************
    // </editor-fold>
    // <editor-fold desc="Events">
    // ***********************************************************************************
    // Begin Events
    // ***********************************************************************************
    /**
     * @event beforeclose
     * Fires before the user closes the panel. Return false from any listener to stop
     * the close event being fired
     * @param {Ext.panel.Panel} panel The Panel object
     */
    /**
     * @event beforecollapse
     * Fires before this panel is collapsed. Return false to prevent the collapse.
     * @param {Ext.panel.Panel} p The Panel being collapsed.
     * @param {String} direction . The direction of the collapse. One of
     *
     *   - Ext.Component.DIRECTION_TOP
     *   - Ext.Component.DIRECTION_RIGHT
     *   - Ext.Component.DIRECTION_BOTTOM
     *   - Ext.Component.DIRECTION_LEFT
     *
     * @param {Boolean} animate True if the collapse is animated, else false.
     */
    /**
     * @event beforeexpand
     * Fires before this panel is expanded. Return false to prevent the expand.
     * @param {Ext.panel.Panel} p The Panel being expanded.
     * @param {Boolean} animate True if the expand is animated, else false.
     */
    /**
     * @event close
     * Fires when the user closes the panel.
     * @param {Ext.panel.Panel} panel The Panel object
     */
    /**
     * @event collapse
     * Fires after this Panel has collapsed.
     * @param {Ext.panel.Panel} p The Panel that has been collapsed.
     */
    /**
     * @event expand
     * Fires after this Panel has expanded.
     * @param {Ext.panel.Panel} p The Panel that has been expanded.
     */
    /**
     * @event float
     * Fires after a collapsed Panel has been "floated" by clicking on
     * it's header. Only applicable when the Panel is an item in a
     * {@link Ext.layout.container.Border Border Layout}.
     */
    /**
     * @event glyphchange
     * Fired when the Panel glyph has been changed by the {@link #setGlyph} method.
     * @param {Ext.panel.Panel} this
     * @param {Number/String} newGlyph
     * @param {Number/String} oldGlyph
     */
    /**
     * @event iconalignchange
     * Fires after the Panel iconAlign has been set or changed.
     * @param {Ext.panel.Panel} this The Panel which has the iconAlign changed.
     * @param {String} newIconAlign
     * @param {String} oldIconAlign
     * @since 6.5.1
     */
    /**
     * @event iconchange
     * Fires after the Panel icon has been set or changed.
     * @param {Ext.panel.Panel} this The Panel which has the icon changed.
     * @param {String} newIcon The path to the new icon image.
     * @param {String} oldIcon The path to the previous panel icon image.
     */
    /**
     * @event iconclschange
     * Fires after the Panel iconCls has been set or changed.
     * @param {Ext.panel.Panel} this The Panel which has the iconCls changed.
     * @param {String} newIconCls The new iconCls.
     * @param {String} oldIconCls The previous panel iconCls.
     */
    /**
     * @event titlealignchange
     * Fires after the Panel titleAlign has been set or changed.
     * @param {Ext.panel.Panel} this the Panel which has the titleAlign changed.
     * @param {String} newTitleAlign
     * @param {String} oldTitleAlign
     * @since 6.5.1
     */
    /**
     * @event titlechange
     * Fires after the Panel title has been set or changed.
     * @param {Ext.panel.Panel} this the Panel which has been resized.
     * @param {String} newTitle The new title.
     * @param {String} oldTitle The previous panel title.
     */
    /**
     * @event titlepositionchange
     * Fires after the Panel titlePosition has been set or changed.
     * @param {Ext.panel.Panel} this the Panel which has the titlePosition changed.
     * @param {String} newTitlePosition
     * @param {String} oldTitlePosition
     * @since 6.5.1
     */
    /**
     * @event titlerotationchange
     * Fires after the Panel titleRotation has been set or changed.
     * @param {Ext.panel.Panel} this the Panel which has the titleRotation changed.
     * @param {String} newTitleRotation
     * @param {String} oldTitleRotation
     * @since 6.5.1
     */
    /**
     * @event unfloat
     * Fires after a "floated" Panel has returned to it's collapsed state
     * as a result of the mouse leaving the Panel. Only applicable when
     * the Panel is an item in a
     * {@link Ext.layout.container.Border Border Layout}.
     */
    // ***********************************************************************************
    // End Events
    // ***********************************************************************************
    // </editor-fold>
    // <editor-fold desc="Component Methods">
    // ***********************************************************************************
    // Begin Methods
    // ***********************************************************************************
    /**
     * Adds a CSS class to the body element. If not rendered, the class will
     * be added when the panel is rendered.
     * @param {String/String[]} cls The class to add
     * @return {Ext.panel.Panel} this
     */
    addBodyCls: function(cls) {
        var me = this,
            body = me.rendered ? me.body : me.getProtoBody();
        body.addCls(cls);
        return me;
    },
    /**
     * Add tools to this panel {@link Ext.panel.Header header}
     * 
     *     panel.addTool({
     *         type: 'gear',
     *         handler: function() {
     *             // ....
     *         }
     *     });
     *     
     *     panel.addTool([{
     *         type: 'gear',
     *         handler: 'viewControllerGearMethod'
     *     }, {
     *         type: 'save',
     *         handler: 'viewControllerSaveMethod'
     *     }]);
     *
     * By default the tools will be accessible via keyboard, with the exception of 
     * automatically added collapse/expand and close tools.
     *
     * If you implement keyboard equivalents of your tools' actions elsewhere and do not
     * want the tools to participate in keyboard navigation, you can make them 
     * presentational instead:
     * 
     *     panel.addTool({
     *         type: 'mytool',
     *         focusable: false,
     *         ariaRole: 'presentation'
     *         // ...
     *     });
     * 
     * @param {Object/Object[]/Ext.panel.Tool/Ext.panel.Tool[]} tools The tool or tools to
     * add.
     */
    addTool: function(tools) {
        if (!Ext.isArray(tools)) {
            tools = [
                tools
            ];
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            header = me.header,
            len = tools.length,
            curTools = me.tools,
            t, tool;
        if (!header || !header.isHeader) {
            header = null;
            if (!curTools) {
                me.tools = curTools = [];
            }
        }
        for (t = 0; t < len; t++) {
            tool = tools[t];
            if (typeof tool !== 'string' && !tool.isTool) {
                tool = Ext.apply({}, tool);
            }
            tool.toolOwner = me;
            if (header) {
                header.addTool(tool);
            } else {
                // only modify the tools array if the header isn't created,
                // otherwise, defer to the header to manage
                curTools.push(tool);
            }
        }
        me.updateHeader();
    },
    /**
     * @method
     * @protected
     * @template
     * Template method to be implemented in subclasses to add their tools after
     * the collapsible tool.
     */
    addTools: Ext.emptyFn,
    getClosable: function() {
        return this.closable;
    },
    setClosable: function(closable) {
        var me = this,
            tab = me.tab;
        closable = !!closable;
        if (me.closable !== closable) {
            me.closable = closable;
            if (tab) {
                tab.setClosable(closable);
            }
        }
    },
    setCollapsible: function(collapsible) {
        var me = this,
            current = me.collapsible,
            collapseTool = me.collapseTool;
        me.collapsible = collapsible;
        if (collapsible && !current) {
            me.updateCollapseTool();
            collapseTool = me.collapseTool;
            if (collapseTool) {
                collapseTool.show();
            }
        } else if (!collapsible && current) {
            if (collapseTool) {
                collapseTool.hide();
            }
        }
    },
    /**
     * @method addUIClsToElement
     * @inheritdoc
     */
    addUIClsToElement: function(cls) {
        var me = this,
            result = me.callParent(arguments);
        me.addBodyCls([
            Ext.baseCSSPrefix + cls,
            me.baseCls + '-body-' + cls,
            me.baseCls + '-body-' + me.ui + '-' + cls
        ]);
        return result;
    },
    /**
     * Invoked after the Panel is Collapsed.
     *
     * @param {Boolean} animated
     *
     * @template
     * @protected
     */
    afterCollapse: function(animated) {
        var me = this,
            ownerLayout = me.ownerLayout;
        me.isCollapsingOrExpanding = 0;
        me.updateCollapseTool();
        // The x-animating-size class sets overflow:hidden so that overflowing
        // content is clipped during animation.
        if (animated) {
            me.removeCls(Ext.baseCSSPrefix + 'animating-size');
        }
        if (ownerLayout) {
            ownerLayout.afterCollapse(me, animated);
        }
        me.setHiddenDocked();
        me.fireEvent('collapse', me);
    },
    /**
     * Invoked after the Panel is Expanded.
     *
     * @param {Boolean} animated
     *
     * @template
     * @protected
     */
    afterExpand: function(animated) {
        var me = this,
            ownerLayout = me.ownerLayout;
        me.isCollapsingOrExpanding = 0;
        me.updateCollapseTool();
        // The x-animating-size class sets overflow:hidden so that overflowing
        // content is clipped during animation.
        if (animated) {
            me.removeCls(Ext.baseCSSPrefix + 'animating-size');
        }
        if (ownerLayout) {
            ownerLayout.afterExpand(me, animated);
        }
        me.fireEvent('expand', me);
        me.fireHierarchyEvent('expand');
    },
    doDestroy: function() {
        var me = this;
        if (me.slideOutTask) {
            me.slideOutTask.cancel();
        }
        Ext.destroy(me.placeholder, me.ghostPanel, me.dd, me.accordionHeaderKeyNav, me.accordionBodyKeyNav, me.defaultButtonKeyNav);
        me.destroyDockedItems();
        me.callParent();
    },
    beforeRender: function() {
        var me = this,
            wasCollapsed;
        // Ensure the protoBody exists so that initOverflow gets right answer from getOverflowEl.
        // If this Panel was applied to an existing element (such as being used as a Viewport)
        // then it will not have been created.
        me.getProtoBody();
        me.callParent();
        // Add class-specific header tools.
        // Panel adds collapsible and closable.
        me.initTools();
        // Dock the header/title unless we are configured specifically not to create a header.
        // If the panel participates in a border layout it should have the ARIA role of 'region'.
        // In that case we need to render a heading element even if the panel is configured
        // not to have a header.
        if (!(me.preventHeader || (me.header === false)) || me.isViewportBorderChild) {
            me.updateHeader();
        }
        me.afterHeaderInit = true;
        // If we are rendering collapsed, we still need to save and modify various configs
        if (me.collapsed) {
            if (me.isPlaceHolderCollapse()) {
                if (!me.hidden) {
                    me.setHiddenState(true);
                    // This will insert the placeholder Component into the ownerCt's
                    // child collection.
                    // Its getRenderTree call which is calling this will then iterate again and
                    // recreate the child items array to include the new Component.
                    // Prevent the first collapse from firing
                    me.preventCollapseFire = true;
                    me.placeholderCollapse();
                    delete me.preventCollapseFire;
                    wasCollapsed = me.collapsed;
                    // Temporarily clear the flag so that the header is rendered
                    // with a collapse tool in it.
                    // Placeholder collapse panels never really collapse, they just hide.
                    // The tool is always a collapse tool.
                    me.collapsed = false;
                }
            } else {
                me.beginCollapse();
                me.addClsWithUI(me.collapsedCls);
            }
        }
        // Restore the flag if we are being rendered initially placeholder collapsed.
        if (wasCollapsed) {
            me.collapsed = wasCollapsed;
        }
    },
    /**
     * @private
     * Memento Factory method
     * @param {String} name Name of the Memento (used as prefix for named Memento)
     */
    getMemento: function(name) {
        var me = this;
        if (name && typeof name === 'string') {
            name += 'Memento';
            return me[name] || (me[name] = new Ext.util.Memento(me));
        }
    },
    /**
     * @private
     * Called before the change from default, configured state into the collapsed state.
     * This method may be called at render time to enable rendering in an initially collapsed state,
     * or at runtime when an existing, fully laid out Panel may be collapsed.
     * It basically saves configs which need to be clobbered for the duration of the collapsed
     * state.
     */
    beginCollapse: function() {
        var me = this,
            lastBox = me.lastBox,
            rendered = me.rendered,
            collapseMemento = me.getMemento('collapse'),
            sizeModel = me.getSizeModel(),
            header = me.header,
            reExpander;
        // When we collapse a panel, the panel is in control of one dimension (depending on
        // collapse direction) and sets that on the component. We must restore the user's
        // original value (including non-existence) when we expand. Using this technique, we
        // mimic setCalculatedSize for the dimension we do not control and setSize for the
        // one we do (only while collapsed).
        // Additionally, the panel may have a shrink wrapped width and/or height. For shrinkWrapped
        // panels this can be problematic, since a collapsed, shrink-wrapped panel has no way
        // of determining its width (or height if the collapse direction is horizontal). It is
        // therefore necessary to capture both the width and height regardless of collapse direction
        // This allows us to set a configured width or height on the panel when it is collapsed,
        // and it will be restored to an unconfigured-width shrinkWrapped state on expand.
        collapseMemento.capture([
            'height',
            'minHeight',
            'width',
            'minWidth'
        ]);
        if (lastBox) {
            collapseMemento.capture(me.restoreDimension(), lastBox, 'last.');
        }
        // If the panel has a shrinkWrapped height/width and is already rendered, configure
        // its width/height as its calculated width/height, so that the collapsed header
        // will have the same width or height as the panel did before it was collapsed.
        // If the shrinkWrapped panel has not yet been rendered, as will be the case when a panel
        // is initially configured with collapsed:true, we attempt to use the configured
        // width/height, and fall back to minWidth or minHeight if width/height has not been
        // configured, and fall back to a value of 100 if a minWidth/minHeight has not been
        // configured.
        if (me.collapsedVertical()) {
            if (sizeModel.width.shrinkWrap) {
                me.width = rendered ? me.getWidth() : me.width || me.minWidth || 100;
            }
            delete me.height;
            me.minHeight = 0;
        } else if (me.collapsedHorizontal()) {
            if (sizeModel.height.shrinkWrap) {
                me.height = rendered ? me.getHeight() : me.height || me.minHeight || 100;
            }
            delete me.width;
            me.minWidth = 0;
        }
        if (me.ownerCt) {
            me.ownerCt.getLayout().beginCollapse(me);
        }
        // Get a reExpander header. This will return the Panel Header if the Header
        // is in the correct orientation
        // If we are using the Header as the reExpander, change its UI to collapsed state
        if (!me.isPlaceHolderCollapse() && header !== false) {
            if (header === (reExpander = me.getReExpander())) {
                header.collapseImmune = true;
                header.getInherited().collapseImmune = true;
                header.addClsWithUI(me.getHeaderCollapsedClasses(header));
                // Ensure that the reExpander has the correct framing applied.
                if (header.rendered) {
                    header.updateFrame();
                }
            } else if (reExpander.el) {
                // We're going to use a temporary reExpander: show it.
                reExpander.el.show();
                reExpander.hidden = false;
            }
        }
        if (me.resizer) {
            me.resizer.disable();
        }
        if (me.rendered) {
            me.ariaEl.dom.setAttribute('aria-expanded', false);
            // In accordion layout, panel body has the role of tabpanel
            // and needs to be updated accordingly when the panel is collapsed
            if (me.isAccordionPanel) {
                me.body.dom.setAttribute('aria-hidden', true);
            }
        }
    },
    beginDrag: function() {
        if (this.floatingDescendants) {
            this.floatingDescendants.hide();
        }
    },
    beginExpand: function() {
        var me = this,
            lastBox = me.lastBox,
            collapseMemento = me.getMemento('collapse'),
            restoreDimension = me.restoreDimension(),
            header = me.header,
            reExpander;
        if (collapseMemento) {
            collapseMemento.restore([
                'minHeight',
                'minWidth',
                restoreDimension
            ]);
            if (lastBox) {
                collapseMemento.restore(restoreDimension, true, lastBox, 'last.');
            }
        }
        if (me.ownerCt) {
            me.ownerCt.getLayout().beginExpand(me);
        }
        if (!me.isPlaceHolderCollapse() && header !== false) {
            // If we have been using our Header as the reExpander then restore the Header
            // to expanded UI
            if (header === (reExpander = me.getReExpander())) {
                delete header.collapseImmune;
                delete header.getInherited().collapseImmune;
                header.removeClsWithUI(me.getHeaderCollapsedClasses(header));
                // Ensure that the reExpander has the correct framing applied.
                if (header.rendered) {
                    header.expanding = true;
                    header.updateFrame();
                    delete header.expanding;
                }
            } else {
                // We've been using a temporary reExpander: hide it.
                reExpander.hidden = true;
                reExpander.el.hide();
            }
        }
        if (me.resizer) {
            me.resizer.enable();
        }
        if (me.rendered) {
            me.ariaEl.dom.setAttribute('aria-expanded', true);
            // In accordion layout, panel body has the role of tabpanel
            // and needs to be updated accordingly when the panel is expanded
            if (me.isAccordionPanel) {
                me.body.dom.setAttribute('aria-hidden', false);
            }
        }
    },
    bridgeToolbars: function() {
        var me = this,
            docked = [],
            minButtonWidth = me.minButtonWidth,
            fbar, fbarDefaults, fbarIsButtons;
        function initToolbar(toolbar, pos, useButtonAlign, disableFocusableContainer) {
            if (Ext.isArray(toolbar)) {
                toolbar = {
                    xtype: 'toolbar',
                    items: toolbar
                };
            } else if (!toolbar.isComponent) {
                // Incoming toolbar config can be a property on the prototype
                toolbar = Ext.apply({}, toolbar);
            }
            if (!toolbar.xtype) {
                toolbar.xtype = 'toolbar';
            }
            toolbar.dock = pos;
            if (disableFocusableContainer) {
                toolbar.focusableContainer = false;
            }
            // Legacy support for buttonAlign (only used by buttons/fbar)
            if (useButtonAlign) {
                toolbar.layout = Ext.apply({
                    // default to 'end' (right-aligned) if me.buttonAlign is undefined or invalid
                    pack: {
                        left: 'start',
                        center: 'center'
                    }[me.buttonAlign] || 'end'
                }, toolbar.layout);
            }
            return toolbar;
        }
        if (me.tbar) {
            docked.push(initToolbar(me.tbar, 'top'));
            me.tbar = null;
        }
        if (me.bbar) {
            docked.push(initToolbar(me.bbar, 'bottom'));
            me.bbar = null;
        }
        if (me.buttons) {
            me.fbar = me.buttons;
            me.buttons = null;
            fbarIsButtons = true;
        }
        if (me.fbar) {
            fbar = initToolbar(me.fbar, 'bottom', true, fbarIsButtons);
            // only we useButtonAlign
            fbar.ui = 'footer';
            // Apply the minButtonWidth config to buttons in the toolbar
            if (minButtonWidth) {
                fbarDefaults = fbar.defaults;
                fbar.defaults = function(config) {
                    var defaults = fbarDefaults || {},
                        // no xtype or a button instance
                        isButton = !config.xtype || config.isButton,
                        cls;
                    // Here we have an object config with an xtype, check if it's a button
                    // or a button subclass
                    if (!isButton) {
                        cls = Ext.ClassManager.getByAlias('widget.' + config.xtype);
                        if (cls) {
                            isButton = cls.prototype.isButton;
                        }
                    }
                    if (isButton && !('minWidth' in defaults)) {
                        defaults = Ext.apply({
                            minWidth: minButtonWidth
                        }, defaults);
                    }
                    return defaults;
                };
            }
            docked.push(fbar);
            me.fbar = null;
        }
        if (me.lbar) {
            docked.push(initToolbar(me.lbar, 'left'));
            me.lbar = null;
        }
        if (me.rbar) {
            docked.push(initToolbar(me.rbar, 'right'));
            me.rbar = null;
        }
        if (me.dockedItems) {
            if (me.dockedItems.isMixedCollection) {
                me.addDocked(docked);
            } else {
                if (!Ext.isArray(me.dockedItems)) {
                    me.dockedItems = [
                        me.dockedItems
                    ];
                }
                me.dockedItems = me.dockedItems.concat(docked);
            }
        } else {
            me.dockedItems = docked;
        }
    },
    /**
     * Closes the Panel. By default, this method, removes it from the DOM,
     * {@link Ext.Component#method-destroy destroy}s the Panel object and all its descendant
     * Components. The {@link #beforeclose beforeclose} event is fired before the
     * close happens and will cancel the close action if it returns false.
     *
     * **Note:** This method is also affected by the {@link #closeAction} setting.
     * For more explicit control use {@link #method-destroy} and {@link #method-hide} methods.
     */
    close: function() {
        if (this.fireEvent('beforeclose', this) !== false) {
            this.doClose();
        }
    },
    /**
     * Collapses the panel body so that the body becomes hidden. Docked Components parallel
     * to the border towards which the collapse takes place will remain visible. Fires the
     * {@link #beforecollapse} event which will cancel the collapse action if it returns false.
     *
     * @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 {@link #collapseDirection}.
     *
     * @param {Boolean/Number} [animate] True to animate the transition, else false
     * (defaults to the value of the {@link #animCollapse} panel config). May
     * also be specified as the animation duration in milliseconds.
     * @return {Ext.panel.Panel} this
     */
    collapse: function(direction, animate) {
        var me = this,
            collapseDir = direction || me.collapseDirection,
            ownerCt = me.ownerCt,
            layout = me.ownerLayout,
            rendered = me.rendered;
        if (me.isCollapsingOrExpanding) {
            return me;
        }
        if (arguments.length < 2) {
            animate = me.animCollapse;
        }
        if (me.collapsed || me.fireEvent('beforecollapse', me, direction, animate) === false) {
            return me;
        }
        if (layout && layout.onBeforeComponentCollapse) {
            if (layout.onBeforeComponentCollapse(me) === false) {
                return me;
            }
        }
        if (rendered && ownerCt && me.isPlaceHolderCollapse()) {
            return me.placeholderCollapse(direction, animate);
        }
        me.collapsed = collapseDir;
        if (rendered) {
            me.beginCollapse();
        }
        me.getInherited().collapsed = true;
        me.fireHierarchyEvent('collapse');
        if (rendered) {
            me.doCollapseExpand(1, animate);
        }
        return me;
    },
    collapsedHorizontal: function() {
        var dir = this.getCollapsed();
        return dir === 'left' || dir === 'right';
    },
    collapsedVertical: function() {
        var dir = this.getCollapsed();
        return dir === 'top' || dir === 'bottom';
    },
    /**
     * converts a collapsdDir into an anchor argument for Element.slideIn
     * overridden in rtl mode to switch "l" and "r"
     */
    convertCollapseDir: function(collapseDir) {
        return collapseDir.substr(0, 1);
    },
    createGhost: function(cls) {
        var me = this,
            header = me.header,
            frame = me.frame && !me.alwaysFramed;
        return {
            xtype: 'panel',
            hidden: false,
            header: header ? {
                titleAlign: header.getTitleAlign()
            } : null,
            ui: frame ? me.ui.replace(/-framed$/, '') : me.ui,
            id: me.id + '-ghost',
            renderTo: Ext.getBody(),
            // The ghost's opacity causes the resize handles to obscure the frame in
            // IE, so always force resizable to be false.
            resizable: false,
            // The ghost must not be draggable (the actual class instantiated
            // may be draggable in its prototype)
            draggable: false,
            // Tools are explicitly copied.
            closable: false,
            focusable: false,
            floating: true,
            alignOnScroll: false,
            shadow: false,
            frame: frame,
            shim: me.shim,
            alwaysFramed: me.alwaysFramed,
            overlapHeader: me.overlapHeader,
            headerPosition: me.getHeaderPosition(),
            titleRotation: me.getTitleRotation(),
            baseCls: me.baseCls,
            getRefOwner: function() {
                return me.getRefOwner();
            },
            cls: me.baseCls + '-ghost ' + (cls || '')
        };
    },
    createReExpander: function(direction, defaults) {
        var me = this,
            isLeft = direction === 'left',
            isRight = direction === 'right',
            isVertical = isLeft || isRight,
            ownerCt = me.ownerCt,
            header = me.header,
            result = Ext.apply({
                hideMode: 'offsets',
                title: me.getTitle(),
                titleAlign: me.getTitleAlign(),
                titlePosition: me.getTitlePosition(),
                vertical: isVertical,
                textCls: me.headerTextCls,
                icon: me.getIcon(),
                iconCls: me.getIconCls(),
                iconAlign: me.getIconAlign(),
                glyph: me.getGlyph(),
                baseCls: me.self.prototype.baseCls + '-header',
                ui: me.ui,
                frame: me.frame && me.frameHeader,
                ignoreParentFrame: me.frame || me.overlapHeader,
                ignoreBorderManagement: me.frame || me.ignoreHeaderBorderManagement,
                indicateDrag: me.draggable,
                collapseImmune: true,
                ariaRole: me.ariaRole,
                preventRefocus: true,
                ownerCt: (ownerCt && me.collapseMode === 'placeholder') ? ownerCt : me,
                ownerLayout: me.componentLayout,
                forceOrientation: true,
                margin: me.margin,
                // When placeholder is focused, focus the expander tool.
                // TODO: When https://sencha.jira.com/browse/EXTJS-19718 is
                // fixed, this should not be needed.
                // placeholder is a FocusableContainer
                defaultFocus: 'tool[isDefaultExpandTool]'
            }, defaults);
        // If we're in mini mode, set the placeholder size to only 1px since
        // we don't need it to show up.
        if (me.collapseMode === 'mini') {
            if (isVertical) {
                result.width = 1;
            } else {
                result.height = 1;
            }
        }
        if (header) {
            Ext.apply(result, {
                focusableContainer: header.focusableContainer,
                activeChildTabIndex: header.activeChildTabIndex,
                inactiveChildTabIndex: header.inactiveChildTabIndex,
                allowFocusingDisabledChildren: header.allowFocusingDisabledChildren
            });
        }
        // Create the re expand tool
        // For UI consistency reasons, collapse:left reExpanders, and region: 'west' placeHolders
        // have the re expand tool at the *top* with a bit of space.
        if (!me.hideCollapseTool) {
            if (!me.maintainTitlePosition && (isLeft || (isRight && me.isPlaceHolderCollapse()))) {
                // adjust the title position if the collapse tool needs to be at the
                // top of a vertical header
                result.titlePosition = 1;
            }
            result.tools = [
                {
                    xtype: 'tool',
                    type: 'expand-' + me.getOppositeDirection(direction),
                    isDefaultExpandTool: true,
                    uiCls: [
                        'top'
                    ],
                    handler: me.toggleCollapse,
                    scope: me,
                    tooltip: me.expandToolText
                }
            ];
        }
        result = new Ext.panel.Header(result);
        result.addClsWithUI(me.getHeaderCollapsedClasses(result));
        result.expandTool = result.down('tool[isDefaultExpandTool=true]');
        return result;
    },
    /**
     * @private
     */
    doClose: function() {
        this.fireEvent('close', this);
        this[this.closeAction]();
    },
    doCollapseExpand: function(flags, animate) {
        var me = this,
            originalAnimCollapse = me.animCollapse,
            ownerLayout = me.ownerLayout;
        // we need to temporarily set animCollapse to the animate value here because ContextItem
        // uses the animCollapse property to determine if the collapse/expand should be animated
        me.animCollapse = animate;
        // Flag used by the layout ContextItem to impose an animation policy based upon the
        // collapse direction and the animCollapse setting.
        me.isCollapsingOrExpanding = flags;
        // The x-animating-size class sets overflow:hidden so that overflowing
        // content is clipped during animation.
        if (animate) {
            me.addCls(Ext.baseCSSPrefix + 'animating-size');
        }
        if (ownerLayout && !animate) {
            ownerLayout.onContentChange(me);
        } else {
            me.updateLayout({
                isRoot: true
            });
        }
        // set animCollapse back to its original value
        me.animCollapse = originalAnimCollapse;
        return me;
    },
    endDrag: function() {
        if (this.floatingDescendants) {
            this.floatingDescendants.show();
        }
    },
    /**
     * Expands the panel body so that it becomes visible.  Fires the {@link #beforeexpand}
     * event which will cancel the expand action if it returns false.
     * @param {Boolean} [animate] True to animate the transition, else false
     * (defaults to the value of the {@link #animCollapse} panel config).  May
     * also be specified as the animation duration in milliseconds.
     * @return {Ext.panel.Panel} this
     */
    expand: function(animate) {
        var me = this,
            layout = me.ownerLayout,
            rendered = me.rendered;
        if (me.isCollapsingOrExpanding) {
            return me;
        }
        if (!arguments.length) {
            animate = me.animCollapse;
        }
        if (!me.collapsed && !me.floatedFromCollapse) {
            return me;
        }
        if (me.fireEvent('beforeexpand', me, animate) === false) {
            return me;
        }
        if (layout && layout.onBeforeComponentExpand) {
            if (layout.onBeforeComponentExpand(me) === false) {
                return me;
            }
        }
        delete me.getInherited().collapsed;
        if (rendered && me.isPlaceHolderCollapse()) {
            return me.placeholderExpand(animate);
        }
        me.restoreHiddenDocked();
        if (rendered) {
            me.beginExpand();
        }
        me.collapsed = false;
        if (me.rendered) {
            me.doCollapseExpand(2, animate);
        }
        return me;
    },
    findReExpander: function(direction) {
        var me = this,
            c = Ext.Component,
            dockedItems = me.dockedItems.items,
            dockedItemCount = dockedItems.length,
            comp, i;
        // never use the header if we're in collapseMode mini
        if (me.collapseMode === 'mini') {
            return;
        }
        switch (direction) {
            case c.DIRECTION_TOP:
            case c.DIRECTION_BOTTOM:
                // Attempt to find a reExpander Component (docked in a horizontal orientation)
                // Also, collect all other docked items which we must hide after collapse.
                for (i = 0; i < dockedItemCount; i++) {
                    comp = dockedItems[i];
                    if (!comp.hidden) {
                        if (comp.isHeader && (!comp.dock || comp.dock === 'top' || comp.dock === 'bottom')) {
                            return comp;
                        }
                    }
                };
                break;
            case c.DIRECTION_LEFT:
            case c.DIRECTION_RIGHT:
                // Attempt to find a reExpander Component (docked in a vertical orientation)
                // Also, collect all other docked items which we must hide after collapse.
                for (i = 0; i < dockedItemCount; i++) {
                    comp = dockedItems[i];
                    if (!comp.hidden) {
                        if (comp.isHeader && (comp.dock === 'left' || comp.dock === 'right')) {
                            return comp;
                        }
                    }
                };
                break;
            default:
                throw new Error('Panel#findReExpander must be passed a valid collapseDirection');
        }
    },
    floatCollapsedPanel: function() {
        var me = this,
            placeholder = me.placeholder,
            splitter = me.splitter,
            phBox = Ext.util.Region.from(placeholder.getBox(false, true)),
            floatCls = Ext.panel.Panel.floatCls,
            collapsed = me.collapsed,
            layoutOwner = me.ownerCt || me,
            slideDirection, myBox,
            hoverlisteners = {
                mouseleave: me.onMouseLeaveFloated,
                mouseenter: me.onMouseEnterFloated,
                scope: me,
                destroyable: true
            };
        if (me.isSliding) {
            return;
        }
        // Already floated
        if (me.el.hasCls(floatCls)) {
            me.slideOutFloatedPanel();
            return;
        }
        me.isSliding = true;
        // Lay out in fully expanded mode to ensure we are at the correct size,
        // and collect our expanded box
        placeholder.el.hide();
        placeholder.hidden = true;
        me.el.show();
        me.setHiddenState(false);
        me.collapsed = false;
        layoutOwner.updateLayout();
        // Then go back immediately to collapsed state from which to initiate the float into view.
        placeholder.el.show();
        placeholder.hidden = false;
        me.el.hide();
        me.setHiddenState(true);
        me.collapsed = collapsed;
        layoutOwner.updateLayout();
        myBox = me.getBox(false, true);
        if (me.fireEvent('beginfloat', me) === false) {
            return;
        }
        me.slideOutTask = me.slideOutTask || new Ext.util.DelayedTask(me.slideOutFloatedPanel, me);
        // Tap/mousedown/mousemove outside the floated element, its placeholder,
        // or its splitter slides it back.
        me.pointerLeaveListener = Ext.getDoc().on({
            mousedown: me.onFloatedPointerEvent,
            mousemove: me.onFloatedPointerEvent,
            scope: me,
            destroyable: true
        });
        if (!me.placeholderListener) {
            me.placeholderListener = placeholder.on({
                resize: me.onPlaceholderResize,
                scope: me,
                destroyable: true
            });
        }
        me.phHoverListeners = placeholder.el.on(hoverlisteners);
        me.elHoverListeners = me.el.on(hoverlisteners);
        me.el.addCls(floatCls);
        me.floated = collapsed;
        // Hide collapse tool in header if there is one (we might be headerless)
        if (me.collapseTool) {
            me.collapseTool.el.hide();
        }
        if (splitter) {
            phBox = phBox.union(splitter.getBox(false, true));
        }
        switch (me.collapsed) {
            case 'top':
                me.width = phBox.width;
                me.setLocalXY(myBox.x, myBox.y + phBox.height);
                break;
            case 'right':
                me.height = phBox.height;
                me.setLocalXY(myBox.x - phBox.width, myBox.y);
                break;
            case 'bottom':
                me.width = phBox.width;
                me.setLocalXY(myBox.x, myBox.y - phBox.height);
                break;
            case 'left':
                me.height = phBox.height;
                me.setLocalXY(myBox.x + phBox.width, myBox.y);
                break;
        }
        slideDirection = me.convertCollapseDir(me.collapsed);
        // Remember how we are really collapsed so we can restore it, but also so we can
        // become a layoutRoot while we are floated:
        me.floatedFromCollapse = me.collapsed;
        me.collapsed = false;
        me.setHiddenState(false);
        me.el.slideIn(slideDirection, {
            preserveScroll: true,
            duration: Ext.Number.from(me.animCollapse, Ext.fx.Anim.prototype.duration),
            listeners: {
                afteranimate: function() {
                    me.isSliding = false;
                    me.fireEvent('endfloat', me);
                    me.fireEvent('float', me);
                }
            }
        });
    },
    onFloatedPointerEvent: function(event) {
        var me = this;
        // If any pointer event occurs inside the component tree, cancel any slideOut.
        // This includes if we are slid out and the pointer is inside the region
        // because locked grids have pointer-events: none to allow interaction with
        // the Y scroller below them.
        if (me.owns(event) || me.placeholder.owns(event) || (me.splitter && me.splitter.owns(event)) || (me.floatCollapsedPanel && me.el.getRegion().contains(event.getPoint()))) {
            me.slideOutTask.cancel();
        } else // Mousemove outside, or tap outside, schedule a slideOut unless they change their minds and
        // tap/mousemove back inside within 500ms
        {
            me.slideOutTask.delay(500);
        }
    },
    onMouseEnterFloated: function(e) {
        this.slideOutTask.cancel();
    },
    onMouseLeaveFloated: function(e) {
        var toElement = e.getRelatedTarget();
        // If the toElement is in the component tree, do not collapse
        if (toElement && (this.owns(toElement) || this.placeholder.owns(toElement))) {
            return;
        }
        this.slideOutTask.delay(500);
    },
    onPlaceholderResize: function(ph, newWidth, newHeight) {
        var me = this,
            splitter = me.splitter,
            myBox = me.getBox(false, true),
            phBox = Ext.util.Region.from(ph.getBox(false, true));
        if (splitter) {
            phBox = phBox.union(splitter.getBox(false, true));
        }
        // Position floated panel alongside the placeholder, and sync the parallel dimension
        switch (me.floated) {
            case 'top':
                me.width = newWidth;
                me.setLocalY(phBox.y + phBox.height);
                break;
            case 'right':
                me.height = newHeight;
                me.setLocalX(phBox.x - myBox.width);
                break;
            case 'bottom':
                me.width = newWidth;
                me.setLocalY(phBox.y - myBox.height);
                break;
            case 'left':
                me.height = newHeight;
                me.setLocalX(phBox.x + phBox.width);
                break;
        }
        me.updateLayout({
            isRoot: true
        });
    },
    getAnimationProps: function() {
        var me = this,
            props;
        props = me.callParent();
        if (typeof me.animCollapseDuration === 'number') {
            props.duration = me.animCollapseDuration;
        } else if (typeof me.animCollapse === 'number') {
            props.duration = me.animCollapse;
        }
        return props;
    },
    /**
     * Returns the current collapsed state of the panel.
     * @return {Boolean/String} False when not collapsed, otherwise the value of
     * {@link #collapseDirection}.
     */
    getCollapsed: function() {
        var me = this;
        // The collapsed flag, when the Panel is collapsed acts as the direction
        // in which the collapse took place. It can still be tested as truthy/falsy
        // if only a truth value is required.
        if (me.collapsed === true) {
            return me.collapseDirection;
        }
        return me.collapsed;
    },
    getCollapsedDockedItems: function() {
        var me = this;
        return me.header === false || (me.collapseMode === 'placeholder' ? me.emptyArray : [
            me.getReExpander()
        ]);
    },
    /**
     * Attempts a default component lookup (see {@link Ext.container.Container#getComponent}).
     * If the component is not found in the normal items, the dockedItems are searched
     * and the matched component (if any) returned (see {@link #getDockedComponent}).
     * Note that docked items will only be matched by component id or itemId -- if you pass
     * a numeric index only non-docked child components will be searched.
     * @param {String/Number} comp The component id, itemId or position to find
     * @return {Ext.Component} The component (if found)
     * @since 2.3.0
     */
    getComponent: function(comp) {
        var component = this.callParent(arguments);
        if (component === undefined && !Ext.isNumber(comp)) {
            // If the arg is a numeric index skip docked items
            component = this.getDockedComponent(comp);
        }
        return component;
    },
    /**
     * Gets the {@link Ext.panel.Header Header} for this panel.
     * @return {Ext.panel.Header}
     */
    getHeader: function() {
        return this.header;
    },
    /**
     * @private
     * Create the class array to add to the Header when collapsed.
     */
    getHeaderCollapsedClasses: function(header) {
        var me = this,
            collapsedCls = me.collapsedCls,
            collapsedClasses;
        collapsedClasses = [
            collapsedCls,
            collapsedCls + '-' + header.getDockName()
        ];
        if (me.border && (!me.frame || (me.frame && Ext.supports.CSS3BorderRadius))) {
            collapsedClasses.push(collapsedCls + '-border-' + header.getDockName());
        }
        return collapsedClasses;
    },
    getOppositeDirection: function(d) {
        var c = Ext.Component;
        switch (d) {
            case c.DIRECTION_TOP:
                return c.DIRECTION_BOTTOM;
            case c.DIRECTION_RIGHT:
                return c.DIRECTION_LEFT;
            case c.DIRECTION_BOTTOM:
                return c.DIRECTION_TOP;
            case c.DIRECTION_LEFT:
                return c.DIRECTION_RIGHT;
        }
    },
    getPlaceholder: function(direction) {
        var me = this,
            collapseDir = direction || me.collapseDirection,
            listeners = null,
            placeholder = me.placeholder,
            floatable = me.floatable,
            titleCollapse = me.titleCollapse;
        if (!placeholder) {
            if (floatable || (me.collapsible && titleCollapse)) {
                listeners = {
                    click: {
                        // titleCollapse needs to take precedence over floatable
                        fn: function(e, target) {
                            var expandTool = placeholder.expandTool;
                            // If the element click was specifically on the tool,
                            // that tool's handler will process it and we must not:
                            // https://sencha.jira.com/browse/EXTJS-21045
                            if (!(expandTool && expandTool.el.dom.contains(arguments[1]))) {
                                // eslint-disable-next-line max-len
                                me[(!titleCollapse && floatable) ? 'floatCollapsedPanel' : 'toggleCollapse']();
                            }
                        },
                        element: 'el',
                        scope: me
                    }
                };
            }
            me.placeholder = placeholder = Ext.widget(me.createReExpander(collapseDir, {
                id: me.id + '-placeholder',
                listeners: listeners
            }));
        }
        // User created placeholder was passed in
        if (!placeholder.placeholderFor) {
            // Handle the case of a placeholder config
            if (!placeholder.isComponent) {
                me.placeholder = placeholder = me.lookupComponent(placeholder);
            }
            Ext.applyIf(placeholder, {
                margin: me.margin,
                placeholderFor: me,
                synthetic: true
            });
            // not user-defined
            placeholder.addCls([
                Ext.baseCSSPrefix + 'region-collapsed-placeholder',
                Ext.baseCSSPrefix + 'region-collapsed-' + collapseDir + '-placeholder',
                me.collapsedCls
            ]);
        }
        return placeholder;
    },
    getProtoBody: function() {
        var me = this,
            body = me.protoBody;
        if (!body) {
            me.protoBody = body = new Ext.util.ProtoElement({
                cls: me.bodyCls,
                style: me.bodyStyle,
                clsProp: 'bodyCls',
                styleProp: 'bodyStyle',
                styleIsText: true
            });
        }
        return body;
    },
    getReExpander: function(direction) {
        var me = this,
            collapseDir = direction || me.collapseDirection,
            reExpander = me.reExpander || me.findReExpander(collapseDir),
            titleCollapse = me.titleCollapse,
            listeners = null;
        me.expandDirection = me.getOppositeDirection(collapseDir);
        if (!reExpander) {
            if (titleCollapse) {
                listeners = {
                    click: {
                        fn: me.toggleCollapse,
                        element: 'el',
                        scope: me
                    }
                };
            }
            // We did not find a Header of the required orientation: create one.
            me.reExpander = reExpander = me.createReExpander(collapseDir, {
                dock: collapseDir,
                cls: Ext.baseCSSPrefix + 'docked ' + me.baseCls + '-' + me.ui + '-collapsed',
                isCollapsedExpander: true,
                listeners: listeners
            });
            me.dockedItems.insert(0, reExpander);
        }
        return reExpander;
    },
    getRefItems: function(deep) {
        var placeholder = this.placeholder,
            result;
        // Collapsed placeholder must be queryable if it is a Container.
        // The placeholder can be objects/ Ext.Component and thus check
        // is required before it is further processed as a component
        if (placeholder && placeholder.isComponent) {
            result = [
                placeholder
            ];
            if (deep && placeholder.getRefItems) {
                result.push.apply(result, placeholder.getRefItems(deep));
            }
        } else {
            result = [];
        }
        // And the rest.
        result.push.apply(result, this.getDockingRefItems(deep, this.callParent([
            deep
        ])));
        return result;
    },
    getState: function() {
        var me = this,
            state = me.callParent() || {},
            collapsed = me.collapsed,
            floated = me.floated,
            memento, placeholder;
        // When taking state to restore on a page refresh, floated means collapsed
        if (floated) {
            me.collapsed = floated;
        }
        state = me.addPropertyToState(state, 'collapsed');
        if (floated) {
            me.collapsed = collapsed;
        }
        // If a collapse has taken place, use remembered values as the dimensions.
        if (me.getCollapsed()) {
            memento = me.getMemento('collapse').data;
            state = me.addPropertyToState(state, 'collapsed', memento);
            placeholder = me.isPlaceHolderCollapse();
            if (me.collapsedVertical()) {
                if (placeholder) {
                    state = me.addPropertyToState(state, 'height');
                    delete state.width;
                } else {
                    delete state.height;
                    if (memento) {
                        state = me.addPropertyToState(state, 'height', memento.height);
                    }
                }
            } else {
                if (placeholder) {
                    state = me.addPropertyToState(state, 'width');
                    delete state.height;
                } else {
                    delete state.width;
                    if (memento) {
                        state = me.addPropertyToState(state, 'width', memento.width);
                    }
                }
            }
        }
        return state;
    },
    applyState: function(state) {
        var me = this,
            collapseMemento = {},
            collapsed;
        if (state) {
            collapsed = state.collapsed;
            if (collapsed) {
                collapseMemento = me.getMemento('collapse');
                Ext.Object.merge(collapseMemento.data, collapsed);
                state.collapsed = true;
            }
            me.callParent([
                state
            ]);
        }
    },
    /**
     * @private
     * Used for dragging.
     */
    ghost: function(cls) {
        var me = this,
            myEl = me.el,
            ghostPanel = me.ghostPanel,
            box = me.getBox(),
            header = me.header,
            ghostHeader, tools, icon, iconCls, glyph, i;
        Ext.suspendLayouts();
        if (!ghostPanel) {
            me.ghostPanel = ghostPanel = Ext.widget(me.createGhost(cls));
            ghostPanel.el.dom.removeAttribute('tabIndex');
        } else {
            ghostPanel.el.show();
        }
        // Important to do this before the setSize call otherwise it won't
        // stamp the sizes onto the element.
        ghostPanel.setHiddenState(false);
        ghostPanel.setXY([
            box.x,
            box.y
        ]);
        ghostPanel.setSize(box.width, box.height);
        ghostPanel.floatParent = me.floatParent;
        // Only churn ghost's header if our header has changed composition
        if (header && !me.preventHeader && me.lastHeaderGen !== header.items.generation) {
            ghostHeader = ghostPanel.header;
            // restore options
            tools = ghostHeader.query('tool');
            for (i = tools.length; i--; ) {
                ghostHeader.remove(tools[i]);
            }
            // reset the title position to ensure that the title gets moved into the correct
            // place after we add the tools (if the position didn't change the updater won't run)
            ghostHeader.setTitlePosition(0);
            ghostPanel.addTool(me.ghostTools());
            ghostPanel.setTitle(me.getTitle());
            ghostHeader.setTitlePosition(header.getTitlePosition());
            ghostHeader.setIconAlign(header.getIconAlign());
            ghostHeader.setTitleAlign(header.getTitleAlign());
            iconCls = me.getIconCls();
            if (iconCls) {
                ghostPanel.setIconCls(iconCls);
            } else {
                icon = me.getIcon();
                if (icon) {
                    ghostPanel.setIcon(icon);
                } else {
                    glyph = me.getGlyph();
                    if (glyph) {
                        ghostPanel.setGlyph(glyph);
                    }
                }
            }
            ghostHeader.addCls(Ext.baseCSSPrefix + 'header-ghost');
            me.lastHeaderGen = header.items.generation;
        }
        Ext.resumeLayouts(true);
        // Hide element, but do not disturb focus or clobber accessibility
        me.elVisMode = myEl.getVisibilityMode();
        myEl.setVisibilityMode(Ext.Element.CLIP);
        myEl.hide();
        return ghostPanel;
    },
    /**
     * @private
     * Helper function for ghost
     */
    ghostTools: function() {
        var tools = [],
            header = this.header,
            headerTools = header ? header.query('tool[hidden=false]') : [],
            t, tLen, tool;
        if (headerTools.length) {
            t = 0;
            tLen = headerTools.length;
            for (; t < tLen; t++) {
                tool = headerTools[t];
                // Some tools can be full components, and copying them into the ghost
                // actually removes them from the owning panel. You could also potentially
                // end up with duplicate DOM ids as well. To avoid any issues we just make
                // a simple bare-minimum clone of each tool for ghosting purposes.
                tools.push({
                    type: tool.type,
                    focusable: false
                });
            }
        } else {
            tools = [
                {
                    type: 'placeholder'
                }
            ];
        }
        return tools;
    },
    initBodyBorder: function() {
        var me = this;
        if (me.frame && me.bodyBorder) {
            if (!Ext.isNumber(me.bodyBorder)) {
                me.bodyBorder = 1;
            }
            me.getProtoBody().setStyle('border-width', this.unitizeBox(me.bodyBorder));
        }
    },
    /**
     * Parses the {@link #bodyStyle} config if available to create a style string
     * that will be applied to the body element.
     * This also includes {@link #bodyPadding} and {@link #bodyBorder} if available.
     * @return {String} A CSS style string with body styles, padding and border.
     * @private
     */
    initBodyStyles: function() {
        var me = this,
            body = me.getProtoBody();
        if (me.bodyPadding !== undefined) {
            if (me.layout.managePadding) {
                // If the container layout manages padding, the layout will apply the
                // padding to an inner element rather than the body element.  The
                // assumed intent is for the configured padding to override any padding
                // that is applied to the body element via style sheet rules.  It is
                // therefore necessary to set the body element's padding to "0".
                body.setStyle('padding', 0);
            } else {
                body.setStyle('padding', this.unitizeBox((me.bodyPadding === true) ? 5 : me.bodyPadding));
            }
        }
        me.initBodyBorder();
    },
    initBorderProps: function() {
        var me = this;
        if (me.frame && me.border && me.bodyBorder === undefined) {
            me.bodyBorder = false;
        }
        if (me.frame && me.border && (me.bodyBorder === false || me.bodyBorder === 0)) {
            me.manageBodyBorders = true;
        }
    },
    initComponent: function() {
        var me = this;
        if (me.collapsible) {
            // Save state on these two events.
            me.addStateEvents([
                'expand',
                'collapse'
            ]);
        }
        if (me.unstyled) {
            me.setUI('plain');
        }
        if (me.frame) {
            me.setUI(me.ui + '-framed');
        }
        // Backwards compatibility
        me.bridgeToolbars();
        me.initBorderProps();
        me.callParent();
        me.collapseDirection = me.collapseDirection || me.getHeaderPosition() || Ext.Component.DIRECTION_TOP;
        // Certain layouts will reset animCollapse to false but we'd like to preserve
        // the duration to use with animations, if it was configured
        if (typeof me.animCollapse === 'number') {
            me.animCollapseDuration = me.animCollapse;
        }
        // Used to track hidden content elements during collapsed state
        me.hiddenOnCollapse = new Ext.dom.CompositeElement();
    },
    initItems: function() {
        this.callParent();
        this.initDockingItems();
    },
    /**
     * Initialized the renderData to be used when rendering the renderTpl.
     * @return {Object} Object with keys and values that are going to be applied to the renderTpl
     * @private
     */
    initRenderData: function() {
        var me = this,
            bodyWrapRole = me.bodyWrapAriaRole,
            bodyRole = me.bodyAriaRole,
            data;
        data = me.callParent();
        me.initBodyStyles();
        me.protoBody.writeTo(data);
        delete me.protoBody;
        if (me.headingText) {
            data.headingText = me.headingText;
            me.addChildEl('headingEl');
        }
        if (bodyWrapRole) {
            data.bodyWrapAriaAttributes = {
                role: bodyWrapRole
            };
            if (!me.ariaStaticRoles[bodyWrapRole] && me.bodyWrapAriaRenderAttributes) {
                Ext.apply(data.bodyWrapAriaAttributes, me.bodyWrapAriaRenderAttributes);
            }
        }
        if (bodyRole) {
            data.bodyAriaAttributes = {
                role: bodyRole
            };
            if (!me.ariaStaticRoles[bodyRole] && me.bodyAriaRenderAttributes) {
                Ext.apply(data.bodyAriaAttributes, me.bodyAriaRenderAttributes);
            }
        }
        return data;
    },
    /**
     * @private
     * Override of Positionable method to calculate constrained position based upon possibly only
     * constraining our header.
     */
    calculateConstrainedPosition: function(constrainTo, proposedPosition, local, proposedSize) {
        var me = this,
            header = me.header,
            lastBox, fp;
        // If we are only constraining the header, ask the header for its constrained position
        // based upon the size the header will take on based upon this panel's proposedSize
        if (me.constrainHeader) {
            lastBox = header.lastBox;
            if (proposedSize) {
                if (!header.vertical) {
                    proposedSize = [
                        proposedSize[0],
                        lastBox ? lastBox.height : proposedSize[1]
                    ];
                } else {
                    proposedSize = [
                        lastBox ? lastBox.width : proposedSize[0],
                        proposedSize[1]
                    ];
                }
            } else if (lastBox) {
                proposedSize = [
                    lastBox.width,
                    lastBox.height
                ];
            }
            fp = me.floatParent;
            constrainTo = constrainTo || me.constrainTo || (fp ? fp.getTargetEl() : null) || me.container || me.el.parent();
        }
        return me.callParent([
            constrainTo,
            proposedPosition,
            local,
            proposedSize
        ]);
    },
    /**
     * @private
     * Tools are a Panel-specific capability.
     * Panel uses initTools. Subclasses may contribute tools by implementing addTools.
     */
    initTools: function() {
        var me = this,
            tools = me.tools,
            len = tools && tools.length,
            i, toolCfg, tool;
        me.tools = [];
        if (len) {
            for (i = 0; i < len; ++i) {
                tool = tools[i];
                if (typeof tool !== 'string' && !tool.isTool) {
                    tool = Ext.apply({}, tool);
                }
                me.tools.push(tool);
                tool.toolOwner = me;
            }
        }
        // Add a collapse tool unless configured to not show a collapse tool
        // or to not even show a header.
        if (me.collapsible && !(me.hideCollapseTool || me.header === false || me.preventHeader)) {
            me.updateCollapseTool();
            // Prepend collapse tool is configured to do so.
            if (me.collapseFirst) {
                me.tools.unshift(me.collapseTool);
            }
        }
        // Add subclass-specific tools.
        me.addTools();
        if (me.pinnable) {
            me.initPinnable();
        }
        // Make Panel closable.
        if (me.closable) {
            me.addClsWithUI('closable');
            toolCfg = {
                xtype: 'tool',
                type: 'close',
                scope: me,
                handler: me.close,
                tooltip: me.closeToolText
            };
            // Same as with the collapse/expand tool, we have a way to close
            // the panel via keyboard by pressing Alt-Del key when panel's
            // title is focused; hence we do not need to have the close tool
            // in the tab order. We still need to have the tool itself for
            // pointer interaction and presentational purposes.
            // This configuration will make sure the tool is working as it was
            // in Ext JS older than 6.0.
            if (me.isAccordionPanel || me.disableCloseToolFocus) {
                toolCfg.focusable = false;
                toolCfg.ariaRole = 'presentation';
            }
            me.addTool(toolCfg);
        }
        // Append collapse tool if needed.
        if (me.collapseTool && !me.collapseFirst) {
            me.addTool(me.collapseTool);
        }
    },
    isLayoutRoot: function() {
        if (this.floatedFromCollapse) {
            return true;
        }
        return this.callParent();
    },
    isPlaceHolderCollapse: function() {
        return this.collapseMode === 'placeholder';
    },
    isVisible: function(deep) {
        var me = this;
        if (me.collapsed && me.placeholder) {
            return me.placeholder.isVisible(deep);
        }
        return me.callParent(arguments);
    },
    onBoxReady: function() {
        var me = this,
            target;
        me.callParent(arguments);
        if (me.collapsed) {
            me.setHiddenDocked();
        }
        if (me.isAccordionPanel) {
            // ARIA state like expanded/collapsed are reflected
            // on the panel header's title component.
            me.ariaEl = me.header.titleCmp.el;
            me.ariaEl.dom.setAttribute('aria-expanded', !me.collapsed);
            me.ariaEl.dom.setAttribute('aria-controls', me.body.id);
            // Body element has the role="tabpanel"; when the panel is collapsed
            // or expanded we will update ARIA attributes on the body.
            me.body.dom.setAttribute('aria-labelledby', me.header.titleCmp.id);
            me.body.dom.setAttribute('aria-hidden', !!me.collapsed);
            me.accordionHeaderKeyNav = new Ext.util.KeyNav({
                target: me.header.titleCmp.el,
                scope: me,
                left: me.navigateAccordionHeader,
                right: me.navigateAccordionHeader,
                up: me.navigateAccordionHeader,
                down: me.navigateAccordionHeader,
                home: me.navigateAccordionHeader,
                end: me.navigateAccordionHeader,
                space: me.toggleCollapse,
                enter: me.toggleCollapse,
                del: {
                    alt: true,
                    fn: me.maybeClose
                }
            });
            me.accordionBodyKeyNav = new Ext.util.KeyNav({
                target: me.bodyWrap,
                scope: me,
                up: {
                    ctrl: true,
                    fn: me.navigateAccordionBody
                }
            });
        }
        if (me.defaultButton) {
            target = me.defaultButtonTarget ? me[me.defaultButtonTarget] : me.body;
            me.defaultButtonKeyNav = new Ext.util.KeyNav({
                target: target,
                scope: me,
                defaultEventAction: 'stopEvent',
                enter: me.fireDefaultButton
            });
        }
    },
    onHide: function(animateTarget, cb, scope) {
        var me = this,
            dd = me.dd;
        // If floated out from collapse, hide the el immediately.
        // We continue with the hide from a collapsed state.
        if (me.floatedFromCollapse) {
            me.slideOutFloatedPanel(true);
        }
        if (me.draggable && dd) {
            // Panels w/o headers won't have a Component Dragger.
            dd.endDrag();
        }
        if (me.collapsed && me.placeholder) {
            if (me.splitter) {
                Ext.suspendLayouts();
                me.splitter.hide();
                Ext.resumeLayouts();
            }
            me.placeholder.hide();
        } else {
            me.callParent([
                animateTarget,
                cb,
                scope
            ]);
        }
    },
    /**
     * @method onRemoved
     * @inheritdoc
     */
    onRemoved: function(destroying) {
        var me = this;
        // If we are removed but not being destroyed, ensure our placeholder is also removed
        // but not destroyed
        // If we are being destroyed, our destroy processing will destroy the placeholder.
        // Must run before callParent because that breaks the ownerCt link
        if (me.placeholder && !destroying) {
            me.ownerCt.remove(me.placeholder, false);
        }
        me.callParent(arguments);
    },
    onShow: function() {
        var me = this;
        if (me.collapsed && me.isPlaceHolderCollapse()) {
            if (me.splitter) {
                Ext.suspendLayouts();
                me.splitter.show();
                Ext.resumeLayouts();
            }
            // force hidden back to true, since this gets set by the layout
            me.setHiddenState(true);
            me.placeholderCollapse();
        } else {
            me.callParent(arguments);
        }
    },
    placeholderCollapse: function(direction, animate) {
        var me = this,
            ownerCt = me.ownerCt,
            collapseDir = direction || me.collapseDirection,
            floatCls = Ext.panel.Panel.floatCls,
            collapseTool = me.collapseTool,
            placeholder = me.getPlaceholder(collapseDir),
            slideInDirection;
        if (Ext.Component.layoutSuspendCount || me.isLayoutSuspended()) {
            animate = false;
        }
        me.fireEvent('beginfloat', me);
        me.isCollapsingOrExpanding = 1;
        // Upcoming layout run will ignore this Component
        me.setHiddenState(true);
        me.collapsed = collapseDir;
        if (placeholder.rendered) {
            // We may have been added to another Container from that in which we rendered
            // the placeholder
            if (placeholder.el.dom.parentNode !== me.el.dom.parentNode) {
                me.el.dom.parentNode.insertBefore(placeholder.el.dom, me.el.dom);
            }
            placeholder.hidden = false;
            placeholder.setHiddenState(false);
            placeholder.el.show();
            ownerCt.updateLayout();
        } else {
            ownerCt.insert(ownerCt.items.indexOf(me), placeholder);
        }
        if (me.rendered) {
            // The doPlaceholderCollapse callback must not make assumptions about where
            // to restore focus to. We decide that here.
            me.focusPlaceholderExpandTool = me.focusPlaceHolder = false;
            // We assume that if collapse was caused by keyboard action
            // on focused collapse tool, the logical focus transition
            // is to placeholder's expand tool. Note that it may not be
            // the case when the user *clicked* collapse tool while focus
            // was elsewhere; in that case we dare not touch focus
            // to avoid sudden jumps.
            if (collapseTool && Ext.ComponentManager.getActiveComponent() === collapseTool) {
                me.focusPlaceholderExpandTool = true;
            }
            // If the focus was otherwise owned by us, just focus the placeholder
            else if (me.containsFocus) {
                me.focusPlaceHolder = true;
            }
            // We MUST NOT hide using display because that resets all scroll information.
            me.el.setVisibilityMode(me.placeholderCollapseHideMode);
            if (animate) {
                me.el.addCls(floatCls);
                placeholder.el.hide();
                slideInDirection = me.convertCollapseDir(collapseDir);
                me.el.slideOut(slideInDirection, {
                    preserveScroll: true,
                    duration: Ext.Number.from(animate, Ext.fx.Anim.prototype.duration),
                    listeners: {
                        scope: me,
                        afteranimate: function() {
                            var me = this,
                                placeholderEl = me.placeholder.el;
                            me.el.removeCls(floatCls);
                            /* We need to show the element so that slideIn will work correctly.
                             * However, if we leave it visible then it can be seen before
                             * the animation starts, causing a flicker. The solution,
                             * borrowed from date picker, is to hide it using display:none.
                             * The slideIn effect includes a call to fixDisplay() that will
                             * undo the display none at the appropriate time.
                             *
                             * Also note that presently there is no way to abort slideIn
                             * animation sequence so this callback will be fired even if
                             * the panel was removed while collapsing.
                             * In that case we must not set "display: none" on the placeholder
                             * because fixDisplay() will not reset it and that will break
                             * subsequent layouts if the panel is re-added back to the owner
                             * container again.
                             */
                            placeholderEl.show();
                            if (me.ownerCt) {
                                placeholderEl.setStyle('display', 'none');
                                placeholderEl.slideIn(slideInDirection, {
                                    easing: 'linear',
                                    duration: 100,
                                    listeners: {
                                        afteranimate: me.doPlaceholderCollapse,
                                        scope: me
                                    }
                                });
                            } else {
                                me.doPlaceholderCollapse();
                            }
                        }
                    }
                });
            } else {
                me.el.hide();
                me.doPlaceholderCollapse();
            }
        } else {
            me.isCollapsingOrExpanding = 0;
            if (!me.preventCollapseFire) {
                me.fireEvent('collapse', me);
            }
        }
        return me;
    },
    doPlaceholderCollapse: function() {
        var me = this,
            placeholder = me.placeholder,
            expandTool = placeholder.expandTool,
            dom;
        // See the comment in placeholderCollapse().
        if (me.focusPlaceholderExpandTool && expandTool) {
            expandTool.focus();
        }
        // However when focus was *not* on the collapse tool,
        // and we contained focus, we still need to try and
        // focus the placeholder itself since it may have been
        // configured with something focusable inside, and delegate focus handling.
        else if (me.focusPlaceHolder) {
            placeholder.focus();
        }
        me.focusPlaceholderExpandTool = false;
        placeholder.setHiddenState(false);
        // Both panel *and* placeholder are collapsed,
        // but only panel is hidden. Calling setHiddenState()
        // above does not reset aria-hidden attribute.
        if (placeholder.rendered) {
            dom = placeholder.ariaEl.dom;
            dom.setAttribute('aria-hidden', false);
            dom.setAttribute('aria-expanded', false);
        }
        dom = me.ariaEl.dom;
        dom.setAttribute('aria-hidden', true);
        dom.setAttribute('aria-expanded', false);
        me.isCollapsingOrExpanding = 0;
        me.fireEvent('collapse', me);
        me.fireEvent('endfloat', me);
    },
    placeholderExpand: function(animate) {
        var me = this,
            collapseDir = me.collapsed,
            expandTool = me.placeholder.expandTool,
            floatCls = Ext.panel.Panel.floatCls,
            center = me.ownerLayout ? me.ownerLayout.centerRegion : null,
            finalPos, floatedPos;
        // Layouts suspended - don't bother with animation shenanigans
        if (Ext.Component.layoutSuspendCount) {
            animate = false;
        }
        if (me.floatedFromCollapse) {
            floatedPos = me.getPosition(true);
            // these are the same cleanups performed by the normal slideOut mechanism:
            me.slideOutFloatedPanelBegin();
            me.slideOutFloatedPanelEnd();
            me.floated = false;
        }
        // We assume that if expand was caused by keyboard action on focused
        // placeholder expand tool, the logical focus transition is to the
        // panel header's collapse tool.
        // Note that it may not be the case when the user *clicked* expand tool
        // while focus was elsewhere; in that case we dare not touch focus to avoid
        // sudden jumps.
        if (expandTool && Ext.ComponentManager.getActiveComponent() === expandTool) {
            me.focusHeaderCollapseTool = true;
            // There is an odd issue with JAWS screen reader: when expanding a panel,
            // it will announce Expand tool again before focus is forced to Collapse
            // tool. I'm not sure why that happens since focus does not move from
            // Expand tool during animation; this hack should work around
            // the problem until we come up with more understanding and a proper
            // solution. The attributes are restored below in doPlaceholderExpand.
            expandTool._ariaRole = expandTool.ariaEl.dom.getAttribute('role');
            expandTool._ariaLabel = expandTool.ariaEl.dom.getAttribute('aria-label');
            expandTool.ariaEl.dom.setAttribute('role', 'presentation');
            expandTool.ariaEl.dom.removeAttribute('aria-label');
        }
        if (animate) {
            // Expand me and hide the placeholder
            Ext.suspendLayouts();
            me.placeholder.hide();
            me.el.show();
            me.collapsed = false;
            me.setHiddenState(false);
            // Stop the center region from moving when laid out without the placeholder there.
            // Unless we are expanding from a floated out situation. In that case,
            // it's laid out immediately.
            if (center && !floatedPos) {
                center.hidden = true;
            }
            Ext.resumeLayouts(true);
            if (center) {
                center.hidden = false;
            }
            if (!me.floatedFromCollapse) {
                me.fireEvent('beginfloat', me);
            }
            me.el.addCls(floatCls);
            // At this point, this Panel is arranged in its correct, expanded layout.
            // The center region has not been affected because it has been flagged as hidden.
            //
            // If we are proceeding from floated, the center region has also been arranged
            // in its new layout to accommodate this expansion, so no further layout is needed, just
            // element animation.
            //
            // If we are proceeding from fully collapsed, the center region has *not* been relayed
            // out because the UI look and feel dictates that it stays stable until the expanding
            // panel has slid in all the way, and *then* it snaps into place.
            me.isCollapsingOrExpanding = 2;
            // Floated, move it back to the floated pos, and thence into the correct place
            if (floatedPos) {
                finalPos = me.getXY();
                me.setLocalXY(floatedPos[0], floatedPos[1]);
                me.setXY([
                    finalPos[0],
                    finalPos[1]
                ], {
                    duration: Ext.Number.from(animate, Ext.fx.Anim.prototype.duration),
                    listeners: {
                        scope: me,
                        afteranimate: function() {
                            var me = this;
                            me.el.removeCls(floatCls);
                            me.isCollapsingOrExpanding = 0;
                            me.fireEvent('expand', me);
                            me.fireEvent('endfloat', me);
                        }
                    }
                });
            } else // Not floated, slide it in to the correct place
            {
                me.el.hide();
                me.placeholder.el.show();
                me.placeholder.hidden = false;
                // Slide this Component's el back into place, after which we lay out AGAIN
                me.setHiddenState(false);
                me.el.slideIn(me.convertCollapseDir(collapseDir), {
                    preserveScroll: true,
                    duration: Ext.Number.from(animate, Ext.fx.Anim.prototype.duration),
                    listeners: {
                        afteranimate: me.doPlaceholderExpand,
                        scope: me
                    }
                });
            }
        } else {
            me.floated = me.collapsed = false;
            me.doPlaceholderExpand(true);
        }
        return me;
    },
    doPlaceholderExpand: function(nonAnimated) {
        var me = this,
            placeholder = me.placeholder,
            collapseTool = me.collapseTool,
            expandTool = placeholder.expandTool;
        nonAnimated = nonAnimated === true;
        if (nonAnimated) {
            Ext.suspendLayouts();
            me.show();
        }
        // the ordering of these two lines appears to be important in
        // IE9.  There is an odd expand issue in IE 9 in the border layout
        // example that causes the index1 child of the south dock region
        // to get 'hidden' after a collapse / expand cycle.  See
        // EXTJSIV-5318 for details
        me.el.removeCls(Ext.panel.Panel.floatCls);
        placeholder.hide();
        if (nonAnimated) {
            Ext.resumeLayouts(true);
        } else {
            // The center region has been left in its larger size, so a layout is needed now
            me.updateLayout();
        }
        // This part is quite tricky in both animated and non-animated sequence.
        // After the panel is collapsed we will show the placeholder,
        // but by that time we had already lost the previous focus state.
        // The subsequent onFocusEnter on the placeholder will thusly reset
        // placeholder's previousFocus property to null; so when we hide
        // the placeholder after expanding the panel again, it can't throw focus
        // back to the panel header by iself.
        // This is why we nudge it a little here; the assumption is that
        // if panel expansion has been caused by keyboard action
        // on focused placeholder expand tool, then the logical focus transition
        // is to panel header's collapse tool.
        if (me.focusHeaderCollapseTool && collapseTool) {
            collapseTool.focus();
        }
        me.focusHeaderCollapseTool = false;
        placeholder.ariaEl.dom.setAttribute('aria-expanded', true);
        me.ariaEl.dom.setAttribute('aria-expanded', true);
        if (expandTool && expandTool._ariaRole) {
            expandTool.ariaEl.dom.setAttribute('role', expandTool._ariaRole);
            expandTool.ariaEl.dom.setAttribute('aria-label', expandTool._ariaLabel);
            expandTool._ariaRole = expandTool._ariaLabel = null;
        }
        me.isCollapsingOrExpanding = 0;
        me.fireEvent('expand', me);
        me.fireEvent('endfloat', me);
    },
    remove: function(component, autoDestroy) {
        var dockedItems = this.dockedItems;
        // When the panel is destroyed, dockedItems is nulled
        if (dockedItems && dockedItems.contains(component)) {
            this.removeDocked(component, autoDestroy);
        } else {
            this.callParent([
                component,
                autoDestroy
            ]);
        }
        return component;
    },
    /**
     * Removes a CSS class from the body element.
     * @param {String/String[]} cls The class to remove
     * @return {Ext.panel.Panel} this
     */
    removeBodyCls: function(cls) {
        var me = this,
            body = me.rendered ? me.body : me.getProtoBody();
        body.removeCls(cls);
        return me;
    },
    removeUIClsFromElement: function(cls) {
        var me = this,
            result = me.callParent(arguments);
        me.removeBodyCls([
            Ext.baseCSSPrefix + cls,
            me.baseCls + '-body-' + cls,
            me.baseCls + '-body-' + me.ui + '-' + cls
        ]);
        return result;
    },
    restoreDimension: function() {
        var dir = this.collapseDirection;
        // If we're collapsing top/bottom, we want to restore the height
        // If we're collapsing left/right, we want to restore the width
        return (dir === 'top' || dir === 'bottom') ? 'height' : 'width';
    },
    restoreHiddenDocked: function() {
        // Re-show Panel content which was hidden after collapse.
        this.setDockedItemsVisibility(this.hiddenOnCollapse, true);
    },
    /**
     * Sets the body style according to the passed parameters.
     * @param {Mixed} style A full style specification string, or object, or the name of a style
     * property to set.
     * @param {String} value If the first param was a style property name, the style property value.
     * @return {Ext.panel.Panel} this
     */
    setBodyStyle: function(style, value) {
        var me = this,
            body = me.rendered ? me.body : me.getProtoBody();
        if (Ext.isFunction(style)) {
            style = style();
        }
        if (arguments.length === 1) {
            if (Ext.isString(style)) {
                style = Ext.Element.parseStyles(style);
            }
            body.setStyle(style);
        } else {
            body.setStyle(style, value);
        }
        return me;
    },
    /**
     * @method setBorder
     * @inheritdoc
     */
    setBorder: function(border, targetEl) {
        if (targetEl) {
            // skip out here, the panel will set the border on the body/header during rendering
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            header = me.header;
        if (!border) {
            border = 0;
        } else if (border === true) {
            border = '1px';
        } else {
            border = me.unitizeBox(border);
        }
        if (header) {
            if (header.isHeader) {
                header.setBorder(border);
            } else {
                header.border = border;
            }
        }
        if (me.rendered && me.bodyBorder !== false) {
            me.body.setStyle('border-width', border);
        }
        me.updateLayout();
        me.border = border;
    },
    /**
     * Collapses or expands the panel.
     * @param {Boolean} collapsed `true` to collapse the panel, `false` to expand it.
     */
    setCollapsed: function(collapsed) {
        this[collapsed ? 'collapse' : 'expand']();
    },
    /**
     * Set visibility of docked items after the panel is collapsed or expanded
     *
     * @param {Ext.dom.CompositeElement} els
     * @param {Boolean} show
     *
     * @private
     */
    setDockedItemsVisibility: function(els, show) {
        var me = this,
            items = me.getDockedItems(),
            len = items.length,
            i = 0,
            item, reExpander;
        if (me.header !== false) {
            reExpander = me.getReExpander();
        }
        for (; i < len; i++) {
            item = items[i];
            if (item && item !== reExpander && item.el) {
                els.add(item.el);
            }
        }
        els.setStyle('visibility', show ? '' : 'hidden');
        els.clear();
    },
    setGlyph: function(glyph) {
        this.setHeaderConfig(glyph, 'glyph', 'setGlyph', true);
    },
    setIcon: function(icon) {
        this.setHeaderConfig(icon, 'icon', 'setIcon', true);
    },
    setIconCls: function(iconCls) {
        this.setHeaderConfig(iconCls, 'iconCls', 'setIconCls', true);
    },
    setIconAlign: function(iconAlign) {
        this.setHeaderConfig(iconAlign, 'iconAlign', 'setIconAlign', true);
    },
    setTitleAlign: function(titleAlign) {
        this.setHeaderConfig(titleAlign, 'titleAlign', 'setTitleAlign', true);
    },
    setTitlePosition: function(titlePosition) {
        this.setHeaderConfig(titlePosition, 'titlePosition', 'setTitlePosition', true);
    },
    setTitleRotation: function(titleRotation) {
        this.setHeaderConfig(titleRotation, 'titleRotation', 'setTitleRotation', true);
    },
    /**
     * Sets the title of this panel.
     * @param {String} title The new title
     */
    setTitle: function(title) {
        var me = this,
            oldTitle = me.title;
        if (title !== oldTitle && me.headingEl) {
            me.headingEl.setHtml(title);
        }
        this.setHeaderConfig(title, 'title', 'setTitle', false);
    },
    setHeaderConfig: function(value, prop, setter, addToHeaderCfg) {
        var me = this,
            oldValue = me[prop],
            header = me.header,
            placeholder = me.placeholder,
            reExpander = me.reExpander,
            eventName;
        if (value !== oldValue) {
            me[prop] = value;
            if (header) {
                if (header.isHeader) {
                    header[setter](value);
                } else if (addToHeaderCfg) {
                    header[prop] = value;
                }
            } else if (me.rendered || me.afterHeaderInit) {
                me.updateHeader();
            }
            if (reExpander) {
                reExpander[setter](value);
            }
            if (placeholder && placeholder[setter]) {
                placeholder[setter](value);
            }
            eventName = prop.toLowerCase() + 'change';
            if (me.hasListeners[eventName]) {
                me.fireEvent(eventName, me, value, oldValue);
            }
        }
    },
    setHiddenDocked: function() {
        // Hide Panel content except reExpander using visibility to prevent focusing of contained
        // elements.
        // Track what we hide to re-show on expand except for docked items
        // Until the panel is expanded the docked items might have been removed
        var me = this,
            toHide = new Ext.dom.CompositeElement();
        me.hiddenOnCollapse.add(me.body);
        toHide.add(me.body);
        me.setDockedItemsVisibility(toHide, false);
    },
    /**
     * @method setUI
     * @inheritdoc
     */
    setUI: function(ui) {
        var me = this;
        me.callParent(arguments);
        if (me.header && me.header.rendered) {
            me.header.setUI(ui);
        }
    },
    /**
     * Shortcut for performing an {@link #method-expand} or {@link #method-collapse} based
     * on the current state of the panel.
     * @return {Ext.panel.Panel} this
     */
    toggleCollapse: function() {
        return (this.collapsed || this.floatedFromCollapse) ? this.expand() : this.collapse();
    },
    updateCollapseTool: function() {
        var me = this,
            collapseTool = me.collapseTool,
            toolCfg;
        if (!collapseTool && me.collapsible) {
            me.collapseDirection = me.collapseDirection || me.getHeaderPosition() || 'top';
            toolCfg = {
                xtype: 'tool',
                handler: me.toggleCollapse,
                scope: me
            };
            // In accordion layout panels are collapsible/expandable with keyboard
            // via the panel title that is focusable. There is no need to have a separate
            // collapse/expand tool for keyboard interaction but we still have to react
            // to mouse clicks, and historically accordion panels had coolapse tools
            // so we leave the tool but make it unfocusable and keyboard inactive.
            // Note that we do the same thing for the automatically added close tool
            // but NOT for the other tools.
            if (me.isAccordionPanel) {
                toolCfg.focusable = false;
                toolCfg.ariaRole = 'presentation';
            }
            me.collapseTool = me.expandTool = collapseTool = Ext.widget(toolCfg);
        }
        if (collapseTool) {
            if (me.collapsed && !me.isPlaceHolderCollapse()) {
                collapseTool.setType('expand-' + me.getOppositeDirection(me.collapseDirection));
                collapseTool.setTooltip(me.expandToolText);
            } else {
                collapseTool.setType('collapse-' + me.collapseDirection);
                collapseTool.setTooltip(me.collapseToolText);
            }
        }
    },
    navigateAccordionHeader: function(e) {
        var me = this,
            key, target;
        key = e.getKey();
        switch (key) {
            case e.UP:
            case e.LEFT:
                target = me.findAccordionSibling('prev');
                break;
            case e.DOWN:
            case e.RIGHT:
                target = me.findAccordionSibling('next');
                break;
            case e.HOME:
                target = me.findAccordionSibling('first');
                break;
            case e.END:
                target = me.findAccordionSibling('last');
                break;
            // Before closing the panel we need to fall back to the previous one,
            // or to the next one if there is no previous one in the list.
            // Jumping to the first panel header does not seem logical.
            case e.DELETE:
                target = me.findAccordionSibling('prev') || me.findAccordionSibling('next');
                // We also need to prevent the panel from closing if it's the only one
                // panel left in the accordion layout.
                if (!target) {
                    e.doNotClose = true;
                };
                break;
        }
        // We need to stop the event in to prevent scrolling the parent container.
        e.stopEvent();
        if (target && target !== me) {
            target.header.titleCmp.focus();
        }
    },
    navigateAccordionBody: function(e) {
        var target;
        if (e.getKey() === e.UP) {
            // Ctrl-Up within the panel body should focus the header element
            target = this;
        }
        // These events are very particular; if they bubbled up to the panel body
        // it means they were not consumed at the origin components.
        e.stopEvent();
        if (target) {
            target.header.titleCmp.focus();
        }
    },
    findAccordionSibling: function(which, forceFind) {
        var me = this,
            siblingSel = '[isAccordionPanel]',
            sibling;
        switch (which) {
            case 'prev':
                sibling = me.prev(siblingSel);
                if (!sibling) {
                    if (me.accordionWrapOver) {
                        sibling = me.ownerCt.child(siblingSel + ':last');
                    } else if (forceFind) {
                        sibling = me;
                    }
                };
                break;
            case 'next':
                sibling = me.next(siblingSel);
                if (!sibling) {
                    if (me.accordionWrapOver) {
                        sibling = me.ownerCt.child(siblingSel + ':first');
                    } else if (forceFind) {
                        sibling = me;
                    }
                };
                break;
            case 'first':
                sibling = me.ownerCt.child(siblingSel + ':first');
                break;
            case 'last':
                sibling = me.ownerCt.child(siblingSel + ':last');
                break;
        }
        return sibling;
    },
    fireDefaultButton: function(e) {
        var me = this,
            refHolder, btn;
        if (e.target.tagName === 'TEXTAREA' || e.target.getAttribute('aria-multiline') === 'true') {
            return true;
        }
        refHolder = me.lookupReferenceHolder(/* skipThis = */
        false) || me;
        btn = refHolder.lookupReference(me.defaultButton);
        // We call it defaultButton but it can really be any object
        // that implements `click` method
        if (btn && btn.click) {
            btn.click(e);
            // Stop event propagation through DOM publisher
            e.stopEvent();
            // ... and in case we have other listeners,
            // stop the loop in Ext.util.Event too
            return false;
        } else if (!btn) {
            Ext.raise({
                msg: 'No component found for defaultButton reference "' + me.defaultButton + '" in ' + me.xtype + ' ' + me.id,
                panel: me
            });
        } else {
            Ext.raise({
                msg: 'Component referenced by defaultButton config "' + me.defaultButton + '" in ' + me.xtype + ' ' + me.id + ' does not have click() method',
                component: btn
            });
        }
    },
    maybeClose: function(e) {
        var me = this;
        if (me.closable) {
            me.navigateAccordionHeader(e);
            // Can't close the last panel in accordion
            if (!e.doNotClose) {
                me.close();
            }
        }
    },
    canFocus: function(skipVisibility, includeFocusTarget) {
        // If we are placeholder collapsed, then the placeholder may be focusable.
        if (this.collapsed) {
            // eslint-disable-next-line max-len
            return !!(this.placeholder && this.placeholder.canFocus(skipVisibility, includeFocusTarget));
        }
        return this.mixins.focusable.canFocus.call(this, skipVisibility, includeFocusTarget);
    },
    focus: function() {
        var me = this,
            placeholder = me.placeholder;
        if (me.collapsed && placeholder && placeholder.canFocus()) {
            placeholder.focus.apply(placeholder, arguments);
        } else {
            me.callParent(arguments);
        }
    },
    onFocusEnter: function(e) {
        var me = this,
            ariaDom = me.ariaEl.dom;
        me.callParent([
            e
        ]);
        if (me.isAccordionPanel && ariaDom) {
            ariaDom.setAttribute('aria-selected', true);
        }
    },
    onFocusLeave: function(e) {
        var me = this,
            ariaDom = me.ariaEl.dom;
        me.callParent([
            e
        ]);
        if (me.isAccordionPanel && ariaDom) {
            ariaDom.setAttribute('aria-selected', 'false');
        }
    },
    updateHeaderPosition: function(position) {
        var header = this.header;
        if (header && header.isHeader) {
            header.setDock(position);
        }
    },
    /**
     * @private
     */
    unghost: function(show, matchPosition, focus) {
        var me = this,
            el = me.el,
            ghostPanel = me.ghostPanel;
        if (!ghostPanel) {
            return;
        }
        // Show el first to restore the element to the original
        // visibility mode.
        el.show();
        el.setVisibilityMode(me.elVisMode);
        if (show !== false) {
            if (matchPosition !== false) {
                me.setPagePosition(ghostPanel.getXY());
            }
        } else {
            el.hide();
        }
        ghostPanel.el.hide();
        ghostPanel.setHiddenState(true);
    },
    /**
     * Create, hide, or show the header component as appropriate based on the current config.
     * @private
     * @param {Boolean} force True to force the header to be created
     */
    updateHeader: function(force) {
        var me = this,
            header = me.header,
            title = me.getTitle(),
            tools = me.tools,
            icon = me.getIcon(),
            glyph = me.getGlyph(),
            iconCls = me.getIconCls(),
            hasIcon = glyph || icon || iconCls,
            ariaDom = me.ariaEl.dom,
            headerPosition = me.getHeaderPosition(),
            vertical = headerPosition === 'left' || headerPosition === 'right',
            headingEl, ariaAttr;
        if (Ext.isObject(header) || (header !== false && (force || (title || hasIcon) || (tools && tools.length) || (me.collapsible && !me.titleCollapse)))) {
            if (header && header.isHeader) {
                header.dockToEl = true;
                header.show();
            } else {
                if (Ext.isObject(header)) {
                    me.syncHeaderConfigs(header);
                }
                // Apply the header property to the header config
                header = me.header = Ext.widget(Ext.merge({
                    xtype: 'header',
                    title: title,
                    titleAlign: me.getTitleAlign(),
                    titlePosition: me.getTitlePosition(),
                    vertical: vertical,
                    dock: me.getHeaderPosition() || 'top',
                    dockToEl: true,
                    titleRotation: me.getTitleRotation(),
                    textCls: me.headerTextCls,
                    iconCls: iconCls,
                    iconAlign: me.getIconAlign(),
                    icon: icon,
                    glyph: glyph,
                    baseCls: me.baseCls + '-header',
                    tools: tools,
                    ui: me.ui,
                    id: me.id + '_header',
                    overCls: me.headerOverCls,
                    indicateDrag: me.draggable,
                    frame: (me.frame || me.alwaysFramed) && me.frameHeader,
                    ignoreParentFrame: me.frame || me.overlapHeader,
                    ignoreBorderManagement: me.frame || me.ignoreHeaderBorderManagement,
                    isAccordionHeader: me.isAccordionPanel,
                    ownerCt: me,
                    synthetic: true,
                    // not user-defined
                    listeners: me.collapsible && me.titleCollapse ? {
                        click: me.toggleCollapse,
                        scope: me
                    } : null
                }, me.header));
                // Header's onAdd mutates the tools array.
                // It replaces tool configs at each index with the instantiated tool
                // It also injects the tool instances as properties keyed by their type.
                me.addDocked(header, 0);
            }
            // Accordion panels are tightly coupled to their headers' titleCmp
            // via aria-labelledby attribute. There should be no aria-label.
            if (me.isAccordionPanel) {
                if (ariaDom) {
                    ariaDom.setAttribute('aria-labelledby', header.id + '-title');
                    ariaDom.removeAttribute('aria-label');
                } else {
                    ariaAttr = me.ariaRenderAttributes || (me.ariaRenderAttributes = {});
                    ariaAttr['aria-labelledby'] = header.id + '-title';
                    delete ariaAttr['aria-label'];
                }
            } else {
                if (title) {
                    // If the panel is not an ARIA a region, it still should have
                    // an accessible name via aria-labelledby attribute unless
                    // it is a tabpanel in which case aria-labelledby points
                    // to the corresponding tab and is managed by TabPanel class.
                    if (me.ariaRole !== 'tabpanel') {
                        if (ariaDom) {
                            ariaDom.setAttribute('aria-labelledby', header.id + '-title-textEl');
                            ariaDom.removeAttribute('aria-label');
                        } else {
                            ariaAttr = me.ariaRenderAttributes || (me.ariaRenderAttributes = {});
                            ariaAttr['aria-labelledby'] = header.id + '-title-textEl';
                            delete ariaAttr['aria-label'];
                        }
                    }
                }
                // If there is no title, just make sure no aria-label attribute was added
                else if (me.ariaRenderAttributes) {
                    delete me.ariaRenderAttributes['aria-label'];
                }
            }
        } else {
            if (header) {
                header.hide();
            }
            // Title may contain HTML markup
            title = Ext.util.Format.stripTags(title);
            // aria-labelledby could have been set by the TabPanel.onAdd()
            if (ariaDom) {
                if (!ariaDom.hasAttribute('aria-labelledby')) {
                    if (title) {
                        ariaDom.setAttribute('aria-label', title);
                    } else {
                        ariaDom.removeAttribute('aria-label');
                    }
                }
            } else {
                ariaAttr = me.ariaRenderAttributes || (me.ariaRenderAttributes = {});
                if (!ariaAttr['aria-labelledby']) {
                    if (title) {
                        ariaAttr['aria-label'] = title;
                    } else {
                        delete ariaAttr['aria-label'];
                    }
                }
            }
        }
        // If the panel is a child of the Viewport that has border layout,
        // that automatically makes it a region unless the user overrode that.
        if (me.isViewportBorderChild && !me.hasOwnProperty('ariaRole')) {
            me.ariaRole = 'region';
        }
        if (me.ariaRole === 'region' && !title) {
            Ext.ariaWarn(me, "Panel " + me.id + " is a region section of the application, " + "but it does not have a title. Per WAI-ARIA, all regions " + "should have a heading element that contains region's title.");
        }
        // An ARIA region should have a title, and a heading which we implement
        // as an element placed before all other elements within the panel's
        // main element. The headingEl is hidden via clipping to avoid
        // visual impact while still allowing it to be published to
        // Assistive Technologies such as screen readers.
        if (title && me.ariaRole === 'region') {
            headingEl = me.headingEl;
            if (headingEl) {
                headingEl.setHtml(title);
            } else {
                if (me.rendered) {
                    me.headingEl = Ext.dom.Helper.insertFirst(me.el, {
                        tag: 'div',
                        id: me.id + '-headingEl',
                        role: 'heading',
                        'class': Ext.baseCSSPrefix + 'hidden-clip',
                        style: 'height:0',
                        html: title
                    }, true);
                    ariaDom.removeAttribute('aria-label');
                    ariaDom.setAttribute('aria-labelledby', me.id + '-headingEl');
                } else {
                    me.headingText = me.title;
                    ariaAttr = me.ariaRenderAttributes || (me.ariaRenderAttributes = {});
                    ariaAttr['aria-labelledby'] = me.id + '-headingEl';
                    delete ariaAttr['aria-label'];
                }
            }
        } else if (me.headingEl) {
            me.headingEl.destroy();
            me.headingEl = null;
        }
    },
    // ***********************************************************************************
    // End Methods
    // ***********************************************************************************
    // </editor-fold>
    statics: {
        floatCls: Ext.baseCSSPrefix + 'border-region-slide-in'
    },
    privates: {
        addUIToElement: function() {
            var me = this;
            me.callParent(arguments);
            me.addBodyCls(me.baseCls + '-body-' + me.ui);
        },
        applyTargetCls: function(targetCls) {
            this.getProtoBody().addCls(targetCls);
        },
        getDefaultContentTarget: function() {
            return this.body;
        },
        getTargetEl: function() {
            var me = this;
            return me.body || me.protoBody || me.frameBody || me.el;
        },
        /**
         * @private
         */
        initDraggable: function() {
            var me = this;
            // For just simple dragging like Windows
            if (me.simpleDrag) {
                me.initSimpleDraggable();
            } else // For DD package aware dragging of Panels
            {
                /**
                 * @property {Ext.dd.DragSource/Ext.util.ComponentDragger} dd
                 *
                 * Only present if this Panel has been configured with {@link #cfg-draggable}
                 * `true`.
                 *
                 * ##Simple dragging##
                 *
                 * If this Panel is configured {@link #cfg-simpleDrag} `true` (the default
                 * is `false`), this property will reference an instance of
                 * {@link Ext.util.ComponentDragger} (A subclass of
                 * {@link Ext.dd.DragTracker DragTracker}) which handles moving the Panel's
                 * DOM Element, and constraining according to the {@link #constrain} and
                 * {@link #constrainHeader} .
                 *
                 * This object fires various events during its lifecycle and during
                 * a drag operation.
                 *
                 * ##Complex dragging interacting with other DragDrop instances##
                 *
                 * By default, this property in a {@link #cfg-draggable} Panel will contain
                 * an instance of {@link Ext.dd.DragSource} which handles dragging the Panel.
                 *
                 * The developer must provide implementations of the abstract methods of
                 * {@link Ext.dd.DragSource} in order to supply behaviour for each stage
                 * of the drag/drop process. 
                 * 
                 * See also {@link #cfg-draggable}.
                 */
                me.dd = new Ext.panel.DD(me, Ext.isBoolean(me.draggable) ? null : me.draggable);
            }
        },
        initResizable: function(resizable) {
            this.callParent([
                resizable
            ]);
            if (this.collapsed) {
                this.resizer.disable();
            }
        },
        /**
         * @private
         * Override Component.initDraggable.
         * Panel (and subclasses) use the header element as the delegate.
         */
        initSimpleDraggable: function() {
            var me = this,
                dd = me.draggable;
            if (!me.header && !dd.delegate) {
                me.updateHeader(true);
            }
            /*
             * Check the header here again. If for whatever reason it wasn't created in
             * updateHeader (we were configured with header: false) then we'll just ignore
             * the rest since the header acts as the drag handle.
             */
            if (me.header || dd.delegate) {
                dd = Ext.apply({
                    el: me.el,
                    delegate: me.header && me.header.el
                }, dd);
                // Add extra configs if Window is specified to be constrained
                if (me.constrain || me.constrainHeader) {
                    dd.constrain = me.constrain;
                    dd.constrainDelegate = me.constrainHeader;
                    dd.constrainTo = me.constrainTo || me.container;
                }
                dd = me.dd = new Ext.util.ComponentDragger(me, dd);
                me.relayEvents(dd, [
                    'dragstart',
                    'drag',
                    'dragend'
                ]);
            }
        },
        removeUIFromElement: function() {
            var me = this;
            me.callParent(arguments);
            me.removeBodyCls(me.baseCls + '-body-' + me.ui);
        },
        setupRenderTpl: function(renderTpl) {
            this.callParent(arguments);
            this.setupDockingRenderTpl(renderTpl);
        },
        slideOutFloatedPanel: function(preventAnimate) {
            var me = this,
                compEl = me.el,
                collapseDirection, focusTarget,
                afterSlideOut = function() {
                    me.slideOutFloatedPanelEnd();
                    // this would be in slideOutFloatedPanelEnd except that the only other
                    // caller removes this cls later
                    me.el.removeCls(Ext.baseCSSPrefix + 'border-region-slide-in');
                    // Event fired when floated stateus ends
                    me.fireEvent('endfloat', me);
                };
            if (me.isSliding || me.destroyed) {
                return;
            }
            me.isSliding = true;
            me.floated = false;
            me.slideOutFloatedPanelBegin();
            if (preventAnimate) {
                compEl.hide();
                return afterSlideOut();
            }
            if (typeof me.collapsed === 'string') {
                collapseDirection = me.convertCollapseDir(me.collapsed);
            }
            me.fireEvent('beginfloat', me);
            compEl.slideOut(collapseDirection, {
                preserveScroll: true,
                duration: Ext.Number.from(me.animCollapse, Ext.fx.Anim.prototype.duration),
                listeners: {
                    afteranimate: afterSlideOut
                }
            });
            // Focus the placeholder which should delegate into itself
            if (this.containsFocus) {
                focusTarget = this.findFocusTarget();
                if (focusTarget) {
                    focusTarget.focus();
                }
            }
        },
        /**
         * This method begins the slide out of the floated panel.
         * @private
         */
        slideOutFloatedPanelBegin: function() {
            var me = this;
            me.collapsed = me.floatedFromCollapse;
            me.setHiddenState(true);
            me.floatedFromCollapse = null;
            // Remove mouse leave/enter monitors, and the mousedown monitor
            Ext.destroy(me.pointerLeaveListener, me.phHoverListeners, me.elHoverListeners);
        },
        /**
         * This method cleans up after the slide out of the floated panel.
         * @private
         */
        slideOutFloatedPanelEnd: function(suppressEvents) {
            var me = this;
            if (me.collapseTool) {
                me.collapseTool.el.show();
            }
            me.slideOutTask.cancel();
            me.isSliding = false;
            if (!suppressEvents) {
                me.fireEvent('unfloat', me);
            }
        },
        /*
         * Syncs shared configs that might be specified on header config
         * @param {Object} header
         */
        syncHeaderConfigs: function(header) {
            var me = this,
                config, value;
            for (config in header) {
                value = header[config];
                if (me.headerConfigs[config] && value !== undefined) {
                    // set config directly so we don't fire events
                    me[config] = value;
                }
            }
        }
    }
}, // private
function() {
    var proto = this.prototype;
    proto.animCollapse = Ext.enableFx;
    proto.placeholderCollapseHideMode = Ext.Element.VISIBILITY;
});

/**
 * This layout allows you to easily render content into an HTML table. The total number of columns
 * can be specified, and rowspan and colspan can be used to create complex layouts within the table.
 * This class is intended to be extended or created via the `layout: {type: 'table'}`
 * {@link Ext.container.Container#layout} config, and should generally not need to be created
 * directly via the new keyword.
 *
 * Note that when creating a layout via config, the layout-specific config properties must be passed
 * in via the {@link Ext.container.Container#layout} object which will then be applied internally
 * to the layout. In the case of TableLayout, the only valid layout config properties are
 * {@link #columns} and {@link #tableAttrs}. However, the items added to a TableLayout can supply
 * the following table-specific config properties:
 *
 *   - **rowspan** Applied to the table cell containing the item.
 *   - **colspan** Applied to the table cell containing the item.
 *   - **cellCls** A CSS class name added to the table cell containing the item.
 *
 * The basic concept of building up a TableLayout is conceptually very similar to building up
 * a standard HTML table. You simply add each panel (or "cell") that you want to include along
 * with any span attributes specified as the special config properties of rowspan and colspan
 * which work exactly like their HTML counterparts. Rather than explicitly creating and nesting rows
 * and columns as you would in HTML, you simply specify the total column count in the layout config
 * and start adding panels in their natural order from left to right, top to bottom. The layout will
 * automatically figure out, based on the column count, rowspans and colspans, how to position each
 * panel within the table. Just like with HTML tables, your rowspans and colspans must add up
 * correctly in your overall layout or you'll end up with missing and/or extra cells!
 * Example usage:
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *         title: 'Table Layout',
 *         width: 300,
 *         height: 150,
 *         layout: {
 *             type: 'table',
 *             // The total column count must be specified here
 *             columns: 3
 *         },
 *         defaults: {
 *             // applied to each contained panel
 *             bodyStyle: 'padding:20px'
 *         },
 *         items: [{
 *             html: 'Cell A content',
 *             rowspan: 2
 *         },{
 *             html: 'Cell B content',
 *             colspan: 2
 *         },{
 *             html: 'Cell C content',
 *             cellCls: 'highlight'
 *         },{
 *             html: 'Cell D content'
 *         }],
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.layout.container.Table', {
    extend: 'Ext.layout.container.Container',
    alternateClassName: 'Ext.layout.TableLayout',
    alias: 'layout.table',
    /**
     * @cfg {Number} columns
     * The total number of columns to create in the table for this layout. If not specified,
     * all Components added to this layout will be rendered into a single row using one column
     * per Component.
     */
    type: 'table',
    createsInnerCt: true,
    targetCls: Ext.baseCSSPrefix + 'table-layout-ct',
    tableCls: Ext.baseCSSPrefix + 'table-layout',
    cellCls: Ext.baseCSSPrefix + 'table-layout-cell',
    childEls: [
        'table',
        'tbody'
    ],
    /**
     * @cfg {Object} tableAttrs
     * An object containing properties which are added to the {@link Ext.dom.Helper DomHelper}
     * specification used to create the layout's `<table>` element. Example:
     *
     *     {
     *         xtype: 'panel',
     *         layout: {
     *             type: 'table',
     *             columns: 3,
     *             tableAttrs: {
     *                 style: {
     *                     width: '100%'
     *                 }
     *             }
     *         }
     *     }
     */
    tableAttrs: null,
    /**
     * @cfg {Object} trAttrs
     * An object containing properties which are added to the {@link Ext.dom.Helper DomHelper}
     * specification used to create the layout's `<tr>` elements.
     */
    /**
     * @cfg {Object} tdAttrs
     * An object containing properties which are added to the {@link Ext.dom.Helper DomHelper}
     * specification used to create the layout's `<td>` elements.
     */
    getItemSizePolicy: function(item) {
        return this.autoSizePolicy;
    },
    initInheritedState: function(inheritedState, inheritedStateInner) {
        inheritedStateInner.inShrinkWrapTable = true;
    },
    getLayoutItems: function() {
        var me = this,
            result = [],
            items = me.callParent(),
            len = items.length,
            item, i;
        for (i = 0; i < len; i++) {
            item = items[i];
            if (!item.hidden) {
                result.push(item);
            }
        }
        return result;
    },
    getHiddenItems: function() {
        var result = [],
            items = this.owner.items.items,
            len = items.length,
            i, item;
        for (i = 0; i < len; ++i) {
            item = items[i];
            if (item.rendered && item.hidden) {
                result.push(item);
            }
        }
        return result;
    },
    /**
     * @private
     * Iterates over all passed items, ensuring they are rendered in a cell in the proper
     * location in the table structure.
     */
    renderChildren: function() {
        var me = this,
            items = me.getLayoutItems(),
            tbody = me.tbody.dom,
            rows = tbody.rows,
            len = items.length,
            hiddenItems = me.getHiddenItems(),
            cells, curCell, rowIdx, cellIdx, item, trEl, tdEl, i;
        // Calculate the correct cell structure for the current items
        cells = me.calculateCells(items);
        // Loop over each cell and compare to the current cells in the table, inserting/
        // removing/moving cells as needed, and making sure each item is rendered into
        // the correct cell.
        for (i = 0; i < len; i++) {
            curCell = cells[i];
            rowIdx = curCell.rowIdx;
            cellIdx = curCell.cellIdx;
            item = items[i];
            // If no row present, create and insert one
            trEl = rows[rowIdx];
            if (!trEl) {
                trEl = tbody.insertRow(rowIdx);
                if (me.trAttrs) {
                    trEl.set(me.trAttrs);
                }
            }
            // If no cell present, create and insert one
            tdEl = Ext.get(trEl.cells[cellIdx] || trEl.insertCell(cellIdx));
            // Render or move the component into the cell
            if (!item.rendered) {
                me.renderItem(item, tdEl, 0);
            } else if (!me.isValidParent(item, tdEl, rowIdx, cellIdx, tbody)) {
                me.moveItem(item, tdEl, 0);
            }
            // Set the cell properties
            if (me.tdAttrs) {
                tdEl.set(me.tdAttrs);
            }
            if (item.tdAttrs) {
                tdEl.set(item.tdAttrs);
            }
            tdEl.set({
                colSpan: item.colspan || 1,
                rowSpan: item.rowspan || 1,
                cls: me.cellCls + ' ' + (item.cellCls || '')
            });
            // If at the end of a row, remove any extra cells
            if (!cells[i + 1] || cells[i + 1].rowIdx !== rowIdx) {
                cellIdx++;
                while (trEl.cells[cellIdx]) {
                    trEl.deleteCell(cellIdx);
                }
            }
        }
        // Delete any extra rows
        rowIdx++;
        while (tbody.rows[rowIdx]) {
            tbody.deleteRow(rowIdx);
        }
        // Check if we've removed any cells that contain a component, we need to move
        // them so they don't get cleaned up by the gc
        for (i = 0 , len = hiddenItems.length; i < len; ++i) {
            me.ensureInDocument(hiddenItems[i].getEl());
        }
    },
    ensureInDocument: function(el) {
        var dom = el.dom.parentNode;
        while (dom) {
            if (dom.tagName.toUpperCase() === 'BODY') {
                return;
            }
            dom = dom.parentNode;
        }
        Ext.getDetachedBody().appendChild(el, true);
    },
    calculate: function(ownerContext) {
        if (!ownerContext.hasDomProp('containerChildrenSizeDone')) {
            this.done = false;
        } else {
            // eslint-disable-next-line vars-on-top
            var targetContext = ownerContext.targetContext,
                widthShrinkWrap = ownerContext.widthModel.shrinkWrap,
                heightShrinkWrap = ownerContext.heightModel.shrinkWrap,
                shrinkWrap = heightShrinkWrap || widthShrinkWrap,
                table = shrinkWrap && this.table.dom,
                targetPadding = shrinkWrap && targetContext.getPaddingInfo();
            if (widthShrinkWrap) {
                ownerContext.setContentWidth(table.offsetWidth + targetPadding.width, true);
            }
            if (heightShrinkWrap) {
                ownerContext.setContentHeight(table.offsetHeight + targetPadding.height, true);
            }
        }
    },
    /**
     * @private
     * Determine the row and cell indexes for each component, taking into consideration
     * the number of columns and each item's configured colspan/rowspan values.
     * @param {Array} items The layout components
     * @return {Object[]} List of row and cell indexes for each of the components
     */
    calculateCells: function(items) {
        var cells = [],
            rowIdx = 0,
            colIdx = 0,
            cellIdx = 0,
            totalCols = this.columns || Infinity,
            rowspans = [],
            // rolling list of active rowspans for each column
            len = items.length,
            item, i, j;
        for (i = 0; i < len; i++) {
            item = items[i];
            // Find the first available row/col slot not taken up by a spanning cell
            while (colIdx >= totalCols || rowspans[colIdx] > 0) {
                if (colIdx >= totalCols) {
                    // move down to next row
                    colIdx = 0;
                    cellIdx = 0;
                    rowIdx++;
                    // decrement all rowspans
                    for (j = 0; j < totalCols; j++) {
                        if (rowspans[j] > 0) {
                            rowspans[j]--;
                        }
                    }
                } else {
                    colIdx++;
                }
            }
            // Add the cell info to the list
            cells.push({
                rowIdx: rowIdx,
                cellIdx: cellIdx
            });
            // Increment
            for (j = item.colspan || 1; j; --j) {
                rowspans[colIdx] = item.rowspan || 1;
                ++colIdx;
            }
            ++cellIdx;
        }
        return cells;
    },
    getRenderTree: function() {
        var me = this,
            items = me.getLayoutItems(),
            rows = [],
            result = Ext.apply({
                tag: 'table',
                id: me.owner.id + '-table',
                "data-ref": 'table',
                role: 'presentation',
                cls: me.tableCls,
                cellspacing: 0,
                cellpadding: 0,
                cn: {
                    tag: 'tbody',
                    id: me.owner.id + '-tbody',
                    "data-ref": 'tbody',
                    role: 'presentation',
                    cn: rows
                }
            }, me.tableAttrs),
            tdAttrs = me.tdAttrs,
            len = items.length,
            item, curCell, tr, rowIdx, cellIdx, cell, cells, i;
        // Calculate the correct cell structure for the current items
        cells = me.calculateCells(items);
        for (i = 0; i < len; i++) {
            item = items[i];
            curCell = cells[i];
            rowIdx = curCell.rowIdx;
            cellIdx = curCell.cellIdx;
            // If no row present, create and insert one
            tr = rows[rowIdx];
            if (!tr) {
                tr = rows[rowIdx] = {
                    tag: 'tr',
                    role: 'presentation',
                    cn: []
                };
                if (me.trAttrs) {
                    Ext.apply(tr, me.trAttrs);
                }
            }
            // If no cell present, create and insert one
            cell = tr.cn[cellIdx] = {
                tag: 'td',
                role: 'presentation'
            };
            if (tdAttrs) {
                Ext.apply(cell, tdAttrs);
            }
            Ext.apply(cell, {
                colSpan: item.colspan || 1,
                rowSpan: item.rowspan || 1,
                cls: me.cellCls + ' ' + (item.cellCls || '')
            });
            me.configureItem(item);
            // The DomHelper config of the item is the cell's sole child
            cell.cn = item.getRenderTree();
        }
        return result;
    },
    isValidParent: function(item, target, rowIdx, cellIdx) {
        // If we were called with the 3 arg signature just check that the item is within our table,
        if (arguments.length === 3) {
            return this.table.isAncestor(item.el);
        }
        return item.el.dom.parentNode === this.tbody.dom.rows[rowIdx].cells[cellIdx];
    },
    destroy: function() {
        var targetEl, cells, i, len;
        // Table layout cells will be referenced by child items who will create
        // Element instances for their container (layout cell). We need to clean up
        // these Element instances.
        if (this.owner.rendered) {
            targetEl = this.getRenderTarget();
            if (targetEl) {
                cells = targetEl.query('.' + this.cellCls, false);
                for (i = 0 , len = cells.length; i < len; i++) {
                    cells[i].destroy();
                }
            }
        }
        this.callParent();
    }
});

/**
 * Provides a container for arranging a group of related Buttons in a tabular manner.
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *         title: 'Panel with ButtonGroup',
 *         width: 300,
 *         height:200,
 *         renderTo: document.body,
 *         bodyPadding: 10,
 *         html: 'HTML Panel Content',
 *         tbar: [{
 *             xtype: 'buttongroup',
 *             columns: 3,
 *             title: 'Clipboard',
 *             items: [{
 *                 text: 'Paste',
 *                 scale: 'large',
 *                 rowspan: 3,
 *                 iconCls: 'add',
 *                 iconAlign: 'top',
 *                 cls: 'btn-as-arrow'
 *             },{
 *                 xtype:'splitbutton',
 *                 text: 'Menu Button',
 *                 scale: 'large',
 *                 rowspan: 3,
 *                 iconCls: 'add',
 *                 iconAlign: 'top',
 *                 arrowAlign:'bottom',
 *                 menu: [{ text: 'Menu Item 1' }]
 *             },{
 *                 xtype:'splitbutton', text: 'Cut', iconCls: 'add16',
 *                 menu: [{ text: 'Cut Menu Item' }]
 *             },{
 *                 text: 'Copy', iconCls: 'add16'
 *             },{
 *                 text: 'Format', iconCls: 'add16'
 *             }]
 *         }]
 *     });
 *
 */
Ext.define('Ext.container.ButtonGroup', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.buttongroup',
    alternateClassName: 'Ext.ButtonGroup',
    requires: [
        'Ext.layout.container.Table'
    ],
    /**
     * @cfg {Number} columns
     * The `columns` configuration property passed to the {@link #layout configured layout manager}.
     * See {@link Ext.layout.container.Table#columns}.
     */
    /**
     * @cfg baseCls
     * @inheritdoc
     */
    baseCls: Ext.baseCSSPrefix + 'btn-group',
    /**
     * @cfg layout
     * @inheritdoc
     */
    layout: {
        type: 'table'
    },
    /**
     * @cfg defaultType
     * @inheritdoc
     */
    defaultType: 'button',
    /**
     * @cfg frame
     * @inheritdoc
     */
    frame: true,
    /**
     * @cfg {String} defaultButtonUI
     * A default {@link Ext.Component#ui ui} to use for {@link Ext.button.Button Button} items
     */
    /**
     * @cfg frameHeader
     * @inheritdoc
     */
    frameHeader: false,
    /**
     * @cfg {String} titleAlign
     * The alignment of the title text within the available space between the icon and the tools.
     */
    titleAlign: 'center',
    noTitleCls: 'notitle',
    bodyAriaRole: 'toolbar',
    /**
     * @property focusableContainerEl
     * @inheritdoc
     */
    focusableContainerEl: 'body',
    /**
     * @cfg focusableContainer
     * @inheritdoc
     */
    focusableContainer: true,
    initComponent: function() {
        // Copy the component's columns config to the layout if specified
        var me = this,
            cols = me.columns;
        if (cols) {
            me.layout = Ext.apply({
                columns: cols
            }, me.layout);
        }
        if (!me.title) {
            me.addClsWithUI(me.noTitleCls);
        }
        me.callParent();
    },
    /**
     * @private
     */
    onBeforeAdd: function(component) {
        if (component.isButton) {
            if (this.defaultButtonUI && component.ui === 'default' && !component.hasOwnProperty('ui')) {
                component.ui = this.defaultButtonUI;
            } else {
                component.ui = component.ui + '-toolbar';
            }
        }
        this.callParent(arguments);
    },
    beforeRender: function() {
        var me = this,
            ariaAttr;
        me.callParent();
        // If header is off we need to set aria-label
        if (me.afterHeaderInit && !me.header && me.title) {
            ariaAttr = me.bodyAriaRenderAttributes || (me.bodyAriaRenderAttributes = {});
            ariaAttr['aria-label'] = me.title;
        }
    },
    updateHeader: function(force) {
        var me = this,
            bodyEl = me.body,
            header, ariaAttr;
        me.callParent([
            force
        ]);
        header = me.header;
        if (header) {
            if (bodyEl) {
                bodyEl.dom.setAttribute('aria-labelledby', header.id + '-title-textEl');
                bodyEl.dom.removeAttribute('aria-label');
            } else {
                ariaAttr = me.bodyAriaRenderAttributes || (me.bodyAriaRenderAttributes = {});
                ariaAttr['aria-labelledby'] = header.id + '-title-textEl';
                delete ariaAttr['aria-label'];
            }
        } else if (me.title) {
            if (bodyEl) {
                bodyEl.dom.setAttribute('aria-label', me.title);
                bodyEl.dom.removeAttribute('aria-labelledby');
            } else {
                ariaAttr = me.bodyAriaRenderAttributes || (me.bodyAriaRenderAttributes = {});
                ariaAttr['aria-label'] = me.title;
                delete ariaAttr['aria-labelledby'];
            }
        }
    },
    privates: {
        applyDefaults: function(c) {
            if (!Ext.isString(c)) {
                c = this.callParent(arguments);
            }
            return c;
        }
    }
});
/**
     * @cfg {Array} tools
     * @private
     */
/**
     * @cfg {Boolean} collapsible
     * @private
     */
/**
     * @cfg {Boolean} collapseMode
     * @private
     */
/**
     * @cfg {Boolean} animCollapse
     * @private
     */
/**
     * @cfg {Boolean} closable
     * @private
     */

/**
 * This is a utility class for being able to track all items of a particular type
 * inside any level at a container. This can be used in favour of bubbling add/remove events
 * which can add a large perf cost when implemented globally
 * @private
 */
Ext.define('Ext.container.Monitor', {
    target: null,
    selector: '',
    scope: null,
    addHandler: null,
    removeHandler: null,
    invalidateHandler: null,
    clearPropertiesOnDestroy: false,
    clearPrototypeOnDestroy: false,
    disabled: 0,
    constructor: function(config) {
        Ext.apply(this, config);
    },
    destroy: function() {
        this.unbind();
        this.callParent();
    },
    bind: function(target) {
        var me = this;
        me.target = target;
        target.on('beforedestroy', me.disable, me);
        me.onContainerAdd(target);
    },
    unbind: function() {
        var me = this,
            target = me.target;
        if (target && !target.destroying && !target.destroyed) {
            me.onContainerRemove(target, target);
            target.un('beforedestroy', me.disable, me);
        }
        me.items = Ext.destroy(me.items);
    },
    disable: function() {
        ++this.disabled;
    },
    enable: function() {
        if (this.disabled > 0) {
            --this.disabled;
        }
    },
    handleAdd: function(ct, comp) {
        if (!this.disabled) {
            if (Ext.ComponentQuery.is(comp, this.selector)) {
                this.onItemAdd(comp.ownerCt, comp);
            }
            if (comp.isQueryable) {
                this.onContainerAdd(comp);
            }
        }
    },
    onItemAdd: function(ct, comp) {
        var me = this,
            items = me.items,
            handler = me.addHandler;
        if (!me.disabled) {
            if (handler) {
                handler.call(me.scope || comp, comp);
            }
            if (items) {
                items.add(comp);
            }
        }
        // This is a temporary hack until we refactor Forms
        // and kill off Ext.container.Monitor
        comp.clearPropertiesOnDestroy = comp.clearPrototypeOnDestroy = false;
    },
    onItemRemove: function(ct, comp) {
        var me = this,
            items = me.items,
            handler = me.removeHandler;
        if (!me.disabled) {
            if (handler) {
                handler.call(me.scope || comp, comp);
            }
            if (items) {
                items.remove(comp);
            }
        }
    },
    onContainerAdd: function(ct, preventChildren) {
        var me = this,
            items, len, i, comp;
        if (ct.isContainer) {
            ct.on({
                scope: me,
                add: me.handleAdd,
                dockedadd: me.handleAdd,
                remove: me.handleRemove,
                dockedremove: me.handleRemove
            });
        }
        // Means we've been called by a parent container so the selector
        // matchers have already been processed
        if (preventChildren !== true) {
            items = ct.query(me.selector);
            for (i = 0 , len = items.length; i < len; ++i) {
                comp = items[i];
                me.onItemAdd(comp.ownerCt, comp);
            }
        }
        items = ct.query('>container');
        for (i = 0 , len = items.length; i < len; ++i) {
            me.onContainerAdd(items[i], true);
        }
        // This is a temporary hack until we refactor Forms
        // and kill off Ext.container.Monitor
        ct.clearPropertiesOnDestroy = ct.clearPrototypeOnDestroy = false;
    },
    handleRemove: function(ct, comp) {
        var me = this;
        // During a destroy we don't want to maintain any of this information,
        // so typically we'll be disabled here
        if (!me.disabled) {
            if (Ext.ComponentQuery.is(comp, me.selector)) {
                me.onItemRemove(ct, comp);
            }
            if (comp.isQueryable) {
                me.onContainerRemove(ct, comp);
            }
        }
    },
    onContainerRemove: function(ct, comp) {
        var me = this,
            items, i, len, item;
        // If it's not a container, it means it's a queryable that isn't a container.
        // For example a button with a menu
        if (!comp.destroyed && comp.isContainer) {
            me.removeCtListeners(comp);
            if (!comp.destroying) {
                items = comp.query(me.selector);
                for (i = 0 , len = items.length; i < len; ++i) {
                    item = items[i];
                    me.onItemRemove(item.ownerCt, item);
                }
                items = comp.query('container');
                for (i = 0 , len = items.length; i < len; ++i) {
                    me.removeCtListeners(items[i]);
                }
            }
        }
        // comp destroying, or we need to invalidate the collection
        me.invalidateItems(true);
    },
    removeCtListeners: function(ct) {
        var me = this;
        ct.un({
            scope: me,
            add: me.handleAdd,
            dockedadd: me.handleAdd,
            remove: me.handleRemove,
            dockedremove: me.handleRemove
        });
    },
    getItems: function() {
        var me = this,
            items = me.items;
        if (!items) {
            items = me.items = new Ext.util.MixedCollection();
            items.addAll(me.target.query(me.selector));
        }
        return items;
    },
    invalidateItems: function(triggerHandler) {
        var me = this,
            handler = me.invalidateHandler;
        if (triggerHandler && handler) {
            handler.call(me.scope || me, me);
        }
        me.items = Ext.destroy(me.items);
    }
});

/**
 * This plugin can be applied to any `Component` (although almost always to a `Container`)
 * to make it fill the browser viewport. This plugin is used internally by the more familiar
 * `Ext.container.Viewport` class.
 *
 * The `Viewport` container is commonly used but it can be an issue if you need to fill the
 * viewport with a container that derives from another class (e.g., `Ext.tab.Panel`). Prior
 * to this plugin, you would have to do this:
 *
 *      Ext.create('Ext.container.Viewport', {
 *          layout: 'fit', // full the viewport with the tab panel
 *
 *          items: [{
 *              xtype: 'tabpanel',
 *              items: [{
 *                  ...
 *              }]
 *          }]
 *      });
 *
 * With this plugin you can create the `tabpanel` as the viewport:
 *
 *      Ext.create('Ext.tab.Panel', {
 *          plugins: {
 *              viewport: true
 *          },
 *
 *          items: [{
 *              ...
 *          }]
 *      });
 *
 * More importantly perhaps is that as a plugin, the view class can be reused in other
 * contexts such as the content of a `{@link Ext.window.Window window}`.
 *
 * The Viewport renders itself to the document body, and automatically sizes itself to the size of
 * the browser viewport and manages window resizing. There may only be one Viewport created
 * in a page.
 *
 * ## Responsive Design
 *
 * This plugin enables {@link Ext.mixin.Responsive#responsiveConfig} for the components
 * by requiring `Ext.Responsive`.
 *
 * @since 5.0.0
 */
Ext.define('Ext.plugin.Viewport', {
    extend: 'Ext.plugin.Abstract',
    requires: [
        'Ext.Responsive'
    ],
    alias: 'plugin.viewport',
    setCmp: function(cmp) {
        this.cmp = cmp;
        if (cmp && !cmp.isViewport) {
            this.decorate(cmp);
            if (cmp.renderConfigs) {
                cmp.flushRenderConfigs();
            }
            cmp.setupViewport();
        }
    },
    destroy: function() {
        var el = this.cmp.el;
        this.callParent();
        // Remove the injected overrides so that the bodyEl singleton
        // can be reused by subsequent code (eg, unit tests)
        if (el) {
            delete el.setHeight;
            delete el.setWidth;
        }
    },
    statics: {
        decorate: function(target) {
            Ext.applyIf(target.prototype || target, {
                ariaRole: 'application',
                viewportCls: Ext.baseCSSPrefix + 'viewport'
            });
            Ext.override(target, {
                isViewport: true,
                preserveElOnDestroy: true,
                initComponent: function() {
                    this.callParent();
                    this.setupViewport();
                },
                // Because we don't stamp the size until onRender, our size model
                // won't return correctly. As we're always going to be configured,
                // just return the value here
                getSizeModel: function() {
                    var configured = Ext.layout.SizeModel.configured;
                    return configured.pairsByHeightOrdinal[configured.ordinal];
                },
                handleViewportResize: function() {
                    var me = this,
                        Element = Ext.dom.Element,
                        width = Element.getViewportWidth(),
                        height = Element.getViewportHeight();
                    if (width !== me.width || height !== me.height) {
                        me.setSize(width, height);
                    }
                },
                setupViewport: function() {
                    var me = this,
                        el = document.body;
                    if (!me.$responsiveId) {
                        me.setResponsiveConfig(true);
                        Ext.mixin.Responsive.register(me);
                        me.setupResponsiveContext();
                    }
                    // Here in the (perhaps unlikely) case that the body dom el doesn't yet have
                    // an id, we want to give it the same id as the viewport component so getCmp
                    // lookups will be able to find the owner component.
                    //
                    // Note that nothing says that components that use configured elements
                    // have to have matching ids (they probably won't), but this is at least making
                    // the attempt so that getCmp *may* be able to find the component.
                    // However, in these cases, it's really better to use Component#from
                    // to find the owner component.
                    if (!el.id) {
                        el.id = me.id;
                    }
                    // In addition, stamp on the data-componentid so lookups using Component's
                    // from will work.
                    el.setAttribute('data-componentid', me.id);
                    if (!me.ariaStaticRoles[me.ariaRole]) {
                        el.setAttribute('role', me.ariaRole);
                    }
                    el = me.el = Ext.getBody();
                    Ext.fly(document.documentElement).addCls(me.viewportCls);
                    el.setHeight = el.setWidth = Ext.emptyFn;
                    el.dom.scroll = 'no';
                    me.allowDomMove = false;
                    me.renderTo = el;
                    if (Ext.supports.Touch) {
                        me.addMeta('apple-mobile-web-app-capable', 'yes');
                    }
                    // Get the DOM disruption over with before the Viewport renders
                    // and begins a layout
                    Ext.scrollbar.size();
                    // Clear any dimensions, we will size later on in onRender
                    me.width = me.height = undefined;
                    // ... but take the measurements now because doing that in onRender
                    // will cause a costly reflow which we just forced with getScrollbarSize()
                    me.initialViewportHeight = Ext.Element.getViewportHeight();
                    me.initialViewportWidth = Ext.Element.getViewportWidth();
                },
                afterLayout: function(layout) {
                    if (Ext.supports.Touch) {
                        document.body.scrollTop = 0;
                    }
                    this.callParent([
                        layout
                    ]);
                },
                onRender: function() {
                    var me = this,
                        overflowEl = me.getOverflowEl(),
                        // eslint-disable-line no-unused-vars
                        body = Ext.getBody();
                    me.callParent(arguments);
                    // The global scroller is our scroller.
                    // We must provide a non-scrolling one if we are not configured to scroll,
                    // otherwise the deferred ready listener in Scroller will create
                    // one with scroll: true
                    Ext.setViewportScroller(me.getScrollable() || {
                        x: false,
                        y: false,
                        element: body
                    });
                    // If we are not scrolling the body, the body has to be overflow:hidden
                    if (me.getOverflowEl() !== body) {
                        body.setStyle('overflow', 'hidden');
                    }
                    // Important to start life as the proper size (to avoid extra layouts)
                    // But after render so that the size is not stamped into the body,
                    // although measurement has to take place before render to avoid
                    // causing a reflow.
                    me.width = me.initialViewportWidth;
                    me.height = me.initialViewportHeight;
                    me.initialViewportWidth = me.initialViewportHeight = null;
                },
                initInheritedState: function(inheritedState, inheritedStateInner) {
                    var me = this,
                        root = Ext.rootInheritedState;
                    if (inheritedState !== root) {
                        // We need to go at this again but with the rootInheritedState object. Let
                        // any derived class poke on the proper object!
                        me.initInheritedState(me.inheritedState = root, me.inheritedStateInner = Ext.Object.chain(root));
                    } else {
                        me.callParent([
                            inheritedState,
                            inheritedStateInner
                        ]);
                    }
                },
                doDestroy: function() {
                    var me = this,
                        root = Ext.rootInheritedState,
                        scroller = me.scrollable,
                        key;
                    // We set the global body scroller aboce in onRender.
                    // Just relinquish it here and allow it to live on.
                    if (scroller) {
                        // Return the body scroller to default; X and Y scrolling
                        scroller.setConfig({
                            x: true,
                            y: true
                        });
                        me.scrollable = null;
                    }
                    // Clear any properties from the inheritedState so we don't pollute the
                    // global namespace. If we have a rtl flag set, leave it alone because it's
                    // likely we didn't write it
                    for (key in root) {
                        if (key !== 'rtl') {
                            delete root[key];
                        }
                    }
                    delete me.el.setHeight;
                    delete me.el.setWidth;
                    me.removeUIFromElement();
                    me.el.removeCls(me.baseCls);
                    Ext.fly(document.body.parentNode).removeCls(me.viewportCls);
                    me.callParent();
                },
                addMeta: function(name, content) {
                    var meta = document.createElement('meta');
                    meta.setAttribute('name', name);
                    meta.setAttribute('content', content);
                    Ext.getHead().appendChild(meta, true);
                },
                privates: {
                    // override here to prevent an extraneous warning
                    applyTargetCls: function(targetCls) {
                        var el = this.el;
                        if (el === this.getTargetEl()) {
                            this.el.addCls(targetCls);
                        } else {
                            this.callParent([
                                targetCls
                            ]);
                        }
                    },
                    // Override here to prevent tabIndex set/reset on the body
                    disableTabbing: function() {
                        var el = this.el;
                        if (el) {
                            el.saveTabbableState({
                                skipSelf: true
                            });
                        }
                    },
                    enableTabbing: function() {
                        var el = this.el;
                        if (el) {
                            el.restoreTabbableState({
                                skipSelf: true
                            });
                        }
                    },
                    updateResponsiveState: function() {
                        // By providing this method we are in sync with the layout suspend/resume as
                        // well as other changes to configs that need to happen during this pulse of
                        // size change.
                        // Since we are not using the Viewport plugin beyond applying its methods on
                        // to our prototype, we need to be Responsive ourselves and call this here:
                        this.handleViewportResize();
                        this.callParent();
                    }
                }
            });
        }
    }
}, function(Viewport) {
    Viewport.prototype.decorate = Viewport.decorate;
});

/**
 * A specialized container representing the viewable application area (the browser viewport).
 *
 * The Viewport renders itself to the document body, and automatically sizes itself to the size of
 * the browser viewport and manages window resizing. There may only be one Viewport created
 * in a page.
 *
 * Like any {@link Ext.container.Container Container}, a Viewport will only perform sizing
 * and positioning on its child Components if you configure it with a {@link #layout}.
 *
 * A Common layout used with Viewports is {@link Ext.layout.container.Border border layout},
 * but if the required layout is simpler, a different layout should be chosen.
 *
 * For example, to simply make a single child item occupy all available space, use
 * {@link Ext.layout.container.Fit fit layout}.
 *
 * To display one "active" item at full size from a choice of several child items, use
 * {@link Ext.layout.container.Card card layout}.
 *
 * Inner layouts are available because all {@link Ext.panel.Panel Panel}s
 * added to the Viewport, either through its {@link #cfg-items}, or the {@link #method-add}
 * method of any of its child Panels may themselves have a layout.
 *
 * The Viewport does not provide scrolling, so child Panels within the Viewport should provide
 * for scrolling if needed using the {@link #scrollable} config.
 *
 * An example showing a classic application border layout:
 *
 *     @example
 *     Ext.create('Ext.container.Viewport', {
 *         layout: 'border',
 *         items: [{
 *             region: 'north',
 *             html: '<h1 class="x-panel-header">Page Title</h1>',
 *             border: false,
 *             margin: '0 0 5 0'
 *         }, {
 *             region: 'west',
 *             collapsible: true,
 *             title: 'Navigation',
 *             width: 150
 *             // could use a TreePanel or AccordionLayout for navigational items
 *         }, {
 *             region: 'south',
 *             title: 'South Panel',
 *             collapsible: true,
 *             html: 'Information goes here',
 *             split: true,
 *             height: 100,
 *             minHeight: 100
 *         }, {
 *             region: 'east',
 *             title: 'East Panel',
 *             collapsible: true,
 *             split: true,
 *             width: 150
 *         }, {
 *             region: 'center',
 *             xtype: 'tabpanel', // TabPanel itself has no title
 *             activeTab: 0,      // First tab active by default
 *             items: {
 *                 title: 'Default Tab',
 *                 html: 'The first tab\'s content. Others may be added dynamically'
 *             }
 *         }]
 *     });
 *
 * Alternatively you can turn any normal Container (or Component) into a Viewport using
 * the `{@link Ext.plugin.Viewport viewport plugin}`.
 */
Ext.define('Ext.container.Viewport', {
    extend: 'Ext.container.Container',
    requires: [
        'Ext.plugin.Viewport',
        'Ext.Responsive'
    ],
    alias: 'widget.viewport',
    alternateClassName: 'Ext.Viewport',
    /**
     * @property {Boolean} isViewport
     * `true` in this class to identify an object as an instantiated Viewport, or subclass thereof.
     * @readonly
     */
    /**
     * @cfg {Number} [maxUserScale=10]
     * The maximum zoom scale. Only applicable for touch devices. Set this to 1 to
     * disable zooming.
     */
    // Privatize config options which, if used, would interfere with the
    // correct operation of the Viewport as the sole manager of the
    // layout of the document body.
    /**
     * @cfg {Boolean} allowDomMove
     * @private
     */
    /**
     * @cfg {String/HTMLElement/Ext.dom.Element} renderTo
     * Always renders to document body.
     * @private
     */
    /**
     * @cfg {Number} height
     * Sets itself to viewport width.
     * @private
     */
    /**
     * @cfg {Number} width
     * Sets itself to viewport height.
     * @private
     */
    /**
     * @cfg scrollable
     * @hide
     */
    ariaRole: 'application',
    responsiveConfig: true,
    privates: {
        updateResponsiveState: function() {
            // By providing this method we are in sync with the layout suspend/resume as
            // well as other changes to configs that need to happen during this pulse of
            // size change.
            // Since we are not using the Viewport plugin beyond applying its methods on
            // to our prototype, we need to be Responsive ourselves and call this here:
            this.handleViewportResize();
            this.mixins.responsive.updateResponsiveState.call(this);
        }
    }
}, function() {
    Ext.plugin.Viewport.decorate(this);
});

/**
 * This is a layout that enables anchoring of contained elements relative to the container's
 * dimensions. If the container is resized, all anchored items are automatically rerendered
 * according to their `{@link #anchor}` rules.
 *
 * This class is intended to be extended or created via the
 * {@link Ext.container.Container#layout layout}: 'anchor' config, and should generally not need
 * to be created directly via the new keyword.
 * 
 * AnchorLayout does not have any direct config options (other than inherited ones). By default,
 * AnchorLayout will calculate anchor measurements based on the size of the container itself.
 * However, the container using the AnchorLayout can supply an anchoring-specific config property
 * of `anchorSize`.
 *
 * If anchorSize is specifed, the layout will use it as a virtual container for the purposes
 * of calculating anchor measurements based on it instead, allowing the container to be sized
 * independently of the anchoring logic if necessary.
 *
 *     @example
 *     Ext.create('Ext.Panel', {
 *         width: 500,
 *         height: 400,
 *         title: "AnchorLayout Panel",
 *         layout: 'anchor',
 *         renderTo: Ext.getBody(),
 *         items: [
 *             {
 *                 xtype: 'panel',
 *                 title: '75% Width and 20% Height',
 *                 anchor: '75% 20%'
 *             },
 *             {
 *                 xtype: 'panel',
 *                 title: 'Offset -300 Width & -200 Height',
 *                 anchor: '-300 -200'   
 *             },
 *             {
 *                 xtype: 'panel',
 *                 title: 'Mixed Offset and Percent',
 *                 anchor: '-250 20%'
 *             }
 *         ]
 *     });
 */
Ext.define('Ext.layout.container.Anchor', {
    extend: 'Ext.layout.container.Auto',
    alternateClassName: 'Ext.layout.AnchorLayout',
    alias: 'layout.anchor',
    type: 'anchor',
    /**
     * @cfg {String} anchor
     *
     * This configuration option is to be applied to **child `items`** of a container managed 
     * by an {@link Ext.layout.container.Anchor Anchor Layout}.
     *
     * This value is what tells the layout how an item should be anchored to the container. `items`
     * added to an AnchorLayout accept an anchoring-specific config property of **anchor**
     * which is a string containing two values: the horizontal anchor value and the vertical anchor
     * value (for example, '100% 50%'). The following types of anchor values are supported:
     *
     * - **Percentage** : Any value between 1 and 100, expressed as a percentage.
     *
     *   The first anchor is the percentage width that the item should take up within the container,
     *   and the second is the percentage height.  For example:
     *
     *       // two values specified
     *       anchor: '100% 50%' // render item complete width of the container and
     *                          // 1/2 height of the container
     *       // one value specified
     *       anchor: '100%'     // the width value; the height will default to auto
     *
     * - **Offsets** : Any positive or negative integer value.
     *
     *   This is a raw adjustment where the first anchor is the offset from the right edge
     *   of the container, and the second is the offset from the bottom edge. For example:
     *
     *       // two values specified
     *       anchor: '-50 -100' // render item the complete width of the container
     *                          // minus 50 pixels and
     *                          // the complete height minus 100 pixels.
     *       // one value specified
     *       anchor: '-50'      // anchor value is assumed to be the right offset value
     *                          // bottom offset will default to 0
     *
     * - **Sides** : Valid values are `right` (or `r`) and `bottom` (or `b`).
     *
     *   Either the container must have a fixed size or an anchorSize config value defined
     *   at render time in order for these to have any effect.
     *   
     * - **Mixed** :
     *
     *   Anchor values can also be mixed as needed.  For example, to render the width offset
     *   from the container right edge by 50 pixels and 75% of the container's height use:
     *   
     *       anchor:   '-50 75%'
     */
    /**
     * @cfg {String} defaultAnchor
     * Default anchor for all child **container** items applied if no anchor or specific width
     * is set on the child item.
     */
    defaultAnchor: '100%',
    parseAnchorRE: /^(r|right|b|bottom)$/i,
    manageOverflow: true,
    // Anchor layout does not read the size of individual items in the shrink-wrapping
    // dimension(s) because, as a subclass of autocontainer, it measures them as a whole
    // using an outer element.  However, anchor layout may set the size of its items in
    // non-shrink-wrapping dimension(s).
    setsItemSize: true,
    beginLayoutCycle: function(ownerContext) {
        var me = this,
            dimensions = 0,
            anchorSpec, childContext, childItems, i, length;
        me.callParent(arguments);
        childItems = ownerContext.childItems;
        // populated by callParent
        length = childItems.length;
        for (i = 0; i < length; ++i) {
            childContext = childItems[i];
            anchorSpec = childContext.target.anchorSpec;
            if (anchorSpec) {
                if (childContext.widthModel.calculated && anchorSpec.right) {
                    dimensions |= 1;
                }
                if (childContext.heightModel.calculated && anchorSpec.bottom) {
                    dimensions |= 2;
                }
                if (dimensions === 3) {
                    // if (both dimensions in play)
                    break;
                }
            }
        }
        ownerContext.anchorDimensions = dimensions;
        me.sanityCheck(ownerContext);
    },
    calculateItems: function(ownerContext, containerSize) {
        var me = this,
            childItems = ownerContext.childItems,
            length = childItems.length,
            gotHeight = containerSize.gotHeight,
            gotWidth = containerSize.gotWidth,
            ownerHeight = containerSize.height,
            ownerWidth = containerSize.width,
            knownDimensions = (gotWidth ? 1 : 0) | (gotHeight ? 2 : 0),
            anchorDimensions = ownerContext.anchorDimensions,
            anchorSpec, childContext, childMargins, height, i, width;
        if (!anchorDimensions) {
            return true;
        }
        for (i = 0; i < length; i++) {
            childContext = childItems[i];
            childMargins = childContext.getMarginInfo();
            anchorSpec = childContext.target.anchorSpec;
            // Check widthModel in case "defaults" has applied an anchor to a component
            // that also has width (which must win). If we did not make this check in this
            // way, we would attempt to calculate a width where it had been configured.
            //
            if (gotWidth && childContext.widthModel.calculated) {
                width = anchorSpec.right(ownerWidth) - childMargins.width;
                width = me.adjustWidthAnchor(width, childContext);
                childContext.setWidth(width);
            }
            // Repeat for height
            if (gotHeight && childContext.heightModel.calculated) {
                height = anchorSpec.bottom(ownerHeight) - childMargins.height;
                height = me.adjustHeightAnchor(height, childContext);
                childContext.setHeight(height);
            }
        }
        // If all required dimensions are known, we're done
        return (knownDimensions & anchorDimensions) === anchorDimensions;
    },
    sanityCheck: function(ownerContext) {
        var shrinkWrapWidth = ownerContext.widthModel.shrinkWrap,
            shrinkWrapHeight = ownerContext.heightModel.shrinkWrap,
            children = ownerContext.childItems,
            anchorSpec, comp, childContext, i, length;
        for (i = 0 , length = children.length; i < length; ++i) {
            childContext = children[i];
            comp = childContext.target;
            anchorSpec = comp.anchorSpec;
            if (anchorSpec) {
                if (childContext.widthModel.calculated && anchorSpec.right) {
                    if (shrinkWrapWidth) {
                        Ext.log({
                            level: 'warn',
                            msg: 'Right anchor on ' + comp.id + ' in shrinkWrap width container'
                        });
                    }
                }
                if (childContext.heightModel.calculated && anchorSpec.bottom) {
                    if (shrinkWrapHeight) {
                        Ext.log({
                            level: 'warn',
                            msg: 'Bottom anchor on ' + comp.id + ' in shrinkWrap height container'
                        });
                    }
                }
            }
        }
    },
    /**
     * @private
     */
    anchorFactory: {
        offset: function(delta) {
            return function(v) {
                return v + delta;
            };
        },
        ratio: function(ratio) {
            return function(v) {
                return Math.floor(v * ratio);
            };
        },
        standard: function(diff) {
            return function(v) {
                return v - diff;
            };
        }
    },
    parseAnchor: function(a, start, cstart) {
        var factory = this.anchorFactory,
            delta;
        if (a && a !== 'none') {
            if (this.parseAnchorRE.test(a)) {
                return factory.standard(cstart - start);
            }
            if (a.indexOf('%') !== -1) {
                return factory.ratio(parseFloat(a.replace('%', '')) * 0.01);
            }
            delta = parseInt(a, 10);
            if (!isNaN(delta)) {
                return factory.offset(delta);
            }
        }
        return null;
    },
    /**
     * @private
     */
    adjustWidthAnchor: function(value, childContext) {
        return value;
    },
    /**
     * @private
     */
    adjustHeightAnchor: function(value, childContext) {
        return value;
    },
    configureItem: function(item) {
        var me = this,
            owner = me.owner,
            anchor = item.anchor,
            anchorsArray, anchorWidth, anchorHeight;
        me.callParent(arguments);
        if (!item.anchor && item.items && !Ext.isNumber(item.width)) {
            item.anchor = anchor = me.defaultAnchor;
        }
        /**
         * @cfg {Number/Object} anchorSize
         * Defines the anchoring size of container.
         * Either a number to define the width of the container or an object
         * with `width` and `height` fields.
         * @member Ext.container.Container
         */
        if (owner.anchorSize) {
            if (typeof owner.anchorSize === 'number') {
                anchorWidth = owner.anchorSize;
            } else {
                anchorWidth = owner.anchorSize.width;
                anchorHeight = owner.anchorSize.height;
            }
        } else {
            anchorWidth = owner.initialConfig.width;
            anchorHeight = owner.initialConfig.height;
        }
        if (anchor) {
            // cache all anchor values
            anchorsArray = anchor.split(' ');
            item.anchorSpec = {
                right: me.parseAnchor(anchorsArray[0], item.initialConfig.width, anchorWidth),
                bottom: me.parseAnchor(anchorsArray[1], item.initialConfig.height, anchorHeight)
            };
        }
    },
    sizePolicy: {
        $: {
            readsWidth: 1,
            readsHeight: 1,
            setsWidth: 0,
            setsHeight: 0
        },
        b: {
            readsWidth: 1,
            readsHeight: 0,
            setsWidth: 0,
            setsHeight: 1
        },
        r: {
            $: {
                readsWidth: 0,
                readsHeight: 1,
                setsWidth: 1,
                setsHeight: 0
            },
            b: {
                readsWidth: 0,
                readsHeight: 0,
                setsWidth: 1,
                setsHeight: 1
            }
        }
    },
    getItemSizePolicy: function(item) {
        var anchorSpec = item.anchorSpec,
            key = '$',
            policy = this.sizePolicy,
            sizeModel;
        if (anchorSpec) {
            sizeModel = this.owner.getSizeModel();
            if (anchorSpec.right && !sizeModel.width.shrinkWrap) {
                policy = policy.r;
            }
            if (anchorSpec.bottom && !sizeModel.height.shrinkWrap) {
                key = 'b';
            }
        }
        return policy[key];
    }
});

/**
 * This class is used to wrap content items in the `Dashboard`. It uses an
 * `anchor` layout by default and provides resizing on the bottom edge only.
 * @protected
 */
Ext.define('Ext.dashboard.Panel', {
    extend: 'Ext.panel.Panel',
    xtype: 'dashboard-panel',
    cls: Ext.baseCSSPrefix + 'dashboard-panel',
    anchor: '100%',
    layout: 'fit',
    frame: true,
    closable: true,
    collapsible: true,
    animCollapse: true,
    titleCollapse: true,
    stateful: true,
    draggable: {
        moveOnDrag: false
    },
    animateClose: true,
    loadMask: true,
    loadMessage: 'Loading...',
    minHeight: 90,
    resizable: true,
    resizeHandles: 's',
    // Override Panel's default doClose to provide a custom fade out effect
    // when a portlet is removed from the portal
    doClose: function() {
        var me = this;
        if (me.animateClose) {
            if (!me.closing) {
                me.closing = true;
                me.el.animate({
                    opacity: 0,
                    callback: me.finishClose,
                    scope: me
                });
            }
        } else {
            me.finishClose();
        }
    },
    finishClose: function() {
        var me = this,
            closeAction = me.closeAction;
        me.closing = false;
        me.fireEvent('close', me);
        // The close of the last portlet within a column results in removal of both the column
        // and its splitter.
        // So coalesce any layouts resulting from this operation.
        Ext.suspendLayouts();
        me[closeAction]();
        Ext.resumeLayouts(true);
        if (closeAction === 'hide') {
            // Restore the opacity from doClose
            me.el.setOpacity(1);
        }
    },
    afterRender: function() {
        this.callParent();
        if (this.loading) {
            this.onViewBeforeLoad();
        }
    },
    doCollapseExpand: function(flags) {
        // Ensure the layout caused by expand or collapse does not propagate
        // to the whole dashboard. The dashboard layout is automatically
        // updated after the operation.
        this._expandCollapseRoot = this._isLayoutRoot;
        this._isLayoutRoot = true;
        this.callParent(arguments);
    },
    afterExpand: function() {
        this._isLayoutRoot = this._expandCollapseRoot;
        this.callParent(arguments);
    },
    afterCollapse: function() {
        this._isLayoutRoot = this._expandCollapseRoot;
        this.callParent(arguments);
    },
    getLoadMask: function() {
        var me = this,
            loadMask = me.rendered && me.loadMask,
            config;
        if (loadMask && !loadMask.isComponent) {
            config = {
                target: me
            };
            if (loadMask === true) {
                loadMask = config;
            } else {
                Ext.apply(config, loadMask);
            }
            me.loadMask = loadMask = Ext.ComponentManager.create(config, 'loadmask');
        }
        return loadMask || null;
    },
    onAdd: function(view) {
        this.callParent(arguments);
        view.on({
            beforeload: 'onViewBeforeLoad',
            load: 'onViewLoaded',
            scope: this
        });
    },
    onViewBeforeLoad: function() {
        var loadMask;
        this.loading = true;
        loadMask = this.getLoadMask();
        if (loadMask) {
            loadMask.show();
        }
    },
    onViewLoaded: function() {
        var loadMask, view, title;
        this.loading = false;
        loadMask = this.getLoadMask();
        if (loadMask) {
            loadMask.hide();
        }
        view = this.items.getAt(0);
        if (view.getTitle) {
            title = view.getTitle();
            if (title) {
                this.setTitle(title);
            }
        }
    },
    /**
     * @private
     */
    setBox: function(box) {
        // The resizer calls setBox which would set our left/top coordinates but
        // that is a BAD thing in a column layout which relies on flow!
        this.setSize(box.width, box.height);
    },
    /**
     * @private
     */
    getState: function() {
        var me = this,
            state = me.callParent() || {};
        if (!state.collapsed) {
            me.addPropertyToState(state, 'height', me.rendered ? me.getHeight() : me.height || me.minHeight || 100);
        }
        return state;
    }
});

/**
 * This class manages columns in a `Dashboard`. The primary role here is the `defaultType`
 * config which points to `Ext.dashboard.Panel` and the self-destruct mechanism to get
 * rid of empty columns.
 * @protected
 */
Ext.define('Ext.dashboard.Column', {
    extend: 'Ext.container.Container',
    xtype: 'dashboard-column',
    requires: [
        'Ext.layout.container.Anchor',
        'Ext.dashboard.Panel'
    ],
    layout: 'anchor',
    isDashboardColumn: true,
    defaultType: 'dashboard-panel',
    cls: Ext.baseCSSPrefix + 'dashboard-column',
    synthetic: true,
    // not user-defined
    onRemove: function(dashPanel, isDestroying) {
        var me = this,
            ownerCt = me.ownerCt,
            remainingSiblings, numRemaining, columnWidth,
            totalColumnWidth = 0,
            i;
        // If we've just emptied this column.
        if (ownerCt && me.items.getCount() === 0) {
            // Collect remaining column siblings of the same row, when this one has gone.
            remainingSiblings = Ext.Array.filter(ownerCt.query('>' + me.xtype + '[rowIndex=' + me.rowIndex + ']'), function(c) {
                return c !== me;
            });
            numRemaining = remainingSiblings.length;
            // If this column is not destroyed, then remove this column (unless it is the last one!)
            if (!me.destroying && !me.destroyed) {
                ownerCt.remove(me);
                // Down to just one column; it must take up full width
                if (numRemaining === 1) {
                    remainingSiblings[0].columnWidth = 1;
                } else // If more than one remaining sibling, redistribute columnWidths proportionally
                // so that they still total 1.0
                {
                    for (i = 0; i < numRemaining; i++) {
                        totalColumnWidth += remainingSiblings[i].columnWidth || 0;
                    }
                    for (i = 0; i < numRemaining; i++) {
                        columnWidth = remainingSiblings[i].columnWidth;
                        remainingSiblings[i].columnWidth = Math.floor(columnWidth / totalColumnWidth * 100) / 100;
                    }
                }
                // Needed if user is *closing* the last portlet in a column as opposed
                // to just dragging it to another place.
                // The destruction will not force a layout
                if (isDestroying) {
                    ownerCt.updateLayout();
                }
            }
        }
    }
});

/**
 * This is the layout style of choice for creating structural layouts in a multi-column format
 * where the width of each column can be specified as a percentage or fixed width, but the height
 * is allowed to vary based on the content. This class is intended to be extended or created
 * via the `layout: 'column'` {@link Ext.container.Container#layout} config, and should generally
 * not need to be created directly via the new keyword.
 *
 * ColumnLayout does not have any direct config options (other than inherited ones), but it does
 * support a specific config property of `columnWidth` that can be included in the config
 * of any panel added to it. The layout will use the columnWidth (if present) or width of each panel
 * during layout to determine how to size each panel. If width or columnWidth is not specified
 * for a given panel, its width will default to the panel's width (or auto).
 *
 * The width property is always evaluated as pixels, and must be a number greater than or equal
 * to 1. The columnWidth property is always evaluated as a percentage, and must be a decimal value
 * greater than 0 and less than 1 (e.g., .25).
 *
 * The basic rules for specifying column widths are pretty simple. The logic makes two passes
 * through the set of contained panels. During the first layout pass, all panels that either
 * have a fixed width or none specified (auto) are skipped, but their widths are subtracted
 * from the overall container width.
 *
 * During the second pass, all panels with columnWidths are assigned pixel widths in proportion
 * to their percentages based on the total **remaining** container width. In other words,
 * percentage width panels are designed to fill the space left over by all the fixed-width
 * and/or auto-width panels. Because of this, while you can specify any number of columns
 * with different percentages, the columnWidths must always add up to 1 (or 100%) when added
 * together, otherwise your layout may not render as expected.
 *
 *     @example
 *     // All columns are percentages -- they must add up to 1
 *     Ext.create('Ext.panel.Panel', {
 *         title: 'Column Layout - Percentage Only',
 *         width: 350,
 *         height: 250,
 *         layout:'column',
 *         items: [{
 *             title: 'Column 1',
 *             columnWidth: 0.25
 *         }, {
 *             title: 'Column 2',
 *             columnWidth: 0.55
 *         }, {
 *             title: 'Column 3',
 *             columnWidth: 0.20
 *         }],
 *         renderTo: Ext.getBody()
 *     });
 *
 *     // Mix of width and columnWidth -- all columnWidth values must add up
 *     // to 1. The first column will take up exactly 120px, and the last two
 *     // columns will fill the remaining container width.
 *
 *     Ext.create('Ext.Panel', {
 *         title: 'Column Layout - Mixed',
 *         width: 350,
 *         height: 250,
 *         layout:'column',
 *         items: [{
 *             title: 'Column 1',
 *             width: 120
 *         }, {
 *             title: 'Column 2',
 *             columnWidth: 0.7
 *         }, {
 *             title: 'Column 3',
 *             columnWidth: 0.3
 *         }],
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.layout.container.Column', {
    extend: 'Ext.layout.container.Auto',
    alternateClassName: 'Ext.layout.ColumnLayout',
    alias: 'layout.column',
    type: 'column',
    itemCls: Ext.baseCSSPrefix + 'column',
    targetCls: Ext.baseCSSPrefix + 'column-layout-ct',
    // The clear value to force floats to wrap back to zero
    clearSide: 'left',
    // Columns with a columnWidth have their width managed.
    columnWidthSizePolicy: {
        readsWidth: 0,
        readsHeight: 1,
        setsWidth: 1,
        setsHeight: 0
    },
    createsInnerCt: true,
    manageOverflow: true,
    // Column layout needs to set the size of items configured with columnWidth, and it
    // needs to read the size of items with a configured width.
    setsItemSize: true,
    needsItemSize: true,
    isItemShrinkWrap: function(item) {
        return true;
    },
    getItemSizePolicy: function(item, ownerSizeModel) {
        if (item.columnWidth) {
            if (!ownerSizeModel) {
                ownerSizeModel = this.owner.getSizeModel();
            }
            if (!ownerSizeModel.width.shrinkWrap) {
                return this.columnWidthSizePolicy;
            }
        }
        return this.autoSizePolicy;
    },
    calculateItems: function(ownerContext, containerSize) {
        var me = this,
            columnCount = me.columnCount,
            targetContext = ownerContext.targetContext,
            items = ownerContext.childItems,
            len = items.length,
            contentWidth = 0,
            gotWidth = containerSize.gotWidth,
            blocked, availableWidth, i, itemContext, itemMarginWidth, itemWidth;
        // No parallel measurement, cannot lay out boxes.
        if (gotWidth === false) {
            // \\ TODO: Deal with target padding width
            // TODO: only block if we have items with columnWidth
            targetContext.domBlock(me, 'width');
            blocked = true;
        } else if (gotWidth) {
            availableWidth = containerSize.width;
        } else {
            // gotWidth is undefined, which means we must be width shrink wrap.
            // cannot calculate columnWidths if we're shrink wrapping.
            return true;
        }
        // we need the widths of the columns we don't manage to proceed so we block on them
        // if they are not ready...
        for (i = 0; i < len; ++i) {
            itemContext = items[i];
            // Ensure that each row start clears to start of row.
            // Tall items would block it as below.
            // "Item 4" requires clear:left to begin at column zero.
            // +------------------------------- +
            // |+--------+ +--------+ +--------+|
            // ||        | |        | |        ||
            // || Item 1 | | Item 2 | | Item 3 ||
            // ||        | +--------+ +--------+|
            // ||        | +--------+           |
            // |+--------+ |        |           |
            // |           | Item 4 |           |
            // |           |        |           |
            // |           +--------+           |
            // +--------------------------------+
            if (columnCount) {
                if (i % columnCount) {
                    itemContext.setProp('clear', null);
                } else {
                    itemContext.setProp('clear', me.clearSide);
                }
            }
            // this is needed below for non-calculated columns, but is also needed in the
            // next loop for calculated columns... this way we only call getMarginInfo in
            // this loop and use the marginInfo property in the next...
            itemMarginWidth = itemContext.getMarginInfo().width;
            if (!itemContext.widthModel.calculated) {
                itemWidth = itemContext.getProp('width');
                if (typeof itemWidth !== 'number') {
                    itemContext.block(me, 'width');
                    blocked = true;
                }
                contentWidth += itemWidth + itemMarginWidth;
            }
        }
        if (!blocked) {
            contentWidth = me.calculateItemSizeWithContent(availableWidth, contentWidth, items);
            ownerContext.setContentWidth(contentWidth + ownerContext.paddingContext.getPaddingInfo().width);
        }
        // we registered all the values that block this calculation, so abort now if blocked...
        return !blocked;
    },
    calculateItemSizeWithContent: function(availableWidth, contentWidth, items) {
        var itemMarginWidth, itemContext, itemWidth, i,
            len = items.length;
        availableWidth = (availableWidth < contentWidth) ? 0 : availableWidth - contentWidth;
        for (i = 0; i < len; ++i) {
            itemContext = items[i];
            if (itemContext.widthModel.calculated) {
                itemMarginWidth = itemContext.marginInfo.width;
                // always set by above loop
                itemWidth = itemContext.target.columnWidth;
                itemWidth = Math.floor(itemWidth * availableWidth) - itemMarginWidth;
                itemWidth = itemContext.setWidth(itemWidth);
                // constrains to min/maxWidth
                contentWidth += itemWidth + itemMarginWidth;
            }
        }
        return contentWidth;
    }
});

/**
 * A DragTracker listens for drag events on an Element and fires events at the start and end of the
 * drag, as well as during the drag. This is useful for components such as {@link Ext.slider.Multi},
 * where there is an element that can be dragged around to change the Slider's value.
 *
 * DragTracker provides a series of template methods that should be overridden to provide
 * functionality in response to detected drag operations. These are onBeforeStart, onStart, onDrag,
 * onCancel and onEnd.
 * See {@link Ext.slider.Multi}'s initEvents function for an example implementation.
 */
Ext.define('Ext.dd.DragTracker', {
    uses: [
        'Ext.util.Region'
    ],
    mixins: {
        observable: 'Ext.util.Observable'
    },
    /**
     * @property {Boolean} active
     * Indicates whether the user is currently dragging this tracker.
     * @readonly
     */
    active: false,
    /**
     * @property {HTMLElement} dragTarget
     * The element being dragged.
     *
     * Only valid during drag operations.
     *
     * If the {@link #delegate} option is used, this will be the delegate element which was
     * mousedowned.
     * @readonly
     */
    /**
     * @cfg {Boolean} trackOver
     * Set to true to fire mouseover and mouseout events when the mouse enters or leaves the target
     * element.
     *
     * This is implicitly set when an {@link #overCls} is specified.
     *
     * If the {@link #delegate} option is used, these events fire only when a delegate element
     * is entered of left.
     */
    trackOver: false,
    /**
     * @cfg {String} overCls
     * A CSS class to add to the DragTracker's target element when the element (or, if the
     * {@link #delegate} option is used, when a delegate element) is mouseovered.
     *
     * If the {@link #delegate} option is used, these events fire only when a delegate element
     * is entered of left.
     */
    /**
     * @cfg {Ext.util.Region/Ext.dom.Element} constrainTo
     * A {@link Ext.util.Region Region} (Or an element from which a Region measurement will be read)
     * which is used to constrain the result of the {@link #getOffset} call.
     *
     * This may be set any time during the DragTracker's lifecycle to set a dynamic constraining
     * region.
     */
    /**
     * @cfg {Number} tolerance
     * Number of pixels the drag target must be moved before dragging is
     * considered to have started.
     */
    tolerance: 5,
    /**
     * @cfg {Boolean/Number} autoStart
     * Specify `true` to defer trigger start by 1000 ms.
     * Specify a Number for the number of milliseconds to defer trigger start.
     */
    autoStart: false,
    /**
     * @cfg {Ext.dom.Element/HTMLElement/String} el
     * The target element or ID of the element on which the DragTracker will be initialized.
     */
    /**
     * @cfg {String} delegate
     * A CSS selector which identifies child elements within the DragTracker's encapsulating
     * Element which are the tracked elements. This limits tracking to only begin when the matching
     * elements are mousedowned.
     *
     * This may also be a specific child element within the DragTracker's encapsulating element
     * to use as the tracked element.
     */
    /**
     * @cfg {Boolean} [preventDefault=true]
     * Specify `false` to enable default actions on onMouseDown events.
     */
    /**
     * @cfg {Boolean} [stopEvent=false]
     * Specify `true` to stop the `mousedown` event from bubbling to outer listeners from the target
     * element (or its delegates).
     */
    /**
     * @event mouseover
     * Fires when the mouse enters the DragTracker's target element (or if {@link #delegate} is
     * used, when the mouse enters a delegate element).
     *
     * **Only available when {@link #trackOver} is `true`**
     *
     * @param {Object} this
     * @param {Object} e event object
     * @param {HTMLElement} target The element mouseovered.
     */
    /**
     * @event mouseout
     * Fires when the mouse exits the DragTracker's target element (or if {@link #delegate} is
     * used, when the mouse exits a delegate element).
     * 
     * **Only available when {@link #trackOver} is `true`**
     *
     * @param {Object} this
     * @param {Object} e event object
     */
    /**
     * @event mousedown
     * Fires when the mouse button is pressed down, but before a drag operation begins. The
     * drag operation begins after either the mouse has been moved by {@link #tolerance} pixels,
     * or after the {@link #autoStart} timer fires.
     *
     * Return `false` to veto the drag operation.
     *
     * @param {Object} this
     * @param {Object} e event object
     */
    /**
     * @event mouseup
     * @param {Object} this
     * @param {Object} e event object
     */
    /**
     * @event mousemove
     * Fired when the mouse is moved. Returning false cancels the drag operation.
     * @param {Object} this
     * @param {Object} e event object
     */
    /**
     * @event beforedragstart
     * @param {Object} this
     * @param {Object} e event object
     */
    /**
     * @event dragstart
     * @param {Object} this
     * @param {Object} e event object
     */
    /**
     * @event dragend
     * @param {Object} this
     * @param {Object} e event object
     */
    /**
     * @event drag
     * @param {Object} this
     * @param {Object} e event object
     */
    constructor: function(config) {
        var me = this;
        Ext.apply(me, config);
        me.dragRegion = new Ext.util.Region(0, 0, 0, 0);
        if (me.el) {
            me.initEl(me.el);
        }
        // Dont pass the config so that it is not applied to 'this' again
        me.mixins.observable.constructor.call(me);
        if (me.disabled) {
            me.disable();
        }
        if (Ext.supports.Touch) {
            Ext.getWin().on({
                touchstart: 'onWindowTouchStart',
                scope: me,
                capture: true
            });
        }
    },
    /**
     * Initializes the DragTracker on a given element.
     * @param {Ext.dom.Element/HTMLElement/String} el The element or element ID
     */
    initEl: function(el) {
        var me = this,
            delegate = me.delegate,
            elCmp, touchScrollable, unselectable;
        me.el = el = Ext.get(el);
        // Disable drag to select. We must take over any drag selecting gestures.
        // The delegate option may also be an element on which to listen
        if (delegate) {
            if (delegate.isElement) {
                me.handle = delegate;
                unselectable = delegate;
            }
        } else {
            unselectable = el;
        }
        // Only make the element unselectable if we have a known delegate, or this item
        // is to be dragged. Otherwise it's too wide of a net to cast, callers will need
        // to apply unselectable to the appropriate delegated elements to get the same effect.
        if (unselectable) {
            unselectable.addCls(Ext.baseCSSPrefix + 'unselectable');
        }
        // If delegate specified an actual element to listen on, we do not use
        // the delegate listener option
        me.delegate = me.handle ? undefined : me.delegate;
        // See if the handle or delegates are inside the scrolling part of the component.
        // If they are, we will need to use longpress to trigger the dragstart.
        if (Ext.supports.Touch) {
            elCmp = Ext.Component.from(el);
            touchScrollable = elCmp && elCmp.getScrollable();
            if (touchScrollable) {
                elCmp = touchScrollable.getElement();
                if (me.handle && !elCmp.contains(me.handle)) {
                    touchScrollable = false;
                } else if (me.delegate && !elCmp.down(me.delegate)) {
                    touchScrollable = false;
                } else {
                    touchScrollable = touchScrollable.getX() || touchScrollable.getY();
                }
            }
        }
        if (!me.handle) {
            me.handle = el;
        }
        // Add a mousedown listener which reacts only on the elements targeted
        // by the delegate config.
        // We process mousedown to begin tracking.
        me.handleListeners = {
            scope: me,
            delegate: me.delegate,
            dragstart: me.onDragStart
        };
        // If the element is part of a component which is scrollable by touch
        // then we have to use a longpress to trigger drag.
        // In this case, we also use untranslated mousedown because of multi input platforms.
        if (touchScrollable) {
            me.handleListeners.longpress = me.onMouseDown;
            me.handleListeners.mousedown = {
                fn: me.onMouseDown,
                delegate: me.delegate,
                translate: false
            };
            me.handleListeners.contextmenu = function(e) {
                e.stopEvent();
            };
        } else {
            me.handleListeners.mousedown = me.onMouseDown;
        }
        // If configured to do so, track mouse entry and exit into the target (or delegate).
        // The mouseover and mouseout CANNOT be replaced with mouseenter and mouseleave
        // because delegate cannot work with those pseudoevents. Entry/exit checking is done
        // in the handler.
        if (!Ext.supports.TouchEvents && (me.trackOver || me.overCls)) {
            Ext.apply(me.handleListeners, {
                mouseover: me.onMouseOver,
                mouseout: me.onMouseOut
            });
        }
        me.mon(me.handle, me.handleListeners);
        // Accessibility
        me.keyNav = new Ext.util.KeyNav({
            target: el,
            up: me.onResizeKeyDown,
            left: me.onResizeKeyDown,
            right: me.onResizeKeyDown,
            down: me.onResizeKeyDown,
            scope: me
        });
    },
    disable: function() {
        this.disabled = true;
    },
    enable: function() {
        this.disabled = false;
    },
    destroy: function() {
        // endDrag has a mandatory event parameter
        this.endDrag({});
        Ext.destroy(this.keyNav);
        this.callParent();
    },
    onWindowTouchStart: function(e) {
        if (this.mouseIsDown) {
            // on devices that support multi-touch the second touch terminates drag
            this.onMouseUp(e);
        }
    },
    // When the pointer enters a tracking element, fire a mouseover if the mouse entered
    // from outside. This is mouseenter functionality, but we cannot use mouseenter because
    // we are using "delegate" to filter mouse targets
    onMouseOver: function(e, target) {
        var me = this,
            handleCls, el, i, len, cls;
        if (!me.disabled) {
            // Note that usually `delegate` is the same as `handleCls` just with a preceding '.'
            // Also, we're now adding the classes directly to the resizer el rather than to
            // an ancestor since this caused unwanted scrollbar flickering in IE 9 and less
            // (both quirks and standards) when the panel contained a textarea with auto overflow.
            // It would cause an unwanted recalc as the ancestor had classes added and removed.
            // See EXTJS-11673.
            if (e.within(e.target, true, true) || me.delegate) {
                handleCls = me.handleCls;
                me.mouseIsOut = false;
                if (handleCls) {
                    for (i = 0 , len = me.handleEls.length; i < len; i++) {
                        el = me.handleEls[i];
                        cls = el.delegateCls;
                        if (!cls) {
                            cls = el.delegateCls = [
                                handleCls,
                                '-',
                                el.region,
                                '-over'
                            ].join('');
                        }
                        el.addCls([
                            cls,
                            me.overCls
                        ]);
                    }
                }
                me.fireEvent('mouseover', me, e, me.delegate ? e.getTarget(me.delegate, target) : me.handle);
            }
        }
    },
    // When the pointer exits a tracking element, fire a mouseout.
    // This is mouseleave functionality, but we cannot use mouseleave because we are using
    // "delegate" to filter mouse targets
    onMouseOut: function(e) {
        var me = this,
            el, i, len;
        if (me.mouseIsDown) {
            me.mouseIsOut = true;
        } else {
            if (me.handleCls) {
                for (i = 0 , len = me.handleEls.length; i < len; i++) {
                    el = me.handleEls[i];
                    el.removeCls([
                        el.delegateCls,
                        me.overCls
                    ]);
                }
            }
            me.fireEvent('mouseout', me, e);
        }
    },
    onMouseDown: function(e, target) {
        var me = this,
            // If this is a translated event, the event object is chained, so
            // we need to track on the parentEvent if it exists.
            trackEvent = e.parentEvent || e;
        // Ignore all mousedown events that were not started by the primary button
        // If this is disabled, or the mousedown has been processed by an upstream DragTracker,
        // return
        if (e.button || me.disabled || trackEvent.dragTracked) {
            return;
        }
        // This information should be available in mousedown listener and onBeforeStart
        // implementations
        me.dragTarget = me.delegate ? target : me.handle.dom;
        me.startXY = me.lastXY = e.getXY();
        me.startRegion = Ext.fly(me.dragTarget).getRegion();
        if (me.fireEvent('mousedown', me, e) === false || me.fireEvent('beforedragstart', me, e) === false || me.onBeforeStart(e) === false) {
            return;
        }
        // Track when the mouse is down so that mouseouts while the mouse is down are not processed.
        // The onMouseOut method will only ever be called after mouseup.
        me.mouseIsDown = true;
        // Flag for downstream DragTracker instances that the mouse is being tracked.
        trackEvent.dragTracked = true;
        // See Ext.dd.DragDropManager::handleMouseDown
        me.el.setCapture();
        e.stopPropagation();
        if (me.preventDefault !== false || e.pointerType === 'touch') {
            e.preventDefault();
        }
        Ext.getDoc().on({
            scope: me,
            capture: true,
            mouseup: me.onMouseUp,
            mousemove: me.onMouseMove,
            selectstart: me.stopSelect
        });
        // Flag for the onMouseMove method.
        // If endDrag is called while active via some other code such as a timer, or key event
        // then it sets dragEnded to indicate to any subsequent mousemove event that
        // it should not proceed.
        me.dragEnded = false;
        if (!me.tolerance) {
            me.triggerStart();
        } else if (me.autoStart) {
            me.timer = Ext.defer(me.triggerStart, me.autoStart === true ? 1000 : me.autoStart, me, [
                e
            ]);
        }
    },
    onMouseMove: function(e, target) {
        var me = this,
            xy = e.getXY(),
            s = me.startXY;
        e.stopPropagation();
        if (me.preventDefault !== false) {
            e.preventDefault();
        }
        // If, during a drag, some other action (eg a keystroke) hides or destroys the target,
        // endDrag will be called and the mousemove listener removed. But is the mouse is down
        // events continue to be delivered to the handler. If this happens, active will be false
        // here.
        if (me.dragEnded) {
            return;
        }
        me.lastXY = xy;
        if (!me.active) {
            if (Math.max(Math.abs(s[0] - xy[0]), Math.abs(s[1] - xy[1])) > me.tolerance) {
                me.triggerStart(e);
            } else {
                return;
            }
        }
        // Returning false from a mousemove listener deactivates
        if (me.fireEvent('mousemove', me, e) === false) {
            me.onMouseUp(e);
        } else {
            me.onDrag(e);
            me.fireEvent('drag', me, e);
        }
    },
    onMouseUp: function(e) {
        var me = this;
        // Clear the flag which ensures onMouseOut fires only after the mouse button
        // is lifted if the mouseout happens *during* a drag.
        me.mouseIsDown = false;
        // If we mouseouted the el *during* the drag, the onMouseOut method will not have fired.
        // Ensure that it gets processed.
        if (me.mouseIsOut) {
            me.mouseIsOut = false;
            me.onMouseOut(e);
        }
        if (me.preventDefault !== false) {
            e.preventDefault();
        }
        // See Ext.dd.DragDropManager::handleMouseDown
        if (Ext.isIE && document.releaseCapture) {
            document.releaseCapture();
        }
        me.fireEvent('mouseup', me, e);
        me.endDrag(e);
    },
    /**
     * @private
     * Stop the drag operation, and remove active mouse listeners.
     */
    endDrag: function(e) {
        var me = this,
            wasActive = me.active;
        Ext.getDoc().un({
            mousemove: me.onMouseMove,
            mouseup: me.onMouseUp,
            selectstart: me.stopSelect,
            capture: true,
            scope: me
        });
        me.clearStart();
        me.active = false;
        me.dragEnded = true;
        if (wasActive) {
            me.onEnd(e);
            me.fireEvent('dragend', me, e);
        } else {
            me.onCancel(e);
        }
        // Private property calculated when first required and only cached during a drag
        me._constrainRegion = null;
    },
    triggerStart: function(e) {
        var me = this;
        me.clearStart();
        me.active = true;
        me.onStart(e);
        me.fireEvent('dragstart', me, e);
    },
    clearStart: function() {
        var timer = this.timer;
        if (timer) {
            Ext.undefer(timer);
            this.timer = null;
        }
    },
    stopSelect: function(e) {
        e.stopEvent();
        return false;
    },
    /**
     * Template method which should be overridden by each DragTracker instance. Called when the user
     * first clicks and holds the mouse button down. Return false to disallow the drag
     * @param {Ext.event.Event} e The event object
     * @template
     */
    onBeforeStart: function(e) {},
    /**
     * Template method which should be overridden by each DragTracker instance. Called when a drag
     * operation starts (e.g. the user has moved the tracked element beyond the specified tolerance)
     * @param {Ext.event.Event} e The event object
     * @template
     */
    onStart: function(e) {},
    /**
     * Template method which should be overridden by each DragTracker instance. Called whenever
     * a drag has been detected.
     * @param {Ext.event.Event} e The event object
     * @template
     */
    onDrag: function(e) {},
    /**
     * Template method which mey be overridden by each DragTracker instance. Called when a mouseup
     * gesture is detected but the onStart has not yet been reached. To clear things up
     * that may have been set up on {@link #onBeforeStart}.
     * @param {Ext.event.Event} e The event object
     * @template
     */
    onCancel: function(e) {},
    /**
     * Template method which should be overridden by each DragTracker instance. Called when a drag
     * operation has been completed (e.g. the user clicked and held the mouse down, dragged
     * the element and then released the mouse button)
     * @param {Ext.event.Event} e The event object
     * @template
     */
    onEnd: function(e) {},
    /**
     * Returns the drag target. This is usually the DragTracker's encapsulating element.
     *
     * If the {@link #delegate} option is being used, this may be a child element which matches the
     * {@link #delegate} selector.
     *
     * @return {Ext.dom.Element} The element currently being tracked.
     */
    getDragTarget: function() {
        return this.dragTarget;
    },
    /**
     * @private
     * @return {Ext.dom.Element} The DragTracker's encapsulating element.
     */
    getDragCt: function() {
        return this.el;
    },
    /**
     * @private
     * Return the Region into which the drag operation is constrained.
     * Either the XY pointer itself can be constrained, or the dragTarget element
     * The private property _constrainRegion is cached until onMouseUp
     */
    getConstrainRegion: function() {
        var me = this;
        if (me.constrainTo) {
            if (me.constrainTo instanceof Ext.util.Region) {
                return me.constrainTo;
            }
            if (!me._constrainRegion) {
                me._constrainRegion = Ext.fly(me.constrainTo).getViewRegion();
            }
        } else {
            if (!me._constrainRegion) {
                me._constrainRegion = me.getDragCt().getViewRegion();
            }
        }
        return me._constrainRegion;
    },
    getXY: function(constrain) {
        return constrain ? this.constrainModes[constrain](this, this.lastXY) : this.lastXY;
    },
    /**
     * Returns the X, Y offset of the current mouse position from the mousedown point.
     *
     * This method may optionally constrain the real offset values, and returns a point coerced
     * in one of two modes:
     *
     *  - `point` The current mouse position is coerced into the constrainRegion and the resulting
     * position is returned.
     *  - `dragTarget` The new {@link Ext.util.Region Region} of the
     * {@link #getDragTarget dragTarget} is calculated based upon the current mouse position,
     * and then coerced into the constrainRegion. The returned mouse position is then adjusted
     * by the same delta as was used to coerce the region.
     *
     * @param {String} constrain (Optional) If omitted the true mouse position is returned.
     * May be passed as `point` or `dragTarget`. See above.
     * @return {Number[]} The `X, Y` offset from the mousedown point, optionally constrained.
     */
    getOffset: function(constrain) {
        var xy = this.getXY(constrain),
            s = this.startXY;
        return [
            xy[0] - s[0],
            xy[1] - s[1]
        ];
    },
    onDragStart: function(e) {
        e.stopPropagation();
    },
    constrainModes: {
        // Constrain the passed point to within the constrain region
        point: function(me, xy) {
            var dr = me.dragRegion,
                constrainTo = me.getConstrainRegion();
            // No constraint
            if (!constrainTo) {
                return xy;
            }
            dr.x = dr.left = dr[0] = dr.right = xy[0];
            dr.y = dr.top = dr[1] = dr.bottom = xy[1];
            dr.constrainTo(constrainTo);
            return [
                dr.left,
                dr.top
            ];
        },
        // Constrain the dragTarget to within the constrain region. Return the passed xy
        // adjusted by the same delta.
        dragTarget: function(me, xy) {
            var s = me.startXY,
                dr = me.startRegion.copy(),
                constrainTo = me.getConstrainRegion(),
                adjust;
            // No constraint
            if (!constrainTo) {
                return xy;
            }
            // See where the passed XY would put the dragTarget if translated by the unconstrained
            // offset. If it overflows, we constrain the passed XY to bring the potential
            // region back within the boundary.
            dr.translateBy(xy[0] - s[0], xy[1] - s[1]);
            // Constrain the X coordinate by however much the dragTarget overflows
            if (dr.right > constrainTo.right) {
                xy[0] += adjust = (constrainTo.right - dr.right);
                // overflowed the right
                dr.left += adjust;
            }
            if (dr.left < constrainTo.left) {
                xy[0] += (constrainTo.left - dr.left);
            }
            // overflowed the left
            // Constrain the Y coordinate by however much the dragTarget overflows
            if (dr.bottom > constrainTo.bottom) {
                xy[1] += adjust = (constrainTo.bottom - dr.bottom);
                // overflowed the bottom
                dr.top += adjust;
            }
            if (dr.top < constrainTo.top) {
                xy[1] += (constrainTo.top - dr.top);
            }
            // overflowed the top
            return xy;
        }
    }
});

/**
 * Private utility class for Ext.Splitter.
 * @private
 */
Ext.define('Ext.resizer.SplitterTracker', {
    extend: 'Ext.dd.DragTracker',
    requires: [
        'Ext.util.Region'
    ],
    enabled: true,
    overlayCls: Ext.baseCSSPrefix + 'resizable-overlay',
    createDragOverlay: function() {
        var overlay,
            El = Ext.dom.Element;
        overlay = this.overlay = Ext.getBody().createChild({
            role: 'presentation',
            cls: this.overlayCls,
            html: '&#160;'
        });
        overlay.unselectable();
        overlay.setSize(El.getDocumentWidth(), El.getDocumentHeight());
        overlay.show();
    },
    getPrevCmp: function() {
        var splitter = this.getSplitter();
        return splitter.previousSibling(':not([hidden])');
    },
    getNextCmp: function() {
        var splitter = this.getSplitter();
        return splitter.nextSibling(':not([hidden])');
    },
    // ensure the tracker is enabled, store boxes of previous and next
    // components and calculate the constrain region
    onBeforeStart: function(e) {
        var me = this,
            prevCmp = me.getPrevCmp(),
            nextCmp = me.getNextCmp(),
            collapseEl = me.getSplitter().collapseEl,
            target = e.getTarget(),
            box;
        if (!prevCmp || !nextCmp) {
            return false;
        }
        if (collapseEl && target === collapseEl.dom) {
            return false;
        }
        // SplitterTracker is disabled if any of its adjacents are collapsed.
        if (nextCmp.collapsed || prevCmp.collapsed) {
            return false;
        }
        // store boxes of previous and next
        me.prevBox = prevCmp.getEl().getBox();
        me.nextBox = nextCmp.getEl().getBox();
        me.constrainTo = box = me.calculateConstrainRegion();
        if (!box) {
            return false;
        }
        return box;
    },
    onMouseDown: function(e, target) {
        // Logic to resize components. If this splitter has an iframe on either side, 
        // dragging over it will make us not detect startDrag, so we need to cover all iframes
        // and this has to be done before triggerStart or onStart. We cannot do this in
        // general (meaning adding this to Ext.dd.DragTracker) because other draggable things
        // cannot  assume that mouseDown is safe for this purpose.
        // In particular ComponentDragger on a maximizable window will get tricked by the maximize
        // button onMouseDown and mask everything but will never get the onMouseUp
        // to unmask the iframes.
        this.callParent([
            e,
            target
        ]);
        if (this.mouseIsDown && this.getSplitter().el.dom === target) {
            Ext.dom.Element.maskIframes();
        }
    },
    onMouseUp: function(e) {
        this.callParent([
            e
        ]);
        Ext.dom.Element.unmaskIframes();
    },
    // We move the splitter el. Add the proxy class.
    onStart: function(e) {
        var splitter = this.getSplitter();
        this.createDragOverlay();
        splitter.addCls(splitter.baseCls + '-active');
    },
    onResizeKeyDown: function(e) {
        var me = this,
            splitter = me.getSplitter(),
            key = e.getKey(),
            incrIdx = splitter.orientation === 'vertical' ? 0 : 1,
            incr = key === e.UP || key === e.LEFT ? -1 : 1,
            easing;
        if (!me.active && me.onBeforeStart(e)) {
            Ext.fly(e.target).on('keyup', me.onResizeKeyUp, me);
            me.triggerStart(e);
            me.onMouseDown(e);
            me.startXY = splitter.getXY();
            me.lastKeyDownXY = Ext.Array.slice(me.startXY);
            // Movement increment eases to 4 over two seconds.
            easing = me.easing = new Ext.fx.easing.Linear();
            easing.setStartTime(Ext.Date.now());
            easing.setStartValue(1);
            easing.setEndValue(4);
            easing.setDuration(2000);
        }
        if (me.active) {
            me.lastKeyDownXY[incrIdx] = Math.round(me.lastKeyDownXY[incrIdx] + (incr * me.easing.getValue()));
            me.lastXY = me.lastKeyDownXY;
            splitter.setXY(me.getXY('dragTarget'));
        }
    },
    onResizeKeyUp: function(e) {
        this.onMouseUp(e);
    },
    // calculate the constrain Region in which the splitter el may be moved.
    calculateConstrainRegion: function() {
        var me = this,
            splitter = me.getSplitter(),
            splitWidth = splitter.getWidth(),
            defaultMin = splitter.defaultSplitMin,
            orient = splitter.orientation,
            prevBox = me.prevBox,
            prevCmp = me.getPrevCmp(),
            nextBox = me.nextBox,
            nextCmp = me.getNextCmp(),
            // prev and nextConstrainRegions are the maximumBoxes minus the
            // minimumBoxes. The result is always the intersection
            // of these two boxes.
            prevConstrainRegion, nextConstrainRegion, constrainOptions;
        // vertical splitters, so resizing left to right
        if (orient === 'vertical') {
            constrainOptions = {
                prevCmp: prevCmp,
                nextCmp: nextCmp,
                prevBox: prevBox,
                nextBox: nextBox,
                defaultMin: defaultMin,
                splitWidth: splitWidth
            };
            // Region constructor accepts (top, right, bottom, left)
            // anchored/calculated from the left
            prevConstrainRegion = new Ext.util.Region(prevBox.y, me.getVertPrevConstrainRight(constrainOptions), prevBox.bottom, me.getVertPrevConstrainLeft(constrainOptions));
            // anchored/calculated from the right
            nextConstrainRegion = new Ext.util.Region(nextBox.y, me.getVertNextConstrainRight(constrainOptions), nextBox.bottom, me.getVertNextConstrainLeft(constrainOptions));
        } else {
            // anchored/calculated from the top
            prevConstrainRegion = new Ext.util.Region(prevBox.y + (prevCmp.minHeight || defaultMin), prevBox.right, // Bottom boundary is y + maxHeight if there IS a maxHeight.
            // Otherwise it is calculated based upon the minWidth of the next Component
            // eslint-disable-next-line max-len
            (prevCmp.maxHeight ? prevBox.y + prevCmp.maxHeight : nextBox.bottom - (nextCmp.minHeight || defaultMin)) + splitWidth, prevBox.x);
            // anchored/calculated from the bottom
            nextConstrainRegion = new Ext.util.Region(// Top boundary is bottom - maxHeight if there IS a maxHeight.
            // Otherwise it is calculated based upon the minHeight of the previous Component
            // eslint-disable-next-line max-len
            (nextCmp.maxHeight ? nextBox.bottom - nextCmp.maxHeight : prevBox.y + (prevCmp.minHeight || defaultMin)) - splitWidth, nextBox.right, nextBox.bottom - (nextCmp.minHeight || defaultMin), nextBox.x);
        }
        // intersection of the two regions to provide region draggable
        return prevConstrainRegion.intersect(nextConstrainRegion);
    },
    // Performs the actual resizing of the previous and next components
    performResize: function(e, offset) {
        var me = this,
            splitter = me.getSplitter(),
            orient = splitter.orientation,
            prevCmp = me.getPrevCmp(),
            nextCmp = me.getNextCmp(),
            owner = splitter.ownerCt,
            flexedSiblings = owner.query('>[flex]'),
            len = flexedSiblings.length,
            vertical = orient === 'vertical',
            i = 0,
            dimension = vertical ? 'width' : 'height',
            item, size;
        // Convert flexes to pixel values proportional to the total pixel width of all flexes.
        for (; i < len; i++) {
            item = flexedSiblings[i];
            size = vertical ? item.getWidth() : item.getHeight();
            item.flex = size;
        }
        offset = vertical ? offset[0] : offset[1];
        if (prevCmp) {
            size = me.prevBox[dimension] + offset;
            if (prevCmp.flex) {
                prevCmp.flex = size;
            } else {
                prevCmp[dimension] = size;
            }
        }
        if (nextCmp) {
            size = me.nextBox[dimension] - offset;
            if (nextCmp.flex) {
                nextCmp.flex = size;
            } else {
                nextCmp[dimension] = size;
            }
        }
        owner.updateLayout();
    },
    // Cleans up the overlay (if we have one) and calls the base. This cannot be done in
    // onEnd, because onEnd is only called if a drag is detected but the overlay is created
    // regardless (by onBeforeStart).
    endDrag: function() {
        var me = this;
        if (me.overlay) {
            me.overlay.destroy();
            delete me.overlay;
        }
        me.callParent(arguments);
    },
    // this calls onEnd
    // perform the resize and remove the proxy class from the splitter el
    onEnd: function(e) {
        var me = this,
            splitter = me.getSplitter();
        splitter.removeCls(splitter.baseCls + '-active');
        me.performResize(e, me.getResizeOffset());
    },
    // Track the proxy and set the proper XY coordinates
    // while constraining the drag
    onDrag: function(e) {
        var me = this,
            offset = me.getOffset('dragTarget'),
            splitter = me.getSplitter(),
            splitEl = splitter.getEl(),
            orient = splitter.orientation;
        if (orient === "vertical") {
            splitEl.setX(me.startRegion.left + offset[0]);
        } else {
            splitEl.setY(me.startRegion.top + offset[1]);
        }
    },
    getSplitter: function() {
        return this.splitter;
    },
    getVertPrevConstrainRight: function(o) {
        // Right boundary is x + maxWidth if there IS a maxWidth.
        // Otherwise it is calculated based upon the minWidth of the next Component
        return (o.prevCmp.maxWidth ? o.prevBox.x + o.prevCmp.maxWidth : o.nextBox.right - (o.nextCmp.minWidth || o.defaultMin)) + o.splitWidth;
    },
    getVertPrevConstrainLeft: function(o) {
        return o.prevBox.x + (o.prevCmp.minWidth || o.defaultMin);
    },
    getVertNextConstrainRight: function(o) {
        return o.nextBox.right - (o.nextCmp.minWidth || o.defaultMin);
    },
    getVertNextConstrainLeft: function(o) {
        // Left boundary is right - maxWidth if there IS a maxWidth.
        // Otherwise it is calculated based upon the minWidth of the previous Component
        return (o.nextCmp.maxWidth ? o.nextBox.right - o.nextCmp.maxWidth : o.prevBox.x + (o.prevBox.minWidth || o.defaultMin)) - o.splitWidth;
    },
    getResizeOffset: function() {
        return this.getOffset('dragTarget');
    }
});

/**
 */
Ext.define('Ext.layout.container.ColumnSplitterTracker', {
    extend: 'Ext.resizer.SplitterTracker',
    // We move the splitter el. Add the proxy class.
    onStart: function(e) {
        Ext.apply(this.getSplitter().el.dom.style, {
            top: 0,
            left: 0
        });
        this.callParent(arguments);
    },
    endDrag: function() {
        var me = this;
        me.callParent(arguments);
        // this calls onEnd
        me.getSplitter().el.dom.style.left = 0;
    },
    performResize: function(e, offset) {
        var me = this,
            prevCmp = me.getPrevCmp(),
            nextCmp = me.getNextCmp(),
            splitter = me.getSplitter(),
            owner = splitter.ownerCt,
            delta = offset[0],
            prevWidth, nextWidth, ratio;
        if (prevCmp && nextCmp) {
            prevCmp.width = prevWidth = me.prevBox.width + delta;
            nextCmp.width = nextWidth = me.nextBox.width - delta;
            ratio = (prevCmp.columnWidth + nextCmp.columnWidth) / (prevWidth + nextWidth);
            prevCmp.columnWidth = prevWidth * ratio;
            nextCmp.columnWidth = nextWidth * ratio;
        }
        owner.updateLayout();
    }
});

/**
 */
Ext.define('Ext.layout.container.ColumnSplitter', {
    extend: 'Ext.resizer.Splitter',
    xtype: 'columnsplitter',
    requires: [
        'Ext.layout.container.ColumnSplitterTracker'
    ],
    isSplitter: true,
    synthetic: true,
    cls: Ext.baseCSSPrefix + 'splitter-vertical',
    orientation: 'vertical',
    collapseDirection: 'left',
    trackerClass: 'Ext.layout.container.ColumnSplitterTracker',
    width: 7,
    height: 1,
    getTrackerConfig: function() {
        var tracker = this.callParent();
        tracker.xclass = this.trackerClass;
        return tracker;
    }
});

/**
 * This layout extends `Ext.layout.container.Column` and adds splitters between adjacent
 * columns allowing the user to resize them.
 * @private
 */
Ext.define('Ext.layout.container.Dashboard', {
    extend: 'Ext.layout.container.Column',
    alias: 'layout.dashboard',
    requires: [
        'Ext.layout.container.ColumnSplitter'
    ],
    type: 'dashboard',
    firstColumnCls: Ext.baseCSSPrefix + 'dashboard-column-first',
    lastColumnCls: Ext.baseCSSPrefix + 'dashboard-column-last',
    /*
     * The geometry of a Column layout with splitters between respective items:
     *
     *             0        1      2       3        4
     *      +-----------------------------------------------+
     *      | +-----------+ || +---------+ || +-----------+ | \
     *      | |           | || |         | || |           | |  \
     *      | |           | || |         | || |           | |   \
     *      | |           | || |         | || |           | |    \
     *      | +-----------+ || |         | || |           | |   row[0]
     *      |               || |         | || |           | |    /
     *      |               || |         | || |           | |   /
     *      |               || |         | || +-----------+ |  /
     *      |               || |         | ||               | /
     *      |               || +---------+ ||               |
     *      | +-------------------+ || +------------------+ | \
     *      | |                   | || |                  | |  \
     *      | |                   | || |                  | |   \
     *      | |                   | || |                  | |  row[1]
     *      | |                   | || |                  | |   /
     *      | |                   | || +------------------+ |  /
     *      | +-------------------+ ||                      | /
     *      +-----------------------------------------------+
     *                  6           7            8
     *
     * The splitter between 4 and 6 will be hidden but still present in the items. It is
     * considered part of row[0].
     */
    getSplitterConfig: function() {
        return {
            xtype: 'columnsplitter'
        };
    },
    /**
     * @private
     * Returns a filtered item list sans splitters
     * @param items
     * @return {Array|*}
     */
    getColumns: function(items) {
        var array = Ext.Array;
        return array.filter(array.from(items), function(item) {
            return item.target && item.target.isSplitter !== true;
        });
    },
    beginLayout: function(ownerContext) {
        var me = this;
        me.callParent([
            ownerContext
        ]);
        // We need to reset the heights of the splitters so that they don't influence the
        // layout (mostly overflow management).
        // eslint-disable-next-line vars-on-top, one-var
        var childItems = ownerContext.childItems,
            rows = (ownerContext.rows = []),
            length = childItems.length,
            totalWidth = 2,
            columnTargets = 0,
            lastRow = 0,
            maxColumns = me.owner.getMaxColumns(),
            child, i, prev, row, splitter, target, width;
        for (i = 0; i < length; ++i) {
            target = (child = childItems[i]).target;
            splitter = target && target.isSplitter;
            columnTargets += (splitter ? 0 : 1);
            width = splitter ? 0 : target.columnWidth || 1;
            if (totalWidth + width > 1 || (maxColumns && (columnTargets > maxColumns))) {
                if (prev) {
                    // We have wrapped and we have a previous item which is a splitter by
                    // definition. We have previously seen that splitter and setHeight(0)
                    // on it. We now setHeight(0) to effectively hide it.
                    prev.orphan = 1;
                    prev.el.setHeight(0);
                }
                totalWidth = 0;
                columnTargets = 1;
                if (rows.length) {
                    // We have encountered a row break condition
                    // As this is floating layout, classify the current row
                    // before proceeding
                    lastRow = rows.length - 1;
                    me.syncFirstLast(me.getColumns(rows[lastRow].items));
                }
                rows.push(row = {
                    index: rows.length,
                    items: [],
                    maxHeight: 0
                });
            }
            totalWidth += width;
            row.items.push(child);
            child.row = row;
            target.rowIndex = row.index;
            if (splitter) {
                child.el.setHeight(1);
            }
            prev = child;
        }
        if (rows.length) {
            me.syncFirstLast(me.getColumns(rows[rows.length - 1].items));
        }
    },
    beforeLayoutCycle: function(ownerContext) {
        var me = this,
            items = me.owner.items;
        // We need to do this in beforeLayoutCycle because this changes the child items
        // and hence needs to be considered before recursing.
        if (me.splitterGen !== items.generation) {
            me.syncSplitters();
            // The syncSplitters call will change items.generation so do this last.
            me.splitterGen = items.generation;
        }
        me.callParent(arguments);
    },
    finishedLayout: function(ownerContext) {
        var items = ownerContext.childItems,
            len = items.length,
            box, child, i, target, row;
        this.callParent([
            ownerContext
        ]);
        for (i = 0; i < len; i += 2) {
            target = (child = items[i]).target;
            box = target.lastBox;
            row = child.row;
            row.maxHeight = Math.max(row.maxHeight, box.height);
            // Put this on the component so that it gets saved (we use this to fix up
            // columnWidth on restore)
            target.width = box.width;
        }
        for (i = 0; i < len; i++) {
            target = (child = items[i]).target;
            if (!child.orphan) {
                if (i % 2 === 0) {
                    // Set min height of column to the max height
                    // So that it can increase on adding placeholder
                    target.el.setMinHeight(child.row.maxHeight);
                } else {
                    // Set height for splitter
                    target.el.setHeight(child.row.maxHeight);
                }
            } else {
                // since this is an orphan child, set its width to 0
                target.el.setWidth(0);
            }
        }
    },
    /**
     * This method synchronizes the splitters so that we have exactly one between each
     * column.
     * @private
     */
    syncSplitters: function() {
        var me = this,
            owner = me.owner,
            items = owner.items.items,
            index = items.length,
            ok = true,
            shouldBeSplitter = false,
            item, splitter;
        // eslint-disable-line no-unused-vars
        // Walk backwards over the items so that an insertion index is stable.
        while (index-- > 0) {
            item = items[index];
            if (shouldBeSplitter) {
                if (item.isSplitter) {
                    shouldBeSplitter = false;
                } else {
                    // An item is adjacent to an item, so inject a splitter beyond
                    // the current item to separate the columns. Keep shouldBeSplitter
                    // at true since we just encountered an item.
                    if (ok) {
                        ok = false;
                        owner.suspendLayouts();
                    }
                    splitter = owner.add(index + 1, me.getSplitterConfig());
                }
            } else {
                if (item.isSplitter) {
                    // A splitter is adjacent to a splitter so we remove this one. We
                    // leave shouldBeSplitter at false because the next thing we see
                    // should still not be a splitter.
                    if (ok) {
                        ok = false;
                        owner.suspendLayouts();
                    }
                    owner.remove(item);
                } else {
                    shouldBeSplitter = true;
                }
            }
        }
        // It is possible to exit the above with a splitter as the first item, but
        // this is invalid so remove any such splitters.
        while (items.length && (item = items[0]).isSplitter) {
            if (ok) {
                ok = false;
                owner.suspendLayouts();
            }
            owner.remove(item);
        }
        if (!ok) {
            owner.resumeLayouts();
        }
    },
    syncFirstLast: function(items) {
        var me = this,
            firstCls = me.firstColumnCls,
            lastCls = me.lastColumnCls,
            len,
            firstAndLast = [
                firstCls,
                lastCls
            ],
            i, item, last;
        items = Ext.Array.from(items);
        len = items.length;
        for (i = 0; i < len; ++i) {
            item = items[i].target;
            last = (i === len - 1);
            if (!i) {
                // if (first)
                if (last) {
                    item.addCls(firstAndLast);
                } else {
                    item.addCls(firstCls);
                    item.removeCls(lastCls);
                }
            } else if (last) {
                item.addCls(lastCls);
                item.removeCls(firstCls);
            } else {
                item.removeCls(firstAndLast);
            }
        }
    },
    calculateItemSizeWithContent: function(availableWidth, contentWidth, items) {
        var itemMarginWidth, itemContext,
            splitterItemWidth = 0,
            halfSplitterItemWidth = 0,
            itemWidth, i,
            len = items.length,
            rowindex, rowLen;
        availableWidth = (availableWidth < contentWidth) ? 0 : availableWidth;
        for (i = 0; i < len; i += 2) {
            itemContext = items[i];
            rowLen = itemContext.row.items.length;
            rowindex = itemContext.row.items.indexOf(itemContext);
            itemMarginWidth = itemContext.marginInfo.width;
            // always set by above loop
            itemWidth = itemContext.target.columnWidth;
            itemWidth = Math.floor(itemWidth * availableWidth) - itemMarginWidth;
            // Get the width of splitter item. We calculate the half value for width
            // since splitter must form a part of both items between which it lies equally.
            if (splitterItemWidth === 0 && (rowindex + 1 < rowLen)) {
                splitterItemWidth = items[i + 1].getProp('width');
                halfSplitterItemWidth = Math.ceil(splitterItemWidth / 2);
            }
            if (halfSplitterItemWidth) {
                // if there exists a splitter to the right and this splitter 
                // is not the last item of this row, reduce the width of the
                // column since splitter will take half width from the item
                if (rowindex + 2 < rowLen) {
                    itemWidth -= halfSplitterItemWidth;
                }
                // if there exists a splitter to the left, reduce the width 
                // of the column since splitter will take half width from the item
                if (rowindex > 0) {
                    itemWidth -= halfSplitterItemWidth;
                }
            }
            itemWidth = itemContext.setWidth(itemWidth);
            // constrains to min/maxWidth
            contentWidth += itemWidth + itemMarginWidth;
        }
        return contentWidth;
    }
});

/*
 * This is a derivative of the similarly named class in the YUI Library.
 * The original license:
 * Copyright (c) 2006, Yahoo! Inc. All rights reserved.
 * Code licensed under the BSD License:
 * http://developer.yahoo.net/yui/license.txt
 */
/**
 * A DragDrop implementation that does not move, but can be a drop
 * target.  You would get the same result by simply omitting implementation
 * for the event callbacks, but this way we reduce the processing cost of the
 * event listener and the callbacks.
 */
Ext.define('Ext.dd.DDTarget', {
    extend: 'Ext.dd.DragDrop',
    /**
     * Creates new DDTarget.
     * @param {String} id the id of the element that is a drop target
     * @param {String} sGroup the group of related DragDrop objects
     * @param {Object} config an object containing configurable attributes.
     * Valid properties for DDTarget in addition to those in DragDrop: none.
     */
    constructor: function(id, sGroup, config) {
        if (id) {
            this.initTarget(id, sGroup, config);
        }
    },
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    getDragEl: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    isValidHandleChild: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    startDrag: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    endDrag: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    onDrag: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    onDragDrop: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    onDragEnter: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    onDragOut: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    onDragOver: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    onInvalidDrop: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    onMouseDown: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    onMouseUp: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    setXConstraint: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    setYConstraint: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    resetConstraints: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    clearConstraints: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    clearTicks: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    setInitPosition: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    setDragElId: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    setHandleElId: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    setOuterHandleElId: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    addInvalidHandleClass: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    addInvalidHandleId: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    addInvalidHandleType: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    removeInvalidHandleClass: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    removeInvalidHandleId: Ext.emptyFn,
    /**
     * Overridden and disabled. A DDTarget does not support being dragged.
     * @method
     */
    removeInvalidHandleType: Ext.emptyFn,
    toString: function() {
        return ("DDTarget " + this.id);
    }
});

/**
 * Provides automatic scrolling of overflow regions in the page during drag operations.
 *
 * The ScrollManager configs will be used as the defaults for any scroll container registered
 * with it, but you can also override most of the configs per scroll container by adding a
 * ddScrollConfig object to the target element that contains these properties: {@link #hthresh},
 * {@link #vthresh}, {@link #increment} and {@link #frequency}. Example usage:
 *
 *     var el = Ext.get('scroll-ct');
 *     el.ddScrollConfig = {
 *         vthresh: 50,
 *         hthresh: -1,
 *         frequency: 100,
 *         increment: 200
 *     };
 *     Ext.dd.ScrollManager.register(el);
 *
 * Note: This class is designed to be used in "Point Mode
 */
Ext.define('Ext.dd.ScrollManager', {
    singleton: true,
    requires: [
        'Ext.dd.DragDropManager'
    ],
    /**
     * The number of pixels from the top or bottom edge of a container the pointer needs to be
     * to trigger scrolling
     */
    vthresh: 25 * (window.devicePixelRatio || 1),
    /**
     * The number of pixels from the right or left edge of a container the pointer needs to be
     * to trigger scrolling
     */
    hthresh: 25 * (window.devicePixelRatio || 1),
    /**
     * The number of pixels to scroll in each scroll increment
     */
    increment: 100,
    /**
     * The frequency of scrolls in milliseconds
     */
    frequency: 500,
    /**
     * True to animate the scroll
     */
    animate: true,
    /**
     * The animation duration in seconds - MUST BE less than Ext.dd.ScrollManager.frequency!
     */
    animDuration: 0.4,
    /**
     * @property {String} ddGroup
     * The named drag drop {@link Ext.dd.DragSource#ddGroup group} to which this container belongs.
     * If a ddGroup is specified, then container scrolling will only occur when a dragged object
     * is in the same ddGroup.
     */
    ddGroup: undefined,
    dirTrans: {
        up: -1,
        left: -1,
        down: 1,
        right: 1
    },
    constructor: function() {
        var ddm = Ext.dd.DragDropManager;
        ddm.fireEvents = Ext.Function.createSequence(ddm.fireEvents, this.onFire, this);
        ddm.stopDrag = Ext.Function.createSequence(ddm.stopDrag, this.onStop, this);
        this.doScroll = this.doScroll.bind(this);
        this.ddmInstance = ddm;
        this.els = {};
        this.dragEl = null;
        this.proc = {};
    },
    onStop: function(e) {
        var sm = Ext.dd.ScrollManager;
        sm.dragEl = null;
        sm.clearProc();
    },
    triggerRefresh: function() {
        if (this.ddmInstance.dragCurrent) {
            this.ddmInstance.refreshCache(this.ddmInstance.dragCurrent.groups);
        }
    },
    doScroll: function() {
        var me = this;
        if (me.ddmInstance.dragCurrent) {
            // eslint-disable-next-line vars-on-top, one-var
            var proc = me.proc,
                procEl = proc.el,
                scrollComponent = proc.component,
                ddScrollConfig = proc.el.ddScrollConfig,
                distance, animate, afterScroll;
            distance = ddScrollConfig && ddScrollConfig.increment ? ddScrollConfig.increment : me.increment;
            animate = ddScrollConfig && 'animate' in ddScrollConfig ? ddScrollConfig.animate : me.animate;
            afterScroll = function() {
                me.triggerRefresh();
            };
            if (animate) {
                if (animate === true) {
                    animate = {
                        callback: afterScroll
                    };
                } else {
                    animate.callback = animate.callback ? Ext.Function.createSequence(animate.callback, afterScroll) : afterScroll;
                }
            }
            // If the element is the overflow element of a Component, and we are scrolling
            // using CSS transform, then scroll using the correct method!
            if (scrollComponent) {
                // Left/right means increment has to be negated
                distance = distance * me.dirTrans[proc.dir];
                // Pass X or Y params depending upon dimension being scrolled
                if (proc.dir === 'up' || proc.dir === 'down') {
                    scrollComponent.scrollBy(0, distance, animate);
                } else {
                    scrollComponent.scrollBy(distance, 0, animate);
                }
            } else {
                procEl.scroll(proc.dir, distance, animate);
            }
            if (!animate) {
                afterScroll();
            }
        }
    },
    clearProc: function() {
        var proc = this.proc;
        if (proc.id) {
            Ext.uninterval(proc.id);
        }
        proc.id = 0;
        proc.el = null;
        proc.dir = "";
    },
    startProc: function(el, dir) {
        var me = this,
            proc = me.proc,
            group, freq;
        me.clearProc();
        proc.el = el;
        proc.dir = dir;
        group = el.ddScrollConfig ? el.ddScrollConfig.ddGroup : undefined;
        freq = (el.ddScrollConfig && el.ddScrollConfig.frequency) ? el.ddScrollConfig.frequency : me.frequency;
        if (group === undefined || me.ddmInstance.dragCurrent.ddGroup === group) {
            proc.id = Ext.interval(me.doScroll, freq);
        }
    },
    onFire: function(e, isDrop) {
        var me = this,
            pt, proc, els, id, el, elementRegion, configSource, ownerCt, scrollerOwner;
        if (isDrop || !me.ddmInstance.dragCurrent) {
            return;
        }
        if (!me.dragEl || me.dragEl !== me.ddmInstance.dragCurrent) {
            me.dragEl = me.ddmInstance.dragCurrent;
            // refresh regions on drag start
            me.refreshCache();
        }
        pt = e.getPoint();
        proc = me.proc;
        els = me.els;
        for (id in els) {
            el = els[id];
            elementRegion = el.getRegion();
            configSource = el.ddScrollConfig || me;
            if (elementRegion && elementRegion.contains(pt)) {
                if (!el.isScrollable()) {
                    ownerCt = el.component && el.component.ownerCt;
                    scrollerOwner = ownerCt && ownerCt.getScrollerOwner && ownerCt.getScrollerOwner();
                    if (scrollerOwner) {
                        el = scrollerOwner.getScrollable().getElement();
                        elementRegion = el.getRegion();
                    } else {
                        
                        continue;
                    }
                }
                if (elementRegion.bottom - pt.y <= configSource.vthresh) {
                    if (proc.el !== el) {
                        me.startProc(el, "down");
                    }
                    return;
                } else if (elementRegion.right - pt.x <= configSource.hthresh) {
                    if (proc.el !== el) {
                        me.startProc(el, "right");
                    }
                    return;
                } else if (pt.y - elementRegion.top <= configSource.vthresh) {
                    if (proc.el !== el) {
                        me.startProc(el, "up");
                    }
                    return;
                } else if (pt.x - elementRegion.left <= configSource.hthresh) {
                    if (proc.el !== el) {
                        me.startProc(el, "left");
                    }
                    return;
                }
            }
        }
        me.clearProc();
    },
    /**
     * Registers new overflow element(s) to auto scroll
     * @param {String/HTMLElement/Ext.dom.Element/String[]/HTMLElement[]/Ext.dom.Element[]} el
     * The id of or the element to be scrolled or an array of either
     */
    register: function(el) {
        var i, len;
        if (Ext.isArray(el)) {
            for (i = 0 , len = el.length; i < len; i++) {
                this.register(el[i]);
            }
        } else {
            el = Ext.get(el);
            this.els[el.id] = el;
        }
    },
    /**
     * Unregisters overflow element(s) so they are no longer scrolled
     * @param {String/HTMLElement/Ext.dom.Element/String[]/HTMLElement[]/Ext.dom.Element[]} el
     * The id of or the element to be removed or an array of either
     */
    unregister: function(el) {
        var i, len;
        if (Ext.isArray(el)) {
            for (i = 0 , len = el.length; i < len; i++) {
                this.unregister(el[i]);
            }
        } else {
            el = Ext.get(el);
            delete this.els[el.id];
        }
    },
    /**
     * Manually trigger a cache refresh.
     */
    refreshCache: function() {
        var els = this.els,
            id;
        for (id in els) {
            if (typeof els[id] === 'object') {
                // for people extending the object prototype
                els[id]._region = els[id].getRegion();
            }
        }
    }
});

/**
 * A simple class that provides the basic implementation needed to make any element a drop target
 * that can have draggable items dropped onto it.  The drop has no effect until an implementation
 * of notifyDrop is provided.
 */
Ext.define('Ext.dd.DropTarget', {
    extend: 'Ext.dd.DDTarget',
    requires: [
        'Ext.dd.ScrollManager'
    ],
    /**
     * Creates new DropTarget.
     * @param {String/HTMLElement/Ext.dom.Element} el The container element or ID of it.
     * @param {Object} config
     */
    constructor: function(el, config) {
        this.el = Ext.get(el);
        Ext.apply(this, config);
        if (this.containerScroll) {
            Ext.dd.ScrollManager.register(this.el);
        }
        this.callParent([
            this.el.dom,
            this.ddGroup || this.group,
            {
                isTarget: true
            }
        ]);
    },
    /**
     * @cfg {Boolean} containerScroll
     * True to register this container with the ScrollManager for auto scrolling during
     * drag operations.
     */
    containerScroll: false,
    /**
     * @cfg {String} ddGroup
     * A named drag drop group to which this object belongs.  If a group is specified, then
     * this object will only interact with other drag drop objects in the same group.
     */
    /**
     * @cfg {String} [overClass=""]
     * The CSS class applied to the drop target element while the drag source is over it.
     */
    /**
     * @cfg {String} dropAllowed
     * The CSS class returned to the drag source when drop is allowed.
     */
    dropAllowed: Ext.baseCSSPrefix + 'dd-drop-ok',
    /**
     * @cfg {String} dropNotAllowed
     * The CSS class returned to the drag source when drop is not allowed.
     */
    dropNotAllowed: Ext.baseCSSPrefix + 'dd-drop-nodrop',
    /**
     * @private
     */
    isTarget: true,
    /**
     * @private
     */
    isNotifyTarget: true,
    /**
     * The function a {@link Ext.dd.DragSource} calls once to notify this drop target that the
     * source is now over the target.  This default implementation adds the CSS class specified
     * by overClass (if any) to the drop element and returns the dropAllowed config value.
     * This method should be overridden if drop validation is required.
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop target
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag source
     * @return {String} status The CSS class that communicates the drop status back to the source
     * so that the underlying {@link Ext.dd.StatusProxy} can be updated
     * @template
     */
    notifyEnter: function(source, e, data) {
        if (this.overClass) {
            this.el.addCls(this.overClass);
        }
        return this.dropAllowed;
    },
    /**
     * The function a {@link Ext.dd.DragSource} calls continuously while it is being dragged over
     * the target. This method will be called on every mouse movement while the drag source is over
     * the drop target. This default implementation simply returns the dropAllowed config value.
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop target
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag source
     * @return {String} status The CSS class that communicates the drop status back to the source
     * so that the underlying {@link Ext.dd.StatusProxy} can be updated
     * @template
     */
    notifyOver: function(source, e, data) {
        return this.dropAllowed;
    },
    /**
     * The function a {@link Ext.dd.DragSource} calls once to notify this drop target
     * that the source has been dragged out of the target without dropping.
     * This default implementation simply removes the CSS class specified by `overClass`
     * (if any) from the drop element.
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop target
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag source
     * @template
     */
    notifyOut: function(source, e, data) {
        if (this.overClass) {
            this.el.removeCls(this.overClass);
        }
    },
    /**
     * The function a {@link Ext.dd.DragSource} calls once to notify this drop target that
     * the dragged item has been dropped on it. This method removes any `overClass` and returns
     * false, so you must provide an implementation that does something to process the drop event
     * and returns true so that the drag source's repair action does not run.
     *
     * You should `callParent` from an override of this method to ensure proper cleanup is
     * performed.
     *
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop target
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag source
     * @return {Boolean} False if the drop was invalid.
     * @template
     */
    notifyDrop: function(source, e, data) {
        if (this.overClass) {
            this.el.removeCls(this.overClass);
        }
        return false;
    },
    destroy: function() {
        if (this.containerScroll) {
            Ext.dd.ScrollManager.unregister(this.el);
        }
        this.callParent();
    }
});

/**
 * Internal class that manages drag/drop for the `Dashboard`.
 * @private
 */
Ext.define('Ext.dashboard.DropZone', {
    extend: 'Ext.dd.DropTarget',
    ddScrollConfig: {
        vthresh: 75,
        hthresh: -1,
        animate: true,
        increment: 200
    },
    containerScroll: true,
    // This causes overflow to go hidden during the drag so that we don't cause panels to
    // wrap by triggering overflow.
    overClass: Ext.baseCSSPrefix + 'dashboard-dd-over',
    constructor: function(dashboard, cfg) {
        this.dashboard = dashboard;
        dashboard.body.ddScrollConfig = this.ddScrollConfig;
        this.callParent([
            dashboard.body,
            cfg
        ]);
    },
    getOverEvent: function(dd, e, data) {
        var dashboard = this.dashboard,
            dbody = dashboard.body,
            items = dashboard.items.items,
            bodyBox = dbody.getBox(),
            count = items.length,
            xy = e.getXY(),
            orginalX = xy[0] - bodyBox.x + dbody.getScrollLeft(),
            orginalY = xy[1] - bodyBox.y + dbody.getScrollTop(),
            x = orginalX,
            y = orginalY,
            yOffset = 0,
            rowIterationIndex = 0,
            over = {
                columnIndex: 0,
                column: null,
                dashboard: dashboard,
                above: null,
                extensible: false,
                beforeAfter: 0,
                data: data,
                panel: data.panel,
                rawEvent: e,
                source: dd,
                status: this.dropAllowed
            },
            t, ht, i, k, item, w, h, childCount, childItems, childItem;
        for (i = 0; i < count; i += 2) {
            item = items[i];
            if (rowIterationIndex !== item.rowIndex) {
                rowIterationIndex = item.rowIndex;
                x = orginalX;
                if (rowIterationIndex > 0) {
                    y -= yOffset;
                    yOffset = 0;
                }
            }
            w = item.lastBox.width;
            h = item.lastBox.height;
            if (yOffset <= h) {
                yOffset = h;
            }
            if (items[i + 1]) {
                // This is for splitter
                w += items[i + 1].lastBox.width;
            }
            if (e.within(item.el)) {
                over.columnIndex = i;
                over.column = item;
                over.extensible = this.isRowExtensible(item.rowIndex);
                t = Math.min(80, w * 0.2);
                over.beforeAfter = t = (over.extensible && ((x < t) ? -1 : ((x > w - t) ? 1 : 0)));
                if (!t || !over.extensible) {
                    childItems = item.items.items;
                    // if we are not on an edge OR reached maxColumns
                    // (which means "insert the panel in between the columns"),
                    // we need to dig one more level down
                    for (k = 0 , childCount = childItems.length; k < childCount; ++k) {
                        childItem = childItems[k];
                        ht = childItem.el.getHeight();
                        if (y < ht / 2) {
                            // if mouse is above the current child's top, Y coord, it
                            // is considered as "above" the previous child
                            over.above = childItem;
                            break;
                        }
                        y -= ht;
                    }
                }
                break;
            }
            x -= w;
        }
        return over;
    },
    notifyOver: function(dd, e, data) {
        var me = this,
            dashboard = me.dashboard,
            hasListeners = dashboard.hasListeners,
            over = me.getOverEvent(dd, e, data),
            colEl = over.column && over.column.el,
            proxy = dd.proxy,
            aboveItem = over.above,
            width = 0,
            proxyProxy, colWidth, padding;
        data.lastOver = over;
        if ((!hasListeners.validatedrop || dashboard.fireEvent('validatedrop', over) !== false) && (!hasListeners.beforedragover || dashboard.fireEvent('beforedragover', over) !== false)) {
            // eslint-disable-line max-len
            proxyProxy = dd.panelProxy.getProxy();
            // make sure proxy width is fluid in different width columns
            proxy.getProxy().setWidth('auto');
            if (colEl) {
                width = colWidth = colEl.getWidth();
                // A floating column was targeted
                if (over.beforeAfter) {
                    dd.panelProxy.moveProxy(colEl.dom, colEl.dom.firstChild);
                    width = colWidth / 2;
                    proxyProxy.setWidth(width);
                } else {
                    if (aboveItem) {
                        dd.panelProxy.moveProxy(aboveItem.el.dom.parentNode, aboveItem.el.dom);
                    } else {
                        dd.panelProxy.moveProxy(colEl.dom, null);
                    }
                    proxyProxy.setWidth('auto');
                }
                proxyProxy.setStyle({
                    'float': 'none',
                    'clear': 'none',
                    'margin-left': (over.beforeAfter > 0) ? (colWidth - width - colEl.getPadding('lr')) + 'px' : '',
                    'margin-top': '7px'
                });
            } else {
                padding = dashboard.body.getPadding('lr');
                proxyProxy.setStyle({
                    'float': 'left',
                    'clear': 'left',
                    'margin': '0 7px 0 7px'
                });
                proxyProxy.setWidth(dashboard.body.getWidth() - padding);
                // Target the innerCt for the move
                dd.panelProxy.moveProxy(dashboard.body.dom.firstChild.firstChild, null);
            }
            this.scrollPos = dashboard.body.getScroll();
            if (hasListeners.dragover) {
                dashboard.fireEvent('dragover', over);
            }
        }
        return over.status;
    },
    isRowExtensible: function(rowIndex) {
        var me = this,
            dashboard = me.dashboard,
            maxColumns = dashboard.getMaxColumns() || 1,
            items;
        items = dashboard.query('>dashboard-column[rowIndex=' + rowIndex + ']');
        return Ext.Array.from(items).length < maxColumns;
    },
    notifyDrop: function(dd, e, data) {
        this.callParent(arguments);
        // eslint-disable-next-line vars-on-top
        var dashboard = this.dashboard,
            over = data.lastOver,
            panel = over.panel,
            fromCt = panel.ownerCt,
            toCt = over.column,
            side = toCt ? over.beforeAfter : 1,
            currentIndex = fromCt.items.indexOf(panel),
            newIndex = toCt ? (over.above ? toCt.items.indexOf(over.above) : toCt.items.getCount()) : 0,
            colIndex, newCol,
            hasListeners = dashboard.hasListeners;
        // Same column tests
        if (fromCt === toCt) {
            if (fromCt.items.getCount() === 1) {
                return;
            }
            if (!side) {
                if (currentIndex < newIndex) {
                    --newIndex;
                }
                if (currentIndex === newIndex) {
                    return;
                }
            }
        }
        if ((hasListeners.validatedrop && dashboard.fireEvent('validatedrop', over) === false) || (hasListeners.beforedrop && dashboard.fireEvent('beforedrop', over) === false)) {
            return;
        }
        Ext.suspendLayouts();
        panel.isMoving = true;
        if (side) {
            colIndex = dashboard.items.indexOf(toCt);
            // inserting into new Row ?
            if (colIndex < 0) {
                colIndex = dashboard.items.getCount();
            } else if (side > 0) {
                ++colIndex;
            }
            newCol = dashboard.createColumn();
            if (toCt) {
                newCol.columnWidth = toCt.columnWidth = toCt.columnWidth / 2;
                delete toCt.width;
            } else {
                newCol.columnWidth = 1;
            }
            // full row
            toCt = dashboard.insert(colIndex, newCol);
            newIndex = 0;
        }
        // make sure panel is visible prior to inserting so the layout doesn't ignore it
        panel.el.dom.style.display = '';
        toCt.insert(newIndex, panel);
        panel.isMoving = false;
        toCt.updateLayout();
        Ext.resumeLayouts(true);
        if (hasListeners.drop) {
            dashboard.fireEvent('drop', over);
        }
    }
});

/**
 * This class encapsulates the creation of items for a `Dashboard`. Generally a `Part` is a
 * component factory that allows all parts of a common type to be easily coordinated as
 * needed for that type. For example, an RSS feed might need certain configuration data to
 * properly initialize. Perahps not all of this data can or should be supplied from the UI
 * that creates new instances for the `Dashboard`.
 *
 * ## Part Configuration
 *
 * The primary role of a `Part` is to provide an abstract way to define the configuration
 * needed to create views. For example, an RSS Part would at least need the URL for the
 * feed.
 *
 * To implement this a derived class provides a `displayForm` method:
 *
 *      Ext.define('App.parts.RSS', {
 *          extend: 'Ext.dashboard.Part',
 *          alias: 'part.rss',
 *
 *          displayForm: function(instance, currentConfig, callback, scope) {
 *              var me = this,
 *                  title = instance ? 'Edit RSS Feed' : 'Add RSS Feed';
 *
 *              // Display a prompt using current URL as default text.
 *              //
 *              Ext.Msg.prompt(title, 'RSS Feed URL', function(btn, text) {
 *                  if (btn === 'ok') {
 *                      var config = {
 *                          feedUrl: text
 *                      };
 *
 *                      callback.call(scope || me, config);
 *                  }
 *              }, me, false, currentConfig ? currentConfig.feedUrl : '');
 *          }
 *      });
 *
 * The returned configuration object is used to create views. It is also passed back to
 * `displayForm` to allow the user to edit the configuration for an existing view.
 *
 * ## Creating Views
 *
 * The next step is to define the view (the components) appropriate for the part. To
 * continue with the above example.
 *
 *      Ext.define('App.parts.RSS', {
 *          extend: 'Ext.dashboard.Part',
 *          alias: 'part.rss',
 *
 *          // The viewTemplate is "component template" that is evaluated against the
 *          // configuration (as returned by displayForm). The top-most component is
 *          // a panel specific to the Dashboard so it can be configured but should
 *          // not be replaced. Instead, fit an appropriate component to the panel.
 *          //
 *          viewTemplate: {
 *              layout: 'fit',
 *              items: [{
 *                  xtype: 'feedpanel',
 *                  feedUrl: '{feedUrl}'  // from the configuration object
 *              }]
 *          },
 *
 *          displayForm: ...
 *      });
 *
 * You can instead choose to override the `createView` method if `viewTemplate` does not
 * provide enough flexibility. It is usually a better solution to create a class (like
 * in the above example) and pass basic configurations to it rather than over-complicate
 * either the `viewTemplate` or a custom `createView` method.
 *
 * @since 5.0.0
 */
Ext.define('Ext.dashboard.Part', {
    mixins: [
        'Ext.mixin.Factoryable',
        'Ext.mixin.Identifiable'
    ],
    requires: [
        'Ext.util.ObjectTemplate'
    ],
    alias: 'part.part',
    factoryConfig: {
        type: 'part'
    },
    isPart: true,
    /**
     * The last assigned identifier for instances created by this `Part`.
     * @private
     */
    _lastId: 0,
    config: {
        id: null,
        /**
         * The `Dashboard` instance that owns this `part`.
         * @property {Ext.dashboard.Panel} dashboard
         * @readonly
         */
        dashboard: null,
        /**
         * @cfg {Object/Ext.util.ObjectTemplate} viewTemplate
         * The configuration object used for creating instances of this `Part`. This is
         * used by the `createView` method to create views.
         */
        viewTemplate: {
            collapsed: '{collapsed}',
            columnIndex: '{columnIndex}',
            id: '{id}',
            title: '{title}',
            height: '{height}'
        }
    },
    viewTemplateOptions: {
        excludeProperties: {
            bind: 1
        }
    },
    valueRe: /^[{][a-z]*[}]$/i,
    constructor: function(config) {
        this.initConfig(config);
    },
    applyViewTemplate: function(template) {
        if (!Ext.isObject(template)) {
            Ext.raise('The viewTemplate for ' + this.$className + ' is not an Object');
        }
        return Ext.util.ObjectTemplate.create(template, this.viewTemplateOptions);
    },
    /**
     * This method should display an appropriate edit form (probably a modal `Ext.Window`
     * or `Ext.Msg.prompt`) to get or edit configuration for an instance of this part.
     *
     * See the class documentation for examples on implementing this method.
     *
     * @param {Ext.Component} instance The already existing view or `null` if called to
     * configure a new instance.
     *
     * @param {Object} currentConfig The configuration returned from this method for the
     * existing view (`instance`) or `null` if called to configure a new instance.
     *
     * @param {Function} callback The function to call passing
     * @param {Object} callback.config The configuration that defines the instance to be
     * created. This value is passed to `createView` and applied to the `viewTemplate`.
     *
     * @param {Object} scope The scope with which to call the `callback`.
     *
     * @method displayForm
     * @abstract
     * @since 5.0.0
     */
    displayForm: function(instance, currentConfig, callback, scope) {
        callback.call(scope || this, {});
    },
    /**
     * This method is responsible for converting a configuration object from `displayForm`
     * into a "view" (an object that can be passed to `Ext.widget`).
     *
     * If you override this method it is recommended that you `callParent` to get the view
     * produced and then edit that result. This is because there are several private
     * properties placed on the returned configuration object.
     *
     *      createView: function(config) {
     *          var view = this.callParent([config]);
     *
     *          // edit view
     *
     *          return view;
     *      }
     *
     * @param {Object} config The object returned from `displayForm`.
     * @return {Object} The view configuration object.
     * @protected
     * @since 5.0.0
     */
    createView: function(config) {
        var me = this,
            template = me.getViewTemplate(),
            ret = template.apply(config);
        ret.dashboard = me.getDashboard();
        ret.part = me;
        ret._partConfig = config;
        return ret;
    }
});

/**
 * This class manages a drag-drop Dashboard similar to the legacy Ext JS Portal example.
 * The user-directed layout of the Dashboard is preserved the Ext JS `stateful` mechanism
 * to preserve potentially dynamic user sizing and collapsed states as well as order of
 * items in their columns.
 */
Ext.define('Ext.dashboard.Dashboard', {
    extend: 'Ext.panel.Panel',
    xtype: 'dashboard',
    requires: [
        'Ext.layout.container.Dashboard',
        'Ext.dashboard.DropZone',
        'Ext.dashboard.Column',
        'Ext.dashboard.Part'
    ],
    isDashboard: true,
    cls: Ext.baseCSSPrefix + 'dashboard',
    bodyCls: Ext.baseCSSPrefix + 'dashboard-body',
    defaultType: 'dashboard-column',
    scrollable: true,
    layout: {
        type: 'dashboard'
    },
    stateful: false,
    idSeed: 1,
    config: {
        /**
         * @cfg {Object} parts
         * An object keyed by `id` for the parts that can be created for this `Dashboard`.
         */
        parts: null
    },
    renderConfig: {
        /**
         * @cfg {Number} maxColumns
         * The maximum number of visible columns.
         * @accessor
         */
        maxColumns: 4
    },
    /**
     * @cfg {Number[]} columnWidths
     * An array designating the width of columns in your dashboard's default state as described
     * by the {@link #cfg-defaultContent} property. For example:
     *
     *     columnWidths: [
     *        0.35,
     *        0.40,
     *        0.25
     *     ]
     *    
     * As you can see, this array contains the default widths for the 3 columns in the dashboard's 
     * initial view. The column widths should total to an integer value, typically 1 as shown
     * above. When column widths exceed 1, they will be wrapped effectively creating "rows". This
     * means that if your column widths add up to more than 1, you would still want the first few
     * to total 1 to ensure that the first row fills the dashboard space. This applies whenever
     * the column widths extend past an integer value.
     * 
     * **Note:** columnWidths will not be utilized if there is stateful information that 
     * dictates different user saved column widths.
     */
    /**
     * @cfg {Object[]} defaultContent
     * An array of {@link Ext.dashboard.Part part} configuration objects that define your
     * dashboard's default state. These should not be confused with component configurations.
     *
     * Each config object should also include:
     *
     * + `type` - The type of {@link Ext.dashboard.Part part} that you want to be generated.
     * + `columnIndex` - The column position in which the {@link Ext.dashboard.Part part} should
     * reside.
     * + `height` - The desired height of the {@link Ext.dashboard.Part part} to be generated.
     *
     * The remaining properties are specific to your part's config object. For example:      
     *
     *       defaultContent: [{
     *           type: 'rss',
     *           columnIndex: 0,
     *           height: 500,
     *           feedUrl: 'http://feeds.feedburner.com/extblog'
     *       }, {
     *           type: 'stockTicker',
     *           columnIndex: 1,
     *           height: 300
     *       }, {
     *           type: 'stocks',
     *           columnIndex: 1,
     *           height: 300
     *       }, {
     *           type: 'rss',
     *           columnIndex: 2,
     *           height: 350,
     *           feedUrl: 'http://rss.cnn.com/rss/edition.rss'
     *       }]
     *
     * Default column widths are defined by {@link #cfg-columnWidths} and not in these
     * part config objects.
     * 
     * **Note:** defaultContent will not be utilized if there is stateful information that 
     * dictates different user saved positioning and componentry.
     */
    /**
     * @event validatedrop
     */
    /**
     * @event beforedragover
     */
    /**
     * @event dragover
     */
    /**
     * @event beforedrop
     */
    /**
     * @event drop
     */
    initComponent: function() {
        var me = this;
        me.callParent();
        me.addStateEvents('remove');
    },
    applyParts: function(parts, collection) {
        var id, part;
        if (!collection) {
            collection = new Ext.util.Collection({
                decoder: Ext.Factory.part
            });
        }
        for (id in parts) {
            part = parts[id];
            if (Ext.isString(part)) {
                part = {
                    type: part
                };
            }
            part.id = id;
            part.dashboard = this;
            collection.add(part);
        }
        return collection;
    },
    /**
     * @private
     */
    getPart: function(type) {
        var parts = this.getParts();
        return parts.getByKey(type);
    },
    addNew: function(type, columnIndex, beforeAfter) {
        var me = this,
            part = me.getPart(type);
        part.displayForm(null, null, function(config) {
            config.type = type;
            me.addView(config, columnIndex, beforeAfter);
        });
    },
    addView: function(instance, columnIndex, beforeAfter) {
        var me = this,
            // We are only concerned with columns (ignore splitters).
            items = me.query('dashboard-column'),
            count = items.length,
            index = columnIndex || 0,
            view = instance.id ? instance : me.createView(instance),
            columnWidths = me.columnWidths,
            column;
        if (!count) {
            // if the layout is empty, assert a full initially
            column = me.add(0, me.createColumn({
                columnWidth: (Ext.isArray(columnWidths) ? columnWidths[0] : 1)
            }));
            items = [
                column
            ];
            count = 1;
        }
        if (index >= count) {
            index = count - 1;
            beforeAfter = 1;
        }
        // after
        if (!beforeAfter) {
            column = items[index];
            if (column) {
                return column.add(view);
            }
        }
        if (beforeAfter > 0) {
            // after...
            ++index;
        }
        column = me.createColumn();
        if (columnWidths) {
            column.columnWidth = columnWidths[index] || (columnWidths[index] = 1);
        }
        if (!column.items) {
            column.items = [];
        }
        column.items.push(view);
        column = me.add(column);
        return column.items.first();
    },
    createColumn: function(config) {
        var cycle = this.cycleLayout;
        return Ext.apply({
            items: [],
            bubbleEvents: [
                'add',
                'childmove',
                'resize'
            ],
            listeners: {
                expand: cycle,
                collapse: cycle,
                scope: this
            }
        }, config);
    },
    createView: function(config) {
        var me = this,
            type = config.type,
            part = me.getPart(type),
            view = part.createView(config);
        if (!view.id) {
            view.id = me.id + '_' + type + (me.idSeed++);
        }
        view.bubbleEvents = Ext.Array.from(view.bubbleEvents).concat([
            'expand',
            'collapse'
        ]);
        view.stateful = me.stateful;
        view.listeners = {
            removed: this.onItemRemoved,
            scope: this
        };
        return view;
    },
    initEvents: function() {
        this.callParent();
        this.dd = new Ext.dashboard.DropZone(this, this.dropConfig);
    },
    /**
     * Readjust column/splitter heights for collapsing child panels
     * @private
     */
    cycleLayout: function() {
        this.updateLayout();
    },
    doDestroy: function() {
        if (this.dd) {
            Ext.destroy(this.dd);
        }
        this.callParent();
    },
    //-------------------------------------------------
    // State and Item Persistence
    applyState: function(state) {
        delete state.items;
        this.callParent([
            state
        ]);
        // eslint-disable-next-line vars-on-top
        var me = this,
            columnWidths = state.columnWidths,
            items = me.items.items,
            length = items.length,
            columnLength = columnWidths ? columnWidths.length : 0,
            i;
        // Splitters have not been inserted so the length is sans-splitter
        if (columnLength) {
            me.columnWidths = [];
            for (i = 0; i < length; ++i) {
                me.columnWidths.push(items[i].columnWidth = (i < columnLength) ? columnWidths[i] : (1 / length));
            }
        }
    },
    getState: function() {
        var me = this,
            columnWidths = [],
            items = me.items.items,
            state = me.callParent() || {},
            length = items.length,
            i, item;
        for (i = 0; i < length; ++i) {
            if (!(item = items[i]).isSplitter) {
                columnWidths.push(item.columnWidth);
            }
        }
        state.idSeed = me.idSeed;
        state.items = me.serializeItems();
        // only overwrite column widths if they are defined in the state
        if (columnWidths.length) {
            state.columnWidths = me.columnWidths = columnWidths;
        } else // no column widths are defined from the state, let's remove it so any defaults
        // can be used instead
        {
            delete state.columnWidths;
        }
        return state;
    },
    initItems: function() {
        var me = this,
            defaultContent = me.defaultContent,
            state;
        if (me.stateful) {
            state = Ext.state.Manager.get(me.getStateId());
            defaultContent = (state && state.items) || defaultContent;
        }
        if (!me.items && defaultContent) {
            me.items = me.deserializeItems(defaultContent);
        }
        me.callParent();
    },
    deserializeItems: function(serialized) {
        var me = this,
            length = serialized.length,
            columns = [],
            columnWidths = me.columnWidths,
            maxColumns = me.getMaxColumns(),
            column, columnIndex, columnWidth, i, item, partConfig;
        for (i = 0; i < length; ++i) {
            partConfig = serialized[i];
            columnIndex = Math.min(partConfig.columnIndex || 0, maxColumns - 1);
            delete partConfig.columnIndex;
            if (!(column = columns[columnIndex])) {
                columns[columnIndex] = column = me.createColumn();
                columnWidth = columnWidths && columnWidths[columnIndex];
                if (columnWidth) {
                    column.columnWidth = columnWidth;
                }
            }
            item = me.createView(partConfig);
            column.items.push(item);
        }
        for (i = 0 , length = columns.length; i < length; ++i) {
            column = columns[i];
            if (!column.columnWidth) {
                column.columnWidth = 1 / length;
            }
        }
        return columns;
    },
    serializeItem: function(item) {
        return Ext.apply({
            type: item.part.id,
            id: item.id,
            columnIndex: item.columnIndex
        }, item._partConfig);
    },
    serializeItems: function() {
        var me = this,
            items = me.items.items,
            length = items.length,
            ret = [],
            columnIndex = 0,
            child, childItems, i, item, j, k;
        for (i = 0; i < length; ++i) {
            item = items[i];
            if (!item.isSplitter) {
                childItems = item.items.items;
                for (j = 0 , k = childItems.length; j < k; ++j) {
                    child = childItems[j];
                    child.columnIndex = columnIndex;
                    ret.push(me.serializeItem(child));
                }
                ++columnIndex;
            }
        }
        return ret;
    },
    onItemRemoved: function(item, column) {
        // Removing items from a Dashboard is a persistent action, so we must remove the
        // state data for it or leak it.
        if (item.stateful && !item.isMoving) {
            Ext.state.Manager.clear(item.getStateId());
        }
    }
});

/**
 * This class provides a container DD instance that allows dragging of multiple child source nodes.
 *
 * This class does not move the drag target nodes, but a proxy element which may contain any DOM
 * structure you wish. The DOM element to show in the proxy is provided by either a provided
 * implementation of {@link #getDragData}, or by registered draggables registered with
 * {@link Ext.dd.Registry}
 *
 * If you wish to provide draggability for an arbitrary number of DOM nodes, each of which represent
 * some application object (For example nodes in a {@link Ext.view.View DataView}) then use of this
 * class is the most efficient way to "activate" those nodes.
 *
 * By default, this class requires that draggable child nodes are registered with
 * {@link Ext.dd.Registry}. However a simpler way to allow a DragZone to manage any number
 * of draggable elements is to configure the DragZone with an implementation of the
 * {@link #getDragData} method which interrogates the passed mouse event to see if it has taken
 * place within an element, or class of elements. This is easily done by using the event's {@link
 * Ext.event.Event#getTarget getTarget} method to identify a node based on a CSS selector.
 * For example, to make the nodes of a DataView draggable, use the following technique.
 * Knowledge of the use of the DataView is required:
 *
 *     myDataView.on('render', function(v) {
 *         myDataView.dragZone = new Ext.dd.DragZone(v.getEl(), {
 *
 *     //      On receipt of a mousedown event, see if it is within a DataView node.
 *     //      Return a drag data object if so.
 *             getDragData: function(e) {
 *
 *     //          Use the DataView's own itemSelector (a mandatory property) to
 *     //          test if the mousedown is within one of the DataView's nodes.
 *                 var sourceEl = e.getTarget(v.itemSelector, 10);
 *
 *     //          If the mousedown is within a DataView node, clone the node to produce
 *     //          a ddel element for use by the drag proxy. Also add application data
 *     //          to the returned data object.
 *                 if (sourceEl) {
 *                     d = sourceEl.cloneNode(true);
 *                     d.id = Ext.id();
 *                     return {
 *                         ddel: d,
 *                         sourceEl: sourceEl,
 *                         repairXY: Ext.fly(sourceEl).getXY(),
 *                         sourceStore: v.store,
 *                         draggedRecord: v.getRecord(sourceEl)
 *                     }
 *                 }
 *             },
 *
 *     //      Provide coordinates for the proxy to slide back to on failed drag.
 *     //      This is the original XY coordinates of the draggable element captured
 *     //      in the getDragData method.
 *             getRepairXY: function() {
 *                 return this.dragData.repairXY;
 *             }
 *         });
 *     });
 *
 * See the {@link Ext.dd.DropZone DropZone} documentation for details about building a DropZone
 * which cooperates with this DragZone.
 */
Ext.define('Ext.dd.DragZone', {
    extend: 'Ext.dd.DragSource',
    /**
     * Creates new DragZone.
     * @param {String/HTMLElement/Ext.dom.Element} el The container element or ID of it.
     * @param {Object} config
     */
    constructor: function(el, config) {
        var me = this,
            scroll;
        me.callParent([
            el,
            config
        ]);
        scroll = me.containerScroll;
        if (scroll) {
            el = me.scrollEl || el;
            el = Ext.get(el);
            if (Ext.isObject(scroll)) {
                el.ddScrollConfig = scroll;
            }
            Ext.dd.ScrollManager.register(el);
        }
    },
    /**
     * @cfg {Object/Boolean} containerScroll
     * True to register this container with the Scrollmanager for auto scrolling during
     * drag operations.
     * A {@link Ext.dd.ScrollManager} configuration may also be passed.
     */
    /**
     * @cfg {String/HTMLElement/Ext.dom.Element} scrollEl
     * An element to register with the ScrollManager if {@link #containerScroll}
     * is set. Defaults to the drag element.
     */
    /**
     * Called when a mousedown occurs in this container. Looks in {@link Ext.dd.Registry}
     * for a valid target to drag based on the mouse down. Override this method to provide your own
     * lookup logic (e.g. finding a child by class name). Make sure your returned object
     * has a "ddel" attribute (with an HTML Element) for other functions to work.
     * @param {Event} e The mouse down event
     * @return {Object} The dragData
     */
    getDragData: function(e) {
        return Ext.dd.Registry.getHandleFromEvent(e);
    },
    /**
     * Called once drag threshold has been reached to initialize the proxy element.
     * By default, it clones the this.dragData.ddel
     * @param {Number} x The x position of the click on the dragged object
     * @param {Number} y The y position of the click on the dragged object
     * @return {Boolean} true to continue the drag, false to cancel
     * @template
     */
    onInitDrag: function(x, y) {
        this.proxy.update(this.dragData.ddel.cloneNode(true));
        this.onStartDrag(x, y);
        return true;
    },
    /**
     * Called before a repair of an invalid drop to get the XY to animate to.
     * By default returns the XY of this.dragData.ddel
     * @param {Event} e The mouse up event
     * @return {Number[]} The xy location (e.g. `[100, 200]`)
     * @template
     */
    getRepairXY: function(e) {
        return Ext.fly(this.dragData.ddel).getXY();
    },
    destroy: function() {
        if (this.containerScroll) {
            Ext.dd.ScrollManager.unregister(this.scrollEl || this.el);
        }
        this.callParent();
    }
});

/**
 * Provides easy access to all drag drop components that are registered on a page.
 * Items can be retrieved either directly by DOM node id, or by passing in the drag drop event
 * that occurred and looking up the event target.
 */
Ext.define('Ext.dd.Registry', {
    singleton: true,
    constructor: function() {
        this.elements = {};
        this.handles = {};
        this.autoIdSeed = 0;
    },
    getId: function(el, autogen) {
        var id;
        if (typeof el === "string") {
            return el;
        }
        id = el.id;
        if (!id && autogen !== false) {
            id = "extdd-" + (++this.autoIdSeed);
            el.id = id;
        }
        return id;
    },
    /**
     * Registers a drag drop element.
     *
     * @param {String/HTMLElement} el The id or DOM node to register
     * @param {Object} data An custom data object that will be passed between the elements that are
     * involved in drag drop operations. You can populate this object with any arbitrary properties
     * that your own code knows how to interpret, plus there are some specific properties known to
     * the Registry that should be populated in the data object (if applicable):
     * @param {HTMLElement[]} data.handles Array of DOM nodes that trigger dragging for the element
     * being registered.
     * @param {Boolean} data.isHandle True if the element passed in triggers dragging itself,
     * else false.
     */
    register: function(el, data) {
        var hs, i, len;
        data = data || {};
        if (typeof el === "string") {
            el = document.getElementById(el);
        }
        data.ddel = el;
        this.elements[this.getId(el)] = data;
        if (data.isHandle !== false) {
            this.handles[data.ddel.id] = data;
        }
        if (data.handles) {
            hs = data.handles;
            for (i = 0 , len = hs.length; i < len; i++) {
                this.handles[this.getId(hs[i])] = data;
            }
        }
    },
    /**
     * Unregister a drag drop element
     * @param {String/HTMLElement} el The id or DOM node to unregister
     */
    unregister: function(el) {
        var id = this.getId(el, false),
            data = this.elements[id],
            hs, i, len;
        if (data) {
            delete this.elements[id];
            if (data.handles) {
                hs = data.handles;
                for (i = 0 , len = hs.length; i < len; i++) {
                    delete this.handles[this.getId(hs[i], false)];
                }
            }
        }
    },
    /**
     * Returns the handle registered for a DOM Node by id
     * @param {String/HTMLElement} id The DOM node or id to look up
     * @return {Object} handle The custom handle data
     */
    getHandle: function(id) {
        if (typeof id !== "string") {
            // must be element?
            id = id.id;
        }
        return this.handles[id];
    },
    /**
     * Returns the handle that is registered for the DOM node that is the target of the event
     * @param {Event} e The event
     * @return {Object} handle The custom handle data
     */
    getHandleFromEvent: function(e) {
        var t = e.getTarget();
        return t ? this.handles[t.id] : null;
    },
    /**
     * Returns a custom data object that is registered for a DOM node by id
     * @param {String/HTMLElement} id The DOM node or id to look up
     * @return {Object} data The custom data
     */
    getTarget: function(id) {
        if (typeof id !== "string") {
            // must be element?
            id = id.id;
        }
        return this.elements[id];
    },
    /**
     * Returns a custom data object that is registered for the DOM node that is the target
     * of the event
     * @param {Event} e The event
     * @return {Object} data The custom data
     */
    getTargetFromEvent: function(e) {
        var t = e.getTarget();
        return t ? this.elements[t.id] || this.handles[t.id] : null;
    }
});

/**
 * This class provides a container DD instance that allows dropping on multiple child target nodes.
 *
 * By default, this class requires that child nodes accepting drop are registered with
 * {@link Ext.dd.Registry}. However a simpler way to allow a DropZone to manage any number of target
 * elements is to configure the DropZone with an implementation of {@link #getTargetFromEvent}
 * which interrogates the passed mouse event to see if it has taken place within an element,
 * or class of elements. This is easily done by using the event's
 * {@link Ext.event.Event#getTarget getTarget} method to identify a node based on a CSS selector.
 *
 * Once the DropZone has detected through calling getTargetFromEvent, that the mouse is over
 * a drop target, that target is passed as the first parameter to {@link #onNodeEnter},
 * {@link #onNodeOver}, {@link #onNodeOut}, {@link #onNodeDrop}. You may configure the instance
 * of DropZone with implementations of these methods to provide application-specific behaviour
 * for these events to update both application state, and UI state.
 *
 * For example to make a GridPanel a cooperating target with the example illustrated in
 * {@link Ext.dd.DragZone DragZone}, the following technique might be used:
 *
 *     myGridPanel.on('render', function() {
 *         myGridPanel.dropZone = new Ext.dd.DropZone(myGridPanel.getView().scroller, {
 *
 *             // If the mouse is over a grid row, return that node. This is
 *             // provided as the "target" parameter in all "onNodeXXXX"
 *             // node event handling functions
 *             getTargetFromEvent: function(e) {
 *                 return e.getTarget(myGridPanel.getView().rowSelector);
 *             },
 *
 *             // On entry into a target node, highlight that node.
 *             onNodeEnter: function(target, dd, e, data) {
 *                 Ext.fly(target).addCls('my-row-highlight-class');
 *             },
 *
 *             // On exit from a target node, unhighlight that node.
 *             onNodeOut: function(target, dd, e, data) {
 *                 Ext.fly(target).removeCls('my-row-highlight-class');
 *             },
 *
 *             // While over a target node, return the default drop allowed class which
 *             // places a "tick" icon into the drag proxy.
 *             onNodeOver: function(target, dd, e, data) {
 *                 return Ext.dd.DropZone.prototype.dropAllowed;
 *             },
 *
 *             // On node drop we can interrogate the target to find the underlying
 *             // application object that is the real target of the dragged data.
 *             // In this case, it is a Record in the GridPanel's Store.
 *             // We can use the data set up by the DragZone's getDragData method to read
 *             // any data we decided to attach in the DragZone's getDragData method.
 *             onNodeDrop: function(target, dd, e, data) {
 *                 var rowIndex = myGridPanel.getView().findRowIndex(target);
 *                 var r = myGridPanel.getStore().getAt(rowIndex);
 *                 Ext.Msg.alert('Drop gesture', 'Dropped Record id ' + data.draggedRecord.id +
 *                     ' on Record id ' + r.id);
 *                 return true;
 *             }
 *         });
 *     }
 *
 * See the {@link Ext.dd.DragZone DragZone} documentation for details about building
 * a DragZone which cooperates with this DropZone.
 */
Ext.define('Ext.dd.DropZone', {
    extend: 'Ext.dd.DropTarget',
    requires: [
        'Ext.dd.Registry'
    ],
    /**
     * Returns a custom data object associated with the DOM node that is the target of the event.
     * By default this looks up the event target in the {@link Ext.dd.Registry}, although you can
     * override this method to provide your own custom lookup.
     * @param {Event} e The event
     * @return {Object} data The custom data
     */
    getTargetFromEvent: function(e) {
        return Ext.dd.Registry.getTargetFromEvent(e);
    },
    /**
     * Called when the DropZone determines that a {@link Ext.dd.DragSource} has entered a drop node
     * that has either been registered or detected by a configured implementation of
     * {@link #getTargetFromEvent}. This method has no default implementation and should be
     * overridden to provide node-specific processing if necessary.
     * @param {Object} nodeData The custom data associated with the drop node (this is the same
     * value returned from  {@link #getTargetFromEvent} for this node)
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag source
     */
    onNodeEnter: function(nodeData, source, e, data) {},
    /**
     * Called while the DropZone determines that a {@link Ext.dd.DragSource} is over a drop node
     * that has either been registered or detected by a configured implementation of
     * {@link #getTargetFromEvent}. The default implementation returns this.dropAllowed,
     * so it should be overridden to provide the proper feedback.
     * @param {Object} nodeData The custom data associated with the drop node (this is the same
     * value returned from {@link #getTargetFromEvent} for this node)
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag source
     * @return {String} status The CSS class that communicates the drop status back to the source
     * so that the underlying {@link Ext.dd.StatusProxy} can be updated
     * @template
     */
    onNodeOver: function(nodeData, source, e, data) {
        return this.dropAllowed;
    },
    /**
     * Called when the DropZone determines that a {@link Ext.dd.DragSource} has been dragged out of
     * the drop node without dropping.  This method has no default implementation and should be
     * overridden to provide node-specific processing if necessary.
     * @param {Object} nodeData The custom data associated with the drop node (this is the same
     * value returned from {@link #getTargetFromEvent} for this node)
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag source
     * @template
     */
    onNodeOut: function(nodeData, source, e, data) {},
    /**
     * Called when the DropZone determines that a {@link Ext.dd.DragSource} has been dropped onto
     * the drop node.  The default implementation returns false, so it should be overridden
     * to provide the appropriate processing of the drop event and return true so that the drag
     * source's repair action does not run.
     * @param {Object} nodeData The custom data associated with the drop node (this is the same
     * value returned from
     * {@link #getTargetFromEvent} for this node)
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag source
     * @return {Boolean} True if the drop was valid, else false
     * @template
     */
    onNodeDrop: function(nodeData, source, e, data) {
        return false;
    },
    /**
     * Called while the DropZone determines that a {@link Ext.dd.DragSource} is being dragged
     * over it, but not over any of its registered drop nodes. The default implementation returns
     * this.dropNotAllowed, so it should be overridden to provide the proper feedback if necessary.
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag source
     * @return {String} status The CSS class that communicates the drop status back to the source
     * so that the underlying {@link Ext.dd.StatusProxy} can be updated
     * @template
     */
    onContainerOver: function(source, e, data) {
        return this.dropNotAllowed;
    },
    /**
     * Called when the DropZone determines that a {@link Ext.dd.DragSource} has been dropped on it,
     * but not on any of its registered drop nodes.  The default implementation returns false, so it
     * should be overridden to provide the appropriate processing of the drop event if you need
     * the drop zone itself to be able to accept drops. It should return true when valid so that
     * the drag source's repair action does not run.
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag source
     * @return {Boolean} True if the drop was valid, else false
     * @template
     */
    onContainerDrop: function(source, e, data) {
        return false;
    },
    /**
     * The function a {@link Ext.dd.DragSource} calls once to notify this drop zone that the source
     * is now over the zone. The default implementation returns this.dropNotAllowed and expects
     * that only registered drop nodes can process drag drop operations, so if you need the drop
     * zone itself to be able to process drops you should override this method and provide a custom
     * implementation.
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag source
     * @return {String} status The CSS class that communicates the drop status back to the source
     * so that the underlying {@link Ext.dd.StatusProxy} can be updated
     * @template
     */
    notifyEnter: function(source, e, data) {
        this.callParent([
            source,
            e,
            data
        ]);
        return this.dropNotAllowed;
    },
    /**
     * The function a {@link Ext.dd.DragSource} calls continuously while it is being dragged over
     * the drop zone. This method will be called on every mouse movement while the drag source
     * is over the drop zone. It will call {@link #onNodeOver} while the drag source is over
     * a registered node, and will also automatically delegate to the appropriate node-specific
     * methods as necessary when the drag source enters and exits registered nodes
     * ({@link #onNodeEnter}, {@link #onNodeOut}). If the drag source is not currently over a
     * registered node, it will call {@link #onContainerOver}.
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag source
     * @return {String} status The CSS class that communicates the drop status back to the source
     * so that the
     * underlying {@link Ext.dd.StatusProxy} can be updated
     * @template
     */
    notifyOver: function(source, e, data) {
        var me = this,
            n = me.getTargetFromEvent(e);
        if (!n) {
            // not over valid drop target
            if (me.lastOverNode) {
                me.onNodeOut(me.lastOverNode, source, e, data);
                me.lastOverNode = null;
            }
            return me.onContainerOver(source, e, data);
        }
        if (me.lastOverNode !== n) {
            if (me.lastOverNode) {
                me.onNodeOut(me.lastOverNode, source, e, data);
            }
            me.onNodeEnter(n, source, e, data);
            me.lastOverNode = n;
        }
        return me.onNodeOver(n, source, e, data);
    },
    /**
     * The function a {@link Ext.dd.DragSource} calls once to notify this drop zone that the source
     * has been dragged out of the zone without dropping.  If the drag source is currently over
     * a registered node, the notification will be delegated to {@link #onNodeOut} for node-specific
     * handling, otherwise it will be ignored.
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop target
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag zone
     * @template
     */
    notifyOut: function(source, e, data) {
        this.callParent([
            source,
            e,
            data
        ]);
        if (this.lastOverNode) {
            this.onNodeOut(this.lastOverNode, source, e, data);
            this.lastOverNode = null;
        }
    },
    /**
     * The function a {@link Ext.dd.DragSource} calls once to notify this drop zone that the dragged
     * item has been dropped on it.  The drag zone will look up the target node based on the event
     * passed in, and if there is a node registered for that event, it will delegate to
     * {@link #onNodeDrop} for node-specific handling, otherwise it will call
     * {@link #onContainerDrop}.
     * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
     * @param {Event} e The event
     * @param {Object} data An object containing arbitrary data supplied by the drag source
     * @return {Boolean} False if the drop was invalid.
     * @template
     */
    notifyDrop: function(source, e, data) {
        var me = this,
            n = me.getTargetFromEvent(e),
            result = n ? me.onNodeDrop(n, source, e, data) : me.onContainerDrop(source, e, data);
        // Exit the overNode upon drop.
        // Must do this after dropping because exiting a node may perform actions
        // which invalidate a drop.
        if (me.lastOverNode) {
            me.onNodeOut(me.lastOverNode, source, e, data);
            me.lastOverNode = null;
        }
        return result;
    },
    /**
     * @private
     */
    triggerCacheRefresh: function() {
        Ext.dd.DDM.refreshCache(this.groups);
    }
});

/**
 * An extended {@link Ext.dom.Element} object that supports a shadow and shim
 *
 * @deprecated 5.1.0 Ext.dom.Element now includes support for shadow and shim
 * see {@link Ext.dom.Element#enableShadow enableShadow} and
 * {@link Ext.dom.Element#enableShim enableShim}
 */
Ext.define('Ext.dom.Layer', {
    extend: 'Ext.dom.Element',
    alternateClassName: 'Ext.Layer',
    /**
     * @cfg {String/Boolean} [shadow=false]
     * True to automatically create an {@link Ext.Shadow}, or a string indicating the
     * shadow's display {@link Ext.Shadow#mode}. False to disable the shadow.
     */
    /**
     * @cfg {String/Boolean} [shim=false]
     * True to automatically create a {@link Ext.dom.Shim}.
     */
    /**
     * @cfg {Object} [dh={tag: 'div', cls: 'x-layer'}]
     * DomHelper object config to create element with.
     */
    /**
     * @cfg {Boolean} [constrain=true]
     * False to disable constrain to viewport.
     */
    /**
     * @cfg {String} cls
     * CSS class to add to the element
     */
    /**
     * @cfg {Number} [zindex=11000]
     * Starting z-index.
     */
    /**
     * @cfg {Number} [shadowOffset=4]
     * Number of pixels to offset the shadow
     */
    /**
     * @cfg {Boolean} [useDisplay=false]
     * Defaults to use css offsets to hide the Layer. Specify <tt>true</tt>
     * to use css style <tt>'display:none;'</tt> to hide the Layer.
     */
    /**
     * @cfg {String} visibilityCls
     * The CSS class name to add in order to hide this Layer if this layer
     * is configured with <code>{@link #hideMode}: 'asclass'</code>
     */
    /**
     * @cfg {String} hideMode
     * A String which specifies how this Layer will be hidden.
     * Values may be:
     *
     * - `'display'` : The Component will be hidden using the `display: none` style.
     * - `'visibility'` : The Component will be hidden using the `visibility: hidden` style.
     * - `'offsets'` : The Component will be hidden by absolutely positioning it out of the
     * visible area of the document. This is useful when a hidden Component must maintain
     * measurable dimensions. Hiding using `display` results in a Component having zero dimensions.
     */
    isLayer: true,
    /**
     * Creates new Layer.
     * @param {Object} [config] An object with config options.
     * @param {String/HTMLElement} [existingEl] Uses an existing DOM element.
     * If the element is not found it creates it.
     */
    constructor: function(config, existingEl) {
        config = config || {};
        // eslint-disable-next-line vars-on-top
        var me = this,
            dh = Ext.DomHelper,
            cp = config.parentEl,
            pel = cp ? Ext.getDom(cp) : document.body,
            hm = config.hideMode,
            cls = Ext.baseCSSPrefix + (config.fixed ? 'fixed-layer' : 'layer'),
            dom, id, element, shadowConfig;
        if (existingEl) {
            dom = Ext.getDom(existingEl);
            if (!dom.parentNode) {
                pel.appendChild(dom);
            }
        }
        if (!dom) {
            dom = dh.append(pel, config.dh || {
                tag: 'div',
                cls: cls
            });
        }
        // primarily to give el 'position:absolute' or, if fixed, 'position:fixed'
        if (config.id) {
            dom.id = config.id;
        }
        id = dom.id;
        if (id) {
            element = Ext.cache[id];
            if (element) {
                // if we have an existing Ext.Element in the cache for this same dom
                // element, delete it, so that it can be replaced by this layer instance
                // when we callParent below.
                delete Ext.cache[id];
                element.dom = null;
            }
        }
        this.callParent([
            dom
        ]);
        if (existingEl) {
            me.addCls(cls);
        }
        if (config.preventSync) {
            me.preventSync = true;
        }
        if (config.cls) {
            me.addCls(config.cls);
        }
        me.constrain = config.constrain !== false;
        // Allow Components to pass their hide mode down to the Layer if they are floating.
        // Otherwise, allow useDisplay to override the default hiding method which is visibility.
        // TODO: Have ExtJS's Element implement visibilityMode by using classes as in Mobile.
        if (hm) {
            me.setVisibilityMode(Ext.Element[hm.toUpperCase()]);
        } else if (config.useDisplay) {
            me.setVisibilityMode(Ext.Element.DISPLAY);
        } else {
            me.setVisibilityMode(Ext.Element.VISIBILITY);
        }
        if (config.shadow) {
            me.shadowOffset = config.shadowOffset || 4;
            shadowConfig = {
                offset: me.shadowOffset,
                fixed: config.fixed
            };
            if (config.shadow !== true) {
                shadowConfig.mode = config.shadow;
            }
            me.enableShadow(shadowConfig);
        } else {
            me.shadowOffset = 0;
        }
        if (config.shim) {
            me.enableShim({
                fixed: config.fixed
            });
        }
        // Keep the following only for cases where Ext.Layer would be instantiated
        // directly.  We don't ever pass hidden in the config in the framework
        // since this is handled by the Component lifecycle.
        if (config.hidden === true) {
            me.hide();
        } else if (config.hidden === false) {
            me.show();
        }
    }
});

//
// Definitions of enums referenced in documentation.
//
/**
 * @enum [Ext.enums.Layout=layout.*]
 * Enumeration of all layout types.
 */
/**
 * @enum [Ext.enums.Widget=widget.*]
 * Enumeration of all xtypes.
 */
/**
 * @enum [Ext.enums.Plugin=plugin.*]
 * Enumeration of all ptypes.
 */
/**
 * @enum [Ext.enums.Feature=feature.*]
 * Enumeration of all ftypes.
 */

/**
 * A publisher that adds support for mousenter and mouseleave events on browsers that do
 * not support those events natively
 * @private
 */
Ext.define('Ext.event.publisher.MouseEnterLeave', {
    extend: 'Ext.event.publisher.Dom',
    type: 'mouseEnterLeave'
}, function(MouseEnterLeave) {
    var eventMap = {
            mouseover: 'mouseenter',
            mouseout: 'mouseleave'
        };
    if (!Ext.supports.MouseEnterLeave) {
        MouseEnterLeave.override({
            handledDomEvents: [
                'mouseover',
                'mouseout'
            ],
            handledEvents: [
                'mouseenter',
                'mouseleave'
            ],
            publishDelegatedDomEvent: function(e) {
                var target, relatedTarget, id, el, type, event;
                // call parent to dispatch the native browser event first (mouseover, mouseout)
                this.callParent([
                    e
                ]);
                target = e.getTarget();
                relatedTarget = e.getRelatedTarget();
                if (relatedTarget && Ext.fly(target).contains(relatedTarget)) {
                    return;
                }
                id = target.id;
                if (id) {
                    el = Ext.cache[id];
                    if (el) {
                        type = eventMap[e.type];
                        e = e.chain({
                            type: type
                        });
                        if (el.hasListeners[type]) {
                            event = el.events[type];
                            if (event) {
                                // mouseenter/leave are always tracked by the "directs"
                                // Ext.util.Event because they are listed in the directEvents
                                // map of Dom publisher
                                event = event.directs;
                                if (event) {
                                    e.setCurrentTarget(el.dom);
                                    event.fire(e, e.target);
                                }
                            }
                        }
                    }
                }
            }
        });
    }
    MouseEnterLeave.instance = new MouseEnterLeave();
});

/**
 * A simple Component for displaying an Adobe Flash SWF movie. The movie will be sized and can
 * participate in layout like any other Component.
 *
 * This component requires the third-party SWFObject library version 2.2 or above. It is not
 * included within the ExtJS distribution, so you will have to include it into your page manually
 * in order to use this component.
 * The SWFObject library can be downloaded from the [SWFObject project page](http://code.google.com/p/swfobject)
 * and then simply import it into the head of your HTML document:
 *
 *     <script type="text/javascript" src="path/to/local/swfobject.js"></script>
 *
 * ## Configuration
 *
 * This component allows several options for configuring how the target Flash movie is embedded.
 * The most important is the required {@link #url} which points to the location of the Flash movie
 * to load. Other configurations include:
 *
 * - {@link #backgroundColor}
 * - {@link #wmode}
 * - {@link #flashVars}
 * - {@link #flashParams}
 * - {@link #flashAttributes}
 *
 * ## Example usage:
 *
 *     var win = Ext.widget('window', {
 *         title: "It's a tiger!",
 *         layout: 'fit',
 *         width: 300,
 *         height: 300,
 *         x: 20,
 *         y: 20,
 *         resizable: true,
 *         items: {
 *             xtype: 'flash',
 *             url: 'tiger.swf'
 *         }
 *     });
 *     win.show();
 *
 * ## Express Install
 *
 * Adobe provides a tool called [Express Install](http://www.adobe.com/devnet/flashplayer/articles/express_install.html)
 * that offers users an easy way to upgrade their Flash player. If you wish to make use of this,
 * you should set the static EXPRESS_INSTALL_URL property to the location of your Express Install
 * SWF file:
 *
 *     Ext.flash.Component.EXPRESS_INSTALL_URL = 'path/to/local/expressInstall.swf';
 */
Ext.define('Ext.flash.Component', {
    extend: 'Ext.Component',
    alternateClassName: 'Ext.FlashComponent',
    alias: 'widget.flash',
    /**
     * @cfg {String} [flashVersion="9.0.115"]
     * Indicates the version the flash content was published for.
     */
    flashVersion: '9.0.115',
    /**
     * @cfg {String} [backgroundColor="#ffffff"]
     * The background color of the SWF movie.
     */
    backgroundColor: '#ffffff',
    /**
     * @cfg {String} [wmode="opaque"]
     * The wmode of the flash object. This can be used to control layering.
     * Set to 'transparent' to ignore the {@link #backgroundColor} and make the background
     * of the Flash movie transparent.
     */
    wmode: 'opaque',
    /**
     * @cfg {Object} flashVars
     * A set of key value pairs to be passed to the flash object as flash variables.
     */
    /**
     * @cfg {Object} flashParams
     * A set of key value pairs to be passed to the flash object as parameters. Possible parameters
     * can be found here: http://kb2.adobe.com/cps/127/tn_12701.html
     */
    /**
     * @cfg {Object} flashAttributes
     * A set of key value pairs to be passed to the flash object as attributes.
     */
    /**
     * @cfg {String} url (required)
     * The URL of the SWF file to include.
     */
    /**
     * @cfg {String/Number} [swfWidth="100%"]
     * The width of the embedded SWF movie inside the component.
     *
     * Defaults to "100%" so that the movie matches the width of the component.
     */
    swfWidth: '100%',
    /**
     * @cfg {String/Number} [swfHeight="100%"]
     * The height of the embedded SWF movie inside the component.
     *
     * Defaults to "100%" so that the movie matches the height of the component.
     */
    swfHeight: '100%',
    /**
     * @cfg {Boolean} [expressInstall=false]
     * True to prompt the user to install flash if not installed. Note that this uses
     * Ext.FlashComponent.EXPRESS_INSTALL_URL, which should be set to the local resource.
     */
    expressInstall: false,
    /**
     * @property {Ext.dom.Element} swf
     * A reference to the object or embed element into which the SWF file is loaded. Only
     * populated after the component is rendered and the SWF has been successfully embedded.
     */
    // Have to create a placeholder div with the swfId, which SWFObject will replace with the
    // object/embed element.
    renderTpl: [
        '<div id="{swfId}" role="application"></div>'
    ],
    /**
     * @event success
     * Fired when the Flash movie has been successfully embedded
     * @param {Ext.flash.Component} this
     */
    /**
     * @event failure
     * Fired when the Flash movie embedding fails
     * @param {Ext.flash.Component} this
     */
    initComponent: function() {
        /* global swfobject */
        if (!('swfobject' in window)) {
            Ext.raise('The SWFObject library is not loaded. Ext.flash.Component requires ' + 'SWFObject version 2.2 or later: http://code.google.com/p/swfobject/');
        }
        if (!this.url) {
            Ext.raise('The "url" config is required for Ext.flash.Component');
        }
        this.callParent();
    },
    beforeRender: function() {
        this.callParent();
        Ext.applyIf(this.renderData, {
            swfId: this.getSwfId()
        });
    },
    afterRender: function() {
        var me = this,
            flashParams = Ext.apply({}, me.flashParams),
            flashVars = Ext.apply({}, me.flashVars);
        me.callParent();
        flashParams = Ext.apply({
            allowScriptAccess: 'always',
            bgcolor: me.backgroundColor,
            wmode: me.wmode
        }, flashParams);
        flashVars = Ext.apply({
            allowedDomain: document.location.hostname
        }, flashVars);
        new swfobject.embedSWF(me.url, me.getSwfId(), me.swfWidth, me.swfHeight, me.flashVersion, me.expressInstall ? me.statics.EXPRESS_INSTALL_URL : undefined, flashVars, flashParams, me.flashAttributes, me.swfCallback.bind(me));
    },
    /**
     * @private
     * The callback method for handling an embedding success or failure by SWFObject
     * @param {Object} e The event object passed by SWFObject - see http://code.google.com/p/swfobject/wiki/api
     */
    swfCallback: function(e) {
        var me = this;
        if (e.success) {
            me.swf = Ext.get(e.ref);
            me.onSuccess();
            me.fireEvent('success', me);
        } else {
            me.onFailure();
            me.fireEvent('failure', me);
        }
    },
    /**
     * Retrieves the id of the SWF object/embed element.
     */
    getSwfId: function() {
        return this.swfId || (this.swfId = "extswf" + this.getAutoId());
    },
    onSuccess: function() {
        // swfobject forces visiblity:visible on the swf element, which prevents it 
        // from getting hidden when an ancestor is given visibility:hidden.
        this.swf.setStyle('visibility', 'inherit');
    },
    onFailure: Ext.emptyFn,
    doDestroy: function() {
        var me = this,
            swf = me.swf;
        if (swf) {
            swfobject.removeSWF(me.getSwfId());
            me.swf = Ext.destroy(swf);
        }
        me.callParent();
    },
    statics: {
        /**
         * @property {String}
         * The url for installing flash if it doesn't exist. This should be set to a local resource.
         * See [http://get.adobe.com/flashplayer/](http://get.adobe.com/flashplayer/) for details.
         * @static
         */
        EXPRESS_INSTALL_URL: 'http://swfobject.googlecode.com/svn/trunk/swfobject/expressInstall.swf'
    }
});

/**
 * The subclasses of this class provide actions to perform upon {@link Ext.form.Basic Form}s.
 *
 * Instances of this class are only created by a {@link Ext.form.Basic Form} when the Form needs
 * to perform an action such as submit or load. The Configuration options listed for this class
 * are set through the Form's action methods: {@link Ext.form.Basic#submit submit},
 * {@link Ext.form.Basic#load load} and {@link Ext.form.Basic#doAction doAction}
 *
 * The instance of Action which performed the action is passed to the success and failure callbacks
 * of the Form's action methods ({@link Ext.form.Basic#submit submit},
 * {@link Ext.form.Basic#load load} and {@link Ext.form.Basic#doAction doAction}), and to the
 * {@link Ext.form.Basic#actioncomplete actioncomplete} and
 * {@link Ext.form.Basic#actionfailed actionfailed} event handlers.
 */
Ext.define('Ext.form.action.Action', {
    alternateClassName: 'Ext.form.Action',
    /**
     * @cfg {Ext.form.Basic} form
     * The {@link Ext.form.Basic BasicForm} instance that is invoking this Action. Required.
     */
    /**
     * @cfg {String} url
     * The URL that the Action is to invoke. Will default to the {@link Ext.form.Basic#url url}
     * configured on the {@link #form}.
     */
    /**
     * @cfg {Boolean} reset
     * When set to **true**, causes the Form to be {@link Ext.form.Basic#reset reset} on Action
     * success. If specified, this happens before the {@link #success} callback is called and before
     * the Form's {@link Ext.form.Basic#actioncomplete actioncomplete} event fires.
     */
    /**
     * @cfg {String} method
     * The HTTP method to use to access the requested URL.
     * Defaults to the {@link Ext.form.Basic#method BasicForm's method}, or 'POST' if not specified.
     */
    /**
     * @cfg {Object/String} params
     * Extra parameter values to pass. These are added to the Form's
     * {@link Ext.form.Basic#baseParams} and passed to the specified URL along with the Form's
     * input fields.
     *
     * Parameters are encoded as standard HTTP parameters using
     * {@link Ext#urlEncode Ext.Object.toQueryString}.
     */
    /**
     * @cfg {Object} headers
     * Extra headers to be sent in the AJAX request for submit and load actions.
     * See {@link Ext.data.proxy.Ajax#headers}.
     * 
     * **Note:** Headers are not sent during file upload.
     */
    /**
     * @cfg {Number} timeout
     * The number of seconds to wait for a server response before failing with the
     * {@link #failureType} as {@link Ext.form.action.Action#CONNECT_FAILURE}. If not specified,
     * defaults to the configured {@link Ext.form.Basic#timeout timeout} of the {@link #form}.
     */
    /**
     * @cfg {Function/String} success
     * The function to call when a valid success return packet is received.
     * @cfg {Ext.form.Basic} success.form The form that requested the action
     * @cfg {Ext.form.action.Action} success.action The Action class. The {@link #result} property
     * of this object may be examined to perform custom post-processing.
     * 
     * @controllable
     */
    /**
     * @cfg {Function/String} failure
     * The function to call when a failure packet was received, or when an error 
     * occurred in the Ajax communication.
     * @cfg {Ext.form.Basic} failure.form The form that requested the action
     * @cfg {Ext.form.action.Action} failure.action The Action class. If an Ajax error 
     * occurred, the failure type will be in {@link #failureType}. The {@link #result} 
     * property of this object may be examined to perform custom post-processing.
     * 
     * @controllable
     */
    /**
     * @cfg {Object} scope
     * The scope in which to call the configured #success and #failure callback functions
     * (the `this` reference for the callback functions).
     */
    /**
     * @cfg {String} waitMsg
     * The message to be displayed by a call to {@link Ext.window.MessageBox#wait} during the time
     * the action is being processed.
     */
    /**
     * @cfg {String} waitTitle
     * The title to be displayed by a call to {@link Ext.window.MessageBox#wait} during the time
     * the action is being processed.
     */
    /**
     * @cfg {Boolean} submitEmptyText
     * If set to true, the emptyText value will be sent with the form when it is submitted.
     */
    submitEmptyText: true,
    /**
     * @property {String} type
     * The type of action this Action instance performs. Currently only "submit" and "load"
     * are supported.
     */
    /* eslint-disable max-len */
    /**
     * @property {String} failureType
     * The type of failure detected will be one of these:
     * {@link #CLIENT_INVALID}, {@link #SERVER_INVALID}, {@link #CONNECT_FAILURE}, or {@link #LOAD_FAILURE}.
     *
     * Usage:
     *
     *     var fp = new Ext.form.Panel({
     *     ...
     *     buttons: [{
     *         text: 'Save',
     *         formBind: true,
     *         handler: function(){
     *             if(fp.getForm().isValid()){
     *                 fp.getForm().submit({
     *                     url: 'form-submit.php',
     *                     waitMsg: 'Submitting your data...',
     *                     success: function(form, action){
     *                         // server responded with success = true
     *                         var result = action.result;
     *                     },
     *                     failure: function(form, action){
     *                         if (action.{@link #failureType} === Ext.form.action.Action.CONNECT_FAILURE) {
     *                             Ext.Msg.alert('Error',
     *                                 'Status:'+action.response.status+': '+
     *                                 action.response.statusText);
     *                         }
     *                         if (action.failureType === Ext.form.action.Action.SERVER_INVALID){
     *                             // server responded with success = false
     *                             Ext.Msg.alert('Invalid', action.result.errormsg);
     *                         }
     *                     }
     *                 });
     *             }
     *         }
     *     },{
     *         text: 'Reset',
     *         handler: function(){
     *             fp.getForm().reset();
     *         }
     *     }]
     */
    /* eslint-enable max-len */
    /**
     * @property {Object} response
     * The raw XMLHttpRequest object used to perform the action.
     */
    /**
     * @property {Object} result
     * The decoded response object containing a boolean `success` property and other,
     * action-specific properties.
     */
    /**
     * Creates new Action.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        var params;
        if (config) {
            Ext.apply(this, config);
        }
        // Normalize the params option to an Object
        params = config.params;
        if (Ext.isString(params)) {
            this.params = Ext.Object.fromQueryString(params);
        }
    },
    /**
     * @method
     * Invokes this action using the current configuration.
     */
    run: Ext.emptyFn,
    /**
     * @private
     * @method onSuccess
     * Callback method that gets invoked when the action completes successfully. Must be implemented
     * by subclasses.
     * @param {Object} response
     */
    /**
     * @private
     * @method handleResponse
     * Handles the raw response and builds a result object from it. Must be implemented
     * by subclasses.
     * @param {Object} response
     */
    /**
     * @private
     * Handles a failure response.
     * @param {Object} response
     */
    onFailure: function(response) {
        var form = this.form,
            formActive = form && !form.destroying && !form.destroyed;
        this.response = response;
        this.failureType = Ext.form.action.Action.CONNECT_FAILURE;
        if (formActive) {
            form.afterAction(this, false);
        }
    },
    /**
     * @private
     * Validates that a response contains either responseText or responseXML and invokes
     * {@link #handleResponse} to build the result object.
     * @param {Object} response The raw response object.
     * @return {Object/Boolean} The result object as built by handleResponse, or `true` if
     * the response had empty responseText and responseXML.
     */
    processResponse: function(response) {
        this.response = response;
        if (!response.responseText && !response.responseXML) {
            return true;
        }
        return (this.result = this.handleResponse(response));
    },
    /**
     * @private
     * Build the URL for the AJAX request. Used by the standard AJAX submit and load actions.
     * @return {String} The URL.
     */
    getUrl: function() {
        return this.url || this.form.url;
    },
    /**
     * @private
     * Determine the HTTP method to be used for the request.
     * @return {String} The HTTP method
     */
    getMethod: function() {
        return (this.method || this.form.method || 'POST').toUpperCase();
    },
    /**
     * @private
     * Get the set of parameters specified in the BasicForm's baseParams and/or the params option.
     * Items in params override items of the same name in baseParams.
     * @return {Object} the full set of parameters
     */
    getParams: function() {
        return Ext.apply({}, this.params, this.form.baseParams);
    },
    /**
     * @private
     * Creates a callback object.
     */
    createCallback: function() {
        var me = this;
        return {
            success: me.onSuccess,
            failure: me.onFailure,
            scope: me,
            timeout: (me.timeout || me.form.timeout) * 1000
        };
    },
    statics: {
        /**
         * @property
         * Failure type returned when client side validation of the Form fails thus aborting
         * a submit action. Client side validation is performed unless
         * {@link Ext.form.action.Submit#clientValidation} is explicitly set to false.
         * @static
         */
        CLIENT_INVALID: 'client',
        /**
         * @property
         * Failure type returned when server side processing fails and the {@link #result}'s
         * `success` property is set to false.
         *
         * In the case of a form submission, field-specific error messages may be returned
         * in the {@link #result}'s errors property.
         * @static
         */
        SERVER_INVALID: 'server',
        /**
         * @property
         * Failure type returned when a communication error happens when attempting to send
         * a request to the remote server. The {@link #response} may be examined to provide
         * further information.
         * @static
         */
        CONNECT_FAILURE: 'connect',
        /**
         * @property
         * Failure type returned when the response's `success` property is set to false,
         * or no field values are returned in the response's data property.
         * @static
         */
        LOAD_FAILURE: 'load'
    }
});

/**
 * A class which handles loading of data from a server into the Fields of an {@link Ext.form.Basic}.
 *
 * Instances of this class are only created by a {@link Ext.form.Basic Form} when
 * {@link Ext.form.Basic#load load}ing.
 *
 * ## Response Packet Criteria
 *
 * A response packet **must** contain:
 *
 *   - **`success`** property : Boolean
 *   - **`data`** property : Object
 *
 * The `data` property contains the values of Fields to load. The individual value object
 * for each Field is passed to the Field's {@link Ext.form.field.Field#setValue setValue} method.
 *
 * ## JSON Packets
 *
 * By default, response packets are assumed to be JSON, so for the following form load call:
 *
 *     var myFormPanel = new Ext.form.Panel({
 *         title: 'Client and routing info',
 *         renderTo: Ext.getBody(),
 *         defaults: {
 *             xtype: 'textfield'
 *         },
 *         items: [{
 *             fieldLabel: 'Client',
 *             name: 'clientName'
 *         }, {
 *             fieldLabel: 'Port of loading',
 *             name: 'portOfLoading'
 *         }, {
 *             fieldLabel: 'Port of discharge',
 *             name: 'portOfDischarge'
 *         }]
 *     });
 *     myFormPanel.getForm().load({
 *         url: '/getRoutingInfo.php',
 *         params: {
 *             consignmentRef: myConsignmentRef
 *         },
 *         failure: function(form, action) {
 *             Ext.Msg.alert("Load failed", action.result.errorMessage);
 *         }
 *     });
 *
 * a **success response** packet may look like this:
 *
 *     {
 *         success: true,
 *         data: {
 *             clientName: "Fred. Olsen Lines",
 *             portOfLoading: "FXT",
 *             portOfDischarge: "OSL"
 *         }
 *     }
 *
 * while a **failure response** packet may look like this:
 *
 *     {
 *         success: false,
 *         errorMessage: "Consignment reference not found"
 *     }
 *
 * Other data may be placed into the response for processing the {@link Ext.form.Basic Form}'s
 * callback or event handler methods. The object decoded from this JSON is available in the
 * {@link Ext.form.action.Action#result result} property.
 */
Ext.define('Ext.form.action.Load', {
    extend: 'Ext.form.action.Action',
    requires: [
        'Ext.data.Connection'
    ],
    alternateClassName: 'Ext.form.Action.Load',
    alias: 'formaction.load',
    type: 'load',
    /**
     * @private
     */
    run: function() {
        Ext.Ajax.request(Ext.apply(this.createCallback(), {
            method: this.getMethod(),
            url: this.getUrl(),
            headers: this.headers,
            params: this.getParams()
        }));
    },
    /**
     * @private
     */
    onSuccess: function(response) {
        var result = this.processResponse(response),
            form = this.form,
            formActive = form && !form.destroying && !form.destroyed;
        if (result === true || !result.success || !result.data) {
            this.failureType = Ext.form.action.Action.LOAD_FAILURE;
            if (formActive) {
                form.afterAction(this, false);
            }
            return;
        }
        if (formActive) {
            form.clearInvalid();
            form.setValues(result.data);
            form.afterAction(this, true);
        }
    },
    /**
     * @private
     */
    handleResponse: function(response) {
        var reader = this.form.reader,
            rs, data;
        if (reader) {
            rs = reader.read(response);
            data = rs.records && rs.records[0] ? rs.records[0].data : null;
            return {
                success: rs.success,
                data: data
            };
        }
        return Ext.decode(response.responseText);
    }
});

/**
 * A class which handles submission of data from {@link Ext.form.Basic Form}s and processes
 * the returned response.
 *
 * Instances of this class are only created by a {@link Ext.form.Basic Form} when
 * {@link Ext.form.Basic#submit submit}ting.
 *
 * # Response Packet Criteria
 *
 * A response packet may contain:
 *
 *   - **`success`** property : Boolean - required.
 *
 *   - **`errors`** property : Object - optional, contains error messages for invalid fields.
 *
 * # JSON Packets
 *
 * By default, response packets are assumed to be JSON, so a typical response packet may look
 * like this:
 *
 *     {
 *         success: false,
 *         errors: {
 *             clientCode: "Client not found",
 *             portOfLoading: "This field must not be null"
 *         }
 *     }
 *
 * Other data may be placed into the response for processing by the {@link Ext.form.Basic}'s
 * callback or event handler methods. The object decoded from this JSON is available in the
 * {@link Ext.form.action.Action#result result} property.
 *
 * Alternatively, if an {@link Ext.form.Basic#errorReader errorReader} is specified as an
 * {@link Ext.data.reader.Xml XmlReader}:
 *
 *     errorReader: new Ext.data.reader.Xml({
 *             record : 'field',
 *             success: '@success'
 *         }, [
 *             'id', 'msg'
 *         ]
 *     )
 *
 * then the results may be sent back in XML format:
 *
 *     <?xml version="1.0" encoding="UTF-8"?>
 *     <message success="false">
 *     <errors>
 *         <field>
 *             <id>clientCode</id>
 *             <msg>
 *                 <![CDATA[
 *                     Code not found. <br />
 *                     <i>This is a test validation message from the server </i>
 *                 ]]>
 *             </msg>
 *         </field>
 *         <field>
 *             <id>portOfLoading</id>
 *             <msg>
 *                 <![CDATA[
 *                     Port not found. <br />
 *                     <i>This is a test validation message from the server </i>
 *                 ]]>
 *             </msg>
 *         </field>
 *     </errors>
 *     </message>
 *
 * Other elements may be placed into the response XML for processing by the {@link Ext.form.Basic}'s
 * callback or event handler methods. The XML document is available in the
 * {@link Ext.form.Basic#errorReader errorReader}'s {@link Ext.data.reader.Xml#xmlData xmlData}
 * property.
 */
Ext.define('Ext.form.action.Submit', {
    extend: 'Ext.form.action.Action',
    alternateClassName: 'Ext.form.Action.Submit',
    alias: 'formaction.submit',
    type: 'submit',
    /**
     * @cfg {Boolean} [clientValidation=true]
     * Determines whether a Form's fields are validated in a final call
     * to {@link Ext.form.Basic#isValid isValid} prior to submission. Pass false in the Form's
     * submit options to prevent this.
     */
    run: function() {
        var me = this,
            form = me.form;
        if (me.clientValidation === false || form.isValid()) {
            me.doSubmit();
        } else {
            // client validation failed
            me.failureType = Ext.form.action.Action.CLIENT_INVALID;
            form.afterAction(me, false);
        }
    },
    /**
     * @private
     * Performs the submit of the form data.
     */
    doSubmit: function() {
        var me = this,
            ajaxOptions = Ext.apply(me.createCallback(), {
                url: me.getUrl(),
                method: me.getMethod(),
                headers: me.headers
            }),
            form = me.form,
            jsonSubmit = me.jsonSubmit || form.jsonSubmit,
            paramsProp = jsonSubmit ? 'jsonData' : 'params',
            formInfo;
        // For uploads we need to create an actual form that contains the file upload fields,
        // and pass that to the ajax call so it can do its iframe-based submit method.
        if (form.hasUpload()) {
            formInfo = me.buildForm();
            ajaxOptions.form = formInfo.formEl;
            ajaxOptions.isUpload = true;
        } else {
            ajaxOptions[paramsProp] = me.getParams(jsonSubmit);
        }
        Ext.Ajax.request(ajaxOptions);
        if (formInfo) {
            me.cleanup(formInfo);
        }
    },
    cleanup: function(formInfo) {
        var formEl = formInfo.formEl,
            uploadEls = formInfo.uploadEls,
            uploadFields = formInfo.uploadFields,
            len = uploadFields.length,
            i, field;
        for (i = 0; i < len; ++i) {
            field = uploadFields[i];
            if (!field.clearOnSubmit) {
                field.restoreInput(uploadEls[i]);
            }
        }
        if (formEl) {
            Ext.removeNode(formEl);
        }
    },
    /**
     * @private
     * Builds the full set of parameters from the field values plus any additional
     * configured params.
     */
    getParams: function(useModelValues) {
        var falseVal = false,
            configParams = this.callParent(),
            fieldParams;
        fieldParams = this.form.getValues(falseVal, falseVal, this.submitEmptyText !== falseVal, useModelValues, /* isSubmitting */
        true);
        return Ext.apply({}, fieldParams, configParams);
    },
    /**
     * @private
     * Builds a form element containing fields corresponding to all the parameters to be
     * submitted (everything returned by {@link #getParams}.
     *
     * NOTE: the form element is automatically added to the DOM, so any code that uses
     * it must remove it from the DOM after finishing with it.
     *
     * @return {HTMLElement}
     */
    buildForm: function() {
        var me = this,
            fieldsSpec = [],
            formSpec, formEl,
            basicForm = me.form,
            params = me.getParams(),
            uploadFields = [],
            uploadEls = [],
            fields = basicForm.getFields().items,
            i,
            len = fields.length,
            field, key, value, v, vLen, el;
        for (i = 0; i < len; ++i) {
            field = fields[i];
            if (field.isFileUpload()) {
                uploadFields.push(field);
            }
        }
        for (key in params) {
            if (params.hasOwnProperty(key)) {
                value = params[key];
                if (Ext.isArray(value)) {
                    vLen = value.length;
                    for (v = 0; v < vLen; v++) {
                        fieldsSpec.push(me.getFieldConfig(key, value[v]));
                    }
                } else {
                    fieldsSpec.push(me.getFieldConfig(key, value));
                }
            }
        }
        formSpec = {
            tag: 'form',
            role: 'presentation',
            action: me.getUrl(),
            method: me.getMethod(),
            target: me.target ? (Ext.isString(me.target) ? me.target : Ext.fly(me.target).dom.name) : '_self',
            style: 'display:none',
            cn: fieldsSpec
        };
        if (!formSpec.target) {
            Ext.raise('Invalid form target.');
        }
        // Set the proper encoding for file uploads
        if (uploadFields.length) {
            formSpec.encoding = formSpec.enctype = 'multipart/form-data';
        }
        // Create the form
        formEl = Ext.DomHelper.append(Ext.getBody(), formSpec);
        // Special handling for file upload fields: since browser security measures prevent setting
        // their values programatically, and prevent carrying their selected values over
        // when cloning, we have to move the actual field instances out of their components
        // and into the form.
        len = uploadFields.length;
        for (i = 0; i < len; ++i) {
            el = uploadFields[i].extractFileInput();
            formEl.appendChild(el);
            uploadEls.push(el);
        }
        return {
            formEl: formEl,
            uploadFields: uploadFields,
            uploadEls: uploadEls
        };
    },
    getFieldConfig: function(name, value) {
        return {
            tag: 'input',
            type: 'hidden',
            name: name,
            value: Ext.String.htmlEncode(value)
        };
    },
    /**
     * @private
     */
    onSuccess: function(response) {
        var form = this.form,
            formActive = form && !form.destroying && !form.destroyed,
            success = true,
            result = this.processResponse(response);
        if (result !== true && !result.success) {
            if (result.errors && formActive) {
                form.markInvalid(result.errors);
            }
            this.failureType = Ext.form.action.Action.SERVER_INVALID;
            success = false;
        }
        if (formActive) {
            form.afterAction(this, success);
        }
    },
    /**
     * @private
     */
    handleResponse: function(response) {
        var form = this.form,
            errorReader = form.errorReader,
            rs, errors, i, len, records, result;
        if (errorReader) {
            rs = errorReader.read(response);
            records = rs.records;
            errors = [];
            if (records) {
                for (i = 0 , len = records.length; i < len; i++) {
                    errors[i] = records[i].data;
                }
            }
            if (errors.length < 1) {
                errors = null;
            }
            result = {
                success: rs.success,
                errors: errors
            };
        } else {
            try {
                result = Ext.decode(response.responseText);
            } catch (e) {
                result = {
                    success: false,
                    errors: []
                };
            }
        }
        return result;
    }
});

/**
 * A class which handles submission of data from {@link Ext.form.Basic Form}s using a standard
 * `<form>` element submit. It does not handle the response from the submit.
 *
 * If validation of the form fields fails, the Form's afterAction method will be called.
 * Otherwise, afterAction will not be called.
 *
 * Instances of this class are only created by a {@link Ext.form.Basic Form} when
 * {@link Ext.form.Basic#submit submit}ting, when the form's {@link Ext.form.Basic#standardSubmit}
 * config option is true.
 */
Ext.define('Ext.form.action.StandardSubmit', {
    extend: 'Ext.form.action.Submit',
    alias: 'formaction.standardsubmit',
    /**
     * @cfg {String} target
     * Optional target attribute to be used for the form when submitting.
     * 
     * Defaults to the current window/frame.
     */
    /**
     * @private
     * Perform the form submit. Creates and submits a temporary form element containing
     * an input element for each field value returned by {@link Ext.form.Basic#getValues},
     * plus any configured {@link #params params} or {@link Ext.form.Basic#baseParams baseParams}.
     */
    doSubmit: function() {
        var formInfo = this.buildForm();
        formInfo.formEl.submit();
        this.cleanup(formInfo);
    }
});

/**
 * A subclass of Ext.dd.DragTracker which handles dragging any Component.
 *
 * This is configured with a Component to be made draggable, and a config object for the
 * {@link Ext.dd.DragTracker}
 * class.
 *
 * A {@link #delegate} may be provided which may be either the element to use as the mousedown
 * target or a CSS selector to activate multiple mousedown targets.
 *
 * When the Component begins to be dragged, its `beginDrag` method will be called if implemented.
 *
 * When the drag ends, its `endDrag` method will be called if implemented.
 */
Ext.define('Ext.util.ComponentDragger', {
    extend: 'Ext.dd.DragTracker',
    /**
     * @cfg {Boolean} constrain
     * Specify as `true` to constrain the Component to within the bounds of the {@link #constrainTo}
     * region.
     */
    /**
     * @cfg {String/Ext.dom.Element} delegate
     * A CSS selector which identifies child elements within the Component's encapsulating
     * Element which are the drag handles. This limits dragging to only begin when the matching
     * elements are mousedowned.
     *
     * This may also be a specific child element within the Component's encapsulating element
     * to use as the drag handle.
     */
    /**
     * @cfg {Boolean} constrainDelegate
     * Specify as `true` to constrain the drag handles within the {@link #constrainTo} region.
     */
    /**
     * @cfg {Boolean} [liveDrag=false]
     * @member Ext.Component
     * True to drag the component itself.  Else a lightweight version of the component
     * will be shown (_using the component's ghost() method_).
     * 
     * **Note:** This config is only relevant when used with dragging implemented via
     * {@link Ext.util.ComponentDragger}.
     */
    autoStart: 500,
    /**
     * Creates new ComponentDragger.
     * @param {Object} comp The Component to provide dragging for.
     * @param {Object} [config] Config object
     */
    constructor: function(comp, config) {
        this.comp = comp;
        this.initialConstrainTo = config.constrainTo;
        this.callParent([
            config
        ]);
    },
    onStart: function(e) {
        var me = this,
            comp = me.comp;
        // ComponentDragger is always dragging the component.
        // The superclass uses the delegated handle as the
        // drag target and the target's start region.
        me.dragTarget = me.el;
        me.startRegion = me.el.getRegion();
        // Cache the start [X, Y] array
        me.startPosition = comp.getXY();
        // If client Component has a ghost method to show a lightweight version of itself
        // then use that as a drag proxy unless configured to liveDrag.
        if (comp.ghost && !comp.liveDrag) {
            me.proxy = comp.ghost();
        }
        // Set the constrainTo Region before we start dragging.
        if (me.constrain || me.constrainDelegate) {
            me.constrainTo = me.calculateConstrainRegion();
        }
        if (comp.beginDrag) {
            comp.beginDrag();
        }
        // We should cover all iframes to avoid them stealing our current drag event
        Ext.dom.Element.maskIframes();
    },
    calculateConstrainRegion: function() {
        var me = this,
            comp = me.comp,
            constrainTo = me.initialConstrainTo || me.comp.el.dom.parentNode,
            constraintInsets = comp.constraintInsets,
            constrainEl, delegateRegion, elRegion,
            dragEl = me.proxy ? me.proxy.el : comp.el,
            shadow = dragEl.shadow,
            shadowSize = (shadow && !me.constrainDelegate && comp.constrainShadow && !shadow.disabled) ? shadow.getShadowSize() : 0;
        // The configured constrainTo might be a Region or an element
        if (!(constrainTo instanceof Ext.util.Region)) {
            constrainEl = Ext.fly(constrainTo);
            // draggable components are constrained to the area inside the borders of
            // their floatParent, but not inside the padding
            constrainTo = constrainEl.getConstrainRegion();
        } else {
            // Create a clone so we don't modify the original
            constrainTo = constrainTo.copy();
        }
        // Apply constraintInsets
        if (constraintInsets) {
            constraintInsets = Ext.isObject(constraintInsets) ? constraintInsets : Ext.Element.parseBox(constraintInsets);
            constrainTo.adjust(constraintInsets.top, constraintInsets.right, constraintInsets.bottom, constraintInsets.left);
        }
        // Reduce the constrain region to allow for shadow
        if (shadowSize) {
            constrainTo.adjust(shadowSize[0], -shadowSize[1], -shadowSize[2], shadowSize[3]);
        }
        // If they only want to constrain the *delegate* to within the constrain region,
        // adjust the region to be larger based on the insets of the delegate from the outer
        // edges of the Component.
        if (me.constrainDelegate) {
            delegateRegion = Ext.fly(me.handle).getRegion();
            elRegion = dragEl.getRegion();
            constrainTo.adjust(elRegion.top - delegateRegion.top, elRegion.right - delegateRegion.right, elRegion.bottom - delegateRegion.bottom, elRegion.left - delegateRegion.left);
        }
        return constrainTo;
    },
    // Move either the ghost Component or the target Component to its new position on drag
    onDrag: function(e) {
        var me = this,
            comp = (me.proxy && !me.comp.liveDrag) ? me.proxy : me.comp,
            offset = me.getOffset(me.constrain || me.constrainDelegate ? 'dragTarget' : null);
        comp.setPagePosition(me.startPosition[0] + offset[0], me.startPosition[1] + offset[1]);
    },
    onEnd: function(e) {
        var comp = this.comp;
        if (comp.destroyed || comp.destroying) {
            return;
        }
        if (this.proxy && !comp.liveDrag) {
            comp.unghost();
        }
        if (comp.endDrag) {
            comp.endDrag();
        }
        // We should uncover all iframes
        Ext.dom.Element.unmaskIframes();
    }
});

/**
 * A specialized panel intended for use as an application window. Windows are floated,
 * {@link #resizable}, and {@link #cfg-draggable} by default. Windows can be
 * {@link #maximizable maximized} to fill the viewport, restored to their prior size, and can be
 * {@link #method-minimize}d.
 *
 * Windows can also be linked to a {@link Ext.ZIndexManager} or managed by the
 * {@link Ext.WindowManager} to provide grouping, activation, to front, to back and other
 * application-specific behavior.
 *
 * By default, Windows will be rendered to document.body. To {@link #constrain} a Window to another
 * element specify {@link Ext.Component#renderTo renderTo}.
 *
 * **As with all {@link Ext.container.Container Container}s, it is important to consider
 * how you want the Window to size and arrange any child Components. Choose an appropriate
 * {@link #layout} configuration which lays out child Components in the required manner.**
 *
 *     @example
 *     Ext.create('Ext.window.Window', {
 *         title: 'Hello',
 *         height: 200,
 *         width: 400,
 *         layout: 'fit',
 *         items: {  // Let's put an empty grid in just to illustrate fit layout
 *             xtype: 'grid',
 *             border: false,
 *             // One header just for show. There's no data
 *             columns: [{ header: 'World' }],
 *             store: Ext.create('Ext.data.ArrayStore', {}) // A dummy empty data store
 *         }
 *     }).show();
 */
Ext.define('Ext.window.Window', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.window',
    alternateClassName: 'Ext.Window',
    requires: [
        'Ext.util.ComponentDragger',
        'Ext.util.Region'
    ],
    /**
     * @cfg {Number} x
     * The X position of the left edge of the window on initial showing. Defaults to centering
     * the Window within the width of the Window's container {@link Ext.dom.Element Element}
     * (The Element that the Window is rendered to).
     */
    /**
     * @cfg {Number} y
     * The Y position of the top edge of the window on initial showing. Defaults to centering
     * the Window within the height of the Window's container {@link Ext.dom.Element Element}
     * (The Element that the Window is rendered to).
     */
    /**
     * @cfg {String/Ext.dom.Element/Ext.Component/Boolean} [animateTarget=null]
     * Id, Component element, or Component from which the window should animate when
     * shown or hidden.
     *
     * You may also pass true to have the Window animate when maximizing and restoring
     * using the maximize / restore tools created via the {@link #maximizable} config.
     *
     *     var btn, win;
     *
     *     btn = Ext.create({
     *         xtype: 'button',
     *         renderTo: Ext.getBody(),
     *         text: 'Show Window',
     *         handler: function() {
     *             win.show();
     *         }
     *     });
     *
     *     win = Ext.create({
     *         xtype: 'window',
     *         title: 'Animate from the Show Window Button',
     *         height: 300,
     *         width: 400,
     *         modal: true,
     *         closeAction: 'hide',
     *         animateTarget: btn
     *         // or btn.getId()
     *         // or btn.getEl()
     *         // or true (when maximizable is true)
     *     });
     */
    /**
     * @cfg {Boolean/Function} ghost
     * Set to false to disable the ghost panel during dragging the window.
     * Do note that you should not set this to true, by default it is a function.
     */
    /**
     * @cfg {String/Number/Ext.Component} defaultFocus
     * Specifies a Component to receive focus when this Window is focused.
     *
     * If a String is provided, the Component will be resolved using the {@link #down} method
     * which uses {@link Ext.ComponentQuery}. If the string begins with an alphanumeric value,
     * it will first attempt to find the Component based on the {@link Ext.Component#id} or
     * {@link Ext.Component#itemId}. If a matching component is not found via id, then an attempt
     * to do a query to find a matching component.
     *
     * An example of finding the Component with an id/itemId:
     *
     *     Ext.create('Ext.window.Window', {
     *         autoShow     : true,
     *         width        : 300,
     *         title        : 'Login',
     *         defaultFocus : 'username',
     *         items        : [
     *             {
     *                 xtype      : 'textfield',
     *                 fieldLabel : 'Username',
     *                 itemId     : 'username',
     *                 name       : 'username'
     *             },
     *             {
     *                 xtype      : 'textfield',
     *                 inputType  : 'password',
     *                 fieldLabel : 'Password',
     *                 itemId     : 'password',
     *                 name       : 'password'
     *             }
     *         ]
     *     });
     *
     * If a Number is provided, this will resolve an {@link Ext.button.Button} at that index.
     * This is very useful if the window has buttons in the {@link #buttons} config and you want
     * to provide default focus to one of them.
     *
     * An example of this would be:
     *
     *     Ext.create('Ext.window.Window', {
     *         autoShow     : true,
     *         width        : 300,
     *         title        : 'Login',
     *         defaultFocus : 1,
     *         items        : [
     *             {
     *                 xtype      : 'textfield',
     *                 fieldLabel : 'Username',
     *                 name       : 'username'
     *            },
     *            {
     *                 xtype      : 'textfield',
     *                 inputType  : 'password',
     *                 fieldLabel : 'Password',
     *                 name       : 'password'
     *             }
     *         ],
     *         buttons      : [
     *             {
     *                 text : 'Cancel'
     *             },
     *             {
     *                 text : 'Login'
     *             }
     *         ]
     *     });
     *
     * In summary, defaultFocus may be one of:
     *
     *   - The index of a footer Button.
     *   - The id or {@link Ext.Component#itemId} of a descendant Component.
     *   - A {@link Ext.ComponentQuery query} to find a {@link Ext.Component}.
     *   - A descendant {@link Ext.Component}.
     */
    /**
     * @cfg {Function} onEsc
     * Allows override of the built-in processing for the escape key. Default action is to close
     * the Window (performing whatever action is specified in {@link #closeAction}. To prevent
     * the Window closing when the escape key is pressed, specify this as
     * {@link Ext#emptyFn Ext.emptyFn}.
     */
    /**
     * @cfg {Boolean} [collapsed=false]
     * True to render the window collapsed, false to render it expanded. Note that if
     * {@link #expandOnShow} is true (the default) it will override the `collapsed` config
     * and the window will always be expanded when shown.
     */
    /**
     * @cfg {Boolean} [maximized=false]
     * True to initially display the window in a maximized state.
     */
    /**
     * @cfg {Boolean} [hideShadowOnDeactivate=false]
     * True to hide this Window's shadow when another floating item in the same z-index stack
     * is activated.
     */
    /**
    * @cfg {String} baseCls
    * The base CSS class to apply to this panel's element.
    */
    baseCls: Ext.baseCSSPrefix + 'window',
    /**
     * @cfg {Boolean/Object} resizable
     * Specify as `true` to allow user resizing at each edge and corner of the window,
     * false to disable resizing.
     *
     * This may also be specified as a config object to Ext.resizer.Resizer
     */
    resizable: true,
    /**
     * @cfg {Boolean} draggable
     * True to allow the window to be dragged by the header bar, false to disable dragging.
     * Note that by default the window will be centered in the viewport, so if dragging is disabled
     * the window may need to be positioned programmatically after render (e.g.,
     * `myWindow.setPosition(100, 100);`).
     */
    draggable: true,
    /**
     * @cfg {Boolean} constrain
     * True to constrain the window within its containing element, false to allow it to fall outside
     * of its containing element. By default the window will be rendered to `document.body`.
     * To render and constrain the window within another element specify {@link #renderTo}.
     * Optionally the header only can be constrained using {@link #constrainHeader}.
     */
    constrain: false,
    /**
     * @cfg {Boolean} constrainHeader
     * True to constrain the window header within its containing element (allowing the window body
     * to fall outside of its containing element) or false to allow the header to fall outside
     * its containing element. Optionally the entire window can be constrained using
     * {@link #constrain}.
     */
    constrainHeader: false,
    /**
     * @cfg simpleDrag
     * @hide
     */
    /**
     * @cfg {Boolean} plain
     * True to render the window body with a transparent background so that it will blend into
     * the framing elements, false to add a lighter background color to visually highlight the body
     * element and separate it more distinctly from the surrounding frame.
     */
    plain: false,
    /**
     * @cfg {Boolean} minimizable
     * True to display the 'minimize' tool button and allow the user to minimize the window,
     * false to hide the button and disallow minimizing the window. Note that this button provides
     * no implementation -- the behavior of minimizing a window is implementation-specific,
     * so the minimize event must be handled and a custom minimize behavior implemented
     * for this option to be useful.
     */
    minimizable: false,
    /**
     * @cfg {Boolean} maximizable
     * True to display the 'maximize' tool button and allow the user to maximize the window,
     * false to hide the button and disallow maximizing the window. Note that when a window
     * is maximized, the tool button will automatically change to a 'restore' button with
     * the appropriate behavior already built-in that will restore the window to its previous size.
     */
    maximizable: false,
    /**
     * @cfg minHeight
     * @inheritdoc
     */
    minHeight: 50,
    /**
     * @cfg minWidth
     * @inheritdoc
     */
    minWidth: 50,
    /**
     * @cfg {Boolean} expandOnShow
     * True to always expand the window when it is displayed, false to keep it in its current state
     * (which may be {@link #collapsed}) when displayed.
     */
    expandOnShow: true,
    /**
     * @cfg collapsible
     * @inheritdoc
     */
    collapsible: false,
    /**
     * @cfg {Boolean} closable
     * True to display the 'close' tool button and allow the user to close the window, false
     * to hide the button and disallow closing the window.
     *
     * By default, when close is requested by either clicking the close button in the header
     * or pressing ESC when the Window has focus, the {@link #method-close} method will be called.
     * This will _{@link Ext.Component#method-destroy destroy}_ the Window and its content
     * meaning that it may not be reused.
     *
     * To make closing a Window _hide_ the Window so that it may be reused, set {@link #closeAction}
     * to 'hide'.
     */
    closable: true,
    /**
     * @cfg {Boolean} monitorResize
     * `true` to listen to the viewport resize event and perform any layout updating if necessary.
     * This is useful if using sizes as percentages for the window.
     */
    /**
     * @cfg {Boolean} hidden
     * Render this Window hidden. If `true`, the {@link #method-hide} method will be called
     * internally.
     */
    hidden: true,
    /**
     * @cfg autoRender
     * @inheritdoc
     * Windows render to the body on first show.
     */
    autoRender: true,
    /**
     * @cfg hideMode
     * @inheritdoc
     * Windows hide using offsets in order to preserve the scroll positions of their descendants.
     * You may review other configuration options here: {@link Ext.Component#hideMode}.
     */
    hideMode: 'offsets',
    /**
     * @cfg floating
     * @inheritdoc Ext.Component#cfg!floating
     */
    floating: true,
    /**
     * @cfg alignOnScroll
     * @inheritdoc
     */
    alignOnScroll: false,
    /**
     * @cfg stateEvents
     * @inheritdoc Ext.state.Stateful#cfg-stateEvents
     * @localdoc By default the following stateEvents are added:
     *
     *  - {@link #event-resize} - _(added by Ext.Component)_
     *  - {@link #event-collapse} - _(added by Ext.panel.Panel)_
     *  - {@link #event-expand} - _(added by Ext.panel.Panel)_
     *  - {@link #event-maximize}
     *  - {@link #event-restore}
     *  - {@link #event-resize}
     *  - {@link #event-dragend}
     */
    itemCls: Ext.baseCSSPrefix + 'window-item',
    /**
     * @cfg overlapHeader
     * @inheritdoc
     */
    overlapHeader: true,
    ignoreHeaderBorderManagement: true,
    // Flag to Renderable to always look up the framing styles for this Component
    alwaysFramed: true,
    // Buffer this so we don't recreate the same object
    isRootCfg: {
        isRoot: true
    },
    /**
     * @property {Boolean} isWindow
     * `true` in this class to identify an object as an instantiated Window, or subclass thereof.
     */
    isWindow: true,
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'dialog',
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: true,
    /**
     * @cfg tabGuard
     * @inheritdoc
     */
    tabGuard: true,
    /**
     * @cfg closeToolText
     * @inheritdoc
     */
    closeToolText: 'Close dialog',
    /**
     * @cfg keyMap
     * @inheritdoc
     */
    keyMap: {
        scope: 'this',
        ESC: 'onEsc'
    },
    /**
     * @cfg {String} maskClickAction
     * The method to call when the window's modal mask is clicked or tapped:
     *
     * - **`'{@link #method-focus}'`** :
     *
     *   The default. Focus the window, which will then pass focus into its
     *   {@link #cfg-defaultFocus} delegate.
     *
     * - **`'{@link #method-destroy}'`** :
     *
     *   Remove  the window from the DOM and {@link Ext.Component#method-destroy destroy}
     *   it and all descendant Components. The window will **not** be available to be redisplayed
     *   via the {@link #method-show} method.
     *
     * - **`'{@link #method-hide}'`** :
     *
     *   {@link #method-hide} the window by setting visibility to hidden and applying negative
     *   offsets. The window will be available to be redisplayed via the {@link #method-show}
     *   method.
     *   @since 6.2.0
     */
    maskClickAction: 'focus',
    /**
     * @event activate
     * Fires after the window has been visually activated via {@link #setActive}.
     * @param {Ext.window.Window} this
     */
    /**
     * @event deactivate
     * Fires after the window has been visually deactivated via {@link #setActive}.
     * @param {Ext.window.Window} this
     */
    /**
     * @event maskclick
     * Fires when this Window's modal mask is clicked or tapped. Returning `false` from 
     * a handler will veto the subsequent preocessing of the {@link #cfg-maskClickAction}..
     * @param {Ext.window.Window} this
     */
    /**
     * @event resize
     * Fires after the window has been resized.
     * @param {Ext.window.Window} this
     * @param {Number} width The window's new width
     * @param {Number} height The window's new height
     */
    /**
     * @event maximize
     * Fires after the window has been maximized.
     * @param {Ext.window.Window} this
     */
    /**
     * @event minimize
     * Fires after the window has been minimized.
     * @param {Ext.window.Window} this
     */
    /**
     * @event restore
     * Fires after the window has been restored to its original size after being maximized.
     * @param {Ext.window.Window} this
     */
    disableCloseToolFocus: true,
    /**
     * @private
     */
    initComponent: function() {
        var me = this;
        // Explicitly set frame to false, since alwaysFramed is
        // true, we only want to lookup framing in a specific instance
        me.frame = false;
        me.callParent();
        if (me.plain) {
            me.addClsWithUI('plain');
        }
        me.addStateEvents([
            'maximize',
            'restore',
            'resize',
            'dragend'
        ]);
    },
    getElConfig: function() {
        var me = this,
            elConfig;
        elConfig = me.callParent();
        elConfig.tabIndex = -1;
        return elConfig;
    },
    /**
     * @protected
     * Returns the focus holder element associated with this Window.
     * By default, this is the Window's element; this can be overridden
     * by setting {@link #defaultFocus} property.
     *
     * @return {Ext.dom.Element/Ext.Component} the focus holding element or Component.
     */
    getFocusEl: function() {
        return this.getDefaultFocus() || this.el;
    },
    // State Management
    /**
     * @private
     */
    getState: function() {
        var me = this,
            state = me.callParent() || {},
            maximized = !!me.maximized,
            ghostBox = me.ghostBox,
            pos;
        state.maximized = maximized;
        if (maximized) {
            pos = me.restorePos;
        } else if (ghostBox) {
            // If we're animating a show, it will be from offscreen, so
            // grab the position from the final box
            pos = [
                ghostBox.x,
                ghostBox.y
            ];
        } else {
            pos = me.getPosition(true);
        }
        Ext.apply(state, {
            size: maximized ? me.restoreSize : me.getSize(),
            pos: pos
        });
        return state;
    },
    applyState: function(state) {
        var me = this;
        if (state) {
            me.maximized = state.maximized;
            if (me.maximized) {
                me.hasSavedRestore = true;
                me.restoreSize = state.size;
                me.restorePos = state.pos;
            } else {
                Ext.apply(me, {
                    width: state.size.width,
                    height: state.size.height,
                    x: state.pos[0],
                    y: state.pos[1]
                });
            }
        }
    },
    onRender: function(ct, position) {
        var me = this;
        me.callParent(arguments);
        // Single clicking a header will focus the defaultFocus child
        if (me.header) {
            me.header.on({
                scope: me,
                click: me.onHeaderClick
            });
        }
        // Double clicking a header will toggleMaximize
        if (me.maximizable) {
            me.header.on({
                scope: me,
                dblclick: me.toggleMaximize
            });
        }
    },
    afterRender: function() {
        var me = this,
            header = me.header;
        // Initialize
        if (me.maximized) {
            me.maximized = false;
            me.maximize(null, true);
            if (header) {
                header.removeCls(header.indicateDragCls);
            }
        }
        me.callParent();
        me.initTabGuards();
    },
    /**
     * @private
     */
    onEsc: function(e) {
        if (this.closable) {
            e.stopEvent();
            this.close();
            return false;
        }
    },
    doDestroy: function() {
        var me = this;
        if (me.rendered) {
            Ext.un('resize', me.onWindowResize, me);
            delete me.animateTarget;
            me.hide();
        }
        me.callParent();
    },
    /**
     * @private
     * Contribute class-specific tools to the header.
     *
     * Called by Panel's initTools at initialization time.
     *
     * Implementations should jst add new tool config objects to `this.tools`
     */
    addTools: function() {
        var me = this,
            tools = [];
        // Call Panel's addTools
        me.callParent();
        if (me.minimizable) {
            tools.push({
                type: 'minimize',
                handler: 'minimize',
                scope: me
            });
        }
        if (me.maximizable) {
            tools.push({
                type: 'maximize',
                handler: 'toggleMaximize',
                scope: me
            });
        }
        if (tools.length) {
            me.addTool(tools);
        }
    },
    addTool: function(tools) {
        var me = this;
        me.callParent([
            tools
        ]);
        if (me.rendered && me.tabGuard) {
            me.initTabGuards();
        }
    },
    add: function() {
        var me = this,
            ret;
        ret = me.callParent(arguments);
        if (me.rendered && me.tabGuard) {
            me.initTabGuards();
        }
        return ret;
    },
    remove: function() {
        var me = this,
            ret;
        ret = me.callParent(arguments);
        if (me.rendered && me.tabGuard) {
            me.initTabGuards();
        }
        return ret;
    },
    addDocked: function() {
        var me = this,
            ret;
        ret = me.callParent(arguments);
        if (me.rendered && me.tabGuard) {
            me.initTabGuards();
        }
        return ret;
    },
    removeDocked: function() {
        var me = this,
            ret;
        ret = me.callParent(arguments);
        if (me.rendered && me.tabGuard) {
            me.initTabGuards();
        }
        return ret;
    },
    onShow: function() {
        var me = this;
        me.callParent(arguments);
        if (me.expandOnShow) {
            me.expand(false);
        }
        me.syncMonitorWindowResize();
        if (me.rendered && me.tabGuard) {
            me.initTabGuards();
        }
    },
    /**
     * @private
     */
    doClose: function() {
        var me = this;
        // Being called as callback after going through the hide call below
        if (me.hidden) {
            me.fireEvent('close', me);
            // This method can be called from hide() which in turn can be called
            // from destroy()
            if (me.closeAction === 'destroy' && !me.destroying && !me.destroyed) {
                me.destroy();
            }
        } else {
            // close after hiding
            me.hide(me.animateTarget, me.doClose, me);
        }
    },
    /**
     * @private
     */
    afterHide: function() {
        var me = this;
        // No longer subscribe to resizing now that we're hidden
        me.syncMonitorWindowResize();
        // Perform superclass's afterHide tasks.
        me.callParent(arguments);
        // Hide may have destroyed a Window.
        if (!me.destroyed && me.rendered && me.tabGuard) {
            me.initTabGuards();
        }
    },
    /**
     * @private
     */
    onWindowResize: function() {
        var me = this,
            sizeModel;
        // This is called on a timer. Window may have been destroyed in the interval.
        if (!me.destroyed) {
            if (me.maximized) {
                me.fitContainer();
            } else {
                sizeModel = me.getSizeModel();
                if (sizeModel.width.natural || sizeModel.height.natural) {
                    me.updateLayout();
                }
                me.doConstrain();
            }
        }
    },
    /**
     * Placeholder method for minimizing the window. By default, this method simply fires the
     * {@link #event-minimize} event since the behavior of minimizing a window
     * is application-specific. To implement custom minimize behavior, either the minimize event
     * can be handled or this method can be overridden.
     * @return {Ext.window.Window} this
     */
    minimize: function() {
        this.fireEvent('minimize', this);
        return this;
    },
    resumeHeaderLayout: function(changed) {
        this.header.resumeLayouts(changed ? this.isRootCfg : null);
    },
    afterCollapse: function() {
        var me = this,
            header = me.header,
            tools = me.tools;
        if (header && me.maximizable) {
            header.suspendLayouts();
            tools.maximize.hide();
            this.resumeHeaderLayout(true);
        }
        if (me.resizer) {
            me.resizer.disable();
        }
        me.callParent(arguments);
    },
    afterExpand: function() {
        var me = this,
            header = me.header,
            tools = me.tools,
            changed;
        if (header) {
            header.suspendLayouts();
            if (me.maximizable) {
                tools.maximize.show();
                changed = true;
            }
            this.resumeHeaderLayout(changed);
        }
        if (me.resizer) {
            me.resizer.enable();
        }
        me.callParent(arguments);
    },
    /**
     * Fits the window within its current container and automatically replaces the
     * {@link #maximizable 'maximize' tool button} with the 'restore' tool button.
     * Also see {@link #toggleMaximize}.
     * @param {Boolean} [animate=false] Pass `true` to animate this Window to full size.
     * @param {Boolean} initial (private)
     * @return {Ext.window.Window} this
     */
    maximize: function(animate, initial) {
        var me = this,
            header = me.header,
            tools = me.tools,
            width = me.width,
            height = me.height,
            changed;
        if (!me.maximized && !me.maximizing) {
            me.maximizing = true;
            me.expand(false);
            if (!me.hasSavedRestore) {
                me.restoreSize = {
                    width: width ? width : null,
                    height: height ? height : null
                };
                // If we're not positioned yet, default back to 0, 0
                if (initial) {
                    me.restorePos = [
                        me.x || 0,
                        me.y || 0
                    ];
                } else {
                    me.restorePos = me.getPosition();
                }
            }
            // Manipulate visibility of header tools if there is a header
            if (header) {
                header.suspendLayouts();
                if (tools.maximize) {
                    tools.maximize.setType('restore');
                }
                if (me.collapseTool) {
                    me.collapseTool.hide();
                    changed = true;
                }
                me.resumeHeaderLayout(changed);
            }
            me.el.disableShadow();
            if (me.dd) {
                me.dd.disable();
                if (header) {
                    header.removeCls(header.indicateDragCls);
                }
            }
            if (me.resizer) {
                me.resizer.disable();
            }
            me.el.addCls(Ext.baseCSSPrefix + 'window-maximized');
            me.container.addCls(Ext.baseCSSPrefix + 'window-maximized-ct');
            me.syncMonitorWindowResize();
            me.fitContainer(animate = (animate || !!me.animateTarget) ? {
                callback: function() {
                    me.maximizing = false;
                    me.maximized = true;
                    if (!initial) {
                        me.fireEvent('maximize', me);
                    }
                }
            } : null);
            if (!animate) {
                me.maximizing = false;
                me.maximized = true;
                if (!initial) {
                    me.fireEvent('maximize', me);
                }
            }
        }
        return me;
    },
    /**
     * Restores a {@link #maximizable maximized} window back to its original size and position
     * prior to being maximized and also replaces the 'restore' tool button with the 'maximize'
     * tool button. Also see {@link #toggleMaximize}.
     * @param {Boolean} [animate=false] Pass `true` to animate the restore.
     * @return {Ext.window.Window} this
     */
    restore: function(animate) {
        var me = this,
            tools = me.tools,
            header = me.header,
            newBox = me.restoreSize,
            changed;
        if (me.maximized) {
            me.hasSavedRestore = null;
            me.removeCls(Ext.baseCSSPrefix + 'window-maximized');
            // Manipulate visibility of header tools if there is a header
            if (header) {
                header.suspendLayouts();
                if (tools.maximize) {
                    tools.maximize.setType('maximize');
                }
                if (me.collapseTool) {
                    me.collapseTool.show();
                    changed = true;
                }
                me.resumeHeaderLayout(changed);
            }
            // Restore the position/sizing
            newBox.x = me.restorePos[0];
            newBox.y = me.restorePos[1];
            me.setBox(newBox, animate = (animate || !!me.animateTarget) ? {
                callback: function() {
                    me.el.enableShadow(null, true);
                    me.maximized = false;
                    me.fireEvent('restore', me);
                }
            } : null);
            // Unset old position/sizing
            me.restorePos = me.restoreSize = null;
            // Allow users to drag and drop again
            if (me.dd) {
                me.dd.enable();
                if (header) {
                    header.addCls(header.indicateDragCls);
                }
            }
            if (me.resizer) {
                me.resizer.enable();
            }
            me.container.removeCls(Ext.baseCSSPrefix + 'window-maximized-ct');
            me.syncMonitorWindowResize();
            if (!animate) {
                me.el.enableShadow(null, true);
                me.maximized = false;
                me.fireEvent('restore', me);
            }
        }
        return me;
    },
    /**
     * Synchronizes the presence of our listener for window resize events. This method
     * should be called whenever this status might change.
     * @private
     */
    syncMonitorWindowResize: function() {
        var me = this,
            currentlyMonitoring = me._monitoringResize,
            // all the states where we should be listening to window resize:
            yes = me.monitorResize || me.constrain || me.constrainHeader || me.maximized,
            // all the states where we veto this:
            veto = me.hidden || me.destroying || me.destroyed;
        if (yes && !veto) {
            // we should be listening...
            if (!currentlyMonitoring) {
                // but we aren't, so set it up.
                // Delay so that we jump over any Viewport resize activity
                Ext.on('resize', me.onWindowResize, me, {
                    buffer: 1
                });
                me._monitoringResize = true;
            }
        } else if (currentlyMonitoring) {
            // we should not be listening, but we are, so tear it down
            Ext.un('resize', me.onWindowResize, me);
            me._monitoringResize = false;
        }
    },
    /**
     * A shortcut method for toggling between {@link #method-maximize} and {@link #method-restore}
     * based on the current maximized state of the window.
     * @return {Ext.window.Window} this
     */
    toggleMaximize: function() {
        return this[this.maximized ? 'restore' : 'maximize']();
    },
    createGhost: function() {
        var ghost = this.callParent(arguments);
        ghost.xtype = 'window';
        ghost.focusOnToFront = false;
        return ghost;
    },
    /**
     * Gets the configured default focus item.  If a {@link #defaultFocus} is set, it will
     * receive focus when the Window's `focus` method is called, otherwise the
     * Window itself will receive focus.
     */
    getDefaultFocus: function() {
        var me = this,
            result,
            defaultComp = me.defaultFocus,
            selector;
        if (defaultComp !== undefined) {
            // Number is index of Button
            if (Ext.isNumber(defaultComp)) {
                result = me.query('button')[defaultComp];
            }
            // String is ID or CQ selector
            else if (Ext.isString(defaultComp)) {
                selector = defaultComp;
                // Try id/itemId match if selector begins with alphanumeric
                // and is not compound xtype/id selector with # in the middle
                // (https://sencha.jira.com/browse/EXTJS-14925)
                if (Ext.validIdRe.test(selector)) {
                    result = me.down(Ext.makeIdSelector(selector));
                }
                // If not found, use as selector
                if (!result) {
                    result = me.down(selector);
                }
            }
            // Otherwise, if it's got a focus method, use it
            else if (defaultComp.focus) {
                result = defaultComp;
            }
        }
        return result;
    },
    privates: {
        // Override. Windows are always simple draggable, they do not use Ext.Panel.DDs
        // The dd property in a Window is always a ComponentDragger
        initDraggable: function() {
            /**
             * @property {Ext.util.ComponentDragger} dd
             * If this Window is configured {@link #cfg-draggable}, this property will contain
             * an instance of {@link Ext.util.ComponentDragger} (A subclass of
             * {@link Ext.dd.DragTracker DragTracker}) which handles dragging the Window's DOM
             * Element, and constraining according to the {@link #constrain} and
             * {@link #constrainHeader} .
             *
             * This has implementations of `onBeforeStart`, `onDrag` and `onEnd` which perform
             * the dragging action. If extra logic is needed at these points, use
             * {@link Ext.Function#createInterceptor createInterceptor} or
             * {@link Ext.Function#createSequence createSequence} to augment the existing
             * implementations.
             */
            this.initSimpleDraggable();
        },
        onHeaderClick: function(header, e) {
            var delegate;
            if (header.el.contains(e.getTarget())) {
                delegate = this.getDefaultFocus();
                if (delegate) {
                    delegate.focus();
                }
            }
        },
        initResizable: function(resizable) {
            var me = this;
            me.callParent([
                resizable
            ]);
            if (me.maximized || me.maximizing) {
                me.resizer.disable();
            }
        },
        initSimpleDraggable: function() {
            var me = this,
                dd;
            me.callParent();
            dd = me.dd;
            if (dd && me.maximized || me.maximizing) {
                dd.disable();
            }
        },
        onTabGuardFocusEnter: function(e, target) {
            var me = this,
                el = me.el,
                beforeGuard = me.tabGuardBeforeEl,
                afterGuard = me.tabGuardAfterEl,
                from = e.relatedTarget,
                nodes, forward, nextFocus;
            nodes = el.findTabbableElements({
                skipSelf: true
            });
            // Tabbables might include two tab guards, so remove them
            if (nodes[0] === beforeGuard.dom) {
                nodes.shift();
            }
            if (nodes[nodes.length - 1] === afterGuard.dom) {
                nodes.pop();
            }
            // Totally possible not to have anything tabbable within the window
            // but we have to do something so focus back the window el. At least
            // in that case the user will be able to press Escape key to close it.
            if (nodes.length === 0) {
                nextFocus = el;
            }
            // The window itself was focused, possibly by clicking or programmatically;
            // but this time we do have something tabbable to choose from.
            else if (from === el.dom) {
                forward = target === beforeGuard.dom;
            }
            // Focus was within the window and is trying to escape; 
            // for topmost guard we need to bounce focus back to the last tabbable
            // element in the window, and vice versa for the bottom guard.
            else if (el.contains(from)) {
                forward = !!e.forwardTab;
            } else // It is entirely possible that focus was outside the window and
            // the user tabbed into the window. In that case we forward the focus
            // to the next available element in the natural tab order, i.e. the element
            // after the topmost guard, or the element before the bottom guard.
            {
                forward = target === beforeGuard.dom;
            }
            nextFocus = nextFocus || (forward ? nodes[0] : nodes[nodes.length - 1]);
            if (nextFocus) {
                // If there is only one focusable node in the window, focusing it
                // while we're in focusenter handler for the tab guard might cause
                // race condition where the focusable node will be refocused first
                // and then its original blur handler will kick in, removing focus
                // styling erroneously.
                Ext.fly(nextFocus).focus(nodes.length === 1 ? 1 : 0);
            }
        }
    }
});

/**
 * A mixin which allows a component to be configured and decorated with a label and/or error message
 * as is common for form fields. This is used by e.g. Ext.form.field.Base and
 * Ext.form.FieldContainer to let them be managed by the Field layout.
 *
 * NOTE: This mixin is mainly for internal library use and most users should not need to use it
 * directly. It is more likely you will want to use one of the component classes that import
 * this mixin, such as Ext.form.field.Base or Ext.form.FieldContainer.
 *
 * Use of this mixin does not make a component a field in the logical sense, meaning it does not
 * provide any logic or state related to values or validation; that is handled by the related
 * Ext.form.field.Field mixin. These two mixins may be used separately
 * (for example Ext.form.FieldContainer is Labelable but not a Field), or in combination
 * (for example Ext.form.field.Base implements both and has logic for connecting the two.)
 *
 * Component classes which use this mixin should use the Field layout
 * or a derivation thereof to properly size and position the label and message according to the
 * component config. They must also call the {@link #initLabelable} method during component
 * initialization to ensure the mixin gets set up correctly.
 */
Ext.define("Ext.form.Labelable", {
    extend: 'Ext.Mixin',
    requires: [
        'Ext.XTemplate',
        'Ext.overrides.dom.Element'
    ],
    isLabelable: true,
    mixinConfig: {
        id: 'labelable',
        on: {
            beforeRender: 'beforeLabelRender',
            onRender: 'onLabelRender'
        }
    },
    config: {
        childEls: [
            /**
             * @property {Ext.dom.Element} labelEl
             * The label Element for this component. Only available after the component
             * has been rendered.
             */
            'labelEl',
            /**
             * @property {Ext.dom.Element} bodyEl
             * The div Element wrapping the component's contents. Only available after
             * the component has been rendered.
             */
            'bodyEl',
            /**
             * @property {Ext.dom.Element} errorEl
             * The div Element that will contain the component's error message(s). Note that
             * depending on the configured {@link #msgTarget}, this element may be hidden
             * in favor of some other form of presentation, but will always be present in the DOM
             * for use by assistive technologies.
             */
            'errorEl',
            'errorWrapEl',
            'ariaErrorEl',
            'ariaStatusEl',
            'ariaHelpEl',
            'labelTextEl'
        ]
    },
    /* eslint-disable indent, max-len */
    /**
     * @cfg {String/String[]/Ext.XTemplate} labelableRenderTpl
     * The rendering template for the field decorations. Component classes using this mixin
     * should include logic to use this as their {@link Ext.Component#renderTpl renderTpl},
     * and implement the {@link #getSubTplMarkup} method to generate the field body content.
     * @private
     */
    labelableRenderTpl: [
        '{beforeLabelTpl}',
        '<label id="{id}-labelEl" data-ref="labelEl" class="{labelCls} {labelCls}-{ui} {labelClsExtra} ',
        '{childElCls} {unselectableCls}" style="{labelStyle}"',
        '<tpl if="inputId && !skipLabelForAttribute"> for="{inputId}"</tpl>',
        ' {labelAttrTpl}>',
        '<span class="{labelInnerCls} {labelInnerCls}-{ui}" style="{labelInnerStyle}">',
        '{beforeLabelTextTpl}',
        '<span id="{id}-labelTextEl" data-ref="labelTextEl" class="{labelTextCls}">',
        '<tpl if="fieldLabel">{fieldLabel}',
        '<tpl if="labelSeparator">{labelSeparator}</tpl>',
        '</tpl>',
        '</span>',
        '{afterLabelTextTpl}',
        '</span>',
        '</label>',
        '{afterLabelTpl}',
        '<div id="{id}-bodyEl" data-ref="bodyEl" role="presentation"',
        ' class="{baseBodyCls} {baseBodyCls}-{ui}<tpl if="fieldBodyCls">',
        ' {fieldBodyCls} {fieldBodyCls}-{ui}</tpl> {growCls} {extraFieldBodyCls}"',
        '<tpl if="bodyStyle"> style="{bodyStyle}"</tpl>>',
        '{beforeBodyEl}',
        '{beforeSubTpl}',
        '{[values.$comp.getSubTplMarkup(values)]}',
        '{afterSubTpl}',
        '{afterBodyEl}',
        // ARIA elements serve different purposes:
        // - ariaHelpEl may contain optional hints about the field, such as
        //   expected format. This text is static and usually does not change
        //   once rendered. It is also optional.
        // - ariaStatusEl is used to convey status of the field. Validation errors
        //   are rendered here, as well as other information that might be helpful
        //   to Assistive Technology users exploring the app in browse mode.
        // - ariaErrorEl is used for announcing dynamic changes in the field state,
        //   so that AT users receive updates while in forms mode.
        //
        // Both ariaHelpEl and ariaStatusEl are referenced by the field's input element
        // via aria-describedby.
        '<tpl if="renderAriaElements">',
        '<tpl if="ariaHelp">',
        '<span id="{id}-ariaHelpEl" data-ref="ariaHelpEl"',
        ' class="' + Ext.baseCSSPrefix + 'hidden-offsets">',
        '{ariaHelp}',
        '</span>',
        '</tpl>',
        '<span id="{id}-ariaStatusEl" data-ref="ariaStatusEl" aria-hidden="true"',
        ' class="' + Ext.baseCSSPrefix + 'hidden-offsets">',
        '{ariaStatus}',
        '</span>',
        '<span id="{id}-ariaErrorEl" data-ref="ariaErrorEl" aria-hidden="true" aria-live="assertive"',
        ' class="' + Ext.baseCSSPrefix + 'hidden-clip">',
        '</span>',
        '</tpl>',
        '</div>',
        '<tpl if="renderError">',
        '<div id="{id}-errorWrapEl" data-ref="errorWrapEl" class="{errorWrapCls} {errorWrapCls}-{ui}',
        ' {errorWrapExtraCls}" style="{errorWrapStyle}">',
        '<div role="presentation" id="{id}-errorEl" data-ref="errorEl" ',
        'class="{errorMsgCls} {invalidMsgCls} {invalidMsgCls}-{ui}" ',
        'data-anchorTarget="{tipAnchorTarget}">',
        '</div>',
        '</div>',
        '</tpl>',
        {
            disableFormats: true
        }
    ],
    /**
     * @cfg {String/String[]/Ext.XTemplate} activeErrorsTpl
     * The template used to format the Array of error messages passed to {@link #setActiveErrors}
     * into a single HTML string. if the {@link #msgTarget} is title, it defaults to a list
     * separated by new lines. Otherwise, it renders each message as an item in an unordered list.
     */
    activeErrorsTpl: undefined,
    htmlActiveErrorsTpl: [
        '<tpl if="errors && errors.length">',
        '<ul class="{listCls}">',
        '<tpl for="errors"><li>{.}</li></tpl>',
        '</ul>',
        '</tpl>'
    ],
    plaintextActiveErrorsTpl: [
        '<tpl if="errors && errors.length">',
        '<tpl for="errors"><tpl if="xindex &gt; 1">\n</tpl>{.}</tpl>',
        '</tpl>'
    ],
    ariaActiveErrorsTpl: [
        '<tpl if="errors && errors.length">',
        '<tpl for="errors" between=". ">{.}</tpl>',
        '</tpl>'
    ],
    /* eslint-enable indent, max-len */
    /**
     * @property {Boolean} isFieldLabelable
     * Flag denoting that this object is labelable as a field. Always true.
     */
    isFieldLabelable: true,
    /**
     * @cfg {String} formItemCls
     * A CSS class to be applied to the outermost element to denote that it is participating
     * in the form field layout.
     */
    formItemCls: Ext.baseCSSPrefix + 'form-item',
    /**
     * @cfg {String} labelCls
     * The CSS class to be applied to the label element. This (single) CSS class is used
     * to formulate the renderSelector and drives the field layout where it is concatenated
     * with a hyphen ('-') and {@link #labelAlign}. To add additional classes, use
     * {@link #labelClsExtra}.
     */
    labelCls: Ext.baseCSSPrefix + 'form-item-label',
    /**
     * @private
     */
    topLabelCls: Ext.baseCSSPrefix + 'form-item-label-top',
    rightLabelCls: Ext.baseCSSPrefix + 'form-item-label-right',
    labelInnerCls: Ext.baseCSSPrefix + 'form-item-label-inner',
    labelTextCls: Ext.baseCSSPrefix + 'form-item-label-text',
    topLabelSideErrorCls: Ext.baseCSSPrefix + 'form-item-label-top-side-error',
    /**
     * @cfg {String} labelClsExtra
     * An optional string of one or more additional CSS classes to add to the label element.
     * Defaults to empty.
     */
    /**
     * @cfg {String} errorMsgCls
     * The CSS class to be applied to the error message element.
     */
    errorMsgCls: Ext.baseCSSPrefix + 'form-error-msg',
    errorWrapCls: Ext.baseCSSPrefix + 'form-error-wrap',
    errorWrapSideCls: Ext.baseCSSPrefix + 'form-error-wrap-side',
    errorWrapUnderCls: Ext.baseCSSPrefix + 'form-error-wrap-under',
    errorWrapUnderSideLabelCls: Ext.baseCSSPrefix + 'form-error-wrap-under-side-label',
    /**
     * @cfg {String} baseBodyCls
     * The CSS class to be applied to the body content element.
     */
    baseBodyCls: Ext.baseCSSPrefix + 'form-item-body',
    invalidIconCls: Ext.baseCSSPrefix + 'form-invalid-icon',
    invalidUnderCls: Ext.baseCSSPrefix + 'form-invalid-under',
    noLabelCls: Ext.baseCSSPrefix + 'form-item-no-label',
    /**
     * @cfg {String} fieldBodyCls
     * An extra CSS class to be applied to the body content element in addition to
     * {@link #baseBodyCls}.
     */
    fieldBodyCls: '',
    extraFieldBodyCls: '',
    /**
     * @cfg {String} invalidCls
     * The CSS class to use when marking the component invalid.
     */
    invalidCls: Ext.baseCSSPrefix + 'form-invalid',
    /**
     * @cfg {String} fieldLabel
     * The label for the field. It gets appended with the {@link #labelSeparator}, and its position
     * and sizing is determined by the {@link #labelAlign} and {@link #labelWidth} configs.
     */
    fieldLabel: undefined,
    /**
     * @cfg {String} labelAlign
     * Controls the position and alignment of the {@link #fieldLabel}. Valid values are:
     *
     *   - "left" (the default) - The label is positioned to the left of the field, with its text
     *     aligned to the left. Its width is determined by the {@link #labelWidth} config.
     *   - "top" - The label is positioned above the field.
     *   - "right" - The label is positioned to the left of the field, with its text aligned
     *     to the right. Its width is determined by the {@link #labelWidth} config.
     */
    labelAlign: 'left',
    /**
     * @cfg {Number} labelWidth
     * The width of the {@link #fieldLabel} in pixels. Only applicable if {@link #labelAlign}
     * is set to "left" or "right".
     */
    labelWidth: 100,
    /**
     * @cfg {Number} labelPad
     * The amount of space in pixels between the {@link #fieldLabel} and the field body.
     * This defaults to `5` for compatibility with Ext JS 4, however, as of Ext JS 5
     * the space between the label and the body can optionally be determined by the theme
     * using the {@link #$form-label-horizontal-spacing} (for side-aligned labels) and
     * {@link #$form-label-vertical-spacing} (for top-aligned labels) SASS variables.
     * In order for the stylesheet values as to take effect, you must use a labelPad value
     * of `null`.
     */
    labelPad: 5,
    /**
     * @cfg {String} labelSeparator
     * Character(s) to be inserted at the end of the {@link #fieldLabel label text}.
     *
     * Set to empty string to hide the separator completely.
     * @locale
     */
    labelSeparator: ':',
    /**
     * @cfg {String} labelStyle
     * A CSS style specification string to apply directly to this field's label.
     */
    /**
     * @cfg {Boolean} hideLabel
     * Set to true to completely hide the label element ({@link #fieldLabel} and
     * {@link #labelSeparator}). Also see {@link #hideEmptyLabel}, which controls whether space
     * will be reserved for an empty fieldLabel.
     */
    hideLabel: false,
    /**
     * @cfg {Boolean} hideEmptyLabel
     * When set to true, the label element ({@link #fieldLabel} and {@link #labelSeparator})
     * will be automatically hidden if the {@link #fieldLabel} is empty. Setting this to false
     * will cause the empty label element to be rendered and space to be reserved for it;
     * this is useful if you want a field without a label to line up with other labeled fields
     * in the same form.
     *
     * If you wish to unconditionall hide the label even if a non-empty fieldLabel is configured,
     * then set the {@link #hideLabel} config to true.
     */
    hideEmptyLabel: true,
    /**
     * @cfg {Boolean} preventMark
     * true to disable displaying any {@link #setActiveError error message} set on this object.
     */
    preventMark: false,
    /**
     * @cfg {Boolean} autoFitErrors
     * Whether to adjust the component's body width to make room for 'side'
     * {@link #msgTarget error messages}.
     */
    autoFitErrors: true,
    /**
     * @cfg {String} msgTarget
     * The location where the error message text should display. Must be one of the following
     * values:
     *
     *   - `qtip` Display a quick tip containing the message when the user hovers over the field.
     *     This is the default.
     *
     *     **{@link Ext.tip.QuickTipManager#init} must have been called for this setting to work.**
     *
     *   - `title` Display the message in a default browser title attribute popup.
     *   - `under` Add a block div beneath the field containing the error message.
     *   - `side` Add an error icon to the right of the field, displaying the message in a popup
     *     on hover.
     *   - `none` Don't display any error message. This might be useful if you are implementing
     *     custom error display.
     *   - `[element id]` Add the error message directly to the innerHTML of the specified element.
     */
    msgTarget: 'qtip',
    /**
     * @private
     * Map for msg target lookup, if target is not in this map it is assumed
     * to be an element id
     */
    msgTargets: {
        qtip: 1,
        title: 1,
        under: 1,
        side: 1,
        none: 1
    },
    /**
     * @cfg {String} activeError
     * If specified, then the component will be displayed with this value as its active error
     * when first rendered. Use {@link #setActiveError} or {@link #unsetActiveError} to change it
     * after component creation.
     */
    /**
     * @private
     * Tells the layout system that the height can be measured immediately because the width
     * does not need setting.
     */
    noWrap: true,
    /**
     * @cfg {String} [ariaHelp] Optional text description for this object. This text will be
     * announced to Assistive Technology users when the object is focused.
     * @locale
     */
    ariaHelp: undefined,
    /**
     * @cfg {String} ariaErrorText Localized announcement text for validation errors. This text
     * will be used by Assistive Technologies such as screen readers to alert the users when
     * field validation fails.
     *
     * This config is used with {@link Ext.String#format}. '{0}' will be replaced with the actual
     * error message(s), '{1}' will be replaced with field label.
     * @locale
     */
    ariaErrorText: 'Input error. {0}.',
    labelableInsertions: [
        /**
         * @cfg {String/Array/Ext.XTemplate} beforeBodyEl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * at the beginning of the input containing element. If an `XTemplate` is used,
         * the component's {@link Ext.Component#renderData render data} serves as the context.
         */
        'beforeBodyEl',
        /**
         * @cfg {String/Array/Ext.XTemplate} afterBodyEl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * at the end of the input containing element. If an `XTemplate` is used, the component's
         * {@link Ext.Component#renderData render data} serves as the context.
         */
        'afterBodyEl',
        /**
         * @cfg {String/Array/Ext.XTemplate} beforeLabelTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * before the label element. If an `XTemplate` is used, the component's
         * {@link Ext.Component#renderData render data} serves as the context.
         */
        'beforeLabelTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} afterLabelTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * after the label element. If an `XTemplate` is used, the component's
         * {@link Ext.Component#renderData render data} serves as the context.
         */
        'afterLabelTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} beforeSubTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * before the {@link #getSubTplMarkup subTpl markup}. If an `XTemplate` is used, the
         * component's {@link Ext.Component#renderData render data} serves as the context.
         */
        'beforeSubTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} afterSubTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * after the {@link #getSubTplMarkup subTpl markup}. If an `XTemplate` is used, the
         * component's {@link Ext.Component#renderData render data} serves as the context.
         */
        'afterSubTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} beforeLabelTextTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * before the label text. If an `XTemplate` is used, the component's
         * {@link Ext.Component#renderData render data} serves as the context.
         */
        'beforeLabelTextTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} afterLabelTextTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * after the label text. If an `XTemplate` is used, the component's
         * {@link Ext.Component#renderData render data} serves as the context.
         */
        'afterLabelTextTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} labelAttrTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * inside the label element (as attributes). If an `XTemplate` is used, the component's
         * {@link Ext.Component#renderData render data} serves as the context.
         */
        'labelAttrTpl'
    ],
    statics: {
        /**
         * Use a custom QuickTip instance separate from the main QuickTips singleton, so that we
         * can give it a custom frame style. Responds to errorqtip rather than the qtip property.
         * @static
         * @private
         */
        initTip: function() {
            var tip = this.tip,
                cfg, copy;
            if (tip) {
                return;
            }
            cfg = {
                id: 'ext-form-error-tip',
                // tell the spec runner to ignore this element when checking if the dom is clean
                sticky: true,
                ui: 'form-invalid'
            };
            // On Touch devices, tapping the target shows the qtip
            if (Ext.supports.Touch) {
                cfg.dismissDelay = 0;
                cfg.anchor = 'top';
                cfg.showDelay = 0;
                cfg.showOnTap = true;
                cfg.listeners = {
                    beforeshow: function() {
                        this.minWidth = Ext.fly(this.activeTarget.el).getWidth();
                    }
                };
            }
            tip = this.tip = Ext.create('Ext.tip.QuickTip', cfg);
            copy = Ext.apply({}, tip.tagConfig);
            copy.attribute = 'errorqtip';
            tip.setTagConfig(copy);
        },
        /**
         * Destroy the error tip instance.
         * @static
         */
        destroyTip: function() {
            this.tip = Ext.destroy(this.tip);
        }
    },
    /**
     * @event errorchange
     * Fires when the active error message is changed via {@link #setActiveError}.
     * @param {Ext.form.Labelable} this
     * @param {String} error The active error message
     */
    /**
     * Performs initialization of this mixin. Component classes using this mixin should call
     * this method during their own initialization.
     */
    initLabelable: function() {
        var me = this,
            padding = me.padding;
        // This Component is rendered as a table. Padding doesn't work on tables
        // Before padding can be applied to the encapsulating table element, copy the padding into
        // an extraMargins property which is to be added to all computed margins post render :(
        if (padding) {
            me.padding = undefined;
            me.extraMargins = Ext.Element.parseBox(padding);
        }
        // IE8 hack for https://sencha.jira.com/browse/EXTJS-17536.
        // Need to force a relayout of the display:table form item.
        // TODO: Remove when IE8 retires.
        if (Ext.isIE8) {
            me.restoreDisplay = Ext.Function.createDelayed(me.doRestoreDisplay, 0, me);
        }
        if (!me.activeErrorsTpl) {
            if (me.msgTarget === 'title') {
                me.activeErrorsTpl = me.plaintextActiveErrorsTpl;
            } else {
                me.activeErrorsTpl = me.htmlActiveErrorsTpl;
            }
        }
        me.addCls([
            me.formItemCls,
            me.formItemCls + '-' + me.ui
        ]);
        // Prevent first render of active error, at Field render time from signalling a change
        // from undefined to "
        me.lastActiveError = '';
        // bubbleEvents on the prototype of a mixin won't work, so call enableBubble
        me.enableBubble('errorchange');
    },
    /**
     * Returns the trimmed label by slicing off the label separator character. Can be overridden.
     * @return {String} The trimmed field label, or empty string if not defined
     */
    trimLabelSeparator: function() {
        var me = this,
            separator = me.labelSeparator,
            label = me.fieldLabel || '',
            lastChar = label.substr(label.length - 1);
        // if the last char is the same as the label separator then slice it off
        // otherwise just return label value
        return lastChar === separator ? label.slice(0, -1) : label;
    },
    /**
     * Returns the label for the field. Defaults to simply returning the {@link #fieldLabel} config.
     * Can be overridden to provide a custom generated label.
     * @template
     * @return {String} The configured field label, or empty string if not defined
     */
    getFieldLabel: function() {
        return this.trimLabelSeparator();
    },
    /**
     * Set the label of this field.
     * @param {String} label The new label. The {@link #labelSeparator} will be automatically
     * appended to the label string.
     */
    setFieldLabel: function(label) {
        var me = this,
            separator = me.labelSeparator,
            errorWrapEl = me.errorWrapEl,
            sideLabel = (me.labelAlign !== 'top'),
            noLabelCls = me.noLabelCls,
            errorWrapUnderSideLabelCls = me.errorWrapUnderSideLabelCls;
        label = label || '';
        me.fieldLabel = label;
        if (me.rendered) {
            if (Ext.isEmpty(label) && me.hideEmptyLabel) {
                me.addCls(noLabelCls);
                if (sideLabel && errorWrapEl) {
                    errorWrapEl.removeCls(errorWrapUnderSideLabelCls);
                }
            } else {
                if (separator) {
                    label = me.trimLabelSeparator() + separator;
                }
                me.labelTextEl.dom.innerHTML = label;
                me.removeCls(noLabelCls);
                if (sideLabel && errorWrapEl) {
                    errorWrapEl.addCls(errorWrapUnderSideLabelCls);
                }
            }
            me.updateLayout();
        }
    },
    setHideLabel: function(hideLabel) {
        var me = this;
        if (hideLabel !== me.hideLabel) {
            me.hideLabel = hideLabel;
            if (me.rendered) {
                me[hideLabel ? 'addCls' : 'removeCls'](me.noLabelCls);
                me.updateLayout();
            }
        }
    },
    setHideEmptyLabel: function(hideEmptyLabel) {
        var me = this,
            hide;
        if (hideEmptyLabel !== me.hideEmptyLabel) {
            me.hideEmptyLabel = hideEmptyLabel;
            if (me.rendered && !me.hideLabel) {
                hide = hideEmptyLabel && !me.getFieldLabel();
                me[hide ? 'addCls' : 'removeCls'](me.noLabelCls);
                me.updateLayout();
            }
        }
    },
    getInsertionRenderData: function(data, names) {
        var i = names.length,
            name, value;
        while (i--) {
            name = names[i];
            value = this[name];
            if (value) {
                if (typeof value !== 'string') {
                    if (!value.isTemplate) {
                        value = Ext.XTemplate.getTpl(this, name);
                    }
                    value = value.apply(data);
                }
            }
            data[name] = value || '';
        }
        return data;
    },
    /**
     * Generates the arguments for the field decorations {@link #labelableRenderTpl
     * rendering template}.
     * @param {Object} data optional object to use as the base data object.  If provided,
     * this method will add properties to the base object instead of creating a new one.
     * @return {Object} The template arguments
     * @protected
     */
    getLabelableRenderData: function() {
        var me = this,
            labelAlign = me.labelAlign,
            topLabel = (labelAlign === 'top'),
            rightLabel = (labelAlign === 'right'),
            sideError = (me.msgTarget === 'side'),
            underError = (me.msgTarget === 'under'),
            errorMsgCls = me.errorMsgCls,
            labelPad = me.labelPad,
            labelWidth = me.labelWidth,
            labelClsExtra = me.labelClsExtra || '',
            errorWrapExtraCls = sideError ? me.errorWrapSideCls : me.errorWrapUnderCls,
            labelStyle = '',
            labelInnerStyle = '',
            labelVisible = me.hasVisibleLabel(),
            autoFitErrors = me.autoFitErrors,
            defaultBodyWidth = me.defaultBodyWidth,
            bodyStyle, data;
        if (topLabel) {
            labelClsExtra += ' ' + me.topLabelCls;
            if (labelPad) {
                labelInnerStyle = 'padding-bottom:' + labelPad + 'px;';
            }
            if (sideError && !autoFitErrors) {
                labelClsExtra += ' ' + me.topLabelSideErrorCls;
            }
        } else {
            if (rightLabel) {
                labelClsExtra += ' ' + me.rightLabelCls;
            }
            if (labelPad) {
                labelStyle += me.getHorizontalPaddingStyle() + labelPad + 'px;';
            }
            labelStyle += 'width:' + (labelWidth + (labelPad ? labelPad : 0)) + 'px;';
            // inner label needs width as well so that setting width on the outside
            // that is smaller than the natural width, will be ensured to take width
            // away from the body, and not the label.
            labelInnerStyle = 'width:' + labelWidth + 'px';
        }
        if (labelVisible) {
            if (!topLabel && underError) {
                errorWrapExtraCls += ' ' + me.errorWrapUnderSideLabelCls;
            }
        }
        if (defaultBodyWidth) {
            // This is here to support textfield's deprecated "size" config
            bodyStyle = 'min-width:' + defaultBodyWidth + 'px;max-width:' + defaultBodyWidth + 'px;';
        }
        data = {
            id: me.id,
            inputId: me.getInputId(),
            labelCls: me.labelCls,
            labelClsExtra: labelClsExtra,
            labelStyle: labelStyle + (me.labelStyle || ''),
            labelInnerStyle: labelInnerStyle,
            labelInnerCls: me.labelInnerCls,
            labelTextCls: me.labelTextCls,
            skipLabelForAttribute: !!me.skipLabelForAttribute,
            unselectableCls: Ext.Element.unselectableCls,
            bodyStyle: bodyStyle,
            baseBodyCls: me.baseBodyCls,
            fieldBodyCls: me.fieldBodyCls,
            extraFieldBodyCls: me.extraFieldBodyCls,
            errorWrapCls: me.errorWrapCls,
            errorWrapExtraCls: errorWrapExtraCls,
            renderError: sideError || underError,
            invalidMsgCls: sideError ? me.invalidIconCls : underError ? me.invalidUnderCls : '',
            errorMsgCls: errorMsgCls,
            growCls: me.grow ? me.growCls : '',
            tipAnchorTarget: me.id + '-inputEl',
            errorWrapStyle: (sideError && !autoFitErrors) ? 'visibility:hidden' : 'display:none',
            fieldLabel: me.getFieldLabel(),
            labelSeparator: me.labelSeparator,
            renderAriaElements: !!me.renderAriaElements,
            ariaStatus: ''
        };
        if (me.ariaHelp) {
            data.ariaHelp = Ext.String.htmlEncode(me.ariaHelp);
        }
        me.getInsertionRenderData(data, me.labelableInsertions);
        return data;
    },
    // hook for rtl
    getHorizontalPaddingStyle: function() {
        return 'padding-right:';
    },
    beforeLabelRender: function() {
        var me = this;
        me.setFieldDefaults(me.getInherited().fieldDefaults);
        if (me.ownerLayout) {
            me.addCls(Ext.baseCSSPrefix + me.ownerLayout.type + '-form-item');
        }
        if (!me.hasVisibleLabel()) {
            me.addCls(me.noLabelCls);
        }
    },
    onLabelRender: function() {
        var me = this,
            style = {},
            ExtElement = Ext.Element,
            errorWrapEl = me.errorWrapEl,
            margins, side;
        if (errorWrapEl) {
            errorWrapEl.setVisibilityMode((me.msgTarget === 'side' && !me.autoFitErrors) ? ExtElement.VISIBILITY : ExtElement.DISPLAY);
        }
        if (me.extraMargins) {
            margins = me.el.getMargin();
            for (side in margins) {
                if (margins.hasOwnProperty(side)) {
                    style['margin-' + side] = (margins[side] + me.extraMargins[side]) + 'px';
                }
            }
            me.el.setStyle(style);
        }
    },
    /**
     * Checks if the field has a visible label
     * @return {Boolean} True if the field has a visible label
     */
    hasVisibleLabel: function() {
        if (this.hideLabel) {
            return false;
        }
        return !(this.hideEmptyLabel && !this.getFieldLabel());
    },
    /**
     * Gets the markup to be inserted into the outer template's bodyEl. Defaults to empty string,
     * should be implemented by classes including this mixin as needed.
     * @return {String} The markup to be inserted
     * @protected
     */
    getSubTplMarkup: function() {
        return '';
    },
    /**
     * Get the input id, if any, for this component. This is used as the "for" attribute on the
     * label element. Implementing subclasses may also use this as e.g. the id for their own
     * input element.
     * @return {String} The input id
     */
    getInputId: function() {
        return '';
    },
    /**
     * Gets the active error message for this component, if any. This does not trigger validation
     * on its own, it merely returns any message that the component may already hold.
     * @return {String} The active error message on the component; if there is no error,
     * an empty string is returned.
     */
    getActiveError: function() {
        return this.activeError || '';
    },
    /**
     * Tells whether the field currently has an active error message. This does not trigger
     * validation on its own, it merely looks for any message that the component may already hold.
     * @return {Boolean}
     */
    hasActiveError: function() {
        return !!this.getActiveError();
    },
    /**
     * Sets the active error message to the given string. This replaces the entire error message
     * contents with the given string. Also see {@link #setActiveErrors} which accepts an Array
     * of messages and formats them according to the {@link #activeErrorsTpl}. Note that this only
     * updates the error message element's text and attributes, you'll have to call
     * doComponentLayout to actually update the field's layout to match. If the field extends
     * {@link Ext.form.field.Base} you should call
     * {@link Ext.form.field.Base#markInvalid markInvalid} instead.
     * @param {String} msg The error message
     */
    setActiveError: function(msg) {
        this.setActiveErrors(msg);
    },
    /**
     * Gets an Array of any active error messages currently applied to the field. This does not
     * trigger validation on its own, it merely returns any messages that the component
     * may already hold.
     * @return {String[]} The active error messages on the component; if there are no errors,
     * an empty Array is returned.
     */
    getActiveErrors: function() {
        return this.activeErrors || [];
    },
    /**
     * Set the active error message to an Array of error messages. The messages are formatted
     * into a single message string using the {@link #activeErrorsTpl}. Also see
     * {@link #setActiveError} which allows setting the entire error contents with a single string.
     * Note that this only updates the error message element's text and attributes, you'll have to
     * call doComponentLayout to actually update the field's layout to match. If the field extends
     * {@link Ext.form.field.Base} you should call
     * {@link Ext.form.field.Base#markInvalid markInvalid} instead.
     * @param {String[]} errors The error messages
     */
    setActiveErrors: function(errors) {
        var me = this,
            errorWrapEl = me.errorWrapEl,
            msgTarget = me.msgTarget,
            isSide = msgTarget === 'side',
            isQtip = msgTarget === 'qtip',
            ariaErrorEl = me.ariaErrorEl,
            actionEl, activeError, tpl, targetEl, ariaTpl, errStr, errText;
        errors = Ext.Array.from(errors);
        tpl = me.lookupTpl('activeErrorsTpl');
        me.activeErrors = errors;
        activeError = me.activeError = tpl.apply({
            fieldLabel: me.fieldLabel,
            errors: errors,
            listCls: Ext.baseCSSPrefix + 'list-plain'
        });
        me.renderActiveError();
        if (me.rendered) {
            actionEl = me.getActionEl();
            if (isSide) {
                me.errorEl.dom.setAttribute('data-errorqtip', activeError);
            } else if (isQtip) {
                actionEl.dom.setAttribute('data-errorqtip', activeError);
            } else if (msgTarget === 'title') {
                actionEl.dom.setAttribute('title', activeError);
            }
            // If msgTarget is title, setting an alert is redundant for ARIA purposes
            if (msgTarget !== 'title' && ariaErrorEl) {
                ariaTpl = me.lookupTpl('ariaActiveErrorsTpl');
                errStr = ariaTpl.apply({
                    errors: errors
                });
                // Setting innerHTML on aria-live element will replace inner text node,
                // and the browser will fire DOM change event even if the text is the same.
                // We don't want the announcement to repeat if the text hasn't changed.
                errText = Ext.String.formatEncode(me.ariaErrorText, errStr, me.fieldLabel);
                if (ariaErrorEl.dom.innerHTML !== errText) {
                    ariaErrorEl.dom.innerHTML = errText;
                }
                // ariaStatusEl is not aria-live so it's OK to change it every time.
                // Contents will be announced only upon focusing the field.
                me.ariaStatusEl.dom.innerHTML = Ext.String.htmlEncode(errStr);
            }
            if (isSide || isQtip) {
                Ext.form.Labelable.initTip();
            }
            if (!me.msgTargets[msgTarget]) {
                targetEl = Ext.get(msgTarget);
                if (targetEl) {
                    targetEl.dom.innerHTML = activeError;
                }
            }
        }
        if (errorWrapEl) {
            errorWrapEl.setVisible(errors.length > 0);
            if (isSide && me.autoFitErrors) {
                me.labelEl.addCls(me.topLabelSideErrorCls);
            }
            me.updateLayout();
        }
    },
    /**
     * Clears the active error message(s). Note that this only clears the error message element's
     * text and attributes, you'll have to call doComponentLayout to actually update the field's
     * layout to match. If the field extends {@link Ext.form.field.Base} you should call
     * {@link Ext.form.field.Base#clearInvalid clearInvalid} instead.
     */
    unsetActiveError: function() {
        var me = this,
            errorWrapEl = me.errorWrapEl,
            ariaErrorEl = me.ariaErrorEl,
            msgTarget = me.msgTarget,
            restoreDisplay = me.restoreDisplay,
            actionEl, targetEl;
        if (me.hasActiveError()) {
            delete me.activeError;
            delete me.activeErrors;
            me.renderActiveError();
            if (me.rendered) {
                actionEl = me.getActionEl();
                if (msgTarget === 'qtip') {
                    actionEl.dom.removeAttribute('data-errorqtip');
                } else if (msgTarget === 'title') {
                    actionEl.dom.removeAttribute('title');
                }
                if (msgTarget !== 'title' && ariaErrorEl) {
                    ariaErrorEl.dom.innerHTML = me.ariaStatusEl.dom.innerHTML = '';
                }
                if (!me.msgTargets[msgTarget]) {
                    targetEl = Ext.get(msgTarget);
                    if (targetEl) {
                        targetEl.dom.innerHTML = '';
                    }
                }
                if (errorWrapEl) {
                    errorWrapEl.hide();
                    if (msgTarget === 'side' && me.autoFitErrors) {
                        me.labelEl.removeCls(me.topLabelSideErrorCls);
                    }
                    me.updateLayout();
                    // IE8 hack for https://sencha.jira.com/browse/EXTJS-17536.
                    // Need to force a relayout of the display:table form item.
                    // TODO: Remove when IE8 retires.
                    if (restoreDisplay) {
                        me.el.dom.style.display = 'block';
                        me.restoreDisplay();
                    }
                }
            }
        }
    },
    doRestoreDisplay: function() {
        // IE8 hack for https://sencha.jira.com/browse/EXTJS-17536.
        // Need to force a relayout of the display:table form item.
        // TODO: Remove this method when IE8 retires.
        var el = this.el;
        if (el && el.dom) {
            el.dom.style.display = '';
        }
    },
    /**
     * @private
     * Updates the rendered DOM to match the current activeError. This only updates the content and
     * attributes, you'll have to call doComponentLayout to actually update the display.
     */
    renderActiveError: function() {
        var me = this,
            activeError = me.getActiveError(),
            hasError = !!activeError;
        if (activeError !== me.lastActiveError) {
            me.lastActiveError = activeError;
            me.fireEvent('errorchange', me, activeError);
        }
        if (me.rendered && !me.destroyed && !me.preventMark) {
            me.toggleInvalidCls(hasError);
            // Update the errorEl (There will only be one if msgTarget is 'side' or 'under')
            // with the error message text
            if (me.errorEl) {
                me.errorEl.dom.innerHTML = activeError;
            }
        }
    },
    /**
     * @private
     * Add/remove invalid class(es)
     * @param {Boolean} hasError
     */
    toggleInvalidCls: function(hasError) {
        this.el[hasError ? 'addCls' : 'removeCls'](this.invalidCls);
    },
    /**
     * Applies a set of default configuration values to this Labelable instance. For each
     * of the properties in the given object, check if this component hasOwnProperty that config;
     * if not then it's inheriting a default value from its prototype and we should apply
     * the default value.
     * @param {Object} defaults The defaults to apply to the object.
     */
    setFieldDefaults: function(defaults) {
        var key;
        for (key in defaults) {
            if (!this.hasOwnProperty(key)) {
                this[key] = defaults[key];
            }
        }
    }
}, function() {
    if (Ext.supports.Touch) {
        this.prototype.msgTarget = 'side';
    }
});

/**
 * This mixin provides a common interface for the logical behavior and state of form fields,
 * including:
 *
 * - Getter and setter methods for field values
 * - Events and methods for tracking value and validity changes
 * - Methods for triggering validation
 *
 * **NOTE**: When implementing custom fields, it is most likely that you will want to extend
 * the {@link Ext.form.field.Base} component class rather than using this mixin directly,
 * as BaseField contains additional logic for generating an actual DOM complete with
 * {@link Ext.form.Labelable label and error message} display and a form input field,
 * plus methods that bind the Field value getters and setters to the input field's value.
 *
 * If you do want to implement this mixin directly and don't want to extend
 * {@link Ext.form.field.Base}, then you will most likely want to override the following methods
 * with custom implementations: {@link #getValue}, {@link #setValue}, and {@link #getErrors}.
 * Other methods may be overridden as needed but their base implementations should be sufficient
 * for common cases. You will also need to make sure that {@link #initField} is called
 * during the component's initialization.
 */
Ext.define('Ext.form.field.Field', {
    mixinId: 'field',
    /**
     * @property {Boolean} isFormField
     * Flag denoting that this component is a Field. Always true.
     */
    isFormField: true,
    config: {
        /**
         * @cfg {Boolean/String} validation
         * This property, when a `String`, contributes its value to the error state of this
         * instance as reported by `getErrors`.
         */
        validation: null,
        /**
         * @cfg {Ext.data.Field} validationField
         * When binding is used with a model, this maps to the underlying
         * {@link Ext.data.field.Field} if it is available. This can be used to validate the value
         * against the model field without needing to push the value back into the model.
         *
         * @private
         */
        validationField: null
    },
    /**
     * @cfg {Object} value
     * A value to initialize this field with.
     */
    /**
     * @cfg {String} name
     * The name of the field. By default this is used as the parameter name when including the
     * {@link #getSubmitData field value} in a {@link Ext.form.Basic#submit form submit()}.
     * To prevent the field from being included in the form submit, set {@link #submitValue}
     * to false.
     */
    /**
     * @cfg {Boolean} disabled
     * True to disable the field. Disabled Fields will not be
     * {@link Ext.form.Basic#submit submitted}.
     */
    disabled: false,
    /**
     * @cfg {Boolean} submitValue
     * Setting this to false will prevent the field from being
     * {@link Ext.form.Basic#submit submitted} even when it is not disabled.
     */
    submitValue: true,
    /**
     * @cfg {Boolean} validateOnChange
     * Specifies whether this field should be validated immediately whenever a change in its value
     * is detected. If the validation results in a change in the field's validity, a
     * {@link #validitychange} event will be fired. This allows the field to show feedback
     * about the validity of its contents immediately as the user is typing.
     *
     * When set to false, feedback will not be immediate. However the form will still be validated
     * before submitting if the clientValidation option to {@link Ext.form.Basic#doAction}
     * is enabled, or if the field or form are validated manually.
     *
     * See also {@link Ext.form.field.Base#checkChangeEvents} for controlling how changes
     * to the field's value are detected.
     */
    validateOnChange: true,
    /**
     * @cfg {String[]/String} valuePublishEvent
     * The event name(s) to use to publish the {@link #value}
     * {@link Ext.form.field.Base#bind} for this field.
     * @since 5.0.1
     */
    valuePublishEvent: 'change',
    /**
     * @private
     */
    suspendCheckChange: 0,
    /**
     * @property {Boolean} dirty
     * The dirty state of the field.
     * @private
     */
    dirty: false,
    /**
     * @event change
     * Fires when the value of a field is changed. The value of a field is 
     * checked for changes when the field's {@link #setValue} method 
     * is called and when any of the events listed in 
     * {@link Ext.form.field.Base#checkChangeEvents checkChangeEvents} are fired.
     * @param {Ext.form.field.Field} this
     * @param {Object} newValue The new value
     * @param {Object} oldValue The original value
     */
    /**
     * @event validitychange
     * Fires when a change in the field's validity is detected.
     * @param {Ext.form.field.Field} this
     * @param {Boolean} isValid Whether or not the field is now valid
     */
    /**
     * @event dirtychange
     * Fires when a change in the field's {@link #isDirty} state is detected.
     * @param {Ext.form.field.Field} this
     * @param {Boolean} isDirty Whether or not the field is now dirty
     */
    /**
     * Initializes this Field mixin on the current instance. Components using this mixin
     * should call this method during their own initialization process.
     */
    initField: function() {
        var me = this,
            valuePublishEvent = me.valuePublishEvent,
            len, i;
        me.initValue();
        // eslint-disable-next-line vars-on-top, one-var
        var badNames = [
                'tagName',
                'nodeName',
                'children',
                'childNodes'
            ],
            name = this.name;
        if (name && Ext.Array.indexOf(badNames, name) > -1) {
            Ext.log.warn('It is recommended to not use "' + name + '" as a field name, because it ' + 'can cause naming collisions during form submission.');
        }
        // Vast majority of cases won't be an array
        if (Ext.isString(valuePublishEvent)) {
            me.on(valuePublishEvent, me.publishValue, me);
        } else {
            for (i = 0 , len = valuePublishEvent.length; i < len; ++i) {
                me.on(valuePublishEvent[i], me.publishValue, me);
            }
        }
    },
    /**
     * Initializes the field's value based on the initial config.
     */
    initValue: function() {
        var me = this;
        // Set the initial value if we have one.
        // Prevent validation on initial set.
        if ('value' in me) {
            me.suspendCheckChange++;
            me.setValue(me.value);
            me.suspendCheckChange--;
        }
        /**
         * @property {Object} originalValue
         * The original value of the field as configured in the {@link #value} configuration,
         * or as loaded by the last form load operation if the form's
         * {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} setting is `true`.
         */
        me.initialValue = me.originalValue = me.lastValue = me.getValue();
    },
    /**
     * Cleans up values initialized by this Field mixin on the current instance. 
     * Components using this mixin should call this method before being destroyed.
     */
    cleanupField: function() {
        delete this._ownerRecord;
    },
    // Fields can be editors, and some editors may not have a name property that maps
    // to its data index, so it's necessary in these cases to look it up by its dataIndex
    // property.  See EXTJSIV-11650.
    getFieldIdentifier: function() {
        return this.isEditorComponent ? this.dataIndex : this.name;
    },
    /**
     * Returns the {@link Ext.form.field.Field#name name} attribute of the field. This is used
     * as the parameter name when including the field value in a
     * {@link Ext.form.Basic#submit form submit()}.
     * @return {String} name The field {@link Ext.form.field.Field#name name}
     */
    getName: function() {
        return this.name;
    },
    /**
     * Returns the current data value of the field. The type of value returned is particular
     * to the type of the particular field (e.g. a Date object for {@link Ext.form.field.Date}).
     * @return {Object} value The field value
     */
    getValue: function() {
        return this.value;
    },
    /**
     * Sets a data value into the field and runs the change detection and validation.
     * @param {Object} value The value to set
     * @return {Ext.form.field.Field} this
     */
    setValue: function(value) {
        var me = this;
        me.value = value;
        me.checkChange();
        return me;
    },
    /**
     * Returns whether two field {@link #getValue values} are logically equal. Field implementations
     * may override this to provide custom comparison logic appropriate for the particular field's
     * data type.
     * @param {Object} value1 The first value to compare
     * @param {Object} value2 The second value to compare
     * @return {Boolean} True if the values are equal, false if inequal.
     */
    isEqual: function(value1, value2) {
        return String(value1) === String(value2);
    },
    /**
     * Returns whether two values are logically equal.
     * Similar to {@link #isEqual}, however null or undefined values will be treated
     * as empty strings.
     * @private
     * @param {Object} value1 The first value to compare
     * @param {Object} value2 The second value to compare
     * @return {Boolean} True if the values are equal, false if inequal.
     */
    isEqualAsString: function(value1, value2) {
        return String(Ext.valueFrom(value1, '')) === String(Ext.valueFrom(value2, ''));
    },
    /**
     * Returns the parameter(s) that would be included in a standard form submit for this field.
     * Typically this will be an object with a single name-value pair, the name being this field's
     * {@link #method-getName name} and the value being its current stringified value.
     * More advanced field implementations may return more than one name-value pair.
     *
     * Note that the values returned from this method are not guaranteed to have been successfully
     * {@link #validate validated}.
     *
     * @return {Object} A mapping of submit parameter names to values; each value should be
     * a string, or an array of strings if that particular name has multiple values. It can also
     * return null if there are no parameters to be submitted.
     */
    getSubmitData: function() {
        var me = this,
            data = null;
        if (!me.disabled && me.submitValue) {
            data = {};
            data[me.getName()] = '' + me.getValue();
        }
        return data;
    },
    /**
     * Returns the value(s) that should be saved to the {@link Ext.data.Model} instance
     * for this field, when {@link Ext.form.Basic#updateRecord} is called. Typically this will be
     * an object with a single name-value pair, the name being this field's
     * {@link #method-getName name} and the value being its current data value. More advanced field
     * implementations may return more than one name-value pair. The returned values will be saved
     * to the corresponding field names in the Model.
     *
     * Note that the values returned from this method are not guaranteed to have been successfully
     * {@link #validate validated}.
     *
     * @param {Boolean} includeEmptyText Whether or not to include empty text
     * @param isSubmitting (private)
     * @return {Object} A mapping of submit parameter names to values; each value should be
     * a string, or an array of strings if that particular name has multiple values. It can also
     * return null if there are no parameters to be submitted.
     */
    getModelData: function(includeEmptyText, isSubmitting) {
        var me = this,
            data = null;
        // Note that we need to check if this operation is being called from a Submit action
        // because displayfields aren't to be submitted, but they can call this
        // to get their model data.
        if (!me.disabled && (me.submitValue || !isSubmitting)) {
            data = {};
            data[me.getFieldIdentifier()] = me.getValue();
        }
        return data;
    },
    /**
     * Resets the current field value to the originally loaded value and clears any validation
     * messages. See {@link Ext.form.Basic}.{@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad}
     */
    reset: function() {
        var me = this;
        me.beforeReset();
        me.setValue(me.originalValue);
        me.clearInvalid();
        // delete here so we reset back to the original state
        delete me.wasValid;
    },
    /**
     * @method
     * Template method before a field is reset.
     * @protected
     */
    beforeReset: Ext.emptyFn,
    /**
     * Resets the field's {@link #originalValue} property so it matches the current
     * {@link #getValue value}. This is called by
     * {@link Ext.form.Basic}.{@link Ext.form.Basic#setValues setValues} if the form's
     * {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} property is set to true.
     */
    resetOriginalValue: function() {
        this.originalValue = this.getValue();
        this.checkDirty();
    },
    /**
     * Checks whether the value of the field has changed since the last time it was checked.
     * If the value has changed, it:
     *
     * 1. Fires the {@link #change change event},
     * 2. Performs validation if the {@link #validateOnChange} config is enabled, firing the
     *    {@link #validitychange validitychange event} if the validity has changed, and
     * 3. Checks the {@link #isDirty dirty state} of the field and fires the
     *    {@link #dirtychange dirtychange event} if it has changed.
     */
    checkChange: function() {
        var me = this,
            newVal, oldVal;
        if (!me.suspendCheckChange && !me.destroying && !me.destroyed) {
            newVal = me.getValue();
            oldVal = me.lastValue;
            if (me.didValueChange(newVal, oldVal)) {
                me.lastValue = newVal;
                me.fireEvent('change', me, newVal, oldVal);
                me.onChange(newVal, oldVal);
            }
        }
    },
    /**
     * @private
     * Checks if the value has changed. Allows subclasses to override for
     * any more complex logic.
     */
    didValueChange: function(newVal, oldVal) {
        return !this.isEqual(newVal, oldVal);
    },
    /**
     * @private
     * Called when the field's value changes. Performs validation if the {@link #validateOnChange}
     * config is enabled, and invokes the dirty check.
     */
    onChange: function(newVal) {
        var me = this;
        if (me.validateOnChange) {
            me.validate();
        }
        me.checkDirty();
    },
    /**
     * Publish the value of this field.
     *
     * @private
     */
    publishValue: function() {
        var me = this;
        if (me.rendered && !me.getErrors().length) {
            me.publishState('value', me.getValue());
        }
    },
    /**
     * @cfg [publishes='value']
     * @inheritdoc Ext.mixin.Bindable#cfg-publishes
     */
    /**
     * Returns true if the value of this Field has been changed from its {@link #originalValue}.
     * Will always return false if the field is disabled.
     *
     * Note that if the owning {@link Ext.form.Basic form} was configured with
     * {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} then the {@link #originalValue}
     * is updated when the values are loaded by
     * {@link Ext.form.Basic}.{@link Ext.form.Basic#setValues setValues}.
     * @return {Boolean} True if this field has been changed from its original value
     * (and is not disabled), false otherwise.
     */
    isDirty: function() {
        var me = this;
        return !me.disabled && !me.isEqual(me.getValue(), me.originalValue);
    },
    /**
     * Checks the {@link #isDirty} state of the field and if it has changed since the last time
     * it was checked, fires the {@link #dirtychange} event.
     */
    checkDirty: function() {
        var me = this,
            isDirty = me.isDirty();
        if (isDirty !== me.wasDirty) {
            me.dirty = isDirty;
            me.fireEvent('dirtychange', me, isDirty);
            me.onDirtyChange(isDirty);
            me.wasDirty = isDirty;
        }
    },
    /**
     * @method
     * @private
     * Called when the field's dirty state changes.
     * @param {Boolean} isDirty
     */
    onDirtyChange: Ext.emptyFn,
    /**
     * Runs this field's validators and returns an array of error messages for any validation
     * failures. This is called internally during validation and would not usually need to be used
     * manually.
     *
     * Each subclass should override or augment the return value to provide their own errors.
     *
     * @param {Object} value The value to get errors for (defaults to the current field value)
     * @return {String[]} All error messages for this field; an empty Array if none.
     */
    getErrors: function(value) {
        var errors = [],
            validationField = this.getValidationField(),
            validation = this.getValidation(),
            result;
        if (validationField) {
            result = validationField.validate(value, null, null, this._ownerRecord);
            if (result !== true) {
                errors.push(result);
            }
        }
        if (validation && validation !== true) {
            errors.push(validation);
        }
        return errors;
    },
    /**
     * Returns whether or not the field value is currently valid by {@link #getErrors validating}
     * the field's current value. The {@link #validitychange} event will not be fired;
     * use {@link #validate} instead if you want the event to fire.
     * **Note**: {@link #disabled} fields are always treated as valid.
     *
     * Implementations are encouraged to ensure that this method does not have side-effects
     * such as triggering error message display.
     *
     * @return {Boolean} True if the value is valid, else false
     */
    isValid: function() {
        var me = this;
        return me.disabled || Ext.isEmpty(me.getErrors());
    },
    /**
     * Returns whether or not the field value is currently valid by {@link #getErrors validating}
     * the field's current value, and fires the {@link #validitychange} event if the field's
     * validity has changed since the last validation.
     * **Note**: {@link #disabled} fields are always treated as valid.
     *
     * Custom implementations of this method are allowed to have side-effects such as triggering
     * error message display. To validate without side-effects, use {@link #isValid}.
     *
     * @return {Boolean} True if the value is valid, else false
     */
    validate: function() {
        return this.checkValidityChange(this.isValid());
    },
    checkValidityChange: function(isValid) {
        var me = this;
        if (isValid !== me.wasValid) {
            me.wasValid = isValid;
            me.fireEvent('validitychange', me, isValid);
        }
        return isValid;
    },
    /**
     * @private 
     */
    setValidationField: function(value, record) {
        this.callParent([
            value
        ]);
        this._ownerRecord = record;
    },
    /**
     * A utility for grouping a set of modifications which may trigger value changes into a single
     * transaction, to prevent excessive firing of {@link #change} events. This is useful
     * for instance if the field has sub-fields which are being updated as a group;
     * you don't want the container field to check its own changed state for each subfield change.
     * @param {Function} fn The function to call with change checks suspended.
     */
    batchChanges: function(fn) {
        try {
            this.suspendCheckChange++;
            fn();
        } finally {
            this.suspendCheckChange--;
        }
        this.checkChange();
    },
    /**
     * Returns whether this Field is a file upload field; if it returns true, forms will use special
     * techniques for {@link Ext.form.Basic#submit submitting the form} via AJAX.
     * See {@link Ext.form.Basic#hasUpload} for details. If this returns true, the
     * {@link #extractFileInput} method must also be implemented to return the corresponding file
     * input element.
     * @return {Boolean}
     */
    isFileUpload: function() {
        return false;
    },
    /**
     * Only relevant if the instance's {@link #isFileUpload} method returns true. Returns
     * a reference to the file input DOM element holding the user's selected file.
     * The input will be appended into the submission form and will not be returned, so this method
     * should also create a replacement.
     * @return {HTMLElement}
     */
    extractFileInput: function() {
        return null;
    },
    /**
     * @method
     * Display one or more error messages associated with this field, using 
     * {@link Ext.form.Labelable#msgTarget} to determine how to display the messages and 
     * applying {@link Ext.form.Labelable#invalidCls} to the field's UI element.
     *
     *     var formPanel = Ext.create('Ext.form.Panel', {
     *         title: 'Contact Info',
     *         width: 300,
     *         bodyPadding: 10,
     *         renderTo: Ext.getBody(),
     *         items: [{
     *             xtype: 'textfield',
     *             name: 'name',
     *             id: 'nameId',
     *             fieldLabel: 'Name'
     *         }],
     *         bbar: [{
     *             text: 'Mark both fields invalid',
     *             handler: function() {
     *                 var nameField = formPanel.getForm().findField('name');
     *                 nameField.markInvalid('Name invalid message');
     *     
     *                 // multiple error string syntax
     *                 // nameField.markInvalid(['First message', 'Second message']);
     *             }
     *         }]
     *     });
     * 
     * **Note**: this method does not cause the Field's {@link #validate} or 
     * {@link #isValid} methods to return `false` if the value does _pass_ validation. 
     * So simply marking a Field as invalid will not prevent submission of forms
     * submitted with the {@link Ext.form.action.Submit#clientValidation} option set.
     * 
     * @param {String/String[]} errors The validation message(s) to display.
     */
    markInvalid: Ext.emptyFn,
    /**
     * @method clearInvalid
     * Clear any invalid styles/messages for this field. Components using this mixin should
     * implement this method to update the components rendering to clear any existing messages.
     *
     * **Note**: this method does not cause the Field's {@link #validate} or {@link #isValid}
     * methods to return `true` if the value does not _pass_ validation. So simply clearing
     * a field's errors will not necessarily allow submission of forms submitted with the
     * {@link Ext.form.action.Submit#clientValidation} option set.
     */
    clearInvalid: Ext.emptyFn,
    updateValidation: function(validation, oldValidation) {
        // Only validate if the validation is changing, not when we initial set it,
        // otherwise it will mark the field invalid as soon as it is bound.
        if (oldValidation) {
            this.validate();
        }
    },
    privates: {
        resetToInitialValue: function() {
            var me = this,
                originalValue = me.originalValue;
            me.originalValue = me.initialValue;
            me.reset();
            me.originalValue = originalValue;
        }
    }
});

/**
 * Base class for form fields that provides default event handling, rendering, and other common
 * functionality needed by all form field types. Utilizes the {@link Ext.form.field.Field} mixin
 * for value handling and validation, and the {@link Ext.form.Labelable} mixin to provide label
 * and error message display.
 *
 * In most cases you will want to use a subclass, such as {@link Ext.form.field.Text} or
 * {@link Ext.form.field.Checkbox}, rather than creating instances of this class directly. However
 * if you are implementing a custom form field, using this as the parent class is recommended.
 *
 * # Values and Conversions
 *
 * Because Base implements the Field mixin, it has a main value that can be initialized with the
 * {@link #value} config and manipulated via the {@link #getValue} and {@link #setValue} methods.
 * This main value can be one of many data types appropriate to the current field, for instance a
 * {@link Ext.form.field.Date Date} field would use a JavaScript Date object as its value type.
 * However, because the field is rendered as a HTML input, this value data type can not always
 * be directly used in the rendered field.
 *
 * Therefore Base introduces the concept of a "raw value". This is the value of the rendered HTML
 * input field, and is normally a String. The {@link #getRawValue} and {@link #setRawValue} methods
 * can be used to directly work with the raw value, though it is recommended to use getValue
 * and setValue in most cases.
 *
 * Conversion back and forth between the main value and the raw value is handled by the
 * {@link #valueToRaw} and {@link #rawToValue} methods. If you are implementing a subclass
 * that uses a non-String value data type, you should override these methods to handle
 * the conversion.
 *
 * # Rendering
 *
 * The content of the field body is defined by the {@link #fieldSubTpl} XTemplate, with its argument
 * data created by the {@link #getSubTplData} method. Override this template and/or method to create
 * custom field renderings.
 */
Ext.define('Ext.form.field.Base', {
    extend: 'Ext.Component',
    alternateClassName: [
        'Ext.form.Field',
        'Ext.form.BaseField'
    ],
    xtype: 'field',
    requires: [
        'Ext.util.DelayedTask',
        'Ext.XTemplate'
    ],
    mixins: [
        'Ext.form.Labelable',
        'Ext.form.field.Field'
    ],
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: true,
    /**
     * @cfg shrinkWrap
     * @inheritdoc
     */
    shrinkWrap: true,
    /* eslint-disable indent, max-len */
    /**
     * @cfg {Ext.XTemplate} fieldSubTpl
     * The content of the field body is defined by this config option.
     * @private
     */
    fieldSubTpl: [
        // note: {id} here is really {inputId}, but {cmpId} is available
        '<input id="{id}" data-ref="inputEl" type="{type}" {inputAttrTpl}',
        ' size="1"',
        // allows inputs to fully respect CSS widths across all browsers
        '<tpl if="name"> name="{name}"</tpl>',
        '<tpl if="value"> value="{[Ext.util.Format.htmlEncode(values.value)]}"</tpl>',
        '<tpl if="placeholder"> placeholder="{placeholder}"</tpl>',
        '{%if (values.maxLength !== undefined){%} maxlength="{maxLength}"{%}%}',
        '<tpl if="readOnly"> readonly="readonly"</tpl>',
        '<tpl if="disabled"> disabled="disabled"</tpl>',
        '<tpl if="tabIdx != null"> tabindex="{tabIdx}"</tpl>',
        '<tpl if="fieldStyle"> style="{fieldStyle}"</tpl>',
        '<tpl if="ariaEl == \'inputEl\'">',
        '<tpl foreach="ariaElAttributes"> {$}="{.}"</tpl>',
        '</tpl>',
        '<tpl foreach="inputElAriaAttributes"> {$}="{.}"</tpl>',
        ' class="{fieldCls} {typeCls} {typeCls}-{ui} {editableCls} {inputCls} {fixCls}" autocomplete="off"/>',
        {
            disableFormats: true
        }
    ],
    /* eslint-enable indent, max-len */
    /**
     * @property defaultBindProperty
     * @inheritdoc
     */
    defaultBindProperty: 'value',
    /**
     * @cfg autoEl
     * @inheritdoc
     */
    autoEl: {
        role: 'presentation'
    },
    subTplInsertions: [
        /**
         * @cfg {String/Array/Ext.XTemplate} inputAttrTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * inside the input element (as attributes). If an `XTemplate` is used, the component's
         * {@link #getSubTplData subTpl data} serves as the context.
         */
        'inputAttrTpl'
    ],
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        /**
         * @property {Ext.dom.Element} inputEl
         * The input Element for this Field. Only available after the field has been rendered.
         */
        'inputEl'
    ],
    /**
     * @cfg {String} name
     * The name of the field. This is used as the parameter name when including the field value
     * in a {@link Ext.form.Basic#submit form submit()}. If no name is configured, it falls back
     * to the {@link #inputId}. To prevent the field from being included in the form submit,
     * set {@link #submitValue} to false.
     */
    /**
     * @cfg {String} inputType
     * The type attribute for input fields -- e.g. radio, text, password, file. The extended types
     * supported by HTML5 inputs (url, email, etc.) may also be used, though using them will cause
     * older browsers to fall back to 'text'.
     *
     * The type 'password' must be used to render that field type currently -- there is no separate
     * Ext component for that. You can use {@link Ext.form.field.File} which creates
     * a custom-rendered file upload field, but if you want a plain unstyled file input you can use
     * a Base with inputType:'file'.
     */
    inputType: 'text',
    /**
     * @cfg {Boolean} isTextInput
     * `true` if this field renders as a text input.
     *
     * @private
     * @since 5.0.1
     */
    isTextInput: true,
    /**
     * @cfg {Number} tabIndex
     * 
     * Sets a DOM tabIndex for this field. tabIndex may be set to `-1` in order to remove
     * the field from the tab rotation.
     * 
     * **Note:** tabIndex only applies to fields that are rendered.  It does not effect 
     * fields built via applyTo
     */
    /**
     * @cfg {String} invalidText
     * The error text to use when marking a field invalid and no message is provided
     * @locale
     */
    invalidText: 'The value in this field is invalid',
    /**
     * @cfg {String} fieldCls
     * The default CSS class for the field input
     */
    fieldCls: Ext.baseCSSPrefix + 'form-field',
    /**
     * @cfg {String} fieldStyle
     * Optional CSS style(s) to be applied to the {@link #inputEl field input element}.
     * Should be a valid argument to {@link Ext.dom.Element#applyStyles}. Defaults to undefined.
     * See also the {@link #setFieldStyle} method for changing the style after initialization.
     */
    /**
     * @cfg [publishes=['rawValue', 'value', 'dirty']]
     * @inheritdoc
     */
    /**
     * @cfg {String} focusCls
     * The CSS class to use when the field receives focus
     */
    focusCls: 'form-focus',
    /**
     * @cfg {String} dirtyCls
     * The CSS class to use when the field value {@link #isDirty is dirty}.
     */
    dirtyCls: Ext.baseCSSPrefix + 'form-dirty',
    /**
     * @cfg {String[]} checkChangeEvents
     * A list of event names that will be listened for on the field's
     * {@link #inputEl input element}, which will cause the field's value to be checked for changes.
     * If a change is detected, the {@link #change change event} will be fired, followed by
     * validation if the {@link #validateOnChange} option is enabled.
     *
     * Defaults to ['change', 'propertychange', 'keyup'] in Internet Explorer, and
     * ['change', 'input', 'textInput', 'keyup', 'dragdrop'] in other browsers.
     * This catches all the ways that field values can be changed in most supported browsers;
     * the only known exceptions at the time of writing are:
     *
     *   - Safari 3.2 and older: cut/paste in textareas via the context menu, and dragging text
     *     into textareas
     *   - Opera 10 and 11: dragging text into text fields and textareas, and cut via the context
     *     menu in text fields and textareas
     *   - Opera 9: Same as Opera 10 and 11, plus paste from context menu in text fields
     *     and textareas
     *
     * If you need to guarantee on-the-fly change notifications including these edge cases, you can
     * call the {@link #checkChange} method on a repeating interval, e.g. using
     * {@link Ext.TaskManager}, or if the field is within a {@link Ext.form.Panel}, you can use
     * the FormPanel's {@link Ext.form.Panel#pollForChanges} configuration to set up
     * such a task automatically.
     */
    checkChangeEvents: Ext.isIE && (!document.documentMode || document.documentMode <= 9) ? [
        'change',
        'propertychange',
        'keyup'
    ] : [
        'change',
        'input',
        'textInput',
        'keyup',
        'dragdrop'
    ],
    // While input is supported in IE9, we use attachEvent for events, so we need to fall back here
    ignoreChangeRe: /data-errorqtip|style\.|className/,
    /**
     * @cfg {Number} checkChangeBuffer
     * Defines a timeout in milliseconds for buffering {@link #cfg!checkChangeEvents} that fire
     * in rapid succession.
     * Defaults to 50 milliseconds.
     */
    checkChangeBuffer: 50,
    /**
     * @cfg liquidLayout
     * @inheritdoc
     */
    liquidLayout: true,
    /**
     * @cfg {Boolean} readOnly
     * true to mark the field as readOnly in HTML.
     */
    readOnly: false,
    /**
     * @cfg {String} readOnlyCls
     * The CSS class applied to the component's main element when it is {@link #readOnly}.
     */
    readOnlyCls: Ext.baseCSSPrefix + 'form-readonly',
    /**
     * @cfg {String} inputId
     * The id that will be given to the generated input DOM element. Defaults to an automatically
     * generated id. If you configure this manually, you must make sure it is unique
     * in the document.
     */
    /**
     * @cfg {Boolean} [validateOnBlur=true]
     * Whether the field should validate when it loses focus. This will cause fields to be validated
     * as the user steps through the fields in the form regardless of whether they are making
     * changes to those fields along the way. See also {@link #validateOnChange}.
     */
    validateOnBlur: true,
    /**
     * @cfg {Boolean} [validateOnFocusLeave=false] Set to `true` to validate the field
     * when focus leaves the field's component hierarchy entirely.
     *
     * The difference between  {@link #validateOnBlur} and this option is that the former
     * will happen when field's _input element_ blurs. In complex fields such as ComboBox
     * or Date focus may leave the input element to the drop-down picker, which will cause
     * {@link #validateOnBlur} to happen prematurely.
     *
     * Using this option is recommended for accessible applications. The default value
     * is `false` for backwards compatibility; this option and {@link #validateOnBlur}
     * are mutually exclusive.
     *
     * @since 6.5.3
     */
    validateOnFocusLeave: false,
    /**
     * @cfg {String} formatText
     * Helpful text describing acceptable format for field values. This text will be
     * announced by Assistive Technologies such as screen readers when the field is
     * focused.
     *
     * This option is superseded by {@link #ariaHelp}.
     *
     * @deprecated 6.2.0 This config is deprecated.
     * @locale
     */
    /**
     * @private
     */
    hasFocus: false,
    /**
     * @cfg baseCls
     * @inheritdoc
     */
    baseCls: Ext.baseCSSPrefix + 'field',
    /**
     * @cfg fieldBodyCls
     * @inheritdoc
     */
    fieldBodyCls: Ext.baseCSSPrefix + 'field-body',
    webkitBorderBoxBugCls: Ext.baseCSSPrefix + 'webkit-border-box-bug',
    /**
     * @property maskOnDisable
     * @inheritdoc
     */
    maskOnDisable: false,
    // Instructs the layout to stretch the inputEl to 100% width when laying
    // out under fixed conditions. Defaults to true for all fields except check/radio
    // Doesn't seem worth it to introduce a whole new layout class just for this flag
    stretchInputElFixed: true,
    // Form fields render their ARIA attributes to the inputEl
    /**
     * @property ariaEl
     * @inheritdoc
     */
    ariaEl: 'inputEl',
    /**
     * @property focusEl
     * @inheritdoc
     */
    focusEl: 'inputEl',
    renderAriaElements: true,
    /**
     * @event specialkey
     * Fires when any key related to navigation (arrows, tab, enter, esc, etc.) is pressed.
     * To handle other keys see {@link Ext.util.KeyMap}. You can check
     * {@link Ext.event.Event#getKey} to determine which key was pressed.
     * For example:
     *
     *     var form = new Ext.form.Panel({
     *         ...
     *         items: [{
     *                 fieldLabel: 'Field 1',
     *                 name: 'field1',
     *                 allowBlank: false
     *             },{
     *                 fieldLabel: 'Field 2',
     *                 name: 'field2',
     *                 listeners: {
     *                     specialkey: function(field, e){
     *                         // e.HOME, e.END, e.PAGE_UP, e.PAGE_DOWN,
     *                         // e.TAB, e.ESC, arrow keys: e.LEFT, e.RIGHT, e.UP, e.DOWN
     *                         if (e.getKey() == e.ENTER) {
     *                             var form = field.up('form').getForm();
     *                             form.submit();
     *                         }
     *                     }
     *                 }
     *             }
     *         ],
     *         ...
     *     });
     *
     * @param {Ext.form.field.Base} this
     * @param {Ext.event.Event} e The event object
     */
    /**
     * @event writeablechange
     * Fires when this field changes its read-only status.
     * @param {Ext.form.field.Base} this
     * @param {Boolean} Read only flag
     */
    initComponent: function() {
        var me = this;
        me.callParent();
        me.subTplData = me.subTplData || {};
        // Init mixins
        me.initLabelable();
        me.initField();
        me.initDefaultName();
        // validateOnBlur and validateOnFocusLeave are mutually exclusive,
        // with latter taking precedence
        if (me.validateOnFocusLeave) {
            me.validateOnBlur = false;
        }
        // Add to protoEl before render
        if (me.readOnly) {
            me.addCls(me.readOnlyCls);
        }
        me.addCls(Ext.baseCSSPrefix + 'form-type-' + me.inputType);
        // formatText is superseded by ariaHelp but we still apply it for compatibility
        if (me.format && me.formatText && !me.ariaHelp) {
            me.ariaHelp = Ext.String.format(me.formatText, me.format);
        }
    },
    /**
     * @private
     */
    initDefaultName: function() {
        var me = this;
        // Default name to inputId
        if (!me.name) {
            me.name = me.getInputId();
        }
    },
    /**
     * Returns the input id for this field. If none was specified via the {@link #inputId} config,
     * then an id will be automatically generated.
     */
    getInputId: function() {
        return this.inputId || (this.inputId = this.id + '-inputEl');
    },
    /**
     * Creates and returns the data object to be used when rendering the {@link #fieldSubTpl}.
     * @return {Object} The template data
     * @template
     */
    getSubTplData: function(fieldData) {
        var me = this,
            id = me.id,
            type = me.inputType,
            inputId = me.getInputId(),
            inputCls = me.inputCls || '',
            fixCls = '',
            data, ariaAttr, inputElAttr;
        if (Ext.supports.WebKitInputTableBoxModelBug) {
            // workaround for https://bugs.webkit.org/show_bug.cgi?id=137693
            // Can't use inputCls or typeCls here since they will be appended
            // with ui in different subclasses which breaks things.
            fixCls += me.webkitBorderBoxBugCls;
        }
        data = Ext.apply({
            ui: me.ui,
            id: inputId,
            cmpId: id,
            name: me.name || inputId,
            disabled: me.disabled,
            readOnly: me.readOnly,
            value: me.getRawValue(),
            type: type,
            fieldCls: me.fieldCls,
            fieldStyle: me.getFieldStyle(),
            childElCls: fieldData.childElCls,
            tabIdx: me.tabIndex,
            inputCls: inputCls,
            typeCls: Ext.baseCSSPrefix + 'form-' + (me.isTextInput ? 'text' : type),
            fixCls: fixCls,
            ariaEl: me.ariaEl
        }, me.subTplData);
        if (me.ariaRole) {
            ariaAttr = {};
            if (!me.ariaStaticRoles[me.ariaRole]) {
                // When ARIA attributes are rendered they should always reflect
                // component's state. This contrasts with the standard HTML attributes
                // like disabled and readonly, which are only present when enabled.
                ariaAttr['aria-hidden'] = !!me.hidden;
                ariaAttr['aria-disabled'] = !!me.disabled;
                // For most of the fields ariaEl === inputEl but certain types like Combo boxes
                // and their descendants are compound widgets and need to have ARIA attributes
                // on different elements.
                inputElAttr = {
                    // Input fields start out as valid
                    'aria-invalid': false,
                    'aria-readonly': !!me.readOnly
                };
                // aria-label is not present by default, and aria-labelledby
                // generally should not be used for fields' inputEls since usually
                // they are referenced by their respective <label> elements.
                if (me.ariaLabel) {
                    ariaAttr['aria-label'] = Ext.String.htmlEncode(me.ariaLabel);
                }
                ariaAttr = Ext.apply(ariaAttr, me.getAriaAttributes());
                // If aria-describedby was set explicitly, don't override. Note that
                // describedby applies to inputEl since most often that's the focusable
                // element.
                if (!ariaAttr['aria-describedby']) {
                    if (me.ariaHelp) {
                        inputElAttr['aria-describedby'] = id + '-ariaStatusEl ' + id + '-ariaHelpEl';
                    } else {
                        inputElAttr['aria-describedby'] = id + '-ariaStatusEl';
                    }
                }
                data.inputElAriaAttributes = inputElAttr;
            }
            if (me.ariaRole !== 'native') {
                ariaAttr.role = me.ariaRole;
            }
            // aria-label is not present by default, and aria-labelledby
            // generally should not be used for fields' inputEls since usually
            // they are referenced by their respective <label> elements.
            if (me.ariaLabel) {
                ariaAttr['aria-label'] = me.ariaLabel;
            }
            if (me.format && me.formatText && !data.title) {
                ariaAttr.title = Ext.String.formatEncode(me.formatText, me.format);
            }
            data.ariaElAttributes = ariaAttr;
        }
        me.getInsertionRenderData(data, me.subTplInsertions);
        return data;
    },
    /**
     * Gets the markup to be inserted into the outer template's bodyEl.
     * For fields this is the actual input element.
     * @protected
     */
    getSubTplMarkup: function(fieldData) {
        var me = this,
            data = me.getSubTplData(fieldData),
            preSubTpl = me.lookupTpl('preSubTpl'),
            postSubTpl = me.lookupTpl('postSubTpl'),
            markup = '';
        if (preSubTpl) {
            markup += preSubTpl.apply(data);
        }
        markup += me.lookupTpl('fieldSubTpl').apply(data);
        if (postSubTpl) {
            markup += postSubTpl.apply(data);
        }
        return markup;
    },
    initRenderData: function() {
        return Ext.applyIf(this.callParent(), this.getLabelableRenderData());
    },
    /**
     * Set the {@link #fieldStyle CSS style} of the {@link #inputEl field input element}.
     * @param {String/Object/Function} style The style(s) to apply. Should be a valid argument
     * to {@link Ext.dom.Element#applyStyles}.
     */
    setFieldStyle: function(style) {
        var me = this,
            inputEl = me.inputEl;
        if (inputEl) {
            inputEl.applyStyles(style);
        }
        me.fieldStyle = style;
    },
    getFieldStyle: function() {
        var style = this.fieldStyle;
        return Ext.isObject(style) ? Ext.DomHelper.generateStyles(style, null, true) : style || '';
    },
    onRender: function() {
        // This noOptimize can be removed after SDKTOOLS-946 is fixed
        // @noOptimize.callParent
        this.callParent(arguments);
        this.mixins.labelable.self.initTip();
        this.renderActiveError();
    },
    beforeBlur: function(e) {
        if (this.validateOnBlur) {
            this.validate();
        }
    },
    onFocusLeave: function(e) {
        if (this.validateOnFocusLeave) {
            this.validate();
        }
        this.callParent([
            e
        ]);
        this.completeEdit();
    },
    /**
     * @method
     * @protected
     * Called when focus leaves this input field.
     * Used to postprocess raw values and perform conversion and validation.
     */
    completeEdit: Ext.emptyFn,
    isFileUpload: function() {
        return this.inputType === 'file';
    },
    /**
     * @private
     * Private override to use getSubmitValue() as a convenience
     */
    getSubmitData: function() {
        var me = this,
            data = null,
            val;
        if (!me.disabled && me.submitValue) {
            val = me.getSubmitValue();
            if (val !== null) {
                data = {};
                data[me.getName()] = val;
            }
        }
        return data;
    },
    /**
     * Returns the value that would be included in a standard form submit for this field.
     * This will be combined with the field's name to form a name=value pair in the
     * {@link #getSubmitData submitted parameters}. If an empty string is returned then just
     * the name= will be submitted; if null is returned then nothing will be submitted.
     *
     * Note that the value returned will have been {@link #processRawValue processed}
     * but may or may not have been successfully {@link #validate validated}.
     *
     * @return {String} The value to be submitted, or null.
     */
    getSubmitValue: function() {
        return this.processRawValue(this.getRawValue());
    },
    /**
     * Returns the raw value of the field, without performing any normalization, conversion,
     * or validation. To get a normalized and converted value see {@link #getValue}.
     * @return {String} value The raw String value of the field
     */
    getRawValue: function() {
        var me = this,
            v = (me.inputEl ? me.inputEl.getValue() : Ext.valueFrom(me.rawValue, ''));
        me.rawValue = v;
        return v;
    },
    /**
     * Sets the field's raw value directly, bypassing {@link #valueToRaw value conversion},
     * change detection, and validation. To set the value with these additional inspections
     * see {@link #setValue}.
     * @param {Object} value The value to set
     * @return {Object} value The field value that is set
     */
    setRawValue: function(value) {
        var me = this,
            rawValue = me.rawValue;
        if (!me.transformRawValue.$nullFn) {
            value = me.transformRawValue(value);
        }
        value = Ext.valueFrom(value, '');
        if (rawValue === undefined || rawValue !== value) {
            me.rawValue = value;
            // Some Field subclasses may not render an inputEl
            if (me.inputEl) {
                me.bindChangeEvents(false);
                me.inputEl.dom.value = value;
                me.bindChangeEvents(true);
            }
        }
        if (me.rendered && me.reference) {
            me.publishState('rawValue', value);
        }
        return value;
    },
    /**
     * @method
     * Transform the raw value before it is set
     * @protected
     * @param {Object} value The value
     * @return {Object} The value to set
     */
    transformRawValue: Ext.identityFn,
    /**
     * Converts a mixed-type value to a raw representation suitable for displaying in the field.
     * This allows controlling how value objects passed to {@link #setValue} are shown to the user,
     * including localization. For instance, for a {@link Ext.form.field.Date}, this would control
     * how a Date object passed to {@link #setValue} would be converted to a String for display
     * in the field.
     *
     * See {@link #rawToValue} for the opposite conversion.
     *
     * The base implementation simply does a standard toString conversion, and converts
     * {@link Ext#isEmpty empty values} to an empty string.
     *
     * @param {Object} value The mixed-type value to convert to the raw representation.
     * @return {Object} The converted raw value.
     */
    valueToRaw: function(value) {
        return '' + Ext.valueFrom(value, '');
    },
    /**
     * Converts a raw input field value into a mixed-type value that is suitable for this particular
     * field type. This allows controlling the normalization and conversion of user-entered values
     * into field-type-appropriate values, e.g. a Date object for {@link Ext.form.field.Date},
     * and is invoked by {@link #getValue}.
     *
     * It is up to individual implementations to decide how to handle raw values that cannot be
     * successfully converted to the desired object type.
     *
     * See {@link #valueToRaw} for the opposite conversion.
     *
     * The base implementation does no conversion, returning the raw value untouched.
     *
     * @param {Object} rawValue
     * @return {Object} The converted value.
     * @method
     */
    rawToValue: Ext.identityFn,
    /**
     * Performs any necessary manipulation of a raw field value to prepare it for
     * {@link #rawToValue conversion} and/or {@link #validate validation}, for instance
     * stripping out ignored characters. In the base implementation it does nothing;
     * individual subclasses may override this as needed.
     *
     * @param {Object} value The unprocessed string value
     * @return {Object} The processed string value
     * @method
     */
    processRawValue: Ext.identityFn,
    /**
     * Returns the current data value of the field. The type of value returned is particular
     * to the type of the particular field (e.g. a Date object for {@link Ext.form.field.Date}),
     * as the result of calling {@link #rawToValue} on the field's
     * {@link #processRawValue processed} String value. To return the raw String value,
     * see {@link #getRawValue}.
     * @return {Object} value The field value
     */
    getValue: function() {
        var me = this,
            val = me.rawToValue(me.processRawValue(me.getRawValue()));
        me.value = val;
        return val;
    },
    /**
     * Sets a data value into the field and runs the change detection and validation.
     * To set the value directly without these inspections see {@link #setRawValue}.
     * @param {Object} value The value to set
     * @return {Ext.form.field.Field} this
     */
    setValue: function(value) {
        var me = this;
        me.setRawValue(me.valueToRaw(value));
        return me.mixins.field.setValue.call(me, value);
    },
    onBoxReady: function() {
        var me = this;
        me.callParent(arguments);
        if (me.setReadOnlyOnBoxReady) {
            me.setReadOnly(me.readOnly);
        }
    },
    onDisable: function() {
        var me = this,
            inputEl = me.inputEl;
        me.callParent();
        if (inputEl) {
            inputEl.dom.disabled = true;
            if (me.hasActiveError()) {
                // clear invalid state since the field is now disabled
                me.clearInvalid();
                me.hadErrorOnDisable = true;
            }
        }
        // Disabled fields always validate to true, so if
        // wasValid is false, the state will have changed, but
        // we only want to do so if we have a previous wasValid value
        if (me.wasValid === false) {
            me.checkValidityChange(true);
        }
    },
    onEnable: function() {
        var me = this,
            inputEl = me.inputEl,
            mark = me.preventMark,
            valid;
        me.callParent();
        if (inputEl) {
            inputEl.dom.disabled = false;
        }
        if (me.wasValid !== undefined) {
            me.forceValidation = true;
            me.preventMark = !me.hadErrorOnDisable;
            valid = me.isValid();
            me.forceValidation = false;
            me.preventMark = mark;
            me.checkValidityChange(valid);
        }
        delete me.hadErrorOnDisable;
    },
    /**
     * Sets the read only state of this field.
     * @param {Boolean} readOnly Whether the field should be read only.
     */
    setReadOnly: function(readOnly) {
        var me = this,
            inputEl = me.inputEl,
            old = me.readOnly;
        readOnly = !!readOnly;
        me[readOnly ? 'addCls' : 'removeCls'](me.readOnlyCls);
        me.readOnly = readOnly;
        if (inputEl) {
            inputEl.dom.readOnly = readOnly;
            inputEl.dom.setAttribute('aria-readonly', readOnly);
        } else if (me.rendering) {
            me.setReadOnlyOnBoxReady = true;
        }
        if (readOnly !== old) {
            me.fireEvent('writeablechange', me, readOnly);
        }
    },
    /**
     * @private
     */
    fireKey: function(e, eOpts) {
        if (e.isSpecialKey()) {
            this.fireEvent('specialkey', this, e, eOpts);
        }
    },
    initEvents: function() {
        var me = this,
            inputEl = me.inputEl,
            onFieldMutation = me.onFieldMutation,
            events = me.checkChangeEvents,
            len = events.length,
            i, event;
        if (inputEl) {
            me.mon(inputEl, Ext.supports.SpecialKeyDownRepeat ? 'keydown' : 'keypress', me.fireKey, me);
            for (i = 0; i < len; ++i) {
                event = events[i];
                if (event === 'propertychange') {
                    me.usesPropertychange = true;
                }
                if (event === 'textInput') {
                    me.usesTextInput = true;
                }
                me.mon(inputEl, event, onFieldMutation, me);
            }
        }
        me.callParent();
    },
    /**
     * @private
     * Called when some event (See the checkChangeEvents property) mutates the input field.
     * We react to changes.
     *
     * Subclasses may provide an inplementation which may perform other tasks (eg ComboBox value
     * matching) before calling the checkChange method.
     */
    onFieldMutation: function(e) {
        // When using propertychange, we want to skip out on various values, since they won't cause
        // the underlying value to change.
        if (!this.readOnly && !(e.type === 'propertychange' && this.ignoreChangeRe.test(e.browserEvent.propertyName))) {
            this.startCheckChangeTask();
        }
    },
    startCheckChangeTask: function() {
        var me = this,
            task = me.checkChangeTask;
        if (!task) {
            me.checkChangeTask = task = new Ext.util.DelayedTask(me.doCheckChangeTask, me);
        }
        if (!me.bindNotifyListener) {
            // We continually create/destroy the listener as needed (see doCheckChangeTask)
            // because we're listening to a global event, so we don't want the event
            // to be triggered unless absolutely necessary. In this case,
            // we only need to fix the value when we have a pending change to check.
            me.bindNotifyListener = Ext.on('beforebindnotify', me.onBeforeNotify, me, {
                destroyable: true
            });
        }
        task.delay(me.checkChangeBuffer);
    },
    doCheckChangeTask: function() {
        var bindNotifyListener = this.bindNotifyListener;
        if (bindNotifyListener) {
            bindNotifyListener.destroy();
            this.bindNotifyListener = null;
        }
        this.checkChange();
    },
    publishValue: function() {
        var me = this;
        if (me.rendered && !me.getErrors().length) {
            me.publishState('value', me.getValue());
        }
    },
    /**
     * @private
     * Called when the field's dirty state changes. Adds/removes the {@link #dirtyCls}
     * on the main element.
     * @param {Boolean} isDirty
     */
    onDirtyChange: function(isDirty) {
        var me = this;
        me[isDirty ? 'addCls' : 'removeCls'](me.dirtyCls);
        if (me.rendered && me.reference) {
            me.publishState('dirty', isDirty);
        }
    },
    /**
     * Returns whether or not the field value is currently valid by {@link #getErrors validating}
     * the {@link #processRawValue processed raw value} of the field. **Note**: {@link #disabled}
     * fields are always treated as valid.
     *
     * @return {Boolean} True if the value is valid, else false
     */
    isValid: function() {
        var me = this,
            disabled = me.disabled,
            validate = me.forceValidation || !disabled;
        return validate ? me.validateValue(me.processRawValue(me.getRawValue())) : disabled;
    },
    /**
     * Uses {@link #getErrors} to build an array of validation errors. If any errors are found,
     * they are passed to {@link #markInvalid} and false is returned, otherwise true is returned.
     *
     * Previously, subclasses were invited to provide an implementation of this to process
     * validations - from 3.2 onwards {@link #getErrors} should be overridden instead.
     *
     * @param {Object} value The value to validate
     * @return {Boolean} True if all validations passed, false if one or more failed
     */
    validateValue: function(value) {
        var me = this,
            errors = me.getErrors(value),
            isValid = Ext.isEmpty(errors);
        if (!me.preventMark) {
            if (isValid) {
                me.clearInvalid();
            } else {
                me.markInvalid(errors);
            }
        }
        return isValid;
    },
    /**
     * @method markInvalid
     * @inheritdoc Ext.form.field.Field#method-markInvalid
     */
    markInvalid: function(errors) {
        // Save the message and fire the 'invalid' event
        var me = this,
            oldMsg = me.getActiveError(),
            active;
        me.setActiveErrors(Ext.Array.from(errors));
        active = me.getActiveError();
        if (oldMsg !== active) {
            me.setError(active);
            if (!me.ariaStaticRoles[me.ariaRole] && me.inputEl) {
                me.inputEl.dom.setAttribute('aria-invalid', true);
            }
        }
    },
    /**
     * Clear any invalid styles/messages for this field.
     *
     * **Note**: this method does not cause the Field's {@link #validate} or {@link #isValid}
     * methods to return `true` if the value does not _pass_ validation. So simply clearing
     * a field's errors will not necessarily allow submission of forms submitted with the
     * {@link Ext.form.action.Submit#clientValidation} option set.
     */
    clearInvalid: function() {
        // Clear the message and fire the 'valid' event
        var me = this,
            hadError = me.hasActiveError();
        delete me.hadErrorOnDisable;
        me.unsetActiveError();
        if (hadError) {
            me.setError('');
            if (!me.ariaStaticRoles[me.ariaRole] && me.inputEl) {
                me.inputEl.dom.setAttribute('aria-invalid', false);
            }
        }
    },
    /**
     * Set the current error state
     * @private
     * @param {String} error The error message to set
     */
    setError: function(error) {
        var me = this,
            msgTarget = me.msgTarget,
            prop;
        if (me.rendered) {
            if (msgTarget === 'title' || msgTarget === 'qtip') {
                prop = msgTarget === 'qtip' ? 'data-errorqtip' : 'title';
                me.getActionEl().dom.setAttribute(prop, error || '');
            } else {
                me.updateLayout();
            }
        }
    },
    /**
     * @private
     * Overrides the method from the Ext.form.Labelable mixin to also add the invalidCls
     * to the inputEl, as that is required for proper styling in IE with nested fields
     * (due to lack of child selector)
     */
    renderActiveError: function() {
        var me = this,
            hasError = me.hasActiveError(),
            invalidCls = me.invalidCls + '-field';
        if (me.inputEl) {
            // Add/remove invalid class
            me.inputEl[hasError ? 'addCls' : 'removeCls']([
                invalidCls,
                invalidCls + '-' + me.ui
            ]);
        }
        me.mixins.labelable.renderActiveError.call(me);
    },
    doDestroy: function() {
        var me = this,
            task = me.checkChangeTask;
        if (task) {
            task.cancel();
        }
        Ext.destroy(me.bindNotifyListener);
        me.cleanupField();
        me.callParent();
    },
    privates: {
        applyBind: function(bind, currentBindings) {
            var me = this,
                valueBinding = currentBindings && currentBindings.value,
                bindings, newValueBind;
            bindings = me.callParent([
                bind,
                currentBindings
            ]);
            if (bindings) {
                newValueBind = bindings.value;
                me.hasBindingValue = !!newValueBind;
                if (newValueBind !== valueBinding && me.getInherited().modelValidation) {
                    me.updateValueBinding(bindings);
                }
            }
            return bindings;
        },
        applyRenderSelectors: function() {
            var me = this;
            me.callParent();
            // If the inputId config is not specified then normal childEls will pick up
            // our inputEl. Otherwise we need to get it now.
            if (!me.inputEl) {
                me.inputEl = me.el.getById(me.getInputId());
            }
        },
        // These 2 events trigger when setting the value programmatically in IE.
        // propertychange for older browsers, textInput for IE10+. Here we 
        bindChangeEvents: function(active) {
            var method = active ? 'resumeEvent' : 'suspendEvent',
                inputEl = this.inputEl;
            if (this.usesPropertychange) {
                inputEl[method]('propertychange');
            }
            if (this.usesTextInput) {
                inputEl[method]('textInput');
            }
        },
        getActionEl: function() {
            return this.inputEl || this.el;
        },
        getFocusEl: function() {
            return this.inputEl;
        },
        initRenderTpl: function() {
            var me = this;
            if (!me.hasOwnProperty('renderTpl')) {
                me.renderTpl = me.lookupTpl('labelableRenderTpl');
            }
            return me.callParent();
        },
        onBeforeNotify: function() {
            // This event is fired before the scheduler fires off any bindings.
            // If we happen to be in the state where we are pending a state change check,
            // force it to flush here so that we have the correct state in the viewmodel before
            // the bindings trigger, otherwise we may get an old value pushed into the field before
            // it runs the check.
            this.checkChangeTask.cancel();
            this.checkChange();
        },
        updateValueBinding: function(bindings) {
            var me = this,
                newBinding = bindings.value,
                fieldBinding = bindings.$fieldBinding;
            if (fieldBinding) {
                fieldBinding.destroy();
                bindings.$fieldBinding = null;
            }
            if (newBinding && newBinding.bindValidationField) {
                me.fieldBinding = newBinding.bindValidationField('setValidationField', me);
            }
        }
    },
    deprecated: {
        "5": {
            methods: {
                doComponentLayout: function() {
                    // In IE if propertychange is one of the checkChangeEvents, we need to remove
                    // the listener prior to layout and re-add it after, to prevent it from firing
                    // needlessly for attribute and style changes applied to the inputEl.
                    this.bindChangeEvents(false);
                    this.callParent(arguments);
                    this.bindChangeEvents(true);
                }
            }
        }
    }
});

Ext.define('Ext.layout.component.field.Text', {
    extend: 'Ext.layout.component.Auto',
    alias: 'layout.textfield',
    beginLayoutCycle: function(ownerContext, firstCycle) {
        var target = ownerContext.target;
        ownerContext.el.toggleCls(target.heightedCls, !ownerContext.heightModel.shrinkWrap || target.minHeight != null);
        this.callParent([
            ownerContext,
            firstCycle
        ]);
    }
});

/**
 * @singleton
 * @alternateClassName Ext.form.VTypes
 *
 * This is a singleton object which contains a set of commonly used field validation functions
 * and provides a mechanism for creating reusable custom field validations.
 * The following field validation functions are provided out of the box:
 *
 * - {@link #alpha}
 * - {@link #alphanum}
 * - {@link #email}
 * - {@link #url}
 *
 * VTypes can be applied to a {@link Ext.form.field.Text Text Field} using the
 * `{@link Ext.form.field.Text#vtype vtype}` configuration:
 *
 *     Ext.create('Ext.form.field.Text', {
 *         fieldLabel: 'Email Address',
 *         name: 'email',
 *         vtype: 'email' // applies email validation rules to this field
 *     });
 *
 * To create custom VTypes:
 *
 *     // custom Vtype for vtype:'time'
 *     Ext.define('Override.form.field.VTypes', {
 *         override: 'Ext.form.field.VTypes',
 *
 *         // vtype validation function
 *         time: function(value) {
 *             return this.timeRe.test(value);
 *         },
 *         // RegExp for the value to be tested against within the validation function
 *         timeRe: /^([1-9]|1[0-9]):([0-5][0-9])(\s[a|p]m)$/i,
 *         // vtype Text property: The error text to display when the validation function
 *         // returns false
 *         timeText: 'Not a valid time.  Must be in the format "12:34 PM".',
 *         // vtype Mask property: The keystroke filter mask
 *         timeMask: /[\d\s:amp]/i
 *     });
 *
 * In the above example the `time` function is the validator that will run when field validation
 * occurs, `timeText` is the error message, and `timeMask` limits what characters can be typed
 * into the field. Note that the `Text` and `Mask` functions must begin with the same name
 * as the validator function.
 *
 * Using a custom validator is the same as using one of the build-in validators - just use the name
 * of the validator function as the `{@link Ext.form.field.Text#vtype vtype}` configuration on a
 * {@link Ext.form.field.Text Text Field}:
 *
 *     Ext.create('Ext.form.field.Text', {
 *         fieldLabel: 'Departure Time',
 *         name: 'departureTime',
 *         vtype: 'time' // applies custom time validation rules to this field
 *     });
 *
 * Another example of a custom validator:
 *
 *     // custom Vtype for vtype:'IPAddress'
 *     Ext.define('Override.form.field.VTypes', {
 *         override: 'Ext.form.field.VTypes',
 *
 *         IPAddress:  function(value) {
 *             return this.IPAddressRe.test(value);
 *         },
 *         IPAddressRe: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
 *         IPAddressText: 'Must be a numeric IP address',
 *         IPAddressMask: /[\d\.]/i
 *     });
 *
 * It's important to note that using {@link Ext#define Ext.define()} with the
 * {@link Ext.Class#override override} property means that the custom validator function as well as
 * `Text` and `Mask` fields are added as properties of the `Ext.form.field.VTypes` singleton.
 */
Ext.define('Ext.form.field.VTypes', function() {
    // closure these in so they are only created once.
    var alpha = /^[a-zA-Z_]+$/,
        alphanum = /^[a-zA-Z0-9_]+$/,
        // http://en.wikipedia.org/wiki/Email_address#Local_part
        // http://stackoverflow.com/a/2049510
        // http://isemail.info/
        // http://blog.stevenlevithan.com/archives/capturing-vs-non-capturing-groups
        //
        // 1. Can begin with a double-quote ONLY IF the local part also ends in a double-quote.
        // 2. Can NOT BEGIN with a period.
        // 3. Can NOT END with a period.
        // 4. Can not have MORE THAN ONE period in a row.
        //
        // Let's break this down:
        //
        // ^(")?
        // The local part may begin with double-quotes, but only if it also ends with it.
        // See the back-reference.  Capturing.
        //
        // (?:[^\."])
        // Here we've defined that the local part cannot begin with a period or a double-quote.
        // Non-capturing.
        //
        // (?:(?:[\.])?(?:[\w\-!#$%&'*+/=?^_`{|}~]))*
        // After the first character is matched, the regex ensures that there is not more than one
        // period in a row. Then, this nested grouping allows for zero or more of the accepted
        // characters. NOTE that this also ensures that any character not defined in the character
        // class is invalid as an ending character for the local part (such as the period).
        //
        // \1@
        // The local part of the address is a backreference to the first (and only) capturing group
        // that allows for a double-quote to wrap the local part of an email address.
        /* eslint-disable no-useless-escape */
        email = /^(")?(?:[^\."\s])(?:(?:[\.])?(?:[\w\-!#$%&'*+/=?^_`{|}~]))*\1@(\w[\-\w]*\.){1,5}([A-Za-z]){2,6}$/,
        url = /(((^https?)|(^ftp)):\/\/((([\-\w]+\.)+\w{2,3}(\/[%\-\w]+(\.\w{2,})?)*(([\w\-\.\?\\\/+@&#;`~=%!]*)(\.\w{2,})?)*)|(localhost|LOCALHOST))\/?)/i;
    /* eslint-enable no-useless-escape */
    // All these messages and functions are configurable
    return {
        singleton: true,
        alternateClassName: 'Ext.form.VTypes',
        /**
         * The function used to validate email addresses. Note that complete validation
         * per the email RFC specifications is very complex and beyond the scope of this class,
         * although this function can be overridden if a more comprehensive validation scheme
         * is desired. See the validation section of the [Wikipedia article on email addresses][1]
         * for additional information. This implementation is intended to validate the following
         * types of emails:
         *
         * - `barney@example.de`
         * - `barney.rubble@example.com`
         * - `barney-rubble@example.coop`
         * - `barney+rubble@example.com`
         * - `barney'rubble@example.com`
         * - `b.arne.y_r.ubbl.e@example.com`
         * - `barney4rubble@example.com`
         * - `barney4rubble!@example.com`
         * - `_barney+rubble@example.com`
         * - `"barney+rubble"@example.com`
         *
         * [1]: http://en.wikipedia.org/wiki/E-mail_address
         *
         * @param {String} value The email address
         * @return {Boolean} true if the RegExp test passed, and false if not.
         */
        email: function(value) {
            return email.test(value);
        },
        /**
         * @property {String} emailText
         * The error text to display when the email validation function returns false.
         * Defaults to: 'This field should be an e-mail address in the format "user@example.com"'
         * @locale
         */
        emailText: 'This field should be an e-mail address in the format "user@example.com"',
        /**
         * @property {RegExp} emailMask
         * The keystroke filter mask to be applied on email input. See the {@link #email} method
         * for information about more complex email validation. Defaults to: /[a-z0-9_\.\-@]/i
         */
        emailMask: /[\w.\-@'"!#$%&'*+/=?^_`{|}~]/i,
        /**
         * The function used to validate URLs
         * @param {String} value The URL
         * @return {Boolean} true if the RegExp test passed, and false if not.
         */
        url: function(value) {
            return url.test(value);
        },
        /**
         * @property {String} urlText
         * The error text to display when the url validation function returns false.
         * Defaults to: 'This field should be a URL in the format "http:/'+'/www.example.com"'
         * @locale
         */
        urlText: 'This field should be a URL in the format "http:/' + '/www.example.com"',
        /**
         * The function used to validate alpha values
         * @param {String} value The value
         * @return {Boolean} true if the RegExp test passed, and false if not.
         */
        alpha: function(value) {
            return alpha.test(value);
        },
        /**
         * @property {String} alphaText
         * The error text to display when the alpha validation function returns false.
         * Defaults to: 'This field should only contain letters and _'
         * @locale
         */
        alphaText: 'This field should only contain letters and _',
        /**
         * @property {RegExp} alphaMask
         * The keystroke filter mask to be applied on alpha input. Defaults to: /[a-z_]/i
         */
        alphaMask: /[a-z_]/i,
        /**
         * The function used to validate alphanumeric values
         * @param {String} value The value
         * @return {Boolean} true if the RegExp test passed, and false if not.
         */
        alphanum: function(value) {
            return alphanum.test(value);
        },
        /**
         * @property {String} alphanumText
         * The error text to display when the alphanumeric validation function returns false.
         * Defaults to: 'This field should only contain letters, numbers and _'
         * @locale
         */
        alphanumText: 'This field should only contain letters, numbers and _',
        /**
         * @property {RegExp} alphanumMask
         * The keystroke filter mask to be applied on alphanumeric input. Defaults to: /[a-z0-9_]/i
         */
        alphanumMask: /[a-z0-9_]/i
    };
});

/**
 * @class Ext.form.trigger.Trigger
 * Base class for {@link Ext.form.field.Text#cfg-triggers Text Field triggers}
 */
Ext.define('Ext.form.trigger.Trigger', {
    alias: 'trigger.trigger',
    requires: [
        'Ext.util.ClickRepeater'
    ],
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    factoryConfig: {
        defaultType: 'trigger'
    },
    /**
     * @cfg {Boolean} repeatClick
     * `true` to attach a {@link Ext.util.ClickRepeater click repeater} to the trigger
     */
    repeatClick: false,
    /**
     * @cfg cls
     * @inheritdoc Ext.panel.Header#cfg-iconCls
     */
    /**
     * @cfg {String} extraCls
     * An additional CSS class (or classes) to be added to the trigger's element. Can
     * be a single class name (e.g. 'foo') or a space-separated list of class names
     * (e.g. 'foo bar').
     */
    /**
     * @cfg {Function/String} [handler=undefined]
     * Function to run when trigger is clicked or tapped.
     * @controllable
     */
    /**
     * @cfg {Boolean} hidden
     * `true` to initially render the trigger hidden.
     */
    hidden: false,
    /**
     * @cfg {Boolean} [hideOnReadOnly=true]
     * Set 'false' to prevent trigger from being hidden even though the related field is
     * set {@link Ext.form.field.Text#readOnly readOnly}
     */
    hideOnReadOnly: undefined,
    /**
     * @cfg {Object} scope
     * Execution context for the {@link #handler} function.
     */
    /**
     * @cfg {String} tooltip
     * The triggers tooltip text. This text is available when using `Ext.QuickTips`.
     * @since 6.2.0
     */
    tooltip: null,
    /**
     * @cfg {Number} weight
     * An optional weighting to change the ordering of the items. The default weight is
     * `0`.  Triggers are sorted by weight in ascending order before being rendered.  
     * The value may be a negative value in order to position custom triggers ahead of 
     * default triggers like that of ComboBox.
     */
    weight: 0,
    /**
     * @cfg {Number} width The trigger's width, in pixels. Typically this is not needed
     * as the trigger width is normally determined by the style sheet, (see
     * {@link Ext.form.field.Text#$form-trigger-width extjs-text-field} or
     * {@link Ext.form.field.Text#$extjs-text-field-ui}).
     */
    /**
     * @cfg {Boolean} preventMouseDown
     * @private
     * If true, preventDefault() will be called on the mousedown event.  This prevents
     * a click on the trigger from blurring the field, which is desirable in most cases.
     * File field sets this to false, because preventing the default behavior of touchstart
     * prevents the browser's file dialog from opening.
     */
    preventMouseDown: true,
    /**
     * @cfg {Boolean} [focusOnMouseDown=false] If `true`, the field will be focused upon
     * mousedown on the trigger. This should be used only for main Picker field triggers
     * that expand and collapse the picker; additional triggers should not focus the field.
     * @private
     */
    focusOnMousedown: false,
    /**
     * @property {String}
     * @private
     * The base CSS class that is always added to the trigger element.
     */
    baseCls: Ext.baseCSSPrefix + 'form-trigger',
    /**
     * @property {String}
     * @private
     * The CSS class that is added to the trigger element when the field is focused.
     */
    focusCls: Ext.baseCSSPrefix + 'form-trigger-focus',
    /**
     * @property {String}
     * @private
     * The CSS class that is added to the trigger element when it is hovered.
     */
    overCls: Ext.baseCSSPrefix + 'form-trigger-over',
    /**
     * @property {String}
     * @private
     * The CSS class that is added to the trigger element it is clicked.
     */
    clickCls: Ext.baseCSSPrefix + 'form-trigger-click',
    /**
     * @private
     */
    validIdRe: Ext.validIdRe,
    /**
     * @property {Ext.Template/String/Array} bodyTpl
     * @protected
     * An optional template for rendering child elements inside the trigger element.
     * Useful for creating more complex triggers such as {@link Ext.form.trigger.Spinner}.
     */
    /* eslint-disable indent */
    renderTpl: [
        '<div id="{triggerId}" class="{baseCls} {baseCls}-{ui} {cls} {cls}-{ui} {extraCls} ',
        '{childElCls}"<tpl if="triggerStyle"> style="{triggerStyle}"</tpl>',
        '<tpl if="ariaRole"> role="{ariaRole}"<tpl else> role="presentation"</tpl>',
        '>',
        '{[values.$trigger.renderBody(values)]}',
        '</div>'
    ],
    /* eslint-enable indent */
    constructor: function(config) {
        var me = this,
            cls;
        Ext.apply(me, config);
        // extra over/click/focus cls for compat with 4.x TriggerField
        if (me.compat4Mode) {
            cls = me.cls;
            me.focusCls = [
                me.focusCls,
                cls + '-focus'
            ];
            me.overCls = [
                me.overCls,
                cls + '-over'
            ];
            me.clickCls = [
                me.clickCls,
                cls + '-click'
            ];
        }
        if (!me.validIdRe.test(me.id)) {
            Ext.raise('Invalid trigger "id": "' + me.id + '"');
        }
    },
    /**
     * @protected
     * Called when this trigger's field is rendered
     */
    afterFieldRender: function() {
        var me = this,
            tip = me.tooltip;
        me.initEvents();
        if (tip) {
            me.tooltip = null;
            me.setTooltip(tip);
        }
    },
    destroy: function() {
        var me = this;
        me.clickRepeater = me.el = Ext.destroy(me.clickRepeater, me.el);
        me.callParent();
    },
    /**
     * @method
     * Allows addition of data to the render data object for the {@link #bodyTpl}.
     * @protected
     * @return {Object}
     */
    getBodyRenderData: Ext.emptyFn,
    /**
     * Get the element for this trigger.
     * @return {Ext.dom.Element} The element for this trigger, `null` if not rendered.
     */
    getEl: function() {
        return this.el || null;
    },
    /**
     * Returns the element that should receive the "state" classes - {@link #focusCls},
     * {@link #overCls}, and {@link #clickCls}.
     * @protected
     */
    getStateEl: function() {
        return this.el;
    },
    /**
     * Hides the trigger
     */
    hide: function() {
        var me = this,
            el = me.el;
        me.hidden = true;
        if (el) {
            el.hide();
        }
    },
    initEvents: function() {
        var me = this,
            isFieldEnabled = me.isFieldEnabled,
            stateEl = me.getStateEl(),
            el = me.el;
        stateEl.addClsOnOver(me.overCls, isFieldEnabled, me);
        stateEl.addClsOnClick(me.clickCls, isFieldEnabled, me);
        if (me.repeatClick) {
            me.clickRepeater = new Ext.util.ClickRepeater(el, {
                preventDefault: true,
                handler: me.onClick,
                listeners: {
                    mousedown: me.onClickRepeaterMouseDown,
                    mouseup: me.onClickRepeaterMouseUp,
                    scope: me
                },
                scope: me
            });
        } else {
            me.field.mon(el, {
                click: me.onClick,
                mousedown: me.onMouseDown,
                scope: me
            });
        }
    },
    /**
     * @private
     */
    isFieldEnabled: function() {
        return !this.field.disabled;
    },
    /**
     * Returns `true` if this Trigger is visible.
     * @return {Boolean} `true` if this trigger is visible, `false` otherwise.
     *
     * @since 5.0.0
     */
    isVisible: function() {
        var me = this,
            field = me.field,
            hidden = false;
        if (me.hidden || !field || !me.rendered || me.destroyed) {
            hidden = true;
        }
        return !hidden;
    },
    /**
     * @protected
     * Handles a click on the trigger's element
     */
    onClick: function() {
        var me = this,
            args = arguments,
            e = me.clickRepeater ? args[1] : args[0],
            handler = me.handler,
            field = me.field;
        if (handler && !field.readOnly && me.isFieldEnabled()) {
            Ext.callback(me.handler, me.scope, [
                field,
                me,
                e
            ], 0, field);
        }
    },
    // "this" refers to our owning input field.
    resolveListenerScope: function(scope) {
        return this.field.resolveSatelliteListenerScope(this, scope);
    },
    onMouseDown: function(e) {
        // If it was a genuine mousedown or pointerdown, NOT a touch, then focus the input field.
        // Usually, the field will be focused, but the mousedown on the trigger
        // might be the user's first contact with the field.
        // It's definitely NOT the user's first contact with our field if the field
        // has the focus.
        // It is also possible that there are multiple triggers on the field, and only one
        // of them causes picker expand/collapse. When picker is about to be collapsed
        // we need to focus the input; otherwise if the picker was focused the focus will go
        // to the document body which is not what we want. However if the mousedown was on
        // a trigger that does not cause collapse we should NOT focus the field.
        if (e.pointerType !== 'touch' && (!this.field.containsFocus || this.focusOnMousedown)) {
            this.field.focus();
        }
        if (this.preventMouseDown) {
            // Stop the mousedown from blurring our field
            e.preventDefault();
        }
    },
    onClickRepeaterMouseDown: function(clickRepeater, e) {
        // If it was a genuine mousedown, NOT a touch, then focus the input field.
        // Usually, the field will be focused, but the mousedown on the trigger
        // might be the user's first contact with the field.
        if (!e.parentEvent || e.parentEvent.type === 'mousedown') {
            this.field.inputEl.focus();
        }
        // Stop the mousedown from blurring our field
        e.preventDefault();
    },
    onClickRepeaterMouseUp: function(clickRepeater, e) {
        var me = this,
            field = me.field;
        Ext.callback(me.endHandler, me.scope, [
            field,
            me,
            e
        ], 0, field);
    },
    /**
     * @protected
     * Called when this trigger's field is blurred
     */
    onFieldBlur: function() {
        this.getStateEl().removeCls(this.focusCls);
    },
    /**
     * @protected
     * Called when this trigger's field is focused
     */
    onFieldFocus: function() {
        this.getStateEl().addCls(this.focusCls);
    },
    /**
     * @protected
     * Called when this trigger's field is rendered
     */
    onFieldRender: function() {
        var me = this,
            /**
             * @property {Ext.dom.Element} el
             * @private
             * The trigger's main element
             */
            el = me.el = me.field.triggerWrap.selectNode('#' + me.domId, false);
        // ensure that the trigger does not consume space when hidden
        el.setVisibilityMode(Ext.Element.DISPLAY);
        me.rendered = true;
    },
    /**
     * Renders the bodyTpl
     * @param renderData
     * @private
     * @return {String}
     */
    renderBody: function(renderData) {
        var me = this,
            bodyTpl = me.bodyTpl;
        Ext.apply(renderData, me.getBodyRenderData());
        return bodyTpl ? Ext.XTemplate.getTpl(me, 'bodyTpl').apply(renderData) : '';
    },
    /**
     * Generates the trigger markup. Called during rendering of the field the trigger
     * belongs to.
     * @param {Object} fieldData The render data object of the parent field.
     * @private
     * @return {String}
     */
    renderTrigger: function(fieldData) {
        var me = this,
            width = me.width,
            triggerStyle = me.hidden ? 'display:none;' : '';
        if (width) {
            triggerStyle += 'width:' + width;
        }
        return Ext.XTemplate.getTpl(me, 'renderTpl').apply({
            $trigger: me,
            fieldData: fieldData,
            ui: fieldData.ui,
            childElCls: fieldData.childElCls,
            triggerId: me.domId = me.field.id + '-trigger-' + me.id,
            cls: me.cls,
            triggerStyle: triggerStyle,
            extraCls: me.extraCls,
            baseCls: me.baseCls,
            ariaRole: me.ariaRole
        });
    },
    setHidden: function(hidden) {
        if (hidden !== this.hidden) {
            this[hidden ? 'hide' : 'show']();
        }
    },
    setTooltip: function(tip) {
        var me = this,
            el = me.el,
            was = me.tooltip;
        if (tip !== was) {
            me.tooltip = tip;
            if (el) {
                el.dom.setAttribute('data-qtip', Ext.htmlEncode(tip));
            }
        }
    },
    setVisible: function(visible) {
        this.setHidden(!visible);
    },
    /**
     * Shows the trigger
     */
    show: function() {
        var me = this,
            el = me.el;
        me.hidden = false;
        if (el) {
            el.show();
        }
    }
});

/**
 * A basic text field.  Can be used as a direct replacement for traditional text inputs,
 * or as the base class for more sophisticated input controls (like {@link Ext.form.field.TextArea}
 * and {@link Ext.form.field.ComboBox}). Has support for empty-field placeholder values
 * (see {@link #emptyText}).
 *
 * # Validation
 *
 * The Text field has a useful set of validations built in:
 *
 * - {@link #allowBlank} for making the field required
 * - {@link #minLength} for requiring a minimum value length
 * - {@link #maxLength} for setting a maximum value length (with {@link #enforceMaxLength} to add it
 *   as the `maxlength` attribute on the input element)
 * - {@link #regex} to specify a custom regular expression for validation
 *
 * In addition, custom validations may be added:
 *
 * - {@link #vtype} specifies a virtual type implementation from {@link Ext.form.field.VTypes}
 *   which can contain custom validation logic
 * - {@link #validator} allows a custom arbitrary function to be called during validation
 *
 * The details around how and when each of these validation options get used are described in the
 * documentation for {@link #getErrors}.
 *
 * By default, the field value is checked for validity immediately while the user is typing in the
 * field. This can be controlled with the {@link #validateOnChange}, {@link #checkChangeEvents}, and
 * {@link #checkChangeBuffer} configurations. Also see the details on Form Validation in the
 * {@link Ext.form.Panel} class documentation.
 *
 * # Masking and Character Stripping
 *
 * Text fields can be configured with custom regular expressions to be applied to entered values
 * before validation: see {@link #maskRe} and {@link #stripCharsRe} for details.
 *
 * # Example usage
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Contact Info',
 *         width: 300,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items: [{
 *             xtype: 'textfield',
 *             name: 'name',
 *             fieldLabel: 'Name',
 *             allowBlank: false  // requires a non-empty value
 *         }, {
 *             xtype: 'textfield',
 *             name: 'email',
 *             fieldLabel: 'Email Address',
 *             vtype: 'email'  // requires value to be a valid email address format
 *         }]
 *     });
 *
 * # Custom Subclasses
 *
 * This class can be extended to provide additional functionality. The example below demonstrates
 * creating a custom search field that uses the HTML5 search input type.
 *
 *     @example
 *     // A simple subclass of Base that creates a HTML5 search field. Redirects to the
 *     // searchUrl when the Enter key is pressed.
 *     Ext.define('Ext.form.SearchField', {
 *         extend: 'Ext.form.field.Text',
 *         alias: 'widget.searchfield',
 *     
 *         inputType: 'search',
 *     
 *         // Config defining the search URL
 *         searchUrl: 'http://www.google.com/search?q={0}',
 *     
 *         // Add specialkey listener
 *         initComponent: function() {
 *             this.callParent();
 *             this.on('specialkey', this.checkEnterKey, this);
 *         },
 *     
 *         // Handle enter key presses, execute the search if the field has a value
 *         checkEnterKey: function(field, e) {
 *             var value = this.getValue();
 *             if (e.getKey() === e.ENTER && !Ext.isEmpty(value)) {
 *                 location.href = Ext.String.format(this.searchUrl, value);
 *             }
 *         }
 *     });
 *     
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Base Example',
 *         bodyPadding: 5,
 *         width: 250,
 *     
 *         // Fields will be arranged vertically, stretched to full width
 *         layout: 'anchor',
 *         defaults: {
 *             anchor: '100%'
 *         },
 *         items: [{
 *             xtype: 'searchfield',
 *             fieldLabel: 'Search',
 *             name: 'query'
 *         }],
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.form.field.Text', {
    extend: 'Ext.form.field.Base',
    alias: 'widget.textfield',
    alternateClassName: [
        'Ext.form.TextField',
        'Ext.form.Text'
    ],
    requires: [
        'Ext.layout.component.field.Text',
        'Ext.form.field.VTypes',
        'Ext.form.trigger.Trigger',
        'Ext.util.TextMetrics'
    ],
    /**
     * @cfg componentLayout
     * @inheritdoc
     */
    componentLayout: 'textfield',
    config: {
        /**
         * @cfg {Boolean} hideTrigger
         * `true` to hide all triggers
         */
        hideTrigger: false,
        /**
         * @cfg {Boolean} [autoHideInputMask=true]
         * Specify as `false` to always show the `inputMask`.
         * @since 6.5.0
         */
        autoHideInputMask: null,
        /**
         * @cfg {String/Ext.field.InputMask} inputMask
         *
         * **Important:** To use this config you must require `Ext.field.InputMask` or
         * use a complete framework build. The logic to implement an `inputMask` is not
         * automatically included in a build.
         * @since 6.5.0
         */
        inputMask: null,
        // @cmd-auto-dependency {aliasPrefix: "trigger.", isKeyedObject: true}
        /**
         * @cfg {Object} triggers
         * {@link Ext.form.trigger.Trigger Triggers} to use in this field.  The keys in
         * this object are unique identifiers for the triggers. The values in this object
         * are {@link Ext.form.trigger.Trigger Trigger} configuration objects.
         *
         *     Ext.create('Ext.form.field.Text', {
         *         renderTo: document.body,
         *         fieldLabel: 'My Custom Field',
         *         triggers: {
         *             foo: {
         *                 cls: 'my-foo-trigger',
         *                 handler: function() {
         *                     console.log('foo trigger clicked');
         *                 }
         *             },
         *             bar: {
         *                 cls: 'my-bar-trigger',
         *                 handler: function() {
         *                     console.log('bar trigger clicked');
         *                 }
         *             }
         *         }
         *     });
         * 
         * The weight value may be a negative value in order to position custom triggers 
         * ahead of default triggers like that of ComboBox.
         * 
         *     Ext.create('Ext.form.field.ComboBox', {
         *         renderTo: Ext.getBody(),
         *         fieldLabel: 'My Custom Field',
         *         triggers: {
         *             foo: {
         *                 cls: 'my-foo-trigger',
         *                 weight: -2, // negative to place before default triggers
         *                 handler: function() {
         *                     console.log('foo trigger clicked');
         *                 }
         *             },
         *             bar: {
         *                 cls: 'my-bar-trigger',
         *                 weight: -1, 
         *                 handler: function() {
         *                     console.log('bar trigger clicked');
         *                 }
         *             }
         *         }
         *     });
         */
        triggers: undefined
    },
    renderConfig: {
        /**
         * @cfg {Boolean} editable
         * false to prevent the user from typing text directly into the field; the field can
         * only have its value set programmatically or via an action invoked by a trigger.
         */
        editable: true
    },
    /**
     * @cfg {String} vtypeText
     * A custom error message to display in place of the default message provided for the
     * **`{@link #vtype}`** currently set for this field.
     * **Note**: only applies if **`{@link #vtype}`** is set, else ignored.
     */
    /**
     * @cfg {RegExp} stripCharsRe
     * A JavaScript RegExp object used to strip unwanted content from the value
     * during input. If `stripCharsRe` is specified,
     * every *character sequence* matching `stripCharsRe` will be removed.
     */
    /**
     * @cfg {Number} size
     * An initial value for the 'size' attribute on the text input element. This is only
     * used if the field has no configured {@link #width} and is not given a width by its
     * container's layout. Defaults to 20.
     * @deprecated 6.5.0 Please use {@link #width} instead.
     */
    /**
     * @cfg {Boolean} [grow=false]
     * true if this field should automatically grow and shrink to its content
     */
    /**
     * @cfg {Number} growMin
     * The minimum width to allow when `{@link #grow} = true`
     */
    growMin: 30,
    /**
     * @cfg {Number} growMax
     * The maximum width to allow when `{@link #grow} = true`
     */
    growMax: 800,
    /**
     * @cfg {String} vtype
     * A validation type name as defined in {@link Ext.form.field.VTypes}
     */
    /**
     * @cfg {RegExp} maskRe An input mask regular expression that will be used to filter keystrokes
     * (character being typed) that do not match.
     * Note: It does not filter characters already in the input.
     */
    /**
     * @cfg {Boolean} [disableKeyFilter=false]
     * Specify true to disable input keystroke filtering. This will ignore the
     * maskRe field.
     */
    /**
     * @cfg {Boolean} allowBlank
     * Specify false to validate that the value's length must be > 0. If `true`, then a blank value
     * is **always** taken to be valid regardless of any {@link #vtype} validation that may be
     * applied.
     *
     * If {@link #vtype} validation must still be applied to blank values, configure
     * {@link #validateBlank} as `true`;
     */
    allowBlank: true,
    /**
     * @cfg {Boolean} validateBlank
     * Specify as `true` to modify the behaviour of {@link #allowBlank} so that blank values
     * are not passed as valid, but are subject to any configure {@link #vtype} validation.
     */
    validateBlank: false,
    /**
     * @cfg {Boolean} allowOnlyWhitespace
     * Specify false to automatically trim the value before validating
     * the whether the value is blank. Setting this to false automatically
     * sets {@link #allowBlank} to false.
     */
    allowOnlyWhitespace: true,
    /**
     * @cfg {Number} minLength
     * Minimum input field length required
     */
    minLength: 0,
    /**
     * @cfg {Number} maxLength
     * Maximum input field length allowed by validation. This behavior is intended to
     * provide instant feedback to the user by improving usability to allow pasting and editing
     * or overtyping and back tracking. To restrict the maximum number of characters that can be
     * entered into the field use the
     * **{@link Ext.form.field.Text#enforceMaxLength enforceMaxLength}** option.
     *
     * Defaults to Number.MAX_VALUE.
     */
    maxLength: Number.MAX_VALUE,
    /**
     * @cfg {Boolean} enforceMaxLength
     * True to set the maxLength property on the underlying input field. Defaults to false
     */
    /**
     * @cfg {String} minLengthText
     * Error text to display if the **{@link #minLength minimum length}** validation fails.
     * @locale
     */
    minLengthText: 'The minimum length for this field is {0}',
    /**
     * @cfg {String} maxLengthText
     * Error text to display if the **{@link #maxLength maximum length}** validation fails.
     * @locale
     */
    maxLengthText: 'The maximum length for this field is {0}',
    /**
     * @cfg {Boolean} [selectOnFocus=false]
     * `true` to automatically select any existing field text when the field receives input
     * focus. Only applies when {@link #editable editable} = true
     */
    /**
     * @cfg {String} blankText
     * The error text to display if the **{@link #allowBlank}** validation fails.
     * @locale
     */
    blankText: 'This field is required',
    /**
     * @cfg {Function} validator
     * A custom validation function to be called during field validation ({@link #getErrors}).
     * If specified, this function will be called first, allowing the developer to override
     * the default validation process.
     * 
     *     Ext.create('Ext.form.field.Text', {
     *         renderTo: document.body,
     *         name: 'phone',
     *         fieldLabel: 'Phone Number',
     *         validator: function(val) {
     *             // remove non-numeric characters
     *             var tn = val.replace(/[^0-9]/g,''),
     *                 errMsg = "Must be a 10 digit telephone number";
     *             // if the numeric value is not 10 digits return an error message
     *             return (tn.length === 10) ? true : errMsg;
     *         }
     *     });
     *
     * @param {Object} value The current field value
     * @return {Boolean/String} response
     *
     *  - True if the value is valid
     *  - An error message if the value is invalid
     */
    /**
     * @cfg {RegExp} regex
     * A JavaScript RegExp object to be tested against the field value during validation.
     * If the test fails, the field will be marked invalid using
     * either **{@link #regexText}** or **{@link #invalidText}**.
     */
    /**
     * @cfg {String} regexText
     * The error text to display if **{@link #regex}** is used and the test fails during validation
     */
    regexText: '',
    /**
     * @cfg {String} emptyText
     * The default text to place into an empty field.
     *
     * Note that normally this value will be submitted to the server if this field is enabled;
     * to prevent this you can set the
     * {@link Ext.form.action.Action#submitEmptyText submitEmptyText} option of
     * {@link Ext.form.Basic#submit} to false.
     *
     * Also note that if you use {@link #inputType inputType}:'file', {@link #emptyText}
     * is not supported and should be avoided.
     *
     * Note that for browsers that support it, setting this property will use the HTML 5 placeholder
     * attribute, and for older browsers that don't support the HTML 5 placeholder attribute
     * the value will be placed directly into the input element itself as the raw value.
     * This means that older browsers will obfuscate the {@link #emptyText} value for
     * password input fields.
     */
    emptyText: '',
    /**
     * @cfg {String} emptyCls
     * The CSS class to apply to an empty field to style the **{@link #emptyText}**.
     * This class is automatically added and removed as needed depending on the current field value.
     */
    emptyCls: Ext.baseCSSPrefix + 'form-empty-field',
    /**
     * @private
     * The default CSS class for the placeholder label cover need when the browser
     * does not support a Placeholder.
     */
    placeholderCoverCls: Ext.baseCSSPrefix + 'placeholder-label',
    /**
     * @cfg {String} requiredCls
     * The CSS class to apply to a required field, i.e. a field where **{@link #allowBlank}**
     * is false.
     */
    requiredCls: Ext.baseCSSPrefix + 'form-required-field',
    /**
     * @cfg {Boolean} [enableKeyEvents=false]
     * true to enable the proxying of key events for the HTML input field
     */
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'textbox',
    /**
     * @cfg {Boolean} repeatTriggerClick
     * `true` to attach a {@link Ext.util.ClickRepeater click repeater} to the trigger(s).
     * Click repeating behavior can also be configured on the individual {@link #triggers
     * trigger instances} using the trigger's {@link Ext.form.trigger.Trigger#repeatClick
     * repeatClick} config.
     */
    repeatTriggerClick: false,
    /**
     * @cfg {Boolean} readOnly
     * `true` to prevent the user from changing the field, and hide all triggers.
     */
    /**
     * @cfg stateEvents
     * @inheritdoc Ext.state.Stateful#cfg-stateEvents
     * @localdoc By default the following stateEvents are added:
     * 
     *  - {@link #event-resize} - _(added by Ext.Component)_
     *  - {@link #event-change}
     */
    /**
     * @cfg {String}
     * The CSS class that is added to the div wrapping the input element and trigger button(s).
     */
    triggerWrapCls: Ext.baseCSSPrefix + 'form-trigger-wrap',
    triggerWrapFocusCls: Ext.baseCSSPrefix + 'form-trigger-wrap-focus',
    triggerWrapInvalidCls: Ext.baseCSSPrefix + 'form-trigger-wrap-invalid',
    /**
     * @cfg fieldBodyCls
     * @inheritdoc
     */
    fieldBodyCls: Ext.baseCSSPrefix + 'form-text-field-body',
    /**
     * @cfg {String}
     * The CSS class that is added to the element wrapping the input element
     */
    inputWrapCls: Ext.baseCSSPrefix + 'form-text-wrap',
    inputWrapFocusCls: Ext.baseCSSPrefix + 'form-text-wrap-focus',
    inputWrapInvalidCls: Ext.baseCSSPrefix + 'form-text-wrap-invalid',
    growCls: Ext.baseCSSPrefix + 'form-text-grow',
    heightedCls: Ext.baseCSSPrefix + 'form-text-heighted',
    /* 
     * @private
     * This property will hold all elements that require emtpyCls to be applied to them
     */
    emptyClsElements: null,
    needArrowKeys: true,
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        /**
         * @property {Ext.dom.Element} triggerWrap
         * A reference to the element which encapsulates the input field and all
         * trigger button(s). Only set after the field has been rendered.
         */
        'triggerWrap',
        /**
         * @property {Ext.dom.Element} inputWrap
         * A reference to the element that wraps the input element. Only set after the
         * field has been rendered.
         */
        'inputWrap',
        'placeholderLabel'
    ],
    /* eslint-disable indent, max-len */
    preSubTpl: [
        '<div id="{cmpId}-triggerWrap" data-ref="triggerWrap"',
        '<tpl if="ariaEl == \'triggerWrap\'">',
        '<tpl foreach="ariaElAttributes"> {$}="{.}"</tpl>',
        '<tpl else>',
        ' role="presentation"',
        '</tpl>',
        ' class="{triggerWrapCls} {triggerWrapCls}-{ui}">',
        '<div id={cmpId}-inputWrap data-ref="inputWrap"',
        ' role="presentation" class="{inputWrapCls} {inputWrapCls}-{ui}">'
    ],
    postSubTpl: [
        '<tpl if="!Ext.supports.Placeholder">',
        '<label id="{cmpId}-placeholderLabel" data-ref="placeholderLabel" for="{id}" class="{placeholderCoverCls} {placeholderCoverCls}-{ui}">{placeholder}</label>',
        '</tpl>',
        '</div>',
        // end inputWrap
        '<tpl for="triggers">{[values.renderTrigger(parent)]}</tpl>',
        '</div>'
    ],
    // end triggerWrap
    /* eslint-enable indent, max-len */
    /**
     * @event autosize
     * Fires when the **{@link #autoSize}** function is triggered and the field is resized
     * according to the {@link #grow}/{@link #growMin}/{@link #growMax} configs as a result.
     * This event provides a hook for the developer to apply additional logic at runtime
     * to resize the field if needed.
     * @param {Ext.form.field.Text} this This text field
     * @param {Number} width The new field width
     */
    /**
     * @event keydown
     * Keydown input field event. This event only fires if **{@link #enableKeyEvents}**
     * is set to true.
     * @param {Ext.form.field.Text} this This text field
     * @param {Ext.event.Event} e
     */
    /**
     * @event keyup
     * Keyup input field event. This event only fires if **{@link #enableKeyEvents}**
     * is set to true.
     * @param {Ext.form.field.Text} this This text field
     * @param {Ext.event.Event} e
     */
    /**
     * @event keypress
     * Keypress input field event. This event only fires if **{@link #enableKeyEvents}**
     * is set to true.
     * @param {Ext.form.field.Text} this This text field
     * @param {Ext.event.Event} e
     */
    /**
     * @event paste
     * Fires when this field is pasted. This event only fires if **{@link #enableKeyEvents}**
     * is set to true.
     * @param {Ext.form.field.Text} this This text field
     * @param {Ext.event.Event} e
     */
    initComponent: function() {
        var me = this,
            emptyCls = me.emptyCls;
        if (me.allowOnlyWhitespace === false) {
            me.allowBlank = false;
        }
        if (me.grow) {
            me.liquidLayout = false;
        }
        if (me.size) {
            Ext.log.warn('Ext.form.field.Text "size" config was deprecated in Ext 5.0. ' + 'Please specify a "width" or use a layout instead.');
        }
        // In Ext JS 4.x the layout system used the following magic formula for converting
        // the "size" config into a pixel value.
        if (me.size) {
            me.defaultBodyWidth = me.size * 6.5 + 20;
        }
        if (!me.onTrigger1Click) {
            // for compat with 4.x TriggerField
            me.onTrigger1Click = me.onTriggerClick;
        }
        me.callParent();
        if (me.readOnly) {
            me.setReadOnly(me.readOnly);
        }
        me.fieldFocusCls = me.baseCls + '-focus';
        me.emptyUICls = emptyCls + ' ' + emptyCls + '-' + me.ui;
        me.addStateEvents('change');
    },
    initEvents: function() {
        var me = this,
            el = me.inputEl;
        me.callParent();
        if (me.maskRe || (me.vtype && me.disableKeyFilter !== true && (me.maskRe = Ext.form.field.VTypes[me.vtype + 'Mask']))) {
            me.mon(el, 'keypress', me.filterKeys, me);
        }
        if (me.enableKeyEvents) {
            me.mon(el, {
                scope: me,
                keyup: me.onKeyUp,
                keydown: me.onKeyDown,
                keypress: me.onKeyPress,
                paste: me.onPaste
            });
        }
    },
    /**
     * @private
     * Treat undefined and null values as equal to an empty string value.
     */
    isEqual: function(value1, value2) {
        return this.isEqualAsString(value1, value2);
    },
    /**
     * @private
     * If grow=true, invoke the autoSize method when the field's value is changed.
     */
    onChange: function(newVal, oldVal) {
        var me = this,
            inputMask = me.getInputMask();
        me.callParent([
            newVal,
            oldVal
        ]);
        me.autoSize();
        if (inputMask) {
            inputMask.onChange(me, newVal, oldVal);
        }
    },
    getSubTplData: function(fieldData) {
        var me = this,
            value = me.getRawValue(),
            isEmpty = me.emptyText && value.length < 1,
            maxLength = me.maxLength,
            placeholder, data, inputElAttr;
        // We can't just dump the value here, since MAX_VALUE ends up
        // being something like 1.xxxxe+300, which gets interpreted as 1
        // in the markup
        if (me.enforceMaxLength) {
            if (maxLength === Number.MAX_VALUE) {
                maxLength = undefined;
            }
        } else {
            maxLength = undefined;
        }
        if (me.emptyText) {
            placeholder = Ext.String.htmlEncode(me.emptyText);
        }
        data = Ext.apply(me.callParent([
            fieldData
        ]), {
            triggerWrapCls: me.triggerWrapCls,
            inputWrapCls: me.inputWrapCls,
            placeholderCoverCls: me.placeholderCoverCls,
            triggers: me.orderedTriggers,
            maxLength: maxLength,
            readOnly: !me.editable || me.readOnly,
            placeholder: placeholder,
            value: value,
            fieldCls: me.fieldCls + (me.allowBlank ? '' : ' ' + me.requiredCls) + (isEmpty ? ' ' + me.emptyUICls : '')
        });
        inputElAttr = data.inputElAriaAttributes;
        if (inputElAttr) {
            inputElAttr['aria-required'] = !me.allowBlank;
        }
        return data;
    },
    beforeRender: function() {
        var me = this,
            heighted;
        heighted = me.height != null || me.minHeight != null || !!(me.ownerLayout && me.ownerLayout.getItemSizePolicy(me, me.fakeSizeModel).setsHeight);
        if (heighted) {
            me.protoEl.addCls(me.heightedCls);
        }
        me.callParent();
    },
    onRender: function() {
        var me = this,
            triggers = me.getTriggers(),
            elements = [],
            id;
        if (Ext.supports.FixedTableWidthBug) {
            // Workaround for https://bugs.webkit.org/show_bug.cgi?id=130239 and
            // https://code.google.com/p/chromium/issues/detail?id=377190
            // See styleHooks for more details
            me.el._needsTableWidthFix = true;
        }
        me.callParent();
        me.emptyClsElements = [
            me.inputEl
        ];
        if (triggers) {
            me.invokeTriggers('onFieldRender');
            /**
             * @property {Ext.CompositeElement} triggerEl
             * @deprecated 5.0
             * A composite of all the trigger button elements. Only set after the field has
             * been rendered.
             */
            for (id in triggers) {
                elements.push(triggers[id].el);
            }
            // for 4.x compat, also set triggerCell
            me.triggerEl = me.triggerCell = new Ext.CompositeElement(elements, true);
        }
        /**
         * @property {Ext.dom.Element} inputCell
         * A reference to the element that wraps the input element. Only set after the
         * field has been rendered.
         * @deprecated 5.0 use {@link #inputWrap} instead
         */
        me.inputCell = me.inputWrap;
        me.refreshEmptyText();
    },
    onResize: function(width, height, oldWidth, oldHeight) {
        var me = this;
        if (me.rendered && me.grow) {
            me.autoSize();
        }
        me.callParent([
            width,
            height,
            oldWidth,
            oldHeight
        ]);
    },
    afterRender: function() {
        this.callParent();
        this.invokeTriggers('afterFieldRender');
    },
    onBoxReady: function(width, height) {
        var me = this;
        me.callParent([
            width,
            height
        ]);
        if (!me.liquidLayout) {
            this.autoSize();
        }
    },
    applyInputMask: function(value, instance) {
        var field = Ext.field,
            // eslint-disable-next-line dot-notation
            InputMask = field && field['InputMask'];
        // prevent Cmd detection
        if (value) {
            if (!InputMask) {
                Ext.raise('Missing Ext.field.InputMask (required to use inputMask)');
            }
        }
        // if (this.getAutoComplete()) {
        //     Ext.log.warn('Combining inputMask and autoComplete is not supported');
        // }
        return value ? InputMask.from(value, instance) : null;
    },
    applyTriggers: function(triggers) {
        var me = this,
            hideAllTriggers = me.getHideTrigger(),
            readOnly = me.readOnly,
            orderedTriggers = me.orderedTriggers = [],
            repeatTriggerClick = me.repeatTriggerClick,
            id, triggerCfg, trigger, triggerCls, i;
        if (me.rendered) {
            Ext.raise("Cannot set triggers after field has already been rendered.");
        }
        // don't warn if we have both triggerCls and triggers, because picker field
        // uses triggerCls to style the "picker" trigger.
        if ((me.triggerCls && !triggers) || me.trigger1Cls) {
            Ext.log.warn("Ext.form.field.Text: 'triggerCls' and 'trigger<n>Cls'" + " are deprecated.  Use 'triggers' instead.");
        }
        if (!triggers) {
            // For compatibility with 4.x, transform the trigger<n>Cls configs into the
            // new "triggers" config.
            triggers = {};
            if (me.triggerCls && !me.trigger1Cls) {
                me.trigger1Cls = me.triggerCls;
            }
            // Assignment in conditional test is deliberate here
            for (i = 1; (triggerCls = me['trigger' + i + 'Cls']); i++) {
                triggers['trigger' + i] = {
                    cls: triggerCls,
                    extraCls: Ext.baseCSSPrefix + 'trigger-index-' + i,
                    handler: 'onTrigger' + i + 'Click',
                    compat4Mode: true,
                    scope: me
                };
            }
        }
        for (id in triggers) {
            if (triggers.hasOwnProperty(id)) {
                triggerCfg = triggers[id];
                triggerCfg.field = me;
                triggerCfg.id = id;
                /*
                 * An explicitly-configured 'triggerConfig.hideOnReadOnly : false' allows
                 * {@link #hideTrigger} analysis
                 */
                if ((readOnly && triggerCfg.hideOnReadOnly !== false) || (hideAllTriggers && triggerCfg.hidden !== false)) {
                    triggerCfg.hidden = true;
                }
                if (repeatTriggerClick && (triggerCfg.repeatClick !== false)) {
                    triggerCfg.repeatClick = true;
                }
                trigger = triggers[id] = Ext.form.trigger.Trigger.create(triggerCfg);
                orderedTriggers.push(trigger);
            }
        }
        Ext.sortByWeight(orderedTriggers);
        return triggers;
    },
    /**
     * Invokes a method on all triggers.
     * @param {String} methodName
     * @param args
     * @private
     */
    invokeTriggers: function(methodName, args) {
        var me = this,
            triggers = me.getTriggers(),
            id, trigger;
        if (triggers) {
            for (id in triggers) {
                if (triggers.hasOwnProperty(id)) {
                    trigger = triggers[id];
                    // IE8 needs "|| []" if args is undefined
                    trigger[methodName].apply(trigger, args || []);
                }
            }
        }
    },
    /**
     * Returns the trigger with the given id
     * @param {String} id
     * @return {Ext.form.trigger.Trigger}
     */
    getTrigger: function(id) {
        return this.getTriggers()[id];
    },
    updateMinHeight: function(minHeight, oldMinHeight) {
        this.callParent([
            minHeight,
            oldMinHeight
        ]);
        this.toggleCls(Ext.baseCSSPrefix + 'has-min-height', !!minHeight);
    },
    updateInputMask: function(inputMask, previous) {
        if (previous) {
            previous.release();
        }
        if (inputMask) {
            this.enableKeyEvents = true;
        }
    },
    updateHideTrigger: function(hideTrigger) {
        this.invokeTriggers(hideTrigger ? 'hide' : 'show');
    },
    updateEditable: function(editable, oldEditable) {
        this.setReadOnlyAttr(!editable || this.readOnly);
    },
    /**
     * Sets the read-only state of this field.
     * @param {Boolean} readOnly True to prevent the user changing the field and explicitly
     * hide the trigger(s). Setting this to true will supersede settings editable and
     * hideTrigger. Setting this to false will defer back to {@link #editable editable} and
     * {@link #hideTrigger hideTrigger}.
     */
    setReadOnly: function(readOnly) {
        var me = this,
            triggers = me.getTriggers(),
            hideTriggers = me.getHideTrigger(),
            trigger, id;
        readOnly = !!readOnly;
        me.callParent([
            readOnly
        ]);
        if (me.rendered) {
            me.setReadOnlyAttr(readOnly || !me.editable);
        }
        if (triggers) {
            for (id in triggers) {
                trigger = triggers[id];
                /*
                 * Controlled trigger visibility state is only managed fully when 'hideOnReadOnly'
                 * is falsy.
                 * Truth table:
                 *   - If the trigger is configured/defaulted as 'hideOnReadOnly : true',
                 *     it's readOnly-visibility is determined solely by readOnly state of the Field.
                 *   - If 'hideOnReadOnly : false/undefined', the
                 *     Fields.{link #hideTrigger hideTrigger} is honored.
                 */
                if (trigger.hideOnReadOnly === true || (trigger.hideOnReadOnly !== false && !hideTriggers)) {
                    trigger.setVisible(!readOnly);
                }
            }
        }
    },
    /**
     * @private
     * Sets the readonly attribute of the input element
     */
    setReadOnlyAttr: function(readOnly) {
        var me = this,
            readOnlyName = 'readonly',
            inputEl = me.inputEl.dom;
        if (readOnly) {
            inputEl.setAttribute(readOnlyName, readOnlyName);
        } else {
            inputEl.removeAttribute(readOnlyName);
        }
        if (!me.ariaStaticRoles[me.ariaRole]) {
            me.inputEl.dom.setAttribute('aria-readonly', !!readOnly);
        }
    },
    /**
     * Performs any necessary manipulation of a raw String value to prepare it for conversion and/or
     * {@link #validate validation}. For text fields this applies the configured
     * {@link #stripCharsRe} to the raw value.
     * @param {String} value The unprocessed string value
     * @return {String} The processed string value
     */
    processRawValue: function(value) {
        var me = this,
            stripRe = me.stripCharsRe,
            mod, newValue;
        if (stripRe) {
            // This will force all instances that match stripRe to be removed
            // in case the user tries to add it with copy and paste EXTJS-18621
            if (!stripRe.global) {
                mod = 'g';
                mod += (stripRe.ignoreCase) ? 'i' : '';
                mod += (stripRe.multiline) ? 'm' : '';
                stripRe = new RegExp(stripRe.source, mod);
            }
            newValue = value.replace(stripRe, '');
            if (newValue !== value) {
                me.setRawValue(newValue);
                // Some components change lastValue as you type, so we need to verify
                // if this is the case here and replace the value of lastValue
                if (me.lastValue === value) {
                    me.lastValue = newValue;
                }
                value = newValue;
            }
        }
        return value;
    },
    onDisable: function() {
        this.callParent();
        if (Ext.isIE) {
            this.inputEl.dom.unselectable = 'on';
        }
    },
    onEnable: function() {
        this.callParent();
        if (Ext.isIE) {
            this.inputEl.dom.unselectable = '';
        }
    },
    onKeyDown: function(event) {
        var me = this,
            inputMask = me.getInputMask();
        if (inputMask) {
            inputMask.onKeyDown(me, me.getValue(), event);
        }
        this.fireEvent('keydown', this, event);
    },
    onKeyUp: function(e) {
        this.fireEvent('keyup', this, e);
    },
    onKeyPress: function(event) {
        var me = this,
            inputMask = me.getInputMask();
        if (inputMask) {
            inputMask.onKeyPress(me, me.getValue(), event);
        }
        me.fireEvent('keypress', me, event);
    },
    onPaste: function(e) {
        var me = this,
            inputMask = me.getInputMask();
        if (inputMask) {
            inputMask.onPaste(me, me.getValue(), e);
        }
        me.fireEvent('paste', me, e);
    },
    /**
     * Returns the value of this field's {@link #cfg-emptyText}
     * @return {String} The value of this field's emptyText
     */
    getEmptyText: function() {
        return this.emptyText;
    },
    /**
     * Sets the default text to place into an empty field
     * @param {String} value The {@link #cfg-emptyText} value for this field
     * @return {Ext.form.field.Text} this
     */
    setEmptyText: function(value) {
        var me = this,
            inputEl = me.inputEl;
        value = value || '';
        me.emptyText = value;
        if (me.rendered) {
            if (Ext.supports.Placeholder && !me.simulatePlaceholder) {
                if (value) {
                    inputEl.dom.setAttribute('placeholder', value);
                } else {
                    inputEl.dom.removeAttribute('placeholder');
                }
            } else {
                me.placeholderLabel.setHtml(value);
            }
            me.refreshEmptyText();
        }
        return this;
    },
    afterFirstLayout: function() {
        var el;
        this.callParent();
        if (Ext.isIE && this.disabled) {
            el = this.inputEl;
            if (el) {
                el.dom.unselectable = 'on';
            }
        }
    },
    /**
     * @private
     */
    toggleInvalidCls: function(hasError) {
        var method = hasError ? 'addCls' : 'removeCls';
        this.callParent([
            hasError
        ]);
        this.triggerWrap[method](this.triggerWrapInvalidCls);
        this.inputWrap[method](this.inputWrapInvalidCls);
    },
    onFieldMutation: function(e) {
        this.refreshEmptyText();
        this.callParent([
            e
        ]);
    },
    refreshEmptyText: function() {
        var me = this,
            inputEl = me.inputEl,
            emptyClsElements = me.emptyClsElements,
            value, isEmpty, i;
        if (inputEl) {
            value = me.getValue();
            isEmpty = !(inputEl.dom.value || (Ext.isArray(value) && value.length));
            if (me.placeholderLabel) {
                me.placeholderLabel.setDisplayed(isEmpty);
            }
            for (i = 0; i < emptyClsElements.length; i++) {
                emptyClsElements[i].toggleCls(me.emptyUICls, isEmpty);
            }
        }
    },
    setValue: function(value) {
        value = this.callParent([
            value
        ]);
        this.refreshEmptyText();
        return value;
    },
    onFocus: function(e) {
        var me = this,
            inputEl = me.inputEl.dom,
            inputMask = me.getInputMask(),
            value, len;
        me.callParent([
            e
        ]);
        if (me.emptyText) {
            me.autoSize();
        }
        if (inputMask) {
            inputMask.onFocus(me, inputEl.value);
        }
        me.addCls(me.fieldFocusCls);
        me.triggerWrap.addCls(me.triggerWrapFocusCls);
        me.inputWrap.addCls(me.inputWrapFocusCls);
        me.invokeTriggers('onFieldFocus', [
            e
        ]);
        // This handler may be called when the focus has already shifted to another element;
        // calling inputEl.select() will forcibly focus again it which in turn might set up
        // a nasty circular race condition if focusEl !== inputEl.
        if (me.selectOnFocus && document.activeElement === inputEl) {
            value = inputEl.value;
            len = value.length;
            // We need a minimal delay here due to a bug that will deselect
            // the text on mouseup + focus.
            // https://code.google.com/p/chromium/issues/detail?id=4505
            // Note: This was fixed years ago but still persists in Safari, IE and Edge.
            Ext.asap(me.selectText, me, [
                0,
                len
            ]);
        }
    },
    /**
     * @private
     */
    onBlur: function(e) {
        var me = this,
            inputEl = me.inputEl.dom,
            inputMask = me.getInputMask(),
            value;
        me.callParent([
            e
        ]);
        value = inputEl && inputEl.value;
        me.removeCls(me.fieldFocusCls);
        me.triggerWrap.removeCls(me.triggerWrapFocusCls);
        me.inputWrap.removeCls(me.inputWrapFocusCls);
        me.invokeTriggers('onFieldBlur', [
            e
        ]);
        if (inputMask && me.getAutoHideInputMask() !== false) {
            inputMask.onBlur(me, value);
        }
    },
    /**
     * @private
     */
    filterKeys: function(e) {
        var charCode;
        /*
         * Current only FF will fire keypress events for special keys.
         * 
         * On European keyboards, the right alt key, Alt Gr, is used to type certain special
         * characters. JS detects a keypress of this as ctrlKey & altKey. As such, we check
         * that alt isn't pressed so we can still process these special characters.
         */
        if ((e.ctrlKey && !e.altKey) || e.isSpecialKey()) {
            return;
        }
        charCode = String.fromCharCode(e.getCharCode());
        if (!this.maskRe.test(charCode)) {
            e.stopEvent();
        }
    },
    getState: function() {
        return this.addPropertyToState(this.callParent(), 'value');
    },
    applyState: function(state) {
        this.callParent([
            state
        ]);
        if (state.hasOwnProperty('value')) {
            this.setValue(state.value);
        }
    },
    /**
     * Validates a value according to the field's validation rules and returns an array of errors
     * for any failing validations. Validation rules are processed in the following order:
     *
     * 1. **Field specific validator**
     *
     *     A validator offers a way to customize and reuse a validation specification.
     *     If a field is configured with a `{@link #validator}`
     *     function, it will be passed the current field value.  The `{@link #validator}`
     *     function is expected to return either:
     *
     *     - Boolean `true`  if the value is valid (validation continues).
     *     - a String to represent the invalid message if invalid (validation halts).
     *
     * 2. **Basic Validation**
     *
     *     If the `{@link #validator}` has not halted validation,
     *     basic validation proceeds as follows:
     *
     *     - `{@link #allowBlank}` : (Invalid message = `{@link #blankText}`)
     *
     *         Depending on the configuration of `{@link #allowBlank}`, a
     *         blank field will cause validation to halt at this step and return
     *         Boolean true or false accordingly.
     *
     *     - `{@link #minLength}` : (Invalid message = `{@link #minLengthText}`)
     *
     *         If the passed value does not satisfy the `{@link #minLength}`
     *         specified, validation halts.
     *
     *     -  `{@link #maxLength}` : (Invalid message = `{@link #maxLengthText}`)
     *
     *         If the passed value does not satisfy the `{@link #maxLength}`
     *         specified, validation halts.
     *
     * 3. **Preconfigured Validation Types (VTypes)**
     *
     *     If none of the prior validation steps halts validation, a field
     *     configured with a `{@link #vtype}` will utilize the
     *     corresponding {@link Ext.form.field.VTypes VTypes} validation function.
     *     If invalid, either the field's `{@link #vtypeText}` or
     *     the VTypes vtype Text property will be used for the invalid message.
     *     Keystrokes on the field will be filtered according to the VTypes
     *     vtype Mask property.
     *
     * 4. **Field specific regex test**
     *
     *     If none of the prior validation steps halts validation, a field's
     *     configured `{@link #regex}` test will be processed.
     *     The invalid message for this test is configured with `{@link #regexText}`
     *
     * @param {Object} value The value to validate. The processed raw value will be used
     * if nothing is passed.
     * @return {String[]} Array of any validation errors
     */
    getErrors: function(value) {
        value = arguments.length ? (value == null ? '' : value) : this.processRawValue(this.getRawValue());
        // eslint-disable-next-line vars-on-top
        var me = this,
            errors = me.callParent([
                value
            ]),
            validator = me.validator,
            vtype = me.vtype,
            vtypes = Ext.form.field.VTypes,
            regex = me.regex,
            format = Ext.String.format,
            msg, trimmed, isBlank;
        if (Ext.isFunction(validator)) {
            msg = validator.call(me, value);
            if (msg !== true) {
                errors.push(msg);
            }
        }
        trimmed = me.allowOnlyWhitespace ? value : Ext.String.trim(value);
        if (trimmed.length < 1) {
            if (!me.allowBlank) {
                errors.push(me.blankText);
            }
            // If we are not configured to validate blank values,
            // there cannot be any additional errors
            if (!me.validateBlank) {
                return errors;
            }
            isBlank = true;
        }
        // If a blank value has been allowed through, then exempt it from the minLength check.
        // It must be allowed to hit the vtype validation.
        if (!isBlank && value.length < me.minLength) {
            errors.push(format(me.minLengthText, me.minLength));
        }
        if (value.length > me.maxLength) {
            errors.push(format(me.maxLengthText, me.maxLength));
        }
        if (vtype) {
            if (!vtypes[vtype](value, me)) {
                errors.push(me.vtypeText || vtypes[vtype + 'Text']);
            }
        }
        if (regex && !regex.test(value)) {
            errors.push(me.regexText || me.invalidText);
        }
        return errors;
    },
    getCaretPos: function() {
        return this.inputEl.getCaretPos();
    },
    setCaretPos: function(pos) {
        this.inputEl.setCaretPos(pos);
    },
    /**
     * Returns the selection range of an input element as an array of three values:
     *
     *      [ start, end, direction ]
     *
     * These have the same meaning as the parameters to `selectText`.
     * @return {Array}
     * @since 6.5.0
     */
    getTextSelection: function() {
        return this.inputEl.getTextSelection();
    },
    /**
     * Select the specified contents of the input field (all by default).
     * @param {Number} [start=0]
     * @param {Number} [end]
     * @param {"f"/"b"/"forward"/"backward"} [direction="f"] Pass "f" for forward,
     * "b" for backwards.
     * @return {Ext.form.field.Text} this
     * @chainable
     */
    selectText: function(start, end, direction) {
        var me = this;
        Ext.defer(function() {
            if (!me.destroyed && me.inputEl.isVisible(true)) {
                me.inputEl.selectText(start, end, direction);
            }
        }, Ext.isIE ? 10 : 0);
        return me;
    },
    // Template method, override in Combobox.
    getGrowWidth: function() {
        return this.inputEl.dom.value;
    },
    /**
     * Automatically grows the field to accommodate the width of the text up to the maximum
     * field width allowed. This only takes effect if {@link #grow} = true, and fires the
     * {@link #autosize} event if the width changes.
     */
    autoSize: function() {
        var me = this,
            triggers, triggerId, triggerWidth, inputEl, width, value;
        if (me.grow && me.rendered && me.getSizeModel().width.auto) {
            inputEl = me.inputEl;
            triggers = me.getTriggers();
            triggerWidth = 0;
            value = Ext.util.Format.htmlEncode(me.getGrowWidth() || (me.hasFocus ? '' : me.emptyText) || '');
            // Translate spaces to non-breaking spaces
            value = value.replace(/\s/g, '&nbsp;');
            for (triggerId in triggers) {
                triggerWidth += triggers[triggerId].el.getWidth();
            }
            width = inputEl.getTextWidth(value) + triggerWidth + // The element that has the border depends on theme - inputWrap (classic)
            // or triggerWrap (neptune)
            me.inputWrap.getBorderWidth('lr') + me.triggerWrap.getBorderWidth('lr') + inputEl.getPadding('lr');
            width = Math.min(Math.max(width, me.growMin), me.growMax);
            me.bodyEl.setWidth(width);
            me.updateLayout();
            me.fireEvent('autosize', me, width);
        }
    },
    doDestroy: function() {
        var me = this;
        me.invokeTriggers('destroy');
        Ext.destroy(me.triggerRepeater);
        me.setInputMask(null);
        me.callParent();
    },
    onTriggerClick: Ext.emptyFn,
    privates: {
        /**
         * @private
         */
        getTdType: function() {
            return 'textfield';
        }
    },
    deprecated: {
        5: {
            methods: {
                /**
                 * @method getTriggerWidth
                 * Get the total width of the trigger button area.
                 * @return {Number} The total trigger width
                 * @deprecated 5.0 This method was removed.
                 */
                getTriggerWidth: function() {
                    var triggers = this.getTriggers(),
                        width = 0,
                        id;
                    if (triggers && this.rendered) {
                        for (id in triggers) {
                            if (triggers.hasOwnProperty(id)) {
                                width += triggers[id].el.getWidth();
                            }
                        }
                    }
                    return width;
                }
            }
        }
    }
}, function(TextField) {
    var calculated = Ext.layout.SizeModel.calculated;
    TextField.prototype.fakeSizeModel = calculated.pairsByHeightOrdinal[calculated.ordinal];
});

/**
 * This class creates a multiline text field, which can be used as a direct replacement
 * for traditional textarea fields. In addition, it supports automatically {@link #grow growing}
 * the height of the textarea to fit its content.
 *
 * All of the configuration options from {@link Ext.form.field.Text} can be used on TextArea.
 *
 * Example usage:
 *
 *     @example
 *     Ext.create('Ext.form.FormPanel', {
 *         title      : 'Sample TextArea',
 *         width      : 400,
 *         bodyPadding: 10,
 *         renderTo   : Ext.getBody(),
 *         items: [{
 *             xtype     : 'textareafield',
 *             grow      : true,
 *             name      : 'message',
 *             fieldLabel: 'Message',
 *             anchor    : '100%'
 *         }]
 *     });
 *
 * Some other useful configuration options when using {@link #grow} are {@link #growMin} and
 * {@link #growMax}. These allow you to set the minimum and maximum grow heights for the textarea.
 * 
 * **NOTE:** In some browsers, carriage returns ('\r', not to be confused with new lines)
 * will be automatically stripped out the value is set to the textarea. Since we cannot
 * use any reasonable method to attempt to re-insert these, they will automatically be
 * stripped out to ensure the behaviour is consistent across browser.
 */
Ext.define('Ext.form.field.TextArea', {
    extend: 'Ext.form.field.Text',
    alias: [
        'widget.textareafield',
        'widget.textarea'
    ],
    alternateClassName: 'Ext.form.TextArea',
    requires: [
        'Ext.XTemplate',
        'Ext.util.DelayedTask'
    ],
    /* eslint-disable indent */
    // This template includes a `\n` after `<textarea>` opening tag so that an
    // initial value starting with `\n` does not lose its first character when
    // the markup is parsed. Both textareas below have the same value:
    //
    //     <textarea>initial value</textarea>
    //
    //     <textarea>
    //     initial value
    //     </textarea>
    //
    fieldSubTpl: [
        '<textarea id="{id}" data-ref="inputEl" rows="1" {inputAttrTpl}',
        '<tpl if="name"> name="{name}"</tpl>',
        '<tpl if="placeholder"> placeholder="{placeholder}"</tpl>',
        '<tpl if="maxLength !== undefined"> maxlength="{maxLength}"</tpl>',
        '<tpl if="readOnly"> readonly="readonly"</tpl>',
        '<tpl if="disabled"> disabled="disabled"</tpl>',
        '<tpl if="tabIdx != null"> tabindex="{tabIdx}"</tpl>',
        ' class="{fieldCls} {typeCls} {typeCls}-{ui} {inputCls} {fixCls}" ',
        '<tpl if="fieldStyle"> style="{fieldStyle}"</tpl>',
        '<tpl foreach="ariaElAttributes"> {$}="{.}"</tpl>',
        '<tpl foreach="inputElAriaAttributes"> {$}="{.}"</tpl>',
        ' autocomplete="off">\n',
        '<tpl if="value">{[Ext.util.Format.htmlEncode(values.value)]}</tpl>',
        '</textarea>',
        {
            disableFormats: true
        }
    ],
    /* eslint-enable indent */
    /**
     * @cfg {Number} growMin
     * The minimum height to allow when {@link #grow}=true
     */
    growMin: 60,
    /**
     * @cfg {Number} growMax
     * The maximum height to allow when {@link #grow}=true
     */
    growMax: 1000,
    /**
     * @cfg {String} growAppend
     * A string that will be appended to the field's current value for the purposes of calculating
     * the target field size. Only used when the {@link #grow} config is true. Defaults to a newline
     * for TextArea to ensure there is always a space below the current line.
     */
    growAppend: '\n-',
    /**
     * @cfg {Boolean} enterIsSpecial
     * True if you want the ENTER key to be classed as a special key and the {@link #specialkey}
     * event to be fired when ENTER is pressed.
     */
    enterIsSpecial: false,
    /**
     * @cfg {Boolean} preventScrollbars
     * true to prevent scrollbars from appearing regardless of how much text is in the field.
     * This option is only relevant when {@link #grow} is true. Equivalent to setting
     * overflow: hidden.
     */
    preventScrollbars: false,
    returnRe: /\r/g,
    inputCls: Ext.baseCSSPrefix + 'form-textarea',
    extraFieldBodyCls: Ext.baseCSSPrefix + 'form-textarea-body',
    ariaAttributes: {
        'aria-multiline': true
    },
    constructor: function(config) {
        this.callParent([
            config
        ]);
        if (this.cols) {
            Ext.log.warn('Ext.form.field.TextArea "cols" config was removed in Ext 5.0. ' + 'Please specify a "width" or use a layout instead.');
        }
        if (this.rows) {
            Ext.log.warn('Ext.form.field.TextArea "rows" config was removed in Ext 5.0. ' + 'Please specify a "height" or use a layout instead.');
        }
    },
    getSubTplData: function(fieldData) {
        var me = this,
            fieldStyle = me.getFieldStyle(),
            ret = me.callParent(arguments);
        if (me.grow) {
            if (me.preventScrollbars) {
                ret.fieldStyle = (fieldStyle || '') + ';overflow:hidden;';
            }
        }
        return ret;
    },
    afterRender: function() {
        var me = this;
        me.callParent(arguments);
        me.needsMaxCheck = me.enforceMaxLength && me.maxLength !== Number.MAX_VALUE && !Ext.supports.TextAreaMaxLength;
        if (me.needsMaxCheck) {
            me.inputEl.on('paste', me.onPaste, me);
        }
    },
    // The following overrides deal with an issue whereby some browsers
    // will strip carriage returns from the textarea input, while others
    // will not. Since there's no way to be sure where to insert returns,
    // the best solution is to strip them out in all cases to ensure that
    // the behaviour is consistent in a cross browser fashion. As such,
    // we override in all cases when setting the value to control this.
    transformRawValue: function(value) {
        return this.stripReturns(value);
    },
    getValue: function() {
        return this.stripReturns(this.callParent());
    },
    valueToRaw: function(value) {
        value = this.stripReturns(value);
        return this.callParent([
            value
        ]);
    },
    stripReturns: function(value) {
        if (value && typeof value === 'string') {
            value = value.replace(this.returnRe, '');
        }
        return value;
    },
    onPaste: function() {
        var me = this;
        if (!me.pasteTask) {
            me.pasteTask = new Ext.util.DelayedTask(me.pasteCheck, me);
        }
        // since we can't get the paste data, we'll give the area a chance to populate
        me.pasteTask.delay(1);
    },
    pasteCheck: function() {
        var me = this,
            value = me.getValue(),
            max = me.maxLength;
        if (value.length > max) {
            value = value.substr(0, max);
            me.setValue(value);
        }
    },
    /**
     * @private
     */
    fireKey: function(e) {
        var me = this,
            key = e.getKey(),
            value;
        if (e.isSpecialKey() && (me.enterIsSpecial || (key !== e.ENTER || e.hasModifier()))) {
            me.fireEvent('specialkey', me, e);
        }
        if (me.needsMaxCheck && key !== e.BACKSPACE && key !== e.DELETE && !e.isNavKeyPress() && !me.isCutCopyPasteSelectAll(e, key)) {
            value = me.getValue();
            if (value.length >= me.maxLength) {
                e.stopEvent();
            }
        }
    },
    isCutCopyPasteSelectAll: function(e, key) {
        if (e.ctrlKey) {
            return key === e.A || key === e.C || key === e.V || key === e.X;
        }
        return false;
    },
    /**
     * Automatically grows the field to accomodate the height of the text up to the maximum
     * field height allowed. This only takes effect if {@link #grow} = true, and fires the
     * {@link #autosize} event if the height changes.
     */
    autoSize: function() {
        var me = this,
            inputEl, hideScroller, height, curWidth, value;
        if (me.grow && me.rendered && me.getSizeModel().height.auto) {
            inputEl = me.inputEl;
            // subtract border/padding to get the available width for the text
            curWidth = inputEl.getWidth(true);
            value = Ext.util.Format.htmlEncode(inputEl.dom.value) || '&#160;';
            value += me.growAppend;
            // Translate newlines to <br> tags
            value = value.replace(/\n/g, '<br/>');
            height = Ext.util.TextMetrics.measure(inputEl, value, curWidth).height + inputEl.getPadding('tb') + // The element that has the border depends on theme - inputWrap (classic)
            // or triggerWrap (neptune)
            me.inputWrap.getBorderWidth('tb') + me.triggerWrap.getBorderWidth('tb');
            height = Math.min(Math.max(height, me.growMin), me.growMax);
            hideScroller = me.preventScrollbars || !me.growMax || height < me.growMax;
            inputEl.setStyle('overflow-y', hideScroller ? 'hidden' : 'auto');
            me.bodyEl.setHeight(height);
            me.updateLayout();
            me.fireEvent('autosize', me, height);
        }
    },
    doDestroy: function() {
        var task = this.pasteTask;
        if (task) {
            task.cancel();
        }
        this.callParent();
    }
});

// @define Ext.MessageBox, Ext.Msg
/**
 * Utility class for generating different styles of message boxes.  The singleton instance,
 * Ext.MessageBox alias `Ext.Msg` can also be used.
 *
 * Note that a MessageBox is asynchronous.  Unlike a regular JavaScript `alert` (which will halt
 * browser execution), showing a MessageBox will not cause the code to stop.  For this reason,
 * if you have code that should only run *after* some user feedback from the MessageBox,
 * you must use a callback function (see the `function` parameter for {@link #method-show}
 * for more details).
 *
 * Basic alert
 *
 *     @example
 *     Ext.Msg.alert('Status', 'Changes saved successfully.');
 *
 * Prompt for user data and process the result using a callback
 *
 *     @example
 *     Ext.Msg.prompt('Name', 'Please enter your name:', function(btn, text){
 *         if (btn == 'ok'){
 *             // process text value and close...
 *         }
 *     });
 *
 * Show a dialog using config options
 *
 *     @example
 *     Ext.Msg.show({
 *         title:'Save Changes?',
 *         message: 'You are closing a tab that has unsaved changes. Do you want to save changes?',
 *         buttons: Ext.Msg.YESNOCANCEL,
 *         icon: Ext.Msg.QUESTION,
 *         fn: function(btn) {
 *             if (btn === 'yes') {
 *                 console.log('Yes pressed');
 *             } else if (btn === 'no') {
 *                 console.log('No pressed');
 *             } else {
 *                 console.log('Cancel pressed');
 *             } 
 *         }
 *     });
 * 
 * Showing Ext.Msg while it's already shown will cause the visible instance to be 
 * overwritten with the newly passed config.  While this may be the desired outcome, you 
 * can also create a new MessageBox that can exist alongside the Ext.Msg 
 * singleton instance.
 * 
 *     @example
 *     var myMsg = Ext.create('Ext.window.MessageBox', {
 *         // set closeAction to 'destroy' if this instance is not
 *         // intended to be reused by the application
 *         closeAction: 'destroy'
 *     }).show({
 *         title: 'Custom MessageBox Instance',
 *         message: 'I can exist along with Ext.Msg'
 *     });
 *     
 *     Ext.Msg.alert('Overlapping', 'Ext.Msg instance');
 *
 * ## Static Values
 *
 * The following values can be utilized by Ext.window.MessageBox and Ext.Msg:
 *
 *     OK    : {text: 'OK',     itemId: 'ok'}
 *     YES   : {text: 'Yes',    itemId: 'yes'}
 *     NO    : {text: 'No',     itemId: 'no'}
 *     CANCEL: {text: 'Cancel', itemId: 'cancel'}
 *
 *     INFO    : Ext.baseCSSPrefix + 'msgbox-info'
 *     WARNING : Ext.baseCSSPrefix + 'msgbox-warning'
 *     QUESTION: Ext.baseCSSPrefix + 'msgbox-question'
 *     ERROR   : Ext.baseCSSPrefix + 'msgbox-error'
 *
 *     OKCANCEL: [
 *         {text: 'Cancel', itemId: 'cancel'},
 *         {text: 'OK',     itemId: 'ok'}
 *     ]
 *
 *     YESNOCANCEL: [
 *         {text: 'Cancel', itemId: 'cancel'},
 *         {text: 'No',     itemId: 'no'},
 *         {text: 'Yes',    itemId: 'yes'}
 *     ]
 *
 *     YESNO: [
 *         {text: 'No',  itemId: 'no'},
 *         {text: 'Yes', itemId: 'yes'}
 *     ]
 *
 */
Ext.define('Ext.window.MessageBox', {
    extend: 'Ext.window.Window',
    requires: [
        'Ext.toolbar.Toolbar',
        'Ext.form.field.Text',
        'Ext.form.field.TextArea',
        'Ext.button.Button',
        'Ext.layout.container.Anchor',
        'Ext.layout.container.HBox',
        'Ext.ProgressBar'
    ],
    alias: 'widget.messagebox',
    /**
     * @property
     * Button config that displays a single OK button
     */
    OK: 1,
    /**
     * @property
     * Button config that displays a single Yes button
     */
    YES: 2,
    /**
     * @property
     * Button config that displays a single No button
     */
    NO: 4,
    /**
     * @property
     * Button config that displays a single Cancel button
     */
    CANCEL: 8,
    /**
     * @property
     * Button config that displays OK and Cancel buttons
     */
    OKCANCEL: 9,
    /**
     * @property
     * Button config that displays Yes and No buttons
     */
    YESNO: 6,
    /**
     * @property
     * Button config that displays Yes, No and Cancel buttons
     */
    YESNOCANCEL: 14,
    /**
     * @property
     * The CSS class that provides the INFO icon image
     */
    INFO: Ext.baseCSSPrefix + 'message-box-info',
    /**
     * @property
     * The CSS class that provides the WARNING icon image
     */
    WARNING: Ext.baseCSSPrefix + 'message-box-warning',
    /**
     * @property
     * The CSS class that provides the QUESTION icon image
     */
    QUESTION: Ext.baseCSSPrefix + 'message-box-question',
    /**
     * @property
     * The CSS class that provides the ERROR icon image
     */
    ERROR: Ext.baseCSSPrefix + 'message-box-error',
    // hide it by offsets. Windows are hidden on render by default.
    hideMode: 'offsets',
    closeAction: 'hide',
    resizable: false,
    scrollable: true,
    title: '&#160;',
    defaultMinWidth: 250,
    defaultMaxWidth: 600,
    defaultMinHeight: 110,
    defaultMaxHeight: 500,
    // Forcibly set these to null on the prototype to override anything set higher in
    // the hierarchy
    minWidth: null,
    maxWidth: null,
    minHeight: null,
    maxHeight: null,
    constrain: true,
    cls: [
        Ext.baseCSSPrefix + 'message-box',
        Ext.baseCSSPrefix + 'hidden-offsets'
    ],
    layout: {
        type: 'vbox',
        align: 'stretch'
    },
    // We want to shrinkWrap around all docked items
    shrinkWrapDock: true,
    /**
     * @property
     * The default height in pixels of the message box's multiline textarea if displayed.
     */
    defaultTextHeight: 75,
    /**
     * @property
     * The minimum width in pixels of the message box if it is a progress-style dialog.
     * This is useful for setting a different minimum width than text-only dialogs may need.
     */
    minProgressWidth: 250,
    /**
     * @property
     * The minimum width in pixels of the message box if it is a prompt dialog.  This is useful
     * for setting a different minimum width than text-only dialogs may need.
     */
    minPromptWidth: 250,
    /**
     * @property
     * An object containing the default button text strings that can be overriden for localized
     * language support. Supported properties are: ok, cancel, yes and no.
     * Generally you should include a locale-specific resource file for handling language support
     * across the framework.
     * Customize the default text like so:
     *
     *     Ext.window.MessageBox.buttonText.yes = "oui"; //french
     * @locale
     */
    buttonText: {
        ok: 'OK',
        yes: 'Yes',
        no: 'No',
        cancel: 'Cancel'
    },
    buttonIds: [
        'ok',
        'yes',
        'no',
        'cancel'
    ],
    baseIconCls: Ext.baseCSSPrefix + 'message-box-icon',
    ariaRole: 'alertdialog',
    makeButton: function(btnIdx) {
        var btnId = this.buttonIds[btnIdx];
        return new Ext.button.Button({
            handler: this.btnCallback,
            itemId: btnId,
            scope: this,
            text: this.buttonText[btnId],
            minWidth: 75
        });
    },
    btnCallback: function(btn, event) {
        var me = this,
            value, field;
        // If this is caused by a keydown event (eg: SPACE on a Button), then the
        // hide will throw focus back to the previously focused element which will
        // then recieve an unexpected keyup event.
        // So defer callback handling until the upcoming keyup event.
        if (event && event.type === 'keydown' && !event.isSpecialKey()) {
            event.getTarget(null, null, true).on({
                keyup: function(e) {
                    me.btnCallback(btn, e);
                },
                single: true
            });
            return;
        }
        if (me.cfg.prompt || me.cfg.multiline) {
            if (me.cfg.multiline) {
                field = me.textArea;
            } else {
                field = me.textField;
            }
            value = field.getValue();
            field.reset();
        }
        // Component.onHide blurs the active element if the Component contains the active element
        me.hide();
        me.userCallback(btn.itemId, value, me.cfg);
    },
    hide: function() {
        var me = this,
            cls = me.cfg ? me.cfg.cls : '';
        me.progressBar.reset();
        if (cls) {
            me.removeCls(cls);
        }
        me.callParent(arguments);
    },
    /**
     * @private
     */
    constructor: function(cfg) {
        var me = this;
        me.callParent(arguments);
        // set the default min/max/Width/Height to the initially configured min/max/Width/Height
        // so that it will be used as the default when reconfiguring.
        me.minWidth = me.defaultMinWidth = (me.minWidth || me.defaultMinWidth);
        me.maxWidth = me.defaultMaxWidth = (me.maxWidth || me.defaultMaxWidth);
        me.minHeight = me.defaultMinHeight = (me.minHeight || me.defaultMinHeight);
        me.maxHeight = me.defaultMaxHeight = (me.maxHeight || me.defaultMaxHeight);
    },
    initComponent: function(cfg) {
        var me = this,
            baseId = me.id,
            i, button;
        // A title or iconCls could have been passed in the config to the constructor.
        me.title = me.title || '&#160;';
        me.iconCls = me.iconCls || '';
        me.topContainer = new Ext.container.Container({
            layout: 'hbox',
            padding: 10,
            style: {
                overflow: 'hidden'
            },
            items: [
                me.iconComponent = new Ext.Component({
                    cls: me.baseIconCls
                }),
                me.promptContainer = new Ext.container.Container({
                    flex: 1,
                    layout: {
                        type: 'vbox',
                        align: 'stretch'
                    },
                    items: [
                        me.msg = new Ext.Component({
                            id: baseId + '-msg',
                            cls: me.baseCls + '-text'
                        }),
                        me.textField = new Ext.form.field.Text({
                            id: baseId + '-textfield',
                            enableKeyEvents: true,
                            ariaAttributes: {
                                'aria-labelledby': me.msg.id
                            },
                            listeners: {
                                keydown: me.onPromptKey,
                                scope: me
                            }
                        }),
                        me.textArea = new Ext.form.field.TextArea({
                            id: baseId + '-textarea',
                            height: 75,
                            ariaAttributes: {
                                'aria-labelledby': me.msg.id
                            }
                        })
                    ]
                })
            ]
        });
        me.progressBar = new Ext.ProgressBar({
            id: baseId + '-progressbar',
            margin: '0 10 10 10'
        });
        me.items = [
            me.topContainer,
            me.progressBar
        ];
        // Create the buttons based upon passed bitwise config
        me.msgButtons = [];
        for (i = 0; i < 4; i++) {
            button = me.makeButton(i);
            me.msgButtons[button.itemId] = button;
            me.msgButtons.push(button);
        }
        me.bottomTb = new Ext.toolbar.Toolbar({
            id: baseId + '-toolbar',
            ui: 'footer',
            dock: 'bottom',
            focusableContainer: false,
            ariaRole: null,
            layout: {
                pack: {
                    left: 'start',
                    right: 'end'
                }[me.buttonAlign] || 'center'
            },
            items: [
                me.msgButtons[0],
                me.msgButtons[1],
                me.msgButtons[2],
                me.msgButtons[3]
            ]
        });
        me.dockedItems = [
            me.bottomTb
        ];
        me.on('close', me.onClose, me);
        me.callParent();
    },
    afterRender: function() {
        var me = this;
        me.callParent(arguments);
        // These fields do not have a visible label, we're using message component instead.
        // Input elements have aria-labelledby pointing to the message component's id;
        // "for" attributes on the labelEl are redundant and get in the way.
        me.textField.labelEl.dom.removeAttribute('for');
        me.textArea.labelEl.dom.removeAttribute('for');
    },
    onClose: function() {
        var btn = this.msgButtons[3];
        if (btn) {
            this.btnCallback(btn);
        }
    },
    onPromptKey: function(textField, e) {
        var me = this;
        if (e.keyCode === e.RETURN || e.keyCode === 10) {
            if (me.msgButtons.ok.isVisible()) {
                me.msgButtons.ok.handler.call(me, me.msgButtons.ok);
            } else if (me.msgButtons.yes.isVisible()) {
                me.msgButtons.yes.handler.call(me, me.msgButtons.yes);
            }
        }
    },
    reconfigure: function(cfg) {
        var me = this,
            buttons = 0,
            hideToolbar = true,
            oldButtonText = me.buttonText,
            resizer = me.resizer,
            header = me.header,
            headerCfg = header && !header.isHeader,
            message = cfg && (cfg.message || cfg.msg),
            buttonTips = cfg.buttonTips,
            title, iconCls, resizeTracker, width, height, i, textArea, textField, msg, progressBar, msgButtons, wait, tool;
        // Restore default buttonText before reconfiguring.
        me.updateButtonText();
        me.cfg = cfg = cfg || {};
        wait = cfg.wait;
        if (cfg.width) {
            width = cfg.width;
        }
        if (cfg.height) {
            height = cfg.height;
        }
        me.minWidth = cfg.minWidth || me.defaultMinWidth;
        me.maxWidth = cfg.maxWidth || me.defaultMaxWidth;
        me.minHeight = cfg.minHeight || me.defaultMinHeight;
        me.maxHeight = cfg.maxHeight || me.defaultMaxHeight;
        if ('maskClickAction' in cfg) {
            me.maskClickAction = cfg.maskClickAction;
        } else {
            delete me.maskClickAction;
        }
        if (resizer) {
            resizeTracker = resizer.resizeTracker;
            resizer.minWidth = resizeTracker.minWidth = me.minWidth;
            resizer.maxWidth = resizeTracker.maxWidth = me.maxWidth;
            resizer.minHeight = resizeTracker.minHeight = me.minHeight;
            resizer.maxHeight = resizeTracker.maxHeight = me.maxHeight;
        }
        // Previous default is rarely going to be valid
        delete me.defaultFocus;
        if (cfg.defaultFocus) {
            me.defaultFocus = cfg.defaultFocus;
        }
        // clear any old animateTarget
        me.animateTarget = cfg.animateTarget || undefined;
        // Defaults to modal
        me.modal = cfg.modal !== false;
        // Show the title/icon if a title/iconCls config was passed in the config
        // to either the constructor or the show() method. Note that anything
        // passed in the config should win.
        //
        // Note that if there is no title/iconCls in the config, check the headerCfg
        // and default to the instance properties. This works because there are default
        // values defined in initComponent.
        if (cfg.title != null) {
            title = cfg.title;
        } else if (headerCfg && header.title != null) {
            title = header.title;
        } else {
            title = me.title;
        }
        if (cfg.iconCls != null) {
            iconCls = cfg.iconCls;
        } else if (headerCfg && header.iconCls != null) {
            iconCls = header.iconCls;
        } else {
            iconCls = me.iconCls;
        }
        me.setTitle(title);
        me.setIconCls(iconCls);
        // Extract button configs
        if (Ext.isObject(cfg.buttons)) {
            me.buttonText = cfg.buttons;
            buttons = 0;
        } else {
            me.buttonText = cfg.buttonText || me.buttonText;
            buttons = Ext.isNumber(cfg.buttons) ? cfg.buttons : 0;
        }
        Ext.each(me.buttonIds, function(buttonId) {
            me.msgButtons[buttonId].setTooltip((buttonTips && buttonTips[buttonId]) || null);
        });
        // Apply custom-configured buttonText
        // Infer additional buttons from the specified property names in the buttonText object
        buttons = buttons | me.updateButtonText();
        // Restore buttonText. Next run of reconfigure will restore to prototype's buttonText
        me.buttonText = oldButtonText;
        // During the on render, or size resetting layouts, and in subsequent hiding and showing,
        // we need to suspend layouts, and flush at the end when the Window's children
        // are at their final visibility.
        Ext.suspendLayouts();
        me.width = me.height = null;
        if (width || height) {
            if (width) {
                me.setWidth(width);
            }
            if (height) {
                me.setHeight(height);
            }
        }
        if (!me.rendered) {
            me.render(Ext.getBody());
        }
        // Hide or show the close tool
        me.closable = cfg.closable !== false && !wait;
        // We need to redefine `header` because me.setIconCls() could create a Header instance.
        header = me.header;
        if (header) {
            tool = header.child('[type=close]');
            if (tool) {
                tool.setVisible(me.closable);
            }
            // Hide or show the header
            if (!cfg.title && !me.closable && !cfg.iconCls) {
                header.hide();
            } else {
                header.show();
            }
        }
        // Default to dynamic drag: drag the window, not a ghost
        me.liveDrag = !cfg.proxyDrag;
        // wrap the user callback
        me.userCallback = Ext.Function.bindCallback(cfg.callback || cfg.fn || Ext.emptyFn, cfg.scope || Ext.global);
        // Hide or show the icon Component
        me.setIcon(cfg.icon);
        // Hide or show the message area
        msg = me.msg;
        if (message) {
            msg.setHtml(message);
            msg.show();
            // As per WAI-ARIA spec, the alertdialog element should point to message element
            // via aria-describedby attribute.
            me.ariaEl.dom.setAttribute('aria-describedby', msg.id);
        } else {
            msg.hide();
            me.ariaEl.dom.removeAttribute('aria-describedby');
        }
        // Hide or show the input field
        textArea = me.textArea;
        textField = me.textField;
        if (cfg.prompt || cfg.multiline) {
            me.multiline = cfg.multiline;
            if (cfg.multiline) {
                textArea.setValue(cfg.value);
                textArea.setHeight(cfg.defaultTextHeight || me.defaultTextHeight);
                textArea.show();
                textField.hide();
                me.defaultFocus = textArea;
            } else {
                textField.setValue(cfg.value);
                textArea.hide();
                textField.show();
                me.defaultFocus = textField;
            }
            // When either input field is displayed, it will reference the message component's
            // element via aria-labelledby. In such cases we need to remove aria-describedby
            // from the window ariaEl to avoid the message being announced twice.
            me.ariaEl.dom.removeAttribute('aria-describedby');
        } else {
            textArea.hide();
            textField.hide();
        }
        // Hide or show the progress bar
        progressBar = me.progressBar;
        if (cfg.progress || wait) {
            progressBar.show();
            me.updateProgress(0, cfg.progressText);
            me.defaultFocus = progressBar;
            if (wait) {
                progressBar.wait(wait === true ? cfg.waitConfig : wait);
            }
        } else {
            progressBar.hide();
        }
        // Hide or show buttons depending on flag value sent.
        msgButtons = me.msgButtons;
        for (i = 0; i < 4; i++) {
            if (buttons & Math.pow(2, i)) {
                // Default to focus on the first visible button if focus not already set
                if (!me.defaultFocus) {
                    me.defaultFocus = msgButtons[i];
                }
                msgButtons[i].show();
                hideToolbar = false;
            } else {
                msgButtons[i].hide();
            }
        }
        // Hide toolbar if no buttons to show
        if (hideToolbar) {
            me.bottomTb.hide();
        } else {
            me.bottomTb.show();
        }
        Ext.resumeLayouts(true);
    },
    /**
     * @private
     * Set button text according to current buttonText property object
     * @return {Number} The buttons bitwise flag based upon the button IDs specified
     * in the buttonText property.
     */
    updateButtonText: function() {
        var me = this,
            buttonText = me.buttonText,
            buttons = 0,
            btnId, btn;
        for (btnId in buttonText) {
            if (buttonText.hasOwnProperty(btnId)) {
                btn = me.msgButtons[btnId];
                if (btn) {
                    if (me.cfg && me.cfg.buttonText) {
                        buttons = buttons | Math.pow(2, Ext.Array.indexOf(me.buttonIds, btnId));
                    }
                    if (btn.text !== buttonText[btnId]) {
                        btn.setText(buttonText[btnId]);
                    }
                }
            }
        }
        return buttons;
    },
    /**
     * Displays a new message box, or re-initializes an existing message box, based on the config
     * options passed in. All display functions (e.g. prompt, alert, etc.) on MessageBox
     * call this function internally, although those calls are basic shortcuts and do not support
     * all of the config options allowed here.
     *
     * Example usage:
     *
     *     Ext.Msg.show({
     *         title: 'Address',
     *         message: 'Please enter your address:',
     *         width: 300,
     *         buttons: Ext.Msg.OKCANCEL,
     *         multiline: true,
     *         fn: saveAddress,
     *         animateTarget: 'addAddressBtn',
     *         icon: Ext.window.MessageBox.INFO
     *     });
     *
     * @param {Object} cfg The following config options are supported:
     *
     * @param {String/Ext.dom.Element} [cfg.animateTarget]
     * An id or Element from which the message box should animate as it opens and closes.
     *
     * @param {Number} [cfg.buttons=false]
     * A bitwise button specifier consisting of the sum of any of the following constants:
     *
     *  - Ext.MessageBox.OK
     *  - Ext.MessageBox.YES
     *  - Ext.MessageBox.NO
     *  - Ext.MessageBox.CANCEL
     *
     * Some common combinations have already been predefined:
     *
     *  - Ext.MessageBox.OKCANCEL
     *  - Ext.MessageBox.YESNO
     *  - Ext.MessageBox.YESNOCANCEL
     *
     * Or false to not show any buttons.
     *
     * This may also be specified as an object hash containing custom button text in the same format
     * as the {@link #buttonText} config. Button IDs present as property names will be made visible.
     *
     * @param {Boolean} [cfg.closable]
     * False to hide the top-right close button (defaults to true). Note that progress and wait
     * dialogs will ignore this property and always hide the close button as they can only be closed
     * programmatically.
     *
     * @param {String} [cfg.cls]
     * A custom CSS class to apply to the message box's container element
     *
     * @param {Number} [cfg.defaultTextHeight=75]
     * The default height in pixels of the message box's multiline textarea if displayed.
     *
     * @param {Function} [cfg.fn]
     * A callback function which is called when the dialog is dismissed either by clicking on the
     * configured buttons, or on the dialog close button, or by pressing the return button
     * to enter input.
     *
     * Progress and wait dialogs will ignore this option since they do not respond to user actions
     * and can only be closed programmatically, so any required function should be called
     * by the same code after it closes the dialog.
     * Parameters passed:
     *
     *  @param {String} cfg.fn.buttonId The ID of the button pressed, one of:
     *
     * - ok
     * - yes
     * - no
     * - cancel
     *
     *  @param {String} cfg.fn.text Value of the input field if either `prompt` or `multiline`
     * is true
     *  @param {Object} cfg.fn.opt The config object passed to show.
     *
     * @param {Object} [cfg.buttonText]
     * An object containing string properties which override the system-supplied button text values
     * just for this invocation. The property names are:
     *
     * - ok
     * - yes
     * - no
     * - cancel
     *
     * @param {Object} [cfg.buttonTips] An object keyed by the button ids containing
     * {@link Ext.button.Button#tooltip tooltip} configurations for each button. eg:
     *
     *     {
     *         ok: 'Commit the record',
     *         cancel: 'Discard the record'
     *     }
     *
     * @param {Object} [cfg.scope]
     * The scope (`this` reference) in which the function will be executed.
     *
     * @param {String} [cfg.icon]
     * A CSS class that provides a background image to be used as the body icon for the dialog.
     * One can use a predefined icon class:
     *
     *  - Ext.MessageBox.INFO
     *  - Ext.MessageBox.WARNING
     *  - Ext.MessageBox.QUESTION
     *  - Ext.MessageBox.ERROR
     *
     * or use just any `'custom-class'`. Defaults to empty string.
     *
     * @param {String} [cfg.iconCls]
     * The standard {@link Ext.window.Window#iconCls} to add an optional header icon
     * (defaults to '')
     * 
     * @param {String} [cfg.defaultFocus]
     * The button to focus when showing the dialog. If not specified, defaults to
     * the first visible button.
     *
     * @param {Number} [cfg.maxWidth]
     * The maximum width in pixels of the message box (defaults to 600)
     *
     * @param {Number} [cfg.minWidth]
     * The minimum width in pixels of the message box (defaults to 100)
     *
     * @param {Boolean} [cfg.modal]
     * False to allow user interaction with the page while the message box is displayed
     * (defaults to true)
     *
     * @param {String} [cfg.message]
     * A string that will replace the existing message box body text (defaults to the
     * XHTML-compliant non-breaking space character '&#160;')
     *
     * @param {Boolean} [cfg.multiline]
     * True to prompt the user to enter multi-line text (defaults to false)
     *
     * @param {Boolean} [cfg.progress]
     * True to display a progress bar (defaults to false)
     *
     * @param {String} [cfg.progressText]
     * The text to display inside the progress bar if progress = true (defaults to '')
     *
     * @param {Boolean} [cfg.prompt]
     * True to prompt the user to enter single-line text (defaults to false)
     *
     * @param {Boolean} [cfg.proxyDrag]
     * True to display a lightweight proxy while dragging (defaults to false)
     *
     * @param {String} cfg.title
     * The title text
     *
     * @param {String} [cfg.value]
     * The string value to set into the active textbox element if displayed
     *
     * @param {Object/Boolean} [cfg.wait=false]
     * `true` to display a progress bar, or a configuration for {@link Ext.ProgressBar#wait}.
     *
     * @param {Object} [cfg.waitConfig]
     * A {@link Ext.ProgressBar#wait} config object (applies only if wait = true)
     *
     * @param {Number} [cfg.width]
     * The width of the dialog in pixels
     *
     * @param {String} [cfg.maskClickAction=focus]
     * The method to call when the window's modal mask is clicked or tapped:
     *
     * - **`'{@link #method-focus}'`** :
     *
     *   The default. Focus the window, which will then pass focus into its
     *   {@link #cfg-defaultFocus} delegate.
     *
     * - **`'{@link #method-hide}'`** :
     *
     *   {@link #method-hide} the window by setting visibility to hidden and applying
     *   negative offsets.
     *   @since 6.2.0
     *
     * @return {Ext.window.MessageBox} this
     */
    show: function(cfg) {
        var me = this,
            visibleFocusables;
        cfg = cfg || {};
        // If called during global layout suspension, make the call after layout resumption
        if (Ext.Component.layoutSuspendCount) {
            Ext.on({
                resumelayouts: function() {
                    me.show(cfg);
                },
                single: true
            });
            return me;
        }
        me.reconfigure(cfg);
        if (cfg.cls) {
            me.addCls(cfg.cls);
        }
        // Do not steal focus from anything that may be focused if the MessageBox
        // has no visible focusable items. For example, a "wait" message box should not get focus.
        visibleFocusables = me.query('textfield:not([hidden]),textarea:not([hidden]),button:not([hidden])');
        me.preventFocusOnActivate = !visibleFocusables.length;
        me.callParent();
        return me;
    },
    onShow: function() {
        this.callParent(arguments);
        this.center();
    },
    updateText: function(text) {
        this.msg.setHtml(text);
    },
    /**
     * Adds the specified icon to the dialog.  By default, the class 'x-messagebox-icon'
     * is applied for default styling, and the class passed in is expected to supply
     * the background image url. Pass in empty string ('') to clear any existing icon.
     * This method must be called before the MessageBox is shown. The following built-in icon
     * classes are supported, but you can also pass in a custom class name:
     *
     *     Ext.window.MessageBox.INFO
     *     Ext.window.MessageBox.WARNING
     *     Ext.window.MessageBox.QUESTION
     *     Ext.window.MessageBox.ERROR
     *
     * @param {String} icon A CSS classname specifying the icon's background image url,
     * or empty string to clear the icon
     * @param {Number} [width] The width of the icon. If not specified, the default is used
     * @param {Number} [height] The height of the icon. If not specified, the default is used
     * @return {Ext.window.MessageBox} this
     */
    setIcon: function(icon, width, height) {
        var me = this,
            iconCmp = me.iconComponent,
            cls = me.messageIconCls;
        if (cls) {
            iconCmp.removeCls(cls);
        }
        if (icon) {
            iconCmp.show();
            if (width || height) {
                iconCmp.setSize(width || iconCmp.getWidth(), height || iconCmp.getHeight());
            }
            iconCmp.addCls(Ext.baseCSSPrefix + 'dlg-icon');
            iconCmp.addCls(me.messageIconCls = icon);
        } else {
            iconCmp.removeCls(Ext.baseCSSPrefix + 'dlg-icon');
            iconCmp.hide();
        }
        return me;
    },
    /**
     * Updates a progress-style message box's text and progress bar. Only relevant on message boxes
     * initiated via {@link Ext.window.MessageBox#progress} or {@link Ext.window.MessageBox#wait},
     * or by calling {@link Ext.window.MessageBox#method-show} with progress: true.
     *
     * @param {Number} [value=0] Any number between 0 and 1 (e.g., .5)
     * @param {String} [progressText=''] The progress text to display inside the progress bar.
     * @param {String} [message] The message box's body text is replaced with the specified string
     * (defaults to undefined so that any existing body text will not get overwritten by default
     * unless a new value is passed in)
     * @return {Ext.window.MessageBox} this
     */
    updateProgress: function(value, progressText, message) {
        this.progressBar.updateProgress(value, progressText);
        if (message) {
            this.updateText(message);
        }
        return this;
    },
    onEsc: function() {
        if (this.closable !== false) {
            this.callParent(arguments);
        }
    },
    /**
     * Displays a confirmation message box with Yes and No buttons (comparable to JavaScript's
     * confirm). If a callback function is passed it will be called after the user clicks either
     * button, and the id of the button that was clicked will be passed as the only parameter
     * to the callback (could also be the top-right close button, which will always report
     * as "cancel").
     *
     * @param {String} title The title bar text
     * @param {String} message The message box body text
     * @param {Function} [fn] The callback function invoked after the message box is closed.
     * See {@link #method-show} method for details.
     * @param {Object} [scope=window] The scope (`this` reference) in which the callback
     * is executed.
     * @return {Ext.window.MessageBox} this
     */
    confirm: function(title, message, fn, scope) {
        if (Ext.isString(title)) {
            title = {
                title: title,
                icon: this.QUESTION,
                message: message,
                buttons: this.YESNO,
                callback: fn,
                scope: scope
            };
        }
        return this.show(title);
    },
    /**
     * Displays a message box with OK and Cancel buttons prompting the user to enter some text
     * (comparable to JavaScript's prompt). The prompt can be a single-line or multi-line textbox.
     * If a callback function is passed it will be called after the user clicks either button,
     * and the id of the button that was clicked (could also be the top-right close button,
     * which will always report as "cancel") and the text that was entered will be passed
     * as the two parameters to the callback.
     *
     * @param {String} title The title bar text
     * @param {String} message The message box body text
     * @param {Function} [fn] The callback function invoked after the message box is closed.
     * See {@link #method-show} method for details.
     * @param {Object} [scope=window] The scope (`this` reference) in which the callback
     * is executed.
     * @param {Boolean/Number} [multiline=false] True to create a multiline textbox
     * using the defaultTextHeight property, or the height in pixels to create the textbox.
     * @param {String} [value=''] Default value of the text input element
     * @return {Ext.window.MessageBox} this
     */
    prompt: function(title, message, fn, scope, multiline, value) {
        if (Ext.isString(title)) {
            title = {
                prompt: true,
                title: title,
                minWidth: this.minPromptWidth,
                message: message,
                buttons: this.OKCANCEL,
                callback: fn,
                scope: scope,
                multiline: multiline,
                value: value
            };
        }
        return this.show(title);
    },
    /**
     * Displays a message box with an infinitely auto-updating progress bar.  This can be used
     * to block user interaction while waiting for a long-running process to complete that does not
     * have defined intervals. You are responsible for closing the message box when the process
     * is complete.
     *
     * @param {String} message The message box body text
     * @param {String} [title] The title bar text
     * @param {Object} [config] A {@link Ext.ProgressBar#wait} config object
     * @return {Ext.window.MessageBox} this
     */
    wait: function(message, title, config) {
        if (Ext.isString(message)) {
            message = {
                title: title,
                message: message,
                closable: false,
                wait: true,
                modal: true,
                minWidth: this.minProgressWidth,
                waitConfig: config
            };
        }
        return this.show(message);
    },
    /**
     * Displays a standard read-only message box with an OK button (comparable to the basic
     * JavaScript alert prompt). If a callback function is passed it will be called after the user
     * clicks the button, and the id of the button that was clicked will be passed as the only
     * parameter to the callback (could also be the top-right close button, which will always
     * report as "cancel").
     *
     * @param {String} title The title bar text
     * @param {String} message The message box body text
     * @param {Function} [fn] The callback function invoked after the message box is closed.
     * See {@link #method-show} method for details.
     * @param {Object} [scope=window] The scope (<code>this</code> reference) in which the callback
     * is executed.
     * @return {Ext.window.MessageBox} this
     */
    alert: function(title, message, fn, scope) {
        if (Ext.isString(title)) {
            title = {
                title: title,
                message: message,
                buttons: this.OK,
                fn: fn,
                scope: scope,
                minWidth: this.minWidth
            };
        }
        return this.show(title);
    },
    /**
     * Displays a message box with a progress bar.
     *
     * You are responsible for updating the progress bar as needed via
     * {@link Ext.window.MessageBox#updateProgress} and closing the message box when the process
     * is complete.
     *
     * @param {String} title The title bar text
     * @param {String} message The message box body text
     * @param {String} [progressText=''] The text to display inside the progress bar
     * @return {Ext.window.MessageBox} this
     */
    progress: function(title, message, progressText) {
        if (Ext.isString(title)) {
            title = {
                title: title,
                message: message,
                progress: true,
                progressText: progressText
            };
        }
        return this.show(title);
    },
    privates: {
        syncHeaderConfigs: Ext.privateFn
    }
}, function(MessageBox) {
    /**
     * @class Ext.MessageBox
     * @alternateClassName Ext.Msg
     * @extends Ext.window.MessageBox
     * @singleton
     * @inheritdoc Ext.window.MessageBox
     */
    // We want to defer creating Ext.MessageBox and Ext.Msg instances
    // until overrides have been applied.
    Ext.onInternalReady(function() {
        Ext.MessageBox = Ext.Msg = new MessageBox();
    });
});

/**
 * Provides input field management, validation, submission, and form loading services for the
 * collection of {@link Ext.form.field.Field Field} instances within a
 * {@link Ext.container.Container}. It is recommended that you use a {@link Ext.form.Panel}
 * as the form container, as that has logic to automatically hook up an instance of
 * {@link Ext.form.Basic} (plus other conveniences related to field configuration.)
 *
 * ## Form Actions
 *
 * The Basic class delegates the handling of form loads and submits to instances of
 * {@link Ext.form.action.Action}. See the various Action implementations for specific details
 * of each one's functionality, as well as the documentation for {@link #doAction} which details
 * the configuration options that can be specified in each action call.
 *
 * The default submit Action is {@link Ext.form.action.Submit}, which uses an Ajax request
 * to submit the form's values to a configured URL. To enable normal browser submission
 * of an Ext form, use the {@link #standardSubmit} config option.
 *
 * ## File uploads
 *
 * File uploads are not performed using normal 'Ajax' techniques; see the description for
 * {@link #hasUpload} for details. If you're using file uploads you should read the method
 * description.
 *
 * ## Example usage:
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Basic Form',
 *         renderTo: Ext.getBody(),
 *         bodyPadding: 5,
 *         width: 350,
 *
 *         // Any configuration items here will be automatically passed along to
 *         // the Ext.form.Basic instance when it gets created.
 *
 *         // The form will submit an AJAX request to this URL when submitted
 *         url: 'save-form.php',
 *
 *         items: [{
 *             xtype: 'textfield',
 *             fieldLabel: 'Field',
 *             name: 'theField'
 *         }],
 *
 *         buttons: [{
 *             text: 'Submit',
 *             handler: function() {
 *                 // The getForm() method returns the Ext.form.Basic instance:
 *                 var form = this.up('form').getForm();
 *                 if (form.isValid()) {
 *                     // Submit the Ajax request and handle the response
 *                     form.submit({
 *                         success: function(form, action) {
 *                            Ext.Msg.alert('Success', action.result.message);
 *                         },
 *                         failure: function(form, action) {
 *                             Ext.Msg.alert(
 *                                 'Failed',
 *                                 action.result ? action.result.message : 'No response'
 *                             );
 *                         }
 *                     });
 *                 }
 *             }
 *         }]
 *     });
 */
Ext.define('Ext.form.Basic', {
    extend: 'Ext.util.Observable',
    alternateClassName: 'Ext.form.BasicForm',
    requires: [
        'Ext.util.MixedCollection',
        'Ext.form.action.Load',
        'Ext.form.action.Submit',
        'Ext.form.action.StandardSubmit',
        'Ext.window.MessageBox',
        'Ext.data.ErrorCollection',
        'Ext.util.DelayedTask'
    ],
    // Not a public API config, this is useful when we're unit testing so we can
    // turn off the delayed tasks so they fire immediately.
    taskDelay: 10,
    /**
     * @event beforeaction
     * Fires before any action is performed. Return false to cancel the action.
     * @param {Ext.form.Basic} this
     * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} to be performed
     */
    /**
     * @event actionfailed
     * Fires when an action fails.
     * @param {Ext.form.Basic} this
     * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} that failed
     */
    /**
     * @event actioncomplete
     * Fires when an action is completed.
     * @param {Ext.form.Basic} this
     * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} that completed
     */
    /**
     * @event validitychange
     * Fires when the validity of the entire form changes.
     * @param {Ext.form.Basic} this
     * @param {Boolean} valid `true` if the form is now valid, `false` if it is now invalid.
     */
    /**
     * @event dirtychange
     * Fires when the dirty state of the entire form changes.
     * @param {Ext.form.Basic} this
     * @param {Boolean} dirty `true` if the form is now dirty, `false` if it is no longer dirty.
     */
    /**
     * @event errorchange
     * Fires when the error of one (or more) of the fields in the form changes.
     * @param {Ext.form.Basic} this
     *
     * @private
     */
    /**
     * Creates new form.
     * @param {Ext.container.Container} owner The component that is the container for the form,
     * usually a {@link Ext.form.Panel}
     * @param {Object} config Configuration options. These are normally specified in the config
     * to the {@link Ext.form.Panel} constructor, which passes them along to the BasicForm
     * automatically.
     */
    constructor: function(owner, config) {
        var me = this,
            reader;
        /**
         * @property {Ext.container.Container} owner
         * The container component to which this BasicForm is attached.
         */
        me.owner = owner;
        me.fieldMonitors = {
            validitychange: me.checkValidityDelay,
            enable: me.checkValidityDelay,
            disable: me.checkValidityDelay,
            dirtychange: me.checkDirtyDelay,
            errorchange: me.checkErrorDelay,
            scope: me
        };
        me.checkValidityTask = new Ext.util.DelayedTask(me.checkValidity, me);
        me.checkDirtyTask = new Ext.util.DelayedTask(me.checkDirty, me);
        me.checkErrorTask = new Ext.util.DelayedTask(me.checkError, me);
        // We use the monitor here as opposed to event bubbling.
        // The problem with bubbling is it doesn't let us react to items being added/remove
        // at different places in the hierarchy which may have an impact on the dirty/valid state.
        me.monitor = new Ext.container.Monitor({
            selector: '[isFormField]:not([excludeForm])',
            scope: me,
            addHandler: me.onFieldAdd,
            removeHandler: me.onFieldRemove,
            invalidateHandler: me.onMonitorInvalidate
        });
        me.monitor.bind(owner);
        Ext.apply(me, config);
        // Normalize the paramOrder to an Array
        if (Ext.isString(me.paramOrder)) {
            me.paramOrder = me.paramOrder.split(/[\s,|]/);
        }
        reader = me.reader;
        if (reader && !reader.isReader) {
            if (typeof reader === 'string') {
                reader = {
                    type: reader
                };
            }
            me.reader = Ext.createByAlias('reader.' + reader.type, reader);
        }
        reader = me.errorReader;
        if (reader && !reader.isReader) {
            if (typeof reader === 'string') {
                reader = {
                    type: reader
                };
            }
            me.errorReader = Ext.createByAlias('reader.' + reader.type, reader);
        }
        me.callParent();
    },
    /**
     * Do any post layout initialization
     * @private
     */
    initialize: function() {
        this.initialized = true;
        this.onValidityChange(!this.hasInvalidField());
    },
    /**
     * @cfg {String} method
     * The request method to use (GET or POST) for form actions if one isn't supplied
     * in the action options.
     */
    /**
     * @cfg {Object/Ext.data.reader.Reader} reader
     * An Ext.data.reader.Reader (e.g. {@link Ext.data.reader.Xml}) instance or
     * configuration to be used to read data when executing 'load' actions. This 
     * is optional as there is built-in support for processing JSON responses.
     */
    /**
     * @cfg {Object/Ext.data.reader.Reader} errorReader
     * An Ext.data.reader.Reader (e.g. {@link Ext.data.reader.Xml}) instance or
     * configuration to be used to read field error messages returned from 'submit' actions. 
     * This is optional as there is built-in support for processing JSON responses.
     *
     * The Records which provide messages for the invalid Fields must use the
     * Field name (or id) as the Record ID, and must contain a field called 'msg'
     * which contains the error message.
     *
     * The errorReader does not have to be a full-blown implementation of a
     * Reader. It simply needs to implement a `read(xhr)` function
     * which returns an Array of Records in an object with the following
     * structure:
     *
     *     {
     *         records: recordArray
     *     }
     */
    /**
     * @cfg {String} url
     * The URL to use for form actions if one isn't supplied in the
     * {@link Ext.form.Basic#doAction doAction} options.
     */
    /**
     * @cfg {Object} baseParams
     * Parameters to pass with all requests. e.g. baseParams: `{id: '123', foo: 'bar'}`.
     *
     * Parameters are encoded as standard HTTP parameters using {@link Ext.Object#toQueryString}.
     */
    /**
     * @cfg {Number} timeout
     * Timeout for form actions in seconds.
     */
    timeout: 30,
    /**
     * @cfg {Object} api
     * If specified, load and submit actions will be handled with
     * {@link Ext.form.action.DirectLoad DirectLoad} and
     * {@link Ext.form.action.DirectSubmit DirectSubmit}. Methods which have been imported by
     * {@link Ext.direct.Manager} can be specified here to load and submit forms. API methods
     * may also be specified as strings. See {@link Ext.data.proxy.Direct#directFn}.
     * Such as the following:
     *
     *     api: {
     *         load: App.ss.MyProfile.load,
     *         submit: App.ss.MyProfile.submit
     *     }
     *
     * Load actions can use {@link #paramOrder} or {@link #paramsAsHash} to customize how the load
     * method is invoked. Submit actions will always use a standard form submit. The `formHandler`
     * configuration (see Ext.direct.RemotingProvider#action) must be set on the associated
     * server-side method which has been imported by {@link Ext.direct.Manager}.
     */
    /**
     * @cfg {String/String[]} paramOrder
     * A list of params to be executed server side. Only used for the {@link #api} `load`
     * configuration.
     *
     * Specify the params in the order in which they must be executed on the
     * server-side as either (1) an Array of String values, or (2) a String of params
     * delimited by either whitespace, comma, or pipe. For example,
     * any of the following would be acceptable:
     *
     *     paramOrder: ['param1','param2','param3']
     *     paramOrder: 'param1 param2 param3'
     *     paramOrder: 'param1,param2,param3'
     *     paramOrder: 'param1|param2|param'
     */
    /**
     * @cfg {Boolean} paramsAsHash
     * Only used for the {@link #api} `load` configuration. If true, parameters will be sent as a
     * single hash collection of named arguments. Providing a {@link #paramOrder} nullifies this
     * configuration.
     */
    paramsAsHash: false,
    /**
     * @cfg {Object/Array} [metadata]
     * Optional metadata to pass with the actions when Ext Direct {@link #api} is used.
     * See {@link Ext.direct.Manager} for more information.
     */
    /**
     * @cfg {String} waitTitle
     * The default title to show for the waiting message box
     * @locale
     */
    waitTitle: 'Please Wait...',
    /**
     * @cfg {Boolean} trackResetOnLoad
     * If set to true, {@link #method-reset}() resets to the last loaded or
     * {@link Ext.form.Basic#setValues}() data instead of when the form was first
     * created.
     */
    trackResetOnLoad: false,
    /**
     * @cfg {Boolean} standardSubmit
     * If set to true, a standard HTML form submit is used instead of a XHR (Ajax) style form
     * submission. All of the field values, plus any additional params configured via
     * {@link #baseParams} and/or the `options` to {@link #submit}, will be included in the values
     * submitted in the form.
     */
    /**
     * @cfg {Boolean} jsonSubmit
     * If set to true, the field values are sent as JSON in the request body.
     * All of the field values, plus any additional params configured via {@link #baseParams}
     * and/or the `options` to {@link #submit}, will be included in the values POSTed in the body
     * of the request.
     */
    /**
     * @cfg {String/HTMLElement/Ext.dom.Element} waitMsgTarget
     * By default wait messages are displayed with Ext.MessageBox.wait. You can target a specific
     * element by passing it or its id or mask the form itself by passing in true.
     */
    /**
     * @private
     */
    wasDirty: false,
    /**
     * Destroys this object.
     */
    destroy: function() {
        var me = this,
            mon = me.monitor;
        Ext.undefer(me.actionTimer);
        if (mon) {
            mon.unbind();
            me.monitor = null;
        }
        me.clearListeners();
        me.checkValidityTask.cancel();
        me.checkDirtyTask.cancel();
        me.checkErrorTask.cancel();
        me.checkValidityTask = me.checkDirtyTask = me.checkErrorTask = null;
        me.callParent();
    },
    onFieldAdd: function(field) {
        field.on(this.fieldMonitors);
        this.onMonitorInvalidate();
    },
    onFieldRemove: function(field) {
        field.un(this.fieldMonitors);
        this.onMonitorInvalidate();
    },
    onMonitorInvalidate: function() {
        if (this.initialized) {
            this.checkValidityDelay();
        }
    },
    /**
     * Return all the {@link Ext.form.field.Field} components in the owner container.
     * @return {Ext.util.MixedCollection} Collection of the Field objects
     */
    getFields: function() {
        return this.monitor.getItems();
    },
    /**
     * @private
     * Finds and returns the set of all items bound to fields inside this form
     * @return {Ext.util.MixedCollection} The set of all bound form field items
     */
    getBoundItems: function() {
        var boundItems = this._boundItems;
        if (!boundItems || boundItems.getCount() === 0) {
            boundItems = this._boundItems = new Ext.util.MixedCollection();
            boundItems.addAll(this.owner.query('[formBind]'));
        }
        return boundItems;
    },
    /**
     * Returns true if the form contains any invalid fields. No fields will be marked as invalid
     * as a result of calling this; to trigger marking of fields use {@link #isValid} instead.
     */
    hasInvalidField: function() {
        return !!this.getFields().findBy(function(field) {
            var preventMark = field.preventMark,
                isValid;
            field.preventMark = true;
            isValid = field.isValid();
            field.preventMark = preventMark;
            return !isValid;
        });
    },
    /**
     * Returns true if client-side validation on the form is successful. Any invalid fields will be
     * marked as invalid. If you only want to determine overall form validity without marking
     * anything, use {@link #hasInvalidField} instead.
     * @return {Boolean}
     */
    isValid: function() {
        var me = this,
            invalid;
        Ext.suspendLayouts();
        invalid = me.getFields().filterBy(function(field) {
            return !field.validate();
        });
        Ext.resumeLayouts(true);
        return invalid.length < 1;
    },
    /**
     * Check whether the validity of the entire form has changed since it was last checked, and
     * if so fire the {@link #validitychange validitychange} event. This is automatically invoked
     * when an individual field's validity changes.
     */
    checkValidity: function() {
        var me = this,
            valid;
        if (me.destroyed) {
            return;
        }
        valid = !me.hasInvalidField();
        if (valid !== me.wasValid) {
            me.onValidityChange(valid);
            me.fireEvent('validitychange', me, valid);
            me.wasValid = valid;
        }
    },
    checkValidityDelay: function() {
        var timer = this.taskDelay;
        if (timer) {
            this.checkValidityTask.delay(timer);
        } else {
            this.checkValidity();
        }
    },
    checkError: function() {
        // Currently this event is private, we don't really care
        // about the summation of the change, rather that something has
        // changed so we may need to recalculate. In the future if this
        // is made public, we would need to track the error on a per-field basis.
        this.fireEvent('errorchange', this);
    },
    checkErrorDelay: function() {
        var timer = this.taskDelay;
        if (timer) {
            this.checkErrorTask.delay(timer);
        } else {
            this.checkError();
        }
    },
    /**
     * @private
     * Handle changes in the form's validity. If there are any sub components with
     * `formBind=true` then they are enabled/disabled based on the new validity.
     * @param {Boolean} valid
     */
    onValidityChange: function(valid) {
        var boundItems = this.getBoundItems(),
            items, i, iLen, cmp;
        if (boundItems) {
            items = boundItems.items;
            iLen = items.length;
            for (i = 0; i < iLen; i++) {
                cmp = items[i];
                if (cmp.disabled === valid) {
                    cmp.setDisabled(!valid);
                }
            }
        }
    },
    /**
     * Returns `true` if any fields in this form have changed from their original values.
     *
     * Note that if this BasicForm was configured with {@link Ext.form.Basic#trackResetOnLoad
     * trackResetOnLoad} then the Fields' *original values* are updated when the values are
     * loaded by {@link Ext.form.Basic#setValues setValues} or {@link #loadRecord}. This means
     * that:
     * 
     * - {@link #trackResetOnLoad}: `false` -> Will return `true` after calling this method.
     * - {@link #trackResetOnLoad}: `true` -> Will return `false` after calling this method.
     *
     * @return {Boolean}
     */
    isDirty: function() {
        return !!this.getFields().findBy(function(f) {
            return f.isDirty();
        });
    },
    checkDirtyDelay: function() {
        var timer = this.taskDelay;
        if (timer) {
            this.checkDirtyTask.delay(timer);
        } else {
            this.checkDirty();
        }
    },
    /**
     * Check whether the dirty state of the entire form has changed since it was last checked, and
     * if so fire the {@link #dirtychange dirtychange} event. This is automatically invoked
     * when an individual field's `dirty` state changes.
     */
    checkDirty: function() {
        var me = this,
            dirty;
        if (me.destroyed) {
            return;
        }
        dirty = this.isDirty();
        if (dirty !== this.wasDirty) {
            this.fireEvent('dirtychange', this, dirty);
            this.wasDirty = dirty;
        }
    },
    /**
     * Returns `true` if the form contains a file upload field. This is used to determine the method
     * for submitting the form: File uploads are not performed using normal 'Ajax' techniques,
     * that is they are **not** performed using XMLHttpRequests. Instead a hidden `<form>` element
     * containing all the fields is created temporarily and submitted with its [target][1]
     * set to refer to a dynamically generated, hidden `<iframe>` which is inserted
     * into the document but removed after the return data has been gathered.
     *
     * The server response is parsed by the browser to create the document for the IFRAME.
     * If the server is using JSON to send the return object, then the [Content-Type][2] header
     * should be set to "text/plain" in order to tell the browser to insert the text unchanged
     * into a '<pre>' element in the document body from which it can be retrieved.
     *
     * If the [Content-Type][2] header is sent as the default, "text/html", then characters
     * which are significant to an HTML parser must be sent as HTML entities, so encode
     * `"<"` as `"&lt;"`, `"&"` as `"&amp;"` etc.
     *
     * The response text is retrieved from the document, and a fake XMLHttpRequest object is created
     * containing a responseText property in order to conform to the requirements of event handlers
     * and callbacks.
     *
     * Be aware that file upload packets are sent with the content type [multipart/form][3]
     * and some server technologies (notably JEE) may require some custom processing in order to
     * retrieve parameter names and parameter values from the packet content.
     *
     * [1]: http://www.w3.org/TR/REC-html40/present/frames.html#adef-target
     * [2]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17
     * [3]: http://www.faqs.org/rfcs/rfc2388.html
     *
     * @return {Boolean}
     */
    hasUpload: function() {
        return !!this.getFields().findBy(function(f) {
            return f.isFileUpload();
        });
    },
    /**
     * Performs a predefined action (an implementation of {@link Ext.form.action.Action})
     * to perform application-specific processing.
     *
     * @param {String/Ext.form.action.Action} action The name of the predefined action type,
     * or instance of {@link Ext.form.action.Action} to perform.
     *
     * @param {Object} [options] The options to pass to the {@link Ext.form.action.Action}
     * that will get created, if the action argument is a String.
     *
     * All of the config options listed below are supported by both the
     * {@link Ext.form.action.Submit submit} and {@link Ext.form.action.Load load} actions
     * unless otherwise noted (custom actions could also accept other config options):
     *
     * @param {String} options.url
     * The url for the action (defaults to the form's {@link #url}.)
     *
     * @param {String} options.method
     * The form method to use (defaults to the form's method, or POST if not defined)
     *
     * @param {String/Object} options.params
     * The params to pass (defaults to the form's baseParams, or none if not defined)
     *
     * Parameters are encoded as standard HTTP parameters using
     * {@link Ext#urlEncode Ext.Object.toQueryString}.
     *
     * @param {Object} options.headers
     * Request headers to set for the action.
     *
     * @param {Function} options.success
     * The callback that will be invoked after a successful response (see top of
     * {@link Ext.form.action.Submit submit} and {@link Ext.form.action.Load load}
     * for a description of what constitutes a successful response).
     * @param {Ext.form.Basic} options.success.form The form that requested the action.
     * @param {Ext.form.action.Action} options.success.action The Action object which performed
     * the operation.
     * The action object contains these properties of interest:
     *
     *  - {@link Ext.form.action.Action#response response}
     *  - {@link Ext.form.action.Action#result result} - interrogate for custom post-processing
     *  - {@link Ext.form.action.Action#type type}
     *
     * @param {Function} options.failure
     * The callback that will be invoked after a failed transaction attempt.
     * @param {Ext.form.Basic} options.failure.form The form that requested the action.
     * @param {Ext.form.action.Action} options.failure.action The Action object which performe
     * d the operation.
     * The action object contains these properties of interest:
     *
     * - {@link Ext.form.action.Action#failureType failureType}
     * - {@link Ext.form.action.Action#response response}
     * - {@link Ext.form.action.Action#result result} - interrogate for custom post-processing
     * - {@link Ext.form.action.Action#type type}
     *
     * @param {Object} options.scope
     * The scope in which to call the callback functions (The this reference for the callback
     * functions).
     *
     * @param {Boolean} options.clientValidation
     * Submit Action only. Determines whether a Form's fields are validated in a final call to
     * {@link Ext.form.Basic#isValid isValid} prior to submission. Set to false to prevent this.
     * If undefined, pre-submission field validation is performed.
     *
     * @return {Ext.form.Basic} this
     */
    doAction: function(action, options) {
        var me = this;
        if (Ext.isString(action)) {
            action = Ext.ClassManager.instantiateByAlias('formaction.' + action, Ext.apply({}, options, {
                form: me
            }));
        }
        if (me.fireEvent('beforeaction', me, action) !== false) {
            me.beforeAction(action);
            me.actionTimer = Ext.defer(action.run, 100, action);
        }
        return me;
    },
    /**
     * Shortcut to {@link #doAction do} a {@link Ext.form.action.Submit submit action}. This will
     * use the {@link Ext.form.action.Submit AJAX submit action} by default. If the
     * {@link #standardSubmit} config is enabled it will use a standard form element to submit,
     * or if the {@link #api} config is present it will use the
     * {@link Ext.form.action.DirectLoad Ext.direct.Direct submit action}.
     *
     * The following code:
     *
     *     myFormPanel.getForm().submit({
     *         clientValidation: true,
     *         url: 'updateConsignment.php',
     *         params: {
     *             newStatus: 'delivered'
     *         },
     *         success: function(form, action) {
     *            Ext.Msg.alert('Success', action.result.msg);
     *         },
     *         failure: function(form, action) {
     *             switch (action.failureType) {
     *                 case Ext.form.action.Action.CLIENT_INVALID:
     *                     Ext.Msg.alert(
     *                         'Failure',
     *                         'Form fields may not be submitted with invalid values'
     *                     );
     *                     break;
     *                 case Ext.form.action.Action.CONNECT_FAILURE:
     *                     Ext.Msg.alert('Failure', 'Ajax communication failed');
     *                     break;
     *                 case Ext.form.action.Action.SERVER_INVALID:
     *                    Ext.Msg.alert('Failure', action.result.msg);
     *            }
     *         }
     *     });
     *
     * would process the following server response for a successful submission:
     *
     *     {
     *         "success":true, // note this is Boolean, not string
     *         "msg":"Consignment updated"
     *     }
     *
     * and the following server response for a failed submission:
     *
     *     {
     *         "success":false, // note this is Boolean, not string
     *         "msg":"You do not have permission to perform this operation"
     *     }
     *
     * @param {Object} options The options to pass to the action (see {@link #doAction}
     * for details).
     * @return {Ext.form.Basic} this
     */
    submit: function(options) {
        var me = this,
            action;
        options = options || {};
        if (options.standardSubmit || me.standardSubmit) {
            action = 'standardsubmit';
        } else {
            action = me.api ? 'directsubmit' : 'submit';
        }
        return me.doAction(action, options);
    },
    /**
     * Shortcut to {@link #doAction do} a {@link Ext.form.action.Load load action}.
     * @param {Object} options The options to pass to the action (see {@link #doAction}
     * for details)
     * @return {Ext.form.Basic} this
     */
    load: function(options) {
        return this.doAction(this.api ? 'directload' : 'load', options);
    },
    /**
     * Persists the values in this form into the passed {@link Ext.data.Model} object
     * in a beginEdit/endEdit block. If the record is not specified, it will attempt to update
     * (if it exists) the record provided to loadRecord.
     * @param {Ext.data.Model} [record] The record to edit
     * @return {Ext.form.Basic} this
     */
    updateRecord: function(record) {
        record = record || this._record;
        if (!record) {
            Ext.raise("A record is required.");
            return this;
        }
        // eslint-disable-next-line vars-on-top
        var fields = record.self.fields,
            values = this.getFieldValues(),
            obj = {},
            i = 0,
            len = fields.length,
            name;
        for (; i < len; ++i) {
            name = fields[i].name;
            if (values.hasOwnProperty(name)) {
                obj[name] = values[name];
            }
        }
        record.beginEdit();
        record.set(obj);
        record.endEdit();
        return this;
    },
    /**
     * Loads an {@link Ext.data.Model} into this form by calling {@link #setValues} with the
     * {@link Ext.data.Model#getData record data}. The fields in the model are mapped to 
     * fields in the form by matching either the {@link Ext.form.field.Base#name} or
     * {@link Ext.Component#itemId}.  See also {@link #trackResetOnLoad}. 
     * @param {Ext.data.Model} record The record to load
     * @return {Ext.form.Basic} this
     */
    loadRecord: function(record) {
        this._record = record;
        return this.setValues(record.getData());
    },
    /**
     * Returns the last Ext.data.Model instance that was loaded via {@link #loadRecord}
     * @return {Ext.data.Model} The record
     */
    getRecord: function() {
        return this._record;
    },
    /**
     * @private
     * Called before an action is performed via {@link #doAction}.
     * @param {Ext.form.action.Action} action The Action instance that was invoked
     */
    beforeAction: function(action) {
        var me = this,
            waitMsg = action.waitMsg,
            maskCls = Ext.baseCSSPrefix + 'mask-loading',
            fields = me.getFields().items,
            f,
            fLen = fields.length,
            field, waitMsgTarget;
        // Call HtmlEditor's syncValue before actions
        for (f = 0; f < fLen; f++) {
            field = fields[f];
            if (field.isFormField && field.syncValue) {
                field.syncValue();
            }
        }
        if (waitMsg) {
            waitMsgTarget = me.waitMsgTarget;
            if (waitMsgTarget === true) {
                me.owner.el.mask(waitMsg, maskCls);
            } else if (waitMsgTarget) {
                waitMsgTarget = me.waitMsgTarget = Ext.get(waitMsgTarget);
                waitMsgTarget.mask(waitMsg, maskCls);
            } else {
                me.floatingAncestor = me.owner.up('[floating]');
                // https://sencha.jira.com/browse/EXTJSIV-6397
                // When the "wait" MessageBox is hidden, the ZIndexManager activates the previous
                // topmost floating item which would be any Window housing this form.
                // That kicks off a delayed focus call on that Window.
                // So if any form post submit processing displayed a MessageBox, that gets
                // stomped on.
                // The solution is to not move focus at all during this process.
                if (me.floatingAncestor) {
                    me.savePreventFocusOnActivate = me.floatingAncestor.preventFocusOnActivate;
                    me.floatingAncestor.preventFocusOnActivate = true;
                }
                Ext.MessageBox.wait(waitMsg, action.waitTitle || me.waitTitle);
            }
        }
    },
    /**
     * @private
     * Called after an action is performed via {@link #doAction}.
     * @param {Ext.form.action.Action} action The Action instance that was invoked
     * @param {Boolean} success True if the action completed successfully, false, otherwise.
     */
    afterAction: function(action, success) {
        var me = this,
            messageBox = Ext.MessageBox,
            waitMsgTarget;
        if (action.waitMsg) {
            waitMsgTarget = me.waitMsgTarget;
            if (waitMsgTarget === true) {
                me.owner.el.unmask();
            } else if (waitMsgTarget) {
                waitMsgTarget.unmask();
            } else {
                messageBox.hide();
            }
        }
        // Restore setting of any floating ancestor which was manipulated in beforeAction
        if (me.floatingAncestor) {
            me.floatingAncestor.preventFocusOnActivate = me.savePreventFocusOnActivate;
        }
        if (success) {
            if (action.reset) {
                me.reset();
            }
            Ext.callback(action.success, action.scope || action, [
                me,
                action
            ]);
            me.fireEvent('actioncomplete', me, action);
        } else {
            Ext.callback(action.failure, action.scope || action, [
                me,
                action
            ]);
            me.fireEvent('actionfailed', me, action);
        }
    },
    /**
     * Find a specific {@link Ext.form.field.Field} in this form by id or name.
     * @param {String} id The value to search for (specify either a {@link Ext.Component#id id} or
     * {@link Ext.form.field.Field#method!getName name} or hiddenName).
     * @return {Ext.form.field.Field} The first matching field, or `null` if none was found.
     */
    findField: function(id) {
        return this.getFields().findBy(function(f) {
            return f.id === id || f.name === id || f.dataIndex === id;
        });
    },
    /**
     * This method allows you to mark one or more fields in a form as invalid along with 
     * one or more invalid messages per field.
     * 
     *     var formPanel = Ext.create('Ext.form.Panel', {
     *         title: 'Contact Info',
     *         width: 300,
     *         bodyPadding: 10,
     *         renderTo: Ext.getBody(),
     *         items: [{
     *             xtype: 'textfield',
     *             name: 'name',
     *             id: 'nameId',
     *             fieldLabel: 'Name'
     *         }, {
     *             xtype: 'textfield',
     *             name: 'email',
     *             id: 'emailId',
     *             fieldLabel: 'Email Address'
     *         }],
     *         bbar: [{
     *             text: 'Mark both fields invalid',
     *             handler: function() {
     *                 formPanel.getForm().markInvalid([{
     *                     field: 'name',
     *                     message: 'Name invalid message'
     *                 }, {
     *                     field: 'email',
     *                     message: ['First invalid message', 'Second message']
     *                 }]);
     *             }
     *         }]
     *     });
     * 
     * **Note**: this method does not cause the Field's 
     * {@link Ext.form.field.Field#validate} or {@link Ext.form.field.Base#isValid} 
     * methods to return `false` if the value does _pass_ validation.  So simply marking 
     * a Field as invalid will not prevent submission of forms submitted with the 
     * {@link Ext.form.action.Submit#clientValidation} option set.
     * 
     * For additional information on how the fields are marked invalid see field's 
     * {@link Ext.form.field.Base#markInvalid markInvalid} method.
     * 
     * @param {Object/Object[]} errors
     * The errors param may be in one of two forms: Object[] or Object
     * 
     * - **Array:** An array of Objects with the following keys:
     *     - _field_ ({@link String}): The {@link Ext.form.field.Base#name name} or 
     * {@link Ext.form.field.Base#id id} of the form field to receive the error message
     *     - _message_ ({@link String}/{@link String}[]): The error message or an array 
     * of messages
     * 
     * Example Array syntax:
     * 
     *     form.markInvalid([{
     *         field: 'email', // the field name
     *         message: 'Error message'
     *     }]);
     * 
     * - **Object:** An Object hash with key/value pairs where the key is the field name 
     * or field ID and the value is the message or array of messages to display.
     * 
     * Example Object syntax:
     * 
     *     form.markInvalid({
     *         name: 'Err. message',
     *         emailId: ['Error1', 'Error 2']
     *     });
     * 
     * @return {Ext.form.Basic} basicForm The Ext.form.Basic instance
     */
    markInvalid: function(errors) {
        var me = this,
            e, eLen, error, value, key;
        function mark(fieldId, msg) {
            var field = me.findField(fieldId);
            if (field) {
                field.markInvalid(msg);
            }
        }
        if (Ext.isArray(errors)) {
            eLen = errors.length;
            for (e = 0; e < eLen; e++) {
                error = errors[e];
                mark(error.id || error.field, error.msg || error.message);
            }
        } else if (errors instanceof Ext.data.ErrorCollection) {
            eLen = errors.items.length;
            for (e = 0; e < eLen; e++) {
                error = errors.items[e];
                mark(error.field, error.message);
            }
        } else {
            for (key in errors) {
                if (errors.hasOwnProperty(key)) {
                    value = errors[key];
                    mark(key, value, errors);
                }
            }
        }
        return this;
    },
    /**
     * Set values for fields in this form in bulk.
     *
     * @param {Object/Object[]} values Either an array in the form:
     *
     *     [{id:'clientName', value:'Fred. Olsen Lines'},
     *      {id:'portOfLoading', value:'FXT'},
     *      {id:'portOfDischarge', value:'OSL'} ]
     *
     * or an object hash of the form:
     *
     *     {
     *         clientName: 'Fred. Olsen Lines',
     *         portOfLoading: 'FXT',
     *         portOfDischarge: 'OSL'
     *     }
     *
     * @return {Ext.form.Basic} this
     */
    setValues: function(values) {
        var me = this,
            v, vLen, val;
        function setVal(fieldId, val) {
            var field = me.findField(fieldId);
            if (field) {
                field.setValue(val);
                if (me.trackResetOnLoad) {
                    field.resetOriginalValue();
                }
            }
        }
        // Suspend here because setting the value on a field could trigger
        // a layout, for example if an error gets set, or it's a display field
        Ext.suspendLayouts();
        if (Ext.isArray(values)) {
            // array of objects
            vLen = values.length;
            for (v = 0; v < vLen; v++) {
                val = values[v];
                setVal(val.id, val.value);
            }
        } else {
            // object hash
            Ext.iterate(values, setVal);
        }
        Ext.resumeLayouts(true);
        return this;
    },
    /**
     * Retrieves the fields in the form as a set of key/value pairs, using their
     * {@link Ext.form.field.Field#getSubmitData getSubmitData()} method to collect the values.
     * If multiple fields return values under the same name those values will be combined
     * into an Array. This is similar to {@link Ext.form.Basic#getFieldValues getFieldValues}
     * except that this method collects only String values for submission, while getFieldValues
     * collects type-specific data values (e.g. Date objects for date fields.)
     *
     * @param {Boolean} [asString=false] If true, will return the key/value collection as a single
     * URL-encoded param string.
     * @param {Boolean} [dirtyOnly=false] If true, only fields that are dirty will be included
     * in the result.
     * @param {Boolean} [includeEmptyText=false] If true, the configured emptyText of empty fields
     * will be used.
     * @param {Boolean} [useDataValues=false] If true, the
     * {@link Ext.form.field.Field#getModelData getModelData}
     * method is used to retrieve values from fields, otherwise the
     * {@link Ext.form.field.Field#getSubmitData getSubmitData} method is used.
     * @param {Boolean} isSubmitting
     * @return {String/Object}
     */
    getValues: function(asString, dirtyOnly, includeEmptyText, useDataValues, isSubmitting) {
        var values = {},
            fields = this.getFields().items,
            fLen = fields.length,
            isArray = Ext.isArray,
            dataMethod = useDataValues ? 'getModelData' : 'getSubmitData',
            field, data, val, bucket, name, f;
        for (f = 0; f < fLen; f++) {
            field = fields[f];
            if (!dirtyOnly || field.isDirty()) {
                data = field[dataMethod](includeEmptyText, isSubmitting);
                if (Ext.isObject(data)) {
                    for (name in data) {
                        if (data.hasOwnProperty(name)) {
                            val = data[name];
                            if (includeEmptyText && val === '') {
                                val = field.emptyText || '';
                            }
                            if (!field.isRadio) {
                                if (values.hasOwnProperty(name)) {
                                    bucket = values[name];
                                    if (!isArray(bucket)) {
                                        bucket = values[name] = [
                                            bucket
                                        ];
                                    }
                                    if (isArray(val)) {
                                        values[name] = bucket.concat(val);
                                    } else {
                                        bucket.push(val);
                                    }
                                } else {
                                    values[name] = val;
                                }
                            } else {
                                values[name] = values[name] || val;
                            }
                        }
                    }
                }
            }
        }
        if (asString) {
            values = Ext.Object.toQueryString(values);
        }
        return values;
    },
    /**
     * Retrieves the fields in the form as a set of key/value pairs, using their
     * {@link Ext.form.field.Field#getModelData getModelData()} method to collect the values.
     * If multiple fields return values under the same name those values will be combined
     * into an Array. This is similar to {@link #getValues} except that this method collects
     * type-specific data values (e.g. Date objects for date fields) while getValues returns
     * only String values for submission.
     *
     * @param {Boolean} [dirtyOnly=false] If true, only fields that are dirty will be included
     * in the result.
     * @return {Object}
     */
    getFieldValues: function(dirtyOnly) {
        return this.getValues(false, dirtyOnly, false, true);
    },
    /**
     * Clears all invalid field messages in this form.
     * @return {Ext.form.Basic} this
     */
    clearInvalid: function() {
        Ext.suspendLayouts();
        // eslint-disable-next-line vars-on-top
        var me = this,
            fields = me.getFields().items,
            fLen = fields.length,
            f;
        for (f = 0; f < fLen; f++) {
            fields[f].clearInvalid();
        }
        Ext.resumeLayouts(true);
        return me;
    },
    /**
     * Resets all fields in this form. By default, any record bound by {@link #loadRecord}
     * will be retained.
     * @param {Boolean} [resetRecord=false] True to unbind any record set
     * by {@link #loadRecord}
     * @return {Ext.form.Basic} this
     */
    reset: function(resetRecord) {
        Ext.suspendLayouts();
        // eslint-disable-next-line vars-on-top
        var me = this,
            fields = me.getFields().items,
            fLen = fields.length,
            f;
        for (f = 0; f < fLen; f++) {
            fields[f].reset();
        }
        Ext.resumeLayouts(true);
        if (resetRecord === true) {
            delete me._record;
        }
        return me;
    },
    /**
     * Calls {@link Ext#apply Ext.apply} for all fields in this form with the passed object.
     * @param {Object} obj The object to be applied
     * @return {Ext.form.Basic} this
     */
    applyToFields: function(obj) {
        var fields = this.getFields().items,
            f,
            fLen = fields.length;
        for (f = 0; f < fLen; f++) {
            Ext.apply(fields[f], obj);
        }
        return this;
    },
    /**
     * Calls {@link Ext#applyIf Ext.applyIf} for all field in this form with the passed object.
     * @param {Object} obj The object to be applied
     * @return {Ext.form.Basic} this
     */
    applyIfToFields: function(obj) {
        var fields = this.getFields().items,
            f,
            fLen = fields.length;
        for (f = 0; f < fLen; f++) {
            Ext.applyIf(fields[f], obj);
        }
        return this;
    }
});

/**
 * @private
 */
Ext.define('Ext.layout.component.field.FieldContainer', {
    extend: 'Ext.layout.component.Auto',
    alias: 'layout.fieldcontainer',
    type: 'fieldcontainer',
    waitForOuterHeightInDom: true,
    waitForOuterWidthInDom: true,
    beginLayout: function(ownerContext) {
        var containerEl = this.owner.containerEl;
        this.callParent([
            ownerContext
        ]);
        // Tell Component.measureAutoDimensions to measure the DOM
        // when containerChildrenSizeDone is true
        ownerContext.hasRawContent = true;
        containerEl.setStyle('width', '');
        containerEl.setStyle('height', '');
        ownerContext.containerElContext = ownerContext.getEl('containerEl');
    },
    calculateOwnerHeightFromContentHeight: function(ownerContext, contentHeight) {
        var h = this.callParent([
                ownerContext,
                contentHeight
            ]);
        return h + this.getHeightAdjustment();
    },
    calculateOwnerWidthFromContentWidth: function(ownerContext, contentWidth) {
        var w = this.callParent([
                ownerContext,
                contentWidth
            ]);
        return w + this.getWidthAdjustment();
    },
    measureContentHeight: function(ownerContext) {
        // since we are measuring the outer el, we have to wait for whatever is in our
        // container to be flushed to the DOM... especially for things like box layouts
        // that size the innerCt since that is all that will contribute to our size!
        return ownerContext.hasDomProp('containerLayoutDone') ? this.callParent([
            ownerContext
        ]) : NaN;
    },
    measureContentWidth: function(ownerContext) {
        // see measureContentHeight
        return ownerContext.hasDomProp('containerLayoutDone') ? this.callParent([
            ownerContext
        ]) : NaN;
    },
    publishInnerHeight: function(ownerContext, height) {
        height -= this.getHeightAdjustment();
        ownerContext.containerElContext.setHeight(height);
    },
    publishInnerWidth: function(ownerContext, width) {
        width -= this.getWidthAdjustment();
        ownerContext.containerElContext.setWidth(width);
    },
    privates: {
        getHeightAdjustment: function() {
            var owner = this.owner,
                h = 0;
            if (owner.labelAlign === 'top' && owner.hasVisibleLabel()) {
                h += owner.labelEl.getHeight();
            }
            if (owner.msgTarget === 'under' && owner.hasActiveError()) {
                h += owner.errorWrapEl.getHeight();
            }
            return h + owner.bodyEl.getPadding('tb');
        },
        getWidthAdjustment: function() {
            var owner = this.owner,
                w = 0;
            if (owner.labelAlign !== 'top' && owner.hasVisibleLabel()) {
                w += (owner.labelWidth + (owner.labelPad || 0));
            }
            if (owner.msgTarget === 'side' && owner.hasActiveError()) {
                w += owner.errorWrapEl.getWidth();
            }
            return w + owner.bodyEl.getPadding('lr');
        }
    }
});

/**
 * A mixin for {@link Ext.container.Container} components that are likely to have form fields
 * in their items subtree. Adds the following capabilities:
 *
 * - Methods for handling the addition and removal of {@link Ext.form.Labelable} and
 *   {@link Ext.form.field.Field} instances at any depth within the container.
 * - Events ({@link #fieldvaliditychange} and {@link #fielderrorchange}) for handling changes
 *   to the state of individual fields at the container level.
 * - Automatic application of {@link #fieldDefaults} config properties to each field added
 *   within the container, to facilitate uniform configuration of all fields.
 *
 * This mixin is primarily for internal use by {@link Ext.form.Panel} and
 * {@link Ext.form.FieldContainer}, and should not normally need to be used directly.
 */
Ext.define('Ext.form.FieldAncestor', {
    extend: 'Ext.Mixin',
    requires: [
        'Ext.container.Monitor'
    ],
    mixinConfig: {
        id: 'fieldAncestor',
        after: {
            initInheritedState: 'initFieldInheritedState'
        },
        before: {
            doDestroy: 'onBeforeDestroy'
        }
    },
    /**
     * @cfg {Object} fieldDefaults
     * If specified, the properties in this object are used as default config values for each
     * {@link Ext.form.Labelable} instance (e.g. {@link Ext.form.field.Base} or
     * {@link Ext.form.FieldContainer}) that is added as a descendant of this container.
     * Corresponding values specified in an individual field's own configuration, or from the
     * {@link Ext.container.Container#defaults defaults config} of its parent container,
     * will take precedence. See the documentation for {@link Ext.form.Labelable} to see
     * what config options may be specified in the fieldDefaults.
     *
     * Example:
     *
     *     new Ext.form.Panel({
     *         fieldDefaults: {
     *             labelAlign: 'left',
     *             labelWidth: 100
     *         },
     *         items: [{
     *             xtype: 'fieldset',
     *             defaults: {
     *                 labelAlign: 'top'
     *             },
     *             items: [{
     *                 name: 'field1'
     *             }, {
     *                 name: 'field2'
     *             }]
     *         }, {
     *             xtype: 'fieldset',
     *             items: [{
     *                 name: 'field3',
     *                 labelWidth: 150
     *             }, {
     *                 name: 'field4'
     *             }]
     *         }]
     *     });
     *
     * In this example, field1 and field2 will get labelAlign: 'top' (from the fieldset's defaults)
     * and labelWidth: 100 (from fieldDefaults), field3 and field4 will both get labelAlign: 'left'
     * (from fieldDefaults and field3 will use the labelWidth: 150 from its own config.
     */
    /**
     * @event fieldvaliditychange
     * Fires when the validity state of any one of the {@link Ext.form.field.Field} instances
     * within this container changes.
     * @param {Ext.form.FieldAncestor} this
     * @param {Ext.form.Labelable} field The Field instance whose validity changed
     * @param {String} isValid The field's new validity state
     */
    /**
     * @event fielderrorchange
     * Fires when the active error message is changed for any one of the {@link Ext.form.Labelable}
     * instances within this container.
     * @param {Ext.form.FieldAncestor} this
     * @param {Ext.form.Labelable} field The Labelable instance whose active error was changed
     * @param {String} error The active error message
     */
    /**
     * Initializes the FieldAncestor's state; this must be called from the initComponent method
     * of any components importing this mixin.
     * @protected
     */
    initFieldAncestor: function() {
        var me = this;
        // We use the monitor here as opposed to event bubbling. The problem with bubbling
        // is it doesn't let us react to items being added/remove at different places
        // in the hierarchy which may have an impact on the error/valid state.
        me.monitor = new Ext.container.Monitor({
            scope: me,
            selector: '[isFormField]:not([excludeForm])',
            addHandler: me.onChildFieldAdd,
            removeHandler: me.onChildFieldRemove
        });
        me.initFieldDefaults();
    },
    initMonitor: function() {
        this.monitor.bind(this);
    },
    initFieldInheritedState: function(inheritedState) {
        var inheritedFieldDefaults = inheritedState.fieldDefaults,
            fieldDefaults = this.fieldDefaults;
        if (fieldDefaults) {
            if (inheritedFieldDefaults) {
                inheritedState.fieldDefaults = Ext.apply(Ext.Object.chain(inheritedFieldDefaults), fieldDefaults);
            } else {
                inheritedState.fieldDefaults = fieldDefaults;
            }
        }
    },
    onChildFieldAdd: function(field) {
        var me = this;
        me.mon(field, 'errorchange', me.handleFieldErrorChange, me);
        me.mon(field, 'validitychange', me.handleFieldValidityChange, me);
    },
    onChildFieldRemove: function(field) {
        var me = this;
        me.mun(field, 'errorchange', me.handleFieldErrorChange, me);
        me.mun(field, 'validitychange', me.handleFieldValidityChange, me);
    },
    /**
     * @private
     * Initialize the {@link #fieldDefaults} object
     */
    initFieldDefaults: function() {
        if (!this.fieldDefaults) {
            this.fieldDefaults = {};
        }
    },
    /**
     * @private
     * Handle bubbled validitychange events from descendants; invoke the aggregated event and method
     */
    handleFieldValidityChange: function(field, isValid) {
        var me = this;
        if (field !== me) {
            me.fireEvent('fieldvaliditychange', me, field, isValid);
            me.onFieldValidityChange(field, isValid);
        }
    },
    /**
     * @private
     * Handle bubbled errorchange events from descendants; invoke the aggregated event and method
     */
    handleFieldErrorChange: function(labelable, activeError) {
        var me = this;
        if (labelable !== me) {
            me.fireEvent('fielderrorchange', me, labelable, activeError);
            me.onFieldErrorChange(labelable, activeError);
        }
    },
    /**
     * @method
     * Fired when the validity of any field within the container changes.
     * @param {Ext.form.field.Field} field The sub-field whose validity changed
     * @param {Boolean} valid The new validity state
     * @protected
     */
    onFieldValidityChange: Ext.emptyFn,
    /**
     * @method
     * Fired when the error message of any field within the container changes.
     * @param {Ext.form.Labelable} field The sub-field whose active error changed
     * @param {String} error The new active error message
     * @protected
     */
    onFieldErrorChange: Ext.emptyFn,
    onBeforeDestroy: function() {
        this.monitor = Ext.destroy(this.monitor);
    }
});

/**
 * FieldContainer is a derivation of {@link Ext.container.Container Container} that implements the
 * {@link Ext.form.Labelable Labelable} mixin. This allows it to be configured so that it is
 * rendered with a {@link #fieldLabel field label} and optional {@link #msgTarget error message}
 * around its sub-items. This is useful for arranging a group of fields within a single item
 * in a form, so that it lines up nicely with other fields. A common use is for grouping a set
 * of related fields under a single label in a form.
 * 
 * The container's configured {@link #cfg-items} will be layed out within the field body area
 * according to the configured {@link #layout} type. The default layout is `'autocontainer'`.
 * 
 * Like regular fields, FieldContainer can inherit its decoration configuration from the
 * {@link Ext.form.Panel#fieldDefaults fieldDefaults} of an enclosing FormPanel. In addition,
 * FieldContainer itself can pass {@link #fieldDefaults} to any {@link Ext.form.Labelable fields}
 * it may itself contain.
 * 
 * If you are grouping a set of {@link Ext.form.field.Checkbox Checkbox} or
 * {@link Ext.form.field.Radio Radio} fields in a single labeled container, consider using
 * a {@link Ext.form.CheckboxGroup} or {@link Ext.form.RadioGroup} instead as they are specialized
 * for handling those types.
 *
 * # Example
 * 
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'FieldContainer Example',
 *         width: 550,
 *         bodyPadding: 10,
 * 
 *         items: [{
 *             xtype: 'fieldcontainer',
 *             fieldLabel: 'Last Three Jobs',
 *             labelWidth: 100,
 * 
 *             // The body area will contain three text fields, arranged
 *             // horizontally, separated by draggable splitters.
 *             layout: 'hbox',
 *             items: [{
 *                 xtype: 'textfield',
 *                 flex: 1
 *             }, {
 *                 xtype: 'splitter'
 *             }, {
 *                 xtype: 'textfield',
 *                 flex: 1
 *             }, {
 *                 xtype: 'splitter'
 *             }, {
 *                 xtype: 'textfield',
 *                 flex: 1
 *             }]
 *         }],
 *         renderTo: Ext.getBody()
 *     });
 * 
 * # Usage of fieldDefaults
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'FieldContainer Example',
 *         width: 350,
 *         bodyPadding: 10,
 * 
 *         items: [{
 *             xtype: 'fieldcontainer',
 *             fieldLabel: 'Your Name',
 *             labelWidth: 75,
 *             defaultType: 'textfield',
 * 
 *             // Arrange fields vertically, stretched to full width
 *             layout: 'anchor',
 *             defaults: {
 *                 layout: '100%'
 *             },
 * 
 *             // These config values will be applied to both sub-fields, except
 *             // for Last Name which will use its own msgTarget.
 *             fieldDefaults: {
 *                 msgTarget: 'under',
 *                 labelAlign: 'top'
 *             },
 * 
 *             items: [{
 *                 fieldLabel: 'First Name',
 *                 name: 'firstName'
 *             }, {
 *                 fieldLabel: 'Last Name',
 *                 name: 'lastName',
 *                 msgTarget: 'under'
 *             }]
 *         }],
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.form.FieldContainer', {
    extend: 'Ext.container.Container',
    alias: 'widget.fieldcontainer',
    requires: [
        'Ext.layout.component.field.FieldContainer'
    ],
    mixins: {
        labelable: 'Ext.form.Labelable',
        fieldAncestor: 'Ext.form.FieldAncestor'
    },
    componentLayout: 'fieldcontainer',
    componentCls: Ext.baseCSSPrefix + 'form-fieldcontainer',
    shrinkWrap: true,
    autoEl: {
        tag: 'div',
        role: 'presentation'
    },
    childEls: [
        'containerEl'
    ],
    /**
     * @cfg {Boolean} combineLabels
     * If set to true, and there is no defined {@link #fieldLabel}, the field container will
     * automatically generate its label by combining the labels of all the fields it contains.
     * Defaults to false.
     */
    combineLabels: false,
    /**
     * @cfg {String} labelConnector
     * The string to use when joining the labels of individual sub-fields, when
     * {@link #combineLabels} is set to true. Defaults to ', '.
     * @locale
     */
    labelConnector: ', ',
    /**
     * @cfg {Boolean} combineErrors
     * If set to true, the field container will automatically combine and display the validation
     * errors from all the fields it contains as a single error on the container, according to the
     * configured {@link #msgTarget}. Defaults to false.
     */
    combineErrors: false,
    maskOnDisable: false,
    // If we allow this to mark with the invalidCls it will cascade to all
    // child fields, let them handle themselves
    invalidCls: '',
    /* eslint-disable indent */
    fieldSubTpl: [
        '<div id="{id}-containerEl" data-ref="containerEl" class="{containerElCls}"',
        '<tpl if="ariaAttributes">',
        '<tpl foreach="ariaAttributes"> {$}="{.}"</tpl>',
        '<tpl else>',
        ' role="presentation"',
        '</tpl>',
        '>',
        '{%this.renderContainer(out,values)%}',
        '</div>'
    ],
    /* eslint-enable indent */
    initComponent: function() {
        var me = this;
        // Init mixins
        me.initLabelable();
        me.initFieldAncestor();
        me.callParent();
        me.initMonitor();
    },
    onAdd: function(labelItem) {
        var me = this;
        // Fix for https://sencha.jira.com/browse/EXTJSIV-6424 Which was *sneakily* fixed
        // in version 37
        // In FF < 37, positioning absolutely within a TD positions relative to the TR!
        // So we must add the width of a visible, left-aligned label cell to the x coordinate.
        if (labelItem.isLabelable && Ext.isGecko && Ext.firefoxVersion < 37 && me.layout.type === 'absolute' && !me.hideLabel && me.labelAlign !== 'top') {
            labelItem.x += (me.labelWidth + me.labelPad);
        }
        me.callParent(arguments);
        if (labelItem.isLabelable && me.combineLabels) {
            labelItem.oldHideLabel = labelItem.hideLabel;
            labelItem.hideLabel = true;
        }
        me.updateLabel();
    },
    onRemove: function(labelItem, isDestroying) {
        var me = this;
        me.callParent(arguments);
        if (!isDestroying) {
            if (labelItem.isLabelable && me.combineLabels) {
                labelItem.hideLabel = labelItem.oldHideLabel;
            }
            me.updateLabel();
        }
    },
    initRenderData: function() {
        var me = this,
            data = me.callParent();
        data.containerElCls = me.containerElCls;
        data = Ext.applyIf(data, me.getLabelableRenderData());
        if (me.labelAlign === 'top' || me.msgTarget === 'under') {
            data.extraFieldBodyCls += ' ' + Ext.baseCSSPrefix + 'field-container-body-vertical';
        }
        data.tipAnchorTarget = me.id + '-containerEl';
        return data;
    },
    /**
     * Returns the combined field label if {@link #combineLabels} is set to true and if there is no
     * set {@link #fieldLabel}. Otherwise returns the fieldLabel like normal. You can also override
     * this method to provide a custom generated label.
     * @template
     * @return {String} The label, or empty string if none.
     */
    getFieldLabel: function() {
        var label = this.fieldLabel || '';
        if (!label && this.combineLabels) {
            label = Ext.Array.map(this.query('[isFieldLabelable]'), function(field) {
                return field.getFieldLabel();
            }).join(this.labelConnector);
        }
        return label;
    },
    getSubTplData: function() {
        var ret = this.initRenderData();
        Ext.apply(ret, this.subTplData);
        return ret;
    },
    getSubTplMarkup: function(fieldData) {
        var me = this,
            tpl = me.lookupTpl('fieldSubTpl'),
            html;
        if (!tpl.renderContent) {
            me.setupRenderTpl(tpl);
        }
        html = tpl.apply(me.getSubTplData(fieldData));
        return html;
    },
    /**
     * @private
     * Updates the content of the labelEl if it is rendered
     */
    updateLabel: function() {
        var me = this,
            label = me.labelEl;
        if (label) {
            me.setFieldLabel(me.getFieldLabel());
        }
    },
    /**
     * @private
     * Fired when the error message of any field within the container changes, and updates the
     * combined error message to match.
     */
    onFieldErrorChange: function() {
        if (this.combineErrors) {
            // eslint-disable-next-line vars-on-top
            var me = this,
                oldError = me.getActiveError(),
                invalidFields = Ext.Array.filter(me.query('[isFormField]'), function(field) {
                    return field.hasActiveError();
                }),
                newErrors = me.getCombinedErrors(invalidFields);
            if (newErrors) {
                me.setActiveErrors(newErrors);
            } else {
                me.unsetActiveError();
            }
            if (oldError !== me.getActiveError()) {
                me.updateLayout();
            }
        }
    },
    /**
     * Takes an Array of invalid {@link Ext.form.field.Field} objects and builds a combined list
     * of error messages from them. Defaults to prepending each message by the field name
     * and a colon. This can be overridden to provide custom combined error message handling,
     * for instance changing the format of each message or sorting the array (it is sorted
     * in order of appearance by default).
     * @param {Ext.form.field.Field[]} invalidFields An Array of the sub-fields which are currently
     * invalid.
     * @return {String[]} The combined list of error messages
     */
    getCombinedErrors: function(invalidFields) {
        var errors = [],
            fLen = invalidFields.length,
            f, field, activeErrors, a, aLen, error, label;
        for (f = 0; f < fLen; f++) {
            field = invalidFields[f];
            activeErrors = field.getActiveErrors();
            aLen = activeErrors.length;
            for (a = 0; a < aLen; a++) {
                error = activeErrors[a];
                label = field.getFieldLabel();
                errors.push((label ? label + ': ' : '') + error);
            }
        }
        return errors;
    },
    privates: {
        applyTargetCls: function(targetCls) {
            var containerElCls = this.containerElCls;
            this.containerElCls = containerElCls ? containerElCls + ' ' + targetCls : targetCls;
        },
        getTargetEl: function() {
            return this.containerEl;
        },
        initRenderTpl: function() {
            var me = this;
            if (!me.hasOwnProperty('renderTpl')) {
                me.renderTpl = me.lookupTpl('labelableRenderTpl');
            }
            return me.callParent();
        }
    }
});

/**
 * This layout implements the column arrangement for {@link Ext.form.CheckboxGroup} and
 * {@link Ext.form.RadioGroup}. It groups the component's sub-items into columns
 * based on the component's {@link Ext.form.CheckboxGroup#columns columns} and
 * {@link Ext.form.CheckboxGroup#vertical} config properties.
 */
Ext.define('Ext.layout.container.CheckboxGroup', {
    extend: 'Ext.layout.container.Container',
    alias: [
        'layout.checkboxgroup'
    ],
    /**
     * @cfg {Boolean} [autoFlex=true]
     * By default,  CheckboxGroup allocates all available space to the configured columns
     * meaning that column are evenly spaced across the container.
     *
     * To have each column only be wide enough to fit the container Checkboxes (or Radios),
     * set `autoFlex` to `false`
     */
    autoFlex: true,
    type: 'checkboxgroup',
    createsInnerCt: true,
    childEls: [
        'innerCt'
    ],
    /* eslint-disable indent, max-len */
    renderTpl: '<table id="{ownerId}-innerCt" data-ref="innerCt" class="' + Ext.baseCSSPrefix + 'table-plain" cellpadding="0"' + 'role="presentation" style="{tableStyle}">' + '<tbody role="presentation">' + '<tr role="presentation">' + '<tpl for="columns">' + '<td class="{parent.colCls}" valign="top" style="{style}" role="presentation">' + '{% this.renderColumn(out,parent,xindex-1) %}' + '</td>' + '</tpl>' + '</tr>' + '</tbody>' + '</table>',
    /* eslint-enable indent, max-len */
    lastOwnerItemsGeneration: null,
    initLayout: function() {
        var me = this,
            owner = me.owner;
        me.columnsArray = Ext.isArray(owner.columns);
        me.autoColumns = !owner.columns || owner.columns === 'auto';
        // Auto layout is always horizontal
        if (!me.autoColumns) {
            // ... but one column is always vertical
            me.vertical = owner.vertical || (owner.columns === 1 || owner.columns.length === 1);
        }
        me.callParent();
    },
    beginLayout: function(ownerContext) {
        var me = this,
            autoFlex = me.autoFlex,
            innerCtStyle = me.innerCt.dom.style,
            totalFlex = 0,
            flexedCols = 0,
            columns, numCols, i, width, cwidth;
        me.callParent(arguments);
        columns = me.rowNodes[0].children;
        ownerContext.innerCtContext = ownerContext.getEl('innerCt', me);
        // The columns config may be an array of widths. Any value < 1 is taken to be a fraction:
        if (!ownerContext.widthModel.shrinkWrap) {
            numCols = columns.length;
            // If columns is an array of numeric widths
            if (me.columnsArray) {
                // first calculate total flex
                for (i = 0; i < numCols; i++) {
                    width = me.owner.columns[i];
                    if (width < 1) {
                        totalFlex += width;
                        flexedCols++;
                    }
                }
                // now apply widths
                for (i = 0; i < numCols; i++) {
                    width = me.owner.columns[i];
                    if (width < 1) {
                        cwidth = ((width / totalFlex) * 100) + '%';
                    } else {
                        cwidth = width + 'px';
                    }
                    columns[i].style.width = cwidth;
                }
            } else // Otherwise it's the *number* of columns, so distributed the widths evenly
            {
                for (i = 0; i < numCols; i++) {
                    // autoFlex: true will automatically calculate % widths
                    // autoFlex: false allows the table to decide (shrinkWrap, in effect)
                    // on a per-column basis
                    cwidth = autoFlex ? (1 / numCols * 100) + '%' : '';
                    columns[i].style.width = cwidth;
                    flexedCols++;
                }
            }
            // no flexed cols -- all widths are fixed
            if (!flexedCols) {
                innerCtStyle.tableLayout = 'fixed';
                innerCtStyle.width = '';
            }
            // some flexed cols -- need to fix some
            else if (flexedCols < numCols) {
                innerCtStyle.tableLayout = 'fixed';
                innerCtStyle.width = '100%';
            } else // let the table decide
            {
                innerCtStyle.tableLayout = 'auto';
                // if autoFlex, fill available space, else compact down
                if (autoFlex) {
                    innerCtStyle.width = '100%';
                } else {
                    innerCtStyle.width = '';
                }
            }
        } else {
            innerCtStyle.tableLayout = 'auto';
            innerCtStyle.width = '';
        }
    },
    cacheElements: function() {
        var me = this;
        // Grab defined childEls
        me.callParent();
        me.rowNodes = me.innerCt.query('tr', true);
        // There always should be at least one row
        me.tBodyNode = me.rowNodes[0].parentNode;
    },
    /*
     * Just wait for the child items to all lay themselves out in the width we are configured
     * to make available to them. Then we can measure our height.
     */
    calculate: function(ownerContext) {
        var me = this,
            targetContext, widthShrinkWrap, heightShrinkWrap, shrinkWrap, table, targetPadding;
        // The column nodes are widthed using their own width attributes, we just need to wait
        // for all children to have arranged themselves in that width, and then collect our height.
        if (!ownerContext.getDomProp('containerChildrenSizeDone')) {
            me.done = false;
        } else {
            targetContext = ownerContext.innerCtContext;
            widthShrinkWrap = ownerContext.widthModel.shrinkWrap;
            heightShrinkWrap = ownerContext.heightModel.shrinkWrap;
            shrinkWrap = heightShrinkWrap || widthShrinkWrap;
            table = targetContext.el.dom;
            targetPadding = shrinkWrap && targetContext.getPaddingInfo();
            if (widthShrinkWrap) {
                ownerContext.setContentWidth(table.offsetWidth + targetPadding.width, true);
            }
            if (heightShrinkWrap) {
                ownerContext.setContentHeight(table.offsetHeight + targetPadding.height, true);
            }
        }
    },
    doRenderColumn: function(out, renderData, columnIndex) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!
        var me = renderData.$layout,
            owner = me.owner,
            columnCount = renderData.columnCount,
            items = owner.items.items,
            itemCount = items.length,
            item, itemIndex, rowCount, increment, tree;
        // Example:
        //      columnCount = 3
        //      items.length = 10
        if (owner.vertical) {
            //    For vertical layouts we're using only one row
            //    with items rendered "vertically" into table cells.
            //    This is to ensure proper DOM order for native
            //    keyboard navigation.
            //
            //        0   1   2
            //      +---+---+---+
            //    0 | 0 | 4 | 8 |
            //    1 | 1 | 5 | 9 |
            //    2 | 2 | 6 |   |
            //    3 | 3 | 7 |   |
            //      +---+---+---+
            rowCount = Math.ceil(itemCount / columnCount);
            // = 4
            itemIndex = columnIndex * rowCount;
            itemCount = Math.min(itemCount, itemIndex + rowCount);
            increment = 1;
        } else {
            //    For horizontal layouts we're using table with rows
            //    and cells, each cell holding one item.
            //
            //        0   1   2
            //      +---+---+---+
            //    0 | 0 | 1 | 2 |
            //      +---+---+---+
            //    1 | 3 | 4 | 5 |
            //      +---+---+---+
            //    2 | 6 | 7 | 8 |
            //      +---+---+---+
            //    3 | 9 |   |   |
            //      +---+---+---+
            itemIndex = columnIndex;
            increment = columnCount;
        }
        for (; itemIndex < itemCount; itemIndex += increment) {
            item = items[itemIndex];
            me.configureItem(item);
            tree = item.getRenderTree();
            Ext.DomHelper.generateMarkup(tree, out);
        }
    },
    /**
     * Returns the number of columns in the checkbox group.
     * @private
     */
    getColumnCount: function() {
        var me = this,
            owner = me.owner,
            ownerColumns = owner.columns;
        // Our columns config is an array of numeric widths.
        // Calculate our total width
        if (me.columnsArray) {
            return ownerColumns.length;
        }
        if (Ext.isNumber(ownerColumns)) {
            return ownerColumns;
        }
        return owner.items.length;
    },
    getItemSizePolicy: function(item) {
        return this.autoSizePolicy;
    },
    getRenderData: function() {
        var me = this,
            data = me.callParent(),
            owner = me.owner,
            columns = me.getColumnCount(),
            autoFlex = me.autoFlex,
            totalFlex = 0,
            flexedCols = 0,
            width, column, cwidth, i;
        // calculate total flex
        if (me.columnsArray) {
            for (i = 0; i < columns; i++) {
                width = me.owner.columns[i];
                if (width < 1) {
                    totalFlex += width;
                    flexedCols++;
                }
            }
        }
        data.colCls = owner.groupCls;
        data.columnCount = columns;
        data.columns = [];
        for (i = 0; i < columns; i++) {
            column = (data.columns[i] = {});
            if (me.columnsArray) {
                width = me.owner.columns[i];
                if (width < 1) {
                    cwidth = ((width / totalFlex) * 100) + '%';
                } else {
                    cwidth = width + 'px';
                }
                column.style = 'width:' + cwidth;
            } else {
                column.style = 'width:' + (1 / columns * 100) + '%';
                flexedCols++;
            }
        }
        /* eslint-disable indent, multiline-ternary, no-multi-spaces */
        // If the columns config was an array of column widths, allow table to auto width
        data.tableStyle = !flexedCols ? 'table-layout:fixed;' : (flexedCols < columns) ? 'table-layout:fixed;width:100%' : (autoFlex) ? 'table-layout:auto;width:100%' : 'table-layout:auto;';
        /* eslint-enable indent, multiline-ternary, no-multi-spaces */
        return data;
    },
    // Always valid. beginLayout ensures the encapsulating elements of all children
    // are in the correct place
    isValidParent: Ext.returnTrue,
    setupRenderTpl: function(renderTpl) {
        this.callParent(arguments);
        renderTpl.renderColumn = this.doRenderColumn;
    },
    renderChildren: function() {
        var me = this,
            generation = me.owner.items.generation;
        if (me.lastOwnerItemsGeneration !== generation) {
            me.lastOwnerItemsGeneration = generation;
            me.renderItems(me.getLayoutItems());
        }
    },
    /**
     * Iterates over all passed items, ensuring they are rendered.  If the items
     * are already rendered, also determines if the items are in the proper place in the dom.
     * @protected
     */
    renderItems: function(items) {
        var me = this,
            itemCount = items.length,
            item, rowCount, columnCount, rowIndex, columnIndex, i;
        if (itemCount) {
            Ext.suspendLayouts();
            // We operate on "virtual" row and column counts here, which is the same
            // as the actual DOM structure for horizontal layouts but is quite different
            // for vertical layouts.
            if (me.autoColumns) {
                columnCount = itemCount;
                rowCount = 1;
            } else {
                columnCount = me.columnsArray ? me.owner.columns.length : me.owner.columns;
                rowCount = Math.ceil(itemCount / columnCount);
            }
            for (i = 0; i < itemCount; i++) {
                item = items[i];
                rowIndex = me.getRenderRowIndex(i, rowCount, columnCount);
                columnIndex = me.getRenderColumnIndex(i, rowCount, columnCount);
                if (!item.rendered) {
                    me.renderItem(item, rowIndex, columnIndex);
                } else if (!me.isItemAtPosition(item, rowIndex, columnIndex)) {
                    me.moveItem(item, rowIndex, columnIndex);
                }
            }
            me.pruneRows(rowCount, columnCount);
            Ext.resumeLayouts(true);
        }
    },
    isItemAtPosition: function(item, rowIndex, columnIndex) {
        return item.el.dom === this.getItemNodeAt(rowIndex, columnIndex);
    },
    getRenderColumnIndex: function(itemIndex, rowCount, columnCount) {
        if (this.vertical) {
            return Math.floor(itemIndex / rowCount);
        } else {
            return itemIndex % columnCount;
        }
    },
    getRenderRowIndex: function(itemIndex, rowCount, columnCount) {
        if (this.vertical) {
            return itemIndex % rowCount;
        } else {
            return Math.floor(itemIndex / columnCount);
        }
    },
    getItemNodeAt: function(rowIndex, columnIndex) {
        var column = this.getColumnNodeAt(rowIndex, columnIndex);
        return this.vertical ? column.children[rowIndex] : column.children[0];
    },
    getRowNodeAt: function(rowIndex) {
        var me = this,
            row;
        // Vertical layout uses only one row with several columns,
        // each column containing one or more items, thus simulating "rows"
        rowIndex = me.vertical ? 0 : rowIndex;
        row = me.rowNodes[rowIndex];
        if (!row) {
            row = me.rowNodes[rowIndex] = document.createElement('tr');
            row.role = 'presentation';
            me.tBodyNode.appendChild(row);
        }
        return row;
    },
    getColumnNodeAt: function(rowIndex, columnIndex, row) {
        var column;
        row = row || this.getRowNodeAt(rowIndex);
        column = row.children[columnIndex];
        if (!column) {
            column = Ext.fly(row).appendChild({
                tag: 'td',
                cls: this.owner.groupCls,
                vAlign: 'top',
                role: 'presentation'
            }, true);
        }
        return column;
    },
    pruneRows: function(rowCount, columnCount) {
        var me = this,
            rows = me.tBodyNode.children,
            columns, row, column, i, j;
        rowCount = me.vertical ? 1 : rowCount;
        while (rows.length > rowCount) {
            row = rows[rows.length - 1];
            while (row.children.length) {
                Ext.get(row.children[0]).destroy();
            }
            // We don't create Element instances for rows
            row.parentNode.removeChild(row);
        }
        for (i = rowCount - 1; i >= 0; i--) {
            row = rows[i];
            columns = row.children;
            while (columns.length > columnCount) {
                column = columns[columns.length - 1];
                Ext.get(column).destroy();
            }
            // We only prune empty cells on 2nd and subsequent rows;
            // the first row needs to have all cells up to columnCount
            // to establish the structure.
            if (i > 0) {
                for (j = columns.length - 1; j >= 0; j--) {
                    column = columns[j];
                    // We only need to test for the last cells that can be empty
                    // due to item removal. As soon as we reach a non-empty column
                    // there's no point in continuing the loop.
                    if (column.children.length === 0) {
                        Ext.get(column).destroy();
                    } else {
                        break;
                    }
                }
            }
        }
    },
    /**
     * Renders the given Component into the specified row and column
     * @param {Ext.Component} item The Component to render
     * @param {number} rowIndex row index
     * @param {number} columnIndex column index
     * @private
     */
    renderItem: function(item, rowIndex, columnIndex) {
        var me = this,
            column, itemIndex;
        me.configureItem(item);
        itemIndex = me.vertical ? rowIndex : 0;
        column = Ext.get(me.getColumnNodeAt(rowIndex, columnIndex));
        item.render(column, itemIndex);
    },
    /**
     * Moves the given already rendered Component to the specified row and column
     * @param {Ext.Component} item The Component to move
     * @param {number} rowIndex row index
     * @param {number} columnIndex column index
     * @private
     */
    moveItem: function(item, rowIndex, columnIndex) {
        var me = this,
            column, itemIndex, targetNode;
        itemIndex = me.vertical ? rowIndex : 0;
        column = me.getColumnNodeAt(rowIndex, columnIndex);
        targetNode = column.children[itemIndex];
        column.insertBefore(item.el.dom, targetNode || null);
    },
    destroy: function() {
        if (this.owner.rendered) {
            // eslint-disable-next-line vars-on-top
            var target = this.getRenderTarget(),
                cells, i, len;
            if (target) {
                cells = target.query('.' + this.owner.groupCls, false);
                for (i = 0 , len = cells.length; i < len; i++) {
                    cells[i].destroy();
                }
            }
        }
        this.callParent();
    }
});

/**
 * @private
 * Private utility class for managing all {@link Ext.form.field.Checkbox} fields grouped by name.
 */
Ext.define('Ext.form.CheckboxManager', {
    extend: 'Ext.util.MixedCollection',
    singleton: true,
    getByName: function(name, formId) {
        return this.filterBy(function(item) {
            return item.name === name && item.getFormId() === formId;
        });
    }
});

/**
 * Single checkbox field. Can be used as a direct replacement for traditional checkbox fields.
 * Also serves as a parent class for {@link Ext.form.field.Radio radio buttons}.
 *
 * ## Labeling
 *
 * In addition to the {@link Ext.form.Labelable standard field labeling options}, checkboxes
 * may be given an optional {@link #boxLabel} which will be displayed immediately after checkbox.
 * Also see {@link Ext.form.CheckboxGroup} for a convenient method of grouping related checkboxes.
 *
 * # Values
 *
 * The main value of a checkbox is a boolean, indicating whether or not the checkbox is checked.
 * The following values will check the checkbox:
 *
 * - `true`
 * - `'true'`
 * - `'1'`
 * - `'on'`
 *
 * Any other value will un-check the checkbox.
 *
 * In addition to the main boolean value, you may also specify a separate {@link #inputValue}.
 * This will be sent as the parameter value when the form is
 * {@link Ext.form.Basic#submit submitted}. You will want to set this value if you have multiple
 * checkboxes with the same {@link #name}. If not specified, the value `on` will be used.
 *
 * ## Example usage
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         bodyPadding: 10,
 *         width: 300,
 *         title: 'Pizza Order',
 *         items: [
 *             {
 *                 xtype: 'fieldcontainer',
 *                 fieldLabel: 'Toppings',
 *                 defaultType: 'checkboxfield',
 *                 items: [
 *                     {
 *                         boxLabel  : 'Anchovies',
 *                         name      : 'topping',
 *                         inputValue: '1',
 *                         id        : 'checkbox1'
 *                     }, {
 *                         boxLabel  : 'Artichoke Hearts',
 *                         name      : 'topping',
 *                         inputValue: '2',
 *                         checked   : true,
 *                         id        : 'checkbox2'
 *                     }, {
 *                         boxLabel  : 'Bacon',
 *                         name      : 'topping',
 *                         inputValue: '3',
 *                         id        : 'checkbox3'
 *                     }
 *                 ]
 *             }
 *         ],
 *         bbar: [
 *             {
 *                 text: 'Select Bacon',
 *                 handler: function() {
 *                     Ext.getCmp('checkbox3').setValue(true);
 *                 }
 *             },
 *             '-',
 *             {
 *                 text: 'Select All',
 *                 handler: function() {
 *                     Ext.getCmp('checkbox1').setValue(true);
 *                     Ext.getCmp('checkbox2').setValue(true);
 *                     Ext.getCmp('checkbox3').setValue(true);
 *                 }
 *             },
 *             {
 *                 text: 'Deselect All',
 *                 handler: function() {
 *                     Ext.getCmp('checkbox1').setValue(false);
 *                     Ext.getCmp('checkbox2').setValue(false);
 *                     Ext.getCmp('checkbox3').setValue(false);
 *                 }
 *             }
 *         ],
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.form.field.Checkbox', {
    extend: 'Ext.form.field.Base',
    alias: [
        'widget.checkboxfield',
        'widget.checkbox'
    ],
    alternateClassName: 'Ext.form.Checkbox',
    requires: [
        'Ext.XTemplate',
        'Ext.form.CheckboxManager'
    ],
    /**
     * @cfg {Boolean/String/Number} modelValue
     * The value to use for {@link #getModelData} when checked.
     *
     * @since 6.2.1
     */
    modelValue: true,
    /**
     * @cfg {Boolean/String/Number} modelValueUnchecked
     * The value to use for {@link #getModelData} when unchecked.
     *
     * @since 6.2.1
     */
    modelValueUnchecked: false,
    // inputEl should always retain the same size, never stretch
    stretchInputElFixed: false,
    /**
     * @property {Ext.dom.Element} boxLabelEl
     * A reference to the label element created for the {@link #boxLabel}. Only present
     * if the component has been rendered and has a boxLabel configured.
     */
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        'boxLabelEl',
        'innerWrapEl',
        'displayEl'
    ],
    /* eslint-disable indent, max-len */
    // note: {id} here is really {inputId}, but {cmpId} is available
    /**
     * @cfg fieldSubTpl
     * @inheritdoc
     */
    fieldSubTpl: [
        '<div id="{cmpId}-innerWrapEl" data-ref="innerWrapEl" role="presentation"',
        ' class="{wrapInnerCls}">',
        '<tpl if="labelAlignedBefore">',
        '{beforeBoxLabelTpl}',
        '<label id="{cmpId}-boxLabelEl" data-ref="boxLabelEl" {boxLabelAttrTpl} class="{boxLabelCls} ',
        '{boxLabelCls}-{ui} {boxLabelCls}-{boxLabelAlign} {noBoxLabelCls} {childElCls}" for="{id}">',
        '{beforeBoxLabelTextTpl}',
        '{boxLabel}',
        '{afterBoxLabelTextTpl}',
        '</label>',
        '{afterBoxLabelTpl}',
        '</tpl>',
        '<span id="{cmpId}-displayEl" data-ref="displayEl" role="presentation" class="{fieldCls} {typeCls} ',
        '{typeCls}-{ui} {inputCls} {inputCls}-{ui} {fixCls} {childElCls} {afterLabelCls}">',
        '<input type="{inputType}" id="{id}" name="{inputName}" data-ref="inputEl" {inputAttrTpl}',
        '<tpl if="tabIdx != null"> tabindex="{tabIdx}"</tpl>',
        '<tpl if="disabled"> disabled="disabled"</tpl>',
        '<tpl if="checked"> checked="checked"</tpl>',
        '<tpl if="fieldStyle"> style="{fieldStyle}"</tpl>',
        ' class="{checkboxCls}" autocomplete="off" hidefocus="true" ',
        '<tpl foreach="ariaElAttributes"> {$}="{.}"</tpl>',
        '<tpl foreach="inputElAriaAttributes"> {$}="{.}"</tpl>',
        '/>',
        '</span>',
        '<tpl if="!labelAlignedBefore">',
        '{beforeBoxLabelTpl}',
        '<label id="{cmpId}-boxLabelEl" data-ref="boxLabelEl" {boxLabelAttrTpl} class="{boxLabelCls} ',
        '{boxLabelCls}-{ui} {boxLabelCls}-{boxLabelAlign} {noBoxLabelCls} {childElCls}" for="{id}">',
        '{beforeBoxLabelTextTpl}',
        '{boxLabel}',
        '{afterBoxLabelTextTpl}',
        '</label>',
        '{afterBoxLabelTpl}',
        '</tpl>',
        '</div>',
        {
            disableFormats: true,
            compiled: true
        }
    ],
    /* eslint-enable indent, max-len */
    /**
     * @cfg publishes
     * @inheritdoc
     */
    publishes: {
        checked: 1
    },
    subTplInsertions: [
        /**
         * @cfg {String/Array/Ext.XTemplate} beforeBoxLabelTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * before the box label element. If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'beforeBoxLabelTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} afterBoxLabelTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * after the box label element. If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'afterBoxLabelTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} beforeBoxLabelTextTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * before the box label text. If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'beforeBoxLabelTextTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} afterBoxLabelTextTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * after the box label text. If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'afterBoxLabelTextTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} boxLabelAttrTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * inside the box label element (as attributes). If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'boxLabelAttrTpl',
        'inputAttrTpl'
    ],
    /**
     * @property {Boolean} isCheckbox
     * `true` in this class to identify an object as an instantiated Checkbox, or subclass thereof.
     */
    isCheckbox: true,
    /**
     * @cfg {String} focusCls
     * The CSS class to use when the checkbox receives focus
     */
    focusCls: 'form-checkbox-focus',
    /**
     * @cfg {String} [fieldCls='x-form-field']
     * The default CSS class for the checkbox
     */
    /**
     * @private
     */
    fieldBodyCls: Ext.baseCSSPrefix + 'form-cb-wrap',
    /**
     * @cfg {Boolean} checked
     * true if the checkbox should render initially checked
     */
    checked: false,
    /**
     * @cfg {String} checkedCls
     * The CSS class(es) added to the component's main element when it is in the checked state.
     * You can add your own class (checkedCls='myClass x-form-cb-checked') or replace the default 
     * class altogether (checkedCls='myClass').
     */
    checkedCls: Ext.baseCSSPrefix + 'form-cb-checked',
    /**
     * @cfg {String} boxLabel
     * An optional text label that will appear next to the checkbox. Whether it appears before
     * or after the checkbox is determined by the {@link #boxLabelAlign} config.
     */
    /**
     * @cfg {String} boxLabelCls
     * The CSS class to be applied to the {@link #boxLabel} element
     */
    boxLabelCls: Ext.baseCSSPrefix + 'form-cb-label',
    /**
     * @cfg {String} boxLabelAlign
     * The position relative to the checkbox where the {@link #boxLabel} should appear.
     * Recognized values are 'before' and 'after'.
     */
    boxLabelAlign: 'after',
    afterLabelCls: Ext.baseCSSPrefix + 'form-cb-after',
    wrapInnerCls: Ext.baseCSSPrefix + 'form-cb-wrap-inner',
    noBoxLabelCls: Ext.baseCSSPrefix + 'form-cb-no-box-label',
    /**
     * @cfg {String/Boolean} inputValue
     * The value that should go into the generated input element's value attribute and
     * should be used as the parameter value when submitting as part of a form.
     */
    inputValue: 'on',
    /**
     * @cfg {String} uncheckedValue
     * If configured, this will be submitted as the checkbox's value during form submit
     * if the checkbox is unchecked. By default this is undefined, which results in
     * nothing being submitted for the checkbox field when the form is submitted
     * (the default behavior of HTML checkboxes).
     */
    /**
     * @cfg {Function/String} [handler=undefined]
     * A function called when the {@link #checked} value changes (can be used instead of handling
     * the {@link #change change event}).
     * @cfg {Ext.form.field.Checkbox} handler.checkbox The Checkbox being toggled.
     * @cfg {Boolean} handler.checked The new checked state of the checkbox.
     * @controllable
     */
    /**
     * @cfg {Object} scope
     * An object to use as the scope ('this' reference) of the {@link #handler} function.
     *
     * Defaults to this Checkbox.
     */
    /**
     * @private
     */
    checkChangeEvents: [],
    // See IE8 override
    changeEventName: 'change',
    /**
     * @cfg inputType
     * @inheritdoc
     */
    inputType: 'checkbox',
    /**
     * @cfg isTextInput
     * @inheritdoc
     */
    isTextInput: false,
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'native',
    /**
     * @private
     */
    onRe: /^on$/i,
    // the form-cb css class is for styling shared between checkbox and subclasses (radio)
    inputCls: Ext.baseCSSPrefix + 'form-cb',
    _checkboxCls: Ext.baseCSSPrefix + 'form-cb-input',
    initComponent: function() {
        var me = this,
            value = me.value;
        if (value !== undefined) {
            me.checked = me.isChecked(value, me.inputValue);
        }
        me.callParent();
        me.getManager().add(me);
    },
    // Checkboxes and Radio buttons may have their names managed by their respective group.
    // This happens in CheckboxGroup.onAdd() so we skip default name assignment here.
    initDefaultName: Ext.emptyFn,
    initValue: function() {
        var me = this,
            checked = !!me.checked;
        /**
         * @property {Object} originalValue
         * The original value of the field as configured in the {@link #checked} configuration,
         * or as loaded by the last form load operation if the form's
         * {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} setting is `true`.
         */
        me.originalValue = me.initialValue = me.lastValue = checked;
        // Set the initial checked state
        me.setValue(checked);
    },
    getElConfig: function() {
        var me = this;
        // Add the checked class if this begins checked
        if (me.isChecked(me.rawValue, me.inputValue)) {
            me.addCls(me.checkedCls);
        }
        if (!me.fieldLabel) {
            me.skipLabelForAttribute = true;
        }
        return me.callParent();
    },
    getModelData: function() {
        var me = this,
            o = me.callParent(arguments);
        if (o) {
            o[me.getName()] = me.checked ? me.modelValue : me.modelValueUnchecked;
        }
        return o;
    },
    getSubTplData: function(fieldData) {
        var me = this,
            boxLabel = me.boxLabel,
            boxLabelAlign = me.boxLabelAlign,
            labelAlignedBefore = boxLabelAlign === 'before',
            data, inputElAttr;
        data = Ext.apply(me.callParent([
            fieldData
        ]), {
            inputType: me.inputType,
            checkboxCls: me._checkboxCls,
            disabled: me.readOnly || me.disabled,
            checked: !!me.checked,
            wrapInnerCls: me.wrapInnerCls,
            boxLabel: boxLabel,
            boxLabelCls: me.boxLabelCls,
            boxLabelAlign: boxLabelAlign,
            labelAlignedBefore: labelAlignedBefore,
            afterLabelCls: labelAlignedBefore ? me.afterLabelCls : '',
            noBoxLabelCls: !boxLabel ? me.noBoxLabelCls : '',
            // We need to have name attribute on the <input> element
            // even if it wasn't specified in component config;
            // some browsers (Chrome, Safari) will treat missing name
            // as empty, grouping all radio buttons with empty name
            // together. This causes funky but unwanted effects
            // with regards to keyboard navigation.
            inputName: me.name || me.id
        });
        inputElAttr = data.inputElAriaAttributes;
        if (inputElAttr) {
            // aria-readonly is not valid for Checkboxes and Radio buttons
            delete inputElAttr['aria-readonly'];
        }
        return data;
    },
    initEvents: function() {
        var me = this;
        me.callParent();
        me.inputEl.on(me.changeEventName, me.onChangeEvent, me, {
            delegated: false
        });
        // In all IE versions it is possible to focus ANY element by clicking
        // regardless of tabIndex attribute. In this case, clicking on boxLabelEl
        // will end up focusing its parent bodyEl before focusing and activating
        // the associated input element. Dark wizardry in Focus publisher fails
        // to propagate the second focusin event so we have to accommodate here
        // by not allowing bodyEl to focus.
        if (Ext.isIE) {
            me.bodyEl.on('mousedown', me.onBodyElMousedown, me);
        }
        // Conversely in Safari and Firefox on Mac clicking either box label or input
        // itself will result in input activation, value change, and immediate blur
        // to the document body. We place more faith in consistency over platform
        // specific quirks so have to force inputEl focus here and prevent blurring.
        // Oh Sanity Where Art Thou. :/
        else if (Ext.isMac && (Ext.isGecko || Ext.isSafari)) {
            me.boxLabelEl.on('mousedown', me.onBoxLabelOrInputMousedown, me);
            me.inputEl.on('mousedown', me.onBoxLabelOrInputMousedown, me);
        }
    },
    /**
     * Sets the {@link #boxLabel} for this checkbox.
     * @param {String} boxLabel The new label
     */
    setBoxLabel: function(boxLabel) {
        var me = this;
        me.boxLabel = boxLabel;
        if (me.rendered) {
            me.boxLabelEl.setHtml(boxLabel);
            me.boxLabelEl[boxLabel ? 'removeCls' : 'addCls'](me.noBoxLabelCls);
            me.updateLayout();
        }
    },
    /**
     * @private
     * Handle mousedown events on bodyEl. See explanations in initEvents().
     */
    onBodyElMousedown: function(e) {
        if (e.target !== this.inputEl.dom) {
            e.preventDefault();
        }
    },
    /**
     * @private
     * Handle mousedown events on boxLabelEl and inputEl.
     * See explanations in initEvents().
     */
    onBoxLabelOrInputMousedown: function(e) {
        this.inputEl.focus();
        e.preventDefault();
    },
    /**
     * @private
     * Handle the change event from the DOM.
     */
    onChangeEvent: function(e) {
        this.updateValueFromDom();
    },
    /**
     * @private
     */
    updateValueFromDom: function() {
        var me = this,
            inputEl = me.inputEl && me.inputEl.dom;
        if (inputEl) {
            me.checked = me.rawValue = me.value = inputEl.checked;
            me.checkChange();
        }
    },
    /**
     * @private
     */
    updateCheckedCls: function(checked) {
        var me = this;
        checked = checked != null ? checked : me.getValue();
        me[checked ? 'addCls' : 'removeCls'](me.checkedCls);
    },
    /**
     * Returns the checked state of the checkbox.
     * @return {Boolean} True if checked, else false
     */
    getRawValue: function() {
        var inputEl = this.inputEl && this.inputEl.dom;
        return inputEl ? inputEl.checked : this.checked;
    },
    /**
     * Returns the checked state of the checkbox.
     * @return {Boolean} True if checked, else false
     */
    getValue: function() {
        var inputEl = this.inputEl && this.inputEl.dom;
        return inputEl ? inputEl.checked : this.checked;
    },
    /**
     * Returns the submit value for the checkbox which can be used when submitting forms.
     * @return {String} If checked the {@link #inputValue} is returned; otherwise the
     * {@link #uncheckedValue} (or null if the latter is not configured).
     */
    getSubmitValue: function() {
        var unchecked = this.uncheckedValue,
            uncheckedVal = Ext.isDefined(unchecked) ? unchecked : null;
        return this.getValue() ? this.inputValue : uncheckedVal;
    },
    isChecked: function(rawValue, inputValue) {
        var ret = false;
        if (rawValue === true || rawValue === 'true') {
            ret = true;
        } else {
            if (inputValue !== 'on' && (inputValue || inputValue === 0) && (Ext.isString(rawValue) || Ext.isNumber(rawValue))) {
                ret = rawValue == inputValue;
            } else // eslint-disable-line eqeqeq
            {
                ret = rawValue === '1' || rawValue === 1 || this.onRe.test(rawValue);
            }
        }
        return ret;
    },
    /**
     * Sets the checked state of the checkbox.
     *
     * @param {Boolean/String/Number} value The following values will check the checkbox:
     * - `true, 'true'.
     * - '1', 1, or 'on'`, when there is no {@link #inputValue}.
     * - Value that matches the {@link #inputValue}.
     * Any other value will un-check the checkbox.
     * @return {Boolean} the new checked state of the checkbox
     */
    setRawValue: function(value) {
        var me = this,
            inputEl = me.inputEl && me.inputEl.dom,
            checked = me.isChecked(value, me.inputValue);
        if (inputEl) {
            // Setting checked property will fire unwanted propertychange event in IE8.
            me.duringSetRawValue = true;
            inputEl.checked = checked;
            me.duringSetRawValue = false;
            me.updateCheckedCls(checked);
        }
        me.checked = me.rawValue = checked;
        if (!me.duringSetValue) {
            me.lastValue = checked;
        }
        return checked;
    },
    /**
     * Sets the checked state of the checkbox, and invokes change detection.
     * @param {Array/Boolean/String} checked The following values will check the checkbox:
     * `true, 'true', '1', or 'on'`, as well as a String that matches the {@link #inputValue}.
     * Any other value will  un-check the checkbox.
     *
     * You may also pass an array of string values. If an array of strings is passed, all checkboxes
     * in the group with a matched name will be checked.  The checkbox will be unchecked
     * if a corresponding value is not found in the array.
     * @return {Ext.form.field.Checkbox} this
     */
    setValue: function(checked) {
        var me = this,
            boxes, i, len, box;
        // If an array of strings is passed, find all checkboxes in the group with the same name
        // as this one and check all those whose inputValue is in the array, un-checking all the 
        // others. This is to facilitate setting values from Ext.form.Basic#setValues, 
        // but is not publicly documented as we don't want users depending on this 
        // behavior.
        if (Ext.isArray(checked)) {
            boxes = me.getManager().getByName(me.name, me.getFormId()).items;
            len = boxes.length;
            for (i = 0; i < len; ++i) {
                box = boxes[i];
                box.setValue(Ext.Array.contains(checked, box.inputValue));
            }
        } else {
            // The callParent() call ends up trigger setRawValue, we only want to modify
            // the lastValue when setRawValue being called independently.
            me.duringSetValue = true;
            me.callParent(arguments);
            delete me.duringSetValue;
        }
        return me;
    },
    /**
     * @method valueToRaw
     * @private
     */
    valueToRaw: Ext.identityFn,
    /**
     * @private
     * Called when the checkbox's checked state changes. Invokes the {@link #handler} callback
     * function if specified.
     */
    onChange: function(newVal, oldVal) {
        var me = this,
            handler = me.handler;
        me.updateCheckedCls(newVal);
        if (handler) {
            Ext.callback(handler, me.scope, [
                me,
                newVal
            ], 0, me);
        }
        me.callParent(arguments);
        if (me.reference && me.publishState) {
            me.publishState('checked', newVal);
        }
    },
    /**
     * @private
     */
    resetOriginalValue: function(fromBoxInGroup) {
        var me = this,
            boxes, box, len, i;
        // If we're resetting the value of a field in a group, also reset the others.
        if (!fromBoxInGroup) {
            boxes = me.getManager().getByName(me.name, me.getFormId()).items;
            len = boxes.length;
            for (i = 0; i < len; ++i) {
                box = boxes[i];
                if (box !== me) {
                    boxes[i].resetOriginalValue(true);
                }
            }
        }
        me.callParent();
    },
    doDestroy: function() {
        this.getManager().removeAtKey(this.id);
        this.callParent();
    },
    getManager: function() {
        return Ext.form.CheckboxManager;
    },
    onEnable: function() {
        var me = this,
            inputEl = me.inputEl && me.inputEl.dom;
        me.callParent();
        if (inputEl) {
            // Can still be disabled if the field is readOnly
            inputEl.disabled = me.readOnly;
        }
    },
    setReadOnly: function(readOnly) {
        var me = this,
            inputEl = me.inputEl && me.inputEl.dom;
        if (inputEl) {
            // Set the button to disabled when readonly
            inputEl.disabled = !!readOnly || me.disabled;
        }
        me.callParent(arguments);
    },
    getFormId: function() {
        var me = this,
            form;
        if (!me.formId) {
            form = me.up('form');
            if (form) {
                me.formId = form.id;
            }
        }
        return me.formId;
    },
    getFocusClsEl: function() {
        return this.displayEl;
    }
});

/**
 * A {@link Ext.form.FieldContainer field container} which has a specialized layout for arranging
 * {@link Ext.form.field.Checkbox} controls into columns, and provides convenience
 * {@link Ext.form.field.Field} methods for {@link #getValue getting}, {@link #setValue setting},
 * and {@link #validate validating} the group of checkboxes as a whole.
 *
 * # Validation
 *
 * Individual checkbox fields themselves have no default validation behavior, but
 * sometimes you want to require a user to select at least one of a group of checkboxes.
 * CheckboxGroup allows this by setting the config `{@link #allowBlank}:false`; when the user
 * does not check at least one of the checkboxes, the entire group will be highlighted as invalid
 * and the {@link #blankText error message} will be displayed according to the {@link #msgTarget}
 * config.
 *
 * # Layout
 *
 * The default layout for CheckboxGroup makes it easy to arrange the checkboxes into
 * columns; see the {@link #columns} and {@link #vertical} config documentation for details.
 * You may also use a completely different layout by setting the {@link #cfg-layout} to one of the 
 * other supported layout types; for instance you may wish to use a custom arrangement 
 * of hbox and vbox containers. In that case the checkbox components at any depth will 
 * still be managed by the CheckboxGroup's validation.
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Checkbox Group',
 *         width: 300,
 *         height: 125,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items:[{
 *             xtype: 'checkboxgroup',
 *             fieldLabel: 'Two Columns',
 *             // Arrange checkboxes into two columns, distributed vertically
 *             columns: 2,
 *             vertical: true,
 *             items: [
 *                 { boxLabel: 'Item 1', name: 'rb', inputValue: '1' },
 *                 { boxLabel: 'Item 2', name: 'rb', inputValue: '2', checked: true },
 *                 { boxLabel: 'Item 3', name: 'rb', inputValue: '3' },
 *                 { boxLabel: 'Item 4', name: 'rb', inputValue: '4' },
 *                 { boxLabel: 'Item 5', name: 'rb', inputValue: '5' },
 *                 { boxLabel: 'Item 6', name: 'rb', inputValue: '6' }
 *             ]
 *         }]
 *     });
 */
Ext.define('Ext.form.CheckboxGroup', {
    extend: 'Ext.form.FieldContainer',
    xtype: 'checkboxgroup',
    /**
     * @property {Boolean} isCheckboxGroup
     * The value `true` to identify an object as an instance of this or derived class.
     * @readonly
     * @since 6.2.0
     */
    isCheckboxGroup: true,
    mixins: {
        field: 'Ext.form.field.Field'
    },
    requires: [
        'Ext.layout.container.CheckboxGroup',
        'Ext.form.field.Checkbox',
        'Ext.form.field.Base'
    ],
    /**
     * @cfg {String} name The value of the `name` attribute of the input elements
     * belonging to this Group. If not set, Group's `id` will be used.
     */
    /**
     * @cfg {Ext.form.field.Checkbox[]/Object[]} items
     * An Array of {@link Ext.form.field.Checkbox Checkbox}es or Checkbox config objects to arrange
     * in the group.
     */
    /**
     * @cfg {String/Number/Number[]} columns
     * Specifies the number of columns to use when displaying grouped checkbox/radio controls using
     * automatic layout. This config can take several types of values:
     *
     * - 'auto' - The controls will be rendered one per column on one row and the width of each
     *   column will be evenly distributed based on the width of the overall field container.
     *   This is the default.
     * - Number - If you specific a number (e.g., 3) that number of columns will be created
     *   and the contained controls will be automatically distributed based on the value
     *   of {@link #vertical}.
     * - Array - You can also specify an array of column widths, mixing integer (fixed width)
     *   and float (percentage width) values as needed (e.g., [100, .25, .75]). Any integer values
     *   will be rendered first, then any float values will be calculated as a percentage
     *   of the remaining space. Float values do not have to add up to 1 (100%)
     *   although if you want the controls to take up the entire field container you should do so.
     */
    columns: 'auto',
    /**
     * @cfg {Boolean} vertical
     * True to distribute contained controls across columns, completely filling each column top
     * to bottom before starting on the next column. The number of controls in each column will be
     * automatically calculated to keep columns as even as possible. The default value is false,
     * so that controls will be added to columns one at a time, completely filling each row
     * left to right before starting on the next row.
     */
    vertical: false,
    /**
     * @cfg {Boolean} allowBlank
     * False to validate that at least one item in the group is checked. If no items are selected at
     * validation time, {@link #blankText} will be used as the error text.
     */
    allowBlank: true,
    /**
     * @cfg {String} blankText
     * Error text to display if the {@link #allowBlank} validation fails
     * @locale
     */
    blankText: "You must select at least one item in this group",
    defaultType: 'checkboxfield',
    defaultBindProperty: 'value',
    /**
     * @private
     */
    groupCls: Ext.baseCSSPrefix + 'form-check-group',
    /**
     * @private
     */
    extraFieldBodyCls: Ext.baseCSSPrefix + 'form-checkboxgroup-body',
    layout: 'checkboxgroup',
    componentCls: Ext.baseCSSPrefix + 'form-checkboxgroup',
    ariaRole: 'group',
    ariaEl: 'containerEl',
    // containerEl is a div, it cannot be referenced by a <label for="...">
    // We set aria-labelledby on the containerEl instead
    skipLabelForAttribute: true,
    // Checkbox and radio groups start as valid
    ariaRenderAttributes: {
        'aria-invalid': false
    },
    initComponent: function() {
        var me = this;
        me.name = me.name || me.id;
        me.callParent();
        me.initField();
    },
    initRenderData: function() {
        var me = this,
            data, ariaAttr;
        data = me.callParent();
        // ariaEl is yet a string
        data.inputId = me.id + '-' + me.ariaEl;
        ariaAttr = data.ariaAttributes;
        if (ariaAttr) {
            if (!ariaAttr['aria-labelledby']) {
                ariaAttr['aria-labelledby'] = me.id + '-labelTextEl';
            }
        }
        return data;
    },
    /**
     * Initializes the field's value based on the initial config. If the {@link #value} config
     * is specified then we use that to set the value; otherwise we initialize the originalValue
     * by querying the values of all sub-checkboxes after they have been initialized.
     * @protected
     */
    initValue: function() {
        var me = this,
            valueCfg = me.value;
        me.originalValue = me.lastValue = valueCfg || me.getValue();
        if (valueCfg) {
            me.setValue(valueCfg);
        }
    },
    /**
     * When a checkbox is added to the group, monitor it for changes
     * @param {Object} field The field being added
     * @protected
     */
    onAdd: function(field) {
        var me = this,
            items, len, i;
        if (field.isCheckbox) {
            // Checkboxes and especially Radio buttons MUST have similar name
            // if they belong to a group but also must allow explicit override.
            if (field.name == null) {
                field.name = me.name;
            }
            me.mon(field, 'change', me.checkChange, me);
        } else if (field.isContainer) {
            items = field.items.items;
            for (i = 0 , len = items.length; i < len; i++) {
                me.onAdd(items[i]);
            }
        }
        me.callParent(arguments);
    },
    onRemove: function(item) {
        var me = this,
            items, len, i;
        if (item.isCheckbox) {
            me.mun(item, 'change', me.checkChange, me);
        } else if (item.isContainer) {
            items = item.items.items;
            for (i = 0 , len = items.length; i < len; i++) {
                me.onRemove(items[i]);
            }
        }
        me.callParent(arguments);
    },
    /**
      * @private
      * The group value is a complex object, compare using object serialization
      */
    isEqual: function(value1, value2) {
        var toQueryString = Ext.Object.toQueryString;
        return toQueryString(value1) === toQueryString(value2);
    },
    /**
     * Runs CheckboxGroup's validations and returns an array of any errors. The only error
     * by default is if allowBlank is set to false and no items are checked.
     * @return {String[]} Array of all validation errors
     */
    getErrors: function() {
        var errors = [];
        if (!this.allowBlank && Ext.isEmpty(this.getChecked())) {
            errors.push(this.blankText);
        }
        return errors;
    },
    /**
     * @private
     * Returns all checkbox components within the container
     * @param {String} [query] An additional query to add to the selector.
     */
    getBoxes: function(query) {
        return this.query('[isCheckbox]' + (query || ''));
    },
    /**
     * @private
     * Convenience function which calls the given function for every checkbox in the group
     * @param {Function} fn The function to call
     * @param {Object} [scope] scope object
     */
    eachBox: function(fn, scope) {
        Ext.Array.forEach(this.getBoxes(), fn, scope || this);
    },
    /**
     * Returns an Array of all checkboxes in the container which are currently checked
     * @return {Ext.form.field.Checkbox[]} Array of Ext.form.field.Checkbox components
     */
    getChecked: function() {
        return this.getBoxes('[checked]');
    },
    /**
     * @private
     */
    isDirty: function() {
        var boxes = this.getBoxes(),
            b,
            bLen = boxes.length;
        for (b = 0; b < bLen; b++) {
            if (boxes[b].isDirty()) {
                return true;
            }
        }
    },
    /**
     * @private
     */
    setReadOnly: function(readOnly) {
        var boxes = this.getBoxes(),
            b,
            bLen = boxes.length;
        for (b = 0; b < bLen; b++) {
            boxes[b].setReadOnly(readOnly);
        }
        this.readOnly = readOnly;
    },
    /**
     * Resets the checked state of all {@link Ext.form.field.Checkbox checkboxes} in the group
     * to their originally loaded values and clears any validation messages.
     * See {@link Ext.form.Basic}.{@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad}
     */
    reset: function() {
        var me = this,
            hadError = me.hasActiveError(),
            preventMark = me.preventMark;
        me.preventMark = true;
        me.batchChanges(function() {
            var boxes = me.getBoxes(),
                b,
                bLen = boxes.length;
            for (b = 0; b < bLen; b++) {
                boxes[b].reset();
            }
        });
        me.preventMark = preventMark;
        me.unsetActiveError();
        if (hadError) {
            me.updateLayout();
        }
    },
    resetOriginalValue: function() {
        var me = this,
            boxes = me.getBoxes(),
            b,
            bLen = boxes.length;
        for (b = 0; b < bLen; b++) {
            boxes[b].resetOriginalValue();
        }
        me.originalValue = me.getValue();
        me.checkDirty();
    },
    /**
     * Sets the value(s) of all checkboxes in the group. The expected format is an Object
     * of name-value pairs corresponding to the names of the checkboxes in the group. Each pair
     * can have either a single or multiple values:
     *
     *   - A single Boolean or String value will be passed to the `setValue` method of the checkbox
     *     with that name. See the rules in {@link Ext.form.field.Checkbox#setValue}
     *     for accepted values.
     *   - An Array of String values will be matched against the
     *     {@link Ext.form.field.Checkbox#inputValue inputValue} of checkboxes in the group
     *     with that name; those checkboxes whose inputValue exists in the array will be
     *     checked and others will be unchecked.
     *
     * If a checkbox's name is not in the mapping at all, it will be unchecked.
     *
     * An example:
     *
     *     var myCheckboxGroup = new Ext.form.CheckboxGroup({
     *         columns: 3,
     *         items: [{
     *             name: 'cb1',
     *             boxLabel: 'Single 1'
     *         }, {
     *             name: 'cb2',
     *             boxLabel: 'Single 2'
     *         }, {
     *             name: 'cb3',
     *             boxLabel: 'Single 3'
     *         }, {
     *             name: 'cbGroup',
     *             boxLabel: 'Grouped 1'
     *             inputValue: 'value1'
     *         }, {
     *             name: 'cbGroup',
     *             boxLabel: 'Grouped 2'
     *             inputValue: 'value2'
     *         }, {
     *             name: 'cbGroup',
     *             boxLabel: 'Grouped 3'
     *             inputValue: 'value3'
     *         }]
     *     });
     *
     *     myCheckboxGroup.setValue({
     *         cb1: true,
     *         cb3: false,
     *         cbGroup: ['value1', 'value3']
     *     });
     *
     * The above code will cause the checkbox named 'cb1' to be checked, as well as the first
     * and third checkboxes named 'cbGroup'. The other three checkboxes will be unchecked.
     *
     * @param {Object} value The mapping of checkbox names to values.
     * @return {Ext.form.CheckboxGroup} this
     */
    setValue: function(value) {
        var me = this,
            boxes = me.getBoxes(),
            b,
            bLen = boxes.length,
            box, name, cbValue;
        me.batchChanges(function() {
            Ext.suspendLayouts();
            for (b = 0; b < bLen; b++) {
                box = boxes[b];
                name = box.getName();
                cbValue = false;
                if (value) {
                    if (Ext.isArray(value[name])) {
                        cbValue = Ext.Array.contains(value[name], box.inputValue);
                    } else {
                        // single value, let the checkbox's own setValue handle conversion
                        cbValue = value[name];
                    }
                }
                box.setValue(cbValue);
            }
            Ext.resumeLayouts(true);
        });
        return me;
    },
    /**
     * Returns an object containing the values of all checked checkboxes within the group.
     * Each key-value pair in the object corresponds to a checkbox
     * {@link Ext.form.field.Checkbox#name name}. If there is only one checked checkbox
     * with a particular name, the value of that pair will be the String
     * {@link Ext.form.field.Checkbox#inputValue inputValue} of that checkbox. If there are
     * multiple checked checkboxes with that name, the value of that pair will be an Array
     * of the selected inputValues.
     *
     * The object format returned from this method can also be passed directly to the
     * {@link #setValue} method.
     *
     * NOTE: In Ext 3, this method returned an array of Checkbox components; this was changed
     * to make it more consistent with other field components and with the {@link #setValue}
     * argument signature. If you need the old behavior in Ext 4+, use the {@link #getChecked}
     * method instead.
     */
    getValue: function() {
        var values = {},
            boxes = this.getBoxes(),
            b,
            bLen = boxes.length,
            box, name, inputValue, bucket;
        for (b = 0; b < bLen; b++) {
            box = boxes[b];
            name = box.getName();
            inputValue = box.inputValue;
            if (box.getValue()) {
                if (values.hasOwnProperty(name)) {
                    bucket = values[name];
                    if (!Ext.isArray(bucket)) {
                        bucket = values[name] = [
                            bucket
                        ];
                    }
                    bucket.push(inputValue);
                } else {
                    values[name] = inputValue;
                }
            }
        }
        return values;
    },
    /*
     * Don't return any data for submit; the form will get the info from the individual checkboxes
     * themselves.
     */
    getSubmitData: function() {
        return null;
    },
    /*
     * Don't return any data for the model; the form will get the info from the individual
     * checkboxes themselves.
     */
    getModelData: function() {
        return null;
    },
    validate: function() {
        var me = this,
            errors, isValid, wasValid;
        if (me.disabled) {
            isValid = true;
        } else {
            errors = me.getErrors();
            isValid = Ext.isEmpty(errors);
            wasValid = me.wasValid;
            if (isValid) {
                me.unsetActiveError();
            } else {
                me.setActiveError(errors);
            }
        }
        if (isValid !== wasValid) {
            me.wasValid = isValid;
            me.fireEvent('validitychange', me, isValid);
            if (wasValid != null || !isValid) {
                me.updateLayout();
            }
        }
        return isValid;
    }
}, function() {
    this.borrow(Ext.form.field.Base, [
        'markInvalid',
        'clearInvalid',
        'setError'
    ]);
});

/**
 * A container for grouping sets of fields, rendered as a HTML `fieldset` element.
 * The {@link #title} config will be rendered as the fieldset's `legend`.
 *
 * While FieldSets commonly contain simple groups of fields, they are general
 * {@link Ext.container.Container Containers} and may therefore contain any type of components
 * in their {@link #cfg-items}, including other nested containers. The default {@link #layout}
 * for the FieldSet's items is `'anchor'`, but it can be configured to use any other layout type.
 *
 * FieldSets may also be collapsed if configured to do so; this can be done in two ways:
 *
 * 1. Set the {@link #collapsible} config to true; this will result in a collapse button being
 *    rendered next to the {@link #title legend title}, or:
 * 2. Set the {@link #checkboxToggle} config to true; this is similar to using {@link #collapsible}
 *    but renders a {@link Ext.form.field.Checkbox checkbox} in place of the toggle button.
 *    The fieldset will be expanded when the checkbox is checked and collapsed when it is unchecked.
 *    The checkbox will also be included in the {@link Ext.form.Basic#submit form submit parameters}
 *    using {@link #checkbox}.
 *
 * # Example usage
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Simple Form with FieldSets',
 *         labelWidth: 75, // label settings here cascade unless overridden
 *         url: 'save-form.php',
 *         frame: true,
 *         bodyStyle: 'padding:5px 5px 0',
 *         width: 550,
 *         renderTo: Ext.getBody(),
 *         layout: 'column', // arrange fieldsets side by side
 *         items: [{
 *             // Fieldset in Column 1 - collapsible via toggle button
 *             xtype:'fieldset',
 *             columnWidth: 0.5,
 *             title: 'Fieldset 1',
 *             collapsible: true,
 *             defaultType: 'textfield',
 *             defaults: {anchor: '100%'},
 *             layout: 'anchor',
 *             items :[{
 *                 fieldLabel: 'Field 1',
 *                 name: 'field1'
 *             }, {
 *                 fieldLabel: 'Field 2',
 *                 name: 'field2'
 *             }]
 *         }, {
 *             // Fieldset in Column 2 - collapsible via checkbox, collapsed by default,
 *             // contains a panel
 *             xtype:'fieldset',
 *             title: 'Show Panel', // title or checkboxToggle creates fieldset header
 *             columnWidth: 0.5,
 *             checkboxToggle: true,
 *             collapsed: true, // fieldset initially collapsed
 *             layout:'anchor',
 *             items :[{
 *                 xtype: 'panel',
 *                 anchor: '100%',
 *                 title: 'Panel inside a fieldset',
 *                 frame: true,
 *                 height: 52
 *             }]
 *         }]
 *     });
 */
Ext.define('Ext.form.FieldSet', {
    extend: 'Ext.container.Container',
    alias: 'widget.fieldset',
    mixins: {
        fieldAncestor: 'Ext.form.FieldAncestor'
    },
    uses: [
        'Ext.form.field.Checkbox',
        'Ext.panel.Tool',
        'Ext.layout.container.Anchor',
        'Ext.layout.component.FieldSet'
    ],
    /**
     * @cfg {String} title
     * A title to be displayed in the fieldset's legend. May contain HTML markup.
     */
    /**
     * @cfg {Boolean} [checkboxToggle=false]
     * Set to true to render a checkbox into the fieldset frame just in front of the legend
     * to expand/collapse the fieldset when the checkbox is toggled. This checkbox will be included
     * in form submits using the {@link #checkbox} configuration.
     */
    /**
     * @cfg {String} checkboxName
     * @deprecated 6.2.0 Use the name property in {@link #checkbox} instead.
     * The name to assign to the fieldset's checkbox if {@link #checkboxToggle} = true
     * (defaults to '[fieldset id]-checkbox').
     */
    /**
     * @cfg {String} checkboxUI
     * The ui to use for the fieldset's checkbox.
     */
    checkboxUI: 'default',
    /**
     * @cfg {Boolean} [collapsible=false]
     * Set to true to make the fieldset collapsible and have the expand/collapse toggle button
     * automatically rendered into the legend element, false to keep the fieldset statically sized
     * with no collapse button. Another option is to configure {@link #checkboxToggle}.
     * Use the {@link #collapsed} config to collapse the fieldset by default.
     */
    /**
     * @cfg {Boolean} collapsed
     * Set to true to render the fieldset as collapsed by default. If {@link #checkboxToggle}
     * is specified, the checkbox will also be unchecked by default.
     */
    collapsed: false,
    /**
     * @cfg {Boolean} toggleOnTitleClick
     * Set to true will add a listener to the titleCmp property for the click event which
     * will execute the {@link #toggle} method. This option is only used when the
     * {@link #collapsible} property is set to true.
     */
    toggleOnTitleClick: true,
    /**
     * @property {Ext.Component} legend
     * The component for the fieldset's legend. Will only be defined if the configuration requires
     * a legend to be created, by setting the {@link #title} or {@link #checkboxToggle} options.
     */
    /**
     * @cfg {String} baseCls
     * The base CSS class applied to the fieldset.
     */
    baseCls: Ext.baseCSSPrefix + 'fieldset',
    /**
     * @cfg {Ext.enums.Layout/Object} layout
     * The {@link Ext.container.Container#layout} for the fieldset's immediate child items.
     */
    layout: 'anchor',
    /**
     * @cfg {String} descriptionText
     * Fieldset description to be announced by screen readers.
     * @locale
     */
    descriptionText: '{0} field set',
    /**
     * @cfg {String} expandText
     * Text to be announced by screen readers when toggle tool
     * or checkbox is focused.
     * @locale
     */
    expandText: 'Expand field set',
    componentLayout: 'fieldset',
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'group',
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: false,
    /**
     * @cfg autoEl
     * @inheritdoc
     */
    autoEl: 'fieldset',
    /**
     * @cfg {Object} checkbox
     * A configuration for the generated checkbox that is adjacent to the {@link #title}
     * in the header. This config is only effective when {@link #checkboxToggle} is true
     *
     * @since 6.2.0
     */
    checkbox: null,
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        'body'
    ],
    /* eslint-disable indent, max-len */
    /**
     * @cfg renderTpl
     * @inheritdoc
     */
    renderTpl: [
        '{%this.renderLegend(out,values);%}',
        '<div id="{id}-body" data-ref="body" class="{baseCls}-body {baseCls}-body-{ui} {bodyTargetCls}" ',
        'role="presentation"<tpl if="bodyStyle"> style="{bodyStyle}"</tpl>>',
        '{%this.renderContainer(out,values);%}',
        '</div>'
    ],
    /* eslint-enable indent, max-len */
    /**
     * @cfg stateEvents
     * @inheritdoc Ext.state.Stateful#cfg-stateEvents
     * @localdoc By default the following stateEvents are added:
     * 
     *  - {@link #event-resize} - _(added by Ext.Component)_
     *  - {@link #event-collapse}
     *  - {@link #event-expand}
     */
    stateEvents: [
        'collapse',
        'expand'
    ],
    /**
     * @property maskOnDisable
     * @inheritdoc
     */
    maskOnDisable: false,
    /**
     * @event beforeexpand
     * Fires before this FieldSet is expanded. Return false to prevent the expand.
     * @param {Ext.form.FieldSet} fieldset The FieldSet being expanded.
     */
    /**
     * @event beforecollapse
     * Fires before this FieldSet is collapsed. Return false to prevent the collapse.
     * @param {Ext.form.FieldSet} fieldset The FieldSet being collapsed.
     */
    /**
     * @event expand
     * Fires after this FieldSet has expanded.
     * @param {Ext.form.FieldSet} fieldset The FieldSet that has been expanded.
     */
    /**
     * @event collapse
     * Fires after this FieldSet has collapsed.
     * @param {Ext.form.FieldSet} fieldset The FieldSet that has been collapsed.
     */
    initComponent: function() {
        var me = this,
            baseCls = me.baseCls;
        // We need to render the aria-label attribute instead of relying on
        // aria-labelledby because the contents of these differ.
        if (me.ariaRole && !me.ariaLabel) {
            me.ariaLabel = Ext.String.formatEncode(me.descriptionText, me.title || '');
        }
        me.ariaRenderAttributes = me.ariaRenderAttributes || {};
        me.ariaRenderAttributes['aria-expanded'] = !me.collapsed;
        me.initFieldAncestor();
        me.callParent();
        // Fieldsets cannot support managePadding because the managePadding config causes
        // the paddding to be added to the innerCt instead of the fieldset element.  The
        // padding must be on the fieldset element because the horizontal position of the
        // legend is determined by the fieldset element's padding
        // 
        // As a consequence of the inability to support managePadding, manageOverflow
        // cannot be supported either because the correct overflow cannot be calculated
        // without managePadding to adjust for cross-browser differences in the way
        // padding is handled on overflowing elements.
        // See Ext.layout.container.Auto for more info.
        me.layout.managePadding = me.layout.manageOverflow = false;
        if (me.collapsed) {
            me.addCls(baseCls + '-collapsed');
            me.collapse();
        }
        if (me.title || me.checkboxToggle || me.collapsible) {
            me.addTitleClasses();
            me.legend = me.createLegendCt();
        }
        me.initMonitor();
    },
    /**
     * Initialized the renderData to be used when rendering the renderTpl.
     * @return {Object} Object with keys and values that are going to be applied to the renderTpl
     * @private
     */
    initRenderData: function() {
        var me = this,
            data = me.callParent();
        data.bodyTargetCls = me.bodyTargetCls;
        me.protoBody.writeTo(data);
        delete me.protoBody;
        return data;
    },
    doDestroy: function() {
        var me = this,
            legend = me.legend;
        if (legend) {
            // get rid of the ownerCt since it's not a proper item
            delete legend.ownerCt;
            legend.destroy();
            me.legend = null;
        }
        me.callParent();
    },
    getState: function() {
        var state = this.callParent();
        state = this.addPropertyToState(state, 'collapsed');
        return state;
    },
    afterCollapse: Ext.emptyFn,
    afterExpand: Ext.emptyFn,
    collapsedHorizontal: function() {
        return true;
    },
    collapsedVertical: function() {
        return true;
    },
    createLegendCt: function() {
        var me = this,
            items = [],
            legendCfg = {
                baseCls: me.baseCls + '-header',
                // use container layout so we don't get the auto layout innerCt/outerCt
                layout: 'container',
                ui: me.ui,
                id: me.id + '-legend',
                autoEl: 'legend',
                ariaRole: null,
                items: items,
                ownerCt: me,
                shrinkWrap: true,
                ownerLayout: me.componentLayout
            },
            legend;
        // Checkbox
        if (me.checkboxToggle) {
            items.push(me.createCheckboxCmp());
        } else if (me.collapsible) {
            // Toggle button
            items.push(me.createToggleCmp());
        }
        // Title
        items.push(me.createTitleCmp());
        legend = new Ext.container.Container(legendCfg);
        return legend;
    },
    /**
     * Creates the legend title component. This is only called internally, but could be overridden
     * in subclasses to customize the title component. If {@link #toggleOnTitleClick} is set
     * to true, a listener for the click event will toggle the collapsed state of the FieldSet.
     * @return {Ext.Component}
     * @protected
     */
    createTitleCmp: function() {
        var me = this,
            cfg = {
                html: me.title,
                ui: me.ui,
                cls: me.baseCls + '-header-text',
                id: me.id + '-legendTitle',
                ariaRole: 'presentation'
            };
        if (me.collapsible && me.toggleOnTitleClick) {
            cfg.listeners = {
                click: {
                    element: 'el',
                    scope: me,
                    fn: me.toggle
                }
            };
            cfg.cls += ' ' + me.baseCls + '-header-text-collapsible';
        }
        me.titleCmp = new Ext.Component(cfg);
        return me.titleCmp;
    },
    /**
     * @property {Ext.form.field.Checkbox} checkboxCmp
     * Refers to the {@link Ext.form.field.Checkbox} component that is added next to the title
     * in the legend. Only populated if the fieldset is configured with
     * {@link #checkboxToggle}: true.
     */
    /**
     * Creates the checkbox component. This is only called internally, but could be overridden
     * in subclasses to customize the checkbox's configuration or even return an entirely different
     * component type.
     * @return {Ext.Component}
     * @protected
     */
    createCheckboxCmp: function() {
        var me = this,
            suffix = '-checkbox',
            cls = me.baseCls + '-header' + suffix,
            checkboxCmp;
        cls += ' ' + cls + '-' + me.ui;
        me.checkboxCmp = checkboxCmp = new Ext.form.field.Checkbox(Ext.apply({
            hideEmptyLabel: true,
            name: me.checkboxName || me.id + suffix,
            cls: cls,
            id: me.id + '-legendChk',
            ui: me.checkboxUI,
            checked: !me.collapsed,
            msgTarget: 'none',
            listeners: {
                change: me.onCheckChange,
                scope: me
            },
            ariaLabel: me.expandText
        }, me.checkbox));
        return checkboxCmp;
    },
    /**
     * @property {Ext.panel.Tool} toggleCmp
     * Refers to the {@link Ext.panel.Tool} component that is added as the collapse/expand button
     * next to the title in the legend. Only populated if the fieldset is configured with
     * {@link #collapsible}: true.
     */
    /**
     * Creates the toggle button component. This is only called internally, but could be overridden
     * in subclasses to customize the toggle component.
     * @return {Ext.Component}
     * @protected
     */
    createToggleCmp: function() {
        var me = this,
            toggleCmp;
        me.toggleCmp = toggleCmp = new Ext.panel.Tool({
            // fieldset tools may be styled differently from regular tools and so we need
            // to tell the layout system not to cache the height if this tool happens
            // to be the first one through the layout system
            cacheHeight: false,
            cls: me.baseCls + '-header-tool-' + me.ui,
            type: 'toggle',
            handler: me.toggle,
            id: me.id + '-legendToggle',
            scope: me,
            // This tool is akin to a checkbox; its is considered "checked"
            // when fieldset is expanded, and vice versa.
            ariaRole: 'checkbox',
            ariaLabel: me.expandText,
            ariaRenderAttributes: {
                'aria-checked': !me.collapsed
            }
        });
        return toggleCmp;
    },
    doRenderLegend: function(out, renderData) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!
        var me = renderData.$comp,
            legend = me.legend,
            tree;
        // Create the Legend component if needed
        if (legend) {
            legend.ownerLayout.configureItem(legend);
            me.setLegendCollapseImmunity(legend);
            tree = legend.getRenderTree();
            Ext.DomHelper.generateMarkup(tree, out);
        }
    },
    getCollapsed: function() {
        return this.collapsed ? 'top' : false;
    },
    getCollapsedDockedItems: function() {
        var legend = this.legend;
        return legend ? [
            legend
        ] : [];
    },
    /**
     * Sets the title of this fieldset.
     * @param {String} title The new title.
     * @return {Ext.form.FieldSet} this
     */
    setTitle: function(title) {
        var me = this,
            legend = me.legend;
        me.title = title;
        me.ariaLabel = Ext.String.formatEncode(me.descriptionText, title || '');
        if (me.rendered) {
            if (!legend) {
                me.legend = legend = me.createLegendCt();
                me.addTitleClasses();
                legend.ownerLayout.configureItem(legend);
                me.setLegendCollapseImmunity(legend);
                legend.render(me.el, 0);
            }
            me.titleCmp.update(title);
            // ariaLabel property was htmlEncoded in initComponent
            me.ariaEl.dom.setAttribute('aria-label', me.ariaLabel);
        } else if (legend) {
            me.titleCmp.update(title);
        } else {
            me.addTitleClasses();
            me.legend = me.createLegendCt();
        }
        return me;
    },
    addTitleClasses: function() {
        var me = this,
            title = me.title,
            baseCls = me.baseCls;
        if (title) {
            me.addCls(baseCls + '-with-title');
        }
        if (title || me.checkboxToggle || me.collapsible) {
            me.addCls(baseCls + '-with-legend');
        }
    },
    /**
     * Expands the fieldset.
     * @return {Ext.form.FieldSet} this
     */
    expand: function() {
        return this.setExpanded(true);
    },
    /**
     * Collapses the fieldset.
     * @return {Ext.form.FieldSet} this
     */
    collapse: function() {
        return this.setExpanded(false);
    },
    /**
     * Set the collapsed state of the fieldset.
     * @param {Boolean} collapsed The collapsed state.
     *
     * @since 6.2.0
     */
    setCollapsed: function(collapsed) {
        this.setExpanded(!collapsed);
    },
    /**
     * @private
     * Collapse or expand the fieldset.
     */
    setExpanded: function(expanded) {
        var me = this,
            checkboxCmp = me.checkboxCmp,
            toggleCmp = me.toggleCmp,
            operation = expanded ? 'expand' : 'collapse';
        if (!me.rendered || me.fireEvent('before' + operation, me) !== false) {
            expanded = !!expanded;
            if (checkboxCmp) {
                checkboxCmp.setValue(expanded);
            } else if (toggleCmp && toggleCmp.ariaEl.dom) {
                toggleCmp.ariaEl.dom.setAttribute('aria-checked', expanded);
            }
            if (expanded) {
                me.removeCls(me.baseCls + '-collapsed');
            } else {
                me.addCls(me.baseCls + '-collapsed');
            }
            if (me.ariaEl.dom) {
                me.ariaEl.dom.setAttribute('aria-expanded', !!expanded);
            }
            me.collapsed = !expanded;
            if (expanded) {
                delete me.getInherited().collapsed;
            } else {
                me.getInherited().collapsed = true;
            }
            if (me.rendered) {
                // say explicitly we are not root because when we have a fixed/configured height
                // our ownerLayout would say we are root and so would not have it's height
                // updated since it's not included in the layout cycle
                me.updateLayout({
                    isRoot: false
                });
                me.fireEvent(operation, me);
            }
        }
        return me;
    },
    getRefItems: function(deep) {
        var refItems = this.callParent(arguments),
            legend = this.legend;
        // Prepend legend items to ensure correct order
        if (legend) {
            refItems.unshift(legend);
            if (deep) {
                refItems.unshift.apply(refItems, legend.getRefItems(true));
            }
        }
        return refItems;
    },
    /**
     * Toggle the fieldset's collapsed state to the opposite of what it is currently.
     */
    toggle: function() {
        this.setExpanded(!!this.collapsed);
    },
    privates: {
        applyTargetCls: function(targetCls) {
            this.bodyTargetCls = targetCls;
        },
        finishRender: function() {
            var legend = this.legend;
            this.callParent();
            if (legend) {
                legend.finishRender();
            }
        },
        getProtoBody: function() {
            var me = this,
                body = me.protoBody;
            if (!body) {
                me.protoBody = body = new Ext.util.ProtoElement({
                    styleProp: 'bodyStyle',
                    styleIsText: true
                });
            }
            return body;
        },
        getDefaultContentTarget: function() {
            return this.body;
        },
        getTargetEl: function() {
            return this.body || this.frameBody || this.el;
        },
        initPadding: function(targetEl) {
            var me = this,
                body = me.getProtoBody(),
                padding = me.padding,
                bodyPadding;
            if (padding !== undefined) {
                if (Ext.isIE8) {
                    // IE8 and below display fieldset top padding outside the border
                    // so we transfer the top padding to the body element.
                    padding = me.parseBox(padding);
                    bodyPadding = Ext.Element.parseBox(0);
                    bodyPadding.top = padding.top;
                    padding.top = 0;
                    body.setStyle('padding', me.unitizeBox(bodyPadding));
                }
                targetEl.setStyle('padding', me.unitizeBox(padding));
            }
        },
        /**
         * @private
         * Handle changes in the checkbox checked state.
         */
        onCheckChange: function(cmp, checked) {
            this.setExpanded(checked);
        },
        setLegendCollapseImmunity: function(legend) {
            // Mark the legend as immune to collapse so that when the fieldset
            // *is* collapsed and the toggle tool or checkbox is focused,
            // calling isVisible(true) on it will return true instead of false.
            // See also below in createCheckboxCmp and createToggleCmp.
            // 
            // It is important to defer this until the inherited hierarchy is setup
            // completely, otherwise we could cause our inheritedState to get 
            // initialized incorrectly.
            legend.collapseImmune = true;
            legend.getInherited().collapseImmune = true;
        },
        setupRenderTpl: function(renderTpl) {
            this.callParent(arguments);
            renderTpl.renderLegend = this.doRenderLegend;
        }
    }
});

/**
 * Produces a standalone `<label />` element which can be inserted into a form and be associated
 * with a field in that form using the {@link #forId} property.
 * 
 * **NOTE:** in most cases it will be more appropriate to use the
 * {@link Ext.form.Labelable#fieldLabel fieldLabel} and associated config properties
 * ({@link Ext.form.Labelable#labelAlign}, {@link Ext.form.Labelable#labelWidth}, etc.)
 * in field components themselves, as that allows labels to be uniformly sized throughout the form.
 * Ext.form.Label should only be used when your layout can not be achieved with the standard
 * {@link Ext.form.Labelable field layout}.
 * 
 * You will likely be associating the label with a field component that extends
 * {@link Ext.form.field.Base}, so you should make sure the {@link #forId} is set to the same value
 * as the {@link Ext.form.field.Base#inputId inputId} of that field.
 * 
 * The label's text can be set using either the {@link #text} or {@link #html} configuration
 * properties; the difference between the two is that the former will automatically escape HTML
 * characters when rendering, while the latter will not.
 *
 * # Example
 * 
 * This example creates a Label after its associated Text field, an arrangement that cannot
 * currently be achieved using the standard Field layout's labelAlign.
 * 
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Field with Label',
 *         width: 400,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         layout: {
 *             type: 'hbox',
 *             align: 'middle'
 *         },
 *         items: [{
 *             xtype: 'textfield',
 *             hideLabel: true,
 *             flex: 1
 *         }, {
 *             xtype: 'label',
 *             forId: 'myFieldId',
 *             text: 'My Awesome Field',
 *             margin: '0 0 0 10'
 *         }]
 *     });
 */
Ext.define('Ext.form.Label', {
    extend: 'Ext.Component',
    alias: 'widget.label',
    requires: [
        'Ext.util.Format'
    ],
    autoEl: 'label',
    /**
     * @cfg {String} [text='']
     * The plain text to display within the label. If you need to include HTML
     * tags within the label's innerHTML, use the {@link #html} config instead.
     */
    /**
     * @cfg {String} forId
     * The id of the input element to which this label will be bound via the standard HTML 'for'
     * attribute. If not specified, the attribute will not be added to the label. In most cases
     * you will be associating the label with a {@link Ext.form.field.Base} component,
     * so you should make sure this matches the {@link Ext.form.field.Base#inputId inputId}
     * of that field.
     */
    /**
     * @cfg {String} [html='']
     * An HTML fragment that will be used as the label's innerHTML.
     * Note that if {@link #text} is specified it will take precedence and this value
     * will be ignored.
     */
    maskOnDisable: false,
    getElConfig: function() {
        var me = this;
        me.html = me.text ? Ext.util.Format.htmlEncode(me.text) : (me.html || '');
        return Ext.apply(me.callParent(), {
            htmlFor: me.forId || ''
        });
    },
    /**
     * Updates the label's innerHTML with the specified string.
     * @param {String} text The new label text
     * @param {Boolean} [encode=true] False to skip HTML-encoding the text when rendering it
     * to the label. This might be useful if you want to include tags in the label's innerHTML
     * rather than rendering them as string literals per the default logic.
     * @return {Ext.form.Label} this
     */
    setText: function(text, encode) {
        var me = this;
        encode = encode !== false;
        if (encode) {
            me.text = text;
            delete me.html;
        } else {
            me.html = text;
            delete me.text;
        }
        if (me.rendered) {
            me.el.dom.innerHTML = encode !== false ? Ext.util.Format.htmlEncode(text) : text;
            me.updateLayout();
        }
        return me;
    }
});

/**
 * FormPanel provides a standard container for forms. It is essentially a standard
 * {@link Ext.panel.Panel} which automatically creates a {@link Ext.form.Basic BasicForm}
 * for managing any {@link Ext.form.field.Field} objects that are added as descendants of the panel.
 * It also includes conveniences for configuring and working with the BasicForm and the collection
 * of Fields.
 * 
 * # Layout
 * 
 * By default, FormPanel is configured with `{@link Ext.layout.container.Anchor layout:'anchor'}`
 * for the layout of its immediate child items. This can be changed to any of the supported
 * container layouts. The layout of sub-containers is configured in
 * {@link Ext.container.Container#layout the standard way}.
 * 
 * # BasicForm
 * 
 * FormPanel class accepts all
 * of the config options supported by the {@link Ext.form.Basic} class, and will pass them along to
 * the internal BasicForm when it is created.
 * 
 * The following events fired by the BasicForm will be re-fired by the FormPanel and can therefore
 * be listened for on the FormPanel itself:
 * 
 * - {@link Ext.form.Basic#beforeaction beforeaction}
 * - {@link Ext.form.Basic#actionfailed actionfailed}
 * - {@link Ext.form.Basic#actioncomplete actioncomplete}
 * - {@link Ext.form.Basic#validitychange validitychange}
 * - {@link Ext.form.Basic#dirtychange dirtychange}
 * 
 * # Field Defaults
 * 
 * The {@link #fieldDefaults} config option conveniently allows centralized configuration of default
 * values for all fields added as descendants of the FormPanel. Any config option recognized
 * by implementations of {@link Ext.form.Labelable} may be included in this object. See the
 * {@link #fieldDefaults} documentation for details of how the defaults are applied.
 * 
 * # Form Validation
 * 
 * With the default configuration, form fields are validated on-the-fly while the user edits
 * their values. This can be controlled on a per-field basis (or via the {@link #fieldDefaults}
 * config) with the field config properties {@link Ext.form.field.Field#validateOnChange} and
 * {@link Ext.form.field.Base#checkChangeEvents}, and the FormPanel's config properties
 * {@link #pollForChanges} and {@link #pollInterval}.
 * 
 * Any component within the FormPanel can be configured with `formBind: true`. This will cause that
 * component to be automatically disabled when the form is invalid, and enabled when it is valid.
 * This is most commonly used for Button components to prevent submitting the form in an invalid
 * state, but can be used on any component type.
 * 
 * For more information on form validation see the following:
 * 
 * - {@link Ext.form.field.Field#validateOnChange}
 * - {@link #pollForChanges} and {@link #pollInterval}
 * - {@link Ext.form.field.VTypes}
 * - {@link Ext.form.Basic#doAction BasicForm.doAction clientValidation notes}
 * 
 * # Form Submission
 * 
 * By default, Ext Forms are submitted through Ajax, using {@link Ext.form.action.Action}.
 * See the documentation for {@link Ext.form.Basic} for details.
 *
 * # Example usage
 * 
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Simple Form',
 *         bodyPadding: 5,
 *         width: 350,
 * 
 *         // The form will submit an AJAX request to this URL when submitted
 *         url: 'save-form.php',
 * 
 *         // Fields will be arranged vertically, stretched to full width
 *         layout: 'anchor',
 *         defaults: {
 *             anchor: '100%'
 *         },
 * 
 *         // The fields
 *         defaultType: 'textfield',
 *         items: [{
 *             fieldLabel: 'First Name',
 *             name: 'first',
 *             allowBlank: false
 *         },{
 *             fieldLabel: 'Last Name',
 *             name: 'last',
 *             allowBlank: false
 *         }],
 * 
 *         // Reset and Submit buttons
 *         buttons: [{
 *             text: 'Reset',
 *             handler: function() {
 *                 this.up('form').getForm().reset();
 *             }
 *         }, {
 *             text: 'Submit',
 *             formBind: true, //only enabled once the form is valid
 *             disabled: true,
 *             handler: function() {
 *                 var form = this.up('form').getForm();
 *                 if (form.isValid()) {
 *                     form.submit({
 *                         success: function(form, action) {
 *                            Ext.Msg.alert('Success', action.result.msg);
 *                         },
 *                         failure: function(form, action) {
 *                             Ext.Msg.alert('Failed', action.result.msg);
 *                         }
 *                     });
 *                 }
 *             }
 *         }],
 *         renderTo: Ext.getBody()
 *     });
 *
 */
Ext.define('Ext.form.Panel', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.form',
    alternateClassName: [
        'Ext.FormPanel',
        'Ext.form.FormPanel'
    ],
    requires: [
        'Ext.form.Basic',
        'Ext.util.TaskRunner'
    ],
    mixins: {
        fieldAncestor: 'Ext.form.FieldAncestor'
    },
    /**
     * @cfg {Boolean} pollForChanges
     * If set to `true`, sets up an interval task (using the {@link #pollInterval}) in which the
     * panel's fields are repeatedly checked for changes in their values. This is in addition
     * to the normal detection each field does on its own input element, and is not needed
     * in most cases. It does, however, provide a means to absolutely guarantee detection
     * of all changes including some edge cases in some browsers which do not fire native events.
     * Defaults to `false`.
     */
    /**
     * @cfg {Number} pollInterval
     * Interval in milliseconds at which the form's fields are checked for value changes. Only used
     * if the {@link #pollForChanges} option is set to `true`. Defaults to 500 milliseconds.
     */
    /**
     * @cfg {Ext.enums.Layout/Object} layout
     * The {@link Ext.container.Container#layout} for the form panel's immediate child items.
     */
    layout: 'anchor',
    bodyAriaRole: 'form',
    basicFormConfigs: [
        /**
         * @cfg api
         * @inheritdoc Ext.form.Basic#cfg!api
         */
        'api',
        /**
         * @cfg baseParams
         * @inheritdoc Ext.form.Basic#cfg!baseParams
         */
        'baseParams',
        /**
         * @cfg errorReader
         * @inheritdoc Ext.form.Basic#cfg!errorReader
         */
        'errorReader',
        /**
         * @cfg jsonSubmit
         * @inheritdoc Ext.form.Basic#cfg!jsonSubmit
         */
        'jsonSubmit',
        /**
         * @cfg method
         * @inheritdoc Ext.form.Basic#cfg!method
         */
        'method',
        /**
         * @cfg paramOrder
         * @inheritdoc Ext.form.Basic#cfg!paramOrder
         */
        'paramOrder',
        /**
         * @cfg paramsAsHash
         * @inheritdoc Ext.form.Basic#cfg!paramsAsHash
         */
        'paramsAsHash',
        /**
         * @cfg reader
         * @inheritdoc Ext.form.Basic#cfg!reader
         */
        'reader',
        /**
         * @cfg standardSubmit
         * @inheritdoc Ext.form.Basic#cfg!standardSubmit
         */
        'standardSubmit',
        /**
         * @cfg timeout
         * @inheritdoc Ext.form.Basic#cfg!timeout
         */
        'timeout',
        /**
         * @cfg trackResetOnLoad
         * @inheritdoc Ext.form.Basic#cfg!trackResetOnLoad
         */
        'trackResetOnLoad',
        /**
         * @cfg url
         * @inheritdoc Ext.form.Basic#cfg!url
         */
        'url',
        /**
         * @cfg waitMsgTarget
         * @inheritdoc Ext.form.Basic#cfg!waitMsgTarget
         */
        'waitMsgTarget',
        /**
         * @cfg waitTitle
         * @inheritdoc Ext.form.Basic#cfg!waitTitle
         */
        'waitTitle'
    ],
    initComponent: function() {
        var me = this;
        if (me.frame) {
            me.border = false;
        }
        me.initFieldAncestor();
        me.callParent();
        me.relayEvents(me.form, [
            /**
             * @event beforeaction
             * @inheritdoc Ext.form.Basic#beforeaction
             */
            'beforeaction',
            /**
             * @event actionfailed
             * @inheritdoc Ext.form.Basic#actionfailed
             */
            'actionfailed',
            /**
             * @event actioncomplete
             * @inheritdoc Ext.form.Basic#actioncomplete
             */
            'actioncomplete',
            /**
             * @event validitychange
             * @inheritdoc Ext.form.Basic#validitychange
             */
            'validitychange',
            /**
             * @event dirtychange
             * @inheritdoc Ext.form.Basic#dirtychange
             */
            'dirtychange'
        ]);
        // Start polling if configured
        if (me.pollForChanges) {
            me.startPolling(me.pollInterval || 500);
        }
    },
    initItems: function() {
        // Create the BasicForm
        this.callParent();
        this.initMonitor();
        this.form = this.createForm();
    },
    // Initialize the BasicForm after all layouts have been completed.
    afterFirstLayout: function() {
        this.callParent(arguments);
        this.form.initialize();
    },
    /**
     * @private
     */
    createForm: function() {
        var cfg = {},
            props = this.basicFormConfigs,
            len = props.length,
            i = 0,
            prop;
        for (; i < len; ++i) {
            prop = props[i];
            cfg[prop] = this[prop];
        }
        return new Ext.form.Basic(this, cfg);
    },
    /**
     * Provides access to the {@link Ext.form.Basic Form} which this Panel contains.
     * @return {Ext.form.Basic} The {@link Ext.form.Basic Form} which this Panel contains.
     */
    getForm: function() {
        return this.form;
    },
    /**
     * Loads an {@link Ext.data.Model} into this form (internally just calls
     * {@link Ext.form.Basic#loadRecord}).
     * See also {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad}. The fields in the model
     * are mapped to  fields in the form by matching either the {@link Ext.form.field.Base#name}
     * or {@link Ext.Component#itemId}.  
     * @param {Ext.data.Model} record The record to load
     * @return {Ext.form.Basic} The Ext.form.Basic attached to this FormPanel
     */
    loadRecord: function(record) {
        return this.getForm().loadRecord(record);
    },
    /**
     * Returns the currently loaded Ext.data.Model instance if one was loaded via
     * {@link #loadRecord}.
     * @return {Ext.data.Model} The loaded instance
     */
    getRecord: function() {
        return this.getForm().getRecord();
    },
    /**
     * Persists the values in this form into the passed {@link Ext.data.Model} object
     * in a beginEdit/endEdit block. If the record is not specified, it will attempt to update
     * (if it exists) the record provided to {@link #loadRecord}.
     * @param {Ext.data.Model} [record] The record to edit
     * @return {Ext.form.Basic} The Ext.form.Basic attached to this FormPanel
     */
    updateRecord: function(record) {
        return this.getForm().updateRecord(record);
    },
    /**
     * @method getValues
     * Convenience function for fetching the current value of each field in the form.
     * This is the same as calling {@link Ext.form.Basic#getValues this.getForm().getValues()}.
     *
     * @inheritdoc Ext.form.Basic#getValues
     */
    getValues: function(asString, dirtyOnly, includeEmptyText, useDataValues) {
        return this.getForm().getValues(asString, dirtyOnly, includeEmptyText, useDataValues);
    },
    /**
     * @method isDirty
     * Convenience function to check if the form has any dirty fields. This is the same as calling
     * {@link Ext.form.Basic#isDirty this.getForm().isDirty()}.
     *
     * @inheritdoc Ext.form.Basic#isDirty
     */
    isDirty: function() {
        return this.form.isDirty();
    },
    /**
     * @method isValid
     * Convenience function to check if the form has all valid fields. This is the same as calling
     * {@link Ext.form.Basic#isValid this.getForm().isValid()}.
     *
     * @inheritdoc Ext.form.Basic#isValid
     */
    isValid: function() {
        return this.form.isValid();
    },
    /**
     * @method reset
     * Convenience function reset the form. This is the same as calling
     * {@link Ext.form.Basic#reset this.getForm().reset()}.
     *
     * @inheritdoc Ext.form.Basic#reset
     */
    reset: function(resetRecord) {
        return this.form.reset(resetRecord);
    },
    /**
     * @method hasInvalidField
     * Convenience function to check if the form has any invalid fields. This is the same as calling
     * {@link Ext.form.Basic#hasInvalidField this.getForm().hasInvalidField()}.
     *
     * @inheritdoc Ext.form.Basic#hasInvalidField
     */
    hasInvalidField: function() {
        return this.form.hasInvalidField();
    },
    doDestroy: function() {
        this.stopPolling();
        this.form.destroy();
        this.callParent();
    },
    /**
     * This is a proxy for the underlying BasicForm's {@link Ext.form.Basic#load} call.
     * @param {Object} options The options to pass to the action (see {@link Ext.form.Basic#load}
     * and {@link Ext.form.Basic#doAction} for details)
     */
    load: function(options) {
        this.form.load(options);
    },
    /**
     * This is a proxy for the underlying BasicForm's {@link Ext.form.Basic#submit} call.
     * @param {Object} options The options to pass to the action (see {@link Ext.form.Basic#submit}
     * and {@link Ext.form.Basic#doAction} for details)
     */
    submit: function(options) {
        this.form.submit(options);
    },
    /**
     * Start an interval task to continuously poll all the fields in the form for changes in their
     * values. This is normally started automatically by setting the {@link #pollForChanges} config.
     * @param {Number} interval The interval in milliseconds at which the check should run.
     */
    startPolling: function(interval) {
        this.stopPolling();
        this.pollTask = Ext.util.TaskManager.start({
            interval: interval,
            run: this.checkChange,
            scope: this
        });
    },
    /**
     * Stop a running interval task that was started by {@link #startPolling}.
     */
    stopPolling: function() {
        var task = this.pollTask;
        if (task) {
            Ext.util.TaskManager.stop(task, true);
            this.pollTask = null;
        }
    },
    /**
     * Forces each field within the form panel to
     * {@link Ext.form.field.Field#checkChange check if its value has changed}.
     */
    checkChange: function() {
        var fields = this.form.getFields().items,
            f,
            fLen = fields.length;
        for (f = 0; f < fLen; f++) {
            fields[f].checkChange();
        }
    }
});

/**
 * @private
 * Private utility class for managing all {@link Ext.form.field.Radio} fields grouped by name.
 */
Ext.define('Ext.form.RadioManager', {
    extend: 'Ext.util.MixedCollection',
    singleton: true,
    getByName: function(name, formId) {
        return this.filterBy(function(item) {
            return item.name === name && item.getFormId() === formId;
        });
    },
    getWithValue: function(name, value, formId) {
        return this.filterBy(function(item) {
            return item.name === name && item.inputValue == value && // eslint-disable-line eqeqeq
            item.getFormId() === formId;
        });
    },
    getChecked: function(name, formId) {
        return this.findBy(function(item) {
            return item.name === name && item.checked && item.getFormId() === formId;
        });
    }
});

/**
 * Single radio field. Similar to checkbox, but automatically handles making sure only one radio
 * is checked at a time within a group of radios with the same name.
 *
 * # Labeling
 *
 * In addition to the {@link Ext.form.Labelable standard field labeling options}, radio buttons
 * may be given an optional {@link #boxLabel} which will be displayed immediately to the right
 * of the input. Also see {@link Ext.form.RadioGroup} for a convenient method of grouping related
 * radio buttons.
 *
 * # Values
 *
 * The main value of a Radio field is a boolean, indicating whether or not the radio is checked.
 *
 * The following values will check the radio:
 *
 * - `true`
 * - `'true'`
 * - `'1'`
 * - `'on'`
 *
 * Any other value will uncheck it.
 *
 * In addition to the main boolean value, you may also specify a separate {@link #inputValue}.
 * This will be sent as the parameter value when the form is
 * {@link Ext.form.Basic#submit submitted}. You will want to set this value if you have multiple
 * radio buttons with the same {@link #name}, as is almost always the case.
 *
 * # Example usage
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title      : 'Order Form',
 *         width      : 300,
 *         bodyPadding: 10,
 *         renderTo   : Ext.getBody(),
 *         items: [
 *             {
 *                 xtype      : 'fieldcontainer',
 *                 fieldLabel : 'Size',
 *                 defaultType: 'radiofield',
 *                 defaults: {
 *                     flex: 1
 *                 },
 *                 layout: 'hbox',
 *                 items: [
 *                     {
 *                         boxLabel  : 'M',
 *                         name      : 'size',
 *                         inputValue: 'm',
 *                         id        : 'radio1'
 *                     }, {
 *                         boxLabel  : 'L',
 *                         name      : 'size',
 *                         inputValue: 'l',
 *                         id        : 'radio2'
 *                     }, {
 *                         boxLabel  : 'XL',
 *                         name      : 'size',
 *                         inputValue: 'xl',
 *                         id        : 'radio3'
 *                     }
 *                 ]
 *             },
 *             {
 *                 xtype      : 'fieldcontainer',
 *                 fieldLabel : 'Color',
 *                 defaultType: 'radiofield',
 *                 defaults: {
 *                     flex: 1
 *                 },
 *                 layout: 'hbox',
 *                 items: [
 *                     {
 *                         boxLabel  : 'Blue',
 *                         name      : 'color',
 *                         inputValue: 'blue',
 *                         id        : 'radio4'
 *                     }, {
 *                         boxLabel  : 'Grey',
 *                         name      : 'color',
 *                         inputValue: 'grey',
 *                         id        : 'radio5'
 *                     }, {
 *                         boxLabel  : 'Black',
 *                         name      : 'color',
 *                         inputValue: 'black',
 *                         id        : 'radio6'
 *                     }
 *                 ]
 *             }
 *         ],
 *         bbar: [
 *             {
 *                 text: 'Smaller Size',
 *                 handler: function() {
 *                     var radio1 = Ext.getCmp('radio1'),
 *                         radio2 = Ext.getCmp('radio2'),
 *                         radio3 = Ext.getCmp('radio3');
 *
 *                     //if L is selected, change to M
 *                     if (radio2.getValue()) {
 *                         radio1.setValue(true);
 *                         return;
 *                     }
 *
 *                     //if XL is selected, change to L
 *                     if (radio3.getValue()) {
 *                         radio2.setValue(true);
 *                         return;
 *                     }
 *
 *                     //if nothing is set, set size to S
 *                     radio1.setValue(true);
 *                 }
 *             },
 *             {
 *                 text: 'Larger Size',
 *                 handler: function() {
 *                     var radio1 = Ext.getCmp('radio1'),
 *                         radio2 = Ext.getCmp('radio2'),
 *                         radio3 = Ext.getCmp('radio3');
 *
 *                     //if M is selected, change to L
 *                     if (radio1.getValue()) {
 *                         radio2.setValue(true);
 *                         return;
 *                     }
 *
 *                     //if L is selected, change to XL
 *                     if (radio2.getValue()) {
 *                         radio3.setValue(true);
 *                         return;
 *                     }
 *
 *                     //if nothing is set, set size to XL
 *                     radio3.setValue(true);
 *                 }
 *             },
 *             '-',
 *             {
 *                 text: 'Select color',
 *                 menu: {
 *                     indent: false,
 *                     items: [
 *                         {
 *                             text: 'Blue',
 *                             handler: function() {
 *                                 var radio = Ext.getCmp('radio4');
 *                                 radio.setValue(true);
 *                             }
 *                         },
 *                         {
 *                             text: 'Grey',
 *                             handler: function() {
 *                                 var radio = Ext.getCmp('radio5');
 *                                 radio.setValue(true);
 *                             }
 *                         },
 *                         {
 *                             text: 'Black',
 *                             handler: function() {
 *                                 var radio = Ext.getCmp('radio6');
 *                                 radio.setValue(true);
 *                             }
 *                         }
 *                     ]
 *                 }
 *             }
 *         ]
 *     });
 */
Ext.define('Ext.form.field.Radio', {
    extend: 'Ext.form.field.Checkbox',
    alias: [
        'widget.radiofield',
        'widget.radio'
    ],
    alternateClassName: 'Ext.form.Radio',
    requires: [
        'Ext.form.RadioManager'
    ],
    /**
     * @property {Boolean} isRadio
     * The value `true` to identify an object as an instance of this or derived class.
     * @readonly
     */
    isRadio: true,
    /**
     * @cfg {String} uncheckedValue
     * @private
     */
    inputType: 'radio',
    formId: null,
    /**
     * @cfg {Boolean/String/Number} [modelValue=inputValue]
     * The value to use for {@link #getModelData} when checked. Uses the {@link #inputValue}
     * by default.
     *
     * @since 6.2.1
     */
    modelValue: undefined,
    /**
     * @cfg {Boolean/String/Number} modelValueUnchecked
     * The value to use for {@link #getModelData} when unchecked.
     *
     * @since 6.2.1
     */
    modelValueUnchecked: null,
    initComponent: function() {
        var me = this;
        if (me.modelValue === undefined) {
            me.modelValue = me.inputValue;
        }
        me.callParent();
    },
    /**
     * If this radio is part of a group, it will return the selected value
     * @return {String}
     */
    getGroupValue: function() {
        var selected = this.getManager().getChecked(this.name, this.getFormId());
        return selected ? selected.inputValue : null;
    },
    onRemoved: function() {
        this.callParent(arguments);
        this.formId = null;
    },
    /**
     * Sets either the checked/unchecked status of this Radio, or, if a string value is passed,
     * checks a sibling Radio of the same name whose value is the value specified.
     * @param {String/Boolean} value Checked value, or the value of the sibling radio button
     * to check.
     * @return {Ext.form.field.Radio} this
     */
    setValue: function(value) {
        var me = this,
            active;
        if (Ext.isBoolean(value)) {
            me.callParent(arguments);
        } else {
            active = me.getManager().getWithValue(me.name, value, me.getFormId()).getAt(0);
            if (active) {
                active.setValue(true);
            }
        }
        return me;
    },
    /**
     * Returns the submit value for the checkbox which can be used when submitting forms.
     * @return {Boolean/Object} True if checked, null if not.
     */
    getSubmitValue: function() {
        return this.checked ? this.inputValue : null;
    },
    onChange: function(newVal, oldVal) {
        var me = this,
            ownerCt = me.ownerCt,
            r, rLen, radio, radios;
        me.callParent(arguments);
        // Standard compliant browsers only fire change event on the radio button
        // that became checked so we need to update other buttons in the group.
        // See also IE8 override.
        if (!me.$groupChange) {
            if (newVal) {
                radios = me.getManager().getByName(me.name, me.getFormId()).items;
                rLen = radios.length;
                for (r = 0; r < rLen; r++) {
                    radio = radios[r];
                    if (radio !== me) {
                        radio.updateValueFromDom();
                    }
                }
            }
            if (ownerCt && ownerCt.isRadioGroup && ownerCt.simpleValue) {
                ownerCt.checkChange();
            }
        }
    },
    getManager: function() {
        return Ext.form.RadioManager;
    }
});

/**
 * A {@link Ext.form.FieldContainer field container} which has a specialized layout for arranging
 * {@link Ext.form.field.Radio} controls into columns, and provides convenience
 * {@link Ext.form.field.Field} methods for {@link #getValue getting}, {@link #setValue setting},
 * and {@link #validate validating} the group of radio buttons as a whole.
 *
 * ## Validation
 *
 * Individual radio buttons themselves have no default validation behavior, but
 * sometimes you want to require a user to select one of a group of radios. RadioGroup
 * allows this by setting the config `{@link #allowBlank}:false`; when the user does not check at
 * one of the radio buttons, the entire group will be highlighted as invalid and the
 * {@link #blankText error message} will be displayed according to the {@link #msgTarget} config.
 *
 * ## Layout
 *
 * The default layout for RadioGroup makes it easy to arrange the radio buttons into
 * columns; see the {@link #columns} and {@link #vertical} config documentation for details.
 * You may also use a completely different layout by setting the {@link #cfg-layout} to one of the 
 * other supported layout types; for instance you may wish to use a custom arrangement 
 * of hbox and vbox containers. In that case the Radio components at any depth will 
 * still be managed by the RadioGroup's validation.
 *
 * ## Example usage
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'RadioGroup Example',
 *         width: 300,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items:[{
 *             xtype: 'radiogroup',
 *             fieldLabel: 'Two Columns',
 *             // Arrange radio buttons into two columns, distributed vertically
 *             columns: 2,
 *             vertical: true,
 *             items: [
 *                 { boxLabel: 'Item 1', name: 'rb', inputValue: '1' },
 *                 { boxLabel: 'Item 2', name: 'rb', inputValue: '2', checked: true},
 *                 { boxLabel: 'Item 3', name: 'rb', inputValue: '3' },
 *                 { boxLabel: 'Item 4', name: 'rb', inputValue: '4' },
 *                 { boxLabel: 'Item 5', name: 'rb', inputValue: '5' },
 *                 { boxLabel: 'Item 6', name: 'rb', inputValue: '6' }
 *             ]
 *         }]
 *     });
 *
 * ## Example with value binding to the RadioGroup.  In the below example, "Item 2" will
 * initially be checked using `myValue: '2'` from the ViewModel.
 *
 *     @example
 *     Ext.define('MyApp.main.view.Main', {
 *         extend: 'Ext.app.ViewModel',
 *         alias: 'viewmodel.main',
 *         data: {
 *             myValue: '2'
 *         }
 *     });
 *
 *     Ext.create('Ext.form.Panel', {
 *         title: 'RadioGroup Example',
 *         viewModel: {
 *             type: 'main'
 *         },
 *         width: 300,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items:[{
 *             xtype: 'radiogroup',
 *             fieldLabel: 'Two Columns',
 *             // Arrange radio buttons into two columns, distributed vertically
 *             columns: 2,
 *             vertical: true,
 *             simpleValue: true,  // set simpleValue to true to enable value binding
 *             bind: '{myValue}',
 *             items: [
 *                 { boxLabel: 'Item 1', name: 'rb', inputValue: '1' },
 *                 { boxLabel: 'Item 2', name: 'rb', inputValue: '2' },
 *                 { boxLabel: 'Item 3', name: 'rb', inputValue: '3' },
 *                 { boxLabel: 'Item 4', name: 'rb', inputValue: '4' },
 *                 { boxLabel: 'Item 5', name: 'rb', inputValue: '5' },
 *                 { boxLabel: 'Item 6', name: 'rb', inputValue: '6' }
 *             ]
 *         }]
 *     });
 *
 */
Ext.define('Ext.form.RadioGroup', {
    extend: 'Ext.form.CheckboxGroup',
    xtype: 'radiogroup',
    /**
     * @property {Boolean} isRadioGroup
     * The value `true` to identify an object as an instance of this or derived class.
     * @readonly
     * @since 6.2.0
     */
    isRadioGroup: true,
    requires: [
        'Ext.form.field.Radio'
    ],
    /**
     * @cfg {Ext.form.field.Radio[]/Object[]} items
     * An Array of {@link Ext.form.field.Radio Radio}s or Radio config objects to arrange
     * in the group.
     */
    /**
     * @cfg {Boolean} allowBlank
     * True to allow every item in the group to be blank.
     * If allowBlank = false and no items are selected at validation time,
     * {@link #blankText} will be used as the error text.
     */
    allowBlank: true,
    /**
     * @cfg {String} blankText
     * Error text to display if the {@link #allowBlank} validation fails
     * @locale
     */
    blankText: 'You must select one item in this group',
    defaultType: 'radiofield',
    /**
     * @cfg {Boolean} [local=false]
     * By default, child {@link Ext.form.field.Radio radio} `name`s are scoped to the
     * encapsulating {@link Ext.form.Panel form panel} if any, of the document.
     *
     * If you are using multiple `RadioGroup`s each of which uses the same `name`
     * configuration in child {@link Ext.form.field.Radio radio}s, configure this as
     * `true` to scope the names to within this `RadioGroup`
     */
    local: false,
    /**
     * @cfg {Boolean} simpleValue
     * When set to `true` the `value` of this group of `radiofield` components will be
     * mapped to the `inputValue` of the checked item. This is, the `getValue` method
     * will return the `inputValue` of the checked item while `setValue` will check the
     * `radiofield` whose `inputValue` matches the given value.
     *
     * This field allows the `radiogroup` to participate in binding an entire group of
     * radio buttons to a single value.
     *
     * In the below example, "Item 2" will initially be checked using `myValue: '2'` from
     * the ViewModel.
     *
     *     @example
     *     Ext.define('MyApp.main.view.Main', {
     *         extend: 'Ext.app.ViewModel',
     *         alias: 'viewmodel.main',
     *         data: {
     *             myValue: '2'
     *         }
     *     });
     *
     *     Ext.create('Ext.form.Panel', {
     *         title: 'RadioGroup Example',
     *         viewModel: {
     *             type: 'main'
     *         },
     *         width: 300,
     *         bodyPadding: 10,
     *         renderTo: Ext.getBody(),
     *         items:[{
     *             xtype: 'radiogroup',
     *             fieldLabel: 'Two Columns',
     *             // Arrange radio buttons into two columns, distributed vertically
     *             columns: 2,
     *             vertical: true,
     *             simpleValue: true,  // set simpleValue to true to enable value binding
     *             bind: '{myValue}',
     *             items: [
     *                 { boxLabel: 'Item 1', name: 'rb', inputValue: '1' },
     *                 { boxLabel: 'Item 2', name: 'rb', inputValue: '2' },
     *                 { boxLabel: 'Item 3', name: 'rb', inputValue: '3' },
     *                 { boxLabel: 'Item 4', name: 'rb', inputValue: '4' },
     *                 { boxLabel: 'Item 5', name: 'rb', inputValue: '5' },
     *                 { boxLabel: 'Item 6', name: 'rb', inputValue: '6' }
     *             ]
     *         }]
     *     });
     *
     * @since 6.2.0
     */
    simpleValue: false,
    defaultBindProperty: 'value',
    /**
     * @private
     */
    groupCls: Ext.baseCSSPrefix + 'form-radio-group',
    ariaRole: 'radiogroup',
    initRenderData: function() {
        var me = this,
            data, ariaAttr;
        data = me.callParent();
        ariaAttr = data.ariaAttributes;
        if (ariaAttr) {
            ariaAttr['aria-required'] = !me.allowBlank;
            ariaAttr['aria-invalid'] = false;
        }
        return data;
    },
    lookupComponent: function(config) {
        var result = this.callParent([
                config
            ]);
        // Local means that the exclusivity of checking by name is scoped to this RadioGroup.
        // So multiple RadioGroups can be used which use the same Radio names.
        // This enables their use as a grid widget.
        if (this.local) {
            result.formId = this.getId();
        }
        return result;
    },
    getBoxes: function(query, root) {
        return (root || this).query('[isRadio]' + (query || ''));
    },
    checkChange: function() {
        var me = this,
            value, key;
        value = me.getValue();
        // Safari might throw an exception on trying to get the keys of a Number
        key = typeof value === 'object' && Ext.Object.getKeys(value)[0];
        // If the value is an array we skip out here because it's during a change
        // between multiple items, so we never want to fire a change
        if (me.simpleValue || (key && !Ext.isArray(value[key]))) {
            me.callParent(arguments);
        }
    },
    isEqual: function(value1, value2) {
        if (this.simpleValue) {
            return value1 === value2;
        }
        return this.callParent([
            value1,
            value2
        ]);
    },
    getValue: function() {
        var me = this,
            items = me.items.items,
            i, item, ret;
        if (me.simpleValue) {
            for (i = items.length; i-- > 0; ) {
                item = items[i];
                if (item.checked) {
                    ret = item.inputValue;
                    break;
                }
            }
        } else {
            ret = me.callParent();
        }
        return ret;
    },
    /**
     * Sets the value of the radio group. The radio with corresponding name and value will be set.
     * This method is simpler than {@link Ext.form.CheckboxGroup#setValue} because only 1 value
     * is allowed for each name. You can use the setValue method as:
     *
     *     var form = Ext.create('Ext.form.Panel', {
     *         title       : 'RadioGroup Example',
     *         width       : 300,
     *         bodyPadding : 10,
     *         renderTo    : Ext.getBody(),
     *         items       : [
     *             {
     *                 xtype      : 'radiogroup',
     *                 fieldLabel : 'Group',
     *                 items      : [
     *                     { boxLabel : 'Item 1', name : 'rb', inputValue : 1 },
     *                     { boxLabel : 'Item 2', name : 'rb', inputValue : 2 }
     *                 ]
     *             }
     *         ],
     *         tbar        : [
     *             {
     *                 text    : 'setValue on RadioGroup',
     *                 handler : function() {
     *                     form.child('radiogroup').setValue({
     *                         rb : 2
     *                     });
     *                 }
     *             }
     *         ]
     *     });
     *
     * @param {Mixed} value An Object to map names to values to be set. If not an Object,
     * this `radiofield` with a matching `inputValue` will be checked.
     * @return {Ext.form.RadioGroup} this
     */
    setValue: function(value) {
        var items = this.items,
            cbValue, cmp, formId, radios, i, len, name;
        Ext.suspendLayouts();
        if (this.simpleValue) {
            for (i = 0 , len = items.length; i < len; ++i) {
                cmp = items.items[i];
                cmp.$groupChange = true;
                cmp.setValue(cmp.inputValue === value);
                delete cmp.$groupChange;
            }
        } else if (Ext.isObject(value)) {
            cmp = items.first();
            formId = cmp ? cmp.getFormId() : null;
            for (name in value) {
                cbValue = value[name];
                radios = Ext.form.RadioManager.getWithValue(name, cbValue, formId).items;
                len = radios.length;
                for (i = 0; i < len; ++i) {
                    radios[i].setValue(true);
                }
            }
        }
        Ext.resumeLayouts(true);
        return this;
    },
    markInvalid: function(errors) {
        var ariaDom = this.ariaEl.dom;
        this.callParent([
            errors
        ]);
        if (ariaDom) {
            ariaDom.setAttribute('aria-invalid', true);
        }
    },
    clearInvalid: function() {
        var ariaDom = this.ariaEl.dom;
        this.callParent();
        if (ariaDom) {
            ariaDom.setAttribute('aria-invalid', false);
        }
    }
}, function() {
    // Firefox has a nasty bug, or a misfeature, with tabbing over radio buttons
    // when there is no checked button in a group. In such case the first button
    // in the group should be focused upon tabbing into the group, and subsequent
    // tab key press should leave the group; however Firefox will tab over every
    // radio button individually as if they were checkboxes.
    // Fortunately for us this bugfeature only applies to tabbing; arrow key
    // navigation is not affected. So the issue can be easily worked around
    // by removing all group buttons from tab order upon focusing the first button
    // in the group, and restoring their tabbable state upon focusleave.
    // This works exactly the same way regardless of having or not a checked button
    // in the group, so we keep the code simple.
    // This condition should get more version specific when this bug is fixed:
    // https://bugzilla.mozilla.org/show_bug.cgi?id=1267488
    if (Ext.isGecko) {
        this.override({
            onFocusEnter: function(e) {
                var target = e.toComponent,
                    radios, i, len;
                if (target.isRadio) {
                    radios = target.getManager().getByName(target.name, target.getFormId()).items;
                    for (i = 0 , len = radios.length; i < len; i++) {
                        radios[i].disableTabbing();
                    }
                }
            },
            onFocusLeave: function(e) {
                var target = e.fromComponent,
                    radios, i, len;
                if (target.isRadio) {
                    radios = target.getManager().getByName(target.name, target.getFormId()).items;
                    for (i = 0 , len = radios.length; i < len; i++) {
                        radios[i].enableTabbing();
                    }
                }
            }
        });
    }
});

/**
 * @class Ext.form.action.DirectAction
 * A mixin that contains methods specific to Ext Direct actions shared
 * by DirectLoad and DirectSubmit.
 * @private
 */
Ext.define('Ext.form.action.DirectAction', {
    extend: 'Ext.Mixin',
    mixinConfig: {
        id: 'directaction'
    },
    resolveMethod: function(type) {
        var me = this,
            form = me.form,
            api, fn;
        api = Ext.direct.Manager.resolveApi(form.api, me);
        if (!api) {
            Ext.raise("Cannot resolve Ext Direct API method for " + type + " action; form " + form.id + " has no api object defined");
        }
        fn = api[type];
        if (!fn) {
            Ext.raise("Cannot resolve Ext Direct API method for " + type + " action");
        }
        return fn;
    }
});

/**
 * Provides Ext Direct support for loading form data.
 *
 * This example illustrates usage of Ext Direct to load a form.
 *
 *     var myFormPanel = new Ext.form.Panel({
 *         // configs for FormPanel
 *         title: 'Basic Information',
 *         renderTo: document.body,
 *         width: 300, height: 160,
 *         padding: 10,
 *
 *         // configs apply to child items
 *         defaults: {anchor: '100%'},
 *         defaultType: 'textfield',
 *         items: [{
 *             fieldLabel: 'Name',
 *             name: 'name'
 *         },{
 *             fieldLabel: 'Email',
 *             name: 'email'
 *         },{
 *             fieldLabel: 'Company',
 *             name: 'company'
 *         }],
 *
 *         // configs for BasicForm
 *         api: {
 *             // The server-side method to call for load() requests
 *             load: 'Profile.getBasicInfo',
 *             // The server-side must mark the submit handler as a 'formHandler'
 *             submit: 'Profile.updateBasicInfo'
 *         },
 *         // specify the order for the passed params
 *         paramOrder: ['uid', 'foo']
 *     });
 *
 *     // load the form
 *     myFormPanel.getForm().load({
 *         // pass 2 arguments to server side getBasicInfo method (len=2)
 *         params: {
 *             foo: 'bar',
 *             uid: 34
 *         }
 *     });
 *
 * Before using DirectLoad action, make sure you set up Ext Direct remoting provider.
 * See {@link Ext.direct.Manager} for more information.
 *
 * For corresponding submit action, see {@link Ext.form.action.DirectSubmit}.
 */
Ext.define('Ext.form.action.DirectLoad', {
    extend: 'Ext.form.action.Load',
    alternateClassName: 'Ext.form.Action.DirectLoad',
    alias: 'formaction.directload',
    requires: [
        'Ext.direct.Manager'
    ],
    mixins: [
        'Ext.form.action.DirectAction'
    ],
    type: 'directload',
    run: function() {
        var me = this,
            form = me.form,
            metadata = me.metadata || form.metadata,
            timeout = me.timeout || form.timeout,
            args, fn;
        fn = me.resolveMethod('load');
        args = fn.directCfg.method.getArgs({
            params: me.getParams(),
            paramOrder: form.paramOrder,
            paramsAsHash: form.paramsAsHash,
            options: timeout != null ? {
                timeout: timeout * 1000
            } : null,
            metadata: metadata,
            callback: me.onComplete,
            scope: me
        });
        fn.apply(window, args);
    },
    // Direct actions have already been processed and therefore
    // we can directly set the result; Direct Actions do not have
    // a this.response property.
    processResponse: function(result) {
        return (this.result = result);
    },
    onComplete: function(data) {
        if (data) {
            this.onSuccess(data);
        } else {
            this.onFailure(null);
        }
    }
});

/**
 * Provides Ext Direct support for submitting form data.
 *
 * This example illustrates usage of Ext Direct to submit a form.
 *
 *     var myFormPanel = new Ext.form.Panel({
 *         // configs for FormPanel
 *         title: 'Basic Information',
 *         renderTo: document.body,
 *         width: 300, height: 160,
 *         padding: 10,
 *         buttons:[{
 *             text: 'Submit',
 *             handler: function(){
 *                 myFormPanel.getForm().submit({
 *                     params: {
 *                         foo: 'bar',
 *                         uid: 34
 *                     }
 *                 });
 *             }
 *         }],
 *
 *         // configs apply to child items
 *         defaults: {anchor: '100%'},
 *         defaultType: 'textfield',
 *         items: [{
 *             fieldLabel: 'Name',
 *             name: 'name'
 *         },{
 *             fieldLabel: 'Email',
 *             name: 'email'
 *         },{
 *             fieldLabel: 'Company',
 *             name: 'company'
 *         }],
 *
 *         // configs for BasicForm
 *         api: {
 *             // The server-side method to call for load() requests
 *             load: 'Profile.getBasicInfo',
 *             // The server-side must mark the submit handler as a 'formHandler'
 *             submit: 'Profile.updateBasicInfo'
 *         },
 *         // specify the order for the passed params
 *         paramOrder: ['uid', 'foo']
 *     });
 *
 * Before using DirectLoad action, make sure you set up Ext Direct remoting provider.
 * See {@link Ext.direct.Manager} for more information.
 *
 * For corresponding load action, see {@link Ext.form.action.DirectLoad}.
 */
Ext.define('Ext.form.action.DirectSubmit', {
    extend: 'Ext.form.action.Submit',
    alternateClassName: 'Ext.form.Action.DirectSubmit',
    alias: 'formaction.directsubmit',
    requires: [
        'Ext.direct.Manager'
    ],
    mixins: [
        'Ext.form.action.DirectAction'
    ],
    type: 'directsubmit',
    doSubmit: function() {
        var me = this,
            form = me.form,
            metadata = me.metadata || form.metadata,
            timeout = me.timeout || form.timeout,
            fn, formInfo, args;
        fn = me.resolveMethod('submit');
        formInfo = me.buildForm();
        args = fn.directCfg.method.getArgs({
            params: formInfo.formEl,
            options: timeout != null ? {
                timeout: timeout * 1000
            } : null,
            metadata: metadata,
            callback: me.onComplete,
            scope: me
        });
        fn.apply(window, args);
        me.cleanup(formInfo);
    },
    // Direct actions have already been processed and therefore
    // we can directly set the result; Direct Actions do not have
    // a this.response property.
    processResponse: function(result) {
        return (this.result = result);
    },
    onComplete: function(data) {
        if (data) {
            this.onSuccess(data);
        } else {
            this.onFailure(null);
        }
    }
});

/**
 * An abstract class for fields that have a single trigger which opens a "picker" popup below
 * the field, e.g. a combobox menu list or a date picker. It provides a base implementation
 * for toggling the picker's visibility when the trigger is clicked, as well as keyboard navigation
 * and some basic events. Sizing and alignment of the picker can be controlled via the
 * {@link #matchFieldWidth} and {@link #pickerAlign}/{@link #pickerOffset} config properties
 * respectively.
 *
 * You would not normally use this class directly, but instead use it as the parent class
 * for a specific picker field implementation. Subclasses must implement the {@link #createPicker}
 * method to create a picker component appropriate for the field.
 */
Ext.define('Ext.form.field.Picker', {
    extend: 'Ext.form.field.Text',
    alias: 'widget.pickerfield',
    alternateClassName: 'Ext.form.Picker',
    requires: [
        'Ext.util.KeyNav'
    ],
    config: {
        triggers: {
            picker: {
                handler: 'onTriggerClick',
                scope: 'this',
                focusOnMousedown: true
            }
        }
    },
    renderConfig: {
        /**
         * @cfg {Boolean} editable
         * False to prevent the user from typing text directly into the field; the field can only
         * have its value set via selecting a value from the picker. In this state, the picker
         * can also be opened by clicking directly on the input field itself.
         */
        editable: true
    },
    keyMap: {
        scope: 'this',
        DOWN: 'onDownArrow',
        ESC: 'onEsc'
    },
    keyMapTarget: 'inputEl',
    /**
     * @property {Boolean} isPickerField
     * `true` in this class to identify an object as an instantiated Picker Field,
     * or subclass thereof.
     */
    isPickerField: true,
    /**
     * @cfg {Boolean} matchFieldWidth
     * Whether the picker dropdown's width should be explicitly set to match the width of the field.
     * Defaults to true.
     */
    matchFieldWidth: true,
    /**
     * @cfg {String} pickerAlign
     * The {@link Ext.util.Positionable#alignTo alignment position} with which to align the picker.
     * Defaults to "tl-bl?"
     */
    pickerAlign: 'tl-bl?',
    /**
     * @cfg {Number[]} pickerOffset
     * An offset [x,y] to use in addition to the {@link #pickerAlign} when positioning the picker.
     * Defaults to undefined.
     */
    /**
     * @cfg {String} [openCls='x-pickerfield-open']
     * A class to be added to the field's {@link #bodyEl} element when the picker is opened.
     */
    openCls: Ext.baseCSSPrefix + 'pickerfield-open',
    /**
     * @property {Boolean} isExpanded
     * True if the picker is currently expanded, false if not.
     */
    isExpanded: false,
    /**
     * @cfg {String} triggerCls
     * An additional CSS class used to style the trigger button. The trigger will always
     * get the class 'x-form-trigger' and triggerCls will be appended if specified.
     */
    /**
     * @event expand
     * Fires when the field's picker is expanded.
     * @param {Ext.form.field.Picker} field This field instance
     */
    /**
     * @event collapse
     * Fires when the field's picker is collapsed.
     * @param {Ext.form.field.Picker} field This field instance
     */
    /**
     * @event select
     * Fires when a value is selected via the picker.
     * @param {Ext.form.field.Picker} field This field instance
     * @param {Object} value The value that was selected. The exact type of this value
     * is dependent on the individual field and picker implementations.
     */
    applyTriggers: function(triggers) {
        var me = this,
            picker = triggers.picker;
        if (!picker.cls) {
            picker.cls = me.triggerCls;
        }
        return me.callParent([
            triggers
        ]);
    },
    getSubTplData: function(fieldData) {
        var me = this,
            data, ariaAttr;
        data = me.callParent([
            fieldData
        ]);
        if (!me.ariaStaticRoles[me.ariaRole]) {
            ariaAttr = data.ariaElAttributes;
            if (ariaAttr) {
                ariaAttr['aria-haspopup'] = true;
                // Picker fields start as collapsed
                ariaAttr['aria-expanded'] = false;
            }
        }
        return data;
    },
    initEvents: function() {
        this.callParent();
        // Disable native browser autocomplete
        if (Ext.isGecko) {
            this.inputEl.dom.setAttribute('autocomplete', 'off');
        }
    },
    updateEditable: function(editable, oldEditable) {
        var me = this;
        // Non-editable allows opening the picker by clicking the field
        if (!editable) {
            me.inputEl.on('click', me.onInputElClick, me);
        } else {
            me.inputEl.un('click', me.onInputElClick, me);
        }
        me.callParent([
            editable,
            oldEditable
        ]);
    },
    /**
     * @private
     */
    onEsc: function(e) {
        if (Ext.isIE) {
            // Stop the esc key from "restoring" the previous value in IE
            // For example, type "foo". Highlight all the text, hit backspace.
            // Hit esc, "foo" will be restored. This behaviour doesn't occur
            // in any other browsers
            e.preventDefault();
        }
        if (this.isExpanded) {
            this.collapse();
            e.stopEvent();
        }
    },
    onDownArrow: function(e) {
        var me = this;
        if ((e.time - me.lastDownArrow) > 150) {
            delete me.lastDownArrow;
        }
        if (!me.isExpanded) {
            // Do not let the down arrow event propagate into the picker
            e.stopEvent();
            // Don't call expand() directly as there may be additional processing involved before
            // expanding, e.g. in the case of a ComboBox query.
            me.onTriggerClick(me, me.getPickerTrigger(), e);
            me.lastDownArrow = e.time;
        } else if (!e.stopped && (e.time - me.lastDownArrow) < 150) {
            delete me.lastDownArrow;
        }
    },
    /**
     * Expands this field's picker dropdown.
     */
    expand: function() {
        var me = this,
            bodyEl, picker, doc;
        if (me.rendered && !me.isExpanded && !me.destroyed) {
            bodyEl = me.bodyEl;
            picker = me.getPicker();
            doc = Ext.getDoc();
            picker.setMaxHeight(picker.initialConfig.maxHeight);
            if (me.matchFieldWidth) {
                picker.setWidth(me.bodyEl.getWidth());
            }
            // Show the picker and set isExpanded flag. alignPicker only works if isExpanded.
            picker.show();
            me.isExpanded = true;
            me.alignPicker();
            bodyEl.addCls(me.openCls);
            if (!me.ariaStaticRoles[me.ariaRole]) {
                if (!me.ariaEl.dom.hasAttribute('aria-owns')) {
                    me.ariaEl.dom.setAttribute('aria-owns', picker.listEl ? picker.listEl.id : picker.el.id);
                }
                me.ariaEl.dom.setAttribute('aria-expanded', true);
            }
            // Collapse on touch outside this component tree.
            // Because touch platforms do not focus document.body on touch
            // so no focusleave would occur to trigger a collapse.
            me.touchListeners = doc.on({
                // Do not translate on non-touch platforms.
                // mousedown will blur the field.
                translate: false,
                touchstart: me.collapseIf,
                scope: me,
                delegated: false,
                destroyable: true
            });
            // Scrolling of anything which causes this field to move should collapse
            me.scrollListeners = Ext.on({
                scroll: me.onGlobalScroll,
                scope: me,
                destroyable: true
            });
            // Buffer is used to allow any layouts to complete before we align
            Ext.on('resize', me.alignPicker, me, {
                buffer: 1
            });
            me.fireEvent('expand', me);
            me.onExpand();
        }
    },
    onExpand: Ext.emptyFn,
    /**
     * Aligns the picker to the input element
     * @protected
     */
    alignPicker: function() {
        var me = this,
            picker;
        if (me.rendered && !me.destroyed) {
            picker = me.getPicker();
            if (picker.isVisible() && picker.isFloating()) {
                me.doAlign();
            }
        }
    },
    /**
     * Performs the alignment on the picker using the class defaults
     * @private
     */
    doAlign: function() {
        var me = this,
            picker = me.picker,
            aboveSfx = '-above',
            newPos, isAbove;
        // Align to the trigger wrap because the border isn't always on the input element, which
        // can cause the offset to be off
        picker.el.alignTo(me.triggerWrap, me.pickerAlign, me.pickerOffset);
        // We used *element* alignTo to bypass the automatic reposition on scroll which
        // Floating#alignTo does. So we must sync the Component state.
        newPos = picker.floatParent ? picker.getOffsetsTo(picker.floatParent.getTargetEl()) : picker.getXY();
        picker.x = newPos[0];
        picker.y = newPos[1];
        // add the {openCls}-above class if the picker was aligned above
        // the field due to hitting the bottom of the viewport
        isAbove = picker.el.getY() < me.inputEl.getY();
        me.bodyEl[isAbove ? 'addCls' : 'removeCls'](me.openCls + aboveSfx);
        picker[isAbove ? 'addCls' : 'removeCls'](picker.baseCls + aboveSfx);
    },
    /**
     * Collapses this field's picker dropdown.
     */
    collapse: function() {
        var me = this,
            openCls = me.openCls,
            aboveSfx = '-above',
            picker;
        if (me.isExpanded && !me.destroyed && !me.destroying) {
            picker = me.picker;
            // hide the picker and set isExpanded flag
            picker.hide();
            me.isExpanded = false;
            // remove the openCls
            me.bodyEl.removeCls([
                openCls,
                openCls + aboveSfx
            ]);
            picker.el.removeCls(picker.baseCls + aboveSfx);
            if (!me.ariaStaticRoles[me.ariaRole]) {
                me.ariaEl.dom.setAttribute('aria-expanded', false);
            }
            // remove event listeners
            me.touchListeners.destroy();
            me.scrollListeners.destroy();
            Ext.un('resize', me.alignPicker, me);
            me.fireEvent('collapse', me);
            me.onCollapse();
        }
    },
    onCollapse: Ext.emptyFn,
    /**
     * @private
     * Runs on touchstart of doc to check to see if we should collapse the picker.
     */
    collapseIf: function(e) {
        var me = this;
        // If what was mousedowned on is outside of this Field, and is not focusable, then collapse.
        // If it is focusable, this Field will blur and collapse anyway.
        if (!me.destroyed && !e.within(me.bodyEl, false, true) && !me.owns(e.target) && !Ext.fly(e.target).isFocusable()) {
            me.collapse();
        }
    },
    /**
     * Returns a reference to the picker component for this field, creating it if necessary by
     * calling {@link #createPicker}.
     * @return {Ext.Component} The picker component
     */
    getPicker: function() {
        var me = this,
            picker = me.picker;
        if (!picker) {
            me.creatingPicker = true;
            me.picker = picker = me.createPicker();
            // For upward component searches.
            picker.ownerCmp = me;
            delete me.creatingPicker;
        }
        return me.picker;
    },
    // When focus leaves the picker component, if it's to outside of this
    // Component's hierarchy
    onFocusLeave: function(e) {
        this.collapse();
        this.callParent([
            e
        ]);
    },
    /**
     * @private
     * The CQ interface. Allow drilling down into the picker when it exists.
     * Important for determining whether an event took place in the bounds of some
     * higher level containing component. See AbstractComponent#owns
     */
    getRefItems: function() {
        var result = [];
        if (this.picker) {
            result[0] = this.picker;
        }
        return result;
    },
    getPickerTrigger: function() {
        return this.triggers && this.triggers.picker;
    },
    /**
     * @method
     * Creates and returns the component to be used as this field's picker.
     * Must be implemented by subclasses of Picker.
     */
    createPicker: Ext.emptyFn,
    onInputElClick: function(e) {
        this.onTriggerClick(this, this.getPickerTrigger(), e);
    },
    /**
     * Handles the trigger click; by default toggles between expanding and collapsing
     * the picker component.
     * @protected
     * @param {Ext.form.field.Picker} field This field instance.
     * @param {Ext.form.trigger.Trigger} trigger This field's picker trigger.
     * @param {Ext.event.Event} e The event that generated this call.
     */
    onTriggerClick: function(field, trigger, e) {
        var me = this;
        if (!me.readOnly && !me.disabled) {
            if (me.isExpanded) {
                me.collapse();
            } else {
                me.expand();
            }
        }
    },
    doDestroy: function() {
        var me = this,
            picker = me.picker;
        Ext.un('resize', me.alignPicker, me);
        Ext.destroy(me.keyNav, picker);
        if (picker) {
            me.picker = picker.pickerField = null;
        }
        me.callParent();
    },
    privates: {
        onGlobalScroll: function(scroller) {
            var scrollEl = scroller.getElement();
            // Collapse if the scroll is anywhere but inside the picker
            if (!this.picker.owns(scrollEl) && scrollEl.isAncestor(this.el)) {
                this.collapse();
            }
        }
    }
});

/**
 * Tracks what records are currently selected in a data-bound component.
 *
 * This is an abstract class and is not meant to be directly used. Data-bound UI widgets such as
 * {@link Ext.grid.Panel Grid} and {@link Ext.tree.Panel Tree} should subclass Ext.selection.Model
 * and provide a way to binding to the component.
 *
 * The abstract method `onSelectChange` should be implemented in these
 * subclasses to update the UI widget.
 */
Ext.define('Ext.selection.Model', {
    extend: 'Ext.mixin.Observable',
    alternateClassName: 'Ext.AbstractSelectionModel',
    alias: 'selection.abstract',
    requires: [
        'Ext.util.Bag'
    ],
    mixins: [
        'Ext.util.StoreHolder',
        'Ext.mixin.Factoryable'
    ],
    factoryConfig: {
        // Need to override the defaultType, otherwise this class would be the default,
        // and it is an abstract base.
        defaultType: 'dataviewmodel'
    },
    // We do not want "_hidden" style backing properties.
    $configPrefixed: false,
    // We also want non-config system properties to go to the instance.
    $configStrict: false,
    config: {
        /**
         * @private
         * The {@link Ext.data.Store store} in which this selection model represents
         * the selected subset.
         */
        store: null,
        /**
         * @private
         * The {@link Ext.util.Collection collection} or {@link Ext.util.Bag bag} of
         * selected records.
         */
        selected: {}
    },
    // lastSelected
    /**
     * @property {Boolean} isSelectionModel
     * `true` in this class to identify an object as an instantiated
     * {@link Ext.selection.Model selection model}, or subclass thereof.
     */
    isSelectionModel: true,
    /**
     * @cfg {"SINGLE"/"SIMPLE"/"MULTI"} mode
     * Mode of selection.  Valid values are:
     *
     * - **"SINGLE"** - Only allows selecting one item at a time.  Use {@link #allowDeselect}
     *   to allow deselecting that item.  Also see {@link #toggleOnClick}. This is the default.
     * - **"SIMPLE"** - Allows simple selection of multiple items one-by-one. Each click in grid
     *   will either select or deselect an item.
     * - **"MULTI"** - Allows complex selection of multiple items using Ctrl and Shift keys.
     */
    /**
     * @cfg {Boolean} allowDeselect
     * Allow users to deselect a record in a DataView, List or Grid.
     * Only applicable when the {@link #mode} is 'SINGLE'.
     */
    allowDeselect: undefined,
    /**
     * @cfg {Boolean} toggleOnClick
     * `true` to toggle the selection state of an item when clicked.
     * Only applicable when the {@link #mode} is 'SINGLE'.
     * Only applicable when the {@link #allowDeselect} is 'true'.
     */
    toggleOnClick: true,
    /**
     * @cfg {Boolean} ordered
     * If this is set to `true`, the selected items will be tracked in an instance of
     * {Ext.util.Collection} instead of {Ext.util.Bag} to maintain insertion order.
     * @private
     */
    ordered: false,
    /**
     * @property {Ext.util.Bag/Ext.util.Collection} selected
     * A collection that maintains all of the currently selected records. The type
     * of collection class depends on the `ordered` config.
     * @readonly
     * @private
     */
    selected: null,
    /**
     * @cfg {Boolean} [pruneRemoved=true]
     * Remove records from the selection when they are removed from the store.
     *
     * **Important:** When using {@link Ext.toolbar.Paging paging} or a
     * {@link Ext.data.BufferedStore}, records which are cached in the Store's
     * {@link Ext.data.Store#property-data data collection} may be removed from the Store
     * when pages change, or when rows are scrolled out of view. For this reason `pruneRemoved`
     * should be set to `false` when using a buffered Store.
     *
     * Also, when previously pruned pages are returned to the cache, the records objects in the page
     * will be *new instances*, and will not match the instances in the selection model's
     * collection. For this reason, you MUST ensure that the Model definition's
     * {@link Ext.data.Model#idProperty idProperty} references a unique key because
     * in this situation, records in the Store have their **IDs** compared to records
     * in the SelectionModel in order to re-select a record which is scrolled back into view.
     */
    pruneRemoved: true,
    suspendChange: 0,
    /**
     * @cfg {Boolean} [ignoreRightMouseSelection=false]
     * True to ignore selections that are made when using the right mouse button if there are
     * records that are already selected. If no records are selected, selection will continue
     * as normal
     */
    ignoreRightMouseSelection: false,
    /**
     * @event selectionchange
     * Fired after a selection change has occurred
     * @param {Ext.selection.Model} this
     * @param {Ext.data.Model[]} selected The selected records
     */
    /**
     * @event focuschange
     * Fired when a row is focused
     * @param {Ext.selection.Model} this
     * @param {Ext.data.Model} oldFocused The previously focused record
     * @param {Ext.data.Model} newFocused The newly focused record
     */
    constructor: function(cfg) {
        var me = this;
        me.modes = {
            SINGLE: true,
            SIMPLE: true,
            MULTI: true
        };
        me.callParent([
            cfg
        ]);
        // sets this.selectionMode
        me.setSelectionMode(me.mode);
        if (me.selectionMode !== 'SINGLE') {
            me.allowDeselect = true;
        }
    },
    updateStore: function(store, oldStore) {
        this.bindStore(store, !oldStore);
    },
    applySelected: function(selected) {
        if (!selected.isBag && !selected.isCollection) {
            selected = new Ext.util[this.ordered ? 'Collection' : 'Bag'](Ext.apply({
                rootProperty: 'data'
            }, selected));
        }
        return selected;
    },
    getStoreListeners: function() {
        var me = this;
        return {
            add: me.onStoreAdd,
            clear: me.onStoreClear,
            remove: me.onStoreRemove,
            update: me.onStoreUpdate,
            idchanged: me.onIdChanged,
            load: me.onStoreLoad,
            refresh: me.onStoreRefresh,
            // BufferedStore events
            pageadd: me.onPageAdd,
            pageremove: me.onPageRemove
        };
    },
    onBindStore: function(store, oldStore, initial) {
        if (!initial) {
            this.updateSelectedInstances(this.selected);
        }
    },
    suspendChanges: function() {
        ++this.suspendChange;
    },
    resumeChanges: function() {
        if (this.suspendChange) {
            --this.suspendChange;
        }
    },
    /**
     * Selects all records in the view.
     * @param {Boolean} suppressEvent True to suppress any select events
     */
    selectAll: function(suppressEvent) {
        var me = this,
            selections = me.store.getRange(),
            start = me.getSelection().length;
        me.suspendChanges();
        me.doSelect(selections, true, suppressEvent);
        if (!me.destroyed) {
            me.resumeChanges();
            // fire selection change only if the number of selections differs
            if (!suppressEvent) {
                me.maybeFireSelectionChange(me.getSelection().length !== start);
            }
        }
    },
    /**
     * Deselects all records in the view.
     * @param {Boolean} [suppressEvent] True to suppress any deselect events
     */
    deselectAll: function(suppressEvent) {
        var me = this,
            selections = me.getSelection(),
            selIndexes = {},
            store = me.store,
            start = selections.length,
            i, l, rec;
        // Cache selection records' indexes first to avoid
        // looking them up on every sort comparison below.
        // We can't rely on store.indexOf being fast because
        // for whatever reason the Store in question may force
        // sequential index lookup, which will result in O(n^2)
        // sort performance below.
        for (i = 0 , l = selections.length; i < l; i++) {
            rec = selections[i];
            selIndexes[rec.id] = store.indexOf(rec);
        }
        // Sort the selections so that the events fire in
        // a predictable order like selectAll
        selections = Ext.Array.sort(selections, function(r1, r2) {
            var idx1 = selIndexes[r1.id],
                idx2 = selIndexes[r2.id];
            // Don't check for equality since indexes will be unique
            return idx1 < idx2 ? -1 : 1;
        });
        me.suspendChanges();
        me.doDeselect(selections, suppressEvent);
        if (!me.destroyed) {
            me.resumeChanges();
            // fire selection change only if the number of selections differs
            if (!suppressEvent) {
                me.maybeFireSelectionChange(me.getSelection().length !== start);
            }
        }
    },
    getSelectionStart: function() {
        return this.selectionStart;
    },
    setSelectionStart: function(selection) {
        this.selectionStart = selection;
    },
    // Provides differentiation of logic between MULTI, SIMPLE and SINGLE
    // selection modes. Requires that an event be passed so that we can know
    // if user held ctrl or shift.
    selectWithEvent: function(record, e) {
        var me = this,
            isSelected = me.isSelected(record),
            shift = e.shiftKey;
        switch (me.selectionMode) {
            case 'MULTI':
                me.selectWithEventMulti(record, e, isSelected);
                break;
            case 'SIMPLE':
                me.selectWithEventSimple(record, e, isSelected);
                break;
            case 'SINGLE':
                me.selectWithEventSingle(record, e, isSelected);
                break;
        }
        // Event handlers could have destroyed the selection model
        if (me.destroyed) {
            return;
        }
        // selectionStart is a start point for shift/mousedown to create a range from.
        // If the mousedowned record was not already selected, then it becomes the
        // start of any range created from now on.
        // If we drop to no records selected, then there is no range start any more.
        if (!shift) {
            if (me.isSelected(record)) {
                me.selectionStart = record;
            } else {
                me.selectionStart = null;
            }
        }
    },
    /**
     * Checks whether a selection should proceed based on the ignoreRightMouseSelection
     * option.
     * @private
     * @param {Ext.event.Event} e The event
     * @return {Boolean} `true` if the selection should not proceed.
     */
    vetoSelection: function(e) {
        // Flag can be stamped into the event at any time.
        // This is used by ActionColumn and CheckColumn to implement their stopSelection config
        if (e.stopSelection) {
            return true;
        } else if (e.type !== 'keydown' && e.button !== 0) {
            if (this.ignoreRightMouseSelection || this.isSelected(e.record)) {
                return true;
            }
        } else {
            return e.type === 'mousedown';
        }
    },
    // Private
    // Called in response to a FocusModel's navigate event when a new record has been navigated to.
    // Event is passed so that shift and ctrl can be handled.
    onNavigate: function(e) {
        // Enforce the ignoreRightMouseSelection setting.
        // Enforce presence of a record.
        // Enforce selection upon click, not mousedown.
        if (!e.record || this.vetoSelection(e.keyEvent)) {
            return;
        }
        this.onBeforeNavigate(e);
        // eslint-disable-next-line vars-on-top
        var me = this,
            keyEvent = e.keyEvent,
            // ctrlKey may be set on the event if we want to treat it like a ctrlKey so
            // we don't mutate the original event object
            ctrlKey = keyEvent.ctrlKey || e.ctrlKey,
            recIdx = e.recordIndex,
            record = e.record,
            lastFocused = e.previousRecord,
            isSelected = me.isSelected(record),
            from = (me.selectionStart && me.isSelected(e.previousRecord)) ? me.selectionStart : (me.selectionStart = e.previousRecord),
            fromIdx = e.previousRecordIndex,
            key = keyEvent.getCharCode(),
            isSpace = key === keyEvent.SPACE,
            changedRec = e.record !== e.previousRecord,
            direction;
        direction = key === keyEvent.UP || key === keyEvent.PAGE_UP || key === keyEvent.HOME || (key === keyEvent.LEFT && changedRec) ? 'up' : (key === keyEvent.DOWN || key === keyEvent.PAGE_DOWN || key === keyEvent.END || (key === keyEvent.RIGHT && changedRec) ? 'down' : null);
        switch (me.selectionMode) {
            case 'MULTI':
                me.setSelectionStart(e.selectionStart);
                if (key === keyEvent.A && ctrlKey) {
                    // Listening to endUpdate on the Collection will be more efficient
                    me.selected.beginUpdate();
                    me.selectRange(0, me.store.getCount() - 1);
                    me.selected.endUpdate();
                } else if (isSpace) {
                    // SHIFT+SPACE, select range
                    if (keyEvent.shiftKey) {
                        me.selectRange(from, record, ctrlKey);
                    } else {
                        // SPACE pressed on a selected item: deselect.
                        if (isSelected) {
                            if (me.allowDeselect) {
                                me.doDeselect(record);
                            }
                        } else // SPACE on an unselected item: select it
                        // keyEvent.ctrlKey means "keep existing"
                        {
                            me.doSelect(record, ctrlKey);
                        }
                    }
                }
                // SHIFT-navigate selects intervening rows from the last selected
                // (or last focused) item and target item
                else if (keyEvent.shiftKey && from) {
                    // If we are heading back TOWARDS the start rec - deselect skipped range...
                    if (direction === 'up' && fromIdx <= recIdx) {
                        me.deselectRange(lastFocused, recIdx + 1);
                    } else if (direction === 'down' && fromIdx >= recIdx) {
                        me.deselectRange(lastFocused, recIdx - 1);
                    }
                    // If we are heading AWAY from start point, or no CTRL key,
                    // so just select the range and let the CTRL control "keepExisting"...
                    else if (from !== record) {
                        me.selectRange(from, record, ctrlKey);
                    }
                    me.lastSelected = record;
                } else if (key) {
                    if (!ctrlKey) {
                        me.doSelect(record, false);
                    }
                } else {
                    me.selectWithEvent(record, keyEvent);
                };
                break;
            case 'SIMPLE':
                if (key === keyEvent.A && ctrlKey) {
                    // Listening to endUpdate on the Collection will be more efficient
                    me.selected.beginUpdate();
                    me.selectRange(0, me.store.getCount() - 1);
                    me.selected.endUpdate();
                } else if (isSelected) {
                    me.doDeselect(record);
                } else {
                    me.doSelect(record, true);
                };
                break;
            case 'SINGLE':
                // CTRL-navigation does not select
                if (!ctrlKey) {
                    // Arrow movement
                    if (direction) {
                        me.doSelect(record, false);
                    }
                    // Space or click
                    else if (isSpace || !key) {
                        me.selectWithEvent(record, keyEvent);
                    }
                };
                break;
        }
        // selectionStart is a start point for shift/mousedown to create a range from.
        // If the mousedowned record was not already selected, then it becomes the
        // start of any range created from now on.
        // If we drop to no records selected, then there is no range start any more.
        if (!keyEvent.shiftKey && !me.destroyed && me.isSelected(record)) {
            me.selectionStart = record;
            me.selectionStartIdx = recIdx;
        }
    },
    /**
     * Selects a range of rows if the selection model {@link #isLocked is not locked}.
     * All rows in between startRow and endRow are also selected.
     * @param {Ext.data.Model/Number} startRow The record or index of the first row in the range
     * @param {Ext.data.Model/Number} endRow The record or index of the last row in the range
     * @param {Boolean} keepExisting (optional) True to retain existing selections
     */
    selectRange: function(startRow, endRow, keepExisting) {
        var me = this,
            store = me.store,
            selected = me.selected.items,
            result, i, len, toSelect, toDeselect, idx, rec;
        if (me.isLocked()) {
            return;
        }
        result = me.normalizeRowRange(startRow, endRow);
        startRow = result[0];
        endRow = result[1];
        toSelect = [];
        for (i = startRow; i <= endRow; i++) {
            if (!me.isSelected(store.getAt(i))) {
                toSelect.push(store.getAt(i));
            }
        }
        if (!keepExisting) {
            // prevent selectionchange from firing
            toDeselect = [];
            me.suspendChanges();
            for (i = 0 , len = selected.length; i < len; ++i) {
                rec = selected[i];
                idx = store.indexOf(rec);
                if (idx < startRow || idx > endRow) {
                    toDeselect.push(rec);
                }
            }
            for (i = 0 , len = toDeselect.length; i < len; ++i) {
                me.doDeselect(toDeselect[i]);
                // We could have brought destruction upon ourselves via handlers;
                // no point in continuing if that happened
                if (me.destroyed) {
                    break;
                }
            }
            if (!me.destroyed) {
                me.resumeChanges();
            }
        }
        if (!me.destroyed) {
            if (toSelect.length) {
                me.doMultiSelect(toSelect, true);
            } else if (toDeselect) {
                me.maybeFireSelectionChange(toDeselect.length > 0);
            }
        }
    },
    /**
     * Deselects a range of rows if the selection model {@link #isLocked is not locked}.
     * @param {Ext.data.Model/Number} startRow The record or index of the first row in the range
     * @param {Ext.data.Model/Number} endRow The record or index of the last row in the range
     */
    deselectRange: function(startRow, endRow) {
        var me = this,
            store = me.store,
            result, i, toDeselect, record;
        if (me.isLocked()) {
            return;
        }
        result = me.normalizeRowRange(startRow, endRow);
        startRow = result[0];
        endRow = result[1];
        toDeselect = [];
        for (i = startRow; i <= endRow; i++) {
            record = store.getAt(i);
            if (me.isSelected(record)) {
                toDeselect.push(record);
            }
        }
        if (toDeselect.length) {
            me.doDeselect(toDeselect);
        }
    },
    normalizeRowRange: function(startRow, endRow) {
        var store = this.store,
            tmp;
        if (!Ext.isNumber(startRow)) {
            startRow = store.indexOf(startRow);
        }
        startRow = Math.max(0, startRow);
        if (!Ext.isNumber(endRow)) {
            endRow = store.indexOf(endRow);
        }
        endRow = Math.min(endRow, store.getCount() - 1);
        // swap values
        if (startRow > endRow) {
            tmp = endRow;
            endRow = startRow;
            startRow = tmp;
        }
        return [
            startRow,
            endRow
        ];
    },
    /**
     * Selects a record instance by record instance or index.
     * @param {Ext.data.Model[]/Number} records An array of records or an index
     * @param {Boolean} [keepExisting=false] True to retain existing selections
     * @param {Boolean} [suppressEvent=false] True to not fire a select event
     */
    select: function(records, keepExisting, suppressEvent) {
        // Automatically selecting eg store.first() or store.last() will pass undefined,
        // so that must just return;
        if (Ext.isDefined(records) && !(Ext.isArray(records) && !records.length)) {
            this.doSelect(records, keepExisting, suppressEvent);
        }
    },
    /**
     * Deselects a record instance by record instance or index.
     * @param {Ext.data.Model[]/Number} records An array of records or an index
     * @param {Boolean} [suppressEvent=false] True to not fire a deselect event
     */
    deselect: function(records, suppressEvent) {
        this.doDeselect(records, suppressEvent);
    },
    doSelect: function(records, keepExisting, suppressEvent) {
        var me = this,
            record;
        if (me.locked || records == null) {
            return;
        }
        if (typeof records === "number") {
            record = me.store.getAt(records);
            // No matching record, jump out.
            if (!record) {
                return;
            }
            records = [
                record
            ];
        }
        if (me.selectionMode === "SINGLE") {
            if (records.isModel) {
                records = [
                    records
                ];
            }
            if (records.length) {
                me.doSingleSelect(records[0], suppressEvent);
            }
        } else {
            me.doMultiSelect(records, keepExisting, suppressEvent);
        }
    },
    doMultiSelect: function(records, keepExisting, suppressEvent) {
        var me = this,
            selected = me.selected,
            change = false,
            result, i, len, record, commit;
        if (me.locked) {
            return;
        }
        records = !Ext.isArray(records) ? [
            records
        ] : records;
        len = records.length;
        if (!keepExisting && selected.getCount() > 0) {
            result = me.deselectDuringSelect(records, suppressEvent);
            if (me.destroyed) {
                return;
            }
            if (result[0]) {
                // We had a failure during selection, so jump out
                // Fire selection change if we did deselect anything
                me.maybeFireSelectionChange(result[1] > 0 && !suppressEvent);
                return;
            } else {
                // Means something has been deselected, so we've had a change
                change = result[1] > 0;
            }
        }
        commit = function() {
            if (!selected.getCount()) {
                me.selectionStart = record;
            }
            if (!suppressEvent) {
                selected.add(record);
            }
            change = true;
        };
        for (i = 0; i < len; i++) {
            record = records[i];
            if (me.isSelected(record)) {
                
                continue;
            }
            me.onSelectChange(record, true, suppressEvent, commit);
            if (me.destroyed) {
                return;
            }
        }
        me.lastSelected = record;
        if (suppressEvent) {
            selected.add(records);
        }
        // fire selchange if there was a change and there is no suppressEvent flag
        me.maybeFireSelectionChange(change && !suppressEvent);
    },
    deselectDuringSelect: function(toSelect, suppressEvent) {
        var me = this,
            selected = me.selected.getRange(),
            len = selected.length,
            changed = 0,
            failed = false,
            item, i;
        // Prevent selection change events from firing, will happen during select
        me.suspendChanges();
        me.deselectingDuringSelect = true;
        for (i = 0; i < len; ++i) {
            item = selected[i];
            if (!Ext.Array.contains(toSelect, item)) {
                if (me.doDeselect(item, suppressEvent)) {
                    ++changed;
                } else {
                    failed = true;
                }
            }
            if (me.destroyed) {
                failed = true;
                changed = 0;
                break;
            }
        }
        me.deselectingDuringSelect = false;
        if (!me.destroyed) {
            me.resumeChanges();
        }
        return [
            failed,
            changed
        ];
    },
    // records can be an index, a record or an array of records
    doDeselect: function(records, suppressEvent) {
        var me = this,
            selected = me.selected,
            i = 0,
            len, record,
            attempted = 0,
            accepted = 0,
            commit;
        if (me.locked || !me.store) {
            return false;
        }
        if (typeof records === "number") {
            record = me.store.getAt(records);
            // No matching record, jump out
            if (!record) {
                return false;
            }
            records = [
                record
            ];
        } else if (!Ext.isArray(records)) {
            records = [
                records
            ];
        }
        commit = function() {
            ++accepted;
            if (!suppressEvent) {
                selected.remove(record);
            }
            if (record === me.selectionStart) {
                me.selectionStart = null;
            }
        };
        len = records.length;
        me.suspendChanges();
        for (; i < len; i++) {
            record = records[i];
            if (me.isSelected(record)) {
                if (me.lastSelected === record) {
                    me.lastSelected = selected.last();
                }
                ++attempted;
                me.onSelectChange(record, false, suppressEvent, commit);
                if (me.destroyed) {
                    return false;
                }
            }
        }
        me.resumeChanges();
        // If we have been suppressing events, we've not been removing individual records
        // Remove them all in one shot.
        if (suppressEvent) {
            selected.remove(records);
        }
        // fire selchange if there was a change and there is no suppressEvent flag
        me.maybeFireSelectionChange(accepted > 0 && !suppressEvent);
        return accepted === attempted;
    },
    doSingleSelect: function(record, suppressEvent) {
        var me = this,
            changed = false,
            selected = me.selected,
            commit;
        if (me.locked) {
            return;
        }
        // already selected.
        // should we also check beforeselect?
        if (me.isSelected(record)) {
            return;
        }
        commit = function() {
            var result;
            // Deselect previous selection.
            if (selected.getCount()) {
                me.suspendChanges();
                result = me.deselectDuringSelect([
                    record
                ], suppressEvent);
                if (me.destroyed) {
                    return;
                }
                me.resumeChanges();
                if (result[0]) {
                    // Means deselection failed, so abort
                    return false;
                }
            }
            me.lastSelected = record;
            if (!selected.getCount()) {
                me.selectionStart = record;
            }
            selected.add(record);
            changed = true;
        };
        me.onSelectChange(record, true, suppressEvent, commit);
        if (changed && !me.destroyed) {
            me.maybeFireSelectionChange(!suppressEvent);
        }
    },
    // fire selection change as long as true is not passed
    // into maybeFireSelectionChange
    maybeFireSelectionChange: function(fireEvent) {
        var me = this;
        if (fireEvent && !me.suspendChange) {
            me.fireEvent('selectionchange', me, me.getSelection());
        }
    },
    /**
     * Returns an array of the currently selected records.
     * @return {Ext.data.Model[]} The selected records
     */
    getSelection: function() {
        return this.selected.getRange();
    },
    /**
     * Returns the current selectionMode.
     * @return {String} The selectionMode: 'SINGLE', 'MULTI' or 'SIMPLE'.
     */
    getSelectionMode: function() {
        return this.selectionMode;
    },
    /**
     * Sets the current selectionMode.
     * @param {String} selMode 'SINGLE', 'MULTI' or 'SIMPLE'.
     */
    setSelectionMode: function(selMode) {
        selMode = selMode ? selMode.toUpperCase() : 'SINGLE';
        // set to mode specified unless it doesnt exist, in that case use single.
        this.selectionMode = this.modes[selMode] ? selMode : 'SINGLE';
    },
    /**
     * Returns true if the selections are locked.
     * @return {Boolean}
     */
    isLocked: function() {
        return this.locked;
    },
    /**
     * Locks the current selection and disables any changes from happening to the selection.
     * @param {Boolean} locked  True to lock, false to unlock.
     */
    setLocked: function(locked) {
        this.locked = !!locked;
    },
    /**
     * Returns true if the specified row is selected.
     * @param {Ext.data.Model/Number} startRow The start of the range to check.
     * @param {Ext.data.Model/Number} endRow The end of the range to check.
     * @return {Boolean}
     */
    isRangeSelected: function(startRow, endRow) {
        var me = this,
            store = me.store,
            i, result;
        result = me.normalizeRowRange(startRow, endRow);
        startRow = result[0];
        endRow = result[1];
        // Loop through. If any of the range is not selected, the answer is false.
        for (i = startRow; i <= endRow; i++) {
            if (!me.isSelected(store.getAt(i))) {
                return false;
            }
        }
        return true;
    },
    /**
     * Returns true if the specified row is selected.
     * @param {Ext.data.Model/Number} record The record or index of the record to check
     * @return {Boolean}
     */
    isSelected: function(record) {
        record = Ext.isNumber(record) ? this.store.getAt(record) : record;
        return this.selected ? this.selected.contains(record) : false;
    },
    /**
     * Returns true if there are any a selected records.
     * @return {Boolean}
     */
    hasSelection: function() {
        var selected = this.getSelected();
        return !!(selected && selected.getCount());
    },
    refresh: function() {
        var me = this,
            store = me.store,
            toBeRemoved = [],
            oldSelections = me.getSelection(),
            len = oldSelections.length,
            // Will be a Collection in this and DataView classes.
            // Will be an Ext.grid.selection.Rows instance for Spreadsheet
            // (does not callParent for other modes).
            // API used in here, getCount() and add() are common.
            selected = me.getSelected(),
            change, d, storeData, selection, rec, i;
        // Not been bound yet, or we have never selected anything.
        if (!store || !(selected.isCollection || selected.isBag || selected.isRows) || !selected.getCount()) {
            return;
        }
        // We need to look beneath any filtering to see if the selected records
        // are still owned by the store
        storeData = store.getData();
        // Attempt to get the underlying source collection to avoid filtering
        if (storeData.getSource) {
            d = storeData.getSource();
            if (d) {
                storeData = d;
            }
        }
        me.refreshing = true;
        // Inhibit update notifications during refresh of the selected collection.
        selected.beginUpdate();
        me.suspendChanges();
        // Add currently records to the toBeSelected list if present in the Store
        // If they are not present, and pruneRemoved is false, we must still retain the record
        for (i = 0; i < len; i++) {
            selection = oldSelections[i];
            rec = storeData.get(selection.getId());
            if (rec) {
                // If the record instance referenced by that ID has changed,
                // silently replace it in the collection
                if (rec !== selection) {
                    if (selected.replace) {
                        selected.replace(rec);
                    } else {
                        selected.add(rec);
                    }
                }
                if (!me.lastSelected) {
                    me.lastSelected = rec;
                }
            }
            // Selected records no longer represented in Store must be retained
            else if (me.pruneRemoved) {
                toBeRemoved.push(selection);
            }
        }
        // there was a change from the old selected and
        // the new selection
        if (toBeRemoved.length) {
            change = true;
            selected.remove(toBeRemoved);
        }
        me.resumeChanges();
        // If the new data caused the selection to change, announce the update using endUpdate,
        // Otherwise, end the update silently.
        // Bindings may be attached to selection - we need to coalesce changes.
        if (change) {
            selected.endUpdate();
        } else {
            selected.updating--;
        }
        me.refreshing = false;
        me.maybeFireSelectionChange(change);
    },
    /**
     * A fast reset of the selections without firing events, updating the ui, etc.
     * For private usage only.
     * @private
     */
    clearSelections: function() {
        // Will be a Collection in this and DataView classes.
        // Will be an Ext.grid.selection.Selection instance for Spreadsheet.
        // API used in here, clear() is common.
        var selected = this.getSelected();
        // reset the entire selection to nothing
        if (selected) {
            selected.clear();
        }
        this.lastSelected = null;
    },
    // when a record is added to a store
    onStoreAdd: Ext.emptyFn,
    // when a store is cleared remove all selections
    // (if there were any)
    onStoreClear: function() {
        // When the store is clearing for reload and there is outstanding selection,
        // make sure to clear it! Otherwise we might end up with incosistent state.
        if ((!this.store.isLoading() || this.store.clearing) && this.hasSelection()) {
            this.clearSelections();
            this.maybeFireSelectionChange(true);
        }
    },
    // prune records from the SelectionModel if
    // they were selected at the time they were
    // removed.
    onStoreRemove: function(store, records, index, isMove) {
        var me = this,
            toDeselect = records,
            i, len, rec, moveMap;
        // If the selection start point is among records being removed,
        // we no longer have a selection start point.
        if (me.selectionStart && Ext.Array.contains(records, me.selectionStart)) {
            me.selectionStart = null;
        }
        if (isMove || me.locked || !me.pruneRemoved) {
            return;
        }
        // Do a cheap check to see if the store is doing any moves before we branch into here
        moveMap = store.isMoving(null, true);
        if (moveMap) {
            toDeselect = null;
            for (i = 0 , len = records.length; i < len; ++i) {
                rec = records[i];
                if (!moveMap[rec.id]) {
                    (toDeselect || (toDeselect = [])).push(rec);
                }
            }
        }
        if (toDeselect) {
            me.deselect(toDeselect);
        }
    },
    // Page evicted from BufferedStore.
    // Remove any selections in that page unless pruneRemoved is false
    onPageRemove: function(pageMap, pageNumber, records) {
        this.onStoreRemove(this.store, records);
    },
    // Page added to BufferedStore.
    // Check for return of already selected records
    onPageAdd: function(pageMap, pageNumber, records) {
        var len = records.length,
            i, record;
        for (i = 0; i < len; i++) {
            record = records[i];
            if (this.selected.get(record.id)) {
                this.selected.replace(record);
            }
        }
    },
    /**
     * Returns the count of selected records.
     * @return {Number} The number of selected records
     */
    getCount: function() {
        return this.selected.getCount();
    },
    // Called when the contents of the node are updated, perform any processing here.
    onUpdate: Ext.emptyFn,
    // cleanup.
    destroy: function() {
        var me = this;
        me.clearSelections();
        me.bindStore(null);
        me.selected = Ext.destroy(me.selected);
        me.callParent();
    },
    // if records are updated
    onStoreUpdate: Ext.emptyFn,
    onIdChanged: function(store, rec, oldId, newId) {
        this.selected.updateKey(rec, oldId);
    },
    onStoreRefresh: function() {
        this.updateSelectedInstances(this.selected);
    },
    /**
     * @private
     * Called when the store is refreshed.
     * Selected records which are no longer present in the store are removed
     * if {@link #pruneRemoved} is `true`.
     * 
     * Selected records which are still present have their instances in the passed collection
     * updated.
     * @param {Ext.util.Collection/Ext.util.Bag} selected A Collection representing
     * the currently selected records.
     */
    updateSelectedInstances: function(selected) {
        var me = this,
            store = me.getStore(),
            lastSelected = me.lastSelected,
            removeCount = 0,
            prune = me.pruneRemovedOnRefresh(),
            items, length, i, selectedRec, rec, lastSelectedChanged;
        if (store && store.isBufferedStore) {
            return;
        }
        items = selected.getRange();
        length = items.length;
        if (lastSelected) {
            me.lastSelected = store.getById(lastSelected.id);
            lastSelectedChanged = me.lastSelected !== lastSelected;
        }
        // Flag so that reactors to collectionEndUpdate know that the collection
        // is not really changing
        me.refreshing = true;
        if (store) {
            for (i = 0; i < length; ++i) {
                selectedRec = items[i];
                // Is the selected record ID still present in the store?
                rec = store.getById(selectedRec.id);
                // Yes, ensure the instance is correct
                if (rec) {
                    if (rec !== selectedRec) {
                        // Silently replace the stale record instance with the new record
                        // by the same ID
                        selected.add(rec);
                    }
                }
                // No, remove it from the selection if we are configured to prune removed records
                else if (prune) {
                    selected.remove(selectedRec);
                    ++removeCount;
                }
            }
        } else {
            removeCount = selected.getCount();
            selected.removeAll();
        }
        me.refreshing = false;
        me.maybeFireSelectionChange(removeCount > 0);
        if (lastSelectedChanged) {
            // Private event for now
            me.fireEvent('lastselectedchanged', me, me.getSelection(), lastSelected);
        }
    },
    // onStoreRefresh asks if it should remove from the selection any selected records which are no
    // longer findable in the store after the refresh.
    // Subclasses may override this.
    // TreeModel does not use the pruneRemoved flag because records are being added and removed
    // from TreeStores on expand and collapse. It uses the pruneRemovedNodes flag.
    pruneRemovedOnRefresh: function() {
        return this.pruneRemoved;
    },
    /**
     * @method
     * @abstract
     * @private
     */
    onStoreLoad: Ext.emptyFn,
    /**
     * @abstract
     */
    onSelectChange: function(record, isSelected, suppressEvent, commitFn) {
        var me = this,
            eventName = isSelected ? 'select' : 'deselect';
        if ((suppressEvent || me.fireEvent('before' + eventName, me, record)) !== false && commitFn() !== false) {
            // Could be destroyed in the handler
            if (!suppressEvent && !me.destroyed) {
                me.fireEvent(eventName, me, record);
            }
        }
    },
    /**
     * @method
     * @abstract
     */
    onEditorKey: Ext.emptyFn,
    /**
     * @protected
     * @template
     * Allows multiple views to be controlled by one selection model.
     * Called by AbstractView's beforeRender method.
     * @param {Ext.view.View} view The View passes itself
     */
    beforeViewRender: function(view) {
        Ext.Array.include(this.views || (this.views = []), view);
    },
    /**
     * @method
     * @protected
     * @template
     * Called by the owning grid's {@link Ext.grid.header.Container header container}
     * when a column header is activated by the UI
     * (clicked, or receives a `SPACE` or `ENTER` key event).
     */
    onHeaderClick: Ext.emptyFn,
    resolveListenerScope: function(defaultScope) {
        var view = this.view,
            scope;
        if (view) {
            scope = view.resolveSatelliteListenerScope(this, defaultScope);
        }
        return scope || this.callParent([
            defaultScope
        ]);
    },
    /**
     * @method
     * @abstract
     */
    bindComponent: Ext.emptyFn,
    privates: {
        onBeforeNavigate: Ext.privateFn,
        /**
         * @private
         * Called by {@link Ext.panel.Table#updateBindSelection} when publishing the `selection`
         * property. It should yield the last record selected.
         * @return {Ext.data.Model} The last record selected. This is only available
         * if the current selection type is cells or rows.
         * In the case of multiple selection, the *last* record added to the selection is returned.
         */
        getLastSelected: function() {
            return this.lastSelected;
        },
        selectWithEventMulti: function(record, e, isSelected) {
            var me = this,
                shift = e.shiftKey,
                ctrl = e.ctrlKey,
                start = shift ? (me.getSelectionStart()) : null,
                selected = me.getSelection(),
                len = selected.length,
                toDeselect, i, item;
            if (shift && start) {
                me.selectRange(start, record, ctrl);
            } else if (ctrl && isSelected) {
                if (me.allowDeselect) {
                    me.doDeselect(record, false);
                }
            } else if (ctrl) {
                me.doSelect(record, true, false);
            } else if (isSelected && !shift && !ctrl && len > 1) {
                if (me.allowDeselect) {
                    toDeselect = [];
                    for (i = 0; i < len; ++i) {
                        item = selected[i];
                        if (item !== record) {
                            toDeselect.push(item);
                        }
                    }
                    me.doDeselect(toDeselect);
                }
            } else if (!isSelected) {
                me.doSelect(record, false);
            }
        },
        selectWithEventSimple: function(record, e, isSelected) {
            if (isSelected) {
                this.doDeselect(record);
            } else {
                this.doSelect(record, true);
            }
        },
        selectWithEventSingle: function(record, e, isSelected) {
            var me = this,
                allowDeselect = me.allowDeselect;
            if (allowDeselect && !e.ctrlKey) {
                allowDeselect = me.toggleOnClick;
            }
            if (allowDeselect && isSelected) {
                me.doDeselect(record);
            } else {
                me.doSelect(record, false);
            }
        }
    }
});

/**
 * The DataViewModel selection model implements item-based selection for Ext.view.View. 
 * DataViewModel is the default dataview selection model and generally will not need to 
 * be specified.
 */
Ext.define('Ext.selection.DataViewModel', {
    extend: 'Ext.selection.Model',
    alias: 'selection.dataviewmodel',
    requires: [
        'Ext.util.KeyNav'
    ],
    deselectOnContainerClick: true,
    /**
     * @cfg {Boolean} [enableKeyNav=true]
     *
     * @deprecated 5.1.0 Keyboard navigation is a function of the view's 
     * {@link Ext.view.NavigationModel navigation model}, and is enabled for 
     * accessibility purposes.
     */
    /**
     * @event beforedeselect
     * Fired before a record is deselected. If any listener returns false, the
     * deselection is cancelled.
     * @param {Ext.selection.DataViewModel} this
     * @param {Ext.data.Model} record The deselected record.
     * @param {Number} index The index within the store of the deselected record.
     */
    /**
     * @event beforeselect
     * Fired before a record is selected. If any listener returns false, the
     * selection is cancelled.
     * @param {Ext.selection.DataViewModel} this
     * @param {Ext.data.Model} record The selected record.
     * @param {Number} index The index within the store of the selected record.
     */
    /**
     * @event deselect
     * Fired after a record is deselected
     * @param {Ext.selection.DataViewModel} this
     * @param  {Ext.data.Model} record The deselected record
     */
    /**
     * @event select
     * Fired after a record is selected
     * @param {Ext.selection.DataViewModel} this
     * @param  {Ext.data.Model} record The selected record.
     * @param {Number} index The index within the store of the selected record.
     */
    bindComponent: function(view) {
        var me = this,
            viewListeners;
        if (me.view !== view) {
            if (me.view) {
                me.navigationModel = null;
                Ext.destroy(me.viewListeners, me.navigationListeners);
            }
            me.view = view;
            if (view) {
                viewListeners = me.getViewListeners();
                viewListeners.scope = me;
                viewListeners.destroyable = true;
                me.navigationModel = view.getNavigationModel();
                me.viewListeners = view.on(viewListeners);
                me.navigationListeners = me.navigationModel.on({
                    navigate: me.onNavigate,
                    scope: me,
                    destroyable: true
                });
            }
        }
    },
    getViewListeners: function() {
        var me = this,
            eventListeners = {};
        eventListeners[me.view.triggerCtEvent] = me.onContainerClick;
        return eventListeners;
    },
    onUpdate: function(record) {
        var view = this.view;
        if (view && this.isSelected(record)) {
            view.onItemSelect(record);
        }
    },
    onContainerClick: function() {
        if (this.deselectOnContainerClick) {
            this.deselectAll();
        }
    },
    // Allow the DataView to update the ui
    onSelectChange: function(record, isSelected, suppressEvent, commitFn) {
        var me = this,
            view = me.view,
            eventName = isSelected ? 'select' : 'deselect',
            recordIndex = me.store.indexOf(record);
        if ((suppressEvent || me.fireEvent('before' + eventName, me, record, recordIndex)) !== false && commitFn() !== false) {
            // Event handler could have destroyed the view...
            if (view && !view.destroyed) {
                if (isSelected) {
                    view.onItemSelect(record);
                } else {
                    view.onItemDeselect(record);
                }
            }
            // ... and the selection model to go with it
            if (!suppressEvent && !me.destroyed) {
                me.fireEvent(eventName, me, record, recordIndex);
            }
        }
    },
    destroy: function() {
        this.bindComponent();
        Ext.destroy(this.keyNav);
        this.callParent();
    }
});

/**
 * @class Ext.view.NavigationModel
 * @private
 * This class listens for key events fired from a {@link Ext.view.View DataView}, and moves
 * the currently focused item by adding the class {@link #focusCls}.
 */
Ext.define('Ext.view.NavigationModel', {
    alias: 'view.navigation.default',
    mixins: [
        'Ext.util.Observable',
        'Ext.mixin.Factoryable',
        'Ext.util.StoreHolder'
    ],
    config: {
        store: null
    },
    /**
     * @event navigate Fired when a key has been used to navigate around the view.
     * @param {Object} event
     * @param {Ext.event.Event} keyEvent The key event which caused the navigation.
     * @param {Number} event.previousRecordIndex The previously focused record index.
     * @param {Ext.data.Model} event.previousRecord The previously focused record.
     * @param {HTMLElement} event.previousItem The previously focused view item.
     * @param {Number} event.recordIndex The newly focused record index.
     * @param {Ext.data.Model} event.record the newly focused record.
     * @param {HTMLElement} event.item the newly focused view item.
     */
    /**
     * @private
     */
    focusCls: Ext.baseCSSPrefix + 'view-item-focused',
    constructor: function() {
        this.mixins.observable.constructor.call(this);
    },
    bindComponent: function(view) {
        if (this.view !== view) {
            this.view = view;
            this.bindView(view);
        }
    },
    bindView: function(view) {
        var me = this,
            dataSource = view.dataSource,
            listeners;
        me.initKeyNav(view);
        if (!dataSource.isEmptyStore) {
            me.setStore(dataSource);
        }
        listeners = me.getViewListeners();
        listeners.destroyable = true;
        me.viewListeners = me.viewListeners || [];
        me.viewListeners.push(view.on(listeners));
    },
    updateStore: function(store) {
        this.mixins.storeholder.bindStore.apply(this, [
            store
        ]);
    },
    getViewListeners: function() {
        var me = this;
        return {
            containermousedown: me.onContainerMouseDown,
            itemmousedown: me.onItemMouseDown,
            // We focus on click if the mousedown handler did not focus
            // because it was a translated "touchstart" event.
            itemclick: me.onItemClick,
            itemcontextmenu: me.onItemMouseDown,
            scope: me
        };
    },
    initKeyNav: function(view) {
        var me = this;
        // Drive the KeyNav off the View's itemkeydown event so that beforeitemkeydown listeners
        // may veto. By default KeyNav uses defaultEventAction: 'stopEvent', and this is required
        // for movement keys which by default affect scrolling.
        me.keyNav = new Ext.util.KeyNav({
            target: view,
            ignoreInputFields: true,
            eventName: 'itemkeydown',
            defaultEventAction: 'stopEvent',
            processEvent: me.processViewEvent,
            up: me.onKeyUp,
            down: me.onKeyDown,
            right: me.onKeyRight,
            left: me.onKeyLeft,
            pageDown: me.onKeyPageDown,
            pageUp: me.onKeyPageUp,
            home: me.onKeyHome,
            end: me.onKeyEnd,
            space: me.onKeySpace,
            enter: me.onKeyEnter,
            A: {
                ctrl: true,
                // Need a separate function because we don't want the key
                // events passed on to selectAll (causes event suppression).
                handler: me.onSelectAllKeyPress
            },
            scope: me
        });
    },
    processViewEvent: function(view, record, node, index, event) {
        return event;
    },
    addKeyBindings: function(binding) {
        this.keyNav.addBindings(binding);
    },
    enable: function() {
        this.keyNav.enable();
        this.disabled = false;
    },
    disable: function() {
        this.keyNav.disable();
        this.disabled = true;
    },
    onContainerMouseDown: function(view, mousedownEvent) {
        // If the mousedown in the view element is NOT inside the client region,
        // that is, it was on a scrollbar, then prevent default.
        //
        // Mousedowning on a scrollbar will focus the View.
        // If they have scrolled to the bottom, then onFocusEnter will
        // try to focus the lastFocused or first item. This is undesirable.
        // So on mousedown outside of view client area, prevent the default focus behaviour.
        // See Ext.view.Table#onFocusEnter for this being acted upon.
        if (Ext.scrollbar.width()) {
            if (!view.el.getClientRegion().contains(mousedownEvent.getPoint())) {
                mousedownEvent.preventDefault();
                view.lastFocused = 'scrollbar';
            }
        }
    },
    onItemMouseDown: function(view, record, item, index, mousedownEvent) {
        // If the event is a touchstart, leave it until the click to focus.
        if (mousedownEvent.pointerType !== 'touch') {
            this.setPosition(index);
        }
    },
    onItemClick: function(view, record, item, index, clickEvent) {
        // If the mousedown that initiated the click has navigated us to the correct spot,
        // just fire the event
        if (this.record === record) {
            this.fireNavigateEvent(clickEvent);
        } else {
            this.setPosition(index, clickEvent);
        }
    },
    setPosition: function(recordIndex, keyEvent, suppressEvent, preventNavigation, preventFocus) {
        var me = this,
            view = me.view,
            selModel = view.getSelectionModel(),
            dataSource = view.dataSource,
            newRecord, newRecordIndex;
        if (recordIndex == null || !view.all.getCount()) {
            me.record = me.recordIndex = null;
        } else {
            if (typeof recordIndex === 'number') {
                newRecordIndex = Math.max(Math.min(recordIndex, dataSource.getCount() - 1), 0);
                newRecord = dataSource.getAt(recordIndex);
            }
            // row is a Record
            else if (recordIndex.isEntity) {
                newRecord = dataSource.getById(recordIndex.id);
                newRecordIndex = dataSource.indexOf(newRecord);
                // Previous record is no longer present; revert to first.
                if (newRecordIndex === -1) {
                    newRecord = dataSource.getAt(0);
                    newRecordIndex = 0;
                }
            }
            // row is a view item
            else if (recordIndex.tagName) {
                newRecord = view.getRecord(recordIndex);
                newRecordIndex = dataSource.indexOf(newRecord);
            } else {
                newRecord = newRecordIndex = null;
            }
        }
        // No change; just ensure the correct item is focused and return early.
        // Do not push current position into previous position, do not fire events.
        // We must check record instances, not indices because of store reloads
        // (combobox remote filtering).
        // If there's a new record, focus it. Note that the index may be different even though
        // the record is the same (filtering, sorting)
        if (newRecord === me.record) {
            me.recordIndex = newRecordIndex;
            return me.focusPosition(newRecordIndex);
        }
        if (me.item) {
            me.item.removeCls(me.focusCls);
        }
        // Track the last position.
        // Used by SelectionModels as the navigation "from" position.
        me.previousRecordIndex = me.recordIndex;
        me.previousRecord = me.record;
        me.previousItem = me.item;
        // Update our position
        me.recordIndex = newRecordIndex;
        me.record = newRecord;
        // Prevent navigation if focus has not moved
        preventNavigation = preventNavigation || me.record === me.lastFocused;
        // Maintain lastFocused, so that on non-specific focus of the View,
        // we can focus the correct descendant.
        if (newRecord) {
            me.focusPosition(me.recordIndex);
        } else if (!preventFocus) {
            me.item = null;
        }
        if (!suppressEvent) {
            selModel.fireEvent('focuschange', selModel, me.previousRecord, me.record);
        }
        // If we have moved, fire an event
        if (!preventNavigation && keyEvent) {
            me.fireNavigateEvent(keyEvent);
        }
    },
    /**
     * @private
     * Focuses the currently active position.
     * This is used on view refresh and on replace.
     */
    focusPosition: function(recordIndex) {
        var me = this;
        if (recordIndex != null && recordIndex !== -1) {
            if (recordIndex.isEntity) {
                recordIndex = me.view.dataSource.indexOf(recordIndex);
            }
            me.item = me.view.all.item(recordIndex);
            if (me.item) {
                me.lastFocused = me.record;
                me.lastFocusedIndex = me.recordIndex;
                me.focusItem(me.item);
            } else {
                me.record = null;
            }
        } else {
            me.item = null;
        }
    },
    /**
     * @template
     * @protected
     * Called to focus an item in the client {@link Ext.view.View DataView}.
     * The default implementation adds the {@link #focusCls} to the passed item focuses it.
     * Subclasses may choose to keep focus in another target.
     *
     * For example {@link Ext.view.BoundListKeyNav} maintains focus in the input field.
     * @param {Ext.dom.Element} item
     * @return {undefined}
     */
    focusItem: function(item) {
        item.addCls(this.focusCls);
        item.focus();
    },
    getPosition: function() {
        return this.record ? this.recordIndex : null;
    },
    getRecordIndex: function() {
        return this.recordIndex;
    },
    getItem: function() {
        return this.item;
    },
    getRecord: function() {
        return this.record;
    },
    getLastFocused: function() {
        // No longer there. The caller must fall back to a default.
        if (this.view.dataSource.indexOf(this.lastFocused) === -1) {
            return null;
        }
        return this.lastFocused;
    },
    onKeyUp: function(keyEvent) {
        var newPosition = this.recordIndex - 1;
        if (newPosition < 0) {
            newPosition = this.view.all.getCount() - 1;
        }
        this.setPosition(newPosition, keyEvent);
    },
    onKeyDown: function(keyEvent) {
        var newPosition = this.recordIndex + 1;
        if (newPosition > this.view.all.getCount() - 1) {
            newPosition = 0;
        }
        this.setPosition(newPosition, keyEvent);
    },
    onKeyRight: function(keyEvent) {
        var newPosition = this.recordIndex + 1;
        if (newPosition > this.view.all.getCount() - 1) {
            newPosition = 0;
        }
        this.setPosition(newPosition, keyEvent);
    },
    onKeyLeft: function(keyEvent) {
        var newPosition = this.recordIndex - 1;
        if (newPosition < 0) {
            newPosition = this.view.all.getCount() - 1;
        }
        this.setPosition(newPosition, keyEvent);
    },
    onKeyPageDown: Ext.emptyFn,
    onKeyPageUp: Ext.emptyFn,
    onKeyHome: function(keyEvent) {
        this.setPosition(0, keyEvent);
    },
    onKeyEnd: function(keyEvent) {
        this.setPosition(this.view.all.getCount() - 1, keyEvent);
    },
    onKeySpace: function(keyEvent) {
        this.fireNavigateEvent(keyEvent);
    },
    // ENTER emulates an itemclick event at the View level
    onKeyEnter: function(keyEvent) {
        // Stop the keydown event so that an ENTER keyup does not get delivered to
        // any element which focus is transferred to in a click handler.
        keyEvent.stopEvent();
        keyEvent.view.fireEvent('itemclick', keyEvent.view, keyEvent.record, keyEvent.item, keyEvent.recordIndex, keyEvent);
    },
    onSelectAllKeyPress: function(keyEvent) {
        this.fireNavigateEvent(keyEvent);
    },
    fireNavigateEvent: function(keyEvent) {
        var me = this;
        me.fireEvent('navigate', {
            navigationModel: me,
            keyEvent: keyEvent,
            previousRecordIndex: me.previousRecordIndex,
            previousRecord: me.previousRecord,
            previousItem: me.previousItem,
            recordIndex: me.recordIndex,
            record: me.record,
            item: me.item
        });
    },
    destroy: function() {
        this.setStore(null);
        Ext.destroy(this.viewListeners, this.keyNav);
        this.callParent();
    }
});

/**
 * @class Ext.view.AbstractView
 * This is an abstract superclass and should not be used directly. Please see {@link Ext.view.View}.
 * @private
 */
Ext.define('Ext.view.AbstractView', {
    extend: 'Ext.Component',
    requires: [
        'Ext.LoadMask',
        'Ext.CompositeElementLite',
        'Ext.selection.DataViewModel',
        'Ext.view.NavigationModel',
        'Ext.util.CSS'
    ],
    mixins: [
        'Ext.util.StoreHolder'
    ],
    isDataView: true,
    inheritableStatics: {
        /**
         * @private
         * @static
         * @inheritable
         */
        getRecord: function(node) {
            return this.getBoundView(node).getRecord(node);
        },
        /**
         * @private
         * @static
         * @inheritable
         */
        getBoundView: function(node) {
            return Ext.getCmp(node.getAttribute('data-boundView'));
        }
    },
    /**
     * @property defaultBindProperty
     * @inheritdoc
     */
    defaultBindProperty: 'store',
    /**
     * @private
     * Used for buffered rendering.
     */
    renderBuffer: new Ext.dom.Fly(document.createElement('div')),
    statics: {
        /**
         * @prop {Number} [updateDelay=200] Global config for use when using
         * {@link #throttledUpdate throttled view updating} if the data in the backing
         * {@link Ext.data.Store store} is being changed rapidly, for example receiving changes
         * from the server through a WebSocket connection.
         *
         * To avoid too-frequent view updates overloading the browser with style recalculation,
         * layout and paint requests, updates can be {@link #throttledUpdate throttled} to 
         * coalesced, and applied at the interval specified in milliseconds.
         *
         * Note that on lower powered devices, updating is throttled to once every second.
         */
        updateDelay: Ext.platformTags.desktop ? 200 : 1000,
        queueRecordChange: function(view, store, record, operation, modifiedFieldNames) {
            var me = this,
                changeQueue = me.changeQueue || (me.changeQueue = {}),
                recId = record.internalId,
                recChange, updated, len, i, fieldName, value, checkForReversion;
            recChange = changeQueue[recId] || (changeQueue[recId] = {
                operation: operation,
                record: record,
                data: {},
                views: []
            });
            // Hash of original values
            updated = recChange.data;
            // Make sure this view is among those updated when record changes are flushed
            Ext.Array.include(recChange.views, view);
            // Note the following condition tests the result of an assignment statement.
            // If we have been informed that specific fields have changed.
            if (modifiedFieldNames && (len = modifiedFieldNames.length)) {
                for (i = 0; i < len; i++) {
                    fieldName = modifiedFieldNames[i];
                    value = record.data[fieldName];
                    // More than one update is being performed...
                    if (updated.hasOwnProperty(fieldName)) {
                        // If the update is back to the original value,
                        // this may have reverted the record to original state
                        if (record.isEqual(updated[fieldName], value)) {
                            delete updated[fieldName];
                            checkForReversion = true;
                        }
                    } else // On first update, cache the original value
                    {
                        updated[fieldName] = value;
                    }
                }
                // If the record has been returned to its original state, delete the queue entry.
                // checkForReversion flag saves the expensive (on legacy browsers)
                // call to Ext.Object.getKeys
                if (checkForReversion && !Ext.Object.getKeys(updated).length) {
                    delete changeQueue[recId];
                }
            } else // Unspecified fields have changed. We have to collect the whole data object.
            {
                Ext.apply(updated, record.data);
            }
            // Create a task which will call flushChangeQueue in updateDelay milliseconds
            // from the time it's invoked.
            if (!me.flushQueueTask) {
                me.flushQueueTask = new Ext.util.DelayedTask(Ext.global.requestAnimationFrame ? Ext.Function.createAnimationFrame(me.flushChangeQueue, me) : me.flushChangeQueue.bind(me), me, null, false);
            }
            if (!me.flushTimer) {
                me.flushTimer = me.flushQueueTask.delay(Ext.view.AbstractView.updateDelay);
            }
        },
        /**
        * @private
        * Flushes all queued field updates to the UI.
        *
        * Called in the context of the AbstractView class.
        *
        * The queue is shared across all Views so that there is only one global flush operation.
        */
        flushChangeQueue: function() {
            // Maintainer: Note that "me" references AbstractView class
            var me = this,
                dirtyViews, len, changeQueue, recChange, recId, i, view;
            // If there is scrolling going on anywhere, requeue the flush operation ASAP.
            if (Ext.isScrolling) {
                return me.flushTimer = me.flushQueueTask.delay(1);
            }
            me.flushTimer = null;
            changeQueue = me.changeQueue;
            // Empty the view's changeQueue
            me.changeQueue = {};
            for (recId in changeQueue) {
                recChange = changeQueue[recId];
                dirtyViews = recChange.views;
                len = dirtyViews.length;
                // Loop through all the views which have outstanding changes.
                for (i = 0; i < len; i++) {
                    view = dirtyViews[i];
                    // View may have been destroyed during the buffered phase.
                    if (!view.destroyed) {
                        view.handleUpdate(view.dataSource, recChange.record, recChange.operation, Ext.Object.getKeys(recChange.data));
                    }
                }
            }
        }
    },
    config: {
        /**
         * @cfg {Ext.data.Store} store
         * The {@link Ext.data.Store} to bind this DataView to.
         * @since 2.3.0
         */
        store: 'ext-empty-store',
        // @cmd-auto-dependency { aliasPrefix: 'view.navigation.' }
        /**
         * @private
         * The {@link Ext.view.NavigationModel} [default] alias to use.
         * @since 5.0.1
         */
        navigationModel: {
            type: 'default'
        },
        // @cmd-auto-dependency { aliasPrefix: 'selection.' }
        /**
         * @cfg {Object/Ext.selection.DataViewModel} selectionModel
         * The {@link Ext.selection.Model selection model} [dataviewmodel] config or alias to use.
         * @since 5.1.0
         */
        selectionModel: {
            type: 'dataviewmodel'
        }
    },
    /**
     * @cfg publishes
     * @inheritdoc
     */
    publishes: [
        'selection'
    ],
    /**
     * @cfg twoWayBindable
     * @inheritdoc
     */
    twoWayBindable: [
        'selection'
    ],
    /**
     * @cfg {Ext.data.Model} selection
     * The selected model. Typically used with {@link #bind binding}.
     */
    selection: null,
    /**
     * @cfg {Boolean} throttledUpdate
     * Configure as `true` to have this view participate in the global throttled update queue
     * which flushes store changes to the UI at a maximum rate determined by the
     * {@link #updateDelay} setting.
     */
    throttledUpdate: false,
    /**
     * @cfg {String/String[]/Ext.XTemplate} tpl (required)
     * The HTML fragment or an array of fragments that will make up the template
     * used by this DataView.  This should be specified in the same format expected by the
     * constructor of {@link Ext.XTemplate}. When a `tpl` is specified, this class assumes that
     * records are rendered in the order they appear in the `{@link #store}`. If a custom `tpl`
     * does not conform to this assumption, index values will be incorrect which may cause the view
     * to misbehave.
     * @since 2.3.0
     */
    /**
     * @cfg {Boolean} deferInitialRefresh
     * Configure as 'true` to defer the initial refresh of the view.
     *
     * This allows the View to execute its render and initial layout more quickly because
     * the process will not be encumbered by the update of the view structure.
     */
    deferInitialRefresh: false,
    /**
     * @cfg {String} itemSelector (required)
     * <b>This is a required setting</b>. A simple CSS selector (e.g. `div.some-class` or
     * `span:first-child`) that will be used to determine what nodes this DataView will be
     * working with. The itemSelector is used to map DOM nodes to records. As such, there should
     * only be one root level element that matches the selector for each record. The itemSelector
     * will be automatically configured if the {@link #itemTpl} config is used.
     * 
     *     new Ext.view.View({
     *         renderTo: Ext.getBody(),
     *         store: {
     *             fields: ['name'],
     *             data: [
     *                 {name: 'Item 1'},
     *                 {name: 'Item 2'}
     *             ]
     *         },
     *         tpl: [
     *             '<ul>',
     *             '<tpl for=".">',
     *                 '<li>{name}</li>',
     *             '</tpl>',
     *             '</ul>'
     *         ],
     *         // Match the li, since each one maps to a record
     *         itemSelector: 'li'
     *     });
     * 
     * @since 2.3.0
     */
    /**
     * @cfg {String} itemCls
     * Specifies the class to be assigned to each element in the view when used in conjunction
     * with the {@link #itemTpl} configuration.
     * @since 2.3.0
     */
    itemCls: Ext.baseCSSPrefix + 'dataview-item',
    /**
     * @cfg {String/String[]/Ext.XTemplate} itemTpl
     * The inner portion of the item template to be rendered. Follows an XTemplate
     * structure and will be placed inside of a tpl.
     */
    /**
     * @cfg {String} overItemCls
     * A CSS class to apply to each item in the view on mouseover.
     * Setting this will automatically set {@link #trackOver} to `true`.
     */
    /**
     * @cfg {String} loadingText
     * A string to display during data load operations.  If specified, this text will be
     * displayed in a loading div and the view's contents will be cleared while loading, otherwise
     * the view's contents will continue to display normally until the new data is loaded
     * and the contents are replaced.
     * @since 2.3.0
     * @locale
     */
    loadingText: 'Loading...',
    /**
     * @cfg {Boolean/Object} loadMask
     * False to disable a load mask from displaying while the view is loading. This can also be a
     * {@link Ext.LoadMask} configuration object.
     */
    loadMask: true,
    /**
     * @cfg {String} loadingCls
     * The CSS class to apply to the loading message element. Defaults to
     * Ext.LoadMask.prototype.msgCls "x-mask-loading".
     */
    /**
     * @cfg {Boolean} loadingUseMsg
     * Whether or not to use the loading message.
     * @private
     */
    loadingUseMsg: true,
    /**
     * @cfg {Number} loadingHeight
     * If specified, gives an explicit height for the data view when it is showing the
     * {@link #loadingText}, if that is specified. This is useful to prevent the view's height
     * from collapsing to zero when the loading mask is applied and there are no other contents
     * in the data view.
     */
    /**
     * @cfg {String} selectedItemCls
     * A CSS class to apply to each selected item in the view.
     */
    selectedItemCls: Ext.baseCSSPrefix + 'item-selected',
    /**
     * @cfg {String} emptyText
     * The text to display in the view when there is no data to display.
     * Note that when using local data the emptyText will not be displayed unless you set
     * the {@link #deferEmptyText} option to false.
     * @since 2.3.0
     * @accessor
     * @locale
     */
    emptyText: "",
    /**
     * @cfg {Boolean} deferEmptyText
     * True to defer emptyText being applied until the store's first load.
     * @since 2.3.0
     */
    deferEmptyText: true,
    /**
     * @cfg {Boolean} trackOver
     * When `true` the {@link #overItemCls} will be applied to items when hovered over.
     * This in return will also cause {@link Ext.view.View#highlightitem highlightitem} and
     * {@link Ext.view.View#unhighlightitem unhighlightitem} events to be fired.
     *
     * Enabled automatically when the {@link #overItemCls} config is set.
     *
     * @since 2.3.0
     */
    trackOver: false,
    /**
     * @cfg {Boolean} blockRefresh
     * Set this to true to ignore refresh events on the bound store. This is useful if
     * you wish to provide custom transition animations via a plugin
     * @since 3.4.0
     */
    blockRefresh: false,
    /**
     * @cfg {Boolean} [disableSelection=false]
     * True to disable selection within the DataView. This configuration will lock
     * the selection model that the DataView uses.
     */
    /**
     * @cfg {Boolean} preserveScrollOnRefresh
     * True to preserve scroll position across refresh operations.
     */
    preserveScrollOnRefresh: false,
    /**
     * @cfg {Boolean} preserveScrollOnReload
     * True to preserve scroll position when the store is reloaded.
     *
     * You may want to configure this as `true` if you are using a
     * {@link Ext.data.BufferedStore buffered store} and you require refreshes of the client side
     * data state not to disturb the state of the UI.
     *
     * @since 5.1.1
     */
    preserveScrollOnReload: false,
    /**
     * @property autoDestroyBoundStore
     * @inheritdoc
     */
    autoDestroyBoundStore: true,
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'listbox',
    itemAriaRole: 'option',
    /**
     * @private
     */
    last: false,
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: true,
    /**
     * @cfg tabIndex
     * @inheritdoc
     */
    tabIndex: 0,
    triggerEvent: 'itemclick',
    triggerCtEvent: 'containerclick',
    // Starts as true by default so that pn the leading edge of the first layout a refresh
    // will be triggered. A refresh opereration sets this flag to false.
    // When a refresh is requested using refreshView, the request may be deferred because of hidden
    // or collapsed state. This is done by setting the refreshNeeded flag to true, and the the next
    // layout will trigger refresh.
    refreshNeeded: true,
    updateSuspendCounter: 0,
    addCmpEvents: Ext.emptyFn,
    /**
     * @event beforerefresh
     * Fires before the view is refreshed
     * @param {Ext.view.View} this The DataView object
     */
    /**
     * @event refresh
     * Fires when the view is refreshed
     * @param {Ext.view.View} this The DataView object
     */
    /**
     * @event viewready
     * Fires when the View's item elements representing Store items has been rendered.
     * No items will be available for selection until this event fires.
     * @param {Ext.view.View} this
     */
    /**
     * @event itemupdate
     * Fires when the node associated with an individual record is updated
     * @param {Ext.data.Model} record The model instance
     * @param {Number} index The index of the record
     * @param {HTMLElement} node The node that has just been updated
     * @param {Ext.view.View} view The view containing the item
     */
    /**
     * @event itemadd
     * Fires when the nodes associated with an recordset have been added to the underlying store
     * @param {Ext.data.Model[]} records The model instance
     * @param {Number} index The index at which the set of records was inserted
     * @param {HTMLElement[]} node The node that has just been updated
     * @param {Ext.view.View} view The view adding the item
     */
    /**
     * @event itemremove
     * Fires when the node associated with an individual record is removed
     * @param {Ext.data.Model[]} records The model instances removed
     * @param {Number} index The index from which the records wer removed
     * @param {HTMLElement[]} item The view items removed
     * @param {Ext.view.View} view The view removing the item
     */
    constructor: function(config) {
        if (config && config.selModel) {
            config.selectionModel = config.selModel;
        }
        this.callParent([
            config
        ]);
    },
    initComponent: function() {
        var me = this,
            isDef = Ext.isDefined,
            itemTpl = me.itemTpl,
            memberFn = {},
            selection = me.selection,
            store;
        if (selection) {
            me.selection = null;
            me.setSelection(selection);
        }
        if (itemTpl) {
            if (Ext.isArray(itemTpl)) {
                // string array
                if (typeof itemTpl[itemTpl.length - 1] !== 'string') {
                    itemTpl = itemTpl.slice(0);
                    memberFn = itemTpl.pop();
                }
                itemTpl = itemTpl.join('');
            } else if (Ext.isObject(itemTpl)) {
                // tpl instance
                memberFn = Ext.apply(memberFn, itemTpl.initialConfig);
                itemTpl = itemTpl.html;
            }
            if (!me.itemSelector) {
                me.itemSelector = '.' + me.itemCls;
            }
            if (memberFn.fn) {
                memberFn.baseFn = memberFn.fn;
                delete memberFn.fn;
                itemTpl = "{%this.baseFn(out, values, parent, xindex, xcount, xkey)%}";
            }
            itemTpl = Ext.String.format('<tpl for="."><div class="{0}" role="{2}">{1}</div></tpl>', me.itemCls, itemTpl, me.itemAriaRole);
            me.tpl = new Ext.XTemplate(itemTpl, memberFn);
        }
        if (!isDef(me.tpl) || !isDef(me.itemSelector)) {
            Ext.raise({
                sourceClass: 'Ext.view.View',
                tpl: me.tpl,
                itemSelector: me.itemSelector,
                msg: "DataView requires both tpl and itemSelector configurations to be defined."
            });
        }
        me.callParent();
        me.tpl = me.lookupTpl('tpl');
        // backwards compat alias for overClass/selectedClass
        // TODO: Consider support for overCls generation Ext.Component config
        if (isDef(me.overCls) || isDef(me.overClass)) {
            if (Ext.isDefined(Ext.global.console)) {
                Ext.global.console.warn('Ext.view.View: Using the deprecated overCls or overClass configuration. ' + 'Use overItemCls instead.');
            }
            me.overItemCls = me.overCls || me.overClass;
            delete me.overCls;
            delete me.overClass;
        }
        if (isDef(me.selectedCls) || isDef(me.selectedClass)) {
            if (Ext.isDefined(Ext.global.console)) {
                Ext.global.console.warn('Ext.view.View: Using the deprecated selectedCls or selectedClass ' + 'configuration. Use selectedItemCls instead.');
            }
            me.selectedItemCls = me.selectedCls || me.selectedClass;
            delete me.selectedCls;
            delete me.selectedClass;
        }
        if (me.overItemCls) {
            me.trackOver = true;
        }
        me.addCmpEvents();
        // Look up the configured Store. If none configured, use the fieldless,
        // empty Store defined in Ext.data.Store.
        store = me.store = Ext.data.StoreManager.lookup(me.store || 'ext-empty-store');
        // Use the provided store as the data source unless a Feature or plugin
        // has injected a special one
        if (!me.dataSource) {
            me.dataSource = store;
        }
        me.bindStore(store, true);
        // Must exist before the selection model.
        // Selection model listens to this for navigation events.
        me.getNavigationModel().bindComponent(this);
        if (!me.all) {
            me.all = new Ext.CompositeElementLite();
        }
        // We track the scroll position
        me.scrollState = {
            top: 0,
            left: 0
        };
        me.savedTabIndexAttribute = 'data-savedtabindex-' + me.id;
    },
    getElConfig: function() {
        var result = this.mixins.renderable.getElConfig.call(this);
        // Subclasses may set focusable to false (BoundList is not focusable)
        if (this.focusable) {
            result.tabIndex = 0;
        }
        return result;
    },
    onRender: function(parentNode, containerIdx) {
        var mask = this.loadMask;
        this.callParent([
            parentNode,
            containerIdx
        ]);
        if (mask) {
            this.createMask(mask);
        }
    },
    beforeLayout: function() {
        var me = this;
        me.callParent();
        // If there is a deferred refresh timer running, allow that to do the refresh.
        if (me.refreshNeeded && !me.pendingRefresh) {
            // If we have refreshed before, just call a refresh now.
            if (me.refreshCounter) {
                me.refreshView();
            } else {
                me.doFirstRefresh(me.dataSource);
            }
        }
    },
    onMaskBeforeShow: function() {
        var me = this,
            loadingHeight = me.loadingHeight;
        if (loadingHeight && loadingHeight > me.getHeight()) {
            me.hasLoadingHeight = true;
            me.oldMinHeight = me.minHeight;
            me.minHeight = loadingHeight;
            me.updateLayout();
        }
    },
    onMaskHide: function() {
        var me = this;
        if (!me.destroying && me.hasLoadingHeight) {
            me.minHeight = me.oldMinHeight;
            me.updateLayout();
            delete me.hasLoadingHeight;
        }
    },
    beforeRender: function() {
        this.callParent();
        this.getSelectionModel().beforeViewRender(this);
    },
    afterRender: function() {
        this.callParent();
        // Subclasses may set focusable to false.
        // BoundList is not focusable.
        // BoundList processes key events from its boundField.
        if (this.focusable) {
            this.focusEl = this.el;
        }
    },
    getRefItems: function() {
        var mask = this.loadMask,
            result = [];
        if (mask && mask.isComponent) {
            result.push(mask);
        }
        return result;
    },
    getSelection: function() {
        return this.getSelectionModel().getSelection();
    },
    /**
     * Sets the value of the selection.
     * @param {Ext.data.Model} selection
     */
    setSelection: function(selection) {
        // This is purposefully written not as a config. Because getSelection
        // is an existing API that doesn't mirror the value for setSelection, we
        // don't want the publish system to call the getter, but rather just the
        // raw property.
        var current = this.selection;
        if (selection !== current) {
            this.selection = selection;
            this.updateSelection(selection, current);
        }
    },
    updateSelection: function(selection) {
        var me = this,
            sm;
        if (!me.ignoreNextSelection) {
            me.ignoreNextSelection = true;
            sm = me.getSelectionModel();
            if (selection) {
                sm.select(selection);
            } else {
                sm.deselectAll();
            }
            me.ignoreNextSelection = false;
        }
        me.publishState('selection', selection);
    },
    updateBindSelection: function(selModel, selection) {
        var me = this,
            selected = null;
        if (!me.ignoreNextSelection) {
            me.ignoreNextSelection = true;
            if (selection.length) {
                selected = selModel.getLastSelected();
                me.hasHadSelection = true;
            }
            if (me.hasHadSelection) {
                me.setSelection(selected);
            }
            me.ignoreNextSelection = false;
        }
    },
    applySelectionModel: function(selModel, oldSelModel) {
        var me = this,
            grid = me.grid,
            mode, ariaAttr, ariaDom;
        if (oldSelModel) {
            // Could be already destroyed, and listeners cleared
            if (!oldSelModel.destroyed) {
                oldSelModel.un({
                    scope: me,
                    selectionchange: me.updateBindSelection,
                    lastselectedchanged: me.updateBindSelection
                });
            }
            Ext.destroy(me.selModelRelayer);
            selModel = Ext.Factory.selection(selModel);
        } else // If this is the initial configuration, pull overriding configs in from this view
        {
            if (selModel && selModel.isSelectionModel) {
                selModel.locked = me.disableSelection;
            } else {
                if (me.simpleSelect) {
                    mode = 'SIMPLE';
                } else if (me.multiSelect) {
                    mode = 'MULTI';
                } else {
                    mode = 'SINGLE';
                }
                if (typeof selModel === 'string') {
                    selModel = {
                        type: selModel
                    };
                }
                selModel = Ext.Factory.selection(Ext.apply({
                    allowDeselect: me.allowDeselect || me.multiSelect,
                    mode: mode,
                    locked: me.disableSelection
                }, selModel));
            }
        }
        // Grids should have aria-multiselectable on their ariaEl instead
        if (selModel.mode !== 'SINGLE') {
            ariaDom = (grid || me).ariaEl.dom;
            if (ariaDom) {
                ariaDom.setAttribute('aria-multiselectable', true);
            } else if (!grid) {
                ariaAttr = me.ariaRenderAttributes || (me.ariaRenderAttributes = {});
                ariaAttr['aria-multiselectable'] = true;
            }
        }
        me.selModelRelayer = me.relayEvents(selModel, [
            'selectionchange',
            'beforeselect',
            'beforedeselect',
            'select',
            'deselect',
            'focuschange'
        ]);
        selModel.on({
            scope: me,
            lastselectedchanged: me.updateBindSelection,
            selectionchange: me.updateBindSelection
        });
        return selModel;
    },
    updateSelectionModel: function(selectionModel) {
        // Keep the legacy property correct
        this.selModel = selectionModel;
    },
    applyNavigationModel: function(navigationModel) {
        return Ext.Factory.viewNavigation(navigationModel);
    },
    onFocusEnter: function(e) {
        var me = this,
            navigationModel = me.getNavigationModel(),
            focusPosition = me.lastFocused;
        // This is set on mousedown on the scrollbar in IE/Edge.
        // Those browsers focus the element on mousedown on its scrollbar
        // which is not what we want, so throw focus back in this
        // situation.
        // See Ext.view.navigationModel for this being set.
        me.lastFocused = null;
        if (focusPosition === 'scrollbar') {
            e.relatedTarget.focus();
            return;
        }
        // Disable tabbability of elements within this view.
        me.toggleChildrenTabbability(false);
        if (!me.itemFocused && me.all.getCount()) {
            // SHIFT+TAB hit the tab guard - focus last item.
            if (e.event.getTarget() === me.tabGuardEl) {
                focusPosition = me.all.getCount() - 1;
            } else {
                focusPosition = navigationModel.getLastFocused();
            }
            navigationModel.setPosition(focusPosition || 0, e.event, null, !focusPosition);
            // We now contain focus is that was successful
            me.itemFocused = navigationModel.getPosition() != null;
        }
        // View's main el should be kept untabbable, otherwise pressing
        // Shift-Tab key in the view would move the focus to the main el
        // which will then bounce it back to the last focused item.
        // That would effectively make Shift-Tab unusable.
        if (me.itemFocused) {
            me.el.dom.setAttribute('tabIndex', -1);
            if (me.tabGuardEl) {
                me.tabGuardEl.setAttribute('tabIndex', -1);
            }
        }
        me.callParent([
            e
        ]);
    },
    onFocusLeave: function(e) {
        var me = this;
        // Ignore this event if we do not actually contain focus,
        // or if the reason for focus exiting was that we are refreshing.
        if (me.itemFocused && !me.refreshing) {
            // Blur the focused cell
            me.getNavigationModel().setPosition(null, e.event, null, true);
            me.itemFocused = false;
            me.el.dom.setAttribute('tabIndex', 0);
            if (me.tabGuardEl) {
                me.tabGuardEl.setAttribute('tabIndex', 0);
            }
        }
        me.callParent([
            e
        ]);
    },
    /**
     * @private
     * Cancel a pending focus task, if any.
     * This is a separate method to allow simple abstraction for locked views.
     */
    cancelFocusTask: function() {
        var task = this.getFocusTask();
        if (task) {
            task.cancel();
        }
    },
    onRemoved: function(isDestroying) {
        this.callParent([
            isDestroying
        ]);
        // IE does not fire focusleave on removal from DOM
        if (!isDestroying) {
            this.onFocusLeave({});
        }
    },
    /**
     * Refreshes the view by reloading the data from the store and re-rendering the template.
     *
     * @since 2.3.0
     */
    refresh: function() {
        var me = this,
            items = me.all,
            prevItemCount = items.getCount(),
            refreshCounter = me.refreshCounter,
            targetEl, records,
            selModel = me.getSelectionModel(),
            restoreFocus,
            // If there are items in the view, then honour preserveScrollOnRefresh
            scroller = refreshCounter && items.getCount() && me.preserveScrollOnRefresh && me.getScrollable(),
            bufferedRenderer = me.bufferedRenderer,
            scrollPos;
        if (!me.rendered || me.destroyed) {
            return;
        }
        if (!me.hasListeners.beforerefresh || me.fireEvent('beforerefresh', me) !== false) {
            // So that listeners to itemremove events know that its because of a refresh
            me.refreshing = true;
            // If focus was in this view, this will restore it
            restoreFocus = me.saveFocusState();
            targetEl = me.getTargetEl();
            records = me.getViewRange();
            if (scroller) {
                scrollPos = scroller.getPosition();
                if (!(scrollPos.x || scrollPos.y)) {
                    scrollPos = null;
                }
            }
            if (refreshCounter || me.emptyEl) {
                me.clearViewEl();
            }
            if (refreshCounter) {
                me.refreshCounter++;
            } else {
                me.refreshCounter = 1;
            }
            // Usually, for an empty record set, this would be blank, but when the Template
            // Creates markup outside of the record loop, this must still be honoured
            // even if there are no records.
            me.tpl.append(targetEl, me.collectData(records, items.startIndex || 0));
            // The emptyText is now appended to the View's element
            // after nodes outside the tpl block.
            if (records.length < 1) {
                // Process empty text unless the store is being cleared.
                me.addEmptyText();
                items.clear();
            } else {
                me.collectNodes(targetEl.dom);
                me.updateIndexes(0);
            }
            // If focus was in any way in this view, this will restore it
            restoreFocus();
            // Some subclasses do not need to do this. TableView does not need to do this -
            // it renders selected class using its tenmplate.
            if (me.refreshSelmodelOnRefresh !== false) {
                selModel.refresh();
            }
            me.refreshNeeded = false;
            // Ensure layout system knows about new content size.
            // If number of items have changed, force a layout.
            me.refreshSize(items.getCount() !== prevItemCount);
            me.fireItemMutationEvent('refresh', me, records);
            if (scroller) {
                scroller.scrollTo(scrollPos);
            }
            // Upon first refresh, fire the viewready event.
            // Reconfiguring the grid "renews" this event.
            if (!me.viewReady) {
                // Fire an event when deferred content becomes available.
                me.viewReady = true;
                me.fireEvent('viewready', me);
            }
            me.refreshing = false;
            if (bufferedRenderer) {
                bufferedRenderer.refreshSize();
            }
            me.cleanupData();
        }
        // The tabGuardEl is only needed before focus has entered the view to prevent
        // naturally tabbable interior elements from attracting focus when using SHIFT+TAB
        // from after the view. Once focus has entered, we disable tabbability on interior
        // elements so it will not be needed.
        // Subsequent *full* refreshes will destroy it.
        // Buffer rendered refreshes are more granular, and do not destroy extraneous
        // DOM, but this does not matter because tha tabGuardEl will be tabIndex="-1"
        // so out of the tab order.
        if (!me.tabGuardEl) {
            // We only need an "after" tab guard.
            // The View el is tabIndex="0", so captures forward TAB.
            // It's only SHIFT+TAB that we have to guard against.
            me.tabGuardEl = me.el.createChild({
                cls: Ext.baseCSSPrefix + 'tab-guard ' + Ext.baseCSSPrefix + 'tab-guard-after',
                tabIndex: "0"
            }, null, true);
        }
    },
    addEmptyText: function() {
        var me = this,
            store = me.getStore();
        if (me.emptyText && !store.isLoading() && (!me.deferEmptyText || me.refreshCounter > 1 || store.isLoaded())) {
            if (!me.emptyEl) {
                me.emptyEl = Ext.core.DomHelper.insertHtml('beforeEnd', me.getTargetEl().dom, me.emptyText);
            } else {
                Ext.fly(me.emptyEl).setHtml(me.emptyText);
            }
        }
    },
    getEmptyText: function() {
        return this.emptyText;
    },
    setEmptyText: function(emptyText) {
        var me = this;
        if (me.emptyText !== emptyText) {
            me.emptyText = emptyText;
            me.refresh();
        }
        return me;
    },
    getViewRange: function() {
        return this.dataSource.getRange();
    },
    /**
     * @private
     * Called by the framework when the view is refreshed, or when rows are added or deleted.
     *
     * These operations may cause the view's dimensions to change, and if the owning container
     * is shrinkwrapping this view, then the layout must be updated to accommodate these new
     * dimensions.
     */
    refreshSize: function(forceLayout) {
        var me = this,
            sizeModel = me.getSizeModel();
        if (sizeModel.height.shrinkWrap || sizeModel.width.shrinkWrap || forceLayout) {
            me.updateLayout();
        }
    },
    afterFirstLayout: function(width, height) {
        var me = this,
            scroller = me.getScrollable();
        if (scroller) {
            me.viewScrollListeners = scroller.on({
                scroll: me.onViewScroll,
                scrollend: me.onViewScrollEnd,
                scope: me,
                onFrame: !!Ext.global.requestAnimationFrame,
                destroyable: true
            });
        }
        me.callParent([
            width,
            height
        ]);
    },
    clearViewEl: function() {
        var me = this,
            targetEl = me.getTargetEl(),
            all = me.all,
            store = me.getStore(),
            nodeContainerIsTarget = me.getNodeContainer() === targetEl,
            i, removedItems, removedRecs;
        // We must ensure that the itemremove event is fired EVERY time an item is removed from the
        // view. This is so that widgets rendered into a view by a WidgetColumn can be recycled.
        removedItems = all.slice();
        removedRecs = [];
        for (i = all.startIndex; i <= all.endIndex; i++) {
            removedRecs.push(store.getByInternalId(all.item(i, true).getAttribute('data-recordId')));
        }
        me.fireItemMutationEvent('itemremove', removedRecs, all.startIndex || 0, removedItems, me);
        me.clearEmptyEl();
        // If nodeContainer is the el, just clear the innerHTML. Otherwise, we need
        // to manually remove each node we know about.
        me.all.clear(!nodeContainerIsTarget);
        targetEl = nodeContainerIsTarget ? targetEl.dom : me.getNodeContainer();
        if (targetEl) {
            targetEl.innerHTML = '';
        }
    },
    clearEmptyEl: function() {
        var emptyEl = this.emptyEl;
        // emptyEl is likely to be a TextNode if emptyText is not HTML code.
        // Use native DOM to remove it.
        if (emptyEl) {
            Ext.removeNode(emptyEl);
        }
        this.emptyEl = null;
    },
    onViewScroll: function(scroller, x, y) {
        if (!this.destroyed) {
            this.fireEvent('scroll', this, x, y);
        }
    },
    onViewScrollEnd: function(scroller, x, y) {
        if (!this.destroyed) {
            this.fireEvent('scrollend', this, x, y);
        }
    },
    /**
     * Saves the scrollState in a private variable.
     * Must be used in conjunction with restoreScrollState.
     * @private
     */
    saveScrollState: function() {
        var me = this,
            state = me.scrollState;
        if (me.rendered) {
            state.left = me.getScrollX();
            state.top = me.getScrollY();
        }
    },
    /**
     * Restores the scrollState.
     * Must be used in conjunction with saveScrollState
     * @private
     */
    restoreScrollState: function() {
        var me = this,
            state = me.scrollState;
        if (me.rendered) {
            me.setScrollX(state.left);
            me.setScrollY(state.top);
        }
    },
    /**
     * Function which can be overridden to provide custom formatting for each Record that is used by
     * this DataView's {@link #tpl template} to render each node.
     * @param {Object/Object[]} data The raw data object that was used to create the Record.
     * @param {Number} recordIndex the index number of the Record being prepared for rendering.
     * @param {Ext.data.Model} record The Record being prepared for rendering.
     * @return {Array/Object} The formatted data in a format expected by the internal
     * {@link #tpl template}'s overwrite() method. (either an array if your params are numeric
     * (i.e. {0}) or an object (i.e. { foo: 'bar' }))
     * @since 2.3.0
     */
    prepareData: function(data, recordIndex, record) {
        var associatedData, attr, hasCopied;
        if (record) {
            associatedData = record.getAssociatedData();
            for (attr in associatedData) {
                if (associatedData.hasOwnProperty(attr)) {
                    // This would be better done in collectData, however
                    // we only need to copy the data object if we have any associations,
                    // so we optimize it by only copying if we must.
                    // We do this so we don't mutate the underlying record.data
                    if (!hasCopied) {
                        data = Ext.Object.chain(data);
                        hasCopied = true;
                    }
                    data[attr] = associatedData[attr];
                }
            }
        }
        return data;
    },
    /**
     * Function which can be overridden which returns the data object passed to this
     * DataView's {@link #cfg-tpl template} to render the whole DataView.
     *
     * This is usually an Array of data objects, each element of which is processed by an
     * {@link Ext.XTemplate XTemplate} which uses `'&lt;tpl for="."&gt;'` to iterate over
     * its supplied data object as an Array. However, <i>named</i> properties may be placed
     * into the data object to provide non-repeating data such as headings, totals etc.
     *
     * @param {Ext.data.Model[]} records An Array of {@link Ext.data.Model}s to be rendered into
     * the DataView.
     * @param {Number} startIndex the index number of the Record being prepared for rendering.
     * @return {Object[]} An Array of data objects to be processed by a repeating XTemplate.
     * May also contain <i>named</i> properties.
     * @since 2.3.0
     */
    collectData: function(records, startIndex) {
        var data = [],
            i = 0,
            len = records.length,
            record;
        for (; i < len; i++) {
            record = records[i];
            data[i] = this.prepareData(record.data, startIndex + i, record);
        }
        return data;
    },
    cleanupData: Ext.emptyFn,
    bufferRender: function(records, index) {
        var me = this,
            div = me.renderBuffer,
            result = document.createDocumentFragment(),
            nodes, len, i;
        me.tpl.overwrite(div, me.collectData(records, index));
        nodes = div.query(me.getItemSelector());
        for (i = 0 , len = nodes.length; i < len; i++) {
            result.appendChild(nodes[i]);
        }
        return {
            fragment: result,
            children: nodes
        };
    },
    // Element which contains rows
    nodeContainerSelector: null,
    /**
     * For use by the {@link Ext.view.DragZone} plugin on platforms which use the
     * [Pointer Events standard](https://www.w3.org/TR/pointerevents/).
     *
     * If using touch scrolling, the `pointerdown` event is reserved for starting the scroll
     * gesture. To enable dragging of items using the ExtJS drag/drop system, items
     * must be set draggable. This means that `pointerdown` on view items initiate an ExtJS drag
     * and *not* a scroll gesture.
     *
     * When items are set draggable: true, pointer events platforms can still scroll using two
     * finger drag, or by dragging empty parts of the view.
     *
     * For normal dataviews, havig the backgrounc-color of items and the view be different will
     * indicate where to touch to initiate a scroll.
     *
     * For grids, if rows need to be dragged, there must be some blank space after rows
     * to touch to initiate the scroll gesture.
     *
     * @param {type} draggable
     * @private
     */
    setItemsDraggable: function(draggable) {
        var me = this,
            selector = '#' + me.id + ' ' + me.getItemSelector(),
            styleSheet = me.viewStyleSheet;
        if (draggable) {
            if (!styleSheet) {
                styleSheet = Ext.view.AbstractView.prototype.viewStyleSheet = Ext.util.CSS.createStyleSheet('', 'AbstractView');
            }
            // Pointer Events platforms implement the touch-action or -ms-touch-action properties
            // which deicate how an element responds to touches.
            // Non Pointer Events platforms such as iOS show a selection rectangle
            // on longpress+drag, and that is disabled by -webkit-user-drag: none;
            Ext.util.CSS.createRule(styleSheet, selector, 'touch-action: pinch-zoom double-tap-zoom;' + '-ms-touch-action: pinch-zoom double-tap-zoom;-webkit-user-drag: none;');
        } else if (styleSheet) {
            Ext.util.CSS.deleteRule(selector);
        }
    },
    /**
     * Returns a CSS selector which selects the element which contains record nodes.
     */
    getNodeContainerSelector: function() {
        return this.nodeContainerSelector;
    },
    onUpdate: function(store, record, operation, modifiedFieldNames, details) {
        var me = this,
            isFiltered = details && details.filtered;
        // If, due to filtering or buffered rendering, or node collapse, the updated record is not
        // represented in the rendered structure, this is a no-op.
        // The correct, new values will be rendered the next time the record becomes visible
        // and is rendered.
        if (!isFiltered && me.getNode(record)) {
            // If we are throttling UI updates (See the updateDelay global config),
            // ensure there's a change entry queued for the record in the global queue.
            if (me.throttledUpdate) {
                me.statics().queueRecordChange(me, store, record, operation, modifiedFieldNames);
            } else {
                // Cannot use arguments array.
                // TableView's signature acceses these arguments plus one more of its own.
                // Event firing passes the addListener options object as rge final parameter
                // and we must not pass that.
                me.handleUpdate(store, record, operation, modifiedFieldNames, details);
            }
        }
    },
    handleUpdate: function(store, record) {
        var me = this,
            selModel = me.getSelectionModel(),
            index, node;
        if (me.viewReady && !me.refreshNeeded) {
            index = me.dataSource.indexOf(record);
            // If the record has been removed from the data source since the changes were made,
            // do nothing
            if (index > -1) {
                // ensure the node actually exists in the DOM
                if (me.getNode(record)) {
                    node = me.bufferRender([
                        record
                    ], index).children[0];
                    me.all.replaceElement(index, node, true);
                    me.updateIndexes(index, index);
                    // Maintain selection after update
                    selModel.onUpdate(record);
                    me.refreshSizePending = true;
                    if (selModel.isSelected(record)) {
                        me.onItemSelect(record);
                    }
                    if (me.hasListeners.itemupdate) {
                        me.fireEvent('itemupdate', record, index, node, me);
                    }
                    return node;
                }
            }
        }
    },
    /**
     * @private
     * Respond to store replace event which is fired by GroupStore group expand/collapse operations.
     * This saves a layout because a remove and add operation are coalesced in this operation.
     */
    onReplace: function(store, startIndex, oldRecords, newRecords) {
        var me = this,
            all = me.all,
            scroller = me.getScrollable(),
            yPos = scroller && scroller.getPosition().y,
            selModel = me.getSelectionModel(),
            origStart = startIndex,
            result, item, fragment, children, oldItems, endIndex, restoreFocus;
        if (me.rendered) {
            // Insert the new items before the remove block
            result = me.bufferRender(newRecords, startIndex, true);
            fragment = result.fragment;
            children = result.children;
            item = all.item(startIndex);
            if (item) {
                all.item(startIndex).insertSibling(fragment, 'before', true);
            } else {
                me.appendNodes(fragment);
            }
            all.insert(startIndex, children);
            if (oldRecords.length) {
                // If focus was in the view, this will return
                // a function which will restore that state.
                // If not, a function which does nothing.
                restoreFocus = me.saveFocusState();
            }
            startIndex += newRecords.length;
            endIndex = startIndex + oldRecords.length - 1;
            // Remove the items which correspond to old records
            oldItems = all.removeRange(startIndex, endIndex, true);
            // Restore scroll position
            if (scroller) {
                scroller.scrollTo(null, yPos);
            }
            // Some subclasses do not need to do this. TableView does not need to do this.
            if (me.refreshSelmodelOnRefresh !== false) {
                selModel.refresh();
            }
            // Update the row indices (TableView) doesn't do this.
            me.updateIndexes(startIndex);
            me.fireItemMutationEvent('itemremove', oldRecords, origStart, oldItems, me);
            me.fireItemMutationEvent('itemadd', newRecords, origStart, children, me);
            // If focus was in this view, this will restore it
            restoreFocus();
            me.refreshSize();
        }
    },
    onAdd: function(store, records, index) {
        var me = this,
            nodes,
            selModel = me.getSelectionModel();
        if (me.rendered && !me.refreshNeeded) {
            // If we are adding into an empty view, we must refresh in order that
            // the *full tpl* is applied which might create boilerplate content *around*
            // the record nodes.
            if (me.all.getCount() === 0) {
                me.refresh();
                nodes = me.all.slice();
            } else {
                nodes = me.doAdd(records, index);
                // Some subclasses do not need to do this. TableView does not need to do this.
                if (me.refreshSelmodelOnRefresh !== false) {
                    selModel.refresh();
                }
                me.updateIndexes(index);
                // Ensure layout system knows about new content size
                me.refreshSizePending = true;
            }
            me.fireItemMutationEvent('itemadd', records, index, nodes, me);
        }
    },
    appendNodes: function(nodes) {
        var all = this.all,
            count = all.getCount();
        if (this.nodeContainerSelector) {
            this.getNodeContainer().appendChild(nodes);
        } else {
            // If we don't have a nodeContainerSelector, we may have our
            // itemSelector nodes wrapped in some other container, so we
            // can't just append them to the node container, it may be the wrong element
            all.item(count - 1).insertSibling(nodes, 'after');
        }
    },
    doAdd: function(records, index) {
        var me = this,
            result = me.bufferRender(records, index, true),
            fragment = result.fragment,
            children = result.children,
            all = me.all,
            count = all.getCount(),
            firstRowIndex = all.startIndex || 0,
            lastRowIndex = all.endIndex || count - 1;
        if (count === 0 || index > lastRowIndex) {
            me.appendNodes(fragment);
        } else if (index <= firstRowIndex) {
            all.item(firstRowIndex).insertSibling(fragment, 'before', true);
        } else {
            all.item(index).insertSibling(children, 'before', true);
        }
        all.insert(index, children);
        return children;
    },
    onRemove: function(store, records, index) {
        var me = this,
            rows = me.all,
            currIdx, i, record, nodes, node, restoreFocus;
        if (me.rendered && !me.refreshNeeded && rows.getCount()) {
            if (me.dataSource.getCount() === 0) {
                me.refresh();
            } else {
                // If this view contains focus, this will return
                // a function which will restore that state.
                restoreFocus = me.saveFocusState();
                // Just remove the elements which corresponds to the removed records
                // The tpl's full HTML will still be in place.
                nodes = [];
                for (i = records.length - 1; i >= 0; --i) {
                    record = records[i];
                    currIdx = index + i;
                    if (nodes) {
                        node = rows.item(currIdx);
                        nodes[i] = node ? node.dom : undefined;
                    }
                    if (rows.item(currIdx)) {
                        me.doRemove(record, currIdx);
                    }
                }
                me.fireItemMutationEvent('itemremove', records, index, nodes, me);
                // If focus was in this view, this will restore it
                restoreFocus();
                me.updateIndexes(index);
            }
            // Ensure layout system knows about new content size
            me.refreshSizePending = true;
        }
    },
    doRemove: function(record, index) {
        this.all.removeElement(index, true);
    },
    eventLifecycleMap: {
        refresh: 'onViewRefresh',
        itemremove: 'onItemRemove',
        itemadd: 'onItemAdd'
    },
    fireItemMutationEvent: function(eventName) {
        var me = this,
            ownerGrid = me.ownerGrid,
            vm;
        Ext.suspendLayouts();
        // Inform the ownerGrid.
        if (ownerGrid) {
            if (eventName !== 'refresh') {
                vm = me.lookupViewModel();
            }
            ownerGrid[me.eventLifecycleMap[eventName]].apply(ownerGrid, Ext.Array.slice(arguments, 1));
        }
        me.fireEvent.apply(me, arguments);
        // The content height MUST be measurable by the caller (the buffered renderer),
        // so data must be flushed to it immediately.
        if (vm) {
            vm.notify();
        }
        Ext.resumeLayouts(true);
    },
    /**
     * @private
     * Called prior to an operation which mey remove focus from this view by some kind
     * of DOM operation.
     *
     * If this view contains focus, this method returns a function which, when called after
     * the disruptive DOM operation will restore focus to the same record, or, if the record has
     * been removed to the same item index..
     *
     * @returns {Function} A function that will restore focus if focus was within this view,
     * or a function which does nothing is focus is not in this view.
     */
    saveFocusState: function() {
        var me = this,
            store = me.dataSource || me.store,
            navModel = me.getNavigationModel(),
            lastFocusedIndex = navModel.recordIndex,
            lastFocusedRec = navModel.record,
            containsFocus = me.el.contains(Ext.Element.getActiveElement());
        // If there is a position to restore...
        if (lastFocusedRec) {
            // Check if we really have focus.
            // Some NavigationModels record position with focus outside of the view.
            // This happens in BoundLists when focus stays in the bound field.
            // Blur the focused descendant, but do not trigger focusLeave.
            if (containsFocus) {
                me.el.dom.focus();
            }
            // The following function will attempt to refocus back to the same record
            // if it is still there, or the same item index.
            return function() {
                // If we still have data, attempt to refocus at the same record,
                // or the same item index..
                if (store.getCount()) {
                    // Adjust expectations of where we are able to refocus according to
                    // what kind of destruction might have been wrought on this view's DOM
                    // during focus save.
                    lastFocusedIndex = Math.min(lastFocusedIndex, me.all.getCount() - 1);
                    navModel.setPosition(store.contains(lastFocusedRec) ? lastFocusedRec : lastFocusedIndex, null, null, true, !containsFocus);
                }
            };
        }
        return Ext.emptyFn;
    },
    /**
     * Refreshes an individual node's data from the store.
     * @param {Ext.data.Model/Number} record The record or index of the record to update.
     * @since 2.3.0
     */
    refreshNode: function(record) {
        if (Ext.isNumber(record)) {
            record = this.store.getAt(record);
        }
        this.onUpdate(this.dataSource, record);
    },
    updateIndexes: function(startIndex, endIndex) {
        var me = this,
            nodes = me.all.elements,
            records = me.getViewRange(),
            selModel = me.getSelectionModel(),
            myId = me.id,
            node, record, i;
        startIndex = startIndex || 0;
        endIndex = endIndex || ((endIndex === 0) ? 0 : (nodes.length - 1));
        for (i = startIndex; i <= endIndex; i++) {
            node = nodes[i];
            record = records[i];
            node.setAttribute('data-recordIndex', i);
            node.setAttribute('data-recordId', record.internalId);
            node.setAttribute('data-boundView', myId);
            if (selModel.getLastSelected()) {
                me[selModel.isSelected(record) ? 'onItemSelect' : 'onItemDeselect'](record);
            }
        }
    },
    /**
     * Changes the data store bound to this view and refreshes it.
     * @param {Ext.data.Store} store The store to bind to this view
     * @param {Object} initial
     * @since 3.4.0
     */
    bindStore: function(store, initial) {
        var me = this,
            selModel = me.getSelectionModel(),
            navModel = me.getNavigationModel();
        // Can be already destroyed if we're called from doDestroy()
        if (selModel && !selModel.destroyed) {
            selModel.bindStore(store, initial);
            selModel.bindComponent(store ? me : null);
        }
        me.mixins.storeholder.bindStore.apply(me, arguments);
        // Navigation model must bind to new store
        if (navModel && !navModel.destroyed) {
            navModel.setStore(store);
        }
        // If we have already achieved our first layout, refresh immediately.
        // If we bind to the Store before the first layout, then beforeLayout will
        // call doFirstRefresh
        if (store && me.componentLayoutCounter && !me.blockRefresh) {
            // If not the initial bind, we enforce noDefer.
            me.doFirstRefresh(store, !initial);
        }
    },
    /**
     * @private
     * Perform the first refresh of the View from a newly bound store.
     *
     * This is called when this View has been sized for the first time.
     */
    doFirstRefresh: function(store, noDefer) {
        var me = this;
        // If we are configured to defer, and *NOT* called from the defer call below
        if (me.deferInitialRefresh && !noDefer) {
            Ext.defer(me.doFirstRefresh, 1, me, [
                store,
                true
            ]);
        } else {
            // 4.1.0: If we have a store, and the Store is *NOT* already loading
            // (a refresh is on the way), then on first layout, refresh regardless of record count.
            // Template may contain boilerplate HTML outside of record iteration loop.
            // Also, emptyText is appended by the refresh method.
            if (store && !me.deferRefreshForLoad(store)) {
                me.refresh();
            }
        }
    },
    onUnbindStore: function(store) {
        this.setMaskBind(null);
        if (this.dataSource === store) {
            this.dataSource = null;
        }
    },
    onBindStore: function(store, oldStore) {
        var me = this;
        // A BufferedStore has to know to reload the most recent visible zone if its View
        // is preserveScrollOnReload
        if (me.store.isBufferedStore) {
            me.store.preserveScrollOnReload = me.preserveScrollOnReload;
        }
        if (oldStore && oldStore.isBufferedStore) {
            delete oldStore.preserveScrollOnReload;
        }
        me.setMaskBind(store);
        // When unbinding the data store, the dataSource will be nulled out
        // if it's the same as the data store. Restore it here.
        if (!me.dataSource) {
            me.dataSource = store;
        }
    },
    setMaskBind: function(store) {
        var mask = this.loadMask;
        if (this.rendered && mask && store && !mask.bindStore) {
            mask = this.createMask();
        }
        if (mask && mask.bindStore) {
            mask.bindStore(store);
        }
    },
    getStoreListeners: function() {
        var me = this;
        return {
            refresh: me.onDataRefresh,
            replace: me.onReplace,
            add: me.onAdd,
            remove: me.onRemove,
            update: me.onUpdate,
            clear: me.onDataRefresh,
            beginupdate: me.onBeginUpdate,
            endupdate: me.onEndUpdate
        };
    },
    onBeginUpdate: function() {
        ++this.updateSuspendCounter;
        Ext.suspendLayouts();
    },
    onEndUpdate: function() {
        var me = this;
        if (me.updateSuspendCounter) {
            --me.updateSuspendCounter;
        }
        Ext.resumeLayouts(true);
        if (me.refreshSizePending) {
            me.refreshSize(true);
            me.refreshSizePending = false;
        }
    },
    /**
     * @private
     * Calls this.refresh if this.blockRefresh is not true
     * @since 3.4.0
     */
    onDataRefresh: function(store) {
        var me = this,
            preserveScrollOnRefresh = me.preserveScrollOnRefresh;
        // If this refresh event is fire from a store load, then use the 
        // preserveScrollOnReload setting to decide whether to preserve scroll position
        if (store.loadCount >= (me.lastRefreshLoadCount || 0)) {
            me.preserveScrollOnRefresh = me.preserveScrollOnReload;
        }
        me.refreshView();
        me.preserveScrollOnRefresh = preserveScrollOnRefresh;
        me.lastRefreshLoadCount = store.loadCount;
    },
    refreshView: function(startIndex) {
        var me = this,
            // If we have an ancestor in a non-boxready state (collapsed or about to collapse,
            // or hidden), then block the refresh because the next layout will trigger the refresh
            blocked = me.blockRefresh || !me.rendered || me.up('[collapsed],[isCollapsingOrExpanding=1],[hidden]'),
            bufferedRenderer = me.bufferedRenderer;
        // If we are blocked in any way due to either a setting, or hidden or collapsed,
        // or animating ancestor, then the next refresh attempt at the upcoming layout
        // must not defer.
        if (blocked) {
            me.refreshNeeded = true;
        } else {
            if (bufferedRenderer) {
                bufferedRenderer.refreshView(startIndex);
            } else {
                me.refresh();
            }
        }
    },
    /**
     * Returns the template node the passed child belongs to, or null if it doesn't belong to one.
     * @param {HTMLElement} node
     * @return {HTMLElement} The template node
     */
    findItemByChild: function(node) {
        return Ext.fly(node).findParent(this.getItemSelector(), this.getTargetEl());
    },
    /**
     * Returns the template node by the Ext.event.Event or null if it is not found.
     * @param {Ext.event.Event} e
     */
    findTargetByEvent: function(e) {
        return e.getTarget(this.getItemSelector(), this.getTargetEl());
    },
    /**
     * Gets the currently selected nodes.
     * @return {HTMLElement[]} An array of HTMLElements
     * @since 2.3.0
     */
    getSelectedNodes: function() {
        var nodes = [],
            records = this.getSelectionModel().getSelection(),
            ln = records.length,
            i = 0;
        for (; i < ln; i++) {
            nodes.push(this.getNode(records[i]));
        }
        return nodes;
    },
    /**
     * Gets an array of the records from an array of nodes
     * @param {HTMLElement[]} nodes The nodes to evaluate
     * @return {Ext.data.Model[]} records The {@link Ext.data.Model} objects
     * @since 2.3.0
     */
    getRecords: function(nodes) {
        var me = this,
            records = [],
            i;
        for (i = 0; i < nodes.length; i++) {
            records.push(me.getRecord(nodes[i]));
        }
        return records;
    },
    /**
     * Gets a record from a node
     * @param {Ext.dom.Element/HTMLElement} node The node to evaluate
     *
     * @return {Ext.data.Model} record The {@link Ext.data.Model} object
     * @since 2.3.0
     */
    getRecord: function(node) {
        var dom = Ext.getDom(node),
            id = dom.getAttribute('data-recordId');
        return this.dataSource.getByInternalId(id);
    },
    /**
     * Returns true if the passed node is selected, else false.
     * @param {HTMLElement/Number/Ext.data.Model} node The node, node index or record to check
     * @return {Boolean} True if selected, else false
     * @since 2.3.0
     */
    isSelected: function(node) {
        var r = this.getRecord(node);
        return this.getSelectionModel().isSelected(r);
    },
    /**
     * Selects a record instance by record instance or index.
     * @param {Ext.data.Model[]/Number} records An array of records or an index
     * @param {Boolean} keepExisting
     * @param {Boolean} suppressEvent Set to false to not fire a select event
     * @deprecated 4.0 Use {@link Ext.selection.Model#select} instead.
     * @since 2.3.0
     */
    select: function(records, keepExisting, suppressEvent) {
        this.getSelectionModel().select(records, keepExisting, suppressEvent);
    },
    /**
     * Deselects a record instance by record instance or index.
     * @param {Ext.data.Model[]/Number} records An array of records or an index
     * @param {Boolean} suppressEvent Set to false to not fire a deselect event
     * @since 2.3.0
     */
    deselect: function(records, suppressEvent) {
        this.getSelectionModel().deselect(records, suppressEvent);
    },
    /**
     * Gets a template node.
     * @param {HTMLElement/String/Number/Ext.data.Model} nodeInfo An HTMLElement template node,
     * index of a template node, the id of a template node or the record associated with the node.
     * @return {HTMLElement} The node or null if it wasn't found
     * @since 2.3.0
     */
    getNode: function(nodeInfo) {
        var me = this,
            out;
        if (me.rendered && (nodeInfo || nodeInfo === 0)) {
            if (Ext.isString(nodeInfo)) {
                // Id
                out = document.getElementById(nodeInfo);
            } else if (nodeInfo.isModel) {
                // Record
                out = me.getNodeByRecord(nodeInfo);
            } else if (Ext.isNumber(nodeInfo)) {
                // Index
                out = me.all.elements[nodeInfo];
            } else {
                if (nodeInfo.target && nodeInfo.target.nodeType) {
                    // An event. Check that target is a node: <a target="_blank">
                    // must pass unchanged
                    nodeInfo = nodeInfo.target;
                }
                // already an HTMLElement
                out = Ext.fly(nodeInfo).findParent(me.itemSelector, me.getTargetEl());
            }
        }
        return out || null;
    },
    /**
     * @private
     */
    getNodeByRecord: function(record) {
        var index = this.store.indexOf(record);
        return this.all.elements[index] || null;
    },
    /**
     * Gets a range nodes.
     * @param {Number} start (optional) The index of the first node in the range
     * @param {Number} end (optional) The index of the last node in the range
     * @return {HTMLElement[]} An array of nodes
     * @since 2.3.0
     */
    getNodes: function(start, end) {
        var all = this.all;
        if (end !== undefined) {
            end++;
        }
        return all.slice(start, end);
    },
    /**
     * Finds the index of the passed node.
     * @param {HTMLElement/String/Number/Ext.data.Model} node An HTMLElement template node,
     * index of a template node, the id of a template node or a record associated with a node.
     * @return {Number} The index of the node or -1
     * @since 2.3.0
     */
    indexOf: function(node) {
        node = this.getNode(node);
        if (!node && node !== 0) {
            return -1;
        }
        if (node.getAttribute('data-recordIndex')) {
            return Number(node.getAttribute('data-recordIndex'));
        }
        return this.all.indexOf(node);
    },
    doDestroy: function() {
        var me = this,
            count = me.updateSuspendCounter,
            tabGuardEl = me.tabGuardEl;
        if (me.viewScrollListeners) {
            me.viewScrollListeners.destroy();
        }
        // Can be already destroyed in Table view
        if (me.all && !me.all.destroyed) {
            me.all.clear();
        }
        if (tabGuardEl) {
            if (tabGuardEl.parentNode) {
                tabGuardEl.parentNode.removeChild(tabGuardEl);
            }
        }
        me.emptyEl = null;
        me.setItemsDraggable(false);
        me.bindStore(null);
        if (me.selModelRelayer) {
            me.selModelRelayer.destroy();
        }
        Ext.destroy(me.navigationModel, me.selectionModel, me.loadMask);
        // We have been destroyed during a begin/end update, which means we're
        // suspending layouts, must forcibly do it here.
        while (count--) {
            Ext.resumeLayouts(true);
        }
        me.callParent();
    },
    // invoked by the selection model to maintain visual UI cues
    onItemSelect: function(record) {
        var node = this.getNode(record);
        if (node) {
            Ext.fly(node).addCls(this.selectedItemCls);
            node.setAttribute('aria-selected', 'true');
        }
        return node;
    },
    // invoked by the selection model to maintain visual UI cues
    onItemDeselect: function(record) {
        var node = this.getNode(record);
        if (node) {
            Ext.fly(node).removeCls(this.selectedItemCls);
            node.setAttribute('aria-selected', 'false');
        }
        return node;
    },
    getItemSelector: function() {
        return this.itemSelector;
    },
    /**
     * Adds a CSS Class to a specific item.
     * @param {HTMLElement/String/Number/Ext.data.Model} itemInfo An HTMLElement,
     * index or instance of a model representing this item
     * @param {String} cls
     */
    addItemCls: function(itemInfo, cls) {
        var item = this.getNode(itemInfo);
        if (item) {
            Ext.fly(item).addCls(cls);
        }
    },
    /**
     * Removes a CSS Class from a specific item.
     * @param {HTMLElement/String/Number/Ext.data.Model} itemInfo An HTMLElement,
     * index or instance of a model representing this item
     * @param {String} cls
     */
    removeItemCls: function(itemInfo, cls) {
        var item = this.getNode(itemInfo);
        if (item) {
            Ext.fly(item).removeCls(cls);
        }
    },
    setStore: function(newStore) {
        // Here we want to override the config system setter because setting the store
        // is a special case that the config system wasn't able to handle.
        //
        // For instance, because `bindStore` is the only API for both binding and unbinding a store,
        // we couldn't unbind the old store using the config system because it would simply unbind
        // the new store that the setter had just poked onto the instance:
        //
        //      setStore    -> intance.store = newStore
        //      updateStore -> view.unbind(null) (unbinds the newStore)
        //
        var me = this;
        if (me.store !== newStore) {
            if (me.isConfiguring) {
                me.store = newStore;
            } else {
                me.bindStore(newStore, /* initial */
                false);
            }
        }
    },
    privates: {
        deferRefreshForLoad: function(store) {
            return store.isLoading();
        },
        toggleChildrenTabbability: function(enableTabbing) {
            var focusEl = this.getTargetEl();
            if (enableTabbing) {
                focusEl.restoreTabbableState({
                    skipSelf: true
                });
            } else {
                // Do NOT includeSaved
                // Once an item has had tabbability saved, do not increment its save level
                focusEl.saveTabbableState({
                    skipSelf: true,
                    includeSaved: false
                });
            }
        },
        /**
         * @private
         * Called by refresh to collect the view item nodes.
         */
        collectNodes: function(targetEl) {
            var all = this.all,
                options = {
                    role: this.itemAriaRole
                };
            all.fill(Ext.fly(targetEl).query(this.getItemSelector()), all.startIndex || 0);
            // Subclasses may set focusable to false (BoundList is not focusable)
            if (this.focusable) {
                options.tabindex = '-1';
            }
            all.set(options);
        },
        createMask: function(mask) {
            var me = this,
                maskStore = me.getStore(),
                cfg;
            if (maskStore && !maskStore.isEmptyStore && !maskStore.loadsSynchronously()) {
                cfg = {
                    target: me,
                    msg: me.loadingText,
                    useMsg: me.loadingUseMsg,
                    // The store gets bound in initComponent, so while
                    // rendering let's push on the store
                    store: maskStore
                };
                // Do not overwrite default msgCls if we do not have a loadingCls
                if (me.loadingCls) {
                    cfg.msgCls = me.loadingCls;
                }
                // either a config object 
                if (Ext.isObject(mask)) {
                    cfg = Ext.apply(cfg, mask);
                }
                // Attach the LoadMask to a *Component* so that it can be sensitive to resizing
                // during long loads. If this DataView is floating, then mask this DataView.
                // Otherwise, mask its owning Container (or this, if there *is* no owning Container)
                // LoadMask captures the element upon render.
                me.loadMask = new Ext.LoadMask(cfg);
                me.loadMask.on({
                    scope: me,
                    beforeshow: me.onMaskBeforeShow,
                    hide: me.onMaskHide
                });
            }
            return me.loadMask;
        },
        /**
         * @private
         * This method returns the inner node containing element. This is useful
         * for the bufferedRenderer or for when the view contains extra elements
         * and we need to point the exact element that will contain the view nodes.
         */
        getNodeContainer: function() {
            var target = this.getTargetEl(),
                selector = this.nodeContainerSelector;
            return selector ? target.down(selector, true) : target;
        },
        getOverflowEl: function() {
            // The desired behavior here is just to inherit from the superclass.  However,
            // the superclass method calls this.getTargetEl, which sends us into an infinte
            // loop because our getTargetEl may call getScrollerEl(), which calls getOverflowEl()
            return Ext.Component.prototype.getTargetEl.call(this);
        }
    }
}, function() {
    // all of this information is available directly
    // from the SelectionModel itself, the only added methods
    // to DataView regarding selection will perform some transformation/lookup
    // between HTMLElement/Nodes to records and vice versa.
    Ext.deprecate('extjs', '4.0', function() {
        Ext.view.AbstractView.override({
            /**
             * @cfg {Boolean} [multiSelect=false]
             * True to allow selection of more than one item at a time, false to allow selection
             * of only a single item at a time or no selection at all, depending on the value of
             * {@link #singleSelect}.
             * @deprecated 4.0 Use {@link Ext.selection.Model#cfg-mode} 'MULTI' instead.
             * @since 2.3.0
             */
            /**
             * @cfg {Boolean} [singleSelect]
             * Allows selection of exactly one item at a time. As this is the default selection mode
             * anyway, this config is completely ignored.
             * @removed 4.0 Use {@link Ext.selection.Model#cfg-mode} 'SINGLE' instead.
             * @since 2.3.0
             */
            /**
             * @cfg {Boolean} [simpleSelect=false]
             * True to enable multiselection by clicking on multiple items without requiring
             * the user to hold Shift or Ctrl, false to force the user to hold Ctrl or Shift
             * to select more than on item.
             * @deprecated 4.0 Use {@link Ext.selection.Model#cfg-mode} 'SIMPLE' instead.
             * @since 2.3.0
             */
            /**
             * Gets the number of selected nodes.
             * @return {Number} The node count
             * @deprecated 4.0 Use {@link Ext.selection.Model#getCount} instead.
             * @since 2.3.0
             */
            getSelectionCount: function() {
                if (Ext.global.console) {
                    Ext.global.console.warn("DataView: getSelectionCount will be removed, please interact " + "with the Ext.selection.DataViewModel");
                }
                return this.selModel.getSelection().length;
            },
            /**
             * Gets an array of the selected records
             * @return {Ext.data.Model[]} An array of {@link Ext.data.Model} objects
             * @deprecated 4.0 Use {@link Ext.selection.Model#getSelection} instead.
             * @since 2.3.0
             */
            getSelectedRecords: function() {
                if (Ext.global.console) {
                    Ext.global.console.warn("DataView: getSelectedRecords will be removed, please interact " + "with the Ext.selection.DataViewModel");
                }
                return this.selModel.getSelection();
            },
            // documented above
            // @ignore
            select: function(records, keepExisting, supressEvents) {
                if (Ext.global.console) {
                    Ext.global.console.warn("DataView: select will be removed, please access select through " + "a DataView's SelectionModel, ie: view.getSelectionModel().select()");
                }
                // eslint-disable-next-line vars-on-top
                var sm = this.getSelectionModel();
                return sm.select.apply(sm, arguments);
            },
            /**
             * Deselects all selected records.
             * @deprecated 4.0 Use {@link Ext.selection.Model#deselectAll} instead.
             * @since 2.3.0
             */
            clearSelections: function() {
                if (Ext.global.console) {
                    Ext.global.console.warn("DataView: clearSelections will be removed, please access " + "deselectAll through DataView's SelectionModel, ie: " + "view.getSelectionModel().deselectAll()");
                }
                // eslint-disable-next-line vars-on-top
                var sm = this.getSelectionModel();
                return sm.deselectAll();
            }
        });
    });
});

/**
 * A mechanism for displaying data using custom layout templates and formatting.
 *
 * The View uses an {@link Ext.XTemplate} as its internal templating mechanism, and is bound to an
 * {@link Ext.data.Store} so that as the data in the store changes the view is automatically updated
 * to reflect the changes. The view also provides built-in behavior for many common events that can
 * occur for its contained items including click, doubleclick, mouseover, mouseout, etc. as well as
 * a built-in selection model. **In order to use these features, an {@link #itemSelector} config
 * must be provided for the View to determine what nodes it will be working with.**
 *
 * The example below binds a View to a {@link Ext.data.Store} and renders it into an
 * {@link Ext.panel.Panel}.
 *
 *     @example
 *     Ext.define('Image', {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             { name: 'src', type: 'string' },
 *             { name: 'caption', type: 'string' }
 *         ]
 *     });
 *
 *     Ext.create('Ext.data.Store', {
 *         id:'imagesStore',
 *         model: 'Image',
 *         data: [
 *             {
 *                 src: 'http://www.sencha.com/img/20110215-feat-drawing.png',
 *                 caption: 'Drawing & Charts'
 *             },
 *             {
 *                 src: 'http://www.sencha.com/img/20110215-feat-data.png',
 *                 caption: 'Advanced Data'
 *             },
 *             {
 *                 src: 'http://www.sencha.com/img/20110215-feat-html5.png',
 *                 caption: 'Overhauled Theme'
 *             },
 *             {
 *                 src: 'http://www.sencha.com/img/20110215-feat-perf.png',
 *                 caption: 'Performance Tuned'
 *             }
 *         ]
 *     });
 *
 *     var imageTpl = new Ext.XTemplate(
 *         '<tpl for=".">',
 *             '<div style="margin-bottom: 10px;" class="thumb-wrap">',
 *               '<img src="{src}" />',
 *               '<br/><span>{caption}</span>',
 *             '</div>',
 *         '</tpl>'
 *     );
 *
 *     Ext.create('Ext.view.View', {
 *         store: Ext.data.StoreManager.lookup('imagesStore'),
 *         tpl: imageTpl,
 *         itemSelector: 'div.thumb-wrap',
 *         emptyText: 'No images available',
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.view.View', {
    extend: 'Ext.view.AbstractView',
    alternateClassName: 'Ext.DataView',
    alias: 'widget.dataview',
    inputTagRe: /^textarea$|^input$/i,
    keyEventRe: /^key/,
    manageLayoutScroll: false,
    inheritableStatics: {
        /**
         * @private
         * @static
         * @inheritable
         */
        EventMap: {
            longpress: 'LongPress',
            mousedown: 'MouseDown',
            mouseup: 'MouseUp',
            click: 'Click',
            dblclick: 'DblClick',
            contextmenu: 'ContextMenu',
            mouseover: 'MouseOver',
            mouseout: 'MouseOut',
            mouseenter: 'MouseEnter',
            mouseleave: 'MouseLeave',
            keydown: 'KeyDown',
            keyup: 'KeyUp',
            keypress: 'KeyPress',
            focus: 'Focus'
        },
        /**
         * @private
         * @static
         * @inheritable
         */
        TouchEventMap: {
            touchstart: 'mousedown',
            touchend: 'mouseup',
            tap: 'click',
            doubletap: 'dblclick'
        }
    },
    /**
     * @event beforeitemmousedown
     * @preventable
     * Fires before the mousedown event on an item is processed. Return false to cancel 
     * the default action.
     * @param {Ext.view.View} this
     * @param {Ext.data.Model} record The record that belongs to the item
     * @param {HTMLElement} item The item's element
     * @param {Number} index The item's index
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event beforeitemmouseup
     * @preventable
     * Fires before the mouseup event on an item is processed. Return false to cancel 
     * the default action.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event beforeitemmouseenter
     * @preventable
     * Fires before the mouseenter event on an item is processed. Return false to cancel 
     * the default action.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event beforeitemmouseleave
     * @preventable
     * Fires before the mouseleave event on an item is processed. Return false to cancel 
     * the default action.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event beforeitemclick
     * @preventable
     * Fires before the click event on an item is processed. Return false to cancel the 
     * default action.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event beforeitemdblclick
     * @preventable
     * Fires before the dblclick event on an item is processed. Return false to cancel 
     * the default action.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event beforeitemcontextmenu
     * @preventable
     * Fires before the contextmenu event on an item is processed. Return false to 
     * cancel the default action.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event beforeitemlongpress
     * @preventable
     * Fires before the longpress event on an item is processed. Return false to 
     * cancel the default action.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event beforeitemkeydown
     * @preventable
     * Fires before the keydown event on an item is processed. Return false to cancel 
     * the default action.
     *
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event beforeitemkeyup
     * @preventable
     * Fires before the keyup event on an item is processed. Return false to cancel the 
     * default action.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event beforeitemkeypress
     * @preventable
     * Fires before the keypress event on an item before it is processed. Return false to cancel
     * the default action.
     *
     * **Note:** beforeitemkeypress is ONLY triggered by characters, numbers, and some action keys
     * (esc, tab, backspace, space). If you need an event that is triggered by other keys,
     * like function keys, arrow keys, or shift, ctrl, alt, use beforeitemkeydown.
     *
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event itemmousedown
     * Fires when there is a mouse down on an item
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event itemmouseup
     * Fires when there is a mouse up on an item
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event itemmouseenter
     * Fires when the mouse enters an item.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event itemmouseleave
     * Fires when the mouse leaves an item.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event itemclick
     * Fires when an item is clicked.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event itemdblclick
     * Fires when an item is double clicked.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event itemcontextmenu
     * Fires when an item is right clicked.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event itemlongpress
     * Fires on a longpress event on an item.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event itemkeydown
     * Fires when a key is pressed down while an item is currently selected.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event itemkeyup
     * Fires when a key is released while an item is currently selected.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event itemkeypress
     * Fires when a key is pressed while an item is currently selected.
     *
     * **Note:** itemkeypress is ONLY triggered by characters, numbers, and some action keys
     * (esc, tab, backspace, space). If you need an event that is triggered by other keys,
     * like function keys, arrow keys, or shift, ctrl, alt, use itemkeydown.
     * @inheritdoc #beforeitemmousedown
     */
    /**
     * @event beforecontainermousedown
     * Fires before the mousedown event on the container is processed.
     * Returns false to cancel the default action.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event beforecontainermouseup
     * Fires before the mouseup event on the container is processed.
     * Returns false to cancel the default action.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event beforecontainermouseover
     * Fires before the mouseover event on the container is processed.
     * Returns false to cancel the default action.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event beforecontainermouseout
     * Fires before the mouseout event on the container is processed.
     * Returns false to cancel the default action.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event beforecontainerclick
     * Fires before the click event on the container is processed.
     * Returns false to cancel the default action.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event beforecontainerdblclick
     * Fires before the dblclick event on the container is processed.
     * Returns false to cancel the default action.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event beforecontainercontextmenu
     * Fires before the contextmenu event on the container is processed.
     * Returns false to cancel the default action.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event beforecontainerkeydown
     * Fires before the keydown event on the container is processed.
     * Returns false to cancel the default action.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object. Use {@link Ext.event.Event#getKey getKey()}
     * to retrieve the key that was pressed.
     */
    /**
     * @event beforecontainerkeyup
     * Fires before the keyup event on the container is processed.
     * Returns false to cancel the default action.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object. Use {@link Ext.event.Event#getKey getKey()}
     * to retrieve the key that was pressed.
     */
    /**
     * @event beforecontainerkeypress
     * Fires before the keypress event on the container is processed.
     * Returns false to cancel the default action.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object. Use {@link Ext.event.Event#getKey getKey()}
     * to retrieve the key that was pressed.
     */
    /**
     * @event containermousedown
     * Fires when there is a mousedown on the container
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event containermouseup
     * Fires when there is a mouseup on the container
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event containermouseover
     * Fires when you move the mouse over the container.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event containermouseout
     * Fires when you move the mouse out of the container.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event containerclick
     * Fires when the container is clicked.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event containerdblclick
     * Fires when the container is double clicked.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event containercontextmenu
     * Fires when the container is right clicked.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object
     */
    /**
     * @event containerkeydown
     * Fires when a key is pressed down while the container is focused, and no item
     * is currently selected.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object. Use {@link Ext.event.Event#getKey getKey()}
     * to retrieve the key that was pressed.
     */
    /**
     * @event containerkeyup
     * Fires when a key is released while the container is focused, and no item
     * is currently selected.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object. Use {@link Ext.event.Event#getKey getKey()}
     * to retrieve the key that was pressed.
     */
    /**
     * @event containerkeypress
     * Fires when a key is pressed while the container is focused, and no item
     * is currently selected.
     * @param {Ext.view.View} this
     * @param {Ext.event.Event} e The raw event object. Use {@link Ext.event.Event#getKey getKey()}
     * to retrieve the key that was pressed.
     */
    /**
     * @event selectionchange
     * @inheritdoc Ext.selection.DataViewModel#selectionchange
     */
    /**
     * @event beforeselect
     * @inheritdoc Ext.selection.DataViewModel#beforeselect
     */
    /**
     * @event beforedeselect
     * @inheritdoc Ext.selection.DataViewModel#beforedeselect
     */
    /**
     * @event select
     * @inheritdoc Ext.selection.DataViewModel#select
     */
    /**
     * @event deselect
     * @inheritdoc Ext.selection.DataViewModel#deselect
     */
    /**
     * @event focuschange
     * @inheritdoc Ext.selection.DataViewModel#focuschange
     */
    /**
     * @event highlightitem
     * Fires when a node is highlighted using keyboard navigation, or mouseover.
     * @param {Ext.view.View} view This View Component.
     * @param {Ext.dom.Element} node The highlighted node.
     */
    /**
     * @event unhighlightitem
     * Fires when a node is unhighlighted using keyboard navigation, or mouseout.
     * @param {Ext.view.View} view This View Component.
     * @param {Ext.dom.Element} node The previously highlighted node.
     */
    afterRender: function() {
        var me = this;
        me.callParent();
        me.mon(me.el, {
            scope: me,
            click: me.handleEvent,
            longpress: me.handleEvent,
            mousedown: me.handleEvent,
            mouseup: me.handleEvent,
            dblclick: me.handleEvent,
            contextmenu: me.handleEvent,
            keydown: me.handleEvent,
            keyup: me.handleEvent,
            keypress: me.handleEvent,
            mouseover: me.handleMouseOver,
            mouseout: me.handleMouseOut
        });
    },
    // Can be overridden by features or anything that needs to use a specific selector as a target.
    getTargetSelector: function() {
        return this.dataRowSelector || this.itemSelector;
    },
    handleMouseOver: function(e) {
        var me = this,
            // this.getTargetSelector() can be used as a template method, e.g., in features.
            itemSelector = me.getTargetSelector(),
            item = e.getTarget(itemSelector);
        // If mouseover/out handling is buffered, view might have been destyroyed
        // during buffer time.
        if (!me.destroyed) {
            if (item) {
                if (me.mouseOverItem !== item && me.el.contains(item)) {
                    me.mouseOverItem = e.item = item;
                    e.newType = 'mouseenter';
                    me.handleEvent(e);
                }
            } else {
                // We're not over an item, so handle a container event.
                me.handleEvent(e);
            }
        }
    },
    handleMouseOut: function(e) {
        var me = this,
            itemSelector = me.getTargetSelector(),
            item = e.getTarget(itemSelector),
            computedRelatedTarget = e.getRelatedTarget(itemSelector),
            sourceView;
        // We can only exit early when mousing within the same row, but we can't simply do
        // an equality check since it's valid for both item and computedRelatedTarget to be null!
        if ((item === computedRelatedTarget) && !(item === null && computedRelatedTarget === null)) {
            return;
        }
        // Note that a mouseout event can trigger either an item event or a container event.
        // If mouseover/out handling is buffered, view might have been destroyed during buffer time.
        if (!me.destroyed) {
            // eslint-disable-next-line no-cond-assign
            if (item && (sourceView = me.self.getBoundView(item))) {
                e.item = item;
                e.newType = 'mouseleave';
                sourceView.handleEvent(e);
                sourceView.mouseOverItem = null;
            } else {
                // We're not over an item, so handle a container event.
                me.handleEvent(e);
            }
        }
    },
    handleEvent: function(e) {
        var me = this,
            isKeyEvent = me.keyEventRe.test(e.type);
        // We need to know if the event target is an input field to block
        // drag n' drop plugin(s) from stopping pointer events as this makes
        // input fields unfocusable and unselectable. We also need to know
        // this for key events to prevent scrolling, see below.
        e.isInputFieldEvent = Ext.fly(e.target).isInputField();
        e.view = me;
        // Find the item from the event target.
        e.item = e.getTarget(me.itemSelector);
        if (e.item) {
            e.record = me.getRecord(e.item);
        }
        // Event handlers could have destroyed the view
        if (me.processUIEvent(e) !== false && !me.destroyed) {
            me.processSpecialEvent(e);
        }
        // We need to prevent default action on navigation keys
        // that can cause View element scroll unless the event is from an input field.
        // We MUST prevent browser's default action on SPACE which is to focus
        // the event's target element.
        // Focusing causes the browser to attempt to scroll the element into view.
        if (isKeyEvent && !e.isInputFieldEvent) {
            if (e.getKey() === e.SPACE || e.isNavKeyPress(true)) {
                e.preventDefault();
            }
        }
        e.view = null;
    },
    /**
     * @private
     */
    processItemEvent: Ext.emptyFn,
    processContainerEvent: Ext.emptyFn,
    processSpecialEvent: Ext.emptyFn,
    processUIEvent: function(e) {
        // If the target event has been removed from the body (data update causing view DOM
        // to be updated),
        // do not process. isAncestor uses native methods to check.
        if (!Ext.getBody().isAncestor(e.target)) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            item = e.item,
            self = me.self,
            map = self.EventMap,
            touchMap = self.TouchEventMap,
            index,
            record = e.record,
            type = e.type,
            newType = type;
        // If the event is a mouseover/mouseout event converted to a mouseenter/mouseleave,
        // use that event type.
        if (e.newType) {
            newType = e.newType;
        }
        if (item) {
            newType = touchMap[newType] || newType;
            index = e.recordIndex = me.indexInStore ? me.indexInStore(record) : me.indexOf(item);
            // It is possible for an event to arrive for which there is no record... this
            // can happen with dblclick where the clicks are on removal actions (think a
            // grid w/"delete row" action column) or if the record was in a page that was
            // pruned by a buffered store.
            if (!record || me.processItemEvent(record, item, index, e) === false) {
                return false;
            }
            if ((me['onBeforeItem' + map[newType]](record, item, index, e) === false) || (me.fireEvent('beforeitem' + newType, me, record, item, index, e) === false) || (me['onItem' + map[newType]](record, item, index, e) === false)) {
                return false;
            }
            me.fireEvent('item' + newType, me, record, item, index, e);
        } else {
            type = touchMap[type] || type;
            if ((me.processContainerEvent(e) === false) || (me['onBeforeContainer' + map[type]](e) === false) || (me.fireEvent('beforecontainer' + type, me, e) === false) || (me['onContainer' + map[type]](e) === false)) {
                return false;
            }
            me.fireEvent('container' + type, me, e);
        }
        return true;
    },
    /**
     * @private
     */
    onItemMouseEnter: function(record, item, index, e) {
        if (this.trackOver) {
            this.highlightItem(item);
        }
    },
    /**
     * @private
     */
    onItemMouseLeave: function(record, item, index, e) {
        if (this.trackOver) {
            this.clearHighlight();
        }
    },
    /**
     * @private
     */
    onItemMouseDown: Ext.emptyFn,
    onItemLongPress: Ext.emptyFn,
    onItemMouseUp: Ext.emptyFn,
    onItemFocus: Ext.emptyFn,
    onItemClick: Ext.emptyFn,
    onItemDblClick: Ext.emptyFn,
    onItemContextMenu: Ext.emptyFn,
    onItemKeyDown: Ext.emptyFn,
    onItemKeyUp: Ext.emptyFn,
    onItemKeyPress: Ext.emptyFn,
    onBeforeItemLongPress: Ext.emptyFn,
    onBeforeItemMouseDown: Ext.emptyFn,
    onBeforeItemMouseUp: Ext.emptyFn,
    onBeforeItemFocus: Ext.emptyFn,
    onBeforeItemMouseEnter: Ext.emptyFn,
    onBeforeItemMouseLeave: Ext.emptyFn,
    onBeforeItemClick: Ext.emptyFn,
    onBeforeItemDblClick: Ext.emptyFn,
    onBeforeItemContextMenu: Ext.emptyFn,
    onBeforeItemKeyDown: Ext.emptyFn,
    onBeforeItemKeyUp: Ext.emptyFn,
    onBeforeItemKeyPress: Ext.emptyFn,
    /**
     * @private
     */
    onContainerMouseDown: Ext.emptyFn,
    onContainerLongPress: Ext.emptyFn,
    onContainerMouseUp: Ext.emptyFn,
    onContainerMouseOver: Ext.emptyFn,
    onContainerMouseOut: Ext.emptyFn,
    onContainerClick: Ext.emptyFn,
    onContainerDblClick: Ext.emptyFn,
    onContainerContextMenu: Ext.emptyFn,
    onContainerKeyDown: Ext.emptyFn,
    onContainerKeyUp: Ext.emptyFn,
    onContainerKeyPress: Ext.emptyFn,
    onBeforeContainerMouseDown: Ext.emptyFn,
    onBeforeContainerLongPress: Ext.emptyFn,
    onBeforeContainerMouseUp: Ext.emptyFn,
    onBeforeContainerMouseOver: Ext.emptyFn,
    onBeforeContainerMouseOut: Ext.emptyFn,
    onBeforeContainerClick: Ext.emptyFn,
    onBeforeContainerDblClick: Ext.emptyFn,
    onBeforeContainerContextMenu: Ext.emptyFn,
    onBeforeContainerKeyDown: Ext.emptyFn,
    onBeforeContainerKeyUp: Ext.emptyFn,
    onBeforeContainerKeyPress: Ext.emptyFn,
    /**
     * @private
     */
    setHighlightedItem: function(item) {
        var me = this,
            highlighted = me.highlightedItem,
            overItemCls = me.overItemCls;
        if (highlighted !== item) {
            if (highlighted) {
                Ext.fly(highlighted).removeCls(overItemCls);
                // Work around for an issue in IE8 where the focus/over/selected borders do not
                // get updated where applied using adjacent sibling selectors.
                if (Ext.isIE8) {
                    me.repaintBorder(highlighted);
                    me.repaintBorder(highlighted.nextSibling);
                }
                if (me.hasListeners.unhighlightitem) {
                    me.fireEvent('unhighlightitem', me, highlighted);
                }
            }
            me.highlightedItem = item;
            if (item) {
                Ext.fly(item).addCls(me.overItemCls);
                // Work around for an issue in IE8 where the focus/over/selected borders do not
                // get updated where applied using adjacent sibling selectors.
                if (Ext.isIE8) {
                    me.repaintBorder(item.nextSibling);
                }
                if (me.hasListeners.highlightitem) {
                    me.fireEvent('highlightitem', me, item);
                }
            }
        }
    },
    /**
     * Highlights a given item in the View. This is called by the mouseover handler
     * if {@link #overItemCls} and {@link #trackOver} are configured, but can also be called
     * manually by other code, for instance to handle stepping through the list
     * via keyboard navigation.
     * @param {HTMLElement} item The item to highlight
     */
    highlightItem: function(item) {
        this.setHighlightedItem(item);
    },
    /**
     * Un-highlights the currently highlighted item, if any.
     */
    clearHighlight: function() {
        this.setHighlightedItem(undefined);
    },
    handleUpdate: function(store, record) {
        var me = this,
            node, newNode, highlighted;
        if (me.viewReady) {
            node = me.getNode(record);
            newNode = me.callParent(arguments);
            highlighted = me.highlightedItem;
            if (highlighted && highlighted === node) {
                delete me.highlightedItem;
                if (newNode) {
                    me.highlightItem(newNode);
                }
            }
        }
    },
    refresh: function() {
        this.clearHighlight();
        this.callParent(arguments);
    },
    /**
     * Focuses a node in the view.
     * @param {Ext.data.Model} rec The record associated to the node that is to be focused.
     */
    focusNode: function(rec) {
        var me = this,
            node = Ext.fly(me.getNode(rec)),
            el = me.el,
            adjustmentY = 0,
            adjustmentX = 0,
            elRegion = el.getRegion(),
            nodeRegion;
        // Viewable region must not include scrollbars, so use
        // DOM client dimensions
        elRegion.bottom = elRegion.top + el.dom.clientHeight;
        elRegion.right = elRegion.left + el.dom.clientWidth;
        if (node) {
            nodeRegion = node.getRegion();
            // node is above
            if (nodeRegion.top < elRegion.top) {
                adjustmentY = nodeRegion.top - elRegion.top;
            }
            // node is below
            else if (nodeRegion.bottom > elRegion.bottom) {
                adjustmentY = nodeRegion.bottom - elRegion.bottom;
            }
            // node is left
            if (nodeRegion.left < elRegion.left) {
                adjustmentX = nodeRegion.left - elRegion.left;
            }
            // node is right
            else if (nodeRegion.right > elRegion.right) {
                adjustmentX = nodeRegion.right - elRegion.right;
            }
            if (adjustmentX || adjustmentY) {
                me.scrollBy(adjustmentX, adjustmentY, false);
            }
            // Poke on a tabIndex to make the node focusable.
            node.set({
                tabIndex: -1
            });
            node.focus();
        }
    },
    privates: {
        // Work around for an issue in IE8 where the focus/over/selected borders do not
        // get updated where applied using adjacent sibling selectors.
        repaintBorder: function(rowIdx) {
            var node = this.getNode(rowIdx);
            if (node) {
                // eslint-disable-next-line no-self-assign
                node.className = node.className;
            }
        }
    }
});

/**
 * A specialized {@link Ext.util.KeyNav} implementation for navigating a {@link Ext.view.BoundList}
 * using the keyboard. The up, down, pageup, pagedown, home, and end keys move the active highlight
 * through the list. The enter key invokes the selection model's select action using the highlighted
 * item.
 */
Ext.define('Ext.view.BoundListKeyNav', {
    extend: 'Ext.view.NavigationModel',
    alias: 'view.navigation.boundlist',
    /**
     * @cfg {Ext.view.BoundList} boundList (required)
     * The {@link Ext.view.BoundList} instance for which key navigation will be managed.
     */
    navigateOnSpace: true,
    initKeyNav: function(view) {
        var me = this,
            field = view.pickerField;
        // Add the regular KeyNav to the view.
        // Unless it's already been done (we may have to defer a call until the field is rendered.
        if (!me.keyNav) {
            me.callParent([
                view
            ]);
            // Add ESC handling to the View's KeyMap to collapse the field
            me.keyNav.map.addBinding({
                key: Ext.event.Event.ESC,
                fn: me.onKeyEsc,
                scope: me
            });
        }
        // BoundLists must be able to function standalone with no bound field
        if (!field) {
            return;
        }
        if (!field.rendered) {
            field.on('render', Ext.Function.bind(me.initKeyNav, me, [
                view
            ], 0), me, {
                single: true
            });
            return;
        }
        // BoundListKeyNav also listens for key events from the field to which it is bound.
        me.fieldKeyNav = new Ext.util.KeyNav({
            disabled: true,
            target: field.inputEl,
            forceKeyDown: true,
            up: me.onKeyUp,
            down: me.onKeyDown,
            right: me.onKeyRight,
            left: me.onKeyLeft,
            pageDown: me.onKeyPageDown,
            pageUp: me.onKeyPageUp,
            home: me.onKeyHome,
            end: me.onKeyEnd,
            tab: me.onKeyTab,
            space: me.onKeySpace,
            enter: me.onKeyEnter,
            A: {
                ctrl: true,
                // Need a separate function because we don't want the key
                // events passed on to selectAll (causes event suppression).
                handler: me.onSelectAllKeyPress
            },
            // This object has to get its key processing in first.
            // Specifically, before any Editor's key hyandling.
            priority: 1001,
            scope: me
        });
    },
    processViewEvent: function(view, record, node, index, event) {
        // Event is valid if it is from within the list
        if (event.within(view.listWrap)) {
            return event;
        }
        // If not from within the list, we're only interested in ESC.
        // Defeat the NavigationModel's ignoreInputFields for that.
        if (event.getKey() === event.ESC) {
            if (Ext.fly(event.target).isInputField()) {
                event.target = event.target.parentNode;
            }
            return event;
        }
    },
    // Falsy return stops the KeyMap processing the event
    enable: function() {
        this.fieldKeyNav.enable();
        this.callParent();
    },
    disable: function() {
        this.fieldKeyNav.disable();
        this.callParent();
    },
    onItemMouseDown: function(view, record, item, index, event) {
        this.callParent([
            view,
            record,
            item,
            index,
            event
        ]);
        if (event.pointerType === 'mouse') {
            // Stop the mousedown from blurring the input field
            // We can't do this for touch events otherwise scrolling
            // won't work.
            event.preventDefault();
        }
    },
    onKeyUp: function(e) {
        var me = this,
            boundList = me.view,
            allItems = boundList.all,
            oldItem = boundList.highlightedItem,
            oldItemIdx = oldItem ? boundList.indexOf(oldItem) : -1,
            newItemIdx = oldItemIdx > 0 ? oldItemIdx - 1 : allItems.getCount() - 1;
        // wraps around
        me.setPosition(newItemIdx);
        // Stop this from moving the cursor in the field
        e.preventDefault();
    },
    onKeyDown: function(e) {
        var me = this,
            boundList = me.view,
            allItems = boundList.all,
            oldItem = boundList.highlightedItem,
            oldItemIdx = oldItem ? boundList.indexOf(oldItem) : -1,
            newItemIdx = oldItemIdx < allItems.getCount() - 1 ? oldItemIdx + 1 : 0;
        // wraps around
        me.setPosition(newItemIdx);
        // Stop this from moving the cursor in the field
        e.preventDefault();
    },
    onKeyLeft: Ext.returnTrue,
    onKeyRight: Ext.returnTrue,
    onKeyTab: function(e) {
        var view = this.view,
            field = view.pickerField;
        if (view.isVisible()) {
            if (field.selectOnTab) {
                this.selectHighlighted(e);
            }
            if (field.collapse) {
                field.collapse();
            }
        }
        // Tab key event is allowed to propagate to field
        return true;
    },
    onKeyEnter: function(e) {
        var view = this.view,
            selModel = view.getSelectionModel(),
            field = view.pickerField,
            count = selModel.getCount();
        // Stop the keydown event so that an ENTER keyup does not get delivered to
        // any element which focus is transferred to in a select handler.
        e.stopEvent();
        this.selectHighlighted(e);
        // Handle the case where the highlighted item is already selected
        // In this case, the change event won't fire, so just collapse
        if (!field.multiSelect && count === selModel.getCount() && field.collapse) {
            field.collapse();
        }
        // Stop propagation of the ENTER keydown event so that any Editor which owns the field
        // does not completeEdit, but we also need to still fire the specialkey event for ENTER, 
        // so lets add fromBoundList to eOpts, and this will be handled by CellEditor#onSpecialKey.
        field.fireEvent('specialkey', field, e, {
            fromBoundList: true
        });
        return false;
    },
    onKeySpace: function() {
        if (this.navigateOnSpace) {
            this.callParent(arguments);
        }
        // Allow to propagate to field
        return true;
    },
    onKeyEsc: function() {
        if (this.view.pickerField) {
            this.view.pickerField.collapse();
        }
    },
    /**
     * Highlights the item at the given index.
     * @param {Number} item
     */
    focusItem: function(item) {
        var me = this,
            boundList = me.view;
        if (typeof item === 'number') {
            item = boundList.all.item(item);
        }
        if (item) {
            item = item.dom;
            boundList.highlightItem(item);
            boundList.getScrollable().ensureVisible(item, {
                x: false
            });
        }
    },
    /**
     * Triggers selection of the currently highlighted item according to the behavior of
     * the configured SelectionModel.
     */
    selectHighlighted: function(e) {
        var me = this,
            boundList = me.view,
            selModel = boundList.getSelectionModel(),
            highlightedRec,
            highlightedPosition = me.recordIndex;
        // If all options have been filtered out, then do NOT add most recently highlighted.
        if (boundList.all.getCount()) {
            highlightedRec = me.getRecord();
            if (highlightedRec) {
                // Select if not already selected.
                // If already selected, selecting with no CTRL flag will deselect the record.
                if (e.getKey() === e.ENTER || !selModel.isSelected(highlightedRec)) {
                    selModel.selectWithEvent(highlightedRec, e);
                    // If the result of that selection is that the record is removed
                    // or filtered out, jump to the next one.
                    if (!boundList.store.data.contains(highlightedRec)) {
                        me.setPosition(Math.min(highlightedPosition, boundList.store.getCount() - 1));
                    }
                }
            }
        }
    },
    destroy: function() {
        this.fieldKeyNav = Ext.destroy(this.fieldKeyNav);
        this.callParent();
    }
});

/**
 * Component layout for {@link Ext.view.BoundList}.
 * @private
 */
Ext.define('Ext.layout.component.BoundList', {
    extend: 'Ext.layout.component.Auto',
    alias: 'layout.boundlist',
    type: 'component',
    beginLayout: function(ownerContext) {
        var me = this,
            owner = me.owner,
            toolbar = owner.pagingToolbar;
        me.scrollPos = owner.listWrap.getScroll();
        me.callParent(arguments);
        if (owner.floating) {
            ownerContext.savedXY = owner.getXY();
            // move way offscreen to prevent any constraining
            // only move on the y axis to avoid triggering a horizontal scrollbar in rtl mode
            owner.setXY([
                0,
                -9999
            ]);
        }
        if (toolbar) {
            ownerContext.toolbarContext = ownerContext.context.getCmp(toolbar);
        }
        ownerContext.listContext = ownerContext.getEl('listWrap');
    },
    beginLayoutCycle: function(ownerContext) {
        var owner = this.owner;
        this.callParent(arguments);
        if (ownerContext.heightModel.auto) {
            // Set the el/listWrap to be autoHeight since they may have been previously sized
            // by another layout process. If the el was at maxHeight first, the listWrap will
            // always size to the maxHeight regardless of the content.
            owner.el.setHeight('auto');
            owner.listWrap.setHeight('auto');
        }
    },
    getLayoutItems: function() {
        var toolbar = this.owner.pagingToolbar;
        return toolbar ? [
            toolbar
        ] : [];
    },
    isValidParent: function() {
        // this only ever gets called with the toolbar, since it's rendered inside we
        // know the parent is always valid
        return true;
    },
    finishedLayout: function(ownerContext) {
        var me = this,
            xy = ownerContext.savedXY,
            owner = me.owner,
            listWrap = owner.listWrap,
            scrollPos = me.scrollPos;
        me.callParent(arguments);
        if (xy) {
            me.owner.setXY(xy);
        }
        listWrap.setScrollLeft(scrollPos.left);
        listWrap.setScrollTop(scrollPos.top);
    },
    measureContentWidth: function(ownerContext) {
        return this.owner.listWrap.getWidth();
    },
    measureContentHeight: function(ownerContext) {
        return this.owner.listWrap.getHeight();
    },
    publishInnerHeight: function(ownerContext, height) {
        var toolbar = ownerContext.toolbarContext,
            toolbarHeight = 0;
        if (toolbar) {
            toolbarHeight = toolbar.getProp('height');
        }
        if (toolbarHeight === undefined) {
            this.done = false;
        } else {
            ownerContext.listContext.setHeight(height - ownerContext.getFrameInfo().height - toolbarHeight);
        }
    },
    calculateOwnerHeightFromContentHeight: function(ownerContext) {
        var height = this.callParent(arguments),
            toolbar = ownerContext.toolbarContext;
        if (toolbar) {
            height += toolbar.getProp('height');
        }
        return height;
    }
});

/**
 * The base class that other non-interacting Toolbar Item classes should extend in order to
 * get some basic common toolbar item functionality.
 */
Ext.define('Ext.toolbar.Item', {
    extend: 'Ext.Component',
    alias: 'widget.tbitem',
    alternateClassName: 'Ext.Toolbar.Item',
    // Toolbar required here because we'll try to decorate it's alternateClassName
    // with this class' alternate name
    requires: [
        'Ext.toolbar.Toolbar'
    ],
    /**
     * @cfg {String} overflowText
     * Text to be used for the menu if the item is overflowed.
     */
    enable: Ext.emptyFn,
    disable: Ext.emptyFn,
    focus: Ext.emptyFn
});

/**
 * A simple class that renders text directly into a toolbar.
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *         title: 'Panel with TextItem',
 *         width: 300,
 *         height: 200,
 *         tbar: [
 *             { xtype: 'tbtext', html: 'Sample Text Item' }
 *         ],
 *         renderTo: Ext.getBody()
 *     });
 *
 */
Ext.define('Ext.toolbar.TextItem', {
    extend: 'Ext.toolbar.Item',
    alias: 'widget.tbtext',
    alternateClassName: 'Ext.Toolbar.TextItem',
    // Toolbar required here because we'll try to decorate it's alternateClassName
    // with this class' alternate name
    requires: [
        'Ext.toolbar.Toolbar',
        'Ext.XTemplate'
    ],
    /**
     * @cfg {String} text
     * The text to be used as innerHTML (html tags are accepted).
     *
     * @deprecated 5.1.0 Use {@link #html}
     */
    text: '',
    baseCls: Ext.baseCSSPrefix + 'toolbar-text',
    ariaRole: null,
    beforeRender: function() {
        var text = this.text;
        this.callParent();
        if (text) {
            this.html = text;
        }
    },
    /**
     * Updates this item's text, setting the text to be used as innerHTML.
     * @param {String} text The text to display (html accepted).
     *
     * @deprecated 5.1.0 Use {@link #update}
     */
    setText: function(text) {
        this.update(text);
    }
});

/**
 * A Trigger that contains 2 clickable elements inside in the form of a "up" and a "down"
 * trigger.
 */
Ext.define('Ext.form.trigger.Spinner', {
    extend: 'Ext.form.trigger.Trigger',
    alias: 'trigger.spinner',
    cls: Ext.baseCSSPrefix + 'form-trigger-spinner',
    spinnerCls: Ext.baseCSSPrefix + 'form-spinner',
    spinnerUpCls: Ext.baseCSSPrefix + 'form-spinner-up',
    spinnerDownCls: Ext.baseCSSPrefix + 'form-spinner-down',
    focusCls: Ext.baseCSSPrefix + 'form-spinner-focus',
    overCls: Ext.baseCSSPrefix + 'form-spinner-over',
    clickCls: Ext.baseCSSPrefix + 'form-spinner-click',
    // restore focus to the input element on spin end.
    focusFieldOnClick: true,
    /**
     * @cfg {Function/String} [upHandler=undefined]
     * The handler for the 'up' button
     * @controllable
     */
    /**
     * @cfg {Function/String} [downHandler=undefined]
     * The handler for the 'down' button
     * @controllable
     */
    /**
     * @cfg {Boolean} vertical
     * True to layout the spinner in a vertical format.
     *
     * **Note:** This is not intended to be configured on an instance level, but is
     * meant to be overridden by mobile-friendly themes that provide styling for
     * vertically oriented triggers.
     */
    vertical: true,
    bodyTpl: '<tpl if="vertical">' + '<div class="{spinnerCls} {spinnerCls}-{ui} {spinnerUpCls} {spinnerUpCls}-{ui}' + ' {childElCls} {upDisabledCls}"></div>' + '</tpl>' + '<div class="{spinnerCls} {spinnerCls}-{ui} {spinnerDownCls} {spinnerDownCls}-{ui}' + ' {childElCls} {downDisabledCls}"></div>' + '<tpl if="!vertical">' + '<div class="{spinnerCls} {spinnerCls}-{ui} {spinnerUpCls} {spinnerUpCls}-{ui}' + ' {childElCls} {upDisabledCls}"></div>' + '</tpl>',
    destroy: function() {
        var me = this;
        if (me.spinnerEl) {
            me.spinnerEl.destroy();
            me.spinnerEl = me.upEl = me.downEl = null;
        }
        me.callParent();
    },
    getBodyRenderData: function() {
        var me = this;
        return {
            vertical: me.vertical,
            upDisabledCls: me.upEnabled ? '' : (me.spinnerUpCls + '-disabled'),
            downDisabledCls: me.downEnabled ? '' : (me.spinnerDownCls + '-disabled'),
            spinnerCls: me.spinnerCls,
            spinnerUpCls: me.spinnerUpCls,
            spinnerDownCls: me.spinnerDownCls
        };
    },
    getStateEl: function() {
        return this.spinnerEl;
    },
    onClick: function() {
        var me = this,
            args = arguments,
            e = me.clickRepeater ? args[1] : args[0],
            field = me.field;
        if (!field.readOnly && !field.disabled) {
            if (me.upEl.contains(e.target)) {
                Ext.callback(me.upHandler, me.scope, [
                    field,
                    me,
                    e
                ], 0, field);
            } else if (me.downEl.contains(e.target)) {
                Ext.callback(me.downHandler, me.scope, [
                    field,
                    me,
                    e
                ], 0, field);
            }
        }
        field.inputEl.focus();
    },
    onFieldRender: function() {
        var me = this,
            vertical = me.vertical,
            spinnerEl, elements;
        me.callParent();
        /**
         * @property {Ext.dom.CompositeElement} spinnerEl
         * @private
         * The "up" spinner element
         */
        spinnerEl = me.spinnerEl = me.el.select('.' + me.spinnerCls, true);
        elements = spinnerEl.elements;
        me.upEl = vertical ? elements[0] : elements[1];
        me.downEl = vertical ? elements[1] : elements[0];
    },
    /**
     * @private
     */
    setUpEnabled: function(enabled) {
        this.upEl[enabled ? 'removeCls' : 'addCls'](this.spinnerUpCls + '-disabled');
    },
    /**
     * @private
     */
    setDownEnabled: function(enabled) {
        this.downEl[enabled ? 'removeCls' : 'addCls'](this.spinnerDownCls + '-disabled');
    }
});

/**
 * A field with a pair of up/down spinner buttons. This class is not normally instantiated directly,
 * instead it is subclassed and the {@link #onSpinUp} and {@link #onSpinDown} methods
 * are implemented to handle when the buttons are clicked. A good example of this is the
 * {@link Ext.form.field.Number} field which uses the spinner to increment and decrement
 * the field's value by its {@link Ext.form.field.Number#step step} config value.
 *
 * For example:
 *
 *     @example
 *     Ext.define('Ext.ux.CustomSpinner', {
 *         extend: 'Ext.form.field.Spinner',
 *         alias: 'widget.customspinner',
 *
 *         // override onSpinUp (using step isn't neccessary)
 *         onSpinUp: function() {
 *             var me = this;
 *             if (!me.readOnly) {
 *                 // gets rid of " Pack", defaults to zero on parse failure
 *                 var val = parseInt(me.getValue().split(' '), 10) || 0;
 *                 me.setValue((val + me.step) + ' Pack');
 *             }
 *         },
 *
 *         // override onSpinDown
 *         onSpinDown: function() {
 *             var me = this;
 *             if (!me.readOnly) {
 *                // gets rid of " Pack", defaults to zero on parse failure
 *                var val = parseInt(me.getValue().split(' '), 10) || 0;
 *                if (val <= me.step) {
 *                    me.setValue('Dry!');
 *                } else {
 *                    me.setValue((val - me.step) + ' Pack');
 *                }
 *             }
 *         }
 *     });
 *
 *     Ext.create('Ext.form.FormPanel', {
 *         title: 'Form with SpinnerField',
 *         bodyPadding: 5,
 *         width: 350,
 *         renderTo: Ext.getBody(),
 *         items:[{
 *             xtype: 'customspinner',
 *             fieldLabel: 'How Much Beer?',
 *             step: 6
 *         }]
 *     });
 *
 * By default, pressing the up and down arrow keys will also trigger the onSpinUp and onSpinDown
 * methods; to prevent this, set `{@link #keyNavEnabled} = false`.
 */
Ext.define('Ext.form.field.Spinner', {
    extend: 'Ext.form.field.Text',
    alias: 'widget.spinnerfield',
    alternateClassName: 'Ext.form.Spinner',
    requires: [
        'Ext.form.trigger.Spinner',
        'Ext.util.KeyNav'
    ],
    config: {
        triggers: {
            spinner: {
                type: 'spinner',
                upHandler: 'onSpinnerUpClick',
                downHandler: 'onSpinnerDownClick',
                endHandler: 'onSpinEnd',
                scope: 'this'
            }
        }
    },
    /**
     * @cfg {Boolean} spinUpEnabled
     * Specifies whether the up spinner button is enabled. Defaults to true. To change this after
     * the component is created, use the {@link #setSpinUpEnabled} method.
     */
    spinUpEnabled: true,
    /**
     * @cfg {Boolean} spinDownEnabled
     * Specifies whether the down spinner button is enabled. Defaults to true. To change this after
     * the component is created, use the {@link #setSpinDownEnabled} method.
     */
    spinDownEnabled: true,
    /**
     * @cfg {Boolean} keyNavEnabled
     * Specifies whether the up and down arrow keys should trigger spinning up and down.
     * Defaults to true.
     */
    keyNavEnabled: true,
    /**
     * @cfg {Boolean} mouseWheelEnabled
     * Specifies whether the mouse wheel should trigger spinning up and down while the field
     * has focus.
     * Defaults to true.
     */
    mouseWheelEnabled: true,
    /**
     * @cfg {Boolean} repeatTriggerClick
     * Whether a {@link Ext.util.ClickRepeater click repeater} should be attached to the spinner
     * buttons.
     * Defaults to true.
     */
    repeatTriggerClick: true,
    /**
     * @method
     * @protected
     * This method is called when the spinner up button is clicked, or when the up arrow key
     * is pressed if {@link #keyNavEnabled} is true. Must be implemented by subclasses.
     */
    onSpinUp: Ext.emptyFn,
    /**
     * @method
     * @protected
     * This method is called when the spinner down button is clicked, or when the down arrow key
     * is pressed if {@link #keyNavEnabled} is true. Must be implemented by subclasses.
     */
    onSpinDown: Ext.emptyFn,
    ariaRole: 'spinbutton',
    /**
     * @event spin
     * Fires when the spinner is made to spin up or down.
     * @param {Ext.form.field.Spinner} this
     * @param {String} direction Either 'up' if spinning up, or 'down' if spinning down.
     */
    /**
     * @event spinup
     * Fires when the spinner is made to spin up.
     * @param {Ext.form.field.Spinner} this
     */
    /**
     * @event spindown
     * Fires when the spinner is made to spin down.
     * @param {Ext.form.field.Spinner} this
     */
    /**
     * @event spinend
     * Fires when a spin command has been finished. For example on mouseup
     * on the spin buttons, when an `UP` or `DOWN` arrow key is released
     * of when a mousewheel stops spinning.
     *
     * When this event fires, the field's value has stabilized.
     * @param {Ext.form.field.Spinner} this
     * @since 6.2.0
     */
    applyTriggers: function(triggers) {
        var me = this,
            spinnerTrigger = triggers.spinner;
        spinnerTrigger.upEnabled = me.spinUpEnabled;
        spinnerTrigger.downEnabled = me.spinDownEnabled;
        return me.callParent([
            triggers
        ]);
    },
    /**
     * @private
     */
    onRender: function() {
        var me = this,
            spinnerTrigger = me.getTrigger('spinner');
        me.callParent();
        // Init up/down arrow keys
        if (me.keyNavEnabled) {
            me.spinnerKeyNav = new Ext.util.KeyNav({
                target: me.inputEl,
                scope: me,
                up: me.spinUp,
                down: me.spinDown
            });
            me.inputEl.on({
                keyup: me.onInputElKeyUp,
                scope: me
            });
        }
        // Init mouse wheel
        if (me.mouseWheelEnabled) {
            me.mon(me.bodyEl, 'mousewheel', me.onMouseWheel, me);
        }
        // in v4 spinUpEl/spinDownEl were childEls, now they are children of the trigger.
        // create references for compatibility
        me.spinUpEl = spinnerTrigger.upEl;
        me.spinDownEl = spinnerTrigger.downEl;
    },
    /**
     * @private
     * Handles the spinner up button clicks.
     */
    onSpinnerUpClick: function() {
        this.spinUp();
    },
    /**
     * @private
     * Handles the spinner down button clicks.
     */
    onSpinnerDownClick: function() {
        this.spinDown();
    },
    /**
     * Triggers the spinner to step up; fires the {@link #spin} and {@link #spinup} events
     * and calls the {@link #onSpinUp} method. Does nothing if the field is {@link #disabled}
     * or if {@link #spinUpEnabled} is false.
     */
    spinUp: function() {
        var me = this;
        if (me.spinUpEnabled && !me.disabled) {
            me.fireEvent('spin', me, 'up');
            me.fireEvent('spinup', me);
            me.onSpinUp();
        }
    },
    /**
     * Triggers the spinner to step down; fires the {@link #spin} and {@link #spindown} events
     * and calls the {@link #onSpinDown} method. Does nothing if the field is {@link #disabled}
     * or if {@link #spinDownEnabled} is false.
     */
    spinDown: function() {
        var me = this;
        if (me.spinDownEnabled && !me.disabled) {
            me.fireEvent('spin', me, 'down');
            me.fireEvent('spindown', me);
            me.onSpinDown();
        }
    },
    /**
     * Sets whether the spinner up button is enabled.
     * @param {Boolean} enabled true to enable the button, false to disable it.
     */
    setSpinUpEnabled: function(enabled) {
        var me = this,
            wasEnabled = me.spinUpEnabled;
        me.spinUpEnabled = enabled;
        if (wasEnabled !== enabled && me.rendered) {
            me.getTrigger('spinner').setUpEnabled(enabled);
        }
    },
    /**
     * Sets whether the spinner down button is enabled.
     * @param {Boolean} enabled true to enable the button, false to disable it.
     */
    setSpinDownEnabled: function(enabled) {
        var me = this,
            wasEnabled = me.spinDownEnabled;
        me.spinDownEnabled = enabled;
        if (wasEnabled !== enabled && me.rendered) {
            me.getTrigger('spinner').setDownEnabled(enabled);
        }
    },
    /**
     * @private
     * Handles mousewheel events on the field
     */
    onMouseWheel: function(e) {
        var me = this,
            delta;
        if (me.hasFocus) {
            delta = e.getWheelDelta();
            if (delta > 0) {
                me.spinUp();
            } else if (delta < 0) {
                me.spinDown();
            }
            e.stopEvent();
            me.onSpinEnd();
        }
    },
    onInputElKeyUp: function(e) {
        if (e.keyCode === e.UP || e.keyCode === e.DOWN) {
            this.onSpinEnd();
        }
    },
    doDestroy: function() {
        Ext.destroyMembers(this, 'spinnerKeyNav');
        this.callParent();
    }
}, function(Spinner) {
    var spinEnd = function() {
            if (!this.destroying && !this.destroyed) {
                this.fireEvent('spinend', this);
            }
        };
    spinEnd.$skipTimerCheck = true;
    Spinner.prototype.onSpinEnd = Ext.Function.createBuffered(spinEnd, 100);
});

/**
 * A numeric text field that provides automatic keystroke filtering to disallow non-numeric
 * characters, and numeric validation to limit the value to a range of valid numbers. The range
 * of acceptable number values can be controlled by setting the {@link #minValue} and
 * {@link #maxValue} configs, and fractional decimals can be disallowed by setting
 * {@link #allowDecimals} to `false`.
 *
 * By default, the number field is also rendered with a set of up/down spinner buttons and has
 * up/down arrow key and mouse wheel event listeners attached for incrementing/decrementing
 * the value by the {@link #step} value. To hide the spinner buttons set
 * `{@link #hideTrigger hideTrigger}:true`; to disable the arrow key and mouse wheel handlers set
 * `{@link #keyNavEnabled keyNavEnabled}:false` and
 * `{@link #mouseWheelEnabled mouseWheelEnabled}:false`. See the example below.
 *
 * # Example usage
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'On The Wall',
 *         width: 300,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items: [{
 *             xtype: 'numberfield',
 *             anchor: '100%',
 *             name: 'bottles',
 *             fieldLabel: 'Bottles of Beer',
 *             value: 99,
 *             maxValue: 99,
 *             minValue: 0
 *         }],
 *         buttons: [{
 *             text: 'Take one down, pass it around',
 *             handler: function() {
 *                 this.up('form').down('[name=bottles]').spinDown();
 *             }
 *         }]
 *     });
 *
 * # Removing UI Enhancements
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Personal Info',
 *         width: 300,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items: [{
 *             xtype: 'numberfield',
 *             anchor: '100%',
 *             name: 'age',
 *             fieldLabel: 'Age',
 *             minValue: 0, //prevents negative numbers
 *
 *             // Remove spinner buttons, and arrow key and mouse wheel listeners
 *             hideTrigger: true,
 *             keyNavEnabled: false,
 *             mouseWheelEnabled: false
 *         }]
 *     });
 *
 * # Using Step
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         renderTo: Ext.getBody(),
 *         title: 'Step',
 *         width: 300,
 *         bodyPadding: 10,
 *         items: [{
 *             xtype: 'numberfield',
 *             anchor: '100%',
 *             name: 'evens',
 *             fieldLabel: 'Even Numbers',
 *
 *             // Set step so it skips every other number
 *             step: 2,
 *             value: 0,
 *
 *             // Add change handler to force user-entered numbers to evens
 *             listeners: {
 *                 change: function(field, value) {
 *                     value = parseInt(value, 10);
 *                     field.setValue(value + value % 2);
 *                 }
 *             }
 *         }]
 *     });
 */
Ext.define('Ext.form.field.Number', {
    extend: 'Ext.form.field.Spinner',
    alias: 'widget.numberfield',
    alternateClassName: [
        'Ext.form.NumberField',
        'Ext.form.Number'
    ],
    /**
     * @cfg {RegExp} stripCharsRe
     * @private
     */
    /**
     * @cfg {RegExp} maskRe
     * @private
     */
    /**
     * @cfg {Boolean} [allowExponential=true]
     * Set to `false` to disallow Exponential number notation
     */
    allowExponential: true,
    /**
     * @cfg {Boolean} [allowDecimals=true]
     * False to disallow decimal values
     */
    allowDecimals: true,
    /**
     * @cfg {String} decimalSeparator
     * Character(s) to allow as the decimal separator.
     * Defaults to {@link Ext.util.Format#decimalSeparator decimalSeparator}.
     * @locale
     */
    decimalSeparator: null,
    /**
     * @cfg {Boolean} [submitLocaleSeparator=true]
     * False to ensure that the {@link #getSubmitValue} method strips
     * always uses `.` as the separator, regardless of the {@link #decimalSeparator}
     * configuration.
     * @locale
     */
    submitLocaleSeparator: true,
    /**
     * @cfg {Number} decimalPrecision
     * The maximum precision to display after the decimal separator
     * @locale
     */
    decimalPrecision: 2,
    /**
     * @cfg {Number} minValue
     * The minimum allowed value. Will be used by the field's validation logic,
     * and for {@link Ext.form.field.Spinner#setSpinUpEnabled enabling/disabling
     * the down spinner button}.
     *
     * Defaults to Number.NEGATIVE_INFINITY.
     */
    minValue: Number.NEGATIVE_INFINITY,
    /**
     * @cfg {Number} maxValue
     * The maximum allowed value. Will be used by the field's validation logic, and for
     * {@link Ext.form.field.Spinner#setSpinUpEnabled enabling/disabling the up spinner button}.
     *
     * Defaults to Number.MAX_VALUE.
     */
    maxValue: Number.MAX_VALUE,
    /**
     * @cfg {Number} step
     * Specifies a numeric interval by which the field's value will be incremented or decremented
     * when the user invokes the spinner.
     */
    step: 1,
    /**
     * @cfg {String} minText
     * Error text to display if the minimum value validation fails.
     * @locale
     */
    minText: 'The minimum value for this field is {0}',
    /**
     * @cfg {String} maxText
     * Error text to display if the maximum value validation fails.
     * @locale
     */
    maxText: 'The maximum value for this field is {0}',
    /**
     * @cfg {String} nanText
     * Error text to display if the value is not a valid number. For example, this can happen
     * if a valid character like '.' or '-' is left in the field with no number.
     * @locale
     */
    nanText: '{0} is not a valid number',
    /**
     * @cfg {String} negativeText
     * Error text to display if the value is negative and {@link #minValue} is set to 0.
     * This is used instead of the {@link #minText} in that circumstance only.
     * @locale
     */
    negativeText: 'The value cannot be negative',
    /**
     * @cfg {String} baseChars
     * The base set of characters to evaluate as valid numbers.
     */
    baseChars: '0123456789',
    /**
     * @cfg {Boolean} autoStripChars
     * True to automatically strip not allowed characters from the field.
     */
    autoStripChars: false,
    initComponent: function() {
        var me = this;
        if (me.decimalSeparator === null) {
            me.decimalSeparator = Ext.util.Format.decimalSeparator;
        }
        me.callParent();
        me.setMinValue(me.minValue);
        me.setMaxValue(me.maxValue);
    },
    getSubTplData: function(fieldData) {
        var me = this,
            min = me.minValue,
            max = me.maxValue,
            data, inputElAttr, value;
        data = me.callParent([
            fieldData
        ]);
        inputElAttr = data.inputElAriaAttributes;
        if (inputElAttr) {
            // The checks are to skip the default min and max values,
            // in which case we don't want to set corresponding ARIA
            // attributes at all
            if (min > Number.NEGATIVE_INFINITY) {
                inputElAttr['aria-valuemin'] = min;
            }
            if (max < Number.MAX_VALUE) {
                inputElAttr['aria-valuemax'] = max;
            }
            value = me.getValue();
            if (value != null && value >= min && value <= max) {
                inputElAttr['aria-valuenow'] = value;
            }
        }
        return data;
    },
    setValue: function(value) {
        var me = this,
            bind, valueBind;
        // This portion of the code is to prevent a binding from stomping over
        // the typed value. Say we have decimalPrecision 4 and the user types
        // 1.23456. The value of the field will be set as 1.2346 and published to
        // the viewmodel, which will trigger the binding to fire and setValue to
        // be called on the field, which would then set the value (and rawValue) to
        // 1.2346. Instead, if we have focus and the value is the same, just leave
        // the rawValue alone
        if (me.hasFocus) {
            bind = me.getBind();
            valueBind = bind && bind.value;
            if (valueBind && valueBind.syncing && value === me.value) {
                return me;
            }
        }
        return me.callParent([
            value
        ]);
    },
    /**
     * Runs all of Number's validations and returns an array of any errors. Note that this first
     * runs Text's validations, so the returned array is an amalgamation of all field errors.
     * The additional validations run test that the value is a number, and that it is within
     * the configured min and max values.
     * @param {Object} [value] The value to get errors for (defaults to the current field value)
     * @return {String[]} All validation errors for this field
     */
    getErrors: function(value) {
        value = arguments.length > 0 ? value : this.processRawValue(this.getRawValue());
        // eslint-disable-next-line vars-on-top
        var me = this,
            errors = me.callParent([
                value
            ]),
            format = Ext.String.format,
            num;
        if (value.length < 1) {
            // if it's blank and textfield didn't flag it then it's valid
            return errors;
        }
        value = String(value).replace(me.decimalSeparator, '.');
        if (isNaN(value)) {
            errors.push(format(me.nanText, value));
        }
        num = me.parseValue(value);
        if (me.minValue === 0 && num < 0) {
            errors.push(this.negativeText);
        } else if (num < me.minValue) {
            errors.push(format(me.minText, me.minValue));
        }
        if (num > me.maxValue) {
            errors.push(format(me.maxText, me.maxValue));
        }
        return errors;
    },
    rawToValue: function(rawValue) {
        var value = this.fixPrecision(this.parseValue(rawValue));
        if (value === null) {
            value = rawValue || null;
        }
        return value;
    },
    valueToRaw: function(value) {
        var me = this,
            decimalSeparator = me.decimalSeparator;
        value = me.parseValue(value);
        value = me.fixPrecision(value);
        value = Ext.isNumber(value) ? value : parseFloat(String(value).replace(decimalSeparator, '.'));
        value = isNaN(value) ? '' : String(value).replace('.', decimalSeparator);
        return value;
    },
    getSubmitValue: function() {
        var me = this,
            value = me.callParent();
        if (!me.submitLocaleSeparator) {
            value = value.replace(me.decimalSeparator, '.');
        }
        return value;
    },
    onChange: function(newValue) {
        var ariaDom = this.ariaEl.dom;
        this.toggleSpinners();
        this.callParent(arguments);
        if (ariaDom) {
            if (Ext.isNumber(newValue) && isFinite(newValue)) {
                ariaDom.setAttribute('aria-valuenow', newValue);
            } else {
                ariaDom.removeAttribute('aria-valuenow');
            }
        }
    },
    toggleSpinners: function() {
        var me = this,
            value = me.getValue(),
            valueIsNull = value === null,
            enabled;
        // If it's disabled, only allow it to be re-enabled if we are
        // the ones who are disabling it.
        if (me.spinUpEnabled || me.spinUpDisabledByToggle) {
            enabled = valueIsNull || value < me.maxValue;
            me.setSpinUpEnabled(enabled, true);
        }
        if (me.spinDownEnabled || me.spinDownDisabledByToggle) {
            enabled = valueIsNull || value > me.minValue;
            me.setSpinDownEnabled(enabled, true);
        }
    },
    /**
     * Replaces any existing {@link #minValue} with the new value.
     * @param {Number} value The minimum value
     */
    setMinValue: function(value) {
        var me = this,
            ariaDom = me.ariaEl.dom,
            minValue, allowed;
        me.minValue = minValue = Ext.Number.from(value, Number.NEGATIVE_INFINITY);
        me.toggleSpinners();
        // May not be rendered yet
        if (ariaDom) {
            if (minValue > Number.NEGATIVE_INFINITY) {
                ariaDom.setAttribute('aria-valuemin', minValue);
            } else {
                ariaDom.removeAttribute('aria-valuemin');
            }
        }
        // Build regexes for masking and stripping based on the configured options
        if (me.disableKeyFilter !== true) {
            allowed = me.baseChars + '';
            if (me.allowExponential) {
                allowed += me.decimalSeparator + 'e+-';
            } else {
                if (me.allowDecimals) {
                    allowed += me.decimalSeparator;
                }
                if (me.minValue < 0) {
                    allowed += '-';
                }
            }
            allowed = Ext.String.escapeRegex(allowed);
            me.maskRe = new RegExp('[' + allowed + ']');
            if (me.autoStripChars) {
                me.stripCharsRe = new RegExp('[^' + allowed + ']', 'gi');
            }
        }
    },
    /**
     * Replaces any existing {@link #maxValue} with the new value.
     * @param {Number} value The maximum value
     */
    setMaxValue: function(value) {
        var ariaDom = this.ariaEl.dom,
            maxValue;
        this.maxValue = maxValue = Ext.Number.from(value, Number.MAX_VALUE);
        // May not be rendered yet
        if (ariaDom) {
            if (maxValue < Number.MAX_VALUE) {
                ariaDom.setAttribute('aria-valuemax', maxValue);
            } else {
                ariaDom.removeAttribute('aria-valuemax');
            }
        }
        this.toggleSpinners();
    },
    /**
     * @private
     */
    parseValue: function(value) {
        value = parseFloat(String(value).replace(this.decimalSeparator, '.'));
        return isNaN(value) ? null : value;
    },
    /**
     * @private
     */
    fixPrecision: function(value) {
        var me = this,
            nan = isNaN(value),
            precision = me.decimalPrecision;
        if (nan || !value) {
            return nan ? '' : value;
        } else if (!me.allowDecimals || precision <= 0) {
            precision = 0;
        }
        return parseFloat(Ext.Number.toFixed(parseFloat(value), precision));
    },
    onBlur: function(e) {
        var me = this,
            v = me.rawToValue(me.getRawValue());
        if (!Ext.isEmpty(v)) {
            me.setValue(v);
        }
        me.callParent([
            e
        ]);
    },
    setSpinUpEnabled: function(enabled, internal) {
        this.callParent(arguments);
        if (!internal) {
            delete this.spinUpDisabledByToggle;
        } else {
            this.spinUpDisabledByToggle = !enabled;
        }
    },
    onSpinUp: function() {
        var me = this;
        if (!me.readOnly) {
            me.setSpinValue(Ext.Number.constrain(me.getValue() + me.step, me.minValue, me.maxValue));
        }
    },
    setSpinDownEnabled: function(enabled, internal) {
        this.callParent(arguments);
        if (!internal) {
            delete this.spinDownDisabledByToggle;
        } else {
            this.spinDownDisabledByToggle = !enabled;
        }
    },
    onSpinDown: function() {
        var me = this;
        if (!me.readOnly) {
            me.setSpinValue(Ext.Number.constrain(me.getValue() - me.step, me.minValue, me.maxValue));
        }
    },
    setSpinValue: function(value) {
        var me = this;
        if (me.enforceMaxLength) {
            // We need to round the value here, otherwise we could end up with a
            // very long number (think 0.1 + 0.2)
            if (me.fixPrecision(value).toString().length > me.maxLength) {
                return;
            }
        }
        me.setValue(value);
    }
});

/**
 * As the number of records increases, the time required for the browser to render them increases.
 * Paging is used to reduce the amount of data exchanged with the client.
 * Note: if there are more records/rows than can be viewed in the available screen area,
 * vertical scrollbars will be added.
 *
 * Paging is typically handled on the server side (see exception below). The client sends parameters
 * to the server side, which the server needs to interpret and then respond with the appropriate
 * data.
 *
 * Ext.toolbar.Paging is a specialized toolbar that is bound to a {@link Ext.data.Store}
 * and provides automatic paging control. This Component {@link Ext.data.Store#method-load load}s
 * blocks of data into the {@link #store} by passing parameters used for paging criteria.
 *
 * Note: The {@link #store} specified must support paging as defined by `Ext.data.Store`.
 * In particular, this means that `Ext.data.ChainedStore` is not supported.
 *
 * {@img Ext.toolbar.Paging/Ext.toolbar.Paging.png Ext.toolbar.Paging component}
 *
 * Paging Toolbar is typically used as one of the Grid's toolbars:
 *
 *     var itemsPerPage = 2; // set the number of items you want per page
 *
 *     Ext.create('Ext.data.Store', {
 *         id: 'simpsonsStore',
 *         autoLoad: false,
 *         fields: ['name', 'email', 'phone'],
 *         pageSize: itemsPerPage, // items per page
 *         proxy: {
 *             type: 'ajax',
 *             // url that will load data with respect to start and limit params
 *             url: 'pagingstore.js',
 *             reader: {
 *                 type: 'json',
 *                 rootProperty: 'items',
 *                 totalProperty: 'total'
 *             }
 *         }
 *     });
 *
 *     // specify segment of data you want to load using params
 *     store.load({
 *         params: {
 *             start: 0,
 *             limit: itemsPerPage
 *         }
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Simpsons',
 *         width: 400,
 *         height: 125,
 *         renderTo: Ext.getBody(),
 *
 *         store: 'simpsonsStore',
 *
 *         columns: [{
 *             text: 'Name',
 *             dataIndex: 'name'
 *         }, {
 *             text: 'Email',
 *             dataIndex: 'email',
 *             flex: 1
 *         }, {
 *             text: 'Phone',
 *             dataIndex: 'phone'
 *         }],
 *
 *         bbar: {
 *             xtype: 'pagingtoolbar',
 *             displayInfo: true
 *         }
 *     });
 *
 * To use paging, you need to set a pageSize configuration on the Store, and pass the paging
 * requirements to the server when the store is first loaded.
 *
 *     store.load({
 *         params: {
 *             // specify params for the first page load if using paging
 *             start: 0,
 *             limit: myPageSize,
 *             // other params
 *             foo:   'bar'
 *         }
 *     });
 *
 * If using {@link Ext.data.Store#autoLoad store's autoLoad} configuration:
 *
 *     var myStore = Ext.create('Ext.data.Store', {
 *         autoLoad: {start: 0, limit: 25},
 *         ...
 *     });
 *
 * The packet sent back from the server would have this form:
 *
 *     {
 *         "success": true,
 *         "results": 2000,
 *         "items": [ // ***Note:** this must be an Array
 *             { "id":  1, "name": "Bill", "occupation": "Gardener" },
 *             { "id":  2, "name":  "Ben", "occupation": "Horticulturalist" },
 *             ...
 *             { "id": 25, "name":  "Sue", "occupation": "Botanist" }
 *         ]
 *     }
 *
 * ## Paging with Local Data
 *
 * Paging can also be accomplished with local data using extensions:
 *
 *   - [Ext.ux.data.PagingStore][1]
 *   - Paging Memory Proxy (examples/ux/PagingMemoryProxy.js)
 *
 *    [1]: http://sencha.com/forum/showthread.php?t=71532
 */
Ext.define('Ext.toolbar.Paging', {
    extend: 'Ext.toolbar.Toolbar',
    xtype: 'pagingtoolbar',
    alternateClassName: 'Ext.PagingToolbar',
    requires: [
        'Ext.toolbar.TextItem',
        'Ext.form.field.Number'
    ],
    mixins: [
        'Ext.util.StoreHolder'
    ],
    /**
     * @cfg {Ext.data.Store/String} store
     * The data source to which the paging toolbar is bound (must be the same store instance
     * used in the grid / tree). Acceptable values for this property are:
     *
     *   - Any {@link Ext.data.Store Store} class or subclass
     *   - An {@link Ext.data.Store#storeId ID of a store}
     *
     * If no `store` is provided, the `store` of the owner component (if there is an
     * owner and it has a store) is used. The owner store is bound when this component
     * is rendered.
     */
    /**
     * @cfg {Boolean} displayInfo
     * true to display the displayMsg
     */
    displayInfo: false,
    /**
     * @cfg {Boolean} prependButtons
     * true to insert any configured items _before_ the paging buttons.
     */
    prependButtons: false,
    /**
     * @cfg {String} displayMsg
     * The paging status message to display. Note that this string is
     * formatted using the braced numbers {0}-{2} as tokens that are replaced by the values
     * for start, end and total respectively. These tokens should be preserved when overriding
     * this string if showing those values is desired.
     * @locale
     */
    displayMsg: 'Displaying {0} - {1} of {2}',
    /**
     * @cfg {String} emptyMsg
     * The message to display when no records are found.
     * @locale
     */
    emptyMsg: 'No data to display',
    /**
     * @cfg {String} beforePageText
     * The text displayed before the input item.
     * @locale
     */
    beforePageText: 'Page',
    /**
     * @cfg {String} afterPageText
     * Customizable piece of the default paging text. Note that this string is formatted using
     * {0} as a token that is replaced by the number of total pages. This token should be preserved
     * when overriding this string if showing the total page count is desired.
     * @locale
     */
    afterPageText: 'of {0}',
    /**
     * @cfg {String} firstText
     * The quicktip text displayed for the first page button.
     * **Note**: quick tips must be initialized for the quicktip to show.
     * @locale
     */
    firstText: 'First Page',
    /**
     * @cfg {String} prevText
     * The quicktip text displayed for the previous page button.
     * **Note**: quick tips must be initialized for the quicktip to show.
     * @locale
     */
    prevText: 'Previous Page',
    /**
     * @cfg {String} nextText
     * The quicktip text displayed for the next page button.
     * **Note**: quick tips must be initialized for the quicktip to show.
     * @locale
     */
    nextText: 'Next Page',
    /**
     * @cfg {String} lastText
     * The quicktip text displayed for the last page button.
     * **Note**: quick tips must be initialized for the quicktip to show.
     * @locale
     */
    lastText: 'Last Page',
    /**
     * @cfg {String} refreshText
     * The quicktip text displayed for the Refresh button.
     * **Note**: quick tips must be initialized for the quicktip to show.
     * @locale
     */
    refreshText: 'Refresh',
    /**
     * @cfg {Number} inputItemWidth
     * The width in pixels of the input field used to display and change the current page number.
     */
    inputItemWidth: 30,
    /**
     * @event change
     * Fires after the active page has been changed.
     * @param {Ext.toolbar.Paging} this
     * @param {Object} pageData An object that has these properties:
     *
     * - `total`: Number
     *
     *   The total number of records in the dataset as returned by the server
     *
     * - `currentPage`: Number
     *
     *   The current page number
     *
     * - `pageCount`: Number
     *
     *   The total number of pages (calculated from the total number of records in the dataset
     *   as returned by the server and the current {@link Ext.data.Store#pageSize pageSize})
     *
     * - `toRecord`: Number
     *
     *   The starting record index for the current page
     *
     * - `fromRecord`: Number
     *
     *   The ending record index for the current page
     */
    /**
     * @event beforechange
     * Fires just before the active page is changed. Return false to prevent the active page
     * from being changed.
     * @param {Ext.toolbar.Paging} this
     * @param {Number} page The page number that will be loaded on change
     */
    emptyPageData: {
        total: 0,
        currentPage: 0,
        pageCount: 0,
        toRecord: 0,
        fromRecord: 0
    },
    /**
     * @property defaultBindProperty
     * @inheritdoc
     */
    defaultBindProperty: 'store',
    _pagingToolbarCls: Ext.baseCSSPrefix + 'grid-paging-toolbar',
    /**
     * Gets the standard paging items in the toolbar
     * @private
     */
    getPagingItems: function() {
        var me = this,
            inputListeners = {
                scope: me,
                blur: me.onPagingBlur
            };
        inputListeners[Ext.supports.SpecialKeyDownRepeat ? 'keydown' : 'keypress'] = me.onPagingKeyDown;
        return [
            {
                itemId: 'first',
                tooltip: me.firstText,
                overflowText: me.firstText,
                iconCls: Ext.baseCSSPrefix + 'tbar-page-first',
                disabled: true,
                handler: me.moveFirst,
                scope: me
            },
            {
                itemId: 'prev',
                tooltip: me.prevText,
                overflowText: me.prevText,
                iconCls: Ext.baseCSSPrefix + 'tbar-page-prev',
                disabled: true,
                handler: me.movePrevious,
                scope: me
            },
            '-',
            me.beforePageText,
            {
                xtype: 'numberfield',
                itemId: 'inputItem',
                name: 'inputItem',
                cls: Ext.baseCSSPrefix + 'tbar-page-number',
                allowDecimals: false,
                minValue: 1,
                hideTrigger: true,
                enableKeyEvents: true,
                keyNavEnabled: false,
                selectOnFocus: true,
                submitValue: false,
                // mark it as not a field so the form will not catch it when getting fields
                isFormField: false,
                width: me.inputItemWidth,
                margin: '-1 2 3 2',
                listeners: inputListeners
            },
            {
                xtype: 'tbtext',
                itemId: 'afterTextItem',
                html: Ext.String.format(me.afterPageText, 1)
            },
            '-',
            {
                itemId: 'next',
                tooltip: me.nextText,
                overflowText: me.nextText,
                iconCls: Ext.baseCSSPrefix + 'tbar-page-next',
                disabled: true,
                handler: me.moveNext,
                scope: me
            },
            {
                itemId: 'last',
                tooltip: me.lastText,
                overflowText: me.lastText,
                iconCls: Ext.baseCSSPrefix + 'tbar-page-last',
                disabled: true,
                handler: me.moveLast,
                scope: me
            },
            '-',
            {
                itemId: 'refresh',
                tooltip: me.refreshText,
                overflowText: me.refreshText,
                iconCls: Ext.baseCSSPrefix + 'tbar-loading',
                disabled: me.store.isLoading(),
                handler: me.doRefresh,
                scope: me
            }
        ];
    },
    initComponent: function() {
        var me = this,
            userItems = me.items || me.buttons || [],
            pagingItems;
        me.bindStore(me.store || 'ext-empty-store', true);
        if (me.store && !me.store.nextPage) {
            Ext.raise('Store is not compatible with this component (does not support paging)');
        }
        pagingItems = me.getPagingItems();
        if (me.prependButtons) {
            me.items = userItems.concat(pagingItems);
        } else {
            me.items = pagingItems.concat(userItems);
        }
        delete me.buttons;
        if (me.displayInfo) {
            me.items.push('->');
            me.items.push({
                xtype: 'tbtext',
                itemId: 'displayItem'
            });
        }
        me.callParent();
    },
    beforeRender: function() {
        this.callParent(arguments);
        this.updateBarInfo();
    },
    afterRender: function() {
        this.callParent();
        this.el.addCls(this._pagingToolbarCls);
    },
    onAdded: function(owner) {
        var me = this,
            oldStore = me.store,
            autoStore = me._autoStore,
            listener, store;
        // When we are added to our first container, if we have no meaningful store,
        // switch into "autoStore" mode:
        if (autoStore === undefined) {
            me._autoStore = autoStore = !(oldStore && !oldStore.isEmptyStore);
        }
        if (autoStore) {
            listener = me._storeChangeListener;
            if (listener) {
                listener.destroy();
                listener = null;
            }
            store = owner && owner.store;
            if (store) {
                listener = owner.on({
                    destroyable: true,
                    scope: me,
                    storechange: 'onOwnerStoreChange'
                });
            }
            me._storeChangeListener = listener;
            me.onOwnerStoreChange(owner, store);
        }
        me.callParent(arguments);
    },
    onOwnerStoreChange: function(owner, store) {
        this.setStore(store || Ext.getStore('ext-empty-store'));
    },
    updateBarInfo: function() {
        var me = this;
        if (!me.store.isLoading()) {
            me.calledInternal = true;
            me.onLoad();
            me.calledInternal = false;
        }
    },
    /**
     * @private
     */
    updateInfo: function() {
        var me = this,
            displayItem = me.child('#displayItem'),
            store = me.store,
            pageData = me.getPageData(),
            count, msg;
        if (displayItem) {
            count = store.getCount();
            if (count === 0) {
                msg = me.emptyMsg;
            } else {
                msg = Ext.String.format(me.displayMsg, pageData.fromRecord, pageData.toRecord, pageData.total);
            }
            displayItem.setText(msg);
        }
    },
    /**
     * @private
     */
    onLoad: function() {
        var me = this,
            pageData, currPage, pageCount, afterText, count, isEmpty, item;
        count = me.store.getCount();
        isEmpty = count === 0;
        if (!isEmpty) {
            pageData = me.getPageData();
            currPage = pageData.currentPage;
            pageCount = pageData.pageCount;
            // Check for invalid current page.
            if (currPage > pageCount) {
                // If the surrent page is beyond the loaded end,
                // jump back to the loaded end if there is a valid page count.
                if (pageCount > 0) {
                    me.store.loadPage(pageCount);
                } else // If no pages, reset the page field.
                {
                    me.getInputItem().reset();
                }
                return;
            }
            afterText = Ext.String.format(me.afterPageText, isNaN(pageCount) ? 1 : pageCount);
        } else {
            currPage = 0;
            pageCount = 0;
            afterText = Ext.String.format(me.afterPageText, 0);
        }
        Ext.suspendLayouts();
        item = me.child('#afterTextItem');
        if (item) {
            item.update(afterText);
        }
        item = me.getInputItem();
        if (item) {
            item.setDisabled(isEmpty).setValue(currPage);
        }
        me.setChildDisabled('#first', currPage === 1 || isEmpty);
        me.setChildDisabled('#prev', currPage === 1 || isEmpty);
        me.setChildDisabled('#next', currPage === pageCount || isEmpty);
        me.setChildDisabled('#last', currPage === pageCount || isEmpty);
        me.setChildDisabled('#refresh', false);
        me.updateInfo();
        Ext.resumeLayouts(true);
        if (!me.calledInternal) {
            me.fireEvent('change', me, pageData || me.emptyPageData);
        }
    },
    setChildDisabled: function(selector, disabled) {
        var item = this.child(selector);
        if (item) {
            item.setDisabled(disabled);
        }
    },
    /**
     * @private
     */
    getPageData: function() {
        var store = this.store,
            totalCount = store.getTotalCount(),
            pageCount = Math.ceil(totalCount / store.pageSize),
            toRecord = Math.min(store.currentPage * store.pageSize, totalCount);
        return {
            total: totalCount,
            currentPage: store.currentPage,
            pageCount: Ext.Number.isFinite(pageCount) ? pageCount : 1,
            fromRecord: ((store.currentPage - 1) * store.pageSize) + 1,
            toRecord: toRecord || totalCount
        };
    },
    /**
     * @private
     */
    onLoadError: function() {
        this.setChildDisabled('#refresh', false);
    },
    getInputItem: function() {
        return this.child('#inputItem');
    },
    /**
     * @private
     */
    readPageFromInput: function(pageData) {
        var inputItem = this.getInputItem(),
            pageNum = false,
            v;
        if (inputItem) {
            v = inputItem.getValue();
            pageNum = parseInt(v, 10);
            if (!v || isNaN(pageNum)) {
                inputItem.setValue(pageData.currentPage);
                return false;
            }
        }
        return pageNum;
    },
    /**
     * @private
     */
    onPagingBlur: function(e) {
        var inputItem = this.getInputItem(),
            curPage;
        if (inputItem) {
            curPage = this.getPageData().currentPage;
            inputItem.setValue(curPage);
        }
    },
    /**
     * @private
     */
    onPagingKeyDown: function(field, e) {
        this.processKeyEvent(field, e);
    },
    processKeyEvent: function(field, e) {
        var me = this,
            key = e.getKey(),
            pageData = me.getPageData(),
            increment = e.shiftKey ? 10 : 1,
            pageNum;
        if (key === e.RETURN) {
            e.stopEvent();
            pageNum = me.readPageFromInput(pageData);
            if (pageNum !== false) {
                pageNum = Math.min(Math.max(1, pageNum), pageData.pageCount);
                if (pageNum !== pageData.currentPage && me.fireEvent('beforechange', me, pageNum) !== false) {
                    me.store.loadPage(pageNum);
                }
            }
        } else if (key === e.HOME || key === e.END) {
            e.stopEvent();
            pageNum = key === e.HOME ? 1 : pageData.pageCount;
            field.setValue(pageNum);
        } else if (key === e.UP || key === e.PAGE_UP || key === e.DOWN || key === e.PAGE_DOWN) {
            e.stopEvent();
            pageNum = me.readPageFromInput(pageData);
            if (pageNum) {
                if (key === e.DOWN || key === e.PAGE_DOWN) {
                    increment *= -1;
                }
                pageNum += increment;
                if (pageNum >= 1 && pageNum <= pageData.pageCount) {
                    field.setValue(pageNum);
                }
            }
        }
    },
    /**
     * @private
     */
    beforeLoad: function() {
        this.setChildDisabled('#refresh', true);
    },
    /**
     * Move to the first page, has the same effect as clicking the 'first' button.
     * Fires the {@link #beforechange} event. If the event returns `false`, then
     * the load will not be attempted.
     * @return {Boolean} `true` if the load was passed to the store.
     */
    moveFirst: function() {
        if (this.fireEvent('beforechange', this, 1) !== false) {
            this.store.loadPage(1);
            return true;
        }
        return false;
    },
    /**
     * Move to the previous page, has the same effect as clicking the 'previous' button.
     * Fires the {@link #beforechange} event. If the event returns `false`, then
     * the load will not be attempted.
     * @return {Boolean} `true` if the load was passed to the store.
     */
    movePrevious: function() {
        var me = this,
            store = me.store,
            prev = store.currentPage - 1;
        if (prev > 0) {
            if (me.fireEvent('beforechange', me, prev) !== false) {
                store.previousPage();
                return true;
            }
        }
        return false;
    },
    /**
     * Move to the next page, has the same effect as clicking the 'next' button.
     * Fires the {@link #beforechange} event. If the event returns `false`, then
     * the load will not be attempted.
     * @return {Boolean} `true` if the load was passed to the store.
     */
    moveNext: function() {
        var me = this,
            store = me.store,
            total = me.getPageData().pageCount,
            next = store.currentPage + 1;
        if (next <= total) {
            if (me.fireEvent('beforechange', me, next) !== false) {
                store.nextPage();
                return true;
            }
        }
        return false;
    },
    /**
     * Move to the last page, has the same effect as clicking the 'last' button.
     * Fires the {@link #beforechange} event. If the event returns `false`, then
     * the load will not be attempted.
     * @return {Boolean} `true` if the load was passed to the store.
     */
    moveLast: function() {
        var me = this,
            last = me.getPageData().pageCount;
        if (me.fireEvent('beforechange', me, last) !== false) {
            me.store.loadPage(last);
            return true;
        }
        return false;
    },
    /**
     * Refresh the current page, has the same effect as clicking the 'refresh' button.
     * Fires the {@link #beforechange} event. If the event returns `false`, then
     * the load will not be attempted.
     * @return {Boolean} `true` if the load was passed to the store.
     */
    doRefresh: function() {
        var me = this,
            store = me.store,
            current = store.currentPage;
        if (me.fireEvent('beforechange', me, current) !== false) {
            store.loadPage(current);
            return true;
        }
        return false;
    },
    getStoreListeners: function() {
        return {
            beforeload: this.beforeLoad,
            load: this.onLoad,
            exception: this.onLoadError
        };
    },
    onBindStore: function() {
        if (this.rendered) {
            this.updateBarInfo();
        }
    },
    doDestroy: function() {
        var me = this,
            listener = me._storeChangeListener;
        if (listener) {
            listener.destroy();
            me._storeChangeListener = null;
        }
        me.bindStore(null);
        me.callParent();
    }
});

/**
 * An internally used DataView for {@link Ext.form.field.ComboBox ComboBox}.
 */
Ext.define('Ext.view.BoundList', {
    extend: 'Ext.view.View',
    alias: 'widget.boundlist',
    alternateClassName: 'Ext.BoundList',
    requires: [
        'Ext.view.BoundListKeyNav',
        'Ext.layout.component.BoundList',
        'Ext.toolbar.Paging'
    ],
    mixins: [
        'Ext.mixin.Queryable'
    ],
    /**
     * @cfg {Number} pageSize
     * If greater than `0`, a {@link Ext.toolbar.Paging} is displayed at the bottom of the list
     * and store queries will execute with page {@link Ext.data.operation.Read#start start} and
     * {@link Ext.data.operation.Read#limit limit} parameters.
     */
    pageSize: 0,
    /**
     * @cfg {String} [displayField=""]
     * The field from the store to show in the view.
     */
    /**
     * @property {Ext.toolbar.Paging} pagingToolbar
     * A reference to the PagingToolbar instance in this view. Only populated if {@link #pageSize}
     * is greater than zero and the BoundList has been rendered.
     */
    /**
     * @cfg baseCls
     * @inheritdoc
     */
    baseCls: Ext.baseCSSPrefix + 'boundlist',
    /**
     * @cfg itemCls
     * @inheritdoc
     */
    itemCls: Ext.baseCSSPrefix + 'boundlist-item',
    listItemCls: '',
    /**
     * @cfg shadow
     * @inheritdoc
     */
    shadow: false,
    /**
     * @cfg trackOver
     * @inheritdoc
     */
    trackOver: true,
    /**
     * @cfg preserveScrollOnRefresh
     * @inheritdoc
     */
    preserveScrollOnRefresh: true,
    enableInitialSelection: false,
    refreshSelmodelOnRefresh: true,
    /**
     * @cfg componentLayout
     * @inheritdoc
     */
    componentLayout: 'boundlist',
    /**
     * @cfg navigationModel
     * @inheritdoc
     */
    navigationModel: 'boundlist',
    /**
     * @cfg scrollable
     * @inheritdoc
     */
    scrollable: true,
    /**
     * @property ariaEl
     * @inheritdoc
     */
    ariaEl: 'listEl',
    /**
     * @cfg tabIndex
     * @inheritdoc
     */
    tabIndex: -1,
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        'listWrap',
        'listEl'
    ],
    /* eslint-disable indent */
    /**
     * @cfg renderTpl
     * @inheritdoc
     */
    renderTpl: [
        '<div id="{id}-listWrap" data-ref="listWrap"',
        ' class="{baseCls}-list-ct ',
        Ext.dom.Element.unselectableCls,
        '">',
        '<ul id="{id}-listEl" data-ref="listEl" class="',
        Ext.baseCSSPrefix,
        'list-plain"',
        '<tpl foreach="ariaAttributes"> {$}="{.}"</tpl>',
        '>',
        '</ul>',
        '</div>',
        '{%',
        'var pagingToolbar=values.$comp.pagingToolbar;',
        'if (pagingToolbar) {',
        'Ext.DomHelper.generateMarkup(pagingToolbar.getRenderTree(), out);',
        '}',
        '%}',
        {
            disableFormats: true
        }
    ],
    /* eslint-enable indent */
    /**
     * @cfg {String/Ext.XTemplate} tpl
     * A String or Ext.XTemplate instance to apply to inner template.
     *
     * {@link Ext.view.BoundList} is used for the dropdown list of 
     * {@link Ext.form.field.ComboBox}. To customize the template you can set the tpl on 
     * the combobox config object:
     *
     *     Ext.create('Ext.form.field.ComboBox', {
     *         fieldLabel   : 'State',
     *         queryMode    : 'local',
     *         displayField : 'text',
     *         valueField   : 'abbr',
     *         store        : Ext.create('StateStore', {
     *             fields : ['abbr', 'text'],
     *             data   : [
     *                 {"abbr":"AL", "name":"Alabama"},
     *                 {"abbr":"AK", "name":"Alaska"},
     *                 {"abbr":"AZ", "name":"Arizona"}
     *                 //...
     *             ]
     *         }),
     *         // Template for the dropdown menu.
     *         // Note the use of the "x-list-plain" and "x-boundlist-item" class,
     *         // this is required to make the items selectable.
     *         tpl: Ext.create('Ext.XTemplate',
     *             '<ul class="x-list-plain"><tpl for=".">',
     *                 '<li role="option" class="x-boundlist-item">{abbr} - {name}</li>',
     *             '</tpl></ul>'
     *         ),
     *     });
     *
     * Defaults to:
     *
     *     Ext.create('Ext.XTemplate',
     *         '<ul><tpl for=".">',
     *             '<li role="option" class="' + itemCls + '">' + me.getInnerTpl(me.displayField) +
     *             '</li>',
     *         '</tpl></ul>'
     *     );
     *
     */
    // Override because on non-touch devices, the bound field
    // retains focus so that typing may narrow the list.
    // Only when the show is triggered by a touch does the BoundList
    // get explicitly focused so that the keyboard does not appear.
    /**
     * @cfg focusOnToFront
     * @inheritdoc
     */
    focusOnToFront: false,
    /**
     * @cfg alignOnScroll
     * @inheritdoc
     */
    alignOnScroll: false,
    initComponent: function() {
        var me = this,
            baseCls = me.baseCls,
            itemCls = me.itemCls;
        me.selectedItemCls = baseCls + '-selected';
        if (me.trackOver) {
            me.overItemCls = baseCls + '-item-over';
        }
        me.itemSelector = '.' + itemCls;
        if (me.floating) {
            me.addCls(baseCls + '-floating');
        }
        if (!me.tpl) {
            // should be setting aria-posinset based on entire set of data
            // not filtered set
            me.generateTpl();
        } else if (!me.tpl.isTemplate) {
            me.tpl = new Ext.XTemplate(me.tpl);
        }
        if (me.pageSize) {
            me.pagingToolbar = me.createPagingToolbar();
        }
        me.callParent();
    },
    /**
     * Allow tpl to be generated programmatically to respond to changes in displayField
     * @private
     */
    generateTpl: function() {
        var me = this;
        /* eslint-disable indent, max-len */
        me.tpl = new Ext.XTemplate('<tpl for=".">', '<li role="option" unselectable="on" class="' + me.itemCls + '">' + me.getInnerTpl(me.displayField) + '</li>', '</tpl>');
    },
    /* eslint-enable indent, max-len */
    /**
     * Updates the display field for this view. This will automatically trigger
     * an regeneration of the tpl so that the updated displayField can be used
     * @param {String} displayField
     */
    setDisplayField: function(displayField) {
        this.displayField = displayField;
        this.generateTpl();
    },
    getRefOwner: function() {
        return this.pickerField || this.callParent();
    },
    getRefItems: function() {
        var result = this.callParent(),
            toolbar = this.pagingToolbar;
        if (toolbar) {
            result.push(toolbar);
        }
        return result;
    },
    createPagingToolbar: function() {
        var me = this;
        return new Ext.toolbar.Paging({
            id: me.id + '-paging-toolbar',
            pageSize: me.pageSize,
            store: me.dataSource,
            border: false,
            ownerCt: me,
            ownerLayout: me.getComponentLayout()
        });
    },
    refresh: function() {
        var me = this,
            tpl = me.tpl;
        // Allow access to the context for XTemplate scriptlets
        tpl.field = me.pickerField;
        tpl.store = me.store;
        me.callParent();
        tpl.field = tpl.store = null;
        if (!me.ariaStaticRoles[me.ariaRole]) {
            me.refreshAriaAttributes();
        }
    },
    // The view selectively removes item nodes, so the toolbar
    // will be preserved in the DOM
    refreshAriaAttributes: function() {
        var me = this,
            store = me.store,
            selModel = me.getSelectionModel(),
            multiSelect, totalCount, nodes, node, record, index, i, len;
        // When the store is filtered or paged, we want to let the Assistive Technology
        // users know that there are more records than currently displayed. This is not
        // a requirement when the whole dataset fits the DOM.
        // Note that it is possible for the store to be filtered but not fit the DOM.
        // In that case we use filtered count as the set size.
        totalCount = store.isFiltered() ? store.getCount() : store.getTotalCount() || store.getCount();
        nodes = me.getNodes();
        multiSelect = me.pickerField && me.pickerField.multiSelect;
        for (i = 0 , len = nodes.length; i < len; i++) {
            node = nodes[i];
            record = null;
            if (totalCount !== len) {
                record = me.getRecord(node);
                index = store.indexOf(record);
                node.setAttribute('aria-setsize', totalCount);
                node.setAttribute('aria-posinset', index);
            }
            // For single-select combos aria-selected must be undefined
            if (multiSelect) {
                record = record || me.getRecord(node);
                node.setAttribute('aria-selected', selModel.isSelected(record));
            }
        }
    },
    bindStore: function(store, initial) {
        var toolbar = this.pagingToolbar;
        this.callParent(arguments);
        if (toolbar) {
            toolbar.bindStore(store, initial);
        }
    },
    /**
     * A method that returns the inner template for displaying items in the list.
     * This method is useful to override when using a more complex display value, for example
     * inserting an icon along with the text.
     *
     * The XTemplate is created with a reference to the owning form field in the `field` property
     * to provide access to context. For example to highlight the currently typed value,
     * the getInnerTpl may be configured into a ComboBox as part of the
     * {@link Ext.form.field.ComboBox#listConfig listConfig}:
     *
     *     listConfig: {
     *         getInnerTpl: function() {
     *             return '{[values.name.replace(this.field.getRawValue(), "<b>" +
     *                    this.field.getRawValue() + "</b>")]}';
     *         }
     *     }
     * @param {String} displayField The {@link #cfg!displayField} for the BoundList.
     * @return {String} The inner template
     */
    getInnerTpl: function(displayField) {
        return '{' + displayField + '}';
    },
    onShow: function() {
        var field = this.pickerField;
        this.callParent();
        // If the input field is not focused, then focus it.
        if (field && field.rendered && !field.hasFocus) {
            field.focus();
        }
    },
    afterComponentLayout: function(width, height, oldWidth, oldHeight) {
        var field = this.pickerField;
        this.callParent([
            width,
            height,
            oldWidth,
            oldHeight
        ]);
        // Bound list may change size, so realign on layout
        // **if the field is an Ext.form.field.Picker which has alignPicker!**
        if (field && field.alignPicker) {
            field.alignPicker();
        }
    },
    onItemSelect: function(record) {
        var me = this,
            node;
        node = me.callParent([
            record
        ]);
        if (node) {
            if (me.ariaSelectable) {
                node.setAttribute('aria-selected', 'true');
            } else {
                node.removeAttribute('aria-selected');
            }
        }
        return node;
    },
    onItemDeselect: function(record) {
        var me = this,
            node;
        node = me.callParent([
            record
        ]);
        if (node && me.ariaSelectable) {
            if (me.pickerField && me.pickerField.multiSelect) {
                node.setAttribute('aria-selected', 'false');
            } else {
                node.removeAttribute('aria-selected');
            }
        }
        return node;
    },
    // Clicking on an already selected item collapses the picker
    onItemClick: function(record) {
        // The selection change events won't fire when clicking on the selected element.
        // Detect it here.
        var me = this,
            field = me.pickerField,
            valueField, selected;
        if (!field) {
            return;
        }
        valueField = field.valueField;
        selected = me.getSelectionModel().getSelection();
        if (!field.multiSelect && selected.length) {
            selected = selected[0];
            // Not all pickerField's have a collapse API, i.e. Ext.ux.form.MultiSelect.
            if (selected && field.isEqual(record.get(valueField), selected.get(valueField)) && field.collapse) {
                field.collapse();
            }
        }
    },
    onContainerClick: function(e) {
        var toolbar = this.pagingToolbar,
            clientRegion;
        // Ext.view.View template method
        // Do not continue to process the event as a container click
        // if it is within the pagingToolbar
        if (toolbar && toolbar.rendered && e.within(toolbar.el)) {
            return false;
        }
        // IE10 and IE11 will fire pointer events when user drags listWrap scrollbars,
        // which may result in selection being reset.
        if (Ext.isIE10 || Ext.isIE11) {
            clientRegion = this.listWrap.getClientRegion();
            if (!e.getPoint().isContainedBy(clientRegion)) {
                return false;
            }
        }
    },
    doDestroy: function() {
        this.pagingToolbar = Ext.destroy(this.pagingToolbar);
        this.callParent();
    },
    privates: {
        /**
         * @method getNodeContainer
         * @private
         * @inheritdoc
         */
        getNodeContainer: function() {
            return this.listEl;
        },
        getTargetEl: function() {
            return this.listEl;
        },
        getOverflowEl: function() {
            return this.listWrap;
        },
        // Do the job of a container layout at this point even though we are not a Container.
        finishRenderChildren: function() {
            var toolbar = this.pagingToolbar;
            this.callParent(arguments);
            if (toolbar) {
                toolbar.finishRender();
            }
        }
    }
});

/**
 * A combobox control with support for autocomplete, remote loading, and many other features.
 *
 * A ComboBox is like a combination of a traditional HTML text `<input>` field and a `<select>`
 * field; the user is able to type freely into the field, and/or pick values from a dropdown
 * selection list. The user can input any value by default, even if it does not appear
 * in the selection list; to prevent free-form values and restrict them to items in the list,
 * set {@link #forceSelection} to `true`.
 *
 * The selection list's options are populated from any {@link Ext.data.Store}, including remote
 * stores. The data items in the store are mapped to each option's displayed text and backing value
 * via the {@link #valueField} and {@link #displayField} configurations, respectively.
 *
 * If your store is not remote, i.e. it depends only on local data and is loaded up front,
 * you should be sure to set the {@link #queryMode} to `'local'`, as this will improve
 * responsiveness for the user.
 *
 * # Example usage:
 *
 *     @example
 *     // The data store containing the list of states
 *     var states = Ext.create('Ext.data.Store', {
 *         fields: ['abbr', 'name'],
 *         data : [
 *             {"abbr":"AL", "name":"Alabama"},
 *             {"abbr":"AK", "name":"Alaska"},
 *             {"abbr":"AZ", "name":"Arizona"}
 *         ]
 *     });
 *
 *     // Create the combo box, attached to the states data store
 *     Ext.create('Ext.form.ComboBox', {
 *         fieldLabel: 'Choose State',
 *         store: states,
 *         queryMode: 'local',
 *         displayField: 'name',
 *         valueField: 'abbr',
 *         renderTo: Ext.getBody()
 *     });
 *
 * # Events
 *
 * To do something when something in ComboBox is selected, configure the select event:
 *
 *     var cb = Ext.create('Ext.form.ComboBox', {
 *         // all of your config options
 *         listeners:{
 *              scope: yourScope,
 *              'select': yourFunction
 *         }
 *     });
 *
 *     // Alternatively, you can assign events after the object is created:
 *     var cb = new Ext.form.field.ComboBox(yourOptions);
 *     cb.on('select', yourFunction, yourScope);
 *
 * # Multiple Selection
 * The {@link #multiSelect} config is deprecated.  For multiple selection use 
 * {@link Ext.form.field.Tag} or {@link Ext.view.MultiSelector}.
 *
 * # Filtered Stores
 *
 * If you have a local store that is already filtered, you can use the {@link #lastQuery}
 * config option to prevent the store from having the filter being cleared on first expand.
 *
 * ## Customized combobox
 *
 * Both the text shown in dropdown menu and text field can be easily customized:
 *
 *     @example
 *     var states = Ext.create('Ext.data.Store', {
 *         fields: ['abbr', 'name'],
 *         data : [
 *             {"abbr":"AL", "name":"Alabama"},
 *             {"abbr":"AK", "name":"Alaska"},
 *             {"abbr":"AZ", "name":"Arizona"}
 *         ]
 *     });
 *
 *     Ext.create('Ext.form.ComboBox', {
 *         fieldLabel: 'Choose State',
 *         store: states,
 *         queryMode: 'local',
 *         valueField: 'abbr',
 *         renderTo: Ext.getBody(),
 *         // Template for the dropdown menu.
 *         // Note the use of the "x-list-plain" and "x-boundlist-item" class,
 *         // this is required to make the items selectable.
 *         tpl: Ext.create('Ext.XTemplate',
 *             '<ul class="x-list-plain"><tpl for=".">',
 *                 '<li role="option" class="x-boundlist-item">{abbr} - {name}</li>',
 *             '</tpl></ul>'
 *         ),
 *         // template for the content inside text field
 *         displayTpl: Ext.create('Ext.XTemplate',
 *             '<tpl for=".">',
 *                 '{abbr} - {name}',
 *             '</tpl>'
 *         )
 *     });
 *
 * See also the {@link #listConfig} option for additional configuration of the dropdown.
 *
 */
Ext.define('Ext.form.field.ComboBox', {
    extend: 'Ext.form.field.Picker',
    alternateClassName: 'Ext.form.ComboBox',
    alias: [
        'widget.combobox',
        'widget.combo'
    ],
    requires: [
        'Ext.util.DelayedTask',
        'Ext.view.BoundList',
        'Ext.data.StoreManager'
    ],
    mixins: [
        'Ext.util.StoreHolder'
    ],
    config: {
        filters: null,
        /**
         * @cfg {Ext.data.Model} selection
         * The selected model. Typically used with {@link #bind binding}.
         */
        selection: null,
        /**
         * @cfg {String} valueNotFoundText
         * When using a name/value combo, if the value passed to setValue is not found in the store,
         * valueNotFoundText will be displayed as the field text if defined. If this default text
         * is used, it means there is no value set and no validation will occur on this field.
         */
        valueNotFoundText: null,
        /**
         * @cfg {String/String[]/Ext.XTemplate} displayTpl
         * The template to be used to display selected records inside the text field. An array
         * of the selected records' data will be passed to the template. Defaults to:
         *
         *     '<tpl for=".">' +
         *         '{[typeof values === "string" ? values : values["' + me.displayField + '"]]}' +
         *         '<tpl if="xindex < xcount">' + me.delimiter + '</tpl>' +
         *     '</tpl>'
         *
         * By default only the immediate data of the record is passed (no associated data).
         * The {@link #getRecordDisplayData} can be overridden to extend this.
         */
        displayTpl: null,
        /**
         * @cfg {String} delimiter
         * The character(s) used to separate the {@link #displayField display values} of multiple
         * selected items when `{@link #multiSelect} = true`.
         * @deprecated 5.1.0 For multiple selection use {@link Ext.form.field.Tag} or 
         * {@link Ext.view.MultiSelector}
         * @locale
         */
        delimiter: ', ',
        /**
         * @cfg {String} displayField
         * The underlying {@link Ext.data.Field#name data field name} to bind to this ComboBox.
         *
         * See also `{@link #valueField}`.
         */
        displayField: 'text'
    },
    /**
     * @cfg publishes
     * @inheritdoc
     */
    publishes: [
        'selection'
    ],
    /**
     * @cfg twoWayBindable
     * @inheritdoc
     */
    twoWayBindable: [
        'selection'
    ],
    /**
     * @cfg {String} triggerCls
     * An additional CSS class used to style the trigger button. The trigger will always get
     * the {@link Ext.form.trigger.Trigger#baseCls} by default and `triggerCls` will be
     * **appended** if specified.
     */
    triggerCls: Ext.baseCSSPrefix + 'form-arrow-trigger',
    /**
     * @cfg {String} [hiddenName=""]
     * The name of an underlying hidden field which will be synchronized with the underlying value
     * of the combo. This option is useful if the combo is part of a form element doing a regular
     * form post. The hidden field will not be created unless a hiddenName is specified.
     */
    hiddenName: '',
    /**
     * @cfg {Boolean} [pinList=true]
     * Has no effect if {@link #multiSelect} is `false`
     *
     * Configure as `false` to automatically collapse the pick list after a selection is made.
     * @deprecated 5.1.0 For multiple selection use {@link Ext.form.field.Tag} or 
     * {@link Ext.view.MultiSelector}
     */
    /**
     * @cfg {Boolean} collapseOnSelect
     * Has no effect if {@link #multiSelect} is `false`
     *
     * Configure as true to automatically collapse the pick list after a selection is made.
     * @deprecated 5.1.0 For multiple selection use {@link Ext.form.field.Tag} or 
     * {@link Ext.view.MultiSelector}
     */
    collapseOnSelect: false,
    /**
     * @property {Ext.dom.Element} hiddenDataEl
     * @private
     */
    /**
     * @private
     * @cfg {String} hiddenDataCls
     * CSS class used to find the {@link #hiddenDataEl}
     */
    hiddenDataCls: Ext.baseCSSPrefix + 'hidden-display ' + Ext.baseCSSPrefix + 'form-data-hidden',
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'combobox',
    /**
     * @property autoDestroyBoundStore
     * @inheritdoc
     */
    autoDestroyBoundStore: true,
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: {
        'hiddenDataEl': true
    },
    /**
     * @property {Boolean} filtered
     * True if there are extra `filters` appllied to this component.
     * @private
     * @readonly
     * @since 5.0.0
     */
    filtered: false,
    afterRender: function() {
        var me = this;
        me.callParent(arguments);
        me.setHiddenValue(me.value);
    },
    /**
     * @cfg {Ext.data.Store/String/Array/Object} store (required)
     * The data source to which the combo / tagfield is bound. Acceptable values for this 
     * property are:
     *
     *   - **any {@link Ext.data.Store Store} class / subclass**
     *   - **an {@link Ext.data.Store#storeId ID of a store}**
     *   - **an Array** : Arrays will be converted to a {@link Ext.data.Store} internally, 
     *     automatically generating {@link Ext.data.Field#name field names} to work with all 
     *     data components.
     *
     *     - **1-dimensional array** : (e.g., `['Foo','Bar']`)
     *
     *       A 1-dimensional array will automatically be expanded (each array item will be 
     *       used for both the combo {@link #valueField} and {@link #displayField})
     *
     *     - **2-dimensional array** : (e.g., `[['f','Foo'],['b','Bar']]`)
     *
     *       For a multi-dimensional array, the value in index 0 of each item will be assumed 
     *       to be the combo {@link #valueField}, while the value at index 1 is assumed to be 
     *       the combo {@link #displayField}.
     *   - **a {@link Ext.data.Store Store} config object**.  When passing a config you can 
     *     specify the store type by alias.  Passing a config object with a store type will 
     *     dynamically create a new store of that type when the combo / tagfield is 
     *     instantiated.
     *
     *     Ext.define('MyApp.store.States', {
     *         extend: 'Ext.data.Store',
     *         alias: 'store.states',
     *         fields: ['name']
     *     });
     *     
     *     Ext.create({
     *         xtype: 'combobox',
     *         renderTo: document.body,
     *         store: {
     *             type: 'states',
     *             data: [{
     *                 name: 'California'
     *             }]
     *         },
     *         queryMode: 'local',
     *         displayField: 'name',
     *         valueField: 'name'
     *     });
     *
     * See also {@link #queryMode}.
     */
    /**
     * @cfg {Boolean} multiSelect
     * If set to `true`, allows the combo field to hold more than one value at a time, and allows
     * selecting multiple items from the dropdown list. The combo's text field will show all
     * selected values separated by the {@link #delimiter}.
     * @deprecated 5.1.0 Use {@link Ext.form.field.Tag} or {@link Ext.view.MultiSelector}
     */
    multiSelect: false,
    /**
     * @cfg {String} valueField (required)
     * The underlying {@link Ext.data.Field#name data value name} to bind to this ComboBox.
     *
     * **Note**: use of a `valueField` requires the user to make a selection in order for a value
     * to be mapped. See also `{@link #displayField}`.
     *
     * Defaults to match the value of the {@link #displayField} config.
     */
    /**
     * @cfg {String} triggerAction
     * The action to execute when the trigger is clicked.
     *
     *   - **`'all'`**: {@link #doQuery run the query} specified by the `{@link #allQuery}`
     * config option
     *
     *   - **`'last'`**: {@link #doQuery run the query} using the
     * `{@link #lastQuery last query value}`.
     *
     *   - **`'query'`**: {@link #doQuery run the query} using the
     * {@link Ext.form.field.Base#getRawValue raw value}.
     *
     * See also `{@link #queryParam}`.
     */
    triggerAction: 'all',
    /**
     * @cfg {String} allQuery
     * The text query to send to the server to return all records for the list with no filtering
     */
    allQuery: '',
    /**
     * @cfg {String} queryParam
     * Name of the parameter used by the Store to pass the typed string when the ComboBox
     * is configured with `{@link #queryMode}: 'remote'`. If explicitly set to a falsy value
     * it will not be sent.
     */
    queryParam: 'query',
    /**
     * @cfg {String} queryMode
     * The mode in which the ComboBox uses the configured Store. Acceptable values are:
     *
     *   - **`'remote'`** :
     *
     *     In `queryMode: 'remote'`, the ComboBox loads its Store dynamically based upon user
     *     interaction.
     *
     *     This is typically used for "autocomplete" type inputs, and after the user finishes
     *     typing, the Store is {@link Ext.data.Store#method-load load}ed.
     *
     *     A parameter containing the typed string is sent in the load request. The default
     *     parameter name for the input string is `query`, but this can be configured using
     *     the {@link #queryParam} config.
     *
     *     In `queryMode: 'remote'`, the Store may be configured with
     *     `{@link Ext.data.Store#remoteFilter remoteFilter}: true`, and further filters may be
     *     _programatically_ added to the Store which are then passed with every load
     *     request which allows the server to further refine the returned dataset.
     *
     *     Typically, in an autocomplete situation, {@link #hideTrigger} is configured `true`
     *     because it has no meaning for autocomplete.
     *
     *   - **`'local'`** :
     *
     *     ComboBox loads local data
     *
     *         var combo = new Ext.form.field.ComboBox({
     *             renderTo: document.body,
     *             queryMode: 'local',
     *             store: new Ext.data.ArrayStore({
     *                 id: 0,
     *                 fields: [
     *                     'myId',  // numeric value is the key
     *                     'displayText'
     *                 ],
     *                 data: [[1, 'item1'], [2, 'item2']]  // data is local
     *             }),
     *             valueField: 'myId',
     *             displayField: 'displayText',
     *             triggerAction: 'all'
     *         });
     */
    queryMode: 'remote',
    /**
     * @cfg {Boolean} queryCaching
     * When true, this prevents the combo from re-querying (either locally or remotely) when
     * the current query is the same as the previous query.
     */
    queryCaching: true,
    /**
     * @cfg {Boolean} autoLoadOnValue
     * This option controls whether to *initially* load the store when a value is set so that
     * the display value can be determined from the appropriate record.
     * The store will only be loaded in a limited set of circumstances:
     * - The store is not currently loading.
     * - The store does not have a pending {@link Ext.data.Store#autoLoad}.
     * - The store has not been loaded before.
     */
    autoLoadOnValue: false,
    /**
     * @cfg {Number} pageSize
     * If greater than `0`, a {@link Ext.toolbar.Paging} is displayed in the footer of the dropdown
     * list and the {@link #doQuery filter queries} will execute with page start
     * and {@link Ext.view.BoundList#pageSize limit} parameters.
     * Only applies when `{@link #queryMode} = 'remote'`.
     */
    pageSize: 0,
    /**
     * @cfg {Number} queryDelay
     * The length of time in milliseconds to delay between the start of typing and sending the query
     * to filter the dropdown list.
     *
     * Defaults to `500` if `{@link #queryMode} = 'remote'` or `10` if
     * `{@link #queryMode} = 'local'`
     */
    /**
     * @cfg {Number} minChars
     * The minimum number of characters the user must type before autocomplete and
     * {@link #typeAhead} activate.
     *
     * Defaults to `4` if `{@link #queryMode} = 'remote'` or `0` if `{@link #queryMode} = 'local'`,
     * does not apply if `{@link Ext.form.field.Trigger#editable editable} = false`.
     */
    /**
     * @cfg {Boolean} anyMatch
     * Configure as `true` to allow matching of the typed characters at any position in the
     * {@link #valueField}'s value.
     */
    anyMatch: false,
    /**
     * @cfg {Boolean} caseSensitive
     * Configure as `true` to make the filtering match with exact case matching
     */
    caseSensitive: false,
    /**
     * @cfg {Boolean} autoSelect
     * `true` to automatically highlight the first result gathered by the data store in the
     * dropdown list when it is opened. A false value would cause nothing in the list to be
     * highlighted automatically, so the user would have to manually highlight an item before
     * pressing the enter or {@link #selectOnTab tab} key to select it (unless the value of
     * ({@link #typeAhead}) were true), or use the mouse to select a value.
     */
    autoSelect: true,
    /**
     * @cfg {Boolean} autoSelectLast
     * When `true`, the last selected record in the dropdown list will be re-selected
     * upon {@link #autoSelect}. Set to `false` to always select the first record in the
     * drop-down list. For accessible applications it is recommended to set this option
     * to `false`.
     */
    autoSelectLast: true,
    /**
     * @cfg {Boolean} typeAhead
     * `true` to populate and autoselect the remainder of the text being typed after a configurable
     * delay ({@link #typeAheadDelay}) if it matches a known value.
     */
    typeAhead: false,
    /**
     * @cfg {Number} typeAheadDelay
     * The length of time in milliseconds to wait until the typeahead text is displayed if
     * `{@link #typeAhead} = true`
     */
    typeAheadDelay: 250,
    /**
     * @cfg {Boolean} selectOnTab
     * Whether the Tab key should select the currently highlighted item.
     */
    selectOnTab: true,
    /**
     * @cfg {Boolean} forceSelection
     * `true` to restrict the selected value to one of the values in the list, `false` to allow
     * the user to set arbitrary text into the field.
     */
    forceSelection: false,
    /**
     * @cfg {Boolean} growToLongestValue
     * `false` to not allow the component to resize itself when its data changes
     * (and its {@link #grow} property is `true`)
     */
    growToLongestValue: true,
    /**
     * @cfg {Boolean} clearFilterOnBlur
     * *When {@link #queryMode} is `'local'` only*
     * 
     * As text is entered, the underlying store is filtered to match the value. When this option
     * is `true`, any filtering applied by this field will be cleared when focus is removed
     * & reinstated on focus.
     * If `false`, the filters will be left in place.
     */
    clearFilterOnBlur: true,
    /**
     * @cfg {Boolean} enableRegEx
     * *When {@link #queryMode} is `'local'` only*
     *
     * Set to `true` to have the ComboBox use the typed value as a RegExp source to filter the store
     * to get possible matches. Invalid regex values will be ignored.
     */
    /**
     * @property {String} lastQuery
     * The value of the match string used to filter the store. Delete this property to force
     * a requery. Example use:
     *
     *     var combo = new Ext.form.field.ComboBox({
     *         ...
     *         queryMode: 'remote',
     *         listeners: {
     *             // delete the previous query in the beforequery event or set
     *             // combo.lastQuery = null (this will reload the store the next time it expands)
     *             beforequery: function(qe){
     *                 delete qe.combo.lastQuery;
     *             }
     *         }
     *     });
     *
     * To make sure the filter in the store is not cleared the first time the ComboBox trigger
     * is used configure the combo with `lastQuery=''`. Example use:
     *
     *     var combo = new Ext.form.field.ComboBox({
     *         ...
     *         queryMode: 'local',
     *         triggerAction: 'all',
     *         lastQuery: ''
     *     });
     */
    /**
     * @cfg {Object} defaultListConfig
     * Set of options that will be used as defaults for the user-configured {@link #listConfig}
     * object.
     */
    defaultListConfig: {
        loadingHeight: 70,
        minWidth: 70,
        maxHeight: 300,
        shadow: 'sides'
    },
    /**
     * @cfg {String/HTMLElement/Ext.dom.Element} transform
     * The id, DOM node or {@link Ext.dom.Element} of an existing HTML `<select>` element to convert
     * into a ComboBox. The target select's options will be used to build the options
     * in the ComboBox dropdown; a configured {@link #store} will take precedence over this.
     */
    /**
     * @cfg {Boolean} transformInPlace
     * `true` to automatically render this combo box in place of the select element that is being 
     * {@link #transform transformed}. If `false`, this combo will be rendered using the normal
     * rendering, either as part of a layout, or using {@link #renderTo} or {@link #method-render}.
     */
    transformInPlace: true,
    /**
     * @cfg {Object} listConfig
     * An optional set of configuration properties that will be passed to the
     * {@link Ext.view.BoundList}'s constructor. Any configuration that is valid for BoundList
     * can be included. Some of the more useful ones are:
     *
     *   - {@link Ext.view.BoundList#cls cls} - defaults to empty
     *   - {@link Ext.view.BoundList#emptyText emptyText} - defaults to empty string
     *   - {@link Ext.view.BoundList#itemSelector itemSelector} - defaults to the value defined
     *     in BoundList
     *   - {@link Ext.view.BoundList#loadingText loadingText} - defaults to `'Loading...'`
     *   - {@link Ext.view.BoundList#minWidth minWidth} - defaults to `70`
     *   - {@link Ext.view.BoundList#maxWidth maxWidth} - defaults to `undefined`
     *   - {@link Ext.view.BoundList#maxHeight maxHeight} - defaults to `300`
     *   - {@link Ext.view.BoundList#resizable resizable} - defaults to `false`
     *   - {@link Ext.view.BoundList#shadow shadow} - defaults to `'sides'`
     *   - {@link Ext.view.BoundList#width width} - defaults to `undefined` (automatically set
     *     to the width of the ComboBox field if {@link #matchFieldWidth} is true)
     *   - {@link Ext.view.BoundList#getInnerTpl getInnerTpl} A function which returns a template
     *     string which renders the ComboBox's {@link #displayField} value in the dropdown.
     *     This defaults to just outputting the raw value, but may use any
     *     {@link Ext.XTemplate XTemplate} methods to produce output.
     *
     *     The running template is configured with some extra properties that provide some context:
     *         - field {@link Ext.form.field.ComboBox ComboBox} This combobox
     *         - store {@link Ext.data.Store Store} This combobox's data store
     */
    /**
     * @private
     */
    clearValueOnEmpty: true,
    newlineRe: /\r?\n/g,
    getGrowWidth: function() {
        var me = this,
            value = me.inputEl.dom.value,
            field, store, dataLn, currentLongestLength, i, item, itemLn;
        if (me.growToLongestValue) {
            field = me.displayField;
            store = me.store;
            dataLn = store.data.length;
            currentLongestLength = 0;
            for (i = 0; i < dataLn; i++) {
                item = store.getAt(i).data[field];
                itemLn = item.length;
                // Compare the current item's length with the current longest length
                // and store the value.
                if (itemLn > currentLongestLength) {
                    currentLongestLength = itemLn;
                    value = item;
                }
            }
        }
        return value;
    },
    /**
     * @event beforequery
     * Fires before all queries are processed. Return false to cancel the query or set
     * the queryPlan's cancel property to true.
     *
     * @param {Object} queryPlan An object containing details about the query to be executed.
     * @param {Ext.form.field.ComboBox} queryPlan.combo A reference to this ComboBox.
     * @param {String} queryPlan.query The query value to be used to match against the ComboBox's
     * {@link #valueField}.
     * @param {Boolean} queryPlan.forceAll If `true`, causes the query to be executed even
     * if the minChars threshold is not met.
     * @param {Boolean} queryPlan.cancel A boolean value which, if set to `true` upon return,
     * causes the query not to be executed.
     * @param {Boolean} queryPlan.rawQuery If `true` indicates that the raw input field value
     * is being used, and upon store load,
     */
    /**
     * @event select
     * Fires when at least one list item is selected.
     * @param {Ext.form.field.ComboBox} combo This combo box
     * @param {Ext.data.Model/Ext.data.Model[]} record With {@link #multiSelect} 
     * `false`, the value will be a single record. With {@link #multiSelect} `true`, the 
     * value will be an array of records.
     */
    /**
     * @event beforeselect
     * Fires before the selected item is added to the collection
     * @param {Ext.form.field.ComboBox} combo This combo box
     * @param {Ext.data.Record} record The selected record
     * @param {Number} index The index of the selected record
     */
    /**
     * @event beforedeselect
     * Fires before the deselected item is removed from the collection
     * @param {Ext.form.field.ComboBox} combo This combo box
     * @param {Ext.data.Record} record The deselected record
     * @param {Number} index The index of the deselected record
     */
    initComponent: function() {
        var me = this,
            isDefined = Ext.isDefined,
            store = me.store,
            transform = me.transform,
            transformSelect, isLocalMode;
        if (me.typeAhead && me.multiSelect) {
            Ext.raise('typeAhead and multiSelect are mutually exclusive options -- ' + 'please remove one of them.');
        }
        if (me.typeAhead && !me.editable) {
            Ext.raise('If typeAhead is enabled the combo must be editable: true -- ' + 'please change one of those settings.');
        }
        if (me.selectOnFocus && !me.editable) {
            Ext.raise('If selectOnFocus is enabled the combo must be editable: true -- ' + 'please change one of those settings.');
        }
        // Check for presence of deprecated pinList config, and convert it to collapseOnSelect
        if ('pinList' in me) {
            me.collapseOnSelect = !me.pinList;
        }
        // Build store from 'transform' HTML select element's options
        if (transform) {
            transformSelect = Ext.getDom(transform);
            if (transformSelect) {
                if (!me.store) {
                    store = Ext.Array.map(Ext.Array.from(transformSelect.options), function(option) {
                        return [
                            option.value,
                            option.text
                        ];
                    });
                }
                if (!me.name) {
                    me.name = transformSelect.name;
                }
                if (!('value' in me)) {
                    me.value = transformSelect.value;
                }
            }
        }
        // Nothing configured, so generate one. This allows the user to
        // specify displayField in initComponent for extended classes.
        // It's necessary to check me.displayTpl.auto here because due to the
        // order of the config system, the displayTpl might be auto generated
        // before we get to use the user's config, so we use me.displayTpl.auto
        // to detect that it is auto generated.
        if (!me.displayTpl || me.displayTpl.auto) {
            me.setDisplayTpl(false);
        }
        me.bindStore(store || 'ext-empty-store', true, true);
        isLocalMode = me.queryMode === 'local';
        if (!isDefined(me.queryDelay)) {
            me.queryDelay = isLocalMode ? 10 : 500;
        }
        if (!isDefined(me.minChars)) {
            me.minChars = isLocalMode ? 0 : 4;
        }
        me.callParent();
        me.doQueryTask = new Ext.util.DelayedTask(me.doRawQuery, me);
        // render in place of 'transform' select
        if (transformSelect) {
            if (me.transformInPlace) {
                me.render(transformSelect.parentNode, transformSelect);
                delete me.renderTo;
            }
            Ext.removeNode(transformSelect);
        }
    },
    initEvents: function() {
        var me = this;
        me.callParent();
        // These key bindings need to have higher priority than BoundList keynav
        // so that Alt-Up/Down arrows would expand and collapse the picker without
        // highlighting items and/or changing selection
        me.altArrowKeyNav = new Ext.util.KeyNav({
            target: me.inputEl,
            forceKeyDown: true,
            priority: 1002,
            // BoundList keynav has 1001
            scope: me,
            down: {
                alt: true,
                handler: me.onAltDownArrow
            },
            up: {
                alt: true,
                handler: me.onAltUpArrow
            }
        });
    },
    getSubTplData: function(fieldData) {
        var me = this,
            id = me.id,
            data, ariaAttr;
        data = me.callParent([
            fieldData
        ]);
        if (!me.ariaStaticRoles[me.ariaRole]) {
            ariaAttr = data.ariaElAttributes;
            if (ariaAttr) {
                ariaAttr['aria-owns'] = id + '-inputEl ' + id + '-picker-listEl';
                // TODO Change that to reflect the real behavior
                ariaAttr['aria-autocomplete'] = 'list';
            }
        }
        return data;
    },
    getSubTplMarkup: function(fieldData) {
        var me = this,
            hiddenDataElMarkup = '',
            markup = me.callParent(arguments);
        if (me.hiddenName) {
            hiddenDataElMarkup = '<div id="' + fieldData.id + '-hiddenDataEl" data-ref="hiddenDataEl" class="' + me.hiddenDataCls + '" role="presentation"></div>';
        }
        return hiddenDataElMarkup + markup;
    },
    applyDisplayTpl: function(displayTpl) {
        var me = this;
        if (!displayTpl) {
            /* eslint-disable indent, max-len */
            displayTpl = new Ext.XTemplate('<tpl for=".">' + '{[typeof values === "string" ? values : values["' + me.getDisplayField() + '"]]}' + '<tpl if="xindex < xcount">' + me.getDelimiter() + '</tpl>' + '</tpl>');
            /* eslint-enable indent, max-len */
            displayTpl.auto = true;
        } else if (!displayTpl.isTemplate) {
            displayTpl = new Ext.XTemplate(displayTpl);
        }
        return displayTpl;
    },
    applyFilters: function(filters, collection) {
        var me = this;
        if (filters === null || filters.isFilterCollection) {
            return filters;
        }
        if (filters) {
            if (!collection) {
                collection = this.getFilters();
            }
            collection.beginUpdate();
            collection.splice(0, collection.length, filters);
            collection.each(function(filter) {
                filter.ownerId = me.id;
            });
            collection.endUpdate();
        }
        return collection;
    },
    applyValueNotFoundText: function(v) {
        var me = this,
            displayField = me.getDisplayField(),
            valueField = me.valueField,
            valueNotFoundRecord = me.valueNotFoundRecord;
        if (!valueNotFoundRecord) {
            valueNotFoundRecord = me.valueNotFoundRecord = new Ext.data.Model();
        }
        valueNotFoundRecord.set(displayField, v);
        if (valueField && displayField !== valueField) {
            valueNotFoundRecord.set(valueField, v);
        }
        return v;
    },
    updateDisplayField: function(displayField) {
        // force displayTpl refresh when displayField changes
        if (displayField && !this.displayTpl || this.displayTpl.auto) {
            this.setDisplayTpl(false);
        }
    },
    /**
     * Returns the `Ext.util.FilterCollection`. Unless `autoCreate` is explicitly passed
     * as `false` this collection will be automatically created if it does not yet exist.
     * @param [autoCreate=true] Pass `false` to disable auto-creation of the collection.
     * @return {Ext.util.FilterCollection} The collection of filters.
     */
    getFilters: function(autoCreate) {
        var ret = this.filters;
        if (!ret && autoCreate !== false) {
            ret = new Ext.util.FilterCollection();
            this.setFilters(ret);
        }
        return ret;
    },
    updateFilters: function(newFilters, oldFilters) {
        var me = this;
        if (oldFilters) {
            oldFilters.un('endupdate', 'onEndUpdateFilters', me);
        }
        if (newFilters) {
            newFilters.on('endupdate', 'onEndUpdateFilters', me);
        }
        me.onEndUpdateFilters(newFilters);
    },
    onEndUpdateFilters: function(filters) {
        var me = this,
            was = me.filtered,
            is = !!filters && (filters.length > 0),
            // booleanize filters
            old, storeFilters;
        if (was || is) {
            me.filtered = is;
            old = [];
            storeFilters = me.store.getFilters();
            storeFilters.each(function(filter) {
                if (filter.ownerId === me.id && !filters.contains(filter)) {
                    old.push(filter);
                }
            });
            storeFilters.splice(0, old, filters.items);
        }
    },
    clearLocalFilter: function() {
        var me = this,
            filter = me.queryFilter;
        if (filter) {
            me.queryFilter = null;
            // Must set changingFilters flag for this.checkValueOnChange.
            // the suppressEvents flag does not affect the filterchange event
            me.changingFilters = true;
            me.store.removeFilter(filter, true);
            me.changingFilters = false;
        }
    },
    completeEdit: function(e) {
        var me = this;
        this.callParent([
            e
        ]);
        me.doQueryTask.cancel();
        me.assertValue();
        if (me.queryFilter && me.queryMode === 'local' && me.clearFilterOnBlur) {
            me.clearLocalFilter();
        }
    },
    onFocus: function(e) {
        var me = this;
        me.callParent([
            e
        ]);
        if (me.triggerAction !== 'all' && me.queryFilter && me.queryMode === 'local' && me.clearFilterOnBlur) {
            delete me.lastQuery;
            me.doRawQuery();
        }
    },
    onAltDownArrow: function(e) {
        e.stopEvent();
        if (!this.isExpanded) {
            this.onDownArrow(e);
        }
        // Stop further keyNav processing
        return false;
    },
    onAltUpArrow: function(e) {
        e.stopEvent();
        if (this.isExpanded) {
            this.onEsc(e);
        }
        // Stop further keyNav processing
        return false;
    },
    /**
     * @private
     */
    assertValue: function() {
        var me = this,
            rawValue = me.getRawValue(),
            displayValue = me.getDisplayValue(),
            lastRecords = me.lastSelectedRecords,
            preventChange = false,
            needToSelect = false,
            value, rec;
        if (me.forceSelection) {
            if (me.multiSelect) {
                // For multiselect, check that the current displayed value matches the current
                // selection, if it does not then revert to the most recent selection.
                if (rawValue !== displayValue) {
                    me.setRawValue(displayValue);
                }
            } else {
                // For single-select, match the displayed value to a record and select it,
                // if it does not match a record then revert to the most recent selection.
                rec = me.findRecordByDisplay(rawValue);
                if (!rec) {
                    // if we have a custom displayTpl it's likely that findRecordByDisplay won't
                    // find the value based on RawValue, so we give it another try using the data
                    // stored in displayTplData if there is any.
                    if (me.displayTplData && me.displayTplData.length) {
                        rec = me.findRecordByValue(me.displayTplData[0][me.valueField]);
                    }
                    if (!rec && lastRecords && (!me.allowBlank || me.rawValue)) {
                        rec = lastRecords[0];
                        // Can't get rec from display value, so resetting previous one. 
                        // We should must fire select for this (As we will change display value)
                        needToSelect = true;
                    }
                }
                // Prevent an issue where we have duplicate display values with
                // different underlying values.
                else if (me.getDisplayValue([
                    me.getRecordDisplayData(rec)
                ]) === displayValue) {
                    rec = null;
                    preventChange = true;
                }
                if (rec) {
                    if (needToSelect || !(lastRecords && lastRecords[0] === rec)) {
                        me.select(rec, true);
                        me.fireEvent('select', me, rec);
                    }
                } else if (!preventChange) {
                    if (lastRecords) {
                        delete me.lastSelectedRecords;
                    }
                    // We need to reset any value that could have been set in the dom before
                    // or during a store load for remote combos. If we don't reset this,
                    // then ComboBox#getValue() will think that the value has changed and will
                    // then set `undefined` as the .value for forceSelection combos.  This then
                    // gets changed AGAIN to `null`, which will get set into the model field
                    // for editors. This is BAD.
                    me.setRawValue('');
                }
            }
        }
        // we can only call getValue() in this process if forceSelection is false
        // otherwise it will break the grid edit on tab
        // eslint-disable-next-line eqeqeq
        else if ((value = me.getValue()) && value == rawValue) {
            rec = me.findRecordByDisplay(value);
            if (rec && (rec !== (lastRecords && lastRecords[0]) || me.displayField !== me.valueField)) {
                me.select(rec, true);
                me.fireEvent('select', me, rec);
            }
        }
        me.collapse();
    },
    onTypeAhead: function() {
        var me = this,
            displayField = me.displayField,
            record = me.store.findRecord(displayField, me.getRawValue()),
            newValue, len, selStart;
        if (record) {
            newValue = record.get(displayField);
            len = newValue.length;
            selStart = me.getRawValue().length;
            if (selStart !== 0 && selStart !== len) {
                // Setting the raw value will cause a field mutation event.
                // Prime the lastMutatedValue so that this does not cause a requery.
                me.lastMutatedValue = newValue;
                me.setRawValue(newValue);
                me.selectText(selStart, newValue.length);
            }
        }
    },
    // invoked when a different store is bound to this combo
    // than the original
    resetToDefault: Ext.emptyFn,
    beforeReset: function() {
        this.callParent();
        this.clearLocalFilter();
    },
    onUnbindStore: function() {
        var me = this,
            picker = me.picker;
        // If we'd added a local filter, remove it.
        // Listeners are unbound, so we don't need the changingFilters flag
        if (me.queryFilter && !me.store.destroyed) {
            me.clearLocalFilter();
        }
        if (picker) {
            picker.bindStore(null);
        }
        me.pickerSelectionModel.destroy();
    },
    onBindStore: function(store, initial) {
        var me = this,
            picker = me.picker,
            extraKeySpec, valueCollectionConfig;
        // We're being bound, not unbound...
        if (store) {
            // If store was created from a 2 dimensional array with generated field names
            // 'field1' and 'field2'
            if (store.autoCreated) {
                me.queryMode = 'local';
                me.valueField = me.displayField = 'field1';
                if (!store.expanded) {
                    me.displayField = 'field2';
                }
                // displayTpl config will need regenerating with the autogenerated displayField
                // name 'field1'
                if (me.getDisplayTpl().auto) {
                    me.setDisplayTpl(null);
                }
            }
            if (!Ext.isDefined(me.valueField)) {
                me.valueField = me.displayField;
            }
            // Add a byValue index to the store so that we can efficiently look up records
            // by the value field when setValue passes string value(s).
            // The two indices (Ext.util.CollectionKeys) are configured unique: false,
            // so that if duplicate keys are found, they are all returned by the get call.
            // This is so that findByText and findByValue are able to return the *FIRST*
            // matching value. By default, if unique is true, CollectionKey keeps the *last*
            // matching value.
            extraKeySpec = {
                byValue: {
                    rootProperty: 'data',
                    unique: false
                }
            };
            extraKeySpec.byValue.property = me.valueField;
            store.setExtraKeys(extraKeySpec);
            if (me.displayField === me.valueField) {
                store.byText = store.byValue;
            } else {
                extraKeySpec.byText = {
                    rootProperty: 'data',
                    unique: false
                };
                extraKeySpec.byText.property = me.displayField;
                store.setExtraKeys(extraKeySpec);
            }
            // We hold a collection of the values which have been selected, keyed by this field's
            // valueField. This collection also functions as the selected items collection
            // for the BoundList's selection model
            valueCollectionConfig = {
                rootProperty: 'data',
                extraKeys: {
                    byInternalId: {
                        property: 'internalId'
                    },
                    byValue: {
                        property: me.valueField,
                        rootProperty: 'data'
                    }
                },
                // Whenever this collection is changed by anyone, whether by this field
                // adding to it, or the BoundList operating, we must refresh our value.
                listeners: {
                    beginupdate: me.onValueCollectionBeginUpdate,
                    endupdate: me.onValueCollectionEndUpdate,
                    scope: me
                }
            };
            // This becomes our collection of selected records for the Field.
            me.valueCollection = new Ext.util.Collection(valueCollectionConfig);
            // This is the selection model we configure into the dropdown BoundList.
            // We use the selected Collection as our value collection and the basis
            // for rendering the tag list.
            me.pickerSelectionModel = new Ext.selection.DataViewModel({
                mode: me.multiSelect ? 'SIMPLE' : 'SINGLE',
                // There are situations when a row is selected on mousedown but then the mouse
                // is dragged to another row and released.  In these situations, the event target
                // for the click event won't be the row where the mouse was released
                // but the boundview. The view will then determine that it should fire a container
                // click, and the DataViewModel will then deselect all prior selections.
                // Setting `deselectOnContainerClick` here will prevent the model from deselecting.
                ordered: true,
                deselectOnContainerClick: false,
                enableInitialSelection: false,
                pruneRemoved: false,
                selected: me.valueCollection,
                store: store,
                listeners: {
                    scope: me,
                    lastselectedchanged: me.updateBindSelection
                }
            });
            if (!initial) {
                me.resetToDefault();
            }
            if (picker) {
                me.pickerSelectionModel.on({
                    scope: me,
                    beforeselect: me.onBeforeSelect,
                    beforedeselect: me.onBeforeDeselect
                });
                picker.setSelectionModel(me.pickerSelectionModel);
                if (picker.getStore() !== store) {
                    picker.bindStore(store);
                }
            }
        }
    },
    /**
     * Binds a store to this instance.
     * @param {Ext.data.AbstractStore/String} [store] The store to bind or ID of the store.
     * When no store given (or when `null` or `undefined` passed), unbinds the existing store.
     * @param {Boolean} [preventFilter] `true` to prevent any active filter from being activated
     * on the newly bound store. This is only valid when used with {@link #queryMode} `'local'`.
     * @param {Boolean} initial (private)
     */
    bindStore: function(store, preventFilter, initial) {
        var me = this,
            filter = me.queryFilter;
        me.mixins.storeholder.bindStore.call(me, store, initial);
        store = me.getStore();
        if (store && filter && !preventFilter) {
            store.getFilters().add(filter);
        }
        if (!initial && store && !store.isEmptyStore) {
            me.setValueOnData();
        }
    },
    getStoreListeners: function(store) {
        var me = this,
            result;
        // Don't bother with listeners on the dummy store that is provided for an unconfigured
        // ComboBox prior to a real store arriving from a ViewModel.
        // Nothing is ever going to be fired.
        if (!store.isEmptyStore) {
            result = {
                datachanged: me.onDataChanged,
                load: me.onLoad,
                exception: me.onException,
                update: me.onStoreUpdate,
                remove: me.checkValueOnChange
            };
            // If we are doing remote filtering, then mutating the store's filters should not
            // result in a re-evaluation of whether the current value is still present in the store.
            if (!store.getRemoteFilter()) {
                result.filterchange = me.checkValueOnChange;
            }
            return result;
        }
    },
    onDataChanged: function() {
        if (this.grow && this.growToLongestValue) {
            this.autoSize();
        }
    },
    checkValueOnChange: function() {
        var me = this;
        // Will be triggered by removal of filters upon destroy
        if (!me.destroying && me.getStore().isLoaded()) {
            // If multiselecting and the base store is modified, we may have to remove records
            // from the valueCollection if they have gone from the base store, or update
            // the rawValue if selected records are mutated.
            // TODO: 5.1.1: Use a ChainedStore for multiSelect so that selected records are not
            // filtered out of the base store and are able to be removed.
            // See https://sencha.jira.com/browse/EXTJS-16096
            if (me.multiSelect) {} else // TODO: Implement in 5.1.1 when selected records are available for modification
            // and not filtered out. valueCollection must be in sync with what's available
            // in the base store, and rendered rawValue/tags must match any updated data.
            {
                if (me.forceSelection && !me.changingFilters && !me.findRecordByValue(me.value)) {
                    // skip this if query mode is remote and the user is typing
                    // or is executing a page load
                    if (me.queryMode !== 'local' && (me.hasFocus || me.isPaging)) {
                        return;
                    }
                    me.setValue(null);
                }
            }
        }
    },
    onStoreUpdate: function(store, record) {
        // Ensure the rawValue is rendered correctly whenever a store record is mutated
        this.updateValue();
    },
    onException: function() {
        this.isPaging = false;
        this.collapse();
    },
    onLoad: function(store, records, success) {
        var me = this,
            // This flag is saying that we need to call setValue to match the value property
            // with the just loaded record set and update the valueCollection
            // (and thereby any bound ViewModel) with that matched record.
            needsValueUpdating = !me.valueCollection.byValue.get(me.value);
        // If not returning from a query, and the value was set from a raw data value,
        // unrelated to a record because the displayField was not honoured when calculating
        // the raw value, then we update the raw value.
        if (success && needsValueUpdating && !me.isPaging && !(store.lastOptions && 'rawQuery' in store.lastOptions)) {
            me.setValueOnData();
        }
        // This synchronizes the value based upon contents of the store
        me.checkValueOnChange();
        me.isPaging = false;
    },
    setValueOnData: function() {
        var me = this;
        me.setValue(me.value);
        // Highlight the selected record
        if (me.isExpanded && me.getStore().getCount()) {
            me.doAutoSelect();
        }
    },
    /**
     * @private
     * Execute the query with the raw contents within the textfield.
     */
    doRawQuery: function() {
        var me = this,
            rawValue = me.inputEl.dom.value;
        // Use final bit after comma as query value if multiselecting
        if (me.multiSelect) {
            rawValue = rawValue.split(me.delimiter).pop();
        }
        me.doQuery(rawValue, false, true);
    },
    /**
     * Executes a query to filter the dropdown list. Fires the {@link #beforequery} event
     * prior to performing the query allowing the query action to be canceled if needed.
     *
     * @param {String} queryString The string to use to filter available items by matching
     * against the configured {@link #valueField}.
     * @param {Boolean} [forceAll=false] `true` to force the query to execute even if there are
     * currently fewer characters in the field than the minimum specified by the
     * `{@link #minChars}` config option. It also clears any filter previously saved in the current
     * store.
     * @param {Boolean} [rawQuery=false] Pass as true if the raw typed value is being used
     * as the query string. This causes the resulting store load to leave the raw value
     * undisturbed.
     * @return {Boolean} true if the query was permitted to run, false if it was cancelled
     * by a {@link #beforequery} handler.
     */
    doQuery: function(queryString, forceAll, rawQuery) {
        var me = this,
            store = me.getStore(),
            filters = store.getFilters(),
            // Decide if, and how we are going to query the store
            queryPlan = me.beforeQuery({
                lastQuery: me.lastQuery || '',
                query: queryString || '',
                rawQuery: rawQuery,
                forceAll: forceAll,
                combo: me,
                cancel: false
            }),
            refreshFilters;
        // Allow veto.
        if (queryPlan !== false && !queryPlan.cancel) {
            // if we have a queryString and we don't have a queryFilter or the queryFilter
            // has changed since the last query, we should run a query.
            refreshFilters = !!queryString && (!me.queryFilter || me.queryFilter && (filters.indexOf(me.queryFilter) < 0));
            // If they're using the same value as last time (and not being asked to query all), 
            // and the filters don't need to be refreshed, just show the dropdown
            if (me.queryCaching && !refreshFilters && queryPlan.query === me.lastQuery) {
                // The filter changing was done with events suppressed, so
                // refresh the picker DOM while hidden and it will layout on show.
                me.getPicker().refresh();
                me.expand();
                me.afterQuery(queryPlan);
            } else // Otherwise filter or load the store
            {
                me.lastQuery = queryPlan.query;
                if (me.queryMode === 'local') {
                    me.doLocalQuery(queryPlan);
                } else {
                    me.doRemoteQuery(queryPlan);
                }
            }
            return true;
        } else // If the query was vetoed we still need to check the change
        // in case custom validators are used
        {
            me.startCheckChangeTask();
        }
        return false;
    },
    /**
     * @template
     * A method which may modify aspects of how the store is to be filtered (if {@link #queryMode}
     * is `"local"`) of loaded (if {@link #queryMode} is `"remote"`).
     *
     * This is called by the {@link #doQuery} method, and may be overridden in subclasses to modify
     * the default behaviour.
     *
     * This method is passed an object containing information about the upcoming query operation
     * which it may modify before returning.
     *
     * @param {Object} queryPlan An object containing details about the query to be executed.
     * @param {String} queryPlan.query The query value to be used to match against the ComboBox's
     * {@link #valueField}.
     * @param {String} queryPlan.lastQuery The query value used the last time a store query
     * was made.
     * @param {Boolean} queryPlan.forceAll If `true`, causes the query to be executed even if
     * the minChars threshold is not met.
     * @param {Boolean} queryPlan.cancel A boolean value which, if set to `true` upon return,
     * causes the query not to be executed.
     * @param {Boolean} queryPlan.rawQuery If `true` indicates that the raw input field value
     * is being used, and upon store load, the input field value should **not** be overwritten.
     *
     */
    beforeQuery: function(queryPlan) {
        var me = this;
        // Allow beforequery event to veto by returning false
        if (me.fireEvent('beforequery', queryPlan) === false) {
            queryPlan.cancel = true;
        }
        // Allow beforequery event to veto by returning setting the cancel flag
        else if (!queryPlan.cancel) {
            // If the minChars threshold has not been met, and we're not forcing an "all" query,
            // cancel the query
            if (queryPlan.query.length < me.minChars && !queryPlan.forceAll) {
                queryPlan.cancel = true;
            }
        }
        return queryPlan;
    },
    doLocalQuery: function(queryPlan) {
        var me = this,
            queryString = queryPlan.query,
            store = me.getStore(),
            value = queryString,
            filter;
        me.clearLocalFilter();
        // Querying by a string...
        if (queryString) {
            // User can be typing a regex in here, if it's invalid
            // just swallow the exception and move on
            if (me.enableRegEx) {
                try {
                    value = new RegExp(value);
                } catch (e) {
                    value = null;
                }
            }
            if (value !== null) {
                // Must set changingFilters flag for this.checkValueOnChange.
                // the suppressEvents flag does not affect the filterchange event
                me.changingFilters = true;
                filter = me.queryFilter = new Ext.util.Filter({
                    id: me.id + '-filter',
                    anyMatch: me.anyMatch,
                    caseSensitive: me.caseSensitive,
                    root: 'data',
                    property: me.displayField,
                    value: value
                });
                store.addFilter(filter, true);
                me.changingFilters = false;
            }
        }
        // Expand after adjusting the filter if there are records or if emptyText is configured.
        if (me.store.getCount() || me.getPicker().emptyText) {
            // The filter changing was done with events suppressed, so
            // refresh the picker DOM while hidden and it will layout on show.
            me.getPicker().refresh();
            me.expand();
        } else {
            me.collapse();
        }
        me.afterQuery(queryPlan);
    },
    doRemoteQuery: function(queryPlan) {
        var me = this,
            loadCallback = function() {
                if (!me.destroyed) {
                    me.afterQuery(queryPlan);
                }
            };
        // expand before loading so LoadMask can position itself correctly
        me.expand();
        // In queryMode: 'remote', we assume Store filters are added by the developer
        // as remote filters, and these are automatically passed as params with every load call,
        // so we do *not* call clearFilter.
        if (me.pageSize) {
            // if we're paging, we've changed the query so start at page 1.
            me.loadPage(1, {
                rawQuery: queryPlan.rawQuery,
                callback: loadCallback
            });
        } else {
            me.store.load({
                params: me.getParams(queryPlan.query),
                rawQuery: queryPlan.rawQuery,
                callback: loadCallback
            });
        }
    },
    /**
     * @template
     * A method called when the filtering caused by the {@link #doQuery} call is complete
     * and the store has been either filtered locally (if {@link #queryMode} is `"local"`),
     * or has been loaded using the specified filtering.
     *
     * @param {Object} queryPlan An object containing details about the query was executed.
     * @param {String} queryPlan.query The query value to be used to match against the ComboBox's
     * {@link #valueField}.
     * @param {Boolean} queryPlan.forceAll If `true`, causes the query to be executed even if
     * the minChars threshold is not met.
     * @param {Boolean} queryPlan.cancel A boolean value which, if set to `true` upon return,
     * causes the query not to be executed.
     * @param {Boolean} queryPlan.rawQuery If `true` indicates that the raw input field value
     * is being used, and upon store load, the input field value should **not** be overwritten.
     */
    afterQuery: function(queryPlan) {
        var me = this;
        if (me.store.getCount()) {
            if (me.typeAhead) {
                me.doTypeAhead(queryPlan);
            }
            if (queryPlan.rawQuery) {
                if (me.picker && !me.picker.getSelectionModel().hasSelection()) {
                    me.doAutoSelect();
                }
            } else {
                me.doAutoSelect();
            }
        }
        // doQuery is called upon field mutation, so check for change
        // after the query has done its thing
        me.startCheckChangeTask();
    },
    loadPage: function(pageNum, options) {
        this.isPaging = true;
        this.store.loadPage(pageNum, Ext.apply({
            params: this.getParams(this.lastQuery)
        }, options));
    },
    onPageChange: function(toolbar, newPage) {
        /*
         * Return false here so we can call load ourselves and inject the query param.
         * We don't want to do this for every store load since the developer may load
         * the store through some other means so we won't add the query param.
         */
        this.loadPage(newPage);
        return false;
    },
    /**
     * @private
     */
    getParams: function(queryString) {
        var params = {},
            param = this.queryParam;
        if (param) {
            params[param] = queryString;
        }
        return params;
    },
    /**
     * @private
     * If the autoSelect config is true, and the picker is open, highlights the first item.
     */
    doAutoSelect: function() {
        var me = this,
            store = me.store,
            picker = me.picker,
            itemNode = 0,
            selectionModel, lastSelected;
        if (picker && me.autoSelect && store.getCount() > 0) {
            if (me.autoSelectLast) {
                selectionModel = picker.getSelectionModel();
                lastSelected = selectionModel.lastSelected;
                // Highlight the last selected item and scroll it into view,
                // but only if it wasn't filtered out
                if (lastSelected && selectionModel.selected.length && store.indexOf(lastSelected) > -1) {
                    itemNode = lastSelected;
                }
            }
            picker.getNavigationModel().setPosition(itemNode);
        }
    },
    doTypeAhead: function(queryPlan) {
        var me = this;
        if (!me.typeAheadTask) {
            me.typeAheadTask = new Ext.util.DelayedTask(me.onTypeAhead, me);
        }
        // Only typeahead when user extends the query string, or it's a completely different query
        // If user is erasing, re-extending with typeahead is not wanted.
        if (queryPlan.query.length > queryPlan.lastQuery.length || !Ext.String.startsWith(queryPlan.lastQuery, queryPlan.query)) {
            me.typeAheadTask.delay(me.typeAheadDelay);
        }
    },
    onTriggerClick: function(comboBox, trigger, e) {
        var me = this,
            oldAutoSelect;
        if (!me.readOnly && !me.disabled) {
            if (me.isExpanded) {
                me.collapse();
            } else {
                // Alt-Down arrow opens the picker but does not select items:
                // http://www.w3.org/TR/wai-aria-practices/#combobox
                if (e && e.type === 'keydown' && e.altKey) {
                    oldAutoSelect = me.autoSelect;
                    me.autoSelect = false;
                    me.expand();
                    me.autoSelect = oldAutoSelect;
                } else {
                    if (me.triggerAction === 'all') {
                        me.doQuery(me.allQuery, true);
                    } else if (me.triggerAction === 'last') {
                        me.doQuery(me.lastQuery, true);
                    } else {
                        me.doQuery(me.getRawValue(), false, true);
                    }
                }
            }
        }
    },
    onFieldMutation: function(e) {
        var me = this,
            key = e.getKey(),
            isDelete = key === e.BACKSPACE || key === e.DELETE,
            rawValue = me.inputEl.dom.value,
            len = rawValue.length;
        // Do not process two events for the same mutation.
        // For example an input event followed by the keyup that caused it.
        // We must process delete keyups.
        // Also, do not process TAB event which fires on arrival.
        if (!me.readOnly && (rawValue !== me.lastMutatedValue || isDelete) && key !== e.TAB) {
            me.lastMutatedValue = rawValue;
            me.refreshEmptyText();
            if (len && (e.type !== 'keyup' || (!e.isSpecialKey() || isDelete))) {
                me.doQueryTask.delay(me.queryDelay);
            } else {
                // We have *erased* back to empty if key is a delete,
                // or it is a non-key event (cut/copy)
                if (!len && (!key || isDelete)) {
                    // This portion of code may end up calling setValue will check for change.
                    // But since it's come from field mutations, we need to respect
                    // the checkChangeBuffer, so we suspend checks here, it will be handled
                    // by callParent
                    ++me.suspendCheckChange;
                    // Essentially a silent setValue.
                    // Clear our value, and the tplData used to construct a mathing raw value.
                    if (!me.multiSelect) {
                        me.value = null;
                        me.displayTplData = undefined;
                    }
                    // If the value is blank we can't have a value
                    if (me.clearValueOnEmpty) {
                        me.valueCollection.beginUpdate();
                        me.pickerSelectionModel.deselectAll();
                        me.valueCollection.removeAll();
                        me.valueCollection.endUpdate();
                    }
                    // Just erased back to empty. Hide the dropdown.
                    me.collapse();
                    // There may have been a local filter if we were querying locally.
                    // Clear the query filter and suppress the consequences
                    // (we do not want a list refresh).
                    if (me.queryFilter) {
                        me.clearLocalFilter();
                    }
                    // When queryCaching if the user deletes the value and then starts typing
                    // the same filter again, doQuery can erroneously expand the picker without
                    // filtering first.
                    me.lastQuery = null;
                    --me.suspendCheckChange;
                }
                me.callParent([
                    e
                ]);
            }
        }
    },
    doDestroy: function() {
        var me = this;
        me.doQueryTask.cancel();
        if (me.typeAheadTask) {
            me.typeAheadTask.cancel();
            me.typeAheadTask = null;
        }
        me.bindStore(null);
        Ext.destroy(me.altArrowKeyNav, me.valueCollection);
        me.callParent();
    },
    // The picker (the dropdown) must have its zIndex managed by the same ZIndexManager which is
    // providing the zIndex of our Container.
    onAdded: function() {
        var me = this;
        me.callParent(arguments);
        if (me.picker) {
            me.picker.ownerCt = me.up('[floating]');
            me.picker.registerWithOwnerCt();
        }
    },
    createPicker: function() {
        var me = this,
            picker,
            pickerCfg = Ext.apply({
                xtype: 'boundlist',
                id: me.id + '-picker',
                pickerField: me,
                selectionModel: me.pickerSelectionModel,
                floating: true,
                hidden: true,
                store: me.getPickerStore(),
                displayField: me.displayField,
                preserveScrollOnRefresh: true,
                pageSize: me.pageSize,
                tpl: me.tpl,
                ariaSelectable: me.ariaSelectable
            }, me.listConfig, me.defaultListConfig);
        picker = me.picker = Ext.widget(pickerCfg);
        if (me.pageSize) {
            picker.pagingToolbar.on('beforechange', me.onPageChange, me);
        }
        // We limit the height of the picker to fit in the space above
        // or below this field unless the picker has its own ideas about that.
        if (!picker.initialConfig.maxHeight) {
            picker.on({
                beforeshow: me.onBeforePickerShow,
                scope: me
            });
        }
        picker.getSelectionModel().on({
            beforeselect: me.onBeforeSelect,
            beforedeselect: me.onBeforeDeselect,
            focuschange: me.onFocusChange,
            scope: me
        });
        picker.getNavigationModel().navigateOnSpace = false;
        return picker;
    },
    getPickerStore: function() {
        return this.store;
    },
    onBeforePickerShow: function(picker) {
        // Just before we show the picker, set its maxHeight so it fits
        // either above or below, it will flip to the side where it fits
        var me = this,
            heightAbove = me.getPosition()[1] - Ext.getBody().getScroll().top,
            heightBelow = Ext.Element.getViewportHeight() - heightAbove - me.getHeight();
        // Then ensure that vertically, the dropdown will fit into the space
        // either above or below the inputEl.
        // Also have some leeway so we aren't flush against the window edge
        picker.maxHeight = Math.max(heightAbove, heightBelow) - 5;
    },
    onBeforeSelect: function(list, record, recordIndex) {
        return this.fireEvent('beforeselect', this, record, recordIndex);
    },
    onBeforeDeselect: function(list, record, recordIndex) {
        return this.fireEvent('beforedeselect', this, record, recordIndex);
    },
    onFocusChange: function(selModel, prevRecord, newRecord) {
        var picker = this.picker,
            inputEl = this.inputEl,
            el;
        if (newRecord) {
            el = picker.getNodeByRecord(newRecord);
            if (el) {
                // Need to make sure el has an id without leaving Ext.cache entry
                if (!el.id) {
                    el.id = Ext.id();
                }
                inputEl.dom.setAttribute('aria-activedescendant', el.id);
            } else {
                inputEl.dom.removeAttribute('aria-activedescendant');
            }
        }
    },
    /**
     * Returns the combobox's selection.
     * @return {Ext.data.Model} The selected record
     */
    getSelection: function() {
        var selModel = this.getPicker().getSelectionModel(),
            selection = selModel.getSelection();
        return selection.length ? selModel.getLastSelected() : null;
    },
    updateSelection: function(selection) {
        var me = this,
            sm;
        if (!me.ignoreNextSelection) {
            me.ignoreNextSelection = true;
            sm = me.getPicker().getSelectionModel();
            if (selection) {
                sm.select(selection);
                me.hasHadSelection = true;
            } else {
                sm.deselectAll();
            }
            me.ignoreNextSelection = false;
        }
    },
    updateBindSelection: function(selModel, selection) {
        var me = this,
            selected = null;
        if (!me.ignoreNextSelection) {
            me.ignoreNextSelection = true;
            if (selection.length) {
                selected = selModel.getLastSelected();
                me.hasHadSelection = true;
            }
            if (me.hasHadSelection) {
                me.setSelection(selected);
            }
            me.ignoreNextSelection = false;
        }
    },
    onValueCollectionBeginUpdate: Ext.emptyFn,
    onValueCollectionEndUpdate: function() {
        var me = this,
            store = me.store,
            selectedRecords = me.valueCollection.getRange(),
            selectedRecord = selectedRecords[0],
            selectionCount = selectedRecords.length;
        me.updateBindSelection(me.pickerSelectionModel, selectedRecords);
        if (me.isSelectionUpdating()) {
            return;
        }
        Ext.suspendLayouts();
        me.lastSelection = selectedRecords;
        if (selectionCount) {
            // Track the last selection with a value (non blank) for use in
            // assertValue
            me.lastSelectedRecords = selectedRecords;
        }
        me.updateValue();
        // If we have selected a value, and it's not possible to select any more values
        // or, we are configured to hide the picker each time, then collapse the picker.
        // eslint-disable-next-line max-len
        if (selectionCount && ((!me.multiSelect && store.contains(selectedRecord)) || me.collapseOnSelect || !store.getCount())) {
            me.updatingValue = true;
            me.collapse();
            me.updatingValue = false;
        }
        Ext.resumeLayouts(true);
        if (!me.suspendCheckChange) {
            if (!me.multiSelect) {
                selectedRecords = selectedRecord;
            }
            me.fireEvent('select', me, selectedRecords);
        }
    },
    isSelectionUpdating: function() {
        var selModel = this.pickerSelectionModel;
        return selModel.deselectingDuringSelect || selModel.refreshing;
    },
    /**
     * @private
     * Enables the key navs for the BoundList when it is expanded.
     */
    onExpand: function() {
        var me = this,
            picker = me.getPicker(),
            keyNav = picker.getNavigationModel(),
            node;
        if (keyNav) {
            keyNav.enable();
        }
        me.doAutoSelect();
        node = picker.highlightedItem;
        if (node) {
            // Need to make sure node has an id without leaving Ext.cache entry
            if (!node.id) {
                node.id = Ext.id();
            }
            me.inputEl.dom.setAttribute('aria-activedescendant', node.id);
        }
    },
    /**
     * @private
     * Disables the key navs for the BoundList when it is collapsed.
     */
    onCollapse: function() {
        var me = this,
            keyNav = me.getPicker().getNavigationModel();
        if (keyNav) {
            keyNav.disable();
        }
        if (me.updatingValue) {
            me.doQueryTask.cancel();
        }
        me.inputEl.dom.removeAttribute('aria-activedescendant');
    },
    /**
     * Selects an item by a {@link Ext.data.Model Model}, or by a key value.
     * @param {Object} r
     * @param {Boolean} assert (private)
     */
    select: function(r, assert) {
        var me = this,
            picker = me.picker,
            fireSelect;
        if (r && r.isModel && assert === true && picker) {
            fireSelect = !picker.getSelectionModel().isSelected(r);
        }
        if (!fireSelect) {
            me.suspendEvent('select');
        }
        me.setValue(r);
        me.resumeEvent('select');
    },
    /**
     * Finds the record by searching for a specific field/value combination.
     * @param {String} field The name of the field to test.
     * @param {Object} value The value to match the field against.
     * @return {Ext.data.Model} The matched record or false.
     */
    findRecord: function(field, value) {
        var ds = this.store,
            idx = ds.findExact(field, value);
        return idx !== -1 ? ds.getAt(idx) : false;
    },
    getSelectedRecord: function() {
        return this.findRecordByValue(this.value) || null;
    },
    /**
     * Finds the record by searching values in the {@link #valueField}.
     * @param {Object} value The value to match the field against.
     * @return {Ext.data.Model} The matched record or `false`.
     */
    findRecordByValue: function(value) {
        var result = this.store.byValue.get(value),
            ret = false;
        // If there are duplicate keys, tested behaviour is to return the *first* match.
        if (result) {
            ret = result[0] || result;
        }
        return ret;
    },
    /**
     * Finds the record by searching values in the {@link #displayField}.
     * @param {Object} value The value to match the field against.
     * @return {Ext.data.Model} The matched record or `false`.
     */
    findRecordByDisplay: function(value) {
        var result = this.store.byText.get(value),
            ret = false;
        // If there are duplicate keys, tested behaviour is to return the *first* match.
        if (result) {
            ret = result[0] || result;
        }
        return ret;
    },
    /**
     * Adds a value or values to the current value of the field
     * @param {Mixed} value The value or values to add to the current value, see {@link #setValue}
     */
    addValue: function(value) {
        if (value != null) {
            return this.doSetValue(value, true);
        }
    },
    /**
     * Sets the specified value(s) into the field. For each value, if a record is found
     * in the {@link #store} that matches based on the {@link #valueField}, then that record's
     * {@link #displayField} will be displayed in the field. If no match is found, and the
     * {@link #valueNotFoundText} config option is defined, then that will be displayed
     * as the default field text. Otherwise a blank value will be shown, although the value
     * will still be set.
     * @param {String/String[]} value The value(s) to be set. Can be either a single String
     * or {@link Ext.data.Model}, or an Array of Strings or Models.
     * @return {Ext.form.field.Field} this
     */
    setValue: function(value) {
        var me = this,
            bind, valueBind;
        if (!me.getDisplayField() && !me.getDisplayTpl()) {
            Ext.error.raise('Combobox must have either displayField or displayTpl');
        }
        // Here we check if the setValue is being called by bind getting synced
        // if this is the case while the field has focus. If this is the case, we
        // don't want to change the field value.
        if (me.hasFocus) {
            bind = me.getBind();
            valueBind = bind && bind.value;
            if (valueBind && valueBind.syncing) {
                if ((Ext.isEmpty(value) && Ext.isEmpty(me.value)) || value === me.value) {
                    return me;
                } else if (Ext.isArray(value) && Ext.isArray(me.value) && Ext.Array.equals(value, me.value)) {
                    return me;
                }
            }
        } else {
            // This is the value used to forceSelection in assertValue if 
            // an invalid value is left in the field at completeEdit. Must be cleared so 
            // that the next usage of the field is not affected, but only if we are setting
            // a new value.
            me.lastSelectedRecords = null;
        }
        if (value != null) {
            me.doSetValue(value);
        } else // Clearing is a special, simpler case.
        {
            me.suspendEvent('select');
            me.valueCollection.beginUpdate();
            me.pickerSelectionModel.deselectAll();
            me.valueCollection.endUpdate();
            me.resumeEvent('select');
        }
        return me;
    },
    setRawValue: function(rawValue) {
        this.callParent([
            rawValue
        ]);
        this.lastMutatedValue = rawValue;
    },
    /**
     * @private
     * Sets or adds a value/values
     */
    doSetValue: function(value, /* private for use by addValue */
    add) {
        var me = this,
            store = me.getStore(),
            Model = store.getModel(),
            matchedRecords = [],
            valueArray = [],
            autoLoadOnValue = me.autoLoadOnValue,
            isLoaded = store.getCount() > 0 || store.isLoaded(),
            pendingLoad = store.hasPendingLoad(),
            unloaded = autoLoadOnValue && !isLoaded && !pendingLoad,
            forceSelection = me.forceSelection,
            selModel = me.pickerSelectionModel,
            displayField = me.displayField,
            valueField = me.valueField,
            displayIsValue = displayField === valueField,
            isEmptyStore = store.isEmptyStore,
            lastSelection = me.lastSelection,
            i, len, record, dataObj, valueChanged, key, val;
        if (add && !me.multiSelect) {
            Ext.raise('Cannot add values to non multiSelect ComboBox');
        }
        // Called while the Store is loading or we don't have the real store bound yet.
        // Ensure it is processed by the onLoad/bindStore.
        // Even if displayField === valueField, we still MUST kick off a load because even though
        // the value may be correct as the raw value, we must still load the store, and
        // upon load, match the value and select a record sop we can publish the *selection* to
        // a ViewModel.
        if (pendingLoad || unloaded || !isLoaded || isEmptyStore) {
            // If they are setting the value to a record instance, we can
            // just add it to the valueCollection and continue with the setValue.
            // We MUST do this before kicking off the load in case the load is synchronous;
            // this.value must be available to the onLoad handler.
            if (!value.isModel) {
                if (add) {
                    me.value = Ext.Array.from(me.value).concat(value);
                } else {
                    me.value = value;
                }
                me.setHiddenValue(me.value);
                // If we know that the display value is the same as the value, then show it.
                // A store load is still scheduled so that the matching record can be published.
                me.setRawValue(displayIsValue ? value : '');
                // if display is value, let's remove the empty text since the store
                // might not be loaded yet
                if (displayIsValue && !Ext.isEmpty(value) && me.inputEl && me.emptyText) {
                    me.inputEl.removeCls(me.emptyUICls);
                }
            }
            // Kick off a load. Doesn't matter whether proxy is remote - it needs loading
            // so we can select the correct record for the value.
            //
            // Must do this *after* setting the value above in case the store loads synchronously
            // and fires the load event, and therefore calls onLoad inline.
            //
            // If it is still the default empty store, then the real store must be arriving
            // in a tick through binding. bindStore will call setValueOnData.
            if (unloaded && !isEmptyStore) {
                store.load();
            }
            // If they had set a string value, another setValue call is scheduled
            // in the onLoad handler. If the store is the defauilt empty one, the setValueOnData
            // call will be made in bindStore when the real store arrives.
            if (!value.isModel || isEmptyStore) {
                return me;
            }
        }
        // This method processes multi-values, so ensure value is an array.
        value = add ? Ext.Array.from(me.value).concat(value) : Ext.Array.from(value);
        // Loop through values, matching each from the Store, and collecting matched records
        for (i = 0 , len = value.length; i < len; i++) {
            record = val = value[i];
            // Set value was a key, look up in the store by that key
            if (!record || !record.isModel) {
                record = me.findRecordByValue(key = record);
                // The value might be in a new record created from an unknown value 
                // (if !me.forceSelection).
                // Or it could be a picked record which is filtered out of the main store.
                // Or it could be a setValue(record) passed to an empty store with autoLoadOnValue
                // and added above.
                if (!record) {
                    record = me.valueCollection.find(valueField, key);
                }
            }
            // record was not found, this could happen because
            // store is not loaded or they set a value not in the store
            if (!record) {
                // If we are allowing insertion of values not represented in the Store,
                // then push the value and create a new record to push as a display value
                // for use by the displayTpl
                if (!forceSelection) {
                    // We are allowing added values to create their own records.
                    // Only if the value is not empty.
                    if (!record && val) {
                        dataObj = {};
                        if (Ext.isObject(val)) {
                            dataObj[displayField] = val[displayField];
                            dataObj[valueField] = val[valueField];
                        } else {
                            dataObj[displayField] = val;
                            if (valueField && displayField !== valueField) {
                                dataObj[valueField] = val;
                            }
                        }
                        record = new Model(dataObj);
                    }
                }
                // Else, if valueNotFoundText is defined, display it,
                // otherwise display nothing for this value
                else if (me.valueNotFoundRecord) {
                    record = me.valueNotFoundRecord;
                }
            }
            // record found, select it.
            if (record) {
                matchedRecords.push(record);
                valueArray.push(record.get(valueField));
            }
        }
        // If the same set of records are selected, this setValue has been a no-op
        if (lastSelection) {
            len = lastSelection.length;
            if (len === matchedRecords.length) {
                for (i = 0; !valueChanged && i < len; i++) {
                    if (Ext.Array.indexOf(me.lastSelection, matchedRecords[i]) === -1) {
                        valueChanged = true;
                    }
                }
            } else {
                valueChanged = true;
            }
        } else {
            valueChanged = matchedRecords.length;
        }
        if (valueChanged) {
            // beginUpdate which means we only want to notify this.onValueCollectionEndUpdate
            // after it's all changed.
            me.suspendEvent('select');
            me.valueCollection.beginUpdate();
            if (matchedRecords.length) {
                selModel.select(matchedRecords, false);
            } else {
                selModel.deselectAll();
            }
            me.valueCollection.endUpdate();
            me.resumeEvent('select');
        } else {
            me.updateValue();
        }
        return me;
    },
    /**
     * @private
     * Internal setting of value when records are added to the valueCollection
     * setValue itself adds to the valueCollection.
     */
    updateValue: function() {
        var me = this,
            selectedRecords = me.valueCollection.getRange(),
            len = selectedRecords.length,
            valueArray = [],
            displayTplData = me.displayTplData || (me.displayTplData = []),
            inputEl = me.inputEl,
            i, record, displayValue;
        // Loop through values, matching each from the Store, and collecting matched records
        displayTplData.length = 0;
        for (i = 0; i < len; i++) {
            record = selectedRecords[i];
            displayTplData.push(me.getRecordDisplayData(record));
            // There might be the bogus "value not found" record if forceSelect was set.
            // Do not include this in the value.
            if (record !== me.valueNotFoundRecord) {
                valueArray.push(record.get(me.valueField));
            }
        }
        // Set the value of this field. If we are multiselecting, then that is an array.
        me.setHiddenValue(valueArray);
        me.value = me.multiSelect ? valueArray : valueArray[0];
        if (!Ext.isDefined(me.value)) {
            me.value = undefined;
        }
        me.displayTplData = displayTplData;
        // store for getDisplayValue method
        displayValue = me.getDisplayValue();
        // Calculate raw value from the collection of Model data
        me.setRawValue(displayValue);
        me.refreshEmptyText();
        me.checkChange();
        if (!me.lastSelectedRecords && selectedRecords.length) {
            me.lastSelectedRecords = selectedRecords;
        }
        if (inputEl && me.typeAhead && me.hasFocus) {
            // if typeahead is configured, deselect any partials
            me.selectText(displayValue.length);
        }
    },
    /**
     * @private
     * Set the value of {@link #hiddenDataEl}
     * Dynamically adds and removes input[type=hidden] elements
     */
    setHiddenValue: function(values) {
        var me = this,
            name = me.hiddenName,
            i, dom, childNodes, input, valueCount, childrenCount;
        if (!me.hiddenDataEl || !name) {
            return;
        }
        values = Ext.Array.from(values);
        dom = me.hiddenDataEl.dom;
        childNodes = dom.childNodes;
        input = childNodes[0];
        valueCount = values.length;
        childrenCount = childNodes.length;
        if (!input && valueCount > 0) {
            me.hiddenDataEl.setHtml(Ext.DomHelper.markup({
                tag: 'input',
                type: 'hidden',
                name: name
            }));
            childrenCount = 1;
            input = dom.firstChild;
        }
        while (childrenCount > valueCount) {
            dom.removeChild(childNodes[0]);
            --childrenCount;
        }
        while (childrenCount < valueCount) {
            dom.appendChild(input.cloneNode(true));
            ++childrenCount;
        }
        for (i = 0; i < valueCount; i++) {
            childNodes[i].value = values[i];
        }
    },
    /**
     * @private
     * Generates the string value to be displayed in the text field for the currently stored value
     */
    getDisplayValue: function(tplData) {
        var s;
        tplData = tplData || this.displayTplData;
        s = this.getDisplayTpl().apply(tplData);
        s = (s == null) ? '' : String(s);
        // The display field may have newlines characters, but the raw value in
        // the field will not because they will be automatically stripped, so do
        // the same here for the sake of comparison.
        return s.replace(this.newlineRe, '');
    },
    /**
     * Gets data for each record to be used for constructing the display value with
     * the {@link #displayTpl}. This may be overridden to provide access to associated records.
     * @param {Ext.data.Model} record The record.
     * @return {Object} The data to be passed for each record to the {@link #displayTpl}.
     *
     * @protected
     */
    getRecordDisplayData: function(record) {
        return record.data;
    },
    getValue: function() {
        // If the user has not changed the raw field value since a value was selected from the list,
        // then return the structured value from the selection. If the raw field value is different
        // than what would be displayed due to selection, return that raw value.
        var me = this,
            store = me.getStore(),
            picker = me.picker,
            rawValue = me.getRawValue(),
            // current value of text field
            value = me.value;
        // stored value from last selection or setValue() call
        // getValue may be called from initValue before a valid store is bound -
        // may still be the default empty one.
        // Also, may be called before the store has been loaded.
        // In these cases, just return the value.
        // In other cases, check that the rawValue matches the selected records.
        if (!store.isEmptyStore && me.getDisplayValue() !== rawValue) {
            me.displayTplData = undefined;
            if (picker) {
                // We do not need to hear about this clearing out of the value collection,
                // so suspend events.
                me.valueCollection.suspendEvents();
                picker.getSelectionModel().deselectAll();
                me.valueCollection.resumeEvents();
                me.lastSelection = null;
            }
            // If the raw input value gets out of sync in a multiple ComboBox, then we have to
            // give up. Multiple is not designed for typing *and* displaying the comma separated
            // result of selection. Same in the case of forceSelection.
            // Unless the store is not yet loaded, which case will be handled in onLoad
            if (store.isLoaded() && (me.multiSelect || me.forceSelection)) {
                value = me.value = undefined;
            } else {
                value = me.value = rawValue;
            }
        }
        // Return null if value is undefined/null, not falsy.
        me.value = value == null ? null : value;
        return me.value;
    },
    getSubmitValue: function() {
        var value = this.getValue();
        // If the value is null/undefined, we still return an empty string. If we
        // don't, the field will never get posted to the server since nulls are ignored.
        if (Ext.isEmpty(value)) {
            value = '';
        }
        return value;
    },
    isEqual: function(v1, v2) {
        var fromArray = Ext.Array.from,
            i, len;
        v1 = fromArray(v1);
        v2 = fromArray(v2);
        len = v1.length;
        if (len !== v2.length) {
            return false;
        }
        for (i = 0; i < len; i++) {
            if (v2[i] !== v1[i]) {
                return false;
            }
        }
        return true;
    },
    /**
     * Clears any value currently set in the ComboBox.
     */
    clearValue: function() {
        this.setValue(null);
    }
});

/**
 * A month / year picker component. This class is used by the 
 * {@link Ext.picker.Date Date picker} to allow browsing and selection of year and 
 * months combinations, but may also be used as a standalone component.
 *
 *     @example
 *     Ext.create({
 *         xtype: 'monthpicker',
 *         renderTo: document.body,
 *         value: new Date(),
 *         onSelect: function() {
 *             Ext.Msg.alert('Selected', this.getValue());
 *         },
 *         listeners: {
 *             okclick: 'onSelect',
 *             monthdblclick: 'onSelect',
 *             yeardblclick: 'onSelect',
 *             cancelclick: function () {
 *                 this.setValue(new Date());
 *             }
 *         }
 *     });
 */
Ext.define('Ext.picker.Month', {
    extend: 'Ext.Component',
    alias: 'widget.monthpicker',
    alternateClassName: 'Ext.MonthPicker',
    requires: [
        'Ext.XTemplate',
        'Ext.util.ClickRepeater',
        'Ext.Date',
        'Ext.button.Button'
    ],
    isMonthPicker: true,
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: true,
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        'bodyEl',
        'prevEl',
        'nextEl',
        'monthEl',
        'yearEl',
        'buttons'
    ],
    /* eslint-disable indent, max-len */
    /**
     * @cfg renderTpl
     * @inheritdoc
     */
    renderTpl: [
        '<div id="{id}-bodyEl" data-ref="bodyEl" class="{baseCls}-body">',
        '<div id="{id}-monthEl" data-ref="monthEl" class="{baseCls}-months">',
        '<tpl for="months">',
        '<div class="{parent.baseCls}-item {parent.baseCls}-month">',
        '<a style="{parent.monthStyle}" role="button" hidefocus="on" class="{parent.baseCls}-item-inner">{.}</a>',
        '</div>',
        '</tpl>',
        '</div>',
        '<div id="{id}-yearEl" data-ref="yearEl" class="{baseCls}-years">',
        '<div class="{baseCls}-yearnav">',
        '<div class="{baseCls}-yearnav-button-ct">',
        '<a id="{id}-prevEl" data-ref="prevEl" class="{baseCls}-yearnav-button {baseCls}-yearnav-prev" hidefocus="on" role="button"></a>',
        '</div>',
        '<div class="{baseCls}-yearnav-button-ct">',
        '<a id="{id}-nextEl" data-ref="nextEl" class="{baseCls}-yearnav-button {baseCls}-yearnav-next" hidefocus="on" role="button"></a>',
        '</div>',
        '</div>',
        '<tpl for="years">',
        '<div class="{parent.baseCls}-item {parent.baseCls}-year">',
        '<a hidefocus="on" class="{parent.baseCls}-item-inner" role="button">{.}</a>',
        '</div>',
        '</tpl>',
        '</div>',
        '<div class="' + Ext.baseCSSPrefix + 'clear"></div>',
        '<tpl if="showButtons">',
        '<div id="{id}-buttons" data-ref="buttons" class="{baseCls}-buttons">{%',
        'var me=values.$comp, okBtn=me.okBtn, cancelBtn=me.cancelBtn;',
        'okBtn.ownerLayout = cancelBtn.ownerLayout = me.componentLayout;',
        'okBtn.ownerCt = cancelBtn.ownerCt = me;',
        'Ext.DomHelper.generateMarkup(okBtn.getRenderTree(), out);',
        'Ext.DomHelper.generateMarkup(cancelBtn.getRenderTree(), out);',
        '%}</div>',
        '</tpl>',
        '</div>'
    ],
    /* eslint-enable indent, max-len */
    /**
     * @cfg {String} okText
     * The text to display on the OK button.
     * @locale
     */
    okText: 'OK',
    /**
     * @cfg {String} cancelText
     * The text to display on the Cancel button.
     * @locale
     */
    cancelText: 'Cancel',
    /**
     * @cfg {String} baseCls
     * The base CSS class to apply to the picker element.
     */
    baseCls: Ext.baseCSSPrefix + 'monthpicker',
    /**
     * @cfg {Boolean} showButtons
     * True to show ok and cancel buttons below the picker.
     */
    showButtons: true,
    /**
     * @property {String} selectedCls
     * The class to be added to selected items in the picker.
     * @readonly
     */
    /**
     * @cfg {Date/Number[]} value
     * The default value to set. See {@link #setValue}
     */
    /**
     * @cfg {String} footerButtonUI
     * The {@link Ext.button.Button#ui} to use for the month picker's footer buttons.
     */
    footerButtonUI: 'default',
    measureWidth: 35,
    measureMaxHeight: 20,
    // used when attached to date picker which isnt showing buttons
    smallCls: Ext.baseCSSPrefix + 'monthpicker-small',
    /**
     * @private
     */
    totalYears: 10,
    yearOffset: 5,
    // 10 years in total, 2 per row
    monthOffset: 6,
    // 12 months, 2 per row
    /**
     * @cfg alignOnScroll
     * @inheritdoc
     */
    alignOnScroll: false,
    /**
     * @event cancelclick
     * Fires when the cancel button is pressed.
     * @param {Ext.picker.Month} this
     */
    /**
     * @event monthclick
     * Fires when a month is clicked.
     * @param {Ext.picker.Month} this
     * @param {Array} value The current value
     */
    /**
     * @event monthdblclick
     * Fires when a month is clicked.
     * @param {Ext.picker.Month} this
     * @param {Array} value The current value
     */
    /**
     * @event okclick
     * Fires when the ok button is pressed.
     * @param {Ext.picker.Month} this
     * @param {Array} value The current value
     */
    /**
     * @event select
     * Fires when a month/year is selected.
     * @param {Ext.picker.Month} this
     * @param {Array} value The current value
     */
    /**
     * @event yearclick
     * Fires when a year is clicked.
     * @param {Ext.picker.Month} this
     * @param {Array} value The current value
     */
    /**
     * @event yeardblclick
     * Fires when a year is clicked.
     * @param {Ext.picker.Month} this
     * @param {Array} value The current value
     */
    /**
     * @method initComponent
     * @inheritdoc
     * @private
     */
    initComponent: function() {
        var me = this;
        me.selectedCls = me.baseCls + '-selected';
        if (me.small) {
            me.addCls(me.smallCls);
        }
        me.setValue(me.value);
        me.activeYear = me.getYear(new Date().getFullYear() - 4, -4);
        if (me.showButtons) {
            me.okBtn = new Ext.button.Button({
                ui: me.footerButtonUI,
                text: me.okText,
                handler: me.onOkClick,
                scope: me
            });
            me.cancelBtn = new Ext.button.Button({
                ui: me.footerButtonUI,
                text: me.cancelText,
                handler: me.onCancelClick,
                scope: me
            });
        }
        me.callParent();
    },
    /**
     * @method beforeRender
     * @inheritdoc
     * @private
     */
    beforeRender: function() {
        var me = this,
            i = 0,
            months = [],
            shortName = Ext.Date.getShortMonthName,
            monthLen = me.monthOffset,
            margin = me.monthMargin,
            style = '';
        if (me.padding && !me.width) {
            me.cacheWidth();
        }
        me.callParent();
        for (; i < monthLen; ++i) {
            months.push(shortName(i), shortName(i + monthLen));
        }
        if (Ext.isDefined(margin)) {
            style = 'margin: 0 ' + margin + 'px;';
        }
        Ext.apply(me.renderData, {
            months: months,
            years: me.getYears(),
            showButtons: me.showButtons,
            monthStyle: style
        });
    },
    cacheWidth: function() {
        var me = this,
            padding = me.parseBox(me.padding),
            widthEl = Ext.getBody().createChild({
                cls: me.baseCls + ' ' + me.borderBoxCls,
                style: 'position:absolute;top:-1000px;left:-1000px;',
                html: '&nbsp;'
            });
        // required for opera 11.64 to measure a width
        me.self.prototype.width = widthEl.getWidth() + padding.left + padding.right;
        widthEl.destroy();
    },
    /**
     * @method afterRender
     * @inheritdoc
     * @private
     */
    afterRender: function() {
        var me = this,
            body = me.bodyEl;
        me.callParent();
        // Month picker is not focusable and essentially is pointer only thing.
        // Clicking on it will focus the document body, which may disrupt the state
        // of the floating parent such as Date picker or a menu, and cause it to hide.
        // To work around that, we stop the mousedown events completely.
        if (me.up('[floating=true]')) {
            me.el.on('mousedown', me.onElClick, me, {
                translate: false
            });
        }
        body.on({
            scope: me,
            click: 'onBodyClick',
            dblclick: 'onBodyClick'
        });
        // keep a reference to the year/month elements since we'll be re-using them
        me.years = body.select('.' + me.baseCls + '-year a');
        me.months = body.select('.' + me.baseCls + '-month a');
        me.backRepeater = new Ext.util.ClickRepeater(me.prevEl, {
            handler: Ext.Function.bind(me.adjustYear, me, [
                -me.totalYears
            ])
        });
        me.prevEl.addClsOnOver(me.baseCls + '-yearnav-prev-over');
        me.nextRepeater = new Ext.util.ClickRepeater(me.nextEl, {
            handler: Ext.Function.bind(me.adjustYear, me, [
                me.totalYears
            ])
        });
        me.nextEl.addClsOnOver(me.baseCls + '-yearnav-next-over');
        me.updateBody();
        if (!Ext.isDefined(me.monthMargin)) {
            Ext.picker.Month.prototype.monthMargin = me.calculateMonthMargin();
        }
    },
    calculateMonthMargin: function() {
        // We use this method for locales where the short month name
        // may be longer than we see in English. For example in the 
        // zh_TW locale the month ends up spanning lines, so we loosen
        // the margins to get some extra space
        var me = this,
            months = me.months,
            first = months.first(),
            itemMargin = first.getMargin('l');
        while (itemMargin && me.getLargest() > me.measureMaxHeight) {
            --itemMargin;
            months.setStyle('margin', '0 ' + itemMargin + 'px');
        }
        return itemMargin;
    },
    getLargest: function(months) {
        var largest = 0;
        this.months.each(function(item) {
            var h = item.getHeight();
            if (h > largest) {
                largest = h;
            }
        });
        return largest;
    },
    /**
     * Set the value for the picker.
     * @param {Date/Number[]} value The value to set. It can be a Date object,
     * where the month/year will be extracted, or it can be an array, with the month
     * as the first index and the year as the second.
     * @return {Ext.picker.Month} this
     */
    setValue: function(value) {
        var me = this,
            active = me.activeYear,
            year;
        if (!value) {
            me.value = [
                null,
                null
            ];
        } else if (Ext.isDate(value)) {
            me.value = [
                value.getMonth(),
                value.getFullYear()
            ];
        } else {
            me.value = [
                value[0],
                value[1]
            ];
        }
        if (me.rendered) {
            year = me.value[1];
            if (year !== null) {
                if ((year < active || year > active + me.yearOffset)) {
                    me.activeYear = year - me.yearOffset + 1;
                }
            }
            me.updateBody();
        }
        return me;
    },
    /**
     * Gets the selected value. It is returned as an array [month, year]. It may
     * be a partial value, for example [null, 2010]. The month is returned as
     * 0 based.
     * @return {Number[]} The selected value
     */
    getValue: function() {
        return this.value;
    },
    /**
     * Checks whether the picker has a selection
     * @return {Boolean} Returns true if both a month and year have been selected
     */
    hasSelection: function() {
        var value = this.value;
        return value[0] !== null && value[1] !== null;
    },
    /**
     * Get an array of years to be pushed in the template. It is not in strict
     * numerical order because we want to show them in columns.
     * @private
     * @return {Number[]} An array of years
     */
    getYears: function() {
        var me = this,
            offset = me.yearOffset,
            start = me.activeYear,
            // put the "active" year on the left
            end = start + offset,
            i = start,
            years = [];
        for (; i < end; ++i) {
            years.push(i, i + offset);
        }
        return years;
    },
    /**
     * Update the years in the body based on any change
     * @private
     */
    updateBody: function() {
        var me = this,
            years = me.years,
            months = me.months,
            yearNumbers = me.getYears(),
            cls = me.selectedCls,
            value = me.getYear(null),
            month = me.value[0],
            monthOffset = me.monthOffset,
            year, yearItems, y, yLen, el;
        if (me.rendered) {
            years.removeCls(cls);
            months.removeCls(cls);
            yearItems = years.elements;
            yLen = yearItems.length;
            for (y = 0; y < yLen; y++) {
                el = Ext.fly(yearItems[y]);
                year = yearNumbers[y];
                el.dom.innerHTML = year;
                if (year === value) {
                    el.addCls(cls);
                }
            }
            if (month !== null) {
                if (month < monthOffset) {
                    month = month * 2;
                } else {
                    month = (month - monthOffset) * 2 + 1;
                }
                months.item(month).addCls(cls);
            }
        }
    },
    /**
     * Gets the current year value, or the default.
     * @private
     * @param {Number} defaultValue The default value to use if the year is not defined.
     * @param {Number} offset A number to offset the value by
     * @return {Number} The year value
     */
    getYear: function(defaultValue, offset) {
        var year = this.value[1];
        offset = offset || 0;
        return year === null ? defaultValue : year + offset;
    },
    onElClick: function(e) {
        e.stopEvent();
    },
    /**
     * React to clicks on the body
     * @private
     */
    onBodyClick: function(e, t) {
        var me = this,
            isDouble = e.type === 'dblclick';
        if (e.getTarget('.' + me.baseCls + '-month')) {
            e.stopEvent();
            me.onMonthClick(t, isDouble);
        } else if (e.getTarget('.' + me.baseCls + '-year')) {
            e.stopEvent();
            me.onYearClick(t, isDouble);
        }
    },
    /**
     * Modify the year display by passing an offset.
     * @param {Number} [offset=10] The offset to move by.
     */
    adjustYear: function(offset) {
        if (typeof offset !== 'number') {
            offset = this.totalYears;
        }
        this.activeYear += offset;
        this.updateBody();
    },
    /**
     * React to the ok button being pressed
     * @private
     */
    onOkClick: function() {
        this.fireEvent('okclick', this, this.value);
    },
    /**
     * React to the cancel button being pressed
     * @private
     */
    onCancelClick: function() {
        this.fireEvent('cancelclick', this);
    },
    /**
     * React to a month being clicked
     * @private
     * @param {HTMLElement} target The element that was clicked
     * @param {Boolean} isDouble True if the event was a doubleclick
     */
    onMonthClick: function(target, isDouble) {
        var me = this;
        me.value[0] = me.resolveOffset(me.months.indexOf(target), me.monthOffset);
        me.updateBody();
        me.fireEvent('month' + (isDouble ? 'dbl' : '') + 'click', me, me.value);
        me.fireEvent('select', me, me.value);
    },
    /**
     * React to a year being clicked
     * @private
     * @param {HTMLElement} target The element that was clicked
     * @param {Boolean} isDouble True if the event was a doubleclick
     */
    onYearClick: function(target, isDouble) {
        var me = this;
        me.value[1] = me.activeYear + me.resolveOffset(me.years.indexOf(target), me.yearOffset);
        me.updateBody();
        me.fireEvent('year' + (isDouble ? 'dbl' : '') + 'click', me, me.value);
        me.fireEvent('select', me, me.value);
    },
    /**
     * Returns an offsetted number based on the position in the collection.
     * Since our collections aren't numerically ordered, this function helps
     * to normalize those differences.
     * @private
     * @param {Object} index
     * @param {Object} offset
     * @return {Number} The correctly offsetted number
     */
    resolveOffset: function(index, offset) {
        if (index % 2 === 0) {
            return (index / 2);
        } else {
            return offset + Math.floor(index / 2);
        }
    },
    doDestroy: function() {
        Ext.destroy(this.backRepeater, this.nextRepeater, this.okBtn, this.cancelBtn);
        this.callParent();
    },
    privates: {
        // Do the job of a container layout at this point even though we are not a Container.
        // TODO: Refactor as a Container.
        finishRenderChildren: function() {
            var me = this;
            this.callParent(arguments);
            if (this.showButtons) {
                me.okBtn.finishRender();
                me.cancelBtn.finishRender();
            }
        }
    }
});

/**
 * A date picker. This class is used by the Ext.form.field.Date field to allow browsing
 * and selection of valid dates in a popup next to the field, but may also be used
 * with other components.
 *
 * Typically you will need to implement a handler function to be notified when the user chooses
 * a date from the picker; you can register the handler using the {@link #select} event,
 * or by implementing the {@link #handler} method.
 *
 * By default the user will be allowed to pick any date; this can be changed by using the
 * {@link #minDate}, {@link #maxDate}, {@link #disabledDays}, {@link #disabledDatesRE},
 * and/or {@link #disabledDates} configs.
 *
 * All the string values documented below may be overridden by including an Ext locale file
 * in your page.
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *         title: 'Choose a future date:',
 *         width: 330,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items: [{
 *             xtype: 'datepicker',
 *             minDate: new Date(),
 *             handler: function(picker, date) {
 *                 // do something with the selected date
 *             }
 *         }]
 *     });
 */
Ext.define('Ext.picker.Date', {
    extend: 'Ext.Component',
    alias: 'widget.datepicker',
    alternateClassName: 'Ext.DatePicker',
    requires: [
        'Ext.XTemplate',
        'Ext.button.Button',
        'Ext.button.Split',
        'Ext.util.ClickRepeater',
        'Ext.util.KeyNav',
        'Ext.fx.Manager',
        'Ext.picker.Month'
    ],
    /**
     * @cfg {String} todayText
     * The text to display on the button that selects the current date.
     * @locale
     */
    todayText: 'Today',
    /**
     * @cfg {String} ariaTitle
     * The text to display for the aria title.
     * @locale
     */
    ariaTitle: 'Date Picker: {0}',
    /**
     * @cfg {String} ariaTitleDateFormat
     * The date format to display for the current value in the {@link #ariaTitle}.
     * @locale
     */
    ariaTitleDateFormat: 'F d',
    /**
     * @cfg {Function} handler
     * Optional. A function that will handle the select event of this picker. The handler
     * is passed the following parameters:
     *
     *   - `picker`: Ext.picker.Date
     *
     * This Date picker.
     *
     *   - `date`: Date
     *
     * The selected date.
     */
    /**
     * @cfg {Object} scope
     * The scope (`this` reference) in which the `{@link #handler}` function will be called.
     *
     * Defaults to this DatePicker instance.
     */
    /**
     * @cfg {String} todayTip
     * A string used to format the message for displaying in a tooltip over the button
     * that selects the current date. The `{0}` token in string is replaced by today's date.
     * @locale
     */
    todayTip: '{0} (Spacebar)',
    /**
     * @cfg {String} minText
     * The error text to display if the minDate validation fails.
     * @locale
     */
    minText: 'This date is before the minimum date',
    /**
     * @cfg {String} ariaMinText
     * The text that will be announced by Assistive Technologies such as screen readers
     * when user is navigating to the cell which date is less than
     * {@link #cfg-minDate minDate}.
     * @locale
     */
    ariaMinText: "This date is before the minimum date",
    /**
     * @cfg {String} maxText
     * The error text to display if the maxDate validation fails.
     * @locale
     */
    maxText: 'This date is after the maximum date',
    /**
     * @cfg {String} ariaMaxText
     * The text that will be announced by Assistive Technologies such as screen readers
     * when user is navigating to the cell which date is later than
     * {@link #cfg-maxDate maxDate}.
     * @locale
     */
    ariaMaxText: "This date is after the maximum date",
    /**
     * @cfg {String} format
     * The default date format string which can be overriden for localization support.
     * The format must be valid according to {@link Ext.Date#parse} (defaults to
     * {@link Ext.Date#defaultFormat}).
     */
    /**
     * @cfg {String} disabledDaysText
     * The tooltip to display when the date falls on a disabled day.
     * @locale
     */
    disabledDaysText: 'Disabled',
    /**
     * @cfg {String} ariaDisabledDaysText
     * The text that Assistive Technologies such as screen readers will announce when the
     * date falls on a disabled day of week.
     * @locale
     */
    ariaDisabledDaysText: "This day of week is disabled",
    /**
     * @cfg {String} disabledDatesText
     * The tooltip text to display when the date falls on a disabled date.
     * @locale
     */
    disabledDatesText: 'Disabled',
    /**
     * @cfg {String} ariaDisabledDatesText
     * The text that Assistive Technologies such as screen readers will announce when the
     * date falls on a disabled date.
     * @locale
     */
    ariaDisabledDatesText: "This date is disabled",
    /**
     * @cfg {String[]} monthNames
     * An array of textual month names which can be overriden for localization support
     * (defaults to Ext.Date.monthNames)
     * @deprecated 6.5.0 This config is deprecated. In future the month names will be retrieved
     * from {@link Ext.Date}
     */
    /**
     * @cfg {String[]} dayNames
     * An array of textual day names which can be overriden for localization support
     * (defaults to Ext.Date.dayNames)
     * @deprecated 6.5.0 This config is deprecated. In future the day names will be retrieved
     * from {@link Ext.Date}
     */
    /**
     * @cfg {String} nextText
     * The next month navigation button tooltip.
     * @locale
     */
    nextText: 'Next Month (Control+Right)',
    /**
     * @cfg {String} prevText
     * The previous month navigation button tooltip.
     * @locale
     */
    prevText: 'Previous Month (Control+Left)',
    /**
     * @cfg {String} monthYearText
     * The header month selector tooltip.
     * @locale
     */
    monthYearText: 'Choose a month (Control+Up/Down to move years)',
    /**
     * @cfg {String} monthYearFormat
     * The date format for the header month.
     * @locale
     */
    monthYearFormat: 'F Y',
    /**
     * @cfg {Number} [startDay=undefined]
     * Day index at which the week should begin, 0-based.
     *
     * Defaults to the value of {@link Ext.Date.firstDayOfWeek}.
     * @locale
     */
    /**
     * @cfg {Boolean} showToday
     * False to hide the footer area containing the Today button and disable the keyboard handler
     * for spacebar that selects the current date.
     * @locale
     */
    showToday: true,
    /**
     * @cfg {Date} [minDate=null]
     * Minimum allowable date (JavaScript date object)
     */
    /**
     * @cfg {Date} [maxDate=null]
     * Maximum allowable date (JavaScript date object)
     */
    /**
     * @cfg {Date} [defaultValue=new Date()]
     * @since 6.5.1
     * A default date for this picker, only used when a value is not set. (JavaScript date object)
     * Note: This value with be constrained to the minDate/maxDate if they are specified.
     */
    /**
     * @cfg {Number[]} [disabledDays=null]
     * An array of days to disable, 0-based. For example, [0, 6] disables Sunday and Saturday.
     */
    /**
     * @cfg {RegExp} [disabledDatesRE=null]
     * JavaScript regular expression used to disable a pattern of dates. The {@link #disabledDates}
     * config will generate this regex internally, but if you specify disabledDatesRE it will take
     * precedence over the disabledDates value.
     */
    /**
     * @cfg {String[]} disabledDates
     * An array of 'dates' to disable, as strings. These strings will be used to build
     * a dynamic regular expression so they are very powerful. Some examples:
     *
     *   - ['03/08/2003', '09/16/2003'] would disable those exact dates
     *   - ['03/08', '09/16'] would disable those days for every year
     *   - ['^03/08'] would only match the beginning (useful if you are using short years)
     *   - ['03/../2006'] would disable every day in March 2006
     *   - ['^03'] would disable every day in every March
     *
     * Note that the format of the dates included in the array should exactly match the
     * {@link #format} config. In order to support regular expressions, if you are using
     * a date format that has '.' in it, you will have to escape the dot when restricting dates.
     * For example: ['03\\.08\\.03'].
     */
    /**
     * @cfg {Boolean} disableAnim
     * True to disable animations when showing the month picker.
     */
    disableAnim: false,
    /**
     * @cfg baseCls
     * @inheritdoc
     */
    baseCls: Ext.baseCSSPrefix + 'datepicker',
    /**
     * @cfg {String} [selectedCls='x-datepicker-selected']
     * The class to apply to the selected cell.
     */
    /**
     * @cfg {String} [disabledCellCls='x-datepicker-disabled']
     * The class to apply to disabled cells.
     */
    /**
     * @cfg {String} longDayFormat
     * The format for displaying a date in a longer format.
     * @locale
     */
    longDayFormat: 'F d, Y',
    /**
     * @cfg {Object} keyNavConfig
     * Specifies optional custom key event handlers for the {@link Ext.util.KeyNav} attached to
     * this date picker. Must conform to the config format recognized by the {@link Ext.util.KeyNav}
     * constructor. Handlers specified in this object will replace default handlers
     * of the same name.
     */
    /**
     * @cfg {String}
     * The {@link Ext.button.Button#ui} to use for the date picker's footer buttons.
     */
    footerButtonUI: 'default',
    isDatePicker: true,
    /**
     * @cfg alignOnScroll
     * @inheritdoc
     */
    alignOnScroll: false,
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'region',
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: true,
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        'innerEl',
        'eventEl',
        'prevEl',
        'nextEl',
        'middleBtnEl',
        'footerEl'
    ],
    /**
     * @cfg border
     * @inheritdoc
     */
    border: true,
    /* eslint-disable indent, max-len */
    /**
     * @cfg renderTpl
     * @inheritdoc
     */
    renderTpl: [
        '<div id="{id}-innerEl" data-ref="innerEl" role="presentation">',
        '<div class="{baseCls}-header">',
        '<div id="{id}-prevEl" data-ref="prevEl" class="{baseCls}-prev {baseCls}-arrow" role="presentation" title="{prevText}"></div>',
        '<div id="{id}-middleBtnEl" data-ref="middleBtnEl" class="{baseCls}-month" role="heading">{%this.renderMonthBtn(values, out)%}</div>',
        '<div id="{id}-nextEl" data-ref="nextEl" class="{baseCls}-next {baseCls}-arrow" role="presentation" title="{nextText}"></div>',
        '</div>',
        '<table role="grid" id="{id}-eventEl" data-ref="eventEl" class="{baseCls}-inner" cellspacing="0" tabindex="0" aria-readonly="true">',
        '<thead>',
        '<tr role="row">',
        '<tpl for="dayNames">',
        '<th role="columnheader" class="{parent.baseCls}-column-header" aria-label="{.}">',
        '<div role="presentation" class="{parent.baseCls}-column-header-inner">{.:this.firstInitial}</div>',
        '</th>',
        '</tpl>',
        '</tr>',
        '</thead>',
        '<tbody>',
        '<tr role="row">',
        '<tpl for="days">',
        '{#:this.isEndOfWeek}',
        '<td role="gridcell">',
        '<div hidefocus="on" class="{parent.baseCls}-date"></div>',
        '</td>',
        '</tpl>',
        '</tr>',
        '</tbody>',
        '</table>',
        '<tpl if="showToday">',
        '<div id="{id}-footerEl" data-ref="footerEl" role="presentation" class="{baseCls}-footer">{%this.renderTodayBtn(values, out)%}</div>',
        '</tpl>',
        // These elements are used with Assistive Technologies such as screen readers
        '<div id="{id}-todayText" class="' + Ext.baseCSSPrefix + 'hidden-clip">{todayText}.</div>',
        '<div id="{id}-ariaMinText" class="' + Ext.baseCSSPrefix + 'hidden-clip">{ariaMinText}.</div>',
        '<div id="{id}-ariaMaxText" class="' + Ext.baseCSSPrefix + 'hidden-clip">{ariaMaxText}.</div>',
        '<div id="{id}-ariaDisabledDaysText" class="' + Ext.baseCSSPrefix + 'hidden-clip">{ariaDisabledDaysText}.</div>',
        '<div id="{id}-ariaDisabledDatesText" class="' + Ext.baseCSSPrefix + 'hidden-clip">{ariaDisabledDatesText}.</div>',
        '</div>',
        {
            firstInitial: function(value) {
                return Ext.picker.Date.prototype.getDayInitial(value);
            },
            isEndOfWeek: function(value) {
                // convert from 1 based index to 0 based
                // by decrementing value once.
                value--;
                // eslint-disable-next-line vars-on-top
                var end = value % 7 === 0 && value !== 0;
                return end ? '</tr><tr role="row">' : '';
            },
            renderTodayBtn: function(values, out) {
                Ext.DomHelper.generateMarkup(values.$comp.todayBtn.getRenderTree(), out);
            },
            renderMonthBtn: function(values, out) {
                Ext.DomHelper.generateMarkup(values.$comp.monthBtn.getRenderTree(), out);
            }
        }
    ],
    /* eslint-enable indent, max-len */
    // Default value used to initialise each date in the DatePicker.
    // __Note:__ 12 noon was chosen because it steers well clear of all DST timezone changes.
    initHour: 12,
    // 24-hour format
    numDays: 42,
    /**
     * @event select
     * Fires when a date is selected
     * @param {Ext.picker.Date} this DatePicker
     * @param {Date} date The selected date
     */
    initComponent: function() {
        var me = this,
            clearTime = Ext.Date.clearTime;
        me.selectedCls = me.baseCls + '-selected';
        me.disabledCellCls = me.baseCls + '-disabled';
        me.prevCls = me.baseCls + '-prevday';
        me.activeCls = me.baseCls + '-active';
        me.cellCls = me.baseCls + '-cell';
        me.nextCls = me.baseCls + '-prevday';
        me.todayCls = me.baseCls + '-today';
        me.defaultValue = Ext.isDate(me.defaultValue) ? me.defaultValue : new Date();
        if (!me.format) {
            me.format = Ext.Date.defaultFormat;
        }
        if (!me.dayNames) {
            me.dayNames = Ext.Date.dayNames;
        }
        if (me.startDay == null) {
            me.startDay = Ext.Date.firstDayOfWeek;
        }
        me.dayNames = me.dayNames.slice(me.startDay).concat(me.dayNames.slice(0, me.startDay));
        me.callParent();
        me.value = me.value ? clearTime(me.value, true) : clearTime(me.defaultValue);
        me.initDisabledDays();
    },
    // Keep the tree structure correct for Ext.form.field.Picker input fields
    // which poke a 'pickerField' reference down into their pop-up pickers.
    getRefOwner: function() {
        return this.pickerField || this.callParent();
    },
    getRefItems: function() {
        var results = [],
            monthBtn = this.monthBtn,
            todayBtn = this.todayBtn;
        if (monthBtn) {
            results.push(monthBtn);
        }
        if (todayBtn) {
            results.push(todayBtn);
        }
        return results;
    },
    beforeRender: function() {
        /*
         * days array for looping through 6 full weeks (6 weeks * 7 days)
         * Note that we explicitly force the size here so the template creates
         * all the appropriate cells.
         */
        var me = this,
            encode = Ext.String.htmlEncode,
            days = new Array(me.numDays),
            today = Ext.Date.format(new Date(), me.format);
        if (me.padding && !me.width) {
            me.cacheWidth();
        }
        me.monthBtn = new Ext.button.Button({
            ownerCt: me,
            ownerLayout: me.getComponentLayout(),
            text: '',
            tooltip: me.monthYearText,
            tabIndex: -1,
            ariaRole: 'presentation',
            showEmptyMenu: false,
            menu: [],
            listeners: {
                click: me.doShowMonthPicker,
                scope: me
            }
        });
        if (me.showToday) {
            me.todayBtn = new Ext.button.Button({
                ui: me.footerButtonUI,
                ownerCt: me,
                ownerLayout: me.getComponentLayout(),
                text: Ext.String.format(me.todayText, today),
                tooltip: Ext.String.format(me.todayTip, today),
                tooltipType: 'title',
                tabIndex: -1,
                ariaRole: 'presentation',
                handler: me.selectToday,
                scope: me
            });
        }
        me.callParent();
        Ext.applyIf(me, {
            renderData: {}
        });
        Ext.apply(me.renderData, {
            dayNames: me.dayNames,
            showToday: me.showToday,
            prevText: encode(me.prevText),
            nextText: encode(me.nextText),
            todayText: encode(me.todayText),
            ariaMinText: encode(me.ariaMinText),
            ariaMaxText: encode(me.ariaMaxText),
            ariaDisabledDaysText: encode(me.ariaDisabledDaysText),
            ariaDisabledDatesText: encode(me.ariaDisabledDatesText),
            days: days
        });
        me.protoEl.unselectable();
    },
    cacheWidth: function() {
        var me = this,
            padding = me.parseBox(me.padding),
            widthEl = Ext.getBody().createChild({
                cls: me.baseCls + ' ' + me.borderBoxCls,
                style: 'position:absolute;top:-1000px;left:-1000px;'
            });
        me.self.prototype.width = widthEl.getWidth() + padding.left + padding.right;
        widthEl.destroy();
    },
    /**
     * @method onRender
     * @inheritdoc
     * @private
     */
    onRender: function(container, position) {
        var me = this,
            dateCellSelector = 'div.' + me.baseCls + '-date';
        me.callParent(arguments);
        me.cells = me.eventEl.select('tbody td');
        me.textNodes = me.eventEl.query(dateCellSelector);
        me.eventEl.set({
            'aria-labelledby': me.monthBtn.id
        });
        me.mon(me.eventEl, {
            scope: me,
            mousewheel: me.handleMouseWheel,
            click: {
                fn: me.handleDateClick,
                delegate: dateCellSelector
            }
        });
    },
    /**
     * @method initEvents
     * @inheritdoc
     * @private
     */
    initEvents: function() {
        var me = this;
        me.callParent();
        // If we're part of a date field, don't allow us to focus on mousedown,
        // the field will handle that. If we are standalone, then allow the default
        // behaviour to occur to receive focus
        if (me.pickerField) {
            me.el.on('mousedown', me.onMouseDown, me);
        }
        // Month button is pointer interactive only, it should not be allowed to focus.
        me.monthBtn.el.on('mousedown', me.onMouseDown, me);
        me.prevRepeater = new Ext.util.ClickRepeater({
            el: me.prevEl,
            handler: me.showPrevMonth,
            scope: me,
            mousedownStopEvent: true
        });
        me.nextRepeater = new Ext.util.ClickRepeater({
            el: me.nextEl,
            handler: me.showNextMonth,
            scope: me,
            mousedownStopEvent: true
        });
        me.keyNav = new Ext.util.KeyNav(Ext.apply({
            target: me.eventEl,
            scope: me,
            left: me.onLeftKey,
            right: me.onRightKey,
            up: me.onUpKey,
            down: me.onDownKey,
            pageUp: me.onPageUpKey,
            pageDown: me.onPageDownKey,
            home: me.onHomeKey,
            end: me.onEndKey,
            enter: me.onEnterKey,
            space: me.onSpaceKey,
            tab: function(e) {
                // When the picker is floating and attached to an input field, its
                // 'select' handler will focus the inputEl so when navigation happens
                // it does so as if the input field was focused all the time.
                // This is the desired behavior and we try not to interfere with it
                // in the picker itself, see below.
                this.handleTabKey(e);
                // Allow default behaviour of TAB - it MUST be allowed to navigate.
                return true;
            }
        }, me.keyNavConfig));
        if (me.disabled) {
            me.syncDisabled(true, true);
        }
        if (me.minDate && me.minDate > me.value) {
            me.value = me.minDate;
        } else if (me.maxDate && me.maxDate < me.value) {
            me.value = me.maxDate;
        }
        me.update(me.value);
    },
    onMouseDown: function(e) {
        e.preventDefault();
    },
    handleTabKey: function(e) {
        var me = this,
            t = me.getSelectedDate(me.activeDate),
            handler = me.handler;
        // The following code is like handleDateClick without the e.stopEvent()
        if (!me.disabled && t.dateValue && !Ext.fly(t.parentNode).hasCls(me.disabledCellCls)) {
            me.setValue(new Date(t.dateValue));
            me.fireEvent('select', me, me.value);
            if (handler) {
                Ext.callback(handler, me.scope, [
                    me,
                    me.value
                ], null, me, me);
            }
            // If the ownerfield is part of an editor we must preventDefault and let
            // the navigationModel handle the tab event.
            if (me.pickerField && me.pickerField.isEditorComponent) {
                e.preventDefault();
            }
            me.onSelect();
        } else // Even if the above condition is not met we have to let the field know
        // that we're tabbing out - that's user action we can do nothing about
        {
            me.fireEventArgs('tabout', [
                me
            ]);
        }
    },
    getSelectedDate: function(date) {
        var me = this,
            t = date.getTime(),
            cells = me.cells,
            cls = me.selectedCls,
            cellItems = cells.elements,
            cLen = cellItems.length,
            cell, c;
        cells.removeCls(cls);
        for (c = 0; c < cLen; c++) {
            cell = cellItems[c].firstChild;
            if (cell.dateValue === t) {
                return cell;
            }
        }
        return null;
    },
    /**
     * Setup the disabled dates regex based on config options
     * @private
     */
    initDisabledDays: function() {
        var me = this,
            dd = me.disabledDates,
            re = '(?:',
            len, d, dLen, dI;
        if (!me.disabledDatesRE && dd) {
            len = dd.length - 1;
            dLen = dd.length;
            for (d = 0; d < dLen; d++) {
                dI = dd[d];
                re += Ext.isDate(dI) ? '^' + Ext.String.escapeRegex(Ext.Date.dateFormat(dI, me.format)) + '$' : dI;
                if (d !== len) {
                    re += '|';
                }
            }
            me.disabledDatesRE = new RegExp(re + ')');
        }
    },
    /**
     * Replaces any existing disabled dates with new values and refreshes the DatePicker.
     * @param {String[]/RegExp} dd An array of date strings (see the {@link #disabledDates} config
     * for details on supported values), or a JavaScript regular expression used to disable
     * a pattern of dates.
     * @return {Ext.picker.Date} this
     */
    setDisabledDates: function(dd) {
        var me = this;
        if (Ext.isArray(dd)) {
            me.disabledDates = dd;
            me.disabledDatesRE = null;
        } else {
            me.disabledDatesRE = dd;
        }
        me.initDisabledDays();
        me.update(me.value, true);
        return me;
    },
    /**
     * Replaces any existing disabled days (by index, 0-6) with new values and refreshes
     * the DatePicker.
     * @param {Number[]} disabledDays An array of disabled day indexes.
     * See the {@link #disabledDays} config for details on supported values.
     * @return {Ext.picker.Date} this
     */
    setDisabledDays: function(disabledDays) {
        this.disabledDays = disabledDays;
        return this.update(this.value, true);
    },
    /**
     * Replaces any existing {@link #minDate} with the new value and refreshes the DatePicker.
     * @param {Date} minDate The minimum date that can be selected
     * @return {Ext.picker.Date} this
     */
    setMinDate: function(minDate) {
        this.minDate = minDate;
        return this.update(this.value, true);
    },
    /**
     * Replaces any existing {@link #maxDate} with the new value and refreshes the DatePicker.
     * @param {Date} maxDate The maximum date that can be selected
     * @return {Ext.picker.Date} this
     */
    setMaxDate: function(maxDate) {
        this.maxDate = maxDate;
        return this.update(this.value, true);
    },
    /**
     * Sets the value of the date field
     * @param {Date} value The date to set
     * @return {Ext.picker.Date} this
     */
    setValue: function(value) {
        // If passed a null value just pass in a new date object.
        this.value = Ext.Date.clearTime(value || this.defaultValue, true);
        return this.update(this.value);
    },
    /**
     * Gets the current selected value of the date field
     * @return {Date} The selected date
     */
    getValue: function() {
        return this.value;
    },
    /**
     * Gets a single character to represent the day of the week
     * @return {String} The character
     * @locale
     */
    getDayInitial: function(value) {
        return value.substr(0, 1);
    },
    /**
     * @method onEnable
     * @inheritdoc
     * @private
     */
    onEnable: function() {
        var me = this;
        me.callParent();
        me.syncDisabled(false, true);
        me.update(me.activeDate);
    },
    /**
     * @method onShow
     * @inheritdoc
     * @private
     */
    onShow: function() {
        var me = this;
        me.callParent();
        me.syncDisabled(false);
        if (me.pickerField) {
            me.startValue = me.pickerField.getValue();
        }
    },
    /**
     * @method onHide
     * @inheritdoc
     * @private
     */
    onHide: function() {
        this.callParent();
        this.syncDisabled(true);
    },
    /**
     * @method onDisable
     * @inheritdoc
     * @private
     */
    onDisable: function() {
        this.callParent();
        this.syncDisabled(true, true);
    },
    /**
     * Get the current active date.
     * @private
     * @return {Date} The active date
     */
    getActive: function() {
        return this.activeDate || this.value;
    },
    /**
     * Run any animation required to hide/show the month picker.
     * @private
     * @param {Boolean} isHide True if it's a hide operation
     */
    runAnimation: function(isHide) {
        var picker = this.monthPicker,
            options = {
                duration: 200,
                callback: function() {
                    picker.setVisible(!isHide);
                }
            };
        if (isHide) {
            picker.el.slideOut('t', options);
        } else {
            picker.el.slideIn('t', options);
        }
    },
    /**
     * Hides the month picker, if it's visible.
     * @param {Boolean} [animate] Indicates whether to animate this action. If the animate
     * parameter is not specified, the behavior will use {@link #disableAnim} to determine
     * whether to animate or not.
     * @return {Ext.picker.Date} this
     */
    hideMonthPicker: function(animate) {
        var me = this,
            picker = me.monthPicker;
        if (picker && picker.isVisible()) {
            // The animated hide causes focus movement to the wrap el
            // from the soon to be focused button (Button focuses in a delay)
            // Controlling the focus now will prevent both erroneous
            // focus movements.
            me.focus();
            if (me.shouldAnimate(animate)) {
                me.runAnimation(true);
            } else {
                picker.hide();
            }
        }
        return me;
    },
    doShowMonthPicker: function() {
        // Wrap in an extra call so we can prevent the button
        // being passed as an animation parameter.
        this.showMonthPicker();
    },
    doHideMonthPicker: function() {
        // Wrap in an extra call so we can prevent this
        // being passed as an animation parameter
        this.hideMonthPicker();
    },
    /**
     * Show the month picker
     * @param {Boolean} [animate] Indicates whether to animate this action. If the animate
     * parameter is not specified, the behavior will use {@link #disableAnim} to determine
     * whether to animate or not.
     * @return {Ext.picker.Date} this
     */
    showMonthPicker: function(animate) {
        var me = this,
            el = me.el,
            picker;
        if (me.rendered && !me.disabled) {
            picker = me.createMonthPicker();
            if (!picker.isVisible()) {
                picker.setValue(me.getActive());
                picker.setSize(el.getSize());
                // Null out floatParent so that the [-1, -1] position is not made relative to this
                picker.floatParent = null;
                picker.setPosition(-el.getBorderWidth('l'), -el.getBorderWidth('t'));
                if (me.shouldAnimate(animate)) {
                    me.runAnimation(false);
                } else {
                    picker.show();
                }
            }
        }
        return me;
    },
    /**
     * Checks whether a hide/show action should animate
     * @private
     * @param {Boolean} [animate] A possible animation value
     * @return {Boolean} Whether to animate the action
     */
    shouldAnimate: function(animate) {
        return Ext.isDefined(animate) ? animate : !this.disableAnim;
    },
    /**
     * Create the month picker instance
     * @private
     * @return {Ext.picker.Month} picker
     */
    createMonthPicker: function() {
        var me = this,
            picker = me.monthPicker;
        if (!picker) {
            me.monthPicker = picker = new Ext.picker.Month({
                renderTo: me.el,
                // We need to set the ownerCmp so that owns() can correctly
                // match up the component hierarchy so that focus does not leave
                // an owning picker field if/when this gets focus.
                ownerCmp: me,
                floating: true,
                padding: me.padding,
                shadow: false,
                small: me.showToday === false,
                footerButtonUI: me.footerButtonUI,
                listeners: {
                    scope: me,
                    cancelclick: me.onCancelClick,
                    okclick: me.onOkClick,
                    yeardblclick: me.onOkClick,
                    monthdblclick: me.onOkClick
                }
            });
            if (!me.disableAnim) {
                // hide the element if we're animating to prevent an initial flicker
                picker.el.setStyle('display', 'none');
            }
            picker.hide();
            me.on('beforehide', me.doHideMonthPicker, me);
        }
        return picker;
    },
    /**
     * @private
     */
    onLeftKey: function(e) {
        if (e.ctrlKey) {
            this.showPrevMonth();
        } else {
            this.update(Ext.Date.add(this.activeDate, Ext.Date.DAY, -1));
        }
        // We need to prevent default to avoid scrolling the nearest container
        // which in case of a floating Date picker will be the document body.
        // This applies to all navigation keys.
        e.preventDefault();
    },
    /**
     * @private
     */
    onRightKey: function(e) {
        if (e.ctrlKey) {
            this.showNextMonth();
        } else {
            this.update(Ext.Date.add(this.activeDate, Ext.Date.DAY, 1));
        }
        e.preventDefault();
    },
    /**
     * @private
     */
    onUpKey: function(e) {
        // This is non-standard behavior kept for backward compatibility.
        // Ctrl-PageUp is reverse to this and it should be used instead.
        if (e.ctrlKey) {
            this.showNextYear();
        } else {
            this.update(Ext.Date.add(this.activeDate, Ext.Date.DAY, -7));
        }
        e.preventDefault();
    },
    /**
     * @private
     */
    onDownKey: function(e) {
        // This is non-standard behavior kept for backward compatibility.
        // Ctrl-PageDown is reverse to this and it should be used instead.
        if (e.ctrlKey) {
            this.showPrevYear();
        } else {
            this.update(Ext.Date.add(this.activeDate, Ext.Date.DAY, 7));
        }
        e.preventDefault();
    },
    /**
     * @private
     */
    onPageUpKey: function(e) {
        if (e.ctrlKey) {
            this.showPrevYear();
        } else {
            this.showPrevMonth();
        }
        e.preventDefault();
    },
    /**
     * @private
     */
    onPageDownKey: function(e) {
        if (e.ctrlKey) {
            this.showNextYear();
        } else {
            this.showNextMonth();
        }
        e.preventDefault();
    },
    /**
     * @private
     */
    onHomeKey: function(e) {
        this.update(Ext.Date.getFirstDateOfMonth(this.activeDate));
        e.preventDefault();
    },
    /**
     * @private
     */
    onEndKey: function(e) {
        this.update(Ext.Date.getLastDateOfMonth(this.activeDate));
        e.preventDefault();
    },
    /**
     * @private
     */
    onEnterKey: function(e) {
        this.handleDateClick(e, this.activeCell.firstChild);
    },
    /**
     * @private
     */
    onSpaceKey: function(e) {
        var me = this,
            pickerField = me.pickerField,
            startValue, value, pickerValue;
        me.setValue(new Date(me.activeCell.firstChild.dateValue));
        if (pickerField) {
            startValue = me.startValue;
            value = me.value;
            pickerValue = pickerField.getValue();
            if (pickerValue && startValue && pickerValue.getTime() === value.getTime()) {
                pickerField.setValue(startValue);
            } else {
                pickerField.setValue(value);
            }
        }
        // Space key causes scrolling, too :(
        e.preventDefault();
    },
    /**
     * Respond to an ok click on the month picker
     * @private
     */
    onOkClick: function(picker, value) {
        var me = this,
            month = value[0],
            year = value[1],
            date = new Date(year, month, me.getActive().getDate());
        if (date.getMonth() !== month) {
            // 'fix' the JS rolling date conversion if needed
            date = Ext.Date.getLastDateOfMonth(new Date(year, month, 1));
        }
        me.setValue(date);
        me.hideMonthPicker();
    },
    /**
     * Respond to a cancel click on the month picker
     * @private
     */
    onCancelClick: function() {
        this.selectedUpdate(this.activeDate);
        this.hideMonthPicker();
    },
    /**
     * Show the previous month.
     * @param {Object} e
     * @return {Ext.picker.Date} this
     */
    showPrevMonth: function(e) {
        return this.setValue(Ext.Date.add(this.activeDate, Ext.Date.MONTH, -1));
    },
    /**
     * Show the next month.
     * @param {Object} e
     * @return {Ext.picker.Date} this
     */
    showNextMonth: function(e) {
        return this.setValue(Ext.Date.add(this.activeDate, Ext.Date.MONTH, 1));
    },
    /**
     * Show the previous year.
     * @return {Ext.picker.Date} this
     */
    showPrevYear: function() {
        return this.setValue(Ext.Date.add(this.activeDate, Ext.Date.YEAR, -1));
    },
    /**
     * Show the next year.
     * @return {Ext.picker.Date} this
     */
    showNextYear: function() {
        return this.setValue(Ext.Date.add(this.activeDate, Ext.Date.YEAR, 1));
    },
    /**
     * Respond to the mouse wheel event
     * @private
     * @param {Ext.event.Event} e
     */
    handleMouseWheel: function(e) {
        var delta;
        e.stopEvent();
        if (!this.disabled) {
            delta = e.getWheelDelta();
            if (delta > 0) {
                this.showPrevMonth();
            } else if (delta < 0) {
                this.showNextMonth();
            }
        }
    },
    /**
     * Respond to a date being clicked in the picker
     * @private
     * @param {Ext.event.Event} e
     * @param {HTMLElement} t
     */
    handleDateClick: function(e, t) {
        var me = this,
            handler = me.handler;
        e.stopEvent();
        if (!me.disabled && t.dateValue && !Ext.fly(t.parentNode).hasCls(me.disabledCellCls)) {
            me.setValue(new Date(t.dateValue));
            me.fireEvent('select', me, me.value);
            if (handler) {
                Ext.callback(handler, me.scope, [
                    me,
                    me.value
                ], null, me, me);
            }
            // event handling is turned off on hide
            // when we are using the picker in a field
            // therefore onSelect comes AFTER the select
            // event.
            me.onSelect();
        }
    },
    /**
     * Perform any post-select actions
     * @private
     */
    onSelect: function() {
        if (this.hideOnSelect) {
            this.hide();
        }
    },
    /**
     * Sets the current value to today.
     * @return {Ext.picker.Date} this
     */
    selectToday: function() {
        var me = this,
            btn = me.todayBtn,
            handler = me.handler;
        if (btn && !btn.disabled) {
            me.setValue(Ext.Date.clearTime(new Date()));
            me.fireEvent('select', me, me.value);
            if (handler) {
                Ext.callback(handler, me.scope, [
                    me,
                    me.value
                ], null, me, me);
            }
            me.onSelect();
        }
        return me;
    },
    /**
     * Update the selected cell
     * @private
     * @param {Date} date The new date
     */
    selectedUpdate: function(date) {
        var me = this,
            t = date.getTime(),
            cells = me.cells,
            cls = me.selectedCls,
            c,
            cLen = cells.getCount(),
            cell;
        me.eventEl.dom.setAttribute('aria-busy', 'true');
        cell = me.activeCell;
        if (cell) {
            Ext.fly(cell).removeCls(cls);
            cell.setAttribute('aria-selected', false);
        }
        for (c = 0; c < cLen; c++) {
            cell = cells.item(c);
            if (me.textNodes[c].dateValue === t) {
                me.activeCell = cell.dom;
                me.eventEl.dom.setAttribute('aria-activedescendant', cell.dom.id);
                cell.dom.setAttribute('aria-selected', true);
                cell.addCls(cls);
                me.fireEvent('highlightitem', me, cell);
                break;
            }
        }
        me.eventEl.dom.removeAttribute('aria-busy');
    },
    /**
     * Update the contents of the picker for a new month
     * @private
     * @param {Date} date The new date
     */
    fullUpdate: function(date) {
        var me = this,
            cells = me.cells.elements,
            textNodes = me.textNodes,
            disabledCls = me.disabledCellCls,
            eDate = Ext.Date,
            i = 0,
            extraDays = 0,
            newDate = +eDate.clearTime(date, true),
            today = +eDate.clearTime(new Date()),
            min = me.minDate ? eDate.clearTime(me.minDate, true) : Number.NEGATIVE_INFINITY,
            max = me.maxDate ? eDate.clearTime(me.maxDate, true) : Number.POSITIVE_INFINITY,
            ddMatch = me.disabledDatesRE,
            ddText = me.disabledDatesText,
            ddays = me.disabledDays ? me.disabledDays.join('') : false,
            ddaysText = me.disabledDaysText,
            format = me.format,
            days = eDate.getDaysInMonth(date),
            firstOfMonth = eDate.getFirstDateOfMonth(date),
            startingPos = firstOfMonth.getDay() - me.startDay,
            previousMonth = eDate.add(date, eDate.MONTH, -1),
            ariaTitleDateFormat = me.ariaTitleDateFormat,
            prevStart, current, disableToday, tempDate, setCellClass, html, cls, formatValue, value;
        if (startingPos < 0) {
            startingPos += 7;
        }
        days += startingPos;
        prevStart = eDate.getDaysInMonth(previousMonth) - startingPos;
        current = new Date(previousMonth.getFullYear(), previousMonth.getMonth(), prevStart, me.initHour);
        if (me.showToday) {
            tempDate = eDate.clearTime(new Date());
            disableToday = (tempDate < min || tempDate > max || (ddMatch && format && ddMatch.test(eDate.dateFormat(tempDate, format))) || (ddays && ddays.indexOf(tempDate.getDay()) !== -1));
            me.todayDisabled = disableToday;
            if (!me.disabled) {
                me.todayBtn.setDisabled(disableToday);
            }
        }
        setCellClass = function(cellIndex, cls) {
            var cell = cells[cellIndex],
                describedBy = [];
            // Cells are not rendered with ids
            if (!cell.hasAttribute('id')) {
                cell.setAttribute('id', me.id + '-cell-' + cellIndex);
            }
            // store dateValue number as an expando
            value = +eDate.clearTime(current, true);
            cell.firstChild.dateValue = value;
            cell.setAttribute('aria-label', eDate.format(current, ariaTitleDateFormat));
            // Here and below we can't use title attribute instead of data-qtip
            // because JAWS will announce title value before cell content
            // which is not what we need. Also we are using aria-describedby attribute
            // and not placing the text in aria-label because some cells may have
            // compound descriptions (like Today and Disabled day).
            cell.removeAttribute('aria-describedby');
            cell.removeAttribute('data-qtip');
            if (value === today) {
                cls += ' ' + me.todayCls;
                describedBy.push(me.id + '-todayText');
            }
            if (value === newDate) {
                me.activeCell = cell;
                me.eventEl.dom.setAttribute('aria-activedescendant', cell.id);
                cell.setAttribute('aria-selected', true);
                cls += ' ' + me.selectedCls;
                me.fireEvent('highlightitem', me, cell);
            } else {
                cell.setAttribute('aria-selected', false);
            }
            if (value < min) {
                cls += ' ' + disabledCls;
                describedBy.push(me.id + '-ariaMinText');
                cell.setAttribute('data-qtip', me.minText);
            } else if (value > max) {
                cls += ' ' + disabledCls;
                describedBy.push(me.id + '-ariaMaxText');
                cell.setAttribute('data-qtip', me.maxText);
            } else if (ddays && ddays.indexOf(current.getDay()) !== -1) {
                cell.setAttribute('data-qtip', ddaysText);
                describedBy.push(me.id + '-ariaDisabledDaysText');
                cls += ' ' + disabledCls;
            } else if (ddMatch && format) {
                formatValue = eDate.dateFormat(current, format);
                if (ddMatch.test(formatValue)) {
                    cell.setAttribute('data-qtip', ddText.replace('%0', formatValue));
                    describedBy.push(me.id + '-ariaDisabledDatesText');
                    cls += ' ' + disabledCls;
                }
            }
            if (describedBy.length) {
                cell.setAttribute('aria-describedby', describedBy.join(' '));
            }
            cell.className = cls + ' ' + me.cellCls;
        };
        me.eventEl.dom.setAttribute('aria-busy', 'true');
        for (; i < me.numDays; ++i) {
            if (i < startingPos) {
                html = (++prevStart);
                cls = me.prevCls;
            } else if (i >= days) {
                html = (++extraDays);
                cls = me.nextCls;
            } else {
                html = i - startingPos + 1;
                cls = me.activeCls;
            }
            textNodes[i].innerHTML = html;
            current.setDate(current.getDate() + 1);
            setCellClass(i, cls);
        }
        me.eventEl.dom.removeAttribute('aria-busy');
        me.monthBtn.setText(Ext.Date.format(date, me.monthYearFormat));
    },
    /**
     * Update the contents of the picker
     * @private
     * @param {Date} date The new date
     * @param {Boolean} forceRefresh True to force a full refresh
     */
    update: function(date, forceRefresh) {
        var me = this,
            active = me.activeDate;
        if (me.rendered) {
            me.activeDate = date;
            if (!forceRefresh && active && me.el && active.getMonth() === date.getMonth() && active.getFullYear() === date.getFullYear()) {
                me.selectedUpdate(date, active);
            } else {
                me.fullUpdate(date, active);
            }
        }
        return me;
    },
    doDestroy: function() {
        var me = this;
        if (me.rendered) {
            Ext.destroy(me.keyNav, me.monthPicker, me.monthBtn, me.nextRepeater, me.prevRepeater, me.todayBtn, me.todayElSpan);
        }
        me.callParent();
    },
    privates: {
        // Do the job of a container layout at this point even though we are not a Container.
        // TODO: Refactor as a Container.
        finishRenderChildren: function() {
            var me = this;
            me.callParent();
            me.monthBtn.finishRender();
            if (me.showToday) {
                me.todayBtn.finishRender();
            }
        },
        getFocusEl: function() {
            return this.eventEl;
        },
        /**
         * Set the disabled state of various internal components
         * @param {Boolean} disabled
         * @param {Boolean} doButton
         * @private
         */
        syncDisabled: function(disabled, doButton) {
            var me = this,
                keyNav = me.keyNav,
                todayBtn = me.todayBtn;
            // If we have one, we have all
            if (keyNav) {
                keyNav.setDisabled(disabled);
                me.prevRepeater.setDisabled(disabled);
                me.nextRepeater.setDisabled(disabled);
            }
            if (doButton && todayBtn) {
                todayBtn.setDisabled(me.todayDisabled || disabled);
            }
        }
    }
});

/**
 * Provides a date input field with a {@link Ext.picker.Date date picker} dropdown and automatic
 * date validation.
 *
 * This field recognizes and uses the JavaScript Date object as its main {@link #value} type.
 * In addition, it recognizes string values which are parsed according to the {@link #format}
 * and/or {@link #altFormats} configs. These may be reconfigured to use date formats appropriate
 * for the user's locale.
 *
 * The field may be limited to a certain range of dates by using the {@link #minValue},
 * {@link #maxValue}, {@link #disabledDays}, and {@link #disabledDates} config parameters.
 * These configurations will be used both in the field's validation, and in the date picker
 * dropdown by preventing invalid dates from being selected.
 *
 * # Example usage
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         renderTo: Ext.getBody(),
 *         width: 300,
 *         bodyPadding: 10,
 *         title: 'Dates',
 *         items: [{
 *             xtype: 'datefield',
 *             anchor: '100%',
 *             fieldLabel: 'From',
 *             name: 'from_date',
 *             maxValue: new Date()  // limited to the current date or prior
 *         }, {
 *             xtype: 'datefield',
 *             anchor: '100%',
 *             fieldLabel: 'To',
 *             name: 'to_date',
 *             value: new Date()  // defaults to today
 *         }]
 *     });
 *
 * # Date Formats Examples
 *
 * This example shows a couple of different date format parsing scenarios. Both use custom date
 * format configurations; the first one matches the configured `format` while the second matches
 * the `altFormats`.
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         renderTo: Ext.getBody(),
 *         width: 300,
 *         bodyPadding: 10,
 *         title: 'Dates',
 *         items: [{
 *             xtype: 'datefield',
 *             anchor: '100%',
 *             fieldLabel: 'Date',
 *             name: 'date',
 *             // The value matches the format; will be parsed and displayed using that format.
 *             format: 'm d Y',
 *             value: '02 04 1978'
 *         }, {
 *             xtype: 'datefield',
 *             anchor: '100%',
 *             fieldLabel: 'Date',
 *             name: 'date',
 *             // The value does not match the format, but does match an altFormat; will be parsed
 *             // using the altFormat and displayed using the format.
 *             format: 'm d Y',
 *             altFormats: 'm,d,Y|m.d.Y',
 *             value: '02.04.1978'
 *         }]
 *     });
 */
Ext.define('Ext.form.field.Date', {
    extend: 'Ext.form.field.Picker',
    alias: 'widget.datefield',
    requires: [
        'Ext.picker.Date'
    ],
    alternateClassName: [
        'Ext.form.DateField',
        'Ext.form.Date'
    ],
    /**
     * @cfg {String} format
     * The default date format string which can be overriden for localization support.
     * The format must be valid according to {@link Ext.Date#parse}.
     * @locale
     */
    format: "m/d/Y",
    /**
     * @cfg {String} ariaFormat
     * This date format will be used to format ARIA attributes in the field and its Picker,
     * to provide Assistive Technologies such as screen readers with user friendly text.
     * The format must be valid {@link Ext.Date#format}.
     * @locale
     */
    ariaFormat: 'M j Y',
    /**
     * @cfg {String} altFormats
     * Multiple date formats separated by "|" to try when parsing a user input value and it
     * does not match the defined format.
     * @locale
     */
    // eslint-disable-next-line max-len
    altFormats: "m/d/Y|n/j/Y|n/j/y|m/j/y|n/d/y|m/j/Y|n/d/Y|m-d-y|m-d-Y|m/d|m-d|md|mdy|mdY|d|Y-m-d|n-j|n/j",
    /**
     * @cfg {String} disabledDaysText
     * The tooltip to display when the date falls on a disabled day of week.
     * @locale
     */
    disabledDaysText: "Disabled",
    /**
     * @cfg {String} ariaDisabledDaysText
     * The text that Assistive Technologies such as screen readers will announce when the
     * date falls on a disabled day of week.
     * @locale
     */
    ariaDisabledDaysText: "This day of week is disabled",
    /**
     * @cfg {String} disabledDatesText
     * The tooltip text to display when the date falls on a disabled date.
     * @locale
     */
    disabledDatesText: "Disabled",
    /**
     * @cfg {String} ariaDisabledDatesText
     * The text that Assistive Technologies such as screen readers will announce when the
     * date falls on a disabled date.
     * @locale
     */
    ariaDisabledDatesText: "This date cannot be selected",
    /**
     * @cfg {String} minText
     * The error text to display when the date in the cell is before {@link #minValue}.
     * @locale
     */
    minText: "The date in this field must be equal to or after {0}",
    /**
     * @cfg {String} ariaMinText
     * The text that Assistive Technologies such as screen readers will announce when the
     * date in the cell is before {@link #minValue}. The date substituted for {0} will be
     * formatted as per {@link #ariaFormat}.
     * @locale
     */
    ariaMinText: "The date must be equal to or after {0}",
    /**
     * @cfg {String} maxText
     * The error text to display when the date in the cell is after {@link #maxValue}.
     * @locale
     */
    maxText: "The date in this field must be equal to or before {0}",
    /**
     * @cfg {String} ariaMaxText
     * The text that Assistive Technologies such as screen readers will announce when the
     * date in the cell is after {@link #maxValue}. The date substituted for {0} will be
     * formatted as per {@link #ariaFormat}.
     * @locale
     */
    ariaMaxText: "The date must be equal to or before {0}",
    /**
     * @cfg {String} invalidText
     * The error text to display when the date in the field is invalid.
     * @locale
     */
    invalidText: "{0} is not a valid date - it must be in the format {1}",
    /**
     * @cfg {String} formatText
     * The format text to be announced by screen readers when the field is focused.
     * @locale
     */
    formatText: 'Expected date format {0}.',
    /**
     * @cfg {String} triggerCls
     * An additional CSS class used to style the trigger button. The trigger will always get
     * the class 'x-form-trigger' and triggerCls will be **appended** if specified
     * (default class displays a calendar icon).
     */
    triggerCls: Ext.baseCSSPrefix + 'form-date-trigger',
    /**
     * @cfg {Boolean} showToday
     * false to hide the footer area of the Date picker containing the Today button and disable
     * the keyboard handler for spacebar that selects the current date.
     */
    showToday: true,
    /**
     * @cfg {Date/String} minValue
     * The minimum allowed date. Can be either a Javascript date object or a string date
     * in a valid format.
     */
    /**
     * @cfg {Date/String} maxValue
     * The maximum allowed date. Can be either a Javascript date object or a string date
     * in a valid format.
     */
    /**
     * @cfg {Number[]} disabledDays
     * An array of days to disable, 0 based. Some examples:
     *
     *     // disable Sunday and Saturday:
     *     disabledDays:  [0, 6]
     *     // disable weekdays:
     *     disabledDays: [1,2,3,4,5]
     */
    /**
     * @cfg {String[]} disabledDates
     * An array of "dates" to disable, as strings. These strings will be used to build
     * a dynamic regular expression so they are very powerful. Some examples:
     *
     *     // disable these exact dates:
     *     disabledDates: ["03/08/2003", "09/16/2003"]
     *     // disable these days for every year:
     *     disabledDates: ["03/08", "09/16"]
     *     // only match the beginning (useful if you are using short years):
     *     disabledDates: ["^03/08"]
     *     // disable every day in March 2006:
     *     disabledDates: ["03/../2006"]
     *     // disable every day in every March:
     *     disabledDates: ["^03"]
     *
     * Note that the format of the dates included in the array should exactly match
     * the {@link #format} config. In order to support regular expressions, if you are using
     * a {@link #format date format} that has "." in it, you will have to escape the dot
     * when restricting dates. For example: `["03\\.08\\.03"]`.
     */
    /**
     * @cfg {String} submitFormat
     * The date format string which will be submitted to the server. The format must be valid
     * according to {@link Ext.Date#parse}.
     *
     * Defaults to {@link #format}.
     */
    /**
     * @cfg {Boolean} useStrict
     * True to enforce strict date parsing to prevent the default Javascript "date rollover".
     * Defaults to the useStrict parameter set on Ext.Date
     * See {@link Ext.Date#parse}.
     */
    useStrict: undefined,
    // in the absence of a time value, a default value of 12 noon will be used
    // (note: 12 noon was chosen because it steers well clear of all DST timezone changes)
    initTime: '12',
    // 24 hour format
    initTimeFormat: 'H',
    /**
     * @cfg matchFieldWidth
     * @inheritdoc
     */
    matchFieldWidth: false,
    /**
     * @cfg {Number} startDay
     * Day index at which the week should begin, 0-based.
     *
     * Defaults to `0` (Sunday).
     * @locale
     */
    startDay: 0,
    /**
     * @cfg valuePublishEvent
     * @inheritdoc
     */
    valuePublishEvent: [
        'select',
        'blur'
    ],
    /**
     * @cfg componentCls
     * @inheritdoc
     */
    componentCls: Ext.baseCSSPrefix + 'form-field-date',
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'combobox',
    /**
     * @private
     */
    rawDate: null,
    /**
     * @private
     */
    rawDateText: '',
    // Date field should validate on focusleave not blur
    validateOnFocusLeave: true,
    /**
     * @event select
     * Fires when a value is selected via the picker or when value is getting set
     * explicitly by setter.
     * @param {Ext.form.field.Date} field This field instance
     * @param {Object} value The value that was selected. 
     */
    initComponent: function() {
        var me = this,
            isString = Ext.isString,
            min, max;
        min = me.minValue;
        max = me.maxValue;
        if (isString(min)) {
            me.minValue = me.parseDate(min);
        }
        if (isString(max)) {
            me.maxValue = me.parseDate(max);
        }
        me.disabledDatesRE = null;
        me.initDisabledDays();
        me.callParent();
    },
    getSubTplData: function(fieldData) {
        var me = this,
            data, ariaAttr;
        data = me.callParent([
            fieldData
        ]);
        if (!me.ariaStaticRoles[me.ariaRole]) {
            ariaAttr = data.ariaElAttributes;
            if (ariaAttr) {
                ariaAttr['aria-owns'] = me.id + '-inputEl ' + me.id + '-picker-eventEl';
                ariaAttr['aria-autocomplete'] = 'none';
            }
        }
        return data;
    },
    initValue: function() {
        var me = this,
            value = me.value;
        // If a String value was supplied, try to convert it to a proper Date
        if (Ext.isString(value)) {
            me.value = me.rawToValue(value);
            me.rawDate = me.value;
            me.rawDateText = me.parseDate(me.value);
        } else {
            me.value = value || null;
            me.rawDate = me.value;
            me.rawDateText = me.value ? me.parseDate(me.value) : '';
        }
        me.callParent();
    },
    /**
     * @private
     */
    initDisabledDays: function() {
        if (this.disabledDates) {
            // eslint-disable-next-line vars-on-top
            var dd = this.disabledDates,
                len = dd.length - 1,
                re = "(?:",
                d,
                dLen = dd.length,
                date;
            for (d = 0; d < dLen; d++) {
                date = dd[d];
                re += Ext.isDate(date) ? '^' + Ext.String.escapeRegex(date.dateFormat(this.format)) + '$' : date;
                if (d !== len) {
                    re += '|';
                }
            }
            this.disabledDatesRE = new RegExp(re + ')');
        }
    },
    /**
     * Replaces any existing disabled dates with new values and refreshes the Date picker.
     * @param {String[]} disabledDates An array of date strings (see the {@link #disabledDates}
     * config for details on supported values) used to disable a pattern of dates.
     */
    setDisabledDates: function(disabledDates) {
        var me = this,
            picker = me.picker;
        me.disabledDates = disabledDates;
        me.initDisabledDays();
        if (picker) {
            picker.setDisabledDates(me.disabledDatesRE);
        }
    },
    /**
     * Replaces any existing disabled days (by index, 0-6) with new values and refreshes
     * the Date picker.
     * @param {Number[]} disabledDays An array of disabled day indexes.
     * See the {@link #disabledDays} config for details on supported values.
     */
    setDisabledDays: function(disabledDays) {
        var picker = this.picker;
        this.disabledDays = disabledDays;
        if (picker) {
            picker.setDisabledDays(disabledDays);
        }
    },
    /**
     * Replaces any existing {@link #minValue} with the new value and refreshes the Date picker.
     * @param {Date} value The minimum date that can be selected
     */
    setMinValue: function(value) {
        var me = this,
            picker = me.picker,
            minValue = (Ext.isString(value) ? me.parseDate(value) : value);
        me.minValue = minValue;
        if (picker) {
            picker.minText = Ext.String.format(me.minText, me.formatDate(me.minValue));
            picker.setMinDate(minValue);
        }
    },
    /**
     * Replaces any existing {@link #maxValue} with the new value and refreshes the Date picker.
     * @param {Date} value The maximum date that can be selected
     */
    setMaxValue: function(value) {
        var me = this,
            picker = me.picker,
            maxValue = (Ext.isString(value) ? me.parseDate(value) : value);
        me.maxValue = maxValue;
        if (picker) {
            picker.maxText = Ext.String.format(me.maxText, me.formatDate(me.maxValue));
            picker.setMaxDate(maxValue);
        }
    },
    /**
     * Runs all of Date's validations and returns an array of any errors. Note that this first
     * runs Text's validations, so the returned array is an amalgamation of all field errors.
     * The additional validation checks are testing that the date format is valid, that the chosen
     * date is within the min and max date constraints set, that the date chosen is not in the
     * disabledDates regex and that the day chosed is not one of the disabledDays.
     * @param {Object} [value] The value to get errors for (defaults to the current field value)
     * @return {String[]} All validation errors for this field
     */
    getErrors: function(value) {
        value = arguments.length > 0 ? value : this.formatDate(this.processRawValue(this.getRawValue()));
        // eslint-disable-next-line vars-on-top
        var me = this,
            format = Ext.String.format,
            clearTime = Ext.Date.clearTime,
            errors = me.callParent([
                value
            ]),
            disabledDays = me.disabledDays,
            disabledDatesRE = me.disabledDatesRE,
            minValue = me.minValue,
            maxValue = me.maxValue,
            len = disabledDays ? disabledDays.length : 0,
            i = 0,
            svalue, fvalue, day, time;
        // if it's blank and textfield didn't flag it then it's valid
        if (value === null || value.length < 1) {
            return errors;
        }
        svalue = value;
        value = me.parseDate(value);
        if (!value) {
            errors.push(format(me.invalidText, svalue, Ext.Date.unescapeFormat(me.format)));
            return errors;
        }
        time = value.getTime();
        if (minValue && time < clearTime(minValue).getTime()) {
            errors.push(format(me.minText, me.formatDate(minValue)));
        }
        if (maxValue && time > clearTime(maxValue).getTime()) {
            errors.push(format(me.maxText, me.formatDate(maxValue)));
        }
        if (disabledDays) {
            day = value.getDay();
            for (; i < len; i++) {
                if (day === disabledDays[i]) {
                    errors.push(me.disabledDaysText);
                    break;
                }
            }
        }
        fvalue = me.formatDate(value);
        if (disabledDatesRE && disabledDatesRE.test(fvalue)) {
            errors.push(format(me.disabledDatesText, fvalue));
        }
        return errors;
    },
    rawToValue: function(rawValue) {
        var me = this;
        if (rawValue === me.rawDateText) {
            return me.rawDate;
        }
        return me.parseDate(rawValue) || rawValue || null;
    },
    valueToRaw: function(value) {
        return this.formatDate(this.parseDate(value));
    },
    /**
     * @method setValue
     * Sets the value of the date field. You can pass a date object or any string that can be parsed
     * into a valid date, using {@link #format} as the date format, according to the same rules
     * as {@link Ext.Date#parse} (the default format used is "m/d/Y").
     *
     * Usage:
     *
     *     // All of these calls set the same date value (May 4, 2006)
     *
     *     // Pass a date object:
     *     var dt = new Date('5/4/2006');
     *     dateField.me = this,
     *     setValue(dt);
     *
     *     // Pass a date string (default format):
     *     dateField.setValue('05/04/2006');
     *
     *     // Pass a date string (custom format):
     *     dateField.format = 'Y-m-d';
     *     dateField.setValue('2006-05-04');
     *
     * @param {String/Date} v The date or valid date string
     * @return {Ext.form.field.Date} this
     */
    setValue: function(v) {
        var me = this,
            utilDate = Ext.Date,
            rawDate,
            oldVal = me.lastValue;
        me.lastValue = me.rawDateText;
        me.lastDate = me.rawDate;
        if (Ext.isDate(v)) {
            rawDate = me.rawDate = v;
            me.rawDateText = me.formatDate(v);
        } else {
            rawDate = me.rawDate = me.rawToValue(v);
            me.rawDateText = me.formatDate(v);
            if (rawDate === v) {
                rawDate = me.rawDate = null;
                me.rawDateText = '';
            }
        }
        if (rawDate && !utilDate.formatContainsHourInfo(me.format)) {
            me.rawDate = utilDate.clearTime(rawDate, true);
        }
        me.callParent(arguments);
        // Checking if there is a value change
        // If yes, trigger select event
        if (!me.destroyed && me.didValueChange(me.rawDate, oldVal)) {
            me.fireEvent('select', me, v);
        }
    },
    /**
     * Checks whether the value of the field has changed since the last time it was checked.
     * If the value has changed, it:
     *
     * 1. Fires the {@link #change change event},
     * 2. Performs validation if the {@link #validateOnChange} config is enabled, firing the
     *    {@link #validitychange validitychange event} if the validity has changed, and
     * 3. Checks the {@link #isDirty dirty state} of the field and fires the
     *    {@link #dirtychange dirtychange event} if it has changed.
     */
    checkChange: function() {
        var me = this,
            newVal, oldVal, lastDate;
        if (!me.suspendCheckChange) {
            newVal = me.getRawValue();
            oldVal = me.lastValue;
            lastDate = me.lastDate;
            if (!me.destroyed && me.didValueChange(newVal, oldVal)) {
                me.rawDate = me.rawToValue(newVal);
                me.rawDateText = me.formatDate(newVal);
                me.lastValue = newVal;
                me.lastDate = me.rawDate;
                me.fireEvent('change', me, me.getValue(), lastDate);
                me.onChange(newVal, oldVal);
            }
        }
    },
    /**
     * Attempts to parse a given string value using a given {@link Ext.Date#parse date format}.
     * @param {String} value The value to attempt to parse
     * @param {String} format A valid date format (see {@link Ext.Date#parse})
     * @return {Date} The parsed Date object, or null if the value could not be successfully parsed.
     */
    safeParse: function(value, format) {
        var me = this,
            utilDate = Ext.Date,
            result = null,
            strict = me.useStrict,
            parsedDate;
        if (utilDate.formatContainsHourInfo(format)) {
            // if parse format contains hour information, no DST adjustment is necessary
            result = utilDate.parse(value, format, strict);
        } else {
            // set time to 12 noon, then clear the time
            parsedDate = utilDate.parse(value + ' ' + me.initTime, format + ' ' + me.initTimeFormat, strict);
            if (parsedDate) {
                result = utilDate.clearTime(parsedDate);
            }
        }
        return result;
    },
    /**
     * @private
     */
    getSubmitValue: function() {
        var format = this.submitFormat || this.format,
            value = this.rawDate;
        return value ? Ext.Date.format(value, format) : '';
    },
    /**
     * Returns the current data value of the field. The type of value returned is particular
     * to the type of the particular field (e.g. a Date object for {@link Ext.form.field.Date}),
     * as the result of calling {@link #rawToValue} on the field's
     * {@link #processRawValue processed} String value. To return the raw String value,
     * see {@link #getRawValue}.
     * @return {Object} value The field value
     */
    getValue: function() {
        return this.rawDate || null;
    },
    setRawValue: function(value) {
        var me = this;
        me.callParent([
            value
        ]);
        me.rawDate = Ext.isDate(value) ? value : me.rawToValue(value);
        me.rawDateText = this.formatDate(value);
    },
    /**
     * @private
     */
    parseDate: function(value) {
        if (!value || Ext.isDate(value)) {
            return value;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            val = me.safeParse(value, me.format),
            altFormats = me.altFormats,
            altFormatsArray = me.altFormatsArray,
            i = 0,
            len;
        if (!val && altFormats) {
            altFormatsArray = altFormatsArray || altFormats.split('|');
            len = altFormatsArray.length;
            for (; i < len && !val; ++i) {
                val = me.safeParse(value, altFormatsArray[i]);
            }
        }
        return val;
    },
    /**
     * @private
     */
    formatDate: function(date, format) {
        return Ext.isDate(date) ? Ext.Date.dateFormat(date, format || this.format) : date;
    },
    createPicker: function() {
        var me = this,
            format = Ext.String.format;
        // Create floating Picker BoundList. It will acquire a floatParent by looking up
        // its ancestor hierarchy (Pickers use their pickerField property as an upward link)
        // for a floating component.
        return new Ext.picker.Date({
            id: me.id + '-picker',
            pickerField: me,
            floating: true,
            preventRefocus: true,
            hidden: true,
            minDate: me.minValue,
            maxDate: me.maxValue,
            disabledDatesRE: me.disabledDatesRE,
            disabledDatesText: me.disabledDatesText,
            ariaDisabledDatesText: me.ariaDisabledDatesText,
            disabledDays: me.disabledDays,
            disabledDaysText: me.disabledDaysText,
            ariaDisabledDaysText: me.ariaDisabledDaysText,
            format: me.format,
            showToday: me.showToday,
            startDay: me.startDay,
            minText: format(me.minText, me.formatDate(me.minValue)),
            ariaMinText: format(me.ariaMinText, me.formatDate(me.minValue, me.ariaFormat)),
            maxText: format(me.maxText, me.formatDate(me.maxValue)),
            ariaMaxText: format(me.ariaMaxText, me.formatDate(me.maxValue, me.ariaFormat)),
            listeners: {
                scope: me,
                select: me.onSelect,
                tabout: me.onTabOut
            },
            keyNavConfig: {
                esc: function() {
                    me.inputEl.focus();
                    me.collapse();
                }
            }
        });
    },
    createInitialDate: function(value) {
        var minValue = this.minValue,
            maxValue = this.maxValue;
        value = value || new Date();
        if (minValue && minValue > value) {
            value = minValue;
        } else if (maxValue && maxValue < value) {
            value = maxValue;
        }
        return value;
    },
    onSelect: function(m, d) {
        var me = this;
        me.setValue(d);
        me.rawDate = d;
        // Select event has already been triggered in setValue()
        // Focus the inputEl first and then collapse. We configure
        // the picker not to revert focus which is a normal thing to do
        // for floaters; in our case when the picker is focusable it will
        // lead to unexpected results on Tab key presses.
        // Note that this focusing might happen synchronously during Tab
        // key handling in the picker, which is the way we want it.
        me.onTabOut(m);
    },
    onTabOut: function(picker) {
        this.inputEl.focus();
        this.collapse();
    },
    /**
     * @private
     * Sets the Date picker's value to match the current field value when expanding.
     */
    onExpand: function() {
        var value = this.rawDate;
        this.picker.setValue(Ext.isDate(value) ? value : this.createInitialDate());
    },
    /**
     * @private
     */
    onBlur: function(e) {
        var me = this,
            v = me.rawToValue(me.getRawValue());
        if (v === '' || Ext.isDate(v)) {
            me.setValue(v);
        }
        me.callParent([
            e
        ]);
    }
});
/**
     * @cfg {Boolean} grow
     * @private
     */
/**
     * @cfg {Number} growMin
     * @private
     */
/**
     * @cfg {Number} growMax
     * @private
     */
/**
     * @method autoSize
     * @private
     */

/**
 * A display-only text field which is not validated and not submitted. This is useful for when
 * you want to display a value from a form's {@link Ext.form.Basic#load loaded data} but do not want
 * to allow the user to edit or submit that value. The value can be optionally
 * {@link #htmlEncode HTML encoded} if it contains HTML markup that you do not want to be rendered.
 *
 * If you have more complex content, or need to include components within the displayed content,
 * also consider using a {@link Ext.form.FieldContainer} instead.
 *
 * Example:
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         renderTo: Ext.getBody(),
 *         width: 175,
 *         height: 150,
 *         bodyPadding: 10,
 *         title: 'Final Score',
 *         items: [{
 *             xtype: 'displayfield',
 *             fieldLabel: 'Home',
 *             name: 'home_score',
 *             value: '10'
 *         }, {
 *             xtype: 'displayfield',
 *             fieldLabel: 'Visitor',
 *             name: 'visitor_score',
 *             value: '11'
 *         }],
 *         buttons: [{
 *             text: 'Update'
 *         }]
 *     });
 */
Ext.define('Ext.form.field.Display', {
    extend: 'Ext.form.field.Base',
    alias: 'widget.displayfield',
    alternateClassName: [
        'Ext.form.DisplayField',
        'Ext.form.Display'
    ],
    requires: [
        'Ext.util.Format',
        'Ext.XTemplate'
    ],
    /* eslint-disable indent, max-len */
    /**
     * @cfg fieldSubTpl
     * @inheritdoc
     */
    fieldSubTpl: [
        '<div id="{id}" data-ref="inputEl" role="textbox" aria-readonly="true"',
        ' aria-labelledby="{cmpId}-labelEl" {inputAttrTpl}',
        ' tabindex="<tpl if="tabIdx != null">{tabIdx}<tpl else>-1</tpl>"',
        '<tpl if="fieldStyle"> style="{fieldStyle}"</tpl>',
        ' class="{fieldCls} {fieldCls}-{ui}">{value}</div>',
        {
            compiled: true,
            disableFormats: true
        }
    ],
    /* eslint-enable indent, max-len */
    // We have the ARIA markup pre-rendered so we don't want it to be applied
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: undefined,
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: false,
    // Display fields are divs not real input fields, so rendering
    // "for" attribute in the label does not do any good.
    skipLabelForAttribute: true,
    /**
     * @cfg readOnly
     * @inheritdoc
     * @private
     */
    readOnly: true,
    /**
     * @cfg fieldCls
     * @inheritdoc
     */
    fieldCls: Ext.baseCSSPrefix + 'form-display-field',
    /**
     * @cfg fieldBodyCls
     * @inheritdoc
     */
    fieldBodyCls: Ext.baseCSSPrefix + 'form-display-field-body',
    /**
     * @cfg {Boolean} htmlEncode
     * True to escape HTML in text when rendering it.
     */
    htmlEncode: false,
    /**
     * @cfg {Function/String} renderer
     * A function to transform the raw value for display in the field.
     * 
     *     Ext.create('Ext.form.Panel', {
     *         renderTo: document.body,
     *         width: 175,
     *         bodyPadding: 10,
     *         title: 'Final Score',
     *         items: [{
     *             xtype: 'displayfield',
     *             fieldLabel: 'Grade',
     *             name: 'final_grade',
     *             value: 68,
     *             renderer: function (value, field) {
     *                 var color = (value < 70) ? 'red' : 'black';
     *                 return '<span style="color:' + color + ';">' + value + '</span>';
     *             }
     *         }]
     *     });
     * 
     * @param {Object} value The raw field {@link #value}
     * @param {Ext.form.field.Display} field The display field
     * @return {String} displayValue The HTML string to be rendered
     * @controllable
     */
    /**
     * @cfg {Object} scope
     * The scope to execute the {@link #renderer} function. Defaults to this.
     */
    /**
     * @property noWrap
     * @inheritdoc
     */
    noWrap: false,
    /**
     * @cfg validateOnChange
     * @inheritdoc
     * @private
     */
    validateOnChange: false,
    /**
     * @method initEvents
     * @inheritdoc
     */
    initEvents: Ext.emptyFn,
    /**
     * @cfg submitValue
     * @inheritdoc
     */
    submitValue: false,
    getValue: function() {
        return this.value;
    },
    valueToRaw: function(value) {
        if (value || value === 0 || value === false) {
            return value;
        } else {
            return '';
        }
    },
    isDirty: function() {
        return false;
    },
    isValid: Ext.returnTrue,
    validate: Ext.returnTrue,
    getRawValue: function() {
        return this.rawValue;
    },
    setRawValue: function(value) {
        var me = this;
        value = Ext.valueFrom(value, '');
        me.rawValue = value;
        if (me.rendered) {
            me.inputEl.dom.innerHTML = me.getDisplayValue();
            me.updateLayout();
        }
        return value;
    },
    /**
     * @private
     * Format the value to display.
     */
    getDisplayValue: function() {
        var me = this,
            value = this.getRawValue(),
            renderer = me.renderer,
            display;
        if (renderer) {
            display = Ext.callback(renderer, me.scope, [
                value,
                me
            ], 0, me);
        } else {
            display = me.htmlEncode ? Ext.util.Format.htmlEncode(value) : value;
        }
        return display;
    },
    getSubTplData: function(fieldData) {
        var ret = this.callParent(arguments);
        ret.value = this.getDisplayValue();
        return ret;
    }
});
/**
     * @cfg {String} inputType
     * @private
     */
/**
     * @cfg {Boolean} disabled
     * @private
     */
/**
     * @cfg {Number} checkChangeEvents
     * @private
     */
/**
     * @cfg {Number} checkChangeBuffer
     * @private
     */

/**
 *
 */
Ext.define('Ext.form.field.FileButton', {
    extend: 'Ext.button.Button',
    alias: 'widget.filebutton',
    childEls: [
        'fileInputEl'
    ],
    inputCls: Ext.baseCSSPrefix + 'form-file-input',
    cls: Ext.baseCSSPrefix + 'form-file-btn',
    preventDefault: false,
    // Button element *looks* focused but it should never really receive focus itself,
    // and with it being a <div></div> we don't need to render tabindex attribute at all
    tabIndex: undefined,
    // IE and Edge implement File input as two elements: text box and a button,
    // both are focusable and have a tab stop. Since we make file input transparent,
    // this results in users having to press Tab key twice with no visible action
    // just to go past our File input widget. There is no way to configure this behavior.
    // The workaround is as follows: we place two tabbable elements around the file input,
    // and forward the focus to the file input element whenever either guard is tabbed
    // into. We also intercept Tab keydown events on the file input, and fudge focus
    // before keyup so that when default action happens the focus will go outside
    // of the widget just like it should.
    // This mechanism is quite similar to what we use in Window component for trapping
    // focus, and in floating mixin to allow tabbing out of the floater.
    useTabGuards: Ext.isIE || Ext.isEdge,
    promptCalled: false,
    autoEl: {
        tag: 'div',
        unselectable: 'on'
    },
    /* eslint-disable indent */
    /*
     * This <input type="file"/> element is placed above the button element to intercept
     * mouse clicks, as well as receive focus. This is the only way to make browser file input
     * dialog open on user action, and populate the file input value when file(s) are selected.
     * The tabIndex value here comes from the template arguments generated in getTemplateArgs
     * method below; it is copied from the owner FileInput's tabIndex property.
     */
    afterTpl: [
        '<input id="{id}-fileInputEl" data-ref="fileInputEl" class="{childElCls} {inputCls}" ',
        'type="file" size="1" name="{inputName}" unselectable="on" ',
        '<tpl if="accept != null">accept="{accept}"</tpl>',
        '<tpl if="tabIndex != null">tabindex="{tabIndex}"</tpl>',
        '>'
    ],
    /* eslint-enable indent */
    keyMap: null,
    ariaEl: 'fileInputEl',
    /**
     * @private
     */
    getAfterMarkup: function(values) {
        return this.lookupTpl('afterTpl').apply(values);
    },
    getTemplateArgs: function() {
        var me = this,
            args;
        args = me.callParent();
        args.inputCls = me.inputCls;
        args.inputName = me.inputName || me.id;
        args.tabIndex = me.tabIndex != null ? me.tabIndex : null;
        args.accept = me.accept || null;
        args.role = me.ariaRole;
        return args;
    },
    afterRender: function() {
        var me = this,
            listeners, cfg;
        me.callParent(arguments);
        // We place focus and blur listeners on fileInputEl to activate Button's
        // focus and blur style treatment
        listeners = {
            scope: me,
            mousedown: me.handlePrompt,
            keydown: me.handlePrompt,
            change: me.fireChange,
            focus: me.onFileFocus,
            blur: me.onFileBlur,
            destroyable: true
        };
        if (me.useTabGuards) {
            cfg = {
                tag: 'span',
                role: 'button',
                'aria-hidden': 'true',
                'data-tabguard': 'true',
                style: {
                    height: 0,
                    width: 0
                }
            };
            cfg.tabIndex = me.tabIndex != null ? me.tabIndex : 0;
            // We are careful about inserting tab guards *around* the fileInputEl.
            // Keep in mind that IE8/9 have framed buttons so DOM structure
            // can be complex.
            me.beforeInputGuard = me.el.createChild(cfg, me.fileInputEl);
            me.afterInputGuard = me.el.createChild(cfg);
            me.afterInputGuard.insertAfter(me.fileInputEl);
            me.beforeInputGuard.on('focus', me.onInputGuardFocus, me);
            me.afterInputGuard.on('focus', me.onInputGuardFocus, me);
            listeners.keydown = me.onFileInputKeydown;
        }
        me.fileInputElListeners = me.fileInputEl.on(listeners);
    },
    doDestroy: function() {
        var me = this;
        if (me.fileInputElListeners) {
            me.fileInputElListeners.destroy();
        }
        if (me.beforeInputGuard) {
            me.beforeInputGuard.destroy();
            me.beforeInputGuard = null;
        }
        if (me.afterInputGuard) {
            me.afterInputGuard.destroy();
            me.afterInputGuard = null;
        }
        me.callParent();
    },
    fireChange: function(e) {
        this.fireEvent('change', this, e, this.fileInputEl.dom.value);
    },
    /**
     * @private
     * Creates the file input element. It is inserted into the trigger button component, made
     * invisible, and floated on top of the button's other content so that it will receive the
     * button's clicks.
     */
    createFileInput: function(isTemporary) {
        var me = this,
            fileInputEl, listeners;
        fileInputEl = me.fileInputEl = me.el.createChild({
            name: me.inputName || me.id,
            id: !isTemporary ? me.id + '-fileInputEl' : undefined,
            cls: me.inputCls + (me.getInherited().rtl ? ' ' + Ext.baseCSSPrefix + 'rtl' : ''),
            tag: 'input',
            type: 'file',
            size: 1,
            unselectable: 'on'
        }, me.afterInputGuard);
        // Nothing special happens outside of IE/Edge
        // This is our focusEl
        fileInputEl.dom.setAttribute('data-componentid', me.id);
        if (me.tabIndex != null) {
            me.setTabIndex(me.tabIndex);
        }
        if (me.accept) {
            fileInputEl.dom.setAttribute('accept', me.accept);
        }
        // We place focus and blur listeners on fileInputEl to activate Button's
        // focus and blur style treatment
        listeners = {
            scope: me,
            change: me.fireChange,
            mousedown: me.handlePrompt,
            keydown: me.handlePrompt,
            focus: me.onFileFocus,
            blur: me.onFileBlur
        };
        if (me.useTabGuards) {
            listeners.keydown = me.onFileInputKeydown;
        }
        fileInputEl.on(listeners);
    },
    handlePrompt: function(e) {
        var key;
        if (e.type === 'keydown') {
            key = e.getKey();
            // We need this conditional here because IE doesn't open the prompt on ENTER
            this.promptCalled = ((!Ext.isIE && key === e.ENTER) || key === e.SPACE) ? true : false;
        } else {
            this.promptCalled = true;
        }
    },
    onFileFocus: function(e) {
        var ownerCt = this.ownerCt;
        if (!this.hasFocus) {
            this.onFocus(e);
        }
        if (ownerCt && !ownerCt.hasFocus) {
            ownerCt.onFocus(e);
        }
    },
    onFileBlur: function(e) {
        var ownerCt = this.ownerCt;
        // We should not go ahead with blur if this was called because
        // the fileInput was clicked and the upload window is causing this event
        if (this.promptCalled) {
            this.promptCalled = false;
            e.preventDefault();
            return;
        }
        if (this.hasFocus) {
            this.onBlur(e);
        }
        if (ownerCt && ownerCt.hasFocus) {
            ownerCt.onBlur(e);
        }
    },
    onInputGuardFocus: function(e) {
        this.fileInputEl.focus();
    },
    onFileInputKeydown: function(e) {
        var key = e.getKey(),
            focusTo;
        if (key === e.TAB) {
            focusTo = e.shiftKey ? this.beforeInputGuard : this.afterInputGuard;
            if (focusTo) {
                // We need to skip the next focus to avoid it bouncing back
                // to the input field.
                focusTo.suspendEvent('focus');
                focusTo.focus();
                // In IE focus events are asynchronous so we can't enable focus event
                // in the same event loop.
                Ext.defer(function() {
                    focusTo.resumeEvent('focus');
                }, 1);
            }
        } else if (key === e.ENTER || key === e.SPACE) {
            this.handlePrompt(e);
        }
        // Returning true will allow the event to take default action
        return true;
    },
    reset: function(remove) {
        var me = this;
        if (remove) {
            me.fileInputEl.destroy();
        }
        me.createFileInput(!remove);
        if (remove) {
            me.ariaEl = me.fileInputEl;
        }
    },
    restoreInput: function(el) {
        var me = this;
        me.fileInputEl.destroy();
        el = Ext.get(el);
        if (me.useTabGuards) {
            el.insertBefore(me.afterInputGuard);
        } else {
            me.el.appendChild(el);
        }
        me.fileInputEl = el;
    },
    onDisable: function() {
        this.callParent();
        this.fileInputEl.dom.disabled = true;
    },
    onEnable: function() {
        this.callParent();
        this.fileInputEl.dom.disabled = false;
    },
    privates: {
        getFocusEl: function() {
            return this.fileInputEl;
        },
        getFocusClsEl: function() {
            return this.el;
        },
        setTabIndex: function(tabIndex) {
            var me = this;
            if (!me.focusable) {
                return;
            }
            me.tabIndex = tabIndex;
            if (!me.rendered || me.destroying || me.destroyed) {
                return;
            }
            if (me.useTabGuards) {
                me.fileInputEl.dom.setAttribute('tabIndex', -1);
                me.beforeInputGuard.dom.setAttribute('tabIndex', tabIndex);
                me.afterInputGuard.dom.setAttribute('tabIndex', tabIndex);
            } else {
                me.fileInputEl.dom.setAttribute('tabIndex', tabIndex);
            }
        }
    }
});

/**
 * A Text Field Trigger that contains a {@link Ext.Component Component} or {@link
 * Ext.Widget Widget}.
 * @private
 */
Ext.define('Ext.form.trigger.Component', {
    extend: 'Ext.form.trigger.Trigger',
    alias: 'trigger.component',
    cls: Ext.baseCSSPrefix + 'form-trigger-cmp',
    /**
     * @cfg {Object/Ext.Component/Ext.Widget} component A config object for a Component or Widget,
     * or an already instantiated Component or Widget.
     */
    /**
     * @property {Ext.Component/Ext.Widget} component The component or widget
     * @readonly
     */
    onFieldRender: function() {
        var me = this,
            component = me.component;
        me.callParent();
        if (!component.isComponent && !component.isWidget) {
            component = Ext.widget(component);
        }
        me.component = component;
        component.render(me.el);
    },
    destroy: function() {
        var component = this.component;
        if (component.isComponent || component.isWidget) {
            component.destroy();
        }
        this.component = null;
        this.callParent();
    }
});

/**
 * A file upload field which has custom styling and allows control over the button text and other
 * features of {@link Ext.form.field.Text text fields} like
 * {@link Ext.form.field.Text#emptyText empty text}. It uses a hidden file input element
 * behind the scenes to allow user selection of a file and to perform the actual upload
 * during {@link Ext.form.Basic#submit form submit}.
 *
 * Because there is no secure cross-browser way to programmatically set the value of a file input,
 * the standard Field `setValue` method is not implemented. The `{@link #getValue}` method
 * will return a value that is browser-dependent; some have just the file name,
 * some have a full path, some use a fake path.
 *
 * **IMPORTANT:** File uploads are not performed using normal 'Ajax' techniques;
 * see the description for {@link Ext.form.Basic#hasUpload} for details.
 *
 * # Example Usage
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Upload a Photo',
 *         width: 400,
 *         bodyPadding: 10,
 *         frame: true,
 *         renderTo: Ext.getBody(),
 *         items: [{
 *             xtype: 'filefield',
 *             name: 'photo',
 *             fieldLabel: 'Photo',
 *             labelWidth: 50,
 *             msgTarget: 'side',
 *             allowBlank: false,
 *             anchor: '100%',
 *             buttonText: 'Select Photo...'
 *         }],
 *
 *         buttons: [{
 *             text: 'Upload',
 *             handler: function() {
 *                 var form = this.up('form').getForm();
 *                 if(form.isValid()) {
 *                     form.submit({
 *                         url: 'photo-upload.php',
 *                         waitMsg: 'Uploading your photo...',
 *                         success: function(fp, o) {
 *                             Ext.Msg.alert('Success', 'Your photo "' + o.result.file +
 *                                                      '" has been uploaded.');
 *                         }
 *                     });
 *                 }
 *             }
 *         }]
 *     });
 */
Ext.define('Ext.form.field.File', {
    extend: 'Ext.form.field.Text',
    alias: [
        'widget.filefield',
        'widget.fileuploadfield'
    ],
    alternateClassName: [
        'Ext.form.FileUploadField',
        'Ext.ux.form.FileUploadField',
        'Ext.form.File'
    ],
    requires: [
        'Ext.form.field.FileButton',
        'Ext.form.trigger.Component'
    ],
    /**
     * @cfg {String} [accept] An optional list of file MIME types accepted by this field.
     * This string will be rendered in to the `accept` attribute of the file input and should
     * conform to HTML requirements: http://www.w3.org/TR/html-markup/input.file.html
     * 
     * @since 6.2.0
     */
    /**
     * @cfg {String} emptyText
     * Overridden to undefined as {@link #emptyText} is not supported with
     * {@link #inputType inputType}:'file' and should be avoided.
     * The default text to place into an empty field.
     */
    emptyText: undefined,
    needArrowKeys: false,
    /**
     * @cfg triggers
     * @inheritdoc
     */
    triggers: {
        filebutton: {
            type: 'component',
            hideOnReadOnly: false,
            // Most form fields prevent the default browser action on mousedown of the trigger.
            // This is intended to prevent the field's input element from losing focus when
            // the trigger is clicked.  File fields disable this behavior because:
            // 1. The input element does not receive focus when the field is focused.
            // The button does.
            // 2. Preventing the default action of touchstart (translated from mousedown
            // on mobile browsers) prevents the browser's file dialog from opening.
            preventMouseDown: false
        }
    },
    /**
     * @cfg {String} buttonText
     * The button text to display on the upload button. Note that if you supply a value for
     * {@link #buttonConfig}, the buttonConfig.text value will be used instead if available.
     * @locale
     */
    buttonText: 'Browse...',
    /**
     * @cfg {Boolean} buttonOnly
     * True to display the file upload field as a button with no visible text field. If true, all
     * inherited Text members will still be available.
     */
    buttonOnly: false,
    /**
     * @cfg {Number} buttonMargin
     * The number of pixels of space reserved between the button and the text field. Note that this
     * only applies if {@link #buttonOnly} = false.
     */
    buttonMargin: 3,
    /**
     * @cfg {Boolean} clearOnSubmit
     * True to clear the selected file value when the form this field belongs to
     * is submitted to the server.
     */
    clearOnSubmit: true,
    /**
     * @cfg {Object} buttonConfig
     * Specify optional custom button {@link Ext.button.Button} config (eg. iconCls, text)
     * for the upload button
     */
    /**
     * @event change
     * Fires when the underlying file input field's value has changed from the user selecting
     * a new file from the system file selection dialog.
     * @param {Ext.ux.form.FileUploadField} this
     * @param {String} value The file value returned by the underlying file input field
     */
    /**
     * @property {Ext.dom.Element} fileInputEl
     * A reference to the invisible file input element created for this upload field.
     * Only populated after this component is rendered.
     */
    /**
     * @property {Ext.button.Button} button
     * A reference to the trigger Button component created for this upload field.
     * Only populated after this component is rendered.
     */
    /**
     * @private
     */
    extraFieldBodyCls: Ext.baseCSSPrefix + 'form-file-wrap',
    /**
     * @private
     */
    inputCls: Ext.baseCSSPrefix + 'form-text-file',
    /**
     * @cfg {Boolean} readOnly
     * Unlike with other form fields, the readOnly config defaults to true in File field.
     */
    readOnly: true,
    /**
     * @cfg editable
     * @inheritdoc
     */
    editable: false,
    /**
     * @cfg submitValue
     * @inheritdoc
     */
    submitValue: false,
    /**
     * Do not show hand pointer over text field since file choose dialog is only shown
     * when clicking in the button
     * @private
     */
    triggerNoEditCls: '',
    /**
     * @private
     * Extract the file element, button outer element, and button active element.
     */
    childEls: [
        'browseButtonWrap'
    ],
    /**
     * @private
     */
    applyTriggers: function(triggers) {
        var me = this,
            triggerCfg = (triggers || {}).filebutton;
        if (triggerCfg) {
            triggerCfg.component = Ext.apply({
                xtype: 'filebutton',
                ownerCt: me,
                id: me.id + '-button',
                ui: me.ui,
                disabled: me.disabled,
                tabIndex: me.tabIndex,
                text: me.buttonText,
                style: me.buttonOnly ? '' : me.getButtonMarginProp() + me.buttonMargin + 'px',
                accept: me.accept,
                inputName: me.getName(),
                listeners: {
                    scope: me,
                    change: me.onFileChange
                }
            }, me.buttonConfig);
            return me.callParent([
                triggers
            ]);
        } else {
            Ext.raise(me.$className + ' requires a valid trigger config containing ' + '"filebutton" specification');
        }
    },
    getSubTplData: function(fieldData) {
        var data = this.callParent([
                fieldData
            ]);
        // Input field itself should not be focusable since it's always decorative;
        // however the input element is naturally focusable (and tabbable) so we have to
        // deactivate it by setting its tabIndex to -1.
        data.tabIdx = -1;
        return data;
    },
    /**
     * @private
     */
    onRender: function() {
        var me = this,
            inputEl, button, buttonEl, trigger;
        me.callParent(arguments);
        inputEl = me.inputEl;
        // name goes on the fileInput, not the text input
        inputEl.dom.name = '';
        // Some browsers will show a blinking cursor in the field, even if it's readonly.
        // If we do happen to receive focus, forward it on to our focusEl.
        inputEl.on('focus', me.onInputFocus, me);
        inputEl.on('mousedown', me.onInputMouseDown, me);
        trigger = me.getTrigger('filebutton');
        button = me.button = trigger.component;
        me.fileInputEl = button.fileInputEl;
        buttonEl = button.el;
        if (me.buttonOnly) {
            me.inputWrap.setDisplayed(false);
            me.shrinkWrap = 3;
        }
        // Ensure the trigger element is sized correctly upon render
        trigger.el.setWidth(buttonEl.getWidth() + buttonEl.getMargin('lr'));
        if (Ext.isIE8) {
            me.button.getEl().repaint();
        }
    },
    /**
     * Gets the markup to be inserted into the subTplMarkup.
     */
    getTriggerMarkup: function() {
        return '<td id="' + this.id + '-browseButtonWrap" data-ref="browseButtonWrap" role="presentation"></td>';
    },
    /**
     * @private
     * Event handler fired when the user selects a file.
     */
    onFileChange: function(button, e, value) {
        this.duringFileSelect = true;
        Ext.form.field.File.superclass.setValue.call(this, value);
        delete this.duringFileSelect;
    },
    didValueChange: function() {
        // In the case of the file field, the change event will only ever fire 
        // if the value actually changes, so we always want to fire the change event
        // This affects Chrome specifically, because hitting the cancel button will
        // reset the file upload.
        return !!this.duringFileSelect;
    },
    /**
     * @method setEmptyText
     * @hide
     */
    setEmptyText: Ext.emptyFn,
    /**
     * @method setValue
     * @hide
     */
    setValue: Ext.emptyFn,
    reset: function() {
        var me = this,
            clear = me.clearOnSubmit;
        if (me.rendered) {
            me.button.reset(clear);
            me.fileInputEl = me.button.fileInputEl;
            if (clear) {
                me.inputEl.dom.value = '';
                // Reset the underlying value if we're clearing it
                Ext.form.field.File.superclass.setValue.call(this, null);
            }
        }
        me.callParent();
    },
    onShow: function() {
        this.callParent();
        // If we started out hidden, the button may have a messed up layout
        // since we don't act like a container
        this.button.updateLayout();
    },
    onDisable: function() {
        this.callParent();
        this.button.disable();
    },
    onEnable: function() {
        this.callParent();
        this.button.enable();
    },
    /**
     * @method isFileUpload
     * @inheritdoc
     */
    isFileUpload: Ext.returnTrue,
    extractFileInput: function() {
        var me = this,
            fileInput;
        if (me.rendered) {
            fileInput = me.button.fileInputEl.dom;
            me.reset();
        } else {
            // Create a fake empty field here so it will still be submitted.
            // All other unrendered fields provide a value.
            fileInput = document.createElement('input');
            fileInput.type = 'file';
            fileInput.className = Ext.baseCSSPrefix + 'hidden-display';
            fileInput.name = me.getName();
        }
        return fileInput;
    },
    restoreInput: function(el) {
        var button;
        // If we're not rendered we don't need to do anything, it will be created
        // when we get flushed to the DOM.
        if (this.rendered) {
            button = this.button;
            button.restoreInput(el);
            this.fileInputEl = button.fileInputEl;
        }
    },
    doDestroy: function() {
        this.fileInputEl = this.button = null;
        this.callParent();
    },
    getButtonMarginProp: function() {
        return this.getInherited().rtl ? 'margin-right:' : 'margin-left:';
    },
    onInputFocus: function(e) {
        var me = this;
        // Text field onFocus() won't call select() so we need to duplicate it here
        if (me.selectOnFocus && document.activeElement === me.inputEl.dom) {
            me.inputEl.dom.select();
        }
        me.focus();
        // Switching focus from read only input element to file input
        // results in incorrect positioning of the file input.
        // Adding and removing position: relative helps to fix that.
        // See https://sencha.jira.com/browse/EXTJS-18933
        if (Ext.isIE9m) {
            me.fileInputEl.addCls(Ext.baseCSSPrefix + 'position-relative');
            me.fileInputEl.removeCls(Ext.baseCSSPrefix + 'position-relative');
        }
    },
    onInputMouseDown: function(e) {
        // Some browsers will show the cursor even if the input is read only,
        // which will be visible in the short instant between inputEl focusing
        // and subsequent focus jump to the FileButton. Preventing inputEl from
        // focusing eliminates that flicker.
        e.preventDefault();
        this.focus();
    },
    privates: {
        getFocusEl: function() {
            return this.button;
        },
        getFocusClsEl: Ext.privateFn
    }
});

/**
 * A basic hidden field for storing hidden values in forms that need to be passed in the form
 * submit.
 *
 * This creates an actual input element with type="hidden" in the DOM. While its label is
 * {@link #hideLabel not rendered} by default, it is still a real component and may be sized
 * according to its owner container's layout.
 *
 * Because of this, in most cases it is more convenient and less problematic to simply
 * {@link Ext.form.action.Action#params pass hidden parameters} directly when
 * {@link Ext.form.Basic#submit submitting the form}.
 *
 * Example:
 *
 *     @example
 *     new Ext.form.Panel({
 *         title: 'My Form',
 *         items: [{
 *             xtype: 'textfield',
 *             fieldLabel: 'Text Field',
 *             name: 'text_field',
 *             value: 'value from text field'
 *         }, {
 *             xtype: 'hiddenfield',
 *             name: 'hidden_field_1',
 *             value: 'value from hidden field'
 *         }],
 *
 *         buttons: [{
 *             text: 'Submit',
 *             handler: function() {
 *                 this.up('form').getForm().submit({
 *                     params: {
 *                         hidden_field_2: 'value from submit call'
 *                     }
 *                 });
 *             }
 *         }]
 *     });
 *
 * Submitting the above form will result in three values sent to the server:
 *
 *     text_field=value+from+text+field&hidden;_field_1=value+from+hidden+field&
 *     hidden_field_2=value+from+submit+call
 *
 */
Ext.define('Ext.form.field.Hidden', {
    extend: 'Ext.form.field.Base',
    alias: [
        'widget.hiddenfield',
        'widget.hidden'
    ],
    alternateClassName: 'Ext.form.Hidden',
    focusable: false,
    inputType: 'hidden',
    isTextInput: false,
    hideLabel: true,
    hidden: true,
    ariaRole: 'presentation',
    initComponent: function() {
        this.formItemCls += '-hidden';
        this.callParent();
    },
    /**
     * @private
     * Override. Treat undefined and null values as equal to an empty string value.
     */
    isEqual: function(value1, value2) {
        return this.isEqualAsString(value1, value2);
    },
    initEvents: Ext.emptyFn,
    /**
     * @method
     * @hide
     */
    setSize: Ext.emptyFn,
    /**
     * @method
     * @hide
     */
    setWidth: Ext.emptyFn,
    /**
     * @method
     * @hide
     */
    setHeight: Ext.emptyFn,
    /**
     * @method
     * @hide
     */
    setPosition: Ext.emptyFn,
    /**
     * @method
     * @hide
     */
    setPagePosition: Ext.emptyFn,
    /**
     * @method
     * @hide
     */
    markInvalid: Ext.emptyFn,
    /**
     * @method
     * @hide
     */
    clearInvalid: Ext.emptyFn
});

/**
 * This is the base class for {@link Ext.tip.QuickTip} and {@link Ext.tip.ToolTip} that provides
 * the basic layout and positioning that all tip-based classes require. This class can be used
 * directly for simple, statically-positioned tips that are displayed programmatically,
 * or it can be extended to provide custom tip implementations.
 */
Ext.define('Ext.tip.Tip', {
    extend: 'Ext.panel.Panel',
    xtype: 'tip',
    alternateClassName: 'Ext.Tip',
    /**
     * @cfg {Boolean} [closable=false]
     * True to render a close tool button into the tooltip header.
     */
    /**
     * @cfg {Number} [width='auto']
     * Width in pixels of the tip.  Width will be ignored if it
     * exceeds the bounds of {@link #minWidth} or {@link #maxWidth}.
     */
    /**
     * @cfg {Number} minWidth
     * The minimum width of the tip in pixels.
     */
    minWidth: 40,
    /**
     * @cfg {Number} maxWidth
     * The maximum width of the tip in pixels.
     */
    maxWidth: 500,
    /**
     * @cfg {Boolean/String} shadow
     * `true` or "sides" for the default effect, "frame" for 4-way shadow, and "drop"
     * for bottom-right shadow.
     */
    shadow: "sides",
    /**
     * @cfg {Boolean} constrainPosition
     * If `true`, then the tooltip will be automatically constrained to stay within
     * the browser viewport.
     */
    constrainPosition: true,
    autoRender: true,
    hidden: true,
    baseCls: Ext.baseCSSPrefix + 'tip',
    focusOnToFront: false,
    maskOnDisable: false,
    /**
     * @cfg {String} closeAction
     * The action to take when the close header tool is clicked:
     *
     * - **{@link #method-destroy}** : {@link #method-remove remove} the window from the DOM and
     *   {@link Ext.Component#method-destroy destroy} it and all descendant Components. The
     *   window will **not** be available to be redisplayed via the {@link #method-show} method.
     *
     * - **{@link #method-hide}** : **Default.** {@link #method-hide} the window by setting
     *   isibility to hidden and applying negative offsets. The window will be available to be
     *   redisplayed via the {@link #method-show} method.
     *
     * **Note:** This behavior has changed! setting *does* affect the {@link #method-close} method
     * which will invoke the approriate closeAction.
     */
    closeAction: 'hide',
    // Flag to Renderable to always look up the framing styles for this Component
    alwaysFramed: true,
    frameHeader: false,
    initComponent: function() {
        var me = this;
        me.floating = Ext.apply({}, {
            shadow: me.shadow
        }, me.self.prototype.floating);
        me.callParent(arguments);
        // Or in the deprecated config. Floating.doConstrain only constrains
        // if the constrain property is truthy.
        me.constrain = me.constrain || me.constrainPosition;
    },
    /**
     * Shows this tip at the specified XY position.  Example usage:
     *
     *     // Show the tip at x:50 and y:100
     *     tip.showAt([50,100]);
     *
     * @param {Number[]} xy An array containing the x and y coordinates
     */
    showAt: function(xy) {
        var me = this;
        me.calledFromShowAt = true;
        me.callParent(arguments);
        // Show may have been vetoed.
        if (me.isVisible()) {
            me.doAlignment(me.getRegion().alignTo({
                target: new Ext.util.Point(xy[0], xy[1]),
                inside: me.constrainPosition ? Ext.getBody().getRegion().adjust(5, -5, -5, 5) : null,
                align: 'tl-tl',
                overlap: true
            }));
        }
        me.calledFromShowAt = 0;
    },
    doAlignment: function(newRegion) {
        var me = this,
            anchorEl = me.anchorEl,
            anchorRegion = newRegion.anchor;
        me.setPagePosition([
            newRegion.x,
            newRegion.y
        ]);
        if (anchorEl) {
            anchorEl.removeCls(me.anchorCls);
            if (anchorRegion) {
                me.anchorCls = Ext.baseCSSPrefix + 'tip-anchor-' + anchorRegion.position;
                anchorEl.addCls(me.anchorCls);
                anchorEl.show();
                // The result is to the left or right of the target
                if (anchorRegion.align & 1) {
                    anchorEl.setTop(newRegion.anchor.y - newRegion.y);
                    anchorEl.dom.style.left = '';
                } else {
                    anchorEl.setLeft(newRegion.anchor.x - newRegion.x);
                    anchorEl.dom.style.top = '';
                }
            } else {
                anchorEl.hide();
            }
        }
    },
    privates: {
        /**
         * @private
         * Set Tip draggable using base Component's draggability.
         */
        initDraggable: function() {
            var me = this;
            me.draggable = {
                el: me.getDragEl(),
                delegate: me.header.el,
                constrain: me,
                constrainTo: me.el.dom.parentNode
            };
            // Important: Bypass Panel's initDraggable. Call direct to Component's implementation.
            Ext.Component.prototype.initDraggable.call(me);
        }
    },
    // Tip does not ghost. Drag is "live"
    ghost: undefined,
    unghost: undefined
});

/**
 * ToolTip is a {@link Ext.tip.Tip} implementation that handles the common case of displaying a
 * tooltip when hovering over a certain element or elements on the page. It allows fine-grained
 * control over the tooltip's alignment relative to the target element or mouse, and the timing
 * of when it is automatically shown and hidden.
 *
 * This implementation does **not** have a built-in method of automatically populating the tooltip's
 * text based on the target element; you must either configure a fixed {@link #html} value for each
 * ToolTip instance, or implement custom logic (e.g. in a {@link #beforeshow} event listener) to
 * generate the appropriate tooltip content on the fly. See {@link Ext.tip.QuickTip} for a more
 * convenient way of automatically populating and configuring a tooltip based on specific DOM
 * attributes of each target element.
 *
 * # Basic Example
 *
 *     @example
 *     Ext.getBody().appendChild({
 *         id: 'clearButton',
 *         html: 'Clear Button',
 *         style: 'display:inline-block;background:#A2C841;padding:7px;cursor:pointer;'
 *     });
 *
 *     var tip = Ext.create('Ext.tip.ToolTip', {
 *         target: 'clearButton',
 *         html: 'Press this button to clear the form'
 *     });
 *
 * # Delegation
 *
 * In addition to attaching a ToolTip to a single element, you can also use delegation to attach
 * one ToolTip to many elements under a common parent. This is more efficient than creating many
 * ToolTip instances. To do this, point the {@link #target} config to a common ancestor of all the
 * elements, and then set the {@link #delegate} config to a CSS selector that will select all the
 * appropriate sub-elements.
 *
 * When using delegation, it is likely that you will want to programmatically change the content
 * of the ToolTip based on each delegate element; you can do this by implementing a custom
 * listener for the {@link #beforeshow} event. Example:
 *
 *     @example
 *     var store = Ext.create('Ext.data.ArrayStore', {
 *         fields: ['company', 'price', 'change'],
 *         data: [
 *             ['3m Co',                               71.72, 0.02],
 *             ['Alcoa Inc',                           29.01, 0.42],
 *             ['Altria Group Inc',                    83.81, 0.28],
 *             ['American Express Company',            52.55, 0.01],
 *             ['American International Group, Inc.',  64.13, 0.31],
 *             ['AT&T Inc.',                           31.61, -0.48]
 *         ]
 *     });
 *
 *     var grid = Ext.create('Ext.grid.Panel', {
 *         title: 'Array Grid',
 *         store: store,
 *         columns: [
 *             {text: 'Company', flex: 1, dataIndex: 'company'},
 *             {text: 'Price', width: 75, dataIndex: 'price'},
 *             {text: 'Change', width: 75, dataIndex: 'change'}
 *         ],
 *         height: 200,
 *         width: 400,
 *         renderTo: Ext.getBody()
 *     });
 *
 *     var view = grid.getView();
 *     var tip = Ext.create('Ext.tip.ToolTip', {
 *         // The overall target element.
 *         target: view.el,
 *         // Each grid row causes its own separate show and hide.
 *         delegate: view.itemSelector,
 *         // Moving within the row should not hide the tip.
 *         trackMouse: true,
 *         // Render immediately so that tip.body can be referenced prior to the first show.
 *         renderTo: Ext.getBody(),
 *         listeners: {
 *             // Change content dynamically depending on which element triggered the show.
 *             beforeshow: function updateTipBody(tip) {
 *                 tip.update('Over company "' + view.getRecord(tip.triggerElement).get('company') +
 *                            '"');
 *             }
 *         }
 *     });
 *
 * # Alignment
 *
 * The following configuration properties allow control over how the ToolTip is aligned relative to
 * the target element and/or mouse pointer:
 *
 * - {@link #anchor}
 * - {@link #anchorToTarget}
 * - {@link #trackMouse}
 * - {@link #mouseOffset}
 *
 * # Showing/Hiding
 *
 * The following configuration properties allow control over how and when the ToolTip
 * is automatically shown and hidden:
 *
 * - {@link #autoHide}
 * - {@link #showDelay}
 * - {@link #hideDelay}
 * - {@link #dismissDelay}
 */
Ext.define('Ext.tip.ToolTip', {
    extend: 'Ext.tip.Tip',
    alias: 'widget.tooltip',
    alternateClassName: 'Ext.ToolTip',
    requires: [
        'Ext.util.Offset'
    ],
    /**
     * @property {HTMLElement} triggerElement
     * When a ToolTip is configured with the `{@link #delegate}`
     * option to cause selected child elements of the `{@link #target}`
     * Element to each trigger a separate show event, this property is set to
     * the DOM element which triggered the show.
     */
    /**
     * @cfg {HTMLElement/Ext.dom.Element/String} target
     * The target element or string id to monitor for mouseover events to trigger
     * showing this ToolTip.
     */
    /**
     * @cfg {Boolean} [autoHide=true]
     * True to automatically hide the tooltip after the
     * mouse exits the target element or after the `{@link #dismissDelay}`
     * has expired if set.  If `{@link #closable} = true`
     * a close tool button will be rendered into the tooltip header.
     */
    autoHide: true,
    /**
     * @cfg {Number} showDelay
     * Delay in milliseconds before the tooltip displays after the mouse enters the target element.
     */
    showDelay: 500,
    /**
     * @cfg {Number} hideDelay
     * Delay in milliseconds after the mouse exits the target element but before the tooltip
     * actually hides. Set to 0 for the tooltip to hide immediately.
     */
    hideDelay: 200,
    /**
     * @cfg {Number} dismissDelay
     * Delay in milliseconds before the tooltip automatically hides. To disable automatic hiding,
     * set dismissDelay = 0.
     */
    dismissDelay: 5000,
    /**
     * @cfg {Number[]} [targetOffset=[0, 0]]
     * When {@link #anchorToTarget} is being used to position this tip relative to its target
     * element, this may be used as an extra XY offset from the target element.
     */
    /**
     * @cfg {Number[]} [mouseOffset=[15, 18]]
     * An XY offset from the mouse position where the tooltip should be shown.
     */
    mouseOffset: [
        15,
        18
    ],
    /**
     * @cfg {Boolean} trackMouse
     * True to have the tooltip follow the mouse as it moves over the target element.
     */
    trackMouse: false,
    /**
     * @cfg {String} anchor
     * If specified, indicates that the tip should be anchored to a
     * particular side of the target element or mouse pointer ("top", "right", "bottom",
     * or "left"), with an arrow pointing back at the target or mouse pointer. If
     * {@link #constrainPosition} is enabled, this will be used as a preferred value
     * only and may be flipped as needed.
     */
    /**
     * @cfg {Boolean} anchorToTarget
     * True to anchor the tooltip to the target element, false to anchor it relative to the mouse
     * coordinates. When `anchorToTarget` is true, use `{@link #defaultAlign}` to control tooltip
     * alignment to the target element. When `anchorToTarget` is false, use `{@link #anchor}`
     * instead to control alignment.
     */
    anchorToTarget: true,
    /**
     * @cfg {String} delegate
     *
     * A {@link Ext.DomQuery DomQuery} simple selector which allows selection of individual elements
     * within the `{@link #target}` element to trigger showing and hiding the ToolTip as the mouse
     * moves within the target. See {@link Ext.dom.Query} for information about simple selectors.
     *
     * When specified, the child element of the target which caused a show event is placed into the
     * `{@link #triggerElement}` property before the ToolTip is shown.
     *
     * This may be useful when a Component has regular, repeating elements in it, each of which need
     * a ToolTip which contains information specific to that element.
     *
     * See the delegate example in class documentation of {@link Ext.tip.ToolTip}.
     */
    /**
     * @cfg {Boolean} [showOnTap=false]
     * On touch platforms, if {@link #showOnTap} is `true`, a tap on the target shows the tip.
     * In this case any {@link #showDelay} is ignored.
     *
     * This is useful for adding tips on elements which do not have tap listeners. It would
     * not be appropriate for a ToolTip on a {@link Ext.Button Button}.
     */
    /**
     * @private
     */
    targetCounter: 0,
    quickShowInterval: 250,
    /**
     * @cfg {String} [hideAction="hide"]
     * The method to use to hide the tooltip. Another useful method for this is `fadeOut`.
     */
    hideAction: 'hide',
    /**
     * @cfg {Number} [fadeOutDuration=1000]
     * The number of milliseconds for the `fadeOut` animation. Only valid if `hideAction`
     * is set to `fadeOut`.
     */
    fadeOutDuration: 1000,
    /**
     * @cfg {String} defaultAlign
     * A string which specifies how this ToolTip is to align with regard to its
     * {@link #currentTarget} by means of identifying the point of the tooltip to
     * join to the point of the target.
     *
     * By default, the tooltip shows at {@link #mouseOffset} pixels from the
     * triggering pointer event. Using this config anchors the ToolTip to its target
     * instead.
     *
     * This may take the following forms:
     * 
     * - **Blank**: Defaults to aligning the element's top-left corner to the target's
     *   bottom-left corner ("tl-bl").
     * - **Two anchors**: If two values from the table below are passed separated by a dash,
     *   the first value is used as the element's anchor point, and the second value is
     *   used as the target's anchor point.
     * - **Two edge/offset descriptors:** An edge/offset descriptor is an edge initial
     *   (`t`/`r`/`b`/`l`) followed by a percentage along that side. This describes a
     *   point to align with a similar point in the target. So `'t0-b0'` would be
     *   the same as `'tl-bl'`, `'l0-r50'` would place the top left corner of this item
     *   halfway down the right edge of the target item. This allows more flexibility
     *   and also describes which two edges are considered adjacent when positioning a tip pointer. 
     *
     * Following are all of the supported predefined anchor positions:
     *
     *      Value  Description
     *      -----  -----------------------------
     *      tl     The top left corner
     *      t      The center of the top edge
     *      tr     The top right corner
     *      l      The center of the left edge
     *      c      The center
     *      r      The center of the right edge
     *      bl     The bottom left corner
     *      b      The center of the bottom edge
     *      br     The bottom right corner
     *
     * You can put a '?' at the end of the alignment string to constrain the positioned element
     * to the {@link Ext.Viewport Viewport}. The element will attempt to align as specified,
     * but the position will be adjusted to constrain to the viewport if necessary. Note that
     * the element being aligned might be swapped to align to a different position than that
     * specified in order to enforce the viewport constraints.
     *
     * Example Usage:
     *
     *     // align the top left corner of the tooltip with the top right corner of its target.
     *     defaultAlign: 'tl-tr'
     *
     *     // align the bottom right corner of the tooltip with the center left edge of its target.
     *     defaultAlign: 'br-l'
     *
     *     // align the top center of the tooltip with the bottom left corner of its target.
     *     defaultAlign: 't-bl'
     *
     *     // align the 25% point on the bottom edge of this tooltip
     *     // with the 75% point on the top edge of its target.
     *     defaultAlign: 'b25-t75'
     */
    defaultAlign: 'bl-tl',
    ariaRole: 'tooltip',
    alwaysOnTop: true,
    initComponent: function() {
        var me = this;
        me.callParent();
        me.setTarget(me.target);
        // currentTarget is a flyweight which points to the activeTarget.
        me.currentTarget = new Ext.dom.Fly();
    },
    onRender: function(ct, position) {
        var me = this;
        me.callParent(arguments);
        if (me.sticky) {
            // tell the spec runner to ignore this element when checking if the dom is clean
            me.el.dom.setAttribute('data-sticky', true);
        }
        me.anchorEl = me.el.createChild({
            role: 'presentation',
            cls: Ext.baseCSSPrefix + 'tip-anchor'
        });
    },
    show: function() {
        // A programmatic show should align to the target
        if (!this.currentTarget.dom && this.target) {
            return this.showBy(this.target);
        }
        this.callParent();
    },
    /**
     * Binds this ToolTip to the specified element. The tooltip will be displayed when the mouse
     * moves over the element.
     * @param {String/HTMLElement/Ext.dom.Element} target The Element, HTMLElement, or
     * ID of an element to bind to
     */
    setTarget: function(target) {
        var me = this,
            listeners;
        if (me.targetListeners) {
            me.targetListeners.destroy();
        }
        if (target) {
            me.target = target = Ext.get(target.el || target);
            listeners = {
                mouseover: 'onTargetOver',
                mouseout: 'onTargetOut',
                mousemove: 'onMouseMove',
                tap: 'onTargetTap',
                scope: me,
                destroyable: true
            };
            me.targetListeners = target.on(listeners);
        } else {
            me.target = null;
        }
    },
    /**
     * @private
     */
    onMouseMove: function(e) {
        var me = this,
            dismissDelay = me.dismissDelay;
        // Always update pointerEvent, so that if there's a delayed show
        // scheduled, it gets the latest pointer to align to.
        me.pointerEvent = e;
        if (me.isVisible() && me.currentTarget.contains(e.target)) {
            // If they move the mouse, restart the dismiss delay
            if (dismissDelay && me.autoHide !== false) {
                me.clearTimer('dismiss');
                me.dismissTimer = Ext.defer(me.hide, dismissDelay, me);
            }
            if (me.trackMouse) {
                me.doAlignment(me.getAlignRegion());
            }
        }
    },
    /**
     * @private
     */
    getAlignRegion: function() {
        var me = this,
            anchorEl = me.anchorEl,
            align = me.getAnchorAlign(),
            overlap, alignSpec, target,
            mouseOffset = me.mouseOffset;
        if (!me.anchorSize) {
            anchorEl.addCls(Ext.baseCSSPrefix + 'tip-anchor-top');
            anchorEl.show();
            me.anchorSize = new Ext.util.Offset(anchorEl.getWidth(false, true), anchorEl.getHeight(false, true));
            anchorEl.removeCls(Ext.baseCSSPrefix + 'tip-anchor-top');
            anchorEl.hide();
        }
        // Target region from the anchorTarget element unless trackMouse set
        if ((me.anchor || me.align) && me.anchorToTarget && !me.trackMouse) {
            target = me.currentTarget.getRegion();
        } else // Here, we're either trackMouse: true, or we're not anchored to the target
        // element, so we should show offset from the mouse.
        // If we are being shown programatically, use 0, 0
        {
            target = me.pointerEvent ? me.pointerEvent.getPoint().adjust(-Math.abs(mouseOffset[1]), Math.abs(mouseOffset[0]), Math.abs(mouseOffset[1]), -Math.abs(mouseOffset[0])) : new Ext.util.Point();
            if (!me.anchor) {
                overlap = true;
                if (mouseOffset[0] > 0) {
                    if (mouseOffset[1] > 0) {
                        align = 'tl-br';
                    } else {
                        align = 'bl-tr';
                    }
                } else {
                    if (mouseOffset[1] > 0) {
                        align = 'tr-bl';
                    } else {
                        align = 'br-tl';
                    }
                }
            }
        }
        alignSpec = {
            align: me.convertPositionSpec(align),
            axisLock: me.axisLock,
            target: target,
            overlap: overlap,
            offset: me.targetOffset,
            inside: me.constrainPosition ? (me.constrainTo || Ext.getBody().getRegion().adjust(5, -5, -5, 5)) : null
        };
        if (me.anchor) {
            alignSpec.anchorSize = me.anchorSize;
        }
        return me.getRegion().alignTo(alignSpec);
    },
    fadeOut: function() {
        var me = this;
        me.el.fadeOut({
            duration: me.fadeOutDuration,
            callback: function() {
                me.hide();
                me.el.setOpacity('');
            }
        });
    },
    /**
     * @private
     */
    getAnchorAlign: function() {
        switch (this.anchor) {
            case 'top':
                return 'tl-bl';
            case 'left':
                return 'tl-tr';
            case 'right':
                return 'tr-tl';
            default:
                return this.defaultAlign;
        }
    },
    onTargetTap: function(e) {
        // On hybrid mouse/touch systems, we want to show the tip on touch, but
        // we don't want to show it if this is coming from a click event, because
        // the mouse is already hovered. Tap occasionally hides - eg: pickers, menus.
        if (this.showOnTap && e.pointerType !== 'mouse' && Ext.fly(e.target).isVisible(true)) {
            this.onTargetOver(e);
        }
    },
    /**
     * @private
     */
    onTargetOver: function(e) {
        var me = this,
            delegate = me.delegate,
            currentTarget = me.currentTarget,
            fromElement = e.relatedTarget || e.fromElement,
            newTarget,
            myListeners = me.hasListeners;
        if (me.disabled) {
            return;
        }
        if (delegate) {
            // Moving inside a delegate
            if (currentTarget.contains(e.target)) {
                return;
            }
            newTarget = e.getTarget(delegate);
            // Mouseovers while within a target do nothing
            if (newTarget && e.getRelatedTarget(delegate) === newTarget) {
                return;
            }
        }
        // Moved from outside the target
        else if (!me.target.contains(fromElement)) {
            newTarget = me.target.dom;
        } else // Moving inside the target
        {
            return;
        }
        // If pointer entered the target or a delegate child, then show.
        if (newTarget) {
            // If users need to see show events on target change, we must hide.
            if ((myListeners.beforeshow || myListeners.show) && me.isVisible()) {
                me.hide();
            }
            me.triggerElement = newTarget;
            me.pointerEvent = e;
            currentTarget.attach(newTarget);
            me.handleTargetOver(newTarget, e);
        }
        // If over a non-delegate child, behave as in target out
        else if (currentTarget.dom) {
            me.handleTargetOut();
        }
    },
    handleTargetOver: function(target, event) {
        // Separated from onTargetOver so that subclasses can handle target over in any way.
        // If we are showing on tap, show immediately
        if (event.pointerType !== 'mouse') {
            this.showFromDelay();
        } else {
            this.delayShow();
        }
    },
    /**
     * @private
     */
    delayShow: function() {
        var me = this;
        me.clearTimer('hide');
        if (me.hidden && !me.showTimer) {
            if (me.delegate && Ext.Date.getElapsed(me.lastHidden) < me.quickShowInterval) {
                me.showFromDelay();
            } else {
                me.showTimer = Ext.defer(me.showFromDelay, me.pointerEvent.pointerType !== 'mouse' ? 0 : me.showDelay, me);
            }
        } else if (!me.hidden && me.autoHide !== false) {
            me.showFromDelay();
        }
    },
    showFromDelay: function() {
        var me = this;
        // Need to check this here since onDisable only gets called after render, which
        // the show call below may trigger
        if (!me.disabled) {
            me.fireEvent('hovertarget', me, me.currentTarget, me.currentTarget.dom);
            if (me.isVisible()) {
                me.realignToTarget();
            } else {
                me.triggerElement = me.currentTarget.dom;
                me.fromDelayShow = true;
                me.show();
                me.fromDelayShow = false;
            }
        }
    },
    /**
     * @private
     */
    onTargetOut: function(e) {
        // We have exited the current target
        if (this.currentTarget.dom && !this.currentTarget.contains(e.relatedTarget)) {
            this.handleTargetOut();
        }
    },
    handleTargetOut: function() {
        var me = this;
        if (me.showTimer) {
            me.clearTimer('show');
        }
        if (me.isVisible() && me.autoHide) {
            me.delayHide();
        }
    },
    /**
     * @private
     */
    delayHide: function() {
        var me = this;
        if (!me.hidden && !me.hideTimer) {
            me.clearTimer('dismiss');
            me.hideTimer = Ext.defer(me[me.hideAction], me.hideDelay, me);
        }
    },
    /**
     * Hides this tooltip if visible.
     */
    hide: function() {
        var me = this;
        // Must also do this on hide in case it was dismissed while over
        me.currentTarget.detach();
        me.clearTimer('dismiss');
        me.lastHidden = new Date();
        if (me.anchorEl) {
            me.anchorEl.hide();
        }
        me.callParent(arguments);
        me.triggerElement = null;
    },
    /**
     * Ensures this tooltip at the current event target XY position.
     */
    afterShow: function() {
        this.callParent();
        this.realignToTarget();
    },
    /**
     * Realign this tooltip to the {@link #cfg-target}.
     *
     * @since 6.2.1
     */
    realignToTarget: function() {
        var me = this;
        me.clearTimers();
        if (!me.calledFromShowAt) {
            me.doAlignment(me.getAlignRegion());
        }
        if (me.dismissDelay && me.autoHide !== false) {
            me.dismissTimer = Ext.defer(me.hide, me.dismissDelay, me);
        }
    },
    /**
     * Shows this ToolTip aligned to the passed Component or element or event according to the
     * {@link #anchor} config.
     * @param {Ext.Component/Ext.event.Event/Ext.dom.Element} target The {@link Ext.Component}
     * or {@link Ext.dom.Element}, or (Ext.event.Event} to show this ToolTip by.
     */
    showBy: function(target) {
        var me = this;
        me.align = me.defaultAlign;
        if (target.isEvent) {
            me.currentTarget.attach(target.target);
            me.pointerEvent = target;
        } else {
            me.currentTarget.attach(Ext.getDom(target.el || target));
            me.triggerElement = me.currentTarget.dom;
        }
        if (me.isVisible()) {
            me.realignToTarget();
        } else {
            me.show();
        }
        return me;
    },
    _timerNames: {},
    /**
     * @private
     */
    clearTimer: function(name) {
        var me = this,
            names = me._timerNames,
            propName = names[name] || (names[name] = name + 'Timer'),
            timer = me[propName];
        if (timer) {
            Ext.undefer(timer);
            me[propName] = null;
            // We were going to show against the target, but now not.
            if (name === 'show' && me.isHidden()) {
                me.currentTarget.detach();
            }
        }
    },
    /**
     * @private
     */
    clearTimers: function() {
        var me = this;
        me.clearTimer('show');
        me.clearTimer('dismiss');
        me.clearTimer('hide');
        me.clearTimer('enable');
    },
    onShow: function() {
        var me = this;
        me.callParent();
        me.mousedownListener = Ext.on({
            mousedown: 'onDocMouseDown',
            scope: me,
            destroyable: true
        });
    },
    onHide: function() {
        var me = this;
        me.callParent();
        Ext.destroy(me.mousedownListener);
    },
    /**
     * @private
     */
    onDocMouseDown: function(e) {
        var me = this,
            delegate = me.delegate;
        if (e.within(me.el.dom)) {
            // A real touch event inside the tip is the equivalent of
            // mousing over the tip to keep it visible, so cancel the
            // dismiss timer.
            if (e.pointerType !== 'mouse' && me.allowOver) {
                me.clearTimer('dismiss');
            }
        }
        // Only respond to the mousedown if it's not on this tip, and it's not on a target.
        // If it's on a target, onTargetTap will handle it.
        else if (!me.closable) {
            if (e.within(me.target) && (!delegate || e.getTarget(delegate))) {
                me.delayHide();
            } else {
                me.disable();
                me.enableTimer = Ext.defer(me.enable, 100, me);
            }
        }
    },
    /**
     * @private
     */
    doEnable: function() {
        if (!this.destroyed) {
            this.enable();
        }
    },
    onDisable: function() {
        this.callParent();
        this.clearTimers();
        this.hide();
    },
    doDestroy: function() {
        var me = this;
        me.clearTimers();
        me.destroyMembers('mousedownListener', 'anchorEl');
        me.callParent();
    },
    privates: {
        /**
         * Implementation for universal apps so that the Tooltip interface they are using works
         * when common code uses the ToolTip API.
         */
        getTrackMouse: function() {
            return this.trackMouse;
        },
        clipTo: function(clippingEl, sides) {
            // Override because we also need to clip the anchor
            var clippingRegion;
            // Allow a Region to be passed
            if (clippingEl.isRegion) {
                clippingRegion = clippingEl;
            } else {
                // eslint-disable-next-line max-len
                clippingRegion = (clippingEl.isComponent ? clippingEl.el : Ext.fly(clippingEl)).getConstrainRegion();
            }
            this.callParent([
                clippingRegion,
                sides
            ]);
            // Clip the anchor to the same bounds
            this.anchorEl.clipTo(clippingRegion, sides);
        }
    }
});

/**
 * A specialized tooltip class for tooltips that can be specified in markup and automatically
 * managed by the global {@link Ext.tip.QuickTipManager} instance. See the QuickTipManager
 * documentation for additional usage details and examples.
 *
 *      @example     
 *      Ext.tip.QuickTipManager.init(); // Instantiate the QuickTipManager 
 *
 *      Ext.create('Ext.Button', {
 *
 *          renderTo: Ext.getBody(),
 *          text: 'My Button',
 *          listeners: {
 *
 *              afterrender: function(me) {
 *
 *                  // Register the new tip with an element's ID
 *                  Ext.tip.QuickTipManager.register({
 *                      target: me.getId(), // Target button's ID
 *                      title : 'My Tooltip',  // QuickTip Header
 *                      text  : 'My Button has a QuickTip' // Tip content  
 *                  });
 *
 *              },
 *              destroy: function(me) {
 *                  Ext.tip.QuickTipManager.unregister(me.getId());
 *              }
 *          }
 *      });
 *
 */
Ext.define('Ext.tip.QuickTip', {
    extend: 'Ext.tip.ToolTip',
    alias: 'widget.quicktip',
    alternateClassName: 'Ext.QuickTip',
    /**
     * @cfg {String/HTMLElement/Ext.dom.Element} target
     * The target HTMLElement, {@link Ext.dom.Element} or id to associate with this Quicktip.
     *
     * Defaults to the document.
     */
    /**
     * @cfg {Boolean} interceptTitles
     * `true` to automatically use the element's DOM title value if available.
     */
    interceptTitles: false,
    /**
     * @cfg {String/Ext.panel.Title} title
     * The title text to be used to display in the Tip header.  May be a string 
     * (including HTML tags) or an {@link Ext.panel.Title} config object.
     */
    title: '&#160;',
    /**
     * @private
     */
    tagConfig: {
        namespace: 'data-',
        attribute: 'qtip',
        width: 'qwidth',
        target: 'target',
        title: 'qtitle',
        hide: 'hide',
        cls: 'qclass',
        align: 'qalign',
        anchor: 'anchor',
        showDelay: 'qshowDelay',
        hideAction: 'hideAction',
        anchorTarget: 'anchorTarget'
    },
    isQuickTip: true,
    /**
     * @cfg shrinkWrapDock
     * @inheritdoc
     */
    shrinkWrapDock: true,
    initComponent: function() {
        var me = this;
        // delegate selector is a function which detects presence
        // of attributes which provide QuickTip text.
        me.delegate = me.delegate.bind(me);
        me.target = me.target || Ext.getDoc();
        me.targets = me.targets || {};
        me.header = me.header || {};
        me.header.focusableContainer = false;
        me.callParent();
    },
    setTagConfig: function(cfg) {
        this.tagConfig = Ext.apply({}, cfg);
        // Let attr get recomputed
        delete this.tagConfig.attr;
    },
    /**
     * @cfg text
     * @inheritdoc Ext.tip.ToolTip#cfg-html
     */
    text: null,
    /**
     * @cfg html
     * @hide
     * -- hidden for Ext.tip.QuickTip - see #cfg-text
     */
    /**
     * Configures a new quick tip instance and assigns it to a target element.
     *
     * For example usage, see the {@link Ext.tip.QuickTipManager} class header.
     *
     * @param {Object} config The config object with the following properties:
     * @param config.target (required) The target HTMLElement, {@link Ext.dom.Element} or 
     * id to associate with this Quicktip.  See {@link Ext.tip.QuickTip#target}.
     * @param config.text Tip body content.  See {@link Ext.tip.QuickTip#text}.
     * @param config.title Tip header.  See {@link Ext.tip.QuickTip#title}.
     * @param config.autoHide False to prevent the tip from automatically hiding on 
     * mouseleave.  See {@link Ext.tip.QuickTip#autoHide}.
     * @param config.cls An optional extra CSS class that will be added to the tip.  See 
     * {@link Ext.tip.QuickTip#cls}.
     * @param config.dismissDelay Delay in milliseconds before the tooltip automatically 
     * hides (overrides singleton value).  See {@link Ext.tip.QuickTip#dismissDelay}.
     * @param config.width Tip width in pixels.  See {@link Ext.tip.QuickTip#width}.
     */
    register: function(config) {
        var configs = Ext.isArray(config) ? config : arguments,
            i = 0,
            len = configs.length,
            target, j, targetLen;
        for (; i < len; i++) {
            config = configs[i];
            target = config.target;
            if (target) {
                if (Ext.isArray(target)) {
                    for (j = 0 , targetLen = target.length; j < targetLen; j++) {
                        this.targets[Ext.id(target[j])] = config;
                    }
                } else {
                    this.targets[Ext.id(target)] = config;
                }
            }
        }
    },
    /**
     * Removes this quick tip from its element and destroys it.
     * @param {String/HTMLElement/Ext.dom.Element} el The element from which the quick tip
     * is to be removed or ID of the element.
     */
    unregister: function(el) {
        delete this.targets[Ext.id(el)];
    },
    /**
     * Hides a visible tip or cancels an impending show for a particular element.
     * @param {String/HTMLElement/Ext.dom.Element} el The element that is the target of
     * the tip or ID of the element.
     */
    cancelShow: function(el) {
        var me = this,
            currentTarget = me.currentTarget;
        el = Ext.getDom(el);
        if (me.isVisible()) {
            if (currentTarget.dom === el) {
                me.hide();
            }
        } else if (currentTarget.dom === el) {
            me.clearTimer('show');
        }
    },
    delegate: function(target) {
        var me = this,
            cfg = me.tagConfig,
            attr = cfg.attr || (cfg.attr = cfg.namespace + cfg.attribute),
            text;
        // We can now only activate on elements which have the required attributes
        text = target.getAttribute(attr) || (me.interceptTitles && target.title);
        return !!text;
    },
    /**
     * @private
     * Reads the tip text from the target.
     */
    getTipText: function(target) {
        var titleText = target.title,
            cfg = this.tagConfig,
            attr = cfg.attr || (cfg.attr = cfg.namespace + cfg.attribute);
        if (this.interceptTitles && titleText) {
            target.setAttribute(attr, titleText);
            target.removeAttribute('title');
            return titleText;
        } else {
            return target.getAttribute(attr);
        }
    },
    onTargetOver: function(event) {
        var me = this,
            currentTarget = me.currentTarget,
            target = event.target,
            targets, registeredTarget, key;
        // If the over target is not an HTMLElement, or is the <html> or the <body>, then return
        if (!target || target.nodeType !== 1 || target === document.documentElement || target === document.body) {
            return;
        }
        me.pointerEvent = event;
        targets = me.targets;
        // Loop through registered targets seeing if we are over one.
        for (key in targets) {
            if (targets.hasOwnProperty(key)) {
                registeredTarget = targets[key];
                target = Ext.getDom(registeredTarget.target);
                // If we moved over a registered target from outside of it, activate it.
                if (target && Ext.fly(target).contains(event.target) && !Ext.fly(target).contains(event.relatedTarget)) {
                    currentTarget.attach(target);
                    me.activeTarget = registeredTarget;
                    registeredTarget.el = currentTarget;
                    me.anchor = registeredTarget.anchor;
                    me.activateTarget();
                    return;
                }
            }
        }
        // We found no registered targets, now continue as a regular ToolTip, and
        // see if we are over any of our delegated targets.
        me.callParent([
            event
        ]);
    },
    handleTargetOver: function(target, event) {
        var me = this,
            currentTarget = me.currentTarget,
            cfg = me.tagConfig,
            ns = cfg.namespace,
            tipText = me.getTipText(target, event),
            autoHide;
        if (tipText) {
            autoHide = currentTarget.getAttribute(ns + cfg.hide);
            me.activeTarget = {
                el: currentTarget,
                text: tipText,
                width: +currentTarget.getAttribute(ns + cfg.width) || null,
                autoHide: autoHide !== "user" && autoHide !== 'false',
                title: currentTarget.getAttribute(ns + cfg.title),
                cls: currentTarget.getAttribute(ns + cfg.cls),
                align: currentTarget.getAttribute(ns + cfg.align),
                showDelay: currentTarget.getAttribute(ns + cfg.showDelay),
                hideAction: currentTarget.getAttribute(ns + cfg.hideAction),
                alignTarget: currentTarget.getAttribute(ns + cfg.anchorTarget)
            };
            // If we were not configured with an anchor,
            // allow it to be set by the target's properties
            if (!me.initialConfig.hasOwnProperty('anchor')) {
                me.anchor = currentTarget.getAttribute(ns + cfg.anchor);
            }
            // If we are anchored, and not configured with an anchorTarget,
            // anchor to the target element, or whatever its 'data-anchortarget' points to
            if (me.anchor && !me.initialConfig.hasOwnProperty('anchorTarget')) {
                me.alignTarget = me.activeTarget.alignTarget || target;
            }
            me.activateTarget();
        }
    },
    activateTarget: function() {
        var me = this,
            activeTarget = me.activeTarget,
            delay = activeTarget.showDelay,
            hideAction = activeTarget.hideAction;
        // If moved from target to target rapidly, the hide delay will not
        // have fired, so just update content and alignment.
        if (me.isVisible()) {
            me.updateContent();
            me.realignToTarget();
        } else {
            if (activeTarget.showDelay) {
                delay = me.showDelay;
                me.showDelay = parseInt(activeTarget.showDelay, 10);
            }
            me.delayShow();
            if (activeTarget.showDelay) {
                me.showDelay = delay;
            }
            if (!(hideAction = activeTarget.hideAction)) {
                delete me.hideAction;
            } else {
                me.hideAction = hideAction;
            }
        }
    },
    getAnchorAlign: function() {
        var active = this.activeTarget;
        return (active && active.align) || this.callParent();
    },
    getAlignRegion: function() {
        var me = this,
            activeTarget = me.activeTarget,
            currentTargetDom = me.currentTarget.dom,
            result;
        // If we are anchored, and not configured with an anchorTarget,
        // align to the target element, or whatever its 'data-anchortarget' points to
        if (activeTarget && activeTarget.alignTarget && me.anchor && !me.initialConfig.hasOwnProperty('anchorTarget')) {
            me.currentTarget.attach(Ext.getDom(activeTarget.alignTarget));
        }
        // Anchor to the target when have an align config or an anchor config
        me.anchorToTarget = !!(activeTarget.align || me.anchor);
        result = me.callParent();
        // Return currentTarget to correctness for pointer event processing
        me.currentTarget.attach(currentTargetDom);
        return result;
    },
    /**
     * @private
     */
    handleTargetOut: function(e) {
        var me = this,
            active = me.activeTarget,
            autoHide = me.autoHide,
            hideDelay = me.hideDelay;
        if (active && autoHide !== false) {
            me.autoHide = true;
            if (active.hideDelay) {
                me.hideDelay = parseInt(active.hideDelay, 10);
            }
            me.callParent([
                e
            ]);
            me.autoHide = autoHide;
            me.hideDelay = hideDelay;
        }
    },
    targetTextEmpty: function() {
        var me = this,
            target = me.activeTarget,
            cfg = me.tagConfig,
            el, text;
        if (target) {
            el = target.el;
            if (el) {
                text = el.getAttribute(cfg.namespace + cfg.attribute);
                // Note that the quicktip could also have been registered with the QuickTipManager.
                // If this was the case, then we don't want to veto showing it.
                // Simply do a lookup in the registered targets collection.
                if (!text && !me.targets[Ext.id(target.el.dom)]) {
                    return true;
                }
            }
        }
        return false;
    },
    show: function() {
        var me = this,
            fromDelay = me.fromDelayShow;
        // We're coming from a delayed show, so check whether
        // the attribute has been removed before we show it
        if (fromDelay && me.targetTextEmpty()) {
            me.activeTarget = null;
            me.currentTarget.detach();
            return;
        }
        me.callParent(arguments);
    },
    /**
     * @method beforeShow
     * @inheritdoc Ext.tip.Tip#method-beforeShow
     */
    beforeShow: function() {
        this.updateContent();
        this.callParent(arguments);
    },
    /**
     * @private
     */
    updateContent: function() {
        var me = this,
            target = me.activeTarget,
            header = me.header,
            dismiss, cls;
        if (target) {
            me.suspendLayouts();
            if (target.title) {
                me.setTitle(target.title);
                header.show();
            } else if (header) {
                header.hide();
            }
            me.update(target.text);
            me.autoHide = target.autoHide;
            dismiss = target.dismissDelay;
            me.dismissDelay = Ext.isNumber(dismiss) ? dismiss : me.dismissDelay;
            cls = me.lastCls;
            if (cls) {
                me.removeCls(cls);
                delete me.lastCls;
            }
            cls = target.cls;
            if (cls) {
                me.addCls(cls);
                me.lastCls = cls;
            }
            me.setWidth(target.width);
            me.align = target.align;
            me.resumeLayouts(true);
        }
    },
    /**
     * @method hide
     * @inheritdoc
     */
    hide: function() {
        this.activeTarget = null;
        this.callParent();
    }
});

/**
 * Provides attractive and customizable tooltips for any element. The QuickTips
 * singleton is used to configure and manage tooltips globally for multiple elements
 * in a generic manner.  To create individual tooltips with maximum customizability,
 * you should consider either {@link Ext.tip.Tip} or {@link Ext.tip.ToolTip}.
 *
 * Quicktips can be configured via tag attributes directly in markup, or by
 * registering quick tips programmatically via the {@link #register} method.
 *
 * The singleton's instance of {@link Ext.tip.QuickTip} is available via
 * {@link #getQuickTip}, and supports all the methods and configuration properties
 * of Ext.tip.QuickTip. These settings will apply to all tooltips shown by the
 * singleton.
 *
 * Below is the summary of the configuration properties which can be used.
 * For detailed descriptions see the config options for the
 * {@link Ext.tip.QuickTip QuickTip} class
 *
 * ## QuickTips singleton configs (all are optional)
 *
 *  - `dismissDelay`
 *  - `hideDelay`
 *  - `maxWidth`
 *  - `minWidth`
 *  - `showDelay`
 *  - `trackMouse`
 *
 * ## Target element configs (optional unless otherwise noted)
 *
 *  - `autoHide`
 *  - `cls`
 *  - `dismissDelay` (overrides singleton value)
 *  - `target` (required)
 *  - `text` (required)
 *  - `title`
 *  - `width`
 *
 * Here is an example showing how some of these config options could be used:
 *
 *     @example
 *     // Init the singleton.  Any tag-based quick tips will start working.
 *     Ext.tip.QuickTipManager.init();
 *
 *     // Apply a set of config properties to the singleton
 *     Ext.apply(Ext.tip.QuickTipManager.getQuickTip(), {
 *         maxWidth: 200,
 *         minWidth: 100,
 *         showDelay: 50      // Show 50ms after entering target
 *     });
 *
 *     // Create a small panel to add a quick tip to
 *     Ext.create('Ext.container.Container', {
 *         id: 'quickTipContainer',
 *         width: 200,
 *         height: 150,
 *         style: {
 *             backgroundColor:'#000000'
 *         },
 *         renderTo: Ext.getBody()
 *     });
 *
 *
 *     // Manually register a quick tip for a specific element
 *     Ext.tip.QuickTipManager.register({
 *         target: 'quickTipContainer',
 *         title: 'My Tooltip',
 *         text: 'This tooltip was added in code',
 *         width: 100,
 *         dismissDelay: 10000 // Hide after 10 seconds hover
 *     });
 *
 * To register a quick tip in markup, you simply add one or more of the valid QuickTip
 * attributes prefixed with the **data-** namespace.  The HTML element itself is
 * automatically set as the quick tip target. Here is the summary of supported attributes
 * (optional unless otherwise noted):
 *
 *  - `hide`: Specifying "user" is equivalent to setting autoHide = false.
 *     Any other value will be the same as autoHide = true.
 *  - `qclass`: A CSS class to be applied to the quick tip
 *     (equivalent to the 'cls' target element config).
 *  - `qtip (required)`: The quick tip text (equivalent to the 'text' target element config).
 *  - `qtitle`: The quick tip title (equivalent to the 'title' target element config).
 *  - `qwidth`: The quick tip width (equivalent to the 'width' target element config).
 *
 * Here is an example of configuring an HTML element to display a tooltip from markup:
 *
 *     // Add a quick tip to an HTML button
 *     <input type="button" value="OK" data-qtitle="OK Button" data-qwidth="100"
 *          data-qtip="This is a quick tip from markup!"></input>
 *
 * @singleton
 */
Ext.define('Ext.tip.QuickTipManager', {
    singleton: true,
    alternateClassName: 'Ext.QuickTips',
    requires: [
        'Ext.tip.QuickTip'
    ],
    disabled: false,
    /**
     * Initializes the global QuickTips instance and prepare any quick tips.
     * @param {Boolean} [autoRender=true] True to render the QuickTips container
     * immediately to preload images.
     * @param {Object} [config] config object for the created QuickTip. By
     * default, the {@link Ext.tip.QuickTip QuickTip} class is instantiated, but this can
     * be changed by supplying an xtype property or a className property in this object.
     * All other properties on this object are configuration for the created component.
     */
    init: function(autoRender, config) {
        var me = this;
        if (!me.tip) {
            if (!Ext.isReady) {
                Ext.onInternalReady(function() {
                    Ext.tip.QuickTipManager.init(autoRender, config);
                });
                return false;
            }
            // eslint-disable-next-line vars-on-top, one-var
            var tipConfig = Ext.apply({
                    // tell the spec runner to ignore this element when checking if the dom is clean
                    sticky: true,
                    disabled: me.disabled,
                    id: 'ext-quicktips-tip'
                }, config),
                className = tipConfig.className,
                xtype = tipConfig.xtype;
            if (className) {
                delete tipConfig.className;
            } else if (xtype) {
                className = 'widget.' + xtype;
                delete tipConfig.xtype;
            }
            if (autoRender !== false) {
                tipConfig.renderTo = document.body;
                if (tipConfig.renderTo.tagName.toUpperCase() !== 'BODY') {
                    // e.g., == 'FRAMESET'
                    Ext.raise({
                        sourceClass: 'Ext.tip.QuickTipManager',
                        sourceMethod: 'init',
                        msg: 'Cannot init QuickTipManager: no document body'
                    });
                }
            }
            me.tip = Ext.create(className || 'Ext.tip.QuickTip', tipConfig);
            // Prevent the tip from being accidentally destroyed.
            // It should stick around pretty much forever.
            me.tip.destroy = Ext.emptyFn;
            // private.
            // Need a globally accessible way of testing whether QuickTipsManager is 
            // both loaded AND initialized.
            Ext.quickTipsActive = true;
        }
    },
    /**
     * Destroys the QuickTips instance.
     */
    destroy: function() {
        var tip = this.tip;
        // The tip should only be destroyed by the Manager
        if (tip) {
            delete tip.destroy;
            tip.destroy();
            this.tip = null;
        }
        Ext.quickTipsActive = false;
    },
    // Don't callparent here, we're a singleton
    // Protected method called by the dd classes
    ddDisable: function() {
        var me = this,
            tip = me.tip;
        // don't disable it if we don't need to
        if (tip && !me.disabled) {
            tip.disable();
        }
    },
    // Protected method called by the dd classes
    ddEnable: function() {
        var me = this,
            tip = me.tip;
        // only enable it if it hasn't been disabled
        if (tip && !me.disabled) {
            tip.enable();
        }
    },
    /**
     * Enables quick tips globally.
     */
    enable: function() {
        var me = this,
            tip = me.tip;
        if (tip) {
            tip.enable();
        }
        me.disabled = false;
    },
    /**
     * Disables quick tips globally.
     */
    disable: function() {
        var me = this,
            tip = me.tip;
        if (tip) {
            tip.disable();
        }
        me.disabled = true;
    },
    /**
     * Returns true if quick tips are enabled, else false.
     * @return {Boolean}
     */
    isEnabled: function() {
        var tip = this.tip;
        return tip !== undefined && !tip.disabled;
    },
    /**
     * Gets the single {@link Ext.tip.QuickTip QuickTip} instance used to show tips
     * from all registered elements.
     * @return {Ext.tip.QuickTip}
     */
    getQuickTip: function() {
        return this.tip;
    },
    /**
     * @method register
     * @inheritdoc Ext.tip.QuickTip#method-register
     */
    register: function() {
        var tip = this.tip;
        tip.register.apply(tip, arguments);
    },
    /**
     * Removes any registered quick tip from the target element and destroys it.
     * @param {String/HTMLElement/Ext.dom.Element} el The element from which the quick tip
     * is to be removed or ID of the element.
     */
    unregister: function() {
        var tip = this.tip;
        tip.unregister.apply(tip, arguments);
    },
    /**
     * @method tips
     * Alias of {@link #method-register register}.
     * @inheritdoc Ext.tip.QuickTipManager#method-register
     */
    tips: function() {
        var tip = this.tip;
        tip.register.apply(tip, arguments);
    }
});

/**
 * Color picker provides a simple color palette for choosing colors. The picker can be rendered
 * to any container. The available default to a standard 40-color palette; this can be customized
 * with the {@link #colors} config.
 *
 * Typically you will need to implement a handler function to be notified when the user chooses
 * a color from the picker; you can register the handler using the {@link #event-select} event,
 * or by implementing the {@link #handler} method.
 *
 *     @example
 *     Ext.create('Ext.picker.Color', {
 *         value: '993300',  // initial selected color
 *         renderTo: Ext.getBody(),
 *         listeners: {
 *             select: function(picker, selColor) {
 *                 alert(selColor);
 *             }
 *         }
 *     });
 */
Ext.define('Ext.picker.Color', {
    extend: 'Ext.Component',
    requires: 'Ext.XTemplate',
    alias: 'widget.colorpicker',
    alternateClassName: 'Ext.ColorPalette',
    focusable: true,
    /**
     * @cfg {String} [componentCls='x-color-picker']
     * The CSS class to apply to the containing element.
     */
    componentCls: Ext.baseCSSPrefix + 'color-picker',
    /**
     * @cfg {String} [selectedCls='x-color-picker-selected']
     * The CSS class to apply to the selected element
     */
    selectedCls: Ext.baseCSSPrefix + 'color-picker-selected',
    /**
     * @cfg {String} itemCls
     * The CSS class to apply to the color picker's items
     */
    itemCls: Ext.baseCSSPrefix + 'color-picker-item',
    /**
     * @cfg {String} value
     * The initial color to highlight (should be a valid 6-digit color hex code
     * without the # symbol). Note that the hex codes are case-sensitive.
     */
    value: null,
    /**
     * @cfg {String} clickEvent
     * The DOM event that will cause a color to be selected. This can be any valid event name
     * (dblclick, contextmenu).
     */
    clickEvent: 'click',
    /**
     * @cfg {Boolean} allowReselect
     * If set to true then reselecting a color that is already selected fires the
     * {@link #event-select} event
     */
    allowReselect: false,
    /**
     * @property {String[]} colors
     * An array of 6-digit color hex code strings (without the # symbol). This array can contain
     * any number of colors, and each hex code should be unique. The width of the picker
     * is controlled via CSS by adjusting the width property of the 'x-color-picker' class
     * (or assigning a custom class), so you can balance the number of colors with the width setting
     * until the box is symmetrical.
     *
     * You can override individual colors if needed:
     *
     *     var cp = new Ext.picker.Color();
     *     cp.colors[0] = 'FF0000';  // change the first box to red
     *
     * Or you can provide a custom array of your own for complete control:
     *
     *     var cp = new Ext.picker.Color();
     *     cp.colors = ['000000', '993300', '333300'];
     */
    colors: [
        '000000',
        '993300',
        '333300',
        '003300',
        '003366',
        '000080',
        '333399',
        '333333',
        '800000',
        'FF6600',
        '808000',
        '008000',
        '008080',
        '0000FF',
        '666699',
        '808080',
        'FF0000',
        'FF9900',
        '99CC00',
        '339966',
        '33CCCC',
        '3366FF',
        '800080',
        '969696',
        'FF00FF',
        'FFCC00',
        'FFFF00',
        '00FF00',
        '00FFFF',
        '00CCFF',
        '993366',
        'C0C0C0',
        'FF99CC',
        'FFCC99',
        'FFFF99',
        'CCFFCC',
        'CCFFFF',
        '99CCFF',
        'CC99FF',
        'FFFFFF'
    ],
    /**
     * @cfg {Function/String} handler
     * A function that will handle the select event of this picker. The handler is passed
     * the following parameters:
     *
     * - `picker`: ColorPicker
     *
     *   The {@link Ext.picker.Color picker}.
     *
     * - `color`: String
     *
     *   The 6-digit color hex code (without the # symbol).
     * 
     * @controllable
     */
    /**
     * @cfg {Object} scope
     * The scope (`this` reference) in which the `{@link #handler}` function will be called.
     *
     * Defaults to this Color picker instance.
     */
    colorRe: /(?:^|\s)color-(.{6})(?:\s|$)/,
    /* eslint-disable indent, max-len */
    renderTpl: [
        '<tpl for="colors">',
        '<a href="#" role="button" class="color-{.} {parent.itemCls}" hidefocus="on">',
        '<span class="{parent.itemCls}-inner" style="background:#{.}">&#160;</span>',
        '</a>',
        '</tpl>'
    ],
    /* eslint-enable indent, max-len */
    /**
     * @event select
     * Fires when a color is selected
     * @param {Ext.picker.Color} this
     * @param {String} color The 6-digit color hex code (without the # symbol)
     */
    /**
     * @private
     */
    initComponent: function() {
        var me = this;
        me.callParent(arguments);
        if (me.handler) {
            me.on('select', me.handler, me.scope, true);
        }
    },
    /**
     * @private
     */
    initRenderData: function() {
        var me = this;
        return Ext.apply(me.callParent(), {
            itemCls: me.itemCls,
            colors: me.colors
        });
    },
    onRender: function() {
        var me = this,
            clickEvent = me.clickEvent;
        me.callParent(arguments);
        me.mon(me.el, clickEvent, me.handleClick, me, {
            delegate: 'a'
        });
        // always stop following the anchors
        if (clickEvent !== 'click') {
            me.mon(me.el, 'click', Ext.emptyFn, me, {
                delegate: 'a',
                stopEvent: true
            });
        }
    },
    /**
     * @private
     */
    afterRender: function() {
        var me = this,
            value;
        me.callParent(arguments);
        if (me.value) {
            value = me.value;
            me.value = null;
            me.select(value, true);
        }
    },
    /**
     * @private
     */
    handleClick: function(event) {
        var me = this,
            color;
        event.stopEvent();
        if (!me.disabled) {
            color = event.currentTarget.className.match(me.colorRe)[1];
            me.select(color.toUpperCase());
        }
    },
    /**
     * Selects the specified color in the picker (fires the {@link #event-select} event)
     * @param {String} color A valid 6-digit color hex code (# will be stripped if included)
     * @param {Boolean} [suppressEvent=false] True to stop the select event from firing.
     */
    select: function(color, suppressEvent) {
        var me = this,
            selectedCls = me.selectedCls,
            value = me.value,
            el, item;
        color = color.replace('#', '');
        if (!me.rendered) {
            me.value = color;
            return;
        }
        if (color !== value || me.allowReselect) {
            el = me.el;
            if (me.value) {
                item = el.down('a.color-' + value, true);
                Ext.fly(item).removeCls(selectedCls);
            }
            item = el.down('a.color-' + color, true);
            Ext.fly(item).addCls(selectedCls);
            me.value = color;
            if (suppressEvent !== true) {
                me.fireEvent('select', me, color);
            }
        }
    },
    /**
     * Clears any selection and sets the value to `null`.
     */
    clear: function() {
        var me = this,
            value = me.value,
            el;
        if (value && me.rendered) {
            el = me.el.down('a.color-' + value, true);
            Ext.fly(el).removeCls(me.selectedCls);
        }
        me.value = null;
    },
    /**
     * Get the currently selected color value.
     * @return {String} value The selected value. Null if nothing is selected.
     */
    getValue: function() {
        return this.value || null;
    }
});

/**
 * Layout class for {@link Ext.form.field.HtmlEditor} fields. Sizes textarea and iframe elements.
 * @private
 */
Ext.define('Ext.layout.component.field.HtmlEditor', {
    extend: 'Ext.layout.component.field.FieldContainer',
    alias: [
        'layout.htmleditor'
    ],
    type: 'htmleditor',
    naturalHeight: 150,
    naturalWidth: 300,
    beginLayout: function(ownerContext) {
        var owner = this.owner,
            dom;
        // In gecko, it can cause the browser to hang if we're running a layout with
        // a heap of data in the textarea (think several images with data urls).
        // So clear the value at the start, then re-insert it once we're done
        if (Ext.isGecko) {
            dom = owner.textareaEl.dom;
            this.lastValue = dom.value;
            dom.value = '';
        }
        this.callParent(arguments);
        ownerContext.toolbarContext = ownerContext.context.getCmp(owner.toolbar);
        ownerContext.inputCmpContext = ownerContext.context.getCmp(owner.inputCmp);
        ownerContext.bodyCellContext = ownerContext.getEl('bodyEl');
        ownerContext.textAreaContext = ownerContext.getEl('textareaEl');
        ownerContext.iframeContext = ownerContext.getEl('iframeEl');
    },
    beginLayoutCycle: function(ownerContext) {
        var me = this,
            widthModel = ownerContext.widthModel,
            heightModel = ownerContext.heightModel,
            owner = me.owner,
            iframeEl = owner.iframeEl,
            textareaEl = owner.textareaEl,
            height = (heightModel.natural || heightModel.shrinkWrap) ? me.naturalHeight : '';
        me.callParent(arguments);
        if (widthModel.shrinkWrap) {
            iframeEl.setStyle('width', '');
            textareaEl.setStyle('width', '');
        } else if (widthModel.natural) {
            ownerContext.bodyCellContext.setWidth(me.naturalWidth);
        }
        iframeEl.setStyle('height', height);
        textareaEl.setStyle('height', height);
    },
    finishedLayout: function() {
        var owner = this.owner;
        this.callParent(arguments);
        if (Ext.isGecko) {
            owner.textareaEl.dom.value = this.lastValue;
        }
    }
});

/**
 * A simple class that adds a vertical separator bar between toolbar items
 * (css class: 'x-toolbar-separator').
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *         title: 'Toolbar Separator Example',
 *         width: 300,
 *         height: 200,
 *         tbar : [
 *             'Item 1',
 *             { xtype: 'tbseparator' },
 *             'Item 2'
 *         ],
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.toolbar.Separator', {
    extend: 'Ext.toolbar.Item',
    alias: 'widget.tbseparator',
    alternateClassName: 'Ext.Toolbar.Separator',
    // Toolbar required here because we'll try to decorate it's alternateClassName
    // with this class' alternate name
    requires: [
        'Ext.toolbar.Toolbar'
    ],
    baseCls: Ext.baseCSSPrefix + 'toolbar-separator',
    ariaRole: 'separator'
});

/**
 * @private
 */
Ext.define('Ext.layout.container.boxOverflow.Menu', {
    extend: 'Ext.layout.container.boxOverflow.None',
    alternateClassName: 'Ext.layout.boxOverflow.Menu',
    alias: [
        'box.overflow.menu',
        'box.overflow.Menu'
    ],
    // capitalized for 4.x compat
    requires: [
        'Ext.toolbar.Separator',
        'Ext.button.Button'
    ],
    /**
     * @property {String} noItemsMenuText
     * HTML fragment to render into the toolbar overflow menu if there are no items to display
     */
    noItemsMenuText: '<div class="' + Ext.baseCSSPrefix + 'toolbar-no-items" role="menuitem">(None)</div>',
    menuCls: Ext.baseCSSPrefix + 'box-menu',
    constructor: function(config) {
        var me = this;
        me.callParent([
            config
        ]);
        /**
         * @property {Array} menuItems
         * Array of all items that are currently hidden and should go into the dropdown menu
         */
        me.menuItems = [];
    },
    beginLayout: function(ownerContext) {
        this.callParent([
            ownerContext
        ]);
        // Before layout, we need to re-show all items which we may have hidden due to a
        // previous overflow...
        this.clearOverflow(ownerContext);
    },
    beginLayoutCycle: function(ownerContext, firstCycle) {
        this.callParent([
            ownerContext,
            firstCycle
        ]);
        if (!firstCycle) {
            // if we are being re-run, we need to clear any overflow from the last run and
            // recache the childItems collection
            this.clearOverflow(ownerContext);
            this.layout.cacheChildItems(ownerContext);
        }
    },
    onRemove: function(comp) {
        Ext.Array.remove(this.menuItems, comp);
    },
    clearItem: function(comp) {
        var menu = comp.menu;
        if (comp.isButton && menu) {
            // If the button had a menu, forcibly set it
            // again so that the ownerCmp is reset correctly
            // and is no longer pointing at the overflow
            comp.setMenu(menu, false);
        }
    },
    // We don't define a prefix in menu overflow.
    getSuffixConfig: function() {
        var me = this,
            layout = me.layout,
            owner = layout.owner,
            oid = owner.id;
        /**
         * @private
         * @property {Ext.menu.Menu} menu
         * The expand menu - holds items for every item that cannot be shown
         * because the container is currently not large enough.
         */
        me.menu = new Ext.menu.Menu({
            listeners: {
                scope: me,
                beforeshow: me.beforeMenuShow
            }
        });
        /**
         * @private
         * @property {Ext.button.Button} menuTrigger
         * The expand button which triggers the overflow menu to be shown
         */
        me.menuTrigger = new Ext.button.Button({
            id: oid + '-menu-trigger',
            cls: me.menuCls + '-after ' + Ext.baseCSSPrefix + 'toolbar-item',
            plain: owner.usePlainButtons,
            // To enable the Menu to ascertain a valid zIndexManager owner in the same tree
            ownerCt: owner,
            ownerLayout: layout,
            iconCls: Ext.baseCSSPrefix + me.getOwnerType(owner) + '-more-icon',
            ui: owner.defaultButtonUI || 'default',
            menu: me.menu,
            // Menu will be empty when we're showing it because we populate items after
            showEmptyMenu: true,
            getSplitCls: function() {
                return '';
            }
        });
        return me.menuTrigger.getRenderTree();
    },
    getOverflowCls: function(direction) {
        return this.menuCls + '-body-' + direction;
    },
    handleOverflow: function(ownerContext) {
        var me = this,
            layout = me.layout;
        me.showTrigger(ownerContext);
        // Center the menuTrigger button only if we are not vertical.
        if (layout.direction !== 'vertical') {
            me.menuTrigger.setLocalY((ownerContext.state.boxPlan.maxSize - me.menuTrigger[layout.names.getHeight]()) / 2);
        }
        return {
            reservedSpace: me.triggerTotalWidth
        };
    },
    /**
     * Finishes the render operation of the trigger Button.
     * @private
     */
    captureChildElements: function() {
        var me = this,
            menuTrigger = me.menuTrigger,
            names = me.layout.names;
        // The rendering flag is set when getRenderTree is called which we do
        // when returning markup string for the owning layout's "suffix"
        if (menuTrigger.rendering) {
            menuTrigger.finishRender();
            me.triggerTotalWidth = menuTrigger[names.getWidth]() + menuTrigger.el.getMargin(names.parallelMargins);
        }
    },
    /**
     * @private
     * Called by the layout, when it determines that there is no overflow.
     * Also called as an interceptor to the layout's onLayout method to reshow
     * previously hidden overflowing items.
     */
    clearOverflow: function(ownerContext) {
        var me = this,
            items = me.menuItems,
            length = items.length,
            owner = me.layout.owner,
            asLayoutRoot = owner._asLayoutRoot,
            item, i;
        owner.suspendLayouts();
        me.captureChildElements();
        me.hideTrigger();
        owner.resumeLayouts();
        for (i = 0; i < length; i++) {
            item = items[i];
            // What we are doing here is preventing the layout bubble from invalidating our
            // owner component. We need just the button to be added to the layout run.
            item.suspendLayouts();
            item.show();
            me.clearItem(item);
            item.resumeLayouts(asLayoutRoot);
        }
        items.length = 0;
    },
    /**
     * @private
     * Shows the overflow trigger when enableOverflow is set to true and the items
     * in the layout are too wide to fit in the space available
     */
    showTrigger: function(ownerContext) {
        var me = this,
            layout = me.layout,
            owner = layout.owner,
            names = layout.names,
            startProp = names.x,
            sizeProp = names.width,
            plan = ownerContext.state.boxPlan,
            available = plan.targetSize[sizeProp],
            childItems = ownerContext.childItems,
            menuTrigger = me.menuTrigger,
            menuItems = me.menuItems,
            childContext, comp, i, props, len;
        // We don't want the menuTrigger.show to cause owner's layout to be invalidated, so
        // we force just the button to be invalidated and added to the current run.
        menuTrigger.suspendLayouts();
        menuTrigger.show();
        menuTrigger.resumeLayouts(me._asLayoutRoot);
        available -= me.triggerTotalWidth;
        owner.suspendLayouts();
        // Hide all items which are off the end, and store them to allow them to be restored
        // before each layout operation.
        for (i = 0 , len = menuItems.length; i < len; ++i) {
            me.clearItem(menuItems[i]);
        }
        menuItems.length = 0;
        for (i = 0 , len = childItems.length; i < len; i++) {
            childContext = childItems[i];
            props = childContext.props;
            if (props[startProp] + props[sizeProp] > available) {
                comp = childContext.target;
                me.menuItems.push(comp);
                comp.hide();
            }
        }
        owner.resumeLayouts();
    },
    /**
     * @private
     */
    hideTrigger: function() {
        var menuTrigger = this.menuTrigger;
        if (menuTrigger) {
            menuTrigger.hide();
        }
    },
    /**
     * @private
     * Called before the overflow menu is shown. This constructs the menu's items,
     * caching them for as long as it can.
     */
    beforeMenuShow: function(menu) {
        var me = this,
            items = me.menuItems,
            i = 0,
            len = items.length,
            item, prev,
            needsSep = function(group, prev) {
                return group.isXType('buttongroup') && !(prev instanceof Ext.toolbar.Separator);
            };
        menu.suspendLayouts();
        menu.removeAll(false);
        for (; i < len; i++) {
            item = items[i];
            // Do not show a separator as a first item
            if (!i && (item instanceof Ext.toolbar.Separator)) {
                
                continue;
            }
            if (prev && (needsSep(item, prev) || needsSep(prev, item))) {
                menu.add('-');
            }
            me.addComponentToMenu(menu, item);
            prev = item;
        }
        // put something so the menu isn't empty if no compatible items found
        if (menu.items.length < 1) {
            menu.add(me.noItemsMenuText);
        }
        menu.resumeLayouts();
    },
    /**
     * @private
     * Returns a menu config for a given component. This config is used to create a menu item
     * to be added to the expander menu
     * @param {Ext.Component} component The component to create the config for
     * @param {Boolean} hideOnClick Passed through to the menu item
     */
    createMenuConfig: function(component, hideOnClick) {
        var config = Ext.apply({}, component.initialConfig),
            group = component.toggleGroup;
        Ext.copy(config, component, [
            'iconCls',
            'icon',
            'itemId',
            'disabled',
            'handler',
            'scope',
            'menu',
            'tabIndex'
        ]);
        Ext.applyIf(config, {
            hideOnClick: hideOnClick,
            destroyMenu: false,
            listeners: null
        });
        config.text = component.overflowText || component.text;
        config.masterComponent = component;
        // Clone must have same value, and must sync original's value on change
        if (component.isFormField) {
            config.value = component.getValue();
            // If the component is a Checkbox/Radio field we replace the config with
            // a menucheckitem so it will give the Menu a better look and feel.
            // See additional information on the #addComponentToMenu method below.
            if (component instanceof Ext.form.field.Checkbox) {
                config = {
                    xtype: 'menucheckitem',
                    group: component.isRadio ? component.name + '_clone' : undefined,
                    text: component.boxLabel || component.fieldLabel,
                    name: component.name,
                    masterComponent: component,
                    checked: component.getValue(),
                    hideOnClick: false,
                    checkChangeDisabled: true
                };
            }
            // Sync the original component's value when the clone changes value.
            // This intentionally overwrites any developer-configured change listener on the clone.
            // That's because we monitor the clone's change event, and sync the
            // original field by calling setValue, so the original field's change
            // event will still fire.
            config.listeners = {
                change: function(c, newVal, oldVal) {
                    c.masterComponent.setValue(newVal);
                }
            };
            // Sync the cloned Component's value when the master changes value.
            component.on('change', function(c, newVal, oldVal) {
                c.overflowClone.setValue(newVal);
            });
        }
        // ToggleButtons become CheckItems
        else if (group || component.enableToggle) {
            Ext.apply(config, {
                hideOnClick: false,
                group: group,
                checked: component.pressed,
                handler: function(item, e) {
                    item.masterComponent.onClick(e);
                }
            });
        }
        // Buttons may have their text or icon changed - this must be propagated
        // to the clone in the overflow menu
        if (component.isButton && !component.changeListenersAdded) {
            component.on({
                textchange: this.onButtonAttrChange,
                iconchange: this.onButtonAttrChange,
                toggle: this.onButtonToggle
            });
            component.changeListenersAdded = true;
        }
        // Adding additional listeners
        component.on({
            enable: this.onComponentStatusChange,
            disable: this.onComponentStatusChange
        });
        // Typically margins are used to separate items in a toolbar
        // but don't really make a lot of sense in a menu, so we strip
        // them out here.
        delete config.margin;
        delete config.ownerCt;
        delete config.xtype;
        delete config.id;
        delete config.itemId;
        return config;
    },
    onButtonAttrChange: function(btn) {
        var clone = btn.overflowClone;
        clone.suspendLayouts();
        clone.setText(btn.text);
        clone.setIcon(btn.icon);
        clone.setIconCls(btn.iconCls);
        clone.resumeLayouts(true);
    },
    onButtonToggle: function(btn, state) {
        // Keep the clone in sync with the original if necessary
        if (btn.overflowClone.checked !== state) {
            btn.overflowClone.setChecked(state);
        }
    },
    onComponentStatusChange: function(cmp) {
        var clone = cmp.overflowClone;
        if (clone) {
            clone.setDisabled(cmp.disabled);
        }
    },
    /**
     * @private
     * Adds the given Toolbar item to the given menu. Buttons inside a buttongroup
     * are added individually.
     * @param {Ext.menu.Menu} menu The menu to add to
     * @param {Ext.Component} component The component to add
     * TODO: Implement overrides in Ext.layout.container.boxOverflow which create overrides
     * for SplitButton, Button, ButtonGroup, and TextField. And a generic one for Component
     * which create clones suitable for use in an overflow menu.
     */
    addComponentToMenu: function(menu, component) {
        var me = this,
            i, items, iLen;
        // No equivalent to fill, skip it
        if (component instanceof Ext.toolbar.Fill) {
            return;
        }
        // Separator maps to MenuSeparator
        else if (component instanceof Ext.toolbar.Separator) {
            menu.add('-');
        } else if (component.overflowClone) {
            menu.add(component.overflowClone);
        }
        // Other types...
        else if (component.isComponent) {
            if (component.isXType('splitbutton')) {
                component.overflowClone = menu.add(me.createMenuConfig(component, true));
            } else if (component.isXType('button')) {
                component.overflowClone = menu.add(me.createMenuConfig(component, !component.menu));
            } else if (component.isXType('buttongroup')) {
                items = component.items.items;
                iLen = items.length;
                for (i = 0; i < iLen; i++) {
                    me.addComponentToMenu(menu, items[i]);
                }
            }
            // If the component is a CheckBox/Radio field, we are supposed to
            // have a menucheckitem that will replace the original component.
            // Because of that, we need to add a value getter/setter and an event listener that
            // will fire the change event on click, making the menuitem behave as a 
            // checkbox/radio field would have.
            else if (component.isCheckbox) {
                component.overflowClone = menu.add(me.createMenuConfig(component));
                Ext.apply(component.overflowClone, {
                    getValue: function() {
                        return component.overflowClone.checked;
                    },
                    setValue: function() {
                        component.overflowClone.setChecked(component.getValue());
                    }
                });
                component.overflowClone.on('click', function(item) {
                    item.setChecked(item.masterComponent.isRadio ? true : !item.checked);
                    item.fireEvent('change', item, item.checked);
                });
            } else {
                component.overflowClone = menu.add(Ext.create(Ext.getClassName(component), me.createMenuConfig(component)));
            }
        }
    },
    destroy: function() {
        Ext.destroy(this.menu, this.menuTrigger);
        this.callParent();
    }
});

/**
 * Provides a lightweight HTML Editor component. Some toolbar features are not supported
 * by Safari and will be automatically hidden when needed. These are noted in the config options
 * where appropriate.
 *
 * The editor's toolbar buttons have tooltips defined in the {@link #buttonTips} property,
 * but they are not enabled by default unless the global {@link Ext.tip.QuickTipManager} singleton
 * is {@link Ext.tip.QuickTipManager#init initialized}.
 *
 * An Editor is a sensitive component that can't be used in all spots standard fields can be used.
 * Putting an Editor within any element that has display set to 'none' can cause problems in Safari
 * and Firefox due to their default iframe reloading bugs.
 *
 * # Example usage
 *
 * Simple example rendered with default options:
 *
 *     @example
 *     Ext.tip.QuickTipManager.init();  // enable tooltips
 *     Ext.create('Ext.form.HtmlEditor', {
 *         width: 580,
 *         height: 250,
 *         renderTo: Ext.getBody()
 *     });
 *
 * Passed via xtype into a container and with custom options:
 *
 *     @example
 *     Ext.tip.QuickTipManager.init();  // enable tooltips
 *     new Ext.panel.Panel({
 *         title: 'HTML Editor',
 *         renderTo: Ext.getBody(),
 *         width: 550,
 *         height: 250,
 *         frame: true,
 *         layout: 'fit',
 *         items: {
 *             xtype: 'htmleditor',
 *             enableColors: false,
 *             enableAlignments: false
 *         }
 *     });
 *     
 * # Reflow issues
 * 
 * In some browsers, a layout reflow will cause the underlying editor iframe to be reset. This
 * is most commonly seen when using the editor in collapsed panels with animation. In these cases
 * it is best to avoid animation.
 * More information can be found here: https://bugzilla.mozilla.org/show_bug.cgi?id=90268 
 */
Ext.define('Ext.form.field.HtmlEditor', {
    extend: 'Ext.form.FieldContainer',
    alias: 'widget.htmleditor',
    alternateClassName: 'Ext.form.HtmlEditor',
    requires: [
        'Ext.tip.QuickTipManager',
        'Ext.picker.Color',
        'Ext.layout.container.VBox',
        'Ext.toolbar.Item',
        'Ext.toolbar.Toolbar',
        'Ext.util.Format',
        'Ext.layout.component.field.HtmlEditor',
        'Ext.util.TaskManager',
        'Ext.layout.container.boxOverflow.Menu'
    ],
    mixins: {
        field: 'Ext.form.field.Field'
    },
    focusable: true,
    componentLayout: 'htmleditor',
    /**
     * @private
     */
    textareaCls: Ext.baseCSSPrefix + 'htmleditor-textarea',
    /* eslint-disable indent, max-len */
    componentTpl: [
        '{beforeTextAreaTpl}',
        '<textarea id="{id}-textareaEl" data-ref="textareaEl" name="{name}" tabindex="-1" {inputAttrTpl}',
        ' class="{textareaCls}" autocomplete="off">',
        '{[Ext.util.Format.htmlEncode(values.value)]}',
        '</textarea>',
        '{afterTextAreaTpl}',
        '{beforeIFrameTpl}',
        '<iframe id="{id}-iframeEl" data-ref="iframeEl" name="{iframeName}" frameBorder="0" {iframeAttrTpl}',
        ' src="{iframeSrc}" class="{iframeCls}"></iframe>',
        '{afterIFrameTpl}',
        {
            disableFormats: true
        }
    ],
    /* eslint-enable indent, max-len */
    stretchInputElFixed: true,
    subTplInsertions: [
        /**
         * @cfg {String/Array/Ext.XTemplate} beforeTextAreaTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * before the textarea element. If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'beforeTextAreaTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} afterTextAreaTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * after the textarea element. If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'afterTextAreaTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} beforeIFrameTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * before the iframe element. If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'beforeIFrameTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} afterIFrameTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * after the iframe element. If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'afterIFrameTpl',
        /**
         * @cfg {String/Array/Ext.XTemplate} iframeAttrTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * inside the iframe element (as attributes). If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'iframeAttrTpl',
        // inherited
        'inputAttrTpl'
    ],
    /**
     * @cfg {Boolean} enableFormat
     * Enable the bold, italic and underline buttons
     */
    enableFormat: true,
    /**
     * @cfg {Boolean} enableFontSize
     * Enable the increase/decrease font size buttons
     */
    enableFontSize: true,
    /**
     * @cfg {Boolean} enableColors
     * Enable the fore/highlight color buttons
     */
    enableColors: true,
    /**
     * @cfg {Boolean} enableAlignments
     * Enable the left, center, right alignment buttons
     */
    enableAlignments: true,
    /**
     * @cfg {Boolean} enableLists
     * Enable the bullet and numbered list buttons. Not available in Safari 2.
     */
    enableLists: true,
    /**
     * @cfg {Boolean} enableSourceEdit
     * Enable the switch to source edit button. Not available in Safari 2.
     */
    enableSourceEdit: true,
    /**
     * @cfg {Boolean} enableLinks
     * Enable the create link button. Not available in Safari 2.
     */
    enableLinks: true,
    /**
     * @cfg {Boolean} enableFont
     * Enable font selection. Not available in Safari 2.
     */
    enableFont: true,
    /**
     * @cfg {String} createLinkText
     * The default text for the create link prompt
     * @locale
     */
    createLinkText: 'Please enter the URL for the link:',
    /**
     * @cfg {String} [defaultLinkValue='http://']
     * The default value for the create link prompt
     */
    defaultLinkValue: 'http:/' + '/',
    /**
     * @cfg {String[]} fontFamilies
     * An array of available font families
     */
    fontFamilies: [
        'Arial',
        'Courier New',
        'Tahoma',
        'Times New Roman',
        'Verdana'
    ],
    /**
     * @cfg {String} defaultValue
     * A default value to be put into the editor to resolve focus issues.
     *
     * Defaults to (Non-breaking space) in Opera,
     * (Zero-width space) in all other browsers.
     */
    defaultValue: Ext.isOpera ? '&#160;' : '&#8203;',
    /**
     * @private
     */
    extraFieldBodyCls: Ext.baseCSSPrefix + 'html-editor-wrap',
    /**
     * @cfg {String} defaultButtonUI
     * A default {@link Ext.Component#ui ui} to use for the HtmlEditor's toolbar
     * {@link Ext.button.Button buttons}.
     */
    defaultButtonUI: 'default-toolbar',
    /**
     * @cfg {Object} buttonDefaults
     * A config object to apply to the toolbar's {@link Ext.button.Button buttons} to affect
     * how they operate, eg:
     *
     *     buttonDefaults: {
     *         tooltip: {
     *             align: 't-b',
     *             anchor: true
     *         }
     *     }
     *
     * @since 6.2.0
     */
    buttonDefaults: null,
    /**
     * @private
     */
    initialized: false,
    /**
     * @private
     */
    activated: false,
    /**
     * @private
     */
    sourceEditMode: false,
    /**
     * @private
     */
    iframePad: 3,
    /**
     * @private
     */
    hideMode: 'offsets',
    layout: {
        type: 'vbox',
        align: 'stretch'
    },
    maskOnDisable: true,
    containerElCls: Ext.baseCSSPrefix + 'html-editor-container',
    // This will strip any number of single or double quotes (in any order)
    // from a string at the anchors.
    reStripQuotes: /^['"]*|['"]*$/g,
    textAlignRE: /text-align:(.*?);/i,
    safariNonsenseRE: /\sclass="(?:Apple-style-span|Apple-tab-span|khtml-block-placeholder)"/gi,
    nonDigitsRE: /\D/g,
    /**
     * @event initialize
     * Fires when the editor is fully initialized (including the iframe)
     * @param {Ext.form.field.HtmlEditor} this
     */
    /**
     * @event activate
     * Fires when the editor is first receives the focus. Any insertion must wait until after
     * this event.
     * @param {Ext.form.field.HtmlEditor} this
     */
    /**
     * @event beforesync
     * Fires before the textarea is updated with content from the editor iframe.
     * Return false to cancel the sync.
     * @param {Ext.form.field.HtmlEditor} this
     * @param {String} html
     */
    /**
     * @event beforepush
     * Fires before the iframe editor is updated with content from the textarea.
     * Return false to cancel the push.
     * @param {Ext.form.field.HtmlEditor} this
     * @param {String} html
     */
    /**
     * @event sync
     * Fires when the textarea is updated with content from the editor iframe.
     * @param {Ext.form.field.HtmlEditor} this
     * @param {String} html
     */
    /**
     * @event push
     * Fires when the iframe editor is updated with content from the textarea.
     * @param {Ext.form.field.HtmlEditor} this
     * @param {String} html
     */
    /**
     * @event editmodechange
     * Fires when the editor switches edit modes
     * @param {Ext.form.field.HtmlEditor} this
     * @param {Boolean} sourceEdit True if source edit, false if standard editing.
     */
    /**
     * @private
     */
    initComponent: function() {
        var me = this;
        me.items = [
            me.createToolbar(),
            me.createInputCmp()
        ];
        // No value set, we must report empty string
        if (me.value == null) {
            me.value = '';
        }
        me.callParent(arguments);
        me.initField();
    },
    createInputCmp: function() {
        this.inputCmp = Ext.widget(this.getInputCmpCfg());
        return this.inputCmp;
    },
    getInputCmpCfg: function() {
        var me = this,
            id = me.id + '-inputCmp',
            data = {
                id: id,
                name: me.name,
                textareaCls: me.textareaCls + ' ' + Ext.baseCSSPrefix + 'hidden',
                value: me.value,
                iframeName: Ext.id(),
                iframeSrc: Ext.SSL_SECURE_URL,
                iframeCls: Ext.baseCSSPrefix + 'htmleditor-iframe'
            };
        me.getInsertionRenderData(data, me.subTplInsertions);
        return {
            flex: 1,
            xtype: 'component',
            tpl: me.lookupTpl('componentTpl'),
            childEls: [
                'iframeEl',
                'textareaEl'
            ],
            id: id,
            cls: Ext.baseCSSPrefix + 'html-editor-input',
            data: data
        };
    },
    /**
     * Called when the editor creates its toolbar. Override this method if you need to
     * add custom toolbar buttons.
     * @param {Ext.form.field.HtmlEditor} editor
     * @protected
     */
    createToolbar: function() {
        this.toolbar = Ext.widget(this.getToolbarCfg());
        return this.toolbar;
    },
    getToolbarCfg: function() {
        var me = this,
            items = [],
            i,
            tipsEnabled = Ext.quickTipsActive && Ext.tip.QuickTipManager.isEnabled(),
            baseCSSPrefix = Ext.baseCSSPrefix,
            fontSelectItem, undef;
        function btn(id, toggle, handler) {
            return Ext.merge({
                itemId: id,
                cls: baseCSSPrefix + 'btn-icon',
                iconCls: baseCSSPrefix + 'edit-' + id,
                enableToggle: toggle !== false,
                scope: me,
                handler: handler || me.relayBtnCmd,
                clickEvent: 'mousedown',
                tooltip: tipsEnabled ? me.buttonTips[id] : undef,
                overflowText: me.buttonTips[id].title || undef,
                tabIndex: -1
            }, me.buttonDefaults);
        }
        if (me.enableFont) {
            fontSelectItem = Ext.widget('component', {
                itemId: 'fontSelect',
                renderTpl: [
                    '<select id="{id}-selectEl" data-ref="selectEl" class="' + baseCSSPrefix + 'font-select">',
                    '</select>'
                ],
                childEls: [
                    'selectEl'
                ],
                afterRender: function() {
                    me.fontSelect = this.selectEl;
                    Ext.Component.prototype.afterRender.apply(this, arguments);
                },
                onDisable: function() {
                    var selectEl = this.selectEl;
                    if (selectEl) {
                        selectEl.dom.disabled = true;
                    }
                    Ext.Component.prototype.onDisable.apply(this, arguments);
                },
                onEnable: function() {
                    var selectEl = this.selectEl;
                    if (selectEl) {
                        selectEl.dom.disabled = false;
                    }
                    Ext.Component.prototype.onEnable.apply(this, arguments);
                },
                listeners: {
                    change: function() {
                        me.win.focus();
                        me.relayCmd('fontName', me.fontSelect.dom.value);
                        me.deferFocus();
                    },
                    element: 'selectEl'
                }
            });
            items.push(fontSelectItem, '-');
        }
        if (me.enableFormat) {
            items.push(btn('bold'), btn('italic'), btn('underline'));
        }
        if (me.enableFontSize) {
            items.push('-', btn('increasefontsize', false, me.adjustFont), btn('decreasefontsize', false, me.adjustFont));
        }
        if (me.enableColors) {
            items.push('-', Ext.merge({
                itemId: 'forecolor',
                cls: baseCSSPrefix + 'btn-icon',
                iconCls: baseCSSPrefix + 'edit-forecolor',
                overflowText: me.buttonTips.forecolor.title,
                tooltip: tipsEnabled ? me.buttonTips.forecolor || undef : undef,
                tabIndex: -1,
                menu: Ext.widget('menu', {
                    plain: true,
                    items: [
                        {
                            xtype: 'colorpicker',
                            allowReselect: true,
                            focus: Ext.emptyFn,
                            value: '000000',
                            plain: true,
                            clickEvent: 'mousedown',
                            handler: function(cp, color) {
                                me.relayCmd('forecolor', Ext.isWebKit || Ext.isIE || Ext.isEdge ? '#' + color : color);
                                this.up('menu').hide();
                            }
                        }
                    ]
                })
            }, me.buttonDefaults), Ext.merge({
                itemId: 'backcolor',
                cls: baseCSSPrefix + 'btn-icon',
                iconCls: baseCSSPrefix + 'edit-backcolor',
                overflowText: me.buttonTips.backcolor.title,
                tooltip: tipsEnabled ? me.buttonTips.backcolor || undef : undef,
                tabIndex: -1,
                menu: Ext.widget('menu', {
                    plain: true,
                    items: [
                        {
                            xtype: 'colorpicker',
                            focus: Ext.emptyFn,
                            value: 'FFFFFF',
                            plain: true,
                            allowReselect: true,
                            clickEvent: 'mousedown',
                            handler: function(cp, color) {
                                if (Ext.isGecko) {
                                    me.execCmd('useCSS', false);
                                    me.execCmd('hilitecolor', '#' + color);
                                    me.execCmd('useCSS', true);
                                    me.deferFocus();
                                } else {
                                    // eslint-disable-next-line max-len
                                    me.relayCmd(Ext.isOpera ? 'hilitecolor' : 'backcolor', Ext.isWebKit || Ext.isIE || Ext.isEdge || Ext.isOpera ? '#' + color : color);
                                }
                                this.up('menu').hide();
                            }
                        }
                    ]
                })
            }, me.buttonDefaults));
        }
        if (me.enableAlignments) {
            items.push('-', btn('justifyleft'), btn('justifycenter'), btn('justifyright'));
        }
        if (me.enableLinks) {
            items.push('-', btn('createlink', false, me.createLink));
        }
        if (me.enableLists) {
            items.push('-', btn('insertorderedlist'), btn('insertunorderedlist'));
        }
        if (me.enableSourceEdit) {
            items.push('-', btn('sourceedit', true, function() {
                me.toggleSourceEdit(!me.sourceEditMode);
            }));
        }
        // Everything starts disabled.
        for (i = 0; i < items.length; i++) {
            if (items[i].itemId !== 'sourceedit') {
                items[i].disabled = true;
            }
        }
        // build the toolbar
        // Automatically rendered in Component.afterRender's renderChildren call
        return {
            xtype: 'toolbar',
            defaultButtonUI: me.defaultButtonUI,
            cls: Ext.baseCSSPrefix + 'html-editor-tb',
            enableOverflow: true,
            items: items,
            // stop form submits
            listeners: {
                click: function(e) {
                    e.preventDefault();
                },
                element: 'el'
            }
        };
    },
    getMaskTarget: function() {
        // Can't be the body td directly because of issues with absolute positioning
        // inside td's in FF
        return Ext.isGecko ? this.inputCmp.el : this.bodyEl;
    },
    /**
     * Sets the read only state of this field.
     * @param {Boolean} readOnly Whether the field should be read only.
     */
    setReadOnly: function(readOnly) {
        var me = this,
            textareaEl = me.textareaEl,
            iframeEl = me.iframeEl,
            body;
        me.readOnly = readOnly;
        if (textareaEl) {
            textareaEl.dom.readOnly = readOnly;
        }
        if (me.initialized) {
            body = me.getEditorBody();
            if (Ext.isIE) {
                // Hide the iframe while setting contentEditable so it doesn't grab focus
                iframeEl.setDisplayed(false);
                body.contentEditable = !readOnly;
                iframeEl.setDisplayed(true);
            } else {
                me.setDesignMode(!readOnly);
            }
            if (body) {
                body.style.cursor = readOnly ? 'default' : 'text';
            }
            me.disableItems(readOnly);
        }
    },
    /**
     * Called when the editor initializes the iframe with HTML contents. Override this method if you
     * want to change the initialization markup of the iframe (e.g. to add stylesheets).
     *
     * **Note:** IE8-Standards has unwanted scroller behavior, so the default meta tag forces IE7
     * compatibility. Also note that forcing IE7 mode works when the page is loaded normally,
     * but if you are using IE's Web Developer Tools to manually set the document mode, that will
     * take precedence and override what this code sets by default. This can be confusing when
     * developing, but is not a user-facing issue.
     * @protected
     */
    getDocMarkup: function() {
        var me = this,
            h = me.iframeEl.getHeight() - me.iframePad * 2;
        /* eslint-disable max-len */
        // - IE9+ require a strict doctype otherwise text outside visible area can't be selected.
        // - Opera inserts <P> tags on Return key, so P margins must be removed to avoid double line-height.
        // - On browsers other than IE, the font is not inherited by the IFRAME so it must be specified.
        return Ext.String.format('<!DOCTYPE html>' + '<html><head><style type="text/css">' + (Ext.isOpera || Ext.isIE ? 'p{margin:0;}' : '') + 'body{border:0;margin:0;padding:{0}px;direction:' + (me.rtl ? 'rtl;' : 'ltr;') + (Ext.isIE8 ? Ext.emptyString : 'min-') + 'height:{1}px;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;cursor:text;background-color:white;' + (Ext.isIE ? '' : 'font-size:12px;font-family:{2}') + '}</style></head><body></body></html>', me.iframePad, h, me.defaultFont);
    },
    /* eslint-enable max-len */
    /**
     * @private
     */
    getEditorBody: function() {
        var doc = this.getDoc();
        return doc && doc.body;
    },
    /**
     * @private
     */
    getDoc: function() {
        return this.iframeEl.dom.contentDocument || this.getWin().document;
    },
    /**
     * @private
     */
    getWin: function() {
        // using window.frames[id] to access the the iframe's window object in FF creates
        // a global variable with name == id in the global scope that references the iframe
        // window.  This is undesirable for unit testing because that global variable
        // is readonly and cannot be deleted.  To avoid this, we use contentWindow if it
        // is available (and it is in all supported browsers at the time of this writing)
        // and fall back to window.frames if contentWindow is not available.
        return this.iframeEl.dom.contentWindow || window.frames[this.iframeEl.dom.name];
    },
    initDefaultFont: function() {
        // It's not ideal to do this here since it's a write phase, but we need to know
        // what the font used in the textarea is so that we can setup the appropriate font
        // options in the select box. The select box will reflow once we populate it, so we want
        // to do so before we layout the first time.
        var me = this,
            selIdx = 0,
            fonts, font, select, option, i, len, lower;
        if (!me.defaultFont) {
            font = me.textareaEl.getStyle('font-family');
            font = Ext.String.capitalize(font.split(',')[0]);
            fonts = Ext.Array.clone(me.fontFamilies);
            Ext.Array.include(fonts, font);
            fonts.sort();
            me.defaultFont = font;
            select = me.down('#fontSelect').selectEl.dom;
            for (i = 0 , len = fonts.length; i < len; ++i) {
                font = fonts[i];
                lower = font.toLowerCase();
                option = new Option(font, lower);
                if (font === me.defaultFont) {
                    selIdx = i;
                }
                option.style.fontFamily = lower;
                if (Ext.isIE) {
                    select.add(option);
                } else {
                    select.options.add(option);
                }
            }
            // Old IE versions have a problem if we set the selected property
            // in the loop, so set it after.
            select.options[selIdx].selected = true;
        }
    },
    isEqual: function(value1, value2) {
        return this.isEqualAsString(value1, value2);
    },
    /**
     * @private
     */
    afterRender: function() {
        var me = this,
            inputCmp = me.inputCmp;
        me.callParent(arguments);
        me.iframeEl = inputCmp.iframeEl;
        me.textareaEl = inputCmp.textareaEl;
        // The input element is interrogated by the layout to extract height when labelAlign
        // is 'top'. It must be set, and then switched between the iframe and the textarea
        me.inputEl = me.iframeEl;
        if (me.enableFont) {
            me.initDefaultFont();
        }
        // Start polling to step through the phases of document readiness.
        me.initPhase = 0;
        me.initializeTask = Ext.TaskManager.start({
            run: me.initFrameDoc,
            scope: me,
            interval: 10,
            duration: 5000
        });
    },
    initFrameDoc: function() {
        var me = this,
            doc = me.getDoc();
        // Destroying the component during initialization cancels initialization.
        if (me.destroying || me.destroyed) {
            return Ext.TaskManager.stop(me.initializeTask);
        }
        switch (me.initPhase) {
            case 0:
                if (doc) {
                    me.win = me.getWin();
                    doc.open();
                    doc.write(me.getDocMarkup());
                    doc.close();
                    me.initPhase++;
                };
                break;
            case 1:
                if (doc.body || doc.readyState === 'complete') {
                    me.setDesignMode(true);
                    me.initPhase++;
                };
                break;
            case 2:
                me.initEditor();
                Ext.TaskManager.stop(me.initializeTask);
        }
    },
    /**
     * @private
     * Sets current design mode. To enable, mode can be true or 'on', off otherwise
     */
    setDesignMode: function(mode) {
        var me = this,
            doc = me.getDoc();
        if (doc) {
            if (me.readOnly) {
                mode = false;
            }
            doc.designMode = (/on|true/i).test(String(mode).toLowerCase()) ? 'on' : 'off';
        }
    },
    /**
     * @private
     */
    getDesignMode: function() {
        var doc = this.getDoc();
        return !doc ? '' : String(doc.designMode).toLowerCase();
    },
    disableItems: function(disabled) {
        var items = this.getToolbar().items.items,
            i,
            iLen = items.length,
            item;
        for (i = 0; i < iLen; i++) {
            item = items[i];
            if (item.getItemId() !== 'sourceedit') {
                item.setDisabled(disabled);
            }
        }
    },
    /**
     * Toggles the editor between standard and source edit mode.
     * @param {Boolean} [sourceEditMode] True for source edit, false for standard
     */
    toggleSourceEdit: function(sourceEditMode) {
        var me = this,
            iframe = me.iframeEl,
            textarea = me.textareaEl,
            hiddenCls = Ext.baseCSSPrefix + 'hidden',
            btn = me.getToolbar().getComponent('sourceedit');
        if (!Ext.isBoolean(sourceEditMode)) {
            sourceEditMode = !me.sourceEditMode;
        }
        me.sourceEditMode = sourceEditMode;
        if (btn.pressed !== sourceEditMode) {
            btn.toggle(sourceEditMode);
        }
        if (sourceEditMode) {
            me.disableItems(true);
            me.syncValue();
            iframe.addCls(hiddenCls);
            textarea.removeCls(hiddenCls);
            textarea.dom.removeAttribute('tabIndex');
            textarea.focus();
            me.inputEl = textarea;
        } else {
            if (me.initialized) {
                me.disableItems(me.readOnly);
            }
            me.pushValue();
            iframe.removeCls(hiddenCls);
            textarea.addCls(hiddenCls);
            textarea.dom.setAttribute('tabIndex', -1);
            me.deferFocus();
            me.inputEl = iframe;
        }
        me.fireEvent('editmodechange', me, sourceEditMode);
        me.updateLayout();
    },
    /**
     * @private
     */
    createLink: function() {
        var url = prompt(this.createLinkText, this.defaultLinkValue);
        if (url && url !== 'http:/' + '/') {
            this.relayCmd('createlink', url);
        }
    },
    clearInvalid: Ext.emptyFn,
    setValue: function(value) {
        var me = this,
            textarea = me.textareaEl;
        if (value === null || value === undefined) {
            value = '';
        }
        // Only update the field if the value has changed
        if (me.value !== value) {
            if (textarea) {
                textarea.dom.value = value;
            }
            me.pushValue();
            if (!me.rendered && me.inputCmp) {
                me.inputCmp.data.value = value;
            }
            me.mixins.field.setValue.call(me, value);
        }
        return me;
    },
    /**
     * If you need/want custom HTML cleanup, this is the method you should override.
     * @param {String} html The HTML to be cleaned
     * @return {String} The cleaned HTML
     * @protected
     */
    cleanHtml: function(html) {
        html = String(html);
        if (Ext.isWebKit) {
            // strip safari nonsense
            html = html.replace(this.safariNonsenseRE, '');
        }
        /*
         * Neat little hack. Strips out all the non-digit characters from the default
         * value and compares it to the character code of the first character in the string
         * because it can cause encoding issues when posted to the server. We need the
         * parseInt here because charCodeAt will return a number.
         */
        if (html.charCodeAt(0) === parseInt(this.defaultValue.replace(this.nonDigitsRE, ''), 10)) {
            html = html.substring(1);
        }
        return html;
    },
    /**
     * Syncs the contents of the editor iframe with the textarea.
     * @protected
     */
    syncValue: function() {
        var me = this,
            body, changed, html, bodyStyleText, match, textElDom;
        if (me.initialized) {
            body = me.getEditorBody();
            html = body.innerHTML;
            textElDom = me.textareaEl.dom;
            if (Ext.isWebKit) {
                // Safari puts text-align styles on the body element!
                bodyStyleText = body.style.cssText;
                match = bodyStyleText.match(me.textAlignRE);
                if (match && match[1]) {
                    html = '<div style="' + match[0] + '">' + html + '</div>';
                }
            }
            html = me.cleanHtml(html);
            if (me.fireEvent('beforesync', me, html) !== false) {
                // Gecko inserts single <br> tag when input is empty
                // and user toggles source mode. See https://sencha.jira.com/browse/EXTJSIV-8542
                if (Ext.isGecko && textElDom.value === '' && html === '<br>') {
                    html = '';
                }
                if (textElDom.value !== html) {
                    textElDom.value = html;
                    changed = true;
                }
                me.fireEvent('sync', me, html);
                if (changed) {
                    // we have to guard this to avoid infinite recursion because getValue
                    // calls this method...
                    me.checkChange();
                }
            }
        }
    },
    getValue: function() {
        var me = this,
            value;
        if (!me.sourceEditMode) {
            me.syncValue();
        }
        value = me.rendered ? me.textareaEl.dom.value : me.value;
        me.value = value;
        return value;
    },
    /**
     * Pushes the value of the textarea into the iframe editor.
     * @protected
     */
    pushValue: function() {
        var me = this,
            v;
        if (me.initialized) {
            v = me.textareaEl.dom.value || '';
            if (!me.activated && v.length < 1) {
                v = me.defaultValue;
            }
            if (me.fireEvent('beforepush', me, v) !== false) {
                me.getEditorBody().innerHTML = v;
                if (Ext.isGecko) {
                    // Gecko hack, see: https://bugzilla.mozilla.org/show_bug.cgi?id=232791#c8
                    me.setDesignMode(false);
                    // toggle off first
                    me.setDesignMode(true);
                }
                me.fireEvent('push', me, v);
            }
        }
    },
    focus: function(selectText, delay) {
        var me = this,
            value, focusEl;
        if (delay) {
            if (!me.focusTask) {
                me.focusTask = new Ext.util.DelayedTask(me.focus);
            }
            me.focusTask.delay(Ext.isNumber(delay) ? delay : 10, null, me, [
                selectText,
                false
            ]);
        } else {
            if (selectText) {
                if (me.textareaEl && me.textareaEl.dom) {
                    value = me.textareaEl.dom.value;
                }
                // Make sure there is content before calling SelectAll,
                // otherwise the caret disappears.
                if (value && value.length) {
                    me.execCmd('selectall', true);
                }
            }
            focusEl = me.getFocusEl();
            if (focusEl && focusEl.focus) {
                focusEl.focus();
            }
        }
        return me;
    },
    /**
     * @private
     */
    initEditor: function() {
        var me = this,
            dbody = me.getEditorBody(),
            ss = me.textareaEl.getStyle([
                'font-size',
                'font-family',
                'background-image',
                'background-repeat',
                'background-color',
                'color'
            ]),
            doc = me.getDoc(),
            docEl = Ext.get(doc),
            fn;
        ss['background-attachment'] = 'fixed';
        // w3c
        dbody.bgProperties = 'fixed';
        // ie
        Ext.DomHelper.applyStyles(dbody, ss);
        if (docEl) {
            try {
                docEl.clearListeners();
            } catch (e) {}
            // ignore
            /*
             * Update toolbar state on a buffered timer when document changes.
             */
            fn = Ext.Function.createBuffered(me.updateToolbar, 100, me);
            docEl.on({
                mousedown: fn,
                dblclick: fn,
                click: fn,
                keyup: fn,
                delegated: false
            });
            // These events need to be relayed from the inner document (where they stop
            // bubbling) up to the outer document. This has to be done at the DOM level so
            // the event reaches listeners on elements like the document body. The effected
            // mechanisms that depend on this bubbling behavior are listed to the right
            // of the event.
            fn = me.onRelayedEvent;
            docEl.on({
                mousedown: fn,
                // menu dismisal (MenuManager) and Window onMouseDown (toFront)
                mousemove: fn,
                // window resize drag detection
                mouseup: fn,
                // window resize termination
                click: fn,
                // not sure, but just to be safe
                dblclick: fn,
                // not sure again
                delegated: false,
                scope: me
            });
            if (Ext.isGecko) {
                docEl.on('keypress', me.applyCommand, me);
            }
            if (me.fixKeys) {
                docEl.on('keydown', me.fixKeys, me, {
                    delegated: false
                });
            }
            if (me.fixKeysAfter) {
                docEl.on('keyup', me.fixKeysAfter, me, {
                    delegated: false
                });
            }
            if (Ext.isIE9) {
                Ext.get(doc.documentElement).on('focus', me.focus, me);
            }
            // In old IEs, clicking on a toolbar button shifts focus from iframe
            // and it loses selection. To avoid this, we save current selection
            // and restore it.
            if (Ext.isIE8) {
                docEl.on('focusout', function() {
                    me.savedSelection = doc.selection.type !== 'None' ? doc.selection.createRange() : null;
                }, me);
                docEl.on('focusin', function() {
                    if (me.savedSelection) {
                        me.savedSelection.select();
                    }
                }, me);
            }
            // We need to be sure we remove all our events from the iframe on unload
            // or we're going to LEAK!
            Ext.getWin().on('unload', me.destroyEditor, me);
            me.initialized = true;
            me.pushValue();
            me.setReadOnly(me.readOnly);
            me.fireEvent('initialize', me);
        }
    },
    /**
     * @private
     */
    destroyEditor: function() {
        var me = this,
            initializeTask = me.initializeTask,
            doc, prop;
        if (initializeTask) {
            Ext.TaskManager.stop(initializeTask, true);
        }
        if (me.rendered) {
            Ext.getWin().un('unload', me.destroyEditor, me);
            doc = me.getDoc();
            if (doc) {
                // removeAll() doesn't currently know how to handle iframe document,
                // so for now we have to wrap it in an Ext.Element,
                // or else IE6/7 will leak big time when the page is refreshed.
                // TODO: this may not be needed once we find a more permanent fix.
                // see EXTJSIV-5891.
                Ext.get(doc).destroy();
                if (doc.hasOwnProperty) {
                    for (prop in doc) {
                        try {
                            if (doc.hasOwnProperty(prop)) {
                                delete doc[prop];
                            }
                        } catch (e) {}
                    }
                }
            }
        }
    },
    // clearing certain props on document MAY throw in IE
    /**
     * @private
     */
    doDestroy: function() {
        this.destroyEditor();
        this.callParent();
    },
    /**
     * @private
     */
    onRelayedEvent: function(event) {
        // relay event from the iframe's document to the document that owns the iframe...
        var iframeEl = this.iframeEl,
            iframeXY = Ext.fly(iframeEl).getTrueXY(),
            originalEventXY = event.getXY(),
            eventXY = event.getXY();
        // the event from the inner document has XY relative to that document's origin,
        // so adjust it to use the origin of the iframe in the outer document:
        event.xy = [
            iframeXY[0] + eventXY[0],
            iframeXY[1] + eventXY[1]
        ];
        event.injectEvent(iframeEl);
        // blame the iframe for the event...
        event.xy = originalEventXY;
    },
    // restore the original XY (just for safety)
    /**
     * @private
     */
    onFirstFocus: function() {
        var me = this,
            selection, range;
        me.activated = true;
        me.disableItems(me.readOnly);
        if (Ext.isGecko) {
            // prevent silly gecko errors
            me.win.focus();
            selection = me.win.getSelection();
            // If the editor contains a <br> tag, clicking on the editor after the text where
            // the <br> broke the line will produce nodeType === 1 (the body tag).
            // It's better to check the length of the selection.focusNode's content.
            //
            // If htmleditor.value = ' ' (note the space)
            // 1. nodeType === 1
            // 2. nodeName === 'BODY'
            // 3. selection.focusNode.textContent.length === 1
            //
            // If htmleditor.value = '' (no chars) nodeType === 3 && nodeName === '#text'
            // 1. nodeType === 3
            // 2. nodeName === '#text'
            // 3. selection.focusNode.textContent.length === 1 (yes, that's right, 1)
            //
            // The editor inserts Unicode code point 8203, a zero-width space when
            // htmleditor.value === '' (call selection.focusNode.textContent.charCodeAt(0))
            // http://www.fileformat.info/info/unicode/char/200b/index.htm
            // So, test with framework method to normalize.
            if (selection.focusNode && !me.getValue().length) {
                range = selection.getRangeAt(0);
                range.selectNodeContents(me.getEditorBody());
                range.collapse(true);
                me.deferFocus();
            }
            try {
                me.execCmd('useCSS', true);
                me.execCmd('styleWithCSS', false);
            } catch (e) {}
        }
        // ignore (why?)
        me.fireEvent('activate', me);
    },
    /**
     * @private
     */
    adjustFont: function(btn) {
        var adjust = btn.getItemId() === 'increasefontsize' ? 1 : -1,
            size = this.getDoc().queryCommandValue('FontSize') || '2',
            isPxSize = Ext.isString(size) && size.indexOf('px') !== -1,
            isSafari;
        size = parseInt(size, 10);
        if (isPxSize) {
            // Safari 3 values
            // 1 = 10px, 2 = 13px, 3 = 16px, 4 = 18px, 5 = 24px, 6 = 32px
            if (size <= 10) {
                size = 1 + adjust;
            } else if (size <= 13) {
                size = 2 + adjust;
            } else if (size <= 16) {
                size = 3 + adjust;
            } else if (size <= 18) {
                size = 4 + adjust;
            } else if (size <= 24) {
                size = 5 + adjust;
            } else {
                size = 6 + adjust;
            }
            size = Ext.Number.constrain(size, 1, 6);
        } else {
            isSafari = Ext.isSafari;
            if (isSafari) {
                // safari
                adjust *= 2;
            }
            size = Math.max(1, size + adjust) + (isSafari ? 'px' : 0);
        }
        this.relayCmd('FontSize', size);
    },
    /**
     * Triggers a toolbar update by reading the markup state of the current selection in the editor.
     * @protected
     */
    updateToolbar: function() {
        var me = this,
            i, l, btns, doc, name, queriedName, fontSelect, toolbarSubmenus;
        if (me.readOnly) {
            return;
        }
        if (!me.activated) {
            me.onFirstFocus();
            return;
        }
        btns = me.getToolbar().items.map;
        doc = me.getDoc();
        if (me.enableFont) {
            // When querying the fontName, Chrome may return an Array of font names
            // with those containing spaces being placed between single-quotes.
            queriedName = doc.queryCommandValue('fontName');
            // eslint-disable-next-line max-len
            name = (queriedName ? queriedName.split(",")[0].replace(me.reStripQuotes, '') : me.defaultFont).toLowerCase();
            fontSelect = me.fontSelect.dom;
            if (name !== fontSelect.value || name !== queriedName) {
                fontSelect.value = name;
            }
        }
        function updateButtons() {
            var state;
            for (i = 0 , l = arguments.length , name; i < l; i++) {
                name = arguments[i];
                // Firefox 18+ sometimes throws NS_ERROR_INVALID_POINTER exception
                // See https://sencha.jira.com/browse/EXTJSIV-9766
                try {
                    state = doc.queryCommandState(name);
                } catch (e) {
                    state = false;
                }
                btns[name].toggle(state);
            }
        }
        if (me.enableFormat) {
            updateButtons('bold', 'italic', 'underline');
        }
        if (me.enableAlignments) {
            updateButtons('justifyleft', 'justifycenter', 'justifyright');
        }
        if (me.enableLists) {
            updateButtons('insertorderedlist', 'insertunorderedlist');
        }
        // Ensure any of our toolbar's owned menus are hidden.
        // The overflow menu must control itself.
        toolbarSubmenus = me.toolbar.query('menu');
        for (i = 0; i < toolbarSubmenus.length; i++) {
            toolbarSubmenus[i].hide();
        }
        me.syncValue();
    },
    /**
     * @private
     */
    relayBtnCmd: function(btn) {
        this.relayCmd(btn.getItemId());
    },
    /**
     * Executes a Midas editor command on the editor document and performs necessary focus
     * and toolbar updates.
     * **This should only be called after the editor is initialized.**
     * @param {String} cmd The Midas command
     * @param {String/Boolean} [value=null] The value to pass to the command
     */
    relayCmd: function(cmd, value) {
        Ext.defer(function() {
            var me = this;
            if (!this.destroyed) {
                me.win.focus();
                me.execCmd(cmd, value);
                me.updateToolbar();
            }
        }, 10, this);
    },
    /**
     * Executes a Midas editor command directly on the editor document. For visual commands,
     * you should use {@link #relayCmd} instead.
     * **This should only be called after the editor is initialized.**
     * @param {String} cmd The Midas command
     * @param {String/Boolean} [value=null] The value to pass to the command
     */
    execCmd: function(cmd, value) {
        var me = this,
            doc = me.getDoc();
        doc.execCommand(cmd, false, (value === undefined ? null : value));
        me.syncValue();
    },
    /**
     * @private
     */
    applyCommand: function(e) {
        if (e.ctrlKey) {
            // eslint-disable-next-line vars-on-top
            var me = this,
                c = e.getCharCode(),
                cmd;
            if (c > 0) {
                c = String.fromCharCode(c);
                switch (c) {
                    case 'b':
                        cmd = 'bold';
                        break;
                    case 'i':
                        cmd = 'italic';
                        break;
                    case 'u':
                        cmd = 'underline';
                        break;
                }
                if (cmd) {
                    me.win.focus();
                    me.execCmd(cmd);
                    me.deferFocus();
                    e.preventDefault();
                }
            }
        }
    },
    /**
     * Inserts the passed text at the current cursor position.
     * __Note:__ the editor must be initialized and activated to insert text.
     * @param {String} text
     */
    insertAtCursor: function(text) {
        // adapted from http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
        var me = this,
            win = me.getWin(),
            doc = me.getDoc(),
            sel, range, el, frag, node, lastNode;
        if (me.activated) {
            win.focus();
            if (win.getSelection) {
                sel = win.getSelection();
                if (sel.getRangeAt && sel.rangeCount) {
                    range = sel.getRangeAt(0);
                    range.deleteContents();
                    // Range.createContextualFragment() would be useful here but is
                    // only relatively recently standardized and is not supported in
                    // some browsers (IE9, for one)
                    el = doc.createElement("div");
                    el.innerHTML = text;
                    frag = doc.createDocumentFragment();
                    while ((node = el.firstChild)) {
                        lastNode = frag.appendChild(node);
                    }
                    range.insertNode(frag);
                    // Preserve the selection
                    if (lastNode) {
                        range = range.cloneRange();
                        range.setStartAfter(lastNode);
                        range.collapse(true);
                        sel.removeAllRanges();
                        sel.addRange(range);
                    }
                }
            } else if (doc.selection && sel.type !== 'Control') {
                sel = doc.selection;
                range = sel.createRange();
                range.collapse(true);
                sel.createRange().pasteHTML(text);
            }
            me.deferFocus();
        }
    },
    /**
     * @private
     * Load time branching for fastest keydown performance.
     */
    fixKeys: (function() {
        if (Ext.isIE10m) {
            return function(e) {
                var me = this,
                    k = e.getKey(),
                    doc = me.getDoc(),
                    readOnly = me.readOnly,
                    range;
                if (k === e.TAB) {
                    e.stopEvent();
                    // TODO: add tab support for IE 11.
                    if (!readOnly) {
                        range = doc.selection.createRange();
                        if (range) {
                            if (range.collapse) {
                                range.collapse(true);
                                range.pasteHTML('&#160;&#160;&#160;&#160;');
                            }
                            me.deferFocus();
                        }
                    }
                }
            };
        }
        if (Ext.isOpera) {
            return function(e) {
                var me = this,
                    k = e.getKey(),
                    readOnly = me.readOnly;
                if (k === e.TAB) {
                    e.stopEvent();
                    if (!readOnly) {
                        me.win.focus();
                        me.execCmd('InsertHTML', '&#160;&#160;&#160;&#160;');
                        me.deferFocus();
                    }
                }
            };
        }
        // Not needed, so null.
        return null;
    }()),
    /**
     * @private
     */
    fixKeysAfter: (function() {
        if (Ext.isIE) {
            return function(e) {
                var me = this,
                    k = e.getKey(),
                    doc = me.getDoc(),
                    readOnly = me.readOnly,
                    innerHTML;
                if (!readOnly && (k === e.BACKSPACE || k === e.DELETE)) {
                    innerHTML = doc.body.innerHTML;
                    // If HtmlEditor had some input and user cleared it, IE inserts <p>&nbsp;</p>
                    // which makes an impression that there is still some text, and creeps
                    // into source mode when toggled. We don't want this.
                    //
                    // See https://sencha.jira.com/browse/EXTJSIV-8542
                    //
                    // N.B. There is **small** chance that user could go to source mode,
                    // type '<p>&nbsp;</p>', switch back to visual mode, type something else
                    // and then clear it -- the code below would clear the <p> tag as well,
                    // which could be considered a bug. However I see no way to distinguish
                    // between offending markup being entered manually and generated by IE,
                    // so this can be considered a nasty corner case.
                    //
                    if (innerHTML === '<p>&nbsp;</p>' || innerHTML === '<P>&nbsp;</P>') {
                        doc.body.innerHTML = '';
                    }
                }
            };
        }
        return null;
    }()),
    /**
     * Returns the editor's toolbar. **This is only available after the editor has been rendered.**
     * @return {Ext.toolbar.Toolbar}
     */
    getToolbar: function() {
        return this.toolbar;
    },
    /**
     * @property {Object} buttonTips
     * Object collection of toolbar tooltips for the buttons in the editor.
     * The key is the command id associated with that button and the value is a valid
     * QuickTips object. For example:
     *
     *     {
     *         bold: {
     *             title: 'Bold (Ctrl+B)',
     *             text: 'Make the selected text bold.',
     *             cls: 'x-html-editor-tip'
     *         },
     *         italic: {
     *             title: 'Italic (Ctrl+I)',
     *             text: 'Make the selected text italic.',
     *             cls: 'x-html-editor-tip'
     *         }
     *         // ...
     *     }
     * @locale
     */
    buttonTips: {
        bold: {
            title: 'Bold (Ctrl+B)',
            text: 'Make the selected text bold.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        },
        italic: {
            title: 'Italic (Ctrl+I)',
            text: 'Make the selected text italic.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        },
        underline: {
            title: 'Underline (Ctrl+U)',
            text: 'Underline the selected text.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        },
        increasefontsize: {
            title: 'Grow Text',
            text: 'Increase the font size.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        },
        decreasefontsize: {
            title: 'Shrink Text',
            text: 'Decrease the font size.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        },
        backcolor: {
            title: 'Text Highlight Color',
            text: 'Change the background color of the selected text.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        },
        forecolor: {
            title: 'Font Color',
            text: 'Change the color of the selected text.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        },
        justifyleft: {
            title: 'Align Text Left',
            text: 'Align text to the left.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        },
        justifycenter: {
            title: 'Center Text',
            text: 'Center text in the editor.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        },
        justifyright: {
            title: 'Align Text Right',
            text: 'Align text to the right.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        },
        insertunorderedlist: {
            title: 'Bullet List',
            text: 'Start a bulleted list.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        },
        insertorderedlist: {
            title: 'Numbered List',
            text: 'Start a numbered list.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        },
        createlink: {
            title: 'Hyperlink',
            text: 'Make the selected text a hyperlink.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        },
        sourceedit: {
            title: 'Source Edit',
            text: 'Switch to source editing mode.',
            cls: Ext.baseCSSPrefix + 'html-editor-tip'
        }
    },
    // hide stuff that is not compatible
    /**
     * @event blur
     * @hide
     */
    /**
     * @event focus
     * @hide
     */
    /**
     * @event specialkey
     * @hide
     */
    /**
     * @cfg {String} fieldCls
     * @hide
     */
    /**
     * @cfg {String} focusCls
     * @hide
     */
    /**
     * @cfg {String} autoCreate
     * @hide
     */
    /**
     * @cfg {String} inputType
     * @hide
     */
    /**
     * @cfg {String} invalidCls
     * @hide
     */
    /**
     * @cfg {String} invalidText
     * @hide
     */
    /**
     * @cfg {Boolean} allowDomMove
     * @hide
     */
    /**
     * @cfg {String} readOnly
     * @hide
     */
    /**
     * @cfg {String} tabIndex
     * @hide
     */
    /**
     * @method validate
     * @hide
     */
    privates: {
        deferFocus: function() {
            this.focus(false, true);
        },
        getFocusEl: function() {
            return this.sourceEditMode ? this.textareaEl : this.iframeEl;
        }
    }
});

/**
 * @class Ext.view.TagKeyNav
 * @private
 */
Ext.define('Ext.view.TagKeyNav', {
    extend: 'Ext.view.BoundListKeyNav',
    alias: 'view.navigation.tagfield',
    onKeySpace: function(e) {
        var me = this,
            field = me.view.pickerField;
        if (field.isExpanded && field.inputEl.dom.value === '') {
            field.preventKeyUpEvent = true;
            me.navigateOnSpace = true;
            me.callParent([
                e
            ]);
            e.stopEvent();
            return false;
        }
        // Allow propagating to the field
        return true;
    }
});

/**
 * `tagfield` provides a combobox that removes the hassle of dealing with long and unruly select 
 * options. The selected list is visually maintained in the value display area instead of 
 * within the picker itself. Users may easily add or remove `tags` from the 
 * display value area.
 *
 *     @example
 *     var shows = Ext.create('Ext.data.Store', {
 *         fields: ['id','show'],
 *         data: [
 *             {id: 0, show: 'Battlestar Galactica'},
 *             {id: 1, show: 'Doctor Who'},
 *             {id: 2, show: 'Farscape'},
 *             {id: 3, show: 'Firefly'},
 *             {id: 4, show: 'Star Trek'},
 *             {id: 5, show: 'Star Wars: Christmas Special'}
 *         ]
 *     });
 *
 *     Ext.create('Ext.form.Panel', {
 *         renderTo: Ext.getBody(),
 *         title: 'Sci-Fi Television',
 *         height: 200,
 *         width: 500,
 *         items: [{
 *             xtype: 'tagfield',
 *             fieldLabel: 'Select a Show',
 *             store: shows,
 *             displayField: 'show',
 *             valueField: 'id',
 *             queryMode: 'local',
 *             filterPickList: true
 *         }]
 *     });
 *       
 * ### History
 *
 * Inspired by the SuperBoxSelect component for ExtJS 3,
 * which in turn was inspired by the BoxSelect component for ExtJS 2.
 *
 * Various contributions and suggestions made by many members of the ExtJS community which
 * can be seen in the [user extension forum post](http://www.sencha.com/forum/showthread.php?134751-Ext.ux.form.field.BoxSelect).
 *
 * By: kvee_iv http://www.sencha.com/forum/member.php?29437-kveeiv
 */
Ext.define('Ext.form.field.Tag', {
    extend: 'Ext.form.field.ComboBox',
    xtype: 'tagfield',
    requires: [
        'Ext.selection.Model',
        'Ext.data.Store',
        'Ext.data.ChainedStore',
        'Ext.view.TagKeyNav'
    ],
    /**
     * @property noWrap
     * @inheritdoc
     */
    noWrap: false,
    /**
     * @cfg allowOnlyWhitespace
     * @hide
     * Currently unsupported since the value of a tagfield is an array of values and shouldn't
     * ever be a string.
     */
    /**
     * @cfg {String} valueParam
     * The name of the parameter used to load unknown records into the store. If left unspecified,
     * {@link #valueField} will be used.
     */
    /**
     * @cfg {Boolean} multiSelect
     * If set to `true`, allows the combo field to hold more than one value at a time, and allows
     * selecting multiple items from the dropdown list. The combo's text field will show all
     * selected values using the template defined by {@link #labelTpl}.
     *
     */
    multiSelect: true,
    /**
     * @cfg {String} delimiter
     * The character(s) used to separate new values to be added when {@link #createNewOnEnter}
     * or {@link #createNewOnBlur} are set.
     * `{@link #multiSelect} = true`.
     */
    delimiter: ',',
    /**
     * @cfg {String/Ext.XTemplate} labelTpl
     * The {@link Ext.XTemplate XTemplate} to use for the inner
     * markup of the labeled items. Defaults to the configured {@link #displayField}
     */
    /**
     * @cfg {String/Ext.XTemplate} tipTpl
     * The {@link Ext.XTemplate XTemplate} to use for the tip of the labeled items. 
     *
     * @since  5.1.1
     */
    tipTpl: undefined,
    /**
     * @cfg forceSelection
     * @inheritdoc
     *
     * When {@link #forceSelection} is `false`, new records can be created by the user as they
     * are typed. These records are **not** added to the combo's store. Multiple new values
     * may be added by separating them with the {@link #delimiter}, and can be further configured
     * using the {@link #createNewOnEnter} and {@link #createNewOnBlur} configuration options.
     *
     * This functionality is primarily useful for things such as an email address.
     */
    forceSelection: true,
    /**
     * @cfg {Boolean} createNewOnEnter
     * Has no effect if {@link #forceSelection} is `true`.
     *
     * With this set to `true`, the creation described in
     * {@link #forceSelection} will also be triggered by the 'enter' key.
     */
    createNewOnEnter: false,
    /**
     * @cfg {Boolean} createNewOnBlur
     * Has no effect if {@link #forceSelection} is `true`.
     *
     * With this set to `true`, the creation described in
     * {@link #forceSelection} will also be triggered when the field loses focus.
     *
     * Please note that this behavior is also affected by the configuration options
     * {@link #autoSelect} and {@link #selectOnTab}. If those are true and an existing
     * item would have been selected as a result, the partial text the user has entered will
     * be discarded and the existing item will be added to the selection.
     *
     * Setting this option to `true` is not recommended for accessible applications.
     */
    createNewOnBlur: false,
    /**
     * @cfg {Boolean} encodeSubmitValue
     * Has no effect if {@link #multiSelect} is `false`.
     *
     * Controls the formatting of the form submit value of the field as returned by
     * {@link #getSubmitValue}
     *
     * - `true` for the field value to submit as a json encoded array in a single GET/POST variable
     * - `false` for the field to submit as an array of GET/POST variables
     */
    encodeSubmitValue: false,
    /**
     * @cfg {Boolean} triggerOnClick
     * `true` to activate the trigger when clicking in empty space in the field. Note that the
     * subsequent behavior of this is controlled by the field's {@link #triggerAction}.
     * This behavior is similar to that of a basic ComboBox with {@link #editable} `false`.
     */
    triggerOnClick: true,
    /**
     * @cfg {Boolean} stacked
     * - `true` to have each selected value fill to the width of the form field
     * - `false to have each selected value size to its displayed contents
     */
    stacked: false,
    /**
     * @cfg {Boolean} filterPickList
     * True to hide the currently selected values from the drop down list.
     *
     * Setting this option to `true` is not recommended for accessible applications.
     *
     * - `true` to hide currently selected values from the drop down pick list
     * - `false` to keep the item in the pick list as a selected item
     */
    filterPickList: false,
    /**
     * @cfg {Boolean} clearOnBackspace
     * Set to `false` to disable clearing selected values with Backspace key. This mode
     * is recommended for accessible applications.
     */
    clearOnBackspace: true,
    /**
     * @cfg {Boolean} grow
     *
     * `true` if this field should automatically grow and shrink vertically to its content.
     * Note that this overrides the natural trigger grow functionality, which is used to size
     * the field horizontally.
     */
    grow: true,
    /**
     * @cfg {Number/Boolean} growMin
     * Has no effect if {@link #grow} is `false`
     *
     * The minimum height to allow when {@link #grow} is `true`, or `false` to allow for
     * natural vertical growth based on the current selected values. See also {@link #growMax}.
     */
    growMin: false,
    /**
     * @cfg {Number/Boolean} growMax
     * Has no effect if {@link #grow} is `false`
     *
     * The maximum height to allow when {@link #grow} is `true`, or `false` to allow for
     * natural vertical growth based on the current selected values. See also {@link #growMin}.
     */
    growMax: false,
    /**
     * @cfg {Boolean} simulatePlaceholder
     * @private
     */
    simulatePlaceholder: true,
    /**
     * @cfg selectOnFocus
     * @inheritdoc
     */
    selectOnFocus: true,
    /**
     * @cfg growToLongestValue
     * @hide
     * Currently unsupported since this is used for horizontal growth and this component
     * only supports vertical growth.
     */
    /**
     * @cfg {String} ariaHelpText
     * The text to be announced by screen readers when input element is
     * focused. This text is used when this component is configured not to allow creating
     * new values; when {@link #createNewOnEnter} is set to `true`, {@link #ariaHelpTextEditable}
     * will be used instead.
     * @locale
     */
    ariaHelpText: 'Use Up and Down arrows to view available values, Enter to select. ' + 'Use Left and Right arrows to view selected values, Delete key to deselect.',
    /**
     * @cfg {String} ariaHelpTextEditable
     * The text to be announced by screen readers when
     * input element is focused. This text is used when {@link #createNewOnEnter} is set to `true`;
     * see also {@link #ariaHelpText}.
     * @locale
     */
    ariaHelpTextEditable: 'Use Up and Down arrows to view available values, Enter to select. ' + 'Type and press Enter to create a new value. ' + 'Use Left and Right arrows to view selected values, ' + 'Delete key to deselect.',
    /**
     * @cfg {String} ariaSelectedText
     * Template text for announcing selected values to screen
     * reader users. '{0}' will be replaced with the list of selected values.
     * @locale
     */
    ariaSelectedText: 'Selected {0}.',
    /**
     * @cfg {String} ariaDeselectedText
     * Template text for announcing deselected values to
     * screen reader users. '{0}' will be replaced with the list of values removed from
     * selected list.
     * @locale
     */
    ariaDeselectedText: '{0} removed from selection.',
    /**
     * @cfg {String} ariaNoneSelectedText
     * Text to announce to screen reader users when no
     * values are currently selected. This text is used when Tag field is focused.
     * @locale
     */
    ariaNoneSelectedText: 'No value selected.',
    /**
     * @cfg {String} ariaSelectedListLabel
     * Label to be announced to screen reader users
     * when they use Left and Right arrow keys to navigate the list of currently selected values.
     * @locale
     */
    ariaSelectedListLabel: 'Selected values',
    /**
     * @cfg {String} ariaAvailableListLabel
     * Label to be announced to screen reader users
     * when they use Up and Down arrow keys to navigate the list of available values.
     * @locale
     */
    ariaAvailableListLabel: 'Available values',
    /**
     * @event autosize
     * Fires when the **{@link #autoSize}** function is triggered and the field is resized
     * according to the {@link #grow}/{@link #growMin}/{@link #growMax} configs as a result.
     * This event provides a hook for the developer to apply additional logic at runtime
     * to resize the field if needed.
     * @param {Ext.form.field.Tag} this This field
     * @param {Number} height The new field height
     */
    /* eslint-disable indent, max-len */
    /**
     * @cfg fieldSubTpl
     * @private
     */
    fieldSubTpl: [
        // listWrapper div is tabbable in Firefox, for some unfathomable reason
        '<div id="{cmpId}-listWrapper" data-ref="listWrapper"' + (Ext.isGecko ? ' tabindex="-1"' : ''),
        '<tpl foreach="ariaElAttributes"> {$}="{.}"</tpl>',
        ' class="' + Ext.baseCSSPrefix + 'tagfield {fieldCls} {typeCls} {typeCls}-{ui}"<tpl if="wrapperStyle"> style="{wrapperStyle}"</tpl>>',
        '<span id="{cmpId}-selectedText" data-ref="selectedText" aria-hidden="true" class="' + Ext.baseCSSPrefix + 'hidden-clip"></span>',
        '<ul id="{cmpId}-itemList" data-ref="itemList" role="presentation" class="' + Ext.baseCSSPrefix + 'tagfield-list{itemListCls}">',
        '<li id="{cmpId}-inputElCt" data-ref="inputElCt" role="presentation" class="' + Ext.baseCSSPrefix + 'tagfield-input">',
        '<input id="{cmpId}-inputEl" data-ref="inputEl" type="{type}" ',
        '<tpl if="name">name="{name}" </tpl>',
        '<tpl if="value"> value="{[Ext.util.Format.htmlEncode(values.value)]}"</tpl>',
        '<tpl if="size">size="{size}" </tpl>',
        '<tpl if="tabIdx != null">tabindex="{tabIdx}" </tpl>',
        '<tpl if="disabled"> disabled="disabled"</tpl>',
        '<tpl foreach="inputElAriaAttributes"> {$}="{.}"</tpl>',
        'class="' + Ext.baseCSSPrefix + 'tagfield-input-field {inputElCls} {emptyCls} {fixCls}" autocomplete="off">',
        '</li>',
        '</ul>',
        '<ul id="{cmpId}-ariaList" data-ref="ariaList" role="listbox"',
        '<tpl if="ariaSelectedListLabel"> aria-label="{ariaSelectedListLabel}"</tpl>',
        '<tpl if="multiSelect"> aria-multiselectable="true"</tpl>',
        ' class="' + Ext.baseCSSPrefix + 'tagfield-arialist">',
        '</ul>',
        '</div>',
        {
            disableFormats: true
        }
    ],
    postSubTpl: [
        '<label id="{cmpId}-placeholderLabel" data-ref="placeholderLabel" for="{cmpId}-inputEl" class="{placeholderCoverCls} {placeholderCoverCls}-{ui} {emptyCls}">{emptyText}</label>',
        '</div>',
        // end inputWrap
        '<tpl for="triggers">{[values.renderTrigger(parent)]}</tpl>',
        '</div>'
    ],
    // end triggerWrap
    /* eslint-enable indent, max-len */
    extraFieldBodyCls: Ext.baseCSSPrefix + 'tagfield-body',
    /**
     * @private
     */
    childEls: [
        'listWrapper',
        'itemList',
        'inputEl',
        'inputElCt',
        'selectedText',
        'ariaList'
    ],
    /**
     * @private
     */
    clearValueOnEmpty: false,
    ariaSelectable: true,
    /**
     * @property ariaEl
     * @inheritdoc
     */
    ariaEl: 'listWrapper',
    tagItemCls: Ext.baseCSSPrefix + 'tagfield-item',
    tagItemTextCls: Ext.baseCSSPrefix + 'tagfield-item-text',
    tagItemCloseCls: Ext.baseCSSPrefix + 'tagfield-item-close',
    tagItemSelector: '.' + Ext.baseCSSPrefix + 'tagfield-item',
    tagItemCloseSelector: '.' + Ext.baseCSSPrefix + 'tagfield-item-close',
    tagSelectedCls: Ext.baseCSSPrefix + 'tagfield-item-selected',
    initComponent: function() {
        var me = this,
            typeAhead = me.typeAhead,
            delimiter = me.delimiter;
        if (typeAhead && !me.editable) {
            Ext.raise('If typeAhead is enabled the combo must be editable: true -- ' + 'please change one of those settings.');
        }
        // Allow unmatched textual values to be converted into new value records.
        if (me.createNewOnEnter || me.createNewOnBlur) {
            me.forceSelection = false;
        }
        me.typeAhead = false;
        if (me.value == null) {
            me.value = [];
        }
        // This is the selection model for selecting tags in the tag list.
        // NOT the dropdown BoundList. Create the selModel before calling parent,
        // we need it to be available when we bind the store.
        me.selectionModel = new Ext.selection.Model({
            mode: 'MULTI',
            onSelectChange: function(record, isSelected, suppressEvent, commitFn) {
                commitFn();
            },
            // Relay these selection events passing the field instead of exposing
            // the underlying selection model
            listeners: {
                scope: me,
                selectionchange: me.onSelectionChange,
                focuschange: me.onFocusChange
            }
        });
        // Users might want to implement centralized help
        if (!me.ariaHelp) {
            me.ariaHelp = me.createNewOnEnter ? me.ariaHelpTextEditable : me.ariaHelpText;
        }
        me.callParent();
        me.typeAhead = typeAhead;
        if (delimiter && me.multiSelect) {
            me.delimiterRegexp = new RegExp(Ext.String.escapeRegex(delimiter));
        }
    },
    initEvents: function() {
        var me = this,
            inputEl = me.inputEl;
        me.callParent(arguments);
        if (!me.enableKeyEvents) {
            inputEl.on('keydown', me.onKeyDown, me);
            inputEl.on('keyup', me.onKeyUp, me);
        }
        me.listWrapper.on({
            scope: me,
            click: me.onItemListClick,
            mousedown: me.onItemMouseDown
        });
    },
    createPicker: function() {
        var me = this,
            config;
        // Avoid munging config on the prototype
        config = Ext.apply({
            navigationModel: 'tagfield'
        }, me.defaultListConfig);
        if (me.ariaAvailableListLabel) {
            config.ariaRenderAttributes = {
                'aria-label': Ext.String.htmlEncode(me.ariaAvailableListLabel)
            };
        }
        me.defaultListConfig = config;
        return me.callParent();
    },
    isValid: function() {
        var me = this,
            disabled = me.disabled,
            validate = me.forceValidation || !disabled;
        return validate ? me.validateValue(me.getValue()) : disabled;
    },
    onBindStore: function(store) {
        var me = this;
        me.callParent([
            store
        ]);
        if (store) {
            // We collect picked records in a value store so that a selection model
            // can track selection
            me.valueStore = new Ext.data.Store({
                model: store.getModel(),
                // Assign a proxy here so we don't get the proxy from the model
                proxy: 'memory',
                // We may have the empty store here, so just ignore empty models
                useModelWarning: false
            });
            me.selectionModel.bindStore(me.valueStore);
            // Picked records disappear from the BoundList
            if (me.filterPickList) {
                me.listFilter = new Ext.util.Filter({
                    scope: me,
                    filterFn: me.filterPicked
                });
                me.changingFilters = true;
                store.filter(me.listFilter);
                me.changingFilters = false;
            }
        }
    },
    filterPicked: function(rec) {
        return !this.valueCollection.contains(rec);
    },
    onUnbindStore: function(store) {
        var me = this,
            valueStore = me.valueStore,
            picker = me.picker;
        if (picker) {
            picker.bindStore(null);
        }
        if (valueStore) {
            valueStore.destroy();
            me.valueStore = null;
        }
        if (me.filterPickList && !store.destroyed) {
            me.changingFilters = true;
            store.removeFilter(me.listFilter);
            me.changingFilters = false;
        }
        me.callParent(arguments);
    },
    clearInput: function() {
        var me = this,
            valueRecords = me.getValueRecords(),
            inputValue = me.inputEl && me.inputEl.dom.value,
            lastDisplayValue;
        if (valueRecords.length && inputValue) {
            lastDisplayValue = valueRecords[valueRecords.length - 1].get(me.displayField);
            if (!Ext.String.startsWith(lastDisplayValue, inputValue, true)) {
                return;
            }
            me.inputEl.dom.value = '';
            if (me.queryMode === 'local') {
                me.clearLocalFilter();
                // we need to refresh the picker after removing 
                // the local filter to display the updated data
                me.getPicker().refresh();
            }
        }
    },
    onValueCollectionEndUpdate: function() {
        var me = this,
            pickedRecords = me.valueCollection.items,
            valueStore = me.valueStore;
        if (me.isSelectionUpdating()) {
            return;
        }
        // Ensure the source store is filtered down
        if (me.filterPickList) {
            me.changingFilters = true;
            me.store.filter(me.listFilter);
            me.changingFilters = false;
        }
        me.callParent();
        Ext.suspendLayouts();
        if (valueStore) {
            valueStore.suspendEvents();
            valueStore.loadRecords(pickedRecords);
            valueStore.resumeEvents();
        }
        me.refreshEmptyText();
        me.clearInput();
        Ext.resumeLayouts(true);
        me.alignPicker();
    },
    checkValueOnDataChange: Ext.emptyFn,
    onSelectionChange: function(selModel, selectedRecs) {
        var me = this,
            inputEl = me.inputEl,
            item;
        me.applyMultiselectItemMarkup();
        me.applyAriaListMarkup();
        me.applyAriaSelectedText();
        // Focus does not really change but we're pretending it does
        if (inputEl) {
            if (selectedRecs.length === 0) {
                inputEl.dom.removeAttribute('aria-activedescendant');
            } else {
                item = me.getAriaListNode(selectedRecs[0]);
                if (item) {
                    inputEl.dom.setAttribute('aria-activedescendant', item.id);
                }
            }
        }
        me.fireEvent('valueselectionchange', me, selectedRecs);
    },
    onFocusChange: function(selectionModel, oldFocused, newFocused) {
        var me = this;
        me.callParent([
            selectionModel,
            oldFocused,
            newFocused
        ]);
        me.fireEvent('valuefocuschange', me, oldFocused, newFocused);
    },
    getAriaListNode: function(record) {
        var ariaList = this.ariaList,
            node;
        if (ariaList && record) {
            node = ariaList.selectNode('[data-recordid="' + record.internalId + '"]');
        }
        return node;
    },
    doDestroy: function() {
        Ext.destroy(this.selectionModel);
        // This will unbind the store, which will destroy the valueStore
        this.callParent();
    },
    getSubTplData: function(fieldData) {
        var me = this,
            id = me.id,
            data = me.callParent(arguments),
            emptyText = me.emptyText,
            isEmpty = emptyText && data.value.length < 1,
            growMin = me.growMin,
            growMax = me.growMax,
            wrapperStyle = '',
            attr;
        data.value = '';
        data.emptyText = isEmpty ? emptyText : '';
        data.itemListCls = '';
        data.emptyCls = isEmpty ? me.emptyUICls : '';
        if (me.grow) {
            if (Ext.isNumber(growMin) && growMin > 0) {
                wrapperStyle += 'min-height:' + growMin + 'px;';
            }
            if (Ext.isNumber(growMax) && growMax > 0) {
                wrapperStyle += 'max-height:' + growMax + 'px;';
            }
        } else {
            wrapperStyle += 'max-height: 1px;';
        }
        data.wrapperStyle = wrapperStyle;
        if (me.stacked === true) {
            data.itemListCls += ' ' + Ext.baseCSSPrefix + 'tagfield-stacked';
        }
        if (!me.multiSelect) {
            data.itemListCls += ' ' + Ext.baseCSSPrefix + 'tagfield-singleselect';
        }
        if (!me.ariaStaticRoles[me.ariaRole]) {
            data.multiSelect = me.multiSelect;
            data.ariaSelectedListLabel = Ext.String.htmlEncode(me.ariaSelectedListLabel);
            attr = data.ariaElAttributes;
            if (attr) {
                attr['aria-owns'] = id + '-inputEl ' + id + '-picker ' + id + '-ariaList';
            }
            attr = data.inputElAriaAttributes;
            if (attr) {
                attr.role = 'textbox';
                attr['aria-describedby'] = id + '-selectedText ' + (attr['aria-describedby'] || '');
            }
        }
        return data;
    },
    onRender: function(container, index) {
        var me = this;
        me.callParent([
            container,
            index
        ]);
        me.emptyClsElements.push(me.listWrapper, me.placeholderLabel);
    },
    afterRender: function() {
        var me = this,
            inputEl = me.inputEl,
            emptyText = me.emptyText;
        if (emptyText) {
            // We remove HTML5 placeholder here because we use the placeholderLabel instead.
            if (Ext.supports.Placeholder && inputEl) {
                inputEl.dom.removeAttribute('placeholder');
            }
        }
        me.applyMultiselectItemMarkup();
        me.applyAriaListMarkup();
        me.applyAriaSelectedText();
        me.callParent();
    },
    findRecord: function(field, value) {
        var matches = this.getStore().queryRecords(field, value);
        return matches.length ? matches[0] : false;
    },
    /**
     * Get the current cursor position in the input field, for key-based navigation
     * @private
     */
    getCursorPosition: function() {
        var cursorPos;
        if (document.selection) {
            cursorPos = document.selection.createRange();
            cursorPos.collapse(true);
            cursorPos.moveStart('character', -this.inputEl.dom.value.length);
            cursorPos = cursorPos.text.length;
        } else {
            cursorPos = this.inputEl.dom.selectionStart;
        }
        return cursorPos;
    },
    /**
     * Check to see if the input field has selected text, for key-based navigation
     * @private
     */
    hasSelectedText: function() {
        var inputEl = this.inputEl.dom,
            sel, range;
        if (document.selection) {
            sel = document.selection;
            range = sel.createRange();
            return (range.parentElement() === inputEl);
        } else {
            return inputEl.selectionStart !== inputEl.selectionEnd;
        }
    },
    /**
     * Handles keyDown processing of key-based selection of labeled items.
     * Supported keyboard controls:
     *
     * - If pick list is expanded
     *
     *     - `CTRL-A` will select all the items in the pick list
     *
     * - If the cursor is at the beginning of the input field and there are values present
     *
     *     - `CTRL-A` will highlight all the currently selected values
     *     - `BACKSPACE` and `DELETE` will remove any currently highlighted selected values
     *     - `RIGHT` and `LEFT` will move the current highlight in the appropriate direction
     *     - `SHIFT-RIGHT` and `SHIFT-LEFT` will add to the current highlight in the appropriate
     *       direction
     *
     * @protected
     */
    onKeyDown: function(e) {
        var me = this,
            key = e.getKey(),
            inputEl = me.inputEl,
            rawValue = inputEl && inputEl.dom.value,
            valueCollection = me.valueCollection,
            selModel = me.selectionModel,
            stopEvent = false,
            valueCount, lastSelectionIndex, records, text, i, len;
        if (me.destroyed || me.readOnly || me.disabled || !me.editable) {
            return;
        }
        valueCount = valueCollection.getCount();
        if (valueCount > 0 && rawValue === '') {
            // Keyboard navigation of current values
            lastSelectionIndex = (selModel.getCount() > 0) ? valueCollection.indexOf(selModel.getLastSelected()) : -1;
            // Backspace can be used to clear the rightmost selected value.
            // Delete key should only remove selected value if it is highlighted.
            if ((key === e.BACKSPACE && me.clearOnBackspace) || (key === e.DELETE && lastSelectionIndex > -1)) {
                // Delete token
                if (lastSelectionIndex > -1) {
                    if (selModel.getCount() > 1) {
                        lastSelectionIndex = -1;
                    }
                    records = selModel.getSelection();
                    text = [];
                    for (i = 0 , len = records.length; i < len; i++) {
                        text.push(records[i].get(me.displayField));
                    }
                    text = text.join(', ');
                } else {
                    records = valueCollection.last();
                    text = records.get(me.displayField);
                }
                valueCollection.remove(records);
                // Announce the change
                if (text) {
                    me.ariaErrorEl.dom.innerHTML = Ext.String.formatEncode(me.ariaDeselectedText, text);
                }
                selModel.clearSelections();
                if (lastSelectionIndex === (valueCount - 1)) {
                    selModel.select(valueCollection.last());
                } else if (lastSelectionIndex > -1) {
                    selModel.select(lastSelectionIndex);
                } else if (valueCollection.getCount()) {
                    selModel.select(valueCollection.last());
                }
                stopEvent = true;
            } else if (key === e.RIGHT || key === e.LEFT) {
                // Navigate and select tokens
                if (lastSelectionIndex === -1 && key === e.LEFT) {
                    selModel.select(valueCollection.last());
                    stopEvent = true;
                } else if (lastSelectionIndex > -1) {
                    if (key === e.RIGHT) {
                        if (lastSelectionIndex < (valueCount - 1)) {
                            selModel.select(lastSelectionIndex + 1, e.shiftKey);
                            stopEvent = true;
                        } else if (!e.shiftKey) {
                            selModel.deselectAll();
                            stopEvent = true;
                        }
                    } else if (key === e.LEFT && (lastSelectionIndex > 0)) {
                        selModel.select(lastSelectionIndex - 1, e.shiftKey);
                        stopEvent = true;
                    }
                }
            } else if (key === e.A && e.ctrlKey) {
                // Select all tokens
                selModel.selectAll();
                stopEvent = e.A;
            }
        }
        if (stopEvent) {
            me.preventKeyUpEvent = stopEvent;
            e.stopEvent();
            return;
        }
        // Prevent key up processing for enter if it is being handled by the picker
        if (me.isExpanded && key === e.ENTER && me.picker.highlightedItem) {
            me.preventKeyUpEvent = true;
        }
        if (me.enableKeyEvents) {
            me.callParent(arguments);
        }
        if (!e.isSpecialKey() && !e.hasModifier()) {
            selModel.deselectAll();
        }
    },
    /**
     * Handles auto-selection and creation of labeled items based on this field's
     * delimiter, as well as the keyUp processing of key-based selection of labeled items.
     * @protected
     */
    onKeyUp: function(e, t) {
        var me = this,
            inputEl = me.inputEl,
            rawValue = inputEl.dom.value,
            preventKeyUpEvent = me.preventKeyUpEvent;
        if (me.preventKeyUpEvent) {
            e.stopEvent();
            if (preventKeyUpEvent === true || e.getKey() === preventKeyUpEvent) {
                delete me.preventKeyUpEvent;
            }
            return;
        }
        if (me.multiSelect && me.delimiterRegexp && me.delimiterRegexp.test(rawValue) || (me.createNewOnEnter && e.getKey() === e.ENTER)) {
            // Announce new value(s)
            if (me.createNewOnEnter && rawValue) {
                me.ariaErrorEl.dom.innerHTML = Ext.String.formatEncode(me.ariaSelectedText, rawValue);
            }
            rawValue = Ext.Array.clean(rawValue.split(me.delimiterRegexp));
            inputEl.dom.value = '';
            me.setValue(me.valueStore.getRange().concat(rawValue));
            inputEl.focus();
        }
        me.callParent([
            e,
            t
        ]);
    },
    onEsc: function(e) {
        var me = this,
            selModel = me.selectionModel,
            isExpanded = me.isExpanded;
        me.callParent([
            e
        ]);
        if (!isExpanded && selModel.getCount() > 0) {
            selModel.deselectAll();
        }
        e.stopEvent();
    },
    /**
     * Overridden to get and set the DOM value directly for type-ahead suggestion
     * (bypassing get/setRawValue)
     * @protected
     */
    onTypeAhead: function() {
        var me = this,
            displayField = me.displayField,
            inputElDom = me.inputEl.dom,
            record = me.getStore().findRecord(displayField, inputElDom.value),
            newValue, len, selStart;
        if (record) {
            newValue = record.get(displayField);
            len = newValue.length;
            selStart = inputElDom.value.length;
            if (selStart !== 0 && selStart !== len) {
                // Setting the raw value will cause a field mutation event.
                // Prime the lastMutatedValue so that this does not cause a requery.
                me.lastMutatedValue = newValue;
                inputElDom.value = newValue;
                me.selectText(selStart, newValue.length);
            }
        }
    },
    /**
     * Delegation control for selecting and removing labeled items or triggering
     * list collapse/expansion
     * @protected
     */
    onItemListClick: function(e) {
        var me = this,
            selectionModel = me.selectionModel,
            itemEl = e.getTarget(me.tagItemSelector),
            closeEl = itemEl ? e.getTarget(me.tagItemCloseSelector) : false;
        if (me.readOnly || me.disabled) {
            return;
        }
        e.stopPropagation();
        if (itemEl) {
            if (closeEl) {
                me.removeByListItemNode(itemEl);
                if (me.valueStore.getCount() > 0) {
                    me.fireEvent('select', me, me.valueStore.getRange());
                }
            } else {
                me.toggleSelectionByListItemNode(itemEl, e.shiftKey);
            }
            // If not using touch interactions, focus the input
            if (!Ext.supports.TouchEvents) {
                me.inputEl.focus();
            }
        } else {
            if (selectionModel.getCount() > 0) {
                selectionModel.deselectAll();
            }
            me.inputEl.focus();
            if (me.triggerOnClick) {
                me.onTriggerClick();
            }
        }
    },
    // Prevent item from receiving focus.
    // See EXTJS-17686.
    onItemMouseDown: function(e) {
        if (e.target !== this.inputEl.dom) {
            e.preventDefault();
        }
    },
    /**
     * Build the markup for the labeled items. Template must be built on demand due to ComboBox
     * initComponent life cycle for the creation of on-demand stores (to account for automatic
     * valueField/displayField setting)
     * @private
     */
    getMultiSelectItemMarkup: function() {
        var me = this,
            childElCls = (me._getChildElCls && me._getChildElCls()) || '';
        // hook for rtl cls
        if (!me.multiSelectItemTpl) {
            if (!me.labelTpl) {
                me.labelTpl = '{' + me.displayField + '}';
            }
            me.labelTpl = me.lookupTpl('labelTpl');
            if (me.tipTpl) {
                me.tipTpl = me.lookupTpl('tipTpl');
            }
            /* eslint-disable indent, max-len */
            me.multiSelectItemTpl = new Ext.XTemplate([
                '<tpl for=".">',
                '<li data-selectionIndex="{[xindex - 1]}" data-recordId="{internalId}" role="presentation" class="' + me.tagItemCls + childElCls,
                '<tpl if="this.isSelected(values)">',
                ' ' + me.tagSelectedCls,
                '</tpl>',
                '{%',
                'values = values.data;',
                '%}',
                me.tipTpl ? '" data-qtip="{[this.getTip(values)]}">' : '">',
                '<div role="presentation" class="' + me.tagItemTextCls + '">{[this.getItemLabel(values)]}</div>',
                '<div role="presentation" class="' + me.tagItemCloseCls + childElCls + '"></div>',
                '</li>',
                '</tpl>',
                {
                    isSelected: function(rec) {
                        return me.selectionModel.isSelected(rec);
                    },
                    getItemLabel: function(values) {
                        return Ext.String.htmlEncode(me.labelTpl.apply(values));
                    },
                    getTip: function(values) {
                        return Ext.String.htmlEncode(me.tipTpl.apply(values));
                    },
                    strict: true
                }
            ]);
        }
        /* eslint-enable indent, max-len */
        if (!me.multiSelectItemTpl.isTemplate) {
            me.multiSelectItemTpl = this.lookupTpl('multiSelectItemTpl');
        }
        return me.multiSelectItemTpl.apply(me.valueCollection.getRange());
    },
    /**
     * Update the labeled items rendering
     * @private
     */
    applyMultiselectItemMarkup: function() {
        var me = this,
            itemList = me.itemList;
        if (itemList) {
            itemList.select('.' + Ext.baseCSSPrefix + 'tagfield-item').destroy();
            me.inputElCt.insertHtml('beforeBegin', me.getMultiSelectItemMarkup());
            me.autoSize();
        }
    },
    /**
     * Build the markup for ARIA listbox.
     * @private
     */
    getAriaListMarkup: function() {
        var me = this,
            values;
        if (!me.ariaListItemTpl) {
            /* eslint-disable indent, max-len */
            me.ariaListItemTpl = new Ext.XTemplate([
                '<tpl for=".">',
                '<li id="' + me.id + '-{internalId}" role="option"',
                ' class="' + Ext.baseCSSPrefix + 'tagfield-arialist-item"',
                ' aria-selected="{[this.isPicked(values)]}"',
                '  data-recordId="{internalId}"',
                '>',
                '{[this.getItemLabel(values.data)]}',
                '</li>',
                '</tpl>',
                {
                    isPicked: function(rec) {
                        return me.filterPicked(rec) ? 'false' : 'true';
                    },
                    isSelected: function(rec) {
                        return me.selectionModel.isSelected(rec) ? 'true' : 'false';
                    },
                    getItemLabel: function(values) {
                        return Ext.String.htmlEncode(me.labelTpl.apply(values));
                    },
                    strict: true
                }
            ]);
        }
        /* eslint-enable indent, max-len */
        if (!me.ariaListItemTpl.isTemplate) {
            me.ariaListtemTpl = me.lookupTpl('ariaListItemTpl');
        }
        values = me.valueCollection.getRange();
        return me.ariaListItemTpl.apply(values);
    },
    applyAriaListMarkup: function() {
        var me = this,
            ariaList = me.ariaList;
        if (ariaList) {
            ariaList.select('*').destroy();
            ariaList.insertHtml('afterBegin', me.getAriaListMarkup());
        }
    },
    getAriaSelectedText: function(values) {
        var me = this;
        if (!me.ariaSelectedItemTpl) {
            /* eslint-disable indent */
            me.ariaSelectedItemTpl = new Ext.XTemplate([
                '<tpl for="." between=", ">',
                '{[this.getItemLabel(values.data)]}',
                '</tpl>',
                {
                    getItemLabel: function(values) {
                        return Ext.String.htmlEncode(me.labelTpl.apply(values));
                    },
                    strict: true
                }
            ]);
        }
        /* eslint-enable indent */
        if (!me.ariaSelectedItemTpl.isTemplate) {
            me.ariaSelectedItemTpl = me.lookupTpl('ariaSelectedItemTpl');
        }
        return Ext.String.format(me.ariaSelectedText, me.ariaSelectedItemTpl.apply(values));
    },
    applyAriaSelectedText: function() {
        var me = this,
            selectedText = me.selectedText,
            records, text;
        if (selectedText) {
            records = me.valueCollection.getRange();
            text = records.length ? me.getAriaSelectedText(records) : me.ariaNoneSelectedText;
            // selectedText element is not aria-live so OK to update every time
            selectedText.dom.innerHTML = Ext.String.htmlEncode(text);
        }
    },
    /**
     * Returns the record from valueStore for the labeled item node
     */
    getRecordByListItemNode: function(itemEl) {
        return this.valueCollection.items[Number(itemEl.getAttribute('data-selectionIndex'))];
    },
    /**
     * Toggle of labeled item selection by node reference
     */
    toggleSelectionByListItemNode: function(itemEl, keepExisting) {
        var me = this,
            rec = me.getRecordByListItemNode(itemEl),
            selModel = me.selectionModel;
        if (rec) {
            if (selModel.isSelected(rec)) {
                selModel.deselect(rec);
            } else {
                selModel.select(rec, keepExisting);
            }
        }
    },
    /**
     * Removal of labelled item by node reference
     */
    removeByListItemNode: function(itemEl) {
        var me = this,
            rec = me.getRecordByListItemNode(itemEl);
        if (rec) {
            me.pickerSelectionModel.deselect(rec);
        }
    },
    // Private implementation.
    // The display value is always the raw value.
    // Picked values are displayed by the tag template.
    getDisplayValue: function() {
        return this.getRawValue();
    },
    /**
     * @method getRawValue
     * @inheritdoc
     * Intercept calls to getRawValue to pretend there is no inputEl for rawValue handling,
     * so that we can use inputEl for user input of just the current value.
     */
    getRawValue: function() {
        var me = this,
            records = me.getValueRecords(),
            values = [],
            i, len;
        for (i = 0 , len = records.length; i < len; i++) {
            values.push(records[i].data[me.displayField]);
        }
        return values.join(',');
    },
    setRawValue: function(value) {
        // setRawValue is not supported for tagfield.
        return;
    },
    /**
     * Removes a value or values from the current value of the field
     * @param {Mixed} value The value or values to remove from the current value,
     * see {@link #setValue}
     */
    removeValue: function(value) {
        var me = this,
            valueCollection = me.valueCollection,
            len, i, item,
            toRemove = [];
        if (value) {
            value = Ext.Array.from(value);
            // Ensure that the remove values are records
            for (i = 0 , len = value.length; i < len; ++i) {
                item = value[i];
                // If a key is supplied, find the matching value record from our value collection
                if (!item.isModel) {
                    item = valueCollection.byValue.get(item);
                }
                if (item) {
                    toRemove.push(item);
                }
            }
            me.valueCollection.beginUpdate();
            me.pickerSelectionModel.deselect(toRemove);
            me.valueCollection.endUpdate();
        }
    },
    getValue: function() {
        var value = this.callParent();
        if (value) {
            value = Ext.Array.from(value);
        }
        return value;
    },
    /**
     * Sets the specified value(s) into the field. The following value formats are recognized:
     *
     * - Single Values
     *
     *     - A string associated to this field's configured {@link #valueField}
     *     - A record containing at least this field's configured {@link #valueField} and
     *      {@link #displayField}
     *
     * - Multiple Values
     *
     *     - If {@link #multiSelect} is `true`, a string containing multiple strings as
     *       specified in the Single Values section above, concatenated in to one string
     *       with each entry separated by this field's configured {@link #delimiter}
     *     - An array of strings as specified in the Single Values section above
     *     - An array of records as specified in the Single Values section above
     *
     * In any of the string formats above, the following occurs if an associated record cannot
     * be found:
     *
     * 1. If {@link #forceSelection} is `false`, a new record of the {@link #store}'s configured
     *    model type will be created using the given value as the {@link #displayField} and
     *    {@link #valueField}. This record will be added to the current value, but it will **not**
     *    be added to the store.
     * 2. If {@link #forceSelection} is `true` and {@link #queryMode} is `remote`, the list
     *    of unknown values will be submitted as a call to the {@link #store}'s load as a parameter
     *    named by the {@link #valueParam} with values separated by the configured
     *    {@link #delimiter}.
     *    ** This process will cause setValue to asynchronously process. ** This will only be
     *    attempted once. Any unknown values that the server does not return records for
     *    will be removed.
     * 3. Otherwise, unknown values will be removed.
     *
     * @param {Mixed} value The value(s) to be set, see method documentation for details
     * @param add (private)
     * @param skipLoad (private)
     * @return {Ext.form.field.Field/Boolean} this, or `false` if asynchronously querying
     * for unknown values
     */
    setValue: function(value, add, skipLoad) {
        var me = this,
            valueStore = me.valueStore,
            valueField = me.valueField,
            unknownValues = [],
            store = me.store,
            autoLoadOnValue = me.autoLoadOnValue,
            isLoaded = store.getCount() > 0 || store.isLoaded(),
            pendingLoad = store.hasPendingLoad(),
            unloaded = autoLoadOnValue && !isLoaded && !pendingLoad,
            record, len, i, valueRecord, cls, params, isNull;
        if (Ext.isEmpty(value)) {
            value = null;
            isNull = true;
        } else if (Ext.isString(value) && me.multiSelect) {
            value = value.split(me.delimiter);
        } else {
            value = Ext.Array.from(value, true);
        }
        if (!isNull && me.queryMode === 'remote' && !store.isEmptyStore && skipLoad !== true && unloaded) {
            for (i = 0 , len = value.length; i < len; i++) {
                record = value[i];
                if (!record || !record.isModel) {
                    valueRecord = valueStore.findExact(valueField, record);
                    if (valueRecord > -1) {
                        value[i] = valueStore.getAt(valueRecord);
                    } else {
                        valueRecord = me.findRecord(valueField, record);
                        if (!valueRecord) {
                            if (me.forceSelection) {
                                unknownValues.push(record);
                            } else {
                                valueRecord = {};
                                valueRecord[me.valueField] = record;
                                valueRecord[me.displayField] = record;
                                cls = me.valueStore.getModel();
                                valueRecord = new cls(valueRecord);
                            }
                        }
                        if (valueRecord) {
                            value[i] = valueRecord;
                        }
                    }
                }
            }
            if (unknownValues.length) {
                params = {};
                params[me.valueParam || me.valueField] = unknownValues.join(me.delimiter);
                store.load({
                    params: params,
                    callback: function() {
                        me.setValue(value, add, true);
                        me.autoSize();
                        me.lastQuery = false;
                    }
                });
                return false;
            }
        }
        // For single-select boxes, use the last good (formal record) value if possible
        if (!isNull && !me.multiSelect && value.length > 0) {
            for (i = value.length - 1; i >= 0; i--) {
                if (value[i].isModel) {
                    value = value[i];
                    break;
                }
            }
            if (Ext.isArray(value)) {
                value = value[value.length - 1];
            }
        }
        return me.callParent([
            value,
            add
        ]);
    },
    // Private internal setting of value when records are added to the valueCollection
    // setValue itself adds to the valueCollection.
    updateValue: function() {
        var me = this,
            valueArray = me.valueCollection.getRange(),
            len = valueArray.length,
            i;
        for (i = 0; i < len; i++) {
            valueArray[i] = valueArray[i].get(me.valueField);
        }
        // Set the value of this field. If we are multi-selecting, then that is an array.
        me.setHiddenValue(valueArray);
        me.value = me.multiSelect ? valueArray : valueArray[0];
        if (!Ext.isDefined(me.value)) {
            me.value = undefined;
        }
        me.applyMultiselectItemMarkup();
        me.applyAriaListMarkup();
        me.applyAriaSelectedText();
        me.checkChange();
    },
    /**
     * Returns the records for the field's current value
     * @return {Array} The records for the field's current value
     */
    getValueRecords: function() {
        return this.valueCollection.getRange();
    },
    /**
     * @method getSubmitData
     * @inheritdoc
     * Overridden to optionally allow for submitting the field as a json encoded array.
     */
    getSubmitData: function() {
        var me = this,
            val = me.callParent(arguments);
        if (me.multiSelect && me.encodeSubmitValue && val && val[me.name]) {
            val[me.name] = Ext.encode(val[me.name]);
        }
        return val;
    },
    /**
     * Overridden to handle partial-input selections more directly
     */
    assertValue: function() {
        var me = this,
            rawValue = me.inputEl.dom.value,
            rec = !Ext.isEmpty(rawValue) ? me.findRecordByDisplay(rawValue) : false,
            value = false;
        if (!rec && !me.forceSelection && me.createNewOnBlur && !Ext.isEmpty(rawValue)) {
            value = rawValue;
        } else if (rec) {
            value = rec;
        }
        if (value) {
            me.addValue(value);
        }
        me.inputEl.dom.value = '';
        me.collapse();
        me.refreshEmptyText();
    },
    /**
     * Overridden to be more accepting of varied value types
     */
    isEqual: function(v1, v2) {
        var fromArray = Ext.Array.from,
            valueField = this.valueField,
            i, len, t1, t2;
        v1 = fromArray(v1);
        v2 = fromArray(v2);
        len = v1.length;
        if (len !== v2.length) {
            return false;
        }
        for (i = 0; i < len; i++) {
            t1 = v1[i].isModel ? v1[i].get(valueField) : v1[i];
            t2 = v2[i].isModel ? v2[i].get(valueField) : v2[i];
            if (t1 !== t2) {
                return false;
            }
        }
        return true;
    },
    /**
     * Intercept calls to onFocus to add focusCls, because the base field
     * classes assume this should be applied to inputEl
     */
    onFocus: function() {
        var me = this,
            focusCls = me.focusCls,
            itemList = me.itemList;
        if (focusCls && itemList) {
            itemList.addCls(focusCls);
        }
        me.callParent(arguments);
    },
    /**
     * Intercept calls to onBlur to remove focusCls, because the base field
     * classes assume this should be applied to inputEl
     */
    onBlur: function() {
        var me = this,
            focusCls = me.focusCls,
            itemList = me.itemList;
        if (focusCls && itemList) {
            itemList.removeCls(focusCls);
        }
        me.callParent(arguments);
    },
    /**
     * Intercept calls to renderActiveError to add invalidCls, because the base
     * field classes assume this should be applied to inputEl
     */
    renderActiveError: function() {
        var me = this,
            invalidCls = me.invalidCls,
            itemList = me.itemList,
            hasError = me.hasActiveError();
        if (invalidCls && itemList) {
            itemList[hasError ? 'addCls' : 'removeCls'](me.invalidCls + '-field');
        }
        me.callParent(arguments);
    },
    /**
     * Initiate auto-sizing for height based on {@link #grow}, if applicable.
     */
    autoSize: function() {
        var me = this;
        if (me.grow && me.rendered) {
            me.autoSizing = true;
            me.updateLayout();
        }
        return me;
    },
    /**
     * Track height change to fire {@link #event-autosize} event, when applicable.
     */
    afterComponentLayout: function() {
        var me = this,
            height;
        if (me.autoSizing) {
            height = me.getHeight();
            if (height !== me.lastInputHeight) {
                if (me.isExpanded) {
                    me.alignPicker();
                }
                me.fireEvent('autosize', me, height);
                me.lastInputHeight = height;
                me.autoSizing = false;
            }
        }
    }
});

/**
 * A time picker which provides a list of times from which to choose. This is used
 * by the {@link Ext.form.field.Time} class to allow browsing and selection of valid times,
 * but could also be used with other components.
 *
 * By default, all times starting at midnight and incrementing every 15 minutes will be presented.
 * This list of available times can be controlled using the {@link #minValue}, {@link #maxValue},
 * and {@link #increment} configuration properties. The format of the times presented in the list
 * can be customized with the {@link #format} config.
 *
 * To handle when the user selects a time from the list, you can subscribe to the
 * {@link #selectionchange} event.
 *
 *     @example
 *     Ext.create('Ext.picker.Time', {
 *        width: 60,
 *        minValue: Ext.Date.parse('04:30:00 AM', 'h:i:s A'),
 *        maxValue: Ext.Date.parse('08:00:00 AM', 'h:i:s A'),
 *        renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.picker.Time', {
    extend: 'Ext.view.BoundList',
    alias: 'widget.timepicker',
    requires: [
        'Ext.data.Store',
        'Ext.Date'
    ],
    config: {
        /**
         * @hide
         * This class creates its own store based upon time range and increment configuration.
         */
        store: true
    },
    statics: {
        /**
         * @private
         * Creates the internal {@link Ext.data.Store} that contains the available times. The store
         * is loaded with all possible times, and it is later filtered to hide those times outside
         * the minValue/maxValue.
         */
        createStore: function(format, increment) {
            var dateUtil = Ext.Date,
                clearTime = dateUtil.clearTime,
                initDate = this.prototype.initDate,
                times = [],
                min, max;
            min = clearTime(new Date(initDate[0], initDate[1], initDate[2]));
            max = dateUtil.add(clearTime(new Date(initDate[0], initDate[1], initDate[2])), 'mi', (24 * 60) - 1);
            while (min <= max) {
                times.push({
                    disp: dateUtil.dateFormat(min, format),
                    date: min
                });
                min = dateUtil.add(min, 'mi', increment);
            }
            return new Ext.data.Store({
                model: Ext.picker.Time.prototype.modelType,
                data: times
            });
        }
    },
    /**
     * @cfg {Date} minValue
     * The minimum time to be shown in the list of times. This must be a Date object
     * (only the time fields will be used); no parsing of String values will be done.
     */
    /**
     * @cfg {Date} maxValue
     * The maximum time to be shown in the list of times. This must be a Date object
     * (only the time fields will be used); no parsing of String values will be done.
     */
    /**
     * @cfg {Number} increment
     * The number of minutes between each time value in the list.
     */
    increment: 15,
    /**
     * @cfg {String} [format=undefined]
     * The default time format string which can be overriden for localization support. The format
     * must be valid according to {@link Ext.Date#parse}.
     *
     * Defaults to `'g:i A'`, e.g., `'3:15 PM'`. For 24-hour time format try `'H:i'` instead.
     * @locale
     */
    format: "g:i A",
    /**
     * The field in the implicitly-generated Model objects that gets displayed in the list. This is
     * an internal field name only and is not useful to change via config.
     * @private
     */
    displayField: 'disp',
    /**
     * Year, month, and day that all times will be normalized into internally.
     * @private
     */
    initDate: [
        2008,
        0,
        1
    ],
    componentCls: Ext.baseCSSPrefix + 'timepicker',
    alignOnScroll: false,
    /**
     * @cfg loadMask
     * @private
     */
    loadMask: false,
    initComponent: function() {
        var me = this,
            dateUtil = Ext.Date,
            clearTime = dateUtil.clearTime,
            initDate = me.initDate;
        // Set up absolute min and max for the entire day
        me.absMin = clearTime(new Date(initDate[0], initDate[1], initDate[2]));
        me.absMax = dateUtil.add(clearTime(new Date(initDate[0], initDate[1], initDate[2])), 'mi', (24 * 60) - 1);
        // Updates the range filter's filterFn according to our configured min and max
        me.updateList();
        me.callParent();
    },
    setStore: function(store) {
        // TimePicker may be used standalone without being configured as a BoundList
        // by a Time field.
        // In this case, we have to create our own store.
        this.store = (store === true) ? Ext.picker.Time.createStore(this.format, this.increment) : store;
    },
    /**
     * Set the {@link #minValue} and update the list of available times. This must be a Date object
     * (only the time fields will be used); no parsing of String values will be done.
     * @param {Date} value
     */
    setMinValue: function(value) {
        this.minValue = value;
        this.updateList();
    },
    /**
     * Set the {@link #maxValue} and update the list of available times. This must be a Date object
     * (only the time fields will be used); no parsing of String values will be done.
     * @param {Date} value
     */
    setMaxValue: function(value) {
        this.maxValue = value;
        this.updateList();
    },
    /**
     * @private
     * Sets the year/month/day of the given Date object to the {@link #initDate}, so that only
     * the time fields are significant. This makes values suitable for time comparison.
     * @param {Date} date
     */
    normalizeDate: function(date) {
        var initDate = this.initDate;
        date.setFullYear(initDate[0], initDate[1], initDate[2]);
        return date;
    },
    /**
     * Update the list of available times in the list to be constrained within the {@link #minValue}
     * and {@link #maxValue}.
     */
    updateList: function() {
        var me = this,
            min = me.normalizeDate(me.minValue || me.absMin),
            max = me.normalizeDate(me.maxValue || me.absMax),
            filters = me.getStore().getFilters(),
            filter = me.rangeFilter;
        filters.beginUpdate();
        if (filter) {
            filters.remove(filter);
        }
        filter = me.rangeFilter = new Ext.util.Filter({
            filterFn: function(record) {
                var date = record.get('date');
                return date >= min && date <= max;
            }
        });
        filters.add(filter);
        filters.endUpdate();
    }
}, function() {
    this.prototype.modelType = Ext.define(null, {
        extend: 'Ext.data.Model',
        fields: [
            'disp',
            'date'
        ]
    });
});

/**
 * Provides a time input field with a time dropdown and automatic time validation.
 *
 * This field recognizes and uses JavaScript Date objects as its main {@link #value} type
 * (only the time portion of the date is used; the month/day/year are ignored). In addition,
 * it recognizes string values which are parsed according to the {@link #format} and/or
 * {@link #altFormats} configs. These may be reconfigured to use time formats appropriate for
 * the user's locale.
 *
 * The field may be limited to a certain range of times by using the {@link #minValue} and
 * {@link #maxValue} configs, and the interval between time options in the dropdown can be changed
 * with the {@link #increment} config.
 *
 * Example usage:
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Time Card',
 *         width: 300,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items: [{
 *             xtype: 'timefield',
 *             name: 'in',
 *             fieldLabel: 'Time In',
 *             minValue: '6:00 AM',
 *             maxValue: '8:00 PM',
 *             increment: 30,
 *             anchor: '100%'
 *         }, {
 *             xtype: 'timefield',
 *             name: 'out',
 *             fieldLabel: 'Time Out',
 *             minValue: '6:00 AM',
 *             maxValue: '8:00 PM',
 *             increment: 30,
 *             anchor: '100%'
 *        }]
 *     });
 */
Ext.define('Ext.form.field.Time', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.timefield',
    alternateClassName: [
        'Ext.form.TimeField',
        'Ext.form.Time'
    ],
    requires: [
        'Ext.form.field.Date',
        'Ext.picker.Time',
        'Ext.view.BoundListKeyNav',
        'Ext.Date'
    ],
    /**
     * @cfg {String} triggerCls
     * An additional CSS class used to style the trigger button. The trigger will always get
     * the {@link Ext.form.trigger.Trigger#baseCls} by default and triggerCls will be **appended**
     * if specified.
     */
    triggerCls: Ext.baseCSSPrefix + 'form-time-trigger',
    /**
     * @cfg {Date/String} minValue
     * The minimum allowed time. Can be either a Javascript date object with a valid time value
     * or a string time in a valid format -- see {@link #format} and {@link #altFormats}.
     */
    /**
     * @cfg {Date/String} maxValue
     * The maximum allowed time. Can be either a Javascript date object with a valid time value
     * or a string time in a valid format -- see {@link #format} and {@link #altFormats}.
     */
    /**
     * @cfg {String} minText
     * The error text to display when the entered time is before {@link #minValue}.
     * @locale
     */
    minText: "The time in this field must be equal to or after {0}",
    /**
     * @cfg {String} maxText
     * The error text to display when the entered time is after {@link #maxValue}.
     * @locale
     */
    maxText: "The time in this field must be equal to or before {0}",
    /**
     * @cfg {String} invalidText
     * The error text to display when the time in the field is invalid.
     * @locale
     */
    invalidText: "{0} is not a valid time",
    /**
     * @cfg {String} format
     * The default time format string which can be overridden for localization support. 
     * The format must be valid according to {@link Ext.Date#parse}.
     *
     * Defaults to `'g:i A'`, e.g., `'3:15 PM'`. For 24-hour time format try `'H:i'` instead.
     * @locale
     */
    format: "g:i A",
    /**
     * @cfg {String} [submitFormat=undefined]
     * The date format string which will be submitted to the server. The format must be valid
     * according to
     * {@link Ext.Date#parse}.
     *
     * Defaults to {@link #format}.
     * @locale
     */
    /**
     * @cfg {String} altFormats
     * Multiple date formats separated by "|" to try when parsing a user input value
     * and it doesn't match the defined format.
     * @locale
     */
    // eslint-disable-next-line max-len
    altFormats: "g:ia|g:iA|g:i a|g:i A|h:i|g:i|H:i|ga|ha|gA|h a|g a|g A|gi|hi|gia|hia|g|H|gi a|hi a|giA|hiA|gi A|hi A",
    /**
     * @cfg {String} formatText
     * The format text to be announced by screen readers when the field is focused.
     * @locale
     */
    formatText: 'Expected time format HH:MM space AM or PM',
    /**
     * @cfg {Number} increment
     * The number of minutes between each time value in the list.
     *
     * Note that this only affects the *list of suggested times.*
     *
     * To enforce that only times on the list are valid, use {@link #snapToIncrement}.
     * That will coerce any typed values to the nearest increment point upon blur.
     */
    increment: 15,
    /**
     * @cfg {Number} pickerMaxHeight
     * The maximum height of the {@link Ext.picker.Time} dropdown.
     */
    pickerMaxHeight: 300,
    /**
     * @cfg {Boolean} selectOnTab
     * Whether the Tab key should select the currently highlighted item.
     */
    selectOnTab: true,
    /**
     * @cfg {Boolean} snapToIncrement
     * Specify as `true` to enforce that only values on the {@link #increment} boundary
     * are accepted.
     *
     * Typed values will be coerced to the nearest {@link #increment} point on blur.
     */
    snapToIncrement: false,
    /**
     * @cfg valuePublishEvent
     * @inheritdoc
     */
    valuePublishEvent: [
        'select',
        'blur'
    ],
    /**
     * @private
     * This is the date to use when generating time values in the absence of either minValue
     * or maxValue.  Using the current date causes DST issues on DST boundary dates, so this is an
     * arbitrary "safe" date that can be any date aside from DST boundary dates.
     */
    initDate: '1/1/2008',
    initDateParts: [
        2008,
        0,
        1
    ],
    initDateFormat: 'j/n/Y',
    /**
     * @cfg queryMode
     * @inheritdoc
     */
    queryMode: 'local',
    /**
     * @cfg displayField
     * @inheritdoc
     */
    displayField: 'disp',
    /**
     * @cfg valueField
     * @inheritdoc
     */
    valueField: 'date',
    initComponent: function() {
        var me = this,
            min = me.minValue,
            max = me.maxValue;
        if (min) {
            me.setMinValue(min);
        }
        if (max) {
            me.setMaxValue(max);
        }
        /* eslint-disable indent, max-len */
        me.displayTpl = new Ext.XTemplate('<tpl for=".">' + '{[typeof values === "string" ? values : this.formatDate(values["' + me.displayField + '"])]}' + '<tpl if="xindex < xcount">' + me.delimiter + '</tpl>' + '</tpl>', {
            formatDate: me.formatDate.bind(me)
        });
        /* eslint-enable indent, max-len */
        // Create a store of times.
        me.store = Ext.picker.Time.createStore(me.format, me.increment);
        me.callParent();
        // Ensure time constraints are applied to the store.
        // TimePicker does this on create.
        me.getPicker();
    },
    afterQuery: function(queryPlan) {
        var me = this;
        me.callParent([
            queryPlan
        ]);
        // Check the field for null value (TimeField returns null for invalid dates).
        // If value is null and a rawValue is present, then we we should manually
        // validate the field to display errors.
        if (me.value === null && me.getRawValue() && me.validateOnChange) {
            me.validate();
        }
    },
    /**
     * @private
     */
    isEqual: function(v1, v2) {
        var fromArray = Ext.Array.from,
            isEqual = Ext.Date.isEqual,
            i, len;
        v1 = fromArray(v1);
        v2 = fromArray(v2);
        len = v1.length;
        if (len !== v2.length) {
            return false;
        }
        for (i = 0; i < len; i++) {
            if (!(v2[i] instanceof Date) || !(v1[i] instanceof Date) || !isEqual(v2[i], v1[i])) {
                return false;
            }
        }
        return true;
    },
    /**
     * Replaces any existing {@link #minValue} with the new time and refreshes the picker's range.
     * @param {Date/String} value The minimum time that can be selected
     */
    setMinValue: function(value) {
        var me = this,
            picker = me.picker;
        me.setLimit(value, true);
        if (picker) {
            picker.setMinValue(me.minValue);
        }
    },
    /**
     * Replaces any existing {@link #maxValue} with the new time and refreshes the picker's range.
     * @param {Date/String} value The maximum time that can be selected
     */
    setMaxValue: function(value) {
        var me = this,
            picker = me.picker;
        me.setLimit(value, false);
        if (picker) {
            picker.setMaxValue(me.maxValue);
        }
    },
    /**
     * @private
     * Updates either the min or max value. Converts the user's value into a Date object whose
     * year/month/day is set to the {@link #initDate} so that only the time fields are significant.
     */
    setLimit: function(value, isMin) {
        var me = this,
            d, val;
        if (Ext.isString(value)) {
            d = me.parseDate(value);
        } else if (Ext.isDate(value)) {
            d = value;
        }
        if (d) {
            val = me.getInitDate();
            val.setHours(d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds());
        } else // Invalid min/maxValue config should result in a null so that defaulting takes over
        {
            val = null;
        }
        me[isMin ? 'minValue' : 'maxValue'] = val;
    },
    getInitDate: function(hours, minutes, seconds) {
        var parts = this.initDateParts;
        return new Date(parts[0], parts[1], parts[2], hours || 0, minutes || 0, seconds || 0, 0);
    },
    valueToRaw: function(value) {
        return this.formatDate(this.parseDate(value));
    },
    /**
     * Runs all of Time's validations and returns an array of any errors. Note that this first
     * runs Text's validations, so the returned array is an amalgamation of all field errors.
     * The additional validation checks are testing that the time format is valid, that the chosen
     * time is within the {@link #minValue} and {@link #maxValue} constraints set.
     * @param {Object} [value] The value to get errors for (defaults to the current field value)
     * @return {String[]} All validation errors for this field
     */
    getErrors: function(value) {
        value = arguments.length > 0 ? value : this.getRawValue();
        // eslint-disable-next-line vars-on-top
        var me = this,
            format = Ext.String.format,
            errors = me.callParent([
                value
            ]),
            minValue = me.minValue,
            maxValue = me.maxValue,
            data = me.displayTplData,
            raw = me.getRawValue(),
            i, len, date, item;
        if (data && data.length > 0) {
            for (i = 0 , len = data.length; i < len; i++) {
                item = data[i];
                item = item.date || item.disp;
                date = me.parseDate(item);
                if (!date) {
                    errors.push(format(me.invalidText, item, Ext.Date.unescapeFormat(me.format)));
                    
                    continue;
                }
            }
        } else if (raw.length) {
            date = me.parseDate(raw);
            if (!date) {
                // If we don't have any data & a rawValue, it means an invalid time was entered.
                errors.push(format(me.invalidText, raw, Ext.Date.unescapeFormat(me.format)));
            }
        }
        // if we have a valid date, we need to check if it's within valid range
        // this is out of the loop because as the user types a date/time, the value
        // needs to be converted before it can be compared to min/max value
        if (!errors.length) {
            if (minValue && date < minValue) {
                errors.push(format(me.minText, me.formatDate(minValue)));
            }
            if (maxValue && date > maxValue) {
                errors.push(format(me.maxText, me.formatDate(maxValue)));
            }
        }
        return errors;
    },
    formatDate: function(items) {
        var formatted = [],
            i, len;
        items = Ext.Array.from(items);
        for (i = 0 , len = items.length; i < len; i++) {
            formatted.push(Ext.form.field.Date.prototype.formatDate.call(this, items[i]));
        }
        return formatted.join(this.delimiter);
    },
    /**
     * @private
     * Parses an input value into a valid Date object.
     * @param {String/Date} value
     */
    parseDate: function(value) {
        var me = this,
            val = value,
            altFormats = me.altFormats,
            altFormatsArray = me.altFormatsArray,
            i = 0,
            len;
        if (value && !Ext.isDate(value)) {
            val = me.safeParse(value, me.format);
            if (!val && altFormats) {
                altFormatsArray = altFormatsArray || altFormats.split('|');
                len = altFormatsArray.length;
                for (; i < len && !val; ++i) {
                    val = me.safeParse(value, altFormatsArray[i]);
                }
            }
        }
        // If configured to snap, snap resulting parsed Date to the closest increment.
        if (val && me.snapToIncrement) {
            val = new Date(Ext.Number.snap(val.getTime(), me.increment * 60 * 1000));
        }
        return val;
    },
    safeParse: function(value, format) {
        var me = this,
            utilDate = Ext.Date,
            parsedDate,
            result = null;
        if (utilDate.formatContainsDateInfo(format)) {
            // assume we've been given a full date
            result = utilDate.parse(value, format);
        } else {
            // Use our initial safe date
            parsedDate = utilDate.parse(me.initDate + ' ' + value, me.initDateFormat + ' ' + format);
            if (parsedDate) {
                result = parsedDate;
            }
        }
        return result;
    },
    /**
     * @private
     */
    getSubmitValue: function() {
        var me = this,
            format = me.submitFormat || me.format,
            value = me.getValue();
        return value ? Ext.Date.format(value, format) : null;
    },
    /**
     * @private
     * Creates the {@link Ext.picker.Time}
     */
    createPicker: function() {
        var me = this;
        me.listConfig = Ext.apply({
            xtype: 'timepicker',
            pickerField: me,
            cls: undefined,
            minValue: me.minValue,
            maxValue: me.maxValue,
            increment: me.increment,
            format: me.format,
            maxHeight: me.pickerMaxHeight
        }, me.listConfig);
        return me.callParent();
    },
    completeEdit: function() {
        var me = this,
            val = me.getValue();
        me.callParent(arguments);
        // Only set the raw value if the current value is valid and is not falsy
        if (me.validateValue(val)) {
            me.setValue(val);
        }
    },
    /**
     * Finds the record by searching values in the {@link #valueField}.
     * @param {Object/String} value The value to match the field against.
     * @return {Ext.data.Model} The matched record or false.
     */
    findRecordByValue: function(value) {
        if (typeof value === 'string') {
            value = this.parseDate(value);
        }
        return this.callParent([
            value
        ]);
    },
    rawToValue: function(item) {
        var me = this,
            items, values, i, len;
        if (me.multiSelect) {
            values = [];
            items = Ext.Array.from(item);
            for (i = 0 , len = items.length; i < len; i++) {
                values.push(me.parseDate(items[i]));
            }
            return values;
        }
        return me.parseDate(item);
    },
    setValue: function(v) {
        var me = this;
        // The timefield can get in a loop when creating its picker. For instance, when creating
        // the picker, the timepicker will add a filter (see TimePicker#updateList) which will
        // then trigger the checkValueOnChange listener which in turn calls into here,
        // rinse and repeat.
        if (me.creatingPicker) {
            return;
        }
        // Store MUST be created for parent setValue to function.
        me.getPicker();
        if (Ext.isDate(v)) {
            v = me.getInitDate(v.getHours(), v.getMinutes(), v.getSeconds());
        }
        return me.callParent([
            v
        ]);
    },
    getValue: function() {
        return this.rawToValue(this.callParent(arguments));
    }
});

/**
 * @deprecated 5.0
 * Provides a convenient wrapper for TextFields that adds a clickable trigger button.
 * (looks like a combobox by default).
 *
 * As of Ext JS 5.0 this class has been deprecated.  It is recommended to use a
 * {@link Ext.form.field.Text Text Field} with the {@link Ext.form.field.Text#triggers
 * triggers} config instead.  This class is provided for compatibility reasons but is
 * not used internally by the framework.
 */
Ext.define('Ext.form.field.Trigger', {
    extend: 'Ext.form.field.Text',
    alias: [
        'widget.triggerfield',
        'widget.trigger'
    ],
    alternateClassName: [
        'Ext.form.TriggerField',
        'Ext.form.TwinTriggerField',
        'Ext.form.Trigger'
    ],
    requires: [
        'Ext.dom.Helper',
        'Ext.util.ClickRepeater'
    ],
    /**
     * @cfg {String} triggerCls
     * An additional CSS class used to style the trigger button. The trigger will always get
     * the {@link Ext.form.trigger.Trigger#baseCls} by default and triggerCls will be **appended**
     * if specified.
     */
    triggerCls: Ext.baseCSSPrefix + 'form-arrow-trigger',
    inheritableStatics: {
        /**
         * @private
         * @static
         * @inheritable
         */
        warnDeprecated: function() {
            // TODO: can we make this warning depend on compat level?
            Ext.log.warn('Ext.form.field.Trigger is deprecated. Use Ext.form.field.Text instead.');
        }
    },
    onClassExtended: function() {
        this.warnDeprecated();
    },
    constructor: function(config) {
        this.self.warnDeprecated();
        this.callParent([
            config
        ]);
    }
});

/**
 * Instances of this class encapsulate a position in a grid's row/column coordinate system.
 *
 * Cells are addressed using the owning {@link #record} and {@link #column} for robustness. 
 * the column may be moved, the store may be sorted, and the CellContext will still reference
 * the same *logical* cell. Be aware that due to buffered rendering the *physical* cell may not
 * exist.
 *
 * The {@link #setPosition} method however allows a numeric row and column to be passed in. These
 * are immediately converted.
 *
 * Be careful not to make `CellContext` objects *too* persistent. If the owning record is removed,
 * or the owning column is removed, the reference will be stale.
 *
 * Freshly created context objects, such as those exposed by events from the
 * {@link Ext.grid.selection.SpreadsheetModel spreadsheet selection model} are safe to use
 * until your application mutates the store, or changes the column set.
 */
Ext.define('Ext.grid.CellContext', {
    /**
     * @property {Boolean} isCellContext
     * @readonly
     * `true` in this class to identify an object as an instantiated CellContext,
     * or subclass thereof.
     */
    isCellContext: true,
    /**
     * @readonly
     * @property {Ext.grid.column.Column} column
     * The grid column which owns the referenced cell.
     */
    /**
     * @readonly
     * @property {Ext.data.Model} record
     * The store record which maps to the referenced cell.
     */
    /**
     * @readonly
     * @property {Number} rowIdx
     * The row number in the store which owns the referenced cell.
     *
     * *Be aware that after the initial call to {@link #setPosition}, this value may become stale
     * due to subsequent store mutation.*
     */
    /**
     * @readonly
     * @property {Number} colIdx
     * The column index in the owning View's leaf column set of the referenced cell.
     *
     * *Be aware that after the initial call to {@link #setPosition}, this value may become stale
     * due to subsequent column mutation.*
     */
    generation: 0,
    /**
      * Creates a new CellContext which references a {@link Ext.view.Table GridView}
      * @param {Ext.view.Table} view The {@link Ext.view.Table GridView} for which the cell context
      * is needed.
      *
      * To complete creation of a valid context, use the {@link #setPosition} method.
      */
    constructor: function(view) {
        this.view = view;
    },
    /**
     * Binds this cell context to a logical cell defined by the {@link #record} and {@link #column}.
     *
     * @param {Number/Ext.data.Model} row The row index or record which owns the required cell.
     * @param {Number/Ext.grid.column.Column} col The column index (In the owning View's leaf
     * column set), or the owning {@link Ext.grid.column.Column column}.
     *
     * A one argument form may be used in the form of an array:
     *
     *     [column, row]
     *
     * Or another CellContext may be passed.
     *
     * @return {Ext.grid.CellContext} this CellContext object.
     */
    setPosition: function(row, col) {
        var me = this;
        // We were passed {row: 1, column: 2, view: myView} or [2, 1]
        if (arguments.length === 1) {
            // A [column, row] array passed
            if (row.length) {
                col = row[0];
                row = row[1];
            } else if (row.isCellContext) {
                return me.setAll(row.view, row.rowIdx, row.colIdx, row.record, row.column);
            } else // An object containing {row: r, column: c}
            {
                if (row.view) {
                    me.view = row.view;
                }
                col = row.column;
                row = row.row;
            }
        }
        me.setRow(row);
        me.setColumn(col);
        return me;
    },
    setAll: function(view, recordIndex, columnIndex, record, columnHeader) {
        var me = this;
        me.view = view;
        me.rowIdx = recordIndex;
        me.colIdx = columnIndex;
        me.record = record;
        me.column = columnHeader;
        me.generation++;
        return me;
    },
    setRow: function(row) {
        var me = this,
            dataSource = me.view.dataSource,
            oldRecord = me.record,
            count;
        // eslint-disable-next-line eqeqeq
        if (row != undefined) {
            // Row index passed, < 0 meaning count from the tail (-1 is the last, etc)
            if (typeof row === 'number') {
                count = dataSource.getCount();
                row = row < 0 ? Math.max(count + row, 0) : Math.max(Math.min(row, count - 1), 0);
                me.rowIdx = row;
                me.record = dataSource.getAt(row);
            }
            // row is a Record
            else if (row.isModel) {
                me.record = row;
                me.rowIdx = dataSource.indexOf(row);
            }
            // row is a grid row, or Element wrapping row
            else if (row.tagName || row.isElement) {
                me.record = me.view.getRecord(row);
                // If it's a placeholder record for a collapsed group, index it correctly
                // eslint-disable-next-line max-len
                me.rowIdx = me.record ? (me.record.isCollapsedPlaceholder ? dataSource.indexOfPlaceholder(me.record) : dataSource.indexOf(me.record)) : -1;
            }
        }
        if (me.record !== oldRecord) {
            me.generation++;
        }
        return me;
    },
    setColumn: function(col) {
        var me = this,
            colMgr = me.view.getVisibleColumnManager(),
            oldColumn = me.column;
        // Maintainer:
        // We MUST NOT update the context view with the column's view because this context
        // may be for an Ext.locking.View which spans two grid views, and a column references
        // its local grid view.
        // eslint-disable-next-line eqeqeq
        if (col != undefined) {
            if (typeof col === 'number') {
                me.colIdx = col;
                me.column = colMgr.getHeaderAtIndex(col);
            } else if (col.isHeader) {
                me.column = col;
                // Must use the Manager's indexOf because view may be a locking view
                // And Column#getVisibleIndex returns the index of the column within its own header.
                me.colIdx = colMgr.indexOf(col);
            }
        }
        if (me.column !== oldColumn) {
            me.generation++;
        }
        return me;
    },
    setView: function(view) {
        this.view = view;
        this.refresh();
    },
    /**
     * Returns the cell object referenced *at the time of calling*. Note that grid DOM is transient,
     * and  the cell referenced may be removed from the DOM due to paging or buffered rendering
     * or column or record removal.
     *
     * @param {Boolean} returnDom Pass `true` to return a DOM object instead of an
     * {@link Ext.dom.Element Element}.
     * @return {HTMLElement/Ext.dom.Element} The cell referenced by this context.
     */
    getCell: function(returnDom) {
        return this.view.getCellByPosition(this, returnDom);
    },
    /**
     * Returns the row object referenced *at the time of calling*. Note that grid DOM is transient,
     * and  the row referenced may be removed from the DOM due to paging or buffered rendering
     * or column or record removal.
     *
     * @param {Boolean} returnDom Pass `true` to return a DOM object instead of an
     * {@link Ext.dom.Element Element}.
     * @return {HTMLElement/Ext.dom.Element} The grid row referenced by this context.
     */
    getRow: function(returnDom) {
        var result = this.view.getRow(this.record);
        return returnDom ? result : Ext.get(result);
    },
    /**
     * Returns the view node object (the encapsulating element of a data row) referenced
     * *at the time of calling*. Note that grid DOM is transient, and the node referenced
     * may be removed from the DOM due to paging or buffered rendering or column or record removal.
     *
     * @param {Boolean} returnDom Pass `true` to return a DOM object instead of an
     * {@link Ext.dom.Element Element}.
     * @return {HTMLElement/Ext.dom.Element} The grid item referenced by this context.
     */
    getNode: function(returnDom) {
        var result = this.view.getNode(this.record);
        return returnDom ? result : Ext.get(result);
    },
    /**
     * Compares this CellContext object to another CellContext to see if they refer to the same
     * cell.
     * @param {Ext.grid.CellContext} other The CellContext to compare.
     * @return {Boolean} `true` if the other cell context references the same cell as this.
     */
    isEqual: function(other) {
        return other && other.isCellContext && other.record === this.record && other.column === this.column;
    },
    /**
     * Creates a clone of this CellContext.
     *
     * The clone may be retargeted without affecting the reference of this context.
     * @return {Ext.grid.CellContext} A copy of this context, referencing the same cell.
     */
    clone: function() {
        var me = this,
            result = new me.self(me.view);
        result.rowIdx = me.rowIdx;
        result.colIdx = me.colIdx;
        result.record = me.record;
        result.column = me.column;
        return result;
    },
    privates: {
        isFirstColumn: function() {
            var cell = this.getCell(true);
            if (cell) {
                return !cell.previousSibling;
            }
        },
        isLastColumn: function() {
            var cell = this.getCell(true);
            if (cell) {
                return !cell.nextSibling;
            }
        },
        isLastRenderedRow: function() {
            return this.view.all.endIndex === this.rowIdx;
        },
        getLastColumnIndex: function() {
            var row = this.getRow(true);
            if (row) {
                return row.lastChild.cellIndex;
            }
            return -1;
        },
        refresh: function() {
            var me = this,
                newRowIdx = me.view.dataSource.indexOf(me.record),
                newColIdx = me.view.getVisibleColumnManager().indexOf(me.column);
            me.setRow(newRowIdx === -1 ? me.rowIdx : me.record);
            me.setColumn(newColIdx === -1 ? me.colIdx : me.column);
        },
        /**
         * @private
         * Navigates left or right within the current row.
         * @param {Number} direction `-1` to go towards the row start or `1` to go towards row end
         */
        navigate: function(direction) {
            var me = this,
                columns = me.view.getVisibleColumnManager().getColumns();
            switch (direction) {
                case -1:
                    do {
                        if (!me.colIdx) {
                            me.colIdx = columns.length - 1;
                        } else {
                            me.colIdx--;
                        }
                        me.setColumn(me.colIdx);
                    } while (// If we iterate off the start, wrap back to the end.
                    !me.getCell(true));
                    break;
                case 1:
                    do {
                        if (me.colIdx >= columns.length) {
                            me.colIdx = 0;
                        } else {
                            me.colIdx++;
                        }
                        me.setColumn(me.colIdx);
                    } while (// If we iterate off the end, wrap back to the start.
                    !me.getCell(true));
                    break;
            }
        }
    },
    statics: {
        compare: function(c1, c2) {
            return c1.rowIdx - c2.rowIdx || c1.colIdx - c2.colIdx;
        }
    }
});

/**
 * Internal utility class that provides default configuration for cell editing.
 * @private
 */
Ext.define('Ext.grid.CellEditor', {
    extend: 'Ext.Editor',
    alias: 'widget.celleditor',
    /**
     * @property {Boolean} isCellEditor
     * @readonly
     * `true` in this class to identify an object as an instantiated CellEditor,
     * or subclass thereof.
     */
    isCellEditor: true,
    alignment: 'l-l!',
    hideEl: false,
    cls: Ext.baseCSSPrefix + 'small-editor ' + Ext.baseCSSPrefix + 'grid-editor ' + Ext.baseCSSPrefix + 'grid-cell-editor',
    treeNodeSelector: '.' + Ext.baseCSSPrefix + 'tree-node-text',
    shim: false,
    shadow: false,
    floating: true,
    alignOnScroll: false,
    useBoundValue: false,
    focusLeaveAction: 'completeEdit',
    // Set the grid that owns this editor.
    // Called by CellEditing#getEditor
    setGrid: function(grid) {
        this.grid = grid;
    },
    startEdit: function(boundEl, value, doFocus, isResuming) {
        this.context = this.editingPlugin.context;
        this.callParent([
            boundEl,
            value,
            doFocus,
            isResuming
        ]);
    },
    /**
     * @private
     * Shows the editor, end ensures that it is rendered into the correct view
     * Hides the grid cell inner element when a cell editor is shown.
     */
    onShow: function() {
        var me = this,
            innerCell = me.boundEl.dom.querySelector(me.context.view.innerSelector);
        if (innerCell) {
            if (me.isForTree) {
                innerCell = innerCell.querySelector(me.treeNodeSelector);
            }
            Ext.fly(innerCell).hide();
        }
        me.callParent(arguments);
    },
    onFocusEnter: function() {
        var me = this,
            context = me.context,
            view = context.view;
        // Focus restoration after a refresh may require realignment and correction
        // of the context because it could have been due to a or filter operation and
        // the context may have changed position.
        me.reattachToBody();
        context.node = view.getNode(context.record);
        context.row = view.getRow(context.record);
        context.cell = context.getCell(true);
        context.rowIdx = view.indexOf(context.row);
        me.realign(true);
        me.callParent(arguments);
        // Ensure that hide processing does not throw focus back to the previously focused element.
        me.focusEnterEvent = null;
    },
    onFocusLeave: function(e) {
        var me = this,
            view = me.context.view,
            related = Ext.fly(e.relatedTarget);
        // Quit editing in whichever way.
        // The default is completeEdit.
        // If we received an ESC, this will be cancelEdit.
        if (me[me.focusLeaveAction]() === false) {
            e.event.stopEvent();
            return;
        }
        delete me.focusLeaveAction;
        // If the related target is not a cell, turn actionable mode off
        if (!view.destroyed && view.el.contains(related) && (!related.isAncestor(e.target) || related === view.el) && !related.up(view.getCellSelector(), view.el, true)) {
            me.context.grid.setActionableMode(false, view.actionPosition);
        }
        me.cacheElement();
        // Bypass Editor's onFocusLeave
        Ext.container.Container.prototype.onFocusLeave.apply(me, arguments);
    },
    completeEdit: function(remainVisible) {
        var me = this,
            context = me.context;
        if (me.editing) {
            context.value = me.field.value;
            if (me.editingPlugin.validateEdit(context) === false) {
                if (context.cancel) {
                    context.value = me.originalValue;
                    me.editingPlugin.cancelEdit();
                }
                return !!context.cancel;
            }
        }
        me.callParent([
            remainVisible
        ]);
    },
    onEditComplete: function(remainVisible, canceling) {
        var me = this,
            activeElement = Ext.Element.getActiveElement(),
            ctx = me.context,
            store = ctx && ctx.store,
            boundEl;
        me.editing = false;
        // Must refresh the boundEl in case DOM has been churned during edit.
        boundEl = me.boundEl = me.context.getCell();
        // We have to test if boundEl is still present because it could have been
        // de-rendered by a bufferedRenderer scroll.
        if (boundEl) {
            me.restoreCell();
            // IF we are just terminating, and NOT being terminated due to focus
            // having moved out of this editor, then we must prevent any upcoming blur
            // from letting focus fly out of the view.
            // onFocusLeave will have no effect because the editing flag is cleared.
            if (boundEl.contains(activeElement) && boundEl.dom !== activeElement) {
                boundEl.focus();
            }
        }
        me.callParent(arguments);
        // Do not rely on events to sync state with editing plugin,
        // Inform it directly.
        if (canceling) {
            me.editingPlugin.cancelEdit(me);
            // When expanding/collapsing a node, the editor will lose focus
            // and cancel the editing, but at the same time the expand/collapse
            // will call actionable#suspend that will cause this editor to remain visible
            // and will prevent the element from being cached. So if remainVisible is true
            // and we are expanding/collapsing we should always cache the element.
            if (remainVisible && store && store.isExpandingOrCollapsing) {
                me.cacheElement();
            }
        } else {
            me.editingPlugin.onEditComplete(me, me.getValue(), me.startValue);
        }
    },
    cacheElement: function(force) {
        if ((!this.editing || force) && !this.destroyed && !this.isDetaching) {
            this.isDetaching = true;
            this.detachFromBody();
            this.isDetaching = false;
        }
    },
    /**
     * @private
     * Hiding blurs, and blur will terminate the edit.
     * We must not allow superclass Editor to terminate the edit and make
     * sure the element has been cached.
     */
    onHide: function() {
        this.cacheElement(true);
        Ext.Editor.superclass.onHide.apply(this, arguments);
    },
    onSpecialKey: function(field, event, eOpts) {
        var me = this,
            key = event.getKey(),
            complete = me.completeOnEnter && key === event.ENTER && (!eOpts || !eOpts.fromBoundList),
            cancel = me.cancelOnEsc && key === event.ESC,
            view = me.editingPlugin.view;
        if (complete || cancel) {
            // Do not let the key event bubble into the NavigationModel
            // after we're done processing it.
            // We control the navigation action here; we focus the cell.
            event.stopEvent();
            // Maintain visibility so that focus doesn't leak.
            // We need to direct focusback to the owning cell.
            if (cancel) {
                me.focusLeaveAction = 'cancelEdit';
            }
            view.ownerGrid.setActionableMode(false);
        }
    },
    getRefOwner: function() {
        return this.column && this.column.getView();
    },
    restoreCell: function() {
        var me = this,
            innerCell = me.boundEl.dom.querySelector(me.context.view.innerSelector);
        if (innerCell) {
            if (me.isForTree) {
                innerCell = innerCell.querySelector(me.treeNodeSelector);
            }
            Ext.fly(innerCell).show();
        }
    },
    /**
     * @private
     * Fix checkbox blur when it is clicked.
     */
    afterRender: function() {
        var me = this,
            field = me.field;
        me.callParent(arguments);
        if (field.isCheckbox) {
            field.mon(field.inputEl, {
                mousedown: me.onCheckBoxMouseDown,
                click: me.onCheckBoxClick,
                scope: me
            });
        }
    },
    /**
     * @private
     * Because when checkbox is clicked it loses focus  completeEdit is bypassed.
     */
    onCheckBoxMouseDown: function() {
        this.completeEdit = Ext.emptyFn;
    },
    /**
     * @private
     * Restore checkbox focus and completeEdit method.
     */
    onCheckBoxClick: function() {
        delete this.completeEdit;
        this.field.focus(false, 10);
    },
    /**
     * @private
     * Realigns the Editor to the grid cell, or to the text node in the grid inner cell
     * if the inner cell contains multiple child nodes.
     */
    realign: function(autoSize) {
        var me = this,
            boundEl = me.boundEl,
            innerCell = boundEl.dom.querySelector(me.context.view.innerSelector),
            innerCellTextNode = innerCell.firstChild,
            width = boundEl.getWidth(),
            offsets = Ext.Array.clone(me.offsets),
            grid = me.grid,
            xOffset,
            v = '',
            // innerCell is empty if there are no children, or there is one text node,
            // and it contains whitespace
            isEmpty = !innerCellTextNode || (innerCellTextNode.nodeType === 3 && !(Ext.String.trim(v = innerCellTextNode.data).length));
        if (me.isForTree) {
            // When editing a tree, adjust the width and offsets of the editor to line
            // up with the tree cell's text element
            xOffset = me.getTreeNodeOffset(innerCell);
            width -= Math.abs(xOffset);
            offsets[0] += xOffset;
        }
        if (grid.columnLines) {
            // Subtract the column border width so that the editor displays inside the
            // borders. The column border could be either on the left or the right depending
            // on whether the grid is RTL - using the sum of both borders works in both modes.
            width -= boundEl.getBorderWidth('rl');
        }
        if (autoSize === true) {
            me.field.setWidth(width);
        }
        // https://sencha.jira.com/browse/EXTJSIV-10871 Ensure the data bearing element
        // has a height from text.
        if (isEmpty) {
            innerCell.innerHTML = 'X';
        }
        me.alignTo(boundEl, me.alignment, offsets);
        if (isEmpty) {
            innerCell.firstChild.data = v;
        }
    },
    getTreeNodeOffset: function(innerCell) {
        return Ext.fly(innerCell.querySelector(this.treeNodeSelector)).getOffsetsTo(innerCell)[0];
    }
});

/**
 * Component layout for grid column headers which have a title element at the top
 * followed by content.
 * @private
 */
Ext.define('Ext.grid.ColumnComponentLayout', {
    extend: 'Ext.layout.component.Auto',
    alias: 'layout.columncomponent',
    type: 'columncomponent',
    setWidthInDom: true,
    _paddingReset: {
        paddingTop: '',
        // reset back to default padding of the style
        paddingBottom: ''
    },
    columnAutoCls: Ext.baseCSSPrefix + 'column-header-text-container-auto',
    beginLayout: function(ownerContext) {
        this.callParent(arguments);
        ownerContext.titleContext = ownerContext.getEl('titleEl');
    },
    beginLayoutCycle: function(ownerContext) {
        var me = this,
            owner = me.owner,
            shrinkWrap = ownerContext.widthModel.shrinkWrap;
        me.callParent(arguments);
        // If shrinkwrapping, allow content width to stretch the element
        if (shrinkWrap) {
            owner.el.setWidth('');
        }
        // eslint-disable-next-line max-len
        owner.textContainerEl[shrinkWrap && !owner.isGroupHeader ? 'addCls' : 'removeCls'](me.columnAutoCls);
        owner.titleEl.setStyle(me._paddingReset);
    },
    // If not shrink wrapping, push height info down into child items
    publishInnerHeight: function(ownerContext, outerHeight) {
        var me = this,
            owner = me.owner,
            innerHeight;
        // TreePanels (and grids with hideHeaders: true) set their column container height to zero
        // to hide them. This is because they need to lay out in order to calculate widths
        // for the columns (eg flexes). If there is no height to lay out, bail out early.
        if (owner.getRootHeaderCt().hiddenHeaders) {
            ownerContext.setProp('innerHeight', 0);
            return;
        }
        // If this ia a group header; that is, it contains subheaders...
        // hasRawContent = !(target.isContainer && target.items.items.length > 0)
        if (!ownerContext.hasRawContent) {
            // We do not have enough information to get the height of the titleEl
            if (owner.headerWrap && !ownerContext.hasDomProp('width')) {
                me.done = false;
                return;
            }
            innerHeight = outerHeight - ownerContext.getBorderInfo().height;
            ownerContext.setProp('innerHeight', innerHeight - owner.titleEl.getHeight(), false);
        }
    },
    // We do not need the Direct2D sub pixel measurement here. Just the offsetHeight will do.
    // TODO: When https://sencha.jira.com/browse/EXTJSIV-7734 is fixed to not do subpixel adjustment
    // on height, remove this override.
    measureContentHeight: function(ownerContext) {
        return ownerContext.el.dom.offsetHeight;
    },
    // If not shrink wrapping, push width info down into child items
    publishInnerWidth: function(ownerContext, outerWidth) {
        // If we are acting as a container, publish the innerWidth for the ColumnLayout to use
        if (!ownerContext.hasRawContent) {
            ownerContext.setProp('innerWidth', outerWidth - ownerContext.getBorderInfo().width, false);
        }
    },
    // Push content height outwards when we are shrinkwrapping
    calculateOwnerHeightFromContentHeight: function(ownerContext, contentHeight) {
        var result = this.callParent(arguments),
            owner = this.owner;
        // If we are NOT a group header, we just use the auto component's measurement
        if (!ownerContext.hasRawContent) {
            if (!owner.headerWrap || ownerContext.hasDomProp('width')) {
                return contentHeight + owner.titleEl.getHeight() + ownerContext.getBorderInfo().height;
            }
            // We do not have the information to return the height yet because we cannot know
            // the final height of the text el
            return null;
        }
        return result;
    },
    // Push content width outwards when we are shrinkwrapping
    calculateOwnerWidthFromContentWidth: function(ownerContext, contentWidth) {
        var owner = this.owner,
            padWidth = ownerContext.getPaddingInfo().width,
            triggerOffset = this.getTriggerOffset(owner, ownerContext),
            inner;
        // Only measure the content if we're not grouped, otherwise
        // the size should be governed by the children
        if (owner.isGroupHeader) {
            inner = contentWidth;
        } else {
            inner = Math.max(contentWidth, owner.textEl.getWidth() + ownerContext.titleContext.getPaddingInfo().width);
        }
        return inner + padWidth + triggerOffset;
    },
    getTriggerOffset: function(owner, ownerContext) {
        var width = 0;
        if (ownerContext.widthModel.shrinkWrap && !owner.menuDisabled) {
            // If we have any children underneath, then we already have space reserved
            if (owner.query('>:not([hidden])').length === 0) {
                width = owner.getTriggerElWidth();
            }
        }
        return width;
    }
});

/**
 * This is a base class for layouts that contain a single item that automatically expands to fill
 * the layout's container. This class is intended to be extended or created via the `layout: 'fit'`
 * {@link Ext.container.Container#layout} config, and should generally not need to be created
 * directly via the new keyword.
 *
 * Fit layout does not have any direct config options (other than inherited ones). To fit a panel
 * to a container using Fit layout, simply set `layout: 'fit'` on the container and add
 * a single panel to it.
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *         title: 'Fit Layout',
 *         width: 300,
 *         height: 150,
 *         layout:'fit',
 *         items: {
 *             title: 'Inner Panel',
 *             html: 'This is the inner panel content',
 *             bodyPadding: 20,
 *             border: false
 *         },
 *         renderTo: Ext.getBody()
 *     });
 *
 * If the container has multiple items, all of the items will all be equally sized. This is usually
 * not desired, so to avoid this, place only a **single** item in the container. This sizing
 * of all items can be used to provide a background {@link Ext.Img image} that is "behind"
 * another item such as a {@link Ext.view.View dataview} if you also absolutely position the items.
 */
Ext.define('Ext.layout.container.Fit', {
    extend: 'Ext.layout.container.Container',
    alternateClassName: [
        'Ext.layout.FitLayout',
        'Ext.layout.Fit'
    ],
    alias: 'layout.fit',
    /**
     * @cfg itemCls
     * @inheritdoc Ext.layout.container.Container#cfg-itemCls
     */
    itemCls: Ext.baseCSSPrefix + 'fit-item',
    type: 'fit',
    manageMargins: true,
    sizePolicies: [
        {
            readsWidth: 1,
            readsHeight: 1,
            setsWidth: 0,
            setsHeight: 0
        },
        {
            readsWidth: 0,
            readsHeight: 1,
            setsWidth: 1,
            setsHeight: 0
        },
        {
            readsWidth: 1,
            readsHeight: 0,
            setsWidth: 0,
            setsHeight: 1
        },
        {
            readsWidth: 0,
            readsHeight: 0,
            setsWidth: 1,
            setsHeight: 1
        }
    ],
    getItemSizePolicy: function(item, ownerSizeModel) {
        // this layout's sizePolicy is derived from its owner's sizeModel:
        var sizeModel = ownerSizeModel || this.owner.getSizeModel(),
            mode = (sizeModel.width.shrinkWrap ? 0 : 1) | (sizeModel.height.shrinkWrap ? 0 : 2);
        return this.sizePolicies[mode];
    },
    beginLayoutCycle: function(ownerContext, firstCycle) {
        var me = this,
            // determine these before the lastSizeModels get updated:
            resetHeight = me.lastHeightModel && me.lastHeightModel.calculated,
            resetWidth = me.lastWidthModel && me.lastWidthModel.calculated,
            resetSizes = resetWidth || resetHeight,
            maxChildMinHeight = 0,
            maxChildMinWidth = 0,
            c, childItems, i, item, length, margins, minHeight, minWidth, style, undef;
        me.callParent(arguments);
        // Clear any dimensions which we set before calculation, in case the current
        // settings affect the available size. This particularly effects self-sizing
        // containers such as fields, in which the target element is naturally sized,
        // and should not be stretched by a sized child item.
        if (resetSizes && ownerContext.targetContext.el.dom.tagName.toUpperCase() !== 'TD') {
            resetSizes = resetWidth = resetHeight = false;
        }
        childItems = ownerContext.childItems;
        length = childItems.length;
        for (i = 0; i < length; ++i) {
            item = childItems[i];
            // On the firstCycle, we determine the max of the minWidth/Height of the items
            // since these can cause the container to grow scrollbars despite our attempts
            // to fit the child to the container.
            if (firstCycle) {
                c = item.target;
                minHeight = c.minHeight;
                minWidth = c.minWidth;
                if (minWidth || minHeight) {
                    margins = item.marginInfo || item.getMarginInfo();
                    // if the child item has undefined minWidth/Height, these will become
                    // NaN by adding the margins...
                    minHeight += margins.height;
                    minWidth += margins.height;
                    // if the child item has undefined minWidth/Height, these comparisons
                    // will evaluate to false... that is, "0 < NaN" == false...
                    if (maxChildMinHeight < minHeight) {
                        maxChildMinHeight = minHeight;
                    }
                    if (maxChildMinWidth < minWidth) {
                        maxChildMinWidth = minWidth;
                    }
                }
            }
            if (resetSizes) {
                style = item.el.dom.style;
                if (resetHeight) {
                    style.height = '';
                }
                if (resetWidth) {
                    style.width = '';
                }
            }
        }
        if (firstCycle) {
            ownerContext.maxChildMinHeight = maxChildMinHeight;
            ownerContext.maxChildMinWidth = maxChildMinWidth;
        }
        // Cache the overflowX/Y flags, but make them false in shrinkWrap mode (since we
        // won't be triggering overflow in that case) and false if we have no minSize (so
        // no child to trigger an overflow).
        c = ownerContext.target;
        ownerContext.overflowX = (!ownerContext.widthModel.shrinkWrap && ownerContext.maxChildMinWidth && c.scrollFlags.x) || undef;
        ownerContext.overflowY = (!ownerContext.heightModel.shrinkWrap && ownerContext.maxChildMinHeight && c.scrollFlags.y) || undef;
    },
    calculate: function(ownerContext) {
        var me = this,
            childItems = ownerContext.childItems,
            length = childItems.length,
            containerSize = me.getContainerSize(ownerContext),
            info = {
                length: length,
                ownerContext: ownerContext,
                targetSize: containerSize
            },
            shrinkWrapWidth = ownerContext.widthModel.shrinkWrap,
            shrinkWrapHeight = ownerContext.heightModel.shrinkWrap,
            overflowX = ownerContext.overflowX,
            overflowY = ownerContext.overflowY,
            scrollbars, scrollbarSize, padding, i, contentWidth, contentHeight;
        ownerContext.state.info = info;
        if (overflowX || overflowY) {
            // If we have children that have minHeight/Width, we may be forced to overflow
            // and gain scrollbars. If so, we want to remove their space from the other
            // axis so that we fit things inside the scrollbars rather than under them.
            scrollbars = me.getScrollbarsNeeded(overflowX && containerSize.width, overflowY && containerSize.height, ownerContext.maxChildMinWidth, ownerContext.maxChildMinHeight);
            if (scrollbars) {
                scrollbarSize = Ext.scrollbar.size();
                if (scrollbars & 1) {
                    // if we need the hscrollbar, remove its height
                    containerSize.height -= scrollbarSize.height;
                }
                if (scrollbars & 2) {
                    // if we need the vscrollbar, remove its width
                    containerSize.width -= scrollbarSize.width;
                }
            }
        }
        // If length === 0, it means we either have no child items, or the children are hidden
        if (length > 0) {
            // Size the child items to the container (if non-shrinkWrap):
            for (i = 0; i < length; ++i) {
                info.index = i;
                me.fitItem(childItems[i], info);
            }
        } else {
            info.contentWidth = info.contentHeight = 0;
        }
        if (shrinkWrapHeight || shrinkWrapWidth) {
            padding = ownerContext.targetContext.getPaddingInfo();
            if (shrinkWrapWidth) {
                if (overflowY && !containerSize.gotHeight) {
                    // if we might overflow vertically and don't have the container height,
                    // we don't know if we will need a vscrollbar or not, so we must wait
                    // for that height so that we can determine the contentWidth...
                    me.done = false;
                } else {
                    contentWidth = info.contentWidth + padding.width;
                    // the scrollbar flag (if set) will indicate that an overflow exists on
                    // the horz(1) or vert(2) axis... if not set, then there could never be
                    // an overflow...
                    if (scrollbars & 2) {
                        // if we need the vscrollbar, add its width
                        contentWidth += scrollbarSize.width;
                    }
                    if (!ownerContext.setContentWidth(contentWidth)) {
                        me.done = false;
                    }
                }
            }
            if (shrinkWrapHeight) {
                if (overflowX && !containerSize.gotWidth) {
                    // if we might overflow horizontally and don't have the container width,
                    // we don't know if we will need a hscrollbar or not, so we must wait
                    // for that width so that we can determine the contentHeight...
                    me.done = false;
                } else {
                    contentHeight = info.contentHeight + padding.height;
                    // the scrollbar flag (if set) will indicate that an overflow exists on
                    // the horz(1) or vert(2) axis... if not set, then there could never be
                    // an overflow...
                    if (scrollbars & 1) {
                        // if we need the hscrollbar, add its height
                        contentHeight += scrollbarSize.height;
                    }
                    if (!ownerContext.setContentHeight(contentHeight)) {
                        me.done = false;
                    }
                }
            }
        }
    },
    fitItem: function(itemContext, info) {
        var me = this;
        if (itemContext.invalid) {
            me.done = false;
            return;
        }
        info.margins = itemContext.getMarginInfo();
        info.needed = info.got = 0;
        me.fitItemWidth(itemContext, info);
        me.fitItemHeight(itemContext, info);
        // If not all required dimensions have been satisfied, we're not done.
        if (info.got !== info.needed) {
            me.done = false;
        }
    },
    fitItemWidth: function(itemContext, info) {
        var contentWidth, width;
        // Attempt to set only dimensions that are being controlled, not shrinkWrap dimensions
        if (info.ownerContext.widthModel.shrinkWrap) {
            // contentWidth must include the margins to be consistent with setItemWidth
            width = itemContext.getProp('width') + info.margins.width;
            // because we add margins, width will be NaN or a number (not undefined)
            contentWidth = info.contentWidth;
            if (contentWidth === undefined) {
                info.contentWidth = width;
            } else {
                info.contentWidth = Math.max(contentWidth, width);
            }
        } else if (itemContext.widthModel.calculated) {
            ++info.needed;
            if (info.targetSize.gotWidth) {
                ++info.got;
                this.setItemWidth(itemContext, info);
            } else {
                // Too early to position
                return;
            }
        }
        this.positionItemX(itemContext, info);
    },
    fitItemHeight: function(itemContext, info) {
        var contentHeight, height;
        if (info.ownerContext.heightModel.shrinkWrap) {
            // contentHeight must include the margins to be consistent with setItemHeight
            height = itemContext.getProp('height') + info.margins.height;
            // because we add margins, height will be NaN or a number (not undefined)
            contentHeight = info.contentHeight;
            if (contentHeight === undefined) {
                info.contentHeight = height;
            } else {
                info.contentHeight = Math.max(contentHeight, height);
            }
        } else if (itemContext.heightModel.calculated) {
            ++info.needed;
            if (info.targetSize.gotHeight) {
                ++info.got;
                this.setItemHeight(itemContext, info);
            } else {
                // Too early to position
                return;
            }
        }
        this.positionItemY(itemContext, info);
    },
    positionItemX: function(itemContext, info) {
        var margins = info.margins;
        // Adjust position to account for configured margins or if we have multiple items
        // (all items should overlap):
        if (info.index || margins.left) {
            itemContext.setProp('x', margins.left);
        }
        if (margins.width && info.ownerContext.widthModel.shrinkWrap) {
            // Need the margins for shrink-wrapping but old IE sometimes collapses
            // the left margin into the padding
            itemContext.setProp('margin-right', margins.width);
        }
    },
    positionItemY: function(itemContext, info) {
        var margins = info.margins;
        if (info.index || margins.top) {
            itemContext.setProp('y', margins.top);
        }
        if (margins.height && info.ownerContext.heightModel.shrinkWrap) {
            // Need the margins for shrink-wrapping but old IE sometimes collapses
            // the top margin into the padding
            itemContext.setProp('margin-bottom', margins.height);
        }
    },
    setItemHeight: function(itemContext, info) {
        itemContext.setHeight(info.targetSize.height - info.margins.height);
    },
    setItemWidth: function(itemContext, info) {
        itemContext.setWidth(info.targetSize.width - info.margins.width);
    }
});

/**
 * This class is the base class for both {@link Ext.tree.Panel TreePanel} and
 * {@link Ext.grid.Panel GridPanel}.
 *
 * TablePanel aggregates:
 *
 *  - a Selection Model
 *  - a View
 *  - a Store
 *  - Ext.grid.header.Container
 * 
 * @mixins Ext.grid.locking.Lockable
 */
Ext.define('Ext.panel.Table', {
    extend: 'Ext.panel.Panel',
    xtype: 'tablepanel',
    requires: [
        'Ext.layout.container.Fit'
    ],
    uses: [
        'Ext.selection.RowModel',
        'Ext.selection.CellModel',
        'Ext.selection.CheckboxModel',
        'Ext.grid.plugin.BufferedRenderer',
        'Ext.grid.header.Container',
        'Ext.grid.locking.Lockable',
        'Ext.grid.NavigationModel',
        'Ext.grid.RowContext',
        'Ext.grid.CellContext'
    ],
    extraBaseCls: Ext.baseCSSPrefix + 'grid',
    extraBodyCls: Ext.baseCSSPrefix + 'grid-body',
    actionableModeCls: Ext.baseCSSPrefix + 'grid-actionable',
    noHeaderBordersCls: Ext.baseCSSPrefix + 'no-header-borders',
    /**
     * @property defaultBindProperty
     * @inheritdoc
     */
    defaultBindProperty: 'store',
    /**
     * @cfg layout
     * @inheritdoc
     */
    layout: 'fit',
    manageLayoutScroll: false,
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'presentation',
    config: {
        /**
         * @cfg {Ext.grid.CellContext/Ext.data.Model/Number} focused
         * The focused cell, model or index. Typically used with {@link #bind binding}.
         *
         * If bound to a record (such as a selection), the first cell will be focused.
         */
        focused: null,
        /**
         * @cfg {Boolean} headerBorders
         * To show no borders around grid headers, configure this as `false`.
         */
        headerBorders: true,
        /**
         * @cfg {Boolean} hideHeaders
         * By default, visibility of headers is managed automatically based upon
         * whether there is textual content to display.
         * This configuration is only necessary if you want to disable automatic
         * header visibility management.
         *
         * If no columns have a {@link Ext.grid.column.Column#title text} config
         * (for example in the case of a {@link Ext.tree.Panel TreePanel} with no
         * columns specified), and no columns have
         * {@link Ext.grid.column.Column#columns child columns} then headers are hidden.
         *
         * If this status changes - if the column set ever goes from none having
         * text, to one having text or vice versa), then the visibility of headers
         * will be recalculated.
         *
         * Configure as `true` to hide column headers. Configure as `false` to show
         * column headers even if none of them have text.
         *
         */
        hideHeaders: null
    },
    /**
     * @cfg publishes
     * @inheritdoc
     */
    publishes: [
        'selection'
    ],
    /**
     * @cfg twoWayBindable
     * @inheritdoc
     */
    twoWayBindable: [
        'selection'
    ],
    /**
     * @cfg {Ext.data.Model} selection
     * The selected model. Typically used with {@link #bind binding}.
     */
    selection: null,
    /**
     * @cfg {Boolean} autoLoad
     * Use `true` to load the store as soon as this component is fully constructed. It is
     * best to initiate the store load this way to allow this component and potentially
     * its plugins (such as `{@link Ext.grid.filters.Filters}`) to be ready to load.
     */
    autoLoad: false,
    /**
     * @cfg {Boolean} variableRowHeight
     * @deprecated 5.0.0 Use {@link Ext.grid.column.Column#variableRowHeight} instead.
     * Configure as `true` if the row heights are not all the same height as the first row.
     */
    variableRowHeight: false,
    /**
     * @cfg {Number} numFromEdge
     * This configures the zone which causes new rows to be appended to the view. As soon
     * as the edge of the rendered grid is this number of rows from the edge of the viewport,
     * the view is moved.
     */
    numFromEdge: 2,
    /**
     * @cfg {Number} trailingBufferZone
     * TableViews are buffer rendered in 5.x and above which means that only the visible subset
     * of data rows are rendered into the DOM. These are removed and added as scrolling demands.
     *
     * This configures the number of extra rows to render on the trailing side of scrolling
     * **outside the {@link #numFromEdge}** buffer as scrolling proceeds.
     */
    trailingBufferZone: 10,
    /**
     * @cfg {Number} [leadingBufferZone]
     * TableViews are buffer rendered in 5.x and above which means that only the visible subset
     * of data rows are rendered into the DOM. These are removed and added as scrolling demands.
     *
     * This configures the number of extra rows to render on the leading side of scrolling
     * **outside the {@link #numFromEdge}** buffer as scrolling proceeds.
     */
    leadingBufferZone: 20,
    /**
     * @property {Boolean} hasView
     * True to indicate that a view has been injected into the panel.
     */
    hasView: false,
    /**
     * @property items
     * @hide
     */
    /**
     * @cfg {String} viewType
     * An xtype of view to use. This is automatically set to 'tableview' by
     * {@link Ext.grid.Panel Grid} and to 'treeview' by {@link Ext.tree.Panel Tree}.
     * @protected
     */
    viewType: null,
    /**
     * @cfg {Object} viewConfig
     * A config object that will be applied to the grid's UI view. Any of the config options
     * available for {@link Ext.view.Table} can be specified here. This option is ignored
     * if {@link #view} is specified.
     */
    /**
     * @cfg {String/Object} rowViewModel
     * The type or a config object specifying the type of the ViewModel to instantiate when creating
     * ViewModels for records to which {@link Ext.grid.column.Widget widgets in widget columns},
     * and widgets in a {@link Ext.grid.plugin.RowWidget RowWidget} row bind.
     */
    /**
     * @cfg {Ext.view.Table} view
     * The {@link Ext.view.Table} used by the grid. Use {@link #viewConfig} to just supply
     * some config options to view (instead of creating an entire View instance).
     */
    /**
     * @cfg {String} [selType]
     * An xtype of selection model to use. This is used to create selection model if just
     * a config object or nothing at all given in {@link #selModel} config.
     *
     * @deprecated 5.1.0 Use the {@link #selModel}'s `type` property. Or, if no other
     * configs are required, use the string form of selModel.
     */
    /**
     * @cfg {Ext.selection.Model/Object/String} [selModel=rowmodel]
     * A {@link Ext.selection.Model selection model} instance or config object, or the selection
     * model class's alias string.
     *
     * In latter case its `type` property determines to which type of selection model this config
     * is applied.
     */
    /**
     * @cfg {Boolean} [multiSelect=false]
     * True to enable 'MULTI' selection mode on selection model.
     * @deprecated 4.1.1 Use {@link Ext.selection.Model#mode} 'MULTI' instead.
     */
    /**
     * @cfg {Boolean} [simpleSelect=false]
     * True to enable 'SIMPLE' selection mode on selection model.
     * @deprecated 4.1.1 Use {@link Ext.selection.Model#mode} 'SIMPLE' instead.
     */
    /**
     * @cfg {Ext.data.Store/String/Object} store (required)
     * The data source to which the grid / tree is bound. Acceptable values for this 
     * property are:
     *
     *   - **any {@link Ext.data.Store Store} class / subclass**
     *   - **an {@link Ext.data.Store#storeId ID of a store}**
     *   - **a {@link Ext.data.Store Store} config object**.  When passing a config you can 
     *   specify the store type by alias.  Passing a config object with a store type will 
     *   dynamically create a new store of that type when the grid / tree is instantiated.
     *
     * For example:
     * 
     *     Ext.define('MyApp.store.Customers', {
     *         extend: 'Ext.data.Store',
     *         alias: 'store.customerstore',
     *         fields: ['name']
     *     });
     *     
     *     Ext.create({
     *         xtype: 'gridpanel',
     *         renderTo: document.body,
     *         store: {
     *             type: 'customerstore',
     *             data: [{
     *                 name: 'Foo'
     *             }]
     *         },
     *         columns: [{
     *             text: 'Name',
     *             dataIndex: 'name'
     *         }]
     *     });
     */
    /**
     * @cfg {String/Boolean} scroll
     * Scrollers configuration. Valid values are 'both', 'horizontal' or 'vertical'.
     * True implies 'both'. False implies 'none'.
     * @deprecated 5.1.0 Use {@link #scrollable} instead
     */
    /**
     * @cfg {Boolean} [reserveScrollbar=false]
     * Set this to true to **always** leave a scrollbar sized space at the end of the grid content
     * when fitting content into the width of the grid.
     *
     * If the grid's record count fluctuates enough to hide and show the scrollbar regularly,
     * this setting avoids the multiple layouts associated with switching from scrollbar present
     * to scrollbar not present.
     */
    /**
     * @cfg {Ext.grid.column.Column[]/Object} columns
     * An array of {@link Ext.grid.column.Column column} definition objects which define all columns
     * that appear in this grid. Each column definition provides the header text for the column,
     * and a definition of where the data for that column comes from.
     *
     * This can also be a configuration object for a
     * {@link Ext.grid.header.Container HeaderContainer} which may override certain default
     * configurations if necessary. For example, the special layout may be overridden to use
     * a simpler layout, or one can set default values shared by all columns:
     * 
     *     columns: {
     *         items: [
     *             {
     *                 text: "Column A",
     *                 dataIndex: "field_A"
     *             }, {
     *                 text: "Column B",
     *                 dataIndex: "field_B"
     *             }, 
     *             ...
     *         ],
     *         defaults: {
     *             flex: 1
     *         }
     *     }
     */
    /**
     * @cfg {Boolean} forceFit
     * True to force the columns to fit into the available width. Headers are first sized according
     * to configuration, whether that be a specific width, or flex. Then they are all proportionally
     * changed in width so that the entire content width is used. For more accurate control,
     * it is more optimal to specify a flex setting on the columns that are to be stretched
     * and explicit widths on columns that are not.
     */
    /**
     * @cfg {Ext.grid.feature.Feature[]/Object[]/Ext.enums.Feature[]} features
     * An array of grid Features to be added to this grid. Can also be just a single feature
     * instead of array.
     *
     * Features config behaves much like {@link #plugins}.
     * A feature can be added by either directly referencing the instance:
     *
     *     features: [
     *         Ext.create('Ext.grid.feature.GroupingSummary', {groupHeaderTpl: 'Subject: {name}'})
     *     ],
     *
     * By using config object with ftype:
     *
     *     features: [{ftype: 'groupingsummary', groupHeaderTpl: 'Subject: {name}'}],
     *
     * Or with just a ftype:
     *
     *     features: ['grouping', 'groupingsummary'],
     *
     * See {@link Ext.enums.Feature} for list of all ftypes.
     */
    /**
     * @cfg {Boolean} deferRowRender
     * Configure as `true` to enable deferred row rendering.
     *
     * This allows the View to execute a refresh quickly, with the update of the row structure
     * deferred so that layouts with GridPanels appear, and lay out more quickly.
     */
    deferRowRender: false,
    /**
     * @cfg {Boolean} sortableColumns
     * False to disable column sorting via clicking the header and via the Sorting menu items.
     */
    sortableColumns: true,
    /**
     * @cfg {Boolean} multiColumnSort
     * Configure as `true` to have columns remember their sorted state after other columns have been
     * clicked upon to sort.
     *
     * As subsequent columns are clicked upon, they become the new primary sort key.
     *
     * The maximum number of sorters allowed in a Store is configurable via its underlying data
     * collection. See {@link Ext.util.Collection#multiSortLimit}
     */
    multiColumnSort: false,
    /**
     * @cfg {Boolean} enableLocking
     * Configure as `true` to enable locking support for this grid. Alternatively, locking will also
     * be automatically enabled if any of the columns in the {@link #columns columns} configuration
     * contain a {@link Ext.grid.column.Column#locked locked} config option.
     * 
     * A locking grid is processed in a special way. The configuration options are cloned and *two*
     * grids are created to be the locked (left) side and the normal (right) side. This Panel
     * becomes merely a {@link Ext.container.Container container} which arranges both in an
     * {@link Ext.layout.container.HBox HBox} layout.
     * 
     * {@link #plugins Plugins} may be targeted at either locked, or unlocked grid, or, both,
     * in which case the plugin is cloned and used on both sides.
     * 
     * Plugins may also be targeted at the containing locking Panel.
     * 
     * This is configured by specifying a `lockableScope` property in your plugin which may have
     * the following values:
     * 
     *  * `"both"` (the default) - The plugin is added to both grids
     *  * `"top"` - The plugin is added to the containing Panel
     *  * `"locked"` - The plugin is added to the locked (left) grid
     *  * `"normal"` - The plugin is added to the normal (right) grid
     *
     * If `both` is specified, then each copy of the plugin gains a property `lockingPartner`
     * which references its sibling on the other side so that they can synchronize operations
     * if necessary.
     * 
     * {@link #features Features} may also be configured with `lockableScope` and may target
     * the locked grid, the normal grid or both grids. Features also get a `lockingPartner`
     * reference injected.
     */
    enableLocking: false,
    /**
     * @private
     * Used to determine where to go down to find views
     * this is here to support locking.
     */
    scrollerOwner: true,
    /**
     * @cfg {Boolean} enableColumnMove
     * False to disable column dragging within this grid.
     */
    enableColumnMove: true,
    /**
     * @cfg {Boolean} sealedColumns
     * True to constrain column dragging so that a column cannot be dragged in or out of it's
     * current group. Only relevant while {@link #enableColumnMove} is enabled.
     */
    sealedColumns: false,
    /**
     * @cfg {Boolean} enableColumnResize
     * False to disable column resizing within this grid.
     */
    enableColumnResize: true,
    /**
     * @cfg {Boolean} [enableColumnHide=true]
     * False to disable column hiding within this grid.
     */
    /**
     * @cfg {Boolean} columnLines
     * Adds column line styling
     */
    columnLines: false,
    /**
     * @cfg {Boolean} rowLines
     * Adds row line styling
     */
    rowLines: true,
    /**
     * @cfg {Boolean} [disableSelection=false]
     * True to disable selection model.
     */
    /**
     * @cfg {String} emptyText Default text (HTML tags are accepted) to display in the 
     * Panel body when the Store is empty. When specified, and the Store is empty, the 
     * text will be rendered inside a DIV with the CSS class "x-grid-empty". The emptyText 
     * will not display until the first load of the associated store by default. If you 
     * want the text to be displayed prior to the first store load use the 
     * {@link Ext.view.Table#deferEmptyText deferEmptyText} config in the {@link #viewConfig}
     * config.
     */
    /**
     * @cfg {Boolean} [allowDeselect=false]
     * True to allow deselecting a record. This config is forwarded to
     * {@link Ext.selection.Model#allowDeselect}.
     */
    /**
     * @cfg {Boolean} bufferedRenderer
     * Buffered rendering is enabled by default.
     * 
     * Configure as `false` to disable buffered rendering.
     * See {@link Ext.grid.plugin.BufferedRenderer}.
     *
     * @since 5.0.0
     */
    bufferedRenderer: true,
    /**
     * @cfg {Boolean} preciseHeight
     * Set to `true` to ensure that measurements (such as locking grid's row-height synchronization)
     * accurately measure rows with sub-pixel sizes. This can be an issue for some types
     * of row content on browsers that support sub-pixel sizing. Note that setting this to `true`
     * may cause a decrease in performance for large amounts of rendered content and therefore
     * should only be used when needed.
     * @since 6.5.1
     */
    preciseHeight: false,
    /**
     * @cfg stateEvents
     * @inheritdoc Ext.state.Stateful#cfg-stateEvents
     * @localdoc By default the following stateEvents are added:
     * 
     *  - {@link #event-resize} - _(added by Ext.Component)_
     *  - {@link #event-collapse} - _(added by Ext.panel.Panel)_
     *  - {@link #event-expand} - _(added by Ext.panel.Panel)_
     *  - {@link #event-columnresize}
     *  - {@link #event-columnmove}
     *  - {@link #event-columnhide}
     *  - {@link #event-columnshow}
     *  - {@link #event-sortchange}
     *  - {@link #event-filterchange}
     *  - {@link #event-groupchange}
     */
    /**
     * @property {Boolean} optimizedColumnMove
     * If you are writing a grid plugin or a {Ext.grid.feature.Feature Feature} which creates
     * a column-based structure which needs a view refresh when columns are moved, then set
     * this property in the grid.
     *
     * An example is the built in {@link Ext.grid.feature.AbstractSummary Summary} Feature.
     * This creates summary rows, and the summary columns must be in the same order
     * as the data columns. This plugin sets the `optimizedColumnMove` to `false.
     */
    /**
     * @property {Ext.panel.Table} ownerGrid
     * A reference to the top-level owning grid component.
     * 
     * This is a reference to this GridPanel if this GridPanel is not part of a locked grid
     * arrangement.
     * @readonly
     * @private
     * @since 5.0.0
     */
    ownerGrid: null,
    colLinesCls: Ext.baseCSSPrefix + 'grid-with-col-lines',
    rowLinesCls: Ext.baseCSSPrefix + 'grid-with-row-lines',
    noRowLinesCls: Ext.baseCSSPrefix + 'grid-no-row-lines',
    hiddenHeaderCtCls: Ext.baseCSSPrefix + 'grid-header-ct-hidden',
    hiddenHeaderCls: Ext.baseCSSPrefix + 'grid-header-hidden',
    resizeMarkerCls: Ext.baseCSSPrefix + 'grid-resize-marker',
    emptyCls: Ext.baseCSSPrefix + 'grid-empty',
    // The TablePanel claims to be focusable, but it does not place a tabIndex
    // on any of its elements.
    // Its focus implementation delegates to its view. TableViews are focusable.
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: true,
    /**
     * @event viewready
     * Fires when the grid view is available (use this for selecting a default row).
     * @param {Ext.panel.Table} this
     */
    constructor: function(config) {
        var me = this,
            topGrid = config && config.ownerGrid,
            store;
        me.ownerGrid = topGrid || me;
        /**
         * @property {Array} actionables An array of objects which register themselves
         * with a grid panel using {@link #registerActionable} which are consulted upon entry
         * into actionable mode.
         *
         * These must implement the following methods:
         *
         *    - activateCell Called when actionable mode is requested upon a cell.
         *    A {@link Ext.grid.CellContext CellContext} object is passed. If that cell
         *    is actionable by the terms of the callee, the callee should return `true` if it
         *    ascertains that the cell is actionable, and that it now contains focusable elements
         *    which may be tabbed to. 
         *    - activateRow Called when the user enters actionable mode in a row. The row DOM
         *    is passed. Actionables should take any action they need to prime the row for cell
         *    activation which happens as users TAB from cell to cell.
         * @readonly
         */
        // One shared array when there's a lockable at the top
        me.actionables = topGrid ? topGrid.actionables : [];
        me.callParent([
            config
        ]);
        store = me.store;
        // Any further changes become stateful.
        store.trackStateChanges = true;
        if (me.autoLoad) {
            // Note: if there is a store bound by a VM, we (might) do the load in #setStore.
            if (!store.isEmptyStore) {
                store.load();
            }
        }
    },
    /**
     * 
     * @param {Object} actionable An object which has an interest in the implementation
     * of actionable mode in this grid.
     *
     * An actionable object may be a Plugin which upon activation injects tabbable elements
     * or Components into a grid row.
     */
    registerActionable: function(actionable) {
        // If a lockableScope: 'both' plugin/feature registers on each side,
        // only include it in the actionables once.
        Ext.Array.include(this.actionables, actionable);
    },
    initComponent: function() {
        if (this.verticalScroller) {
            Ext.raise("The verticalScroller config is not supported.");
        }
        if (!this.viewType) {
            Ext.raise("You must specify a viewType config.");
        }
        if (this.headers) {
            Ext.raise("The headers config is not supported. Please specify columns instead.");
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            columns = me.columns || me.colModel || [],
            selection = me.selection,
            store, view, i, len, bufferedRenderer, headerCtCfg, headerCt;
        if (selection) {
            me.selection = null;
            me.setSelection(selection);
        }
        // Look up the configured Store. If none configured, use the fieldless, empty Store
        // defined in Ext.data.Store. If store configuration is present with no storeId
        // we will be creating a new Store instance unique to this Panel, and we should
        // destroy it as well.
        store = me.store;
        if (store && Ext.isObject(store) && !store.isStore && !store.storeId) {
            store = Ext.apply({
                autoDestroy: true
            }, store);
        }
        store = me.store = Ext.data.StoreManager.lookup(store || 'ext-empty-store');
        me.enableLocking = me.enableLocking || me.hasLockedColumns(columns);
        // Construct the plugins now rather than in the constructor of AbstractComponent
        // because the component may have a subclass that has overridden initComponent
        // and defined plugins in it. For plugins like RowExpander that rely upon a grid feature,
        // this is a problem because the view needs to know about all its features before it's
        // constructed. Constructing the plugins now ensures that plugins defined in the instance
        // config or in initComponent are all constructed before the view.
        // See EXTJSIV-11927.
        //
        // Note that any components that do not inherit from this class will still have
        // their plugins constructed in AbstractComponent#initComponent.
        if (me.plugins) {
            me.plugins = me.constructPlugins();
        }
        // Add the row/column line classes to the body element so that the settings are not
        // inherited by docked grids (https://sencha.jira.com/browse/EXTJSIV-9263).
        if (me.columnLines) {
            me.addBodyCls(me.colLinesCls);
        }
        me.addBodyCls(me.rowLines ? me.rowLinesCls : me.noRowLinesCls);
        me.addBodyCls(me.extraBodyCls);
        // If any of the Column objects contain a locked property, and are not processed,
        // this is a lockable TablePanel, a special view will be injected by the
        // Ext.grid.locking.Lockable mixin, so no processing of.
        if (me.enableLocking) {
            // Only first invocation mixes Lockable into the TablePanel class
            if (!me.mixins.lockable) {
                me.self.mixin('lockable', Ext.grid.locking.Lockable);
            }
            me.injectLockable();
        } else // Not lockable - create the HeaderContainer
        {
            // It's a fully instantiated HeaderContainer
            if (columns.isRootHeader) {
                me.headerCt = headerCt = columns;
                headerCt.grid = me;
                headerCt.forceFit = !!me.forceFit;
                columns = [];
                // If it's an instance then the column managers were already created and bound
                // to the headerCt.
                me.columnManager = headerCt.columnManager;
                me.visibleColumnManager = headerCt.visibleColumnManager;
            } else // It's an array of Column definitions, or a config object of a HeaderContainer
            {
                headerCtCfg = {
                    grid: me,
                    $initParent: me,
                    forceFit: me.forceFit,
                    sortable: me.sortableColumns,
                    enableColumnMove: me.enableColumnMove,
                    enableColumnResize: me.enableColumnResize,
                    columnLines: me.columnLines,
                    sealed: me.sealedColumns
                };
                if (Ext.isObject(columns)) {
                    Ext.apply(headerCtCfg, columns);
                    columns = columns.items;
                    delete headerCtCfg.items;
                }
                me.headerCt = headerCt = new Ext.grid.header.Container(headerCtCfg);
            }
            if (Ext.isDefined(me.enableColumnHide)) {
                headerCt.enableColumnHide = me.enableColumnHide;
            }
        }
        me.scrollTask = new Ext.util.DelayedTask(me.syncHorizontalScroll, me);
        me.cls = (me.cls || '') + (' ' + me.extraBaseCls);
        // autoScroll is not a valid configuration
        delete me.autoScroll;
        bufferedRenderer = me.plugins && Ext.Array.findBy(me.plugins, function(p) {
            return p.isBufferedRenderer;
        });
        // If we find one in the plugins, just use that.
        if (bufferedRenderer) {
            me.bufferedRenderer = bufferedRenderer;
        }
        // If this TablePanel is lockable (Either configured lockable, or any of the defined 
        // columns has a 'locked' property) then a special lockable view containing 2 side-by-side
        // grids will have been injected so we do not need to set up any UI.
        if (!me.hasView) {
            // If the store is paging blocks of the dataset in, then it can only be sorted remotely.
            // And if the store is not remoteSort, then we cannot sort it at all.
            if (store.isBufferedStore && !store.getRemoteSort()) {
                for (i = 0 , len = columns.length; i < len; i++) {
                    columns[i].sortable = false;
                }
            }
            me.relayHeaderCtEvents(headerCt);
            me.features = me.features || [];
            if (!Ext.isArray(me.features)) {
                me.features = [
                    me.features
                ];
            }
            me.viewConfig = me.viewConfig || {};
            // AbstractDataView will look up a Store configured as an object
            // getView converts viewConfig into a View instance
            view = me.getView();
            me.items = [
                view
            ];
            me.hasView = true;
            // Attach this Panel to the Store
            me.bindStore(store, true);
            me.mon(view, {
                viewready: me.onViewReady,
                refresh: me.onRestoreHorzScroll,
                scope: me
            });
        }
        // Whatever kind of View we have, be it a TableView, or a LockingView, we are interested
        // in the selection model
        me.selModel = me.view.getSelectionModel();
        // We update the bound selection whenever the selectionchange event fires.
        // Even a CellModel, or a SpreadsheetModel in cell selection mode can yield
        // the *records* that are selected, and it is the first record which is published
        // to the selection property.
        me.selModel.on({
            scope: me,
            lastselectedchanged: me.updateBindSelection,
            selectionchange: me.updateBindSelection
        });
        // Relay events from the View whether it be a LockingView, or a regular GridView
        me.relayEvents(me.view, [
            /**
             * @event beforeitemlongpress
             * @inheritdoc Ext.view.View#beforeitemlongpress
             */
            'beforeitemlongpress',
            /**
             * @event beforeitemmousedown
             * @inheritdoc Ext.view.View#beforeitemmousedown
             */
            'beforeitemmousedown',
            /**
             * @event beforeitemmouseup
             * @inheritdoc Ext.view.View#beforeitemmouseup
             */
            'beforeitemmouseup',
            /**
             * @event beforeitemmouseenter
             * @inheritdoc Ext.view.View#beforeitemmouseenter
             */
            'beforeitemmouseenter',
            /**
             * @event beforeitemmouseleave
             * @inheritdoc Ext.view.View#beforeitemmouseleave
             */
            'beforeitemmouseleave',
            /**
             * @event beforeitemclick
             * @inheritdoc Ext.view.View#beforeitemclick
             */
            'beforeitemclick',
            /**
             * @event beforeitemdblclick
             * @inheritdoc Ext.view.View#beforeitemdblclick
             */
            'beforeitemdblclick',
            /**
             * @event beforeitemcontextmenu
             * @inheritdoc Ext.view.View#beforeitemcontextmenu
             */
            'beforeitemcontextmenu',
            /**
             * @event itemlongpress
             * @inheritdoc Ext.view.View#itemlongpress
             */
            'itemlongpress',
            /**
             * @event itemmousedown
             * @inheritdoc Ext.view.View#itemmousedown
             */
            'itemmousedown',
            /**
             * @event itemmouseup
             * @inheritdoc Ext.view.View#itemmouseup
             */
            'itemmouseup',
            /**
             * @event itemmouseenter
             * @inheritdoc Ext.view.View#itemmouseenter
             */
            'itemmouseenter',
            /**
             * @event itemmouseleave
             * @inheritdoc Ext.view.View#itemmouseleave
             */
            'itemmouseleave',
            /**
             * @event itemclick
             * @inheritdoc Ext.view.View#itemclick
             */
            'itemclick',
            /**
             * @event itemdblclick
             * @inheritdoc Ext.view.View#itemdblclick
             */
            'itemdblclick',
            /**
             * @event itemcontextmenu
             * @inheritdoc Ext.view.View#itemcontextmenu
             */
            'itemcontextmenu',
            /**
             * @event beforecellclick
             * @inheritdoc Ext.view.Table#beforecellclick
             */
            'beforecellclick',
            /**
             * @event cellclick
             * @inheritdoc Ext.view.Table#cellclick
             */
            'cellclick',
            /**
             * @event beforecelldblclick
             * @inheritdoc Ext.view.Table#beforecelldblclick
             */
            'beforecelldblclick',
            /**
             * @event celldblclick
             * @inheritdoc Ext.view.Table#celldblclick
             */
            'celldblclick',
            /**
             * @event beforecellcontextmenu
             * @inheritdoc Ext.view.Table#beforecellcontextmenu
             */
            'beforecellcontextmenu',
            /**
             * @event cellcontextmenu
             * @inheritdoc Ext.view.Table#cellcontextmenu
             */
            'cellcontextmenu',
            /**
             * @event beforecellmousedown
             * @inheritdoc Ext.view.Table#beforecellmousedown
             */
            'beforecellmousedown',
            /**
             * @event cellmousedown
             * @inheritdoc Ext.view.Table#cellmousedown
             */
            'cellmousedown',
            /**
             * @event beforecellmouseup
             * @inheritdoc Ext.view.Table#beforecellmouseup
             */
            'beforecellmouseup',
            /**
             * @event cellmouseup
             * @inheritdoc Ext.view.Table#cellmouseup
             */
            'cellmouseup',
            /**
             * @event beforecellkeydown
             * @inheritdoc Ext.view.Table#beforecellkeydown
             */
            'beforecellkeydown',
            /**
             * @event cellkeydown
             * @inheritdoc Ext.view.Table#cellkeydown
             */
            'cellkeydown',
            /**
             * @event rowclick
             * @inheritdoc Ext.view.Table#rowclick
             */
            'rowclick',
            /**
             * @event rowdblclick
             * @inheritdoc Ext.view.Table#rowdblclick
             */
            'rowdblclick',
            /**
             * @event rowcontextmenu
             * @inheritdoc Ext.view.Table#rowcontextmenu
             */
            'rowcontextmenu',
            /**
             * @event rowmousedown
             * @inheritdoc Ext.view.Table#rowmousedown
             */
            'rowmousedown',
            /**
             * @event rowmouseup
             * @inheritdoc Ext.view.Table#rowmouseup
             */
            'rowmouseup',
            /**
             * @event rowkeydown
             * @inheritdoc Ext.view.Table#rowkeydown
             */
            'rowkeydown',
            /**
             * @event beforeitemkeydown
             * @inheritdoc Ext.view.View#event!beforeitemkeydown
             */
            'beforeitemkeydown',
            /**
             * @event itemkeydown
             * @inheritdoc Ext.view.View#event!itemkeydown
             */
            'itemkeydown',
            /**
             * @event beforeitemkeyup
             * @inheritdoc Ext.view.View#event!beforeitemkeyup
             */
            'beforeitemkeyup',
            /**
             * @event itemkeyup
             * @inheritdoc Ext.view.View#event!itemkeyup
             */
            'itemkeyup',
            /**
             * @event beforeitemkeypress
             * @inheritdoc Ext.view.View#event!beforeitemkeypress
             */
            'beforeitemkeypress',
            /**
             * @event itemkeypress
             * @inheritdoc Ext.view.View#event!itemkeypress
             */
            'itemkeypress',
            /**
             * @event beforecontainermousedown
             * @inheritdoc Ext.view.View#beforecontainermousedown
             */
            'beforecontainermousedown',
            /**
             * @event beforecontainermouseup
             * @inheritdoc Ext.view.View#beforecontainermouseup
             */
            'beforecontainermouseup',
            /**
             * @event beforecontainermouseover
             * @inheritdoc Ext.view.View#beforecontainermouseover
             */
            'beforecontainermouseover',
            /**
             * @event beforecontainermouseout
             * @inheritdoc Ext.view.View#beforecontainermouseout
             */
            'beforecontainermouseout',
            /**
             * @event beforecontainerclick
             * @inheritdoc Ext.view.View#beforecontainerclick
             */
            'beforecontainerclick',
            /**
             * @event beforecontainerdblclick
             * @inheritdoc Ext.view.View#beforecontainerdblclick
             */
            'beforecontainerdblclick',
            /**
             * @event beforecontainercontextmenu
             * @inheritdoc Ext.view.View#beforecontainercontextmenu
             */
            'beforecontainercontextmenu',
            /**
             * @event beforecontainerkeydown
             * @inheritdoc Ext.view.View#beforecontainerkeydown
             */
            'beforecontainerkeydown',
            /**
             * @event beforecontainerkeyup
             * @inheritdoc Ext.view.View#beforecontainerkeyup
             */
            'beforecontainerkeyup',
            /**
             * @event beforecontainerkeypress
             * @inheritdoc Ext.view.View#beforecontainerkeypress
             */
            'beforecontainerkeypress',
            /**
             * @event containermouseup
             * @inheritdoc Ext.view.View#containermouseup
             */
            'containermouseup',
            /**
             * @event containermousedown
             * @inheritdoc Ext.view.View#containermousedown
             */
            'containermousedown',
            /**
             * @event containermouseover
             * @inheritdoc Ext.view.View#containermouseover
             */
            'containermouseover',
            /**
             * @event containermouseout
             * @inheritdoc Ext.view.View#containermouseout
             */
            'containermouseout',
            /**
             * @event containerclick
             * @inheritdoc Ext.view.View#containerclick
             */
            'containerclick',
            /**
             * @event containerdblclick
             * @inheritdoc Ext.view.View#containerdblclick
             */
            'containerdblclick',
            /**
             * @event containercontextmenu
             * @inheritdoc Ext.view.View#containercontextmenu
             */
            'containercontextmenu',
            /**
             * @event containerkeydown
             * @inheritdoc Ext.view.View#containerkeydown
             */
            'containerkeydown',
            /**
             * @event containerkeyup
             * @inheritdoc Ext.view.View#containerkeyup
             */
            'containerkeyup',
            /**
             * @event containerkeypress
             * @inheritdoc Ext.view.View#containerkeypress
             */
            'containerkeypress',
            /**
             * @event beforeselect
             * @inheritdoc Ext.selection.RowModel#beforeselect
             */
            'beforeselect',
            /**
             * @event select
             * @inheritdoc Ext.selection.RowModel#select
             */
            'select',
            /**
             * @event beforedeselect
             * @inheritdoc Ext.selection.RowModel#beforedeselect
             */
            'beforedeselect',
            /**
             * @event deselect
             * @inheritdoc Ext.selection.RowModel#deselect
             */
            'deselect',
            /**
             * @event beforerowexit
             * @inheritdoc Ext.view.Table#beforerowexit
             */
            'beforerowexit'
        ]);
        // Only relay the event if it's not SpreadsheetModel.
        // SpreadsheetModel fires it directly through the Panel.
        if (!me.selModel.isSpreadsheetModel) {
            me.relayEvents(me.view, [
                /**
                 * @event selectionchange
                 * @inheritdoc Ext.selection.Model#selectionchange
                 */
                'selectionchange'
            ]);
        }
        // If we have our own headerCt (not gone through injectLockable), then add it
        // to our docked items and then add the columns. In this way, the columns
        // will immediately be able to interrogate their environment through getView
        // and getRootHeaderCt
        if (headerCt) {
            headerCt.view = me.view;
            (me.dockedItems = Ext.Array.from(me.dockedItems, true)).unshift(headerCt);
            headerCt.add(columns);
        }
        // Maintain backward compatibiliy by providing the initial leaf column set as a property.
        me.columns = me.headerCt.getGridColumns();
        me.callParent();
        me.syncHeaderVisibility();
        if (me.enableLocking) {
            me.afterInjectLockable();
        }
        me.addStateEvents([
            'columnresize',
            'columnmove',
            'columnhide',
            'columnshow',
            'sortchange',
            'filteractivate',
            'filterdeactivate',
            'filterchange',
            'groupchange'
        ]);
    },
    // rowBody feature events
    /**
         * @event beforerowbodymousedown
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodymousedown
         */
    /**
         * @event beforerowbodymouseup
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodymouseup
         */
    /**
         * @event beforerowbodyclick
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodyclick
         */
    /**
         * @event beforerowbodydblclick
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodydblclick
         */
    /**
         * @event beforerowbodycontextmenu
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodycontextmenu
         */
    /**
         * @event beforerowbodylongpress
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodylongpress
         */
    /**
         * @event beforerowbodykeydown
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodykeydown
         */
    /**
         * @event beforerowbodykeyup
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodykeyup
         */
    /**
         * @event beforerowbodykeypress
         * @preventable
         * @inheritdoc Ext.view.Table#event-beforerowbodykeypress
         */
    /**
         * @event rowbodymousedown
         * @inheritdoc Ext.view.Table#event-rowbodymousedown
         */
    /**
         * @event rowbodymouseup
         * @inheritdoc Ext.view.Table#event-rowbodymouseup
         */
    /**
         * @event rowbodyclick
         * @inheritdoc Ext.view.Table#event-rowbodyclick
         */
    /**
         * @event rowbodydblclick
         * @inheritdoc Ext.view.Table#event-rowbodydblclick
         */
    /**
         * @event rowbodycontextmenu
         * @inheritdoc Ext.view.Table#event-rowbodycontextmenu
         */
    /**
         * @event rowbodylongpress
         * @inheritdoc Ext.view.Table#event-rowbodylongpress
         */
    /**
         * @event rowbodykeydown
         * @inheritdoc Ext.view.Table#event-rowbodykeydown
         */
    /**
         * @event rowbodykeyup
         * @inheritdoc Ext.view.Table#event-rowbodykeyup
         */
    /**
         * @event rowbodykeypress
         * @inheritdoc Ext.view.Table#event-rowbodykeypress
         */
    updateHideHeaders: function(hideHeaders) {
        // Must only update the visibility after all configuration is finished.
        // initComponent calls syncHeaderVisibility
        if (!this.isConfiguring) {
            this.syncHeaderVisibility();
        }
    },
    beforeRender: function() {
        var me = this,
            bufferedRenderer = me.bufferedRenderer,
            ariaAttr;
        // If this is the topmost container of a lockable assembly, add the special class body
        if (me.lockable) {
            me.getProtoBody().addCls(me.lockingBodyCls);
        } else // Don't create a buffered renderer for a locked grid.
        {
            // If we're auto heighting, we can't buffered render, so don't create it
            if (bufferedRenderer && me.getSizeModel().height.auto) {
                if (bufferedRenderer.isBufferedRenderer) {
                    Ext.raise('Cannot use buffered rendering with auto height');
                }
                me.bufferedRenderer = bufferedRenderer = false;
            }
            if (bufferedRenderer && !bufferedRenderer.isBufferedRenderer) {
                // Create a BufferedRenderer as a plugin if we have not already configured with one.
                bufferedRenderer = {
                    xclass: 'Ext.grid.plugin.BufferedRenderer'
                };
                // eslint-disable-next-line max-len
                Ext.copy(bufferedRenderer, me, 'variableRowHeight,numFromEdge,trailingBufferZone,leadingBufferZone,scrollToLoadBuffer', true);
                me.bufferedRenderer = me.addPlugin(bufferedRenderer);
            }
            ariaAttr = me.ariaRenderAttributes || (me.ariaRenderAttributes = {});
            ariaAttr['aria-readonly'] = !me.isEditable;
            ariaAttr['aria-multiselectable'] = me.selModel.selectionMode !== 'SINGLE';
        }
        me.callParent(arguments);
    },
    beforeLayout: function() {
        var lockable = this.mixins.lockable;
        if (lockable) {
            lockable.beforeLayout.call(this);
        }
        this.callParent();
    },
    onHide: function(animateTarget, cb, scope) {
        this.getView().onOwnerGridHide();
        this.callParent([
            animateTarget,
            cb,
            scope
        ]);
    },
    onShow: function() {
        this.callParent();
        this.getView().onOwnerGridShow();
    },
    /**
     * Gets the {@link Ext.grid.header.Container headercontainer} for this grid / tree.
     * @return {Ext.grid.header.Container} headercontainer
     *
     * **Note:** While a locked grid / tree will return an instance of
     * {@link Ext.grid.locking.HeaderContainer} you will code to the
     * {@link Ext.grid.header.Container} API.
     */
    getHeaderContainer: function() {
        return this.getView().getHeaderCt();
    },
    /**
     * @method getColumns
     * @inheritdoc Ext.grid.header.Container#getGridColumns
     */
    getColumns: function() {
        return this.getColumnManager().getColumns();
    },
    /**
     * @method getVisibleColumns
     * @inheritdoc Ext.grid.header.Container#getVisibleGridColumns
     */
    getVisibleColumns: function() {
        return this.getVisibleColumnManager().getColumns();
    },
    getScrollable: function() {
        // Lockable grids own a separate Y scroller which scrolls both grids in a single
        // scrolling element.
        // Regular grids return their view's scroller.
        return this.scrollable || this.view.getScrollable();
    },
    focus: function() {
        // TablePanel is not focusable, but allow a call to delegate into the view
        var view = this.getView();
        if (!view.isVisible(true)) {
            return false;
        }
        view.focus();
    },
    /**
     * Disables interaction with, and masks this grid's column headers.
     */
    disableColumnHeaders: function() {
        this.headerCt.disable();
    },
    /**
     * Enables interaction with, and unmasks this grid's column headers after a call
     * to {#disableColumnHeaders}.
     */
    enableColumnHeaders: function() {
        this.headerCt.enable();
    },
    /**
     * @private
     * Determine if there are any columns with a locked configuration option.
     */
    hasLockedColumns: function(columns) {
        var i, len, column;
        // Fully instantiated HeaderContainer
        if (columns.isRootHeader) {
            columns = columns.items.items;
        }
        // Config object with items
        else if (Ext.isObject(columns)) {
            columns = columns.items;
        }
        for (i = 0 , len = columns.length; i < len; i++) {
            column = columns[i];
            if (!column.processed && column.locked) {
                return true;
            }
        }
    },
    relayHeaderCtEvents: function(headerCt) {
        this.relayEvents(headerCt, [
            /**
             * @event columnresize
             * @inheritdoc Ext.grid.header.Container#columnresize
             */
            'columnresize',
            /**
             * @event columnmove
             * @inheritdoc Ext.grid.header.Container#columnmove
             */
            'columnmove',
            /**
             * @event columnhide
             * @inheritdoc Ext.grid.header.Container#columnhide
             */
            'columnhide',
            /**
             * @event columnshow
             * @inheritdoc Ext.grid.header.Container#columnshow
             */
            'columnshow',
            /**
             * @event columnschanged
             * @inheritdoc Ext.grid.header.Container#columnschanged
             */
            'columnschanged',
            /**
             * @event sortchange
             * @inheritdoc Ext.grid.header.Container#sortchange
             */
            'sortchange',
            /**
             * @event headerclick
             * @inheritdoc Ext.grid.header.Container#headerclick
             */
            'headerclick',
            /**
             * @event headercontextmenu
             * @inheritdoc Ext.grid.header.Container#headercontextmenu
             */
            'headercontextmenu',
            /**
             * @event headertriggerclick
             * @inheritdoc Ext.grid.header.Container#headertriggerclick
             */
            'headertriggerclick'
        ]);
    },
    getState: function() {
        var me = this,
            state = me.callParent(),
            storeState = me.store.getState();
        state = me.addPropertyToState(state, 'columns', me.headerCt.getColumnsState());
        if (storeState) {
            state.storeState = storeState;
        }
        return state;
    },
    applyState: function(state) {
        var me = this,
            sorter = state.sort,
            storeState = state.storeState,
            store = me.store,
            columns = state.columns = me.buildColumnHash(state.columns);
        // Ensure superclass has applied *its* state.
        // Component saves dimensions (and anchor/flex) plus collapsed state.
        me.callParent([
            state
        ]);
        if (columns) {
            // Column state restoration needs to examine store state
            me.headerCt.applyColumnsState(columns, storeState);
        }
        if (store.isEmptyStore) {
            return;
        }
        // Old stored sort state. Deprecated and will die out.
        if (sorter) {
            if (store.getRemoteSort()) {
                // Pass false to prevent a sort from occurring.
                store.sort({
                    property: sorter.property,
                    direction: sorter.direction,
                    root: sorter.root
                }, null, false);
            } else {
                store.sort(sorter.property, sorter.direction);
            }
        }
        // New storeState which encapsulates groupers, sorters and filters.
        else if (storeState) {
            store.applyState(storeState);
        }
    },
    buildColumnHash: function(columns) {
        var len, columnState, i, result;
        // Create a usable state lookup hash from which each column
        // may look up its state based upon its stateId
        // {
        //      col_name: {
        //          index: 0,
        //          width: 100,
        //          locked: true
        //      },
        //      col_details: {
        //          index: 1,
        //          width: 200,
        //          columns: {
        //              col_details_1: {
        //                  index: 0,
        //                  width: 100
        //              },
        //              col_details_2: {
        //                  index: 1,
        //                  width: 100
        //              }
        //          }
        //      },
        // }
        if (columns) {
            result = {};
            for (i = 0 , len = columns.length; i < len; i++) {
                columnState = columns[i];
                columnState.index = i;
                if (columnState.columns) {
                    columnState.columns = this.buildColumnHash(columnState.columns);
                }
                result[columnState.id] = columnState;
            }
            return result;
        }
    },
    /**
     * Returns the store associated with this Panel.
     * @return {Ext.data.Store} The store
     */
    getStore: function() {
        return this.store;
    },
    onViewRefresh: function(view, records) {
        this.onItemAdd(records, 0);
    },
    onItemAdd: function(records, index, nodes, view) {
        var me = this,
            recCount = records.length,
            freeRowContexts = me.freeRowContexts,
            liveRowContexts = me.liveRowContexts || (me.liveRowContexts = {}),
            rowContext, i, record;
        // Ensure we have RowContexts ready for all the widget owners
        // (Widget columns or RowWidget plugin) which will be needing instantiated
        // Widgets with attached ViewModels.
        for (i = 0; i < recCount; i++) {
            record = records[i];
            // We may have already been informed about the addition of this item
            // by the opposite locking partner
            if (!liveRowContexts[record.internalId]) {
                // Attempt to read from free RowContexts which may have been freed
                // by a previous item remove event. Shift of the front
                // to improve the chances of using the same RowContext for a record;
                // They were pushed on in the item remove handler.
                rowContext = freeRowContexts && freeRowContexts.shift();
                // Need a new one
                if (!rowContext) {
                    rowContext = new Ext.grid.RowContext({
                        ownerGrid: me
                    });
                }
                me.liveRowContexts[record.internalId] = rowContext;
                rowContext.setRecord(record, index++);
            }
        }
    },
    onItemRemove: function(records, index, nodes, view) {
        var me = this,
            freeRowContexts = me.freeRowContexts || (me.freeRowContexts = []),
            liveRowContexts = me.liveRowContexts,
            len = nodes.length,
            i, id, context;
        for (i = 0; i < len; i++) {
            id = nodes[i].getAttribute('data-recordId');
            context = liveRowContexts[id];
            // We may have already been informed about the removal of this item
            // by the opposite locking partner
            if (context) {
                context.free(view);
                freeRowContexts.push(context);
                delete liveRowContexts[id];
            }
        }
    },
    createManagedWidget: function(view, ownerId, widgetConfig, record) {
        return this.liveRowContexts[record.internalId].getWidget(view, ownerId, widgetConfig);
    },
    destroyManagedWidgets: function(ownerId) {
        var me = this,
            contexts = me.liveRowContexts,
            freeRowContexts = me.freeRowContexts,
            len = freeRowContexts && freeRowContexts.length,
            i, recInternalId, rowWidgets;
        // Destroy widgets from both live contexts, and free ones
        for (recInternalId in contexts) {
            rowWidgets = contexts[recInternalId].widgets;
            if (rowWidgets) {
                Ext.destroy(rowWidgets[ownerId]);
                delete rowWidgets[ownerId];
            }
        }
        for (i = 0; i < len; i++) {
            rowWidgets = freeRowContexts[i].widgets;
            if (rowWidgets) {
                Ext.destroy(rowWidgets[ownerId]);
                delete rowWidgets[ownerId];
            }
        }
    },
    getManagedWidgets: function(ownerId) {
        var me = this,
            contexts = me.liveRowContexts,
            recInternalId,
            result = [];
        for (recInternalId in contexts) {
            result.push(contexts[recInternalId].widgets[ownerId]);
        }
        return result;
    },
    /**
     * Gets the view for this panel.
     * @return {Ext.view.Table}
     */
    getView: function() {
        var me = this,
            scroll, scrollable, viewConfig;
        if (!me.view) {
            viewConfig = me.viewConfig;
            scroll = viewConfig.scroll || me.scroll;
            scrollable = me.scrollable;
            if (scrollable == null && viewConfig.scrollable == null && scroll !== null) {
                // transform deprecated scroll config into scrollable config
                if (scroll === true || scroll === 'both') {
                    scrollable = true;
                } else if (scroll === false || scroll === 'none') {
                    scrollable = false;
                } else if (scroll === 'vertical') {
                    scrollable = {
                        x: false,
                        y: true
                    };
                } else if (scroll === 'horizontal') {
                    scrollable = {
                        x: true,
                        y: false
                    };
                }
            }
            viewConfig = Ext.apply({
                // TableView injects the view reference into this grid so that we have a reference
                // as early as possible and Features need a reference to the grid.
                // For these reasons, we configure a reference to this grid into the View
                grid: me,
                ownerGrid: me.ownerGrid,
                deferInitialRefresh: me.deferRowRender,
                variableRowHeight: me.variableRowHeight,
                preserveScrollOnRefresh: true,
                trackOver: me.trackMouseOver !== false,
                throttledUpdate: me.throttledUpdate === true,
                xtype: me.viewType,
                store: me.store,
                headerCt: me.headerCt,
                columnLines: me.columnLines,
                rowLines: me.rowLines,
                navigationModel: 'grid',
                features: me.features,
                panel: me,
                emptyText: me.emptyText || ''
            }, me.viewConfig);
            // Impose our calculated scrollable config only if scrollability is not configured.
            // eslint-disable-next-line max-len
            if (!('scrollable' in viewConfig || 'scroll' in viewConfig || 'autoScroll' in viewConfig) && scrollable != null) {
                viewConfig.scrollable = scrollable;
            }
            viewConfig.$initParent = me;
            Ext.create(viewConfig);
            delete viewConfig.$initParent;
            // Normalize the application of the markup wrapping the emptyText config.
            // `emptyText` can now be defined on the grid as well as on its viewConfig,
            // and this led to the emptyText not having the wrapping markup when it was defined
            // in the viewConfig. It should be backwards compatible.
            // Note that in the unlikely event that emptyText is defined on both the grid config
            // and the viewConfig that the viewConfig wins.
            if (me.view.emptyText) {
                me.view.emptyText = '<div class="' + me.emptyCls + '">' + me.view.emptyText + '</div>';
            }
            // TableView's custom component layout, Ext.view.TableLayout requires a reference
            // to the headerCt because it depends on the headerCt doing its work.
            me.view.getComponentLayout().headerCt = me.headerCt;
            me.mon(me.view, {
                uievent: me.processEvent,
                scope: me
            });
            // Plugins and features may need to access the view as soon as it is created.
            if (me.hasListeners.viewcreated) {
                me.fireEvent('viewcreated', me, me.view);
            }
        }
        return me.view;
    },
    getEmptyText: function() {
        return this.view.emptyText;
    },
    setEmptyText: function(emptyText) {
        this.emptyText = emptyText;
        this.view.setEmptyText('<div class="' + this.emptyCls + '">' + emptyText + '</div>');
        return this;
    },
    getColumnManager: function() {
        return this.columnManager;
    },
    getVisibleColumnManager: function() {
        return this.visibleColumnManager;
    },
    getTopLevelColumnManager: function() {
        return this.ownerGrid.getColumnManager();
    },
    getTopLevelVisibleColumnManager: function() {
        return this.ownerGrid.getVisibleColumnManager();
    },
    /**
     * @method setAutoScroll
     */
    setAutoScroll: Ext.emptyFn,
    applyScrollable: function(scrollable) {
        var view = this.view;
        view = view && (view.normalView || view);
        if (view) {
            view.setScrollable(scrollable);
        }
        // The view might not yet exists so we just stash the raw config away so it
        // can be processed by getView()
        return scrollable;
    },
    /**
     * @private
     * Processes UI events from the view. Propagates them to whatever internal Components
     * need to process them.
     * @param {String} type Event type, eg 'click'
     * @param {Ext.view.Table} view TableView Component
     * @param {HTMLElement} cell Cell HTMLElement the event took place within
     * @param {Number} recordIndex Index of the associated Store Model (-1 if none)
     * @param {Number} cellIndex Cell index within the row
     * @param {Ext.event.Event} e Original event
     * @param {Ext.data.Model} record
     * @param {Object} row
     */
    processEvent: function(type, view, cell, recordIndex, cellIndex, e, record, row) {
        var header = e.position.column;
        if (header) {
            return header.processEvent.apply(header, arguments);
        }
    },
    /**
     * Scrolls the specified record into view.
     * @param {Number/String/Ext.data.Model} record The record, record id, or the zero-based
     * position in the dataset to scroll to.
     * @param {Object} [options] An object containing options to modify the operation.
     * @param {Number/Ext.grid.column.Column} [options.column] The column to scroll into view.
     * @param {Boolean} [options.animate] Pass `true` to animate the row into view.
     * @param {Boolean} [options.highlight] Pass `true` to highlight the row with a glow animation
     * when it is in view.
     * @param {Boolean} [options.select] Pass as `true` to select the specified row.
     * @param {Boolean} [options.focus] Pass as `true` to focus the specified row.
     * @param {Function} [options.callback] A function to execute when the record is in view.
     * This may be necessary if the first parameter is a record index and the view is backed by a
     * {@link Ext.data.BufferedStore buffered store} which does not contain that record.
     * @param {Boolean} options.callback.success `true` if acquiring the record's view node
     * was successful.
     * @param {Ext.data.Model}  options.callback.record If successful, the target record.
     * @param {HTMLElement} options.callback.node If successful, the record's view node.
     * @param {Object} [options.scope] The scope (`this` reference) in which the callback function
     * is executed.
     */
    ensureVisible: function(record, options) {
        this.doEnsureVisible(record, options);
    },
    scrollByDeltaY: function(yDelta, animate) {
        // xDelta should be null here not 0! We're not scrolling horizontally,
        // and the Scroller is sensitive to these things.
        this.getView().scrollBy(null, yDelta, animate);
    },
    scrollByDeltaX: function(xDelta, animate) {
        // Ditto yDelta.
        this.getView().scrollBy(xDelta, null, animate);
    },
    afterCollapse: function() {
        this.saveScrollPos();
        this.callParent(arguments);
    },
    afterExpand: function() {
        this.callParent(arguments);
        this.restoreScrollPos();
    },
    saveScrollPos: Ext.emptyFn,
    restoreScrollPos: Ext.emptyFn,
    onHeaderResize: Ext.emptyFn,
    // Update the view when a header moves
    onHeaderMove: function(headerCt, header, colsToMove, fromIdx, toIdx) {
        var me = this;
        // If there are Features or Plugins which create DOM which must match column order,
        // they set the optimizedColumnMove flag to false.
        // In this case we must refresh the view on column move.
        if (me.optimizedColumnMove === false) {
            me.view.refreshView();
        } else // Simplest case for default DOM structure is just to swap the columns round in the view.
        {
            me.view.moveColumn(fromIdx, toIdx, colsToMove);
        }
        me.delayScroll();
    },
    // Section onHeaderHide is invoked after view.
    onHeaderHide: function(headerCt, header) {
        var view = this.view;
        // The headerCt may be hiding multiple children if a leaf level column
        // causes a parent (and possibly other parents) to be hidden. Only run the refresh
        // once we're done
        if (!headerCt.childHideCount && view.refreshCounter) {
            view.refreshView();
        }
    },
    onHeaderShow: function(headerCt, header) {
        var view = this.view;
        if (view.refreshCounter) {
            view.refreshView();
        }
    },
    // To be triggered on add/remove/move for a leaf header
    onHeadersChanged: function(headerCt, header) {
        var me = this;
        if (me.rendered && !me.reconfiguring) {
            me.view.refreshView();
            me.delayScroll();
        }
    },
    delayScroll: function() {
        var target = this.view;
        if (target) {
            // Do not cause a layout by reading scrollX now.
            // It must be read from the target when the task finally executes.
            this.scrollTask.delay(10, null, null, [
                target
            ]);
        }
    },
    /**
     * @private
     * Fires the TablePanel's viewready event when the view declares that its internal DOM is ready
     */
    onViewReady: function() {
        this.fireEvent('viewready', this);
    },
    /**
     * @private
     * Tracks when things happen to the view and preserves the horizontal scroll position.
     */
    onRestoreHorzScroll: function() {
        var me = this,
            x = me.scrollXPos;
        if (x) {
            // We need to restore the body scroll position here
            me.syncHorizontalScroll(me, true);
        }
    },
    getScrollerOwner: function() {
        var rootCmp = this;
        if (!this.scrollerOwner) {
            rootCmp = this.up('[scrollerOwner]');
        }
        return rootCmp;
    },
    /**
     * Gets left hand side marker for header resizing.
     * @private
     */
    getLhsMarker: function() {
        var me = this;
        return me.lhsMarker || (me.lhsMarker = Ext.DomHelper.append(me.el, {
            role: 'presentation',
            cls: me.resizeMarkerCls
        }, true));
    },
    /**
     * Gets right hand side marker for header resizing.
     * @private
     */
    getRhsMarker: function() {
        var me = this;
        return me.rhsMarker || (me.rhsMarker = Ext.DomHelper.append(me.el, {
            role: 'presentation',
            cls: me.resizeMarkerCls
        }, true));
    },
    /**
     * @method getSelection
     * Returns the grid's selection. See `{@link Ext.selection.Model#getSelection}`.
     * @inheritdoc Ext.selection.Model#getSelection
     */
    getSelection: function() {
        return this.getSelectionModel().getSelection();
    },
    /**
     * Sets the value of the selection.
     * @param {Ext.data.Model} selection
     */
    setSelection: function(selection) {
        // This is purposefully written not as a config. Because getSelection
        // is an existing API that doesn't mirror the value for setSelection, we
        // don't want the publish system to call the getter, but rather just the
        // raw property.
        var current = this.selection;
        if (selection !== current) {
            this.selection = selection;
            this.updateSelection(selection, current);
        }
    },
    updateSelection: function(selection) {
        var me = this,
            sm;
        if (!me.ignoreNextSelection) {
            me.ignoreNextSelection = true;
            sm = me.getSelectionModel();
            if (selection) {
                sm.select(selection);
            } else {
                sm.deselectAll();
            }
            me.ignoreNextSelection = false;
        }
        me.publishState('selection', selection);
    },
    updateBindSelection: function(selModel, selection) {
        var me = this,
            selected = null;
        if (!me.ignoreNextSelection) {
            me.ignoreNextSelection = true;
            if (selection.length) {
                selected = selModel.getLastSelected();
                me.hasHadSelection = true;
            }
            if (me.hasHadSelection) {
                me.setSelection(selected);
            }
            me.ignoreNextSelection = false;
        }
    },
    updateFocused: function(record) {
        this.getNavigationModel().setPosition(record);
    },
    updateHeaderBorders: function(headerBorders) {
        this[headerBorders ? 'removeCls' : 'addCls'](this.noHeaderBordersCls);
    },
    getNavigationModel: function() {
        return this.getView().getNavigationModel();
    },
    /**
     * Returns the selection model being used by this grid's {@link Ext.view.Table view}.
     * @return {Ext.selection.Model} The selection model being used by this grid's
     * {@link Ext.view.Table view}.
     */
    getSelectionModel: function() {
        return this.getView().getSelectionModel();
    },
    getScrollTarget: function() {
        var items = this.getScrollerOwner().query('tableview');
        // Last view has the scroller
        return items[items.length - 1];
    },
    syncHorizontalScroll: function(target, setBody) {
        var me = this,
            x = me.view.getScrollX(),
            scrollTarget;
        setBody = setBody === true;
        // Only set the horizontal scroll if we've changed position,
        // so that we don't set this on vertical scrolls
        if (me.rendered && (setBody || x !== me.scrollXPos)) {
            // Only set the body position if we're reacting to a refresh, otherwise
            // we just need to set the header.
            if (setBody) {
                scrollTarget = me.getScrollTarget();
                scrollTarget.setScrollX(x);
            }
            me.headerCt.setScrollX(x);
            me.scrollXPos = x;
        }
    },
    // template method meant to be overriden
    onStoreLoad: Ext.emptyFn,
    getEditorParent: function() {
        return this.body;
    },
    bindStore: function(store, initial) {
        var me = this,
            view = me.getView(),
            oldStore = me.getStore();
        // Normally, this method will always be called with a valid store (because there is
        // a symmetric .unbindStore method), but there are cases where this method will be called
        // and passed a null value, i.e., a panel is used as a pickerfield. See EXTJS-13089.
        if (store) {
            // Bind to store immediately because subsequent processing
            // looks for grid's store property
            me.store = store;
            if (view.store !== store) {
                // If coming from a reconfigure, we need to set the actual store property
                // on the view. Setting the store will then also set the dataSource.
                //
                // Note that if it's a grid feature then this is sorted out in view.bindStore(),
                // and its own implementation of .bindStore() will be called.
                view.bindStore(store, false);
            }
            me.mon(store, {
                load: me.onStoreLoad,
                scope: me
            });
            me.storeRelayers = me.relayEvents(store, [
                /**
                 * @event filterchange
                 * @inheritdoc Ext.data.Store#filterchange
                 */
                'filterchange',
                /**
                 * @event groupchange
                 * @inheritdoc Ext.data.Store#groupchange
                 */
                'groupchange'
            ]);
            // If this is being called from reconfigure then the storechange will be called
            // by the reconfigure machinery at the end of all processing. Otherwise, fire here.
            if (!me.reconfiguring && me.hasListeners.storechange && store !== oldStore) {
                me.fireEvent('storechange', me, store, oldStore);
            }
        } else {
            me.unbindStore();
        }
    },
    unbindStore: function() {
        var me = this,
            store = me.store,
            view;
        if (store) {
            store.trackStateChanges = false;
            me.store = null;
            me.mun(store, {
                load: me.onStoreLoad,
                scope: me
            });
            Ext.destroy(me.storeRelayers);
            view = me.view;
            if (view.store) {
                view.bindStore(null);
            } else if (!store.destroyed && store.autoDestroy) {
                store.destroy();
            }
            // If this is being called from reconfigure then the storechange will be called
            // by the reconfigure machinery at the end of all processing. Otherwise, fire here.
            if (!me.reconfiguring && me.hasListeners.storechange) {
                me.fireEvent('storechange', me, null, store);
            }
        }
    },
    setColumns: function(columns) {
        // If being reconfigured from zero columns to zero columns, skip operation.
        // This can happen if columns are being set from a binding and the initial value
        // of the bound data in the ViewModel is []
        if (columns.length || this.getColumnManager().getColumns().length) {
            this.reconfigure(undefined, columns);
        }
    },
    /**
     * A convenience method that fires {@link #event-reconfigure} with the store param.
     * To set the store AND change columns, use the {@link #method-reconfigure reconfigure method}.
     *
     * @param {Ext.data.Store} [store] The new store.
     */
    setStore: function(store) {
        var me = this;
        me.reconfigure(store, undefined, true);
        // If we are visible, load the store
        if (me.isVisible(true)) {
            if (store && me.autoLoad && !store.isEmptyStore && !(store.loading || store.isLoaded())) {
                store.load();
            }
        }
        // Otherwise, ensure that we will load as soon as we become visible
        else if (!me.globalShowListener) {
            me.globalShowListener = Ext.GlobalEvents.on({
                show: me.onGlobalShow,
                scope: me,
                destroyable: true
            });
        }
    },
    onGlobalShow: function(comp) {
        var me = this,
            store = me.store;
        // If the global show caused this to be shown, then load
        // unless there's already a locked kicked off.
        if (comp === me || (comp.isAncestor(me) && me.isVisible(true))) {
            if (store && me.autoLoad && !store.isEmptyStore && !(store.loading || store.isLoaded())) {
                store.load();
            }
            Ext.destroy(me.globalShowListener);
        }
    },
    /**
     * Reconfigures the grid or tree with a new store and/or columns. Stores and columns 
     * may also be passed as params.
     *
     *     grid.reconfigure(store, columns);
     *
     * Additionally, you can pass just a store or columns.
     *
     *     tree.reconfigure(store);
     *     // or
     *     grid.reconfigure(columns);
     *     // or
     *     tree.reconfigure(null, columns);
     *
     * If you're using locked columns, the {@link #enableLocking} config should be set 
     * to `true` before the reconfigure method is executed.
     *
     * @param {Ext.data.Store/Object} [store] The new store instance or store config. You can 
     * pass `null` if no new store.
     * @param {Object[]} [columns] An array of column configs
     * @param {Boolean} allowUnbind (private)
     */
    reconfigure: function(store, columns, allowUnbind) {
        var me = this,
            oldStore = me.store,
            headerCt = me.headerCt,
            lockable = me.lockable,
            oldColumns = headerCt ? headerCt.items.getRange() : me.columns,
            view = me.getView(),
            scroller, block, refreshCounter, storeChanged, columnsChanged, state, stateId, restoreFocus;
        // Allow optional store argument to be fully omitted, and the columns argument to be solo
        if (arguments.length === 1 && Ext.isArray(store)) {
            columns = store;
            store = null;
        }
        // Make copy in case the beforereconfigure listener mutates it.
        if (columns) {
            columns = Ext.Array.slice(columns);
        }
        me.reconfiguring = true;
        if (store) {
            store = Ext.StoreManager.lookup(store);
            storeChanged = store && store !== oldStore;
        }
        // Allow for nulling the store (convert to the empty store)
        else if (allowUnbind) {
            store = Ext.StoreManager.lookup('ext-empty-store');
            storeChanged = store !== oldStore;
        }
        me.fireEvent('beforereconfigure', me, store, columns, oldStore, oldColumns);
        Ext.suspendLayouts();
        if (me.rendered && me.layoutCounter && (scroller = me.getScrollable())) {
            scroller.scrollTo(0, 0);
        }
        if (lockable) {
            me.reconfigureLockable(store, columns, allowUnbind);
        } else {
            // Prevent the view from refreshing until we have resumed layouts
            // and any columns are rendered
            block = view.blockRefresh;
            view.blockRefresh = true;
            restoreFocus = view.saveFocusState();
            // Note that we need to process the store first in case one or more passed columns
            // (if there are any) have active gridfilters with values which would filter
            // the currently-bound store.
            if (storeChanged) {
                me.unbindStore();
                me.bindStore(store);
            }
            if (columns) {
                // new columns, delete scroll pos
                delete me.scrollXPos;
                headerCt.removeAll();
                headerCt.add(columns);
                columnsChanged = true;
            }
            headerCt.onOwnerGridReconfigure(storeChanged, columnsChanged);
            refreshCounter = view.refreshCounter;
        }
        if (me.stateful) {
            stateId = me.getStateId();
            state = stateId && Ext.state.Manager.get(stateId);
            if (state) {
                me.applyState(state);
            }
        }
        Ext.resumeLayouts(true);
        me.reconfiguring = false;
        if (lockable) {
            me.afterReconfigureLockable();
        } else {
            view.blockRefresh = block;
            // If the layout resumption didn't trigger the view to refresh, do it here
            if (view.refreshCounter === refreshCounter) {
                view.refreshView();
                restoreFocus();
            }
        }
        me.fireEvent('reconfigure', me, store, columns, oldStore, oldColumns);
        delete me.reconfiguring;
        if (storeChanged) {
            me.fireEvent('storechange', me, store, oldStore);
            if (!oldStore.destroyed && oldStore.autoDestroy) {
                oldStore.destroy();
            }
        }
    },
    doDestroy: function() {
        var me = this,
            task = me.scrollTask,
            view = me.view;
        if (view) {
            view.destroying = true;
        }
        if (me.lockable) {
            me.destroyLockable();
        }
        if (task) {
            task.cancel();
        }
        // Need to destroy plugins here because they may have listeners on the View
        Ext.destroy(me.rowContextParent, me.plugins, me.focusEnterLeaveListeners, me.freeRowContents, Ext.Object.getValues(me.liveRowContexts), me.lhsMarker, me.rhsMarker);
        me.callParent();
        // Have to unbind the store this late because plugins and other things
        // may still need it until the very end.
        me.unbindStore();
    },
    getElementHeight: function(el) {
        var rect = this.preciseHeight && el.getBoundingClientRect();
        return rect ? (rect.height || (rect.bottom - rect.top)) : el.offsetHeight;
    },
    getElementSize: function(el) {
        var rect = this.preciseHeight && el.getBoundingClientRect();
        return {
            width: rect ? (rect.width || (rect.right - rect.left)) : el.offsetWidth,
            height: rect ? (rect.height || (rect.bottom - rect.top)) : el.offsetHeight
        };
    },
    privates: {
        // The focusable flag is set, but there is no focusable element.
        // Focus is delegated to the view by the focus implementation.
        initFocusableElement: function() {},
        doEnsureVisible: function(record, options) {
            // Handle the case where this is a lockable assembly
            if (this.lockable) {
                return this.ensureLockedVisible(record, options);
            }
            // Allow them to pass the record id.
            if (typeof record !== 'number' && !record.isEntity) {
                record = this.store.getById(record);
            }
            // eslint-disable-next-line vars-on-top
            var me = this,
                view = me.getView(),
                domNode = view.getNode(record),
                isLocking = me.ownerGrid.lockable,
                callback, scope, animate, highlight, select, doFocus, verticalScroller, column, cell, targetContext, internalCallback, scrollPromise;
            if (options) {
                callback = options.callback;
                scope = options.scope;
                animate = options.animate;
                highlight = options.highlight;
                select = options.select;
                doFocus = options.focus;
                column = options.column;
            }
            // Always supercede any prior deferred request
            if (me.deferredEnsureVisible) {
                me.deferredEnsureVisible.destroy();
            }
            // We have not yet run the layout.
            // Add this to the end of the first sizing process.
            // By using the resize event, we will come in AFTER any Component's onResize
            // and onBoxReady handling.
            if (!view.componentLayoutCounter) {
                me.deferredEnsureVisible = view.on({
                    resize: me.doEnsureVisible,
                    args: Ext.Array.slice(arguments),
                    scope: me,
                    single: true,
                    destroyable: true
                });
                return;
            }
            if (typeof column === 'number') {
                column = me.ownerGrid.getVisibleColumnManager().getColumns()[column];
            }
            // We found the DOM node associated with the record
            if (domNode) {
                if (!record.isEntity) {
                    record = view.getRecord(domNode);
                }
                verticalScroller = isLocking ? me.ownerGrid.getScrollable() : view.getScrollable();
                // Scrolling *may* be asynchronous if animation is used, so post-process
                // the target node in a callback.
                if (callback || select || doFocus) {
                    internalCallback = function() {
                        targetContext = new Ext.grid.CellContext(view).setPosition(record, column || 0);
                        if (select) {
                            view.getSelectionModel().selectByPosition(targetContext);
                        }
                        if (doFocus) {
                            view.getNavigationModel().setPosition(targetContext);
                        }
                        Ext.callback(callback, scope || me, [
                            true,
                            record,
                            domNode
                        ]);
                    };
                }
                if (verticalScroller) {
                    if (column) {
                        cell = Ext.fly(domNode).selectNode(column.getCellSelector());
                    }
                    // We're going to need two scrollers if we are locking, and we need
                    // to scroll horizontally. The whole arrangement of side by side views
                    // scrolls up and down. Each view itself scrolls horizontally.
                    if (isLocking && column) {
                        verticalScroller.ensureVisible(domNode, {
                            x: false
                        });
                        scrollPromise = view.getScrollable().ensureVisible(cell || domNode, {
                            animation: animate,
                            highlight: highlight
                        });
                    } else // No locking, it's simple - we just use the view's scroller
                    {
                        scrollPromise = verticalScroller.ensureVisible(cell || domNode, {
                            animation: animate,
                            highlight: highlight,
                            x: !!column
                        });
                    }
                    if (scrollPromise && internalCallback) {
                        scrollPromise.then(internalCallback);
                    }
                }
            }
            // If we didn't find it, it's probably because of buffered rendering
            else if (view.bufferedRenderer) {
                view.bufferedRenderer.scrollTo(record, {
                    animate: animate,
                    highlight: highlight,
                    select: select,
                    focus: doFocus,
                    column: column,
                    callback: function(recordIdx, record, domNode) {
                        Ext.callback(callback, scope || me, [
                            true,
                            record,
                            domNode
                        ]);
                    }
                });
            } else {
                Ext.callback(callback, scope || me, [
                    false,
                    null
                ]);
            }
        },
        getFocusEl: function() {
            return this.getView().getFocusEl();
        },
        /**
         * Provide a single parent viewmodel for the grid so that any VM for
         * row contents share the same scheduler.
         * @return {Ext.app.ViewModel}
         *
         * @private
         */
        getRowContextViewModelParent: function() {
            var vm = this.lookupViewModel() || this.rowContextParent;
            if (!vm) {
                // If we get to this point, it means that there's no parent VM above us
                // so we have nothing to hook up to
                this.rowContextParent = vm = new Ext.app.ViewModel();
            }
            return vm;
        },
        handleWidgetViewChange: function(view, ownerId) {
            var contexts = this.liveRowContexts,
                freeRowContexts = this.freeRowContexts,
                len = freeRowContexts && freeRowContexts.length,
                i, recInternalId;
            for (recInternalId in contexts) {
                contexts[recInternalId].handleWidgetViewChange(view, ownerId);
            }
            for (i = 0; i < len; i++) {
                freeRowContexts[i].handleWidgetViewChange(view, ownerId);
            }
        },
        initInheritedState: function(inheritedState, inheritedStateInner) {
            inheritedState.inLockedGrid = !!this.isLocked;
            this.callParent([
                inheritedState,
                inheritedStateInner
            ]);
        },
        /**
         * Toggles ARIA actionable mode on/off
         * @param {Boolean} enabled
         * @param {Ext.grid.CellContext} position The cell to activate.
         * @param {HTMLElement/Ext.dom.Element} [position.target] The element within
         * the referenced cell to focus.
         * @return {Boolean} `true` if actionable mode was entered
         * @private
         */
        setActionableMode: function(enabled, position) {
            // Always set the topmost grid in a lockable assembly
            var me = this.ownerGrid;
            // Can be called to exit actionable mode upon a focusLeave caused by destruction
            if (!me.destroying && me.view.setActionableMode(enabled, position) !== false) {
                me.fireEvent('actionablemodechange', enabled);
                me[enabled ? 'addCls' : 'removeCls'](me.actionableModeCls);
                return true;
            }
        },
        /**
         * Override for TablePanel.
         * A TablePanel can never scroll. Its View scrolls.
         * @private
         */
        getOverflowStyle: function() {
            // eslint-disable-next-line dot-notation
            this.scrollFlags = this._scrollFlags['false']['false'];
            return {
                overflowX: 'hidden',
                overflowY: 'hidden'
            };
        },
        getOverflowEl: function() {
            return null;
        },
        shouldAutoHideHeaders: function() {
            var me = this,
                columns = me.headerCt.items.items,
                len = columns.length,
                autoHideHeaders = !!len,
                column, i;
            // Loop until we find a column with content.
            for (i = 0; autoHideHeaders && i < len; i++) {
                column = columns[i];
                // If any column was configured with visible text, we must show headers.
                if (!column.isEmptyText(column.text, true) || column.columns || (column.isGroupHeader && column.items.items.length)) {
                    autoHideHeaders = false;
                }
            }
            return autoHideHeaders;
        },
        syncHeaderVisibility: function() {
            var me = this,
                headerCt = me.headerCt,
                hideHeaders = me.hideHeaders,
                viewScroller, currentHideHeaderState;
            if (me.lockable) {
                me.syncLockableHeaderVisibility();
                return;
            }
            if (hideHeaders == null) {
                hideHeaders = me.shouldAutoHideHeaders();
            }
            currentHideHeaderState = headerCt.height === 0;
            if (!headerCt.rendered || hideHeaders !== currentHideHeaderState) {
                headerCt.setHeight(hideHeaders ? 0 : null);
                headerCt.hiddenHeaders = hideHeaders;
                headerCt.toggleCls(me.hiddenHeaderCtCls, hideHeaders);
                me.toggleCls(me.hiddenHeaderCls, hideHeaders);
                if (!hideHeaders) {
                    headerCt.setScrollable({
                        x: false,
                        y: false
                    });
                    viewScroller = me.view.getScrollable();
                    if (viewScroller) {
                        headerCt.getScrollable().addPartner(viewScroller, 'x');
                    }
                }
            }
        }
    }
});

/**
 * @private
 *
 * This class is used only by the grid's HeaderContainer docked child.
 *
 * It adds the ability to shrink the vertical size of the inner container element back if a grouped
 * column header has all its child columns dragged out, and the whole HeaderContainer needs
 * to shrink back down.
 *
 * Also, after every layout, after all headers have attained their 'stretchmax' height,
 * it goes through and calls `setPadding` on the columns so that they lay out correctly.
 */
Ext.define('Ext.grid.ColumnLayout', {
    extend: 'Ext.layout.container.HBox',
    alias: 'layout.gridcolumn',
    type: 'gridcolumn',
    requires: [
        'Ext.panel.Table'
    ],
    firstHeaderCls: Ext.baseCSSPrefix + 'column-header-first',
    lastHeaderCls: Ext.baseCSSPrefix + 'column-header-last',
    initLayout: function() {
        this.callParent();
        if (this.scrollbarWidth === undefined) {
            this.self.prototype.scrollbarWidth = Ext.scrollbar.width();
        }
    },
    beginLayout: function(ownerContext) {
        var me = this,
            owner = me.owner,
            firstCls = me.firstHeaderCls,
            lastCls = me.lastHeaderCls,
            bothCls = [
                firstCls,
                lastCls
            ],
            items = me.getVisibleItems(),
            len = items.length,
            i, item;
        me.callParent([
            ownerContext
        ]);
        // Sync the first/lastCls states for all the headers.
        for (i = 0; i < len; i++) {
            item = items[i];
            if (len === 1) {
                // item is the only item so it is both first and last
                item.addCls(bothCls);
            } else if (i === 0) {
                // item is the first of 2+ items
                item.addCls(firstCls);
                item.removeCls(lastCls);
            } else if (i === len - 1) {
                // item is the last of 2+ items
                item.removeCls(firstCls);
                item.addCls(lastCls);
            } else {
                item.removeCls(bothCls);
            }
        }
        // Start this at 0 and for the root headerCt call determineScrollbarWidth to get
        // it set properly. Typically that amounts to a "delete" to expose the system's
        // scrollbar width stored on our prototype.
        me.scrollbarWidth = 0;
        if (owner.isRootHeader && !owner.grid.isLocked) {
            // In a locking grid, the scrollbar is only managed on the normal side.
            me.determineScrollbarWidth(ownerContext);
        }
        if (!me.scrollbarWidth) {
            // By default Mac OS X has overlay scrollbars that do not take space, but also
            // the RTL override may have set this to 0... so make sure we don't try to
            // compensate for a scrollbar when there isn't one.
            ownerContext.manageScrollbar = false;
        }
    },
    moveItemBefore: function(item, before) {
        var prevOwner = item.ownerCt,
            nextSibling = before && before.nextSibling();
        // Due to the nature of grid headers, index calculation for
        // moving items is complicated, especially since removals can trigger
        // groups to be removed (and thus alter indexes). As such, the logic
        // is simplified by removing the item first, then calculating the index
        // and inserting it.
        // When removing from previous container ensure the header is not destroyed
        // or removed from the DOM (which would destroy focus).
        // The layout's moveItem method will preserve focus when it does the move.
        if (item !== before && prevOwner) {
            prevOwner.remove(item, {
                destroy: false,
                detach: false
            });
            // If the removal caused destruction of the before, this was
            // the last subheader, so move to beore its next sibling
            if (before && before.destroyed) {
                before = nextSibling;
            }
        }
        return this.callParent([
            item,
            before
        ]);
    },
    determineScrollbarWidth: function(ownerContext) {
        var me = this,
            owner = me.owner,
            grid = owner.grid,
            // We read this value off of the immediate grid since the locked side of a
            // locking grid will not have this set. The ownerGrid in that case would have
            // it set but will pass along true only to the normal side.
            reserveScrollbar = grid.reserveScrollbar,
            scrollable = grid.view.getScrollable(),
            manageScrollbar = !reserveScrollbar && scrollable && scrollable.getY();
        // If we have reserveScrollbar then we will always have a vertical scrollbar so
        // manageScrollbar should be false. Otherwise it is based on overflow-y:
        ownerContext.manageScrollbar = manageScrollbar;
        // Determine if there is any need to deal with the width of the vertical scrollbar
        // and set "scrollbarWidth" to 0 if not or the system determined value (stored on
        // our prototype).
        //
        if (!grid.ownerGrid.collapsed && (reserveScrollbar || manageScrollbar)) {
            // Ensure the real scrollbarWidth value is exposed from the prototype. This
            // may be needed if the scrollFlags have changed since we may have a 0 set on
            // this instance from a previous layout run.
            delete me.scrollbarWidth;
        }
    },
    // On return, the RTL override (Ext.rtl.grid.ColumnLayout) will deal with various
    // browser bugs and may set me.scrollbarWidth to 0 or a negative value.
    calculate: function(ownerContext) {
        var me = this,
            owner = me.owner,
            grid = owner.grid,
            // Our TableLayout buddy sets this in its beginLayout so we can work this
            // out together:
            viewContext = ownerContext.viewContext,
            state = ownerContext.state,
            context = ownerContext.context,
            lockingPartnerContext, columnsChanged, columns, len, i, column, scrollbarAdjustment, viewOverflowY;
        me.callParent([
            ownerContext
        ]);
        if (grid && owner.isRootHeader && state.parallelDone) {
            lockingPartnerContext = viewContext.lockingPartnerContext;
            // A force-fit needs to be "reflexed" so check that now. If we have to reflex
            // the items, we need to re-cacheFlexes and invalidate ourselves.
            if (grid.forceFit && !state.reflexed) {
                if (me.convertWidthsToFlexes(ownerContext)) {
                    me.cacheFlexes(ownerContext);
                    me.done = false;
                    ownerContext.invalidate({
                        state: {
                            reflexed: true,
                            scrollbarAdjustment: me.getScrollbarAdjustment(ownerContext)
                        }
                    });
                    return;
                }
            }
            // Once the parallelDone flag goes up, we need to pack up the changed column
            // widths for our TableLayout partner.
            if ((columnsChanged = state.columnsChanged) === undefined) {
                columns = ownerContext.target.getVisibleGridColumns();
                columnsChanged = false;
                for (i = 0 , len = columns.length; i < len; i++) {
                    column = context.getCmp(columns[i]);
                    // Since we are parallelDone, all of the children should have width,
                    // so we can
                    if (!column.lastBox || column.props.width !== column.lastBox.width) {
                        (columnsChanged || (columnsChanged = []))[i] = column;
                    }
                }
                state.columnsChanged = columnsChanged;
                // This will trigger our TableLayout partner and allow it to proceed.
                ownerContext.setProp('columnsChanged', columnsChanged);
            }
            if (ownerContext.manageScrollbar) {
                // If we changed the column widths, we need to wait for the TableLayout to
                // return whether or not we have overflowY... well, that is, if we are
                // needing to tweak the scrollbarAdjustment...
                scrollbarAdjustment = me.getScrollbarAdjustment(ownerContext);
                if (scrollbarAdjustment) {
                    // Since we start with the assumption that we will need the scrollbar,
                    // we now need to wait to see if our guess was correct.
                    viewOverflowY = viewContext.getProp('viewOverflowY');
                    if (viewOverflowY === undefined) {
                        // The TableLayout has not determined this yet, so park it.
                        me.done = false;
                        return;
                    }
                    if (!viewOverflowY) {
                        // We have our answer, and it turns out the view did not overflow
                        // (even with the reduced width we gave it), so we need to remove
                        // the scrollbarAdjustment and go again.
                        if (lockingPartnerContext) {
                            // In a locking grid, only the normal side plays this game,
                            // so now that we know the resolution, we need to invalidate
                            // the locking view and its headerCt.
                            lockingPartnerContext.invalidate();
                            lockingPartnerContext.headerContext.invalidate();
                        }
                        viewContext.invalidate();
                        ownerContext.invalidate({
                            state: {
                                // Pass a 0 adjustment on into our next life. If this is
                                // the invalidate that resets ownerContext then this is
                                // put onto the new state. If not, it will reset back to
                                // undefined and we'll have to begin again (which is the
                                // correct thing to do in that case).
                                scrollbarAdjustment: 0
                            }
                        });
                    }
                }
            }
        }
    },
    // else {
    // We originally assumed we would need the scrollbar and since we do
    // not now, we must be on the second pass, so we can move on...
    // }
    finishedLayout: function(ownerContext) {
        this.callParent([
            ownerContext
        ]);
        if (this.owner.ariaRole === 'rowgroup') {
            this.innerCt.dom.setAttribute('role', 'row');
        }
        // Wipe this array because it holds component references and gets cached on the object
        // Can cause a circular reference
        ownerContext.props.columnsChanged = null;
    },
    convertWidthsToFlexes: function(ownerContext) {
        var me = this,
            totalWidth = 0,
            calculated = me.sizeModels.calculated,
            childItems, len, i, childContext, item;
        childItems = ownerContext.childItems;
        len = childItems.length;
        for (i = 0; i < len; i++) {
            childContext = childItems[i];
            item = childContext.target;
            totalWidth += childContext.props.width;
            // Only allow to be flexed if it's a resizable column
            if (!(item.fixed || item.resizable === false)) {
                // For forceFit, just use allocated width as the flex value, and the proportions
                // will end up the same whatever HeaderContainer width they are being forced into.
                item.flex = ownerContext.childItems[i].flex = childContext.props.width;
                item.width = null;
                childContext.widthModel = calculated;
            }
        }
        // Only need to loop back if the total column width is not already an exact fit
        return totalWidth !== ownerContext.props.width;
    },
    getScrollbarAdjustment: function(ownerContext) {
        var me = this,
            state = ownerContext.state,
            grid = me.owner.grid,
            scrollbarAdjustment = state.scrollbarAdjustment;
        // If there is potential for a vertical scrollbar, then we start by assuming
        // we will need to reserve space for it. Unless, of course, there are no
        // records!
        if (scrollbarAdjustment === undefined) {
            scrollbarAdjustment = 0;
            if (grid.reserveScrollbar || (ownerContext.manageScrollbar && !grid.ownerGrid.getSizeModel().height.shrinkWrap)) {
                scrollbarAdjustment = me.scrollbarWidth;
            }
            state.scrollbarAdjustment = scrollbarAdjustment;
        }
        return scrollbarAdjustment;
    },
    /**
     * @private
     * Local getContainerSize implementation accounts for vertical scrollbar in the view.
     */
    getContainerSize: function(ownerContext) {
        var me = this,
            got, needed, padding, gotWidth, gotHeight, width, height, result;
        if (me.owner.isRootHeader) {
            result = me.callParent([
                ownerContext
            ]);
            if (result.gotWidth) {
                result.width -= me.getScrollbarAdjustment(ownerContext);
            }
        } else {
            padding = ownerContext.paddingContext.getPaddingInfo();
            got = needed = 0;
            // The container size here has to be provided by the ColumnComponentLayout to
            // account for borders in its odd way.
            if (!ownerContext.widthModel.shrinkWrap) {
                ++needed;
                width = ownerContext.getProp('innerWidth');
                gotWidth = (typeof width === 'number');
                if (gotWidth) {
                    ++got;
                    width -= padding.width;
                    if (width < 0) {
                        width = 0;
                    }
                }
            }
            if (!ownerContext.heightModel.shrinkWrap) {
                ++needed;
                height = ownerContext.getProp('innerHeight');
                gotHeight = (typeof height === 'number');
                if (gotHeight) {
                    ++got;
                    height -= padding.height;
                    if (height < 0) {
                        height = 0;
                    }
                }
            }
            return {
                width: width,
                height: height,
                needed: needed,
                got: got,
                gotAll: got === needed,
                gotWidth: gotWidth,
                gotHeight: gotHeight
            };
        }
        return result;
    },
    publishInnerCtSize: function(ownerContext) {
        var me = this,
            owner = me.owner,
            cw = ownerContext.peek('contentWidth'),
            adjustment = 0;
        // Pass negative "reservedSpace", so that the innerCt gets *extra* size
        // to accommodate the view's vertical scrollbar
        if (cw != null && owner.isRootHeader) {
            adjustment = -ownerContext.state.scrollbarAdjustment;
        }
        return me.callParent([
            ownerContext,
            adjustment
        ]);
    },
    roundFlex: function(width) {
        return Math.round(width);
    }
});

/**
 * @private
 *
 * Manages and provides information about a TablePanel's *visible leaf* columns.
 */
Ext.define('Ext.grid.ColumnManager', {
    alternateClassName: [
        'Ext.grid.ColumnModel'
    ],
    columns: null,
    constructor: function(visibleOnly, headerCt, secondHeaderCt) {
        if (!headerCt.isRootHeader && !headerCt.isGroupHeader) {
            Ext.raise('ColumnManager must be passed an instantiated HeaderContainer ' + 'or group header');
        }
        this.headerCt = headerCt;
        // We are managing columns for a lockable grid...
        if (secondHeaderCt) {
            if (!headerCt.isRootHeader && !headerCt.isGroupHeader) {
                Ext.raise('ColumnManager must be passed an instantiated HeaderContainer ' + 'or group header');
            }
            this.secondHeaderCt = secondHeaderCt;
        }
        this.visibleOnly = !!visibleOnly;
    },
    getColumns: function() {
        if (!this.columns) {
            this.cacheColumns();
        }
        return this.columns;
    },
    hasVariableRowHeight: function() {
        var me = this,
            columns = me.getColumns(),
            len = columns.length,
            i;
        if (me.variableRowHeight == null) {
            me.variableRowHeight = false;
            for (i = 0; !me.variableRowHeight && i < len; i++) {
                me.variableRowHeight = !!columns[i].variableRowHeight;
            }
        }
        return me.variableRowHeight;
    },
    /**
     * If called from a root header, returns the index of a leaf level header regardless of what
     * the nesting structure is.
     *
     * If called from a group header, returns the index of a leaf level header relative
     * to the group header.
     *
     * If a group header is passed, the index of the first leaf level header within it is returned.
     *
     * @param {Ext.grid.column.Column} header The header to find the index of
     * @return {Number} The index of the specified column header
     */
    getHeaderIndex: function(header) {
        if (header.isGroupHeader) {
            // Get the first header for the particular group header. The .getHeaderColumns API
            // will sort out if it's to be just visible columns or all columns.
            header = this.getHeaderColumns(header)[0];
        }
        return Ext.Array.indexOf(this.getColumns(), header);
    },
    /**
     * If called from a root header, gets a leaf level header by index regardless of what
     * the nesting structure is.
     *
     * If called from a group header, returns the index of a leaf level header relative
     * to the group header.
     *
     * @param {Number} index The column index for which to retrieve the column.
     * @return {Ext.grid.column.Column} The header. `null` if it doesn't exist.
     */
    getHeaderAtIndex: function(index) {
        var columns = this.getColumns(),
            col = columns[index];
        return col || null;
    },
    getPreviousSibling: function(header) {
        var index = this.getHeaderIndex(header),
            col = null;
        if (index > 0) {
            col = this.getColumns()[index - 1];
        }
        return col;
    },
    getNextSibling: function(header) {
        var index = this.getHeaderIndex(header),
            col;
        if (index !== -1) {
            col = this.getColumns()[index + 1];
        }
        return col || null;
    },
    /**
     * Get the first column.
     * @return {Ext.grid.column.Column} The header. `null` if it doesn't exist
     */
    getFirst: function() {
        var columns = this.getColumns();
        return columns.length > 0 ? columns[0] : null;
    },
    /**
     * Get the last column.
     * @return {Ext.grid.column.Column} The header. `null` if it doesn't exist
     */
    getLast: function() {
        var columns = this.getColumns(),
            len = columns.length;
        return len > 0 ? columns[len - 1] : null;
    },
    /**
     * Get a leaf level header by data index regardless of what the nesting
     * structure is.
     * @param {String} dataIndex The data index
     * @return {Ext.grid.column.Column} The header. `null` if it doesn't exist.
     */
    getHeaderByDataIndex: function(dataIndex) {
        var columns = this.getColumns(),
            len = columns.length,
            i, header;
        // don't match on ambiguous empty or null values (e.g., template columns)
        if (Ext.isEmpty(dataIndex)) {
            return null;
        }
        for (i = 0; i < len; ++i) {
            header = columns[i];
            if (header.dataIndex === dataIndex) {
                return header;
            }
        }
        return null;
    },
    /**
     * Get a leaf level header by index regardless of what the nesting
     * structure is.
     * @param {String} id The id
     * @return {Ext.grid.column.Column} The header. `null` if it doesn't exist.
     */
    getHeaderById: function(id) {
        var columns = this.getColumns(),
            len = columns.length,
            i, header;
        for (i = 0; i < len; ++i) {
            header = columns[i];
            if (header.getItemId() === id) {
                return header;
            }
        }
        return null;
    },
    /**
     * When passed a column index, returns the closet *visible* column to that. If the column
     * at the passed index is visible, that is returned. If it is hidden, either the next visible,
     * or the previous visible column is returned.
     *
     * If called from a group header, returns the visible index of a leaf level header
     * relative to the group header with the same stipulations as outlined above.
     *
     * @param {Number} index Position at which to find the closest visible column.
     */
    getVisibleHeaderClosestToIndex: function(index) {
        var result = this.getHeaderAtIndex(index);
        if (result && result.hidden) {
            result = result.next(':not([hidden])') || result.prev(':not([hidden])');
        }
        return result;
    },
    cacheColumns: function() {
        var columns = this.getHeaderColumns(this.headerCt),
            second = this.secondHeaderCt;
        if (second) {
            columns = columns.concat(this.getHeaderColumns(second));
        }
        this.columns = columns;
    },
    getHeaderColumns: function(header) {
        var result = this.visibleOnly ? header.getVisibleGridColumns() : header.getGridColumns();
        return Ext.Array.clone(result);
    },
    invalidate: function() {
        var root = this.rootColumns;
        this.columns = this.variableRowHeight = null;
        // If we are part of a lockable assembly, invalidate the root column manager
        if (root) {
            root.invalidate();
        }
    },
    destroy: function() {
        this.columns = this.rootColumns = null;
        this.callParent();
    }
}, function() {
    this.createAlias('indexOf', 'getHeaderIndex');
});

// TODO: Implement http://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#grid standards
/**
 * @class Ext.grid.NavigationModel
 * @private
 * This class listens for key events fired from a {@link Ext.grid.Panel GridPanel},
 * and moves the currently focused item by adding the class {@link #focusCls}.
 */
Ext.define('Ext.grid.NavigationModel', {
    extend: 'Ext.view.NavigationModel',
    alias: 'view.navigation.grid',
    /**
     * @event navigate Fired when a key has been used to navigate around the view.
     * @param {Object} event
     * @param {Ext.event.Event} event.keyEvent The key event which caused the navigation.
     * @param {Number} event.previousRecordIndex The previously focused record index.
     * @param {Ext.data.Model} event.previousRecord The previously focused record.
     * @param {HTMLElement} event.previousItem The previously focused grid cell.
     * @param {Ext.grid.Column} event.previousColumn The previously focused grid column.
     * @param {Number} event.recordIndex The newly focused record index.
     * @param {Ext.data.Model} event.record the newly focused record.
     * @param {HTMLElement} event.item the newly focused grid cell.
     * @param {Ext.grid.Column} event.column The newly focused grid column.
     */
    /**
     * @event cellactivate Fired when a cell is activated while in actionable mode
     * @param {Ext.grid.Panel} grid The grid panel that has the cell activated
     * @param {Ext.grid.CellContext} position The position in the grid that was activated
     * @param {Object} event
     */
    focusCls: Ext.baseCSSPrefix + 'grid-item-focused',
    getViewListeners: function() {
        var me = this;
        return {
            focusmove: {
                element: 'el',
                fn: me.onFocusMove
            },
            containermousedown: me.onContainerMouseDown,
            cellmousedown: me.onCellMouseDown,
            // We focus on click if the mousedown handler did not focus
            // because it was a translated "touchstart" event.
            cellclick: me.onCellClick,
            itemmousedown: me.onItemMouseDown,
            // We focus on click if the mousedown handler did not focus because
            // it was a translated "touchstart" event.
            itemclick: me.onItemClick,
            itemcontextmenu: me.onItemClick,
            scope: me
        };
    },
    initKeyNav: function(view) {
        var me = this,
            nav;
        // We will have two keyNavs if we are the navigation model for a lockable assembly
        if (!me.keyNav) {
            me.keyNav = [];
            me.position = new Ext.grid.CellContext(view);
        }
        // Drive the KeyNav off the View's itemkeydown event so that beforeitemkeydown listeners
        // may veto. By default KeyNav uses defaultEventAction: 'stopEvent', and this is required
        // for movement keys which by default affect scrolling.
        nav = new Ext.util.KeyNav({
            target: view,
            ignoreInputFields: true,
            // Must use the same event that form fields use to detect keystrokes.
            // keypress happens *after* keydown, but the framework must see key events
            // in bubble sequence order
            // So a field in actionable mode must see its key event before this nav model.
            eventName: Ext.supports.SpecialKeyDownRepeat ? 'itemkeydown' : 'itemkeypress',
            defaultEventAction: 'stopEvent',
            processEvent: me.processViewEvent,
            up: me.onKeyUp,
            down: me.onKeyDown,
            right: me.onKeyRight,
            left: me.onKeyLeft,
            pageDown: me.onKeyPageDown,
            pageUp: me.onKeyPageUp,
            home: me.onKeyHome,
            end: me.onKeyEnd,
            space: me.onKeySpace,
            enter: me.onKeyEnter,
            esc: me.onKeyEsc,
            113: {
                // Actionable mode is triggered by F2 key with no modifiers
                ctrl: false,
                shift: false,
                alt: false,
                handler: me.onKeyF2
            },
            tab: me.onKeyTab,
            A: {
                ctrl: true,
                // Need a separate function because we don't want the key
                // events passed on to selectAll (causes event suppression).
                handler: me.onSelectAllKeyPress
            },
            scope: me
        });
        me.keyNav.push(nav);
        me.onKeyNavCreate(nav);
    },
    onKeyNavCreate: Ext.emptyFn,
    addKeyBindings: function(binding) {
        var len = this.keyNav.length,
            i;
        // We will have two keyNavs if we are the navigation model for a lockable assembly
        for (i = 0; i < len; i++) {
            this.keyNav[i].addBindings(binding);
        }
    },
    enable: function() {
        var len = this.keyNav.length,
            i;
        // We will have two keyNavs if we are the navigation model for a lockable assembly
        for (i = 0; i < len; i++) {
            this.keyNav[i].enable();
        }
        this.disabled = false;
    },
    disable: function() {
        var len = this.keyNav.length,
            i;
        // We will have two keyNavs if we are the navigation model for a lockable assembly
        for (i = 0; i < len; i++) {
            this.keyNav[i].disable();
        }
        this.disabled = true;
    },
    /**
     * @private
     * Every key event is tagged with the source view, so the NavigationModel is independent.
     * Called in the scope of the KeyNav. This function is injected into the NavigationModel's
     * {@link Ext.util.KeyNav KeyNav) as its {@link Ext.util.KeyNav#processEvent processEvent}
     * config.
     */
    processViewEvent: function(view, record, row, recordIndex, event) {
        var key = event.getKey();
        // In actionable mode, we only listen for TAB, F2 and ESC to exit actionable mode
        if (view.actionableMode) {
            this.map.ignoreInputFields = false;
            if (key === event.TAB || key === event.ESC || key === event.F2) {
                return event;
            }
        } else // In navigation mode, we process all keys
        {
            this.map.ignoreInputFields = true;
            // Ignore TAB key in navigable mode
            return key === event.TAB ? null : event;
        }
    },
    onContainerMouseDown: function(view, mousedownEvent) {
        var me = this,
            context = new Ext.grid.CellContext(view),
            lastFocused, position;
        me.callParent([
            view,
            mousedownEvent
        ]);
        lastFocused = view.lastFocused;
        position = (view.actionableMode && view.actionPosition) || lastFocused;
        if (!position || lastFocused === 'scrollbar') {
            return;
        }
        context.setPosition(position.record, position.column);
        mousedownEvent.position = context;
        me.attachClosestCell(mousedownEvent);
        // If we are not already on that position, set position there.
        if (!me.position.isEqual(context)) {
            me.setPosition(context, null, mousedownEvent);
        }
    },
    onCellMouseDown: function(view, cell, cellIndex, record, row, recordIndex, mousedownEvent) {
        var targetComponent = Ext.Component.from(mousedownEvent, cell),
            actionableEl = mousedownEvent.getTarget(this.isFocusableEl, cell),
            ac;
        // If actionable mode, and		
        //  (mousedown on a tabbable, or anywhere in the ownership tree of an inner active
        // component),		 we should just keep the action position synchronized.
        // The tabbable element will be part of actionability.	
        // If the mousedown was NOT on some focusable object, we need to exit actionable mode.
        if (view.actionableMode) {
            // If mousedown is on a focusable element, or in the component tree of the active
            // component (which is NOT this)
            if (!actionableEl) {
                actionableEl = (ac = Ext.ComponentManager.getActiveComponent()) && ac !== view && ac.owns(mousedownEvent);
            }
            if (actionableEl) {
                // Keep actionPosition synched
                view.setActionableMode(true, mousedownEvent.position);
            } else // Not on anything actionable, then exit actionable mode
            {
                view.setActionableMode(false, mousedownEvent.position);
            }
            return;
        }
        // If the event is a touchstart, leave it until the click to focus.
        // Mousedowns may have a focusing effect.
        if (mousedownEvent.pointerType !== 'touch') {
            if (mousedownEvent.position.column.cellFocusable !== false) {
                if (actionableEl) {
                    // So that the impending onFocusEnter does not
                    // process the event and delegate focus. We
                    // control that here. This means disabling tabbability.
                    if (!view.containsFocus) {
                        view.containsFocus = true;
                        view.toggleChildrenTabbability(false);
                    }
                    if (view.setActionableMode(true, mousedownEvent.position) !== false) {
                        actionableEl.focus();
                    }
                } else {
                    cell.focus();
                }
                if (mousedownEvent.button === 2) {
                    this.fireNavigateEvent(mousedownEvent);
                }
                // If mousedowning on a focusable Component.
                // After having set the position according to the mousedown, we then
                // enter actionable mode and focus the component just as if the user
                // Had navigated here and pressed F2.
                if (targetComponent && targetComponent.isFocusable && targetComponent.isFocusable()) {
                    view.setActionableMode(true, mousedownEvent.position);
                    // Focus the targeted Component
                    targetComponent.focus();
                }
            } else {
                mousedownEvent.preventDefault(true);
            }
        }
    },
    onCellClick: function(view, cell, cellIndex, record, row, recordIndex, clickEvent) {
        var me = this,
            targetComponent = Ext.Component.from(clickEvent, cell),
            clickOnFocusable = targetComponent && targetComponent.isFocusable && targetComponent.isFocusable();
        // If a prior click handler has moved focus out of the view
        // we cannot navigate because navigation has moved outside of the view.
        // Must check that we contains the focused element, not the containsFocus flag
        // because asynchronous focus events might mean that flag is not yet set
        // even though the active element is within the view.
        if (!Ext.isIE10m && !view.el.contains(Ext.Element.getActiveElement()) && clickEvent.pointerType !== 'touch') {
            return;
        }
        // In actionable mode, we fire a navigate event in case the column's
        // stopSelection is false
        if (view.actionableMode) {
            // Only continue if action position is in a different place
            // Test using the guaranteed present clickEvent.position.
            // actionPosition might be null if action is right now in the other
            // side of a lockable.
            if (!clickEvent.position.isEqual(view.actionPosition)) {
                // Must still set position so that the other actionable
                // at the different action position blurs and finishes.
                if (!clickOnFocusable) {
                    view.setActionableMode(false, clickEvent.position);
                }
            }
            me.fireEvent('navigate', {
                view: view,
                navigationModel: me,
                keyEvent: clickEvent,
                previousPosition: me.previousPosition,
                previousRecordIndex: me.previousRecordIndex,
                previousRecord: me.previousRecord,
                previousItem: me.previousItem,
                previousCell: me.previousCell,
                previousColumnIndex: me.previousColumnIndex,
                previousColumn: me.previousColumn,
                position: clickEvent.position,
                recordIndex: clickEvent.position.rowIdx,
                record: clickEvent.position.record,
                selectionStart: me.selectionStart,
                item: clickEvent.item,
                cell: clickEvent.position.cellElement,
                columnIndex: clickEvent.position.colIdx,
                column: clickEvent.position.column
            });
        } else {
            // If the mousedown that initiated the click has navigated us to the correct spot,
            // just fire the event
            if (me.position.isEqual(clickEvent.position) || clickOnFocusable) {
                // IE10m has asynchronous focus events and the only way to detect
                // if something else was focused after onCellMouseDown was executed
                // is to verify if navigationModel has a record
                //<legacyBrowser>
                if (Ext.isIE10m && !me.record) {
                    return;
                }
                //</legacyBrowser>
                me.fireNavigateEvent(clickEvent);
            }
            // If the column is focusable, focus the cell.
            // The onFocusMove listener will react to the focus change
            else if (clickEvent.position.column.cellFocusable !== false) {
                me.setPosition(clickEvent.position, null, clickEvent);
            } else {
                clickEvent.preventDefault();
            }
        }
    },
    /**
     * @private
     * @param {Ext.event.Event} e The focusmove event
     * This is where we are informed of intra-view cell navigation which may be caused
     * by screen readers. We have to react to that and keep our internal state consistent.
     */
    onFocusMove: function(e) {
        var view = Ext.Component.from(e.delegatedTarget, null, 'tableview'),
            cell = e.target,
            isCell = Ext.fly(cell).is(view.cellSelector),
            record, column, newPosition;
        if (view) {
            // Focus moved to the view - process as if an onfocusEnter
            if (e.toElement === view.el.dom) {
                view.actionableMode = false;
                return view.onFocusEnter(e);
            }
            // If what was focused was a cell...
            if (!view.actionableMode && isCell) {
                record = view.getRecord(cell);
                column = view.getHeaderByCell(cell);
                if (record && column) {
                    newPosition = new Ext.grid.CellContext(view).setPosition(record, column);
                    // The focus might have been the *result* of setting the position
                    if (!newPosition.isEqual(this.position)) {
                        this.setPosition(newPosition);
                    }
                }
            } else if ((view.actionableMode || view.activating) && !isCell && view.el.contains(e.target) && view.el.dom !== e.target) {
                view.ownerGrid.fireEvent('cellactivate', view.ownerGrid, view.actionPosition);
            }
        }
    },
    onItemMouseDown: function(view, record, item, index, mousedownEvent) {
        var me = this,
            scroller;
        // If the event is a touchstart, leave it until the click to focus
        // A mousedown outside a cell. Must be in a Feature, or on a row border
        if (!mousedownEvent.position.cellElement && (mousedownEvent.pointerType !== 'touch')) {
            // We are going to redirect focus, so do not allow default focus to proceed
            // but allow text selection if the view is configured with enableTextSelection
            if (!view.enableTextSelection) {
                mousedownEvent.preventDefault();
            }
            // Stamp the closest cell into the event as if it were a cellmousedown
            me.attachClosestCell(mousedownEvent);
            // If we are not already on that position, set position there.
            if (!me.position.isEqual(mousedownEvent.position)) {
                me.setPosition(mousedownEvent.position, null, mousedownEvent);
            }
            // If the browser autoscrolled to bring the cell into focus
            // undo that.
            scroller = view.getScrollable();
            if (scroller) {
                scroller.restoreState();
            }
        }
    },
    onItemClick: function(view, record, item, index, clickEvent) {
        // A mousedown outside a cell. Must be in a Feature, or on a row border
        if (!clickEvent.position.cellElement) {
            this.attachClosestCell(clickEvent);
            // touchstart does not focus the closest cell, leave it until touchend
            // (translated as a click)
            if (clickEvent.pointerType === 'touch') {
                this.setPosition(clickEvent.position, null, clickEvent);
            } else {
                this.fireNavigateEvent(clickEvent);
            }
        }
    },
    attachClosestCell: function(event) {
        var position = event.position,
            targetCell = position.cellElement,
            x, columns, len, i, b;
        if (!targetCell) {
            x = event.getX();
            columns = position.view.getVisibleColumnManager().getColumns();
            len = columns.length;
            for (i = 0; i < len; i++) {
                b = columns[i].getBox();
                if (x >= b.left && x < b.right) {
                    position.setColumn(columns[i]);
                    position.rowElement = position.getRow(true);
                    position.cellElement = position.getCell(true);
                    return;
                }
            }
        }
    },
    // eslint-disable-next-line max-len
    deferSetPosition: function(delay, recordIndex, columnIndex, keyEvent, suppressEvent, preventNavigation) {
        var setPositionTask = this.view.getFocusTask();
        // This is essentially a focus operation. Use the singleton focus task
        // used by Focusable Components to schedule a setPosition call. This way
        // it can be superseded programmatically by regular Component focus calls.
        setPositionTask.delay(delay, this.setPosition, this, [
            recordIndex,
            columnIndex,
            keyEvent,
            suppressEvent,
            preventNavigation
        ]);
        return setPositionTask;
    },
    setPosition: function(recordIndex, columnIndex, keyEvent, suppressEvent, preventNavigation) {
        var me = this,
            clearing = recordIndex == null && columnIndex == null,
            isClear = me.record == null && me.recordIndex == null && me.item == null,
            view, scroller, selModel, dataSource, columnManager, newRecordIndex, newColumnIndex, newRecord, newColumn, columns;
        // Work out the view we are operating on.
        // If they passed a CellContext, use the view from that.
        // Otherwise, use the view injected into the event by Ext.view.View#processEvent.
        // Otherwise, use the last focused view.
        // Failing that, use the view we were bound to.
        if (recordIndex && recordIndex.isCellContext) {
            view = recordIndex.view;
        } else if (keyEvent && keyEvent.view) {
            view = keyEvent.view;
        } else if (me.lastFocused) {
            view = me.lastFocused.view;
        } else {
            view = me.view;
        }
        // In case any async focus was requested before this call.
        view.cancelFocusTask();
        // Return if the view was destroyed between the deferSetPosition call and now,
        // or if the call is a no-op or if there are no items which could be focused.
        if (view.destroyed || !view.refreshCounter || !view.ownerCt || clearing && isClear || !view.all.getCount()) {
            return;
        }
        selModel = view.getSelectionModel();
        dataSource = view.dataSource;
        columnManager = view.getVisibleColumnManager();
        columns = columnManager.getColumns();
        // If a CellContext is passed, use it.
        // Passing null happens on blur to remove focus class.
        if (recordIndex && recordIndex.isCellContext) {
            newRecord = recordIndex.record;
            newRecordIndex = recordIndex.rowIdx;
            newColumnIndex = Math.min(recordIndex.colIdx, columns.length - 1);
            newColumn = columns[newColumnIndex];
            // If the record being focused is not available (eg, after a removal),
            // then go to the same position
            if (dataSource.indexOf(newRecord) === -1) {
                scroller = view.getScrollable();
                // Change recordIndex so that the "No movement" test is bypassed
                // if the record is not found
                me.recordIndex = -1;
                // If the view will not jump upwards to bring the next row under the mouse
                // as expected because it's at the end, focus the previous row
                // eslint-disable-next-line max-len
                if (scroller && (scroller.getPosition().y >= scroller.getMaxPosition().y - view.all.last(true).offsetHeight)) {
                    recordIndex.rowIdx--;
                }
                newRecordIndex = Math.min(recordIndex.rowIdx, dataSource.getCount() - 1);
                newRecord = dataSource.getAt(newRecordIndex);
            }
        } else {
            // Both axes are null, we defocus
            if (clearing) {
                newRecord = newRecordIndex = null;
            } else {
                // AbstractView's default behaviour on focus is to call setPosition(0);
                // A call like this should default to the last column focused, or column 0;
                if (columnIndex == null) {
                    columnIndex = me.lastFocused ? me.lastFocused.column : 0;
                }
                if (typeof recordIndex === 'number') {
                    newRecordIndex = Math.max(Math.min(recordIndex, dataSource.getCount() - 1), 0);
                    newRecord = dataSource.getAt(recordIndex);
                }
                // row is a Record
                else if (recordIndex.isEntity) {
                    newRecord = recordIndex;
                    newRecordIndex = dataSource.indexOf(newRecord);
                }
                // row is a grid row
                else if (recordIndex.tagName) {
                    newRecord = view.getRecord(recordIndex);
                    newRecordIndex = dataSource.indexOf(newRecord);
                    if (newRecordIndex === -1) {
                        newRecord = null;
                    }
                } else {
                    if (isClear) {
                        return;
                    }
                    clearing = true;
                    newRecord = newRecordIndex = null;
                }
            }
            // Record position was successful
            if (newRecord) {
                // If the record being focused is not available (eg, after a sort), then go to 0,0
                if (newRecordIndex === -1) {
                    // Change recordIndex so that the "No movement" test is bypassed
                    // if the record is not found
                    me.recordIndex = -1;
                    newRecord = dataSource.getAt(0);
                    newRecordIndex = 0;
                    columnIndex = null;
                }
                // No columnIndex passed, and no previous column position - default to column 0
                if (columnIndex == null) {
                    if (!(newColumn = me.column)) {
                        newColumnIndex = 0;
                        newColumn = columns[0];
                    }
                } else if (typeof columnIndex === 'number') {
                    newColumn = columns[columnIndex];
                    newColumnIndex = columnIndex;
                } else {
                    newColumn = columnIndex;
                    newColumnIndex = columnManager.indexOf(columnIndex);
                }
            } else {
                clearing = true;
                newColumn = newColumnIndex = null;
            }
        }
        // The column requested may have been hidden or removed (eg reconfigure)
        // Fall back to column index.
        if (newColumn && columnManager.indexOf(newColumn) === -1) {
            if (newColumnIndex === -1) {
                newColumnIndex = 0;
            } else {
                newColumnIndex = Math.min(newColumnIndex, columns.length - 1);
            }
            newColumn = columns[newColumnIndex];
        }
        // If we are in actionable mode and focusing a cell, exit actionable mode
        // at the requested position
        if (view.actionableMode && !clearing) {
            return view.ownerGrid.setActionableMode(false, new Ext.grid.CellContext(view).setPosition(newRecord, newColumn));
        }
        // No movement; just ensure the correct item is focused and return early.
        // Do not push current position into previous position, do not fire events.
        if (newRecordIndex === me.recordIndex && newColumnIndex === me.columnIndex && view === me.position.view) {
            return me.focusPosition(me.position);
        }
        if (me.cell) {
            me.cell.removeCls(me.focusCls);
        }
        // Track the last position.
        // Used by SelectionModels as the navigation "from" position.
        me.previousRecordIndex = me.recordIndex;
        me.previousRecord = me.record;
        me.previousItem = me.item;
        me.previousCell = me.cell;
        me.previousColumn = me.column;
        me.previousColumnIndex = me.columnIndex;
        me.previousPosition = me.position.clone();
        // Track the last selectionStart position to correctly track ranges
        // (i.e., SHIFT + selection).
        me.selectionStart = selModel.selectionStart;
        // Set our CellContext to the new position
        me.position.setAll(view, me.recordIndex = newRecordIndex, me.columnIndex = newColumnIndex, me.record = newRecord, me.column = newColumn);
        if (clearing) {
            me.item = me.cell = null;
        } else {
            me.focusPosition(me.position, preventNavigation);
        }
        // Legacy API is that the SelectionModel fires focuschange events
        // and the TableView fires rowfocus and cellfocus events.
        if (!suppressEvent) {
            selModel.fireEvent('focuschange', selModel, me.previousRecord, me.record);
            view.fireEvent('rowfocus', me.record, me.item, me.recordIndex);
            view.fireEvent('cellfocus', me.record, me.cell, me.position);
        }
        // If we have moved, fire an event
        if (keyEvent && !preventNavigation && me.cell !== me.previousCell) {
            me.fireNavigateEvent(keyEvent);
        }
    },
    /**
     * @private
     * Focuses the currently active position.
     * This is used on view refresh and on replace.
     * @return {undefined}
     */
    focusPosition: function(position) {
        var me = this,
            view, row, scroller;
        me.item = me.cell = null;
        if (position && position.record && position.column) {
            view = position.view;
            // If the position is passed from a grid event, the rowElement will be stamped into it.
            // Otherwise, select it from the indicated item.
            if (position.rowElement) {
                row = me.item = position.rowElement;
            } else {
                // Get the dataview item for the position's record
                row = view.getRowByRecord(position.record);
            }
            // If there is no item at that index, it's probably because
            // there's buffered rendering. This is handled below.
            if (row) {
                // If the position is passed from a grid event, the cellElement
                // will be stamped into it. Otherwise, select it from the row.
                me.cell = position.cellElement || Ext.fly(row).down(position.column.getCellSelector(), true);
                // Maintain the cell as a Flyweight to avoid transient elements ending up
                // in the cache as full Ext.Elements.
                if (me.cell) {
                    me.cell = new Ext.dom.Fly(me.cell);
                    // Maintain lastFocused in the view so that on non-specific focus of the View,
                    // we can focus the view's correct descendant.
                    view.lastFocused = me.lastFocused = me.position.clone();
                    // Use explicit scrolling rather than relying on the browser's focus behaviour.
                    // Scroll on focus overscrolls. ensureVisible scrolls exatly correctly.
                    scroller = view.getScrollable();
                    if (scroller) {
                        scroller.ensureVisible(me.cell);
                    }
                    me.focusItem(me.cell);
                    view.focusEl = me.cell;
                } else // Cell no longer in view. Clear current position.
                {
                    me.position.setAll();
                    me.record = me.column = me.recordIndex = me.columnIndex = null;
                }
            } else // View node no longer in view. Clear current position.
            // Attempt to scroll to the record if it is in the store, but out of rendered range.
            {
                row = view.dataSource.indexOf(position.record);
                me.position.setAll();
                me.record = me.column = me.recordIndex = me.columnIndex = null;
                // The reason why the row could not be selected from the DOM could be because it's
                // out of rendered range, so scroll to the row, and then try focusing it.
                if (row !== -1 && view.bufferedRenderer) {
                    me.lastKeyEvent = null;
                    view.bufferedRenderer.scrollTo(row, false, me.afterBufferedScrollTo, me);
                }
            }
        }
    },
    /**
     * @template
     * @protected
     * Called to focus an item in the client {@link Ext.view.View DataView}.
     * The default implementation adds the {@link #focusCls} to the passed item focuses it.
     * Subclasses may choose to keep focus in another target.
     *
     * For example {@link Ext.view.BoundListKeyNav} maintains focus in the input field.
     * @param {Ext.dom.Element} item
     * @return {undefined}
     */
    focusItem: function(item) {
        item.addCls(this.focusCls);
        item.focus();
    },
    getCell: function() {
        return this.cell;
    },
    getPosition: function(skipChecks) {
        var me = this,
            position = me.position,
            curIndex, view, dataSource;
        if (position.record && position.column) {
            // If caller doesn't care whether the record and column is still there,
            // just needs to know about focus
            if (skipChecks) {
                return position;
            }
            view = position.view;
            dataSource = view.dataSource;
            curIndex = dataSource.indexOf(position.record);
            // If not with the same ID, at the same index if that is in range
            if (curIndex === -1) {
                curIndex = position.rowIdx;
                // If no record now at that index (even if it's less than the totalCount,
                // it may be a BufferedStore) then there is no focus position,
                // and we must return null
                if (!(position.record = dataSource.getAt(curIndex))) {
                    curIndex = -1;
                }
            }
            // If the positioned record or column has gone away, we have no position
            if (curIndex === -1 || view.getVisibleColumnManager().indexOf(position.column) === -1) {
                position.setAll();
                me.record = me.column = me.recordIndex = me.columnIndex = null;
            } else {
                return position;
            }
        }
        return null;
    },
    getLastFocused: function() {
        var me = this,
            view,
            lastFocused = me.lastFocused;
        if (lastFocused && lastFocused.record && lastFocused.column) {
            view = lastFocused.view;
            // If the last focused record or column has gone away, we have no lastFocused
            if (view.dataSource.indexOf(lastFocused.record) !== -1 && view.getVisibleColumnManager().indexOf(lastFocused.column) !== -1) {
                return lastFocused;
            }
        }
    },
    onKeyTab: function(keyEvent) {
        var forward = !keyEvent.shiftKey,
            view = keyEvent.position.view,
            ret, focusTarget, position;
        ret = view.findFocusPosition(keyEvent.target, keyEvent.position, forward, keyEvent);
        focusTarget = ret.target;
        position = ret.position;
        // We found a focus target either in the cell or in a sibling cell
        // in the direction of navigation.
        if (focusTarget) {
            // Keep actionPosition synched
            this.actionPosition = position.view.actionPosition = position;
            Ext.fly(focusTarget).focus();
        } else // Focus target not found, we need to exit the row
        {
            view.onRowExit(keyEvent, keyEvent.item, keyEvent.item[forward ? 'nextSibling' : 'previousSibling'], forward);
        }
        // We control navigation when in actionable mode.
        // no TAB events must navigate.
        keyEvent.preventDefault();
    },
    onKeyUp: function(keyEvent) {
        var newRecord = keyEvent.view.walkRecs(keyEvent.record, -1),
            pos = this.getPosition();
        if (newRecord) {
            pos.setRow(newRecord);
            // If no cell at the current column, move towards row start
            if (!pos.getCell(true)) {
                pos.navigate(-1);
            }
            this.setPosition(pos, null, keyEvent);
        }
    },
    onKeyDown: function(keyEvent) {
        // If we are in the middle of an animated node expand, jump to next sibling.
        // The first child record is in a temp animation DIV and will be removed, so will blur.
        // eslint-disable-next-line max-len
        var newRecord = keyEvent.record.isExpandingOrCollapsing ? null : keyEvent.view.walkRecs(keyEvent.record, 1),
            pos = this.getPosition();
        if (newRecord) {
            pos.setRow(newRecord);
            // If no cell at the current column, move towards row start
            if (!pos.getCell(true)) {
                pos.navigate(-1);
            }
            this.setPosition(pos, null, keyEvent);
        }
    },
    onKeyRight: function(keyEvent) {
        var newPosition = this.move('right', keyEvent);
        if (newPosition) {
            this.setPosition(newPosition, null, keyEvent);
        }
    },
    onKeyLeft: function(keyEvent) {
        var newPosition = this.move('left', keyEvent);
        if (newPosition) {
            this.setPosition(newPosition, null, keyEvent);
        }
    },
    // ENTER emulates a dblclick event at the TableView level
    onKeyEnter: function(keyEvent) {
        var eventArgs = [
                'cellclick',
                keyEvent.view,
                keyEvent.position.cellElement,
                keyEvent.position.colIdx,
                keyEvent.record,
                keyEvent.position.rowElement,
                keyEvent.recordIndex,
                keyEvent
            ],
            actionCell = keyEvent.position.getCell(true);
        // May have been deleted by now by an ActionColumn handler
        if (actionCell) {
            // Stop the keydown event so that an ENTER keyup does not get delivered to
            // any element which focus is transferred to in a click handler.
            if (!actionCell.querySelector('[tabIndex="-1"]')) {
                keyEvent.stopEvent();
                keyEvent.view.fireEvent.apply(keyEvent.view, eventArgs);
                eventArgs[0] = 'celldblclick';
                keyEvent.view.fireEvent.apply(keyEvent.view, eventArgs);
            }
            // Enters actionable mode. Unless the emulated evenbts have done it
            if (!this.view.actionableMode) {
                this.view.ownerGrid.setActionableMode(true, this.getPosition());
            }
        }
    },
    onKeyF2: function(keyEvent) {
        // Toggles actionable mode
        var grid = this.view.ownerGrid,
            actionableMode = grid.actionableMode;
        grid.setActionableMode(!actionableMode, actionableMode ? null : this.getPosition());
    },
    onKeyEsc: function(keyEvent) {
        var grid = this.view.ownerGrid;
        // Exits actionable mode
        if (grid.actionableMode) {
            grid.setActionableMode(false);
        } else // If we are NOT in actionable mode, we must return true so that the event is not stopped.
        // ESC might be consumed at a higher level - for example an encapsulating Window.
        {
            return true;
        }
    },
    move: function(dir, keyEvent) {
        var me = this,
            position = me.getPosition(),
            result = position,
            rowVeto = keyEvent.shiftKey && (dir === 'right' || dir === 'left');
        if (position && position.record) {
            while (result) {
                // Do not allow SHIFT+(left|right) to wrap.
                // Important to use result.view, since a call to walkCells could change the
                // resulting view if we're using locking.
                result = result.view.walkCells(result, dir, rowVeto ? me.vetoRowChange : null, me);
                // If the new position is fousable, we're done.
                if (result && result.getCell(true) && result.column.cellFocusable !== false) {
                    return result;
                }
            }
        }
        // Enforce code correctness in unbuilt source.
        return null;
    },
    vetoRowChange: function(newPosition) {
        return this.getPosition().record === newPosition.record;
    },
    // Go one page down from the lastFocused record in the grid.
    onKeyPageDown: function(keyEvent) {
        var me = this,
            view = keyEvent.view,
            rowsVisible = me.getRowsVisible(),
            newIdx, newRecord;
        if (rowsVisible) {
            // If rendering is buffered, we cannot just increment the row - the row may not be there
            // We have to ask the BufferedRenderer to navigate to the target.
            // And that may involve asynchronous I/O, so must post-process in a callback.
            if (view.bufferedRenderer) {
                newIdx = Math.min(keyEvent.recordIndex + rowsVisible, view.dataSource.getCount() - 1);
                me.lastKeyEvent = keyEvent;
                view.bufferedRenderer.scrollTo(newIdx, false, me.afterBufferedScrollTo, me);
            } else {
                newRecord = view.walkRecs(keyEvent.record, rowsVisible);
                me.setPosition(newRecord, null, keyEvent);
            }
        }
    },
    // Go one page up from the lastFocused record in the grid.
    onKeyPageUp: function(keyEvent) {
        var me = this,
            view = keyEvent.view,
            rowsVisible = me.getRowsVisible(),
            newIdx, newRecord;
        if (rowsVisible) {
            // If rendering is buffered, we cannot just increment the row - the row may not be there
            // We have to ask the BufferedRenderer to navigate to the target.
            // And that may involve asynchronous I/O, so must post-process in a callback.
            if (view.bufferedRenderer) {
                newIdx = Math.max(keyEvent.recordIndex - rowsVisible, 0);
                me.lastKeyEvent = keyEvent;
                view.bufferedRenderer.scrollTo(newIdx, false, me.afterBufferedScrollTo, me);
            } else {
                newRecord = view.walkRecs(keyEvent.record, -rowsVisible);
                me.setPosition(newRecord, null, keyEvent);
            }
        }
    },
    // Home moves the focus to the first cell of the current row.
    onKeyHome: function(keyEvent) {
        var me = this,
            view = keyEvent.view;
        // ALT+Home - go to first visible record in grid.
        if (keyEvent.altKey) {
            if (view.bufferedRenderer) {
                // If rendering is buffered, we cannot just increment the row - the row
                // may not be there
                // We have to ask the BufferedRenderer to navigate to the target.
                // And that may involve asynchronous I/O, so must post-process in a callback.
                me.lastKeyEvent = keyEvent;
                view.bufferedRenderer.scrollTo(0, false, me.afterBufferedScrollTo, me);
            } else {
                // Walk forwards to the first record
                me.setPosition(view.walkRecs(keyEvent.record, -view.dataSource.indexOf(keyEvent.record)), null, keyEvent);
            }
        } else // Home moves the focus to the First cell in the current row.
        {
            me.setPosition(keyEvent.record, 0, keyEvent);
        }
    },
    afterBufferedScrollTo: function(newIdx, newRecord) {
        this.setPosition(newRecord, null, this.lastKeyEvent, null, !this.lastKeyEvent);
    },
    // End moves the focus to the last cell in the current row.
    onKeyEnd: function(keyEvent) {
        var me = this,
            view = keyEvent.view;
        // ALT/End - go to last visible record in grid.
        if (keyEvent.altKey) {
            if (view.bufferedRenderer) {
                // If rendering is buffered, we cannot just increment the row - the row
                // may not be there
                // We have to ask the BufferedRenderer to navigate to the target.
                // And that may involve asynchronous I/O, so must postprocess in a callback.
                me.lastKeyEvent = keyEvent;
                view.bufferedRenderer.scrollTo(view.store.getCount() - 1, false, me.afterBufferedScrollTo, me);
            } else {
                // Walk forwards to the end record
                me.setPosition(view.walkRecs(keyEvent.record, view.dataSource.getCount() - 1 - view.dataSource.indexOf(keyEvent.record)), null, keyEvent);
            }
        } else // End moves the focus to the last cell in the current row.
        {
            me.setPosition(keyEvent.record, keyEvent.view.getVisibleColumnManager().getColumns().length - 1, keyEvent);
        }
    },
    // Returns the number of rows currently visible on the screen or
    // false if there were no rows. This assumes that all rows are
    // of the same height and the first view is accurate.
    getRowsVisible: function() {
        var rowsVisible = false,
            view = this.view,
            firstRow = view.all.first(),
            rowHeight, gridViewHeight;
        if (firstRow) {
            rowHeight = firstRow.getHeight();
            gridViewHeight = view.el.getHeight();
            rowsVisible = Math.floor(gridViewHeight / rowHeight);
        }
        return rowsVisible;
    },
    fireNavigateEvent: function(keyEvent) {
        var me = this;
        me.fireEvent('navigate', {
            view: me.position.view,
            navigationModel: me,
            keyEvent: keyEvent || new Ext.event.Event({}),
            previousPosition: me.previousPosition,
            previousRecordIndex: me.previousRecordIndex,
            previousRecord: me.previousRecord,
            previousItem: me.previousItem,
            previousCell: me.previousCell,
            previousColumnIndex: me.previousColumnIndex,
            previousColumn: me.previousColumn,
            position: me.position,
            recordIndex: me.recordIndex,
            record: me.record,
            selectionStart: me.selectionStart,
            item: me.item,
            cell: me.cell,
            columnIndex: me.columnIndex,
            column: me.column
        });
    },
    isFocusableEl: function(el) {
        return Ext.fly(el).isFocusable();
    }
});

/**
 *  Component layout for {@link Ext.view.Table}
 *  @private
 *
 */
Ext.define('Ext.view.TableLayout', {
    extend: 'Ext.layout.component.Auto',
    alias: 'layout.tableview',
    type: 'tableview',
    beginLayout: function(ownerContext) {
        var me = this,
            owner = me.owner,
            ownerGrid = owner.ownerGrid,
            partner = owner.lockingPartner,
            partnerContext = ownerContext.lockingPartnerContext,
            context = ownerContext.context,
            scrollable = ownerGrid.getScrollable(),
            partnerVisible;
        partnerVisible = partner && partner.grid.isVisible() && !(partner.grid.collapsed || partner.grid.floatedFromCollapse);
        // Flag whether we need to do row height synchronization.
        // syncRowHeightOnNextLayout is a one time flag used when some code knows it has changed
        // data height and that the upcoming layout must sync row heights even if the grid
        // is configured not to for general row rendering.
        ownerContext.doSyncRowHeights = partnerVisible && (ownerGrid.syncRowHeight || ownerGrid.syncRowHeightOnNextLayout);
        // The reason for checking .config here is that by setting the overflow on the context, it
        // overwrites the value in the scrollable. As such, all we're trying to do here is capture
        // the initial intent to see if the user configured the scroller as x: false.
        // It's not perfect but will cover 99% of cases.
        ownerContext.allowScrollX = scrollable && scrollable.config && scrollable.config.x;
        if (!me.columnFlusherId) {
            me.columnFlusherId = me.id + '-columns';
            me.rowHeightFlusherId = me.id + '-rows';
        }
        me.callParent([
            ownerContext
        ]);
        // If we are in a twinned grid (locked view) then set up bidirectional links with
        // the other side's layout context. If the locked or normal side is hidden then
        // we should treat it as though we were laying out a single grid, so don't setup
        // the partners.
        // This is typically if a grid is configured with locking but starts with no locked columns.
        if (partnerVisible) {
            if (!partnerContext && partner.componentLayout.isRunning()) {
                // eslint-disable-next-line max-len
                (partnerContext = ownerContext.lockingPartnerContext = context.getCmp(partner)).lockingPartnerContext = ownerContext;
                // Set up opposite side's link if not already aware.
                if (!partnerContext.lockingPartnerContext) {
                    partnerContext.lockingPartnerContext = ownerContext;
                }
            }
            if (ownerContext.doSyncRowHeights) {
                if (partnerContext && !partnerContext.rowHeightSynchronizer) {
                    partnerContext.rowHeightSynchronizer = partnerContext.target.syncRowHeightBegin();
                }
                ownerContext.rowHeightSynchronizer = me.owner.syncRowHeightBegin();
            }
        }
        // Grab a ContextItem for the header container (and make sure the TableLayout can
        // reach us as well):
        (ownerContext.headerContext = context.getCmp(me.headerCt)).viewContext = ownerContext;
    },
    beginLayoutCycle: function(ownerContext, firstCycle) {
        this.callParent([
            ownerContext,
            firstCycle
        ]);
        if (ownerContext.syncRowHeights) {
            ownerContext.target.syncRowHeightClear(ownerContext.rowHeightSynchronizer);
            ownerContext.syncRowHeights = false;
        }
    },
    calculate: function(ownerContext) {
        var me = this,
            context = ownerContext.context,
            lockingPartnerContext = ownerContext.lockingPartnerContext,
            headerContext = ownerContext.headerContext,
            ownerCtContext = ownerContext.ownerCtContext,
            state = ownerContext.state,
            owner = me.owner,
            bodyDom = owner.body.dom,
            columnsChanged = headerContext.getProp('columnsChanged'),
            overflowable, columnFlusher, otherSynchronizer, synchronizer, rowHeightFlusher, bodyHeight, ctSize, overflowY, overflowX, scrollbarHeight;
        // Shortcut when empty grid - let the base handle it.
        // EXTJS-14844: Even when no data rows (all.getCount() === 0) there may be
        // summary rows to size.
        if (!owner.all.getCount() && (!bodyDom || !owner.body.child('table', true))) {
            ownerContext.setProp('viewOverflowY', false);
            me.callParent([
                ownerContext
            ]);
            return;
        }
        // BufferedRenderer#beforeTableLayout reads, so call it in a read phase.
        if (me.calcCount === 1 && me.owner.bufferedRenderer) {
            me.owner.bufferedRenderer.beforeTableLayout(ownerContext);
        }
        if (columnsChanged === undefined) {
            // We cannot proceed when we have rows but no columnWidths determined...
            me.done = false;
            return;
        }
        if (columnsChanged) {
            if (!(columnFlusher = state.columnFlusher)) {
                // Since the columns have changed, we need to write the widths to the DOM.
                // Queue (and possibly replace) a pseudo ContextItem, who's flush method
                // routes back into this class.
                context.queueFlush(state.columnFlusher = columnFlusher = {
                    ownerContext: ownerContext,
                    columnsChanged: columnsChanged,
                    layout: me,
                    id: me.columnFlusherId,
                    flush: me.flushColumnWidths
                }, true);
            }
            if (!columnFlusher.flushed) {
                // We have queued the columns to be written, but they are still pending, so
                // we cannot proceed.
                me.done = false;
                return;
            }
        }
        // They have to turn row height synchronization on, or there may be variable row heights
        // Either no columns changed, or we have flushed those changes.. which means the
        // column widths in the DOM are correct. Now we can proceed to syncRowHeights (if
        // we are locking) or wrap it up by determining our vertical overflow.
        if (ownerContext.doSyncRowHeights) {
            if (!(rowHeightFlusher = state.rowHeightFlusher)) {
                // When we are locking, both sides need to read their row heights in a read
                // phase (i.e., right now).
                if (!(synchronizer = state.rowHeights)) {
                    state.rowHeights = synchronizer = ownerContext.rowHeightSynchronizer;
                    me.owner.syncRowHeightMeasure(synchronizer);
                    ownerContext.setProp('rowHeights', synchronizer);
                }
                if (!(otherSynchronizer = lockingPartnerContext.getProp('rowHeights'))) {
                    me.done = false;
                    return;
                }
                // Queue (and possibly replace) a pseudo ContextItem, who's flush method
                // routes back into this class.
                context.queueFlush(state.rowHeightFlusher = rowHeightFlusher = {
                    ownerContext: ownerContext,
                    synchronizer: synchronizer,
                    otherSynchronizer: otherSynchronizer,
                    layout: me,
                    id: me.rowHeightFlusherId,
                    flush: me.flushRowHeights
                }, true);
            }
            if (!rowHeightFlusher.flushed) {
                me.done = false;
                return;
            }
        }
        me.callParent([
            ownerContext
        ]);
        if (!ownerContext.heightModel.shrinkWrap) {
            if (!ownerCtContext.heightModel.shrinkWrap) {
                overflowable = true;
                // We are placed in a fit layout of the gridpanel (our ownerCt), so we need to
                // consult its containerSize when we are not shrink-wrapping to see if our
                // content will overflow vertically.
                ctSize = ownerCtContext.target.layout.getContainerSize(ownerCtContext);
                if (!ctSize.gotHeight) {
                    me.done = false;
                    return;
                }
                bodyHeight = bodyDom.offsetHeight;
                if (bodyHeight > ctSize.height) {
                    overflowY = true;
                }
            }
        }
        // Adjust the presence of X scrollability depending upon whether the headers
        // overflow, and scrollbars take up space.
        // This has two purposes.
        //
        // For lockable assemblies, if there is horizontal overflow in the normal side,
        // The locked side (which shrinkwraps the columns) must be set to overflow: scroll
        // in order that it has acquires a matching horizontal scrollbar.
        //
        // If no locking, then if there is no horizontal overflow, we set overflow-x: hidden
        // This avoids "pantom" scrollbars which are only caused by the presence of another
        // scrollbar.
        scrollbarHeight = Ext.scrollbar.height();
        if (me.done && ownerContext.allowScrollX && scrollbarHeight) {
            // No locking sides, ensure X scrolling is on if there is overflow,
            // but not if there is no overflow
            // This eliminates "phantom" scrollbars which are only caused by other scrollbars.
            // Locking horizontal scrollbars are handled in Ext.grid.locking.Lockable#afterLayout
            if (!owner.lockingPartner) {
                if (owner.isAutoTree) {
                    overflowX = true;
                } else {
                    overflowX = !!ownerContext.headerContext.state.boxPlan.tooNarrow;
                }
                ownerContext.setProp('overflowX', overflowX);
            }
            // If the overflowY was set to false but then adding a horizontal scrollbar
            // will overflow the view vertically we need to set overflowY to true
            if (overflowX && bodyHeight && overflowable) {
                overflowY = (bodyHeight + scrollbarHeight) > ctSize.height;
            }
        }
        if (me.done || overflowY != null) {
            ownerContext.setProp('viewOverflowY', !!overflowY);
        }
    },
    measureContentHeight: function(ownerContext) {
        var owner = this.owner,
            bodyDom = owner.body.dom,
            emptyEl = owner.emptyEl,
            bodyHeight = 0;
        if (emptyEl) {
            bodyHeight += emptyEl.offsetHeight;
        }
        if (bodyDom) {
            bodyHeight += bodyDom.offsetHeight;
        }
        // This will have been figured out by now because the columnWidths have been
        // published...
        if (ownerContext.headerContext.state.boxPlan.tooNarrow) {
            bodyHeight += Ext.scrollbar.height();
        }
        return bodyHeight;
    },
    flushColumnWidths: function() {
        // NOTE: The "this" pointer here is the flusher object that was queued.
        var flusher = this,
            me = flusher.layout,
            ownerContext = flusher.ownerContext,
            columnsChanged = flusher.columnsChanged,
            owner = ownerContext.target,
            len = columnsChanged.length,
            column, i, colWidth, lastBox;
        if (ownerContext.state.columnFlusher !== flusher) {
            return;
        }
        // Set column width corresponding to each header
        for (i = 0; i < len; i++) {
            if (!(column = columnsChanged[i])) {
                
                continue;
            }
            colWidth = column.props.width;
            owner.body.select(owner.getColumnSizerSelector(column.target)).setWidth(colWidth);
            // Allow columns which need to perform layouts on resize queue a layout
            if (column.target.onCellsResized) {
                column.target.onCellsResized(colWidth);
            }
            // Enable the next go-round of headerCt's ColumnLayout change check to
            // read true, flushed lastBox widths that are in the Table
            lastBox = column.lastBox;
            if (lastBox) {
                lastBox.width = colWidth;
            }
        }
        flusher.flushed = true;
        if (!me.pending) {
            ownerContext.context.queueLayout(me);
        }
    },
    flushRowHeights: function() {
        // NOTE: The "this" pointer here is the flusher object that was queued.
        var flusher = this,
            me = flusher.layout,
            ownerContext = flusher.ownerContext;
        if (ownerContext.state.rowHeightFlusher !== flusher) {
            return;
        }
        ownerContext.target.syncRowHeightFinish(flusher.synchronizer, flusher.otherSynchronizer);
        flusher.flushed = true;
        ownerContext.syncRowHeights = true;
        if (!me.pending) {
            ownerContext.context.queueLayout(me);
        }
    },
    finishedLayout: function(ownerContext) {
        var me = this,
            ownerGrid = me.owner.ownerGrid,
            nodeContainer = Ext.fly(me.owner.getNodeContainer()),
            scroller = this.owner.getScrollable(),
            buffered;
        me.callParent([
            ownerContext
        ]);
        if (nodeContainer) {
            nodeContainer.setWidth(ownerContext.headerContext.props.contentWidth);
        }
        // Inform any buffered renderer about completion of the layout of its view
        buffered = me.owner.bufferedRenderer;
        if (buffered) {
            buffered.afterTableLayout(ownerContext);
        }
        if (ownerGrid) {
            ownerGrid.syncRowHeightOnNextLayout = false;
        }
        if (scroller && !scroller.isScrolling) {
            // BufferedRenderer only sets nextRefreshStartIndex to zero when preserveScrollOnReload
            // is false. And if variableRowHeight is true, restoring the scroller will be handled
            // by the bufferedRenderer
            if (buffered) {
                if (buffered.nextRefreshStartIndex === 0 || me.owner.hasVariableRowHeight()) {
                    return;
                }
            }
            scroller.restoreState();
        }
    },
    getLayoutItems: function() {
        return this.owner.getRefItems();
    },
    isValidParent: function() {
        return true;
    }
});

/**
 * @private
 */
Ext.define('Ext.grid.locking.RowSynchronizer', {
    constructor: function(view, rowEl) {
        var me = this,
            rowTpl;
        me.view = view;
        me.rowEl = rowEl;
        me.els = {};
        me.add('data', view.rowSelector);
        for (rowTpl = view.rowTpl; rowTpl; rowTpl = rowTpl.nextTpl) {
            if (rowTpl.beginRowSync) {
                rowTpl.beginRowSync(me);
            }
        }
    },
    add: function(name, selector) {
        var el = Ext.fly(this.rowEl).down(selector, true);
        if (el) {
            this.els[name] = {
                el: el
            };
        }
    },
    finish: function(other) {
        var me = this,
            els = me.els,
            otherEls = other.els,
            otherEl,
            growth = 0,
            otherGrowth = 0,
            delta, name, otherHeight;
        for (name in els) {
            otherEl = otherEls[name];
            // Partner RowSynchronizer may not have the element.
            // For example, group summary may not be wanted in locking side.
            otherHeight = otherEl ? otherEl.height : 0;
            delta = otherHeight - els[name].height;
            if (delta > 0) {
                growth += delta;
                Ext.fly(els[name].el).setHeight(otherHeight);
            } else {
                otherGrowth -= delta;
            }
        }
        // Compare the growth to both rows and see if this row is lacking.
        otherHeight = other.rowHeight + otherGrowth;
        // IE9 uses content box sizing on table, so height must not include border
        if (Ext.isIE9 && me.view.ownerGrid.rowLines) {
            otherHeight--;
        }
        if (me.rowHeight + growth < otherHeight) {
            Ext.fly(me.rowEl).setHeight(otherHeight);
        }
    },
    measure: function() {
        var me = this,
            els = me.els,
            grid = me.view.ownerGrid,
            name;
        me.rowHeight = grid.getElementHeight(me.rowEl);
        for (name in els) {
            els[name].height = grid.getElementHeight(els[name].el);
        }
    },
    reset: function() {
        var els = this.els,
            name;
        this.rowEl.style.height = '';
        for (name in els) {
            els[name].el.style.height = '';
        }
    }
});

/**
 * @private
 * A cache of View elements keyed using the index of the associated record in the store.
 * 
 * This implements the methods of {Ext.dom.CompositeElement} which are used by
 * {@link Ext.view.AbstractView} to provide a map of record nodes and methods to manipulate
 * the nodes.
 * @class Ext.view.NodeCache
 */
Ext.define('Ext.view.NodeCache', {
    requires: [
        'Ext.dom.CompositeElementLite'
    ],
    statics: {
        range: document.createRange && document.createRange()
    },
    constructor: function(view) {
        this.view = view;
        this.clear();
        this.el = new Ext.dom.Fly();
    },
    destroy: function() {
        var me = this;
        if (!me.destroyed) {
            me.el.destroy();
            me.el = me.view = null;
            me.destroyed = true;
        }
        me.callParent();
    },
    /**
    * Removes all elements from this NodeCache.
    * @param {Boolean} [removeDom] True to also remove the elements from the document.
    */
    clear: function(removeDom) {
        var me = this,
            elements = me.elements,
            range = me.statics().range,
            key;
        if (me.count && removeDom) {
            // Some browsers throw error if Range used on detached DOM
            if (range && Ext.getBody().contains(elements[0])) {
                range.setStartBefore(elements[me.startIndex]);
                range.setEndAfter(elements[me.endIndex]);
                range.deleteContents();
            } else {
                for (key in elements) {
                    Ext.removeNode(elements[key]);
                }
            }
        }
        me.elements = {};
        me.count = me.startIndex = 0;
        me.endIndex = -1;
    },
    /**
     * Clears this NodeCache and adds the elements passed.
     * @param {HTMLElement[]} newElements An array of DOM elements from which to fill
     * this NodeCache.
     * @param {Number} startIndex
     * @param fixedNodes
     * @return {Ext.view.NodeCache} this
     */
    fill: function(newElements, startIndex, fixedNodes) {
        var me = this,
            elements = me.elements = {},
            i, len;
        fixedNodes = fixedNodes || 0;
        if (!startIndex) {
            startIndex = 0;
        }
        for (i = 0 , len = newElements.length - fixedNodes; i < len; i++) {
            elements[startIndex + i] = newElements[i + fixedNodes];
        }
        me.startIndex = startIndex;
        me.endIndex = startIndex + len - 1;
        me.count = len;
        return this;
    },
    insert: function(insertPoint, nodes) {
        var me = this,
            elements = me.elements,
            i,
            nodeCount = nodes.length;
        // If not inserting into empty cache, validate, and possibly shuffle.
        if (me.count) {
            if (insertPoint > me.endIndex + 1 || insertPoint + nodes.length < me.startIndex) {
                Ext.raise('Discontiguous range would result from inserting ' + nodes.length + ' nodes at ' + insertPoint);
            }
            // Move following nodes forwards by <nodeCount> positions
            if (insertPoint < me.count) {
                for (i = me.endIndex + nodeCount; i >= insertPoint + nodeCount; i--) {
                    elements[i] = elements[i - nodeCount];
                    elements[i].setAttribute('data-recordIndex', i);
                }
            }
            me.endIndex = me.endIndex + nodeCount;
        } else // Empty cache. set up counters
        {
            me.startIndex = insertPoint;
            me.endIndex = insertPoint + nodeCount - 1;
        }
        // Insert new nodes into place
        for (i = 0; i < nodeCount; i++ , insertPoint++) {
            elements[insertPoint] = nodes[i];
            elements[insertPoint].setAttribute('data-recordIndex', insertPoint);
        }
        me.count += nodeCount;
    },
    invoke: function(fn, args) {
        var me = this,
            element, i;
        fn = Ext.dom.Element.prototype[fn];
        for (i = me.startIndex; i <= me.endIndex; i++) {
            element = me.item(i);
            if (element) {
                fn.apply(element, args);
            }
        }
        return me;
    },
    item: function(index, asDom) {
        var el = this.elements[index],
            result = null;
        if (el) {
            result = asDom ? this.elements[index] : this.el.attach(this.elements[index]);
        }
        return result;
    },
    first: function(asDom) {
        return this.item(this.startIndex, asDom);
    },
    last: function(asDom) {
        return this.item(this.endIndex, asDom);
    },
    /**
     * @private
     * Used by buffered renderer when adding or removing record ranges which are above the
     * rendered block. The element block must be shuffled up or down the index range,
     * and the data-recordIndex connector attribute must be updated.
     *
     */
    moveBlock: function(increment) {
        var me = this,
            elements = me.elements,
            node, end, step, i;
        // No movement; return
        if (!increment) {
            return;
        }
        if (increment < 0) {
            i = me.startIndex - 1;
            end = me.endIndex;
            step = 1;
        } else {
            i = me.endIndex + 1;
            end = me.startIndex;
            step = -1;
        }
        me.startIndex += increment;
        me.endIndex += increment;
        do {
            i += step;
            node = elements[i + increment] = elements[i];
            node.setAttribute('data-recordIndex', i + increment);
            if (i < me.startIndex || i > me.endIndex) {
                delete elements[i];
            }
        } while (// "from" element is outside of the new range, then delete it.
        i !== end);
        delete elements[i];
    },
    getCount: function() {
        return this.count;
    },
    slice: function(start, end) {
        var elements = this.elements,
            result = [],
            i;
        if (!end) {
            end = this.endIndex;
        } else {
            end = Math.min(this.endIndex, end - 1);
        }
        for (i = start || this.startIndex; i <= end; i++) {
            result.push(elements[i]);
        }
        return result;
    },
    /**
    * Replaces the specified element with the passed element.
    * @param {String/HTMLElement/Ext.dom.Element/Number} el The id of an element,
    * the Element itself, the index of the element in this composite to replace.
    * @param {String/Ext.dom.Element} replacement The id of an element or the Element itself.
    * @param {Boolean} [domReplace] True to remove and replace the element in the document too.
    */
    replaceElement: function(el, replacement, domReplace) {
        var elements = this.elements,
            index = (typeof el === 'number') ? el : this.indexOf(el);
        if (index > -1) {
            replacement = Ext.getDom(replacement);
            if (domReplace) {
                el = elements[index];
                el.parentNode.insertBefore(replacement, el);
                Ext.removeNode(el);
                replacement.setAttribute('data-recordIndex', index);
            }
            this.elements[index] = replacement;
        }
        return this;
    },
    /**
    * Find the index of the passed element within the composite collection.
    * @param {String/HTMLElement/Ext.dom.Element/Number} el The id of an element,
    * or an Ext.dom.Element, or an HTMLElement to find within the composite collection.
    * @return {Number} The index of the passed Ext.dom.Element in the composite collection,
    * or -1 if not found.
    */
    indexOf: function(el) {
        var elements = this.elements,
            index;
        el = Ext.getDom(el);
        for (index = this.startIndex; index <= this.endIndex; index++) {
            if (elements[index] === el) {
                return index;
            }
        }
        return -1;
    },
    clip: function(removeEnd, removeCount) {
        var me = this,
            elements = me.elements,
            removed = [],
            start, end, el, i;
        // Clipping from start
        if (removeEnd === 1) {
            start = me.startIndex;
            me.startIndex += removeCount;
        } else // Clipping from end
        {
            me.endIndex -= removeCount;
            start = me.endIndex + 1;
        }
        for (i = start , end = start + removeCount - 1; i <= end; i++) {
            el = elements[i];
            removed.push(el);
            Ext.removeNode(el);
            delete elements[i];
        }
        me.count -= removeCount;
        me.view.fireItemMutationEvent('itemremove', me.view.dataSource.getRange(start, end), start, removed, me.view);
    },
    removeRange: function(start, end, removeDom) {
        var me = this,
            elements = me.elements,
            removed = [],
            el, i, removeCount, fromPos;
        if (end == null) {
            end = me.endIndex + 1;
        } else {
            end = Math.min(me.endIndex + 1, end + 1);
        }
        if (start == null) {
            start = me.startIndex;
        }
        removeCount = end - start;
        for (i = start , fromPos = end; i <= me.endIndex; i++ , fromPos++) {
            el = elements[i];
            // Within removal range and we are removing from DOM
            if (i < end) {
                removed.push(el);
                if (removeDom) {
                    Ext.removeNode(el);
                }
            }
            // If the from position is occupied, shuffle that entry back into reference "i"
            if (fromPos <= me.endIndex) {
                el = elements[i] = elements[fromPos];
                el.setAttribute('data-recordIndex', i);
            } else // The from position has walked off the end, so delete reference "i"
            {
                delete elements[i];
            }
        }
        me.count -= removeCount;
        me.endIndex -= removeCount;
        return removed;
    },
    /**
    * Removes the specified element(s).
    * @param {String/HTMLElement/Ext.dom.Element/Number} keys The id of an element,
    * the Element itself, the index of the element in this composite or an array of any of those.
    * @param {Boolean} [removeDom] True to also remove the element from the document
    */
    removeElement: function(keys, removeDom) {
        var me = this,
            elements = me.elements,
            keyIndex = 0,
            inKeys, key, el, deleteCount, index, fromIndex;
        // Sort the keys into ascending order so that we can iterate through the elements
        // collection, and delete items encountered in the keys array as we encounter them.
        if (Ext.isArray(keys)) {
            inKeys = keys;
            keys = [];
            deleteCount = inKeys.length;
            for (keyIndex = 0; keyIndex < deleteCount; keyIndex++) {
                key = inKeys[keyIndex];
                if (typeof key !== 'number') {
                    key = me.indexOf(key);
                }
                // Could be asked to remove data above the start, or below the end
                // of rendered zone in a buffer rendered view
                // So only collect keys which are within our range
                if (key >= me.startIndex && key <= me.endIndex) {
                    keys[keys.length] = key;
                }
            }
            Ext.Array.sort(keys);
            deleteCount = keys.length;
        } else {
            // Could be asked to remove data above the start, or below the end of rendered zone
            // in a buffer rendered view
            if (keys < me.startIndex || keys > me.endIndex) {
                return;
            }
            deleteCount = 1;
            keys = [
                keys
            ];
        }
        // Iterate through elements starting at the element referenced by the first deletion key.
        // We also start off and index zero in the keys to delete array.
        // eslint-disable-next-line max-len
        for (index = fromIndex = keys[0] , keyIndex = 0; index <= me.endIndex; index++ , fromIndex++) {
            // If the current index matches the next key in the delete keys array, this 
            // entry is being deleted, so increment the fromIndex to skip it.
            // Advance to next entry in keys array.
            if (keyIndex < deleteCount && index === keys[keyIndex]) {
                fromIndex++;
                keyIndex++;
                if (removeDom) {
                    Ext.removeNode(elements[index]);
                }
            }
            // Shuffle entries forward of the delete range back into contiguity.
            if (fromIndex <= me.endIndex && fromIndex >= me.startIndex) {
                el = elements[index] = elements[fromIndex];
                el.setAttribute('data-recordIndex', index);
            } else {
                delete elements[index];
            }
        }
        me.endIndex -= deleteCount;
        me.count -= deleteCount;
    },
    /**
     * Appends/prepends records depending on direction flag
     * @param {Ext.data.Model[]} newRecords Items to append/prepend
     * @param {Number} direction `-1' = scroll up, `0` = scroll down.
     * @param {Number} removeCount The number of records to remove from the end. if scrolling
     * down, rows are removed from the top and the new rows are added at the bottom.
     * @return {HTMLElement[]} The view item nodes added either at the top or the bottom
     * of the view.
     */
    scroll: function(newRecords, direction, removeCount) {
        var me = this,
            view = me.view,
            store = view.store,
            elements = me.elements,
            recCount = newRecords.length,
            nodeContainer = view.getNodeContainer(),
            range = me.statics().range,
            i, el, removeEnd, children, result, removeStart, removedRecords, removedItems;
        if (!(newRecords.length || removeCount)) {
            return;
        }
        // Scrolling up (content moved down - new content needed at top, remove from bottom)
        if (direction === -1) {
            if (removeCount) {
                removedRecords = [];
                removedItems = [];
                removeStart = (me.endIndex - removeCount) + 1;
                if (range) {
                    range.setStartBefore(elements[removeStart]);
                    range.setEndAfter(elements[me.endIndex]);
                    range.deleteContents();
                    for (i = removeStart; i <= me.endIndex; i++) {
                        el = elements[i];
                        delete elements[i];
                        removedRecords.push(store.getByInternalId(el.getAttribute('data-recordId')));
                        removedItems.push(el);
                    }
                } else {
                    for (i = removeStart; i <= me.endIndex; i++) {
                        el = elements[i];
                        delete elements[i];
                        Ext.removeNode(el);
                        removedRecords.push(store.getByInternalId(el.getAttribute('data-recordId')));
                        removedItems.push(el);
                    }
                }
                view.fireItemMutationEvent('itemremove', removedRecords, removeStart, removedItems, view);
                me.endIndex -= removeCount;
            }
            // Only do rendering if there are rows to render.
            // This could have been a remove only operation due to a view resize event.
            if (newRecords.length) {
                // grab all nodes rendered, not just the data rows
                result = view.bufferRender(newRecords, me.startIndex -= recCount);
                children = result.children;
                for (i = 0; i < recCount; i++) {
                    elements[me.startIndex + i] = children[i];
                }
                nodeContainer.insertBefore(result.fragment, nodeContainer.firstChild);
                // pass the new DOM to any interested parties
                view.fireItemMutationEvent('itemadd', newRecords, me.startIndex, children, view);
            }
        } else // Scrolling down (content moved up - new content needed at bottom, remove from top)
        {
            if (removeCount) {
                removedRecords = [];
                removedItems = [];
                removeEnd = me.startIndex + removeCount;
                if (range) {
                    range.setStartBefore(elements[me.startIndex]);
                    range.setEndAfter(elements[removeEnd - 1]);
                    range.deleteContents();
                    for (i = me.startIndex; i < removeEnd; i++) {
                        el = elements[i];
                        delete elements[i];
                        removedRecords.push(store.getByInternalId(el.getAttribute('data-recordId')));
                        removedItems.push(el);
                    }
                } else {
                    for (i = me.startIndex; i < removeEnd; i++) {
                        el = elements[i];
                        delete elements[i];
                        Ext.removeNode(el);
                        removedRecords.push(store.getByInternalId(el.getAttribute('data-recordId')));
                        removedItems.push(el);
                    }
                }
                view.fireItemMutationEvent('itemremove', removedRecords, me.startIndex, removedItems, view);
                me.startIndex = removeEnd;
            }
            // grab all nodes rendered, not just the data rows
            result = view.bufferRender(newRecords, me.endIndex + 1);
            children = result.children;
            for (i = 0; i < recCount; i++) {
                elements[me.endIndex += 1] = children[i];
            }
            nodeContainer.appendChild(result.fragment);
            // pass the new DOM to any interested parties
            view.fireItemMutationEvent('itemadd', newRecords, me.endIndex + 1, children, view);
        }
        // Keep count consistent.
        me.count = me.endIndex - me.startIndex + 1;
        return children;
    },
    sumHeights: function() {
        var result = 0,
            elements = this.elements,
            i;
        for (i = this.startIndex; i <= this.endIndex; i++) {
            result += elements[i].offsetHeight;
        }
        return result;
    }
}, function() {
    Ext.dom.CompositeElementLite.importElementMethods.call(this);
});

Ext.define('Ext.scroll.TableScroller', {
    extend: 'Ext.scroll.Scroller',
    alias: 'scroller.table',
    config: {
        lockingScroller: null
    },
    privates: {
        getEnsureVisibleXY: function(el, options) {
            var lockingScroller = this.getLockingScroller(),
                position = this.getPosition(),
                newPosition;
            if (el && el.element && !el.isElement) {
                options = el;
                el = options.element;
            }
            options = options || {};
            if (lockingScroller) {
                position.y = lockingScroller.position.y;
            }
            newPosition = Ext.fly(el).getScrollIntoViewXY(this.getElement(), position.x, position.y);
            newPosition.x = (options.x === false) ? position.x : newPosition.x;
            if (lockingScroller) {
                newPosition.y = (options.y === false) ? position.y : Ext.fly(el).getScrollIntoViewXY(lockingScroller.getElement(), position.x, position.y).y;
            }
            return newPosition;
        },
        doScrollTo: function(x, y, animate) {
            var lockingScroller, lockedPromise, ret;
            if (y != null) {
                lockingScroller = this.getLockingScroller();
                if (lockingScroller) {
                    lockedPromise = lockingScroller.doScrollTo(null, y, animate);
                    y = null;
                }
            }
            ret = this.callParent([
                x,
                y,
                animate
            ]);
            if (lockedPromise) {
                ret = Ext.Promise.all([
                    ret,
                    lockedPromise
                ]);
            }
            return ret;
        },
        restoreState: function() {
            var me = this,
                el = me.getScrollElement(),
                lockingScroller = me.getLockingScroller(),
                trackingScrollTop;
            if (el) {
                // scrollTop is managed by the LockingScroller if there is one.
                trackingScrollTop = lockingScroller ? lockingScroller.trackingScrollTop : me.trackingScrollTop;
                // Only restore state if has been previously captured! For example,
                // floaters probably have not been hidden before initially shown.
                if (trackingScrollTop !== undefined) {
                    // If we're restoring the scroll position, we don't want to publish
                    // scroll events since the scroll position should not have changed
                    // at all as far as the user is concerned, so just do it silently
                    // while ensuring we maintain the correct internal state. 50ms is
                    // enough to capture the async scroll events, anything after that
                    // we re-enable.
                    if (!me.restoreTimer) {
                        me.restoreTimer = Ext.defer(function() {
                            me.restoreTimer = null;
                        }, 50);
                    }
                    me.doScrollTo(me.trackingScrollLeft, trackingScrollTop, false);
                }
            }
        }
    }
});
// Do not discard the state.
// It may need to be restored again.

/**
 * This class encapsulates the user interface for a tabular data set.
 * It acts as a centralized manager for controlling the various interface
 * elements of the view. This includes handling events, such as row and cell
 * level based DOM events. It also reacts to events from the underlying {@link Ext.selection.Model}
 * to provide visual feedback to the user.
 *
 * This class does not provide ways to manipulate the underlying data of the configured
 * {@link Ext.data.Store}.
 *
 * This is the base class for both {@link Ext.grid.View} and {@link Ext.tree.View} and is not
 * to be used directly.
 */
Ext.define('Ext.view.Table', {
    extend: 'Ext.view.View',
    xtype: [
        'tableview',
        'gridview'
    ],
    alternateClassName: 'Ext.grid.View',
    requires: [
        'Ext.grid.CellContext',
        'Ext.view.TableLayout',
        'Ext.grid.locking.RowSynchronizer',
        'Ext.view.NodeCache',
        'Ext.util.DelayedTask',
        'Ext.util.MixedCollection',
        'Ext.scroll.TableScroller'
    ],
    // View is now queryable by virtue of having managed widgets either in widget columns
    // or in RowWidget plugin
    mixins: [
        'Ext.mixin.Queryable'
    ],
    /**
     * @property {Boolean}
     * `true` in this class to identify an object as an instantiated Ext.view.TableView,
     * or subclass thereof.
     */
    isTableView: true,
    config: {
        selectionModel: {
            type: 'rowmodel'
        }
    },
    inheritableStatics: {
        // Events a TableView may fire. Used by Ext.grid.locking.View to relay events to its
        // ownerGrid in order to quack like a genuine Ext.table.View.
        // 
        // The events below are to be relayed only from the normal side view because the events
        // are relayed from the selection model, so both sides will fire them.
        /**
         * @private
         * @static
         * @inheritable
         */
        normalSideEvents: [
            "deselect",
            "select",
            "beforedeselect",
            "beforeselect",
            "selectionchange"
        ],
        // These events are relayed from both views because they are fired independently.
        /**
         * @private
         * @static
         * @inheritable
         */
        events: [
            "blur",
            "focus",
            "move",
            "resize",
            "destroy",
            "beforedestroy",
            "boxready",
            "afterrender",
            "render",
            "beforerender",
            "removed",
            "hide",
            "beforehide",
            "show",
            "beforeshow",
            "enable",
            "disable",
            "added",
            "deactivate",
            "beforedeactivate",
            "activate",
            "beforeactivate",
            "cellkeydown",
            "beforecellkeydown",
            "cellmouseup",
            "beforecellmouseup",
            "cellmousedown",
            "beforecellmousedown",
            "cellcontextmenu",
            "beforecellcontextmenu",
            "celldblclick",
            "beforecelldblclick",
            "cellclick",
            "beforecellclick",
            "refresh",
            "itemremove",
            "itemadd",
            "beforeitemupdate",
            "itemupdate",
            "viewready",
            "beforerefresh",
            "unhighlightitem",
            "highlightitem",
            "focuschange",
            "containerkeydown",
            "containercontextmenu",
            "containerdblclick",
            "containerclick",
            "containermouseout",
            "containermouseover",
            "containermouseup",
            "containermousedown",
            "beforecontainerkeydown",
            "beforecontainercontextmenu",
            "beforecontainerdblclick",
            "beforecontainerclick",
            "beforecontainermouseout",
            "beforecontainermouseover",
            "beforecontainermouseup",
            "beforecontainermousedown",
            "itemkeydown",
            "itemcontextmenu",
            "itemdblclick",
            "itemclick",
            "itemmouseleave",
            "itemmouseenter",
            "itemmouseup",
            "itemmousedown",
            "rowclick",
            "rowcontextmenu",
            "rowdblclick",
            "rowkeydown",
            "rowmouseup",
            "rowmousedown",
            "rowkeydown",
            "beforeitemkeydown",
            "beforeitemcontextmenu",
            "beforeitemdblclick",
            "beforeitemclick",
            "beforeitemmouseleave",
            "beforeitemmouseenter",
            "beforeitemmouseup",
            "beforeitemmousedown",
            "statesave",
            "beforestatesave",
            "staterestore",
            "beforestaterestore",
            "uievent",
            "groupcollapse",
            "groupexpand",
            "scroll"
        ]
    },
    scrollable: true,
    componentLayout: 'tableview',
    baseCls: Ext.baseCSSPrefix + 'grid-view',
    unselectableCls: Ext.baseCSSPrefix + 'unselectable',
    /**
     * @cfg {String} [firstCls='x-grid-cell-first']
     * A CSS class to add to the *first* cell in every row to enable special styling
     * for the first column.
     * If no styling is needed on the first column, this may be configured as `null`.
     */
    firstCls: Ext.baseCSSPrefix + 'grid-cell-first',
    /**
     * @cfg {String} [lastCls='x-grid-cell-last']
     * A CSS class to add to the *last* cell in every row to enable special styling
     * for the last column.
     * If no styling is needed on the last column, this may be configured as `null`.
     */
    lastCls: Ext.baseCSSPrefix + 'grid-cell-last',
    itemCls: Ext.baseCSSPrefix + 'grid-item',
    selectedItemCls: Ext.baseCSSPrefix + 'grid-item-selected',
    selectedCellCls: Ext.baseCSSPrefix + 'grid-cell-selected',
    focusedItemCls: Ext.baseCSSPrefix + 'grid-item-focused',
    overItemCls: Ext.baseCSSPrefix + 'grid-item-over',
    altRowCls: Ext.baseCSSPrefix + 'grid-item-alt',
    dirtyCls: Ext.baseCSSPrefix + 'grid-dirty-cell',
    rowClsRe: new RegExp('(?:^|\\s*)' + Ext.baseCSSPrefix + 'grid-item-alt(?:\\s+|$)', 'g'),
    cellRe: new RegExp(Ext.baseCSSPrefix + 'grid-cell-([^\\s]+)(?:\\s|$)', ''),
    positionBody: true,
    positionCells: false,
    stripeOnUpdate: null,
    /**
     * @property {Boolean} actionableMode
     * This value is `true` when the grid has been set to actionable mode by the user.
     *
     * See http://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#grid
     * @readonly
     */
    actionableMode: false,
    // cfg docs inherited
    trackOver: true,
    /**
     * Override this function to apply custom CSS classes to rows during rendering. This function
     * should return the CSS class name (or empty string '' for none) that will be added
     * to the row's wrapping element. To apply multiple class names, simply return them
     * space-delimited within the string (e.g. 'my-class another-class').
     * Example usage:
     *
     *     viewConfig: {
     *         getRowClass: function(record, rowIndex, rowParams, store){
     *             return record.get("valid") ? "row-valid" : "row-error";
     *         }
     *     }
     *
     * @param {Ext.data.Model} record The record corresponding to the current row.
     * @param {Number} index The row index.
     * @param {Object} rowParams **DEPRECATED.** For row body use the
     * {@link Ext.grid.feature.RowBody#getAdditionalData getAdditionalData} method of the rowbody
     * feature.
     * @param {Ext.data.Store} store The store this grid is bound to
     * @return {String} a CSS class name to add to the row.
     * @method
     */
    getRowClass: null,
    /**
     * @cfg {Boolean} stripeRows
     * True to stripe the rows.
     *
     * This causes the CSS class **`x-grid-row-alt`** to be added to alternate rows of
     * the grid. A default CSS rule is provided which sets a background color, but you can override
     * this with a rule which either overrides the **background-color** style using the `!important`
     * modifier, or which uses a CSS selector of higher specificity.
     */
    stripeRows: true,
    /**
     * @cfg {Boolean} markDirty
     * True to show the dirty cell indicator when a cell has been modified.
     */
    markDirty: true,
    /**
     * @cfg {Boolean} [enableTextSelection=false]
     * True to enable text selection inside this view.
     */
    ariaRole: 'rowgroup',
    rowAriaRole: 'row',
    cellAriaRole: 'gridcell',
    /**
     * @property {Ext.view.Table} ownerGrid
     * A reference to the top-level owning grid component. This is actually the TablePanel
     * so it could be a tree.
     * @readonly
     * @private
     * @since 5.0.0
     */
    /**
     * @method disable
     * Disable this view.
     *
     * Disables interaction with, and masks this view.
     *
     * Note that the encapsulating {@link Ext.panel.Table} panel is *not* disabled, and other
     * *docked* components such as the panel header, the column header container, and docked
     * toolbars will still be enabled. The panel itself can be disabled if that is required,
     * or individual docked components could be disabled.
     *
     * See {@link Ext.panel.Table #disableColumnHeaders disableColumnHeaders} and
     * {@link Ext.panel.Table #enableColumnHeaders enableColumnHeaders}.
     *
     * @param {Boolean} [silent=false] Passing `true` will suppress the `disable` event
     * from being fired.
     * @since 1.1.0
     */
    /* eslint-disable indent, max-len */
    /**
     * @cfg tpl
     * @private
     * Outer tpl for TableView just to satisfy the validation within AbstractView.initComponent.
     */
    tpl: [
        '{%',
        'view = values.view;',
        'if (!(columns = values.columns)) {',
        'columns = values.columns = view.ownerCt.getVisibleColumnManager().getColumns();',
        '}',
        'values.fullWidth = 0;',
        // Stamp cellWidth into the columns
        'for (i = 0, len = columns.length; i < len; i++) {',
        'column = columns[i];',
        'values.fullWidth += (column.cellWidth = column.lastBox ? column.lastBox.width : column.width || column.minWidth);',
        '}',
        // Add the row/column line classes to the container element.
        'tableCls=values.tableCls=[];',
        '%}',
        '<div class="' + Ext.baseCSSPrefix + 'grid-item-container" role="presentation" style="width:{fullWidth}px">',
        '{[view.renderTHead(values, out, parent)]}',
        '{%',
        'view.renderRows(values.rows, values.columns, values.viewStartIndex, out);',
        '%}',
        '{[view.renderTFoot(values, out, parent)]}',
        '</div>',
        // This template is shared on the Ext.view.Table prototype, so we have to
        // clean up the closed over variables. Otherwise we'll retain the last values
        // of the template execution!
        '{% ',
        'view = columns = column = null;',
        '%}',
        {
            definitions: 'var view, tableCls, columns, i, len, column;',
            priority: 0
        }
    ],
    outerRowTpl: [
        '<table id="{rowId}" role="presentation" ',
        'data-boundView="{view.id}" ',
        'data-recordId="{record.internalId}" ',
        'data-recordIndex="{recordIndex}" ',
        'class="{[values.itemClasses.join(" ")]}" cellpadding="0" cellspacing="0" style="{itemStyle};width:0">',
        // Do NOT emit a <TBODY> tag in case the nextTpl has to emit a <COLGROUP> column sizer element.
        // Browser will create a tbody tag when it encounters the first <TR>
        '{%',
        'this.nextTpl.applyOut(values, out, parent)',
        '%}',
        '</table>',
        {
            priority: 9999
        }
    ],
    rowTpl: [
        '{%',
        'var dataRowCls = values.recordIndex === -1 ? "" : " ' + Ext.baseCSSPrefix + 'grid-row";',
        '%}',
        '<tr class="{[values.rowClasses.join(" ")]} {[dataRowCls]}"',
        ' role="{rowRole}" {rowAttr:attributes}>',
        '<tpl for="columns">' + '{%',
        'parent.view.renderCell(values, parent.record, parent.recordIndex, parent.rowIndex, xindex - 1, out, parent)',
        '%}',
        '</tpl>',
        '</tr>',
        {
            priority: 0
        }
    ],
    cellTpl: [
        '<td class="{tdCls}" {tdAttr} {cellAttr:attributes}',
        ' style="width:{column.cellWidth}px;',
        '{% if(values.tdStyle){out.push(values.tdStyle);}%}"',
        '{% if (values.column.cellFocusable === false) {%}',
        ' role="presentation"',
        '{% } else { %}',
        ' role="{cellRole}" tabindex="-1"',
        '{% } %}',
        '  data-columnid="{[values.column.getItemId()]}">',
        '<div {unselectableAttr} class="' + Ext.baseCSSPrefix + 'grid-cell-inner {innerCls}" ',
        'style="text-align:{align};',
        '{% if (values.style) {out.push(values.style);} %}" ',
        '{cellInnerAttr:attributes}>{value}</div>',
        '</td>',
        {
            priority: 0
        }
    ],
    /* eslint-enable indent, max-len */
    /**
     * @private
     * Flag to disable refreshing SelectionModel on view refresh. Table views render rows
     * with selected CSS class already added if necessary.
     */
    refreshSelmodelOnRefresh: false,
    scrollableType: 'table',
    tableValues: {},
    // Private properties used during the row and cell render process.
    // They are allocated here on the prototype, and cleared/re-used to avoid GC churn
    // during repeated rendering.
    rowValues: {
        itemClasses: [],
        rowClasses: []
    },
    cellValues: {
        classes: [
            // for styles shared between cell and rowwrap
            Ext.baseCSSPrefix + 'grid-cell ' + Ext.baseCSSPrefix + 'grid-td'
        ]
    },
    /**
     * @event beforecellclick
     * Fired before the cell click is processed. Return false to cancel the default action.
     * @param {Ext.view.Table} this
     * @param {HTMLElement} td The TD element for the cell.
     * @param {Number} cellIndex
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the cell.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
     */
    /**
     * @event cellclick
     * Fired when table cell is clicked.
     * @param {Ext.view.Table} this
     * @param {HTMLElement} td The TD element for the cell.
     * @param {Number} cellIndex
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the cell.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
     */
    /**
     * @event beforecelldblclick
     * Fired before the cell double click is processed. Return false to cancel the default action.
     * @param {Ext.view.Table} this
     * @param {HTMLElement} td The TD element for the cell.
     * @param {Number} cellIndex
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the cell.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
     */
    /**
     * @event celldblclick
     * Fired when table cell is double clicked.
     * @param {Ext.view.Table} this
     * @param {HTMLElement} td The TD element for the cell.
     * @param {Number} cellIndex
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the cell.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
     */
    /**
     * @event beforecellcontextmenu
     * Fired before the cell right click is processed. Return false to cancel the default action.
     * @param {Ext.view.Table} this
     * @param {HTMLElement} td The TD element for the cell.
     * @param {Number} cellIndex
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the cell.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
     */
    /**
     * @event cellcontextmenu
     * Fired when table cell is right clicked.
     * @param {Ext.view.Table} this
     * @param {HTMLElement} td The TD element for the cell.
     * @param {Number} cellIndex
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the cell.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
     */
    /**
     * @event beforecellmousedown
     * Fired before the cell mouse down is processed. Return false to cancel the default action.
     * @param {Ext.view.Table} this
     * @param {HTMLElement} td The TD element for the cell.
     * @param {Number} cellIndex
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the cell.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
     */
    /**
     * @event cellmousedown
     * Fired when the mousedown event is captured on the cell.
     * @param {Ext.view.Table} this
     * @param {HTMLElement} td The TD element for the cell.
     * @param {Number} cellIndex
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the cell.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
     */
    /**
     * @event beforecellmouseup
     * Fired before the cell mouse up is processed. Return false to cancel the default action.
     * @param {Ext.view.Table} this
     * @param {HTMLElement} td The TD element for the cell.
     * @param {Number} cellIndex
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the cell.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
     */
    /**
     * @event cellmouseup
     * Fired when the mouseup event is captured on the cell.
     * @param {Ext.view.Table} this
     * @param {HTMLElement} td The TD element for the cell.
     * @param {Number} cellIndex
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the cell.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
     */
    /**
     * @event beforecellkeydown
     * Fired before the cell key down is processed. Return false to cancel the default action.
     * @param {Ext.view.Table} this
     * @param {HTMLElement} td The TD element for the cell.
     * @param {Number} cellIndex
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the cell.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
     */
    /**
     * @event cellkeydown
     * Fired when the keydown event is captured on the cell.
     * @param {Ext.view.Table} this
     * @param {HTMLElement} td The TD element for the cell.
     * @param {Number} cellIndex
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the cell.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
     */
    /**
     * @event rowclick
     * Fired when a table row is clicked.
     * @param {Ext.view.Table} this
     * @param {Ext.data.Model} record
     * @param {HTMLElement} element The TR element for the row.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines
     * the target row.
     */
    /**
     * @event rowdblclick
     * Fired when table row is double clicked.
     * @param {Ext.view.Table} this
     * @param {Ext.data.Model} record
     * @param {HTMLElement} element The TR element for the row.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines
     * the target row.
     */
    /**
     * @event rowcontextmenu
     * Fired when table row is right clicked.
     * @param {Ext.view.Table} this
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the row.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines
     * the target row.
     */
    /**
     * @event rowmousedown
     * Fired when the mousedown event is captured on the row.
     * @param {Ext.view.Table} this
     * @param {Ext.data.Model} record
     * @param {HTMLElement} tr The TR element for the row.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines
     * the target row.
     */
    /**
     * @event rowmouseup
     * Fired when the mouseup event is captured on the row.
     * @param {Ext.view.Table} this
     * @param {Ext.data.Model} record
     * @param {HTMLElement} element The TR element for the row.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines
     * the target row.
     */
    /**
     * @event rowkeydown
     * Fired when the keydown event is captured on the row.
     * @param {Ext.view.Table} this
     * @param {Ext.data.Model} record
     * @param {HTMLElement} element The TR element for the row.
     * @param {Number} rowIndex
     * @param {Ext.event.Event} e
     * @param {Ext.grid.CellContext} e.position A CellContext object which defines
     * the target row.
     */
    /**
     * @event beforerowexit
     * Fired when View is asked to exit Actionable mode in the current row,
     * and proceed to the previous/next row. If the handler returns `false`,
     * View processing is aborted.
     * @param {Ext.view.Table} this
     * @param {Ext.event.Event} keyEvent The key event that caused navigation.
     * @param {HTMLElement} prevRow Currently active table row.
     * @param {HTMLElement} nextRow Table row that is going to be focused and activated.
     * @param {Boolean} forward `true` if we're navigating forward (Tab), `false` if
     * navigating backward (Shift-Tab).
     */
    constructor: function(config) {
        // Adjust our base class if we are inside a TreePanel
        if (config.grid.isTree) {
            config.baseCls = Ext.baseCSSPrefix + 'tree-view';
        }
        this.callParent([
            config
        ]);
    },
    /**
     * @private
     * Returns `true` if this view has been configured with variableRowHeight (or this has been set
     * by a plugin/feature) which might insert arbitrary markup into a grid item. Or if at least one
     * visible column has been configured with variableRowHeight. Or if the store is grouped.
     */
    hasVariableRowHeight: function(fromLockingPartner) {
        var me = this;
        return me.variableRowHeight || me.store.isGrouped() || me.getVisibleColumnManager().hasVariableRowHeight() || (// If not already called from a locking partner, and there is a locking partner,
        // and the partner has variableRowHeight, then WE have variableRowHeight too.
        !fromLockingPartner && me.lockingPartner && me.lockingPartner.hasVariableRowHeight(true));
    },
    initComponent: function() {
        var me = this;
        if (me.columnLines) {
            me.addCls(me.grid.colLinesCls);
        }
        if (me.rowLines) {
            me.addCls(me.grid.rowLinesCls);
        }
        /**
         * @private
         * @property {Ext.dom.Fly} body
         * A flyweight Ext.Element which encapsulates a reference to the view's main row
         * containing element.
         * *Note that the `dom` reference will not be present until the first data refresh*
         */
        me.body = new Ext.dom.Fly();
        me.body.id = me.id + 'gridBody';
        // If trackOver has been turned off, null out the overCls because documented behaviour
        // in AbstractView is to turn trackOver on if overItemCls is set.
        if (!me.trackOver) {
            me.overItemCls = null;
        }
        me.headerCt.view = me;
        // Features need a reference to the grid.
        // Grid needs an immediate reference to its view so that the view
        // can reliably be got from the grid during initialization
        me.grid.view = me;
        me.initFeatures(me.grid);
        me.itemSelector = me.getItemSelector();
        me.all = new Ext.view.NodeCache(me);
        me.actionRowFly = new Ext.dom.Fly();
        me.callParent();
    },
    /**
     * @private
     * Create a config object for this view's selection model based upon the passed grid's
     * configurations.
     */
    applySelectionModel: function(selModel, oldSelModel) {
        var me = this,
            grid = me.ownerGrid,
            defaultType = selModel.type,
            disableSelection = me.disableSelection || grid.disableSelection;
        // If this is the initial configuration, pull overriding configs
        // in from the owning TablePanel.
        if (!oldSelModel) {
            // Favour a passed instance
            if (!(selModel && selModel.isSelectionModel)) {
                selModel = grid.selModel || selModel;
            }
        }
        if (selModel) {
            if (selModel.isSelectionModel) {
                selModel.allowDeselect = grid.allowDeselect || selModel.selectionMode !== 'SINGLE';
                selModel.locked = disableSelection;
            } else {
                if (typeof selModel === 'string') {
                    selModel = {
                        type: selModel
                    };
                } else // Copy obsolete selType property to type property now that selection models
                // are Factoryable
                // TODO: Remove selType config after deprecation period
                {
                    selModel.type = grid.selType || selModel.selType || selModel.type || defaultType;
                }
                if (!selModel.mode) {
                    if (grid.simpleSelect) {
                        selModel.mode = 'SIMPLE';
                    } else if (grid.multiSelect) {
                        selModel.mode = 'MULTI';
                    }
                }
                selModel = Ext.Factory.selection(Ext.apply({
                    allowDeselect: grid.allowDeselect,
                    locked: disableSelection
                }, selModel));
            }
        }
        return selModel;
    },
    updateSelectionModel: function(selModel, oldSelModel) {
        var me = this;
        if (oldSelModel) {
            oldSelModel.un({
                scope: me,
                lastselectedchanged: me.updateBindSelection,
                selectionchange: me.updateBindSelection
            });
            Ext.destroy(me.selModelRelayer);
        }
        me.selModelRelayer = me.relayEvents(selModel, [
            'selectionchange',
            'beforeselect',
            'beforedeselect',
            'select',
            'deselect',
            'focuschange'
        ]);
        selModel.on({
            scope: me,
            lastselectedchanged: me.updateBindSelection,
            selectionchange: me.updateBindSelection
        });
        me.selModel = selModel;
    },
    getVisibleColumnManager: function() {
        return this.ownerCt.getVisibleColumnManager();
    },
    getColumnManager: function() {
        return this.ownerCt.getColumnManager();
    },
    getTopLevelVisibleColumnManager: function() {
        // ownerGrid refers to the topmost responsible Ext.panel.Grid.
        // This could be this view's ownerCt, or if part of a locking arrangement, the locking grid
        return this.ownerGrid.getVisibleColumnManager();
    },
    /**
     * @private
     * Move a grid column from one position to another
     * @param {Number} fromIdx The index from which to move columns
     * @param {Number} toIdx The index at which to insert columns.
     * @param {Number} [colsToMove=1] The number of columns to move beginning at the `fromIdx`
     */
    moveColumn: function(fromIdx, toIdx, colsToMove) {
        var me = this,
            multiMove = colsToMove > 1,
            range = multiMove && document.createRange ? document.createRange() : null,
            fragment = multiMove && !range ? document.createDocumentFragment() : null,
            destinationCellIdx = toIdx,
            colCount = me.getGridColumns().length,
            lastIndex = colCount - 1,
            i, j, rows, len, tr, cells, colGroups, doFirstLastClasses;
        doFirstLastClasses = (me.firstCls || me.lastCls) && (toIdx === 0 || toIdx === colCount || fromIdx === 0 || fromIdx === lastIndex);
        // Dragging between locked and unlocked side first refreshes the view,
        // and calls onHeaderMoved with fromIndex and toIndex the same.
        if (me.rendered && toIdx !== fromIdx) {
            // Grab all rows which have column cells in.
            // That is data rows.
            rows = me.el.query(me.rowSelector);
            for (i = 0 , len = rows.length; i < len; i++) {
                tr = rows[i];
                cells = tr.childNodes;
                // Keep first cell class and last cell class correct *only if needed*
                if (doFirstLastClasses) {
                    if (cells.length === 1) {
                        Ext.fly(cells[0]).addCls(me.firstCls);
                        Ext.fly(cells[0]).addCls(me.lastCls);
                        
                        continue;
                    }
                    if (fromIdx === 0) {
                        Ext.fly(cells[0]).removeCls(me.firstCls);
                        Ext.fly(cells[1]).addCls(me.firstCls);
                    } else if (fromIdx === lastIndex) {
                        Ext.fly(cells[lastIndex]).removeCls(me.lastCls);
                        Ext.fly(cells[lastIndex - 1]).addCls(me.lastCls);
                    }
                    if (toIdx === 0) {
                        Ext.fly(cells[0]).removeCls(me.firstCls);
                        Ext.fly(cells[fromIdx]).addCls(me.firstCls);
                    } else if (toIdx === colCount) {
                        Ext.fly(cells[lastIndex]).removeCls(me.lastCls);
                        Ext.fly(cells[fromIdx]).addCls(me.lastCls);
                    }
                }
                // Move multi using the best technique.
                // Extract a range straight into a fragment if possible.
                if (multiMove) {
                    if (range) {
                        range.setStartBefore(cells[fromIdx]);
                        range.setEndAfter(cells[fromIdx + colsToMove - 1]);
                        fragment = range.extractContents();
                    } else {
                        for (j = 0; j < colsToMove; j++) {
                            fragment.appendChild(cells[fromIdx]);
                        }
                    }
                    tr.insertBefore(fragment, cells[destinationCellIdx] || null);
                } else {
                    tr.insertBefore(cells[fromIdx], cells[destinationCellIdx] || null);
                }
            }
            // Shuffle the <col> elements in all <colgroup>s
            colGroups = me.el.query('colgroup');
            for (i = 0 , len = colGroups.length; i < len; i++) {
                // Extract the colgroup
                tr = colGroups[i];
                // Move multi using the best technique.
                // Extract a range straight into a fragment if possible.
                if (multiMove) {
                    if (range) {
                        range.setStartBefore(tr.childNodes[fromIdx]);
                        range.setEndAfter(tr.childNodes[fromIdx + colsToMove - 1]);
                        fragment = range.extractContents();
                    } else {
                        for (j = 0; j < colsToMove; j++) {
                            fragment.appendChild(tr.childNodes[fromIdx]);
                        }
                    }
                    tr.insertBefore(fragment, tr.childNodes[destinationCellIdx] || null);
                } else {
                    tr.insertBefore(tr.childNodes[fromIdx], tr.childNodes[destinationCellIdx] || null);
                }
            }
        }
    },
    // scroll the view to the top
    scrollToTop: Ext.emptyFn,
    /**
     * Add a listener to the main view element. It will be destroyed with the view.
     * @private
     */
    addElListener: function(eventName, fn, scope) {
        this.mon(this, eventName, fn, scope, {
            element: 'el'
        });
    },
    /**
     * Get the leaf columns used for rendering the grid rows.
     * @private
     */
    getGridColumns: function() {
        return this.ownerCt.getVisibleColumnManager().getColumns();
    },
    /**
     * Get a leaf level header by index regardless of what the nesting
     * structure is.
     * @private
     * @param {Number} index The index
     */
    getHeaderAtIndex: function(index) {
        return this.ownerCt.getVisibleColumnManager().getHeaderAtIndex(index);
    },
    /**
     * Get the cell (td) for a particular record and column.
     * @param {Ext.data.Model} record
     * @param {Ext.grid.column.Column/Number} column
     * @param {Boolean} [returnElement=false] `true` to return an Ext.Element,
     * else a raw `<td>` is returned.
     * @private
     */
    getCell: function(record, column, returnElement) {
        var row = this.getRow(record),
            cell;
        if (row) {
            if (typeof column === 'number') {
                column = this.getHeaderAtIndex(column);
            }
            cell = row.querySelector(column.getCellSelector());
            return returnElement ? Ext.get(cell) : cell;
        }
    },
    /**
     * Get a reference to a feature
     * @param {String} id The id of the feature
     * @return {Ext.grid.feature.Feature} The feature. Undefined if not found
     */
    getFeature: function(id) {
        var features = this.featuresMC;
        if (features) {
            return features.get(id);
        }
    },
    /**
     * @private
     * Finds a features by ftype in the features array
     */
    findFeature: function(ftype) {
        if (this.features) {
            return Ext.Array.findBy(this.features, function(feature) {
                if (feature.ftype === ftype) {
                    return true;
                }
            });
        }
    },
    /**
     * Initializes each feature and bind it to this view.
     * @private
     */
    initFeatures: function(grid) {
        var me = this,
            features, feature, i, len;
        // Row container element emitted by tpl
        me.tpl = this.lookupTpl('tpl');
        // The rowTpl emits a <div>
        me.rowTpl = me.lookupTpl('rowTpl');
        me.addRowTpl(me.lookupTpl('outerRowTpl'));
        // Each cell is emitted by the cellTpl
        me.cellTpl = me.lookupTpl('cellTpl');
        me.featuresMC = new Ext.util.MixedCollection();
        features = me.features = me.constructFeatures();
        for (i = 0 , len = features ? features.length : 0; i < len; i++) {
            feature = features[i];
            // inject a reference to view and grid - Features need both
            feature.view = me;
            feature.grid = grid;
            me.featuresMC.add(feature);
            feature.init(grid);
        }
    },
    renderTHead: function(values, out, parent) {
        var headers = values.view.headerFns,
            len, i;
        if (headers) {
            for (i = 0 , len = headers.length; i < len; ++i) {
                headers[i].call(this, values, out, parent);
            }
        }
    },
    // Currently, we don't have ordering support for header/footer functions,
    // they will be pushed on at construction time. If the need does arise,
    // we can add this functionality in the future, but for now it's not
    // really necessary since currently only the summary feature uses this.
    addHeaderFn: function(fn) {
        var headers = this.headerFns;
        if (!headers) {
            headers = this.headerFns = [];
        }
        headers.push(fn);
    },
    renderTFoot: function(values, out, parent) {
        var footers = values.view.footerFns,
            len, i;
        if (footers) {
            for (i = 0 , len = footers.length; i < len; ++i) {
                footers[i].call(this, values, out, parent);
            }
        }
    },
    addFooterFn: function(fn) {
        var footers = this.footerFns;
        if (!footers) {
            footers = this.footerFns = [];
        }
        footers.push(fn);
    },
    addTpl: function(newTpl) {
        return this.insertTpl('tpl', newTpl);
    },
    addRowTpl: function(newTpl) {
        return this.insertTpl('rowTpl', newTpl);
    },
    addCellTpl: function(newTpl) {
        return this.insertTpl('cellTpl', newTpl);
    },
    insertTpl: function(which, newTpl) {
        var me = this,
            tpl, prevTpl;
        // Clone an instantiated XTemplate
        if (newTpl.isTemplate) {
            newTpl = Ext.Object.chain(newTpl);
        } else // If we have been passed an object of the form
        // {
        //      before: fn
        //      after: fn
        // }
        // Create a template from it using the object as the member configuration
        {
            newTpl = new Ext.XTemplate('{%this.nextTpl.applyOut(values, out, parent);%}', newTpl);
        }
        // Stop at the first TPL who's priority is less than the passed rowTpl
        for (tpl = me[which]; newTpl.priority < tpl.priority; tpl = tpl.nextTpl) {
            prevTpl = tpl;
        }
        // If we had skipped over some, link the previous one to the passed rowTpl
        if (prevTpl) {
            prevTpl.nextTpl = newTpl;
        } else // First one
        {
            me[which] = newTpl;
        }
        newTpl.nextTpl = tpl;
        return newTpl;
    },
    tplApplyOut: function(values, out, parent) {
        if (this.before) {
            if (this.before(values, out, parent) === false) {
                return;
            }
        }
        this.nextTpl.applyOut(values, out, parent);
        if (this.after) {
            this.after(values, out, parent);
        }
    },
    /**
     * @private
     * Converts the features array as configured, into an array of instantiated Feature objects.
     *
     * Must have no side effects other than Feature instantiation.
     *
     * MUST NOT update the this.features property, and MUST NOT update the instantiated Features.
     */
    constructFeatures: function() {
        var me = this,
            features = me.features,
            feature, result, i, len;
        if (features) {
            result = [];
            for (i = 0 , len = features.length; i < len; i++) {
                feature = features[i];
                if (!feature.isFeature) {
                    feature = Ext.create('feature.' + feature.ftype, feature);
                }
                result[i] = feature;
            }
        }
        return result;
    },
    beforeRender: function() {
        this.callParent();
        if (!this.enableTextSelection) {
            this.protoEl.unselectable();
        }
    },
    updateScrollable: function(scrollable) {
        var me = this,
            ownerGrid = me.grid.ownerGrid;
        if (!ownerGrid.lockable && scrollable.isScroller && scrollable !== ownerGrid.scrollable) {
            ownerGrid.scrollable = scrollable;
        }
    },
    afterComponentLayout: function(width, height, oldWidth, oldHeight) {
        var me = this,
            ownerGrid = me.grid.ownerGrid;
        if (ownerGrid.mixins.lockable) {
            ownerGrid.syncLockableLayout();
        }
        me.callParent([
            width,
            height,
            oldWidth,
            oldHeight
        ]);
    },
    getElConfig: function() {
        var config = this.callParent();
        // Table views are special in this regard; they should not have
        // aria-hidden and aria-disabled attributes.
        delete config['aria-hidden'];
        delete config['aria-disabled'];
        return config;
    },
    onBindStore: function(store) {
        var me = this,
            bufferedRenderer = me.bufferedRenderer;
        if (bufferedRenderer && bufferedRenderer.store !== store) {
            bufferedRenderer.bindStore(store);
        }
        // Clear view el unless we're reconfiguring - a refresh will happen.
        if (me.all && me.all.getCount() && !me.grid.reconfiguring) {
            me.clearViewEl(true);
        }
        me.callParent(arguments);
    },
    onOwnerGridHide: function() {
        var scroller = this.getScrollable(),
            bufferedRenderer = this.bufferedRederer;
        // Hide using display sets scroll to zero.
        // We should not tell any partners about this.
        if (scroller) {
            scroller.suspendPartnerSync();
        }
        // A buffered renderer should also not respond to that scroll.
        if (bufferedRenderer) {
            bufferedRenderer.disable();
        }
    },
    onOwnerGridShow: function() {
        var scroller = this.getScrollable(),
            bufferedRenderer = this.bufferedRederer;
        // Hide using display sets scroll to zero.
        // We should not tell any partners about this.
        if (scroller) {
            scroller.resumePartnerSync();
        }
        // A buffered renderer should also not respond to that scroll.
        if (bufferedRenderer) {
            bufferedRenderer.enable();
        }
    },
    getStoreListeners: function(store) {
        var me = this,
            result = me.callParent([
                store
            ]),
            dataSource = me.dataSource;
        if (dataSource && dataSource.isFeatureStore) {
            // GroupStore triggers a refresh on add/remove, we don't want to have
            // it process twice
            delete result.add;
            delete result.remove;
        }
        // The BufferedRenderer handles clearing down the view on its onStoreClear method
        if (me.bufferedRenderer) {
            delete result.clear;
        }
        result.beforepageremove = me.beforePageRemove;
        return result;
    },
    beforePageRemove: function(pageMap, pageNumber) {
        var rows = this.all,
            pageSize = pageMap.getPageSize();
        // If the rendered block needs the page, access it which moves it
        // to the end of the LRU cache, and veto removal.
        if (rows.startIndex >= (pageNumber - 1) * pageSize && rows.endIndex <= (pageNumber * pageSize - 1)) {
            pageMap.get(pageNumber);
            return false;
        }
    },
    /**
     * @private
     * Template method implemented starting at the AbstractView class.
     */
    onViewScroll: function(scroller, x, y) {
        // We ignore scrolling caused by focusing
        if (!this.destroyed && !this.ignoreScroll) {
            this.callParent([
                scroller,
                x,
                y
            ]);
        }
    },
    /** 
     * @private
     * Create the DOM element which encapsulates the passed record.
     * Used when updating existing rows, so drills down into resulting structure.
     */
    createRowElement: function(record, index, updateColumns) {
        var me = this,
            div = me.renderBuffer,
            tplData = me.collectData([
                record
            ], index),
            result;
        tplData.columns = updateColumns;
        me.tpl.overwrite(div, tplData);
        // We don't want references to be retained on the prototype
        me.cleanupData();
        // Return first element within node containing element
        result = div.dom.querySelector(me.getNodeContainerSelector()).firstChild;
        Ext.fly(result).saveTabbableState(me.saveTabOptions);
        return result;
    },
    /** 
     * @private
     * Override so that we can use a quicker way to access the row nodes.
     * They are simply all child nodes of the nodeContainer element.
     */
    bufferRender: function(records, index) {
        var me = this,
            div = me.renderBuffer,
            range = document.createRange ? document.createRange() : null,
            result;
        me.tpl.overwrite(div, me.collectData(records, index));
        // We don't want references to be retained on the prototype
        me.cleanupData();
        // Newly added rows must be untabbable by default
        div.saveTabbableState(me.saveTabOptions);
        div = div.dom.querySelector(me.getNodeContainerSelector());
        if (range) {
            range.selectNodeContents(div);
            result = range.extractContents();
        } else {
            result = document.createDocumentFragment();
            while (div.firstChild) {
                result.appendChild(div.firstChild);
            }
        }
        return {
            fragment: result,
            children: Ext.Array.toArray(result.childNodes)
        };
    },
    collectData: function(records, startIndex) {
        var me = this,
            tableValues = me.tableValues;
        me.rowValues.view = me;
        tableValues.view = me;
        tableValues.rows = records;
        tableValues.columns = null;
        tableValues.viewStartIndex = startIndex;
        tableValues.tableStyle = 'width:' + me.headerCt.getTableWidth() + 'px';
        return tableValues;
    },
    cleanupData: function() {
        var tableValues = this.tableValues;
        // Clean up references on the prototype
        tableValues.view = tableValues.columns = tableValues.rows = this.rowValues.view = null;
    },
    /** 
     * @private
     * Called when the table changes height.
     * For example, see examples/grid/group-summary-grid.html
     * If we have flexed column headers, we need to update the header layout
     * because it may have to accommodate (or cease to accommodate) a vertical scrollbar.
     * Only do this on platforms which have a space-consuming scrollbar.
     * Only do it when vertical scrolling is enabled.
     */
    refreshSize: function(forceLayout) {
        var me = this,
            bodySelector = me.getBodySelector(),
            lockingPartner = me.lockingPartner,
            restoreFocus;
        // keeping the current position to be restored by afterComponentLayout
        // once it's called because of resumeLayouts.
        if (!me.actionableMode) {
            restoreFocus = me.saveFocusState();
        }
        // On every update of the layout system due to data update, capture the view's main element
        // in our private flyweight.
        // IF there *is* a main element. Some TplFactories emit naked rows.
        if (bodySelector) {
            // use "down" instead of "child" because the grid table element is not a direct
            // child of the view element when a touch scroller is in use.
            me.body.attach(me.el.dom.querySelector(bodySelector));
        }
        if (!me.hasLoadingHeight) {
            // Suspend layouts in case the superclass requests a layout. We might too, so they
            // must be coalesced.
            Ext.suspendLayouts();
            me.callParent([
                forceLayout
            ]);
            // We only need to adjust for height changes in the data if we, or any visible columns
            // have been configured with variableRowHeight: true
            // OR, if we are being passed the forceUpdate flag which is passed when the view's
            // item count changes.
            if (forceLayout || (me.hasVariableRowHeight() && me.dataSource.getCount())) {
                me.grid.updateLayout();
            }
            // Only flush layouts if there's no *visible* locking partner, or
            // the two partners have both refreshed to the same rendered block size.
            // If we are the first of a locking view pair, refreshing in response to a change of
            // view height, our rendered block size will be out of sync with our partner's
            // so row height equalization (called as part of a layout) will walk off the end.
            // This must be deferred until both views have refreshed to the same size.
            Ext.resumeLayouts(!lockingPartner || !lockingPartner.grid.isVisible() || (lockingPartner.all.getCount() === me.all.getCount()));
            // Restore focus to the previous position in case layout cycles
            // scrolled the view back up.
            if (restoreFocus) {
                restoreFocus();
            }
        }
    },
    /**
     * @private
     * TableView is unable to lay out in isolation. It acquires information from
     * the HeaderContainer, so a request to layout a TableView MUST propagate upwards
     * into the grid.
     */
    isLayoutRoot: function() {
        return false;
    },
    clearViewEl: function(leaveNodeContainer) {
        var me = this,
            nodeContainer;
        // AbstractView will clear the view correctly
        // It also resets the scrollrange.
        if (me.rendered) {
            me.callParent();
            // If we are also removing the noe container, destroy it.
            if (!leaveNodeContainer) {
                nodeContainer = Ext.get(me.getNodeContainer());
                if (nodeContainer && nodeContainer.dom !== me.getTargetEl().dom) {
                    nodeContainer.destroy();
                }
            }
        }
    },
    getRefItems: function(deep) {
        // @private
        // CQ interface
        var me = this,
            rowContexts = me.ownerGrid.liveRowContexts,
            isLocked = !!me.isLockedView,
            result = me.callParent([
                deep
            ]),
            widgetCount, i, widgets, widget, recordId;
        // Add the widgets from the RowContexts.
        // If deep, add any descendant widgets within them.
        for (recordId in rowContexts) {
            widgets = rowContexts[recordId].getWidgets();
            widgetCount = widgets.length;
            for (i = 0; i < widgetCount; i++) {
                widget = widgets[i];
                // If we're in a lockable assembly, check that we're in the same side
                if (isLocked === widget.$fromLocked) {
                    result[result.length] = widget;
                    if (deep && widget.getRefItems) {
                        result.push.apply(result, widget.getRefItems(true));
                    }
                }
            }
        }
        return result;
    },
    getMaskTarget: function() {
        // Masking a TableView masks its IMMEDIATE parent GridPanel's body.
        // Disabling/enabling a locking view relays the call to both child views.
        return this.ownerCt.body;
    },
    statics: {
        getBoundView: function(node) {
            return Ext.getCmp(node.getAttribute('data-boundView'));
        }
    },
    getRecord: function(node) {
        // If store.destroy has been called before some delayed event fires on a node,
        // we must ignore the event.
        if (this.store.destroyed) {
            return;
        }
        if (node.isModel) {
            return node;
        }
        node = this.getNode(node);
        // Must use the internalId stamped into the DOM because if this is called after a sort
        // or filter, but before the refresh, then the "data-recordIndex" will be stale.
        if (node) {
            return this.dataSource.getByInternalId(node.getAttribute('data-recordId'));
        }
    },
    indexOf: function(node) {
        node = this.getNode(node);
        if (!node && node !== 0) {
            return -1;
        }
        return this.all.indexOf(node);
    },
    indexInStore: function(node) {
        // We cannot use the stamped in data-recordindex because that is the index in the original
        // configured store NOT the index in the dataSource that is being used -
        // that may be a GroupStore.
        return node ? this.dataSource.indexOf(this.getRecord(node)) : -1;
    },
    indexOfRow: function(record) {
        var dataSource = this.dataSource,
            idx;
        if (record.isCollapsedPlaceholder) {
            idx = dataSource.indexOfPlaceholder(record);
        } else {
            idx = dataSource.indexOf(record);
        }
        return idx;
    },
    renderRows: function(rows, columns, viewStartIndex, out) {
        var me = this,
            rowValues = me.rowValues,
            rowCount = rows.length,
            i;
        rowValues.view = me;
        rowValues.columns = columns;
        // The roles are the same for all data rows and cells
        rowValues.rowRole = me.rowAriaRole;
        me.cellValues.cellRole = me.cellAriaRole;
        for (i = 0; i < rowCount; i++ , viewStartIndex++) {
            rowValues.itemClasses.length = rowValues.rowClasses.length = 0;
            me.renderRow(rows[i], viewStartIndex, out);
        }
        // Dereference objects since rowValues is a persistent on our prototype
        rowValues.view = rowValues.columns = rowValues.record = null;
    },
    /* Alternative column sizer element renderer.
    renderTHeadColumnSizer: function(values, out) {
        var columns = this.getGridColumns(),
            len = columns.length, i,
            column, width;

        out.push('<thead><tr class="' + Ext.baseCSSPrefix + 'grid-header-row">');
        
        for (i = 0; i < len; i++) {
            column = columns[i];
            
            width = column.lastBox 
                ? column.lastBox.width
                : Ext.grid.header.Container.prototype.defaultWidth;
            
            out.push(
                '<th class="', Ext.baseCSSPrefix, 'grid-cell-', columns[i].getItemId(),
                '" style="width:' + width + 'px"></th>'
            );
        }
        out.
        push('</tr></thead>');
    },
    */
    renderColumnSizer: function(values, out) {
        var columns = values.columns || this.getGridColumns(),
            len = columns.length,
            i, column, width;
        out.push('<colgroup role="presentation">');
        for (i = 0; i < len; i++) {
            column = columns[i];
            width = column.cellWidth ? column.cellWidth : Ext.grid.header.Container.prototype.defaultWidth;
            out.push('<col role="presentation" class="', Ext.baseCSSPrefix, 'grid-cell-', columns[i].getItemId(), '" style="width:' + width + 'px">');
        }
        out.push('</colgroup>');
    },
    /**
     * @private
     * Renders the HTML markup string for a single row into the passed array as a sequence
     * of strings, or returns the HTML markup for a single row.
     *
     * @param {Ext.data.Model} record The record to render.
     * @param {Number} rowIdx The index of the row
     * @param {String[]} [out] A string array onto which to append the resulting HTML string.
     * If omitted, the resulting HTML string is returned.
     * @return {String} **only when the out parameter is omitted** The resulting HTML string.
     */
    renderRow: function(record, rowIdx, out) {
        var me = this,
            isMetadataRecord = rowIdx === -1,
            selModel = me.selectionModel,
            rowValues = me.rowValues,
            itemClasses = rowValues.itemClasses,
            rowClasses = rowValues.rowClasses,
            itemCls = me.itemCls,
            cls,
            rowTpl = me.rowTpl;
        // Define the rowAttr object now. We don't want to do it in the treeview treeRowTpl
        // because anything this is processed in a deferred callback (such as deferring initial
        // view refresh in gridview) could poke rowAttr that are then shared in tableview.rowTpl.
        // See EXTJSIV-9341.
        //
        // For example, the following shows the shared ref between a treeview's rowTpl nextTpl
        // and the superclass tableview.rowTpl:
        //
        //      tree.view.rowTpl.nextTpl === grid.view.rowTpl
        //
        rowValues.rowAttr = {};
        // Set up mandatory properties on rowValues
        rowValues.record = record;
        rowValues.recordId = record.internalId;
        // recordIndex is index in true store (NOT the data source - possibly a GroupStore)
        rowValues.recordIndex = me.store.indexOf(record);
        // rowIndex is the row number in the view.
        rowValues.rowIndex = rowIdx;
        rowValues.rowId = me.getRowId(record);
        rowValues.itemCls = rowValues.rowCls = '';
        if (!rowValues.columns) {
            rowValues.columns = me.ownerCt.getVisibleColumnManager().getColumns();
        }
        itemClasses.length = rowClasses.length = 0;
        // If it's a metadata record such as a summary record.
        // So do not decorate it with the regular CSS.
        // The Feature which renders it must know how to decorate it.
        if (!isMetadataRecord) {
            itemClasses[0] = itemCls;
            if (!me.ownerCt.disableSelection && selModel.isRowSelected) {
                // Selection class goes on the outermost row, so it goes into itemClasses
                if (selModel.isRowSelected(record)) {
                    itemClasses.push(me.selectedItemCls);
                }
            }
            if (me.stripeRows && rowIdx % 2 !== 0) {
                itemClasses.push(me.altRowCls);
            }
            if (me.getRowClass) {
                cls = me.getRowClass(record, rowIdx, null, me.dataSource);
                if (cls) {
                    rowClasses.push(cls);
                }
            }
        }
        if (out) {
            rowTpl.applyOut(rowValues, out, me.tableValues);
        } else {
            return rowTpl.apply(rowValues, me.tableValues);
        }
    },
    /**
     * @private
     * Emits the HTML representing a single grid cell into the passed output stream
     * (which is an array of strings).
     *
     * @param {Ext.grid.column.Column} column The column definition for which to render a cell.
     * @param {Ext.data.Model} record The record
     * @param {Number} recordIndex The row index (zero based within the {@link #store}) for which
     * to render the cell.
     * @param {Number} rowIndex The row index (zero based within this view for which
     * to render the cell.
     * @param {Number} columnIndex The column index (zero based) for which to render the cell.
     * @param {String[]} out The output stream into which the HTML strings are appended.
     */
    renderCell: function(column, record, recordIndex, rowIndex, columnIndex, out) {
        var me = this,
            renderer = column.renderer,
            fullIndex,
            selModel = me.selectionModel,
            cellValues = me.cellValues,
            classes = cellValues.classes,
            fieldValue = record.data[column.dataIndex],
            cellTpl = me.cellTpl,
            enableTextSelection = column.enableTextSelection,
            value, clsInsertPoint,
            lastFocused = me.navigationModel.getPosition();
        // Only use the view's setting if it's not been overridden on the column
        if (enableTextSelection == null) {
            enableTextSelection = me.enableTextSelection;
        }
        cellValues.record = record;
        cellValues.column = column;
        cellValues.recordIndex = recordIndex;
        cellValues.rowIndex = rowIndex;
        cellValues.columnIndex = cellValues.cellIndex = columnIndex;
        cellValues.align = column.textAlign;
        cellValues.innerCls = column.innerCls;
        cellValues.tdCls = cellValues.tdStyle = cellValues.tdAttr = cellValues.style = "";
        cellValues.unselectableAttr = enableTextSelection ? '' : 'unselectable="on"';
        // Begin setup of classes to add to cell
        classes[1] = column.getCellId();
        // On IE8, array[len] = 'foo' is twice as fast as array.push('foo')
        // So keep an insertion point and use assignment to help IE!
        clsInsertPoint = 2;
        if (renderer && renderer.call) {
            // Avoid expensive header index calculation (uses Array#indexOf)
            // if renderer doesn't use it.
            fullIndex = renderer.length > 4 ? me.ownerCt.columnManager.getHeaderIndex(column) : columnIndex;
            value = renderer.call(column.usingDefaultRenderer ? column : column.scope || me.ownerCt, fieldValue, cellValues, record, recordIndex, fullIndex, me.dataSource, me);
            if (cellValues.css) {
                // This warning attribute is used by the compat layer
                // TODO: remove when compat layer becomes deprecated
                record.cssWarning = true;
                cellValues.tdCls += ' ' + cellValues.css;
                cellValues.css = null;
            }
            // Add any tdCls which was added to the cellValues by the renderer.
            if (cellValues.tdCls) {
                classes[clsInsertPoint++] = cellValues.tdCls;
            }
        } else {
            value = fieldValue;
        }
        cellValues.value = (value == null || value.length === 0) ? column.emptyCellText : value;
        if (column.tdCls) {
            classes[clsInsertPoint++] = column.tdCls;
        }
        if (me.markDirty && record.dirty && record.isModified(column.dataIndex)) {
            classes[clsInsertPoint++] = me.dirtyCls;
            if (column.dirtyTextElementId) {
                cellValues.tdAttr = (cellValues.tdAttr ? cellValues.tdAttr + ' ' : '') + 'aria-describedby="' + column.dirtyTextElementId + '"';
            }
        }
        if (column.isFirstVisible) {
            classes[clsInsertPoint++] = me.firstCls;
        }
        if (column.isLastVisible) {
            classes[clsInsertPoint++] = me.lastCls;
        }
        if (!enableTextSelection) {
            classes[clsInsertPoint++] = me.unselectableCls;
        }
        if (selModel && (selModel.isCellModel || selModel.isSpreadsheetModel) && selModel.isCellSelected(me, recordIndex, column)) {
            classes[clsInsertPoint++] = me.selectedCellCls;
        }
        if (lastFocused && lastFocused.record.id === record.id && lastFocused.column === column) {
            classes[clsInsertPoint++] = me.focusedItemCls;
        }
        // Chop back array to only what we've set
        classes.length = clsInsertPoint;
        cellValues.tdCls = classes.join(' ');
        cellTpl.applyOut(cellValues, out);
        // Dereference objects since cellValues is a persistent var in the XTemplate's scope chain
        cellValues.column = cellValues.record = null;
    },
    /**
     * Returns the table row given the passed Record, or index or node.
     * @param {HTMLElement/String/Number/Ext.data.Model} nodeInfo The node or record, or row index.
     * to return the top level row.
     * @return {HTMLElement} The node or null if it wasn't found
     */
    getRow: function(nodeInfo) {
        var me = this,
            rowSelector = me.rowSelector;
        if ((!nodeInfo && nodeInfo !== 0) || !me.rendered) {
            return null;
        }
        // An id
        if (Ext.isString(nodeInfo)) {
            nodeInfo = Ext.getDom(nodeInfo);
            return nodeInfo && nodeInfo.querySelectorAll(rowSelector)[0];
        }
        // Row index
        if (Ext.isNumber(nodeInfo)) {
            nodeInfo = me.all.item(nodeInfo, true);
            return nodeInfo && nodeInfo.querySelectorAll(rowSelector)[0];
        }
        // Record
        if (nodeInfo.isModel) {
            return me.getRowByRecord(nodeInfo);
        }
        // If we were passed an event, use its target
        nodeInfo = Ext.fly(nodeInfo.target || nodeInfo);
        // Passed an item, go down and get the row
        if (nodeInfo.is(me.itemSelector)) {
            return me.getRowFromItem(nodeInfo);
        }
        // Passed a child element of a row
        return nodeInfo.findParent(rowSelector, me.getTargetEl());
    },
    // already an HTMLElement
    getRowId: function(record) {
        return this.id + '-record-' + record.internalId;
    },
    constructRowId: function(internalId) {
        return this.id + '-record-' + internalId;
    },
    getNodeById: function(id) {
        id = this.constructRowId(id);
        return this.retrieveNode(id, false);
    },
    getRowById: function(id) {
        id = this.constructRowId(id);
        return this.retrieveNode(id, true);
    },
    getNodeByRecord: function(record) {
        return this.retrieveNode(this.getRowId(record), false);
    },
    getRowByRecord: function(record) {
        return this.retrieveNode(this.getRowId(record), true);
    },
    getRowFromItem: function(item) {
        var rows = Ext.getDom(item).tBodies[0].childNodes,
            len = rows.length,
            i;
        for (i = 0; i < len; i++) {
            if (Ext.fly(rows[i]).is(this.rowSelector)) {
                return rows[i];
            }
        }
    },
    retrieveNode: function(id, dataRow) {
        var result = this.el.getById(id, true);
        if (dataRow && result) {
            return result.querySelector(this.rowSelector, true);
        }
        return result;
    },
    // Links back from grid rows are installed by the XTemplate as data attributes
    updateIndexes: Ext.emptyFn,
    // Outer table
    bodySelector: 'div.' + Ext.baseCSSPrefix + 'grid-item-container',
    // Element which contains rows
    nodeContainerSelector: 'div.' + Ext.baseCSSPrefix + 'grid-item-container',
    // view item. This wraps a data row
    itemSelector: 'table.' + Ext.baseCSSPrefix + 'grid-item',
    // Grid row which contains cells as opposed to wrapping item.
    rowSelector: 'tr.' + Ext.baseCSSPrefix + 'grid-row',
    // cell
    cellSelector: 'td.' + Ext.baseCSSPrefix + 'grid-cell',
    // Select column sizers and cells.
    // This may target `<COL>` elements as well as `<TD>` elements
    // `<COLGROUP>` element is inserted if the first row does not have the regular cell pattern
    // (eg is a colspanning group header row)
    sizerSelector: '.' + Ext.baseCSSPrefix + 'grid-cell',
    innerSelector: 'div.' + Ext.baseCSSPrefix + 'grid-cell-inner',
    /**
     * Returns a CSS selector which selects the outermost element(s) in this view.
     */
    getBodySelector: function() {
        return this.bodySelector;
    },
    /**
     * Returns a CSS selector which selects the element(s) which define the width of a column.
     *
     * This is used by the {@link Ext.view.TableLayout} when resizing columns.
     *
     */
    getColumnSizerSelector: function(header) {
        var selector = this.sizerSelector + '-' + header.getItemId();
        return 'td' + selector + ',col' + selector;
    },
    /**
     * Returns a CSS selector which selects items of the view rendered by the outerRowTpl
     */
    getItemSelector: function() {
        return this.itemSelector;
    },
    /**
     * Returns a CSS selector which selects a particular column if the desired header is passed,
     * or a general cell selector is no parameter is passed.
     *
     * @param {Ext.grid.column.Column} [header] The column for which to return the selector. If
     * omitted, the general cell selector which matches **ant cell** will be returned.
     *
     */
    getCellSelector: function(header) {
        return header ? header.getCellSelector() : this.cellSelector;
    },
    /*
     * Returns a CSS selector which selects the content carrying element within cells.
     */
    getCellInnerSelector: function(header) {
        return this.getCellSelector(header) + ' ' + this.innerSelector;
    },
    /**
     * Adds a CSS Class to a specific row.
     * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement,
     * index or instance of a model representing this row
     * @param {String} cls
     */
    addRowCls: function(rowInfo, cls) {
        var row = this.getRow(rowInfo);
        if (row) {
            Ext.fly(row).addCls(cls);
        }
    },
    /**
     * Removes a CSS Class from a specific row.
     * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement,
     * index or instance of a model representing this row
     * @param {String} cls
     */
    removeRowCls: function(rowInfo, cls) {
        var row = this.getRow(rowInfo);
        if (row) {
            Ext.fly(row).removeCls(cls);
        }
    },
    // GridSelectionModel invokes onRowSelect as selection changes
    onRowSelect: function(rowIdx) {
        var me = this,
            rowNode;
        me.addItemCls(rowIdx, me.selectedItemCls);
        rowNode = me.getRow(rowIdx);
        if (rowNode) {
            rowNode.setAttribute('aria-selected', true);
        }
        if (Ext.isIE8) {
            me.repaintBorder(rowIdx + 1);
        }
    },
    // GridSelectionModel invokes onRowDeselect as selection changes
    onRowDeselect: function(rowIdx) {
        var me = this,
            rowNode;
        me.removeItemCls(rowIdx, me.selectedItemCls);
        rowNode = me.getRow(rowIdx);
        if (rowNode) {
            rowNode.removeAttribute('aria-selected');
        }
        if (Ext.isIE8) {
            me.repaintBorder(rowIdx + 1);
        }
    },
    onCellSelect: function(position) {
        var cell = this.getCellByPosition(position, true);
        if (cell) {
            Ext.fly(cell).addCls(this.selectedCellCls);
            cell.setAttribute('aria-selected', true);
        }
    },
    onCellDeselect: function(position) {
        var cell = this.getCellByPosition(position, true);
        if (cell) {
            Ext.fly(cell).removeCls(this.selectedCellCls);
            cell.removeAttribute('aria-selected');
        }
    },
    // Old API. Used by tests now to test coercion of navigation from hidden column
    // to closest visible. Position.column includes all columns including hidden ones.
    getCellInclusive: function(position, returnDom) {
        var header, row, cell;
        if (position) {
            row = this.getRow(position.row);
            header = this.ownerCt.getColumnManager().getHeaderAtIndex(position.column);
            if (header && row) {
                cell = row.querySelector(this.getCellSelector(header));
                return returnDom ? cell : Ext.get(cell);
            }
        }
        return false;
    },
    getColumnByPosition: function(position) {
        var view, column;
        if (position) {
            column = position.column;
            // Previous column can be already destroyed via reconfigure
            if (column && !column.destroyed && column.isColumn) {
                return column;
            } else {
                view = position.view || this;
                // Column can be a number
                column = typeof column === 'number' ? column : position.colIdx;
                return view.getVisibleColumnManager().getHeaderAtIndex(column);
            }
        }
        return false;
    },
    getCellByPosition: function(position, returnDom) {
        var view, row, header, cell;
        if (position) {
            view = position.view || this;
            row = view.getRow(position.record || position.row);
            header = view.getColumnByPosition(position);
            if (header && row) {
                cell = row.querySelector(view.getCellSelector(header));
                return returnDom ? cell : Ext.get(cell);
            }
        }
        return false;
    },
    onFocusEnter: function(e) {
        // We need to react in a correct way to focus entering the TableView.
        // Much of this is based upon http://www.w3.org/TR/wai-aria-practices-1.1/#h-grid
        // specifically: "Once focus has been moved inside the grid, subsequent tab presses
        // that re-enter the grid shall return focus to the cell that last held focus."
        //
        // If an interior element is being focused, then if it is a cell, we enter navigable mode
        // at that cell.
        // If an interior element *within* a cell is being focused, we enter actionable mode
        // at that cell and focus that element.
        // If just the view itself is being focused we focus the lastFocused CellContext. 
        // This is the last cell position which the user navigated to in any mode, actionable
        // or navigable. It is maintained during navigation in navigable mode.
        // It is set upon focus leave if focus left during actionable mode - set to actionPosition.
        // actionPosition is cleared when actionable mode is exited.
        //
        // The important context is lastFocused.
        var me = this,
            fromComponent = e.fromComponent,
            navigationModel = me.getNavigationModel(),
            focusPosition, cell, focusTarget;
        // If a mousedown listener has synchronously focused an internal element
        // from outside and proceeded to process focus consequences, then the impending focusenter
        // MUST NOT process focus consequences.
        // See Ext.grid.NavigationModel#onCellMouseDown
        if (me.containsFocus) {
            return Ext.Component.prototype.onFocusEnter.call(me, e);
        }
        // FocusEnter while in actionable mode.
        if (me.actionableMode) {
            // If we own the actionPosition it must be due to a setActionPosition call
            // setting the actionPosition and then focusing the actionable element.
            // We need to disable view outer el focusing while focus is inside.
            if (me.actionPosition) {
                me.el.dom.setAttribute('tabIndex', '-1');
                me.cellFocused = true;
                return;
            }
            // Must have swapped sides of a lockable.
            // We don't know what we're focusing into yet.
            // So exit actionable mode.
            // We could be focusing a cell, in which case navigable mode is correct.
            // If we are focusing an interior element that is not a cell,
            // we will enter actionable mode.
            me.ownerGrid.setActionableMode(false);
        }
        // The underlying DOM event
        e = e.event;
        // We can only focus if there are rows in the row cache to focus *and* records
        // in the store to back them. Buffered Stores can produce a state where
        // the view is not cleared on the leading end of a reload operation, but the
        // store can be empty.
        if (!me.cellFocused && me.all.getCount() && me.dataSource.getCount()) {
            focusTarget = e.getTarget();
            // The View's el has been focused.
            // We now have to decide which cell to focus
            if (focusTarget === me.el.dom) {
                // This lastFocused value is set on mousedown on the scrollbar in IE/Edge.
                // Those browsers focus the element on mousedown on its scrollbar
                // which is not what we want, so throw focus back in this
                // situation.
                // See Ext.view.navigationModel for this being set.
                if (me.lastFocused === 'scrollbar') {
                    if (e.relatedTarget && e.relatedTarget.focus) {
                        e.relatedTarget.focus();
                    }
                    return;
                }
                focusPosition = me.getDefaultFocusPosition(fromComponent);
                // Not a descendant which we allow to carry focus. Focus the view el.
                if (!focusPosition) {
                    e.stopEvent();
                    me.el.focus();
                    return;
                }
                // We are entering navigable mode, so we have a focusPosition but no focusTarget
                focusTarget = null;
            }
            // Hit the invisible focus guard. This mean SHIT+TAB back into the grid.
            // Focus last cell.
            else if (focusTarget === me.tabGuardEl) {
                focusPosition = new Ext.grid.CellContext(me).setPosition(me.all.endIndex, me.getVisibleColumnManager().getColumns().length - 1);
                focusTarget = null;
            }
            // Now there are just two valid choices.
            // Focused a cell, or an interior element within a cell.
            // eslint-disable-next-line no-cond-assign
            else if (cell = e.getTarget(me.getCellSelector())) {
                // Programmatic focus of a cell...
                if (focusTarget === cell) {
                    // We are entering navigable mode, so we have a focusPosition but no focusTarget
                    focusPosition = new Ext.grid.CellContext(me).setPosition(me.getRecord(focusTarget), me.getHeaderByCell(cell));
                    focusTarget = null;
                }
                // If what is being focused an interior element, but is not a cell, we plan to enter
                // actionable mode. This will happen when an ActionColumn invokes a modal window
                // and that window is dismissed leading to automatic focus of the previously focused
                // element. This also happens when SHIFT+TAB moves back towards the view.
                // It navigated to the last tabbable element.
                // Testing whether the focusTarget isFocusable is a fix for IE. It can sometimes
                // fire a focus event with the .x-scroll-scroller as the target
                else if (focusTarget && Ext.fly(focusTarget).isFocusable() && me.el.contains(focusTarget)) {
                    // We are entering actionable mode, so we have a focusPosition and a focusTarget
                    focusPosition = new Ext.grid.CellContext(me).setPosition(me.getRecord(focusTarget), me.getHeaderByCell(cell));
                }
            }
        }
        // We must exit from the above code block with focusPosition set to a CellContext
        // which is going to be either the navigable or actionable position. If focusPosition
        // is null, we are not focusing the view.
        //
        // IF we are entering actionable mode, then focusTarget will be set to an internal
        // focusable element within the cell referenced by focusPosition.
        // We calculated a cell to focus on. Either from the target element,
        // or the last focused position
        if (focusPosition) {
            // Disable tabbability of elements within this view.
            me.toggleChildrenTabbability(false);
            // If we fall through to here with a focusTarget, it means that it's an internal
            // focusable element and we request to enter actionable mode at the focusPosition
            if (focusTarget) {
                // Tell actionable mode which element we want to focus.
                // By default it focuses the first focusable in the cell.
                focusPosition.target = focusTarget;
                // If we successfully entered actionable mode at the requested position,
                // prevent entering navigable mode by nulling the focusPosition, and focus
                // the intended target (setActionableMode will have focused the *first* tabbable
                // in the cell). If we were unsuccessful, then we must proceed with focusPosition
                // set in order to enter navigable mode here.
                if (me.ownerGrid.setActionableMode(true, focusPosition)) {
                    focusPosition = null;
                }
            }
            // Test again here.
            // If we successfully entered actionable mode, this will be null.
            // If the attempt failed, it should fall back to navigable mode.
            if (focusPosition) {
                navigationModel.setPosition(focusPosition, null, e, null, true);
            }
            // We now contain focus if that was successful
            me.cellFocused = me.el.contains(Ext.Element.getActiveElement());
            if (me.cellFocused) {
                me.el.dom.setAttribute('tabIndex', '-1');
            }
        }
        // Skip the AbstractView's implementation.
        // It initializes its NavModel differently.
        Ext.Component.prototype.onFocusEnter.call(me, e);
    },
    onFocusLeave: function(e) {
        var me = this,
            isLeavingGrid;
        // If the blur was caused by a refresh, we expect things to be refocused.
        if (!me.destroying && !me.refreshing) {
            // See if focus is really leaving the grid.
            // If we have a locking partner, and focus is going to that, we're NOT leaving the grid.
            isLeavingGrid = !me.lockingPartner || !e.toComponent || (e.toComponent !== me.lockingPartner && !me.lockingPartner.isAncestor(e.toComponent));
            // Ignore this event if we do not actually contain focus.
            // CellEditors are rendered into the view's encapsulating element,
            // So focusleave will fire when they are programmatically blurred.
            // We will not have focus at that point.
            if (me.cellFocused) {
                // Blur the focused cell unless we are navigating into a locking partner,
                // in which case, the focus of that will setPosition to the target
                // without an intervening position to null.
                if (isLeavingGrid) {
                    me.getNavigationModel().setPosition(null, null, e.event, null, true);
                }
                me.cellFocused = false;
                me.focusEl = me.el;
                me.focusEl.dom.setAttribute('tabIndex', 0);
            }
            // Exiting to outside, switch back to navigation mode before clearing
            // the navigation position so that the current position's row
            // can have its tabbability saved.
            if (isLeavingGrid) {
                if (me.ownerGrid.actionableMode) {
                    // If focus is thrown back in with no specific target, it should go back into
                    // navigable mode at this position.
                    // See http://www.w3.org/TR/wai-aria-practices-1.1/#h-grid
                    // "Once focus has been moved inside the grid, subsequent tab presses
                    // that re-enter the grid shall return focus to the cell that last held focus."
                    me.lastFocused = me.actionPosition;
                    me.ownerGrid.setActionableMode(false);
                }
            } else {
                me.actionPosition = null;
            }
            // Skip the AbstractView's implementation.
            Ext.Component.prototype.onFocusLeave.call(me, e);
        }
    },
    // GridSelectionModel invokes onRowFocus to 'highlight'
    // the last row focused
    onRowFocus: function(rowIdx, highlight, suppressFocus) {
        var me = this;
        if (highlight) {
            me.addItemCls(rowIdx, me.focusedItemCls);
            if (!suppressFocus) {
                me.focusRow(rowIdx);
            }
        } else // this.el.dom.setAttribute('aria-activedescendant', row.id);
        {
            me.removeItemCls(rowIdx, me.focusedItemCls);
        }
        if (Ext.isIE8) {
            me.repaintBorder(rowIdx + 1);
        }
    },
    /**
     * Focuses a particular row and brings it into view. Will fire the rowfocus event.
     * @param {HTMLElement/String/Number/Ext.data.Model} row An HTMLElement template node,
     * index of a template node, the id of a template node or the
     * @param {Boolean/Number} [delay] Delay the focus this number of milliseconds
     * (true for 10 milliseconds). record associated with the node.
     */
    focusRow: function(row, delay) {
        var me = this,
            focusTask = me.getFocusTask();
        if (delay) {
            focusTask.delay(Ext.isNumber(delay) ? delay : 10, me.focusRow, me, [
                row,
                false
            ]);
            return;
        }
        // An immediate focus call must cancel any outstanding delayed focus calls.
        focusTask.cancel();
        // Do not attempt to focus if hidden or within collapsed Panel.
        if (me.isVisible(true)) {
            me.getNavigationModel().setPosition(me.getRecord(row));
        }
    },
    // Override the version in Ext.view.View because the focusable elements are the grid cells.
    /**
     * Focuses a particular row and brings it into view. Will fire the rowfocus event.
     * @param {HTMLElement/String/Number/Ext.data.Model} row An HTMLElement template node,
     * index of a template node, the id of a template node or the
     * @param {Boolean/Number} [delay] Delay the focus this number of milliseconds
     * (true for 10 milliseconds).
     * record associated with the node.
     */
    focusNode: function(row, delay) {
        this.focusRow(row, delay);
    },
    scrollRowIntoView: function(row, animate) {
        row = this.getRow(row);
        if (row) {
            this.scrollElIntoView(row, false, animate);
        }
    },
    /**
     * Focuses a particular cell and brings it into view. Will fire the rowfocus event.
     * @param {Ext.grid.CellContext} position The cell to select
     * @param {Boolean/Number} [delay] Delay the focus this number of milliseconds
     * (true for 10 milliseconds).
     */
    focusCell: function(position, delay) {
        var me = this,
            focusTask = me.getFocusTask(),
            cell;
        // eslint-disable-line no-unused-vars
        if (delay) {
            focusTask.delay(Ext.isNumber(delay) ? delay : 10, me.focusCell, me, [
                position,
                false
            ]);
            return;
        }
        // An immediate focus call must cancel any outstanding delayed focus calls.
        focusTask.cancel();
        // Do not attempt to focus if hidden or within collapsed Panel
        // Maintainer: Note that to avoid an unnecessary call to me.getCellByPosition
        // if not visible, or another, nested if test, the assignment of the cell var
        // is embedded inside the condition expression.
        // eslint-disable-next-line no-cond-assign
        if (me.isVisible(true) && (cell = me.getCellByPosition(position))) {
            me.getNavigationModel().setPosition(position);
        }
    },
    findFocusPosition: function(from, currentPosition, forward, keyEvent) {
        var me = this,
            cell = currentPosition.cellElement,
            actionables = me.ownerGrid.actionables,
            len = actionables.length,
            position, tabbableChildren, focusTarget, i;
        position = currentPosition.clone();
        tabbableChildren = Ext.fly(cell).findTabbableElements();
        // Find the next or previous tabbable in this cell.
        focusTarget = tabbableChildren[Ext.Array.indexOf(tabbableChildren, from) + (forward ? 1 : -1)];
        // If we are exiting the cell:
        // Find next cell if possible, otherwise, we are exiting the row
        while (!focusTarget && (cell = cell[forward ? 'nextSibling' : 'previousSibling'])) {
            // Move position pointer to point to the new cell
            position.setColumn(me.getHeaderByCell(cell));
            // Inform all Actionables that we intend to activate this cell.
            // If they are actionable, they will show/insert tabbable elements in this cell.
            for (i = 0; i < len; i++) {
                actionables[i].activateCell(position);
            }
            // In case any code in the cell activation churned
            // the grid DOM and the position got refreshed.
            // eg: edit handler on previously active editor.
            cell = position.getCell(true);
            // If there are now tabbable elements in this cell (entering a row restores tabbability)
            // and Actionables also show/insert tabbables), then focus in the current direction.
            if (cell && (tabbableChildren = Ext.fly(cell).findTabbableElements()).length) {
                focusTarget = tabbableChildren[forward ? 0 : tabbableChildren.length - 1];
            }
        }
        return {
            target: focusTarget,
            position: position
        };
    },
    getDefaultFocusPosition: function(fromComponent) {
        var me = this,
            store = me.dataSource,
            focusPosition = me.lastFocused,
            newPosition = new Ext.grid.CellContext(me).setPosition(0, 0),
            targetCell, scroller;
        if (fromComponent) {
            // Tabbing in from one of our column headers; the user will expect to land
            // in that column.
            // Unless it is configured cellFocusable: false
            if (fromComponent.isColumn && fromComponent.cellFocusable !== false) {
                if (!focusPosition) {
                    focusPosition = newPosition;
                }
                focusPosition.setColumn(fromComponent);
                focusPosition.setView(fromComponent.getView());
            }
            // Tabbing in from the neighbouring TableView (eg, locking).
            // Go to column zero, same record
            else if (fromComponent.isTableView && fromComponent.lastFocused && fromComponent.ownerGrid === me.ownerGrid) {
                focusPosition = new Ext.grid.CellContext(me).setPosition(fromComponent.lastFocused.record, 0);
            }
        }
        // We found a position from the "fromComponent, or there was a previously focused context
        if (focusPosition) {
            scroller = me.getScrollable();
            // Record is not in the store, or not in the rendered block.
            // Fall back to using the same row index.
            if (!store.contains(focusPosition.record) || (scroller && !scroller.isInView(focusPosition.getRow(true)).y)) {
                focusPosition.setRow(store.getAt(Math.min(focusPosition.rowIdx, store.getCount() - 1)));
            }
        } else // All else fails, find the first focusable cell.
        {
            focusPosition = newPosition;
            // Find the first focusable cell.
            targetCell = me.el.dom.querySelector(me.getCellSelector() + '[tabIndex="-1"]');
            if (targetCell) {
                focusPosition.setPosition(me.getRecord(targetCell), me.getHeaderByCell(targetCell));
            } else // All visible columns are cellFocusable: false
            {
                focusPosition = null;
            }
        }
        return focusPosition;
    },
    getLastFocused: function() {
        var me = this,
            lastFocused = me.lastFocused;
        if (lastFocused && lastFocused.record && lastFocused.column) {
            // If the last focused record or column has gone away, or the record
            // is no longer in the visible rendered block, we have no lastFocused
            if (me.dataSource.indexOf(lastFocused.record) !== -1 && me.getVisibleColumnManager().indexOf(lastFocused.column) !== -1 && me.getNode(lastFocused.record)) {
                return lastFocused;
            }
        }
    },
    scrollCellIntoView: function(cell, animate) {
        if (cell.isCellContext) {
            cell = this.getCellByPosition(cell);
        }
        if (cell) {
            this.scrollElIntoView(cell, null, animate);
        }
    },
    scrollElIntoView: function(el, hscroll, animate) {
        var scroller = this.getScrollable();
        if (scroller) {
            scroller.ensureVisible(el, {
                animation: animate,
                x: hscroll
            });
        }
    },
    syncRowHeightBegin: function() {
        var me = this,
            itemEls = me.all,
            ln = itemEls.count,
            synchronizer = [],
            RowSynchronizer = Ext.grid.locking.RowSynchronizer,
            i, j, rowSync;
        for (i = 0 , j = itemEls.startIndex; i < ln; i++ , j++) {
            synchronizer[i] = rowSync = new RowSynchronizer(me, itemEls.elements[j]);
            rowSync.reset();
        }
        return synchronizer;
    },
    syncRowHeightClear: function(synchronizer) {
        var me = this,
            itemEls = me.all,
            ln = itemEls.count,
            i;
        for (i = 0; i < ln; i++) {
            synchronizer[i].reset();
        }
    },
    syncRowHeightMeasure: function(synchronizer) {
        var ln = synchronizer.length,
            i;
        for (i = 0; i < ln; i++) {
            synchronizer[i].measure();
        }
    },
    syncRowHeightFinish: function(synchronizer, otherSynchronizer) {
        var ln = synchronizer.length,
            bufferedRenderer = this.bufferedRenderer,
            i;
        for (i = 0; i < ln; i++) {
            synchronizer[i].finish(otherSynchronizer[i]);
        }
        // Ensure that both BufferedRenderers have the same idea about scroll range and row height
        if (bufferedRenderer) {
            bufferedRenderer.syncRowHeightsFinish();
        }
    },
    refreshNode: function(record) {
        // Override from AbstractView.
        // Refreshing a node must force all columns to be updated.
        if (Ext.isNumber(record)) {
            record = this.store.getAt(record);
        }
        // For a TableView, refreshNode has to pass the "allColumns" flag to the handleUpdate
        // method to indicate that the whole column set must be rendered in a new row, and that
        // cell updaters may not be used.
        this.handleUpdate(this.dataSource, record, null, null, null, true);
    },
    handleUpdate: function(store, record, operation, changedFieldNames, info, allColumns) {
        var me = this,
            recordIndex = me.store.indexOf(record),
            rowTpl = me.rowTpl,
            markDirty = me.markDirty,
            dirtyCls = me.dirtyCls,
            columnsToUpdate = [],
            hasVariableRowHeight = me.variableRowHeight,
            updateTypeFlags = 0,
            ownerCt = me.ownerCt,
            cellFly = me.cellFly || (me.self.prototype.cellFly = new Ext.dom.Fly()),
            oldItemDom, oldDataRow, newItemDom, newAttrs, attLen, attName, attrIndex, overItemCls, columns, column, len, i, cellUpdateFlag, cell, fieldName, value, clearDirty, defaultRenderer, scope, elData, emptyValue;
        operation = operation || Ext.data.Model.EDIT;
        clearDirty = operation !== Ext.data.Model.EDIT;
        if (me.viewReady) {
            // Some features might need to know that we're updating
            me.updatingRows = true;
            // Table row being updated
            oldItemDom = me.getNodeByRecord(record);
            // Row might not be rendered due to buffered rendering
            // or being part of a collapsed group...
            if (oldItemDom) {
                // refreshNode can be called on a collapsed placeholder record.
                // Update it from a new rendering.
                if (record.isCollapsedPlaceholder) {
                    Ext.fly(oldItemDom).syncContent(me.createRowElement(record, me.indexOfRow(record)));
                    return;
                }
                overItemCls = me.overItemCls;
                columns = me.ownerCt.getVisibleColumnManager().getColumns();
                // A refreshNode operation must update all columns, and must do a full rerender.
                // Set the flags appropriately.
                if (allColumns) {
                    columnsToUpdate = columns;
                    updateTypeFlags = 1;
                } else {
                    // Collect an array of the columns which must be updated.
                    // If the field at this column index was changed, or column has a custom
                    // renderer (which means value could rely on any other changed field)
                    // we include the column.
                    for (i = 0 , len = columns.length; i < len; i++) {
                        column = columns[i];
                        // We are not going to update the cell, but we still need
                        // to mark it as dirty.
                        if (column.preventUpdate) {
                            cell = oldItemDom.querySelector(column.getCellSelector());
                            // Mark the field's dirty status if we are configured to do so
                            // (defaults to true)
                            if (cell && !clearDirty && markDirty) {
                                cellFly.attach(cell);
                                if (record.isModified(column.dataIndex)) {
                                    cellFly.addCls(dirtyCls);
                                    if (column.dirtyTextElementId) {
                                        cell.setAttribute('aria-describedby', column.dirtyTextElementId);
                                    }
                                } else {
                                    cellFly.removeCls(dirtyCls);
                                    cell.removeAttribute('aria-describedby');
                                }
                            }
                        } else {
                            // 0 = Column doesn't need update.
                            // 1 = Column needs update, and renderer has > 1 argument;
                            // We need to render a whole new HTML item.
                            // 2 = Column needs update, but renderer has 1 argument
                            // or column uses an updater.
                            cellUpdateFlag = me.shouldUpdateCell(record, column, changedFieldNames);
                            if (cellUpdateFlag) {
                                // Track if any of the updating columns yields a flag
                                // with the 1 bit set. This means that there is a custom renderer
                                // involved and a new TableView item will need rendering.
                                updateTypeFlags = updateTypeFlags | cellUpdateFlag;
                                columnsToUpdate[columnsToUpdate.length] = column;
                                hasVariableRowHeight = hasVariableRowHeight || column.variableRowHeight;
                            }
                        }
                    }
                }
                // Give CellEditors or other transient in-cell items a chance to get out of the way
                // if there are in the cells destined for update.
                if (me.hasListeners.beforeitemupdate) {
                    me.fireEvent('beforeitemupdate', record, recordIndex, oldItemDom, columnsToUpdate);
                }
                // If there's no data row (some other rowTpl has been used; eg group header)
                // or we have a getRowClass
                // or one or more columns has a custom renderer
                // or there's more than one <TR>, we must use the full render pathway
                // to create a whole new TableView item
                if (me.getRowClass || !me.getRowFromItem(oldItemDom) || (updateTypeFlags & 1) || (oldItemDom.tBodies[0].childNodes.length > 1)) {
                    elData = oldItemDom._extData;
                    newItemDom = me.createRowElement(record, me.indexOfRow(record), columnsToUpdate);
                    if (Ext.fly(oldItemDom, '_internal').hasCls(overItemCls)) {
                        Ext.fly(newItemDom).addCls(overItemCls);
                    }
                    // Copy new row attributes across. Use IE-specific method if possible.
                    // In IE10, there is a problem where the className will not get updated
                    // in the view, even though the className on the dom element is correct.
                    // See EXTJSIV-9462
                    if (Ext.isIE9m && oldItemDom.mergeAttributes) {
                        oldItemDom.mergeAttributes(newItemDom, true);
                    } else {
                        newAttrs = newItemDom.attributes;
                        attLen = newAttrs.length;
                        for (attrIndex = 0; attrIndex < attLen; attrIndex++) {
                            attName = newAttrs[attrIndex].name;
                            value = newAttrs[attrIndex].value;
                            if (attName !== 'id' && oldItemDom.getAttribute(attName) !== value) {
                                oldItemDom.setAttribute(attName, value);
                            }
                        }
                    }
                    // The element's data is no longer synchronized. We just overwrite it in the DOM
                    if (elData) {
                        elData.isSynchronized = false;
                    }
                    // If we have columns which may *need* updating (think locked side
                    // of lockable grid with all columns unlocked) and the changed record is within
                    // our view, then update the view.
                    if (columns.length && (oldDataRow = me.getRow(oldItemDom))) {
                        me.updateColumns(oldDataRow, newItemDom.querySelector(me.rowSelector), columnsToUpdate, record);
                    }
                    // Loop thru all of rowTpls asking them to sync the content
                    // they are responsible for if any.
                    while (rowTpl) {
                        if (rowTpl.syncContent) {
                            // *IF* we are selectively updating columns (have been passed
                            // changedFieldNames), then pass the column set, else pass null,
                            // and it will sync all content.
                            // eslint-disable-next-line max-len
                            if (rowTpl.syncContent(oldItemDom, newItemDom, changedFieldNames ? columnsToUpdate : null) === false) {
                                break;
                            }
                        }
                        rowTpl = rowTpl.nextTpl;
                    }
                } else // No custom renderers found in columns to be updated,
                // we can simply update the existing cells.
                {
                    // Loop through columns which need updating.
                    for (i = 0 , len = columnsToUpdate.length; i < len; i++) {
                        column = columnsToUpdate[i];
                        // The dataIndex of the column is the field name
                        fieldName = column.dataIndex;
                        value = record.get(fieldName);
                        cell = oldItemDom.querySelector(column.getCellSelector());
                        cellFly.attach(cell);
                        // Mark the field's dirty status if we are configured to do so
                        // (defaults to true)
                        if (!clearDirty && markDirty) {
                            if (record.isModified(column.dataIndex)) {
                                cellFly.addCls(dirtyCls);
                                if (column.dirtyTextElementId) {
                                    cell.setAttribute('aria-describedby', column.dirtyTextElementId);
                                }
                            } else {
                                cellFly.removeCls(dirtyCls);
                                cell.removeAttribute('aria-describedby');
                            }
                        }
                        defaultRenderer = column.usingDefaultRenderer;
                        scope = defaultRenderer ? column : column.scope;
                        // Call the column updater which gets passed the TD element
                        if (column.updater) {
                            Ext.callback(column.updater, scope, [
                                cell,
                                value,
                                record,
                                me,
                                me.dataSource
                            ], 0, column, ownerCt);
                        } else {
                            if (column.renderer) {
                                value = Ext.callback(column.renderer, scope, [
                                    value,
                                    null,
                                    record,
                                    0,
                                    0,
                                    me.dataSource,
                                    me
                                ], 0, column, ownerCt);
                            }
                            emptyValue = value == null || value.length === 0;
                            value = emptyValue ? column.emptyCellText : value;
                            // Update the value of the cell's inner in the best way.
                            // We only use innerHTML of the cell's inner DIV if the renderer
                            // produces HTML. Otherwise we change the value of the single text node
                            // within the inner DIV. The emptyValue may be HTML,
                            // typically defaults to &#160;
                            if (column.producesHTML || emptyValue) {
                                cell.querySelector(me.innerSelector).innerHTML = value;
                            } else {
                                cell.querySelector(me.innerSelector).childNodes[0].data = value;
                            }
                        }
                        // Add the highlight class if there is one
                        if (me.highlightClass) {
                            Ext.fly(cell).addCls(me.highlightClass);
                            // Start up a DelayedTask which will purge the changedCells stack,
                            // removing the highlight class after the expiration time
                            if (!me.changedCells) {
                                me.self.prototype.changedCells = [];
                                me.prototype.clearChangedTask = new Ext.util.DelayedTask(me.clearChangedCells, me.prototype);
                                me.clearChangedTask.delay(me.unhighlightDelay);
                            }
                            // Post a changed cell to the stack along with expiration time
                            me.changedCells.push({
                                cell: cell,
                                cls: me.highlightClass,
                                expires: Ext.Date.now() + 1000
                            });
                        }
                    }
                }
                // If we have a commit or a reject, some fields may no longer be dirty but may
                // not appear in the modified field names.
                // Remove all the dirty class here to be sure.
                if (clearDirty && markDirty && !record.dirty) {
                    Ext.fly(oldItemDom, '_internal').select('.' + dirtyCls).removeCls(dirtyCls).set({
                        'aria-describedby': undefined
                    });
                }
                // Coalesce any layouts which happen due to any itemupdate handlers
                // (eg Widget columns) with the final refreshSize layout.
                if (hasVariableRowHeight) {
                    Ext.suspendLayouts();
                }
                // Since we don't actually replace the row, we need to fire the event
                // with the old row because it's the thing that is still in the DOM
                if (me.hasListeners.itemupdate) {
                    me.fireEvent('itemupdate', record, recordIndex, oldItemDom, me);
                }
                // We only need to update the layout if any of the columns can change
                // the row height.
                if (hasVariableRowHeight) {
                    // Must climb to ownerGrid in case we've only updated one field
                    // in one side of a lockable assembly. ownerGrid is always the topmost
                    // GridPanel.
                    me.ownerGrid.updateLayout();
                    // Ensure any layouts queued by itemupdate handlers
                    // and/or the refreshSize call are executed.
                    Ext.resumeLayouts(true);
                }
            }
            me.updatingRows = false;
        }
    },
    clearChangedCells: function() {
        var me = this,
            now = Ext.Date.now(),
            changedCell, i, len;
        for (i = 0 , len = me.changedCells.length; i < len; ) {
            changedCell = me.changedCells[i];
            if (changedCell.expires <= now) {
                Ext.fly(changedCell.cell).removeCls(changedCell.highlightClass);
                Ext.Array.erase(me.changedCells, i, 1);
                len--;
            } else {
                break;
            }
        }
        // Keep repeating the delay until all highlighted cells have been cleared
        if (len) {
            me.clearChangedTask.delay(me.unhighlightDelay);
        }
    },
    updateColumns: function(oldRow, newRow, columnsToUpdate, record) {
        var me = this,
            cellSelector = me.getCellSelector(),
            colCount = columnsToUpdate.length,
            newAttrs, attLen, attName, attrIndex, colIndex, column, oldCell, newCell, elData, value;
        // Copy new row attributes across. Use IE-specific method if possible.
        // Must do again at this level because the row DOM passed here may be the nested row
        // in a row wrap.
        if (oldRow.mergeAttributes) {
            oldRow.mergeAttributes(newRow, true);
        } else {
            newAttrs = newRow.attributes;
            attLen = newAttrs.length;
            for (attrIndex = 0; attrIndex < attLen; attrIndex++) {
                attName = newAttrs[attrIndex].name;
                value = newAttrs[attrIndex].value;
                if (attName !== 'id' && oldRow.getAttribute(attName) !== value) {
                    oldRow.setAttribute(attName, value);
                }
            }
        }
        // The element's data is no longer synchronized. We just overwrote it in the DOM
        elData = oldRow._extData;
        if (elData) {
            elData.isSynchronized = false;
        }
        // Replace changed cells in the existing row structure with the new version
        // from the rendered row.
        for (colIndex = 0; colIndex < colCount; colIndex++) {
            column = columnsToUpdate[colIndex];
            // Pluck out cells using the column's unique cell selector.
            // Because in a wrapped row, there may be several TD elements.
            cellSelector = me.getCellSelector(column);
            oldCell = oldRow.querySelector(cellSelector);
            newCell = newRow.querySelector(cellSelector);
            // Copy new cell attributes across.
            newAttrs = newCell.attributes;
            attLen = newAttrs.length;
            for (attrIndex = 0; attrIndex < attLen; attrIndex++) {
                attName = newAttrs[attrIndex].name;
                value = newAttrs[attrIndex].value;
                if (attName !== 'id' && oldCell.getAttribute(attName) !== value) {
                    oldCell.setAttribute(attName, value);
                }
            }
            // The element's data is no longer synchronized. We just overwrote it in the DOM
            elData = oldCell._extData;
            if (elData) {
                elData.isSynchronized = false;
            }
            // Carefully replace just the *contents* of the content bearing inner element.
            me.oldCellFly.attach(oldCell.querySelector(me.innerSelector)).syncContent(newCell.querySelector(me.innerSelector));
            if (record && column.onItemAdd) {
                column.onItemAdd([
                    record
                ]);
            }
        }
    },
    /**
     * @private
     * Decides whether the column needs updating
     * @return {Number} 0 = Doesn't need update.
     * 1 = Column needs update, and renderer has > 1 argument; We need to render a whole new
     * HTML item.
     * 2 = Column needs update, but renderer has 1 argument or column uses an updater.
     */
    shouldUpdateCell: function(record, column, changedFieldNames) {
        return column.shouldUpdateCell(record, changedFieldNames);
    },
    /**
     * Refreshes the grid view. Sets the sort state and focuses the previously focused row.
     *
     * **Note:** This method should only be used when `bufferedRenderer` is set to `false`.
     * BufferedRender has its own methods for managing its data's state.
     */
    refresh: function() {
        var me = this;
        if (me.destroying) {
            return;
        }
        // If there are visible columns, then refresh
        if (me.getVisibleColumnManager().getColumns().length) {
            me.callParent(arguments);
            me.headerCt.setSortState();
        } else // If no visible columns, clear the view
        {
            if (me.refreshCounter) {
                me.clearViewEl(true);
            }
            me.addEmptyText();
        }
    },
    processContainerEvent: function(e) {
        // If we find a component & it belongs to our grid, don't fire the event.
        // For example, grid editors resolve to the parent grid
        var cmp = Ext.Component.from(e.target.parentNode);
        if (cmp && cmp.up(this.ownerCt)) {
            return false;
        }
    },
    processItemEvent: function(record, item, rowIndex, e) {
        var me = this,
            self = me.self,
            map = self.EventMap,
            type = e.type,
            features = me.features,
            len = features.length,
            i, cellIndex, result, feature, column,
            eventPosition = e.position = me.eventPosition || (me.eventPosition = new Ext.grid.CellContext()),
            row, cell;
        // IE has a bug whereby if you mousedown in a cell editor in one side of a locking grid
        // and then drag out of that, and mouseup in *the other side*, the mousedowned side
        // still receives the event!
        // Even though the mouseup target is *not* within it! Ignore the mouseup in this case.
        if (Ext.isIE && type === 'mouseup' && !e.within(me.el)) {
            return false;
        }
        // Only process the event if it occurred within an item which maps to a record in the store
        if (me.indexInStore(item) !== -1) {
            row = eventPosition.rowElement = item.querySelector(me.rowSelector);
            // Access the cell from the event target.
            cell = e.getTarget(me.getCellSelector(), row);
            type = self.TouchEventMap[type] || type;
            if (cell) {
                if (!cell.parentNode) {
                    // If we have no parentNode, the td has been removed from the DOM,
                    // probably via an update, so just jump out since the target for the event
                    // isn't valid
                    return false;
                }
                column = me.getHeaderByCell(cell);
                // Find the index of the header in the *full* (including hidden columns)
                // leaf column set. Because In 4.0.0 we rendered hidden cells,
                // and the cellIndex included the hidden ones.
                if (column) {
                    cellIndex = me.ownerCt.getColumnManager().getHeaderIndex(column);
                } else {
                    column = cell = null;
                    cellIndex = -1;
                }
            } else {
                cellIndex = -1;
            }
            eventPosition.setAll(me, rowIndex, column ? me.getVisibleColumnManager().getHeaderIndex(column) : -1, record, column);
            eventPosition.cellElement = cell;
            result = me.fireEvent('uievent', type, me, cell, rowIndex, cellIndex, e, record, row);
            // If the event has been stopped by a handler, tell the selModel (if it is interested)
            // and return early.
            // For example, action columns by default will stop event propagation
            // by returning `false` from its 'uievent' event handler.
            if ((result === false || me.callParent(arguments) === false)) {
                return false;
            }
            for (i = 0; i < len; ++i) {
                feature = features[i];
                // In some features, the first/last row might be wrapped to contain extra info,
                // such as grouping or summary, so we may need to stop the event
                if (feature.wrapsItem) {
                    if (feature.vetoEvent(record, row, rowIndex, e) === false) {
                        // If the feature is vetoing the event, there's a good chance that
                        // it's for some feature action in the wrapped row.
                        me.processSpecialEvent(e);
                        return false;
                    }
                }
            }
            /* eslint-disable indent, max-len */
            // if the element whose event is being processed is not an actual cell (for example
            // if using a rowbody feature and the rowbody element's event is being processed)
            // then do not fire any "cell" events
            // Don't handle cellmouseenter and cellmouseleave events for now
            if (cell && type !== 'mouseover' && type !== 'mouseout') {
                result = !(// We are adding cell and feature events
                (me['onBeforeCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) || (me.fireEvent('beforecell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false) || (me['onCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) || (me.fireEvent('cell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false));
            }
            /* eslint-enable indent, max-len */
            if (result !== false) {
                result = me.fireEvent('row' + type, me, record, row, rowIndex, e);
            }
            return result;
        } else {
            // If it's not in the store, it could be a feature event, so check here
            me.processSpecialEvent(e);
            // Prevent focus/selection here until proper focus handling is added for non-data rows
            // This should probably be removed once this is implemented.
            if (e.pointerType === 'mouse') {
                e.preventDefault();
            }
            return false;
        }
    },
    processSpecialEvent: function(e) {
        var me = this,
            features = me.features,
            ln = features.length,
            type = e.type,
            i, feature, prefix, featureTarget, beforeArgs, args,
            panel = me.ownerCt;
        me.callParent(arguments);
        if (type === 'mouseover' || type === 'mouseout') {
            return;
        }
        type = me.self.TouchEventMap[type] || type;
        for (i = 0; i < ln; i++) {
            feature = features[i];
            if (feature.hasFeatureEvent) {
                featureTarget = e.getTarget(feature.eventSelector, me.getTargetEl());
                if (featureTarget) {
                    prefix = feature.eventPrefix;
                    // allows features to implement getFireEventArgs to change the
                    // fireEvent signature
                    beforeArgs = feature.getFireEventArgs('before' + prefix + type, me, featureTarget, e);
                    args = feature.getFireEventArgs(prefix + type, me, featureTarget, e);
                    /* eslint-disable indent */
                    if (// before view event
                    (me.fireEvent.apply(me, beforeArgs) === false) || // panel grid event
                    (panel.fireEvent.apply(panel, beforeArgs) === false) || // view event
                    (me.fireEvent.apply(me, args) === false) || (// panel event
                    panel.fireEvent.apply(panel, args) === false)) {
                        return false;
                    }
                }
            }
        }
        /* eslint-enable indent */
        return true;
    },
    onCellMouseDown: Ext.emptyFn,
    onCellLongPress: Ext.emptyFn,
    onCellMouseUp: Ext.emptyFn,
    onCellClick: Ext.emptyFn,
    onCellDblClick: Ext.emptyFn,
    onCellContextMenu: Ext.emptyFn,
    onCellKeyDown: Ext.emptyFn,
    onCellKeyUp: Ext.emptyFn,
    onCellKeyPress: Ext.emptyFn,
    onBeforeCellMouseDown: Ext.emptyFn,
    onBeforeCellLongPress: Ext.emptyFn,
    onBeforeCellMouseUp: Ext.emptyFn,
    onBeforeCellClick: Ext.emptyFn,
    onBeforeCellDblClick: Ext.emptyFn,
    onBeforeCellContextMenu: Ext.emptyFn,
    onBeforeCellKeyDown: Ext.emptyFn,
    onBeforeCellKeyUp: Ext.emptyFn,
    onBeforeCellKeyPress: Ext.emptyFn,
    /**
     * Expands a particular header to fit the max content width.
     * @deprecated 6.5.0 Use {@link #autoSizeColumn} instead.
     */
    expandToFit: function(header) {
        this.autoSizeColumn(header);
    },
    /**
     * Sizes the passed header to fit the max content width.
     * *Note that group columns shrinkwrap around the size of leaf columns. Auto sizing
     * a group column autosizes descendant leaf columns.*
     * @param {Ext.grid.column.Column/Number} header The header (or index of header) to auto size.
     */
    autoSizeColumn: function(header) {
        if (Ext.isNumber(header)) {
            header = this.getGridColumns()[header];
        }
        if (header) {
            if (header.isGroupHeader) {
                header.autoSize();
                return;
            }
            delete header.flex;
            header.setWidth(this.getMaxContentWidth(header));
        }
    },
    /**
     * Returns the max contentWidth of the header's text and all cells
     * in the grid under this header.
     * @private
     */
    getMaxContentWidth: function(header) {
        var me = this,
            cells = me.getHeaderCells(header),
            originalWidth = header.getWidth(),
            columnSizers = me.getColumnResizers(header),
            ln = cells.length,
            max = Math.max,
            widthAdjust = 0,
            i, maxWidth;
        if (ln > 0) {
            if (Ext.supports.ScrollWidthInlinePaddingBug) {
                widthAdjust += me.getCellPaddingAfter(cells[0]);
            }
            if (me.columnLines) {
                widthAdjust += Ext.fly(cells[0].parentNode).getBorderWidth('lr');
            }
        }
        // Set column width to 1px so we can detect the content width by measuring scrollWidth
        for (i = 0; i < columnSizers.length; i++) {
            columnSizers[i].setWidth(1);
        }
        // We are about to measure the offsetWidth of the textEl to determine how much
        // space the text occupies, but it will not report the correct width if the titleEl
        // has text-overflow:ellipsis.  Set text-overflow to 'clip' before proceeding to
        // ensure we get the correct measurement.
        header.textEl.setStyle({
            "text-overflow": 'clip',
            display: 'table-cell'
        });
        // Allow for padding round text of header
        maxWidth = header.textEl.dom.offsetWidth + header.titleEl.getPadding('lr');
        // revert to using text-overflow defined by the stylesheet
        header.textEl.setStyle({
            "text-overflow": '',
            display: ''
        });
        for (i = 0; i < ln; i++) {
            maxWidth = max(maxWidth, cells[i].scrollWidth);
        }
        // in some browsers, the "after" padding is not accounted for in the scrollWidth
        maxWidth += widthAdjust;
        // 40 is the minimum column width.  TODO: should this be configurable?
        // One extra pixel needed. EXACT width shrinkwrap of text causes ellipsis to appear.
        maxWidth = max(maxWidth + 1, 40);
        // Set column width back to original width
        for (i = 0; i < columnSizers.length; i++) {
            columnSizers[i].setWidth(originalWidth);
        }
        return maxWidth;
    },
    getColumnResizers: function(header) {
        var me = this,
            features = me.features || [],
            resizers = [
                me.body.select(me.getColumnSizerSelector(header))
            ],
            featureSizer, i;
        for (i = 0; i < features.length; i++) {
            featureSizer = features[i].columnSizer;
            if (featureSizer) {
                resizers.push(featureSizer.select(me.getColumnSizerSelector(header)));
            }
        }
        return resizers;
    },
    getHeaderCells: function(header) {
        var me = this,
            features = me.features || [],
            headerCells = me.el.query(header.getCellInnerSelector()),
            featureSizer, i;
        for (i = 0; i < features.length; i++) {
            featureSizer = features[i].columnSizer;
            if (featureSizer) {
                headerCells = headerCells.concat(featureSizer.query(header.getCellInnerSelector()));
            }
        }
        return headerCells;
    },
    getPositionByEvent: function(e) {
        var me = this,
            cellNode = e.getTarget(me.cellSelector),
            rowNode = e.getTarget(me.itemSelector),
            record = me.getRecord(rowNode),
            header = me.getHeaderByCell(cellNode);
        return me.getPosition(record, header);
    },
    getHeaderByCell: function(cell) {
        if (cell) {
            return this.ownerGrid.getVisibleColumnManager().getHeaderById(Ext.getDom(cell).getAttribute('data-columnId'));
        }
        return false;
    },
    /**
     * @param {Ext.grid.CellContext} pos The current navigation position.
     * @param {String} direction 'up', 'down', 'right' and 'left'
     * @param {Function} [verifierFn] A function to verify the validity of the calculated position.
     * When using this function, you must return true to allow the newPosition to be returned.
     * @param {Ext.grid.CellContext} [verifierFn.position] The calculated new position to verify.
     * @param {Object} [scope] Scope (`this` context) to run the verifierFn in.
     * Defaults to this View.
     * @return {Ext.grid.CellContext} An object encapsulating the unique cell position.
     *
     * @private
     */
    walkCells: function(pos, direction, verifierFn, scope) {
        var me = this,
            result = pos.clone(),
            lockingPartner = me.lockingPartner && me.lockingPartner.grid.isVisible() ? me.lockingPartner : null,
            rowIdx = pos.rowIdx,
            maxRowIdx = me.dataSource.getCount() - 1,
            columns = me.ownerCt.getVisibleColumnManager().getColumns();
        switch (direction.toLowerCase()) {
            case 'right':
                // At end of row.
                if (pos.isLastColumn()) {
                    // If we're at the end of the locked view, same row, else wrap downwards
                    rowIdx = lockingPartner && me.isLockedView ? rowIdx : rowIdx + 1;
                    // If stepped past the bottom row, deny the action
                    if (rowIdx > maxRowIdx) {
                        return false;
                    }
                    // There's a locking partner to move into
                    if (lockingPartner) {
                        result.view = lockingPartner;
                    }
                    result.setPosition(rowIdx, 0);
                } else // Not at end, just go forwards one column
                {
                    result.navigate(+1);
                };
                break;
            case 'left':
                // At start of row.
                if (pos.isFirstColumn()) {
                    // If we're at the start of the normal view, same row, else wrap upwards
                    rowIdx = lockingPartner && me.isNormalView ? rowIdx : rowIdx - 1;
                    // If top row, deny up
                    if (rowIdx < 0) {
                        return false;
                    }
                    // There's a locking partner to move into
                    if (lockingPartner) {
                        result.view = lockingPartner;
                        columns = lockingPartner.getVisibleColumnManager().getColumns();
                    }
                    result.setPosition(rowIdx, columns[columns.length - 1]);
                } else // Not at end, just go backwards one column
                {
                    result.navigate(-1);
                };
                break;
            case 'up':
                // if top row, deny up
                if (rowIdx === 0) {
                    return false;
                } else // go up
                {
                    result.setRow(rowIdx - 1);
                };
                break;
            case 'down':
                // if bottom row, deny down
                if (rowIdx === maxRowIdx) {
                    return false;
                } else // go down
                {
                    result.setRow(rowIdx + 1);
                };
                break;
        }
        if (verifierFn && verifierFn.call(scope || me, result) !== true) {
            return false;
        }
        return result;
    },
    /**
     * Increments the passed row index by the passed increment which may be +ve or -ve
     *
     * Skips hidden rows.
     *
     * If no row is visible in the specified direction, returns the input row index unchanged.
     * @param {Number} startRow The zero-based row index to start from.
     * @param {Number} distance The distance to move the row by. May be +ve or -ve.
     * @deprecated 6.5.0 This method is deprecated.
     * @private
     */
    walkRows: function(startRow, distance) {
        // Note that we use the **dataSource** here because row indices mean view row indices
        // so records in collapsed groups must be omitted.
        var me = this,
            store = me.dataSource,
            moved = 0,
            lastValid = startRow,
            node,
            limit = (distance < 0) ? 0 : store.getCount() - 1,
            increment = limit ? 1 : -1,
            result = startRow;
        do {
            if (limit ? result >= limit : result <= limit) {
                return lastValid || limit;
            }
            result += increment;
            if ((node = Ext.fly(me.getRow(result))) && node.isVisible(true)) {
                moved += increment;
                lastValid = result;
            }
        } while (// Walked off the end: return the last encountered valid row
        // Move the result pointer on by one position.
        // We have to count intervening VISIBLE nodes
        // Stepped onto VISIBLE record: Increment the moved count.
        // We must not count stepping onto a non-rendered record as a move.
        moved !== distance);
        return result;
    },
    /**
     * Navigates from the passed record by the passed increment which may be +ve or -ve
     *
     * Skips hidden records.
     *
     * If no record is visible in the specified direction, returns the starting record
     * index unchanged.
     * @param {Ext.data.Model} startRec The Record to start from.
     * @param {Number} distance The distance to move from the record. May be +ve or -ve.
     */
    walkRecs: function(startRec, distance) {
        // Note that we use the **store** to access the records by index
        // because the dataSource omits records in collapsed groups.
        // This is used by selection models which use the **store**
        var me = this,
            store = me.dataSource,
            moved = 0,
            lastValid = startRec,
            // eslint-disable-next-line max-len
            limit = (distance < 0) ? 0 : (store.isBufferedStore ? store.getTotalCount() : store.getCount()) - 1,
            increment = limit ? 1 : -1,
            testIndex = store.indexOf(startRec),
            node, rec;
        do {
            if (limit ? testIndex >= limit : testIndex <= limit) {
                return lastValid;
            }
            testIndex += increment;
            rec = store.getAt(testIndex);
            if (!rec.isCollapsedPlaceholder && (node = Ext.fly(me.getNodeByRecord(rec))) && node.isVisible(true)) {
                moved += increment;
                lastValid = rec;
            }
        } while (// Walked off the end: return the last encountered valid record
        // Move the result pointer on by one position.
        // We have to count intervening VISIBLE nodes
        // Stepped onto VISIBLE record: Increment the moved count.
        // We must not count stepping onto a non-rendered record as a move.
        moved !== distance);
        return lastValid;
    },
    /**
     * Returns the index of the first row in your table view deemed to be visible.
     * @return {Number}
     * @private
     */
    getFirstVisibleRowIndex: function() {
        var me = this,
            result = me.indexOf(me.all.first()) - 1,
            count;
        count = me.dataSource.isBufferedStore ? me.dataSource.getTotalCount() : me.dataSource.getCount();
        do {
            result += 1;
            if (result === count) {
                return;
            }
        } while (!Ext.fly(me.getRow(result)).isVisible(true));
        return result;
    },
    /**
     * Returns the index of the last row in your table view deemed to be visible.
     * @return {Number}
     * @private
     */
    getLastVisibleRowIndex: function() {
        var me = this,
            result = me.indexOf(me.all.last());
        do {
            result -= 1;
            if (result === -1) {
                return;
            }
        } while (!Ext.fly(me.getRow(result)).isVisible(true));
        return result;
    },
    getHeaderCt: function() {
        return this.headerCt;
    },
    getPosition: function(record, header) {
        return new Ext.grid.CellContext(this).setPosition(record, header);
    },
    doDestroy: function() {
        var me = this,
            features = me.featuresMC,
            feature, i, len;
        // We need to unbind the store first to avoid firing update events;
        // all kinds of things are bound to this store and they don't need
        // updates anymore.
        me.bindStore(null);
        if (features) {
            for (i = 0 , len = features.getCount(); i < len; ++i) {
                feature = features.getAt(i);
                // Features could be already destroyed
                if (feature && !feature.destroyed) {
                    feature.destroy();
                }
            }
        }
        me.all.destroy();
        me.body.destroy();
        me.actionRowFly.destroy();
        me.callParent();
    },
    /** 
     * @private
     * Respond to store replace event which is fired by GroupStore group expand/collapse operations.
     * This saves a layout because a remove and add operation are coalesced in this operation.
     */
    onReplace: function(store, startIndex, oldRecords, newRecords) {
        var me = this,
            bufferedRenderer = me.bufferedRenderer,
            restoreFocus;
        // If there's a buffered renderer and the removal range falls inside the current view...
        if (me.rendered && bufferedRenderer) {
            // If focus was in any way in the view, whether actionable or navigable,
            // this will return a function which will restore that state.
            restoreFocus = me.saveFocusState();
            bufferedRenderer.onReplace(store, startIndex, oldRecords, newRecords);
            // If focus was in any way in this view, this will restore it
            restoreFocus();
        } else {
            me.callParent(arguments);
        }
        me.setPendingStripe(startIndex);
    },
    onResize: function(width, height, oldWidth, oldHeight) {
        var me = this,
            bufferedRenderer = me.bufferedRenderer;
        // Ensure the buffered renderer makes its adjustments before user resize listeners
        if (bufferedRenderer) {
            bufferedRenderer.onViewResize(me, width, height, oldWidth, oldHeight);
        }
        me.callParent([
            width,
            height,
            oldWidth,
            oldHeight
        ]);
    },
    // after adding a row stripe rows from then on
    onAdd: function(store, records, index) {
        var me = this,
            bufferedRenderer = me.bufferedRenderer;
        // Some features might need to know if we're refreshing or just adding rows
        me.addingRows = true;
        // Only call the buffered renderer's handler if there's a need to.
        // That is if the rendered block has been moved down the dataset, or
        // the addition will tip the rendered block size over the buffered renderer's
        // calculated viewSize.
        // eslint-disable-next-line max-len
        if (me.rendered && bufferedRenderer && (bufferedRenderer.bodyTop || me.dataSource.getCount() + records.length >= bufferedRenderer.viewSize)) {
            bufferedRenderer.onReplace(store, index, [], records);
        } else {
            me.callParent(arguments);
        }
        me.setPendingStripe(index);
        me.addingRows = false;
    },
    // after removing a row stripe rows from then on
    onRemove: function(store, records, index) {
        var me = this,
            bufferedRenderer = me.bufferedRenderer,
            restoreFocus;
        // If there's a BufferedRenderer, and it's being used (dataset size before removal
        // was >= rendered block size)...
        if (me.rendered && bufferedRenderer && me.dataSource.getCount() + records.length >= bufferedRenderer.viewSize) {
            // If focus was in any way in the view, whether actionable or navigable,
            // this will return a function which will restore that state.
            restoreFocus = me.saveFocusState();
            bufferedRenderer.onReplace(store, index, records, []);
            // If focus was in any way in this view, this will restore it
            restoreFocus();
        } else {
            me.callParent(arguments);
        }
        if (me.actionPosition && Ext.Array.indexOf(records, me.actionPosition.record) !== -1) {
            me.actionPosition = null;
        }
        me.setPendingStripe(index);
    },
    /**
     * @private
     * Called prior to an operation which may remove focus from this view by some kind
     * of DOM operation.
     *
     * If this view contains focus in any sense, either navigable mode, or actionable mode,
     * this method returns a function which, when called after the disruptive DOM operation
     * will restore focus to the same record/column, or, if the record has been removed, to the same
     * row index/column.
     *
     * @returns {Function} A function that will restore focus if focus was within this view,
     * or a function which does nothing is focus is not in this view.
     */
    saveFocusState: function() {
        var me = this,
            store = me.dataSource,
            actionableMode = me.actionableMode,
            navModel = me.getNavigationModel(),
            focusPosition = actionableMode ? me.actionPosition : navModel.getPosition(true),
            activeElement = Ext.fly(Ext.Element.getActiveElement()),
            focusCell = focusPosition && focusPosition.view === me && Ext.fly(focusPosition.getCell(true)),
            refocusRow, refocusCol, record;
        // The navModel may return a position that is in a locked partner, so check that
        // the focusPosition's cell contains the focus before going forward.
        // The skipSaveFocusState is set by Actionables which actively control
        // focus destination. See CellEditing#activateCell.
        if (!me.skipSaveFocusState && focusCell && focusCell.contains(activeElement)) {
            // Separate this from the instance that the nav model is using.
            focusPosition = focusPosition.clone();
            // While we deactivate the focused element, suspend focus processing on it.
            activeElement.suspendFocusEvents();
            // Suspend actionable mode.
            // Each Actionable must silently save its state ready to resume when focus
            // can be restored but should only do that if the activeElement is not the cell itself,
            // this happens when the grid is refreshed while one of the actionables is being
            // deactivated (e.g. Calling  view refresh inside CellEditor 'edit' event listener).
            if (actionableMode && focusCell.dom !== activeElement.dom) {
                me.suspendActionableMode();
            } else // Clear position, otherwise the setPosition on the other side
            // will be rejected as a no-op if the resumption position is logically
            // equivalent.
            {
                actionableMode = false;
                navModel.setPosition();
            }
            // Do not leave the element in tht state in case refresh fails, and restoration
            // closure not called.
            activeElement.resumeFocusEvents();
            // if the store is expanding or collapsing, we should never scroll the view.
            if (store.isExpandingOrCollapsing) {
                return Ext.emptyFn;
            }
            // The following function will attempt to refocus back in the same mode to the same cell
            // as it was at before based upon the previous record (if it's still in the store),
            // or the row index.
            return function() {
                var all;
                // May have changed due to reconfigure
                store = me.dataSource;
                // If we still have data, attempt to refocus in the same mode.
                if (store.getCount()) {
                    all = me.all;
                    // Adjust expectations of where we are able to refocus according to
                    // what kind of destruction might have been wrought on this view's DOM
                    // during focus save.
                    refocusRow = Math.min(Math.max(focusPosition.rowIdx, all.startIndex), all.endIndex);
                    refocusCol = Math.min(focusPosition.colIdx, me.getVisibleColumnManager().getColumns().length - 1);
                    record = focusPosition.record;
                    focusPosition = new Ext.grid.CellContext(me).setPosition(record && store.contains(record) && !record.isCollapsedPlaceholder ? record : refocusRow, refocusCol);
                    // Maybe there are no cells. eg: all groups collapsed.
                    if (focusPosition.getCell(true)) {
                        if (actionableMode) {
                            me.resumeActionableMode(focusPosition);
                        } else {
                            // Pass "preventNavigation" as true
                            // so that that does not cause selection.
                            navModel.setPosition(focusPosition, null, null, null, true);
                            if (!navModel.getPosition()) {
                                focusPosition.column.focus();
                            }
                        }
                    }
                } else // No rows - focus associated column header
                {
                    focusPosition.column.focus();
                }
            };
        }
        return Ext.emptyFn;
    },
    onDataRefresh: function(store) {
        // When there's a buffered renderer present, store refresh events cause TableViews to
        // go to scrollTop:0
        var me = this,
            owner = me.ownerCt;
        // If triggered during an animation, refresh once we're done
        if (owner && owner.isCollapsingOrExpanding === 2) {
            owner.on('expand', me.onDataRefresh, me, {
                single: true
            });
            return;
        }
        me.callParent([
            store
        ]);
    },
    getViewRange: function() {
        var me = this;
        if (me.bufferedRenderer) {
            return me.bufferedRenderer.getViewRange();
        }
        return me.callParent();
    },
    setPendingStripe: function(index) {
        var current = this.stripeOnUpdate;
        if (current === null) {
            current = index;
        } else {
            current = Math.min(current, index);
        }
        this.stripeOnUpdate = current;
    },
    onEndUpdate: function() {
        var me = this,
            stripeOnUpdate = me.stripeOnUpdate,
            startIndex = me.all.startIndex;
        if (me.rendered && (stripeOnUpdate || stripeOnUpdate === 0)) {
            if (stripeOnUpdate < startIndex) {
                stripeOnUpdate = startIndex;
            }
            me.doStripeRows(stripeOnUpdate);
            me.stripeOnUpdate = null;
        }
        me.callParent(arguments);
    },
    /**
     * Stripes rows from a particular row index.
     * @param {Number} startRow
     * @param {Number} [endRow] argument specifying the last row to process.
     * By default process up to the last row.
     * @private
     */
    doStripeRows: function(startRow, endRow) {
        var me = this,
            rows, rowsLn, i, row;
        // ensure stripeRows configuration is turned on
        if (me.rendered && me.stripeRows) {
            rows = me.getNodes(startRow, endRow);
            for (i = 0 , rowsLn = rows.length; i < rowsLn; i++) {
                row = rows[i];
                // Remove prior applied row classes.
                row.className = row.className.replace(me.rowClsRe, ' ');
                startRow++;
                // Every odd row will get an additional cls
                if (startRow % 2 === 0) {
                    row.className += (' ' + me.altRowCls);
                }
            }
        }
    },
    hasActiveFeature: function() {
        return (this.isGrouping && this.store.isGrouped()) || this.isRowWrapped;
    },
    getCellPaddingAfter: function(cell) {
        return Ext.fly(cell).getPadding('r');
    },
    privates: {
        saveTabOptions: {
            skipSelf: true,
            includeHidden: true
        },
        /*
         * Overridden implementation.
         * Called by refresh to collect the view item nodes.
         * Note that these may be wrapping rows which *contain* rows which map to records
         * @private
         */
        collectNodes: function(targetEl) {
            this.all.fill(this.getNodeContainer().childNodes, this.all.startIndex);
        },
        /**
         * 
         * @param {Boolean} enabled
         * @param {Ext.grid.CellContext} position The cell to activate.
         * @param {HTMLElement/Ext.dom.Element} [position.target] The element within the referenced
         * cell to focus.
         * @return {Boolean} Returns `false` if the mode did not change.
         * @private
         */
        setActionableMode: function(enabled, position) {
            var me = this,
                navModel = me.getNavigationModel(),
                actionables = me.grid.actionables,
                len = actionables.length,
                isActionable = false,
                activeEl, record, column, lockingPartner, cell, i;
            // No mode change.
            // ownerGrid's call will NOT fire mode change event upon false return.
            if (me.actionableMode === enabled) {
                // If we're not actionable already, or (we are actionable already at that position)
                // return false.
                // Test using mandatory passed position because we may not have an actionPosition
                // if we are  the lockingPartner of an actionable view that contained
                // the action position.
                //
                // If we being told to go into actionable mode but at another position,
                // we must continue. This is just actionable navigation.
                if (!enabled || position.isEqual(me.actionPosition)) {
                    return false;
                }
            }
            // If this View or its lockingPartner contains the current focus position,
            // then make the tab bumpers tabbable and move them to surround the focused row.
            if (enabled) {
                if (position && (position.view === me || (position.view === (lockingPartner = me.lockingPartner) && lockingPartner.actionableMode))) {
                    isActionable = me.activateCell(position);
                }
                // Did not enter actionable mode.
                // ownerGrid's call will NOT fire mode change event upon false return.
                return isActionable;
            } else {
                // Capture before exiting from actionable mode moves focus
                activeEl = Ext.fly(Ext.Element.getActiveElement());
                // Blur the focused descendant, but do not trigger focusLeave.
                // This is so that when the focus is restored to the cell which contained
                // the active content, it will not be a FocusEnter from the universe.
                if (me.el.contains(activeEl) && !Ext.fly(activeEl).is(me.getCellSelector())) {
                    // Row to return focus to.
                    record = (me.actionPosition && me.actionPosition.record) || me.getRecord(activeEl);
                    column = me.getHeaderByCell(activeEl.findParent(me.getCellSelector()));
                    cell = position && position.getCell(true);
                    // Do not allow focus to fly out of the view when the actionables
                    // are deactivated (and blurred/hidden). Restore focus to the cell in which
                    // actionable mode is active.
                    // Note that the original position may no longer be valid, e.g. when the record
                    // was removed.
                    if (!position || !cell) {
                        position = new Ext.grid.CellContext(me).setPosition(record || 0, column || 0);
                        cell = position.getCell(true);
                    }
                    // Ext.grid.NavigationModel#onFocusMove will NOT react and navigate
                    // because the actionableMode flag is still set at this point.
                    Ext.fly(cell).focus();
                    // Let's update the activeEl after focus here
                    activeEl = Ext.fly(Ext.Element.getActiveElement());
                    // If that focus triggered handlers (eg CellEditor after edit handlers) which
                    // programmatically moved focus somewhere, and the target cell has been
                    // unfocused, defer to that, null out position, so that we do not navigate
                    // to that cell below.
                    // See EXTJS-20395
                    if (!(me.el.contains(activeEl) && activeEl.is(me.getCellSelector()))) {
                        position = null;
                    }
                }
                // We are exiting actionable mode.
                // Tell all registered Actionables about this fact if they need to know.
                for (i = 0; i < len; i++) {
                    if (actionables[i].deactivate) {
                        actionables[i].deactivate();
                    }
                }
                // If we had begun action (we may be a dormant lockingPartner),
                // make any tabbables untabbable
                if (me.actionRow) {
                    me.actionRow.saveTabbableState({
                        skipSelf: true,
                        includeSaved: false
                    });
                }
                if (me.destroyed) {
                    return false;
                }
                // These flags MUST be set before focus restoration to the owning cell.
                // so that when Ext.grid.NavigationModel#setPosition attempts to exit
                // actionable mode, we don't recurse.
                me.actionableMode = me.ownerGrid.actionableMode = false;
                me.actionPosition = navModel.actionPosition = me.actionRow = null;
                // Push focus out to where it was requested to go.
                if (position) {
                    navModel.setPosition(position);
                }
            }
        },
        /**
         * Called to silently enter actionable mode at the passed position.
         * May be called from the {@link #setActionableMode} method, or from the
         * {@link #resumeActionableMode} method.
         * @private
         */
        activateCell: function(position) {
            var me = this,
                lockingPartner = position.view !== me ? me.lockingPartner : null,
                actionables = me.grid.actionables,
                len = actionables.length,
                navModel = me.getNavigationModel(),
                focusTarget = position.target,
                record, prevRow, focusRow, focusCell, i, isActionable, tabbableChildren;
            position = position.clone();
            record = position.record;
            position.view.grid.ensureVisible(record, {
                column: position.column
            });
            focusRow = me.all.item(position.rowIdx, true);
            // Deactivate remaining tabbables in the row we were last actionable upon.
            if (me.actionPosition) {
                prevRow = me.all.item(me.actionPosition.rowIdx, true);
                if (prevRow && focusRow !== prevRow) {
                    Ext.fly(prevRow).saveTabbableState({
                        skipSelf: true,
                        includeSaved: false
                    });
                }
            }
            // We need to set the activating flag here because we will focus the editor at during
            // the rest of this method and if this happens before actionableMode is true,
            // navigationModel's  onFocusMove method needs to know if activating events
            // should be fired.
            me.activating = true;
            // We're the focused side - attempt to see if ths focused cell is actionable
            if (!lockingPartner) {
                focusCell = Ext.fly(position.getCell(true));
                me.actionPosition = position;
                // Inform all Actionables that we intend to activate this cell.
                // If any return true, isActionable will be set
                for (i = 0; i < len; i++) {
                    isActionable = isActionable || actionables[i].activateCell(position, null, true);
                }
                // In case any of the activations called external handlers which
                // caused view DOM churn, reacquire the cell.
                focusCell = Ext.fly(position.getCell(true));
            }
            // If we have a lockingPartner that is actionable
            //  or if we find some elements we can restore to tabbability
            //  or there are existing tabbable elements
            //  or a plugin declared it was actionable at this position:
            //      dive in and activate the row
            // Note that a bitwise OR operator is used in this expression so that
            // no shortcutting is used. tabbableChildren must be extracted even if
            // restoreTabbableState found some previously disabled (tabIndex === -1)
            // nodes to restore.
            if (lockingPartner || (focusCell && (focusCell.restoreTabbableState({
                skipSelf: true
            }).length | (tabbableChildren = focusCell.findTabbableElements()).length)) || isActionable) {
                // We are entering actionable mode.
                // Tell all registered Actionables about this fact if they need to know.
                for (i = 0; i < len; i++) {
                    if (actionables[i].activateRow) {
                        actionables[i].activateRow(focusRow);
                    }
                }
                // Only enter actionable mode if there is an already actionable locking partner,
                // or there are tabbable children in current cell.
                if (lockingPartner || tabbableChildren.length) {
                    // Restore tabbabilty to all elements in this row
                    Ext.fly(focusRow).restoreTabbableState({
                        skipSelf: true
                    });
                    // If we are the locking partner of an actionable side, we are successful
                    // already. But we must not have an actionPosition. We are not actually
                    // in possession of an active cell and we must not reject an action request
                    // at that cell in the isEqual test above.
                    if (lockingPartner) {
                        me.actionableMode = true;
                        me.actionPosition = null;
                        me.activating = false;
                        return true;
                    }
                    // If there are focusables in the actioned cell, we can enter actionable mode.
                    if (tabbableChildren) {
                        /**
                         * @property {Ext.dom.Element} actionRow
                         * Only valid when a view is in actionableMode. The currently actioned row
                         */
                        me.actionRow = me.actionRowFly.attach(focusRow);
                        me.actionableMode = me.ownerGrid.actionableMode = true;
                        // Clear current position on entry into actionable mode
                        navModel.setPosition();
                        navModel.actionPosition = me.actionPosition = position;
                        // If position was loaded with a target, focus that if it is a valid target
                        if (focusTarget && Ext.Array.contains(tabbableChildren, focusTarget)) {
                            Ext.fly(focusTarget).focus();
                        } else {
                            Ext.fly(tabbableChildren[0]).focus();
                        }
                        me.activating = false;
                        // Avoid falling through to returning false
                        return true;
                    }
                }
            }
            me.activating = false;
        },
        /**
         * Called by TableView#saveFocus
         * @private
         */
        suspendActionableMode: function() {
            var me = this,
                actionables = me.grid.actionables,
                len = actionables.length,
                i;
            for (i = 0; i < len; i++) {
                actionables[i].suspend();
            }
        },
        resumeActionableMode: function(position) {
            var me = this,
                actionables = me.grid.actionables,
                len = actionables.length,
                i, activated;
            // Disable tabbability of elements within this view.
            me.toggleChildrenTabbability(false);
            for (i = 0; i < len; i++) {
                activated = activated || actionables[i].resume(position);
            }
            // If non of the Actionable responded, attempt to find a naturally focusable
            // child element.
            if (!activated) {
                me.activateCell(position);
            }
        },
        onRowExit: function(keyEvent, prevRow, newRow, forward, wrapDone) {
            var me = this,
                direction = forward ? 'nextSibling' : 'previousSibling',
                lockingPartner = me.lockingPartner,
                rowIdx;
            if (lockingPartner && lockingPartner.grid.isVisible()) {
                rowIdx = me.all.indexOf(prevRow);
                // TAB out of right side of view
                if (forward) {
                    // If normal side go to next row in locked side
                    if (me.isNormalView) {
                        rowIdx++;
                    }
                }
                // TAB out of left side of view
                // If locked side go to previous row in normal side
                else if (me.isLockedView) {
                    rowIdx--;
                }
                // We've switched sides.
                me.actionPosition = null;
                me = lockingPartner;
                newRow = me.all.item(rowIdx, true);
            }
            if (!me.hasListeners.beforerowexit || me.fireEvent('beforerowexit', me, keyEvent, prevRow, newRow, forward) !== false) {
                // Activate the next row.
                // This moves actionables' tabbable items to next row, restores that row's
                // tabbability and focuses the first/last tabbable element it finds
                // depending on direction.
                me.findFirstActionableElement(keyEvent, newRow, direction, forward, wrapDone);
            } else {
                return false;
            }
        },
        /**
         * Finds the first actionable element in the passed direction starting by
         * looking in the passed row.
         * @private
         */
        findFirstActionableElement: function(keyEvent, focusRow, direction, forward, wrapDone) {
            var me = this,
                columns = me.getVisibleColumnManager().getColumns(),
                columnCount = columns.length,
                actionables = me.grid.actionables,
                actionableCount = actionables.length,
                position = new Ext.grid.CellContext(me),
                focusCell, focusTarget, i, j, column, isActionable, tabbableChildren, prevRow;
            if (focusRow) {
                position.setRow(focusRow);
                for (i = 0; i < actionableCount; i++) {
                    // Tell all actionables who need to know that we are moving actionable mode
                    // to a new row.
                    // They should insert any tabbable elements into appropriate cells in the row.
                    if (actionables[i].activateRow) {
                        actionables[i].activateRow(focusRow);
                    }
                }
                // Look through the columns until we find one where the Actionables return
                // that the cell is actionable or there are tabbable elements found.
                // eslint-disable-next-line max-len
                for (i = (forward ? 0 : columnCount - 1); (forward ? i < columnCount : i > -1) && !focusTarget; i = i + (forward ? 1 : -1)) {
                    column = columns[i];
                    position.setColumn(column);
                    focusCell = (focusRow.dom || focusRow).querySelector(position.column.getCellSelector());
                    for (j = 0; j < actionableCount; j++) {
                        isActionable = isActionable || actionables[j].activateCell(position);
                    }
                    // In case any code in the cell activation churned
                    // the grid DOM and the position got refreshed.
                    // eg: 'edit' handler on previously active editor.
                    focusCell = Ext.fly(position.getCell(true));
                    if (focusCell) {
                        focusRow = position.getNode(true);
                        // TODO?
                        // If the focusCell is available (when using features with colspan
                        // the cell won't be there) and of there are restored tabbable elements
                        // rendered in the cell, or an Actionable is activated on this cell...
                        // If there are restored tabbable elements rendered in the cell,
                        // or an Actionable is activated on this cell...
                        focusCell.restoreTabbableState({
                            skipSelf: true
                        });
                        // Read tabbable children out to determine actionability.
                        // In case new DOM has been inserted by an 'edit' handler
                        // on previously active editor.
                        if ((tabbableChildren = focusCell.findTabbableElements()).length || isActionable) {
                            prevRow = me.actionRow && me.actionRow.dom;
                            me.actionRow = me.actionRowFly.attach(focusRow);
                            // Restore tabbabilty to all elements in this row.
                            me.actionRow.restoreTabbableState({
                                skipSelf: true
                            });
                            focusTarget = tabbableChildren[forward ? 0 : tabbableChildren.length - 1];
                        }
                    }
                }
                // Found a focusable element, focus it.
                if (focusTarget) {
                    // Keep actionPosition synched
                    me.actionPosition = me.getNavigationModel().actionPosition = position;
                    // If an async focus platform we must wait for the blur
                    // from the deactivate to clear before we can focus the next.
                    Ext.fly(focusTarget).focus(Ext.asyncFocus ? 1 : 0);
                    // Deactivate remaining tabbables in the row we were last actionable upon.
                    if (prevRow && focusRow !== prevRow) {
                        Ext.fly(prevRow).saveTabbableState({
                            skipSelf: true,
                            includeSaved: false
                        });
                    }
                } else {
                    // We walked off the end of the columns  without finding a focusTarget
                    // Process onRowExit in the current direction
                    me.onRowExit(keyEvent, focusRow, me.all.item(position.rowIdx + (forward ? 1 : -1)), forward, wrapDone);
                }
            }
            // No focusRow and not already wrapped round the whole view;
            // wrap round in the correct direction.
            else if (!wrapDone) {
                me.grid.ensureVisible(forward ? 0 : me.dataSource.getCount() - 1, {
                    callback: function(success, record, row) {
                        if (success) {
                            // Pass the flag saying we've already wrapped round once.
                            me.findFirstActionableElement(keyEvent, row, direction, forward, true);
                        } else {
                            me.ownerGrid.setActionableMode(false);
                        }
                    }
                });
            } else // If we've already wrapped, but not found a focus target, we must exit actionable mode.
            {
                me.ownerGrid.setActionableMode(false);
            }
        },
        stretchHeight: function(height) {
            /*
             * This is used when a table view is used in a lockable assembly.
             * Y scrolling is handled by an element which contains both grid views.
             * So each view has to be stretched to the full dataset height.
             * Setting the element height does not attain the maximum possible height.
             * Maximum content height is attained by adding "stretcher" elements
             * which have large margin-top values.
             */
            var me = this,
                scroller = me.getScrollable(),
                stretchers = me.stretchers,
                shortfall;
            if (height && me.tabGuardEl) {
                if (stretchers) {
                    stretchers[0].style.marginTop = stretchers[1].style.marginTop = me.el.dom.style.height = 0;
                }
                me.el.dom.style.height = scroller.constrainScrollRange(height) + 'px';
                shortfall = height - me.el.dom.offsetHeight;
                // Only resort to the stretcher els if they are needed
                if (shortfall > 0) {
                    me.el.dom.style.height = '';
                    stretchers = me.getStretchers();
                    shortfall = height - me.el.dom.offsetHeight;
                    if (shortfall > 0) {
                        stretchers[0].style.marginTop = scroller.constrainScrollRange(shortfall) + 'px';
                        shortfall = height - me.el.dom.offsetHeight;
                        if (shortfall > 0) {
                            stretchers[1].style.marginTop = Math.min(shortfall, scroller.maxSpacerMargin || 0) + 'px';
                        }
                    }
                }
            }
        },
        getStretchers: function() {
            var me = this,
                stretchers = me.stretchers,
                stretchCfg;
            if (stretchers) {
                // Ensure they're at the end
                me.el.appendChild(stretchers);
            } else {
                stretchCfg = {
                    cls: 'x-scroller-spacer',
                    style: 'position:relative'
                };
                stretchers = me.stretchers = me.el.appendChild([
                    stretchCfg,
                    stretchCfg
                ], true);
            }
            return stretchers;
        }
    }
}, function(Table) {
    // Create a flyweight for manipulating cells without having to
    // create a transient Ext.Element which has then to be garbage collected.
    Table.prototype.oldCellFly = new Ext.dom.Fly();
});

/**
 * Grids are an excellent way of showing large amounts of tabular data on the client side.
 * Essentially a supercharged `<table>`, GridPanel makes it easy to fetch, sort and filter
 * large amounts of data.
 *
 * Grids are composed of two main pieces - a {@link Ext.data.Store Store} full of data and
 * a set of columns to render.
 *
 * ## Basic GridPanel
 *
 *     @example
 *     Ext.create('Ext.data.Store', {
 *         storeId: 'simpsonsStore',
 *         fields:[ 'name', 'email', 'phone'],
 *         data: [
 *             { name: 'Lisa', email: 'lisa@simpsons.com', phone: '555-111-1224' },
 *             { name: 'Bart', email: 'bart@simpsons.com', phone: '555-222-1234' },
 *             { name: 'Homer', email: 'homer@simpsons.com', phone: '555-222-1244' },
 *             { name: 'Marge', email: 'marge@simpsons.com', phone: '555-222-1254' }
 *         ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Simpsons',
 *         store: Ext.data.StoreManager.lookup('simpsonsStore'),
 *         columns: [
 *             { text: 'Name', dataIndex: 'name' },
 *             { text: 'Email', dataIndex: 'email', flex: 1 },
 *             { text: 'Phone', dataIndex: 'phone' }
 *         ],
 *         height: 200,
 *         width: 400,
 *         renderTo: Ext.getBody()
 *     });
 *
 * The code above produces a simple grid with three columns. We specified a Store which
 * will load JSON data inline.
 * In most apps we would be placing the grid inside another container and wouldn't need to
 * use the {@link #height}, {@link #width} and {@link #renderTo} configurations but they
 * are included here to make it easy to get up and running.
 *
 * The grid we created above will contain a header bar with a title ('Simpsons'), a row of
 * column headers directly underneath and finally the grid rows under the headers.
 *
 * **Height config with bufferedRenderer: true**
 *
 * The {@link #height} config must be set when creating a grid using
 * {@link #bufferedRenderer bufferedRenderer}: true _and_ the grid's height is not managed
 * by an owning container layout.  In Ext JS 5.x bufferedRendering is true by default.
 *
 * ## Configuring columns
 *
 * By default, each column is sortable and will toggle between ASC and DESC sorting when
 * you click on its header. Each column header is also reorderable by default, and each
 * gains a drop-down menu with options to hide and show columns.  It's easy to configure
 * each column - here we use the same example as above and just modify the columns config:
 *
 *     columns: [
 *         {
 *             text: 'Name',
 *             dataIndex: 'name',
 *             sortable: false,
 *             hideable: false,
 *             flex: 1
 *         },
 *         {
 *             text: 'Email',
 *             dataIndex: 'email',
 *             hidden: true
 *         },
 *         {
 *             text: 'Phone',
 *             dataIndex: 'phone',
 *             width: 100
 *         }
 *     ]
 *
 * We turned off sorting and hiding on the 'Name' column so clicking its header now has no
 * effect. We also made the Email column hidden by default (it can be shown again by using
 * the menu on any other column). We also set the Phone column to a fixed with of 100px
 * and flexed the Name column, which means it takes up all remaining width after the other
 * columns have been accounted for. See the {@link Ext.grid.column.Column column docs} for
 * more details.
 *
 * ## Renderers
 *
 * As well as customizing columns, it's easy to alter the rendering of individual cells
 * using renderers. A renderer is tied to a particular column and is passed the value that
 * would be rendered into each cell in that column. For example, we could define a
 * renderer function for the email column to turn each email address into a mailto link:
 *
 *     columns: [
 *         {
 *             text: 'Email',
 *             dataIndex: 'email',
 *             renderer: function(value) {
 *                 return Ext.String.format('<a href="mailto:{0}">{1}</a>', value, value);
 *             }
 *         }
 *     ]
 *
 * See the {@link Ext.grid.column.Column column docs} for more information on renderers.
 *
 * ## Selection Models
 *
 * Sometimes you simply want to render data for viewing, but usually it's
 * necessary to interact with or update that data. Grids use a concept called a Selection
 * Model, which is simply a mechanism for selecting some part of the data in the grid. The
 * two main types of Selection Model are RowSelectionModel, where entire rows are
 * selected, and CellSelectionModel, where individual cells are selected.
 *
 * Grids use a Row Selection Model by default, but this is easy to customize like so:
 *
 *     Ext.create('Ext.grid.Panel', {
 *         selModel: 'cellmodel',
 *         store: ...
 *     });
 *
 *
 * Specifying the `cellmodel` changes a couple of things. Firstly, clicking on a cell now
 * selects just that cell (using a {@link Ext.selection.RowModel rowmodel} will select the
 * entire row), and secondly the keyboard navigation will walk from cell to cell instead 
 * of row to row. Cell-based selection models are usually used in conjunction with
 * editing.
 *
 * You may also utilize selModel as a config object for an instance of {@link Ext.selection.Model}.
 *
 * For example:
 *
 *     selModel: {
 *         selType: 'cellmodel',
 *         mode   : 'MULTI'
 *     }
 *
 * This allows you to modify additional selection model configurations such as:
 *
 * + {@link Ext.selection.Model#mode mode} - Specifies whether user may select multiple
 * rows or single rows
 * + {@link Ext.selection.Model#allowDeselect allowDeselect} - Specifies whether user may
 * deselect records (when in SINGLE mode)
 * + {@link Ext.selection.Model#ignoreRightMouseSelection ignoreRightMouseSelection} - Specifies
 * whether user may ignore right clicks
 * for selection purposes
 *
 * ## Sorting & Filtering
 *
 * Every grid is attached to a {@link Ext.data.Store Store}, which provides multi-sort and
 * filtering capabilities. It's
 * easy to set up a grid to be sorted from the start:
 *
 *     var myGrid = Ext.create('Ext.grid.Panel', {
 *         store: {
 *             fields: ['name', 'email', 'phone'],
 *             sorters: ['name', 'phone']
 *         },
 *         columns: [
 *             { text: 'Name',  dataIndex: 'name' },
 *             { text: 'Email', dataIndex: 'email' }
 *         ]
 *     });
 *
 * Sorting at run time is easily accomplished by simply clicking each column header. If
 * you need to perform sorting on more than one field at run time it's easy to do so by
 * adding new sorters to the store:
 *
 *     myGrid.store.sort([
 *         { property: 'name',  direction: 'ASC' },
 *         { property: 'email', direction: 'DESC' }
 *     ]);
 *
 * See {@link Ext.data.Store} for examples of filtering.
 *
 * ## State saving
 *
 * When configured {@link #stateful}, grids save their column state (order and width)
 * encapsulated within the default Panel state of changed width and height and
 * collapsed/expanded state.
 *
 * On a `stateful` grid, not only should the Grid have a {@link #stateId}, each 
 * {@link #columns column} of the grid should also be configured with a
 * {@link Ext.grid.column.Column#stateId stateId} which identifies that column locally
 * within the grid.
 * 
 * Omitting the `stateId` config from the columns results in columns with generated 
 * internal ID's.  The generated ID's are subject to change on each page load 
 * making it impossible for the state manager to restore the previous state of the 
 * columns.
 *
 * ## Plugins and Features
 *
 * Grid supports addition of extra functionality through features and plugins:
 *
 * - {@link Ext.grid.plugin.CellEditing CellEditing} - editing grid contents one cell at a time.
 *
 * - {@link Ext.grid.plugin.RowEditing RowEditing} - editing grid contents an entire row at a time.
 *
 * - {@link Ext.grid.plugin.DragDrop DragDrop} - drag-drop reordering of grid rows.
 *
 * - {@link Ext.toolbar.Paging Paging toolbar} - paging through large sets of data.
 *
 * - {@link Ext.grid.plugin.BufferedRenderer Infinite scrolling} - another way to handle
 *   large sets of data.
 *
 * - {@link Ext.grid.RowNumberer RowNumberer} - automatically numbered rows.
 *
 * - {@link Ext.grid.feature.Grouping Grouping} - grouping together rows having the same value
 *   in a particular field.
 *
 * - {@link Ext.grid.feature.Summary Summary} - a summary row at the bottom of a grid.
 *
 * - {@link Ext.grid.feature.GroupingSummary GroupingSummary} - a summary row at the bottom
 *   of each group.
 */
Ext.define('Ext.grid.Panel', {
    extend: 'Ext.panel.Table',
    alias: [
        'widget.gridpanel',
        'widget.grid'
    ],
    alternateClassName: [
        'Ext.list.ListView',
        'Ext.ListView',
        'Ext.grid.GridPanel'
    ],
    requires: [
        'Ext.view.Table'
    ],
    /**
     * @cfg viewType
     * @inheritdoc
     */
    viewType: 'tableview',
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'grid',
    lockable: false,
    /**
     * @cfg {Boolean} rowLines
     * False to remove row line styling
     */
    rowLines: true
});
// Columns config is required in Grid
/**
     * @cfg columns (required)
     * @inheritdoc
     */
/**
     * @event beforereconfigure
     * Fires before a reconfigure to enable modification of incoming Store and columns.
     * @param {Ext.grid.Panel} this
     * @param {Ext.data.Store} store The store that was passed to the {@link #method-reconfigure}
     * method
     * @param {Object[]} columns The column configs that were passed to the
     * {@link #method-reconfigure} method
     * @param {Ext.data.Store} oldStore The store that will be replaced
     * @param {Ext.grid.column.Column[]} oldColumns The column headers that will be replaced.
     */
/**
     * @event reconfigure
     * Fires after a reconfigure.
     * @param {Ext.grid.Panel} this
     * @param {Ext.data.Store} store The store that was passed to the
     * {@link #method-reconfigure} method
     * @param {Object[]} columns The column configs that were passed to the
     * {@link #method-reconfigure} method
     * @param {Ext.data.Store} oldStore The store that was replaced
     * @param {Ext.grid.column.Column[]} oldColumns The column headers that were replaced.
     */

/**
 * @private
 * This class encapsulates a row of managed Widgets/Components when a WidgetColumn, or
 * RowWidget plugin is used in a grid.
 *
 * The instances are recycled, and this class holds instances which are derendered so that they can
 * be moved back into newly rendered rows.
 *
 * Developers should not use this class.
 *
 */
Ext.define('Ext.grid.RowContext', {
    constructor: function(config) {
        Ext.apply(this, config);
        this.widgets = {};
    },
    setRecord: function(record, recordIndex) {
        var viewModel = this.viewModel;
        this.record = record;
        this.recordIndex = recordIndex;
        if (viewModel) {
            viewModel.set('record', record);
            viewModel.set('recordIndex', recordIndex);
        }
    },
    free: function(view) {
        var me = this,
            widgets = me.widgets,
            widgetId, widget, focusEl,
            viewModel = me.viewModel;
        me.record = null;
        if (viewModel) {
            viewModel.set('record');
            viewModel.set('recordIndex');
        }
        // All the widgets this RowContext manages must be blurred
        // and moved into the detached body to save them from garbage collection.
        for (widgetId in widgets) {
            widget = widgets[widgetId];
            // if the view is being refresh only remove widgets owned by this view
            if (view && view.refreshing && !view.el.contains(widget.el)) {
                
                continue;
            }
            // Focusables in a grid must not be tabbable by default when they get put back in.
            focusEl = widget.getFocusEl();
            if (focusEl) {
                // Widgets are reused so we must reset their tabbable state
                // regardless of their visibility.
                // For example, when removing rows in IE8 we're attaching
                // the nodes to a document-fragment which itself is invisible,
                // so isTabbable() returns false. Next time when we're reusing
                // this widget it will be attached to the document with its
                // tabbable state unreset, which might lead to undesired results.
                if (focusEl.isTabbable(true)) {
                    focusEl.saveTabbableState({
                        includeHidden: true
                    });
                }
                // Some browsers do not deliver a focus change upon DOM removal.
                // Force the issue here.
                focusEl.blur();
            }
            widget.detachFromBody();
        }
    },
    getWidget: function(view, ownerId, widgetCfg) {
        var me = this,
            widgets = me.widgets || (me.widgets = {}),
            ownerGrid = me.ownerGrid,
            rowViewModel = ownerGrid.rowViewModel,
            rowVM = me.viewModel,
            result;
        // Only spin up an attached ViewModel when we instantiate our first managed Widget
        // which uses binding.
        if ((widgetCfg.bind || rowViewModel) && !rowVM) {
            if (typeof rowViewModel === 'string') {
                rowViewModel = {
                    type: rowViewModel
                };
            }
            me.viewModel = rowVM = Ext.Factory.viewModel(Ext.merge({
                parent: ownerGrid.getRowContextViewModelParent(),
                data: {
                    record: me.record,
                    recordIndex: me.recordIndex
                }
            }, rowViewModel));
        }
        if (!(result = widgets[ownerId])) {
            result = widgets[ownerId] = Ext.widget(Ext.apply({
                ownerCmp: view,
                _rowContext: me,
                // This will spin up a VM on the grid if we don't have one that
                // will be shared across all instances. If we don't have a rowVM
                // or a viewmodel on the created object, we don't  need it, but
                // we can't really tell until we create an instance.
                $vmParent: rowVM || ownerGrid.getRowContextViewModelParent(),
                initInheritedState: me.initInheritedStateHook,
                lookupViewModel: me.lookupViewModelHook
            }, widgetCfg));
            result.$fromLocked = !!view.isLockedView;
            // Components initialize binding on render.
            // Widgets in finishRender which will not be called in this case.
            // That is only called when rendered by a layout.
            if (result.isWidget) {
                result.initBindable();
            } else {
                // Components store an Element reference to their container element.
                // For ordinary components that is not a problem since usually
                // that element is also referenced by Container instance, and gets
                // cleaned up when Container is destroyed. However for row widgets
                // the container element is a cell; cells do not get Element instances
                // and View is not going to clean them up.
                // So we have to clean up explicitly when the widget is destroyed
                // to avoid orphan Element instances in Ext.cache.
                result.collectContainerElement = true;
            }
        }
        return result;
    },
    getWidgets: function() {
        var widgets = this.widgets,
            id,
            result = [];
        for (id in widgets) {
            result.push(widgets[id]);
        }
        return result;
    },
    handleWidgetViewChange: function(view, ownerId) {
        var widget = this.widgets[ownerId];
        if (widget) {
            // In this particular case poking the ownerCmp doesn't really have any significance here
            // since users will typically be interacting at the grid level. However, this is more
            // for the sake of correctness. We don't need to do anything other
            // than poke the reference.
            widget.ownerCmp = view;
            widget.$fromLocked = !!view.isLockedView;
        }
    },
    destroy: function() {
        var me = this,
            widgets = me.widgets,
            widgetId, widget;
        for (widgetId in widgets) {
            widget = widgets[widgetId];
            widget._rowContext = null;
            widget.destroy();
        }
        Ext.destroy(me.viewModel);
        me.callParent();
    },
    privates: {
        initInheritedStateHook: function(inheritedState, inheritedStateInner) {
            var vmParent = this.$vmParent;
            this.self.prototype.initInheritedState.call(this, inheritedState, inheritedStateInner);
            if (!inheritedState.hasOwnProperty('viewModel') && vmParent) {
                inheritedState.viewModel = vmParent;
            }
        },
        lookupViewModelHook: function(skipThis) {
            var ret = skipThis ? null : this.getViewModel();
            if (!ret) {
                ret = this.$vmParent || null;
            }
            return ret;
        }
    }
});

/**
 * @private
 * Private Container class used by the {@link Ext.grid.RowEditor} to hold its buttons.
 */
Ext.define('Ext.grid.RowEditorButtons', {
    extend: 'Ext.container.Container',
    alias: 'widget.roweditorbuttons',
    frame: true,
    shrinkWrap: true,
    position: 'bottom',
    ariaRole: 'toolbar',
    constructor: function(config) {
        var me = this,
            rowEditor = config.rowEditor,
            cssPrefix = Ext.baseCSSPrefix,
            plugin = rowEditor.editingPlugin;
        config = Ext.apply({
            baseCls: cssPrefix + 'grid-row-editor-buttons',
            defaults: {
                xtype: 'button',
                ui: rowEditor.buttonUI,
                scope: plugin,
                flex: 1,
                minWidth: Ext.panel.Panel.prototype.minButtonWidth
            },
            items: [
                {
                    cls: cssPrefix + 'row-editor-update-button',
                    itemId: 'update',
                    handler: plugin.completeEdit,
                    text: rowEditor.saveBtnText,
                    disabled: rowEditor.updateButtonDisabled,
                    listeners: {
                        element: 'el',
                        keydown: me.onUpdateKeyDown,
                        scope: me
                    }
                },
                {
                    cls: cssPrefix + 'row-editor-cancel-button',
                    itemId: 'cancel',
                    handler: plugin.cancelEdit,
                    text: rowEditor.cancelBtnText,
                    listeners: {
                        element: 'el',
                        keydown: me.onCancelKeyDown,
                        scope: me
                    }
                }
            ]
        }, config);
        me.callParent([
            config
        ]);
        me.addClsWithUI(me.position);
    },
    // SHIFT+TAB off the update button loops back into the last field.
    onUpdateKeyDown: function(e) {
        if (e.shiftKey && e.getKey() === e.TAB) {
            e.stopEvent();
            // Must delay the focus, otherwise the imminent keyup will TAB off that field
            this.rowEditor.child(':focusable:not([isButton]):last').focus(false, true);
        }
    },
    // TAB off the cancel button loops back into the first field.
    onCancelKeyDown: function(e) {
        if (!e.shiftKey && e.getKey() === e.TAB) {
            e.stopEvent();
            // Must delay the focus, otherwise the imminent keyup will TAB off that field
            this.rowEditor.child(':focusable').focus(false, true);
        }
    },
    setButtonPosition: function(position) {
        var me = this,
            rowEditor = this.rowEditor,
            rowEditorHeight = rowEditor.getHeight(),
            rowEditorBody = rowEditor.body,
            bottom = '',
            top = '';
        me.removeClsWithUI(me.position);
        me.position = position;
        me.addClsWithUI(position);
        // we tried setting the top/bottom value in the stylesheet based on form field
        // height + row editor padding, but that approach does not work when there are
        // larger things inside the editor, e.g. textarea, so we have to measure
        // the row editor height and position the buttons accordingly (see EXTJSIV-9914).
        if (position === 'top') {
            bottom = (rowEditorHeight - rowEditorBody.getBorderWidth('t')) + 'px';
        } else {
            top = (rowEditorHeight - rowEditorBody.getBorderWidth('b')) + 'px';
        }
        me.el.setStyle({
            top: top,
            bottom: bottom
        });
    },
    privates: {
        getFramingInfoCls: function() {
            return this.baseCls + '-' + this.ui + '-' + this.position;
        },
        getFrameInfo: function() {
            var frameInfo = this.callParent();
            // Trick Renderable into rendering the top framing elements, even though they
            // are not needed in the default "bottom" position.  This allows us to flip the
            // buttons into "top" position without re-rendering.
            frameInfo.top = true;
            return frameInfo;
        }
    }
});

// Currently has the following issues:
// - Does not handle postEditValue
// - Fields without editors need to sync with their values in Store
// - starting to edit another record while already editing and dirty should probably prevent it
// - aggregating validation messages
// - tabIndex is not managed bc we leave elements in dom, and simply move via positioning
// - layout issues when changing sizes/width while hidden (layout bug)
/**
 * Internal utility class used to provide row editing functionality. For developers, they should use
 * the RowEditing plugin to use this functionality with a grid.
 *
 * @private
 */
Ext.define('Ext.grid.RowEditor', {
    extend: 'Ext.form.Panel',
    alias: 'widget.roweditor',
    requires: [
        'Ext.tip.ToolTip',
        'Ext.util.KeyNav',
        'Ext.grid.RowEditorButtons'
    ],
    /**
     * @cfg {Boolean} [removeUnmodified=false]
     * If configured as `true`, then canceling an edit on a newly inserted
     * record which has not been modified will delete that record from the store.
     */
    /**
     * @cfg {String} saveBtnText
     * The text for the Update button below the row edit form.
     * @locale
     */
    saveBtnText: 'Update',
    /**
     * @cfg {String} cancelBtnText
     * The text for the Cancel button below the row edit form.
     * @locale
     */
    cancelBtnText: 'Cancel',
    /**
     * @cfg {String} errorsText
     * The title for displaying an error tip.
     * @locale
     */
    errorsText: 'Errors',
    /**
     * @cfg {String} dirtyText
     * The message to display when dirty data prevents closing the row editor.
     * @locale
     */
    dirtyText: 'You need to commit or cancel your changes',
    lastScrollLeft: 0,
    lastScrollTop: 0,
    border: false,
    tabGuard: true,
    _wrapCls: Ext.baseCSSPrefix + 'grid-row-editor-wrap',
    errorCls: Ext.baseCSSPrefix + 'grid-row-editor-errors-item',
    buttonUI: 'default',
    // Change the hideMode to offsets so that we get accurate measurements when
    // the roweditor is hidden for laying out things like a TriggerField.
    hideMode: 'offsets',
    defaultFocus: 'field:canfocus',
    layout: {
        type: 'hbox',
        align: 'middle'
    },
    _cachedNode: false,
    initComponent: function() {
        var me = this,
            grid = me.editingPlugin.grid,
            Container = Ext.container.Container,
            form, normalCt, lockedCt;
        me.cls = Ext.baseCSSPrefix + 'grid-editor ' + Ext.baseCSSPrefix + 'grid-row-editor';
        me.lockable = grid.lockable;
        // Create field containing structure for when editing a lockable grid.
        if (me.lockable) {
            me.items = [
                // Locked columns container shrinkwraps the fields
                lockedCt = me.lockedColumnContainer = new Container({
                    $initParent: me,
                    id: grid.id + '-locked-editor-cells',
                    scrollable: {
                        x: false,
                        y: false
                    },
                    layout: {
                        type: 'hbox',
                        align: 'middle'
                    },
                    // Locked grid has a border, we must be exactly the same width
                    margin: '0 1 0 0'
                }),
                // Normal columns container flexes the remaining RowEditor width
                normalCt = me.normalColumnContainer = new Container({
                    $initParent: me,
                    id: grid.id + '-normal-editor-cells',
                    // not user scrollable, but needs a Scroller instance for syncing with view
                    scrollable: {
                        x: false,
                        y: false
                    },
                    layout: {
                        type: 'hbox',
                        align: 'middle'
                    },
                    flex: 1
                })
            ];
            delete lockedCt.$initParent;
            delete normalCt.$initParent;
            // keep horizontal position of fields in sync with view's horizontal scroll position
            lockedCt.getScrollable().addPartner(grid.lockedGrid.view.getScrollable(), 'x');
            normalCt.getScrollable().addPartner(grid.normalGrid.view.getScrollable(), 'x');
            grid.lockedGrid.on({
                collapse: me.onGridResize,
                expand: me.onGridResize,
                beginfloat: me.onBeginFloat,
                scope: me
            });
        } else {
            // initialize a scroller instance for maintaining horizontal scroll position
            me.setScrollable({
                x: false,
                y: false
            });
            // keep horizontal position of fields in sync with view's horizontal scroll position
            me.getScrollable().addPartner(grid.view.getScrollable(), 'x');
            me.lockedColumnContainer = me.normalColumnContainer = me;
        }
        me.callParent();
        if (me.fields) {
            me.addFieldsForColumn(me.fields, true);
            me.insertColumnEditor(me.fields);
            delete me.fields;
        }
        me.mon(Ext.GlobalEvents, {
            scope: me,
            show: me.repositionIfVisible
        });
        form = me.getForm();
        form.trackResetOnLoad = true;
        form.on('validitychange', me.onValidityChange, me);
        form.on('errorchange', me.onErrorChange, me);
    },
    //
    // Grid listener added when this is rendered.
    // Keep our containing element sized correctly
    //
    onGridResize: function() {
        if (this.rendered) {
            // eslint-disable-next-line vars-on-top
            var me = this,
                clientWidth = me.getClientWidth(),
                grid = me.editingPlugin.grid,
                gridBody = grid.body,
                btns = me.getFloatingButtons();
            me.wrapEl.setLocalX(gridBody.getOffsetsTo(grid)[0] + gridBody.getBorderWidth('l') - grid.el.getBorderWidth('l'));
            me.setWidth(clientWidth);
            btns.setLocalX((clientWidth - btns.getWidth()) / 2);
            if (me.lockable) {
                me.lockedColumnContainer.setWidth(grid.normalGrid.el.getLeft(true));
            }
        }
    },
    onBeginFloat: function(lockedGrid) {
        if (lockedGrid.isSliding && this.isVisible()) {
            return false;
        }
    },
    syncAllFieldWidths: function() {
        var me = this,
            editors = me.query('[isEditorComponent]'),
            len = editors.length,
            column, i;
        me.preventReposition = true;
        // In a locked grid, a RowEditor uses 2 inner containers, so need to use CQ to retrieve
        // configured editors which were stamped with the isEditorComponent property
        // in Editing.createColumnField
        for (i = 0; i < len; ++i) {
            column = editors[i].column;
            if (column.isVisible()) {
                me.onColumnShow(column);
            }
        }
        me.preventReposition = false;
    },
    syncFieldWidth: function(column) {
        var field = column.getEditor(),
            width;
        field._marginWidth = (field._marginWidth || field.el.getMargin('lr'));
        // Avoid negative width as this will throw Invalid Argument errors in IE
        width = Math.max(column.getWidth() - field._marginWidth, 0);
        field.setWidth(width);
        if (field.xtype === 'displayfield') {
            // displayfield must have the width set on the inputEl for ellipsis to work
            field.inputWidth = width;
        }
    },
    onValidityChange: function(form, valid) {
        this.updateButton(valid);
    },
    onErrorChange: function() {
        var me = this,
            valid;
        if (me.errorSummary && me.isVisible()) {
            valid = me.getForm().isValid();
            me[valid ? 'hideToolTip' : 'showToolTip']();
        }
    },
    updateButton: function(valid) {
        var buttons = this.floatingButtons;
        if (buttons) {
            buttons.child('#update').setDisabled(!valid);
        } else {
            // set flag so we can disabled when created if needed
            this.updateButtonDisabled = !valid;
        }
    },
    afterRender: function() {
        var me = this,
            plugin = me.editingPlugin,
            grid = plugin.grid;
        me.scroller = grid.getScrollable();
        me.callParent(arguments);
        // The scrollingViewEl is the TableView which scrolls
        me.scrollingView = grid.lockable ? grid.normalGrid.view : grid.view;
        me.scrollingViewEl = me.scrollingView.el;
        me.scroller.on('scroll', me.onViewScroll, me);
        // Prevent from bubbling click events to the grid view
        me.mon(me.el, {
            click: Ext.emptyFn,
            stopPropagation: true
        });
        // Ensure that the editor width always matches the total header width
        me.mon(grid, 'resize', me.onGridResize, me);
        if (me.lockable) {
            grid.lockedGrid.view.on('resize', 'onGridResize', me);
        }
        me.el.swallowEvent([
            'keypress',
            'keydown'
        ]);
        me.initKeyNav();
        me.mon(plugin.view, {
            beforerefresh: me.onBeforeViewRefresh,
            refresh: me.onViewRefresh,
            itemremove: me.onViewItemRemove,
            scope: me
        });
        me.syncAllFieldWidths();
        if (me.floatingButtons) {
            me.body.dom.setAttribute('aria-owns', me.floatingButtons.id);
        }
    },
    initKeyNav: function() {
        var me = this,
            plugin = me.editingPlugin;
        me.keyNav = new Ext.util.KeyNav({
            target: me.el,
            tab: {
                fn: me.onFieldTab,
                scope: me
            },
            enter: plugin.onEnterKey,
            esc: plugin.onEscKey,
            scope: plugin
        });
    },
    onBeforeViewRefresh: function(view) {
        var me = this,
            viewDom = view.el.dom;
        if (me.el.dom.parentNode === viewDom) {
            viewDom.removeChild(me.el.dom);
        }
    },
    onViewRefresh: function(view) {
        var me = this,
            context = me.context,
            row;
        // Ignore refresh caused by the completion process
        if (!me.completing) {
            // Recover our row node after a view refresh
            // Note that refresh could have been caused by column removal
            if (context && !context.column.destroyed && (row = view.getRow(context.record))) {
                if (view === context.column.getView()) {
                    context.row = row;
                    context.view = view;
                    me.reposition();
                    if (me.tooltip && me.tooltip.isVisible()) {
                        me.tooltip.setTarget(context.row);
                    }
                }
            } else {
                me.editingPlugin.cancelEdit();
            }
        }
    },
    onViewItemRemove: function(records, index, items, view) {
        var me = this,
            context = me.context,
            grid, store, gridView, plugin;
        // If the itemremove is due to refreshing, or we are not visible ignore it.
        // If the row for the current context record has gone after the
        // refresh, editing will be canceled there. See onViewRefresh above.
        if (!view.refreshing && context) {
            plugin = me.editingPlugin;
            grid = plugin.grid;
            store = grid.getStore();
            gridView = me.editingPlugin.view;
            // Checking if this is a deleted record or an element being derendered
            if (store.getById(me.getRecord().getId()) && !me._cachedNode) {
                // if this is an item being derendered and is also being edited
                // the flag _cachedNode will be set to true and an itemadd event will
                // be added to monitor when the editor should be reactivated.
                if (plugin.editing) {
                    me._cachedNode = true;
                    me.mon(gridView, {
                        itemadd: me.onViewItemAdd,
                        scope: me
                    });
                }
            } else if (!me._cachedNode) {
                me.activeField = null;
                me.editingPlugin.cancelEdit();
            }
        }
    },
    onViewItemAdd: function(records, index, items, view) {
        var me = this,
            plugin = me.editingPlugin,
            gridView, idx, record;
        // Checks if BufferedRenderer is adding the items 
        // if there was an item being edited, and it belongs to this batch
        // then update the row and node associations.
        if (me._cachedNode && me.context) {
            gridView = plugin.view;
            // Checks if there is an array of records being added
            // and if within this array, any record matches the one being edited before
            // if it does, the editor context is updated, the itemadd
            // event listener is removed and _cachedNode is cleared.
            if ((idx = Ext.Array.indexOf(records, me.context.record)) !== -1) {
                record = records[idx];
                me.context.node = record;
                me.context.row = gridView.getRow(record);
                me.context.cell = gridView.getCellByPosition(me.context, true);
                me.clearCache();
            }
        }
    },
    onViewScroll: function() {
        var me = this,
            viewEl = me.editingPlugin.view.el,
            scrollingView = me.scrollingView,
            scrollTop = me.scroller.getPosition().y,
            scrollLeft = scrollingView.getScrollX(),
            scrollTopChanged = scrollTop !== me.lastScrollTop,
            row;
        me.lastScrollTop = scrollTop;
        me.lastScrollLeft = scrollLeft;
        if (me.isVisible()) {
            row = Ext.getDom(me.context.row);
            // Only reposition if the row is in the DOM (buffered rendering may mean
            // the context row is not there)
            if (row && viewEl.contains(row)) {
                // This makes sure the Editor is repositioned if it was scrolled out of buffer range
                if (me.getLocalY()) {
                    me.setLocalY(0);
                }
                if (scrollTopChanged) {
                    // The row element in the context may be stale due to buffered rendering
                    // removing out-of-view rows, then re-inserting newly rendered ones
                    me.context.row = row;
                    me.reposition(null, true);
                    if ((me.tooltip && me.tooltip.isVisible())) {
                        me.repositionTip();
                    }
                }
            } else // If row is NOT in the DOM, ensure the editor is out of sight
            {
                me.setLocalY(-400);
                me.floatingButtons.hide();
            }
        }
    },
    onColumnResize: function(column, width) {
        var me = this;
        if (me.rendered && !me.editingPlugin.reconfiguring) {
            // Need to ensure our lockable/normal horizontal scrollrange is set
            me.onGridResize();
            me.onViewScroll();
            // The layout will have zeroed scroll position on the header, and we will
            // have synced to that, so resync to the correct state.
            if (me.lockable) {
                me.lockedColumnContainer.getScrollable().syncWithPartners();
                me.normalColumnContainer.getScrollable().syncWithPartners();
            } else {
                me.getScrollable().syncWithPartners();
            }
            if (!column.isGroupHeader) {
                me.syncFieldWidth(column);
                me.repositionIfVisible();
            }
        }
    },
    onColumnHide: function(column) {
        if (!this.editingPlugin.reconfiguring && !column.isGroupHeader) {
            column.getEditor().hide();
            this.repositionIfVisible();
        }
    },
    onColumnShow: function(column) {
        var me = this;
        if (me.rendered && !me.editingPlugin.reconfiguring && !column.isGroupHeader && column.getEditor) {
            column.getEditor().show();
            me.syncFieldWidth(column);
            if (!me.preventReposition) {
                me.repositionIfVisible();
            }
        }
    },
    onColumnMove: function(column, fromIdx, toIdx) {
        var me = this,
            locked = column.isLocked(),
            fieldContainer = locked ? me.lockedColumnContainer : me.normalColumnContainer,
            columns, i, len, after, offset;
        // If moving a group, move each leaf header
        if (column.isGroupHeader) {
            Ext.suspendLayouts();
            after = toIdx > fromIdx;
            offset = after ? 1 : 0;
            columns = column.getGridColumns();
            for (i = 0 , len = columns.length; i < len; ++i) {
                column = columns[i];
                toIdx = column.getIndex();
                if (after) {
                    ++offset;
                }
                me.setColumnEditor(column, toIdx + offset, fieldContainer);
            }
            Ext.resumeLayouts(true);
        } else {
            me.setColumnEditor(column, column.getIndex(), fieldContainer);
        }
    },
    setColumnEditor: function(column, idx, fieldContainer) {
        this.addFieldsForColumn(column);
        fieldContainer.insert(idx, column.getEditor());
    },
    onColumnAdd: function(column, pos) {
        // If a column header added, process its leaves
        if (column.isGroupHeader) {
            column = column.getGridColumns();
        }
        this.preventReposition = true;
        this.addFieldsForColumn(column);
        this.insertColumnEditor(column, pos);
        this.preventReposition = false;
    },
    insertColumnEditor: function(column, pos) {
        var me = this,
            field, fieldContainer, len, i;
        if (Ext.isArray(column)) {
            for (i = 0 , len = column.length; i < len; i++) {
                me.insertColumnEditor(column[i]);
            }
            return;
        }
        if (!column.getEditor) {
            return;
        }
        if (pos == null) {
            pos = column.getIndex();
        }
        fieldContainer = column.isLocked() ? me.lockedColumnContainer : me.normalColumnContainer;
        // Insert the column's field into the editor panel.
        fieldContainer.insert(pos, field = column.getEditor());
        // Ensure the view scrolls the field into view on focus
        field.on('focus', me.onFieldFocus, me);
        me.needsSyncFieldWidths = true;
    },
    onFieldFocus: function(field) {
        // Cache the active field so that we can restore focus into its cell onHide
        // Makes the cursor always be placed at the end of the textfield
        // when the field is being edited for the first time.
        if (field.selectText) {
            field.selectText(field.inputEl.dom.value.length);
        }
        this.activeField = field;
        this.context.setColumn(field.column);
        // skipFocusScroll should be true right after the editor has been started
        if (!this.skipFocusScroll) {
            field.column.getView().getScrollable().ensureVisible(field.el);
        } else {
            this.skipFocusScroll = null;
        }
    },
    onFieldTab: function(e) {
        var me = this,
            activeField = me.activeField,
            rowIdx = me.context.rowIdx,
            forwards = !e.shiftKey,
            target = activeField[forwards ? 'nextNode' : 'previousNode'](':focusable'),
            count;
        // We must control where the focus goes on Tab key press in fields.
        // The reason is that if there are elements with tabIndex > 0 elsewhere
        // in the document, natural tabbing might go out of the RowEditor, and
        // it might take an undeterminable amount of Tab key presses to get back
        // to the RowEditor.
        e.stopEvent();
        // No field to TAB to, navigate forwards or backwards
        if (!target || !target.isDescendant(me)) {
            // Tabbing out of a dirty editor - wrap to the update button
            if (me.isDirty() && !me.autoUpdate) {
                me.floatingButtons.child('#update').focus();
            } else {
                count = me.view.dataSource.getCount();
                // Editor is clean - navigate to next or previous row
                rowIdx = rowIdx + (forwards ? 1 : -1);
                // Wrap around if we reached the end
                if (rowIdx < 0) {
                    rowIdx = count - 1;
                } else if (rowIdx >= count) {
                    rowIdx = 0;
                }
                if (forwards) {
                    target = me.down(':focusable:not([isButton]):first');
                    // If going back to the first column, scroll back to field.
                    // If we're in a locking view, this has to be done programatically
                    // to avoid jarring when navigating from the locked back into the normal side
                    activeField.column.getView().getScrollable().ensureVisible(activeField.ownerCt.child(':focusable').el);
                } else {
                    target = me.down(':focusable:not([isButton]):last');
                }
                // We need to park focus on a tab guard while the fields
                // are being updated with the values from new row. Also
                // we might need to scroll the view, and RowEditor transition
                // can be animated. We don't want screen readers to announce
                // the transitions.
                me.tabGuardBeforeEl.focus();
                me.editingPlugin.startEdit(rowIdx, target.column);
            }
        } else {
            target.focus();
        }
    },
    destroyColumnEditor: function(column) {
        var field;
        if (column.hasEditor() && (field = column.getEditor())) {
            field.destroy();
        }
    },
    getFloatingButtons: function() {
        var me = this,
            btns = me.floatingButtons;
        if (!btns && !me.destroying && !me.destroyed) {
            me.floatingButtons = btns = new Ext.grid.RowEditorButtons({
                ownerCmp: me,
                rowEditor: me,
                hidden: me.hidden
            });
        }
        return btns;
    },
    repositionIfVisible: function(c) {
        var me = this,
            view = me.view;
        // If we're showing ourselves, jump out
        // If the component we're showing doesn't contain the view
        if (c && (c === me || !c.el.isAncestor(view.el))) {
            return;
        }
        if (me.isVisible() && view.isVisible(true)) {
            me.reposition();
        }
    },
    isLayoutChild: function(ownerCandidate) {
        // RowEditor is not a floating component, but won't be laid out by the grid
        return false;
    },
    getRefOwner: function() {
        return this.editingPlugin.grid;
    },
    getRefItems: function(deep) {
        var me = this,
            result, buttons;
        if (me.lockable) {
            // refItems must include ALL children. Must include the two containers
            // because we don't know what is being searched for.
            result = [
                me.lockedColumnContainer
            ];
            result.push.apply(result, me.lockedColumnContainer.getRefItems(deep));
            result.push(me.normalColumnContainer);
            result.push.apply(result, me.normalColumnContainer.getRefItems(deep));
        } else {
            result = me.callParent(arguments);
        }
        buttons = me.getFloatingButtons();
        if (buttons) {
            result.push.apply(result, buttons.getRefItems(deep));
        }
        return result;
    },
    reposition: function(animateConfig, fromScrollHandler) {
        var me = this,
            context = me.context,
            row = context && context.row,
            wrapEl = me.wrapEl,
            rowTop, localY, deltaY, afterPosition;
        // Position this editor if the context row is rendered (buffered rendering may mean
        // that it's not in the DOM at all)
        if (row && Ext.isElement(row)) {
            deltaY = me.syncButtonPosition(context);
            rowTop = me.calculateLocalRowTop(row);
            localY = me.calculateEditorTop(rowTop);
            // If not being called from scroll handler...
            // If the editor's top will end up above the fold
            // or the bottom will end up below the fold,
            // organize an afterPosition handler which will bring it into view and focus
            // the correct input field
            afterPosition = function() {
                me.syncEditorClip();
                me.wrapAnim = null;
                if (!fromScrollHandler) {
                    if (deltaY) {
                        me.scroller.scrollBy(0, deltaY, true);
                    }
                    me.focusColumnField(context.column);
                }
            };
            // Get the y position of the row relative to its top-most static parent.
            // offsetTop will be relative to the table, and is incorrect
            // when mixed with certain grid features (e.g., grouping).
            if (animateConfig) {
                me.wrapAnim = wrapEl.addAnimation(Ext.applyIf({
                    to: {
                        top: localY
                    },
                    duration: animateConfig.duration || 125,
                    callback: afterPosition
                }, animateConfig));
            } else {
                wrapEl.setLocalY(localY);
                afterPosition();
            }
        }
    },
    /**
     * @private
     * Returns the scroll delta required to scroll the context row into view in order to make
     * the whole of this editor visible.
     * @return {Number} the scroll delta. Zero if scrolling is not required.
     */
    getScrollDelta: function() {
        var me = this,
            scrollingViewDom = me.scroller.getElement().dom,
            context = me.context,
            body = me.body,
            deltaY = 0,
            clientHeight, scrollHeight, editorHeight;
        if (context) {
            deltaY = Ext.fly(context.row).getOffsetsTo(scrollingViewDom)[1];
            if (deltaY < 0) {
                deltaY -= body.getBorderPadding().beforeY;
            } else if (deltaY > 0) {
                clientHeight = scrollingViewDom.clientHeight;
                scrollHeight = scrollingViewDom.scrollHeight;
                editorHeight = me.getHeight() + me.floatingButtons.getHeight();
                // There might be not enough height to scroll
                if (clientHeight === scrollHeight && editorHeight > clientHeight) {
                    return 0;
                }
                deltaY = Math.max(deltaY + editorHeight - clientHeight - body.getBorderWidth('b'), 0);
                if (deltaY > 0) {
                    deltaY -= body.getBorderPadding().afterY;
                }
            }
        }
        return deltaY;
    },
    //
    // Calculates the top pixel position of the passed row within the view's scroll space.
    // So in a large, scrolled grid, this could be several thousand pixels.
    //
    calculateLocalRowTop: function(row) {
        var grid = this.editingPlugin.grid;
        return Ext.fly(row).getOffsetsTo(grid)[1] - grid.el.getBorderWidth('t') + this.lastScrollTop;
    },
    // Given the top pixel position of a row in the scroll space,
    // calculate the editor top position in the view's encapsulating element.
    // This will only ever be in the visible range of the view's element.
    calculateEditorTop: function(rowTop) {
        var result = rowTop - this.lastScrollTop;
        if (this._buttonsOnTop) {
            result -= (this.body.dom.offsetHeight - this.context.row.offsetHeight - this.body.getBorderPadding().afterY);
        } else {
            result -= this.body.getBorderPadding().beforeY;
        }
        return result;
    },
    getClientWidth: function() {
        var me = this,
            grid = me.editingPlugin.grid,
            lockedCmp, result;
        if (me.lockable) {
            lockedCmp = (grid.lockedGrid.collapsed && grid.lockedGrid.placeholder) || grid.lockedGrid;
            result = lockedCmp.getRegion().union(grid.scrollBody.el.getClientRegion()).width;
        } else {
            result = grid.view.el.dom.clientWidth;
        }
        return result;
    },
    getEditor: function(fieldInfo) {
        var me = this;
        if (Ext.isNumber(fieldInfo)) {
            // In a locked grid, a RowEditor uses 2 inner containers, so need to use CQ to retrieve
            // configured editors which were stamped with the isEditorComponent property
            // in Editing.createColumnField
            return me.query('[isEditorComponent]')[fieldInfo];
        } else if (fieldInfo.isHeader && !fieldInfo.isGroupHeader) {
            return fieldInfo.getEditor();
        }
    },
    addFieldsForColumn: function(column, initial) {
        var me = this,
            i, len, field, style;
        if (Ext.isArray(column)) {
            for (i = 0 , len = column.length; i < len; i++) {
                me.addFieldsForColumn(column[i], initial);
            }
            return;
        }
        if (column.getEditor) {
            // Get a default display field if necessary
            field = column.getEditor(null, me.getDefaultFieldCfg());
            // Focus is managed by RowEditor
            field.preventRefocus = true;
            if (column.align === 'right') {
                style = field.fieldStyle;
                if (style) {
                    if (Ext.isObject(style)) {
                        // Create a copy so we don't clobber the object
                        style = Ext.apply({}, style);
                    } else {
                        style = Ext.dom.Element.parseStyles(style);
                    }
                    if (!style.textAlign && !style['text-align']) {
                        style.textAlign = 'right';
                    }
                } else {
                    style = 'text-align:right';
                }
                field.fieldStyle = style;
            }
            if (column.xtype === 'actioncolumn') {
                field.fieldCls += ' ' + Ext.baseCSSPrefix + 'form-action-col-field';
            }
            if (me.isVisible() && me.context) {
                if (field.is('displayfield')) {
                    me.renderColumnData(field, me.context.record, column);
                } else {
                    field.suspendEvents();
                    field.setValue(me.context.record.get(column.dataIndex));
                    field.resumeEvents();
                }
            }
            if (column.hidden) {
                me.onColumnHide(column);
            } else if (column.rendered && !initial) {
                // Setting after initial render
                me.onColumnShow(column);
            }
        }
    },
    getDefaultFieldCfg: function() {
        return {
            xtype: 'displayfield',
            skipLabelForAttribute: true,
            // Override Field's implementation so that the default display fields
            // will not return values. This is done because
            // the display field will pick up column renderers from the grid.
            getModelData: function() {
                return null;
            }
        };
    },
    loadRecord: function(record) {
        var me = this,
            form = me.getForm(),
            fields = form.getFields(),
            items = fields.items,
            length = items.length,
            i, displayFields, isValid, item;
        // temporarily suspend events on form fields before loading record to prevent
        // the fields' change events from firing
        for (i = 0; i < length; i++) {
            item = items[i];
            item.suspendEvents();
            item.resetToInitialValue();
        }
        form.loadRecord(record);
        for (i = 0; i < length; i++) {
            items[i].resumeEvents();
        }
        // Because we suspend the events, none of the field events will get propagated to
        // the form, so the valid state won't be correct.
        if (form.hasInvalidField() === form.wasValid) {
            delete form.wasValid;
        }
        isValid = form.isValid();
        if (me.errorSummary) {
            if (isValid) {
                me.hideToolTip();
            } else {
                me.showToolTip();
            }
        }
        me.updateButton(isValid);
        // render display fields so they honor the column renderer/template
        displayFields = me.query('>displayfield');
        length = displayFields.length;
        for (i = 0; i < length; i++) {
            me.renderColumnData(displayFields[i], record);
        }
    },
    renderColumnData: function(field, record, activeColumn) {
        var me = this,
            grid = me.editingPlugin.grid,
            headerCt = grid.headerCt,
            view = me.scrollingView,
            store = view.dataSource,
            column = activeColumn || field.column,
            value = record.get(column.dataIndex),
            renderer = column.editRenderer || column.renderer,
            metaData, rowIdx, colIdx,
            scope = (column.usingDefaultRenderer && !column.scope) ? column : column.scope;
        // honor our column's renderer (TemplateHeader sets renderer for us!)
        if (renderer) {
            metaData = {
                tdCls: '',
                style: ''
            };
            rowIdx = store.indexOf(record);
            colIdx = headerCt.getHeaderIndex(column);
            value = renderer.call(scope || headerCt.ownerCt, value, metaData, record, rowIdx, colIdx, store, view);
        }
        field.setRawValue(value);
    },
    beforeEdit: function() {
        var me = this,
            scrollDelta;
        // Can't show the editor on a fragile, floated locked side
        if (me.lockable && me.editingPlugin.grid.lockedGrid.floatedFromCollapse) {
            return false;
        }
        if (me.isVisible() && (me.isDirty() || me.context.record.phantom)) {
            if (me.autoUpdate) {
                me.editingPlugin.completeEdit();
            } else if (me.autoCancel) {
                me.editingPlugin.cancelEdit();
            } else if (me.errorSummary) {
                // Scroll the visible RowEditor that is in error state back into view
                scrollDelta = me.getScrollDelta();
                if (scrollDelta) {
                    me.scrollingViewEl.scrollBy(0, scrollDelta, true);
                }
                me.showToolTip();
                return false;
            }
        }
    },
    /**
     * Start editing the specified grid at the specified position.
     * @param {Ext.data.Model} record The Store data record which backs the row to be edited.
     * @param {Ext.data.Model} columnHeader The Column object defining the column to be focused
     */
    startEdit: function(record, columnHeader) {
        var me = this,
            editingPlugin = me.editingPlugin,
            grid = editingPlugin.grid,
            context = me.context = editingPlugin.context,
            alreadyVisible = me.isVisible(),
            wrapEl = me.wrapEl,
            wasRendered = me.rendered,
            label;
        if (me._cachedNode) {
            me.clearCache();
        }
        // Ensure that the render operation does not lay out
        // The show call will update the layout
        Ext.suspendLayouts();
        if (!wasRendered) {
            me.width = me.getClientWidth();
            me.render(grid.el, grid.el.dom.firstChild);
            // The wrapEl is a container for the editor and buttons.  We use a wrap el
            // (instead of rendering the buttons inside the editor) so that the editor and
            // buttons can be clipped separately when overflowing.
            // See https://sencha.jira.com/browse/EXTJS-13851
            wrapEl = me.wrapEl = me.el.wrap();
            // Change the visibilityMode to offsets so that we get accurate measurements
            // when the roweditor is hidden for laying out things like a TriggerField.
            wrapEl.setVisibilityMode(3);
            wrapEl.addCls(me._wrapCls);
            me.getFloatingButtons().render(wrapEl);
            // On first show we need to ensure that we have the scroll positions cached
            me.onViewScroll();
        }
        me.setLocalY(0);
        // Select at the clicked position.
        context.grid.getSelectionModel().selectByPosition({
            row: record,
            column: columnHeader
        });
        if (me.rendered && me.formAriaLabel) {
            label = Ext.String.formatEncode(me.formAriaLabel, me.formAriaLabelRowBase + context.rowIdx);
            me.body.dom.setAttribute('aria-label', label);
        }
        // Make sure the container el is correctly sized.
        me.onGridResize();
        // Layout the form with the new content if we are already visible.
        // Otherwise, just allow resumption, and the show will update the layout.
        Ext.resumeLayouts(alreadyVisible);
        if (alreadyVisible) {
            me.reposition(true);
        } else {
            // this will prevent the onFieldFocus method from calling
            // scrollIntoView right after startEdit as this will be
            // handled by the Editing plugin.
            me.skipFocusScroll = true;
            me.show();
        }
        // Reload the record data.
        // After positioning so that any error tip will be aligned correctly.
        me.loadRecord(record);
        // Sync our scroll position on first show
        if (!wasRendered) {
            if (me.lockable) {
                me.lockedColumnContainer.getScrollable().syncWithPartners();
                me.normalColumnContainer.getScrollable().syncWithPartners();
            } else {
                me.getScrollable().syncWithPartners();
            }
        }
    },
    // determines the amount by which the row editor will overflow, and flips the buttons
    // to the top of the editor if the required scroll amount is greater than the available
    // scroll space. Returns the scrollDelta required to scroll the editor into view after
    // adjusting the button position.
    syncButtonPosition: function(context) {
        var me = this,
            scrollDelta = me.getScrollDelta(),
            floatingButtons = me.getFloatingButtons(),
            // If this is negative, it means we're not scrolling so lets just ignore it
            scrollHeight = Math.max(0, me.scroller.getSize().y - me.scroller.getClientSize().y),
            overflow = scrollDelta - (scrollHeight - me.scroller.getPosition().y);
        floatingButtons.show();
        // If that's the last visible row, buttons should be at the top regardless of scrolling,
        // but not if there is just one row which is both first and last.
        if (overflow > 0 || (context.rowIdx > 0 && context.isLastRenderedRow())) {
            if (!me._buttonsOnTop) {
                floatingButtons.setButtonPosition('top');
                me._buttonsOnTop = true;
                me.layout.setAlign('bottom');
                me.updateLayout();
            }
            scrollDelta = 0;
        } else if (me._buttonsOnTop !== false) {
            floatingButtons.setButtonPosition('bottom');
            me._buttonsOnTop = false;
            me.layout.setAlign('top');
            me.updateLayout();
        } else // Ensure button Y position is synced with Editor height even if button
        // orientation doesn't change
        {
            floatingButtons.setButtonPosition(floatingButtons.position);
        }
        return scrollDelta;
    },
    syncEditorClip: function() {
        // Since the editor is rendered to the grid el, all its visible parts must be clipped
        // when scrolled outside of the grid view area so that it does not overlap the scrollbar
        // or docked items.
        var me = this,
            tip = me.tooltip,
            // Clipping region must be *within* scrollbars, so in the case of locking view,
            // we cannot use the lockingView's el because that *contains* two grids.
            // We must use the scroller el.
            clipRegion = me.scroller.getElement().getConstrainRegion();
        me.clipTo(clipRegion);
        me.floatingButtons.clipTo(clipRegion);
        if (tip && tip.isVisible()) {
            tip.clipTo(clipRegion, 5);
        }
    },
    focusColumnField: function(column) {
        var field, didFocus;
        if (column && !column.destroyed) {
            if (column.isVisible()) {
                field = this.getEditor(column);
                if (field && field.isFocusable(true)) {
                    didFocus = true;
                    field.focus();
                }
            }
            if (!didFocus) {
                this.focusColumnField(column.next());
            }
        }
    },
    cancelEdit: function() {
        var me = this,
            form = me.getForm(),
            fields = form.getFields(),
            items = fields.items,
            length = items.length,
            i,
            record = me.context.record;
        if (me._cachedNode) {
            me.clearCache();
        }
        me.hide();
        // If we are editing a new record, and we cancel still in invalid state, then remove it.
        if (record && record.phantom && !record.modified && me.removeUnmodified) {
            me.editingPlugin.grid.store.remove(record);
        }
        form.clearInvalid();
        // temporarily suspend events on form fields before reseting the form to prevent
        // the fields' change events from firing
        for (i = 0; i < length; i++) {
            items[i].suspendEvents();
        }
        form.reset();
        for (i = 0; i < length; i++) {
            items[i].resumeEvents();
        }
    },
    /*
    * @private
    */
    clearCache: function() {
        var me = this;
        me.mun(me.editingPlugin.view, {
            itemadd: me.onViewItemAdd,
            scope: me
        });
        me._cachedNode = false;
    },
    completeEdit: function() {
        var me = this,
            form = me.getForm();
        if (!form.isValid()) {
            return false;
        }
        me.completing = true;
        form.updateRecord(me.context.record);
        me.hide();
        me.completing = false;
        return true;
    },
    onShow: function() {
        var me = this;
        me.wrapEl.show();
        me.callParent(arguments);
        if (me.needsSyncFieldWidths) {
            me.suspendLayouts();
            me.preventReposition = true;
            me.syncAllFieldWidths();
            me.preventReposition = false;
            me.resumeLayouts(true);
        }
        delete me.needsSyncFieldWidths;
        if (me.rendered) {
            me.initTabGuards(true);
        }
        me.reposition();
    },
    onHide: function() {
        var me = this,
            context = me.context,
            column, focusContext,
            activeEl = Ext.Element.getActiveElement();
        me.context = null;
        // If they used ESC or ENTER in a Field
        if (me.el.contains(activeEl) && me.activeField) {
            column = me.activeField.column;
        } else // If they used a button
        {
            column = context.column;
        }
        // Hiding could have been caused by removing our column
        if (column && !column.destroyed) {
            focusContext = new Ext.grid.CellContext(column.getView()).setPosition(context.record, column);
            focusContext.view.getNavigationModel().setPosition(focusContext);
        }
        me.activeField = null;
        me.wrapEl.hide();
        me.callParent(arguments);
        // RowEditor is hidden via offsets so need to deactivate tab guards manually
        if (me.rendered) {
            me.initTabGuards(false);
        }
        if (me.tooltip) {
            me.hideToolTip();
        }
    },
    onResize: function(width, height) {
        this.wrapEl.setSize(width, height);
    },
    isDirty: function() {
        return this.getForm().isDirty();
    },
    getToolTip: function() {
        var me = this,
            tip = me.tooltip,
            grid = me.editingPlugin.grid;
        if (!tip) {
            me.tooltip = tip = new Ext.tip.ToolTip({
                cls: Ext.baseCSSPrefix + 'grid-row-editor-errors',
                title: me.errorsText,
                autoHide: false,
                closable: true,
                closeAction: 'disable',
                anchor: 'left',
                anchorToTarget: true,
                targetOffset: [
                    Ext.scrollbar.width(),
                    0
                ],
                constrainPosition: true,
                constrainTo: document.body
            });
            grid.add(tip);
            // Layout may change the grid's positioning.
            me.mon(grid, {
                afterlayout: me.onGridLayout,
                scope: me
            });
        }
        return tip;
    },
    hideToolTip: function() {
        var me = this,
            tip = me.getToolTip();
        if (tip.rendered) {
            tip.disable();
        }
    },
    showToolTip: function(wrapAnim) {
        var me = this,
            tip = me.getToolTip();
        // If called while we are moving, wait till new position.
        if (!wrapAnim && me.wrapAnim) {
            return me.wrapAnim.on({
                afteranimate: me.showToolTip,
                scope: me,
                single: true
            });
        }
        tip.update(me.getErrors());
        me.repositionTip();
        tip.enable();
    },
    onGridLayout: function() {
        if (this.tooltip && this.tooltip.isVisible()) {
            this.repositionTip();
        }
    },
    repositionTip: function() {
        var me = this,
            tip = me.getToolTip();
        if (tip.isVisible()) {
            tip.realignToTarget();
        } else {
            tip.showBy(me.el);
        }
        me.syncEditorClip();
    },
    getErrors: function() {
        var me = this,
            errors = [],
            fields = me.query('>[isFormField]'),
            length = fields.length,
            i, fieldErrors, field;
        for (i = 0; i < length; i++) {
            field = fields[i];
            fieldErrors = field.getErrors();
            if (fieldErrors.length) {
                errors.push(me.createErrorListItem(fieldErrors[0], field.column.text));
            }
        }
        // Only complain about unsaved changes if all the fields are valid
        if (!errors.length && !me.autoCancel && me.isDirty()) {
            errors[0] = me.createErrorListItem(me.dirtyText);
        }
        return '<ul class="' + Ext.baseCSSPrefix + 'list-plain">' + errors.join('') + '</ul>';
    },
    createErrorListItem: function(e, name) {
        e = name ? name + ': ' + e : e;
        return '<li class="' + this.errorCls + '">' + e + '</li>';
    },
    doDestroy: function() {
        var me = this;
        if (me.wrapAnim) {
            Ext.fx.Manager.removeAnim(me.wrapAnim);
            me.wrapAnim = null;
        }
        // Properties must be cleared because class-specific getRefItems explicitly references them.
        me.keyNav = me.floatingButtons = me.tooltip = Ext.destroy(me.keyNav, me.floatingButtons, me.tooltip, me.wrapEl);
        me.callParent();
    }
});

Ext.define('Ext.grid.Scroller', {
    constructor: Ext.deprecated()
});

/**
 * @private
 */
Ext.define('Ext.view.DropZone', {
    extend: 'Ext.dd.DropZone',
    indicatorCls: Ext.baseCSSPrefix + 'grid-drop-indicator',
    indicatorHtml: [
        '<div class="',
        Ext.baseCSSPrefix,
        'grid-drop-indicator-left" role="presentation"></div>',
        '<div class="' + Ext.baseCSSPrefix + 'grid-drop-indicator-right" role="presentation"></div>'
    ].join(''),
    constructor: function(config) {
        var me = this;
        Ext.apply(me, config);
        // Create a ddGroup unless one has been configured.
        // User configuration of ddGroups allows users to specify which
        // DD instances can interact with each other. Using one
        // based on the id of the View would isolate it and mean it can only
        // interact with a DragZone on the same View also using a generated ID.
        if (!me.ddGroup) {
            me.ddGroup = 'view-dd-zone-' + me.view.id;
        }
        // The DropZone's encapsulating element is the View's main element.
        // It must be this because drop gestures may require scrolling on hover near a scrolling
        // boundary. In Ext 4.x two DD instances may not use the same element, so a DragZone
        // on this same View must use the View's parent element as its element.
        me.callParent([
            me.view.el
        ]);
    },
    // Fire an event through the client DataView. Lock this DropZone during the event processing
    // so that its data does not become corrupted by processing mouse events.
    fireViewEvent: function() {
        var me = this,
            result;
        me.lock();
        result = me.view.fireEvent.apply(me.view, arguments);
        me.unlock();
        return result;
    },
    getTargetFromEvent: function(e) {
        var node = e.getTarget(this.view.getItemSelector()),
            mouseY, nodeList, testNode, i, len, box;
        // Not over a row node: The content may be narrower than the View's encapsulating element,
        // so return the closest. If we fall through because the mouse is below the nodes
        // (or there are no nodes), we'll get an onContainerOver call.
        if (!node) {
            mouseY = e.getY();
            for (i = 0 , nodeList = this.view.getNodes() , len = nodeList.length; i < len; i++) {
                testNode = nodeList[i];
                box = Ext.fly(testNode).getBox();
                if (mouseY <= box.bottom) {
                    return testNode;
                }
            }
        }
        return node;
    },
    getIndicator: function() {
        var me = this;
        if (!me.indicator) {
            me.indicator = new Ext.Component({
                ariaRole: 'presentation',
                html: me.indicatorHtml,
                cls: me.indicatorCls,
                ownerCt: me.view,
                floating: true,
                alignOnScroll: false,
                shadow: false
            });
        }
        return me.indicator;
    },
    getPosition: function(e, node) {
        var y = e.getXY()[1],
            region = Ext.fly(node).getRegion(),
            pos;
        if ((region.bottom - y) >= (region.bottom - region.top) / 2) {
            pos = "before";
        } else {
            pos = "after";
        }
        return pos;
    },
    /**
     * @private
     * Determines whether the record at the specified offset from the passed record
     * is in the drag payload.
     * @param records
     * @param record
     * @param offset
     * @return {Boolean} True if the targeted record is in the drag payload
     */
    containsRecordAtOffset: function(records, record, offset) {
        if (!record) {
            return false;
        }
        // eslint-disable-next-line vars-on-top
        var view = this.view,
            recordIndex = view.indexOf(record),
            nodeBefore = view.getNode(recordIndex + offset),
            recordBefore = nodeBefore ? view.getRecord(nodeBefore) : null;
        return recordBefore && Ext.Array.contains(records, recordBefore);
    },
    positionIndicator: function(node, data, e) {
        var me = this,
            view = me.view,
            pos = me.getPosition(e, node),
            overRecord = view.getRecord(node),
            draggingRecords = data.records,
            indicatorY, scrollable, scrollableEl, container, containerY;
        if (!Ext.Array.contains(draggingRecords, overRecord) && (pos === 'before' && !me.containsRecordAtOffset(draggingRecords, overRecord, -1) || pos === 'after' && !me.containsRecordAtOffset(draggingRecords, overRecord, 1))) {
            me.valid = true;
            if (me.overRecord !== overRecord || me.currentPosition !== pos) {
                scrollable = me.view.getScrollable();
                scrollableEl = scrollable && scrollable.getElement();
                container = (scrollableEl && scrollableEl.isScrollable()) ? scrollableEl : Ext.fly(view.getNodeContainer());
                containerY = container.getY();
                indicatorY = Ext.fly(node).getY() - containerY - 1;
                if (pos === 'after') {
                    indicatorY += Ext.fly(node).getHeight();
                }
                me.getIndicator().setWidth(Ext.fly(view.el).getWidth()).showAt(0, indicatorY);
                // Cache the overRecord and the 'before' or 'after' indicator.
                me.overRecord = overRecord;
                me.currentPosition = pos;
            }
        } else {
            me.invalidateDrop();
        }
    },
    invalidateDrop: function() {
        if (this.valid) {
            this.valid = false;
            this.getIndicator().hide();
        }
    },
    // The mouse is over a View node
    onNodeOver: function(node, dragZone, e, data) {
        var me = this;
        if (!Ext.Array.contains(data.records, me.view.getRecord(node))) {
            me.positionIndicator(node, data, e);
        }
        return me.valid ? me.dropAllowed : me.dropNotAllowed;
    },
    // Moved out of the DropZone without dropping.
    // Remove drop position indicator
    notifyOut: function(node, dragZone, e, data) {
        var me = this;
        me.callParent([
            node,
            dragZone,
            e,
            data
        ]);
        me.overRecord = me.currentPosition = null;
        me.valid = false;
        if (me.indicator) {
            me.indicator.hide();
        }
    },
    // The mouse is past the end of all nodes (or there are no nodes)
    onContainerOver: function(dd, e, data) {
        var me = this,
            view = me.view,
            count = view.dataSource.getCount();
        // There are records, so position after the last one
        if (count) {
            me.positionIndicator(view.all.last(), data, e);
        } else // No records, position the indicator at the top
        {
            me.overRecord = me.currentPosition = null;
            me.getIndicator().setWidth(Ext.fly(view.el).getWidth()).showAt(0, 0);
            me.valid = true;
        }
        return me.dropAllowed;
    },
    onContainerDrop: function(dd, e, data) {
        return this.onNodeDrop(dd, null, e, data);
    },
    onNodeDrop: function(targetNode, dragZone, e, data) {
        var me = this,
            dropHandled = false,
            overRecord = me.overRecord,
            currentPosition = me.currentPosition,
            // Create a closure to perform the operation which the event handler may use.
            // Users may now set the wait parameter in the beforedrop handler, and perform any kind
            // of asynchronous processing such as an Ext.Msg.confirm, or an Ajax request,
            // and complete the drop gesture at some point in the future by calling either the
            // processDrop or cancelDrop methods.
            dropHandlers = {
                wait: false,
                processDrop: function() {
                    me.invalidateDrop();
                    me.handleNodeDrop(data, overRecord, currentPosition);
                    dropHandled = true;
                    me.fireViewEvent('drop', targetNode, data, overRecord, currentPosition);
                },
                cancelDrop: function() {
                    me.invalidateDrop();
                    dropHandled = true;
                }
            },
            performOperation = false;
        if (me.valid) {
            performOperation = me.fireViewEvent('beforedrop', targetNode, data, overRecord, currentPosition, dropHandlers);
            if (dropHandlers.wait) {
                return;
            }
            if (performOperation !== false) {
                // If either of the drop handlers were called in the event handler,
                // do not do it again.
                if (!dropHandled) {
                    dropHandlers.processDrop();
                }
            }
        }
        return performOperation;
    },
    destroy: function() {
        this.indicator = Ext.destroy(this.indicator);
        this.callParent();
    }
});

/**
 * @private
 */
Ext.define('Ext.grid.ViewDropZone', {
    extend: 'Ext.view.DropZone',
    indicatorHtml: '<div class="' + Ext.baseCSSPrefix + 'grid-drop-indicator-left" role="presentation"></div><div class="' + Ext.baseCSSPrefix + 'grid-drop-indicator-right" role="presentation"></div>',
    indicatorCls: Ext.baseCSSPrefix + 'grid-drop-indicator',
    handleNodeDrop: function(data, record, position) {
        var view = this.view,
            store = view.getStore(),
            crossView = view !== data.view,
            selectAfter = crossView || data.records.length > 1,
            index, records, i, len;
        // If the copy flag is set, create a copy of the models
        if (data.copy) {
            records = data.records;
            for (i = 0 , len = records.length; i < len; i++) {
                records[i] = records[i].copy();
            }
        } else if (selectAfter) {
            /*
             * Remove from the source store only if we are moving to a different store
             * or shifting multiple records.
             */
            data.view.store.remove(data.records);
        }
        if (record && position) {
            index = store.indexOf(record);
            // 'after', or undefined (meaning a drop at index -1 on an empty View)...
            if (position !== 'before') {
                index++;
            }
            store.insert(index, data.records);
        } else // No position specified - append.
        {
            store.add(data.records);
        }
        // Select the dropped nodes if needed
        if (selectAfter) {
            view.getSelectionModel().select(data.records);
        }
        // Focus the first dropped node.
        view.getNavigationModel().setPosition(data.records[0]);
    }
});

/**
 * Plugin to add header resizing functionality to a HeaderContainer.
 * Always resizing header to the left of the splitter you are resizing.
 */
Ext.define('Ext.grid.plugin.HeaderResizer', {
    extend: 'Ext.plugin.Abstract',
    alias: 'plugin.gridheaderresizer',
    requires: [
        'Ext.dd.DragTracker',
        'Ext.util.Region'
    ],
    disabled: false,
    config: {
        /**
         * @cfg {Boolean} dynamic
         * True to resize on the fly rather than using a proxy marker.
         * @accessor
         */
        dynamic: false
    },
    colHeaderCls: Ext.baseCSSPrefix + 'column-header',
    minColWidth: 40,
    maxColWidth: 1000,
    eResizeCursor: 'col-resize',
    init: function(headerCt) {
        var me = this;
        me.headerCt = headerCt;
        headerCt.on('render', me.afterHeaderRender, me, {
            single: me
        });
        // Pull minColWidth from the minWidth in the Column prototype
        if (!me.minColWidth) {
            me.self.prototype.minColWidth = Ext.grid.column.Column.prototype.minWidth;
        }
    },
    destroy: function() {
        var me = this,
            tracker = me.tracker;
        if (tracker) {
            tracker.destroy();
            me.tracker = null;
        }
        // The grid may happen to never render
        me.headerCt.un('render', me.afterHeaderRender, me);
        me.headerCt = null;
        me.callParent();
    },
    afterHeaderRender: function() {
        var me = this,
            headerCt = me.headerCt,
            el = headerCt.el;
        headerCt.mon(el, 'mousemove', me.onHeaderCtMouseMove, me);
        me.markerOwner = me.ownerGrid = me.headerCt.up('tablepanel').ownerGrid;
        me.tracker = new Ext.dd.DragTracker({
            disabled: me.disabled,
            onBeforeStart: me.onBeforeStart.bind(me),
            onStart: me.onStart.bind(me),
            onDrag: me.onDrag.bind(me),
            onEnd: me.onEnd.bind(me),
            onCancel: me.onCancel.bind(me),
            tolerance: 3,
            autoStart: 300,
            el: el
        });
        headerCt.setTouchAction({
            panX: false
        });
    },
    // As we mouse over individual headers, change the cursor to indicate
    // that resizing is available, and cache the resize target header for use
    // if/when they mousedown.
    onHeaderCtMouseMove: function(e) {
        var me = this;
        if (me.headerCt.dragging || me.disabled) {
            if (me.activeHd) {
                me.activeHd.el.dom.style.cursor = '';
                delete me.activeHd;
            }
        } else if (e.pointerType !== 'touch') {
            me.findActiveHeader(e);
        }
    },
    findActiveHeader: function(e) {
        var me = this,
            headerCt = me.headerCt,
            headerEl = e.getTarget('.' + me.colHeaderCls, headerCt.el, true),
            ownerGrid = me.ownerGrid,
            ownerLockable = ownerGrid.ownerLockable,
            overHeader, resizeHeader, headers, header;
        me.activeHd = null;
        if (headerEl) {
            overHeader = Ext.getCmp(headerEl.id);
            // If near the right edge, we're resizing the column we are over.
            if (overHeader.isAtEndEdge(e)) {
                // Cannot resize the only column in a forceFit grid.
                if (headerCt.visibleColumnManager.getColumns().length === 1 && headerCt.forceFit) {
                    return;
                }
                resizeHeader = overHeader;
            }
            // Else... we might be near the right edge
            else if (overHeader.isAtStartEdge(e)) {
                // Extract previous visible leaf header
                headers = headerCt.visibleColumnManager.getColumns();
                header = overHeader.isGroupHeader ? overHeader.getGridColumns()[0] : overHeader;
                resizeHeader = headers[Ext.Array.indexOf(headers, header) - 1];
                // If there wasn't one, and we are the normal side of a lockable assembly then
                // use the last visible leaf header of the locked side.
                if (!resizeHeader && ownerLockable && !ownerGrid.isLocked) {
                    headers = ownerLockable.lockedGrid.headerCt.visibleColumnManager.getColumns();
                    resizeHeader = headers[headers.length - 1];
                }
            }
            // We *are* resizing
            if (resizeHeader) {
                // If we're attempting to resize a group header, that cannot be resized,
                // so find its last visible leaf header; Group headers are sized
                // by the size of their child headers.
                if (resizeHeader.isGroupHeader) {
                    headers = resizeHeader.getGridColumns();
                    resizeHeader = headers[headers.length - 1];
                }
                // Check if the header is resizable. Continue checking the old "fixed" property,
                // but also check whether the resizable property is set to false.
                if (resizeHeader && !(resizeHeader.fixed || (resizeHeader.resizable === false))) {
                    me.activeHd = resizeHeader;
                    overHeader.el.dom.style.cursor = me.eResizeCursor;
                    if (overHeader.triggerEl) {
                        overHeader.triggerEl.dom.style.cursor = me.eResizeCursor;
                    }
                }
            } else // reset
            {
                overHeader.el.dom.style.cursor = '';
                if (overHeader.triggerEl) {
                    overHeader.triggerEl.dom.style.cursor = '';
                }
            }
        }
        return me.activeHd;
    },
    // only start when there is an activeHd
    onBeforeStart: function(e) {
        var me = this;
        // If on touch, we will have received no mouseover, so we have to
        // decide whether the touchstart is in a resize zone, and if so, which header
        // is to be sized. Cache any activeHd because it will be cleared on subsequent
        // mousemoves outside the resize zone.
        me.dragHd = me.activeHd || e.pointerType === 'touch' && me.findActiveHeader(e);
        if (me.dragHd && !me.headerCt.dragging) {
            // Prevent drag and longpress gestures being triggered by this mousedown
            e.claimGesture();
            // Calculate how far off the right marker line the mouse pointer is.
            // This will be the xDelta during the following drag operation.
            me.xDelta = me.dragHd.getX() + me.dragHd.getWidth() - me.tracker.getXY()[0];
            me.tracker.constrainTo = me.getConstrainRegion();
            return true;
        } else {
            me.headerCt.dragging = false;
            return false;
        }
    },
    // When mouseup and we have not begun dragging.
    // The setup done in onbeforeStart must be cleared.
    onCancel: function(e) {
        this.dragHd = this.activeHd = null;
        this.headerCt.dragging = false;
    },
    // get the region to constrain to, takes into account max and min col widths
    getConstrainRegion: function() {
        var me = this,
            dragHdEl = me.dragHd.el,
            nextHd,
            ownerGrid = me.ownerGrid,
            widthModel = ownerGrid.getSizeModel().width,
            // eslint-disable-next-line max-len
            maxColWidth = widthModel.shrinkWrap ? me.headerCt.getWidth() - me.headerCt.visibleColumnManager.getColumns().length * me.minColWidth : me.maxColWidth,
            result;
        // If forceFit, then right constraint is based upon not being able to force the next header
        // beyond the minColWidth. If there is no next header, then the header may not be expanded.
        if (me.headerCt.forceFit) {
            nextHd = me.dragHd.nextNode('gridcolumn:not([hidden]):not([isGroupHeader])');
            if (nextHd && me.headerInSameGrid(nextHd)) {
                maxColWidth = dragHdEl.getWidth() + (nextHd.getWidth() - me.minColWidth);
            }
        }
        // If resize header is in a locked grid, the maxWidth has to be 30px
        // within the available locking grid's width.
        // But only if the locked grid shrinkwraps its columns
        else if (ownerGrid.isLocked && widthModel.shrinkWrap) {
            maxColWidth = me.dragHd.up('[scrollerOwner]').getTargetEl().getWidth(true) - ownerGrid.getWidth() - (ownerGrid.ownerLockable.normalGrid.visibleColumnManager.getColumns().length * me.minColWidth + Ext.scrollbar.width());
        }
        result = me.adjustConstrainRegion(dragHdEl.getRegion(), 0, 0, 0, me.minColWidth);
        result.right = dragHdEl.getX() + maxColWidth;
        return result;
    },
    // initialize the left and right hand side markers around
    // the header that we are resizing
    onStart: function(e) {
        var me = this,
            dragHd = me.dragHd,
            width = dragHd.el.getWidth(),
            headerCt = dragHd.getRootHeaderCt(),
            x, y, markerOwner, lhsMarker, rhsMarker, markerHeight;
        me.headerCt.dragging = true;
        me.origWidth = width;
        // setup marker proxies
        if (!me.dynamic) {
            markerOwner = me.markerOwner;
            // https://sencha.jira.com/browse/EXTJSIV-11299
            // In Neptune (and other themes with wide frame borders), resize handles are embedded
            // in borders, *outside* of the outer element's content area, therefore
            // the outer element is set to overflow:visible.
            // During column resize, we should not see the resize markers outside the grid,
            // so set to overflow:hidden.
            if (markerOwner.frame && markerOwner.resizable) {
                me.gridOverflowSetting = markerOwner.el.dom.style.overflow;
                markerOwner.el.dom.style.overflow = 'hidden';
            }
            x = me.getLeftMarkerX(markerOwner);
            lhsMarker = markerOwner.getLhsMarker();
            rhsMarker = markerOwner.getRhsMarker();
            markerHeight = me.ownerGrid.body.getHeight() + headerCt.getHeight();
            y = headerCt.getOffsetsTo(markerOwner)[1] - markerOwner.el.getBorderWidth('t');
            // Ensure the markers have the correct cursor in case the cursor is *exactly* over
            // this single pixel line, not just within the active resize zone
            lhsMarker.dom.style.cursor = me.eResizeCursor;
            rhsMarker.dom.style.cursor = me.eResizeCursor;
            lhsMarker.setLocalY(y);
            rhsMarker.setLocalY(y);
            lhsMarker.setHeight(markerHeight);
            rhsMarker.setHeight(markerHeight);
            me.setMarkerX(lhsMarker, x);
            me.setMarkerX(rhsMarker, x + width);
        }
    },
    // synchronize the rhsMarker with the mouse movement
    onDrag: function(e) {
        var me = this;
        if (me.dynamic) {
            me.doResize();
        } else {
            me.setMarkerX(me.getMovingMarker(me.markerOwner), me.calculateDragX(me.markerOwner));
        }
    },
    getMovingMarker: function(markerOwner) {
        return markerOwner.getRhsMarker();
    },
    onEnd: function(e) {
        var me = this,
            markerOwner = me.markerOwner;
        me.headerCt.dragging = false;
        if (me.dragHd) {
            if (!me.dynamic) {
                // If we had saved the gridOverflowSetting, restore it
                if ('gridOverflowSetting' in me) {
                    markerOwner.el.dom.style.overflow = me.gridOverflowSetting;
                }
                // hide markers
                me.setMarkerX(markerOwner.getLhsMarker(), -9999);
                me.setMarkerX(markerOwner.getRhsMarker(), -9999);
            }
            me.doResize();
            // On mouseup (a real mouseup), we must be ready to start dragging again immediately -
            // Leave the activeHd active.
            if (e.pointerType !== 'touch') {
                me.dragHd = null;
                me.activeHd.el.dom.style.cursor = me.eResizeCursor;
            } else {
                me.dragHd = me.activeHd = null;
            }
        }
        // Do not process the upcoming click after this mouseup. It's not a click gesture
        me.headerCt.blockNextEvent();
    },
    doResize: function() {
        var me = this,
            dragHd = me.dragHd,
            nextHd,
            offset = me.tracker.getOffset('point');
        // Only resize if we have dragged any distance in the X dimension...
        if (dragHd && offset[0]) {
            // resize the dragHd
            if (dragHd.flex) {
                delete dragHd.flex;
            }
            Ext.suspendLayouts();
            // Set the new column width.
            // Adjusted for the offset from the actual column border that the mousedown
            // too place at.
            me.adjustColumnWidth(offset[0] - me.xDelta);
            // In the case of forceFit, change the following Header width.
            // Constraining so that neither neighbour can be sized to below minWidth is handled
            // in getConstrainRegion
            if (me.headerCt.forceFit) {
                nextHd = dragHd.nextNode('gridcolumn:not([hidden]):not([isGroupHeader])');
                if (nextHd && !me.headerInSameGrid(nextHd)) {
                    nextHd = null;
                }
                if (nextHd) {
                    delete nextHd.flex;
                    nextHd.setWidth(nextHd.getWidth() - offset[0]);
                }
            }
            // Apply the two width changes by laying out the owning HeaderContainer
            Ext.resumeLayouts(true);
        }
    },
    // nextNode can traverse out of this grid, possibly to others on the page, so limit it here
    headerInSameGrid: function(header) {
        var grid = this.dragHd.up('tablepanel');
        return !!header.up(grid);
    },
    disable: function() {
        var tracker = this.tracker;
        this.disabled = true;
        if (tracker) {
            tracker.disable();
        }
    },
    enable: function() {
        var tracker = this.tracker;
        this.disabled = false;
        if (tracker) {
            tracker.enable();
        }
    },
    calculateDragX: function(markerOwner) {
        return this.tracker.getXY('point')[0] + this.xDelta - markerOwner.getX() - markerOwner.el.getBorderWidth('l');
    },
    getLeftMarkerX: function(markerOwner) {
        return this.dragHd.getX() - markerOwner.getX() - markerOwner.el.getBorderWidth('l') - 1;
    },
    setMarkerX: function(marker, x) {
        marker.setLocalX(x);
    },
    adjustConstrainRegion: function(region, t, r, b, l) {
        return region.adjust(t, r, b, l);
    },
    adjustColumnWidth: function(offsetX) {
        this.dragHd.setWidth(this.origWidth + offsetX);
    }
});

/**
 * @private
 */
Ext.define('Ext.grid.header.DragZone', {
    extend: 'Ext.dd.DragZone',
    colHeaderSelector: '.' + Ext.baseCSSPrefix + 'column-header',
    colInnerSelector: '.' + Ext.baseCSSPrefix + 'column-header-inner',
    maxProxyWidth: 120,
    constructor: function(headerCt) {
        var me = this;
        me.headerCt = headerCt;
        me.ddGroup = me.getDDGroup();
        me.autoGroup = true;
        me.callParent([
            headerCt.el
        ]);
        me.proxy.el.addCls(Ext.baseCSSPrefix + 'grid-col-dd');
    },
    getDDGroup: function() {
        return 'header-dd-zone-' + this.headerCt.up('[scrollerOwner]').id;
    },
    getDragData: function(e) {
        if (e.getTarget(this.colInnerSelector)) {
            // eslint-disable-next-line vars-on-top
            var header = e.getTarget(this.colHeaderSelector),
                headerCmp, ddel;
            if (header) {
                headerCmp = Ext.getCmp(header.id);
                if (!this.headerCt.dragging && headerCmp.draggable && !(headerCmp.isAtStartEdge(e) || headerCmp.isAtEndEdge(e))) {
                    ddel = document.createElement('div');
                    ddel.role = 'presentation';
                    ddel.innerHTML = headerCmp.text;
                    return {
                        ddel: ddel,
                        header: headerCmp
                    };
                }
            }
        }
        return false;
    },
    onBeforeDrag: function() {
        return !(this.headerCt.dragging || this.disabled);
    },
    onInitDrag: function() {
        this.headerCt.dragging = true;
        this.headerCt.hideMenu();
        this.callParent(arguments);
    },
    onDragDrop: function() {
        this.headerCt.dragging = false;
        this.callParent(arguments);
    },
    afterRepair: function() {
        this.callParent();
        this.headerCt.dragging = false;
    },
    getRepairXY: function() {
        return this.dragData.header.el.getXY();
    },
    disable: function() {
        this.disabled = true;
    },
    enable: function() {
        this.disabled = false;
    }
});

/**
 * @private
 */
Ext.define('Ext.grid.header.DropZone', {
    extend: 'Ext.dd.DropZone',
    colHeaderCls: Ext.baseCSSPrefix + 'column-header',
    proxyOffsets: [
        -4,
        -9
    ],
    constructor: function(headerCt) {
        var me = this;
        me.headerCt = headerCt;
        me.ddGroup = me.getDDGroup();
        me.autoGroup = true;
        me.callParent([
            headerCt.el
        ]);
    },
    destroy: function() {
        Ext.destroy(this.topIndicator, this.bottomIndicator);
        this.callParent();
    },
    getDDGroup: function() {
        return 'header-dd-zone-' + this.headerCt.up('[scrollerOwner]').id;
    },
    getTargetFromEvent: function(e) {
        return e.getTarget('.' + this.colHeaderCls);
    },
    getTopIndicator: function() {
        if (!this.topIndicator) {
            this.topIndicator = Ext.getBody().createChild({
                role: 'presentation',
                cls: Ext.baseCSSPrefix + "col-move-top",
                // tell the spec runner to ignore this element when checking if the dom is clean
                "data-sticky": true,
                html: "&#160;"
            });
            this.indicatorXOffset = Math.floor((this.topIndicator.dom.offsetWidth + 1) / 2);
        }
        return this.topIndicator;
    },
    getBottomIndicator: function() {
        if (!this.bottomIndicator) {
            this.bottomIndicator = Ext.getBody().createChild({
                role: 'presentation',
                cls: Ext.baseCSSPrefix + "col-move-bottom",
                // tell the spec runner to ignore this element when checking if the dom is clean
                "data-sticky": true,
                html: "&#160;"
            });
        }
        return this.bottomIndicator;
    },
    getLocation: function(e, t) {
        var x = e.getXY()[0],
            region = Ext.fly(t).getRegion(),
            pos;
        if ((region.right - x) <= (region.right - region.left) / 2) {
            pos = "after";
        } else {
            pos = "before";
        }
        return {
            pos: pos,
            header: Ext.getCmp(t.id),
            node: t
        };
    },
    positionIndicator: function(data, node, e) {
        var me = this,
            dragHeader = data.header,
            dropLocation = me.getLocation(e, node),
            targetHeader = dropLocation.header,
            pos = dropLocation.pos,
            nextHd, prevHd, topIndicator, bottomIndicator, topAnchor, bottomAnchor, topXY, bottomXY, headerCtEl, minX, maxX, allDropZones, ln, i, dropZone;
        // Avoid expensive CQ lookups and DOM calculations if dropPosition has not changed
        if (targetHeader === me.lastTargetHeader && pos === me.lastDropPos) {
            return;
        }
        nextHd = dragHeader.nextSibling('gridcolumn:not([hidden])');
        prevHd = dragHeader.previousSibling('gridcolumn:not([hidden])');
        me.lastTargetHeader = targetHeader;
        me.lastDropPos = pos;
        // Cannot drag to before non-draggable start column
        if (!targetHeader.draggable && pos === 'before' && targetHeader.getIndex() === 0) {
            return false;
        }
        data.dropLocation = dropLocation;
        if ((dragHeader !== targetHeader) && ((pos === "before" && nextHd !== targetHeader) || (pos === "after" && prevHd !== targetHeader)) && !targetHeader.isDescendantOf(dragHeader)) {
            // As we move in between different DropZones that are in the same
            // group (such as the case when in a locked grid), invalidateDrop
            // on the other dropZones.
            allDropZones = Ext.dd.DragDropManager.getRelated(me);
            ln = allDropZones.length;
            i = 0;
            for (; i < ln; i++) {
                dropZone = allDropZones[i];
                if (dropZone !== me && dropZone.invalidateDrop) {
                    dropZone.invalidateDrop();
                }
            }
            me.valid = true;
            topIndicator = me.getTopIndicator();
            bottomIndicator = me.getBottomIndicator();
            if (pos === 'before') {
                topAnchor = 'bc-tl';
                bottomAnchor = 'tc-bl';
            } else {
                topAnchor = 'bc-tr';
                bottomAnchor = 'tc-br';
            }
            // Calculate arrow positions. Offset them to align exactly with column border line
            topXY = topIndicator.getAlignToXY(targetHeader.el, topAnchor);
            bottomXY = bottomIndicator.getAlignToXY(targetHeader.el, bottomAnchor);
            // constrain the indicators to the viewable section
            headerCtEl = me.headerCt.el;
            minX = headerCtEl.getX() - me.indicatorXOffset;
            maxX = headerCtEl.getX() + headerCtEl.getWidth();
            topXY[0] = Ext.Number.constrain(topXY[0], minX, maxX);
            bottomXY[0] = Ext.Number.constrain(bottomXY[0], minX, maxX);
            // position and show indicators
            topIndicator.setXY(topXY);
            bottomIndicator.setXY(bottomXY);
            topIndicator.show();
            bottomIndicator.show();
        } else // invalidate drop operation and hide indicators
        {
            me.invalidateDrop();
        }
    },
    invalidateDrop: function() {
        this.valid = false;
        this.hideIndicators();
    },
    onNodeOver: function(node, dragZone, e, data) {
        var me = this,
            from = data.header,
            doPosition, fromPanel, to, toPanel;
        if (data.header.el.dom === node) {
            doPosition = false;
        } else {
            data.isLock = data.isUnlock = data.crossPanel = false;
            to = me.getLocation(e, node).header;
            // Dragging within the same container - always valid
            doPosition = (from.ownerCt === to.ownerCt);
            // If from different containers, and they are not sealed, then continue checking
            if (!doPosition && (!from.ownerCt.isSealed() && !to.ownerCt.isSealed())) {
                doPosition = true;
                fromPanel = from.up('tablepanel');
                toPanel = to.up('tablepanel');
                if (fromPanel !== toPanel) {
                    data.crossPanel = true;
                    // If it's a lock operation, check that it's allowable.
                    data.isLock = toPanel.isLocked && !fromPanel.isLocked;
                    data.isUnlock = !toPanel.isLocked && fromPanel.isLocked;
                    if ((data.isUnlock && from.lockable === false) || (data.isLock && !from.isLockable())) {
                        doPosition = false;
                    }
                }
            }
        }
        if (doPosition) {
            me.positionIndicator(data, node, e);
        } else {
            me.valid = false;
        }
        return me.valid ? me.dropAllowed : me.dropNotAllowed;
    },
    hideIndicators: function() {
        var me = this;
        me.getTopIndicator().hide();
        me.getBottomIndicator().hide();
        me.lastTargetHeader = me.lastDropPos = null;
    },
    onNodeOut: function() {
        this.hideIndicators();
    },
    /**
     * @private
     * Used to determine the move position for the view's data columns for nested headers
     * at any level.
     */
    getNestedHeader: function(header, first) {
        var items = header.items,
            pos;
        if (header.isGroupHeader && items.length) {
            pos = !first ? 'first' : 'last';
            header = this.getNestedHeader(items[pos](), first);
        }
        return header;
    },
    onNodeDrop: function(node, dragZone, e, data) {
        // Do not process the upcoming click after this mouseup. It's not a click gesture
        this.headerCt.blockNextEvent();
        // Note that dropLocation.pos refers to whether the header is dropped
        // before or after the target node!
        if (!this.valid) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            dragHeader = data.header,
            dropLocation = data.dropLocation,
            dropPosition = dropLocation.pos,
            targetHeader = dropLocation.header,
            fromCt = dragHeader.ownerCt,
            fromCtRoot = fromCt.getRootHeaderCt(),
            toCt = targetHeader.ownerCt,
            // Use the full column manager here, the indices we want are for moving the actual items
            // in the container. The HeaderContainer translates this to visible columns
            // for informing the view and firing events.
            visibleColumnManager = me.headerCt.visibleColumnManager,
            visibleFromIdx = visibleColumnManager.getHeaderIndex(dragHeader),
            visibleToIdx, colsToMove, scrollerOwner, savedWidth;
        // If we are dragging in between two HeaderContainers that have had the lockable mixin
        // injected we will lock/unlock headers in between sections, and then continue with another
        // execution of onNodeDrop to ensure the header is dropped into the correct group.
        if (data.isLock || data.isUnlock) {
            scrollerOwner = fromCt.up('[scrollerOwner]');
            visibleToIdx = toCt.items.indexOf(targetHeader);
            if (dropPosition === 'after') {
                visibleToIdx++;
            }
            if (data.isLock) {
                scrollerOwner.lock(dragHeader, visibleToIdx, toCt);
            } else {
                scrollerOwner.unlock(dragHeader, visibleToIdx, toCt);
            }
        } else // This is a drop within the same HeaderContainer.
        {
            // For the after position, we need to update the visibleToIdx index. In case it's nested
            // in one or more grouped headers, we need to get the last header (or the first,
            // depending on the dropPosition) in the items collection for the most deeply-nested
            // header, whether it be first or last in the collection. This will yield the header
            // index in the visibleColumnManager, which will correctly maintain a list
            // of all the headers.
            visibleToIdx = dropPosition === 'after' ? // Get the last header in the most deeply-nested header group and add one.
            visibleColumnManager.getHeaderIndex(me.getNestedHeader(targetHeader, 1)) + 1 : // Get the first header in the most deeply-nested header group.
            visibleColumnManager.getHeaderIndex(me.getNestedHeader(targetHeader, 0));
            me.invalidateDrop();
            // Cache the width here, we need to get it before we removed it from the DOM
            savedWidth = dragHeader.getWidth();
            // Suspend layouts while we sort all this out.
            Ext.suspendLayouts();
            // When removing and then adding, the owning gridpanel will be informed of column
            // mutation twice
            // Both remove and add handling inform the owning grid.
            // The isDDMoveInGrid flag will prevent the remove operation from doing this.
            // See Ext.grid.header.Container#onRemove.
            // It's enough to inform the root container about the move
            fromCtRoot.isDDMoveInGrid = !data.crossPanel;
            // ***Move the headers***
            //
            // If both drag and target headers are groupHeaders, we have to check and see if
            // they are nested, i.e., there are multiple stacked group headers with only subheaders
            // at the lowest level:
            //
            //           +-----------------------------------+
            //           |               Group 1             |
            //           |-----------------------------------|
            //           |               Group 2             |
            //   other   |-----------------------------------|   other
            //  headers  |               Group 3             |  headers
            //           |-----------------------------------|
            //           | Field3 | Field4 | Field5 | Field6 |
            //           |===================================|
            //           |               view                |
            //           +-----------------------------------+
            //
            // In these cases, we need to mark the groupHeader that is the ownerCt
            // of the targetHeader and then only remove the headers up until that
            // (removal of headers is recursive and assumes that any header with no children
            // can be safely removed, which is not a safe assumption).
            // See Ext.grid.header.Container#onRemove.
            if (dragHeader.isGroupHeader && targetHeader.isGroupHeader) {
                dragHeader.setNestedParent(targetHeader);
            }
            // We only need to be concerned with moving the dragHeader component before or after
            // the targetHeader component rather than trying to pass indices, which is too ambiguous
            // and could refer to any collection at any level of (grouped) header containers.
            if (dropPosition === 'before') {
                toCt.moveBefore(dragHeader, targetHeader);
            } else {
                toCt.moveAfter(dragHeader, targetHeader);
            }
            // ***Move the view data columns***
            // Refresh the view if it's not the last header in a group. If it is the last header,
            // we don't need to refresh the view as the headers and the corrresponding data columns
            // will already be correctly aligned (think of the group header sitting directly atop
            // the last header in the group).
            // Also, it's not necessary to refresh the view if the indices are the same.
            // NOTE that targetHeader can be destroyed by this point if it was a group header
            // and we just dragged the last column out of it; in that case header's items collection
            // will be nulled.
            if (visibleToIdx >= 0 && !(targetHeader.isGroupHeader && (!targetHeader.items || !targetHeader.items.length)) && visibleFromIdx !== visibleToIdx) {
                colsToMove = dragHeader.isGroupHeader ? dragHeader.query('gridcolumn:not([hidden]):not([isGroupHeader])').length : 1;
                // We need to adjust the visibleToIdx when both of the following conditions are met:
                //   1. The drag is forward, i.e., the dragHeader is being dragged to the right.
                //   2. There is more than one column being dragged, i.e., an entire group.
                if ((visibleFromIdx <= visibleToIdx) && colsToMove > 1) {
                    visibleToIdx -= colsToMove;
                }
                // It's necessary to lookup the ancestor grid of the grouped header b/c the header
                // could be nested at any level.
                toCt.getRootHeaderCt().grid.view.moveColumn(visibleFromIdx, visibleToIdx, colsToMove);
            }
            // We need to always fire a columnmove event. Check for an .ownerCt first in case
            // this is a grouped header.
            fromCtRoot.fireEvent('columnmove', fromCt, dragHeader, visibleFromIdx, visibleToIdx);
            fromCtRoot.isDDMoveInGrid = false;
            // Group headers skrinkwrap their child headers.
            // Therefore a child header may not flex; it must contribute a fixed width.
            // But we restore the flex value when moving back into the main header container
            //
            // Note that we don't need to save the flex if coming from another group header
            // b/c it couldn't have had one!
            if (toCt.isGroupHeader && !fromCt.isGroupHeader) {
                // Adjust the width of the "to" group header only if we dragged in
                // from somewhere else. If not within the same container.
                if (fromCt !== toCt) {
                    dragHeader.savedFlex = dragHeader.flex;
                    delete dragHeader.flex;
                    dragHeader.width = savedWidth;
                }
            } else if (!fromCt.isGroupHeader) {
                if (dragHeader.savedFlex) {
                    dragHeader.flex = dragHeader.savedFlex;
                    delete dragHeader.width;
                }
            }
            Ext.resumeLayouts(true);
            // The grid must lay out so that its headerCt lays out.
            // It will not be thrown into the mix by BorderLayout#getLayoutItems
            // if it's floated, so we have to force the issue.
            if (me.headerCt.grid.floated) {
                me.headerCt.grid.updateLayout();
            }
        }
    }
});
// Ext.grid.header.Container will handle the removal of empty groups,
// don't handle it here.

/**
 * @private
 */
Ext.define('Ext.grid.plugin.HeaderReorderer', {
    extend: 'Ext.plugin.Abstract',
    alias: 'plugin.gridheaderreorderer',
    requires: [
        'Ext.grid.header.DragZone',
        'Ext.grid.header.DropZone'
    ],
    init: function(headerCt) {
        this.headerCt = headerCt;
        headerCt.on({
            boxready: this.onHeaderCtRender,
            single: true,
            scope: this
        });
    },
    destroy: function() {
        var me = this;
        // The grid may happen to never render
        me.headerCt.un('boxready', me.onHeaderCtRender, me);
        Ext.destroy(me.dragZone, me.dropZone);
        me.headerCt = me.dragZone = me.dropZone = null;
        me.callParent();
    },
    onHeaderCtRender: function(headerCt) {
        var me = this;
        me.dragZone = new Ext.grid.header.DragZone(me.headerCt);
        me.dropZone = new Ext.grid.header.DropZone(me.headerCt);
        if (me.disabled) {
            me.dragZone.disable();
        }
        headerCt.setTouchAction({
            panX: false
        });
    },
    enable: function() {
        this.disabled = false;
        if (this.dragZone) {
            this.dragZone.enable();
        }
    },
    disable: function() {
        this.disabled = true;
        if (this.dragZone) {
            this.dragZone.disable();
        }
    }
});

/**
 * Headercontainer is a docked container (_`top` or `bottom` only_) that holds the
 * headers ({@link Ext.grid.column.Column grid columns}) of a
 * {@link Ext.grid.Panel grid} or {@link Ext.tree.Panel tree}.  The headercontainer
 * handles resizing, moving, and hiding columns.  As columns are hidden, moved or
 * resized, the headercontainer triggers changes within the grid or tree's
 * {@link Ext.view.Table view}.  You will not generally need to instantiate this class
 * directly.
 *
 * You may use the
 * {@link Ext.panel.Table#method-getHeaderContainer getHeaderContainer()}
 * accessor method to access the tree or grid's headercontainer.
 *
 * Grids and trees also have an alias to the two more useful headercontainer methods:
 *
 *  - **{@link Ext.panel.Table#method-getColumns getColumns}** - aliases
 * {@link Ext.grid.header.Container#getGridColumns}
 *  - **{@link Ext.panel.Table#method-getVisibleColumns getVisibleColumns}** - aliases
 * {@link Ext.grid.header.Container#getVisibleGridColumns}
 */
Ext.define('Ext.grid.header.Container', {
    extend: 'Ext.container.Container',
    alias: 'widget.headercontainer',
    requires: [
        'Ext.grid.ColumnLayout',
        'Ext.grid.plugin.HeaderResizer',
        'Ext.grid.plugin.HeaderReorderer',
        'Ext.util.KeyNav'
    ],
    uses: [
        'Ext.grid.column.Column',
        'Ext.grid.ColumnManager',
        'Ext.menu.Menu',
        'Ext.menu.CheckItem',
        'Ext.menu.Separator'
    ],
    border: true,
    baseCls: Ext.baseCSSPrefix + 'grid-header-ct',
    dock: 'top',
    /**
     * @cfg {Number} weight
     * HeaderContainer overrides the default weight of 0 for all docked items to 100.
     * This is so that it has more priority over things like toolbars.
     */
    weight: 100,
    defaultType: 'gridcolumn',
    /**
     * @cfg {Number} defaultWidth
     * Width of the header if no width or flex is specified.
     */
    defaultWidth: 100,
    /**
     * @cfg {Boolean} [sealed=false]
     * Specify as `true` to constrain column dragging so that a column cannot be dragged into
     * or out of this column.
     *
     * **Note that this config is only valid for column headers which contain child column headers,
     * eg:**
     *     {
     *         sealed: true
     *         text: 'ExtJS',
     *         columns: [{
     *             text: '3.0.4',
     *             dataIndex: 'ext304'
     *         }, {
     *             text: '4.1.0',
     *             dataIndex: 'ext410'
     *         }
     *     }
     *
     */
    /**
     * @cfg {String} sortAscText
     * The text to display in the sort menu to sort items in ascending order.
     * @locale
     */
    sortAscText: 'Sort Ascending',
    /**
     * @cfg {String} sortDescText
     * The text to display in the sort menu to sort items in descending order.
     * @locale
     */
    sortDescText: 'Sort Descending',
    /**
     * @cfg {String} sortClearText
     * The text to display in the sort menu to clear the sort order.
     * @locale
     */
    sortClearText: 'Clear Sort',
    /**
     * @cfg {String} columnsText
     * The text for the columns submenu item.
     * @locale
     */
    columnsText: 'Columns',
    headerOpenCls: Ext.baseCSSPrefix + 'column-header-open',
    menuSortAscCls: Ext.baseCSSPrefix + 'hmenu-sort-asc',
    menuSortDescCls: Ext.baseCSSPrefix + 'hmenu-sort-desc',
    menuColsIcon: Ext.baseCSSPrefix + 'cols-icon',
    blockEvents: false,
    dragging: false,
    // May be set to false by a SptreadSheetSelectionModel
    sortOnClick: true,
    // Disable FocusableContainer behavior by default, since we only want it
    // to be enabled for the root header container (we'll set the flag in initComponent)
    focusableContainer: false,
    childHideCount: 0,
    /**
     * @property {Boolean} isGroupHeader
     * True if this HeaderContainer is in fact a group header which contains sub headers.
     */
    /**
     * @cfg {Boolean} sortable
     * Provides the default sortable state for all Headers within this HeaderContainer.
     * Also turns on or off the menus in the HeaderContainer. Note that the menu is
     * shared across every header and therefore turning it off will remove the menu
     * items for every header.
     */
    sortable: true,
    /**
     * @cfg {Boolean} [enableColumnHide=true]
     * False to disable column hiding within this grid.
     */
    enableColumnHide: true,
    /**
     * @event columnresize
     * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates
     * all column headers.
     * @param {Ext.grid.column.Column} column The Column header Component which provides
     * the column definition
     * @param {Number} width
     */
    /**
     * @event headerclick
     * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates
     * all column headers.
     * @param {Ext.grid.column.Column} column The Column header Component which provides
     * the column definition
     * @param {Ext.event.Event} e
     * @param {HTMLElement} t
     */
    /**
     * @event headercontextmenu
     * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates
     * all column headers.
     * @param {Ext.grid.column.Column} column The Column header Component which provides
     * the column definition
     * @param {Ext.event.Event} e
     * @param {HTMLElement} t
     */
    /**
     * @event headertriggerclick
     * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates
     * all column headers.
     * @param {Ext.grid.column.Column} column The Column header Component which provides
     * the column definition
     * @param {Ext.event.Event} e
     * @param {HTMLElement} t
     */
    /**
     * @event columnmove
     * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates
     * all column headers.
     * @param {Ext.grid.column.Column} column The Column header Component which provides
     * the column definition
     * @param {Number} fromIdx
     * @param {Number} toIdx
     */
    /**
     * @event columnhide
     * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates
     * all column headers.
     * @param {Ext.grid.column.Column} column The Column header Component which provides
     * the column definition
     */
    /**
     * @event columnshow
     * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates
     * all column headers.
     * @param {Ext.grid.column.Column} column The Column header Component which provides
     * the column definition
     */
    /**
     * @event columnschanged
     * Fired after the columns change in any way, when a column has been hidden or shown,
     * or when a column is added to or removed from this header container.
     * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates
     * all column headers.
     */
    /**
     * @event sortchange
     * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates
     * all column headers.
     * @param {Ext.grid.column.Column} column The Column header Component which provides
     * the column definition
     * @param {String} direction
     */
    /**
     * @event menucreate
     * Fired immediately after the column header menu is created.
     * @param {Ext.grid.header.Container} ct This instance
     * @param {Ext.menu.Menu} menu The Menu that was created
     */
    /**
     * @event headermenucreate
     * Fired immediately after the column header menu is created.
     * @param {Ext.panel.Table} grid This grid instance
     * @param {Ext.menu.Menu} menu The Menu that was created
     * @param {Ext.grid.header.Container} headerCt This header container
     * @member Ext.panel.Table
     */
    initComponent: function() {
        var me = this;
        me.plugins = me.plugins || [];
        me.defaults = me.defaults || {};
        // TODO: Pass in configurations to turn on/off dynamic
        //       resizing and disable resizing all together
        // Only set up a Resizer and Reorderer for the root HeaderContainer.
        // Nested Group Headers are themselves HeaderContainers
        if (!me.isColumn) {
            me.isRootHeader = true;
            if (me.enableColumnResize) {
                me.resizer = new Ext.grid.plugin.HeaderResizer();
                me.plugins.push(me.resizer);
            }
            if (me.enableColumnMove) {
                me.reorderer = new Ext.grid.plugin.HeaderReorderer();
                me.plugins.push(me.reorderer);
            }
        }
        // If this is a leaf column header, and is NOT functioning as a container,
        // use Container layout with a no-op calculate method.
        if (me.isColumn && !me.isGroupHeader) {
            if (!me.items || me.items.length === 0) {
                me.isContainer = me.isFocusableContainer = false;
                // Allow overriding via instance config
                if (!me.hasOwnProperty('focusable')) {
                    me.focusable = true;
                }
                me.layout = {
                    type: 'container',
                    calculate: Ext.emptyFn
                };
            }
        } else // HeaderContainer and Group header needs a gridcolumn layout.
        {
            me.layout = Ext.apply({
                type: 'gridcolumn',
                align: 'stretch'
            }, me.initialConfig.layout);
            // All HeaderContainers need to know this so that leaf Columns can adjust
            // for cell border width if using content box model
            me.defaults.columnLines = me.columnLines;
            // Initialize the root header.
            if (me.isRootHeader) {
                // The root header is a FocusableContainer if it's not carrying hidden headers.
                if (!me.hiddenHeaders) {
                    me.focusableContainer = true;
                    me.ariaRole = 'rowgroup';
                }
                // Create column managers for the root header.
                me.columnManager = new Ext.grid.ColumnManager(false, me);
                me.visibleColumnManager = new Ext.grid.ColumnManager(true, me);
                // In the grid config, if grid.columns is a header container instance
                // and not a columns config, then it currently has no knowledge
                // of a containing grid. Create the column manager now and bind it to the grid
                // later in Ext.panel.Table:initComponent().
                //
                // In most cases, though, grid.columns will be a config, so the grid
                // is already known and the column manager can be bound to it.
                if (me.grid) {
                    me.grid.columnManager = me.columnManager;
                    me.grid.visibleColumnManager = me.visibleColumnManager;
                }
            } else {
                // Is a group header, also create column managers.
                me.visibleColumnManager = new Ext.grid.ColumnManager(true, me);
                me.columnManager = new Ext.grid.ColumnManager(false, me);
            }
        }
        me.menuTask = new Ext.util.DelayedTask(me.updateMenuDisabledState, me);
        me.callParent();
    },
    isNested: function() {
        return !!this.getRootHeaderCt().down('[isNestedParent]');
    },
    isNestedGroupHeader: function() {
        // The owner only has one item that isn't hidden and it's me; hide the owner.
        var header = this,
            items = header.getRefOwner().query('>:not([hidden])');
        return (items.length === 1 && items[0] === header);
    },
    /**
     * Returns the column's sealed status.
     *
     * @return {Boolean} `true` if this column is sealed, `false` otherwise.
     *
     * @since 6.5.0
     */
    isSealed: function() {
        return !!(this.sealed || this.getInherited().sealed);
    },
    maybeShowNestedGroupHeader: function() {
        // Group headers are special in that they are auto-hidden when their subheaders are all
        // hidden and auto-shown when the first subheader is reshown. They are the only headers
        // that should now be auto-shown or -hidden.
        //
        // It follows that since group headers are dictated by some automation depending upon the
        // state of their child items that all group headers should be shown if anyone in the
        // hierarchy is shown since these special group headers only contain one child, which is
        // the next group header in the stack.
        // This only should apply to the following grouped header scenario:
        //
        //           +-----------------------------------+
        //           |               Group 1             |
        //           |-----------------------------------|
        //           |               Group 2             |
        //   other   |-----------------------------------|   other
        //  headers  |               Group 3             |  headers
        //           |-----------------------------------|
        //           | Field3 | Field4 | Field5 | Field6 |
        //           |===================================|
        //           |               view                |
        //           +-----------------------------------+
        //
        var items = this.items,
            item;
        if (items && items.length === 1 && (item = items.getAt(0)) && item.hidden) {
            item.show();
        }
    },
    setNestedParent: function(target) {
        // Here we need to prevent the removal of ancestor group headers from occuring
        // if a flag is set. This is needed when there are stacked group headers
        // and only the deepest nested group header has leaf items in its collection.
        // In this specific scenario, the group headers above it only have 1 item, which is its
        // child nested group header.
        //
        // If we don't set this flag, then all of the grouped headers will be recursively removed
        // all the way up to the root container b/c Ext.grid.header.Container#onRemove will remove
        // all containers that don't contain any items.
        //
        // Note that if an ownerCt only has one item, then we know that this item
        // is the group header that we're currently dragging.
        //
        // Also, note that we mark the owner as the target header because everything up to that
        // should be removed.
        //
        // We have to reset any previous headers that may have been target.ownerCts!
        target.isNestedParent = false;
        target.ownerCt.isNestedParent = !!(this.ownerCt.items.length === 1 && target.ownerCt.items.length === 1);
    },
    initEvents: function() {
        var me = this,
            onHeaderCtEvent, listeners;
        me.callParent();
        // If this is top level, listen for events to delegate to descendant headers.
        if (!me.isColumn && !me.isGroupHeader) {
            onHeaderCtEvent = me.onHeaderCtEvent;
            listeners = {
                click: onHeaderCtEvent,
                dblclick: onHeaderCtEvent,
                contextmenu: onHeaderCtEvent,
                mousedown: me.onHeaderCtMouseDown,
                mouseover: me.onHeaderCtMouseOver,
                mouseout: me.onHeaderCtMouseOut,
                scope: me
            };
            if (Ext.supports.Touch) {
                listeners.longpress = me.onHeaderCtLongPress;
            }
            me.mon(me.el, listeners);
        }
    },
    onHeaderCtEvent: function(e, t) {
        var me = this,
            headerEl = me.getHeaderElByEvent(e),
            header, targetEl, activeHeader;
        if (me.longPressFired) {
            // if we just showed the menu as a result of a longpress, do not process
            // the click event and sort the column.
            me.longPressFired = false;
            return;
        }
        if (headerEl && !me.blockEvents) {
            header = Ext.getCmp(headerEl.id);
            if (header) {
                targetEl = header[header.clickTargetName];
                // If there's no possibility that the mouseEvent was on child header items,
                // or it was definitely in our titleEl, then process it
                if ((!header.isGroupHeader && !header.isContainer) || e.within(targetEl)) {
                    if (e.type === 'click' || e.type === 'tap') {
                        // The header decides which header to activate on click
                        // on Touch, anywhere in the splitter zone activates
                        // the left header.
                        activeHeader = header.onTitleElClick(e, targetEl, me.sortOnClick);
                        if (activeHeader) {
                            // If activated by touch, there is no trigger el to align with,
                            // so align to the header element.
                            me.onHeaderTriggerClick(activeHeader, e, e.pointerType === 'touch' ? activeHeader.el : activeHeader.triggerEl);
                        } else {
                            me.onHeaderClick(header, e, t);
                        }
                    } else if (e.type === 'contextmenu') {
                        me.onHeaderContextMenu(header, e, t);
                    } else if (e.type === 'dblclick') {
                        header.onTitleElDblClick(e, targetEl.dom);
                    }
                }
            }
        }
    },
    blockNextEvent: function() {
        var me = this;
        me.blockEvents = true;
        if (!me.unblockTimer) {
            me.unblockTimer = Ext.asap(me.unblockEvents, me);
        }
    },
    unblockEvents: function() {
        this.blockEvents = this.unblockTimer = false;
    },
    onHeaderCtMouseDown: function(e, target) {
        var targetCmp = Ext.Component.from(target),
            cols, i, len, scrollable, col;
        if (!e.defaultPrevented && targetCmp !== this) {
            // The DDManager (Header Containers are draggable) prevents mousedown default
            // So we must explicitly focus the header
            if (targetCmp.isGroupHeader) {
                cols = targetCmp.getVisibleGridColumns();
                scrollable = this.getScrollable();
                for (i = 0 , len = cols.length; i < len; ++i) {
                    col = cols[i];
                    if (scrollable.doIsInView(col.el, true).x) {
                        targetCmp = col;
                        break;
                    }
                }
            }
            targetCmp.focus();
        }
    },
    onHeaderCtMouseOver: function(e, t) {
        var headerEl, header, targetEl;
        // Only proces the mouse entering this HeaderContainer.
        // From header to header, and exiting this HeaderContainer we track using mouseout events.
        if (!e.within(this.el, true)) {
            headerEl = e.getTarget('.' + Ext.grid.column.Column.prototype.baseCls);
            header = headerEl && Ext.getCmp(headerEl.id);
            if (header) {
                targetEl = header[header.clickTargetName];
                if (e.within(targetEl)) {
                    header.onTitleMouseOver(e, targetEl.dom);
                }
            }
        }
    },
    onHeaderCtMouseOut: function(e, t) {
        var headerSelector = '.' + Ext.grid.column.Column.prototype.baseCls,
            outHeaderEl = e.getTarget(headerSelector),
            inHeaderEl = e.getRelatedTarget(headerSelector),
            header, targetEl;
        // It's a mouseenter/leave, not an internal element change within a Header
        if (outHeaderEl !== inHeaderEl) {
            if (outHeaderEl) {
                header = Ext.getCmp(outHeaderEl.id);
                if (header) {
                    targetEl = header[header.clickTargetName];
                    header.onTitleMouseOut(e, targetEl.dom);
                }
            }
            if (inHeaderEl) {
                header = Ext.getCmp(inHeaderEl.id);
                if (header) {
                    targetEl = header[header.clickTargetName];
                    header.onTitleMouseOver(e, targetEl.dom);
                }
            }
        }
    },
    onHeaderCtLongPress: function(e) {
        var me = this,
            headerEl = me.getHeaderElByEvent(e),
            header;
        // Might be outside the headers.
        if (headerEl) {
            header = Ext.getCmp(headerEl.id);
            if (header && !header.menuDisabled) {
                me.longPressFired = true;
                me.showMenuBy(e, headerEl, header);
            }
        }
    },
    getHeaderElByEvent: function(e) {
        return e.getTarget('.' + Ext.grid.column.Column.prototype.baseCls);
    },
    isLayoutRoot: function() {
        // Since we're docked, the width is always calculated
        // If we're hidden, the height is explicitly 0, which
        // means we'll be considered a layout root. However, we
        // still need the view to layout to update the underlying
        // table to match the size.
        if (this.hiddenHeaders) {
            return false;
        }
        return this.callParent();
    },
    // Find the topmost HeaderContainer
    getRootHeaderCt: function() {
        return this.isRootHeader ? this : this.up('[isRootHeader]');
    },
    doDestroy: function() {
        var me = this;
        if (me.menu) {
            me.menu.un('hide', me.onMenuHide, me);
        }
        Ext.unasap(me.unblockTimer);
        me.menuTask.cancel();
        Ext.destroy(me.visibleColumnManager, me.columnManager, me.menu);
        me.callParent();
    },
    removeAll: function(autoDestroy) {
        var me = this;
        // fire a single columnschanged event after all removes have been made
        me.suspendEvent('columnschanged');
        me.callParent([
            autoDestroy
        ]);
        me.resumeEvent('columnschanged');
        me.fireEvent('columnschanged', me);
    },
    applyColumnsState: function(columnsState, storeState) {
        if (!columnsState) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            items = me.items.items,
            count = items.length,
            i = 0,
            moved = false,
            newOrder = [],
            newCols = [],
            length, col, columnState, index;
        me.purgeCache();
        for (i = 0; i < count; i++) {
            col = items[i];
            columnState = columnsState[col.getStateId()];
            // There's a column state for this column.
            // Add it to the newOrder array at the specified index
            if (columnState) {
                index = columnState.index;
                newOrder[index] = col;
                if (i !== index) {
                    moved = true;
                }
                if (col.applyColumnState) {
                    col.applyColumnState(columnState, storeState);
                }
            } else // A new column.
            // It must be inserted at this index after state restoration,
            {
                newCols.push({
                    index: i,
                    column: col
                });
            }
        }
        // If any saved columns were missing, close the gaps where they were
        newOrder = Ext.Array.clean(newOrder);
        // New column encountered.
        // Insert them into the newOrder at their configured position
        length = newCols.length;
        if (length) {
            for (i = 0; i < length; i++) {
                columnState = newCols[i];
                index = columnState.index;
                if (index < newOrder.length) {
                    moved = true;
                    Ext.Array.splice(newOrder, index, 0, columnState.column);
                } else {
                    newOrder.push(columnState.column);
                }
            }
        }
        if (moved) {
            // This flag will prevent the groupheader from being removed by its owner
            // when it (temporarily) has no child items.
            me.applyingState = true;
            me.removeAll(false);
            delete me.applyingState;
            me.add(newOrder);
        }
    },
    getColumnsState: function() {
        var me = this,
            columns = [],
            state;
        me.items.each(function(col) {
            state = col.getColumnState && col.getColumnState();
            if (state) {
                columns.push(state);
            }
        });
        return columns;
    },
    // Invalidate column cache on add
    // We cannot refresh the View on every add because this method is called
    // when the HeaderDropZone moves Headers around, that will also refresh the view
    onAdd: function(c) {
        var me = this,
            rootHeader,
            // eslint-disable-line semi
            stateId = c.getStateId();
        // eslint-disable-line comma-style
        if (stateId != null) {
            if (!me._usedIDs) {
                me._usedIDs = {};
            }
            if (me._usedIDs[stateId] && me._usedIDs[stateId] !== c) {
                Ext.log.warn(this.$className + ' attempted to reuse an existing id: ' + stateId);
            }
            me._usedIDs[stateId] = c;
        }
        me.callParent(arguments);
        rootHeader = me.getRootHeaderCt();
        me.onHeadersChanged(c, rootHeader && rootHeader.isDDMoveInGrid);
    },
    move: function(fromIdx, toIdx) {
        var me = this,
            items = me.items,
            headerToMove;
        if (fromIdx.isComponent) {
            headerToMove = fromIdx;
            fromIdx = items.indexOf(headerToMove);
        } else {
            headerToMove = items.getAt(fromIdx);
        }
        // Take real grid column index of column being moved
        headerToMove.visibleFromIdx = me.getRootHeaderCt().visibleColumnManager.indexOf(headerToMove);
        me.callParent(arguments);
    },
    onMove: function(headerToMove, fromIdx, toIdx) {
        var me = this,
            gridHeaderCt = me.getRootHeaderCt(),
            gridVisibleColumnManager = gridHeaderCt.visibleColumnManager,
            numColsToMove = 1,
            visibleToIdx;
        // Purges cache so that indexOf returns new position of header
        me.onHeadersChanged(headerToMove, true);
        visibleToIdx = gridVisibleColumnManager.indexOf(headerToMove);
        if (visibleToIdx >= headerToMove.visibleFromIdx) {
            visibleToIdx++;
        }
        me.callParent(arguments);
        // If what is being moved is a group header, then pass the correct column count
        if (headerToMove.isGroupHeader) {
            numColsToMove = headerToMove.visibleColumnManager.getColumns().length;
        }
        gridHeaderCt.onHeaderMoved(headerToMove, numColsToMove, headerToMove.visibleFromIdx, visibleToIdx);
    },
    // @private
    maybeContinueRemove: function() {
        var me = this;
        // Note that if the column is a group header and is the current target of a drag,
        // we don't want to remove it if it since it could be one of any number of (empty)
        // nested group headers. See #isNested.
        //
        // There are also other scenarios in which the remove should not occur. For instance,
        // when applying column state to a groupheader, the subheaders are all removed
        // before being re-added in their stateful order, and the groupheader should not be removed
        // in the meantime.
        // See EXTJS-17577.
        return (me.isGroupHeader && !me.applyingState) && !me.isNestedParent && me.ownerCt && !me.items.getCount();
    },
    // Invalidate column cache on remove
    // We cannot refresh the View on every remove because this method is called
    // when the HeaderDropZone moves Headers around, that will also refresh the view
    onRemove: function(c, isDestroying) {
        var me = this,
            ownerCt = me.ownerCt;
        me.callParent([
            c,
            isDestroying
        ]);
        if (!me._usedIDs) {
            me._usedIDs = {};
        }
        delete me._usedIDs[c.headerId];
        if (!me.destroying) {
            // isDDMoveInGrid flag set by Ext.grid.header.DropZone when moving into another
            // container *within the same grid*. This stops header change processing
            // from being executed twice, once on remove and then on the subsequent add.
            if (!me.getRootHeaderCt().isDDMoveInGrid) {
                me.onHeadersChanged(c, false);
            }
            if (me.maybeContinueRemove()) {
                // Detach the header from the DOM here. Since we're removing and destroying
                // the container, the inner DOM may get overwritten,
                // since Container#detachOnRemove gets processed after onRemove.
                if (c.rendered) {
                    c.detachFromBody();
                }
                // If we don't have any items left and we're a group, remove ourselves.
                // This will cascade up if necessary. DO NOT destroy ourselves here,
                // we have to defer that until all moves are done and events are fired.
                me.destroyAfterRemoving = true;
                Ext.suspendLayouts();
                ownerCt.remove(me, false);
                Ext.resumeLayouts(true);
            }
        }
    },
    // Private
    // Called to clear all caches of columns whenever columns are added, removed to just moved.
    // We need to be informed if it's just a move operation so that we don't call the heavier
    // grid.onHeadersChanged which refreshes the view.
    // The onMove handler ensures that grid.inHeaderMove is called which just swaps cells.
    onHeadersChanged: function(c, isMove) {
        var gridPanel,
            gridHeaderCt = this.getRootHeaderCt();
        // Each HeaderContainer up the chain must have its cache purged so that its getGridColumns
        // method will return correct results.
        this.purgeHeaderCtCache(this);
        if (gridHeaderCt) {
            gridHeaderCt.onColumnsChanged();
            gridPanel = gridHeaderCt.ownerCt;
            // The grid needs to be informed even if the added/removed column is a group header
            // If it an add or remove operation causing this header change call, then inform
            // the grid which refreshes.
            // Moving calls the onHeaderMoved method of the grid which just swaps cells.
            if (gridPanel && !isMove) {
                gridPanel.onHeadersChanged(gridHeaderCt, c);
            }
        }
    },
    // Private
    onHeaderMoved: function(header, colsToMove, fromIdx, toIdx) {
        var me = this,
            gridSection = me.ownerCt;
        if (me.rendered) {
            if (gridSection && gridSection.onHeaderMove) {
                gridSection.onHeaderMove(me, header, colsToMove, fromIdx, toIdx);
            }
            me.fireEvent('columnmove', me, header, fromIdx, toIdx);
        }
    },
    // Private
    // Only called on the grid's headerCt.
    // Called whenever a column is added or removed or moved at any level below.
    // Ensures that the gridColumns caches are cleared.
    onColumnsChanged: function() {
        var me = this,
            menu = me.menu,
            columnItemSeparator, columnItem;
        if (me.rendered) {
            me.fireEvent('columnschanged', me);
            // Column item (and its associated menu) menu has to be destroyed (if it exits)
            // when columns are changed.
            // It will be recreated just before the main container menu is next shown.
            if (menu) {
                columnItemSeparator = menu.child('#columnItemSeparator');
                columnItem = menu.child('#columnItem');
                // Destroy the column visibility items
                // They will be recreated before the next show
                if (columnItemSeparator) {
                    columnItemSeparator.destroy();
                }
                if (columnItem) {
                    columnItem.destroy();
                }
            }
        }
    },
    /**
     * @private
     */
    lookupComponent: function(comp) {
        var result = this.callParent(arguments);
        // Apply default width unless it's a group header (in which case it must be left
        // to shrinkwrap), or it's flexed. Test whether width is undefined so that width: null
        // can be used to have the header shrinkwrap its text.
        if (!result.isGroupHeader && result.width === undefined && !result.flex) {
            result.width = this.defaultWidth;
        }
        return result;
    },
    /**
     * @private
     * Synchronize column UI visible sort state with Store's sorters.
     */
    setSortState: function() {
        var store = this.up('[store]').store,
            columns = this.visibleColumnManager.getColumns(),
            len = columns.length,
            i, header, sorter;
        for (i = 0; i < len; i++) {
            header = columns[i];
            // Access the column's custom sorter in preference to one keyed on the data index.
            sorter = header.getSorter();
            if (sorter) {
                // If the column was configured with a sorter, we must check that the sorter
                // is part of the store's sorter collection to update the UI to the correct state.
                // The store may not actually BE sorted by that sorter.
                if (!store.getSorters().contains(sorter)) {
                    sorter = null;
                }
            } else {
                sorter = store.getSorters().get(header.getSortParam());
            }
            // Important: A null sorter for this column will *clear* the UI sort indicator.
            header.setSortState(sorter);
        }
    },
    getHeaderMenu: function() {
        var menu = this.getMenu(),
            item;
        if (menu) {
            item = menu.child('#columnItem');
            if (item) {
                return item.menu;
            }
        }
        return null;
    },
    onHeaderVisibilityChange: function(header, visible) {
        var me = this,
            menu = me.getHeaderMenu(),
            item;
        // Invalidate column collections upon column hide/show
        me.purgeHeaderCtCache(header.ownerCt);
        if (menu) {
            // If the header was hidden programmatically, sync the Menu state
            item = me.getMenuItemForHeader(menu, header);
            if (item) {
                item.setChecked(visible, true);
            }
            // delay this since the headers may fire a number of times
            // if we're hiding/showing groups
            if (menu.isVisible()) {
                me.menuTask.delay(50);
            }
        }
    },
    updateMenuDisabledState: function(menu) {
        var me = this,
            columns = me.query('gridcolumn:not([hidden])'),
            i,
            len = columns.length,
            item, checkItem, method;
        // If called from menu creation, it will be passed to avoid infinite recursion
        if (!menu) {
            menu = me.getMenu();
        }
        for (i = 0; i < len; ++i) {
            item = columns[i];
            checkItem = me.getMenuItemForHeader(menu, item);
            if (checkItem) {
                method = item.isHideable() ? 'enable' : 'disable';
                if (checkItem.menu) {
                    method += 'CheckChange';
                }
                checkItem[method]();
            }
        }
    },
    getMenuItemForHeader: function(menu, header) {
        return header ? menu.down('menucheckitem[headerId=' + header.id + ']') : null;
    },
    onHeaderShow: function(header) {
        var me = this,
            ownerCt = me.ownerCt,
            lastHiddenHeader = header.lastHiddenHeader;
        if (!ownerCt) {
            return;
        }
        if (me.forceFit) {
            delete me.flex;
        }
        // If lastHiddenHeader exists we know that header is a groupHeader
        // and if all its subheaders are hidden then we need to show the last one that was hidden.
        if (lastHiddenHeader && !header.query('[hidden=false]').length) {
            lastHiddenHeader.show();
            header.lastHiddenHeader = null;
        }
        me.onHeaderVisibilityChange(header, true);
        ownerCt.onHeaderShow(me, header);
        me.fireEvent('columnshow', me, header);
        me.fireEvent('columnschanged', this);
    },
    onHeaderHide: function(header) {
        var me = this,
            ownerCt = me.ownerCt;
        if (!ownerCt) {
            return;
        }
        me.onHeaderVisibilityChange(header, false);
        ownerCt.onHeaderHide(me, header);
        me.fireEvent('columnhide', me, header);
        me.fireEvent('columnschanged', this);
    },
    onHeaderResize: function(header, w) {
        var me = this,
            gridSection = me.ownerCt;
        if (gridSection) {
            gridSection.onHeaderResize(me, header, w);
        }
        me.fireEvent('columnresize', me, header, w);
    },
    onHeaderClick: function(header, e, t) {
        var me = this,
            selModel = header.getView().getSelectionModel(),
            ret;
        header.fireEvent('headerclick', me, header, e, t);
        ret = me.fireEvent('headerclick', me, header, e, t);
        if (ret !== false) {
            if (selModel.onHeaderClick) {
                selModel.onHeaderClick(me, header, e);
            }
        }
        return ret;
    },
    onHeaderContextMenu: function(header, e, t) {
        header.fireEvent('headercontextmenu', this, header, e, t);
        this.fireEvent('headercontextmenu', this, header, e, t);
    },
    onHeaderTriggerClick: function(header, e, t) {
        var me = this;
        if (header.fireEvent('headertriggerclick', me, header, e, t) !== false && me.fireEvent('headertriggerclick', me, header, e, t) !== false) {
            // If menu is already active...
            if (header.activeMenu) {
                // Click/tap toggles the menu visibility.
                if (e.pointerType) {
                    header.activeMenu.hide();
                } else {
                    header.activeMenu.focus();
                }
            } else {
                me.showMenuBy(e, t, header);
            }
        }
    },
    /**
     * @private
     *
     * Shows the column menu under the target element passed. This method is used when
     * the trigger element on the column
     * header is clicked on and rarely should be used otherwise.
     *
     * @param {Ext.event.Event} [clickEvent] The event which triggered the current handler.
     * If omitted or a key event, the menu autofocuses its first item.
     * @param {HTMLElement/Ext.dom.Element} t The target to show the menu by
     * @param {Ext.grid.header.Container} header The header container that the trigger
     * was clicked on.
     */
    showMenuBy: function(clickEvent, t, header) {
        var menu = this.getMenu(),
            ascItem = menu.down('#ascItem'),
            descItem = menu.down('#descItem'),
            sortableMth;
        // Use ownerCmp as the upward link. Menus *must have no ownerCt* - they are global floaters.
        // Upward navigation is done using the up() method.
        menu.activeHeader = menu.ownerCmp = header;
        header.setMenuActive(menu);
        // enable or disable asc & desc menu items based on header being sortable
        sortableMth = header.sortable ? 'enable' : 'disable';
        if (ascItem) {
            ascItem[sortableMth]();
        }
        if (descItem) {
            descItem[sortableMth]();
        }
        // Pointer-invoked menus do not auto focus, key invoked ones do.
        menu.autoFocus = !clickEvent || clickEvent.keyCode;
        // For longpress t is the header, for click/hover t is the trigger
        menu.showBy(t, 'tl-bl?');
        // Menu show was vetoed by event handler - clear context
        if (!menu.isVisible()) {
            this.onMenuHide(menu);
        }
    },
    hideMenu: function() {
        if (this.menu) {
            this.menu.hide();
        }
    },
    // remove the trigger open class when the menu is hidden
    onMenuHide: function(menu) {
        menu.activeHeader.setMenuActive(false);
    },
    purgeHeaderCtCache: function(headerCt) {
        while (headerCt) {
            headerCt.purgeCache();
            if (headerCt.isRootHeader) {
                return;
            }
            headerCt = headerCt.ownerCt;
        }
    },
    purgeCache: function() {
        var me = this,
            visibleColumnManager = me.visibleColumnManager,
            columnManager = me.columnManager;
        // Delete column cache - column order has changed.
        me.gridVisibleColumns = me.gridDataColumns = me.hideableColumns = null;
        // ColumnManager. Only the top
        if (visibleColumnManager) {
            visibleColumnManager.invalidate();
            columnManager.invalidate();
        }
    },
    /**
     * Gets the menu (and will create it if it doesn't already exist)
     * @private
     */
    getMenu: function() {
        var me = this,
            grid = me.view && me.view.ownerGrid;
        if (!me.menu) {
            me.menu = new Ext.menu.Menu({
                hideOnParentHide: false,
                // Persists when owning ColumnHeader is hidden
                items: me.getMenuItems(),
                listeners: {
                    beforeshow: me.beforeMenuShow,
                    hide: me.onMenuHide,
                    scope: me
                }
            });
            me.fireEvent('menucreate', me, me.menu);
            if (grid) {
                grid.fireEvent('headermenucreate', grid, me.menu, me);
            }
        }
        return me.menu;
    },
    // Render our menus to the first enclosing scrolling element so that they scroll with the grid
    beforeMenuShow: function(menu) {
        var me = this,
            columnItem = menu.child('#columnItem'),
            hideableColumns, insertPoint;
        // If a change of column structure caused destruction of the column menu item
        // or the main menu was created without the column menu item because it began
        // with no hideable headers. Then create it and its menu now.
        if (!columnItem) {
            hideableColumns = me.enableColumnHide ? me.getColumnMenu(me) : null;
            // Insert after the "Sort Ascending", "Sort Descending" menu items if they are present.
            insertPoint = me.sortable ? 2 : 0;
            if (hideableColumns && hideableColumns.length) {
                menu.insert(insertPoint, [
                    {
                        itemId: 'columnItemSeparator',
                        xtype: 'menuseparator'
                    },
                    {
                        itemId: 'columnItem',
                        text: me.columnsText,
                        iconCls: me.menuColsIcon,
                        menu: {
                            items: hideableColumns
                        },
                        hideOnClick: false
                    }
                ]);
            }
        }
        me.updateMenuDisabledState(me.menu);
    },
    // TODO: rendering the menu to the nearest overlfowing ancestor has been disabled
    // since DomQuery is no longer available by default in 5.0
    //        if (!menu.rendered) {
    //            menu.render(this.el.up('{overflow=auto}') || document.body);
    //        }
    /**
     * Returns an array of menu items to be placed into the shared menu
     * across all headers in this header container.
     * @return {Array} menuItems
     */
    getMenuItems: function() {
        var me = this,
            menuItems = [],
            hideableColumns = me.enableColumnHide ? me.getColumnMenu(me) : null;
        if (me.sortable) {
            menuItems = [
                {
                    itemId: 'ascItem',
                    text: me.sortAscText,
                    iconCls: me.menuSortAscCls,
                    handler: me.onSortAscClick,
                    scope: me
                },
                {
                    itemId: 'descItem',
                    text: me.sortDescText,
                    iconCls: me.menuSortDescCls,
                    handler: me.onSortDescClick,
                    scope: me
                }
            ];
        }
        if (hideableColumns && hideableColumns.length) {
            if (me.sortable) {
                menuItems.push({
                    itemId: 'columnItemSeparator',
                    xtype: 'menuseparator'
                });
            }
            menuItems.push({
                itemId: 'columnItem',
                text: me.columnsText,
                iconCls: me.menuColsIcon,
                menu: hideableColumns,
                hideOnClick: false
            });
        }
        return menuItems;
    },
    // sort asc when clicking on item in menu
    onSortAscClick: function() {
        var menu = this.getMenu(),
            activeHeader = menu.activeHeader;
        activeHeader.sort('ASC');
    },
    // sort desc when clicking on item in menu
    onSortDescClick: function() {
        var menu = this.getMenu(),
            activeHeader = menu.activeHeader;
        activeHeader.sort('DESC');
    },
    /**
     * Returns an array of menu CheckItems corresponding to all immediate children
     * of the passed Container which have been configured as hideable.
     */
    getColumnMenu: function(headerContainer) {
        var menuItems = [],
            i = 0,
            item,
            items = headerContainer.query('>gridcolumn[hideable]'),
            itemsLn = items.length,
            menuItem;
        for (; i < itemsLn; i++) {
            item = items[i];
            menuItem = new Ext.menu.CheckItem({
                text: item.menuText || item.text,
                checked: !item.hidden,
                hideOnClick: false,
                headerId: item.id,
                menu: item.isGroupHeader ? this.getColumnMenu(item) : undefined,
                checkHandler: this.onColumnCheckChange,
                scope: this
            });
            menuItems.push(menuItem);
        }
        // Prevent creating a submenu if we have no items
        return menuItems.length ? menuItems : null;
    },
    onColumnCheckChange: function(checkItem, checked) {
        var header = Ext.getCmp(checkItem.headerId);
        if (header.rendered) {
            header[checked ? 'show' : 'hide']();
        } else {
            header.hidden = !checked;
        }
    },
    /**
     * Returns the number of <b>grid columns</b> descended from this HeaderContainer.
     * Group Columns are HeaderContainers. All grid columns are returned, including hidden ones.
     */
    getColumnCount: function() {
        return this.getGridColumns().length;
    },
    /**
     * Gets the full width of all columns that are visible for setting width of tables.
     */
    getTableWidth: function() {
        var fullWidth = 0,
            headers = this.getVisibleGridColumns(),
            headersLn = headers.length,
            i;
        for (i = 0; i < headersLn; i++) {
            fullWidth += headers[i].getCellWidth() || 0;
        }
        return fullWidth;
    },
    /**
     * Returns an array of the **visible** columns in the grid. This goes down to the
     * lowest column header level, and does not return **grouped** headers which contain
     * sub headers.
     *
     * See also {@link Ext.grid.header.Container#getGridColumns}
     * @return {Ext.grid.column.Column[]} columns An array of visible columns.  Returns
     * an empty array if no visible columns are found.
     */
    getVisibleGridColumns: function() {
        var me = this,
            allColumns, rootHeader, result, len, i, column;
        if (me.gridVisibleColumns) {
            return me.gridVisibleColumns;
        }
        allColumns = me.getGridColumns();
        rootHeader = me.getRootHeaderCt();
        result = [];
        len = allColumns.length;
        // Use an inline check instead of ComponentQuery filtering for better performance for
        // repeated grid row rendering - as in buffered rendering.
        for (i = 0; i < len; i++) {
            column = allColumns[i];
            if (!column.hidden && !column.isColumnHidden(rootHeader)) {
                result[result.length] = column;
            }
        }
        me.gridVisibleColumns = result;
        return result;
    },
    isColumnHidden: function(rootHeader) {
        var owner = this.getRefOwner();
        while (owner && owner !== rootHeader) {
            if (owner.hidden) {
                return true;
            }
            owner = owner.getRefOwner();
        }
        return false;
    },
    /**
     * @method getGridColumns
     * Returns an array of all columns which exist in the grid's View, visible or not.
     * This goes down to the leaf column header level, and does not return **grouped**
     * headers which contain sub headers.
     *
     * It includes hidden headers even though they are not rendered. This is for
     * collection of menu items for the column hide/show menu.
     *
     * Headers which have a hidden ancestor have a `hiddenAncestor: true` property
     * injected so that descendants are known to be hidden without interrogating that
     * header's ownerCt axis for a hidden ancestor.
     *
     * See also {@link Ext.grid.header.Container#getVisibleGridColumns}
     * @return {Ext.grid.column.Column[]} columns An array of columns.  Returns an
     * empty array if no columns are found.
     */
    getGridColumns: function(/* private - used in recursion */
    inResult, hiddenAncestor) {
        if (!inResult && this.gridDataColumns) {
            return this.gridDataColumns;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            result = inResult || [],
            items, i, len, item, lastVisibleColumn;
        hiddenAncestor = hiddenAncestor || me.hidden;
        if (me.items) {
            items = me.items.items;
            // An ActionColumn (Columns extend HeaderContainer) may have an items *array*
            // being the action items that it renders.
            if (items) {
                for (i = 0 , len = items.length; i < len; i++) {
                    item = items[i];
                    if (item.isGroupHeader) {
                        // Group headers will need a visibleIndex for if/when they're removed
                        // from their owner.
                        // See Ext.layout.container.Container#moveItemBefore.
                        item.visibleIndex = result.length;
                        item.getGridColumns(result, hiddenAncestor);
                    } else {
                        item.hiddenAncestor = hiddenAncestor;
                        result.push(item);
                    }
                }
            }
        }
        if (!inResult) {
            me.gridDataColumns = result;
        }
        // If top level, correct first and last visible column flags
        if (!inResult && len) {
            // Set firstVisible and lastVisible flags
            for (i = 0 , len = result.length; i < len; i++) {
                item = result[i];
                // The column index within all (visible AND hidden) leaf level columns.
                // Used as the cellIndex in TableView's cell renderer call
                item.fullColumnIndex = i;
                item.isFirstVisible = item.isLastVisible = false;
                if (!(item.hidden || item.hiddenAncestor)) {
                    if (!lastVisibleColumn) {
                        item.isFirstVisible = true;
                    }
                    lastVisibleColumn = item;
                }
            }
            // If we haven't hidden all columns, tag the last visible one encountered
            if (lastVisibleColumn) {
                lastVisibleColumn.isLastVisible = true;
            }
        }
        return result;
    },
    /**
     * @private
     * For use by column headers in determining whether there are any hideable columns
     * when deciding whether or not
     * the header menu should be disabled.
     */
    getHideableColumns: function() {
        var me = this,
            result = me.hideableColumns;
        if (!result) {
            result = me.hideableColumns = me.query('[hideable]');
        }
        return result;
    },
    /**
     * Returns the index of a leaf level header regardless of what the nesting
     * structure is.
     *
     * If a group header is passed, the index of the first leaf level header within it is returned.
     *
     * @param {Ext.grid.column.Column} header The header to find the index of
     * @return {Number} The index of the specified column header
     */
    getHeaderIndex: function(header) {
        // Binding the columnManager to a column makes it backwards-compatible with versions
        // that only bind the columnManager to a root header.
        if (!this.columnManager) {
            this.columnManager = this.getRootHeaderCt().columnManager;
        }
        return this.columnManager.getHeaderIndex(header);
    },
    /**
     * Get a leaf level header by index regardless of what the nesting
     * structure is.
     *
     * @param {Number} index The column index for which to retrieve the column.
     */
    getHeaderAtIndex: function(index) {
        // Binding the columnManager to a column makes it backwards-compatible with versions
        // that only bind the columnManager to a root header.
        if (!this.columnManager) {
            this.columnManager = this.getRootHeaderCt().columnManager;
        }
        return this.columnManager.getHeaderAtIndex(index);
    },
    /**
     * When passed a column index, returns the closet *visible* column to that. If the column
     * at the passed index is visible, that is returned. If it is hidden, either the next visible,
     * or the previous visible column is returned.
     *
     * @param {Number} index Position at which to find the closest visible column.
     */
    getVisibleHeaderClosestToIndex: function(index) {
        // Binding the columnManager to a column makes it backwards-compatible with versions
        // that only bind the columnManager to a root header.
        if (!this.visibleColumnManager) {
            this.visibleColumnManager = this.getRootHeaderCt().visibleColumnManager;
        }
        return this.visibleColumnManager.getVisibleHeaderClosestToIndex(index);
    },
    applyForceFit: function(header) {
        var me = this,
            view = me.view,
            minWidth = Ext.grid.plugin.HeaderResizer.prototype.minColWidth,
            // Used when a column's max contents are larger than the available view width.
            useMinWidthForFlex = false,
            defaultWidth = Ext.grid.header.Container.prototype.defaultWidth,
            // eslint-disable-next-line max-len
            availFlex = me.el.dom.clientWidth - (view.el.dom.scrollHeight > view.el.dom.clientHeight ? Ext.scrollbar.width() : 0),
            totalFlex = 0,
            items = me.getVisibleGridColumns(),
            hidden = header.hidden,
            len, i, item, maxAvailFlexOneColumn, myWidth;
        function getTotalFlex() {
            for (i = 0 , len = items.length; i < len; i++) {
                item = items[i];
                // Skip the current header.
                if (item === header) {
                    
                    continue;
                }
                item.flex = item.flex || item.width || item.getWidth();
                totalFlex += item.flex;
                item.width = null;
            }
        }
        function applyWidth() {
            // The currently-sized column (whether resized or reshown) will already
            // have a width, so all other columns will need to be flexed.
            var isCurrentHeader;
            for (i = 0 , len = items.length; i < len; i++) {
                item = items[i];
                isCurrentHeader = (item === header);
                if (useMinWidthForFlex && !isCurrentHeader) {
                    // The selected column is extremely large so set all the others 
                    // as flex minWidth.
                    item.flex = minWidth;
                    item.width = null;
                } else if (!isCurrentHeader) {
                    // Note that any widths MUST be converted to flex. Imagine that all but one
                    // columns are hidden.  The widths of each column very easily could be greater
                    // than the total available width (think about the how visible header widths
                    // increase as sibling columns are hidden), so they cannot be reliably used
                    // to size the header, and the only safe approach is to convert any all widths
                    // to flex (except for the current header).
                    myWidth = item.flex || defaultWidth;
                    item.flex = Math.max(Math.ceil((myWidth / totalFlex) * availFlex), minWidth);
                    item.width = null;
                }
                item.setWidth(item.width || item.flex);
            }
        }
        Ext.suspendLayouts();
        // Determine the max amount of flex that a single column can have.
        maxAvailFlexOneColumn = (availFlex - ((items.length + 1) * minWidth));
        // First, remove the header's flex as it should always receive a set width
        // since it is the header being operated on.
        header.flex = null;
        if (hidden) {
            myWidth = header.width || header.savedWidth || Math.floor(maxAvailFlexOneColumn / (items.length + 1));
            header.savedWidth = null;
        } else {
            myWidth = view.getMaxContentWidth(header);
        }
        // We need to know if the max content width of the selected column would blow out the
        // grid. If so, all the other visible columns will be flexed to minWidth. 
        if (myWidth > maxAvailFlexOneColumn) {
            header.width = maxAvailFlexOneColumn;
            useMinWidthForFlex = true;
        } else {
            header.width = myWidth;
            // Substract the current header's width from the available flex + some padding
            // to ensure that the last column doesn't get nudged out of the view.
            availFlex -= myWidth + defaultWidth;
            getTotalFlex();
        }
        applyWidth();
        Ext.resumeLayouts(true);
    },
    autoSizeColumn: function(header) {
        var view = this.view;
        if (view) {
            view.autoSizeColumn(header);
            if (this.forceFit) {
                this.applyForceFit(header);
            }
        }
    },
    getRefItems: function(deep) {
        // Override to include the header menu in the component tree
        var result = this.callParent([
                deep
            ]);
        if (this.menu) {
            result.push(this.menu);
        }
        return result;
    },
    initInheritedState: function(inheritedState, inheritedStateInner) {
        if (this.sealed) {
            inheritedState.sealed = true;
        }
        this.callParent([
            inheritedState,
            inheritedStateInner
        ]);
    },
    privates: {
        beginChildHide: function() {
            ++this.childHideCount;
        },
        endChildHide: function() {
            --this.childHideCount;
        },
        getFocusables: function() {
            return this.isRootHeader ? this.getVisibleGridColumns() : this.items.items;
        },
        initFocusableContainerKeyNav: function(el) {
            var me = this;
            if (!me.focusableKeyNav) {
                me.focusableKeyNav = new Ext.util.KeyNav({
                    target: el,
                    scope: me,
                    down: me.showHeaderMenu,
                    left: me.onFocusableContainerLeftKey,
                    right: me.onFocusableContainerRightKey,
                    home: me.onHomeKey,
                    end: me.onEndKey,
                    space: me.onHeaderActivate,
                    enter: me.onHeaderActivate
                });
            }
        },
        onHomeKey: function(e) {
            return this.focusChild(null, true, e);
        },
        onEndKey: function(e) {
            return this.focusChild(null, false, e);
        },
        showHeaderMenu: function(e) {
            var column = this.getFocusableFromEvent(e);
            // DownArrow event must be from a column, not a Component within the column
            // (eg filter fields)
            if (column && column.isColumn && column.triggerEl) {
                this.onHeaderTriggerClick(column, e, column.triggerEl);
            }
        },
        onHeaderActivate: function(e) {
            var column = this.getFocusableFromEvent(e),
                view, lastFocused;
            // Remember that not every descendant of a headerCt is a column!
            // It could be a child component of a column.
            if (column && column.isColumn) {
                view = column.getView();
                // Sort the column is configured that way.
                // sortOnClick may be set to false by SpreadsheelSelectionModel
                // to allow click to select a column.
                if (column.sortable && this.sortOnClick) {
                    lastFocused = view.getNavigationModel().getLastFocused();
                    column.toggleSortState();
                    // After keyboard sort, bring last focused record into view
                    if (lastFocused) {
                        view.ownerCt.ensureVisible(lastFocused.record);
                    }
                } else if (e.getKey() === e.SPACE) {
                    column.onTitleElClick(e, e.target, this.sortOnClick);
                }
                // onHeaderClick is a necessary part of accessibility processing, sortable or not.
                return this.onHeaderClick(column, e, column.el);
            }
        },
        onOwnerGridReconfigure: function(storeChanged, columnsChanged) {
            var me = this;
            if (!me.rendered || me.destroying || me.destroyed) {
                return;
            }
            // Adding or removing columns during reconfiguration could result
            // in changed FocusableContainer state.
            if (storeChanged || columnsChanged) {
                if (Ext.Component.layoutSuspendCount) {
                    me.$initFocusableContainerAfterLayout = true;
                } else {
                    me.initFocusableContainer();
                }
            }
        }
    }
});

/* eslint-disable max-len */
/**
 * This class specifies the definition for a column inside a {@link Ext.grid.Panel}. It encompasses
 * both the grid header configuration as well as displaying data within the grid itself. If the
 * {@link #columns} configuration is specified, this column will become a column group and can
 * contain other columns inside. In general, this class will not be created directly, rather
 * an array of column configurations will be passed to the grid:
 *
 *     @example
 *     Ext.create('Ext.data.Store', {
 *         storeId:'employeeStore',
 *         fields:['firstname', 'lastname', 'seniority', 'dep', 'hired'],
 *         data:[
 *             {firstname:"Michael", lastname:"Scott", seniority:7, dep:"Management", hired:"01/10/2004"},
 *             {firstname:"Dwight", lastname:"Schrute", seniority:2, dep:"Sales", hired:"04/01/2004"},
 *             {firstname:"Jim", lastname:"Halpert", seniority:3, dep:"Sales", hired:"02/22/2006"},
 *             {firstname:"Kevin", lastname:"Malone", seniority:4, dep:"Accounting", hired:"06/10/2007"},
 *             {firstname:"Angela", lastname:"Martin", seniority:5, dep:"Accounting", hired:"10/21/2008"}
 *         ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Column Demo',
 *         store: Ext.data.StoreManager.lookup('employeeStore'),
 *         columns: [
 *             {text: 'First Name',  dataIndex:'firstname'},
 *             {text: 'Last Name',  dataIndex:'lastname'},
 *             {text: 'Hired Month',  dataIndex:'hired', xtype:'datecolumn', format:'M'},
 *             {text: 'Department (Yrs)', xtype:'templatecolumn', tpl:'{dep} ({seniority})'}
 *         ],
 *         width: 400,
 *         forceFit: true,
 *         renderTo: Ext.getBody()
 *     });
 *
 * # Convenience Subclasses
 *
 * There are several column subclasses that provide default rendering for various data types
 *
 *  - {@link Ext.grid.column.Action}: Renders icons that can respond to click events inline
 *  - {@link Ext.grid.column.Boolean}: Renders for boolean values
 *  - {@link Ext.grid.column.Date}: Renders for date values
 *  - {@link Ext.grid.column.Number}: Renders for numeric values
 *  - {@link Ext.grid.column.Template}: Renders a value using an {@link Ext.XTemplate} using the record data
 *
 * # Setting Widths
 *
 * The columns are laid out by a {@link Ext.layout.container.HBox} layout, so a column can either
 * be given an explicit width value or a {@link #flex} configuration. If no width is specified the grid will
 * automatically the size the column to 100px.
 * 
 * Group columns (columns with {@link #columns child columns}) may be sized using {@link #flex},
 * in which case they will apply `forceFit` to their child columns so as not to leave blank space.
 * 
 * If a group column is not flexed, its width is calculated by measuring the width of the
 * child columns, so a width option should not be specified in that case.
 *
 * # Header Options
 *
 *  - {@link #text}: Sets the header text for the column
 *  - {@link #sortable}: Specifies whether the column can be sorted by clicking the header or using the column menu
 *  - {@link #hideable}: Specifies whether the column can be hidden using the column menu
 *  - {@link #menuDisabled}: Disables the column header menu
 *  - {@link #cfg-draggable}: Specifies whether the column header can be reordered by dragging
 *  - {@link #groupable}: Specifies whether the grid can be grouped by the column dataIndex. See also {@link Ext.grid.feature.Grouping}
 *
 * # Data Options
 *
 *  - {@link #dataIndex}: The dataIndex is the field in the underlying {@link Ext.data.Store} to use as the value for the column.
 *  - {@link Ext.grid.column.Column#renderer}: Allows the underlying store
 *  value to be transformed before being displayed in the grid
 *
 * ## State saving
 *
 * When the owning {@link Ext.grid.Panel Grid} is configured
 * {@link Ext.grid.Panel#cfg-stateful}, it will save its column state (order and width)
 * encapsulated within the default Panel state of changed width and height and
 * collapsed/expanded state.
 *
 * On a `stateful` grid, not only should the Grid have a
 * {@link Ext.grid.Panel#cfg-stateId}, each column of the grid should also be configured
 * with a {@link #stateId} which identifies that column locally within the grid.
 *
 * Omitting the `stateId` config from the columns results in columns with generated
 * internal ID's.  The generated ID's are subject to change on each page load
 * making it impossible for the state manager to restore the previous state of the
 * columns.
 */
Ext.define('Ext.grid.column.Column', {
    /* eslint-enable max-len */
    extend: 'Ext.grid.header.Container',
    xtype: 'gridcolumn',
    requires: [
        'Ext.grid.ColumnComponentLayout',
        'Ext.grid.ColumnLayout',
        'Ext.app.bind.Parser'
    ],
    // for "format" support
    alternateClassName: 'Ext.grid.Column',
    config: {
        triggerVisible: false,
        /**
         * @cfg {Function/String/Object/Ext.util.Sorter} sorter
         * A Sorter, or sorter config object to apply when the standard user interface
         * sort gesture is invoked. This is usually clicking this column header, but
         * there are also menu options to sort ascending or descending.
         *
         * Note that a sorter may also be specified as a function which accepts two
         * records to compare.
         *
         * In 6.2.0, a `{@link Ext.app.ViewController controller}` method can be used
         * like so:
         *
         *      sorter: {
         *          sorterFn: 'sorterMethodName'
         *      }
         *
         * @since 6.0.1
         */
        sorter: null,
        /**
         * @cfg {'start'/'center'/'end'} [align='start']
         * Sets the alignment of the header and rendered columns.
         * Possible values are: `'start'`, `'center'`, and `'end'`.
         *
         * Since 6.2.0, `'left'` and `'right'` will still work, but retain their meaning
         * even when the application is in RTL mode.
         *
         * `'start'` and `'end'` always conform to the locale's text direction.
         */
        align: 'start'
    },
    baseCls: Ext.baseCSSPrefix + 'column-header',
    // Not the standard, automatically applied overCls because we must filter out
    // overs of child headers.
    hoverCls: Ext.baseCSSPrefix + 'column-header-over',
    ariaRole: 'columnheader',
    focusableContainer: false,
    sortState: null,
    possibleSortStates: [
        'ASC',
        'DESC'
    ],
    // These are not readable descriptions; the values go in the aria-sort attribute.
    ariaSortStates: {
        ASC: 'ascending',
        DESC: 'descending'
    },
    childEls: [
        'titleEl',
        'triggerEl',
        'textEl',
        'textContainerEl',
        'textInnerEl'
    ],
    /**
     * @cfg {Boolean} [headerWrap=false]
     * The default setting indicates that external CSS rules dictate that the title is
     * `white-space: nowrap` and therefore, width cannot affect the measured height by causing
     * text wrapping. This is what the Sencha-supplied styles set. If you change those styles
     * to allow text wrapping, you must set this to `true`.
     * @private
     */
    headerWrap: false,
    /* eslint-disable indent, max-len */
    renderTpl: [
        '<div id="{id}-titleEl" data-ref="titleEl" role="presentation"',
        '{tipMarkup}class="',
        Ext.baseCSSPrefix,
        'column-header-inner<tpl if="!$comp.isContainer"> ',
        Ext.baseCSSPrefix,
        'leaf-column-header</tpl>',
        '<tpl if="empty"> ',
        Ext.baseCSSPrefix,
        'column-header-inner-empty</tpl>">',
        //
        // TODO:
        // When IE8 retires, revisit https://jsbin.com/honawo/quiet for better way to center header text
        //
        '<div id="{id}-textContainerEl" data-ref="textContainerEl" role="presentation" class="',
        Ext.baseCSSPrefix,
        'column-header-text-container">',
        '<div role="presentation" class="',
        Ext.baseCSSPrefix,
        'column-header-text-wrapper">',
        '<div id="{id}-textEl" data-ref="textEl" role="presentation" class="',
        Ext.baseCSSPrefix,
        'column-header-text',
        '{childElCls}">',
        '<span id="{id}-textInnerEl" data-ref="textInnerEl" role="presentation" class="',
        Ext.baseCSSPrefix,
        'column-header-text-inner">{text}</span>',
        '</div>',
        '{%',
        'values.$comp.afterText(out, values);',
        '%}',
        '</div>',
        '</div>',
        '<tpl if="!menuDisabled">',
        '<div id="{id}-triggerEl" data-ref="triggerEl" role="presentation" unselectable="on" class="',
        Ext.baseCSSPrefix,
        'column-header-trigger',
        '{childElCls}" style="{triggerStyle}"></div>',
        '</tpl>',
        '</div>',
        '{%this.renderContainer(out,values)%}'
    ],
    /* eslint-enable indent, max-len */
    /**
     * @cfg {Object[]} columns
     * An optional array of sub-column definitions. This column becomes a group, and houses
     * the columns defined in the `columns` config.
     *
     * Group columns may not be sortable. But they may be hideable and moveable. And you may move
     * headers into and out of a group. Note that if all sub columns are dragged out of a group,
     * the group is destroyed.
     */
    /**
     * @cfg {String} stateId
     * An identifier which identifies this column uniquely within the owning grid's
     * {@link #stateful state}.
     *
     * This does not have to be *globally* unique. A column's state is not saved standalone.
     * It is encapsulated within the owning grid's state.
     */
    /**
     * @cfg {String} dataIndex
     * The name of the field in the grid's {@link Ext.data.Store}'s {@link Ext.data.Model}
     * definition from which to draw the column's value. **Required.**
     */
    dataIndex: null,
    /**
     * @cfg {String} text
     * The header text to be used as innerHTML (html tags are accepted) to display in the Grid.
     */
    text: '\xa0',
    /**
     * @cfg {String} header
     * The header text.
     * @deprecated 4.0 Use {@link #text} instead.
     */
    /**
     * @cfg {String} menuText
     * The text to render in the column visibility selection menu for this column.  If not
     * specified, will default to the text value.
     */
    menuText: null,
    /**
     * @cfg {String} [emptyCellText=undefined]
     * The text to display in empty cells (cells with a value of `undefined`, `null`, or `''`).
     *
     * Defaults to `&#160;` aka `&nbsp;`.
     */
    emptyCellText: '\xa0',
    /**
     * @cfg {Boolean} sortable
     * False to disable sorting of this column. Whether local/remote sorting is used is specified in
     * `{@link Ext.data.Store#remoteSort}`.
     */
    sortable: true,
    /**
     * @cfg {Boolean} [enableTextSelection=false]
     * True to enable text selection inside grid cells in this column.
     */
    /**
     * @cfg {Boolean} lockable
     * If the grid is configured with {@link Ext.panel.Table#enableLocking enableLocking}, or has
     * columns which are configured with a {@link #locked} value, this option may be used to disable
     * user-driven locking or unlocking of this column. This column will remain in the side
     * into which its own {@link #locked} configuration placed it.
     */
    /**
     * @cfg {Boolean} groupable
     * If the grid uses a {@link Ext.grid.feature.Grouping}, this option may be used to disable
     * the header menu item to group by the column selected. By default, the header menu group
     * option is enabled. Set to false to disable (but still show) the group option
     * in the header menu for the column.
     */
    /**
     * @cfg {Boolean} fixed
     * True to prevent the column from being resizable.
     * @deprecated 4.0 Use {@link #resizable} instead.
     */
    /**
     * @cfg {Boolean} [locked=false]
     * True to lock this column in place.  Implicitly enables locking on the grid.
     * See also {@link Ext.grid.Panel#enableLocking}.
     */
    /**
     * @cfg {Boolean} [cellWrap=false]
     * True to allow whitespace in this column's cells to wrap, and cause taller column height where
     * necessary.
     *
     * This implicitly sets the {@link #variableRowHeight} config to `true`
     */
    /**
     * @cfg {Boolean} [variableRowHeight=false]
     * True to indicate that data in this column may take on an unpredictable height, possibly
     * differing from row to row.
     *
     * If this is set, then View refreshes, and removal and addition of new rows will result
     * in an ExtJS layout of the grid in order to adjust for possible addition/removal of scrollbars
     * in the case of data changing height.
     *
     * This config also tells the View's buffered renderer that row heights are unpredictable,
     * and must be remeasured as the view is refreshed.
     */
    /**
     * @cfg {Boolean} resizable
     * False to prevent the column from being resizable.
     */
    resizable: true,
    /**
     * @cfg {Boolean} hideable
     * False to prevent the user from hiding this column.
     */
    hideable: true,
    /**
     * @cfg {Boolean} menuDisabled
     * True to disable the column header menu containing sort/hide options.
     */
    menuDisabled: false,
    /**
     * @cfg {Function/String} renderer
     * A renderer is an 'interceptor' method which can be used to transform data (value,
     * appearance, etc.) before it is rendered. Example:
     *
     * **NOTE:** In previous releases, a string was treated as a method on
     * `Ext.util.Format` but that is now handled by the {@link #formatter} config.
     *
     * @param {Object} value The data value for the current cell
     *
     *     renderer: function(value){
     *         // evaluates `value` to append either `person' or `people`
     *         return Ext.util.Format.plural(value, 'person', 'people');
     *     }
     *
     * @param {Object} metaData A collection of metadata about the current cell; can be
     * used or modified by the renderer. Recognized properties are: `tdCls`, `tdAttr`,
     * and `tdStyle`.
     *
     * To add style attributes to the `&lt;td>` element, you must use the `tdStyle`
     * property. Using a style attribute in the `tdAttr` property will override the
     * styles the column sets, such as the width which will break the rendering.
     *
     * You can see an example of using the metaData parameter below.
     *
     *      Ext.create('Ext.data.Store', {
     *           storeId: 'simpsonsStore',
     *           fields: ['class', 'attr', 'style'],
     *           data: {
     *               'class': 'red-bg',
     *               'attr': 'lightyellow',
     *               'style': 'red'
     *           }
     *      });
     *
     *      Ext.create('Ext.grid.Panel', {
     *           title: 'Simpsons',
     *           store: Ext.data.StoreManager.lookup('simpsonsStore'),
     *           columns: [{
     *               text: 'Name',
     *               dataIndex: 'class',
     *               renderer: function (value, metaData) {
     *                   metaData.tdCls = value;
     *                   return value;
     *               }
     *           }, {
     *               text: 'Email',
     *               dataIndex: 'attr',
     *               flex: 1,
     *               renderer: function (value, metaData) {
     *                   metaData.tdAttr = 'bgcolor="' + value + '"';
     *                   return value;
     *               }
     *           }, {
     *               text: 'Phone',
     *               dataIndex: 'style',
     *               renderer: function (value, metaData) {
     *                   metaData.tdStyle = 'color:' + value;
     *                   return value;
     *               }
     *           }],
     *           height: 200,
     *           width: 400,
     *           renderTo: Ext.getBody()
     *       });
     *
     * @param {Ext.data.Model} record The record for the current row
     *
     *     renderer: function (value, metaData, record) {
     *         // evaluate the record's `updated` field and if truthy return the value
     *         // from the `newVal` field, else return value
     *         var updated = record.get('updated');
     *         return updated ? record.get('newVal') : value;
     *     }
     *
     * @param {Number} rowIndex The index of the current row
     *
     *     renderer: function (value, metaData, record, rowIndex) {
     *         // style the cell differently for even / odd values
     *         var odd = (rowIndex % 2 === 0);
     *         metaData.tdStyle = 'color:' + (odd ? 'gray' : 'red');
     *     }
     *
     * @param {Number} colIndex The index of the current column
     *
     *     var myRenderer = function(value, metaData, record, rowIndex, colIndex) {
     *         if (colIndex === 0) {
     *             metaData.tdAttr = 'data-qtip=' + value;
     *         }
     *         // additional logic to apply to values in all columns
     *         return value;
     *     }
     *
     *     // using the same renderer on all columns you can process the value for
     *     // each column with the same logic and only set a tooltip on the first column
     *     renderer: myRenderer
     *
     * _See also {@link Ext.tip.QuickTipManager}_
     *
     * @param {Ext.data.Store} store The data store
     *
     *     renderer: function (value, metaData, record, rowIndex, colIndex, store) {
     *         // style the cell differently depending on how the value relates to the
     *         // average of all values
     *         var average = store.average('grades');
     *         metaData.tdCls = (value < average) ? 'needsImprovement' : 'satisfactory';
     *         return value;
     *     }
     *
     * @param {Ext.view.View} view The data view
     *
     *     renderer: function (value, metaData, record, rowIndex, colIndex, store, view) {
     *         // style the cell using the dataIndex of the column
     *         var headerCt = this.getHeaderContainer(),
     *             column = headerCt.getHeaderAtIndex(colIndex);
     *
     *         metaData.tdCls = 'app-' + column.dataIndex;
     *         return value;
     *     }
     *
     * @return {String}
     * The HTML string to be rendered.
     * @controllable
     */
    renderer: false,
    /**
     * @cfg {Function/String} updater
     * An updater is a method which is used when records are updated, and an *existing* grid row
     * needs updating. The method is passed the cell element and may manipulate it in any way.
     *
     * **Note**: The updater is required to insert the {@link #emptyCellText} if there
     * is no value in the cell.
     *
     *     Ext.create('Ext.grid.Panel', {
     *         title: 'Grades',
     *         store: {
     *             fields: ['originalScore', 'newScore'],
     *             data: [{
     *                 originalScore: 70,
     *                 newScore: 70
     *             }]
     *         },
     *         columns: [{
     *             text: 'Score',
     *             dataIndex: 'newScore',
     *             editor: 'numberfield',
     *             flex: 1,
     *             updater: function (cell, value, record, view) {
     *                 var inner = Ext.get(cell).first(),
     *                     originalScore = record.get('originalScore'),
     *                     color = (value === originalScore)
     *                         ? 'black' : (value > originalScore) ? 'green' : 'red';
     *
     *                 // set the color based on the current value
     *                 // relative to the originalScore value
     *                 // * same   = black
     *                 // * higher = green
     *                 // * less   = red
     *                 inner.applyStyles({
     *                     color: color
     *                 });
     *                 // pass the value to the cell's inner el
     *                 inner.setHtml(value);
     *             }
     *         }],
     *         height: 200,
     *         width: 400,
     *         renderTo: document.body,
     *         tbar: [{
     *             xtype: 'numberfield',
     *             fieldLabel: 'New Score',
     *             value: 70,
     *             listeners: {
     *                 change: function (field, newValue) {
     *                     this.up('grid').getStore().first().set('newScore', newValue);
     *                 }
     *             }
     *         }]
     *     });
     *
     * If a string is passed it is assumed to be the name of a method defined by the
     * {@link #method-getController ViewController} or an ancestor component configured as
     * {@link #defaultListenerScope}.
     * @cfg {HTMLElement} updater.cell The HTML cell element to update.
     * @cfg {Object} updater.value The data value for the current cell
     * @cfg {Ext.data.Model} updater.record The record for the current row
     * @cfg {Ext.view.View} updater.view The current view
     *
     * **Note**: The updater is required to insert the {@link #emptyCellText} if there is no value
     * in the cell.
     *
     * @controllable
     */
    /**
     * @cfg {Object} scope
     * The scope to use when calling the
     * {@link Ext.grid.column.Column#renderer} function.
     */
    /**
     * @method defaultRenderer
     * When defined this will take precedence over the
     * {@link Ext.grid.column.Column#renderer renderer} config.
     * This is meant to be defined in subclasses that wish to supply their own renderer.
     * @protected
     * @template
     */
    /**
     * @cfg {Function/String} editRenderer
     * A renderer to be used in conjunction with
     * {@link Ext.grid.plugin.RowEditing RowEditing}. This renderer is used to display a
     * custom value for non-editable fields.
     *
     * **Note:** The editRenderer is called when the roweditor is initially shown.
     * Changes to the record during editing will not call editRenderer.
     *
     *     var store = Ext.create('Ext.data.Store', {
     *         fields: ['name', 'email'],
     *         data: [{
     *             "name": "Finn",
     *             "email": "finn@adventuretime.com"
     *         }, {
     *             "name": "Jake",
     *             "email": "jake@adventuretime.com"
     *         }]
     *     });
     *
     *     Ext.create('Ext.grid.Panel', {
     *         title: 'Land Of Ooo',
     *         store: store,
     *         columns: [{
     *             text: 'Name',
     *             dataIndex: 'name',
     *             editRenderer: function(value){
     *                 return '<span style="color:gray;">' + value + '</span>';
     *               }
     *         }, {
     *             text: 'Email',
     *             dataIndex: 'email',
     *             flex: 1,
     *             editor: {
     *                 xtype: 'textfield',
     *                 allowBlank: false
     *             }
     *         }],
     *         plugins: {
     *             rowediting: {
     *                 clicksToEdit: 1
     *             }
     *         },
     *         height: 200,
     *         width: 400,
     *         renderTo: document.body
     *     });
     *
     * @param {Object} value The data value for the current cell
     *
     *     editRenderer: function(value){
     *         // evaluates `value` to append either `person' or `people`
     *         return Ext.util.Format.plural(value, 'person', 'people');
     *     }
     *
     * @param {Object} metaData **Note:** The metadata param is passed to the
     * editRenderer, but is not used.
     *
     * @param {Ext.data.Model} record The record for the current row
     *
     *     editRenderer: function (value, metaData, record) {
     *         // evaluate the record's `updated` field and if truthy return the value
     *         // from the `newVal` field, else return value
     *         var updated = record.get('updated');
     *         return updated ? record.get('newVal') : value;
     *     }
     *
     * @param {Number} rowIndex The index of the current row
     *
     *     editRenderer: function (value, metaData, record, rowIndex) {
     *         // style the value differently for even / odd values
     *         var odd = (rowIndex % 2 === 0),
     *             color = (odd ? 'gray' : 'red');
     *         return '<span style="color:' + color + ';">' + value + '</span>';
     *     }
     *
     * @param {Number} colIndex The index of the current column
     *
     * @param {Ext.data.Store} store The data store
     *
     *     editRenderer: function (value, metaData, record, rowIndex, colIndex, store) {
     *         // style the cell differently depending on how the value relates to the
     *         // average of all values
     *         var average = store.average('grades'),
     *             status = (value < average) ? 'needsImprovement' : 'satisfactory';
     *         return '<span class="' + status + '">' + value + '</span>';
     *     }
     *
     * @param {Ext.view.View} view The data view
     *
     *     editRenderer: function (value, metaData, record, rowIndex, colIndex, store, view) {
     *         // style the value using the dataIndex of the column
     *         var headerCt = this.getHeaderContainer(),
     *             column = headerCt.getHeaderAtIndex(colIndex);
     *
     *         return '<span class="app-' + column.dataIndex + '">' + value + '</span>';
     *     }
     *
     * @return {String}
     * The HTML string to be rendered.
     * @controllable
     */
    /**
     * @cfg {Function/String} summaryRenderer
     * A renderer to be used in conjunction with the {@link Ext.grid.feature.Summary Summary} or
     * {@link Ext.grid.feature.GroupingSummary GroupingSummary} features. This renderer is used to
     * display a summary value for this column.
     * @controllable
     */
    /**
     * @cfg {Boolean} draggable
     * False to disable drag-drop reordering of this column.
     */
    draggable: true,
    /**
     * @cfg {String} tooltip
     * A tooltip to display for this column header
     */
    /**
     * @cfg {String} [tooltipType="qtip"]
     * The type of {@link #tooltip} to use. Either 'qtip' for QuickTips or 'title' for title
     * attribute.
     */
    tooltipType: 'qtip',
    // Header does not use the typical ComponentDraggable class and therefore we
    // override this with an emptyFn. It is controlled at the HeaderDragZone.
    initDraggable: Ext.emptyFn,
    /**
     * @cfg {String} tdCls
     * A CSS class names to apply to the table cells for this column.
     */
    tdCls: '',
    /**
     * @cfg {Object/String} editor
     * An optional xtype or config object for a {@link Ext.form.field.Field Field} to use
     * for editing.
     * Only applicable if the grid is using an {@link Ext.grid.plugin.Editing Editing} plugin.
     *
     * **Note:** The {@link Ext.form.field.HtmlEditor HtmlEditor} field is not a
     * supported editor field type.
     */
    /**
     * @cfg {String} [dirtyText="Cell value has been edited"]
     * This text will be announced by Assistive Technologies such as screen readers when
     * a cell with changed ("dirty") value is focused.
     * @locale
     */
    dirtyText: "Cell value has been edited",
    /**
     * @cfg {Object/String} field
     * Alias for {@link #editor}.
     * @deprecated 4.0.5 Use {@link #editor} instead.
     */
    /**
     * @cfg {Boolean} producesHTML
     * This flag indicates that the renderer produces HTML.
     *
     * If this column is going to be updated rapidly, and the
     * {@link Ext.grid.column.Column#renderer} or {@link #cfg-updater} only produces
     * text, then to avoid the expense of HTML parsing and element production during the
     * update, this property may be configured as `false`.
     */
    producesHTML: true,
    /**
     * @cfg {Boolean} ignoreExport
     * This flag indicates that this column will be ignored when grid data is exported.
     *
     * When grid data is exported you may want to export only some columns that are important
     * and not everything. Widget, check and action columns are not relevant when data is
     * exported. You can set this flag on any column that you want to be ignored during export.
     *
     * This is used by {@link Ext.grid.plugin.Clipboard clipboard plugin} and
     * {@link Ext.grid.plugin.Exporter exporter plugin}.
     */
    ignoreExport: false,
    /**
     * @cfg {Ext.exporter.file.Style/Ext.exporter.file.Style[]} exportStyle
     *
     * A style definition that is used during data export via the {@link Ext.grid.plugin.Exporter}.
     * This style will be applied to the columns generated in the exported file.
     *
     * You could define it as a single object that will be used by all exporters:
     *
     *      {
     *          xtype: 'numbercolumn',
     *          dataIndex: 'price',
     *          text: 'Price',
     *          exportStyle: {
     *              format: 'Currency',
     *              alignment: {
     *                  horizontal: 'Right'
     *              },
     *              font: {
     *                  italic: true
     *              }
     *          }
     *      }
     *
     * You could also define it as an array of objects, each object having a `type`
     * that specifies by which exporter will be used:
     *
     *      {
     *          xtype: 'numbercolumn',
     *          dataIndex: 'price',
     *          text: 'Price',
     *          exportStyle: [{
     *              type: 'html', // used by the `html` exporter
     *              format: 'Currency',
     *              alignment: {
     *                  horizontal: 'Right'
     *              },
     *              font: {
     *                  italic: true
     *              }
     *          },{
     *              type: 'csv', // used by the `csv` exporter
     *              format: 'General'
     *          }]
     *      }
     *
     * Or you can define it as an array of objects that has:
     *
     * - one object with no `type` key that is considered the style to use by all exporters
     * - objects with the `type` key defined that are exceptions of the above rule
     *
     *
     *      {
     *          xtype: 'numbercolumn',
     *          dataIndex: 'price',
     *          text: 'Price',
     *          exportStyle: [{
     *              // no type defined means this is the default
     *              format: 'Currency',
     *              alignment: {
     *                  horizontal: 'Right'
     *              },
     *              font: {
     *                  italic: true
     *              }
     *          },{
     *              type: 'csv', // only the CSV exporter has a special style
     *              format: 'General'
     *          }]
     *      }
     *
     */
    exportStyle: null,
    /**
     * @cfg {Boolean/Function/String} exportRenderer
     *
     * During data export via the {@link Ext.grid.plugin.Exporter} plugin the data for
     * this column could be formatted in multiple ways:
     *
     * - using the `exportStyle.format`
     * - using the `formatter` if no `exportStyle` is defined
     * - using the `exportRenderer`
     *
     * If you want to use the `renderer` defined on this column then set `exportRenderer`
     * to `true`. Beware that this should only happen if the `renderer` deals only with
     * data on the record or value and it does NOT style the cell or returns an html
     * string.
     *
     *      {
     *          xtype: 'numbercolumn',
     *          dataIndex: 'price',
     *          text: 'Price',
     *          renderer: function (value, metaData, record, rowIndex, colIndex, store, view) {
     *              return Ext.util.Format.currency(value);
     *          },
     *          exportRenderer: true
     *      }
     *
     * If you don't want to use the `renderer` during export but you still want to format
     * the value in a special way then you can provide a function to `exportRenderer` or
     * a string (which is a function name on the ViewController).
     * The provided function has the same signature as the renderer.
     *
     *      {
     *          xtype: 'numbercolumn',
     *          dataIndex: 'price',
     *          text: 'Price',
     *          exportRenderer: function (value, metaData, record, rowIndex, colIndex, store, view){
     *              return Ext.util.Format.currency(value);
     *          }
     *      }
     *
     *
     *      {
     *          xtype: 'numbercolumn',
     *          dataIndex: 'price',
     *          text: 'Price',
     *          exportRenderer: 'exportAsCurrency' // this is a function on the ViewController
     *      }
     *
     *
     * If `exportStyle.format`, `formatter` and `exportRenderer` are all defined on the
     * column then the `exportStyle` wins and will be used to format the data for this
     * column.
     */
    exportRenderer: false,
    /**
     * @cfg {Boolean/Function/String} exportSummaryRenderer
     *
     * This config is similar to {@link #exportRenderer} but is applied to summary
     * records.
     *
     */
    exportSummaryRenderer: false,
    /**
     * @property {Ext.dom.Element} triggerEl
     * Element that acts as button for column header dropdown menu.
     */
    /**
     * @property {Ext.dom.Element} textEl
     * Element that contains the text in column header.
     */
    /**
     * @cfg {Boolean} [cellFocusable=true]
     * Configure as `false` to remove all cells in this column from navigation.
     *
     * This is currently used by the PivotGrid package to create columns which have
     * no semantic role, but are purely for visual indentation purposes.
     * @since 6.2.0.
     */
    /**
     * @property {Boolean} isHeader
     * @deprecated 6.5.0 see isColumn
     * Set in this class to identify, at runtime, instances which are not instances of the
     * HeaderContainer base class, but are in fact, the subclass: Header.
     */
    isHeader: true,
    /**
     * @property {Boolean} isColumn
     * @readonly
     * Set in this class to identify, at runtime, instances which are not instances of the
     * HeaderContainer base class, but are in fact simple column headers.
     */
    isColumn: true,
    scrollable: false,
    // Override scrollable config from HeaderContainr class
    requiresMenu: false,
    // allow plugins to set this property to influence if menu can be disabled
    tabIndex: -1,
    ascSortCls: Ext.baseCSSPrefix + 'column-header-sort-ASC',
    descSortCls: Ext.baseCSSPrefix + 'column-header-sort-DESC',
    componentLayout: 'columncomponent',
    groupSubHeaderCls: Ext.baseCSSPrefix + 'group-sub-header',
    groupHeaderCls: Ext.baseCSSPrefix + 'group-header',
    clickTargetName: 'titleEl',
    // So that when removing from group headers which are then empty and then get destroyed,
    // there's no child DOM left
    detachOnRemove: true,
    // We need to override the default component resizable behaviour here
    initResizable: Ext.emptyFn,
    // Property names to reference the different types of renderers and formatters that
    // we can use.
    rendererNames: {
        column: 'renderer',
        edit: 'editRenderer',
        summary: 'summaryRenderer'
    },
    formatterNames: {
        column: 'formatter',
        edit: 'editFormatter',
        summary: 'summaryFormatter'
    },
    initComponent: function() {
        var me = this;
        // Preserve the scope to resolve a custom renderer.
        // Subclasses (TreeColumn) may insist on scope being this.
        if (!me.rendererScope) {
            me.rendererScope = me.scope;
        }
        if (me.header != null) {
            me.text = me.header;
            me.header = null;
        }
        if (me.cellWrap) {
            me.tdCls = (me.tdCls || '') + ' ' + Ext.baseCSSPrefix + 'wrap-cell';
        }
        // A group header; It contains items which are themselves Headers
        if (me.columns != null) {
            me.isGroupHeader = true;
            me.ariaRole = 'presentation';
            if (me.dataIndex) {
                Ext.raise('Ext.grid.column.Column: Group header may not accept a dataIndex');
            }
            if ((me.width && me.width !== Ext.grid.header.Container.prototype.defaultWidth)) {
                Ext.raise('Ext.grid.column.Column: Group header does not support ' + 'setting explicit widths. A group header either shrinkwraps ' + 'its children, or must be flexed.');
            }
            // The headers become child items
            me.items = me.columns;
            me.columns = null;
            me.cls = (me.cls || '') + ' ' + me.groupHeaderCls;
            // A group cannot be sorted, or resized - it shrinkwraps its children
            me.sortable = me.resizable = false;
            me.align = 'center';
        } else {
            // Flexed Headers need to have a minWidth defined so that they can never be squeezed out
            // of existence by the HeaderContainer's specialized Box layout, the ColumnLayout.
            // The ColumnLayout's overridden calculateChildboxes method extends the available layout
            // space to accommodate the "desiredWidth" of all the columns.
            if (me.flex) {
                me.minWidth = me.minWidth || Ext.grid.plugin.HeaderResizer.prototype.minColWidth;
            }
        }
        me.addCls(Ext.baseCSSPrefix + 'column-header-align-' + me.getMappedAlignment(me.align));
        // Set up the renderer types: 'renderer', 'editRenderer', and 'summaryRenderer'
        me.setupRenderer();
        me.setupRenderer('edit');
        me.setupRenderer('summary');
        // Initialize as a HeaderContainer
        me.callParent();
    },
    beforeLayout: function() {
        var me = this,
            items = me.items,
            colCount = 0,
            flex = me.flex,
            len, i, item, hasFlexedChildren;
        if (flex && me.isGroupHeader) {
            if (!Ext.isArray(items)) {
                items = items.items;
            }
            len = items.length;
            for (i = 0; !hasFlexedChildren && i < len; i++) {
                item = items[i];
                if (item.isColumn && !item.hidden) {
                    ++colCount;
                    hasFlexedChildren = item.flex;
                }
            }
            // If all child columns have been given a width, we must fall back to shrinkwrapping
            // them. Save any current flex state and restore it once the layout finishes so this
            // column isn't permanently flexed
            if (!hasFlexedChildren && colCount) {
                me.savedFlex = flex;
                me.flex = null;
            }
        }
        me.callParent();
    },
    onAdded: function(container, pos, instanced) {
        var me = this;
        me.callParent([
            container,
            pos,
            instanced
        ]);
        // Invalidate references, so that when asked for, they have to be regathered
        me.view = me.rootHeaderCt = me.cellSelector = me.visibleIndex = null;
        if (!me.headerId) {
            me.calculateHeaderId();
        }
        me.configureStateInfo();
    },
    _initSorterFn: function(a, b) {
        // NOTE: this method is placed as a "sorterFn" on a Sorter instance,
        // so "this" is not a Column! Our goal is to replace the sorterFn of
        // this Sorter on first use and then never get called again.
        var sorter = this,
            column = sorter.column,
            scope = column.resolveListenerScope(),
            name = sorter.methodName,
            fn = scope && scope[name],
            ret = 0;
        if (fn) {
            sorter.setSorterFn(fn);
            sorter.column = null;
            // no need anymore (GC friendly)
            // We are called by sort() so the ASC/DESC will be applied to what
            // we return. Therefore, the correct delegation is to directly call
            // the real sorterFn directly.
            ret = fn.call(scope, a, b);
        } else if (!scope) {
            Ext.raise('Cannot resolve scope for column ' + column.id);
        } else {
            Ext.raise('No such method "' + name + '" on ' + scope.$className);
        }
        return ret;
    },
    applySorter: function(sorter) {
        var me = this,
            sorterFn = sorter ? sorter.sorterFn : null,
            tablepanel, ret;
        if (typeof sorterFn === 'string') {
            // Instead of treating a string as a fieldname, it makes more sense to
            // expect it to be a sortFn on the controller.
            ret = new Ext.util.Sorter(Ext.applyIf({
                sorterFn: me._initSorterFn
            }, sorter));
            ret.methodName = sorterFn;
            ret.column = me;
        } else {
            tablepanel = me.getRootHeaderCt().up('tablepanel');
            // Have the sorter spec decoded by the collection that will host it.
            ret = tablepanel.store.getData().getSorters().decodeSorter(sorter);
        }
        return ret;
    },
    updateAlign: function(align) {
        // Translate according to the locale.
        // This property is read by Ext.view.Table#renderCell
        this.textAlign = this.getMappedAlignment(align);
    },
    bindFormatter: function(format) {
        var me = this;
        return function(v) {
            return format(v, me.rendererScope || me.resolveListenerScope());
        };
    },
    bindRenderer: function(renderer) {
        var me = this;
        if (renderer in Ext.util.Format) {
            Ext.log.warn('Use "formatter" config instead of "renderer" to use ' + 'Ext.util.Format to format cell values');
        }
        me.hasCustomRenderer = true;
        return function() {
            return Ext.callback(renderer, me.rendererScope, arguments, 0, me);
        };
    },
    setupRenderer: function(type) {
        var me = this,
            format, renderer, isColumnRenderer, parser, dynamic;
        // type can be null or 'edit', or 'summary'
        type = type || 'column';
        format = me[me.formatterNames[type]];
        renderer = me[me.rendererNames[type]];
        isColumnRenderer = type === 'column';
        if (!format) {
            if (renderer) {
                // Resolve a string renderer into the correct property: 'renderer',
                // 'editRenderer', or 'summaryRenderer'
                if (typeof renderer === 'string') {
                    renderer = me[me.rendererNames[type]] = me.bindRenderer(renderer);
                    dynamic = true;
                }
                if (isColumnRenderer) {
                    // If we are setting up a normal column renderer, detect if it's a custom one
                    // (reads more than one parameter)
                    // We can't read the arg list until we resolve the scope, so we must assume
                    // it's a renderer that needs a full update if it's dynamic
                    me.hasCustomRenderer = dynamic || me.shouldFlagCustomRenderer(renderer);
                }
            }
            // Column renderer could not be resolved: use the default one.
            else if (isColumnRenderer && me.defaultRenderer) {
                me.renderer = me.defaultRenderer;
                me.usingDefaultRenderer = true;
            }
        } else {
            /**
             * @cfg {String} formatter
             * This config accepts a format specification as would be used in a `Ext.Template`
             * formatted token. For example `'round(2)'` to round numbers to 2 decimal places
             * or `'date("Y-m-d")'` to format a Date.
             *
             * In previous releases the `renderer` config had limited abilities to use one
             * of the `Ext.util.Format` methods but `formatter` now replaces that usage and
             * can also handle formatting parameters.
             *
             * When the value begins with `"this."` (for example, `"this.foo(2)"`), the
             * implied scope on which "foo" is found is the `scope` config for the column.
             *
             * If the `scope` is not given, or implied using a prefix of `"this"`, then either the
             * {@link #method-getController ViewController} or the closest ancestor component
             * configured as {@link #defaultListenerScope} is assumed to be the object
             * with the method.
             * @since 5.0.0
             */
            parser = Ext.app.bind.Parser.fly(format);
            format = parser.compileFormat();
            parser.release();
            // processed - trees come back here to add its renderer
            me[me.formatterNames[type]] = null;
            // Set up the correct property: 'renderer', 'editRenderer', or 'summaryRenderer'
            me[me.rendererNames[type]] = me.bindFormatter(format);
        }
    },
    getView: function() {
        var rootHeaderCt;
        // Only traverse to get our view once.
        if (!this.view) {
            rootHeaderCt = this.getRootHeaderCt();
            if (rootHeaderCt) {
                this.view = rootHeaderCt.view;
            }
        }
        return this.view;
    },
    onFocusLeave: function(e) {
        this.callParent([
            e
        ]);
        if (this.activeMenu) {
            this.activeMenu.hide();
        }
    },
    initItems: function() {
        var me = this;
        me.callParent(arguments);
        if (me.isGroupHeader) {
            // We need to hide the groupheader straightaway if it's configured as hidden
            // or all its children are.
            if (me.config.hidden || !me.hasVisibleChildColumns()) {
                me.hide();
            }
        }
    },
    hasVisibleChildColumns: function() {
        var items = this.items.items,
            len = items.length,
            i, item;
        for (i = 0; i < len; ++i) {
            item = items[i];
            if (item.isColumn && !item.hidden) {
                return true;
            }
        }
        return false;
    },
    onAdd: function(child) {
        var me = this;
        if (child.isColumn) {
            child.isSubHeader = true;
            child.addCls(me.groupSubHeaderCls);
        }
        if (me.isGroupHeader && me.hidden && me.hasVisibleChildColumns()) {
            me.show();
        }
        me.callParent([
            child
        ]);
    },
    onRemove: function(child, isDestroying) {
        var me = this;
        if (child.isSubHeader) {
            child.isSubHeader = false;
            child.removeCls(me.groupSubHeaderCls);
        }
        me.callParent([
            child,
            isDestroying
        ]);
        // By this point, the component will be removed from the items collection.
        //
        // Note that we don't want to remove any grouped headers that have a descendant
        // that is currently the drag target of an even lower stacked grouped header.
        // See the comments in Ext.grid.header.Container#isNested.
        if (!(me.destroyed || me.destroying) && !me.hasVisibleChildColumns() && (me.ownerCt && !me.ownerCt.isNested())) {
            me.hide();
        }
    },
    initRenderData: function() {
        var me = this,
            tipMarkup = '',
            tip = me.tooltip,
            text = me.text,
            attr = me.tooltipType === 'qtip' ? 'data-qtip' : 'title';
        if (!Ext.isEmpty(tip)) {
            tipMarkup = attr + '="' + tip + '" ';
        }
        return Ext.applyIf(me.callParent(arguments), {
            text: text,
            empty: me.isEmptyText(text),
            menuDisabled: me.menuDisabled,
            tipMarkup: tipMarkup,
            triggerStyle: this.getTriggerVisible() ? 'display:block' : ''
        });
    },
    applyColumnState: function(state, storeState) {
        var me = this,
            sorter = me.getSorter(),
            stateSorters = storeState && storeState.sorters,
            len, i, savedSorter, mySorterId;
        // If we have been configured with a sorter, then there SHOULD be a sorter config
        // in the storeState with a corresponding ID from which we must restore our sorter's state.
        // (The only state we can restore is direction).
        // Then we replace the state entry with the real sorter. We MUST do this because the sorter
        // is likely to have a custom sortFn.
        if (sorter && stateSorters && (len = stateSorters.length)) {
            mySorterId = sorter.getId();
            for (i = 0; !savedSorter && i < len; i++) {
                if (stateSorters[i].id === mySorterId) {
                    sorter.setDirection(stateSorters[i].direction);
                    stateSorters[i] = sorter;
                    break;
                }
            }
        }
        // apply any columns
        me.applyColumnsState(state.columns);
        // Only state properties which were saved should be restored.
        // (Only user-changed properties were saved by getState)
        if (state.hidden != null) {
            me.hidden = state.hidden;
        }
        if (state.locked != null) {
            me.locked = state.locked;
        }
        if (state.sortable != null) {
            me.sortable = state.sortable;
        }
        if (state.width != null) {
            me.flex = null;
            me.width = state.width;
        } else if (state.flex != null) {
            me.width = null;
            me.flex = state.flex;
        }
    },
    getColumnState: function() {
        var me = this,
            items = me.items.items,
            state = {
                id: me.getStateId()
            };
        me.savePropsToState([
            'hidden',
            'sortable',
            'locked',
            'flex',
            'width'
        ], state);
        // Check for the existence of items, since column.Action won't have them
        if (me.isGroupHeader && items && items.length) {
            state.columns = me.getColumnsState();
        }
        if ('width' in state) {
            delete state.flex;
        }
        // width wins
        return state;
    },
    /**
     * Sets the header text for this Column.
     * @param {String} text The header to display on this Column.
     */
    setText: function(text) {
        var me = this,
            grid;
        me.text = text;
        if (me.rendered) {
            grid = me.getView().ownerGrid;
            me.textInnerEl.setHtml(text);
            me.titleEl.toggleCls(Ext.baseCSSPrefix + 'column-header-inner-empty', me.isEmptyText(text));
            grid.syncHeaderVisibility();
        }
    },
    /**
     * Returns the index of this column only if this column is a base level Column. If it
     * is a group column, it returns `false`.
     * @return {Number}
     */
    getIndex: function() {
        return this.isGroupColumn ? false : this.getRootHeaderCt().getHeaderIndex(this);
    },
    /**
     * Returns the index of this column in the list of *visible* columns only if this column
     * is a base level Column. If it is a group column, it returns `false`.
     * @return {Number}
     */
    getVisibleIndex: function() {
        // Note that the visibleIndex property is assigned by the owning HeaderContainer
        // when assembling the visible column set for the view.
        // eslint-disable-next-line max-len
        return this.visibleIndex != null ? this.visibleIndex : this.isGroupColumn ? false : Ext.Array.indexOf(this.getRootHeaderCt().getVisibleGridColumns(), this);
    },
    getLabelChain: function() {
        var child = this,
            labels = [],
            parent;
        while ((parent = child.up('headercontainer'))) {
            if (parent.text) {
                labels.unshift(Ext.util.Format.stripTags(parent.text));
            }
            child = parent;
        }
        return labels;
    },
    beforeRender: function() {
        var me = this,
            rootHeaderCt = me.getRootHeaderCt(),
            isSortable = me.isSortable(),
            labels = [],
            ariaAttr;
        me.textAlign = me.getMappedAlignment(me.getAlign());
        me.callParent();
        // Disable the menu if there's nothing to show in the menu, ie:
        // Column cannot be sorted, grouped or locked, and there are no grid columns
        // which may be hidden
        if (!me.requiresMenu && !isSortable && !me.groupable && !me.lockable && (rootHeaderCt.grid.enableColumnHide === false || !rootHeaderCt.getHideableColumns().length)) {
            me.menuDisabled = true;
        }
        // Wrapping text may cause unpredictable line heights.
        // variableRowHeight is interrogated by the View for all visible columns to determine
        // whether addition of new rows should cause an ExtJS layout.
        // The View's summation of the presence of visible variableRowHeight columns is also used by
        // any buffered renderer to determine how row height should be calculated when determining
        // scroll range.
        if (me.cellWrap) {
            me.variableRowHeight = true;
        }
        ariaAttr = me.ariaRenderAttributes || (me.ariaRenderAttributes = {});
        // Ext JS does not support editable column headers
        ariaAttr['aria-readonly'] = true;
        if (isSortable) {
            ariaAttr['aria-sort'] = me.ariaSortStates[me.sortState];
        }
        if (me.isSubHeader) {
            labels = me.getLabelChain();
            if (me.text) {
                labels.push(Ext.util.Format.stripTags(me.text));
            }
            if (labels.length) {
                ariaAttr['aria-label'] = labels.join(' ');
            }
        }
        me.protoEl.unselectable();
    },
    getTriggerElWidth: function() {
        var me = this,
            triggerEl = me.triggerEl,
            width = me.self.triggerElWidth;
        if (triggerEl && width === undefined) {
            triggerEl.setStyle('display', 'block');
            width = me.self.triggerElWidth = triggerEl.getWidth();
            triggerEl.setStyle('display', '');
        }
        return width;
    },
    afterComponentLayout: function(width, height, oldWidth, oldHeight) {
        var me = this,
            rootHeaderCt = me.getRootHeaderCt(),
            savedFlex = me.savedFlex;
        me.callParent([
            width,
            height,
            oldWidth,
            oldHeight
        ]);
        if (rootHeaderCt && (oldWidth != null || me.flex) && width !== oldWidth) {
            rootHeaderCt.onHeaderResize(me, width);
        }
        if (savedFlex) {
            me.flex = savedFlex;
            delete me.savedFlex;
        }
    },
    doDestroy: function() {
        // force destroy on the textEl, IE reports a leak
        Ext.destroy(this.field, this.editor);
        this.callParent();
    },
    onTitleMouseOver: function() {
        this.titleEl.addCls(this.hoverCls);
    },
    onTitleMouseOut: function() {
        this.titleEl.removeCls(this.hoverCls);
    },
    onDownKey: function(e) {
        if (this.triggerEl) {
            this.onTitleElClick(e, this.triggerEl.dom || this.el.dom);
        }
    },
    onEnterKey: function(e) {
        this.onTitleElClick(e, this.el.dom);
    },
    /**
     * @private
     * Double click handler which, if on left or right edges, auto-sizes the column to the left.
     * @param e The dblclick event
     */
    onTitleElDblClick: function(e) {
        var me = this,
            prev, leafColumns, headerCt;
        // On left edge, resize previous *leaf* column in the grid
        if (me.isAtStartEdge(e)) {
            // Look for the previous visible column header which is a leaf
            // Note: previousNode can walk out of the container (this may be first child of a group)
            prev = me.previousNode('gridcolumn:not([hidden]):not([isGroupHeader])');
            // If found in the same grid, auto-size it
            if (prev && prev.getRootHeaderCt() === me.getRootHeaderCt()) {
                prev.autoSize();
            }
        }
        // On right edge, resize this column, or last sub-column within it
        else if (me.isAtEndEdge(e)) {
            // Click on right but in child container - auto-size last leaf column
            if (me.isGroupHeader && e.getPoint().isContainedBy(me.layout.innerCt)) {
                leafColumns = me.query('gridcolumn:not([hidden]):not([isGroupHeader])');
                leafColumns[leafColumns.length - 1].autoSize();
                return;
            } else {
                headerCt = me.getRootHeaderCt();
                // Cannot resize the only column in a forceFit grid.
                if (headerCt.visibleColumnManager.getColumns().length === 1 && headerCt.forceFit) {
                    return;
                }
            }
            me.autoSize();
        }
    },
    /**
     * Sizes this Column to fit the max content width.
     * *Note that group columns shrink-wrap around the size of leaf columns. Auto sizing
     * a group column auto-sizes descendant leaf columns.*
     */
    autoSize: function() {
        var me = this,
            leafColumns, numLeaves, i, headerCt;
        if (me.resizable) {
            // Group headers are shrinkwrap width, so auto-sizing one means auto-sizing leaf
            // descendants.
            if (me.isGroupHeader) {
                leafColumns = me.query('gridcolumn:not([hidden]):not([isGroupHeader])');
                numLeaves = leafColumns.length;
                headerCt = me.getRootHeaderCt();
                Ext.suspendLayouts();
                for (i = 0; i < numLeaves; i++) {
                    headerCt.autoSizeColumn(leafColumns[i]);
                }
                Ext.resumeLayouts(true);
                return;
            }
            me.getRootHeaderCt().autoSizeColumn(me);
        }
    },
    isEmptyText: function(text, visual) {
        // visual means if there's no visual information, so even &npsb; and other hard spaces are
        // reported as empty. This is used to determine whether we should hideHeaders.
        if (visual) {
            return Ext.String.trim(text).length === 0;
        } else // Non visual means there's really nothing there to shape the container.
        // So null and empty string is empty, but "hard" spaces like '\u00a0' are content.
        // This is to determine whether the "text is empty" CSS class should be applied.
        {
            return text == null || text === '';
        }
    },
    onTitleElClick: function(e, t, sortOnClick) {
        var me = this,
            activeHeader, prevSibling, tapMargin;
        // Tap on the resize zone triggers the menu
        if (e.pointerType === 'touch') {
            prevSibling = me.previousSibling(':not([hidden])');
            // Tap on right edge, activate this header
            if (!me.menuDisabled) {
                tapMargin = parseInt(me.triggerEl.getStyle('width'), 10);
                // triggerEl can have width: auto, in which case we use handle width * 3
                // that yields 30px for touch events. Should be enough in most cases.
                if (isNaN(tapMargin)) {
                    tapMargin = me.getHandleWidth(e) * 3;
                }
                if (me.isAtEndEdge(e, tapMargin)) {
                    activeHeader = me;
                }
            }
            // Tap on left edge, activate previous header
            if (!activeHeader && prevSibling && !prevSibling.menuDisabled && me.isAtStartEdge(e)) {
                activeHeader = prevSibling;
            }
        } else {
            // Firefox doesn't check the current target in a within check.
            // Therefore we check the target directly and then within (ancestors)
            // eslint-disable-next-line max-len
            activeHeader = me.triggerEl && (e.target === me.triggerEl.dom || t === me.triggerEl || e.within(me.triggerEl)) ? me : null;
        }
        // If it's not a click on the trigger or extreme edges.
        // Or if we are called from a key handler, sort this column.
        if (sortOnClick !== false && (!activeHeader && !me.isAtStartEdge(e) && !me.isAtEndEdge(e) || e.getKey())) {
            me.toggleSortState();
        }
        return activeHeader;
    },
    /**
     * @private
     * Process UI events from the view. The owning TablePanel calls this method, relaying events
     * from the TableView
     * @param {String} type Event type, eg 'click'
     * @param {Ext.view.Table} view TableView Component
     * @param {HTMLElement} cell Cell HTMLElement the event took place within
     * @param {Number} recordIndex Index of the associated Store Model (-1 if none)
     * @param {Number} cellIndex Cell index within the row
     * @param {Ext.event.Event} e Original event
     */
    processEvent: function(type, view, cell, recordIndex, cellIndex, e) {
        return this.fireEvent.apply(this, arguments);
    },
    isSortable: function() {
        var rootHeader = this.getRootHeaderCt(),
            grid = rootHeader ? rootHeader.grid : null,
            sortable = this.sortable;
        if (grid && grid.sortableColumns === false) {
            sortable = false;
        }
        return sortable;
    },
    toggleSortState: function() {
        if (this.isSortable()) {
            this.sort();
        }
    },
    sort: function(direction) {
        var me = this,
            grid = me.up('tablepanel'),
            store = grid.store,
            storeIsSorted = store.isSorted(),
            storeSorters = storeIsSorted && store.getSorters(),
            sorter = me.getSorter(),
            idx = storeSorters && storeSorters.indexOf(sorter),
            currentDirection;
        // Maintain backward compatibility.
        // If the grid is NOT configured with multi column sorting, then specify "replace".
        // Only if we are doing multi column sorting do we insert it as one of a multi set.
        // Suspend layouts in case multiple views depend upon this grid's store
        // (eg lockable assemblies)
        Ext.suspendLayouts();
        if (sorter) {
            currentDirection = sorter.getDirection();
            if (!direction || currentDirection !== direction || !storeIsSorted || idx === -1) {
                // when the store is not being sorted by the current sorter, we need to manually 
                // update the direction because the store will not take care of it.
                if ((!storeIsSorted || idx === -1) && currentDirection !== direction) {
                    sorter.setDirection(direction);
                }
                store.sort(sorter, grid.multiColumnSort ? 'multi' : 'replace');
            }
        } else {
            store.sort(me.getSortParam(), direction, grid.multiColumnSort ? 'multi' : 'replace');
        }
        Ext.resumeLayouts(true);
    },
    /**
     * Returns the parameter to sort upon when sorting this header.
     * By default this returns the dataIndex and will not need to be overridden in most cases.
     * @return {String}
     */
    getSortParam: function() {
        return this.dataIndex;
    },
    setSortState: function(sorter) {
        // Set the UI state to reflect the state of any passed Sorter
        // Called by the grid's HeaderContainer on view refresh
        var me = this,
            direction = sorter && sorter.getDirection(),
            ascCls = me.ascSortCls,
            descCls = me.descSortCls,
            rootHeaderCt = me.getRootHeaderCt(),
            ariaDom = me.ariaEl.dom,
            changed;
        switch (direction) {
            case 'DESC':
                if (!me.hasCls(descCls)) {
                    me.addCls(descCls);
                    me.sortState = 'DESC';
                    changed = true;
                };
                me.removeCls(ascCls);
                break;
            case 'ASC':
                if (!me.hasCls(ascCls)) {
                    me.addCls(ascCls);
                    me.sortState = 'ASC';
                    changed = true;
                };
                me.removeCls(descCls);
                break;
            default:
                me.removeCls([
                    ascCls,
                    descCls
                ]);
                me.sortState = null;
                break;
        }
        if (ariaDom) {
            if (me.sortState) {
                ariaDom.setAttribute('aria-sort', me.ariaSortStates[me.sortState]);
            } else {
                ariaDom.removeAttribute('aria-sort');
            }
        }
        // we only want to fire the event if we have actually sorted
        if (changed) {
            rootHeaderCt.fireEvent('sortchange', rootHeaderCt, me, direction);
        }
    },
    /**
     * Determines whether the UI should be allowed to offer an option to hide this column.
     *
     * A column may *not* be hidden if to do so would leave the grid with no visible columns.
     *
     * This is used to determine the enabled/disabled state of header hide menu items.
     */
    isHideable: function() {
        var result = {
                hideCandidate: this,
                result: this.hideable
            };
        if (result.result) {
            this.ownerCt.bubble(this.hasOtherMenuEnabledChildren, null, [
                result
            ]);
        }
        return result.result;
    },
    hasOtherMenuEnabledChildren: function(result) {
        // Private bubble function used in determining whether this column is hideable.
        // Executes in the scope of each component in the bubble sequence
        var visibleChildren, count;
        // If we've bubbled out the top of the topmost HeaderContainer without finding a level
        // with at least one visible, menu-enabled child *which is not the hideCandidate*, no hide!
        if (!this.isXType('headercontainer')) {
            result.result = false;
            return false;
        }
        // If we find an ancestor level with at least one visible, menu-enabled child
        // *which is not the hideCandidate*, then the hideCandidate is hideable.
        // Note that we are not using CQ #id matchers - ':not(#' + result.hideCandidate.id + ')' -
        // to exclude the hideCandidate because CQ queries are cached for the document's lifetime.
        visibleChildren = this.query('>gridcolumn:not([hidden]):not([menuDisabled])');
        count = visibleChildren.length;
        if (Ext.Array.contains(visibleChildren, result.hideCandidate)) {
            count--;
        }
        if (count) {
            return false;
        }
        // If we go up, it's because the hideCandidate was the only hideable child,
        // so *this* becomes the hide candidate.
        result.hideCandidate = this;
    },
    /**
     * Determines whether the UI should be allowed to offer an option to lock or unlock this column.
     * Note that this includes dragging a column into the opposite side of a
     * {@link Ext.panel.Table#enableLocking lockable} grid.
     *
     * A column may *not* be moved from one side to the other of a
     * {@link Ext.panel.Table#enableLocking lockable} grid if to do so would leave one side
     * with no visible columns.
     *
     * This is used to determine the enabled/disabled state of the lock/unlock
     * menu item used in {@link Ext.panel.Table#enableLocking lockable} grids, and to determine
     * droppabilty when dragging a header.
     */
    isLockable: function() {
        var result = {
                result: this.lockable !== false
            };
        if (result.result) {
            this.ownerCt.bubble(this.hasMultipleVisibleChildren, null, [
                result
            ]);
        }
        return result.result;
    },
    /**
     * Determines whether this column is in the locked side of a grid.
     * It may be a descendant node of a locked column and as such will *not* have the
     * {@link #locked} flag set.
     */
    isLocked: function() {
        if (this.locked == null) {
            this.locked = this.getInherited().inLockedGrid;
        }
        return this.locked;
    },
    hasMultipleVisibleChildren: function(result) {
        // Private bubble function used in determining whether this column is lockable.
        // Executes in the scope of each component in the bubble sequence
        // If we've bubbled out the top of the topmost HeaderContainer
        // without finding a level with more than one visible child, no hide!
        if (!this.isXType('headercontainer')) {
            result.result = false;
            return false;
        }
        // If we find an ancestor level with more than one visible child, it's fine to hide
        if (this.query('>gridcolumn:not([hidden])').length > 1) {
            return false;
        }
    },
    hide: function() {
        var me = this,
            rootHeaderCt = me.getRootHeaderCt(),
            owner = me.getRefOwner();
        // During object construction, so just set the hidden flag and jump out
        if (owner.constructing) {
            me.callParent();
            return me;
        }
        if (me.rendered && !me.isVisible()) {
            // Already hidden
            return me;
        }
        // Save our last shown width so we can gain space when shown back into fully flexed
        // HeaderContainer. If we are, say, flex: 1 and all others are fixed width,
        // then removing will do a layout which will convert all widths to flexes
        // which will mean this flex value is too small.
        if (rootHeaderCt.forceFit) {
            me.visibleSiblingCount = rootHeaderCt.getVisibleGridColumns().length - 1;
            if (me.flex) {
                me.savedWidth = me.getWidth();
                me.flex = null;
            }
        }
        rootHeaderCt.beginChildHide();
        Ext.suspendLayouts();
        // owner is a group, hide call didn't come from the owner
        if (owner.isGroupHeader) {
            // The owner only has one item that isn't hidden and it's me; hide the owner.
            if (me.isNestedGroupHeader()) {
                owner.hide();
            }
            if (me.isSubHeader && !me.isGroupHeader && owner.query('>gridcolumn:not([hidden])').length === 1) {
                owner.lastHiddenHeader = me;
            }
        }
        me.callParent();
        // Notify owning HeaderContainer. Will trigger a layout and a view refresh.
        rootHeaderCt.endChildHide();
        rootHeaderCt.onHeaderHide(me);
        Ext.resumeLayouts(true);
        return me;
    },
    show: function() {
        var me = this,
            rootHeaderCt = me.getRootHeaderCt(),
            ownerCt = me.getRefOwner();
        if (me.isVisible()) {
            return me;
        }
        if (ownerCt.isGroupHeader) {
            ownerCt.lastHiddenHeader = null;
        }
        if (me.rendered) {
            // Size all other columns to accommodate re-shown column.
            if (rootHeaderCt.forceFit) {
                rootHeaderCt.applyForceFit(me);
            }
        }
        Ext.suspendLayouts();
        // If a sub header, ensure that the group header is visible
        if (me.isSubHeader && ownerCt.hidden) {
            ownerCt.show(false, true);
        }
        me.callParent(arguments);
        if (me.isGroupHeader) {
            me.maybeShowNestedGroupHeader();
        }
        // Notify owning HeaderContainer. Will trigger a layout and a view refresh.
        ownerCt = me.getRootHeaderCt();
        if (ownerCt) {
            ownerCt.onHeaderShow(me);
        }
        Ext.resumeLayouts(true);
        return me;
    },
    /**
     * @private
     * Decides whether the column needs updating
     * @return {Number} 0 = Doesn't need update.
     * 1 = Column needs update, and renderer has > 1 argument; We need to render
     * a whole new HTML item.
     * 2 = Column needs update, but renderer has 1 argument or column uses an updater.
     */
    shouldUpdateCell: function(record, changedFieldNames) {
        var len, i, field;
        // If the column has a renderer which peeks and pokes at other data,
        // return 1 which means that a whole new TableView item must be rendered.
        //
        // Note that widget columns shouldn't ever be updated.
        if (!this.preventUpdate) {
            if (this.hasCustomRenderer) {
                return 1;
            }
            // If there is a changed field list, and it's NOT a custom column renderer
            // (meaning it doesn't peek at other data, but just uses the raw field value),
            // we only have to update it if the column's field is among those changes.
            if (changedFieldNames) {
                len = changedFieldNames.length;
                for (i = 0; i < len; ++i) {
                    field = changedFieldNames[i];
                    if (field === this.dataIndex || field === record.idProperty) {
                        return 2;
                    }
                }
            } else {
                return 2;
            }
        }
    },
    getCellWidth: function() {
        var me = this,
            result;
        if (me.rendered && me.componentLayout && me.componentLayout.lastComponentSize) {
            // headers always have either a width or a flex
            // because HeaderContainer sets a defaults width
            // therefore we can ignore the natural width
            // we use the componentLayout's tracked width so that
            // we can calculate the desired width when rendered
            // but not visible because its being obscured by a layout
            result = me.componentLayout.lastComponentSize.width;
        } else if (me.width) {
            result = me.width;
        }
        // This is a group header.
        // Use getTableWidth and remember that getTableWidth adjusts for column lines and box model
        else if (!me.isColumn) {
            result = me.getTableWidth();
        }
        return result;
    },
    getCellId: function() {
        return Ext.baseCSSPrefix + 'grid-cell-' + this.getItemId();
    },
    getCellSelector: function() {
        var view;
        if (!this.cellSelector) {
            view = this.getView();
            // We must explicitly access the view's cell selector as well as this column's
            // own ID class because <col> elements are given this column's ID class.
            // If we are still atached to a view. If not, the identifying class will do.
            this.cellSelector = (view ? view.getCellSelector() : '') + '.' + this.getCellId();
        }
        return this.cellSelector;
    },
    getCellInnerSelector: function() {
        return this.getCellSelector() + ' .' + Ext.baseCSSPrefix + 'grid-cell-inner';
    },
    isAtStartEdge: function(e) {
        var offset = e.getXY()[0] - this.getX();
        // To the left of the first column, not over
        if (offset < 0 && this.getIndex() === 0) {
            return false;
        }
        return (offset < this.getHandleWidth(e));
    },
    isAtEndEdge: function(e, margin) {
        return (this.getX() + this.getWidth() - e.getXY()[0] <= (margin || this.getHandleWidth(e)));
    },
    getHandleWidth: function(e) {
        return e.pointerType === 'touch' ? 10 : 4;
    },
    setMenuActive: function(menu) {
        // Called when the column menu is activated/deactivated.
        // Change the UI to indicate active/inactive menu
        this.activeMenu = menu;
        this.titleEl[menu ? 'addCls' : 'removeCls'](this.headerOpenCls);
    },
    privates: {
        /**
         * @private
         * Mapping for locale-neutral align setting.
         * Overridden in Ext.rtl.grid.column.Column
         */
        _alignMap: {
            start: 'left',
            end: 'right'
        },
        /**
         * A method called by the render template to allow extra content after the header text.
         * @private
         */
        afterText: function(out, values) {
            if (this.dirtyText) {
                this.dirtyTextElementId = this.id + '-dirty-cell-text';
                out.push('<span id="' + this.dirtyTextElementId + '" class="' + Ext.baseCSSPrefix + 'hidden-offsets">' + this.dirtyText + '</span>');
            }
        },
        calculateHeaderId: function() {
            var me = this,
                ownerGrid, counterOwner, items, item, i, len;
            if (!me.headerId) {
                // Sequential header counter MUST be based on the top level grid
                // to avoid duplicates from sides of a lockable assembly.
                ownerGrid = me.up('tablepanel');
                if (!ownerGrid) {
                    return;
                }
                items = me.items.items;
                // Action column has items as an array, so skip out here.
                if (items) {
                    for (i = 0 , len = items.length; i < len; ++i) {
                        item = items[i];
                        if (item.isColumn) {
                            item.calculateHeaderId();
                        }
                    }
                }
                counterOwner = ownerGrid ? ownerGrid.ownerGrid : me.getRootHeaderCt();
                counterOwner.headerCounter = (counterOwner.headerCounter || 0) + 1;
                me.headerId = 'h' + counterOwner.headerCounter;
            }
            me.configureStateInfo();
        },
        getMappedAlignment: function(align) {
            return this._alignMap[align] || align;
        },
        configureStateInfo: function() {
            var me = this,
                sorter;
            // MUST stamp a stateId into this object; state application relies on
            // reading the property, NOT using the getter!
            // Only generate a stateId if it really needs one.
            if (!me.stateId) {
                // This was the headerId generated in 4.0, so to preserve saved state, we now
                // assign a default stateId in that same manner. The stateId's of a column are
                // not global at the stateProvider, but are local to the grid state data. The
                // headerId should still follow our standard naming convention.
                me.stateId = me.initialConfig.id || me.headerId;
            }
            sorter = me.getSorter();
            if (!me.hasSetSorter && sorter && !sorter.initialConfig.id) {
                if (me.dataIndex || me.stateId) {
                    sorter.setId((me.dataIndex || me.stateId) + '-sorter');
                    me.hasSetSorter = true;
                }
            }
        },
        onLock: function(header) {
            var items = this.items.items,
                len = items.length,
                i, item;
            for (i = 0; i < len; ++i) {
                item = items[i];
                if (item.isColumn) {
                    item.onLock(header);
                }
            }
        },
        onUnlock: function(header) {
            var items = this.items.items,
                len = items.length,
                i, item;
            for (i = 0; i < len; ++i) {
                item = items[i];
                if (item.isColumn) {
                    item.onUnlock(header);
                }
            }
        },
        shouldFlagCustomRenderer: function(renderer) {
            return renderer.length > 1;
        }
    },
    deprecated: {
        5: {
            methods: {
                bindRenderer: function(renderer) {
                    // This method restores the pre-5 meaning of "renderer" as a string:
                    // a method in Ext.util.Format. But at least we don't send all of
                    // the renderer arguments at the poor thing!
                    return function(value) {
                        return Ext.util.Format[renderer](value);
                    };
                }
            }
        }
    }
});
// intentionally omit getEditor and setEditor definitions bc we applyIf into columns
// when the editing plugin is injected
/**
     * @method getEditor
     * Retrieves the editing field for editing associated with this header.  If the
     * field has not been instantiated it will be created.
     *
     * **Note:** This method will only have an implementation if an Editing plugin has
     * been enabled on the grid ({@link Ext.grid.plugin.CellEditing cellediting} /
     * {@link Ext.grid.plugin.RowEditing rowediting}).
     *
     * @param {Object} [record] The {@link Ext.data.Model Model} instance being edited.
     * @param {Object/String} [defaultField] An xtype or config object for a
     * {@link Ext.form.field.Field Field} to be created as the default editor if it does
     * not already exist
     * @return {Ext.form.field.Field/Boolean} The editor field associated with
     * this column.  Returns false if there is no field associated with the
     * {@link Ext.grid.column.Column Column}.
     */
/**
     * @method setEditor
     * Sets the form field to be used for editing.
     *
     * **Note:** This method will only have an implementation if an Editing plugin has
     * been enabled on the grid ({@link Ext.grid.plugin.CellEditing cellediting} /
     * {@link Ext.grid.plugin.RowEditing rowediting}).
     *
     * @param {Object} field An object representing a field to be created.
     * If no xtype is specified a 'textfield' is assumed.
     */

/**
 * @private
 * A Class which encapsulates individual action items within an ActionColumn and
 * acts as a proxy for various Component methods to allow ActionColumn items to be 
 * manipulated en masse by the {@link Ext.Action}s used to create them.
 */
Ext.define('Ext.grid.column.ActionProxy', {
    constructor: function(column, item, itemIndex) {
        this.column = column;
        this.item = item;
        this.itemIndex = itemIndex;
    },
    setHandler: function(handler) {
        this.item.handler = handler;
    },
    setDisabled: function(disabled) {
        if (disabled) {
            this.column.disableAction(this.itemIndex);
        } else {
            this.column.enableAction(this.itemIndex);
        }
    },
    setIconCls: function(iconCls) {
        this.item.iconCls = iconCls;
        this.column.getView().refreshView();
    },
    setIconGlyph: function(glyph) {
        this.item.glyph = glyph;
        this.column.getView().refreshView();
    },
    setHidden: function(hidden) {
        this.item.hidden = hidden;
        this.column.getView().refreshView();
    },
    setVisible: function(visible) {
        this.setHidden(!visible);
    },
    on: function() {
        // Allow the Action to attach its destroy listener.
        return this.column.on.apply(this.column, arguments);
    }
});

/**
 * A Grid header type which renders an icon, or a series of icons in a grid cell, and offers
 * a scoped click handler for each icon.
 *
 *     @example
 *     // Init the singleton.  Any tag-based quick tips will start working.
 *     Ext.tip.QuickTipManager.init();
 *
 *     Ext.create('Ext.data.Store', {
 *         storeId:'employeeStore',
 *         fields:['firstname', 'lastname', 'seniority', 'dep', 'hired'],
 *         data:[
 *             {firstname:"Michael", lastname:"Scott"},
 *             {firstname:"Dwight", lastname:"Schrute"},
 *             {firstname:"Jim", lastname:"Halpert"},
 *             {firstname:"Kevin", lastname:"Malone"},
 *             {firstname:"Angela", lastname:"Martin"}
 *         ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Action Column Demo',
 *         store: Ext.data.StoreManager.lookup('employeeStore'),
 *         columns: [
 *             {text: 'First Name',  dataIndex:'firstname'},
 *             {text: 'Last Name',  dataIndex:'lastname'},
 *             {
 *                 xtype:'actioncolumn',
 *                 width:50,
 *                 items: [{
 *                     iconCls: 'x-fa fa-cog',
 *                     tooltip: 'Edit',
 *                     handler: function(grid, rowIndex, colIndex) {
 *                         var rec = grid.getStore().getAt(rowIndex);
 *                         alert("Edit " + rec.get('firstname'));
 *                     }
 *                 },{
 *                     icon: 'extjs-build/examples/restful/images/delete.png',
 *                     tooltip: 'Delete',
 *                     handler: function(grid, rowIndex, colIndex) {
 *                         var rec = grid.getStore().getAt(rowIndex);
 *                         alert("Terminate " + rec.get('firstname'));
 *                     }
 *                 }]
 *             }
 *         ],
 *         width: 250,
 *         renderTo: Ext.getBody()
 *     });
 *
 * The action column can be at any index in the columns array, and a grid can have any number of
 * action columns.
 */
Ext.define('Ext.grid.column.Action', {
    extend: 'Ext.grid.column.Column',
    alias: [
        'widget.actioncolumn'
    ],
    alternateClassName: 'Ext.grid.ActionColumn',
    requires: [
        'Ext.grid.column.ActionProxy',
        'Ext.util.Format',
        'Ext.Glyph'
    ],
    /**
     * @cfg glyph
     * @inheritdoc Ext.panel.Header#cfg-glyph
     * @since 6.2.0
     */
    /**
     * @cfg [icon=Ext#BLANK_IMAGE_URL]
     * @inheritdoc Ext.panel.Header#cfg-icon
     */
    /**
     * @cfg iconCls
     * @inheritdoc Ext.panel.Header#cfg-iconCls
     * @localdoc **Note:** To determine the class dynamically, configure the Column with
     * a `{@link #getClass}` function.
     */
    /**
     * @cfg {Function/String} handler
     * A function called when the icon is clicked.  The handler function is passed the
     * following params:
     * @param {Ext.view.Table} view The owning TableView
     * @param {Number} rowIndex The row index clicked on
     * @param {Number} colIndex The column index clicked on
     * @param {Object} item The clicked item (or this Column if multiple
     * {@link #cfg-items items} were not configured)
     * @param {Event} e The click event
     * @param {Ext.data.Model} record The Record underlying the clicked row
     * @param {HTMLElement} row The table row clicked upon
     * @controllable
     */
    /**
     * @cfg {Object} scope
     * The scope (`this` reference) in which the `{@link #handler}`, 
     * `{@link #getClass}`, `{@link #cfg-isActionDisabled}` and `{@link #getTip}` functions 
     * are executed.
     * Defaults to this Column.
     */
    /**
     * @cfg {String} tooltip
     * A tooltip message to be displayed on hover.
     * {@link Ext.tip.QuickTipManager#init Ext.tip.QuickTipManager} must have been initialized.
     *
     * The tooltip may also be determined on a row by row basis by configuring a
     * {@link #getTip} method.
     */
    /**
     * @cfg {Boolean} disabled
     * If true, the action will not respond to click events, and will be displayed semi-opaque.
     *
     * This Column may also be disabled on a row by row basis by configuring a
     * {@link #cfg-isActionDisabled} method.
     */
    /**
     * @cfg {Boolean} stopSelection
     * Prevent grid selection upon click.
     * Beware that if you allow for the selection to happen then the selection model will steal
     * focus from any possible floating window (like a message box) raised in the handler.
     * This will prevent closing the window when pressing the Escape button since it will no longer
     * contain a focused component.
     */
    stopSelection: true,
    /**
     * @cfg {Function} getClass
     * A function which returns the CSS class to apply to the icon image.
     * 
     * For information on using the icons provided in the SDK see 
     * {@link #cfg-iconCls iconCls}.  The function is passed the following params:
     * @param {Object} value The value of the column's configured field (if any)
     * @param {Object} metadata An object in which you may set the following attributes:
     * @param {String} metadata.css A CSS class name to add to the cell's TD element
     * @param {String} metadata.attr An HTML attribute definition string to apply to the
     * data container element *within* the table cell (e.g. 'style="color:red;"')
     * @param {Ext.data.Model} record The Record providing the data
     * @param {Number} rowIndex The row index
     * @param {Number} colIndex The column index
     * @param {Ext.data.Store} store The Store which is providing the data Model
     * @return {String} The CSS class
     *
     * @controllable
     */
    /**
     * @cfg {Function} isDisabled A function which determines whether the action item for any row
     * is disabled and returns `true` or `false`.
     *
     * @deprecated 6.5.0 Use {@link #isActionDisabled} instead. Will be removed in 7.0.
     *
     * @controllable
     */
    /**
     * @cfg {Function} isActionDisabled A function which determines whether the action
     * item for any row is disabled and returns `true` or `false`.
     * 
     * The function is passed the following params:
     * @param {Ext.view.Table} view The owning TableView.
     * @param {Number} rowIndex The row index.
     * @param {Number} colIndex The column index.
     * @param {Object} item The clicked item (or this Column if multiple
     * {@link #cfg-items items} were not configured).
     * @param {Ext.data.Model} record The Record underlying the row.
     * @return {Boolean} `true` or `false` indicating whether the action item is disabled
     *
     * @controllable
     */
    /**
     * @cfg {Function} getTip A function which returns the tooltip string for any row.
     * 
     * *Note*: Outside of an Ext.application() use of this config requires 
     * {@link Ext.tip.QuickTipManager#init} to be called.
     * 
     *     Ext.tip.QuickTipManager.init();
     *     
     *     Ext.create('Ext.data.Store', {
     *         storeId: 'employeeStore',
     *         fields: ['firstname', 'grade'],
     *         data: [{
     *             firstname: "Michael",
     *             grade: 50
     *         }, {
     *             firstname: "Dwight",
     *             grade: 100
     *         }]
     *     });
     *     
     *     Ext.create('Ext.grid.Panel', {
     *         title: 'Action Column Demo',
     *         store: Ext.data.StoreManager.lookup('employeeStore'),
     *         columns: [{
     *             text: 'First Name',
     *             dataIndex: 'firstname'
     *         }, {
     *             text: 'Last Name',
     *             dataIndex: 'grade'
     *         }, {
     *             xtype: 'actioncolumn',
     *             width: 50,
     *             icon: 'sample/icons/action-icons.png',
     *             getTip: function(value, metadata, record, row, col, store) {
     *                 var avg = store.average('grade'),
     *                     grade = record.get('grade');
     *     
     *                 if (grade < avg) {
     *                     metadata.tdCls = "below-average";
     *                 }
     *     
     *                 return grade > 70 ? 'Pass' : 'Fail';
     *             },
     *             handler: function(grid, rowIndex, colIndex) {
     *                 var rec = grid.getStore().getAt(rowIndex);
     *                 alert("Edit " + rec.get('firstname'));
     *             }
     *         }],
     *         width: 250,
     *         renderTo: document.body
     *     });
     * 
     * @param {Object} value The value of the column's configured field (if any).
     * @param {Object} metadata An object in which you may set the following attributes:
     * @param {String} metadata.tdCls A CSS class name to add to the cell's TD element.
     * 
     *     metadata.tdCls = "custom-cell-cls";
     * 
     * @param {String} metadata.tdAttr An HTML attribute definition string to apply to 
     * the data container element _within_ the table cell.
     * 
     *     metadata.tdCls = tdAttr = "*";
     *     // * see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
     *     // be aware that setting cell attributes may override the cell layout
     *     // provided by the framework
     * 
     * @param {String} metadata.tdStyle An inline style for the table cell
     * 
     *     metadata.tdStyle = "background-color:red;";
     * 
     * @param {Ext.data.Model} record The Record providing the data.
     * @param {Number} rowIndex The row index.
     * @param {Number} colIndex The column index.
     * @param {Ext.data.Store} store The Store which is providing the data Model.
     * @return {String} tip The tip text
     *
     * @controllable
     */
    /**
     * @cfg {Object[]} items
     * An Array which may contain multiple icon definitions, each element of which may contain:
     *
     * @cfg {String} items.icon The url of an image to display as the clickable element
     * in the column.
     *
     * @cfg {String} items.iconCls A CSS class to apply to the icon element. To 
     * determine the class dynamically, configure the item with a `getClass` function.
     *
     * @cfg {Number} items.tabIndex The tabIndex attribute value for the action item. If this
     * value is not defined, {@link #itemTabIndex} will be used instead.
     *
     * @cfg {String} items.ariaRole The ARIA role attribute value for the action item. If this
     * value is not defined, {@link #itemAriaRole} will be used instead.
     * 
     * For information on using the icons provided in the SDK see {@link #iconCls}.
     *
     * @cfg {Function} items.getClass A function which returns the CSS class to apply to the icon
     * image.
     * @cfg {Object} items.getClass.v The value of the column's configured field (if any).
     * @cfg {Object} items.getClass.metadata An object in which you may set the following
     * attributes:
     * @cfg {String} items.getClass.metadata.css A CSS class name to add to the cell's TD element.
     * @cfg {String} items.getClass.metadata.attr An HTML attribute definition string to apply
     * to the data container element _within_ the table cell (e.g. 'style="color:red;"').
     * @cfg {Ext.data.Model} items.getClass.r The Record providing the data.
     * @cfg {Number} items.getClass.rowIndex The row index.
     * @cfg {Number} items.getClass.colIndex The column index.
     * @cfg {Ext.data.Store} items.getClass.store The Store which is providing the data Model.
     *
     * @cfg {Function} items.handler A function called when the icon is clicked.
     * @cfg {Ext.view.Table} items.handler.view The owning TableView.
     * @cfg {Number} items.handler.rowIndex The row index clicked on.
     * @cfg {Number} items.handler.colIndex The column index clicked on.
     * @cfg {Object} items.handler.item The clicked item (or this Column if multiple
     * {@link #cfg-items} were not configured).
     * @cfg {Event} items.handler.e The click event.
     * @cfg {Ext.data.Model} items.handler.record The Record underlying the clicked row.
     * @cfg {HTMLElement} items.row The table row clicked upon.
     *
     * @cfg {Function} items.isDisabled A function which determines whether the action item
     * for any row is disabled and returns `true` or `false`. **This item property is deprecated**.
     * Use `isActionDisabled` instead.
     *
     * @cfg {Function} items.isActionDisabled A function which determines whether the action item
     * for any row is disabled and returns `true` or `false`.
     * @cfg {Ext.view.Table} items.isActionDisabled.view The owning TableView.
     * @cfg {Number} items.isActionDisabled.rowIndex The row index.
     * @cfg {Number} items.isActionDisabled.colIndex The column index.
     * @cfg {Object} items.isActionDisabled.item The clicked item (or this Column if multiple
     * {@link #cfg-items} were not configured).
     * @cfg {Ext.data.Model} items.isActionDisabled.record The Record underlying the row.
     *
     * @cfg {Function} items.getTip A function which returns the tooltip string for any row.
     * @cfg {Object} items.getTip.v The value of the column's configured field (if any).
     * @cfg {Object} items.getTip.metadata An object in which you may set the following attributes:
     * @cfg {String} items.getTip.metadata.css A CSS class name to add to the cell's TD element.
     * @cfg {String} items.getTip.metadata.attr An HTML attribute definition string to apply
     * to the data container element _within_ the table cell (e.g. 'style="color:red;"').
     * @cfg {Ext.data.Model} items.getTip.r The Record providing the data.
     * @cfg {Number} items.getTip.rowIndex The row index.
     * @cfg {Number} items.getTip.colIndex The column index.
     * @cfg {Ext.data.Store} items.getTip.store The Store which is providing the data Model.
     *
     * @cfg {Object} items.scope The scope (`this` reference) in which the `handler`, `getClass`,
     * `isActionDisabled` and `getTip` functions are executed. Fallback defaults are this Column's
     * configured scope, then this Column.
     *
     * @cfg {String} items.tooltip A tooltip message to be displayed on hover.
     * {@link Ext.tip.QuickTipManager#init Ext.tip.QuickTipManager} must have been initialized.
     *
     * The tooltip may also be determined on a row by row basis by configuring a `getTip` method.
     *
     * @cfg {Boolean} items.disabled If true, the action will not respond to click events,
     * and will be displayed semi-opaque.
     *
     * This item may also be disabled on a row by row basis by configuring an `isActionDisabled`
     * method.
     */
    /**
     * @property {Array} items
     * An array of action items copied from the configured {@link #cfg-items items} configuration.
     * Each will have an `enable` and `disable` method added which will enable and disable
     * the associated action, and update the displayed icon accordingly.
     */
    actionIdRe: new RegExp(Ext.baseCSSPrefix + 'action-col-(\\d+)'),
    /**
     * @cfg {String} altText
     * The alt text to use for the image element.
     */
    altText: '',
    /**
     * @cfg {String} menuText
     * The text to render in the column's visibility selection menu for this column. If
     * not specified, menuText will display {@link #text text's} value.
     */
    menuText: '<i>Actions</i>',
    /**
     * @cfg {Number} itemTabIndex
     * Default tabIndex attribute value for each action item.
     */
    itemTabIndex: 0,
    /**
     * @cfg {String} itemAriaRole
     * Default ARIA role for each action item.
     */
    itemAriaRole: 'button',
    /**
     * @property maskOnDisable
     * @inheritdoc
     * @localdoc On Action Columns the masking applies to the action or actions 
     * themselves
     */
    maskOnDisable: false,
    // Disable means the action(s)
    /**
     * @cfg ignoreExport
     * @inheritdoc
     */
    ignoreExport: true,
    /**
     * @cfg sortable
     * @inheritdoc
     */
    sortable: false,
    innerCls: Ext.baseCSSPrefix + 'grid-cell-inner-action-col',
    actionIconCls: Ext.baseCSSPrefix + 'action-col-icon',
    constructor: function(config) {
        var me = this,
            cfg = Ext.apply({}, config),
            // Items may be defined on the prototype
            items = cfg.items || me.items || [
                me
            ],
            hasGetClass, i, len, item;
        me.origRenderer = cfg.renderer || me.renderer;
        me.origScope = cfg.scope || me.scope;
        me.renderer = me.scope = cfg.renderer = cfg.scope = null;
        // This is a Container. Delete the items config to be reinstated after construction.
        cfg.items = null;
        me.callParent([
            cfg
        ]);
        // isDisabled property conflicts with isDisabled predicate method on Component.
        // We provide backward compatibility but strongly discourage using `isDisabled`
        // as a property.
        // TODO Remove in 7.0
        if (me.hasOwnProperty('isDisabled')) {
            Ext.log.warn('[Ext.grid.column.Action] The isDisabled config is deprecated. ' + 'Use isActionDisabled to avoid conflict with Ext.Component#isDisabled().');
            me.isActionDisabled = me.isDisabled;
            delete me.isDisabled;
        }
        // Items is an array property of ActionColumns
        me.items = items;
        for (i = 0 , len = items.length; i < len; ++i) {
            item = items[i];
            if (item.substr && item[0] === '@') {
                item = me.getAction(item.substr(1));
            }
            // TODO Remove in 7.0
            if (item.hasOwnProperty('isDisabled')) {
                Ext.log.warn('[Ext.grid.column.Action] The isDisabled config is deprecated. ' + 'Use isActionDisabled to avoid conflict with Ext.Component#isDisabled().');
                item.isActionDisabled = item.isDisabled;
                delete item.isDisabled;
            }
            if (item.isAction) {
                items[i] = item.initialConfig;
                // Register an ActinoProxy as a Component with the Action.
                // Action methods will be relayed down into the targeted item set.
                item.addComponent(new Ext.grid.column.ActionProxy(me, items[i], i));
            }
            if (item.getClass) {
                hasGetClass = true;
            }
        }
        // Also need to check for getClass, since it changes how the cell renders
        if (me.origRenderer || hasGetClass) {
            me.hasCustomRenderer = true;
        }
    },
    initComponent: function() {
        var me = this;
        me.callParent();
        if (me.sortable && !me.dataIndex) {
            me.sortable = false;
        }
    },
    // Renderer closure iterates through items creating an <img> element for each
    // and tagging with an identifying class name x-action-col-{n}
    defaultRenderer: function(v, cellValues, record, rowIdx, colIdx, store, view) {
        var me = this,
            scope = me.origScope || me,
            items = me.items,
            len = items.length,
            i, item, ret, disabled, tooltip, altText, icon, glyph, tabIndex, ariaRole;
        // Allow a configured renderer to create initial value (And set the other values 
        // in the "metadata" argument!)
        // Assign a new variable here, since if we modify "v" it will also modify the arguments
        // collection, meaning we will pass an incorrect value to getClass/getTip
        ret = Ext.isFunction(me.origRenderer) ? me.origRenderer.apply(scope, arguments) || '' : '';
        cellValues.tdCls += ' ' + Ext.baseCSSPrefix + 'action-col-cell';
        /* eslint-disable max-len */
        for (i = 0; i < len; i++) {
            item = items[i];
            icon = item.icon;
            glyph = item.glyph;
            disabled = item.disabled || (item.isActionDisabled ? Ext.callback(item.isActionDisabled, item.scope || me.origScope, [
                view,
                rowIdx,
                colIdx,
                item,
                record
            ], 0, me) : false);
            tooltip = item.tooltip || (item.getTip ? Ext.callback(item.getTip, item.scope || me.origScope, arguments, 0, me) : null);
            altText = item.getAltText ? Ext.callback(item.getAltText, item.scope || me.origScope, arguments, 0, me) : item.altText || me.altText;
            // Only process the item action setup once.
            if (!item.hasActionConfiguration) {
                // Apply our documented default to all items
                item.stopSelection = me.stopSelection;
                item.disable = Ext.Function.bind(me.disableAction, me, [
                    i
                ], 0);
                item.enable = Ext.Function.bind(me.enableAction, me, [
                    i
                ], 0);
                item.hasActionConfiguration = true;
            }
            // If the ActionItem is using a glyph, convert it to an Ext.Glyph instance so we can extract the data easily.
            if (glyph) {
                glyph = Ext.Glyph.fly(glyph);
            }
            // Pull in tabIndex and ariarRols from item, unless the item is this, in which case
            // that would be wrong, and the icon would get column header values.
            tabIndex = (item !== me && item.tabIndex !== undefined) ? item.tabIndex : me.itemTabIndex;
            ariaRole = (item !== me && item.ariaRole !== undefined) ? item.ariaRole : me.itemAriaRole;
            ret += '<' + (icon ? 'img' : 'div') + (typeof tabIndex === 'number' ? ' tabIndex="' + tabIndex + '"' : '') + (ariaRole ? ' role="' + ariaRole + '"' : ' role="presentation"') + (icon ? (' alt="' + altText + '" src="' + item.icon + '"') : '') + ' class="' + me.actionIconCls + ' ' + Ext.baseCSSPrefix + 'action-col-' + String(i) + ' ' + (disabled ? me.disabledCls + ' ' : ' ') + (item.hidden ? Ext.baseCSSPrefix + 'hidden-display ' : '') + (item.getClass ? Ext.callback(item.getClass, item.scope || me.origScope, arguments, undefined, me) : (item.iconCls || me.iconCls || '')) + '"' + (tooltip ? ' data-qtip="' + Ext.util.Format.htmlEncode(tooltip) + '"' : '') + (icon ? '/>' : glyph ? (' style="font-family:' + glyph.fontFamily + '">' + glyph.character + '</div>') : '></div>');
        }
        /* eslint-enable max-len */
        return ret;
    },
    updater: function(cell, value, record, view, dataSource) {
        var cellValues = {};
        Ext.fly(cell).addCls(cellValues.tdCls).down(this.getView().innerSelector, true).innerHTML = this.defaultRenderer(value, cellValues, record, null, null, dataSource, view);
    },
    /**
     * Enables this ActionColumn's action at the specified index.
     * @param {Number/Ext.grid.column.Action} index
     * @param {Boolean} [silent=false]
     */
    enableAction: function(index, silent) {
        var me = this;
        if (!index) {
            index = 0;
        } else if (!Ext.isNumber(index)) {
            index = Ext.Array.indexOf(me.items, index);
        }
        me.items[index].disabled = false;
        me.up('tablepanel').el.select('.' + Ext.baseCSSPrefix + 'action-col-' + index).removeCls(me.disabledCls);
        if (!silent) {
            me.fireEvent('enable', me);
        }
    },
    /**
     * Disables this ActionColumn's action at the specified index.
     * @param {Number/Ext.grid.column.Action} index
     * @param {Boolean} [silent=false]
     */
    disableAction: function(index, silent) {
        var me = this;
        if (!index) {
            index = 0;
        } else if (!Ext.isNumber(index)) {
            index = Ext.Array.indexOf(me.items, index);
        }
        me.items[index].disabled = true;
        me.up('tablepanel').el.select('.' + Ext.baseCSSPrefix + 'action-col-' + index).addCls(me.disabledCls);
        if (!silent) {
            me.fireEvent('disable', me);
        }
    },
    doDestroy: function() {
        // Action column items property is an array, unlike the normal Container's MixedCollection.
        // If we don't null it here, parent doDestroy() can blow up.
        this.renderer = this.items = null;
        return this.callParent();
    },
    /**
     * @private
     * Process and re-fire events routed from the Ext.panel.Table's processEvent method.
     * Also fires any configured click handlers. By default, cancels the mousedown event
     * to prevent selection.
     * Returns the event handler's status to allow canceling of GridView's bubbling process.
     */
    processEvent: function(type, view, cell, recordIndex, cellIndex, e, record, row) {
        var me = this,
            target = e.getTarget(),
            key = type === 'keydown' && e.getKey(),
            match, item, disabled,
            cellFly = Ext.fly(cell),
            oldActiveEl;
        // Flag event to tell SelectionModel not to process it.
        e.stopSelection = !key && me.stopSelection;
        // If the target was not within a cell (ie it's a keydown event from the View), then
        // IF there's only one action icon, action it. If there is more than one, the user must
        // invoke actionable mode to navigate into the cell.
        if (key && (target === cell || !cellFly.contains(target))) {
            target = cellFly.query('.' + me.actionIconCls, true);
            if (target.length === 1) {
                target = target[0];
            } else {
                return;
            }
        }
        // NOTE: The statement below tests the truthiness of an assignment.
        if (target && (match = target.className.match(me.actionIdRe))) {
            item = me.items[parseInt(match[1], 10)];
            // eslint-disable-next-line max-len
            disabled = item.disabled || (item.isActionDisabled ? Ext.callback(item.isActionDisabled, item.scope || me.origScope, [
                view,
                recordIndex,
                cellIndex,
                item,
                record
            ], 0, me) : false);
            if (item && !disabled) {
                // Do not allow focus to follow from this mousedown unless the grid
                // is already in actionable mode
                if (type === 'mousedown' && !me.getView().actionableMode) {
                    e.preventDefault();
                } else if (type === 'click' || (key === e.ENTER || key === e.SPACE)) {
                    oldActiveEl = Ext.Element.getActiveElement();
                    Ext.callback(item.handler || me.handler, item.scope || me.origScope, [
                        view,
                        recordIndex,
                        cellIndex,
                        item,
                        e,
                        record,
                        row
                    ], undefined, me);
                    // Handler could possibly destroy the grid, so check we're still available.
                    if (view.destroyed) {
                        return false;
                    } else {
                        // If the record was deleted by the handler, refresh
                        // the position based upon coordinates.
                        if (!e.position.getNode(true)) {
                            e.position.refresh();
                        }
                        // If the handler moved focus, do not allow this event to propagate
                        // to cause any navigation.
                        if (Ext.Element.getActiveElement() !== oldActiveEl) {
                            return false;
                        }
                    }
                }
            }
        }
        return me.callParent(arguments);
    },
    cascade: function(fn, scope) {
        fn.call(scope || this, this);
    },
    // Private override because this cannot function as a Container,
    // and it has an items property which is an Array, NOT a MixedCollection.
    getRefItems: function() {
        return [];
    },
    contains: function() {
        // Same as above
        return false;
    },
    privates: {
        getFocusables: function() {
            // Override is here to prevent the default behaviour which tries to access
            // this.items.items, which will be null.
            return [];
        },
        // Overriden method to always return a bitwise value that will result
        // in a call to this column's updater.
        shouldUpdateCell: function() {
            return 2;
        }
    }
});

/**
 * A Column definition class which renders boolean data fields.
 * See the {@link Ext.grid.column.Column#xtype xtype} config option of
 * {@link Ext.grid.column.Column} for more details.
 *
 *     @example
 *     var store = Ext.create('Ext.data.Store', {
 *        fields: [
 *            {name: 'framework', type: 'string'},
 *            {name: 'rocks', type: 'boolean'}
 *        ],
 *        data: [
 *            { framework: 'Ext JS 5', rocks: true },
 *            { framework: 'Ext GWT', rocks: true },
 *            { framework: 'Other Guys', rocks: false }
 *        ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Boolean Column Demo',
 *         store: store,
 *         columns: [
 *             { text: 'Framework',  dataIndex: 'framework', flex: 1 },
 *             {
 *                 xtype: 'booleancolumn',
 *                 text: 'Rocks',
 *                 trueText: 'Yes',
 *                 falseText: 'No',
 *                 dataIndex: 'rocks'
 *             }
 *         ],
 *         height: 200,
 *         width: 400,
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.grid.column.Boolean', {
    extend: 'Ext.grid.column.Column',
    alias: [
        'widget.booleancolumn'
    ],
    alternateClassName: 'Ext.grid.BooleanColumn',
    /**
     * @cfg {String} trueText
     * The string returned by the renderer when the column value is not falsey.
     * @locale
     */
    trueText: 'true',
    /**
     * @cfg {String} falseText
     * The string returned by the renderer when the column value is falsey (but not undefined).
     * @locale
     */
    falseText: 'false',
    /**
     * @cfg {String} undefinedText
     * The string returned by the renderer when the column value is undefined.
     */
    undefinedText: '\xa0',
    defaultFilterType: 'boolean',
    /**
     * @cfg {Object} renderer
     * @hide
     */
    /**
     * @cfg {Object} scope
     * @hide
     */
    /**
     * @cfg producesHTML
     * @inheritdoc
     */
    producesHTML: false,
    defaultRenderer: function(value) {
        if (value === undefined) {
            return this.undefinedText;
        }
        if (!value || value === 'false') {
            return this.falseText;
        }
        return this.trueText;
    },
    updater: function(cell, value) {
        Ext.fly(cell).down(this.getView().innerSelector, true).innerHTML = Ext.grid.column.Boolean.prototype.defaultRenderer.call(this, value);
    }
});

/**
 * A Column subclass which renders a checkbox in each column cell which toggles the truthiness
 * of the associated data field on click.
 *
 * Example usage:
 *
 *     @example
 *     var store = Ext.create('Ext.data.Store', {
 *         fields: ['name', 'email', 'phone', 'active'],
 *         data: [
 *             { name: 'Lisa', email: 'lisa@simpsons.com', phone: '555-111-1224', active: true },
 *             { name: 'Bart', email: 'bart@simpsons.com', phone: '555-222-1234', active: true },
 *             { name: 'Homer', email: 'homer@simpsons.com', phone: '555-222-1244', active: false },
 *             { name: 'Marge', email: 'marge@simpsons.com', phone: '555-222-1254', active: true }
 *         ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Simpsons',
 *         height: 200,
 *         width: 400,
 *         renderTo: Ext.getBody(),
 *         store: store,
 *         columns: [
 *             { text: 'Name', dataIndex: 'name' },
 *             { text: 'Email', dataIndex: 'email', flex: 1 },
 *             { text: 'Phone', dataIndex: 'phone' },
 *             { xtype: 'checkcolumn', text: 'Active', dataIndex: 'active' }
 *         ]
 *     });
 *
 * The check column can be at any index in the columns array.
 */
Ext.define('Ext.grid.column.Check', {
    extend: 'Ext.grid.column.Column',
    alternateClassName: [
        'Ext.ux.CheckColumn',
        'Ext.grid.column.CheckColumn'
    ],
    alias: 'widget.checkcolumn',
    /**
     * @property {Boolean} isCheckColumn
     * `true` in this class to identify an object as an instantiated Check column,
     * or subclass thereof.
     */
    isCheckColumn: true,
    config: {
        /**
         * @cfg {Boolean} [headerCheckbox=false]
         * Configure as `true` to display a checkbox below the header text.
         *
         * Clicking the checkbox will check/uncheck all records.
         */
        headerCheckbox: false
    },
    /**
     * @cfg
     * @hide
     * Overridden from base class. Must center to line up with editor.
     */
    align: 'center',
    /**
     * @cfg {String} [triggerEvent=click]
     * The mouse event which triggers the toggle of a single cell.
     */
    triggerEvent: 'click',
    /**
     * @cfg {Boolean} invert
     * Use `true` to display a check when the value is `false` instead of when the value
     * is `true`.
     */
    invert: false,
    /**
     * @cfg {String} tooltip
     * The tooltip text to show upon hover of a unchecked cell.
     */
    /**
     * @cfg {String} checkedTooltip
     * The tooltip text to show upon hover of an checked cell.
     */
    ignoreExport: true,
    /**
     * @cfg {Boolean} [stopSelection=true]
     * Prevent grid selection upon mousedown.
     */
    stopSelection: true,
    /**
     * @private
     */
    headerCheckedCls: Ext.baseCSSPrefix + 'grid-hd-checker-on',
    /**
     * @private
     * The CSS class used to style and select the header checkbox.
     */
    headerCheckboxCls: Ext.baseCSSPrefix + 'column-header-checkbox',
    checkboxCls: Ext.baseCSSPrefix + 'grid-checkcolumn',
    checkboxCheckedCls: Ext.baseCSSPrefix + 'grid-checkcolumn-checked',
    innerCls: Ext.baseCSSPrefix + 'grid-checkcolumn-cell-inner',
    clickTargetName: 'el',
    defaultFilterType: 'boolean',
    checkboxAriaRole: 'button',
    /**
     * @event beforecheckchange
     * Fires when the UI requests a change of check status.
     * The change may be vetoed by returning `false` from a listener.
     * @param {Ext.grid.column.Check} this CheckColumn.
     * @param {Number} rowIndex The row index.
     * @param {Boolean} checked `true` if the box is to be checked.
     * @param {Ext.data.Model} record The record to be updated.
     * @param {Ext.event.Event} e The underlying event which caused the check change.
     * @param {Ext.grid.CellContext} e.position A {@link Ext.grid.CellContext CellContext} object
     * containing all contextual information about where the event was triggered.
     */
    /**
     * @event checkchange
     * Fires when the UI has successfully changed the checked state of a row.
     * @param {Ext.grid.column.Check} this CheckColumn.
     * @param {Number} rowIndex The row index.
     * @param {Boolean} checked `true` if the box is now checked.
     * @param {Ext.data.Model} record The record which was updated.
     * @param {Ext.event.Event} e The underlying event which caused the check change.
     * @param {Ext.grid.CellContext} e.position A {@link Ext.grid.CellContext CellContext} object
     */
    /**
     * @event beforeheadercheckchange
     * Fires when the header is clicked and before the mass check/uncheck takes place.
     * The change may be vetoed by returning `false` from a listener.
     * @param {Ext.grid.column.Check} this CheckColumn.
     * @param {Boolean} checked `true` if all boxes are to be checked.
     * @param {Ext.event.Event} e The underlying event which caused the check change.
     */
    /**
     * @event headercheckchange
     * Fires after the header is clicked and a mass check/uncheck operation has been completed.
     * @param {Ext.grid.column.Check} this CheckColumn.
     * @param {Boolean} checked `true` if all boxes are now checked.
     * @param {Ext.event.Event} e The underlying event which caused the check change.
     */
    constructor: function(config) {
        this.scope = this;
        this.callParent([
            config
        ]);
    },
    afterComponentLayout: function() {
        var me = this;
        me.callParent(arguments);
        if (me.useAriaElements && me.headerCheckbox) {
            me.updateHeaderAriaDescription(me.areAllChecked());
        }
        // Only do this once
        if (!me.storeListeners) {
            // Ensure initial rendered state is correct.
            // This will update the header state on the next animation frame
            // after all rows have been rendered.
            me.updateHeaderState();
            // We need to listen to data changed. This includes add and remove as well as reload.
            // We cannot rely on the renderer or updater to kick off an updateHeaderState call
            // because buffered rendering may mean that the UI does not process the entire dataset.
            me.storeListeners = me.getView().dataSource.on({
                datachanged: me.onDataChanged,
                scope: me,
                destroyable: true
            });
        }
    },
    onRemoved: function() {
        this.callParent(arguments);
        this.storeListeners = Ext.destroy(this.storeListeners);
    },
    onDataChanged: function(store, records) {
        // If any records are added or removed, we need up to date the header state.
        this.updateHeaderState();
    },
    updateHeaderCheckbox: function(headerCheckbox) {
        var me = this,
            cls = Ext.baseCSSPrefix + 'column-header-checkbox';
        if (headerCheckbox) {
            me.addCls(cls);
            // So that SPACE/ENTER does not sort, but routes to the checkbox
            me.sortable = false;
            if (me.useAriaElements) {
                me.updateHeaderAriaDescription(me.areAllChecked());
            }
        } else {
            me.removeCls(cls);
            if (me.useAriaElements && me.ariaEl.dom) {
                me.ariaEl.dom.removeAttribute('aria-describedby');
            }
        }
        // Keep the header checkbox up to date
        me.updateHeaderState();
    },
    /**
     * @private
     * Process and refire events routed from the GridView's processEvent method.
     */
    processEvent: function(type, view, cell, recordIndex, cellIndex, e, record, row) {
        var me = this,
            key = type === 'keydown' && e.getKey(),
            isClick = type === me.triggerEvent,
            disabled = me.disabled,
            ret, checked;
        // Flag event to tell SelectionModel not to process it.
        e.stopSelection = !key && me.stopSelection;
        if (!disabled && (isClick || (key === e.ENTER || key === e.SPACE))) {
            checked = !me.isRecordChecked(record);
            // Allow apps to hook beforecheckchange
            if (me.fireEvent('beforecheckchange', me, recordIndex, checked, record, e) !== false) {
                me.setRecordCheck(record, recordIndex, checked, cell, e);
                // Do not allow focus to follow from this mousedown unless the grid
                // is already in actionable mode
                if (isClick && !view.actionableMode) {
                    e.preventDefault();
                }
                if (me.hasListeners.checkchange) {
                    me.fireEvent('checkchange', me, recordIndex, checked, record, e);
                }
            }
        } else {
            ret = me.callParent(arguments);
        }
        return ret;
    },
    onTitleElClick: function(e, t, sortOnClick) {
        var me = this;
        // Toggle if no text, or it's activated by SPACE key,
        // or the click is on the checkbox element.
        if (!me.disabled && (e.keyCode || !me.text || (Ext.fly(e.target).hasCls(me.headerCheckboxCls)))) {
            me.toggleAll(e);
        } else {
            return me.callParent([
                e,
                t,
                sortOnClick
            ]);
        }
    },
    toggleAll: function(e) {
        var me = this,
            view = me.getView(),
            store = view.getStore(),
            checked = !me.allChecked;
        if (me.fireEvent('beforeheadercheckchange', me, checked, e) !== false) {
            // Only create and maintain a CellContext if there are consumers
            // in the form of event listeners. The event is a click on a 
            // column header and will have no position property.
            if (me.hasListeners.checkchange || me.hasListeners.beforecheckchange) {
                e.position = new Ext.grid.CellContext(view);
            }
            store.each(function(record, recordIndex) {
                me.setRecordCheck(record, recordIndex, checked, view.getCell(record, me));
            });
            me.setHeaderStatus(checked, e);
            me.fireEvent('headercheckchange', me, checked, e);
        }
    },
    setHeaderStatus: function(checked, e) {
        var me = this;
        // Will fire initially due to allChecked being undefined and using !==
        if (me.allChecked !== checked) {
            me.allChecked = checked;
            if (me.headerCheckbox) {
                me[checked ? 'addCls' : 'removeCls'](me.headerCheckedCls);
                if (me.useAriaElements) {
                    me.updateHeaderAriaDescription(checked);
                }
            }
        }
    },
    updateHeaderState: function(e) {
        var me = this;
        if (!me.headerStateTimer) {
            me.headerStateTimer = Ext.raf(me.doUpdateHeaderState, me);
        }
    },
    doUpdateHeaderState: function(e) {
        var me = this;
        me.headerStateTimer = null;
        // This is called on a timer, so ignore if it fires after destruction
        if (!me.destroyed && me.headerCheckbox) {
            me.setHeaderStatus(me.areAllChecked(), e);
        }
    },
    /**
     * Enables this CheckColumn.
     */
    onEnable: function() {
        this.callParent(arguments);
        this._setDisabled(false);
    },
    /**
     * Disables this CheckColumn.
     */
    onDisable: function() {
        this._setDisabled(true);
    },
    // Don't want to conflict with the Component method
    _setDisabled: function(disabled) {
        var me = this,
            cls = me.disabledCls,
            items;
        items = me.up('tablepanel').el.select(me.getCellSelector());
        if (disabled) {
            items.addCls(cls);
        } else {
            items.removeCls(cls);
        }
    },
    defaultRenderer: function(value, cellValues) {
        var me = this,
            cls = me.checkboxCls,
            tip = '';
        if (me.invert) {
            value = !value;
        }
        if (me.disabled) {
            cellValues.tdCls += ' ' + me.disabledCls;
        }
        if (value) {
            cls += ' ' + me.checkboxCheckedCls;
            tip = me.checkedTooltip;
        } else {
            tip = me.tooltip;
        }
        if (tip) {
            cellValues.tdAttr += ' data-qtip="' + Ext.htmlEncode(tip) + '"';
        }
        if (me.useAriaElements) {
            cellValues.tdAttr += ' aria-describedby="' + me.id + '-cell-description' + (!value ? '-not' : '') + '-selected"';
        }
        // This will update the header state on the next animation frame
        // after all rows have been rendered.
        me.updateHeaderState();
        return '<span class="' + cls + '" role="' + me.checkboxAriaRole + '"' + (!me.ariaStaticRoles[me.checkboxAriaRole] ? ' tabIndex="0"' : '') + '></span>';
    },
    isRecordChecked: function(record) {
        var prop = this.property;
        if (prop) {
            return record[prop];
        }
        return record.get(this.dataIndex);
    },
    areAllChecked: function() {
        var me = this,
            store = me.getView().getStore(),
            records, len, i;
        if (!store.isBufferedStore && store.getCount() > 0) {
            records = store.getData().items;
            len = records.length;
            for (i = 0; i < len; ++i) {
                if (!me.isRecordChecked(records[i])) {
                    return false;
                }
            }
            return true;
        }
    },
    setRecordCheck: function(record, recordIndex, checked, cell) {
        var me = this,
            prop = me.property;
        // Only proceed if we NEED to change
        // eslint-disable-next-line eqeqeq
        if ((prop ? record[prop] : record.get(me.dataIndex)) != checked) {
            if (prop) {
                record[prop] = checked;
                me.updater(cell, checked);
            } else {
                record.set(me.dataIndex, checked);
            }
        }
    },
    updater: function(cell, value) {
        var me = this,
            tip;
        if (me.invert) {
            value = !value;
        }
        if (value) {
            tip = me.checkedTooltip;
        } else {
            tip = me.tooltip;
        }
        if (tip) {
            cell.setAttribute('data-qtip', tip);
        } else {
            cell.removeAttribute('data-qtip');
        }
        if (me.useAriaElements) {
            me.updateCellAriaDescription(null, value, cell);
        }
        cell = Ext.fly(cell);
        cell[me.disabled ? 'addCls' : 'removeCls'](me.disabledCls);
        // eslint-disable-next-line max-len
        Ext.fly(cell.down(me.getView().innerSelector, true).firstChild)[value ? 'addCls' : 'removeCls'](Ext.baseCSSPrefix + 'grid-checkcolumn-checked');
        // This will update the header state on the next animation frame
        // after all rows have been updated.
        me.updateHeaderState();
    },
    /**
     * @private
     */
    updateHeaderAriaDescription: function(isSelected) {
        var me = this;
        if (me.useAriaElements && me.ariaEl.dom) {
            me.ariaEl.dom.setAttribute('aria-describedby', me.id + '-header-description' + (!isSelected ? '-not' : '') + '-selected');
        }
    },
    /**
     * @private
     */
    updateCellAriaDescription: function(record, isSelected, cell) {
        var me = this;
        if (me.useAriaElements) {
            cell = cell || me.getView().getCell(record, me);
            if (cell) {
                cell.setAttribute('aria-describedby', me.id + '-cell-description' + (!isSelected ? '-not' : '') + '-selected');
            }
        }
    },
    doDestroy: function() {
        Ext.unraf(this.headerStateTimer);
        this.callParent();
    },
    privates: {
        /**
         * A method called by the render template to allow extra content after the header text.
         * Needs to be a seperate element to carry this. Cannot be a :after pseudo element
         * on one of the textual elements because we need to filter the click target to this
         * element for header checkbox clicking.
         * @private
         */
        afterText: function(out, values) {
            var me = this,
                id = me.id;
            out.push('<span role="presentation" class="', me.headerCheckboxCls, '"></span>');
            if (me.useAriaElements) {
                out.push('<span id="' + id + '-header-description-selected" class="' + Ext.baseCSSPrefix + 'hidden-offsets">' + me.headerDeselectText + '</span>' + '<span id="' + id + '-header-description-not-selected" class="' + Ext.baseCSSPrefix + 'hidden-offsets">' + me.headerSelectText + '</span>' + '<span id="' + id + '-cell-description-selected" class="' + Ext.baseCSSPrefix + 'hidden-offsets">' + me.rowDeselectText + '</span>' + '<span id="' + id + '-cell-description-not-selected" class="' + Ext.baseCSSPrefix + 'hidden-offsets">' + me.rowSelectText + '</span>');
            }
        }
    }
});

/* eslint-disable max-len */
/**
 * A Column definition class which renders a passed date according to the default locale, or a configured
 * {@link #format}.
 *
 *     @example
 *     Ext.create('Ext.data.Store', {
 *         storeId:'sampleStore',
 *         fields:[
 *             { name: 'symbol', type: 'string' },
 *             { name: 'date',   type: 'date' },
 *             { name: 'change', type: 'number' },
 *             { name: 'volume', type: 'number' },
 *             { name: 'topday', type: 'date' }
 *         ],
 *         data:[
 *             { symbol: "msft",   date: '2011/04/22', change: 2.43, volume: 61606325, topday: '04/01/2010' },
 *             { symbol: "goog",   date: '2011/04/22', change: 0.81, volume: 3053782,  topday: '04/11/2010' },
 *             { symbol: "apple",  date: '2011/04/22', change: 1.35, volume: 24484858, topday: '04/28/2010' },
 *             { symbol: "sencha", date: '2011/04/22', change: 8.85, volume: 5556351,  topday: '04/22/2010' }
 *         ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Date Column Demo',
 *         store: Ext.data.StoreManager.lookup('sampleStore'),
 *         columns: [
 *             { text: 'Symbol',   dataIndex: 'symbol', flex: 1 },
 *             { text: 'Date',     dataIndex: 'date',   xtype: 'datecolumn',   format:'Y-m-d' },
 *             { text: 'Change',   dataIndex: 'change', xtype: 'numbercolumn', format:'0.00' },
 *             { text: 'Volume',   dataIndex: 'volume', xtype: 'numbercolumn', format:'0,000' },
 *             { text: 'Top Day',  dataIndex: 'topday', xtype: 'datecolumn',   format:'l' }
 *         ],
 *         height: 200,
 *         width: 450,
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.grid.column.Date', {
    /* eslint-enable max-len */
    extend: 'Ext.grid.column.Column',
    alias: [
        'widget.datecolumn'
    ],
    alternateClassName: 'Ext.grid.DateColumn',
    requires: [
        'Ext.Date'
    ],
    isDateColumn: true,
    defaultFilterType: 'date',
    /**
     * @cfg {String} format
     * A formatting string as used by {@link Ext.Date#format} to format a Date for this Column.
     *
     * Defaults to the default date from {@link Ext.Date#defaultFormat} which itself
     * my be overridden in a locale file.
     */
    /**
     * @cfg {Object} renderer
     * @hide
     */
    /**
     * @cfg {Object} scope
     * @hide
     */
    /**
     * @cfg producesHTML
     * @inheritdoc
     */
    producesHTML: false,
    initComponent: function() {
        if (!this.format) {
            this.format = Ext.Date.defaultFormat;
        }
        this.callParent(arguments);
    },
    defaultRenderer: function(value) {
        return Ext.util.Format.date(value, this.format);
    },
    updater: function(cell, value) {
        Ext.fly(cell).down(this.getView().innerSelector, true).innerHTML = Ext.grid.column.Date.prototype.defaultRenderer.call(this, value);
    }
});

/**
 * A Column definition class which renders a numeric data field according to a
 * {@link #format} string.
 *
 *     @example
 *     Ext.create('Ext.data.Store', {
 *        storeId:'sampleStore',
 *        fields:[
 *            { name: 'symbol', type: 'string' },
 *            { name: 'price',  type: 'number' },
 *            { name: 'change', type: 'number' },
 *            { name: 'volume', type: 'number' }
 *        ],
 *        data:[
 *            { symbol: "msft",   price: 25.76,  change: 2.43, volume: 61606325 },
 *            { symbol: "goog",   price: 525.73, change: 0.81, volume: 3053782  },
 *            { symbol: "apple",  price: 342.41, change: 1.35, volume: 24484858 },
 *            { symbol: "sencha", price: 142.08, change: 8.85, volume: 5556351  }
 *        ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Number Column Demo',
 *         store: Ext.data.StoreManager.lookup('sampleStore'),
 *         columns: [
 *             { text: 'Symbol', dataIndex: 'symbol', flex: 1 },
 *             { text: 'Current Price', dataIndex: 'price',  renderer: Ext.util.Format.usMoney },
 *             { text: 'Change', dataIndex: 'change', xtype: 'numbercolumn', format:'0.00' },
 *             { text: 'Volume', dataIndex: 'volume', xtype: 'numbercolumn', format:'0,000' }
 *         ],
 *         height: 200,
 *         width: 400,
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.grid.column.Number', {
    extend: 'Ext.grid.column.Column',
    alias: [
        'widget.numbercolumn'
    ],
    alternateClassName: 'Ext.grid.NumberColumn',
    requires: [
        'Ext.util.Format'
    ],
    defaultFilterType: 'number',
    /**
     * @cfg {String} format
     * A formatting string as used by {@link Ext.util.Format#number} to format a numeric
     * value for this Column.
     * @locale
     */
    format: '0,000.00',
    /**
     * @cfg {Object} renderer
     * @hide
     */
    /**
     * @cfg {Object} scope
     * @hide
     */
    /**
     * @cfg producesHTML
     * @inheritdoc
     */
    producesHTML: false,
    defaultRenderer: function(value) {
        return Ext.util.Format.number(value, this.format);
    },
    updater: function(cell, value) {
        Ext.fly(cell).down(this.getView().innerSelector, true).innerHTML = Ext.grid.column.Number.prototype.defaultRenderer.call(this, value);
    }
});

/* eslint-disable max-len */
/**
 * A special type of Grid {@link Ext.grid.column.Column} that provides automatic
 * row numbering.
 *
 * Usage:
 *
 *     columns: [
 *         {xtype: 'rownumberer'},
 *         {text: "Company", flex: 1, sortable: true, dataIndex: 'company'},
 *         {text: "Price", width: 120, sortable: true, renderer: Ext.util.Format.usMoney, dataIndex: 'price'},
 *         {text: "Change", width: 120, sortable: true, dataIndex: 'change'},
 *         {text: "% Change", width: 120, sortable: true, dataIndex: 'pctChange'},
 *         {text: "Last Updated", width: 120, sortable: true, renderer: Ext.util.Format.dateRenderer('m/d/Y'), dataIndex: 'lastChange'}
 *     ]
 *
 */
Ext.define('Ext.grid.column.RowNumberer', {
    /* eslint-enable max-len */
    extend: 'Ext.grid.column.Column',
    alternateClassName: 'Ext.grid.RowNumberer',
    alias: 'widget.rownumberer',
    /**
     * @property {Boolean} isRowNumberer
     * `true` in this class to identify an object as an instantiated RowNumberer,
     * or subclass thereof.
     */
    isRowNumberer: true,
    /**
     * @cfg {String} text
     * Any valid text or HTML fragment to display in the header cell for the row number column.
     */
    text: "&#160;",
    /**
     * @cfg {Number} width
     * The default width in pixels of the row number column.
     */
    width: 30,
    /**
     * @cfg {Boolean} sortable
     * @hide
     */
    sortable: false,
    /**
     * @cfg {Boolean} draggable
     * False to disable drag-drop reordering of this column.
     */
    draggable: false,
    // Flag to Lockable to move instances of this column to the locked side.
    autoLock: true,
    // May not be moved from its preferred locked side when grid is enableLocking:true
    lockable: false,
    /**
     * @cfg align
     * @inheritdoc
     */
    align: 'right',
    /**
     * @cfg producesHTML
     * @inheritdoc
     */
    producesHTML: false,
    /**
     * @cfg ignoreExport
     * @inheritdoc
     */
    ignoreExport: true,
    constructor: function(config) {
        var me = this;
        // Copy the prototype's default width setting into an instance property to provide
        // a default width which will not be overridden by Container.applyDefaults
        // use of Ext.applyIf
        // eslint-disable-next-line no-self-assign
        me.width = me.width;
        me.callParent(arguments);
        // Override any setting from the HeaderContainer's defaults
        me.sortable = false;
        me.scope = me;
    },
    /**
     * @cfg resizable
     * @inheritdoc
     */
    resizable: false,
    /**
     * @cfg hideable
     * @inheritdoc
     */
    hideable: false,
    /**
     * @cfg menuDisabled
     * @inheritdoc
     */
    menuDisabled: true,
    /**
     * @cfg dataIndex
     * @inheritdoc
     */
    dataIndex: '',
    /**
     * @cfg cls
     * @inheritdoc
     */
    cls: Ext.baseCSSPrefix + 'row-numberer',
    /**
     * @cfg tdCls
     * @inheritdoc
     */
    tdCls: Ext.baseCSSPrefix + 'grid-cell-row-numberer ' + Ext.baseCSSPrefix + 'grid-cell-special',
    innerCls: Ext.baseCSSPrefix + 'grid-cell-inner-row-numberer',
    rowspan: undefined,
    onAdded: function() {
        var me = this;
        // Coalesce multiple item mutation events by routing them to a buffered function
        me.renumberRows = Ext.Function.createBuffered(me.renumberRows, 1, me);
        me.callParent(arguments);
        me.storeListener = me.getView().on({
            itemadd: me.renumberRows,
            itemremove: me.renumberRows,
            destroyable: true
        });
    },
    onRemoved: function() {
        var me = this;
        me.callParent(arguments);
        if (me.storeListener) {
            me.storeListener = me.storeListener.destroy();
        }
        if (me.renumberRows.timer) {
            Ext.undefer(me.renumberRows.timer);
        }
        me.renumberRows = null;
        delete me.renumberRows;
    },
    defaultRenderer: function(value, metaData, record, rowIdx, colIdx, dataSource, view) {
        var me = this,
            rowspan = me.rowspan,
            page = dataSource.currentPage,
            result = record ? view.store.indexOf(record) : value - 1;
        if (metaData && rowspan) {
            metaData.tdAttr = 'rowspan="' + rowspan + '"';
        }
        if (page > 1) {
            result += (page - 1) * dataSource.pageSize;
        }
        return result + 1;
    },
    updater: function(cell, value, record, view, dataSource) {
        var cellInner = cell && cell.querySelector(this.getView().innerSelector);
        if (cellInner) {
            cellInner.innerHTML = this.defaultRenderer(value, null, record, null, null, dataSource, view);
        }
    },
    renumberRows: function() {
        if (this.destroying || this.destroyed) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            view = me.getView(),
            dataSource = view.dataSource,
            recCount = dataSource.getCount(),
            context = new Ext.grid.CellContext(view).setColumn(me),
            rows = me.getView().all,
            index = rows.startIndex;
        while (index <= rows.endIndex && index < recCount) {
            context.setRow(index);
            me.updater(context.getCell(true), ++index, null, view, dataSource);
        }
    }
});

/* eslint-disable max-len */
/**
 * A Column definition class which renders a value by processing a {@link Ext.data.Model Model}'s
 * {@link Ext.data.Model#getData data} using a {@link #tpl configured}
 * {@link Ext.XTemplate XTemplate}.
 *
 *     @example
 *     Ext.create('Ext.data.Store', {
 *         storeId:'employeeStore',
 *         fields:['firstname', 'lastname', 'seniority', 'department'],
 *         groupField: 'department',
 *         data:[
 *             { firstname: "Michael", lastname: "Scott",   seniority: 7, department: "Management" },
 *             { firstname: "Dwight",  lastname: "Schrute", seniority: 2, department: "Sales" },
 *             { firstname: "Jim",     lastname: "Halpert", seniority: 3, department: "Sales" },
 *             { firstname: "Kevin",   lastname: "Malone",  seniority: 4, department: "Accounting" },
 *             { firstname: "Angela",  lastname: "Martin",  seniority: 5, department: "Accounting" }
 *         ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Column Template Demo',
 *         store: Ext.data.StoreManager.lookup('employeeStore'),
 *         columns: [
 *             { text: 'Full Name',       xtype: 'templatecolumn', tpl: '{firstname} {lastname}', flex:1 },
 *             { text: 'Department (Yrs)', xtype: 'templatecolumn', tpl: '{department} ({seniority})' }
 *         ],
 *         height: 200,
 *         width: 300,
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.grid.column.Template', {
    /* eslint-enable max-len */
    extend: 'Ext.grid.column.Column',
    alias: [
        'widget.templatecolumn'
    ],
    alternateClassName: 'Ext.grid.TemplateColumn',
    requires: [
        'Ext.XTemplate'
    ],
    /**
     * @cfg {String/Ext.XTemplate} tpl
     * An {@link Ext.XTemplate XTemplate}, or an XTemplate *definition string* to use to process a
     * {@link Ext.data.Model Model}'s data object to produce a cell's rendered value.
     */
    /**
     * @cfg {Object} renderer
     * @hide
     */
    /**
     * @cfg {Object} scope
     * @hide
     */
    initComponent: function() {
        var me = this;
        me.tpl = (!Ext.isPrimitive(me.tpl) && me.tpl.compile) ? me.tpl : new Ext.XTemplate(me.tpl);
        // Set this here since the template may access any record values,
        // so we must always run the update for this column
        me.hasCustomRenderer = true;
        me.callParent();
    },
    defaultRenderer: function(value, meta, record) {
        var data = Ext.apply({}, record.data, record.getAssociatedData());
        return this.tpl.apply(data);
    },
    updater: function(cell, value) {
        Ext.fly(cell).down(this.getView().innerSelector, true).innerHTML = Ext.grid.column.CheckColumn.prototype.defaultRenderer.call(this, value);
    }
});

/**
 * A widget column is configured with a {@link #widget} config object which specifies an
 * {@link Ext.Component#cfg-xtype xtype} to indicate which type of Widget or Component belongs
 * in the cells of this column.
 *
 * When a widget cell is rendered, a {@link Ext.Widget Widget} or {@link Ext.Component Component}
 * of the specified type is rendered into that cell.
 * 
 * There are two ways of setting values in a cell widget.
 * 
 * The simplest way is to use data binding. To use column widget data binding, the widget must
 * either contain  a top-level bind statement, which will cause a
 * {@link Ext.app.ViewModel ViewModel} to be automatically injected into the widget. This ViewModel
 * will inherit data from any ViewModel that the grid is using, and it will also contain
 * two extra properties:
 *
 * - `record`: {@link Ext.data.Model Model}<br>The record which backs the grid row.
 * - `recordIndex`: {@link Number}<br>The index in the dataset of the record which backs
 *    the grid row.
 *
 * For complex widgets, where the widget may be a container that does not directly use any
 * data binding, but has items which do, the specification of a
 * {@link Ext.panel.Table#rowViewModel rowViewModel} type or configuration  is required on the grid.
 * This can simply be an empty object if grid widgets only require binding to the row record.
 * 
 * The deprecated way is to configure the column with a {@link #dataIndex}. The widget's
 * {@link Ext.Component#defaultBindProperty defaultBindProperty} will be set using the
 * specified field from the associated record.
 *
 * In the example below we are monitoring the throughput of electricity substations. The capacity
 * being used as a proportion of the maximum rated capacity is displayed as a progress bar.
 * As new data arrives and the instantaneous usage value is updated, the `capacityUsed`
 * field updates itself, and the record's change is broadcast to all bindings.
 * The {@link Ext.Progress Progress Bar Widget}'s
 * {@link Ext.ProgressBarWidget#defaultBindProperty defaultBindProperty} (which is
 * "value") is set to the calculated `capacityUsed`.
 *
 *     @example
 *     var grid = new Ext.grid.Panel({
 *         title: 'Substation power monitor',
 *         width: 600,
 *         viewConfig: {
 *             enableTextSelection: false,
 *             markDirty: false
 *         },
 *         columns: [{
 *             text: 'Id',
 *             dataIndex: 'id',
 *             width: 120
 *         }, {
 *             text: 'Rating',
 *             dataIndex: 'maxCapacity',
 *             width: 80
 *         }, {
 *             text: 'Avg.',
 *             dataIndex: 'avg',
 *             width: 85,
 *             formatter: 'number("0.00")'
 *         }, {
 *             text: 'Max',
 *             dataIndex: 'max',
 *             width: 80
 *         }, {
 *             text: 'Instant',
 *             dataIndex: 'instant',
 *             width: 80
 *         }, {
 *             text: '%Capacity',
 *             width: 150,
 *
 *             // This is our Widget column
 *             xtype: 'widgetcolumn',
 *
 *             // This is the widget definition for each cell.
 *             // The Progress widget class's defaultBindProperty is 'value'
 *             // so its "value" setting is taken from the ViewModel's record "capacityUsed" field
 *             // Note that a row ViewModel will automatically be injected due to the existence of 
 *             // the bind property in the widget configuration.
 *             widget: {
 *                 xtype: 'progressbarwidget',
 *                 bind: '{record.capacityUsed}',
 *                 textTpl: [
 *                     '{percent:number("0")}% capacity'
 *                 ]
 *             }
 *         }],
 *         renderTo: document.body,
 *         disableSelection: true,
 *         store: {
 *            fields: [{
 *                name: 'id',
 *                type: 'string'
 *            }, {
 *                name: 'maxCapacity',
 *                type: 'int'
 *            }, {
 *                name: 'avg',
 *                type: 'int',
 *                calculate: function(data) {
 *                    // Make this depend upon the instant field being set which sets
 *                    // the sampleCount and total.
 *                    // Use subscript format to access the other pseudo fields
 *                    //  which are set by the instant field's converter
 *                    return data.instant && data['total'] / data['sampleCount'];
 *                }
 *            }, {
 *                name: 'max',
 *                type: 'int',
 *                calculate: function(data) {
 *                    // This will be seen to depend on the "instant" field.
 *                    // Use subscript format to access this field's current value
 *                    //  to avoid circular dependency error.
 *                    return (data['max'] || 0) < data.instant ? data.instant : data['max'];
 *                }
 *            }, {
 *                name: 'instant',
 *                type: 'int',
 *
 *                // Upon every update of instantaneous power throughput,
 *                // update the sample count and total so that the max field can calculate itself
 *                convert: function(value, rec) {
 *                    rec.data.sampleCount = (rec.data.sampleCount || 0) + 1;
 *                    rec.data.total = (rec.data.total || 0) + value;
 *                    return value;
 *                },
 *               depends: []
 *            }, {
 *                name: 'capacityUsed',
 *                calculate: function(data) {
 *                    return data.instant / data.maxCapacity;
 *                }
 *            }],
 *            data: [{
 *                id: 'Substation A',
 *                maxCapacity: 1000,
 *                avg: 770,
 *                max: 950,
 *                instant: 685
 *            }, {
 *                id: 'Substation B',
 *                maxCapacity: 1000,
 *                avg: 819,
 *                max: 992,
 *                instant: 749
 *            }, {
 *                id: 'Substation C',
 *                maxCapacity: 1000,
 *                avg: 588,
 *                  max: 936,
 *                instant: 833
 *            }, {
 *                id: 'Substation D',
 *                maxCapacity: 1000,
 *                avg: 639,
 *                max: 917,
 *                instant: 825
 *            }]
 *        }
 *     });
 *
 *     // Fake data updating...
 *     // Change one record per second to a random power value
 *     Ext.interval(function() {
 *         var recIdx = Ext.Number.randomInt(0, 3),
 *             newPowerReading = Ext.Number.randomInt(500, 1000);
 *
 *         grid.store.getAt(recIdx).set('instant', newPowerReading);
 *     }, 1000);
 *
 * @since 5.0.0
 */
Ext.define('Ext.grid.column.Widget', {
    extend: 'Ext.grid.column.Column',
    xtype: 'widgetcolumn',
    mixins: [
        'Ext.mixin.StyleCacher'
    ],
    config: {
        /**
         * @cfg {Object} defaultWidgetUI
         * A map of xtype to {@link Ext.Component#ui} names to use when using Components
         * in this column.
         *
         * Currently {@link Ext.Button Button} and all subclasses of
         * {@link Ext.form.field.Text TextField} default to using `ui: "default"`
         * when in a WidgetColumn except for in the "classic" theme, when they use ui "grid-cell".
         */
        defaultWidgetUI: {}
    },
    /**
     * @cfg ignoreExport
     * @inheritdoc
     */
    ignoreExport: true,
    /**
     * @cfg sortable
     * @inheritdoc
     */
    sortable: false,
    /**
     * @cfg {Object} renderer
     * @hide
     */
    /**
     * @cfg {Object} scope
     * @hide
     */
    /**
     * @cfg {Object} widget
     * A config object containing an {@link Ext.Component#cfg-xtype xtype}.
     *
     * This is used to create the widgets or components which are rendered into the cells
     * of this column.
     *
     * The rendered component has a {@link Ext.app.ViewModel ViewModel} injected which inherits
     * from any ViewModel that the grid is using, and contains two extra properties:
     *
     * - `record`: {@link Ext.data.Model Model}<br>The record which backs the grid row.
     * - `recordIndex`: {@link Number}<br>The index in the dataset of the record which backs
     * the grid row.
     *
     * The widget configuration may contain a {@link #cfg-bind} config which uses
     * the ViewModel's data.
     *
     * The derecated way of obtaining data from the record is still supported if the widget
     * does *not* use a {@link #cfg-bind} config.
     *
     * This column's {@link #dataIndex} is used to update the widget/component's
     * {@link Ext.Component#defaultBindProperty defaultBindProperty}.
     *
     * The widget will be decorated with 2 methods:
     * {@link #method-getWidgetRecord} - Returns the {@link Ext.data.Model record} the widget
     * is associated with.
     * {@link #method-getWidgetColumn} - Returns the {@link Ext.grid.column.Widget column}
     * the widget was associated with.
     */
    /**
     * @cfg {Function/String} onWidgetAttach
     * A function that will be called when a widget is attached to a record. This may be useful for
     * doing any post-processing.
     * 
     *     Ext.create({
     *         xtype: 'grid',
     *         title: 'Student progress report',
     *         width: 250,
     *         renderTo: Ext.getBody(),
     *         disableSelection: true,
     *         store: {
     *             fields: ['name', 'isHonorStudent'],
     *             data: [{
     *                 name: 'Finn',
     *                 isHonorStudent: true
     *             }, {
     *                 name: 'Jake',
     *                 isHonorStudent: false
     *             }]
     *         },
     *         columns: [{
     *             text: 'Name',
     *             dataIndex: 'name',
     *             flex: 1
     *         }, {
     *             xtype: 'widgetcolumn',
     *             text: 'Honor Roll',
     *             dataIndex: 'isHonorStudent',
     *             width: 150,
     *             widget: {
     *                 xtype: 'button',
     *                 handler: function() {
     *                     // print certificate handler
     *                 }
     *             },
     *             // called when the widget is initially instantiated
     *             // on the widget column
     *             onWidgetAttach: function(col, widget, rec) {
     *                 widget.setText('Print Certificate');
     *                 widget.setDisabled(!rec.get('isHonorStudent'));
     *             }
     *         }]
     *     });
     * 
     * @param {Ext.grid.column.Column} column The column.
     * @param {Ext.Component/Ext.Widget} widget The {@link #widget} rendered to each cell.
     * @param {Ext.data.Model} record The record used with the current widget (cell).
     * @controllable
     */
    onWidgetAttach: null,
    preventUpdate: true,
    innerCls: Ext.baseCSSPrefix + 'grid-widgetcolumn-cell-inner',
    /**
     * @cfg {Boolean} [stopSelection=true]
     * Prevent grid selection upon click on the widget.
     */
    stopSelection: true,
    initComponent: function() {
        var me = this,
            widget;
        me.callParent(arguments);
        widget = me.widget;
        if (!widget || widget.isComponent) {
            Ext.raise('column.Widget requires a widget configuration.');
        }
        me.widget = widget = Ext.apply({}, widget);
        // Apply the default UI for the xtype which is going to feature in this column.
        if (!widget.ui) {
            widget.ui = me.getDefaultWidgetUI()[widget.xtype] || 'default';
        }
        me.isFixedSize = Ext.isNumber(widget.width);
    },
    /**
     * @method getWidgetRecord
     * getWidgetRecord is a method that decorates every widget.
     * Returns the {@link Ext.data.Model record} the widget is associated with.
     * @return {Ext.data.Model}
     */
    /**
     * @method getWidgetColumn
     * getWidgetColumn is a method that decorates every widget.
     * Returns the {@link Ext.grid.column.Widget column} the widget was associated with.
     * @return {Ext.grid.column.Widget}
     */
    processEvent: function(type, view, cell, recordIndex, cellIndex, e, record, row) {
        var target;
        if (this.stopSelection && type === 'click') {
            // Grab the target that matches the cell inner selector. If we have a target, then,
            // that means we either clicked on the inner part or the widget inside us. If 
            // target === e.target, then it was on the cell, so it's ok. Otherwise, inside so
            // prevent the selection from happening
            target = e.getTarget(view.innerSelector);
            if (target && target !== e.target) {
                e.stopSelection = true;
            }
        }
    },
    beforeRender: function() {
        var me = this,
            tdCls = me.tdCls,
            widget;
        // Need an instantiated example to retrieve the tdCls that it needs
        widget = Ext.widget(me.widget);
        // If the widget is not using binding, but we have a dataIndex, and there's
        // a defaultBindProperty to push it into, set flag to indicate to do that.
        me.bindDataIndex = me.dataIndex && widget.defaultBindProperty && !widget.bind;
        tdCls = tdCls ? tdCls + ' ' : '';
        me.tdCls = tdCls + widget.getTdCls();
        me.setupViewListeners(me.getView());
        me.callParent();
        widget.destroy();
    },
    afterRender: function() {
        var view = this.getView();
        this.callParent();
        // View already ready, means we were added later so go and set up our widgets,
        // but if the grid is reconfiguring, then the column will be rendered & the view
        // will be ready, so wait until the reconfigure forces a refresh
        if (view && view.viewReady && !view.ownerGrid.reconfiguring) {
            this.onViewRefresh(view, view.getViewRange());
        }
    },
    // Cell must be left blank
    /**
     * @method defaultRenderer
     * @inheritdoc
     * @localdoc **Important:** Cell must be left blank
     */
    defaultRenderer: Ext.emptyFn,
    updater: function(cell, value, record) {
        this.updateWidget(record);
    },
    onCellsResized: function(newWidth) {
        var me = this,
            liveWidgets = me.ownerGrid.getManagedWidgets(me.getId()),
            len = liveWidgets.length,
            view = me.getView(),
            i, cell;
        if (!me.isFixedSize && me.rendered && view && view.viewReady) {
            cell = view.getEl().down(me.getCellInnerSelector());
            if (cell) {
                // Subtract innerCell padding width
                newWidth -= parseInt(me.getCachedStyle(cell, 'padding-left'), 10) + parseInt(me.getCachedStyle(cell, 'padding-right'), 10);
                for (i = 0; i < len; ++i) {
                    // Ensure these are treated as the top of the modified tree.
                    // If not within a layout run, this will work fine.
                    // If within a layout run, Component#updateLayout will
                    // just ask its runningLayoutContext to invalidate it.
                    liveWidgets[i].ownerLayout = null;
                    liveWidgets[i].setWidth(newWidth);
                    liveWidgets[i].ownerLayout = view.componentLayout;
                }
            }
        }
    },
    onAdded: function() {
        var me = this,
            view;
        me.callParent(arguments);
        me.ownerGrid = me.up('tablepanel').ownerGrid;
        // If the grid is lockable we should mark this column with variableRowHeight,
        // as widgets can cause rows to be taller and this config will force them
        // to be synced on every layout cycle.
        if (me.ownerGrid.lockable) {
            me.variableRowHeight = true;
        }
        view = me.getView();
        // If we are being added to a rendered HeaderContainer
        if (view) {
            me.setupViewListeners(view);
        }
    },
    onRemoved: function(isDestroying) {
        var viewListeners = this.viewListeners;
        if (viewListeners) {
            Ext.destroy(viewListeners);
        }
        if (isDestroying) {
            this.ownerGrid.destroyManagedWidgets(this.getId());
        }
        this.callParent(arguments);
    },
    doDestroy: function() {
        this.ownerGrid.destroyManagedWidgets(this.getId());
        this.callParent();
    },
    privates: {
        getWidget: function(record) {
            var me = this,
                result = null;
            if (record) {
                result = me.ownerGrid.createManagedWidget(me.getView(), me.getId(), me.widget, record);
                result.getWidgetRecord = me.widgetRecordDecorator;
                result.getWidgetColumn = me.widgetColumnDecorator;
                result.measurer = me;
                // The ownerCmp of the widget is the encapsulating view, which means
                // it will be considered as a layout child, but it isn't really, we always need
                // the layout on the component to run if asked.
                result.isLayoutChild = me.returnFalse;
            }
            return result;
        },
        onItemAdd: function(records) {
            var me = this,
                view = me.getView(),
                hasAttach = !!me.onWidgetAttach,
                dataIndex = me.dataIndex,
                isFixedSize = me.isFixedSize,
                len = records.length,
                i, record, cell, widget, el, focusEl, width;
            // Loop through all records added, ensuring that our corresponding cell in each item
            // has a Widget of the correct type in it, and is updated with the correct value
            // from the record.
            if (me.isVisible(true)) {
                for (i = 0; i < len; i++) {
                    record = records[i];
                    if (record.isNonData) {
                        
                        continue;
                    }
                    cell = view.getCell(record, me);
                    // May be a placeholder with no data row
                    if (cell) {
                        cell = cell.firstChild;
                        if (!isFixedSize && !width && me.lastBox) {
                            width = me.lastBox.width - parseInt(me.getCachedStyle(cell, 'padding-left'), 10) - parseInt(me.getCachedStyle(cell, 'padding-right'), 10);
                        }
                        widget = me.getWidget(record);
                        widget.$widgetColumn = me;
                        widget.$widgetRecord = record;
                        // Render/move a widget into the new row
                        Ext.fly(cell).empty();
                        // Call the appropriate setter with this column's data field
                        if (widget.defaultBindProperty && dataIndex) {
                            widget.setConfig(widget.defaultBindProperty, record.get(dataIndex));
                        }
                        el = widget.el || widget.element;
                        if (el) {
                            cell.appendChild(el.dom);
                            if (!isFixedSize) {
                                widget.setWidth(width);
                            }
                            widget.reattachToBody();
                        } else {
                            if (!isFixedSize) {
                                // Must have a width so that the initial layout works
                                widget.width = width || 100;
                            }
                            widget.render(cell);
                        }
                        // We have to run the callback *after* reattaching the Widget
                        // back to the document body. Otherwise widget's layout may fail
                        // because there are no dimensions to measure when the callback is fired!
                        if (hasAttach) {
                            Ext.callback(me.onWidgetAttach, me.scope, [
                                me,
                                widget,
                                record
                            ], 0, me);
                        }
                        // If the widget has a focusEl, ensure that its tabbability status
                        // is synched with the view's navigable/actionable state.
                        focusEl = widget.getFocusEl();
                        if (focusEl) {
                            if (view.actionableMode) {
                                if (!focusEl.isTabbable()) {
                                    focusEl.restoreTabbableState();
                                }
                            } else {
                                if (focusEl.isTabbable()) {
                                    focusEl.saveTabbableState();
                                }
                            }
                        }
                    }
                }
            } else {
                view.refreshNeeded = true;
            }
        },
        onItemUpdate: function(record, recordIndex, oldItemDom) {
            this.updateWidget(record);
        },
        onLock: function(header) {
            this.callParent([
                header
            ]);
            this.resetView();
        },
        onUnlock: function(header) {
            this.callParent([
                header
            ]);
            this.resetView();
        },
        onViewRefresh: function(view, records) {
            Ext.suspendLayouts();
            this.onItemAdd(records);
            Ext.resumeLayouts(true);
        },
        resetView: function() {
            var me = this,
                viewListeners = me.viewListeners;
            if (viewListeners) {
                Ext.destroy(viewListeners);
            }
            me.setupViewListeners(me.getView());
            me.ownerGrid.handleWidgetViewChange(me.getView(), me.getId());
        },
        returnFalse: function() {
            return false;
        },
        setupViewListeners: function(view) {
            var me = this,
                listeners = {
                    refresh: me.onViewRefresh,
                    itemadd: me.onItemAdd,
                    scope: me,
                    destroyable: true
                };
            // If we are set up to push a dataIndex property into the widget's defaultBindProperty
            // then we must react to itemupdate events to keep the widget fresh.
            if (me.bindDataIndex) {
                listeners.itemUpdate = me.onItemUpdate;
            }
            me.viewListeners = view.on(listeners);
        },
        updateWidget: function(record) {
            var dataIndex = this.dataIndex,
                widget;
            if (this.rendered && this.bindDataIndex) {
                widget = this.getWidget(record);
                // Call the appropriate setter with this column's data field
                // unless it's using binding
                if (widget) {
                    widget.setConfig(widget.defaultBindProperty, record.get(dataIndex));
                }
            }
        },
        widgetRecordDecorator: function() {
            return this.$widgetRecord;
        },
        widgetColumnDecorator: function() {
            return this.$widgetColumn;
        }
    }
});

/**
 * A feature is a type of plugin that is specific to the {@link Ext.grid.Panel}. It provides several
 * hooks that allows the developer to inject additional functionality at certain points throughout
 * the grid creation cycle. This class provides the base template methods that are available
 * to the developer, it should be extended.
 *
 * There are several built in features that extend this class, for example:
 *
 *  - {@link Ext.grid.feature.Grouping} - Shows grid rows in groups as specified by the
 *    {@link Ext.data.Store}
 *  - {@link Ext.grid.feature.RowBody} - Adds a body section for each grid row that can contain
 *    markup.
 *  - {@link Ext.grid.feature.Summary} - Adds a summary row at the bottom of the grid
 *    with aggregate totals for a column.
 *
 * ## Using Features
 * A feature is added to the grid by specifying it an array of features in the configuration:
 *
 *     var groupingFeature = Ext.create('Ext.grid.feature.Grouping');
 *     Ext.create('Ext.grid.Panel', {
 *         // other options
 *         features: [groupingFeature]
 *     });
 *
 * ## Writing Features
 *
 * A Feature may add new DOM structure within the structure of a grid.
 *
 * A grid is essentially a `<table>` element. A {@link Ext.view.Table TableView} instance uses
 * four {@link Ext.XTemplate XTemplates} to render the grid, `tpl`, `itemTpl`, `rowTpl`, `cellTpl`.
 *
 * A {@link Ext.view.Table TableView} uses its `tpl` to emit the item encapsulating HTML tags
 * into its output stream. To render the rows, it invokes {@link Ext.view.Table#renderRows}
 * passing the `rows` member of its data object and the `columns` member of its data object.
 *
 * The `tpl`'s data object Looks like this:
 *     {
 *         view: owningTableView,
 *         rows: recordsToRender,
 *         viewStartIndex: indexOfFirstRecordInStore,
 *         tableStyle: styleString
 *     }
 *
 * * A {@link Ext.view.Table TableView} uses its `rowTpl` to emit a `<tr>` HTML tag to its output
 * stream. To render cells, it invokes {@link Ext.view.Table#renderCell} passing the `rows` member
 * of its data object.
 *
 * The `itemTpl` and `rowTpl`'s data object looks like this:
 *
 *     {
 *         view:        owningTableView,
 *         record:      recordToRender,
 *         recordIndex: indexOfRecordInStore,
 *         rowIndex:    indexOfRowInView,
 *         columns:     arrayOfColumnDefinitions,
 *         itemClasses: arrayOfClassNames, // For outermost row in case of wrapping
 *         rowClasses:  arrayOfClassNames,  // For internal data bearing row in case of wrapping
 *         rowStyle:    styleString
 *     }
 *
 * * A {@link Ext.view.Table TableView} uses its `cellTpl` to emit a `<td>` HTML tag to its output
 * stream.
 *
 * The `cellTpl's` data object looks like this:
 *
 *     {
 *         record: recordToRender
 *         column: columnToRender;
 *         recordIndex: indexOfRecordInStore,
 *         rowIndex:    indexOfRowInView,
 *         columnIndex: columnIndex,
 *         align: columnAlign,
 *         tdCls: classForCell
 *     }
 *
 * A Feature may inject its own tpl or rowTpl or cellTpl into the {@link Ext.view.Table TableView}'s
 * rendering by calling {@link Ext.view.Table#addTpl} or {@link Ext.view.Table#addRowTpl} or
 * {@link Ext.view.Table#addCellTpl}.
 *
 * The passed XTemplate is added *upstream* of the default template for the table element in a link
 * list of XTemplates which contribute to the element's HTML. It may emit appropriate HTML strings
 * into the output stream *around* a call to
 *
 *     this.nextTpl.apply(values, out, parent);
 *
 * This passes the current value context, output stream and the parent value context to the next
 * XTemplate in the list.
 *
 * @abstract
 */
Ext.define('Ext.grid.feature.Feature', {
    extend: 'Ext.util.Observable',
    alias: 'feature.feature',
    wrapsItem: false,
    /**
     * @property {Boolean} isFeature
     * `true` in this class to identify an object as an instantiated Feature, or subclass thereof.
     */
    isFeature: true,
    /**
     * True when feature is disabled.
     */
    disabled: false,
    /**
     * @property {Boolean}
     * Most features will expose additional events, some may not and will
     * need to change this to false.
     */
    hasFeatureEvent: true,
    /**
     * @property {String}
     * Prefix to use when firing events on the view.
     * For example a prefix of group would expose "groupclick", "groupcontextmenu", "groupdblclick".
     */
    eventPrefix: null,
    /**
     * @property {String}
     * Selector used to determine when to fire the event with the eventPrefix.
     */
    eventSelector: null,
    /**
     * @property {Ext.view.Table}
     * Reference to the TableView.
     */
    view: null,
    /**
     * @property {Ext.grid.Panel}
     * Reference to the grid panel
     */
    grid: null,
    /**
     * @property {Element} columnSizer
     * Contains a reference to the element which define the width of a column.
     *
     * This is used by the {@link Ext.view.TableLayout} when resizing columns.
     */
    columnSizer: null,
    constructor: function(config) {
        this.initialConfig = config;
        this.callParent(arguments);
    },
    clone: function() {
        return new this.self(this.initialConfig);
    },
    /**
     * Protected method called during {@link Ext.view.Table View} construction.  The 
     * owning {@link Ext.grid.Panel Grid} is passed as a param.
     * @param {Ext.grid.Panel} grid The View's owning Grid.  **Note** that in a 
     * {@link Ext.grid.Panel#cfg-enableLocking locking Grid} the passed grid will be 
     * either the normal grid or the locked grid, which is the view's direct owner.
     * @method
     * @protected
     */
    init: Ext.emptyFn,
    /**
     * Abstract method to be overriden when a feature should add additional
     * arguments to its event signature. By default the event will fire:
     *
     * - view - The underlying Ext.view.Table
     * - featureTarget - The matched element by the defined {@link #eventSelector}
     *
     * The method must also return the eventName as the first index of the array
     * to be passed to fireEvent.
     * @template
     */
    getFireEventArgs: function(eventName, view, featureTarget, e) {
        return [
            eventName,
            view,
            featureTarget,
            e
        ];
    },
    vetoEvent: Ext.emptyFn,
    /**
     * Enables the feature.
     */
    enable: function() {
        this.disabled = false;
    },
    /**
     * Disables the feature.
     */
    disable: function() {
        this.disabled = true;
    }
});

/**
 * A small abstract class that contains the shared behaviour for any summary
 * calculations to be used in the grid.
 */
Ext.define('Ext.grid.feature.AbstractSummary', {
    extend: 'Ext.grid.feature.Feature',
    alias: 'feature.abstractsummary',
    summaryRowCls: Ext.baseCSSPrefix + 'grid-row-summary',
    summaryRowSelector: '.' + Ext.baseCSSPrefix + 'grid-row-summary',
    readDataOptions: {
        recordCreator: Ext.identityFn
    },
    // High priority rowTpl interceptor which sees summary rows early, and renders them correctly
    // and then aborts the row rendering chain. This will only see action when summary rows
    // are being updated and Table.onUpdate->Table.bufferRender renders the individual
    // updated sumary row.
    summaryRowTpl: {
        fn: function(out, values, parent) {
            // If a summary record comes through the rendering pipeline,
            // render it simply instead of proceeding through the tplchain
            if (values.record.isSummary) {
                this.summaryFeature.outputSummaryRecord(values.record, values, out, parent);
            } else {
                this.nextTpl.applyOut(values, out, parent);
            }
        },
        priority: 1000
    },
    /**
     * @cfg {Boolean}
     * True to show the summary row.
     */
    showSummaryRow: true,
    // Listen for store updates. Eg, from an Editor.
    init: function() {
        var me = this;
        me.view.summaryFeature = me;
        me.rowTpl = me.view.self.prototype.rowTpl;
        // Add a high priority interceptor which renders summary records simply
        // This will only see action ona bufferedRender situation where summary records are updated.
        me.view.addRowTpl(me.summaryRowTpl).summaryFeature = me;
        // Define on the instance to store info needed by summary renderers.
        me.summaryData = {};
        me.groupInfo = {};
        // Cell widths in the summary table are set directly into the cells.
        // There's no <colgroup><col>
        // Some browsers use content box and some use border box when applying the style width
        // of a TD
        if (!me.summaryTableCls) {
            me.summaryTableCls = Ext.baseCSSPrefix + 'grid-item';
        }
        // We have been configured with another class. Revert to building the selector
        if (me.hasOwnProperty('summaryRowCls')) {
            me.summaryRowSelector = '.' + me.summaryRowCls;
        }
    },
    bindStore: function(grid, store) {
        var me = this;
        Ext.destroy(me.readerListeners);
        if (me.remoteRoot) {
            me.readerListeners = store.getProxy().getReader().on({
                scope: me,
                destroyable: true,
                rawdata: me.onReaderRawData
            });
        }
    },
    onReaderRawData: function(data) {
        // Invalidate potentially existing summaryRows to force recalculation
        this.summaryRows = null;
        this.readerRawData = data;
    },
    /**
     * Toggle whether or not to show the summary row.
     * @param {Boolean} visible True to show the summary row
     * @param fromLockingPartner (private)
     */
    toggleSummaryRow: function(visible, fromLockingPartner) {
        var me = this,
            prev = me.showSummaryRow,
            doRefresh;
        visible = visible != null ? !!visible : !me.showSummaryRow;
        me.showSummaryRow = visible;
        if (visible && visible !== prev) {
            // If being shown, something may have changed while not visible, so
            // force the summary records to recalculate
            me.updateSummaryRow = true;
        }
        // If there is another side to be toggled, then toggle it (as long as we are not
        // already being commanded from that other side);
        // Then refresh the whole arrangement.
        if (me.lockingPartner) {
            if (!fromLockingPartner) {
                me.lockingPartner.toggleSummaryRow(visible, true);
                doRefresh = true;
            }
        } else {
            doRefresh = true;
        }
        if (doRefresh) {
            me.grid.ownerGrid.getView().refreshView();
        }
    },
    createRenderer: function(column, record) {
        var me = this,
            ownerGroup = record.ownerGroup,
            summaryData = ownerGroup ? me.summaryData[ownerGroup] : me.summaryData,
            // Use the column.getItemId() for columns without a dataIndex.
            // The populateRecord method does the same.
            dataIndex = column.dataIndex || column.getItemId();
        return function(value, metaData) {
            return column.summaryRenderer ? column.summaryRenderer(record.data[dataIndex], summaryData, dataIndex, metaData) : // For no summaryRenderer, return the field value in the Feature record.
            record.data[dataIndex];
        };
    },
    outputSummaryRecord: function(summaryRecord, contextValues, out) {
        var view = contextValues.view,
            savedRowValues = view.rowValues,
            columns = contextValues.columns || view.headerCt.getVisibleGridColumns(),
            colCount = columns.length,
            i, column,
            // Set up a row rendering values object so that we can call the rowTpl directly
            // to inject the markup of a grid row into the output stream.
            values = {
                view: view,
                record: summaryRecord,
                rowStyle: '',
                rowClasses: [
                    this.summaryRowCls,
                    this.summaryItemCls
                ],
                itemClasses: [],
                recordIndex: -1,
                rowId: view.getRowId(summaryRecord),
                columns: columns
            };
        // Because we are using the regular row rendering pathway, temporarily swap out
        // the renderer for the summaryRenderer
        for (i = 0; i < colCount; i++) {
            column = columns[i];
            column.savedRenderer = column.renderer;
            if (column.summaryType || column.summaryRenderer) {
                column.renderer = this.createRenderer(column, summaryRecord);
            } else {
                column.renderer = Ext.emptyFn;
            }
        }
        // Use the base template to render a summary row
        view.rowValues = values;
        view.self.prototype.rowTpl.applyOut(values, out, parent);
        view.rowValues = savedRowValues;
        // Restore regular column renderers
        for (i = 0; i < colCount; i++) {
            column = columns[i];
            column.renderer = column.savedRenderer;
            column.savedRenderer = null;
        }
    },
    /**
     * Get the summary data for a field.
     * @private
     * @param {Ext.data.Store} store The store to get the data from
     * @param {String/Function} type The type of aggregation. If a function is specified it will
     * be passed to the stores aggregate function.
     * @param {String} field The field to aggregate on
     * @param {Ext.util.Group} group The group from which to calculate the value
     * @return {Number/String/Object} See the return type for the store functions.
     * if the group parameter is `true` An object is returned with a property named for each group
     * who's value is the summary value.
     */
    getSummary: function(store, type, field, group) {
        var isGrouped = !!group,
            item = isGrouped ? group : store;
        if (type) {
            if (Ext.isFunction(type)) {
                if (isGrouped) {
                    return item.aggregate(field, type);
                } else {
                    return item.aggregate(type, null, false, [
                        field
                    ]);
                }
            }
            switch (type) {
                case 'count':
                    return item.count();
                case 'min':
                    return item.min(field);
                case 'max':
                    return item.max(field);
                case 'sum':
                    return item.sum(field);
                case 'average':
                    return item.average(field);
                default:
                    return '';
            }
        }
    },
    getRawData: function() {
        var data = this.readerRawData;
        if (data) {
            return data;
        }
        // Synchronous Proxies such as Memory proxy will set keepRawData to true
        // on their Reader instances, and may have been loaded before we were bound
        // to the store. Or the Reader may have been configured with keepRawData: true
        // manually.
        // In these cases, the Reader should have rawData on the instance.
        return this.view.getStore().getProxy().getReader().rawData;
    },
    generateSummaryData: function(groupField) {
        var me = this,
            summaryRows = me.summaryRows,
            convertedSummaryRow = {},
            remoteData = {},
            storeReader, reader, rawData, i, len, rows, row;
        // Summary rows may have been cached by previous run
        if (!summaryRows) {
            rawData = me.getRawData();
            if (!rawData) {
                return;
            }
            // Construct a new Reader instance of the same type to avoid
            // munging the one in the Store
            storeReader = me.view.store.getProxy().getReader();
            reader = Ext.create('reader.' + storeReader.type, storeReader.getConfig());
            // reset reader root and rebuild extractors to extract summaries data
            reader.setRootProperty(me.remoteRoot);
            // At this point summaryRows is still raw data, e.g. XML node
            summaryRows = reader.getRoot(rawData);
            if (summaryRows) {
                rows = [];
                if (!Ext.isArray(summaryRows)) {
                    summaryRows = [
                        summaryRows
                    ];
                }
                len = summaryRows.length;
                for (i = 0; i < len; ++i) {
                    // Convert a raw data row into a Record's hash object using the Reader.
                    row = reader.extractRecordData(summaryRows[i], me.readDataOptions);
                    rows.push(row);
                }
                me.summaryRows = summaryRows = rows;
            }
            // By the next time the configuration may change
            reader.destroy();
            // We also no longer need the whole raw dataset
            me.readerRawData = null;
        }
        if (summaryRows) {
            for (i = 0 , len = summaryRows.length; i < len; i++) {
                convertedSummaryRow = summaryRows[i];
                if (groupField) {
                    remoteData[convertedSummaryRow[groupField]] = convertedSummaryRow;
                }
            }
        }
        return groupField ? remoteData : convertedSummaryRow;
    },
    setSummaryData: function(record, colId, summaryValue, groupName) {
        var summaryData = this.summaryData;
        if (groupName) {
            if (!summaryData[groupName]) {
                summaryData[groupName] = {};
            }
            summaryData[groupName][colId] = summaryValue;
        } else {
            summaryData[colId] = summaryValue;
        }
    },
    destroy: function() {
        Ext.destroy(this.readerListeners);
        this.readerRawData = this.summaryRows = null;
        this.callParent();
    }
});

/**
 * Private record store class which takes the place of the view's data store to provide a grouped
 * view of the data when the Grouping feature is used.
 *
 * Relays granular mutation events from the underlying store as refresh events to the view.
 *
 * On mutation events from the underlying store, updates the summary rows by firing update events
 * on the corresponding summary records.
 * @private
 */
Ext.define('Ext.grid.feature.GroupStore', {
    extend: 'Ext.util.Observable',
    isStore: true,
    // Number of records to load into a buffered grid before it has been bound to a view
    // of known size
    defaultViewSize: 100,
    // Use this property moving forward for all feature stores. It will be used to ensure
    // that the correct object is used to call various APIs. See EXTJSIV-10022.
    isFeatureStore: true,
    badGrouperKey: '[object Object]',
    constructor: function(groupingFeature, store) {
        var me = this;
        me.callParent();
        me.groupingFeature = groupingFeature;
        me.bindStore(store);
        // We don't want to listen to store events in a locking assembly.
        if (!groupingFeature.grid.isLocked) {
            me.bindViewStoreListeners();
        }
    },
    bindStore: function(store) {
        var me = this;
        if (!store || me.store !== store) {
            Ext.destroy(me.storeListeners);
            me.store = null;
        }
        if (store) {
            me.storeListeners = store.on({
                datachanged: me.onDataChanged,
                groupchange: me.onGroupChange,
                idchanged: me.onIdChanged,
                update: me.onUpdate,
                scope: me,
                destroyable: true
            });
            me.store = store;
            me.processStore(store);
        }
    },
    bindViewStoreListeners: function() {
        var view = this.groupingFeature.view,
            listeners = view.getStoreListeners(this);
        listeners.scope = view;
        this.on(listeners);
    },
    each: function(fn, scope, includeOptions) {
        this.store.each(fn, scope, includeOptions);
    },
    processStore: function(store) {
        var me = this,
            groupingFeature = me.groupingFeature,
            collapseAll = groupingFeature.startCollapsed,
            data = me.data,
            groups = store.getGroups(),
            groupCount = groups ? groups.length : 0,
            groupField = store.getGroupField(),
            metaGroup, i, featureGrouper, group, key;
        if (data) {
            data.clear();
        } else {
            data = me.data = new Ext.util.Collection({
                rootProperty: 'data',
                extraKeys: {
                    byInternalId: {
                        property: 'internalId',
                        rootProperty: ''
                    }
                }
            });
        }
        if (store.getCount()) {
            // Upon first process of a loaded store, clear the "always" collapse" flag
            groupingFeature.startCollapsed = false;
            if (groupCount > 0) {
                for (i = 0; i < groupCount; i++) {
                    group = groups.getAt(i);
                    // Cache group information by group name.
                    key = group.getGroupKey();
                    // If there is no store grouper and the groupField looks up a complex data type,
                    // the store will stringify it and the group name will be '[object Object]'.
                    // To fix this, groupers can be defined in the feature config, so we'll
                    // simply do a lookup here and re-group the store.
                    //
                    // Note that if a grouper wasn't defined on the feature that we'll just default
                    // to the old behavior and still try to group.
                    // eslint-disable-next-line max-len
                    if (me.badGrouperKey === key && (featureGrouper = groupingFeature.getGrouper(groupField))) {
                        // We must reset the value because store.group() will call
                        // into this method again!
                        store.getGroups().remove(group);
                        groupingFeature.startCollapsed = collapseAll;
                        store.group(featureGrouper);
                        return;
                    }
                    metaGroup = groupingFeature.getMetaGroup(group);
                    // This is only set at initialization time to handle startCollapsed
                    if (collapseAll) {
                        metaGroup.isCollapsed = collapseAll;
                    }
                    // Collapsed group - add the group's placeholder.
                    if (metaGroup.isCollapsed) {
                        data.add(metaGroup.placeholder);
                    } else // Expanded group - add the group's child records.
                    {
                        data.insert(me.data.length, group.items);
                    }
                }
            } else {
                data.add(store.getRange());
            }
        }
    },
    isCollapsed: function(name) {
        return this.groupingFeature.getCache()[name].isCollapsed;
    },
    isLoading: function() {
        return false;
    },
    getData: function() {
        return this.data;
    },
    getCount: function() {
        return this.data.getCount();
    },
    getTotalCount: function() {
        return this.data.getCount();
    },
    // This class is only created for fully loaded, non-buffered stores
    rangeCached: function(start, end) {
        return end < this.getCount();
    },
    getRange: function(start, end, options) {
        // Collection's getRange is exclusive.
        // Do NOT mutate the value: it is passed to the callback.
        var result = this.data.getRange(start, Ext.isNumber(end) ? end + 1 : end);
        if (options && options.callback) {
            options.callback.call(options.scope || this, result, start, end, options);
        }
        return result;
    },
    getAt: function(index) {
        return this.data.getAt(index);
    },
    /**
     * Get the Record with the specified id.
     *
     * This method is not affected by filtering, lookup will be performed from all records
     * inside the store, filtered or not.
     *
     * @param {Mixed} id The id of the Record to find.
     * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.
     */
    getById: function(id) {
        return this.store.getById(id);
    },
    /**
     * @private
     * Get the Record with the specified internalId.
     *
     * This method is not effected by filtering, lookup will be performed from all records
     * inside the store, filtered or not.
     *
     * @param {Mixed} internalId The id of the Record to find.
     * @return {Ext.data.Model} The Record with the passed internalId. Returns null if not found.
     */
    getByInternalId: function(internalId) {
        // Find the record in the base store.
        // If it was a placeholder, then it won't be there, it will be in our data Collection.
        return this.store.getByInternalId(internalId) || this.data.byInternalId.get(internalId);
    },
    expandGroup: function(group) {
        var me = this,
            groupingFeature = me.groupingFeature,
            lockingPartner = groupingFeature.lockingPartner,
            metaGroup, placeholder, startIdx, items;
        if (typeof group === 'string') {
            group = groupingFeature.getGroup(group);
        }
        if (group) {
            items = group.items;
            metaGroup = groupingFeature.getMetaGroup(group);
            placeholder = metaGroup.placeholder;
        }
        if (items.length && (startIdx = me.data.indexOf(placeholder)) !== -1) {
            // Any event handlers must see the new state
            metaGroup.isCollapsed = false;
            if (lockingPartner) {
                lockingPartner.getMetaGroup(group).isCollapsed = false;
            }
            me.isExpandingOrCollapsing = 1;
            // Remove the collapsed group placeholder record
            me.data.removeAt(startIdx);
            // Insert the child records in its place
            me.data.insert(startIdx, group.items);
            // Update views
            me.fireEvent('replace', me, startIdx, [
                placeholder
            ], group.items);
            me.fireEvent('groupexpand', me, group);
            me.isExpandingOrCollapsing = 0;
        }
    },
    collapseGroup: function(group) {
        var me = this,
            groupingFeature = me.groupingFeature,
            lockingPartner = groupingFeature.lockingPartner,
            startIdx, placeholder, len, items;
        if (typeof group === 'string') {
            group = groupingFeature.getGroup(group);
        }
        if (group) {
            items = group.items;
        }
        if (items && (len = items.length) && (startIdx = me.data.indexOf(items[0])) !== -1) {
            // Any event handlers must see the new state
            groupingFeature.getMetaGroup(group).isCollapsed = true;
            if (lockingPartner) {
                lockingPartner.getMetaGroup(group).isCollapsed = true;
            }
            me.isExpandingOrCollapsing = 2;
            // Remove the group child records
            me.data.removeAt(startIdx, len);
            // Insert a placeholder record in their place
            me.data.insert(startIdx, placeholder = me.getGroupPlaceholder(group));
            // Update views
            me.fireEvent('replace', me, startIdx, items, [
                placeholder
            ]);
            me.fireEvent('groupcollapse', me, group);
            me.isExpandingOrCollapsing = 0;
        }
    },
    getGroupPlaceholder: function(group) {
        var metaGroup = this.groupingFeature.getMetaGroup(group);
        if (!metaGroup.placeholder) {
            // eslint-disable-next-line vars-on-top, one-var
            var store = this.store,
                Model = store.getModel(),
                modelData = {},
                key = group.getGroupKey(),
                groupPlaceholder;
            modelData[store.getGroupField()] = key;
            groupPlaceholder = metaGroup.placeholder = new Model(modelData, store.session, true);
            groupPlaceholder.isNonData = groupPlaceholder.isCollapsedPlaceholder = true;
            // Adding the groupKey instead of storing a reference to the group
            // itself. The latter can cause problems if the store is reloaded and the referenced
            // group is lost. See EXTJS-18655
            groupPlaceholder.groupKey = key;
        }
        return metaGroup.placeholder;
    },
    // Find index of record in group store.
    // If it's in a collapsed group, then it's -1, not present
    indexOf: function(record) {
        var ret = -1;
        if (record && !record.isCollapsedPlaceholder) {
            ret = this.data.indexOf(record);
        }
        return ret;
    },
    contains: function(record) {
        return this.indexOf(record) > -1;
    },
    indexOfPlaceholder: function(record) {
        return this.data.indexOf(record);
    },
    /**
     * Get the index within the store of the Record with the passed id.
     *
     * Like #indexOf, this method is effected by filtering.
     *
     * @param {String} id The id of the Record to find.
     * @return {Number} The index of the Record. Returns -1 if not found.
     */
    indexOfId: function(id) {
        return this.data.indexOfKey(id);
    },
    /**
     * Get the index within the entire dataset. From 0 to the totalCount.
     *
     * Like #indexOf, this method is effected by filtering.
     *
     * @param {Ext.data.Model} record The Ext.data.Model object to find.
     * @return {Number} The index of the passed Record. Returns -1 if not found.
     */
    indexOfTotal: function(record) {
        return this.store.indexOf(record);
    },
    onIdChanged: function(store, rec, oldId, newId) {
        this.data.updateKey(rec, oldId);
    },
    onUpdate: function(store, record, operation, modifiedFieldNames) {
        var me = this,
            groupingFeature = me.groupingFeature,
            group, metaGroup, firstRec, lastRec, items;
        // The grouping field value has been modified.
        // This could either move a record from one group to another, or introduce a new group.
        // Either way, we have to refresh the grid
        if (store.isGrouped()) {
            // Updating a single record, attach the group to the record for Grouping.setupRowData
            // to use.
            group = record.group = groupingFeature.getGroup(record);
            // Make sure that still we have a group and that the last member of it
            // wasn't just filtered. See EXTJS-18083.
            if (group) {
                metaGroup = groupingFeature.getMetaGroup(record);
                if (modifiedFieldNames && Ext.Array.contains(modifiedFieldNames, groupingFeature.getGroupField())) {
                    me.onDataChanged();
                    delete record.group;
                    return;
                }
                // Fire an update event on the collapsed metaGroup placeholder record
                if (metaGroup.isCollapsed) {
                    me.fireEvent('update', me, metaGroup.placeholder);
                } else // Not in a collapsed group, fire update event on the modified record
                // and, if in a grouped store, on the first and last records in the group.
                {
                    Ext.suspendLayouts();
                    // Propagate the record's update event
                    me.fireEvent('update', me, record, operation, modifiedFieldNames);
                    // Fire update event on first and last record in group (only once
                    // if a single row group)
                    // So that custom header TPL is applied, and the summary row is updated
                    items = group.items;
                    firstRec = items[0];
                    lastRec = items[items.length - 1];
                    // Fire an update on the first and last row in the group (ensure we don't refire
                    // update on the modified record). This is to give interested Features
                    // the opportunity to update the first item (a wrapped group header + data row),
                    // and last item (a wrapped data row + group summary)
                    if (firstRec !== record) {
                        firstRec.group = group;
                        me.fireEvent('update', me, firstRec, 'edit', modifiedFieldNames);
                        delete firstRec.group;
                    }
                    if (lastRec !== record && lastRec !== firstRec && groupingFeature.showSummaryRow) {
                        lastRec.group = group;
                        me.fireEvent('update', me, lastRec, 'edit', modifiedFieldNames);
                        delete lastRec.group;
                    }
                    Ext.resumeLayouts(true);
                }
            }
            delete record.group;
        } else {
            // Propagate the record's update event
            me.fireEvent('update', me, record, operation, modifiedFieldNames);
        }
    },
    // Relay the groupchange event
    onGroupChange: function(store, grouper) {
        if (!grouper) {
            this.processStore(store);
        }
        this.fireEvent('groupchange', store, grouper);
    },
    onDataChanged: function() {
        this.processStore(this.store);
        this.fireEvent('refresh', this);
    },
    destroy: function() {
        var me = this;
        me.bindStore(null);
        Ext.destroy(me.data);
        me.groupingFeature = null;
        me.callParent();
    }
});

/**
 * This feature allows to display the grid rows aggregated into groups as specified by the
 * {@link Ext.data.Store#grouper grouper} underneath. The groups can also be expanded and collapsed.
 *
 * ## Extra Events
 *
 * This feature adds several extra events that will be fired on the grid to interact
 * with the groups:
 *
 *  - {@link #groupclick}
 *  - {@link #groupdblclick}
 *  - {@link #groupcontextmenu}
 *  - {@link #groupexpand}
 *  - {@link #groupcollapse}
 *
 * ## Menu Augmentation
 *
 * This feature adds extra options to the grid column menu to provide the user with functionality
 * to modify the grouping. This can be disabled by setting the {@link #enableGroupingMenu} option.
 * The option to disallow grouping from being turned off by the user is {@link #enableNoGroups}.
 *
 * ## Controlling Group Text
 *
 * The {@link #groupHeaderTpl} is used to control the rendered title for each group. It can modified
 * to customized the default display.
 *
 * ## Groupers
 *
 * By default, this feature expects that the data field that is mapped to by the 
 * {@link Ext.data.AbstractStore#groupField} config is a simple data type such as a 
 * String or a Boolean. However, if you intend to group by a data field that is a 
 * complex data type such as an Object or Array, it is necessary to define one or more 
 * {@link Ext.util.Grouper groupers} on the feature that it can then use to lookup 
 * internal group information when grouping by different fields.
 *
 *     var feature = Ext.create('Ext.grid.feature.Grouping', {
 *         startCollapsed: true,
 *         groupers: [{
 *             property: 'asset',
 *             groupFn: function(val) {
 *                 return val.data.name;
 *             }
 *         }]
 *     });
 *
 * ## Example Usage
 *
 *     @example
 *     var store = Ext.create('Ext.data.Store', {
 *         fields: ['name', 'seniority', 'department'],
 *         groupField: 'department',
 *         data: [
 *             { name: 'Michael Scott', seniority: 7, department: 'Management' },
 *             { name: 'Dwight Schrute', seniority: 2, department: 'Sales' },
 *             { name: 'Jim Halpert', seniority: 3, department: 'Sales' },
 *             { name: 'Kevin Malone', seniority: 4, department: 'Accounting' },
 *             { name: 'Angela Martin', seniority: 5, department: 'Accounting' }
 *         ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Employees',
 *         store: store,
 *         columns: [
 *             { text: 'Name', dataIndex: 'name', flex: 1 },
 *             { text: 'Seniority', dataIndex: 'seniority', flex: 1 }
 *         ],
 *         features: [{ftype:'grouping'}],
 *         renderTo: Ext.getBody()
 *     });
 *
 * **Note:** To use grouping with a grid that has
 * {@link Ext.grid.column.Column#locked locked columns}, you need to supply the grouping feature
 * as a config object - so the grid can create two instances of the grouping feature.
 * 
 * ## Grouping Summary
 * 
 * An aggregate summary row may be added at the bottom of each group by setting 
 * {@link #cfg-showSummaryRow showSummaryRow} to `true`. There are two 
 * aspects to the summary:
 *
 * ## Calculation
 *
 * The summary value needs to be calculated for each column in the grid. This is 
 * controlled by the {@link Ext.grid.column.Column#cfg-summaryType summaryType} option 
 * specified on the column. There are several built in summary types which can be 
 * specified as a string on the column configuration. These call underlying methods on 
 * the store:
 *
 *  - {@link Ext.data.Store#method-count count}
 *  - {@link Ext.data.Store#method-sum sum}
 *  - {@link Ext.data.Store#method-min min}
 *  - {@link Ext.data.Store#method-max max}
 *  - {@link Ext.data.Store#method-average average}
 *
 * Alternatively, the summaryType can be a function definition. If this is the case,
 * the function is called with two parameters:
 * 
 *  - records {Array} - an array of records
 *  - values {Array} - an array of field values to calculate the summary value
 *
 * ## Rendering
 *
 * Similar to a column, the summary also supports a 
 * {@link Ext.grid.column.Column.html#cfg-summaryRenderer summaryRenderer} function. This
 * summaryRenderer is called before displaying a value. The function is optional.  If
 * not specified, the default calculated value is shown. The summaryRenderer is called 
 * with:
 *
 *  - value {Object} - The calculated value.
 *  - summaryData {Object} - Contains all raw summary values for the row.
 *  - field {String} - The name of the field we are calculating
 *  - metaData {Object} - A collection of metadata about the current cell; can be used or 
 * modified by the renderer.
 *
 * ## Example Usage
 *
 *     @example
 *     Ext.define('TestResult', {
 *         extend: 'Ext.data.Model',
 *         fields: ['student', 'subject', {
 *             name: 'mark',
 *             type: 'int'
 *         }]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         width: 200,
 *         height: 240,
 *         renderTo: document.body,
 *         features: [{
 *             ftype: 'grouping',
 *             groupHeaderTpl: 'Subject: {name}',
 *             showSummaryRow: true
 *         }],
 *         store: {
 *             model: 'TestResult',
 *             groupField: 'subject',
 *             data: [{
 *                 student: 'Student 1',
 *                 subject: 'Math',
 *                 mark: 84
 *             },{
 *                 student: 'Student 1',
 *                 subject: 'Science',
 *                 mark: 72
 *             },{
 *                 student: 'Student 2',
 *                 subject: 'Math',
 *                 mark: 96
 *             },{
 *                 student: 'Student 2',
 *                 subject: 'Science',
 *                 mark: 68
 *             }]
 *         },
 *         columns: [{
 *             dataIndex: 'student',
 *             text: 'Name',
 *             summaryType: 'count',
 *             summaryRenderer: function(value){
 *                 return Ext.String.format('{0} student{1}', value, value !== 1 ? 's' : '');
 *             }
 *         }, {
 *             dataIndex: 'mark',
 *             text: 'Mark',
 *             summaryType: 'average'
 *         }]
 *     });
 */
Ext.define('Ext.grid.feature.Grouping', {
    extend: 'Ext.grid.feature.Feature',
    alias: 'feature.grouping',
    mixins: {
        summary: 'Ext.grid.feature.AbstractSummary'
    },
    requires: [
        'Ext.grid.feature.GroupStore'
    ],
    eventPrefix: 'group',
    eventSelector: '.' + Ext.baseCSSPrefix + 'grid-group-hd',
    refreshData: {},
    wrapsItem: true,
    /**
     * @event groupclick
     * @param {Ext.view.Table} view
     * @param {HTMLElement} node
     * @param {String} group The name of the group
     * @param {Ext.event.Event} e
     */
    /**
     * @event groupdblclick
     * @param {Ext.view.Table} view
     * @param {HTMLElement} node
     * @param {String} group The name of the group
     * @param {Ext.event.Event} e
     */
    /**
     * @event groupcontextmenu
     * @param {Ext.view.Table} view
     * @param {HTMLElement} node
     * @param {String} group The name of the group
     * @param {Ext.event.Event} e
     */
    /**
     * @event groupcollapse
     * @param {Ext.view.Table} view
     * @param {HTMLElement} node
     * @param {String} group The name of the group
     */
    /**
     * @event groupexpand
     * @param {Ext.view.Table} view
     * @param {HTMLElement} node
     * @param {String} group The name of the group
     */
    /**
     * @cfg {String/Array/Ext.Template} groupHeaderTpl
     * A string Template snippet, an array of strings (optionally followed by an object
     * containing Template methods) to be used to construct a Template, or a Template instance.
     *
     * - Example 1 (Template snippet):
     *
     *       groupHeaderTpl: 'Group: {name}'
     *
     * - Example 2 (Array):
     *
     *       groupHeaderTpl: [
     *           'Group: ',
     *           '<div>{name:this.formatName}</div>',
     *           {
     *               formatName: function(name) {
     *                   return Ext.String.trim(name);
     *               }
     *           }
     *       ]
     *
     * - Example 3 (Template Instance):
     *
     *       groupHeaderTpl: Ext.create('Ext.XTemplate',
     *           'Group: ',
     *           '<div>{name:this.formatName}</div>',
     *           {
     *               formatName: function(name) {
     *                   return Ext.String.trim(name);
     *               }
     *           }
     *       )
     *
     * @cfg {String} groupHeaderTpl.groupField The field name being grouped by.
     * @cfg {String} groupHeaderTpl.columnName The column header associated with the field
     * being grouped by *if there is a column for the field*, falls back to the groupField name.
     * @cfg {Mixed}  groupHeaderTpl.groupValue The value of the
     * {@link Ext.data.Store#groupField groupField} for the group header being rendered.
     * @cfg {String} groupHeaderTpl.renderedGroupValue The rendered value of the
     * {@link Ext.data.Store#groupField groupField} for the group header being rendered,
     * as produced by the column renderer.
     * @cfg {String} groupHeaderTpl.name An alias for renderedGroupValue
     * @cfg {Ext.data.Model[]} groupHeaderTpl.rows Deprecated - use children instead.
     * An array containing the child records for the group being rendered.
     * *Not available if the store is a {@link Ext.data.BufferedStore BufferedStore}*
     * @cfg {Ext.data.Model[]} groupHeaderTpl.children An array containing the child records
     * for the group being rendered. *Not available if the store is a
     * {@link Ext.data.BufferedStore BufferedStore}*
     */
    groupHeaderTpl: '{columnName}: {name}',
    /**
     * @cfg {Number} [depthToIndent=17]
     * Number of pixels to indent per grouping level
     */
    depthToIndent: 17,
    collapsedCls: Ext.baseCSSPrefix + 'grid-group-collapsed',
    hdCollapsedCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsed',
    hdNotCollapsibleCls: Ext.baseCSSPrefix + 'grid-group-hd-not-collapsible',
    collapsibleCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsible',
    ctCls: Ext.baseCSSPrefix + 'group-hd-container',
    /**
     * @cfg {String} [groupByText="Group by this field"]
     * Text displayed in the grid header menu for grouping by header.
     * @locale
     */
    groupByText: 'Group by this field',
    /**
     * @cfg {String} [showGroupsText="Show in groups"]
     * Text displayed in the grid header for enabling/disabling grouping.
     * @locale
     */
    showGroupsText: 'Show in groups',
    /**
     * @cfg {Boolean} [hideGroupedHeader=false]
     * True to hide the header that is currently grouped.
     */
    hideGroupedHeader: false,
    /**
     * @cfg {Boolean} [startCollapsed=false]
     * True to start all groups collapsed.
     */
    startCollapsed: false,
    /**
     * @cfg {Boolean} [enableGroupingMenu=true]
     * True to enable the grouping control in the header menu.
     */
    enableGroupingMenu: true,
    /**
     * @cfg {Boolean} [enableNoGroups=true]
     * True to allow the user to turn off grouping.
     */
    enableNoGroups: true,
    /**
     * @cfg {Boolean} [collapsible=true]
     * Set to `false` to disable collapsing groups from the UI.
     *
     * This is set to `false` when the associated {@link Ext.data.Store store} is
     * a {@link Ext.data.BufferedStore BufferedStore}.
     */
    collapsible: true,
    /**
     * @cfg {Array} [groupers=null]
     * These are grouper objects defined for the feature. If the group names are derived
     * from complex data types, it is necessary to convert them as a store would.
     *
     * However, since only one grouper can be defined on the store at a time and
     * this feature clears the current grouper when a new one is added, it is
     * necessary to define a cache of groupers that the feature can lookup as needed.
     *
     * Expected grouper object properties are `property` and `groupFn`.
     */
    groupers: null,
    /**
     * @cfg {String} expandTip
     * The tooltip for the group expander tool when the group is collapsed.
     * @locale
     */
    expandTip: 'Click to expand. CTRL key collapses all others',
    /**
     * @cfg {String} collapseTip
     * The tooltip for the group expander tool when the group is expanded.
     * @locale
     */
    collapseTip: 'Click to collapse. CTRL/click collapses all others',
    /**
     * `True` to display a summary row for each grouping.
     * 
     * **Note:** Requires the setting of 
     * {@link Ext.grid.column.Column#cfg-summaryType summaryType} on each participating 
     * grid column.  To specify the summary text you may configure the column's 
     * {@link Ext.grid.column.Column#cfg-summaryRenderer summaryRenderer} option.  See 
     * the Grouping Summary heading in the Grouping class description for a working 
     * example.
     */
    showSummaryRow: false,
    /* eslint-disable indent, max-len */
    outerTpl: [
        '{%',
        // Set up the grouping unless we are disabled, or it's just a summary record
        'if (!(this.groupingFeature.disabled || values.rows.length === 1 && values.rows[0].isSummary)) {',
        'this.groupingFeature.setup(values.rows, values.view.rowValues);',
        '}',
        // Process the item
        'this.nextTpl.applyOut(values, out, parent);',
        // Clean up the grouping unless we are disabled, or it's just a summary record
        'if (!(this.groupingFeature.disabled || values.rows.length === 1 && values.rows[0].isSummary)) {',
        'this.groupingFeature.cleanup(values.rows, values.view.rowValues);',
        '}',
        '%}',
        {
            priority: 200
        }
    ],
    groupRowTpl: [
        '{%',
        'var me = this.groupingFeature,',
        'colspan = "colspan=" + values.columns.length;',
        // If grouping is disabled or it's just a summary record, do not call setupRowData, and do not wrap
        'if (me.disabled || parent.rows.length === 1 && parent.rows[0].isSummary) {',
        'values.needsWrap = false;',
        '} else {',
        // setupRowData requires the index in the data source, not the index in the real store
        'me.setupRowData(values.record, values.rowIndex, values);',
        '}',
        '%}',
        '<tpl if="needsWrap">',
        '<tpl if="isFirstRow">',
        // MUST output column sizing elements because the first row in this table
        // contains one colspanning TD, and that overrides subsequent column width settings.
        '{% values.view.renderColumnSizer(values, out); %}',
        '<tr data-boundView="{view.id}" data-recordId="{record.internalId:htmlEncode}" data-recordIndex="{[values.isCollapsedGroup ? -1 : values.recordIndex]}" class="{groupHeaderCls}">',
        '<td class="{[me.ctCls]}" {[colspan]}>',
        '{%',
        // Group title is visible if not locking, or we are the locked side, or the locked side has no columns/
        // Use visibility to keep row heights synced without intervention.
        'var groupTitleStyle = (!values.view.lockingPartner || (values.view.ownerCt === values.view.ownerCt.ownerLockable.lockedGrid) || (values.view.lockingPartner.headerCt.getVisibleGridColumns().length === 0)) ? "" : "visibility:hidden",',
        'tooltip = "";',
        // Only display a tooltip if the group is collapsible
        'if (me.collapsible) {',
        'tooltip = Ext.String.format(\'data-qtip="{0}"\', values.isCollapsedGroup ? me.expandTip : me.collapseTip);',
        '}',
        '%}',
        // TODO. Make the group header tabbable with tabIndex="0" and enable grid navigation "Action Mode"
        // to activate it.
        '<div data-groupname="{groupName:htmlEncode}" class="',
        Ext.baseCSSPrefix,
        'grid-group-hd {collapsibleCls}" nottabindex="0" hidefocus="on" {ariaCellInnerAttr}>',
        '<div class="',
        Ext.baseCSSPrefix,
        'grid-group-title" style="{[groupTitleStyle]}" {ariaGroupTitleAttr} {[tooltip]}>',
        '{[values.groupHeaderTpl.apply(values.groupRenderInfo, parent) || "&#160;"]}',
        '</div>',
        '</div>',
        '</td>',
        '</tr>',
        '</tpl>',
        // Only output the first row if this is *not* a collapsed group
        '<tpl if="!isCollapsedGroup">',
        '{%',
        'values.itemClasses.length = 0;',
        'this.nextTpl.applyOut(values, out, parent);',
        '%}',
        '</tpl>',
        '<tpl if="summaryRecord">',
        '{%me.outputSummaryRecord(values.summaryRecord, values, out, parent);%}',
        '</tpl>',
        '<tpl else>',
        '{%this.nextTpl.applyOut(values, out, parent);%}',
        '</tpl>',
        {
            priority: 200,
            beginRowSync: function(rowSync) {
                var groupingFeature = this.groupingFeature;
                rowSync.add('header', groupingFeature.eventSelector);
                rowSync.add('summary', groupingFeature.summaryRowSelector);
            },
            syncContent: function(destRow, sourceRow, columnsToUpdate) {
                destRow = Ext.fly(destRow, 'syncDest');
                sourceRow = Ext.fly(sourceRow, 'syncSrc');
                // eslint-disable-next-line vars-on-top
                var groupingFeature = this.groupingFeature,
                    destHd = destRow.down(groupingFeature.eventSelector, true),
                    sourceHd = sourceRow.down(groupingFeature.eventSelector, true),
                    destSummaryRow = destRow.down(groupingFeature.summaryRowSelector, true),
                    sourceSummaryRow = sourceRow.down(groupingFeature.summaryRowSelector, true);
                // Sync the content of header element.
                if (destHd && sourceHd) {
                    Ext.fly(destHd).syncContent(sourceHd);
                }
                // Sync just the updated columns in the summary row.
                if (destSummaryRow && sourceSummaryRow) {
                    // If we were passed a column set, only update them
                    if (columnsToUpdate) {
                        this.groupingFeature.view.updateColumns(destSummaryRow, sourceSummaryRow, columnsToUpdate);
                    } else {
                        Ext.fly(destSummaryRow).syncContent(sourceSummaryRow);
                    }
                }
            }
        }
    ],
    /* eslint-enable indent, max-len */
    relayedEvents: [
        'groupcollapse',
        'groupexpand'
    ],
    init: function(grid) {
        var me = this,
            view = me.view,
            store = me.gridStore = grid.getStore(),
            dataSource;
        view.isGrouping = store.isGrouped();
        me.mixins.summary.init.call(me);
        me.callParent([
            grid
        ]);
        view.headerCt.on({
            columnhide: me.onColumnHideShow,
            columnshow: me.onColumnHideShow,
            columnmove: me.onColumnMove,
            scope: me
        });
        // Add a table level processor
        view.addTpl(Ext.XTemplate.getTpl(me, 'outerTpl')).groupingFeature = me;
        // Add a row level processor
        view.addRowTpl(Ext.XTemplate.getTpl(me, 'groupRowTpl')).groupingFeature = me;
        view.preserveScrollOnRefresh = true;
        // Sparse store - we can never collapse groups
        if (store.isBufferedStore) {
            me.collapsible = false;
        }
        // If it's a local store we can build a grouped store for use as the view's dataSource
        else if (!store.isEmptyStore) {
            dataSource = me.createDataSource();
        }
        grid = grid.ownerLockable || grid;
        // Before the reconfigure, rebind our GroupStore dataSource to the new store
        grid.on('beforereconfigure', me.beforeReconfigure, me);
        if (!view.isLockedView) {
            me.gridEventRelayers = grid.relayEvents(view, me.relayedEvents);
        }
        view.on({
            afterrender: me.afterViewRender,
            scope: me,
            single: true
        });
        me.groupRenderInfo = {};
        if (store.isEmptyStore) {
            return;
        } else if (dataSource) {
            // Listen to dataSource groupchange so it has a chance to do any processing
            // before we react to it
            dataSource.on('groupchange', me.onGroupChange, me);
        } else {
            me.setupStoreListeners(store);
        }
        me.mixins.summary.bindStore.call(me, grid, grid.getStore());
    },
    getGridStore: function() {
        return this.gridStore;
    },
    indexOf: function(record) {
        if (record.isCollapsedPlaceholder) {
            return this.dataSource.indexOfPlaceholder(record);
        }
        return this.dataSource.indexOf(record);
    },
    indexOfPlaceholder: function(record) {
        return this.dataSource.indexOfPlaceholder(record);
    },
    isInCollapsedGroup: function(record) {
        var me = this,
            store = me.getGridStore(),
            result = false,
            metaGroup;
        if (store.isGrouped() && (metaGroup = me.getMetaGroup(record))) {
            result = !!(metaGroup && metaGroup.isCollapsed);
        }
        return result;
    },
    getCache: function() {
        var me = this,
            id = me.getId(),
            metaGroupCache = {},
            groups = this.getGridStore().getGroups(),
            groupingContext;
        // Our group contextual data is stored as an expand on the Ext.util.Group
        if (groups) {
            groups.eachKey(function(key, group) {
                groupingContext = group.$groupingContext || (group.$groupingContext = {});
                metaGroupCache[key] = groupingContext[id];
            });
        }
        return metaGroupCache;
    },
    invalidateCache: function() {
        var me = this,
            id = me.getId(),
            groups = me.getGridStore().getGroups(),
            groupingContext;
        // Our group contextual data is stored as an expand on the Ext.util.Group
        if (groups) {
            groups.eachKey(function(key, group) {
                groupingContext = group.$groupingContext;
                if (groupingContext) {
                    groupingContext[id] = null;
                }
            });
        }
    },
    vetoEvent: function(record, row, rowIndex, e) {
        var shouldVeto = false;
        // Do not veto mouseover/mouseout
        if (e.type !== 'mouseover' && e.type !== 'mouseout' && e.type !== 'mouseenter' && e.type !== 'mouseleave' && e.getTarget(this.eventSelector)) {
            shouldVeto = true;
        }
        if (this.showSummaryRow && !shouldVeto && e.getTarget(this.summaryRowSelector)) {
            shouldVeto = true;
        }
        if (shouldVeto) {
            return false;
        }
    },
    enable: function() {
        var me = this,
            view = me.view,
            store = me.getGridStore(),
            currentGroupedHeader = me.hideGroupedHeader && me.getGroupedHeader(),
            groupToggleMenuItem;
        view.isGrouping = true;
        if (view.lockingPartner) {
            view.lockingPartner.isGrouping = true;
        }
        me.callParent();
        if (me.lastGrouper) {
            store.group(me.lastGrouper);
            me.lastGrouper = null;
        }
        // Update the UI.
        if (currentGroupedHeader) {
            currentGroupedHeader.hide();
        }
        groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
        if (groupToggleMenuItem) {
            groupToggleMenuItem.setChecked(true, true);
        }
    },
    disable: function() {
        var me = this,
            view = me.view,
            store = me.getGridStore(),
            currentGroupedHeader = me.hideGroupedHeader && me.getGroupedHeader(),
            lastGrouper = store.getGrouper(),
            groupToggleMenuItem;
        view.isGrouping = false;
        if (view.lockingPartner) {
            view.lockingPartner.isGrouping = false;
        }
        me.callParent();
        if (lastGrouper) {
            me.lastGrouper = lastGrouper;
            store.clearGrouping();
        }
        // Update the UI.
        if (currentGroupedHeader) {
            currentGroupedHeader.show();
        }
        groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
        if (groupToggleMenuItem) {
            groupToggleMenuItem.setChecked(false, true);
            groupToggleMenuItem.disable();
        }
    },
    // Attach events to view
    afterViewRender: function() {
        var me = this,
            view = me.view;
        view.on({
            scope: me,
            groupmousedown: me.onGroupMousedown,
            groupclick: me.onGroupClick
        });
        if (me.enableGroupingMenu) {
            me.injectGroupingMenu();
        }
        me.pruneGroupedHeader();
        me.lastGrouper = me.getGridStore().getGrouper();
        // If disabled in the config, disable now so the store load won't
        // send the grouping query params in the request.
        if (me.disabled) {
            me.disable();
        }
    },
    injectGroupingMenu: function() {
        var me = this,
            headerCt = me.view.headerCt;
        headerCt.showMenuBy = Ext.Function.createInterceptor(headerCt.showMenuBy, me.showMenuBy);
        headerCt.getMenuItems = me.getMenuItems();
    },
    onColumnHideShow: function(headerOwnerCt, header) {
        var me = this,
            view = me.view,
            headerCt = view.headerCt,
            menu = headerCt.getMenu(),
            activeHeader = menu.activeHeader,
            groupMenuItem = menu.down('#groupMenuItem'),
            groupMenuMethod,
            colCount = me.grid.getVisibleColumnManager().getColumns().length,
            items, len, i;
        // "Group by this field" must be disabled if there's only one column left visible.
        if (activeHeader && groupMenuItem) {
            groupMenuMethod = (activeHeader.groupable === false || !activeHeader.dataIndex || (me.view.headerCt.getVisibleGridColumns().length < 2)) ? 'disable' : 'enable';
            groupMenuItem[groupMenuMethod]();
        }
        // header containing TDs have to span all columns, hidden are just zero width
        // Also check the colCount on the off chance that they are all hidden
        if (view.rendered && colCount) {
            items = view.el.query('.' + me.ctCls);
            for (i = 0 , len = items.length; i < len; ++i) {
                items[i].colSpan = colCount;
            }
        }
    },
    // Update first and last records in groups when column moves
    // Because of the RowWrap template, this will update the groups' headers and footers
    onColumnMove: function() {
        var me = this,
            view = me.view,
            groupName, groupNames, group, firstRec, lastRec, metaGroup;
        if (view.getStore().isGrouped()) {
            groupNames = me.getCache();
            Ext.suspendLayouts();
            for (groupName in groupNames) {
                group = me.getGroup(groupName);
                if (group) {
                    firstRec = group.first();
                    lastRec = group.last();
                    metaGroup = me.getMetaGroup(group);
                    if (metaGroup.isCollapsed) {
                        firstRec = lastRec = me.dataSource.getGroupPlaceholder(groupName);
                    }
                    view.refreshNode(firstRec);
                    if (me.showSummaryRow && lastRec !== firstRec) {
                        view.refreshNode(lastRec);
                    }
                }
            }
            Ext.resumeLayouts(true);
        }
    },
    showMenuBy: function(clickEvent, t, header) {
        var me = this,
            menu = me.getMenu(),
            groupMenuItem = menu.down('#groupMenuItem'),
            groupMenuMethod = (header.groupable === false || !header.dataIndex || (me.view.headerCt.getVisibleGridColumns().length < 2)) ? 'disable' : 'enable',
            groupToggleMenuItem = menu.down('#groupToggleMenuItem'),
            isGrouped = me.grid.getStore().isGrouped();
        groupMenuItem[groupMenuMethod]();
        if (groupToggleMenuItem) {
            groupToggleMenuItem.setChecked(isGrouped, true);
            groupToggleMenuItem[isGrouped ? 'enable' : 'disable']();
        }
    },
    getMenuItems: function() {
        var me = this,
            groupByText = me.groupByText,
            disabled = me.disabled || !me.getGroupField(),
            showGroupsText = me.showGroupsText,
            enableNoGroups = me.enableNoGroups,
            getMenuItems = me.view.headerCt.getMenuItems;
        // runs in the scope of headerCt
        return function() {
            // We cannot use the method from HeaderContainer's prototype here
            // because other plugins or features may already have injected an implementation
            var o = getMenuItems.call(this);
            o.push('-', {
                iconCls: Ext.baseCSSPrefix + 'group-by-icon',
                itemId: 'groupMenuItem',
                text: groupByText,
                handler: me.onGroupMenuItemClick,
                scope: me
            });
            if (enableNoGroups) {
                o.push({
                    itemId: 'groupToggleMenuItem',
                    text: showGroupsText,
                    checked: !disabled,
                    checkHandler: me.onGroupToggleMenuItemClick,
                    scope: me
                });
            }
            return o;
        };
    },
    /**
     * Group by the header the user has clicked on.
     * @private
     */
    onGroupMenuItemClick: function(menuItem, e) {
        var me = this,
            menu = menuItem.parentMenu,
            hdr = menu.activeHeader,
            view = me.view,
            store = me.getGridStore();
        if (me.disabled) {
            me.lastGrouper = null;
            me.block();
            me.enable();
            me.unblock();
        }
        view.isGrouping = true;
        // First check if there is a grouper defined for the feature. This is necessary
        // when the value is a complex type.
        store.group(me.getGrouper(hdr.dataIndex) || hdr.dataIndex);
        me.pruneGroupedHeader();
    },
    block: function(fromPartner) {
        var me = this;
        me.blockRefresh = me.view.blockRefresh = true;
        if (me.lockingPartner && !fromPartner) {
            me.lockingPartner.block(true);
        }
    },
    unblock: function(fromPartner) {
        var me = this;
        me.blockRefresh = me.view.blockRefresh = false;
        if (me.lockingPartner && !fromPartner) {
            me.lockingPartner.unblock(true);
        }
    },
    /**
     * Turn on and off grouping via the menu
     * @private
     */
    onGroupToggleMenuItemClick: function(menuItem, checked) {
        this[checked ? 'enable' : 'disable']();
    },
    /**
     * Prunes the grouped header from the header container
     * @private
     */
    pruneGroupedHeader: function() {
        var me = this,
            header = me.getGroupedHeader();
        if (me.hideGroupedHeader && header) {
            Ext.suspendLayouts();
            if (me.prunedHeader && me.prunedHeader !== header) {
                me.prunedHeader.show();
            }
            me.prunedHeader = header;
            if (header.rendered) {
                header.hide();
            }
            Ext.resumeLayouts(true);
        }
    },
    getHeaderNode: function(groupName) {
        var el = this.view.getEl(),
            nodes, i, len, node;
        if (el) {
            // Don't htmlEncode the groupName here. The name in the attribute has already been
            // "decoded" so we don't need to do it.
            nodes = el.query(this.eventSelector);
            for (i = 0 , len = nodes.length; i < len; ++i) {
                node = nodes[i];
                if (node.getAttribute('data-groupName') === groupName) {
                    return node;
                }
            }
        }
    },
    getGroup: function(name) {
        var store = this.getGridStore(),
            value = name,
            groups, group;
        if (store.isGrouped()) {
            if (name.isModel) {
                name = name.get(store.getGroupField());
            }
            // If a complex type let's try to get the string from a groupFn.
            if (typeof name !== 'string') {
                name = store.getGrouper().getGroupString(value);
            }
            // BufferedStore does not own real Groups.
            if (store.isBufferedStore) {
                groups = store.groups || (store.groups = {});
                group = groups[name] || (groups[name] = {
                    getGroupKey: function() {
                        return name;
                    }
                });
            } else {
                group = store.getGroups().getByKey(name);
            }
        }
        return group;
    },
    // Groupers may be defined on the feature itself if the datIndex is a complex type.
    /**
     * @private
     *
     */
    getGrouper: function(dataIndex) {
        var groupers = this.groupers;
        if (!groupers) {
            return null;
        }
        return Ext.Array.findBy(groupers, function(grouper) {
            return grouper.property === dataIndex;
        });
    },
    getGroupField: function() {
        return this.getGridStore().getGroupField();
    },
    getMetaGroup: function(group) {
        var me = this,
            id = me.getId(),
            gridStore, key, metaGroup, Model, modelData, groupPlaceholder, aggregateRecord, groupingContext;
        // Get the group from a member record or the group name
        if (group.isModel || typeof group === 'string') {
            group = me.getGroup(group);
        }
        if (group) {
            key = group.getGroupKey();
            groupingContext = group.$groupingContext || (group.$groupingContext = {});
            metaGroup = groupingContext[id];
            if (!metaGroup) {
                gridStore = me.getGridStore();
                Model = gridStore.getModel();
                // Add a placeholder record which represents the group if collapsed.
                modelData = {};
                modelData[me.getGroupField()] = key;
                // Create the two records a group needs to produce a UI.
                // One to represent a collapsed group.
                // And one to append to the end of an expanded group.
                groupPlaceholder = new Model(modelData, gridStore.session, true);
                groupPlaceholder.isNonData = groupPlaceholder.isCollapsedPlaceholder = true;
                groupPlaceholder.groupKey = key;
                aggregateRecord = new Ext.data.Model(modelData);
                aggregateRecord.isNonData = aggregateRecord.isSummary = true;
                aggregateRecord.groupKey = key;
                metaGroup = groupingContext[id] = {
                    placeholder: groupPlaceholder,
                    isCollapsed: false,
                    lastGroup: null,
                    lastGroupGeneration: null,
                    lastFilterGeneration: null,
                    aggregateRecord: aggregateRecord
                };
            }
        }
        return metaGroup;
    },
    /**
     * Returns `true` if the named group is expanded.
     * @param {String} groupName The group name. This is the value of
     * the {@link Ext.data.Store#groupField groupField}.
     * @return {Boolean} `true` if the group defined by that value is expanded.
     */
    isExpanded: function(groupName) {
        return !this.getMetaGroup(groupName).isCollapsed;
    },
    /**
     * Expand a group
     * @param {String} groupName The group name.
     * @param {Object} [options] Pass when the group should be scrolled into view.
     * This contains flags for postProcessing the group's first row after
     * expansion. See {@link Ext.panel.Table#ensureVisible} for details. *note:*
     * a boolean may be passed to indicate whether to focus the target group after expand.
     */
    expand: function(groupName, options) {
        this.doCollapseExpand(false, groupName, options);
    },
    /**
     * Expand all groups
     */
    expandAll: function() {
        var me = this,
            metaGroupCache = me.getCache(),
            lockingPartner = me.lockingPartner,
            groupName;
        // Clear all collapsed flags.
        // metaGroupCache is shared between two lockingPartners
        for (groupName in metaGroupCache) {
            if (metaGroupCache.hasOwnProperty(groupName)) {
                metaGroupCache[groupName].isCollapsed = false;
            }
        }
        // We do not need to inform our lockingPartner.
        // It shares the same group cache - it will have the same set of expanded groups.
        Ext.suspendLayouts();
        me.dataSource.onDataChanged();
        Ext.resumeLayouts(true);
        // Fire event for all groups post expand
        for (groupName in metaGroupCache) {
            if (metaGroupCache.hasOwnProperty(groupName)) {
                me.afterCollapseExpand(false, groupName);
                if (lockingPartner) {
                    lockingPartner.afterCollapseExpand(false, groupName);
                }
            }
        }
    },
    /**
     * Collapse a group
     * @param {String} groupName The group name.
     * @param {Object} options. Pass when the group should be scrolled into view.
     * This contains flags for postProcessing the group's header row after
     * collapsing. See {@link Ext.panel.Table#ensureVisible} for details.
     */
    collapse: function(groupName, options) {
        this.doCollapseExpand(true, groupName, options);
    },
    /**
     * @private
     * Returns true if all groups are collapsed
     * @return {boolean}
     */
    isAllCollapsed: function() {
        var me = this,
            metaGroupCache = me.getCache(),
            groupName;
        // Clear all collapsed flags.
        // metaGroupCache is shared between two lockingPartners
        for (groupName in metaGroupCache) {
            if (metaGroupCache.hasOwnProperty(groupName)) {
                if (!metaGroupCache[groupName].isCollapsed) {
                    return false;
                }
            }
        }
        return true;
    },
    /**
     * @private
     * Returns true if all groups are expanded
     * @return {boolean}
     */
    isAllExpanded: function() {
        var me = this,
            metaGroupCache = me.getCache(),
            groupName;
        // Clear all collapsed flags.
        // metaGroupCache is shared between two lockingPartners
        for (groupName in metaGroupCache) {
            if (metaGroupCache.hasOwnProperty(groupName)) {
                if (metaGroupCache[groupName].isCollapsed) {
                    return false;
                }
            }
        }
        return true;
    },
    /**
     * Collapse all groups
     */
    collapseAll: function() {
        var me = this,
            metaGroupCache = me.getCache(),
            groupName,
            lockingPartner = me.lockingPartner;
        // Set all collapsed flags
        // metaGroupCache is shared between two lockingPartners
        for (groupName in metaGroupCache) {
            if (metaGroupCache.hasOwnProperty(groupName)) {
                metaGroupCache[groupName].isCollapsed = true;
            }
        }
        // We do not need to inform our lockingPartner.
        // It shares the same group cache - it will have the same set of collapsed groups.
        Ext.suspendLayouts();
        me.dataSource.onDataChanged();
        Ext.resumeLayouts(true);
        // Fire event for all groups post collapse
        for (groupName in metaGroupCache) {
            if (metaGroupCache.hasOwnProperty(groupName)) {
                me.afterCollapseExpand(true, groupName);
                if (lockingPartner) {
                    lockingPartner.afterCollapseExpand(true, groupName);
                }
            }
        }
    },
    doCollapseExpand: function(collapsed, groupName, options) {
        var me = this,
            lockingPartner = me.lockingPartner,
            group = me.getGroup(groupName);
        if (options === true) {
            options = {
                focus: true
            };
        }
        // metaGroupCache is shared between two lockingPartners.
        if (me.getMetaGroup(group).isCollapsed !== collapsed) {
            me.isExpandingOrCollapsing = true;
            // The GroupStore is shared by partnered Grouping features, so this will refresh
            // both sides. We only want one layout as a result though, so suspend layouts
            // while refreshing.
            Ext.suspendLayouts();
            if (collapsed) {
                me.dataSource.collapseGroup(group);
            } else {
                me.dataSource.expandGroup(group);
            }
            Ext.resumeLayouts(true);
            // Sync the group state and focus the row if requested.
            me.afterCollapseExpand(collapsed, groupName, options);
            // Sync the lockingPartner's group state.
            if (lockingPartner) {
                // Clear focus flag (without mutating a passed in object).
                // If we were told to focus, we must focus, not the other side.
                if (options && options.focus) {
                    options = Ext.Object.chain(options);
                    options.focus = false;
                }
                lockingPartner.afterCollapseExpand(collapsed, groupName, options);
            }
            me.isExpandingOrCollapsing = false;
        }
    },
    afterCollapseExpand: function(collapsed, groupName, options) {
        var me = this,
            view = me.view,
            header, record;
        header = me.getHeaderNode(groupName);
        view.fireEvent(collapsed ? 'groupcollapse' : 'groupexpand', view, header, groupName);
        if (options) {
            // NavigationModel cannot focus a collapsed group header. They are not navigable yet.
            if (collapsed) {
                options.focus = false;
                record = me.getMetaGroup(groupName).placeholder;
            } else {
                record = me.getGroup(groupName).getAt(0);
            }
            me.grid.ensureVisible(record, options);
        }
    },
    onGroupChange: function(store, grouper) {
        // If changed to a non-null grouper, the Store will be sorted (either remotely or locally),
        // and therefore fire a refresh. If changed to a null grouper - setGrouper(null) -
        // that causes no mutation to a store, so we must refresh the view to remove
        // the group headers/footers.
        if (!grouper) {
            this.view.ownerGrid.getView().refreshView();
        } else {
            this.lastGrouper = grouper;
        }
    },
    /**
     * Gets the related menu item for a dataIndex
     * @private
     * @return {Ext.grid.header.Container} The header
     */
    getMenuItem: function(dataIndex) {
        var view = this.view,
            header = view.headerCt.down('gridcolumn[dataIndex=' + dataIndex + ']'),
            menu = view.headerCt.getMenu();
        return header ? menu.down('menuitem[headerId=' + header.id + ']') : null;
    },
    onGroupKey: function(keyCode, event) {
        var me = this,
            groupName = me.getGroupName(event.target);
        if (groupName) {
            me.onGroupClick(me.view, event.target, groupName, event);
        }
    },
    /**
     * Prevent focusing - it causes a scroll between mousedown and mouseup.
     * @private
     */
    onGroupMousedown: function(view, rowElement, groupName, e) {
        e.preventDefault();
    },
    /**
     * Toggle between expanded/collapsed state when clicking on
     * the group.
     * @private
     */
    onGroupClick: function(view, rowElement, groupName, e) {
        var me = this,
            metaGroupCache, groupIsCollapsed, g;
        if (!me.collapsible) {
            return;
        }
        metaGroupCache = me.getCache();
        groupIsCollapsed = !me.isExpanded(groupName);
        // CTRL means collapse all others.
        if (e.ctrlKey) {
            Ext.suspendLayouts();
            for (g in metaGroupCache) {
                if (g === groupName) {
                    if (groupIsCollapsed) {
                        me.expand(groupName);
                    }
                } else if (!metaGroupCache[g].isCollapsed) {
                    me.doCollapseExpand(true, g, false);
                }
            }
            Ext.resumeLayouts(true);
        } else {
            me[groupIsCollapsed ? 'expand' : 'collapse'](groupName);
        }
    },
    setupRowData: function(record, idx, rowValues) {
        var me = this,
            recordIndex = rowValues.recordIndex,
            data = me.refreshData,
            groupRenderInfo = me.groupRenderInfo,
            header = data.header,
            groupField = data.groupField,
            store = me.getGridStore(),
            dataSource = me.view.dataSource,
            isBufferedStore = dataSource.isBufferedStore,
            column = me.grid.columnManager.getHeaderByDataIndex(groupField),
            hasRenderer = !!(column && column.renderer),
            groupKey = record.groupKey,
            // MetaGroup placeholder records store the groupKey not a reference.
            // See EXTJS-18655.
            group = record.isCollapsedPlaceholder && Ext.isDefined(groupKey) ? me.getGroup(groupKey) : record.group,
            grouper, groupName, prev, next, items;
        rowValues.isCollapsedGroup = false;
        rowValues.summaryRecord = rowValues.groupHeaderCls = null;
        if (data.doGrouping) {
            grouper = store.getGrouper();
            // This is a placeholder record which represents a whole collapsed group
            // It is a special case.
            if (record.isCollapsedPlaceholder) {
                groupName = group.getGroupKey();
                items = group.items;
                record = items[0];
                rowValues.isFirstRow = rowValues.isLastRow = true;
                rowValues.groupHeaderCls = me.hdCollapsedCls;
                rowValues.isCollapsedGroup = rowValues.needsWrap = true;
                rowValues.groupName = groupName;
                rowValues.groupRenderInfo = groupRenderInfo;
                groupRenderInfo.groupField = groupField;
                groupRenderInfo.groupValue = record.get(groupField);
                groupRenderInfo.name = groupRenderInfo.renderedGroupValue = hasRenderer ? column.renderer(groupRenderInfo.groupValue, {}, record) : groupName;
                groupRenderInfo.columnName = header ? header.text : groupField;
                rowValues.collapsibleCls = me.collapsible ? me.collapsibleCls : me.hdNotCollapsibleCls;
                groupRenderInfo.rows = groupRenderInfo.children = items;
                if (me.showSummaryRow) {
                    rowValues.summaryRecord = data.summaryData[groupName];
                }
                return;
            }
            groupName = grouper.getGroupString(record);
            // If caused by an update event on the first or last records of a group
            // fired by a GroupStore, the record's group will be attached.
            if (group) {
                items = group.items;
                rowValues.isFirstRow = record === items[0];
                rowValues.isLastRow = record === items[items.length - 1];
            } else {
                // See if the current record is the last in the group
                rowValues.isFirstRow = recordIndex === 0;
                if (!rowValues.isFirstRow) {
                    prev = store.getAt(recordIndex - 1);
                    // If the previous row is of a different group, then we're at the first
                    // for a new group
                    if (prev) {
                        // Must use Model's comparison because Date objects are never equal
                        rowValues.isFirstRow = !prev.isEqual(grouper.getGroupString(prev), groupName);
                    }
                }
                // See if the current record is the last in the group
                // eslint-disable-next-line max-len
                rowValues.isLastRow = recordIndex === (isBufferedStore ? store.getTotalCount() : store.getCount()) - 1;
                if (!rowValues.isLastRow) {
                    next = store.getAt(recordIndex + 1);
                    if (next) {
                        // Must use Model's comparison because Date objects are never equal
                        rowValues.isLastRow = !next.isEqual(grouper.getGroupString(next), groupName);
                    }
                }
            }
            if (rowValues.isFirstRow) {
                groupRenderInfo.groupField = groupField;
                groupRenderInfo.name = groupRenderInfo.renderedGroupValue = hasRenderer ? column.renderer(record.get(groupField), {}, record) : groupName;
                groupRenderInfo.groupValue = record.get(groupField);
                groupRenderInfo.columnName = header ? header.text : groupField;
                rowValues.collapsibleCls = me.collapsible ? me.collapsibleCls : me.hdNotCollapsibleCls;
                rowValues.groupName = groupName;
                if (!me.isExpanded(groupName)) {
                    rowValues.itemClasses.push(me.hdCollapsedCls);
                    rowValues.isCollapsedGroup = true;
                }
                // We only get passed a GroupStore if the store is not buffered.
                if (isBufferedStore) {
                    groupRenderInfo.rows = groupRenderInfo.children = [];
                } else {
                    groupRenderInfo.rows = groupRenderInfo.children = me.getRecordGroup(record).items;
                }
                rowValues.groupRenderInfo = groupRenderInfo;
            }
            if (rowValues.isLastRow) {
                // Add the group's summary record to the last record in the group
                if (me.showSummaryRow) {
                    rowValues.summaryRecord = data.summaryData[groupName];
                    rowValues.itemClasses.push(Ext.baseCSSPrefix + 'grid-group-last');
                }
            }
            rowValues.needsWrap = (rowValues.isFirstRow || rowValues.summaryRecord);
        }
    },
    setup: function(rows, rowValues) {
        var me = this,
            data = me.refreshData,
            view = rowValues.view,
            // Need to check if groups have been added since init(), such as in the case
            // of stateful grids.
            isGrouping = view.isGrouping = !me.disabled && me.getGridStore().isGrouped(),
            bufferedRenderer = view.bufferedRenderer;
        me.skippedRows = 0;
        if (bufferedRenderer) {
            bufferedRenderer.variableRowHeight = view.hasVariableRowHeight() || isGrouping;
        }
        data.groupField = me.getGroupField();
        data.header = me.getGroupedHeader(data.groupField);
        data.doGrouping = isGrouping;
        rowValues.groupHeaderTpl = Ext.XTemplate.getTpl(me, 'groupHeaderTpl');
        if (isGrouping && me.showSummaryRow) {
            data.summaryData = me.generateSummaryData();
        }
    },
    cleanup: function(rows, rowValues) {
        var data = this.refreshData;
        rowValues.groupRenderInfo = rowValues.groupHeaderTpl = rowValues.isFirstRow = null;
        data.groupField = data.header = data.summaryData = null;
    },
    /**
     * Used by the Grouping Feature when {@link #cfg!showSummaryRow} is `true`.
     *
     * Generates group summary data for the whole store.
     * @private
     * @return {Object} An object hash keyed by group name containing summary records.
     */
    generateSummaryData: function() {
        var me = this,
            store = me.getGridStore(),
            filters = store.getFilters(),
            groups = store.getGroups().items,
            groupField = me.getGroupField(),
            lockingPartner = me.lockingPartner,
            updateSummaryRow = me.updateSummaryRow,
            data = {},
            ownerCt = me.view.ownerCt,
            columnsChanged = me.didColumnsChange(),
            i, len, group, metaGroup, record, hasRemote, remoteData;
        /**
         * @cfg {String} [remoteRoot=undefined]
         * The name of the property which contains the Array of summary objects.
         * It allows to use server-side calculated summaries.
         */
        if (me.remoteRoot) {
            remoteData = me.mixins.summary.generateSummaryData.call(me, groupField);
            hasRemote = !!remoteData;
        }
        for (i = 0 , len = groups.length; i < len; ++i) {
            group = groups[i];
            metaGroup = me.getMetaGroup(group);
            // Something has changed or it doesn't exist, populate it.
            if (updateSummaryRow || hasRemote || store.updating || me.grid.reconfiguring || columnsChanged || me.didGroupChange(group, metaGroup, filters)) {
                record = me.populateRecord(group, metaGroup, remoteData);
                // Clear the dirty state of the group if this is the only Summary,
                // or this is the right hand (normal grid's) summary.
                if (!lockingPartner || (ownerCt === ownerCt.ownerLockable.normalGrid)) {
                    metaGroup.lastGroup = group;
                    metaGroup.lastGroupGeneration = group.generation;
                    metaGroup.lastFilterGeneration = filters.generation;
                }
            } else {
                record = metaGroup.aggregateRecord;
            }
            data[group.getGroupKey()] = record;
        }
        me.updateSummaryRow = false;
        return data;
    },
    getGroupName: function(element) {
        var me = this,
            view = me.view,
            eventSelector = me.eventSelector,
            targetEl, row;
        // See if element is, or is within a group header. If so, we can extract its name
        targetEl = Ext.fly(element).findParent(eventSelector);
        if (!targetEl) {
            // Otherwise, navigate up to the row and look down to see if we can find it
            row = Ext.fly(element).findParent(view.itemSelector);
            if (row) {
                targetEl = row.down(eventSelector, true);
            }
        }
        if (targetEl) {
            // Explicitly not html decoding here. Once the attribute value is set, when we
            // retrieve it, the value is already automatically "unescaped", so doing it here
            // would be double.
            return targetEl.getAttribute('data-groupname');
        }
    },
    /**
     * Returns the group data object for the group to which the passed record belongs
     * **if the Store is grouped**.
     *
     * @param {Ext.data.Model} record The record for which to return group information.
     * @return {Object} A single group data block as returned from
     * {@link Ext.data.Store#getGroups Store.getGroups}. Returns `undefined` if the Store
     * is not grouped.
     *
     */
    getRecordGroup: function(record) {
        var store = this.getGridStore(),
            grouper = store.getGrouper();
        if (grouper) {
            return store.getGroups().getItemGroup(record);
        }
    },
    getGroupedHeader: function(groupField) {
        var me = this,
            headerCt = me.view.headerCt,
            partner = me.lockingPartner,
            selector, header;
        groupField = groupField || me.getGroupField();
        if (groupField) {
            selector = '[dataIndex=' + groupField + ']';
            header = headerCt.down(selector);
            // The header may exist in the locking partner, so check there as well
            if (!header && partner) {
                header = partner.view.headerCt.down(selector);
            }
        }
        return header || null;
    },
    getFireEventArgs: function(type, view, targetEl, e) {
        return [
            type,
            view,
            targetEl,
            this.getGroupName(targetEl),
            e
        ];
    },
    destroy: function() {
        var me = this,
            dataSource = me.dataSource;
        Ext.destroy(me.gridEventRelayers);
        me.gridEventRelayers = null;
        me.storeListeners = Ext.destroy(me.storeListeners);
        me.view = me.prunedHeader = me.grid = me.dataSource = me.groupers = null;
        if (dataSource && !dataSource.destroyed) {
            dataSource.bindStore(null);
            Ext.destroy(dataSource);
        }
        me.callParent();
    },
    beforeReconfigure: function(grid, store, columns, oldStore, oldColumns) {
        var me = this,
            view = me.view,
            dataSource = me.dataSource,
            bufferedRenderer = view.bufferedRenderer,
            bufferedStore;
        if (store && store !== oldStore) {
            me.gridStore = store;
            bufferedStore = store.isBufferedStore;
            if (me.storeListeners) {
                Ext.destroy(me.storeListeners);
            }
            // Grouping involves injecting a dataSource in early
            if (!oldStore.isEmptyStore && bufferedStore !== oldStore.isBufferedStore) {
                Ext.raise('Cannot reconfigure grouping switching between buffered ' + 'and non-buffered stores');
            }
            if (!dataSource) {
                if (bufferedStore) {
                    me.collapsible = false;
                    me.setupStoreListeners(store);
                } else {
                    dataSource = me.createDataSource();
                    dataSource.on('groupchange', me.onGroupChange, me);
                    if (bufferedRenderer) {
                        bufferedRenderer.bindStore(dataSource);
                    }
                }
            }
            if (!bufferedStore) {
                view.isGrouping = !!store.getGrouper();
                dataSource.bindStore(store);
            }
            me.mixins.summary.bindStore.call(me, grid, store);
        }
    },
    createDataSource: function() {
        var me = this,
            view = me.view,
            lockPartner = me.lockingPartner,
            dataSource;
        // Share the GroupStore between both sides of a locked grid
        if (lockPartner && lockPartner.dataSource) {
            me.dataSource = view.dataSource = dataSource = lockPartner.dataSource;
        } else {
            me.dataSource = view.dataSource = dataSource = new Ext.grid.feature.GroupStore(me, me.gridStore);
        }
        return dataSource;
    },
    populateRecord: function(group, metaGroup, remoteData) {
        var me = this,
            view = me.grid.ownerLockable ? me.grid.ownerLockable.view : me.view,
            store = me.getGridStore(),
            record = metaGroup.aggregateRecord,
            // Use the full column set, regardless of locking
            columns = view.headerCt.getGridColumns(),
            len = columns.length,
            groupName = group.getGroupKey(),
            groupData, field, i, column, fieldName, summaryValue;
        record.beginEdit();
        if (remoteData) {
            // Remote summary grouping provides the grouping totals so there's no need to
            // iterate through the columns to map the column's dataIndex to the field name.
            // Instead, enumerate the grouping record and set the field in the aggregate
            // record for each one.
            groupData = remoteData[groupName];
            for (field in groupData) {
                if (groupData.hasOwnProperty(field)) {
                    if (field !== record.idProperty) {
                        record.set(field, groupData[field]);
                    }
                }
            }
        }
        // Here we iterate through the columns with two objectives:
        //    1. For local grouping, get the summary for each column and update the record.
        //    2. For both local and remote grouping, set the summary data object
        //       which is passed to the summaryRenderer (if defined).
        for (i = 0; i < len; ++i) {
            column = columns[i];
            // Use the column id if there's no mapping, could be a calculated field
            fieldName = column.dataIndex || column.getItemId();
            // We need to capture the summary value because it could get overwritten when
            // setting on the model if there is a convert() method on the model.
            if (!remoteData) {
                summaryValue = me.getSummary(store, column.summaryType, fieldName, group);
                record.set(fieldName, summaryValue);
            } else {
                // For remote groupings, just get the value from the model.
                summaryValue = record.get(column.dataIndex);
            }
            // Capture the columnId:value for the summaryRenderer in the summaryData object.
            me.setSummaryData(record, column.getItemId(), summaryValue, groupName);
        }
        // Poke on the owner group for easy lookup in this.createRenderer().
        record.ownerGroup = groupName;
        record.endEdit(true);
        record.commit();
        return record;
    },
    privates: {
        didGroupChange: function(group, metaGroup, filters) {
            var ret = true;
            if (group === metaGroup.lastGroup) {
                ret = metaGroup.lastGroupGeneration !== group.generation || metaGroup.lastFilterGeneration !== filters.generation;
            }
            return ret;
        },
        didColumnsChange: function() {
            var me = this,
                result = (me.view.headerCt.items.generation !== me.lastHeaderCtGeneration);
            me.lastHeaderCtGeneration = me.view.headerCt.items.generation;
            return result;
        },
        setupStoreListeners: function(store) {
            var me = this;
            me.storeListeners = store.on({
                groupchange: me.onGroupChange,
                scope: me,
                destroyable: true
            });
        }
    }
});

/**
 * @private
 * See {@link Ext.grid.feature.Grouping} for an example of how to add a summary row to 
 * a grouped grid
 */
Ext.define('Ext.grid.feature.GroupingSummary', {
    extend: 'Ext.grid.feature.Grouping',
    alias: 'feature.groupingsummary',
    showSummaryRow: true
});

/**
 * The rowbody feature enhances the grid's markup to have an additional
 * tr -> td -> div which spans the entire width of the original row.
 *
 * This is useful to to associate additional information with a particular
 * record in an Ext.grid.Grid.
 *
 * Rowbodies are initially hidden unless you override {@link #getAdditionalData}.
 *
 * The events fired by RowBody are relayed to the owning 
 * {@link Ext.view.Table grid view} (and subsequently the owning grid).
 *
 * # Example
 *
 *     @example
 *     Ext.define('Animal', {
 *         extend: 'Ext.data.Model',
 *         fields: ['name', 'latin', 'desc', 'lifespan']
 *     });
 *     
 *     Ext.create('Ext.grid.Panel', {
 *         width: 400,
 *         height: 300,
 *         renderTo: Ext.getBody(),
 *         store: {
 *             model: 'Animal',
 *             data: [{
 *                 name: 'Tiger',
 *                 latin: 'Panthera tigris',
 *                 desc: 'The largest cat species, weighing up to 306 kg (670 lb).',
 *                 lifespan: '20 - 26 years (in captivity)'
 *             }, {
 *                 name: 'Roman snail',
 *                 latin: 'Helix pomatia',
 *                 desc: 'A species of large, edible, air-breathing land snail.',
 *                 lifespan: '20 - 35 years'
 *             }, {
 *                 name: 'Yellow-winged darter',
 *                 latin: 'Sympetrum flaveolum',
 *                 desc: 'A dragonfly found in Europe and mid and Northern China.',
 *                 lifespan: '4 - 6 weeks'
 *             }, {
 *                 name: 'Superb Fairy-wren',
 *                 latin: 'Malurus cyaneus',
 *                 desc: 'Common and familiar across south-eastern Australia.',
 *                 lifespan: '5 - 6 years'
 *             }]
 *         },
 *         columns: [{
 *             dataIndex: 'name',
 *             text: 'Common name',
 *             width: 125
 *         }, {
 *             dataIndex: 'latin',
 *             text: 'Scientific name',
 *             flex: 1
 *         }],
 *         features: [{
 *             ftype: 'rowbody',
 *             getAdditionalData: function(data, idx, record, orig) {
 *                 // Usually you would style the my-body-class in a CSS file
 *                 return {
 *                     rowBody: '<div style="padding: 1em">' + record.get("desc") + '</div>',
 *                     rowBodyCls: "my-body-class"
 *                 };
 *             }
 *         }],
 *         listeners: {
 *             rowbodyclick: function(view, rowEl, e, eOpts) {
 *                 var itemEl = Ext.get(rowEl).up(view.itemSelector),
 *                     rec = view.getRecord(itemEl);
 *                 
 *                 Ext.Msg.alert(rec.get('name') + ' life span', rec.get('lifespan'));
 *             }
 *         }
 *     });
 *
 *  # Cell Editing and Cell Selection Model
 *
 * Note that if {@link Ext.grid.plugin.CellEditing cell editing} or the
 * {@link Ext.selection.CellModel cell selection model} are going to be used, then the
 * {@link Ext.grid.feature.RowBody RowBody} feature, or
 * {@link Ext.grid.plugin.RowExpander RowExpander} plugin MUST be used for intra-cell navigation
 * to be correct.
 *
 * **Note:** The {@link Ext.grid.plugin.RowExpander rowexpander} plugin and the rowbody
 * feature are exclusive and cannot both be set on the same grid / tree.
 */
Ext.define('Ext.grid.feature.RowBody', {
    extend: 'Ext.grid.feature.Feature',
    alias: 'feature.rowbody',
    rowBodyCls: Ext.baseCSSPrefix + 'grid-row-body',
    innerSelector: '.' + Ext.baseCSSPrefix + 'grid-rowbody',
    rowBodyHiddenCls: Ext.baseCSSPrefix + 'grid-row-body-hidden',
    rowBodyTdSelector: 'td.' + Ext.baseCSSPrefix + 'grid-cell-rowbody',
    eventPrefix: 'rowbody',
    eventSelector: 'tr.' + Ext.baseCSSPrefix + 'grid-rowbody-tr',
    /**
     * @cfg {Boolean} [bodyBefore=false]
     * Configure as `true` to put the row expander body *before* the data row.
     */
    bodyBefore: false,
    outerTpl: {
        fn: function(out, values, parent) {
            var me = this.rowBody,
                view = values.view,
                columns = view.getVisibleColumnManager().getColumns(),
                rowValues = view.rowValues,
                rowExpanderCol = me.rowExpander && me.rowExpander.expanderColumn;
            rowValues.rowBodyColspan = columns.length;
            rowValues.rowBodyCls = me.rowBodyCls;
            rowValues.rowIdCls = me.rowIdCls;
            if (rowExpanderCol && rowExpanderCol.getView() === view) {
                view.grid.removeCls(Ext.baseCSSPrefix + 'grid-hide-row-expander-spacer');
                rowValues.addSpacerCell = true;
                rowValues.rowBodyColspan -= 1;
                rowValues.spacerCellCls = Ext.baseCSSPrefix + 'grid-cell ' + Ext.baseCSSPrefix + 'grid-row-expander-spacer ' + Ext.baseCSSPrefix + 'grid-cell-special';
            } else {
                view.grid.addCls(Ext.baseCSSPrefix + 'grid-hide-row-expander-spacer');
                rowValues.addSpacerCell = false;
            }
            this.nextTpl.applyOut(values, out, parent);
            rowValues.rowBodyCls = rowValues.rowBodyColspan = rowValues.rowBody = null;
        },
        priority: 100
    },
    /* eslint-disable indent, max-len */
    extraRowTpl: [
        '{%',
        'if(this.rowBody.bodyBefore) {',
        // MUST output column sizing elements because the first row in this table
        // contains one colspanning TD, and that overrides subsequent column width settings.
        'values.view.renderColumnSizer(values, out);',
        '} else {',
        'this.nextTpl.applyOut(values, out, parent);',
        '}',
        'values.view.rowBodyFeature.setupRowData(values.record, values.recordIndex, values);',
        '%}',
        '<tr class="' + Ext.baseCSSPrefix + 'grid-rowbody-tr {rowBodyCls} {rowIdCls}" {ariaRowAttr}>',
        '<tpl if="addSpacerCell">',
        '<td class="{spacerCellCls}"></td>',
        '</tpl>',
        '<td class="' + Ext.baseCSSPrefix + 'grid-td ' + Ext.baseCSSPrefix + 'grid-cell-rowbody" colspan="{rowBodyColspan}" {ariaCellAttr}>',
        '<div class="' + Ext.baseCSSPrefix + 'grid-rowbody {rowBodyDivCls}" {ariaCellInnerAttr}>{rowBody}</div>',
        '</td>',
        '</tr>',
        '{%',
        'if(this.rowBody.bodyBefore) {',
        'this.nextTpl.applyOut(values, out, parent);',
        '}',
        '%}',
        {
            priority: 100,
            beginRowSync: function(rowSync) {
                rowSync.add('rowBody', this.owner.eventSelector);
            },
            syncContent: function(destRow, sourceRow, columnsToUpdate) {
                var rowBody = this.rowBody,
                    destRowBody, sourceRowBody;
                if (rowBody.doSync) {
                    destRowBody = Ext.fly(destRow).down(rowBody.eventSelector, true);
                    // Sync the heights of row body elements in each row if they need it.
                    if (destRowBody && (sourceRowBody = Ext.fly(sourceRow).down(rowBody.eventSelector, true))) {
                        Ext.fly(destRowBody).syncContent(sourceRowBody);
                    }
                }
            }
        }
    ],
    /* eslint-enable indent, max-len */
    doSync: true,
    init: function(grid) {
        var me = this,
            view = me.view = grid.getView();
        if (!me.rowExpander && grid.findPlugin('rowexpander')) {
            Ext.raise('The RowBody feature shouldn\'t be manually added when the grid' + 'has a RowExpander.');
        }
        // The extra data means variableRowHeight
        grid.variableRowHeight = view.variableRowHeight = true;
        view.rowBodyFeature = me;
        view.headerCt.on({
            columnschanged: me.onColumnsChanged,
            scope: me
        });
        view.addTpl(me.outerTpl).rowBody = me;
        view.addRowTpl(Ext.XTemplate.getTpl(this, 'extraRowTpl')).rowBody = me;
        me.callParent(arguments);
    },
    getSelectedRow: function(view, rowIndex) {
        var selectedRow = view.getNode(rowIndex);
        if (selectedRow) {
            return Ext.fly(selectedRow).down(this.eventSelector);
        }
        return null;
    },
    // When columns added/removed, keep row body colspan in sync with number of columns.
    onColumnsChanged: function(headerCt) {
        var view = this.view,
            items, colspan, len, i;
        if (!view.rendered) {
            return;
        }
        items = view.el.query(this.rowBodyTdSelector);
        colspan = headerCt.getVisibleGridColumns().length;
        len = items.length;
        for (i = 0; i < len; ++i) {
            items[i].setAttribute('colSpan', colspan);
        }
    },
    /**
     * @method getAdditionalData
     * @protected
     * @template
     * Provides additional data to the prepareData call within the grid view.
     * The rowbody feature adds 3 additional variables into the grid view's template.
     * These are `rowBody`, `rowBodyCls`, and `rowBodyColspan`.
     * 
     *  - **rowBody:** *{String}* The HTML to display in the row body element.  Defaults 
     * to *undefined*.
     *  - **rowBodyCls:** *{String}* An optional CSS class (or multiple classes 
     * separated by spaces) to apply to the row body element.  Defaults to 
     * {@link #rowBodyCls}.
     *  - **rowBodyColspan:** *{Number}* The number of columns that the row body element 
     * should span.  Defaults to the number of visible columns.
     * 
     * @param {Object} data The data for this particular record.
     * @param {Number} idx The row index for this record.
     * @param {Ext.data.Model} record The record instance
     * @param {Object} orig The original result from the prepareData call to massage.
     * @return {Object} An object containing additional variables for use in the grid 
     * view's template
     */
    /*
     * @private
     */
    setupRowData: function(record, rowIndex, rowValues) {
        if (this.getAdditionalData) {
            Ext.apply(rowValues, this.getAdditionalData(record.data, rowIndex, record, rowValues));
        }
    }
});
/**
     * @event beforerowbodymousedown
     * @preventable
     * @member Ext.view.Table
     * Fires before the mousedown event on a row body element is processed. Return false 
     * to cancel the default action.
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @param {Ext.view.View} view The rowbody's owning View
     * @param {HTMLElement} rowBodyEl The row body's element
     * @param {Ext.event.Event} e The raw event object
     */
/**
     * @event beforerowbodymouseup
     * @preventable
     * @member Ext.view.Table
     * Fires before the mouseup event on a row body element is processed. Return false 
     * to cancel the default action.
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event beforerowbodyclick
     * @preventable
     * @member Ext.view.Table
     * Fires before the click event on a row body element is processed. Return false to 
     * cancel the default action.
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event beforerowbodydblclick
     * @preventable
     * @member Ext.view.Table
     * Fires before the dblclick event on a row body element is processed. Return false 
     * to cancel the default action.
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event beforerowbodycontextmenu
     * @preventable
     * @member Ext.view.Table
     * Fires before the contextmenu event on a row body element is processed. Return 
     * false to cancel the default action.
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event beforerowbodylongpress
     * @preventable
     * @member Ext.view.Table
     * Fires before the longpress event on a row body element is processed. Return 
     * false to cancel the default action.
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event beforerowbodykeydown
     * @preventable
     * @member Ext.view.Table
     * Fires before the keydown event on a row body element is processed. Return false 
     * to cancel the default action.
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event beforerowbodykeyup
     * @preventable
     * @member Ext.view.Table
     * Fires before the keyup event on a row body element is processed. Return false to 
     * cancel the default action.
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event beforerowbodykeypress
     * @preventable
     * @member Ext.view.Table
     * Fires before the keypress event on a row body element is processed. Return false 
     * to cancel the default action.
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event rowbodymousedown
     * @member Ext.view.Table
     * Fires when there is a mouse down on a row body element
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event rowbodymouseup
     * @member Ext.view.Table
     * Fires when there is a mouse up on a row body element
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event rowbodyclick
     * @member Ext.view.Table
     * Fires when a row body element is clicked
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event rowbodydblclick
     * @member Ext.view.Table
     * Fires when a row body element is double clicked
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event rowbodycontextmenu
     * @member Ext.view.Table
     * Fires when a row body element is right clicked
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event rowbodylongpress
     * @member Ext.view.Table
     * Fires on a row body element longpress event
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event rowbodykeydown
     * @member Ext.view.Table
     * Fires when a key is pressed down while a row body element is currently selected
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event rowbodykeyup
     * @member Ext.view.Table
     * Fires when a key is released while a row body element is currently selected
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */
/**
     * @event rowbodykeypress
     * @member Ext.view.Table
     * Fires when a key is pressed while a row body element is currently selected.
     * 
     * **Note:** This event is fired only when the Ext.grid.feature.RowBody feature is 
     * used.
     * 
     * @inheritdoc #beforerowbodymousedown
     */

/**
 * This feature is used to place a summary row at the bottom of the grid. If using a grouping,
 * see {@link Ext.grid.feature.GroupingSummary}. There are 2 aspects to calculating the summaries,
 * calculation and rendering.
 *
 * ## Calculation
 * The summary value needs to be calculated for each column in the grid. This is controlled
 * by the summaryType option specified on the column. There are several built in summary types,
 * which can be specified as a string on the column configuration. These call underlying methods
 * on the store:
 *
 *  - {@link Ext.data.Store#count count}
 *  - {@link Ext.data.Store#sum sum}
 *  - {@link Ext.data.Store#min min}
 *  - {@link Ext.data.Store#max max}
 *  - {@link Ext.data.Store#average average}
 *
 * Alternatively, the summaryType can be a function definition. If this is the case,
 * the function is called with an array of records to calculate the summary value.
 *
 * ## Rendering
 * Similar to a column, the summary also supports a summaryRenderer function. This
 * summaryRenderer is called before displaying a value. The function is optional, if
 * not specified the default calculated value is shown. The summaryRenderer is called with:
 *
 *  - value {Object} - The calculated value.
 *  - summaryData {Object} - Contains all raw summary values for the row.
 *  - field {String} - The name of the field we are calculating
 *  - metaData {Object} - A collection of metadata about the current cell; can be used or modified
 *    by the renderer.
 *
 * ## Example Usage
 *
 *     @example
 *     Ext.define('TestResult', {
 *         extend: 'Ext.data.Model',
 *         fields: ['student', {
 *             name: 'mark',
 *             type: 'int'
 *         }]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         width: 400,
 *         height: 200,
 *         title: 'Summary Test',
 *         style: 'padding: 20px',
 *         renderTo: document.body,
 *         features: [{
 *             ftype: 'summary'
 *         }],
 *         store: {
 *             model: 'TestResult',
 *             data: [{
 *                 student: 'Student 1',
 *                 mark: 84
 *             },{
 *                 student: 'Student 2',
 *                 mark: 72
 *             },{
 *                 student: 'Student 3',
 *                 mark: 96
 *             },{
 *                 student: 'Student 4',
 *                 mark: 68
 *             }]
 *         },
 *         columns: [{
 *             dataIndex: 'student',
 *             text: 'Name',
 *             summaryType: 'count',
 *             summaryRenderer: function(value, summaryData, dataIndex) {
 *                 return Ext.String.format('{0} student{1}', value, value !== 1 ? 's' : '');
 *             }
 *         }, {
 *             dataIndex: 'mark',
 *             text: 'Mark',
 *             summaryType: 'average'
 *         }]
 *     });
 */
Ext.define('Ext.grid.feature.Summary', {
    extend: 'Ext.grid.feature.AbstractSummary',
    alias: 'feature.summary',
    /**
     * @cfg {String} dock
     * Configure `'top'` or `'bottom'` top create a fixed summary row either above or below
     * the scrollable table.
     *
     */
    dock: undefined,
    summaryItemCls: Ext.baseCSSPrefix + 'grid-row-summary-item',
    dockedSummaryCls: Ext.baseCSSPrefix + 'docked-summary',
    summaryRowCls: Ext.baseCSSPrefix + 'grid-row-summary ' + Ext.baseCSSPrefix + 'grid-row-total',
    summaryRowSelector: '.' + Ext.baseCSSPrefix + 'grid-row-summary.' + Ext.baseCSSPrefix + 'grid-row-total',
    panelBodyCls: Ext.baseCSSPrefix + 'summary-',
    // turn off feature events.
    hasFeatureEvent: false,
    fullSummaryTpl: {
        fn: function(out, values, parent) {
            var me = this.summaryFeature,
                record = me.summaryRecord,
                view = values.view,
                bufferedRenderer = view.bufferedRenderer;
            this.nextTpl.applyOut(values, out, parent);
            if (!me.disabled && me.showSummaryRow && !view.addingRows && view.store.isLast(values.record)) {
                if (bufferedRenderer && !me.dock) {
                    bufferedRenderer.variableRowHeight = true;
                }
                me.outputSummaryRecord((record && record.isModel) ? record : me.createSummaryRecord(view), values, out, parent);
            }
        },
        priority: 300,
        beginRowSync: function(rowSync) {
            rowSync.add('fullSummary', this.summaryFeature.summaryRowSelector);
        },
        syncContent: function(destRow, sourceRow, columnsToUpdate) {
            destRow = Ext.fly(destRow, 'syncDest');
            sourceRow = Ext.fly(sourceRow, 'syncSrc');
            // eslint-disable-next-line vars-on-top
            var summaryFeature = this.summaryFeature,
                selector = summaryFeature.summaryRowSelector,
                destSummaryRow = destRow.down(selector, true),
                sourceSummaryRow = sourceRow.down(selector, true);
            // Sync just the updated columns in the summary row.
            if (destSummaryRow && sourceSummaryRow) {
                // If we were passed a column set, only update those, otherwise do the entire row
                if (columnsToUpdate) {
                    this.summaryFeature.view.updateColumns(destSummaryRow, sourceSummaryRow, columnsToUpdate);
                } else {
                    Ext.fly(destSummaryRow).syncContent(sourceSummaryRow);
                }
            }
        }
    },
    init: function(grid) {
        var me = this,
            view = me.view,
            dock = me.dock;
        me.callParent([
            grid
        ]);
        if (dock) {
            grid.addBodyCls(me.panelBodyCls + dock);
            grid.headerCt.on({
                add: me.onStoreUpdate,
                // we need to fire onStoreUpdate afterlayout for docked items
                // to re-run the renderSummaryRow on show/hide columns.
                afterlayout: me.onStoreUpdate,
                remove: me.onStoreUpdate,
                scope: me
            });
            grid.on({
                beforerender: function() {
                    var tableCls = [
                            me.summaryTableCls
                        ];
                    if (view.columnLines) {
                        tableCls[tableCls.length] = view.ownerCt.colLinesCls;
                    }
                    me.summaryBar = grid.addDocked({
                        childEls: [
                            'innerCt',
                            'item'
                        ],
                        /* eslint-disable indent, max-len */
                        renderTpl: [
                            '<div id="{id}-innerCt" data-ref="innerCt" role="presentation">',
                            '<table id="{id}-item" data-ref="item" cellPadding="0" cellSpacing="0" class="' + tableCls.join(' ') + '">',
                            '<tr class="' + me.summaryRowCls + '"></tr>',
                            '</table>',
                            '</div>'
                        ],
                        /* eslint-enable indent, max-len */
                        scrollable: {
                            x: false,
                            y: false
                        },
                        hidden: !me.showSummaryRow,
                        itemId: 'summaryBar',
                        cls: [
                            me.dockedSummaryCls,
                            me.dockedSummaryCls + '-' + dock
                        ],
                        xtype: 'component',
                        dock: dock,
                        weight: 10000000
                    })[0];
                },
                afterrender: function() {
                    grid.getView().getScrollable().addPartner(me.summaryBar.getScrollable(), 'x');
                    me.onStoreUpdate();
                    me.columnSizer = me.summaryBar.el;
                },
                single: true
            });
        } else {
            if (grid.bufferedRenderer) {
                me.wrapsItem = true;
                view.addRowTpl(me.fullSummaryTpl).summaryFeature = me;
                view.on('refresh', me.onViewRefresh, me);
            } else {
                me.wrapsItem = false;
                me.view.addFooterFn(me.renderSummaryRow);
            }
        }
        grid.headerCt.on({
            afterlayout: me.afterHeaderCtLayout,
            scope: me
        });
        grid.ownerGrid.on({
            beforereconfigure: me.onBeforeReconfigure,
            columnmove: me.onStoreUpdate,
            scope: me
        });
        me.bindStore(grid, grid.getStore());
    },
    onBeforeReconfigure: function(grid, store) {
        this.summaryRecord = null;
        if (store) {
            this.bindStore(grid, store);
        }
    },
    bindStore: function(grid, store) {
        var me = this;
        Ext.destroy(me.storeListeners);
        me.storeListeners = store.on({
            scope: me,
            destroyable: true,
            update: me.onStoreUpdate,
            datachanged: me.onStoreUpdate
        });
        me.callParent([
            grid,
            store
        ]);
    },
    renderSummaryRow: function(values, out, parent) {
        var view = values.view,
            me = view.findFeature('summary'),
            record;
        // If we get to here we won't be buffered
        if (!me.disabled && me.showSummaryRow && !view.addingRows && !view.updatingRows) {
            record = me.summaryRecord;
            out.push('<table cellpadding="0" cellspacing="0" class="' + me.summaryItemCls + '" style="table-layout: fixed; width: 100%;">');
            me.outputSummaryRecord((record && record.isModel) ? record : me.createSummaryRecord(view), values, out, parent);
            out.push('</table>');
        }
    },
    toggleSummaryRow: function(visible, fromLockingPartner) {
        var me = this,
            bar = me.summaryBar;
        me.callParent([
            visible,
            fromLockingPartner
        ]);
        if (bar) {
            bar.setVisible(me.showSummaryRow);
            me.onViewScroll();
        }
    },
    getSummaryBar: function() {
        return this.summaryBar;
    },
    getSummaryRowPlaceholder: function(view) {
        var placeholderCls = this.summaryItemCls,
            nodeContainer, row;
        nodeContainer = Ext.fly(view.getNodeContainer());
        if (!nodeContainer) {
            return null;
        }
        row = nodeContainer.down('.' + placeholderCls, true);
        if (!row) {
            row = nodeContainer.createChild({
                tag: 'table',
                cellpadding: 0,
                cellspacing: 0,
                cls: placeholderCls,
                style: 'table-layout: fixed; width: 100%',
                children: [
                    {
                        tag: 'tbody'
                    }
                ]
            }, // Ensure tBodies property is present on the row
            false, true);
        }
        return row;
    },
    vetoEvent: function(record, row, rowIndex, e) {
        return !e.getTarget(this.summaryRowSelector);
    },
    onViewScroll: function() {
        this.summaryBar.setScrollX(this.view.getScrollX());
    },
    onViewRefresh: function(view) {
        var me = this,
            record, row;
        // Only add this listener if in buffered mode, if there are no rows then
        // we won't have anything rendered, so we need to push the row in here
        if (!me.disabled && me.showSummaryRow && !view.all.getCount()) {
            record = me.createSummaryRecord(view);
            row = me.getSummaryRowPlaceholder(view);
            row.tBodies[0].appendChild(view.createRowElement(record, -1).querySelector(me.summaryRowSelector));
        }
    },
    createSummaryRecord: function(view) {
        var me = this,
            columns = view.headerCt.getGridColumns(),
            remoteRoot = me.remoteRoot,
            summaryRecord = me.summaryRecord || (me.summaryRecord = new Ext.data.Model({
                id: view.id + '-summary-record'
            })),
            colCount = columns.length,
            i, column, dataIndex, summaryValue;
        // Set the summary field values
        summaryRecord.beginEdit();
        if (remoteRoot) {
            summaryValue = me.generateSummaryData();
            if (summaryValue) {
                summaryRecord.set(summaryValue);
            }
        } else {
            for (i = 0; i < colCount; i++) {
                column = columns[i];
                // In summary records, if there's no dataIndex, then the value in regular rows
                // must come from a renderer. We set the data value in using the column ID.
                dataIndex = column.dataIndex || column.getItemId();
                // We need to capture this value because it could get overwritten when setting
                // on the model if there is a convert() method on the model.
                summaryValue = me.getSummary(view.store, column.summaryType, dataIndex);
                summaryRecord.set(dataIndex, summaryValue);
                // Capture the columnId:value for the summaryRenderer in the summaryData object.
                me.setSummaryData(summaryRecord, column.getItemId(), summaryValue);
            }
        }
        summaryRecord.endEdit(true);
        // It's not dirty
        summaryRecord.commit(true);
        summaryRecord.isSummary = true;
        return summaryRecord;
    },
    onStoreUpdate: function() {
        var me = this,
            view = me.view,
            selector = me.summaryRowSelector,
            dock = me.dock,
            record, newRowDom, oldRowDom, p;
        if (!view.rendered) {
            return;
        }
        record = me.createSummaryRecord(view);
        newRowDom = Ext.fly(view.createRowElement(record, -1)).down(selector, true);
        if (!newRowDom) {
            return;
        }
        // Summary row is inside the docked summaryBar Component
        if (dock) {
            p = me.summaryBar.item.dom.firstChild;
            oldRowDom = p.firstChild;
            p.insertBefore(newRowDom, oldRowDom);
            p.removeChild(oldRowDom);
        } else // Summary row is a regular row in a THEAD inside the View.
        // Downlinked through the summary record's ID
        {
            oldRowDom = view.el.down(selector, true);
            p = oldRowDom && oldRowDom.parentNode;
            if (p) {
                p.removeChild(oldRowDom);
            }
            // We're always inserting the new summary row into the last rendered row,
            // unless no rows exist. In that case we will be appending to the special
            // placeholder in the node container.
            p = view.getRow(view.all.last());
            if (p) {
                p = p.parentElement;
            } else // View might not have nodeContainer yet.
            {
                p = me.getSummaryRowPlaceholder(view);
                p = p && p.tBodies && p.tBodies[0];
            }
            if (p) {
                p.appendChild(newRowDom);
            }
        }
    },
    // Synchronize column widths in the docked summary Component or the inline summary row
    // depending on whether we are docked or not.
    afterHeaderCtLayout: function(headerCt) {
        var me = this,
            view = me.view,
            columns = view.getVisibleColumnManager().getColumns(),
            len = columns.length,
            i, column, summaryEl, el, width, innerCt;
        if (me.showSummaryRow && view.refreshCounter) {
            if (me.dock) {
                summaryEl = me.summaryBar.el;
                width = headerCt.getTableWidth();
                innerCt = me.summaryBar.innerCt;
                // Stretch the innerCt of the summary bar upon headerCt layout
                me.summaryBar.item.setWidth(width);
                // headerCt's tooNarrow flag is set by its layout if the columns overflow.
                // Must not measure+set in after layout phase, this is a write phase.
                if (headerCt.tooNarrow) {
                    width += Ext.scrollbar.width();
                }
                innerCt.setWidth(width);
            } else {
                summaryEl = Ext.fly(Ext.fly(view.getNodeContainer()).down('.' + me.summaryItemCls, true));
            }
            // If the layout was in response to a clearView, there'll be no summary element
            if (summaryEl) {
                for (i = 0; i < len; i++) {
                    column = columns[i];
                    el = summaryEl.down(view.getCellSelector(column), true);
                    if (el) {
                        Ext.fly(el).setWidth(column.width || (column.lastBox ? column.lastBox.width : 100));
                    }
                }
            }
        }
    },
    destroy: function() {
        var me = this;
        me.summaryRecord = me.storeListeners = Ext.destroy(me.storeListeners);
        me.callParent();
    }
});

/**
 * A base class for all menu items that require menu-related functionality such as click handling,
 * sub-menus, icons, etc.
 *
 *     @example
 *     Ext.create('Ext.menu.Menu', {
 *         width: 100,
 *         height: 100,
 *         floating: false,  // usually you want this set to True (default)
 *         renderTo: Ext.getBody(),  // usually rendered by it's containing component
 *         items: [{
 *             text: 'icon item',
 *             iconCls: 'add16'
 *         }, {
 *             text: 'text item'
 *         }, {
 *             text: 'plain item',
 *             plain: true
 *         }]
 *     });
 */
Ext.define('Ext.menu.Item', {
    extend: 'Ext.Component',
    alias: 'widget.menuitem',
    alternateClassName: 'Ext.menu.TextItem',
    /**
     * @property {Boolean} isMenuItem
     * `true` in this class to identify an object as an instantiated Menu Item, or subclass thereof.
     */
    isMenuItem: true,
    mixins: [
        'Ext.mixin.Queryable'
    ],
    requires: [
        'Ext.Glyph'
    ],
    config: {
        /**
         * @cfg glyph
         * @inheritdoc Ext.panel.Header#cfg-glyph
         */
        glyph: null
    },
    /**
     * @property {Boolean} activated
     * Whether or not this item is currently activated
     */
    activated: false,
    /**
     * @property {Ext.menu.Menu} parentMenu
     * The parent Menu of this item.
     */
    /**
     * @cfg {String} activeCls
     * The CSS class added to the menu item when the item is focused.
     */
    activeCls: Ext.baseCSSPrefix + 'menu-item-active',
    /**
     * @cfg {Boolean} canActivate
     * Whether or not this menu item can be focused.
     * @deprecated 5.1.0 Use the {@link #focusable} config.
     */
    /**
     * @cfg {Number} clickHideDelay
     * The delay in milliseconds to wait before hiding the menu after clicking the menu item.
     * This only has an effect when `hideOnClick: true`.
     */
    clickHideDelay: 0,
    /**
     * @cfg {Boolean} destroyMenu
     * Whether or not to destroy any associated sub-menu when this item is destroyed.
     */
    destroyMenu: true,
    /**
     * @cfg {String} disabledCls
     * The CSS class added to the menu item when the item is disabled.
     */
    disabledCls: Ext.baseCSSPrefix + 'menu-item-disabled',
    /**
     * @cfg {String} [href='#']
     * The href attribute to use for the underlying anchor link.
     */
    /**
     * @cfg {String} hrefTarget
     * The target attribute to use for the underlying anchor link.
     */
    /**
     * @cfg {Boolean} hideOnClick
     * Whether to not to hide the owning menu when this item is clicked.
     */
    hideOnClick: true,
    /**
     * @cfg [icon=Ext#BLANK_IMAGE_URL]
     * @inheritdoc Ext.panel.Header#cfg-icon
     */
    /**
     * @cfg iconCls
     * @inheritdoc Ext.panel.Header#cfg-iconCls
     */
    /**
     * @cfg {Ext.menu.Menu/Object} menu
     * Either an instance of {@link Ext.menu.Menu} or a config object for an {@link Ext.menu.Menu}
     * which will act as a sub-menu to this item.
     */
    /**
     * @property {Ext.menu.Menu} menu The sub-menu associated with this item, if one was configured.
     */
    /**
     * @cfg {String} menuAlign
     * The default {@link Ext.util.Positionable#getAlignToXY Ext.util.Positionable.getAlignToXY}
     * anchor position value for this item's sub-menu relative to this item's position.
     */
    menuAlign: 'tl-tr?',
    /**
     * @cfg {Number} menuExpandDelay
     * The delay in milliseconds before this item's sub-menu expands after this item is moused over.
     */
    menuExpandDelay: 200,
    /**
     * @cfg {Number} menuHideDelay
     * The delay in milliseconds before this item's sub-menu hides after this item is moused out.
     */
    menuHideDelay: 200,
    /**
     * @cfg {Boolean} plain
     * Whether or not this item is plain text/html with no icon or visual submenu indication.
     */
    /**
     * @cfg {String/Object} tooltip
     * The tooltip for the button - can be a string to be used as innerHTML (html tags are accepted)
     * or QuickTips config object.
     */
    /**
     * @cfg {String} tooltipType
     * The type of tooltip to use. Either 'qtip' for QuickTips or 'title' for title attribute.
     */
    tooltipType: 'qtip',
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: true,
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'menuitem',
    /**
     * @property ariaEl
     * @inheritdoc
     */
    ariaEl: 'itemEl',
    /**
     * @cfg baseCls
     * @inheritdoc
     */
    baseCls: Ext.baseCSSPrefix + 'menu-item',
    arrowCls: Ext.baseCSSPrefix + 'menu-item-arrow',
    baseIconCls: Ext.baseCSSPrefix + 'menu-item-icon',
    textCls: Ext.baseCSSPrefix + 'menu-item-text',
    indentCls: Ext.baseCSSPrefix + 'menu-item-indent',
    indentNoSeparatorCls: Ext.baseCSSPrefix + 'menu-item-indent-no-separator',
    indentRightIconCls: Ext.baseCSSPrefix + 'menu-item-indent-right-icon',
    indentRightArrowCls: Ext.baseCSSPrefix + 'menu-item-indent-right-arrow',
    linkCls: Ext.baseCSSPrefix + 'menu-item-link',
    linkHrefCls: Ext.baseCSSPrefix + 'menu-item-link-href',
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        'itemEl',
        'iconEl',
        'textEl',
        'arrowEl'
    ],
    /* eslint-disable indent, max-len */
    /**
     * @cfg renderTpl
     * @inheritdoc
     */
    renderTpl: '<tpl if="plain">' + '{text}' + '<tpl else>' + '<a id="{id}-itemEl" data-ref="itemEl"' + ' class="{linkCls}<tpl if="hasHref"> {linkHrefCls}</tpl>{childElCls}"' + ' href="{href}" ' + '<tpl if="hrefTarget"> target="{hrefTarget}"</tpl>' + ' hidefocus="true"' + // For most browsers the text is already unselectable but Opera needs an explicit unselectable="on".
    ' unselectable="on"' + '<tpl if="tabIndex != null">' + ' tabindex="{tabIndex}"' + '</tpl>' + '<tpl foreach="ariaAttributes"> {$}="{.}"</tpl>' + '>' + '<span id="{id}-textEl" data-ref="textEl" class="{textCls} {textCls}-{ui} {indentCls}{childElCls}" unselectable="on" role="presentation">{text}</span>' + '<tpl if="hasIcon">' + '<div role="presentation" id="{id}-iconEl" data-ref="iconEl" class="{baseIconCls}-{ui} {baseIconCls}' + '{[values.rightIcon ? "-right" : ""]} {iconCls}' + '{childElCls} {glyphCls}" style="<tpl if="icon">background-image:url({icon});</tpl>' + '<tpl if="glyph">' + '<tpl if="glyphFontFamily">' + 'font-family:{glyphFontFamily};' + '</tpl>' + '">' + '{glyph}' + '<tpl else>' + '">' + '</tpl>' + '</div>' + '</tpl>' + '<tpl if="showCheckbox">' + '<div role="presentation" id="{id}-checkEl" data-ref="checkEl" class="{baseIconCls}-{ui} {baseIconCls}' + '{[(values.hasIcon && !values.rightIcon) ? "-right" : ""]} ' + '{groupCls} {checkboxCls}{childElCls}">' + '</div>' + '</tpl>' + '<tpl if="hasMenu">' + '<div role="presentation" id="{id}-arrowEl" data-ref="arrowEl" class="{arrowCls} {arrowCls}-{ui}{childElCls}"></div>' + '</tpl>' + '</a>' + '</tpl>',
    /* eslint-enable indent, max-len */
    /**
     * @cfg autoEl
     * @inheritdoc
     */
    autoEl: {
        role: 'presentation'
    },
    /**
     * @property maskOnDisable
     * @inheritdoc
     */
    maskOnDisable: false,
    iconAlign: 'left',
    /**
     * @cfg {String} text
     * The text/html to display in this item.
     */
    /**
     * @cfg {Function/String} handler
     * A function called when the menu item is clicked (can be used instead of {@link #click}
     * event).
     * @cfg {Ext.menu.Item} handler.item The item that was clicked
     * @cfg {Ext.event.Event} handler.e The underlying {@link Ext.event.Event}.
     * @controllable
     */
    /**
     * @event activate
     * Fires when this item is activated
     * @param {Ext.menu.Item} item The activated item
     */
    /**
     * @event click
     * Fires when this item is clicked
     * @param {Ext.menu.Item} item The item that was clicked
     * @param {Ext.event.Event} e The underlying {@link Ext.event.Event}.
     */
    /**
     * @event deactivate
     * Fires when this item is deactivated
     * @param {Ext.menu.Item} item The deactivated item
     */
    /**
     * @event textchange
     * Fired when the item's text is changed by the {@link #setText} method.
     * @param {Ext.menu.Item} this
     * @param {String} oldText
     * @param {String} newText
     */
    /**
     * @event iconchange
     * Fired when the item's icon is changed by the {@link #setIcon} or {@link #setIconCls} methods.
     * @param {Ext.menu.Item} this
     * @param {String} oldIcon
     * @param {String} newIcon
     */
    initComponent: function() {
        var me = this,
            cls = me.cls ? [
                me.cls
            ] : [],
            menu;
        // During deprecation period of canActivate config, copy it into focusable config.
        if (me.hasOwnProperty('canActivate')) {
            me.focusable = me.canActivate;
        }
        if (me.plain) {
            cls.push(Ext.baseCSSPrefix + 'menu-item-plain');
        }
        if (cls.length) {
            me.cls = cls.join(' ');
        }
        if (me.menu) {
            menu = me.menu;
            me.menu = null;
            me.setMenu(menu);
        }
        me.callParent(arguments);
    },
    canFocus: function() {
        var me = this;
        // This is an override of the implementation in Focusable.
        // We do not refuse focus if the Item is disabled.
        // http://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#menu
        // "Disabled menu items receive focus but have no action when Enter or
        // Left Arrow/Right Arrow is pressed."
        // Test that deprecated canActivate config has not been set to false.
        return me.focusable && me.rendered && me.canActivate !== false && !me.destroying && !me.destroyed && me.isVisible(true);
    },
    onFocus: function(e) {
        var me = this;
        me.callParent([
            e
        ]);
        // We do not refuse activation if the Item is disabled.
        // http://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#menu
        // "Disabled menu items receive focus but have no action when Enter or
        // Left Arrow/Right Arrow is pressed."
        if (!me.plain) {
            me.addCls(me.activeCls);
        }
        me.activated = true;
        if (me.hasListeners.activate) {
            me.fireEvent('activate', me);
        }
    },
    onFocusLeave: function(e) {
        var me = this;
        me.callParent([
            e
        ]);
        if (!me.plain) {
            me.removeCls(me.activeCls);
        }
        me.doHideMenu();
        me.activated = false;
        if (me.hasListeners.deactivate) {
            me.fireEvent('deactivate', me);
        }
    },
    doHideMenu: function() {
        var menu = this.menu;
        this.cancelDeferExpand();
        if (menu && menu.isVisible()) {
            menu.hide();
        }
    },
    /**
     * @private
     * Hides the entire floating menu tree that we are within.
     * Walks up the refOwner axis hiding each Menu instance it find until it hits
     * a non-floating ancestor.
     */
    deferHideParentMenus: function() {
        var menu;
        // eslint-disable-next-line max-len
        for (menu = this.getRefOwner(); menu && ((menu.isMenu && menu.floating) || menu.isMenuItem); menu = menu.getRefOwner()) {
            if (menu.isMenu) {
                menu.hide();
            }
        }
    },
    expandMenu: function(event, delay) {
        var me = this;
        // An item can be focused (active), but disabled.
        // Disabled items must not action on click (or up/down arrow)
        // http://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#menu
        // "Disabled menu items receive focus but have no action when Enter or
        // Left Arrow/Right Arrow is pressed."
        if (!me.disabled && me.activated && me.menu) {
            // hideOnClick makes no sense when there's a child menu
            me.hideOnClick = false;
            me.cancelDeferHide();
            // Allow configuration of zero to perform immediate expansion.
            delay = delay == null ? me.menuExpandDelay : delay;
            if (delay === 0) {
                me.doExpandMenu(event);
            } else {
                me.cancelDeferExpand();
                // Delay can't be 0 by this point
                me.expandMenuTimer = Ext.defer(me.doExpandMenu, delay, me, [
                    event
                ]);
            }
        }
    },
    doExpandMenu: function(clickEvent) {
        var me = this,
            menu = me.menu;
        if (!menu.isVisible()) {
            me.parentMenu.activeChild = menu;
            menu.ownerCmp = me;
            menu.parentMenu = me.parentMenu;
            menu.constrainTo = document.body;
            // Pointer-invoked menus do not auto focus, key invoked ones do.
            menu.autoFocus = !clickEvent || !clickEvent.pointerType;
            menu.showBy(me, me.menuAlign);
        }
        // Keyboard events should focus the first menu item even if it was already expanded
        else if (clickEvent && clickEvent.type === 'keydown') {
            menu.focus();
        }
    },
    getRefItems: function(deep) {
        var menu = this.menu,
            items;
        if (menu) {
            items = menu.getRefItems(deep);
            items.unshift(menu);
        }
        return items || [];
    },
    getValue: function() {
        return this.value;
    },
    hideMenu: function(delay) {
        var me = this;
        if (me.menu) {
            me.cancelDeferExpand();
            me.hideMenuTimer = Ext.defer(me.doHideMenu, Ext.isNumber(delay) ? delay : me.menuHideDelay, me);
        }
    },
    onClick: function(e) {
        var me = this,
            clickHideDelay = me.clickHideDelay,
            browserEvent = e.browserEvent,
            clickResult, preventDefault;
        if (!me.href || me.disabled) {
            e.stopEvent();
            if (me.disabled) {
                return false;
            }
        }
        if (me.disabled || me.handlingClick) {
            return;
        }
        if (me.hideOnClick && !me.menu) {
            // on mobile webkit, when the menu item has an href, a longpress will 
            // trigger the touch call-out menu to show.  If this is the case, the tap 
            // event object's browser event type will be 'touchcancel', and we do not 
            // want to hide the menu.
            // items with submenus are activated by touchstart on mobile browsers, so
            // we cannot hide the menu on "tap"
            if (!clickHideDelay) {
                me.deferHideParentMenus();
            } else {
                me.deferHideParentMenusTimer = Ext.defer(me.deferHideParentMenus, clickHideDelay, me);
            }
        }
        // Click event may have destroyed the menu, don't do anything further
        clickResult = me.fireEvent('click', me, e);
        // Click listener could have destroyed the menu and/or item.
        if (me.destroyed) {
            return;
        }
        if (clickResult !== false && me.handler) {
            Ext.callback(me.handler, me.scope, [
                me,
                e
            ], 0, me);
        }
        // And the handler could have done the same. We check this twice
        // because if the menu was destroyed in the click listener, the handler
        // should not have been called.
        if (me.destroyed) {
            return;
        }
        // If there's an href, invoke dom.click() after we've fired the click event in case a click
        // listener wants to handle it.
        //
        // Note that we're having to do this because the key navigation code will blindly call
        // stopEvent() on all key events that it handles!
        //
        // But, we need to check the browser event object that was passed to the listeners
        // to determine if the default action has been prevented.
        // If so, we don't want to honor the .href config.
        if (Ext.isIE9m) {
            // Here we need to invert the value since it's meaning is the opposite
            // of defaultPrevented.
            preventDefault = browserEvent.returnValue === false ? true : false;
        } else {
            preventDefault = !!browserEvent.defaultPrevented;
        }
        // We only manually need to trigger the click event if it's come from a key event.
        if (me.href && e.type !== 'click' && !preventDefault) {
            me.handlingClick = true;
            me.itemEl.dom.click();
            me.handlingClick = false;
        }
        if (!me.hideOnClick && !me.hasFocus) {
            me.focus();
        }
        return clickResult;
    },
    onRemoved: function() {
        var me = this;
        // Removing the active item, must deactivate it.
        if (me.activated && me.parentMenu.activeItem === me) {
            me.parentMenu.deactivateActiveItem();
        }
        me.callParent(arguments);
        me.parentMenu = me.ownerCmp = null;
    },
    doDestroy: function() {
        var me = this;
        if (me.rendered) {
            me.clearTip();
        }
        me.cancelDeferExpand();
        me.cancelDeferHide();
        Ext.undefer(me.deferHideParentMenusTimer);
        me.setMenu(null);
        me.callParent();
    },
    beforeRender: function() {
        var me = this,
            glyph = me.glyph,
            glyphFontFamily,
            hasIcon = !!(me.icon || me.iconCls || glyph),
            hasMenu = !!me.menu,
            rightIcon = ((me.iconAlign === 'right') && !hasMenu),
            isCheckItem = me.isMenuCheckItem,
            indentCls = [],
            ownerCt = me.ownerCt,
            isOwnerPlain = ownerCt.plain;
        if (me.plain) {
            me.ariaEl = 'el';
        }
        me.callParent();
        if (hasIcon) {
            if (hasMenu && me.showCheckbox) {
                // nowhere to put the icon, menu arrow on one side, checkbox on the other.
                // TODO:  maybe put the icon or checkbox next to the arrow?
                hasIcon = false;
            }
        }
        // Transform Glyph to the useful parts
        if (glyph) {
            glyphFontFamily = glyph.fontFamily;
            glyph = glyph.character;
        }
        if (!isOwnerPlain || (hasIcon && !rightIcon) || isCheckItem) {
            if (ownerCt.showSeparator && !isOwnerPlain) {
                indentCls.push(me.indentCls);
            } else {
                indentCls.push(me.indentNoSeparatorCls);
            }
        }
        if (hasMenu) {
            indentCls.push(me.indentRightArrowCls);
        } else if (hasIcon && (rightIcon || isCheckItem)) {
            indentCls.push(me.indentRightIconCls);
        }
        Ext.applyIf(me.renderData, {
            hasHref: !!me.href,
            href: me.href || '#',
            hrefTarget: me.hrefTarget,
            icon: me.icon,
            iconCls: me.iconCls,
            glyph: glyph,
            glyphCls: glyph ? Ext.baseCSSPrefix + 'menu-item-glyph' : undefined,
            glyphFontFamily: glyphFontFamily,
            hasIcon: hasIcon,
            hasMenu: hasMenu,
            indent: !isOwnerPlain || hasIcon || isCheckItem,
            isCheckItem: isCheckItem,
            rightIcon: rightIcon,
            plain: me.plain,
            text: me.text,
            arrowCls: me.arrowCls,
            baseIconCls: me.baseIconCls,
            textCls: me.textCls,
            indentCls: indentCls.join(' '),
            linkCls: me.linkCls,
            linkHrefCls: me.linkHrefCls,
            groupCls: me.group ? me.groupCls : '',
            tabIndex: me.tabIndex
        });
    },
    onRender: function() {
        var me = this;
        me.callParent(arguments);
        if (me.tooltip) {
            me.setTooltip(me.tooltip, true);
        }
    },
    /**
     * Get the attached sub-menu for this item.
     * @return {Ext.menu.Menu} The sub-menu. `null` if it doesn't exist.
     */
    getMenu: function() {
        return this.menu || null;
    },
    /**
     * Set a child menu for this item. See the {@link #cfg-menu} configuration.
     * @param {Ext.menu.Menu/Object} menu A menu, or menu configuration. null may be
     * passed to remove the menu.
     * @param {Boolean} [destroyMenu] True to destroy any existing menu. False to
     * prevent destruction. If not specified, the {@link #destroyMenu} configuration
     * will be used.
     */
    setMenu: function(menu, destroyMenu) {
        var me = this,
            oldMenu = me.menu,
            arrowEl = me.arrowEl,
            ariaDom = me.ariaEl.dom,
            ariaAttr, instanced;
        if (oldMenu) {
            oldMenu.ownerCmp = oldMenu.parentMenu = null;
            if (destroyMenu === true || (destroyMenu !== false && me.destroyMenu)) {
                Ext.destroy(oldMenu);
            }
            if (ariaDom) {
                ariaDom.removeAttribute('aria-haspopup');
                ariaDom.removeAttribute('aria-owns');
            } else {
                ariaAttr = (me.ariaRenderAttributes || (me.ariaRenderAttributes = {}));
                delete ariaAttr['aria-haspopup'];
                delete ariaAttr['aria-owns'];
            }
        }
        if (menu) {
            instanced = menu.isMenu;
            menu = me.menu = Ext.menu.Manager.get(menu, {
                ownerCmp: me,
                focusOnToFront: false
            });
            // We need to forcibly set this here because we could be passed
            // an existing menu, which means the config above won't get applied
            // during creation.
            menu.setOwnerCmp(me, instanced);
            if (ariaDom) {
                ariaDom.setAttribute('aria-haspopup', true);
                ariaDom.setAttribute('aria-owns', menu.id);
            } else {
                ariaAttr = (me.ariaRenderAttributes || (me.ariaRenderAttributes = {}));
                ariaAttr['aria-haspopup'] = true;
                ariaAttr['aria-owns'] = menu.id;
            }
        } else {
            menu = me.menu = null;
        }
        if (menu && me.rendered && !me.destroying && arrowEl) {
            arrowEl[menu ? 'addCls' : 'removeCls'](me.arrowCls);
        }
    },
    /**
     * Sets the {@link #click} handler of this item
     * @param {Function} fn The handler function
     * @param {Object} [scope] The scope of the handler function
     */
    setHandler: function(fn, scope) {
        this.handler = fn || null;
        this.scope = scope;
    },
    /**
     * Sets the {@link #icon} on this item.
     * @param {String} icon The new icon URL. If this `MenuItem` was configured with a
     * {@link #cfg-glyph}, this may be a glyph configuration. See {@link #cfg-glyph}.
     */
    setIcon: function(icon) {
        var me = this,
            iconEl = me.iconEl,
            oldIcon = me.icon;
        // If setIcon is called when we are configured with a glyph, clear the glyph
        if (me.glyph) {
            me.setGlyph(null);
        }
        if (iconEl) {
            iconEl.setStyle('background-image', icon ? 'url(' + icon + ')' : '');
        }
        me.icon = icon;
        me.fireEvent('iconchange', me, oldIcon, icon);
    },
    /**
     * Sets the {@link #iconCls} of this item
     * @param {String} iconCls The CSS class to set to {@link #iconCls}
     */
    setIconCls: function(iconCls) {
        var me = this,
            iconEl = me.iconEl,
            oldCls = me.iconCls;
        // If setIcon is called when we are configured with a glyph, clear the glyph
        if (me.glyph) {
            me.setGlyph(null);
        }
        if (iconEl) {
            // In case it had been set to 'none' by a glyph setting.
            iconEl.setStyle('background-image', '');
            if (me.iconCls) {
                iconEl.removeCls(me.iconCls);
            }
            if (iconCls) {
                iconEl.addCls(iconCls);
            }
        }
        me.iconCls = iconCls;
        me.fireEvent('iconchange', me, oldCls, iconCls);
    },
    /**
     * Sets the {@link #text} of this item
     * @param {String} text The {@link #text}
     */
    setText: function(text) {
        var me = this,
            el = me.textEl || me.el,
            oldText = me.text;
        me.text = text;
        if (me.rendered) {
            el.setHtml(text || '');
            me.updateLayout();
        }
        me.fireEvent('textchange', me, oldText, text);
    },
    getTipAttr: function() {
        return this.tooltipType === 'qtip' ? 'data-qtip' : 'title';
    },
    /**
     * @private
     */
    clearTip: function() {
        if (Ext.quickTipsActive && Ext.isObject(this.tooltip)) {
            Ext.tip.QuickTipManager.unregister(this.itemEl);
        }
    },
    /**
     * Sets the tooltip for this menu item.
     *
     * @param {String/Object} tooltip This may be:
     *
     *   - **String** : A string to be used as innerHTML (html tags are accepted) to show
     *     in a tooltip
     *   - **Object** : A configuration object for {@link Ext.tip.QuickTipManager#register}.
     *
     * @param {Boolean} [initial] (private)
     *
     * @return {Ext.menu.Item} this
     */
    setTooltip: function(tooltip, initial) {
        var me = this;
        if (me.rendered) {
            if (!initial) {
                me.clearTip();
            }
            if (Ext.quickTipsActive && Ext.isObject(tooltip)) {
                Ext.tip.QuickTipManager.register(Ext.apply({
                    target: me.itemEl.id
                }, tooltip));
                me.tooltip = tooltip;
            } else {
                me.itemEl.dom.setAttribute(me.getTipAttr(), tooltip);
            }
        } else {
            me.tooltip = tooltip;
        }
        return me;
    },
    getFocusEl: function() {
        return this.plain ? this.el : this.itemEl;
    },
    getFocusClsEl: function() {
        return this.el;
    },
    privates: {
        cancelDeferExpand: function() {
            window.clearTimeout(this.expandMenuTimer);
        },
        cancelDeferHide: function() {
            window.clearTimeout(this.hideMenuTimer);
        }
    },
    applyGlyph: function(glyph, oldGlyph) {
        if (glyph) {
            if (!glyph.isGlyph) {
                glyph = new Ext.Glyph(glyph);
            }
            if (glyph.isEqual(oldGlyph)) {
                glyph = undefined;
            }
        }
        return glyph;
    },
    updateGlyph: function(glyph, oldGlyph) {
        var iconEl = this.iconEl;
        if (iconEl) {
            iconEl.setStyle('background-image', 'none');
            this.icon = null;
            if (glyph) {
                iconEl.dom.innerHTML = glyph.character;
                iconEl.setStyle(glyph.getStyle());
            } else {
                iconEl.dom.innerHTML = '';
            }
        }
    }
});

/**
 * A menu item that contains a togglable checkbox by default, but that can also be a part
 * of a radio group.
 *
 *     @example
 *     Ext.create('Ext.menu.Menu', {
 *         width: 100,
 *         height: 110,
 *         floating: false,  // usually you want this set to True (default)
 *         renderTo: Ext.getBody(),  // usually rendered by it's containing component
 *         items: [{
 *             xtype: 'menucheckitem',
 *             text: 'select all'
 *         }, {
 *             xtype: 'menucheckitem',
 *             text: 'select specific'
 *         }, {
 *             iconCls: 'add16',
 *             text: 'icon item'
 *         }, {
 *             text: 'regular item'
 *         }]
 *     });
 */
Ext.define('Ext.menu.CheckItem', {
    extend: 'Ext.menu.Item',
    alias: 'widget.menucheckitem',
    /**
     * @cfg {Boolean} [checked=false]
     * True to render the menuitem initially checked.
     */
    /**
     * @cfg {Function/String} checkHandler
     * Alternative for the {@link #checkchange} event.  Gets called with the same parameters.
     * @controllable
     */
    /**
     * @cfg {Object} scope
     * Scope for the {@link #checkHandler} callback.
     */
    /**
     * @cfg {String} group
     * Name of a radio group that the item belongs.
     *
     * Specifying this option will turn check item into a radio item.
     *
     * Note that the group name must be globally unique.
     */
    /**
     * @cfg {String} checkedCls
     * The CSS class used by {@link #cls} to show the checked state.
     * Defaults to `Ext.baseCSSPrefix + 'menu-item-checked'`.
     */
    checkedCls: Ext.baseCSSPrefix + 'menu-item-checked',
    /**
     * @cfg {String} uncheckedCls
     * The CSS class used by {@link #cls} to show the unchecked state.
     * Defaults to `Ext.baseCSSPrefix + 'menu-item-unchecked'`.
     */
    uncheckedCls: Ext.baseCSSPrefix + 'menu-item-unchecked',
    /**
     * @cfg {String} groupCls
     * The CSS class applied to this item's icon image to denote being a part of a radio group.
     * Defaults to `Ext.baseCSSClass + 'menu-group-icon'`.
     * Any specified {@link #iconCls} overrides this.
     */
    groupCls: Ext.baseCSSPrefix + 'menu-group-icon',
    /**
     * @cfg {Boolean} [hideOnClick=false]
     * Whether to not to hide the owning menu when this item is clicked.
     * Defaults to `false` for checkbox items, and to `true` for radio group items.
     */
    hideOnClick: false,
    /**
     * @cfg {Boolean} [checkChangeDisabled=false]
     * True to prevent the checked item from being toggled. Any submenu will still be accessible.
     */
    checkChangeDisabled: false,
    /**
     * @cfg {String} submenuText Text to be announced by screen readers when a check item
     * submenu is focused.
     * @locale
     */
    submenuText: '{0} submenu',
    ariaRole: 'menuitemcheckbox',
    childEls: [
        'checkEl'
    ],
    defaultBindProperty: 'checked',
    showCheckbox: true,
    isMenuCheckItem: true,
    checkboxCls: Ext.baseCSSPrefix + 'menu-item-checkbox',
    /**
     * @event beforecheckchange
     * Fires before a change event. Return false to cancel.
     * @param {Ext.menu.CheckItem} this
     * @param {Boolean} checked
     */
    /**
     * @event checkchange
     * Fires after a change event.
     * @param {Ext.menu.CheckItem} this
     * @param {Boolean} checked
     */
    initComponent: function() {
        var me = this,
            checked = me.checked;
        me.checkedConfigure = checked;
        // coerce to bool straight away
        me.checked = !!checked;
        me.callParent();
        if (me.group) {
            Ext.menu.Manager.registerCheckable(me);
            if (me.initialConfig.hideOnClick !== false) {
                me.hideOnClick = true;
            }
        }
    },
    beforeRender: function() {
        var me = this,
            ariaAttr;
        me.callParent();
        Ext.apply(me.renderData, {
            checkboxCls: me.checkboxCls,
            showCheckbox: me.showCheckbox
        });
        ariaAttr = (me.ariaRenderAttributes || (me.ariaRenderAttributes = {}));
        ariaAttr['aria-checked'] = me.menu ? 'mixed' : me.checked;
        // For some reason JAWS will not announce that a check item has a submenu
        // so users will get no indication whatsoever, unless we set the label.
        if (me.menu) {
            ariaAttr['aria-label'] = Ext.String.formatEncode(me.submenuText, me.text);
        }
    },
    afterRender: function() {
        var me = this;
        me.callParent();
        me.checked = !me.checked;
        me.initial = true;
        me.setChecked(!me.checked, true);
        me.initial = false;
        if (me.checkChangeDisabled) {
            me.disableCheckChange();
        }
        // For reasons unknown, clicking a div inside anchor element might cause
        // the anchor to be blurred in Firefox. We can't allow this to happen
        // because blurring will cause focusleave which will hide the menu
        // before click event fires. See https://sencha.jira.com/browse/EXTJS-18882
        if (Ext.isGecko && me.checkEl) {
            me.checkEl.on('mousedown', me.onMouseDownCheck);
        }
    },
    /**
     * Disables just the checkbox functionality of this menu Item. If this menu item has
     * a submenu, that submenu will still be accessible
     */
    disableCheckChange: function() {
        var me = this,
            checkEl = me.checkEl;
        if (checkEl) {
            checkEl.addCls(me.disabledCls);
        }
        // In some cases the checkbox will disappear until repainted, see: EXTJSIV-6412
        if (Ext.isIE8 && me.rendered) {
            me.el.repaint();
        }
        me.checkChangeDisabled = true;
    },
    /**
     * Re-enables the checkbox functionality of this menu item after having been 
     * disabled by {@link #disableCheckChange}
     */
    enableCheckChange: function() {
        var me = this,
            checkEl = me.checkEl;
        if (checkEl) {
            checkEl.removeCls(me.disabledCls);
        }
        me.checkChangeDisabled = false;
    },
    onMouseDownCheck: function(e) {
        e.preventDefault();
    },
    onClick: function(e) {
        var me = this;
        // If pointer type is touch, we should only toggle check status if there's no submenu
        // or they tapped in the checkEl. This is because there's no hover to invoke the submenu
        // on touch devices, so a tap is needed to show it. That tap should not toggle
        // unless it's on the checkbox.
        if (!(me.disabled || me.checkChangeDisabled || me.checked && me.group || me.menu && "touch" === e.pointerType && !me.checkEl.contains(e.target))) {
            me.setChecked(!me.checked);
            // Clicked using SPACE or ENTER just un-checks.
            // RightArrow to invoke any submenu
            if (e.type === 'keydown' && me.menu) {
                return false;
            }
        }
        return me.callParent([
            e
        ]);
    },
    doDestroy: function() {
        Ext.menu.Manager.unregisterCheckable(this);
        this.callParent();
    },
    setText: function(text) {
        var me = this,
            ariaDom = me.ariaEl.dom;
        me.callParent([
            text
        ]);
        if (ariaDom && me.menu) {
            ariaDom.setAttribute('aria-label', Ext.String.formatEncode(me.submenuText, text));
        }
    },
    /**
     * @cfg [publishes='checked']
     * @inheritdoc
     */
    /**
     * Sets the checked state of the item
     * @param {Boolean} checked True to check, false to un-check
     * @param {Boolean} [suppressEvents=false] True to prevent firing the checkchange events.
     */
    setChecked: function(checked, suppressEvents) {
        var me = this,
            checkedCls = me.checkedCls,
            uncheckedCls = me.uncheckedCls,
            el = me.el,
            ariaDom = me.ariaEl.dom,
            checkedConfigure = me.checkedConfigure;
        if (me.checked !== checked && (suppressEvents || me.fireEvent('beforecheckchange', me, checked) !== false)) {
            if (el) {
                if (checked) {
                    el.addCls(checkedCls);
                    el.removeCls(uncheckedCls);
                } else {
                    el.addCls(uncheckedCls);
                    el.removeCls(checkedCls);
                }
            }
            if (ariaDom) {
                ariaDom.setAttribute('aria-checked', me.menu ? 'mixed' : !!checked);
            }
            me.checked = checked;
            me.checkedConfigure = checked;
            Ext.menu.Manager.onCheckChange(me, checked);
            // Don't publish the state if we're initially setting the
            // checked state and we didn't get configured with a value
            if (!(me.initial && checkedConfigure == null)) {
                me.publishState('checked', checked);
            }
            if (!suppressEvents) {
                Ext.callback(me.checkHandler, me.scope, [
                    me,
                    checked
                ], 0, me);
                me.fireEvent('checkchange', me, checked);
            }
        }
    }
});

/**
 * Adds a separator bar to a menu, used to divide logical groups of menu items. Generally you will
 * add one of these by using "-" in your call to add() or in your items config rather than creating
 * one directly.
 *
 *     @example
 *     Ext.create('Ext.menu.Menu', {
 *         width: 100,
 *         height: 100,
 *         floating: false,  // usually you want this set to True (default)
 *         renderTo: Ext.getBody(),  // usually rendered by it's containing component
 *         items: [{
 *             text: 'icon item',
 *             iconCls: 'add16'
 *         },{
 *             xtype: 'menuseparator'
 *         },{
 *            text: 'separator above'
 *         },{
 *            text: 'regular item'
 *         }]
 *     });
 */
Ext.define('Ext.menu.Separator', {
    extend: 'Ext.menu.Item',
    alias: 'widget.menuseparator',
    focusable: false,
    /**
     * @cfg {String} activeCls
     * @private
     */
    /**
     * @cfg {Boolean} canActivate
     * @private
     */
    canActivate: false,
    /**
     * @cfg {Boolean} clickHideDelay
     * @private
     */
    /**
     * @cfg {Boolean} destroyMenu
     * @private
     */
    /**
     * @cfg {Boolean} disabledCls
     * @private
     */
    /**
     * @cfg {String} href
     * @private
     */
    /**
     * @cfg {String} hrefTarget
     * @private
     */
    /**
     * @cfg {Boolean} hideOnClick
     * @private
     */
    hideOnClick: false,
    /**
     * @cfg {String} icon
     * @private
     */
    /**
     * @cfg {String} iconCls
     * @private
     */
    /**
     * @cfg {Object} menu
     * @private
     */
    /**
     * @cfg {String} menuAlign
     * @private
     */
    /**
     * @cfg {Number} menuExpandDelay
     * @private
     */
    /**
     * @cfg {Number} menuHideDelay
     * @private
     */
    /**
     * @cfg {Boolean} plain
     * @private
     */
    plain: true,
    /**
     * @cfg {String} separatorCls
     * The CSS class used by the separator item to show the incised line.
     */
    separatorCls: Ext.baseCSSPrefix + 'menu-item-separator',
    /**
     * @cfg {String} text
     * @private
     */
    text: '&#160;',
    ariaRole: 'separator',
    beforeRender: function() {
        this.addCls(this.separatorCls);
        this.callParent();
    }
});

/**
 * A menu object. This is the container to which you may add {@link Ext.menu.Item menu items}.
 *
 * Menus may contain either {@link Ext.menu.Item menu items}, or general
 * {@link Ext.Component Components}. Menus may also contain
 * {@link Ext.panel.Panel#dockedItems docked items} because it extends {@link Ext.panel.Panel}.
 *
 * By default, non {@link Ext.menu.Item menu items} are indented so that they line up with the text
 * of menu items, clearing the icon column. To make a contained general
 * {@link Ext.Component Component} left aligned configure the child Component with `indent: false`.
 *
 * By default, Menus are absolutely positioned, floating Components. By configuring a 
 * Menu with `{@link #cfg-floating}: false`, a Menu may be used as a child of a 
 * {@link Ext.container.Container Container}.
 *
 *     @example
 *     Ext.create('Ext.menu.Menu', {
 *         width: 100,
 *         margin: '0 0 10 0',
 *         floating: false,  // usually you want this set to True (default)
 *         renderTo: Ext.getBody(),  // usually rendered by it's containing component
 *         items: [{
 *             text: 'regular item 1'
 *         }, {
 *             text: 'regular item 2'
 *         }, {
 *             text: 'regular item 3'
 *         }]
 *     });
 *
 *     Ext.create('Ext.menu.Menu', {
 *         width: 100,
 *         plain: true,
 *         floating: false,  // usually you want this set to True (default)
 *         renderTo: Ext.getBody(),  // usually rendered by it's containing component
 *         items: [{
 *             text: 'plain item 1'
 *         }, {
 *             text: 'plain item 2'
 *         }, {
 *             text: 'plain item 3'
 *         }]
 *     });
 */
Ext.define('Ext.menu.Menu', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.menu',
    requires: [
        'Ext.layout.container.VBox',
        'Ext.menu.CheckItem',
        'Ext.menu.Item',
        'Ext.menu.Manager',
        'Ext.menu.Separator'
    ],
    defaultType: 'menuitem',
    /**
     * @property {Ext.menu.Menu} parentMenu
     * The parent Menu of this Menu.
     */
    /**
     * @cfg {Boolean} [enableKeyNav=true]
     * @deprecated 5.1.0 Intra-menu key navigation is always enabled.
     */
    enableKeyNav: true,
    /**
     * @cfg {Boolean} [allowOtherMenus=false]
     * True to allow multiple menus to be displayed at the same time.
     */
    allowOtherMenus: false,
    /**
     * @cfg {String} ariaRole
     * @private
     */
    ariaRole: 'menu',
    /**
     * @cfg {Boolean} autoRender
     * Floating is true, so autoRender always happens.
     * @private
     */
    /**
     * @cfg {Boolean} [floating=true]
     * A Menu configured as `floating: true` (the default) will be rendered as an 
     * absolutely positioned,
     * {@link Ext.Component#cfg-floating floating} {@link Ext.Component Component}. If 
     * configured as `floating: false`, the Menu may be used as a child item of another 
     * {@link Ext.container.Container Container}.
     */
    floating: true,
    /**
     * @cfg {Boolean} constrain
     * Menus are constrained to the document body by default.
     * @private
     */
    constrain: true,
    /**
     * @cfg {Boolean} [hidden]
     * True to initially render the Menu as hidden, requiring to be shown manually.
     *
     * Defaults to `true` when `floating: true`, and defaults to `false` when `floating: false`.
     */
    hidden: true,
    hideMode: 'visibility',
    /**
     * @cfg {Boolean} [ignoreParentClicks=false]
     * True to ignore clicks on any item in this menu that is a parent item (displays a submenu)
     * so that the submenu is not dismissed when clicking the parent item.
     */
    ignoreParentClicks: false,
    /**
     * @cfg {Number} [mouseLeaveDelay]
     * The delay in ms as to how long the framework should wait before firing a mouseleave event.
     * This allows submenus not to be collapsed while hovering other menu items.
     *
     * Defaults to 50
     */
    mouseLeaveDelay: 50,
    /**
     * @property {Boolean} isMenu
     * `true` in this class to identify an object as an instantiated Menu, or subclass thereof.
     */
    isMenu: true,
    /**
     * @cfg {Ext.enums.Layout/Object} layout
     * @private
     */
    /**
     * @cfg {Boolean} [showSeparator=true]
     * True to show the icon separator.
     */
    showSeparator: true,
    /**
     * @cfg {Number} [minWidth=120]
     * The minimum width of the Menu. The default minWidth only applies when the 
     * {@link #cfg-floating} config is true.
     */
    minWidth: undefined,
    defaultMinWidth: 120,
    /**
     * @cfg {String} [defaultAlign="tl-bl?"]
     * The default {@link Ext.util.Positionable#getAlignToXY Ext.dom.Element#getAlignToXY}
     * anchor position value for this menu relative to its owner. Used in conjunction with
     * {@link #showBy}.
     */
    defaultAlign: 'tl-bl?',
    /**
     * @cfg {Boolean} [plain=false]
     * True to remove the incised line down the left side of the menu and to not indent general
     * Component items.
     * 
     * {@link Ext.menu.Item MenuItem}s will *always* have space at their start for an icon.
     * With the `plain` setting, non {@link Ext.menu.Item MenuItem} child components will not
     * be indented to line up.
     * 
     * Basically, `plain:true` makes a Menu behave more like a regular
     * {@link Ext.layout.container.HBox HBox layout} {@link Ext.panel.Panel Panel}
     * which just has the same background as a Menu.
     * 
     * See also the {@link #showSeparator} config.
     */
    /**
     * @cfg focusOnToFront
     * @inheritdoc
     */
    focusOnToFront: false,
    bringParentToFront: false,
    alignOnScroll: false,
    // Menus are focusable
    focusable: true,
    tabIndex: -1,
    focusableContainer: true,
    // When a Menu is used as a carrier to float some focusable Component such as a DatePicker
    // or ColorPicker
    // This will be used to delegate focus to its focusable child.
    // In normal usage, a Menu is a FocusableContainer, and this will not be consulted.
    defaultFocus: ':focusable',
    // We need to focus disabled menu items when arrowing as per WAI-ARIA:
    // http://www.w3.org/TR/wai-aria-practices/#menu
    allowFocusingDisabledChildren: true,
    /**
     * @private
     */
    menuClickBuffer: 0,
    baseCls: Ext.baseCSSPrefix + 'menu',
    _iconSeparatorCls: Ext.baseCSSPrefix + 'menu-icon-separator',
    _itemCmpCls: Ext.baseCSSPrefix + 'menu-item-cmp',
    /**
     * @event click
     * Fires when this menu is clicked
     * @param {Ext.menu.Menu} menu The menu which has been clicked
     * @param {Ext.Component} item The menu item that was clicked. `undefined` if not applicable.
     * @param {Ext.event.Event} e The underlying {@link Ext.event.Event}.
     */
    /**
     * @event mouseenter
     * Fires when the mouse enters this menu
     * @param {Ext.menu.Menu} menu The menu
     * @param {Ext.event.Event} e The underlying {@link Ext.event.Event}
     */
    /**
     * @event mouseleave
     * Fires when the mouse leaves this menu
     * @param {Ext.menu.Menu} menu The menu
     * @param {Ext.event.Event} e The underlying {@link Ext.event.Event}
     */
    /**
     * @event mouseover
     * Fires when the mouse is hovering over this menu
     * @param {Ext.menu.Menu} menu The menu
     * @param {Ext.Component} item The menu item that the mouse is over. `undefined`
     * if not applicable.
     * @param {Ext.event.Event} e The underlying {@link Ext.event.Event}
     */
    layout: {
        type: 'vbox',
        align: 'stretchmax',
        overflowHandler: 'Scroller'
    },
    initComponent: function() {
        var me = this,
            cls = [
                Ext.baseCSSPrefix + 'menu'
            ],
            bodyCls = me.bodyCls ? [
                me.bodyCls
            ] : [],
            isFloating = me.floating !== false,
            listeners = {
                element: 'el',
                click: me.onClick,
                mouseover: me.onMouseOver,
                scope: me
            };
        if (Ext.supports.Touch) {
            listeners.pointerdown = me.onMouseOver;
        }
        me.on(listeners);
        me.on({
            beforeshow: me.onBeforeShow,
            scope: me
        });
        // Menu classes
        if (me.plain) {
            cls.push(Ext.baseCSSPrefix + 'menu-plain');
        }
        me.cls = cls.join(' ');
        // Menu body classes
        bodyCls.push(Ext.baseCSSPrefix + 'menu-body', Ext.dom.Element.unselectableCls);
        me.bodyCls = bodyCls.join(' ');
        if (isFloating) {
            // only apply the minWidth when we're floating & one hasn't already been set
            if (me.minWidth === undefined) {
                me.minWidth = me.defaultMinWidth;
            }
        } else {
            // hidden defaults to false if floating is configured as false
            me.hidden = !!me.initialConfig.hidden;
            me.constrain = false;
        }
        me.callParent();
        // Configure items prior to render with special classes to align
        // non MenuItem child components with their MenuItem siblings.
        Ext.override(me.getLayout(), {
            configureItem: me.configureItem
        });
        me.itemOverTask = new Ext.util.DelayedTask(me.handleItemOver, me);
    },
    // Private implementation for Menus. They are a special case, in that in the vast majority
    // (nearly all?) of use cases they shouldn't be constrained to anything other than the viewport.
    // See EXTJS-13596.
    /**
     * @method
     * @private
     */
    initFloatConstrain: Ext.emptyFn,
    getInherited: function() {
        // As floating menus are never contained, a floating Menu's visibility only ever depends
        // upon its own hidden state.
        // Ignore hiddenness from the ancestor hierarchy, override it with local hidden state.
        var result = this.callParent();
        if (this.floating) {
            result.hidden = this.hidden;
        }
        return result;
    },
    beforeRender: function() {
        var me = this;
        me.callParent();
        // Menus are usually floating: true, which means they shrink wrap their items.
        // However, when they are contained, and not auto sized, we must stretch the items.
        if (!me.getSizeModel().width.shrinkWrap) {
            me.layout.align = 'stretch';
        }
        if (me.floating) {
            me.ariaRenderAttributes = me.ariaRenderAttributes || {};
            me.ariaRenderAttributes['aria-expanded'] = !!me.autoShow;
        }
    },
    onBoxReady: function(width, height) {
        var me = this,
            iconSeparatorCls = me._iconSeparatorCls,
            keyNav = me.focusableKeyNav;
        // Keyboard handling can be disabled, e.g. by the DatePicker menu
        // or the Date filter menu constructed by the Grid
        if (keyNav) {
            // Handle ESC key
            keyNav.map.addBinding([
                {
                    key: Ext.event.Event.ESC,
                    handler: me.onEscapeKey,
                    scope: me
                },
                // Handle character shortcuts
                {
                    key: /[\w]/,
                    handler: me.onShortcutKey,
                    scope: me,
                    shift: false,
                    ctrl: false,
                    alt: false
                }
            ]);
        } else {
            // Even when FocusableContainer key event processing is disabled,
            // we still need to handle the Escape key!
            me.escapeKeyNav = new Ext.util.KeyNav({
                target: me.el,
                eventName: 'keydown',
                scope: me,
                esc: me.onEscapeKey
            });
        }
        me.callParent([
            width,
            height
        ]);
        // TODO: Move this to a subTemplate When we support them in the future
        if (me.showSeparator) {
            me.iconSepEl = me.body.insertFirst({
                role: 'presentation',
                cls: iconSeparatorCls + ' ' + iconSeparatorCls + '-' + me.ui,
                html: '&#160;'
            });
        }
        // Modern IE browsers have click events translated to PointerEvents, and b/c of this the
        // event isn't being canceled like it needs to be. So, we need to add an extra listener.
        // For devices that have touch support, the default click event may be a gesture that
        // runs asynchronously, so by the time we try and prevent it, it's already happened
        if (Ext.supports.Touch || Ext.supports.MSPointerEvents || Ext.supports.PointerEvents) {
            me.el.on({
                scope: me,
                click: me.preventClick,
                translate: false
            });
        }
        me.mouseMonitor = me.el.monitorMouseLeave(me.mouseLeaveDelay, me.onMouseLeave, me);
    },
    onFocusEnter: function(e) {
        var me = this,
            hierarchyState;
        me.callParent([
            e
        ]);
        me.mixins.focusablecontainer.onFocusEnter.call(me, e);
        if (me.floating) {
            hierarchyState = me.getInherited();
            // The topmost focusEnter event upon entry into a floating menu stack
            // is recorded in the hierarchy state.
            //
            // Focusing upwards from descendant menus in a stack will NOT trigger onFocusEnter
            // on the parent menu because focus is already in its component tree.
            // For focusing downwards we check for presence of the topmostFocusEvent
            // already being present in the hierarchy.
            //
            // If we need to explicitly access a focus reversion point, we can use that.
            // This is only ever needed if tabbing forwards from the menu. We explicitly
            // push focus to the topmost focusEnter component, and then allow natural
            // tabbing to proceed from there.
            //
            // In all other focus reversion scenarios we use the immediate focusEnter event
            if (!hierarchyState.topmostFocusEvent) {
                hierarchyState.topmostFocusEvent = e;
            }
        }
    },
    onFocusLeave: function(e) {
        var me = this;
        me.callParent([
            e
        ]);
        // We need to make sure that menus do not "remember" the last focused item
        // so that the first menu item is always activated when the menu is shown.
        // This is the expected behavior according to WAI-ARIA spec.
        me.lastFocusedChild = null;
        me.mixins.focusablecontainer.onFocusLeave.call(me, e);
        if (me.floating) {
            me.hide();
        }
    },
    handleItemOver: function(e, item) {
        // Only focus non-menuitem on real mouseover events.
        if (!item.containsFocus && (e.pointerType === 'mouse' || item.isMenuItem)) {
            item.focus();
        }
        if (item.expandMenu) {
            item.expandMenu(e);
        }
    },
    /**
     * @param {Ext.Component} item The child item to test for focusability.
     * Returns whether a menu item can be activated or not.
     * @return {Boolean} `true` if the passed item is focusable.
     */
    canActivateItem: function(item) {
        return item && item.isFocusable();
    },
    /**
     * Deactivates the current active item on the menu, if one exists.
     */
    deactivateActiveItem: function() {
        var me = this,
            activeItem = me.lastFocusedChild;
        if (activeItem) {
            activeItem.blur();
        }
    },
    /**
     * @private
     */
    getItemFromEvent: function(e) {
        var me = this,
            renderTarget = me.layout.getRenderTarget().dom,
            toEl = e.getTarget();
        // See which top level element the event is in and find its owning Component.
        while (toEl.parentNode !== renderTarget) {
            toEl = toEl.parentNode;
            if (!toEl) {
                return;
            }
        }
        return Ext.getCmp(toEl.id);
    },
    lookupComponent: function(cmp) {
        var me = this;
        if (typeof cmp === 'string') {
            if (cmp[0] === '@') {
                cmp = this.callParent([
                    cmp
                ]);
            } else {
                cmp = me.lookupItemFromString(cmp);
            }
        } else if (Ext.isObject(cmp)) {
            cmp = me.lookupItemFromObject(cmp);
        }
        // Apply our minWidth to all of our non-docked child components (Menu extends Panel)
        // so it's accounted for in our VBox layout
        if (!cmp.dock) {
            cmp.minWidth = cmp.minWidth || me.minWidth;
        }
        return cmp;
    },
    /**
     * @private
     */
    lookupItemFromObject: function(cmp) {
        var type = this.defaultType;
        if (!cmp.isComponent) {
            if (!cmp.xtype && Ext.isBoolean(cmp.checked)) {
                type = 'menucheckitem';
            }
            cmp = Ext.ComponentManager.create(cmp, type);
        }
        if (cmp.isMenuItem) {
            cmp.parentMenu = this;
        }
        return cmp;
    },
    /**
     * @private
     */
    lookupItemFromString: function(cmp) {
        return (cmp === 'separator' || cmp === '-') ? new Ext.menu.Separator() : new Ext.menu.Item({
            canActivate: false,
            hideOnClick: false,
            plain: true,
            text: cmp
        });
    },
    // Override applied to the Menu's layout. Runs in the context of the layout.
    // Add special classes to allow non MenuItem components to coexist with MenuItems.
    // If there is only *one* child, then this Menu is just a vehicle for floating
    // and aligning the component, so do not do this.
    configureItem: function(cmp) {
        var me = this.owner,
            prefix = Ext.baseCSSPrefix,
            ui = me.ui,
            cls, cmpCls;
        if (cmp.isMenuItem) {
            cmp.setUI(ui);
        } else if (me.items.getCount() > 1 && !cmp.rendered && !cmp.dock) {
            cmpCls = me._itemCmpCls;
            cls = [
                cmpCls,
                cmpCls + '-' + ui
            ];
            // The "plain" setting means that the menu does not look so much like a menu.
            // It's more like a grey Panel. So it has no vertical separator.
            // Plain menus also will not indent non MenuItem components;
            // there is nothing to indent them to the right of.
            if (!me.plain && (cmp.indent !== false || cmp.iconCls === 'no-icon')) {
                cls.push(prefix + 'menu-item-indent-' + ui);
            }
            if (cmp.rendered) {
                cmp.el.addCls(cls);
            } else {
                cmp.cls = (cmp.cls || '') + ' ' + cls.join(' ');
            }
            // So we can clean the item if it gets removed.
            cmp.$extraMenuCls = cls;
        }
        // @noOptimize.callParent
        this.callParent(arguments);
    },
    onRemove: function(cmp) {
        this.callParent([
            cmp
        ]);
        // Remove any extra classes we added to non-MenuItem child items
        if (!cmp.destroyed && cmp.$extraMenuCls) {
            cmp.el.removeCls(cmp.$extraMenuCls);
        }
    },
    onClick: function(e) {
        var me = this,
            type = e.type,
            item, clickResult,
            iskeyEvent = type === 'keydown';
        if (me.disabled) {
            e.stopEvent();
            return;
        }
        item = me.getItemFromEvent(e);
        if (item && item.isMenuItem) {
            if (!item.menu || !me.ignoreParentClicks) {
                clickResult = item.onClick(e);
            } else {
                e.stopEvent();
            }
            // Click handler on the item could have destroyed the menu
            if (me.destroyed) {
                return;
            }
            // SPACE and ENTER invokes the menu
            if (item.menu && clickResult !== false && iskeyEvent) {
                item.expandMenu(e, 0);
            }
        }
        // Click event may be fired without an item, so we need a second check
        if (!item || item.disabled) {
            item = undefined;
        }
        me.fireEvent('click', me, item, e);
    },
    doDestroy: function() {
        var me = this;
        if (me.escapeKeyNav) {
            me.escapeKeyNav.destroy();
        }
        me.itemOverTask.cancel();
        me.parentMenu = me.ownerCmp = me.escapeKeyNav = null;
        if (me.rendered) {
            me.el.un(me.mouseMonitor);
            Ext.destroy(me.iconSepEl);
        }
        // Menu can be destroyed while shown;
        // we should notify the Manager
        Ext.menu.Manager.onHide(me);
        me.callParent();
    },
    onMouseLeave: function(e) {
        var me = this;
        if (me.itemOverTask) {
            me.itemOverTask.cancel();
        }
        if (me.disabled) {
            return;
        }
        me.fireEvent('mouseleave', me, e);
    },
    onMouseOver: function(e) {
        var me = this,
            fromEl = e.getRelatedTarget(),
            mouseEnter = !me.el.contains(fromEl),
            item = me.getItemFromEvent(e),
            parentMenu = me.parentMenu,
            ownerCmp = me.ownerCmp;
        if (mouseEnter && parentMenu) {
            parentMenu.setActiveItem(ownerCmp);
            ownerCmp.cancelDeferHide();
            parentMenu.mouseMonitor.mouseenter();
            parentMenu.itemOverTask.cancel();
        }
        if (me.disabled) {
            return;
        }
        // Do not activate the item if the mouseover was within the item, and it's already active
        if (item) {
            // Activate the item in time specified by mouseLeaveDelay.
            // If we mouseout, or move to another item this invocation will be canceled.
            if (e.pointerType === 'touch') {
                me.handleItemOver(e, item);
            } else {
                me.itemOverTask.delay(me.expanded ? me.mouseLeaveDelay : 0, null, null, [
                    e,
                    item
                ]);
            }
        }
        if (mouseEnter) {
            me.fireEvent('mouseenter', me, e);
        }
        me.fireEvent('mouseover', me, item, e);
    },
    setActiveItem: function(item) {
        var me = this;
        if (item && (item !== me.lastFocusedChild)) {
            me.focusChild(item, 1);
        }
    },
    // Focusing will scroll the item into view.
    onEscapeKey: function() {
        if (this.floating) {
            this.hide();
        }
    },
    onShortcutKey: function(keyCode, e) {
        var shortcutChar = String.fromCharCode(e.getCharCode()),
            items = this.query('>[text]'),
            len = items.length,
            item = this.lastFocusedChild,
            focusIndex = Ext.Array.indexOf(items, item),
            i = focusIndex;
        if (len === 0) {
            return;
        }
        // Loop through all items which have a text property
        // starting at the one after the current focus.
        for (; ; ) {
            if (++i === len) {
                i = 0;
            }
            item = items[i];
            // Looped back to start - no matches
            if (i === focusIndex) {
                return;
            }
            // Found a text match
            if (item.text && item.text[0].toUpperCase() === shortcutChar) {
                item.focus();
                return;
            }
        }
    },
    onBeforeShow: function() {
        // Do not allow show immediately after a hide
        if (Ext.Date.getElapsed(this.lastHide) < this.menuClickBuffer) {
            return false;
        }
    },
    beforeShow: function() {
        var me = this,
            parent;
        // Constrain the height to the containing element's viewable area
        if (me.floating) {
            parent = me.hasFloatMenuParent();
            if (!parent && !me.allowOtherMenus) {
                Ext.menu.Manager.hideAll();
            }
        }
        me.callParent();
    },
    afterShow: function(animateTarget, callback, scope) {
        var me = this,
            ariaDom = me.ariaEl.dom;
        me.callParent([
            animateTarget,
            callback,
            scope
        ]);
        Ext.menu.Manager.onShow(me);
        if (me.parentMenu) {
            me.parentMenu.expanded = true;
        }
        if (me.floating && ariaDom) {
            ariaDom.setAttribute('aria-expanded', true);
        }
        // Restore configured maxHeight
        if (me.floating) {
            me.maxHeight = me.savedMaxHeight;
        }
        if (me.autoFocus) {
            me.focus();
        }
    },
    onHide: function(animateTarget, cb, scope) {
        var me = this,
            ariaDom = me.ariaEl.dom;
        me.callParent([
            animateTarget,
            cb,
            scope
        ]);
        me.lastHide = Ext.Date.now();
        Ext.menu.Manager.onHide(me);
        if (me.parentMenu) {
            me.parentMenu.expanded = false;
        }
        if (me.floating && ariaDom) {
            ariaDom.setAttribute('aria-expanded', false);
        }
    },
    afterHide: function(cb, scope) {
        this.callParent([
            cb,
            scope
        ]);
        // Top level focusEnter is only valid when focused
        delete this.getInherited().topmostFocusEvent;
    },
    preventClick: function(e) {
        var item = this.getItemFromEvent(e);
        if (item && item.isMenuItem && !item.href) {
            e.preventDefault();
        }
    },
    privates: {
        /**
         * @private
         */
        applyDefaults: function(config) {
            if (!Ext.isString(config)) {
                config = this.callParent([
                    config
                ]);
            }
            return config;
        },
        initFocusableElement: function() {
            var me = this,
                tabIndex = me.tabIndex,
                el = me.el;
            // Floating menus always need to have focusable main el
            // so that mouse clicks within the menu would not close it.
            // We're not checking focusable property here, Component
            // will do that before we can reach this method.
            if (me.floating && tabIndex != null && el && el.dom) {
                el.dom.setAttribute('tabIndex', tabIndex);
                el.dom.setAttribute('data-componentid', me.id);
            }
        },
        processFocusableContainerKeyEvent: function(e) {
            // ESC may be from input fields, and FocusableContainers ignore keys from
            // input fields. We do not want to ignore ESC. ESC hide menus.
            if (e.keyCode === e.ESC) {
                e.target = this.el.dom;
            }
            // TAB from textual input fields is converted into UP or DOWN.
            else if (e.keyCode === e.TAB && Ext.fly(e.target).is('input[type=text],textarea')) {
                e.preventDefault();
                e.target = this.getItemFromEvent(e).el.dom;
                if (e.shiftKey) {
                    e.shiftKey = false;
                    e.keyCode = e.UP;
                } else {
                    e.keyCode = e.DOWN;
                }
            } else {
                return this.callParent([
                    e
                ]);
            }
            return e;
        },
        // Tabbing in a floating menu must hide, but not move focus.
        // onHide takes care of moving focus back to an owner Component.
        onFocusableContainerTabKey: function(e) {
            var me = this;
            if (me.floating) {
                if (e.shiftKey) {
                    // We do not want TAB behaviour to proceed.
                    // SHIFT+TAB reverts "backwards" to the menu's invoker
                    // which is the automatic behaviour.
                    e.preventDefault();
                } else {
                    // If we want to navigate forwards, we cannot allow the
                    // automatic focus reversion to go to the parent menu.
                    // It must behave as if it were the topmost menu in the
                    // floating stack, revert to there, and then TAB onwards.
                    me.focusEnterEvent = me.getInherited().topmostFocusEvent;
                }
                me.hide();
            }
        },
        onFocusableContainerEnterKey: function(e) {
            this.onClick(e);
        },
        onFocusableContainerSpaceKey: function(e) {
            this.onClick(e);
        },
        onFocusableContainerLeftKey: function(e) {
            // The default action is to scroll the nearest horizontally scrollable container
            e.preventDefault();
            // If we are a submenu, then left arrow focuses the owning MenuItem
            if (this.parentMenu) {
                this.ownerCmp.focus();
                this.hide();
            }
        },
        onFocusableContainerRightKey: function(e) {
            var me = this,
                focusItem = me.lastFocusedChild;
            // See above
            e.preventDefault();
            if (focusItem && focusItem.expandMenu) {
                focusItem.expandMenu(e, 0);
            }
        },
        hasFloatMenuParent: function() {
            return this.parentMenu || this.up('menu[floating=true]');
        },
        setOwnerCmp: function(comp, instanced) {
            var me = this;
            me.parentMenu = comp.isMenuItem ? comp : null;
            me.ownerCmp = comp;
            me.registerWithOwnerCt();
            delete me.hierarchicallyHidden;
            me.onInheritedAdd(comp, instanced);
            me.containerOnAdded(comp, instanced);
        }
    }
});

/**
 * Abstract base class for filter implementations.
 */
Ext.define('Ext.grid.filters.filter.Base', {
    mixins: [
        'Ext.mixin.Factoryable'
    ],
    factoryConfig: {
        type: 'grid.filter'
    },
    $configPrefixed: false,
    $configStrict: false,
    config: {
        /**
         * @cfg {Object} [itemDefaults]
         * The default configuration options for any menu items created by this filter.
         *
         * Example usage:
         *
         *      itemDefaults: {
         *          width: 150
         *      },
         */
        itemDefaults: null,
        menuDefaults: {
            xtype: 'menu'
        },
        /**
         * @cfg {Number} updateBuffer
         * Number of milliseconds to wait after user interaction to fire an update. Only supported
         * by filters: 'list', 'numeric', and 'string'.
         */
        updateBuffer: 500,
        /**
         * @cfg {Function} [serializer]
         * A function to post-process any serialization. Accepts a filter state object
         * containing `property`, `value` and `operator` properties, and may either
         * mutate it, or return a completely new representation.
         * @since 6.2.0
         */
        serializer: null
    },
    /**
     * @property {Boolean} active
     * True if this filter is active. Use setActive() to alter after configuration. If
     * you set a value, the filter will be actived automatically.
     */
    /**
     * @cfg {Boolean} active
     * Indicates the initial status of the filter (defaults to false).
     */
    active: false,
    /**
     * @property {String} type
     * The filter type. Used by the filters.Feature class when adding filters and applying state.
     */
    type: 'string',
    /**
     * @cfg {String} dataIndex
     * The {@link Ext.data.Store} dataIndex of the field this filter represents.
     * The dataIndex does not actually have to exist in the store.
     */
    dataIndex: null,
    /**
     * @property {Ext.menu.Menu} menu
     * The filter configuration menu that will be installed into the filter submenu
     * of a column menu.
     */
    menu: null,
    isGridFilter: true,
    defaultRoot: 'data',
    /**
     * The prefix for id's used to track stateful Store filters.
     * @private
     */
    filterIdPrefix: Ext.baseCSSPrefix + 'gridfilter',
    /**
     * @event filteractivate
     * Fires when an inactive filter becomes active
     * @param {Ext.grid.filters.Filters} this
     * @param {Ext.grid.column.Column} column This filter's assigned column
     * @since 6.5.0
     * @member Ext.panel.Table
     */
    /**
     * @event filterdeactivate
     * Fires when an active filter becomes inactive
     * @param {Ext.grid.filters.Filters} this
     * @param {Ext.grid.column.Column} column This filter's assigned column
     * @since 6.5.0
     * @member Ext.panel.Table
     */
    /**
     * Initializes the filter given its configuration.
     * @param {Object} config
     */
    constructor: function(config) {
        var me = this,
            column;
        // Calling Base constructor is very desirable for testing
        me.callParent([
            config
        ]);
        me.initConfig(config);
        column = me.column;
        me.columnListeners = column.on('destroy', me.destroy, me, {
            destroyable: true
        });
        me.dataIndex = me.dataIndex || column.dataIndex;
        me.task = new Ext.util.DelayedTask(me.setValue, me);
    },
    /**
     * Destroys this filter by purging any event listeners, and removing any menus.
     */
    destroy: function() {
        var me = this;
        if (me.task) {
            me.task.cancel();
            me.task = null;
        }
        me.columnListeners = me.columnListeners.destroy();
        me.grid = me.menu = Ext.destroy(me.menu);
        me.callParent();
    },
    addStoreFilter: function(filter) {
        var filters = this.getGridStore().getFilters(),
            idx = filters.indexOf(filter),
            existing = idx !== -1 ? filters.getAt(idx) : null;
        // If the filter being added doesn't exist in the collection we should add it.
        // But if there is a filter with the same id (indexOf tests for the same id), we should
        // check if the filter being added has the same properties as the existing one
        if (!existing || !Ext.util.Filter.isEqual(existing, filter)) {
            filters.add(filter);
        }
    },
    createFilter: function(config, key) {
        var filter = new Ext.util.Filter(this.getFilterConfig(config, key));
        filter.isGridFilter = true;
        return filter;
    },
    // Note that some derived classes may need to do specific processing
    // and will have its own version of this method before calling parent (see the List filter).
    getFilterConfig: function(config, key) {
        config.id = this.getBaseIdPrefix();
        if (!config.property) {
            config.property = this.dataIndex;
        }
        if (!config.root) {
            config.root = this.defaultRoot;
        }
        if (key) {
            config.id += '-' + key;
        }
        config.serializer = this.getSerializer();
        return config;
    },
    /**
     * @private
     * Creates the Menu for this filter.
     * @param {Object} config Filter configuration
     * @return {Ext.menu.Menu}
     */
    createMenu: function() {
        this.menu = Ext.widget(this.getMenuConfig());
    },
    getActiveState: function(config, value) {
        // An `active` config must take precedence over a `value` config.
        var active = config.active;
        return (active !== undefined) ? active : value !== undefined;
    },
    getBaseIdPrefix: function() {
        return this.filterIdPrefix + '-' + this.dataIndex;
    },
    getMenuConfig: function() {
        return Ext.apply({}, this.getMenuDefaults());
    },
    getGridStore: function() {
        return this.grid.getStore();
    },
    getStoreFilter: function(key) {
        var id = this.getBaseIdPrefix();
        if (key) {
            id += '-' + key;
        }
        return this.getGridStore().getFilters().get(id);
    },
    /**
     * @private
     * Handler method called when there is a significant event on an input item.
     */
    onValueChange: function(field, e) {
        var me = this,
            keyCode = e.getKey(),
            updateBuffer = me.updateBuffer,
            value;
        // Don't process tabs!
        if (keyCode === e.TAB) {
            return;
        }
        if (!field.isFormField) {
            Ext.raise('`field` should be a form field instance.');
        }
        if (field.isValid()) {
            if (keyCode === e.RETURN) {
                me.menu.hide();
                return;
            }
            value = me.getValue(field);
            if (value === me.value) {
                return;
            }
            if (updateBuffer) {
                me.task.delay(updateBuffer, null, null, [
                    value
                ]);
            } else {
                me.setValue(value);
            }
        }
    },
    /**
     * @private
     * @method preprocess
     * Template method to be implemented by all subclasses that need to perform
     * any operations before the column filter has finished construction.
     * @template
     */
    preprocess: Ext.emptyFn,
    removeStoreFilter: function(filter) {
        this.getGridStore().getFilters().remove(filter);
    },
    /**
     * @private
     * @method getValue
     * Template method to be implemented by all subclasses that is to
     * get and return the value of the filter.
     * @return {Object} The 'serialized' form of this filter
     * @template
     */
    getValue: Ext.emptyFn,
    /**
     * @private
     * @method setValue
     * Template method to be implemented by all subclasses that is to
     * set the value of the filter and fire the grid's 'filterchange' event.
     * @param {Object} data The value to set the filter
     * @template
     */
    /**
     * Sets the status of the filter and fires the appropriate events.
     * @param {Boolean} active The new filter state.
     */
    setActive: function(active) {
        var me = this,
            menuItem = me.owner.activeFilterMenuItem,
            filterCollection;
        if (me.active !== active) {
            me.active = active;
            filterCollection = me.getGridStore().getFilters();
            filterCollection.beginUpdate();
            if (active) {
                me.activate();
            } else {
                me.deactivate();
            }
            filterCollection.endUpdate();
            // Make sure we update the 'Filters' menu item.
            if (menuItem && menuItem.activeFilter === me) {
                menuItem.setChecked(active);
            }
            me.setColumnActive(active);
            me.grid.fireEventArgs(active ? 'filteractivate' : 'filterdeactivate', [
                me,
                me.column
            ]);
        }
    },
    setColumnActive: function(active) {
        this.column[active ? 'addCls' : 'removeCls'](this.owner.filterCls);
    },
    showMenu: function(menuItem) {
        var me = this;
        if (!me.menu) {
            me.createMenu();
        }
        menuItem.activeFilter = me;
        menuItem.setMenu(me.menu, false);
        menuItem.setChecked(me.active);
        // Disable the menu if filter.disabled explicitly set to true.
        menuItem.setDisabled(me.disabled === true);
        me.activate(/* showingMenu */
        true);
    },
    updateStoreFilter: function() {
        this.getGridStore().getFilters().notify('endupdate');
    }
});

/**
 * This abstract base class is used by grid filters that have a single
 * {@link Ext.data.Store#cfg-filters store filter}.
 * @protected
 */
Ext.define('Ext.grid.filters.filter.SingleFilter', {
    extend: 'Ext.grid.filters.filter.Base',
    constructor: function(config) {
        var me = this,
            filter, value;
        me.callParent([
            config
        ]);
        value = me.value;
        filter = me.getStoreFilter();
        if (filter) {
            // This filter was restored from stateful filters on the store so enforce it as active.
            me.active = true;
        } else {
            // Once we've reached this block, we know that this grid filter doesn't have a stateful
            // filter, so if our flag to begin saving future filter mutations is set we know
            // that any configured filter must be nulled out or it will replace our stateful filter.
            if (me.grid.stateful && me.getGridStore().saveStatefulFilters) {
                value = undefined;
            }
            // TODO: What do we mean by value === null ?
            me.active = me.getActiveState(config, value);
            // Now we're acting on user configs so let's not futz with any assumed settings.
            filter = me.createFilter({
                operator: me.operator,
                value: value
            });
            if (me.active) {
                me.addStoreFilter(filter);
            }
        }
        if (me.active) {
            me.setColumnActive(true);
        }
        me.filter = filter;
    },
    activate: function(showingMenu) {
        if (showingMenu) {
            this.activateMenu();
        } else {
            this.addStoreFilter(this.filter);
        }
    },
    deactivate: function() {
        this.removeStoreFilter(this.filter);
    },
    getValue: function(field) {
        return field.getValue();
    },
    onFilterRemove: function() {
        // Filters can be removed at any time, even before a column filter's menu
        // has been created (i.e., store.clearFilter()).
        if (!this.menu || this.active) {
            this.active = false;
        }
    }
});

/**
 * The boolean grid filter allows you to create a filter selection that limits results
 * to values matching true or false.  The filter can be set programmatically or via 
 * user input with a configurable {@link Ext.form.field.Radio radio field} in the filter section 
 * of the column header.
 * 
 * Boolean filters use unique radio group IDs, so you may utilize more than one.
 *
 * Example Boolean Filter Usage:
 * 
 *     @example
 *     var shows = Ext.create('Ext.data.Store', {
 *         fields: ['id','show', 'visible'],
 *         data: [
 *             {id: 0, show: 'Battlestar Galactica', visible: true},
 *             {id: 1, show: 'Doctor Who', visible: true},
 *             {id: 2, show: 'Farscape', visible: false},
 *             {id: 3, show: 'Firefly', visible: true},
 *             {id: 4, show: 'Star Trek', visible: true},
 *             {id: 5, show: 'Star Wars: Christmas Special', visible: false}
 *         ]
 *     });
 *   
 *     Ext.create('Ext.grid.Panel', {
 *         renderTo: Ext.getBody(),
 *         title: 'Sci-Fi Television',
 *         height: 250,
 *         width: 375,
 *         store: shows,
 *         plugins: {
 *             gridfilters: true
 *         },
 *         columns: [{
 *             dataIndex: 'id',
 *             text: 'ID',
 *             width: 50
 *         },{
 *             dataIndex: 'show',
 *             text: 'Show',
 *             flex: 1                  
 *         },{
 *             dataIndex: 'visible',
 *             text: 'Visibility',
 *             width: 125,
 *             filter: {
 *                 type: 'boolean',
 *                 value: true,
 *                 yesText: 'True',
 *                 noText: 'False'
 *             }
 *         }]
 *     });  
 */
Ext.define('Ext.grid.filters.filter.Boolean', {
    extend: 'Ext.grid.filters.filter.SingleFilter',
    alias: 'grid.filter.boolean',
    type: 'boolean',
    operator: '==',
    /**
     * @cfg {Boolean} defaultValue
     * Set this to null if you do not want either option to be checked by default.
     * Defaults to false.
     */
    defaultValue: false,
    /**
     * @cfg {String} [yesText]
     * The text to display for `true`.
     * @locale
     */
    yesText: 'Yes',
    /**
     * @cfg {String} [noText]
     * The text to display for `false`.
     * @locale
     */
    noText: 'No',
    updateBuffer: 0,
    constructor: function(config) {
        var me = this,
            filterValue;
        me.callParent([
            config
        ]);
        if (me.filter) {
            filterValue = me.filter.getValue();
            if (Ext.isEmpty(filterValue, true) && me.defaultValue !== null) {
                me.filter.setValue(!!me.defaultValue);
            }
        }
    },
    /**
     * @private
     * Template method that is to initialize the filter and install required menu items.
     */
    createMenu: function(config) {
        var me = this,
            gId = Ext.id(),
            listeners = {
                scope: me,
                click: me.onClick
            },
            itemDefaults = me.getItemDefaults();
        me.callParent(arguments);
        me.menu.add([
            Ext.apply({
                text: me.yesText,
                filterKey: 1,
                group: gId,
                checked: !!me.defaultValue,
                hideOnClick: false,
                listeners: listeners
            }, itemDefaults),
            Ext.apply({
                text: me.noText,
                filterKey: 0,
                group: gId,
                checked: !me.defaultValue && me.defaultValue !== null,
                hideOnClick: false,
                listeners: listeners
            }, itemDefaults)
        ]);
    },
    /**
     * @private
     */
    onClick: function(field) {
        this.setValue(!!field.filterKey);
    },
    /**
     * @private
     * Template method that is to set the value of the filter.
     * @param {Object} value The value to set the filter.
     */
    setValue: function(value) {
        var me = this;
        me.filter.setValue(value);
        if (value !== undefined && me.active) {
            me.value = value;
            me.updateStoreFilter();
        } else {
            me.setActive(true);
        }
    },
    // This is supposed to be just a stub.
    activateMenu: Ext.emptyFn
});

/**
 * This abstract base class is used by grid filters that have a three
 * {@link Ext.data.Store#cfg-filters store filter}.
 * @protected
 */
Ext.define('Ext.grid.filters.filter.TriFilter', {
    extend: 'Ext.grid.filters.filter.Base',
    /**
     * @property {String[]} menuItems
     * The items to be shown in this menu.  Items are added to the menu
     * according to their position within this array.
     * Defaults to:
     *      menuItems : ['lt', 'gt', '-', 'eq']
     * @private
     */
    menuItems: [
        'lt',
        'gt',
        '-',
        'eq'
    ],
    constructor: function(config) {
        var me = this,
            stateful = false,
            filter = {},
            filterGt, filterLt, filterEq, value, operator;
        me.callParent([
            config
        ]);
        value = me.value;
        filterLt = me.getStoreFilter('lt');
        filterGt = me.getStoreFilter('gt');
        filterEq = me.getStoreFilter('eq');
        if (filterLt || filterGt || filterEq) {
            // This filter was restored from stateful filters on the store so enforce it as active.
            stateful = me.active = true;
            if (filterLt) {
                me.onStateRestore(filterLt);
            }
            if (filterGt) {
                me.onStateRestore(filterGt);
            }
            if (filterEq) {
                me.onStateRestore(filterEq);
            }
        } else {
            // Once we've reached this block, we know that this grid filter doesn't have a stateful
            // filter, so if our flag to begin saving future filter mutations is set we know
            // that any configured filter must be nulled out or it will replace our stateful filter.
            if (me.grid.stateful && me.getGridStore().saveStatefulFilters) {
                value = undefined;
            }
            // TODO: What do we mean by value === null ?
            me.active = me.getActiveState(config, value);
        }
        // Note that stateful filters will have already been gotten above. If not, or if all filters
        // aren't stateful, we need to make sure that there is an actual filter instance created,
        // with or without a value.
        //
        // Note use the alpha alias for the operators ('gt', 'lt', 'eq') so they map
        // in Filters.onFilterRemove().
        filter.lt = filterLt || me.createFilter({
            operator: 'lt',
            value: (!stateful && value && Ext.isDefined(value.lt)) ? value.lt : null
        }, 'lt');
        filter.gt = filterGt || me.createFilter({
            operator: 'gt',
            value: (!stateful && value && Ext.isDefined(value.gt)) ? value.gt : null
        }, 'gt');
        filter.eq = filterEq || me.createFilter({
            operator: 'eq',
            value: (!stateful && value && Ext.isDefined(value.eq)) ? value.eq : null
        }, 'eq');
        me.filter = filter;
        if (me.active) {
            me.setColumnActive(true);
            if (!stateful) {
                for (operator in value) {
                    me.addStoreFilter(me.filter[operator]);
                }
            }
        }
    },
    // TODO: maybe call this.activate?
    /**
     * @private
     * This method will be called when a column's menu trigger is clicked as well as when a filter
     * is activated. Depending on the caller, the UI and the store will be synced.
     */
    activate: function(showingMenu) {
        var me = this,
            filters = me.filter,
            fields = me.fields,
            filter, field, operator, value, isRootMenuItem;
        if (me.preventFilterRemoval) {
            return;
        }
        for (operator in filters) {
            filter = filters[operator];
            field = fields[operator];
            value = filter.getValue();
            if (value || value === 0) {
                if (field.isComponent) {
                    field.setValue(value);
                    // Some types, such as Date, have additional menu check items in their Filter
                    // menu hierarchy. Others, such as Number, do not. Because of this,
                    // it is necessary to make sure that the direct menuitem ancestor of the fields
                    // is not the rootMenuItem (the "Filters" menu item), which has its
                    // checked state controlled elsewhere.
                    //
                    // In other words, if the ancestor is not the rootMenuItem, check it.
                    if (isRootMenuItem === undefined) {
                        isRootMenuItem = me.owner.activeFilterMenuItem === field.up('menuitem');
                    }
                    if (!isRootMenuItem) {
                        field.up('menuitem').setChecked(true, /* suppressEvents */
                        true);
                    }
                } else {
                    field.value = value;
                }
                // Note that we only want to add store filters when they've been removed,
                // which means that when Filter.showMenu() is called
                // we DO NOT want to add a filter as they've already been added!
                if (!showingMenu) {
                    me.addStoreFilter(filter);
                }
            }
        }
    },
    /**
     * @private
     * This method will be called when a filter is deactivated. The UI and the store will be synced.
     */
    deactivate: function() {
        var me = this,
            filters = me.filter,
            f, filter, value;
        if (!me.countActiveFilters() || me.preventFilterRemoval) {
            return;
        }
        me.preventFilterRemoval = true;
        for (f in filters) {
            filter = filters[f];
            value = filter.getValue();
            if (value || value === 0) {
                me.removeStoreFilter(filter);
            }
        }
        me.preventFilterRemoval = false;
    },
    countActiveFilters: function() {
        var filters = this.filter,
            filterCollection = this.getGridStore().getFilters(),
            prefix = this.getBaseIdPrefix(),
            i = 0,
            filter;
        if (filterCollection.length) {
            for (filter in filters) {
                if (filterCollection.get(prefix + '-' + filter)) {
                    i++;
                }
            }
        }
        return i;
    },
    onFilterRemove: function(operator) {
        var me = this,
            value;
        // Filters can be removed at any time, even before a column filter's menu has been created
        // (i.e., store.clearFilter()). So, only call setValue() if the menu has been created
        // since that method assumes that menu fields exist.
        if (!me.menu && me.countActiveFilters()) {
            me.active = false;
        } else if (me.menu) {
            value = {};
            value[operator] = null;
            me.setValue(value);
        }
    },
    onStateRestore: Ext.emptyFn,
    setValue: function(value) {
        var me = this,
            filters = me.filter,
            add = [],
            remove = [],
            active = false,
            filterCollection = me.getGridStore().getFilters(),
            filter, v, i, rLen, aLen;
        if (me.preventFilterRemoval) {
            return;
        }
        me.preventFilterRemoval = true;
        if ('eq' in value) {
            v = filters.lt.getValue();
            if (v || v === 0) {
                remove.push(filters.lt);
            }
            v = filters.gt.getValue();
            if (v || v === 0) {
                remove.push(filters.gt);
            }
            v = value.eq;
            if (v || v === 0) {
                add.push(filters.eq);
                filters.eq.setValue(v);
            } else {
                remove.push(filters.eq);
            }
        } else {
            v = filters.eq.getValue();
            if (v || v === 0) {
                remove.push(filters.eq);
            }
            if ('lt' in value) {
                v = value.lt;
                if (v || v === 0) {
                    add.push(filters.lt);
                    filters.lt.setValue(v);
                } else {
                    remove.push(filters.lt);
                }
            }
            if ('gt' in value) {
                v = value.gt;
                if (v || v === 0) {
                    add.push(filters.gt);
                    filters.gt.setValue(v);
                } else {
                    remove.push(filters.gt);
                }
            }
        }
        // Note that we don't want to update the filter collection unnecessarily, so we must know
        // the current number of active filters that this TriFilter has +/- the number of filters
        // we're adding and removing, respectively. This will determine the present active state
        // of the TriFilter which we can use to not only help determine if the condition below
        // should pass but (if it does) how the active state should then be updated.
        rLen = remove.length;
        aLen = add.length;
        active = !!(me.countActiveFilters() + aLen - rLen);
        if (rLen || aLen || active !== me.active) {
            // Begin the update now because the update could also be triggered if #setActive
            // is called. We must wrap all the calls that could change the filter collection.
            filterCollection.beginUpdate();
            if (rLen) {
                for (i = 0; i < rLen; i++) {
                    filter = remove[i];
                    me.fields[filter.getOperator()].setValue(null);
                    filter.setValue(null);
                    me.removeStoreFilter(filter);
                }
            }
            if (aLen) {
                for (i = 0; i < aLen; i++) {
                    me.addStoreFilter(add[i]);
                }
            }
            me.setActive(active);
            filterCollection.endUpdate();
        }
        me.preventFilterRemoval = false;
    }
});

/**
 * The date grid filter allows you to create a filter selection that limits results
 * to values matching specific date constraints.  The filter can be set programmatically or via 
 * user input with a configurable {@link Ext.picker.Date DatePicker menu} in the filter section 
 * of the column header.
 * 
 * Example Date Filter Usage:
 * 
 *     @example 
 *     var shows = Ext.create('Ext.data.Store', {
 *         fields: ['id','show', {
 *               name: 'airDate',
 *               type: 'date',
 *               dateFormat: 'Y-m-d'
 *         }],
 *         data: [
 *             {id: 0, show: 'Battlestar Galactica', airDate: '1978-09-17'},
 *             {id: 1, show: 'Doctor Who', airDate: '1963-11-23'},
 *             {id: 2, show: 'Farscape', airDate: '1999-03-19'},
 *             {id: 3, show: 'Firefly', airDate: '2002-12-20'},
 *             {id: 4, show: 'Star Trek', airDate: '1966-09-08'},
 *             {id: 5, show: 'Star Wars: Christmas Special', airDate: '1978-11-17'}
 *         ]
 *     });
 *   
 *     Ext.create('Ext.grid.Panel', {
 *         renderTo: Ext.getBody(),
 *         title: 'Sci-Fi Television',
 *         height: 250,
 *         width: 375,
 *         store: shows,
 *         plugins: {
 *             gridfilters: true
 *         },
 *         columns: [{
 *             dataIndex: 'id',
 *             text: 'ID',
 *             width: 50
 *         },{
 *             dataIndex: 'show',
 *             text: 'Show',
 *             flex: 1                  
 *         },{
 *             xtype: 'datecolumn',
 *             dataIndex: 'airDate',
 *             text: 'Original Air Date',
 *             width: 125,
 *             filter: {
 *                 type: 'date',
 *                 
 *                 // optional picker config
 *                 pickerDefaults: {
 *                     // any DatePicker configs
 *                 } 
 *             }
 *         }]
 *     }); 
 */
Ext.define('Ext.grid.filters.filter.Date', {
    extend: 'Ext.grid.filters.filter.TriFilter',
    alias: 'grid.filter.date',
    uses: [
        'Ext.picker.Date',
        'Ext.menu.DatePicker'
    ],
    type: 'date',
    config: {
        /**
         * @cfg {Object} [fields]
         * Configures field items individually. These properties override those defined
         * by `{@link #itemDefaults}`.
         *
         * Example usage:
         *      fields: {
         *          gt: { // override fieldCfg options
         *              width: 200
         *          }
         *      },
         * @locale
         */
        fields: {
            lt: {
                text: 'Before'
            },
            gt: {
                text: 'After'
            },
            eq: {
                text: 'On'
            }
        },
        /**
         * @cfg {Object} pickerDefaults
         * Configuration options for the date picker associated with each field.
         */
        pickerDefaults: {
            xtype: 'datepicker',
            border: 0
        },
        updateBuffer: 0,
        /**
        * @cfg {String} dateFormat
        * The date format to return when using getValue.
        * Defaults to {@link Ext.Date#defaultFormat}.
        */
        dateFormat: undefined
    },
    itemDefaults: {
        xtype: 'menucheckitem',
        selectOnFocus: true,
        width: 125,
        menu: {
            layout: 'auto',
            plain: true
        }
    },
    /**
     * @cfg {Date} maxDate
     * Allowable date as passed to the Ext.DatePicker
     * Defaults to undefined.
     */
    /**
     * @cfg {Date} minDate
     * Allowable date as passed to the Ext.DatePicker
     * Defaults to undefined.
     */
    applyDateFormat: function(dateFormat) {
        return dateFormat || Ext.Date.defaultFormat;
    },
    /**
     * @private
     * Template method that is to initialize the filter and install required menu items.
     */
    createMenu: function(config) {
        var me = this,
            listeners = {
                scope: me,
                checkchange: me.onCheckChange
            },
            menuItems = me.menuItems,
            fields, itemDefaults, pickerCfg, i, len, key, item, cfg, field;
        me.callParent(arguments);
        itemDefaults = me.getItemDefaults();
        fields = me.getFields();
        pickerCfg = Ext.apply({
            minDate: me.minDate,
            maxDate: me.maxDate,
            format: me.dateFormat,
            listeners: {
                scope: me,
                select: me.onMenuSelect
            }
        }, me.getPickerDefaults());
        me.fields = {};
        for (i = 0 , len = menuItems.length; i < len; i++) {
            key = menuItems[i];
            if (key !== '-') {
                cfg = {
                    menu: {
                        xtype: 'datemenu',
                        hideOnClick: false,
                        pickerCfg: Ext.apply({
                            itemId: key
                        }, pickerCfg)
                    }
                };
                if (itemDefaults) {
                    Ext.merge(cfg, itemDefaults);
                }
                if (fields) {
                    Ext.merge(cfg, fields[key]);
                }
                item = me.menu.add(cfg);
                // Date filter types need the field to be the datepicker in TriFilter.setValue().
                field = me.fields[key] = item.down('datepicker');
                field.filter = me.filter[key];
                field.filterKey = key;
                item.on(listeners);
            } else {
                me.menu.add(key);
            }
        }
    },
    /**
     * Gets the menu picker associated with the passed field
     * @param {String} item The field identifier ('lt', 'gt', 'eq')
     * @return {Object} The menu picker
     */
    getPicker: function(item) {
        return this.fields[item];
    },
    /**
     * @private
     * Remove the filter from the store but don't update its value or the field UI.
    */
    onCheckChange: function(field, checked) {
        // Only do something if unchecked.  If checked, it doesn't mean anything at this point
        // since the column's store filter won't have any value (i.e., if a user checked this
        // from an unchecked state, the corresponding field won't have a value for its filter).
        var filter = field.down('datepicker').filter,
            v;
        // Only proceed if unchecked AND there's a filter value (i.e., there's something to do!).
        if (!checked && filter.getValue()) {
            // Normally we just want to remove the filter from the store, not also to null out
            // the filter value. But, we want to call setValue() which will take care of unchecking
            // the top-level menu item if it's been determined that Date* doesn't have any filters.
            v = {};
            v[filter.getOperator()] = null;
            this.setValue(v);
        }
    },
    onFilterRemove: function(operator) {
        var v = {};
        v[operator] = null;
        this.setValue(v);
        this.fields[operator].up('menuitem').setChecked(false, /* suppressEvents */
        true);
    },
    onStateRestore: function(filter) {
        filter.setSerializer(this.getSerializer());
        filter.setConvert(this.convertDateOnly);
    },
    getFilterConfig: function(config, key) {
        config = this.callParent([
            config,
            key
        ]);
        config.serializer = this.getSerializer();
        config.convert = this.convertDateOnly;
        return config;
    },
    convertDateOnly: function(v) {
        var result = null;
        if (v) {
            result = Ext.Date.clearTime(v, true).getTime();
        }
        return result;
    },
    getSerializer: function() {
        var me = this;
        return function(data) {
            var value = data.value;
            if (value) {
                data.value = Ext.Date.format(value, me.getDateFormat());
            }
        };
    },
    /**
     * Handler for when the DatePicker for a field fires the 'select' event
     * @param {Ext.picker.Date} picker
     * @param {Object} date
     */
    onMenuSelect: function(picker, date) {
        var me = this,
            fields = me.fields,
            filters = me.filter,
            field = fields[picker.itemId],
            gt = fields.gt,
            lt = fields.lt,
            eq = fields.eq,
            v = {};
        field.up('menuitem').setChecked(true, /* suppressEvents */
        true);
        if (field === eq) {
            lt.up('menuitem').setChecked(false, true);
            gt.up('menuitem').setChecked(false, true);
        } else {
            eq.up('menuitem').setChecked(false, true);
            if (field === gt && (+lt.value < +date)) {
                lt.up('menuitem').setChecked(false, true);
                // Null so filter will be removed from store, but only if it currently has a value.
                // The Trifilter uses the number of removed filters as one of the determinants
                // to determine whether the gridfilter should be active, so don't push a value in
                // unless it's changed.
                if (filters.lt.getValue() != null) {
                    v.lt = null;
                }
            } else if (field === lt && (+gt.value > +date)) {
                gt.up('menuitem').setChecked(false, true);
                // Null so filter will be removed from store, but only if it currently has a value.
                // The Trifilter uses the number of removed filters as one of the determinants
                // to determine whether the gridfilter should be active, so don't push a value in
                // unless it's changed.
                if (filters.gt.getValue() != null) {
                    v.gt = null;
                }
            }
        }
        v[field.filterKey] = date;
        me.setValue(v);
        picker.up('menu').hide();
    }
});

/**
 * The list grid filter allows you to create a filter selection that limits results
 * to values matching an element in a list.  The filter can be set programmatically or via 
 * user input with a configurable {@link Ext.form.field.Checkbox check box field} in the filter
 * section  of the column header.
 * 
 * List filters are able to be preloaded/backed by an Ext.data.Store to load
 * their options the first time they are shown.  They are also able to create their own 
 * list of values from  all unique values of the specified {@link #dataIndex} field in 
 * the store at first time of filter invocation.
 *
 * Example List Filter Usage:
 *
 *     @example
 *     var shows = Ext.create('Ext.data.Store', {
 *         fields: ['id','show','rating'],
 *         data: [
 *             {id: 0, show: 'Battlestar Galactica', rating: 2},
 *             {id: 1, show: 'Doctor Who', rating: 4},
 *             {id: 2, show: 'Farscape', rating: 3},
 *             {id: 3, show: 'Firefly', rating: 4},
 *             {id: 4, show: 'Star Trek', rating: 1},
 *             {id: 5, show: 'Star Wars: Christmas Special', rating: 5}
 *         ]
 *     });
 *   
 *     Ext.create('Ext.grid.Panel', {
 *         renderTo: Ext.getBody(),
 *         title: 'Sci-Fi Television',
 *         height: 250,
 *         width: 350,
 *         store: shows,
 *         plugins: {
 *             gridfilters: true
 *         },
 *         columns: [{
 *             dataIndex: 'id',
 *             text: 'ID',
 *             width: 50
 *         },{
 *             dataIndex: 'show',
 *             text: 'Show',
 *             flex: 1                  
 *         },{
 *             dataIndex: 'rating',
 *             text: 'Rating',
 *             width: 75,
 *             filter: {
 *                 type: 'list',
 *                 value: 5                      
 *             }
 *         }]
 *     });
 *
 * ## Options
 * 
 * There are three means to determine the list of options to present to the user:
 * 
 *   * The `{@link #cfg-options options}` config.
 *   * The `{@link #cfg-store store}` config. In this mode, the `{@link #cfg-idField}`
 *     and `{@link #cfg-labelField}` configs are used to extract the presentation and
 *     filtering values from the `store` and apply to the menu items and grid store
 *     filter, respectively.
 *   * If none of the above is specified, the associated grid's store is used. In this
 *     case, the `{@link #cfg-dataIndex}` is used to determine the filter values and
 *     the `{@link #cfg-labelIndex}` is used to populate the menu items. These fields
 *     are extracted from the records in the associated grid's store. Both of these
 *     configs default to the column's `dataIndex` property.
 * 
 * In all of these modes, a store is created that is synchronized with the menu items.
 * The records in this store have `{@link #cfg-idField}` and `{@link #cfg-labelField}`
 * fields that get populated from which ever source was provided.
 * 
 *     var filters = Ext.create('Ext.grid.Panel', {
 *         ...
 *         columns: [{
 *             text: 'Size',
 *             dataIndex: 'size',
 *
 *             filter: {
 *                 type: 'list',
 *                 // options will be used as data to implicitly creates an ArrayStore
 *                 options: ['extra small', 'small', 'medium', 'large', 'extra large']
 *             }
 *         }],
 *         ...
 *     });
 */
Ext.define('Ext.grid.filters.filter.List', {
    extend: 'Ext.grid.filters.filter.SingleFilter',
    alias: 'grid.filter.list',
    type: 'list',
    operator: 'in',
    /**
     * @cfg {Object} [itemDefaults]
     * See the {@link Ext.grid.filters.filter.Base#cfg-itemDefaults documentation} for
     * the base class for details.
     * 
     * In the case of this class, however, note that the `checked` config should **not** be
     * specified.
     */
    itemDefaults: {
        checked: false,
        hideOnClick: false
    },
    /**
     * @cfg {Array} [options]
     * The data to be used to implicitly create a data store to back this list. This is used only
     * when the data source is **local**. If the data for the list is remote, use the {@link #store}
     * config instead.
     *
     * If neither store nor {@link #options} is specified, then the choices list is automatically
     * populated from all unique values of the specified {@link #dataIndex} field in the store
     * at first time of filter invocation.
     *
     * Each item within the provided array may be in one of the following formats:
     *
     *   - **Array** :
     *
     *         options: [
     *             [11, 'extra small'],
     *             [18, 'small'],
     *             [22, 'medium'],
     *             [35, 'large'],
     *             [44, 'extra large']
     *         ]
     *
     *   - **Object** :
     *
     *         labelField: 'name', // override default of 'text'
     *         options: [
     *             {id: 11, name:'extra small'},
     *             {id: 18, name:'small'},
     *             {id: 22, name:'medium'},
     *             {id: 35, name:'large'},
     *             {id: 44, name:'extra large'}
     *         ]
     *
     *   - **String** :
     *
     *         options: ['extra small', 'small', 'medium', 'large', 'extra large']
     *
     */
    /**
     * @cfg {String} [idField="id"]
     * The field name for the `id` of records in this list's `{@link #cfg-store}`. These values are
     * used to populate the filter for the grid's store.
     */
    idField: 'id',
    /**
     * @cfg {String} [labelField="text"]
     * The field name for the menu item text in the records in this list's `{@link #cfg-store}`.
     */
    labelField: 'text',
    /**
     * @cfg {String} [labelIndex]
     * The field in the records of the grid's store from which the menu item text should be
     * retrieved. This field is only used when no `{@link #cfg-options}` and no `{@link #cfg-store}`
     * is provided and the distinct value of the grid's store need to be generated dynamically.
     * 
     * If not provided, this field defaults to the column's `dataIndex` property.
     * @since 5.1.0
     */
    labelIndex: null,
    /**
     * @cfg {String} [loadingText]
     * The text that is displayed while the configured store is loading.
     * @locale
     */
    loadingText: 'Loading...',
    /**
     * @cfg {Boolean} loadOnShow
     * Defaults to true.
     */
    loadOnShow: true,
    /**
     * @cfg {Boolean} single
     * Specify true to group all items in this list into a single-select
     * radio button group. Defaults to false.
     */
    single: false,
    plain: true,
    /**
     * @cfg {Ext.data.Store} [store]
     * The {@link Ext.data.Store} this list should use as its data source.
     *
     * If neither store nor {@link #options} is specified, then the choices list is automatically
     * populated from all unique values of the specified {@link #dataIndex} field in the store
     * at first time of filter invocation.
     */
    /**
     * @private
     */
    gridStoreListenersCfg: {
        add: 'onDataChanged',
        refresh: 'onDataChanged',
        remove: 'onDataChanged',
        update: 'onDataChanged'
    },
    constructor: function(config) {
        var me = this,
            gridStore;
        me.callParent([
            config
        ]);
        if (me.itemDefaults.checked) {
            Ext.raise('The itemDefaults.checked config is not supported, ' + 'use the value config instead.');
        }
        me.labelIndex = me.labelIndex || me.column.dataIndex;
        if (me.store) {
            me.store = Ext.StoreManager.lookup(me.store);
        }
        // In order to fully support the `active` config, we need to do some preprocessing in case
        // we need to fetch store data in order to create the options menu items.
        //
        // For instance, imagine if a list filter has the following definition:
        //
        //    filter: {
        //        type: 'list',
        //        value: 'Bruce Springsteen'
        //    }
        //
        // Since there is no `options` or `store` config, it will need to infer its options store
        // data from the grid store. Since it is also active by default if not explicitly
        // configured as `value: false`, it must register listeners with the grid store now
        // so its own column filter store will be created and filtered immediately and properly sync
        // its options when the grid store changes.
        //
        // So here we need to subscribe to very specific events. We can't subscribe to a catch-all
        // like 'datachanged' because the listener will get called too many times. This will respond
        // to the following scenarios:
        //  1. Removing a filter
        //  2. Adding a filter
        //  3. (Re)loading the store
        //  4. Updating a model
        //
        if (!me.store && !me.options) {
            gridStore = me.getGridStore();
            if (me.value != null && me.active) {
                me.gridStoreListeners = gridStore.on(Ext.apply({
                    scope: me,
                    destroyable: true
                }, me.gridStoreListenersCfg));
            }
            me.gridListeners = me.grid.on({
                reconfigure: me.onReconfigure,
                scope: me,
                destroyable: true
            });
            me.inferOptionsFromGridStore = true;
        }
    },
    destroy: function() {
        var me = this,
            store = me.store,
            autoStore = me.autoStore;
        // We may bind listeners to both the options store & grid store, so we
        // need to unbind both sets here
        if (store && store.isStore) {
            if (autoStore || store.autoDestroy) {
                store.destroy();
            } else {
                store.un('load', me.bindMenuStore, me);
            }
            me.store = null;
        }
        Ext.destroy(me.gridStoreListeners, me.gridListeners);
        me.callParent();
    },
    activateMenu: function() {
        var me = this,
            value = me.filter.getValue(),
            items, i, len, checkItem;
        if (!value || !value.length) {
            return;
        }
        items = me.menu.items;
        for (i = 0 , len = items.length; i < len; i++) {
            checkItem = items.getAt(i);
            if (Ext.Array.indexOf(value, checkItem.value) > -1) {
                checkItem.setChecked(true, /* suppressEvents */
                true);
            }
        }
    },
    bindMenuStore: function(options) {
        var me = this;
        if (me.grid.destroyed || me.preventFilterRemoval) {
            return;
        }
        me.createListStore(options);
        me.createMenuItems(me.store);
        me.loaded = true;
    },
    /**
     * Returns a store for the filter.
     * An instantiated store may be passed.
     * 
     * If that store is the grid's store, then all unique values of this filter's
     * {@link #dataIndex} field are extracted for use in the filter.
     * 
     * Otherwise the passed store is used.
     *
     * If the passed parameter is not a store, it is taken to be a list of possible
     * values for the filter.
     * 
     * @private
     */
    createListStore: function(options) {
        var me = this,
            store = me.store,
            isStore = options.isStore,
            idField = me.idField,
            labelField = me.labelField,
            optionsStore = false,
            storeData, o, i, len, value;
        if (isStore) {
            if (options !== me.getGridStore()) {
                optionsStore = true;
                store = me.store = options;
            } else {
                me.autoStore = true;
                storeData = me.getOptionsFromStore(options);
            }
        } else {
            storeData = [];
            for (i = 0 , len = options.length; i < len; i++) {
                value = options[i];
                switch (Ext.typeOf(value)) {
                    case 'array':
                        storeData.push(value);
                        break;
                    case 'object':
                        storeData.push(value);
                        break;
                    default:
                        if (value != null) {
                            o = {};
                            o[idField] = value;
                            o[labelField] = value;
                            storeData.push(o);
                        };
                }
            }
        }
        if (!optionsStore) {
            if (store) {
                store.destroy();
            }
            store = me.store = new Ext.data.Store({
                fields: [
                    idField,
                    labelField
                ],
                data: storeData
            });
            // Note that the grid store listeners may have been bound in the constructor
            // if it was determined that the grid filter was active and defined with a value.
            if (me.inferOptionsFromGridStore & !me.gridStoreListeners) {
                me.gridStoreListeners = me.getGridStore().on(Ext.apply({
                    scope: me,
                    destroyable: true
                }, me.gridStoreListenersCfg));
            }
            me.loaded = true;
        }
        me.setStoreFilter(store);
    },
    /**
     * @private
     * Creates the Menu for this filter.
     * @param {Object} config Filter configuration
     * @return {Ext.menu.Menu}
     */
    createMenu: function(config) {
        var me = this,
            gridStore = me.getGridStore(),
            store = me.store,
            options = me.options,
            menu;
        if (store) {
            me.store = store = Ext.StoreManager.lookup(store);
        }
        me.callParent([
            config
        ]);
        menu = me.menu;
        if (store) {
            if (!store.getCount()) {
                menu.add({
                    text: me.loadingText,
                    iconCls: Ext.baseCSSPrefix + 'mask-msg-text'
                });
                // Add a listener that will auto-load the menu store if `loadOnShow` is true
                // (the default). Don't bother with mon here, the menu is destroyed when we are.
                menu.on({
                    show: me.show,
                    scope: me
                });
                store.on('load', me.bindMenuStore, me, {
                    single: true
                });
            } else {
                me.createMenuItems(store);
            }
        }
        // If there are supplied options, then we know the store is local.
        else if (options) {
            me.bindMenuStore(options);
        }
        // A ListMenu which is completely unconfigured acquires its store from the unique values
        // of its field in the store. Note that the gridstore may have already been filtered on load
        // if the column filter had been configured as active with no items checked by default.
        else if (gridStore.getCount() || gridStore.isFiltered()) {
            me.bindMenuStore(gridStore);
        } else // If there are no records in the grid store, then we know it's async and we need to listen
        // for its 'load' event.
        {
            gridStore.on('load', me.bindMenuStore, me, {
                single: true
            });
        }
    },
    /**
     * @private
     */
    createMenuItems: function(store) {
        var me = this,
            menu = me.menu,
            len = store.getCount(),
            contains = Ext.Array.contains,
            itemDefaults, record, gid, idValue, idField, labelValue, labelField, i, processed;
        // B/c we're listening to datachanged event, we need to make sure there's a menu.
        if (len && menu) {
            itemDefaults = me.getItemDefaults();
            menu.suspendLayouts();
            menu.removeAll(true);
            gid = me.single ? Ext.id() : null;
            idField = me.idField;
            labelField = me.labelField;
            processed = [];
            for (i = 0; i < len; i++) {
                record = store.getAt(i);
                idValue = record.get(idField);
                labelValue = record.get(labelField);
                // Only allow unique values.
                if (labelValue == null || contains(processed, idValue)) {
                    
                    continue;
                }
                processed.push(labelValue);
                // Note that the menu items will be set checked in filter#activate()
                // if the value of the menu item is in the cfg.value array.
                menu.add(Ext.apply({
                    text: labelValue,
                    group: gid,
                    value: idValue,
                    checkHandler: me.onCheckChange,
                    scope: me
                }, itemDefaults));
            }
            menu.resumeLayouts(true);
        }
    },
    getFilterConfig: function(config, key) {
        // List filter needs to have its value set immediately or else could will fail
        // when filtering since its _value would be undefined.
        var value = config.value;
        if (Ext.isEmpty(value)) {
            value = [];
        } else if (!Ext.isArray(value)) {
            value = [
                value
            ];
        }
        config.value = value;
        return this.callParent([
            config,
            key
        ]);
    },
    getOptionsFromStore: function(store) {
        var me = this,
            data = store.getData(),
            // eslint-disable-line no-unused-vars
            map = {},
            ret = [],
            dataIndex = me.dataIndex,
            labelIndex = me.labelIndex,
            recData, idValue, labelValue;
        if (store.isFiltered() && !store.remoteFilter) {
            data = data.getSource();
        }
        // Use store type agnostic each method.
        // TreeStore and Store implement this differently.
        // In a TreeStore, the items array only contains nodes
        // below *expanded* ancestors. Nodes below a collapsed ancestor
        // are removed from the collection. TreeStores walk the tree
        // to implement each.
        store.each(function(record) {
            recData = record.data;
            idValue = recData[dataIndex];
            labelValue = recData[labelIndex];
            if (labelValue === undefined) {
                labelValue = idValue;
            }
            // TODO: allow null?
            // if ((allowNull || !Ext.isEmpty(value)) && !map[strValue1]) {
            if (!map[idValue]) {
                map[idValue] = 1;
                ret.push([
                    idValue,
                    labelValue
                ]);
            }
        }, null, {
            filtered: true,
            // Include filtered out nodes.
            collapsed: true
        });
        // Include nodes below collapsed ancestors.
        return ret;
    },
    onCheckChange: function() {
        // Note that we don't care about the checked state here because #setValue
        // will sort this out. #setValue will get the values of the currently-checked items
        // and set its filter value from that.
        var me = this,
            updateBuffer = me.updateBuffer;
        if (updateBuffer) {
            me.task.delay(updateBuffer);
        } else {
            me.setValue();
        }
    },
    onDataChanged: function(store) {
        // If the menu item options (and the options store) are being auto-generated
        // from the grid store, then it needs to know when the grid store has changed its data
        // so it can remain in sync.
        if (this.preventDefault) {
            this.preventDefault = false;
        } else {
            this.bindMenuStore(store);
        }
    },
    onReconfigure: function(grid, store) {
        // We need to listen for reconfigure not only for when the list filter has inferred
        // its options from the grid store but also when the grid has a VM and is late-binding
        // the store.
        if (store) {
            this.bindMenuStore(store);
        }
    },
    setActive: function(active) {
        if (this.active !== active) {
            // The store filter will be updated, but we don't want to recreate the list store
            // or the menu items in the onDataChanged listener so we need to set this flag.
            // It will be reset in the onDatachanged listener when the store has filtered/cleared
            // filters.
            this.preventDefault = true;
            this.callParent([
                active
            ]);
        }
    },
    setStoreFilter: function(options) {
        var me = this,
            value = me.value,
            filter = me.filter;
        // If there are user-provided values we trust that they are valid
        // (an empty array IS valid!).
        if (value) {
            if (!Ext.isArray(value)) {
                value = [
                    value
                ];
            }
            filter.setValue(value);
        }
        if (me.active) {
            me.preventFilterRemoval = true;
            me.addStoreFilter(filter);
            me.preventFilterRemoval = false;
        }
    },
    /**
     * @private
     * Template method that is to set the value of the filter.
     */
    setValue: function() {
        var me = this,
            items = me.menu.items,
            value = [],
            i, len, checkItem;
        // The store filter will be updated, but we don't want to recreate the list store
        // or the menu items in the onDataChanged listener so we need to set this flag.
        // It will be reset in the onDatachanged listener when the store has filtered.
        me.preventDefault = true;
        for (i = 0 , len = items.length; i < len; i++) {
            checkItem = items.getAt(i);
            if (checkItem.checked) {
                value.push(checkItem.value);
            }
        }
        // Only update the store if the value has changed
        if (!Ext.Array.equals(value, me.filter.getValue())) {
            me.filter.setValue(value);
            len = value.length;
            if (len && me.active) {
                me.updateStoreFilter();
            } else {
                me.setActive(!!len);
            }
        }
    },
    show: function() {
        var store = this.store;
        if (this.loadOnShow && !this.loaded && !store.hasPendingLoad()) {
            store.load();
        }
    }
});

/**
 * Filter type for {@link Ext.grid.column.Number number columns}.
 *
 *     @example
 *     var shows = Ext.create('Ext.data.Store', {
 *           fields: ['id','show'],
 *           data: [
 *               {id: 0, show: 'Battlestar Galactica'},
 *               {id: 1, show: 'Doctor Who'},
 *               {id: 2, show: 'Farscape'},
 *               {id: 3, show: 'Firefly'},
 *               {id: 4, show: 'Star Trek'},
 *               {id: 5, show: 'Star Wars: Christmas Special'}
 *           ]
 *        });
 *       
 *       Ext.create('Ext.grid.Panel', {
 *           renderTo: Ext.getBody(),
 *           title: 'Sci-Fi Television',
 *           height: 250,
 *           width: 250,
 *           store: shows,
 *           plugins: {
 *               gridfilters: true
 *           },
 *           columns: [{
 *               dataIndex: 'id',
 *               text: 'ID',
 *               width: 50,
 *               filter: 'number' // May also be 'numeric'
 *           },{
 *               dataIndex: 'show',
 *               text: 'Show',
 *               flex: 1                  
 *           }]
 *       });
 * 
 */
Ext.define('Ext.grid.filters.filter.Number', {
    extend: 'Ext.grid.filters.filter.TriFilter',
    alias: [
        'grid.filter.number',
        'grid.filter.numeric'
    ],
    uses: [
        'Ext.form.field.Number'
    ],
    type: 'number',
    config: {
        /**
         * @cfg {Object} [fields]
         * Configures field items individually. These properties override those defined
         * by `{@link #itemDefaults}`.
         *
         * Example usage:
         *
         *      fields: {
         *          // Override itemDefaults for one field:
         *          gt: {
         *              width: 200
         *          }
         *
         *          // "lt" and "eq" fields retain all itemDefaults
         *      },
         */
        fields: {
            gt: {
                iconCls: Ext.baseCSSPrefix + 'grid-filters-gt',
                margin: '0 0 3px 0'
            },
            lt: {
                iconCls: Ext.baseCSSPrefix + 'grid-filters-lt',
                margin: '0 0 3px 0'
            },
            eq: {
                iconCls: Ext.baseCSSPrefix + 'grid-filters-eq',
                margin: 0
            }
        }
    },
    /**
     * @cfg {String} emptyText
     * The empty text to show for each field.
     * @locale
     */
    emptyText: 'Enter Number...',
    itemDefaults: {
        xtype: 'numberfield',
        enableKeyEvents: true,
        hideEmptyLabel: false,
        labelSeparator: '',
        labelWidth: 29,
        selectOnFocus: false
    },
    menuDefaults: {
        // A menu with only form fields needs some body padding. Normally this padding
        // is managed by the items, but we have no normal menu items.
        bodyPadding: 3,
        showSeparator: false
    },
    createMenu: function() {
        var me = this,
            listeners = {
                scope: me,
                keyup: me.onValueChange,
                spin: {
                    fn: me.onInputSpin,
                    buffer: 200
                },
                el: {
                    click: me.stopFn
                }
            },
            itemDefaults = me.getItemDefaults(),
            menuItems = me.menuItems,
            fields = me.getFields(),
            field, i, len, key, item, cfg;
        me.callParent();
        me.fields = {};
        for (i = 0 , len = menuItems.length; i < len; i++) {
            key = menuItems[i];
            if (key !== '-') {
                field = fields[key];
                cfg = {
                    labelClsExtra: Ext.baseCSSPrefix + 'grid-filters-icon ' + field.iconCls
                };
                if (itemDefaults) {
                    Ext.merge(cfg, itemDefaults);
                }
                Ext.merge(cfg, field);
                cfg.emptyText = cfg.emptyText || me.emptyText;
                delete cfg.iconCls;
                me.fields[key] = item = me.menu.add(cfg);
                item.filter = me.filter[key];
                item.filterKey = key;
                item.on(listeners);
            } else {
                me.menu.add(key);
            }
        }
    },
    getValue: function(field) {
        var value = {};
        value[field.filterKey] = field.getValue();
        return value;
    },
    /**
     * @private
     * Handler method called when there is a spin event on a NumberField
     * item of this menu.
     */
    onInputSpin: function(field, direction) {
        var value = {};
        value[field.filterKey] = field.getValue();
        this.setValue(value);
    },
    stopFn: function(e) {
        e.stopPropagation();
    }
});

/**
 * The string grid filter allows you to create a filter selection that limits results
 * to values matching a particular string.  The filter can be set programmatically or via 
 * user input with a configurable {@link Ext.form.field.Text text field} in the filter section 
 * of the column header.
 *
 * Example String Filter Usage:
 *
 *     @example
 *     var shows = Ext.create('Ext.data.Store', {
 *         fields: ['id','show'],
 *         data: [
 *             {id: 0, show: 'Battlestar Galactica'},
 *             {id: 1, show: 'Doctor Who'},
 *             {id: 2, show: 'Farscape'},
 *             {id: 3, show: 'Firefly'},
 *             {id: 4, show: 'Star Trek'},
 *             {id: 5, show: 'Star Wars: Christmas Special'}
 *         ]
 *     });
 *   
 *     Ext.create('Ext.grid.Panel', {
 *         renderTo: Ext.getBody(),
 *         title: 'Sci-Fi Television',
 *         height: 250,
 *         width: 250,
 *         store: shows,
 *         plugins: {
 *             gridfilters: true
 *         },
 *         columns: [{
 *             dataIndex: 'id',
 *             text: 'ID',
 *             width: 50
 *         },{
 *             dataIndex: 'show',
 *             text: 'Show',
 *             flex: 1,
 *             filter: {
 *                 // required configs
 *                 type: 'string',
 *                 // optional configs
 *                 value: 'star',  // setting a value makes the filter active. 
 *                 itemDefaults: {
 *                     // any Ext.form.field.Text configs accepted
 *                 }
 *             }
 *         }]
 *     }); 
 */
Ext.define('Ext.grid.filters.filter.String', {
    extend: 'Ext.grid.filters.filter.SingleFilter',
    alias: 'grid.filter.string',
    type: 'string',
    operator: 'like',
    /**
     * @cfg {String} emptyText
     * The empty text to show for each field.
     * @locale
     */
    emptyText: 'Enter Filter Text...',
    itemDefaults: {
        xtype: 'textfield',
        enableKeyEvents: true,
        hideEmptyLabel: false,
        iconCls: Ext.baseCSSPrefix + 'grid-filters-find',
        labelSeparator: '',
        labelWidth: 29,
        margin: 0,
        selectOnFocus: true
    },
    menuDefaults: {
        // A menu with only form fields needs some body padding. Normally this padding
        // is managed by the items, but we have no normal menu items.
        bodyPadding: 3,
        showSeparator: false
    },
    /**
     * @private
     * Template method that is to initialize the filter and install required menu items.
     */
    createMenu: function() {
        var me = this,
            config;
        me.callParent();
        config = Ext.apply({}, me.getItemDefaults());
        if (config.iconCls && !('labelClsExtra' in config)) {
            config.labelClsExtra = Ext.baseCSSPrefix + 'grid-filters-icon ' + config.iconCls;
        }
        delete config.iconCls;
        config.emptyText = config.emptyText || me.emptyText;
        me.inputItem = me.menu.add(config);
        me.inputItem.on({
            scope: me,
            keyup: me.onValueChange,
            el: {
                click: function(e) {
                    e.stopPropagation();
                }
            }
        });
    },
    /**
     * @private
     * Template method that is to set the value of the filter.
     * @param {Object} value The value to set the filter.
     */
    setValue: function(value) {
        var me = this;
        if (me.inputItem) {
            me.inputItem.setValue(value);
        }
        me.filter.setValue(value);
        if (value && me.active) {
            me.value = value;
            me.updateStoreFilter();
        } else {
            me.setActive(!!value);
        }
    },
    activateMenu: function() {
        this.inputItem.setValue(this.filter.getValue());
    },
    createFilter: function(config, key) {
        var me = this;
        if (me.filterFn) {
            return new Ext.util.Filter({
                filterFn: function(rec) {
                    return Ext.callback(me.filterFn, me.scope, [
                        rec,
                        me.inputItem.getValue()
                    ]);
                }
            });
        } else {
            return me.callParent([
                config,
                key
            ]);
        }
    }
});

/**
 * This class is a grid {@link Ext.AbstractPlugin plugin} that adds a simple and flexible
 * presentation for {@link Ext.data.AbstractStore#filters store filters}.
 *
 * Filters can be modified by the end-user using the grid's column header menu. Through
 * this menu users can configure, enable, and disable filters for each column.
 *
 * # Example Usage
 *
 *     @example
 *     var shows = Ext.create('Ext.data.Store', {
 *         fields: ['id','show'],
 *         data: [
 *             {id: 0, show: 'Battlestar Galactica'},
 *             {id: 1, show: 'Doctor Who'},
 *             {id: 2, show: 'Farscape'},
 *             {id: 3, show: 'Firefly'},
 *             {id: 4, show: 'Star Trek'},
 *             {id: 5, show: 'Star Wars: Christmas Special'}
 *         ]
 *     });
 *   
 *     Ext.create('Ext.grid.Panel', {
 *         renderTo: Ext.getBody(),
 *         title: 'Sci-Fi Television',
 *         height: 250,
 *         width: 250,
 *         store: shows,
 *         plugins: {
 *             gridfilters: true
 *         },
 *         columns: [{
 *             dataIndex: 'id',
 *             text: 'ID',
 *             width: 50
 *         },{
 *             dataIndex: 'show',
 *             text: 'Show',
 *             flex: 1,
 *             filter: {
 *                 // required configs
 *                 type: 'string',
 *                 // optional configs
 *                 value: 'star',  // setting a value makes the filter active. 
 *                 itemDefaults: {
 *                     // any Ext.form.field.Text configs accepted
 *                 }
 *             }
 *         }]
 *     }); 
 *
 * # Features
 *
 * ## Filtering implementations
 *
 * Currently provided filter types are:
 *
 *   * `{@link Ext.grid.filters.filter.Boolean boolean}`
 *   * `{@link Ext.grid.filters.filter.Date date}`
 *   * `{@link Ext.grid.filters.filter.List list}`
 *   * `{@link Ext.grid.filters.filter.Number number}`
 *   * `{@link Ext.grid.filters.filter.String string}`
 *
 * **Note:** You can find inline examples for each filter on its specific filter page. 
 *
 * ## Graphical Indicators
 *
 * Columns that are filtered have {@link #filterCls CSS class} applied to their column
 * headers. This style can be managed using that CSS class or by setting these Sass
 * variables in your theme or application:
 *
 *      $grid-filters-column-filtered-font-style: italic !default;
 *
 *      $grid-filters-column-filtered-font-weight: bold !default;
 *
 * ## Stateful
 *
 * Filter information will be persisted across page loads by specifying a `stateId`
 * in the Grid configuration. In actuality this state is saved by the `store`, but this
 * plugin ensures that saved filters are properly identified and reclaimed on subsequent
 * visits to the page.
 *
 * ## Grid Changes
 *
 * - A `filters` property is added to the Grid using this plugin.
 *
 * # Upgrading From Ext.ux.grid.FilterFeature
 *
 * The biggest change for developers converting from the user extension is most likely the
 * conversion to standard {@link Ext.data.AbstractStore#filters store filters}. In the
 * process, the "like" and "in" operators are now supported by `{@link Ext.util.Filter}`.
 * These filters and all other filters added to the store will be sent in the standard
 * way (using the "filters" parameter by default).
 *
 * Since this plugin now uses actual store filters, the `onBeforeLoad` listener and all
 * helper methods that were used to clean and build the params have been removed. The store
 * will send the filters managed by this plugin along in its normal request.
 */
Ext.define('Ext.grid.filters.Filters', {
    extend: 'Ext.plugin.Abstract',
    alias: 'plugin.gridfilters',
    mixins: [
        'Ext.util.StoreHolder'
    ],
    requires: [
        'Ext.grid.filters.filter.*'
    ],
    id: 'gridfilters',
    /**
     * @property {Object} defaultFilterTypes
     * This property maps {@link Ext.data.Model#cfg-fields field type} to the
     * appropriate grid filter type.
     * @private
     */
    defaultFilterTypes: {
        'boolean': 'boolean',
        'int': 'number',
        date: 'date',
        number: 'number'
    },
    /**
     * @property {String} [filterCls="x-grid-filters-filtered-column"]
     * The CSS applied to column headers with active filters.
     */
    filterCls: Ext.baseCSSPrefix + 'grid-filters-filtered-column',
    /**
     * @cfg {String} [menuFilterText]
     * The text for the filters menu.
     * @locale
     */
    menuFilterText: 'Filters',
    /**
     * @cfg {Boolean} showMenu
     * Defaults to true, including a filter submenu in the default header menu.
     */
    showMenu: true,
    /**
     * @cfg {String} stateId
     * Name of the value to be used to store state information.
     */
    stateId: undefined,
    init: function(grid) {
        var me = this,
            store, headerCt;
        Ext.Assert.falsey(me.grid);
        me.grid = grid;
        grid.filters = me;
        if (me.grid.normalGrid) {
            me.isLocked = true;
        }
        grid.clearFilters = me.clearFilters.bind(me);
        store = grid.store;
        headerCt = grid.headerCt;
        me.headerCtListeners = headerCt.on({
            destroyable: true,
            scope: me,
            add: me.onAdd,
            menucreate: me.onMenuCreate
        });
        // if menu is already created before filter is initialized
        if (headerCt.menu && !headerCt.menu.destroyed) {
            this.onMenuCreate(headerCt, headerCt.menu);
        }
        me.gridListeners = grid.on({
            destroyable: true,
            scope: me,
            reconfigure: me.onReconfigure
        });
        if (store.isEmptyStore) {
            return;
        }
        me.bindStore(store);
        me.initColumns();
    },
    /**
     * Creates the Filter objects for the current configuration.
     * Reconfigure and on add handlers.
     * @private
     */
    initColumns: function() {
        var grid = this.grid,
            store = grid.getStore(),
            columns = grid.columnManager.getColumns(),
            len = columns.length,
            i, column, filter, filterCollection;
        // We start with filters defined on any columns.
        for (i = 0; i < len; i++) {
            column = columns[i];
            filter = column.filter;
            if (filter && !filter.isGridFilter) {
                if (!filterCollection) {
                    filterCollection = store.getFilters();
                    filterCollection.beginUpdate();
                }
                this.createColumnFilter(column);
            }
        }
        if (filterCollection) {
            filterCollection.endUpdate();
        }
    },
    createColumnFilter: function(column) {
        var me = this,
            columnFilter = column.filter,
            filter = {
                column: column,
                grid: me.grid,
                owner: me
            },
            field, model, type;
        if (Ext.isString(columnFilter)) {
            filter.type = columnFilter;
        } else {
            Ext.apply(filter, columnFilter);
        }
        if (!filter.type) {
            model = me.store.getModel();
            // If no filter type given, first try to get it from the data field.
            field = model && model.getField(column.dataIndex);
            type = field && field.type;
            filter.type = (type && me.defaultFilterTypes[type]) || column.defaultFilterType || 'string';
        }
        column.filter = Ext.Factory.gridFilter(filter);
        if (!column.menuDisabled) {
            column.requiresMenu = true;
        }
    },
    onAdd: function(headerCt, column, index) {
        var filter = column.filter;
        if (filter && !filter.isGridFilter) {
            this.createColumnFilter(column);
        }
    },
    /**
     * @private
     * Handle creation of the grid's header menu.
     */
    onMenuCreate: function(headerCt, menu) {
        var me = this;
        if (me.headerMenuListeners) {
            Ext.destroy(me.headerMenuListeners);
            me.headerMenuListeners = null;
        }
        me.headerMenuListeners = menu.on({
            beforeshow: me.onMenuBeforeShow,
            destroyable: true,
            scope: me
        });
    },
    /**
     * @private
     * Handle showing of the grid's header menu. Sets up the filter item and menu
     * appropriate for the target column.
     */
    onMenuBeforeShow: function(menu) {
        var me = this,
            menuItem, filter, parentTable, parentTableId;
        if (me.showMenu) {
            // In the case of a locked grid, we need to cache the 'Filters' menuItem for each grid
            // since there's only one Filters instance. Both grids/menus can't share the same
            // menuItem!
            if (!me.filterMenuItem) {
                me.filterMenuItem = {};
            }
            // Don't get the owner panel if in a locking grid since we need to get the unique
            // filterMenuItem key. Instead, get a ref to the parent, i.e., lockedGrid, normalGrid,
            // etc.
            parentTable = menu.up('tablepanel');
            parentTableId = parentTable.id;
            menuItem = me.filterMenuItem[parentTableId];
            if (!menuItem || menuItem.destroyed) {
                menuItem = me.createMenuItem(menu, parentTableId);
            }
            // Save a ref to the root "Filters" menu item, column filters make use of it.
            me.activeFilterMenuItem = menuItem;
            filter = me.getMenuFilter(parentTable.headerCt);
            if (filter) {
                filter.showMenu(menuItem);
            }
            menuItem.setVisible(!!filter);
            if (me.sep) {
                me.sep.setVisible(!!filter);
            }
        }
    },
    createMenuItem: function(menu, parentTableId) {
        var me = this,
            item;
        // only add separator if there are other menu items
        if (menu.items.length) {
            me.sep = menu.add('-');
        }
        item = menu.add({
            checked: false,
            itemId: 'filters',
            text: me.menuFilterText,
            listeners: {
                scope: me,
                checkchange: me.onCheckChange
            }
        });
        return (me.filterMenuItem[parentTableId] = item);
    },
    destroy: function() {
        var me = this,
            filterMenuItem = me.filterMenuItem,
            item;
        Ext.destroy(me.headerCtListeners, me.gridListeners, me.headerMenuListeners);
        me.bindStore(null);
        me.sep = Ext.destroy(me.sep);
        for (item in filterMenuItem) {
            filterMenuItem[item].destroy();
        }
        this.callParent();
    },
    onUnbindStore: function(store) {
        if (store && !store.destroyed) {
            store.getFilters().un('remove', this.onFilterRemove, this);
        }
    },
    onBindStore: function(store, initial, propName) {
        var me = this;
        me.local = !store.getRemoteFilter();
        store.getFilters().on('remove', me.onFilterRemove, me);
        if (me.grid.stateful && store.initialConfig.statefulFilters !== false) {
            store.statefulFilters = true;
        }
    },
    onFilterRemove: function(filterCollection, list) {
        // We need to know when a store filter has been removed by an operation of the gridfilters
        // UI, i.e., store.clearFilter().  The preventFilterRemoval flag lets us know whether or not
        // this listener has been reached by a filter operation (preventFilterRemoval === true)
        // or by something outside of the UI (preventFilterRemoval === undefined).
        var len = list.items.length,
            columnManager = this.grid.columnManager,
            i, item, header, filter;
        for (i = 0; i < len; i++) {
            item = list.items[i];
            header = columnManager.getHeaderByDataIndex(item.getProperty());
            if (header) {
                // First, we need to make sure there is indeed a filter and that its menu
                // has been created. If not, there's no point in continuing.
                //
                // Also, even though the store may be filtered by this dataIndex, it doesn't
                // necessarily mean that it was created via the gridfilters API. To be sure,
                // we need to check the prefix, as this is the only way we can be sure
                // of its provenance (note that we can't check `operator`).
                //
                // Note that we need to do an indexOf check on the string because TriFilters
                // will contain extra characters specifying its type.
                //
                // TODO: Should we support updating the gridfilters if one or more of its filters
                // have been removed directly by the bound store?
                filter = header.filter;
                if (!filter || !filter.menu || item.getId().indexOf(filter.getBaseIdPrefix()) === -1) {
                    
                    continue;
                }
                if (!filter.preventFilterRemoval) {
                    // This is only called on the filter if called
                    // from outside of the gridfilters UI.
                    filter.onFilterRemove(item.getOperator());
                }
            }
        }
    },
    /**
     * @private
     * Get the filter menu from the filters MixedCollection based on the clicked header.
     */
    getMenuFilter: function(headerCt) {
        return headerCt.getMenu().activeHeader.filter;
    },
    /**
     * @private
     *
     */
    onCheckChange: function(item, value) {
        // Locking grids must lookup the correct grid.
        var parentTable = this.isLocked ? item.up('tablepanel') : this.grid,
            filter = this.getMenuFilter(parentTable.headerCt);
        filter.setActive(value);
    },
    getHeaders: function() {
        return this.grid.view.headerCt.columnManager.getColumns();
    },
    /**
     * Checks the plugin's grid for statefulness.
     * @return {Boolean}
     */
    isStateful: function() {
        return this.grid.stateful;
    },
    /**
     * Adds a filter to the collection and creates a store filter if has a `value` property.
     * @param {Object/Object[]/Ext.util.Filter/Ext.util.Filter[]} filters A filter
     * configuration or a filter object.
     */
    addFilter: function(filters) {
        var me = this,
            grid = me.grid,
            store = me.store,
            hasNewColumns = false,
            suppressNextFilter = true,
            dataIndex, column, i, len, filter, columnFilter;
        if (!Ext.isArray(filters)) {
            filters = [
                filters
            ];
        }
        for (i = 0 , len = filters.length; i < len; i++) {
            filter = filters[i];
            dataIndex = filter.dataIndex;
            column = grid.columnManager.getHeaderByDataIndex(dataIndex);
            // We only create filters that map to an existing column.
            if (column) {
                hasNewColumns = true;
                // Don't suppress active filters.
                if (filter.value) {
                    suppressNextFilter = false;
                }
                columnFilter = column.filter;
                // If already a gridfilter, let's destroy it and recreate another
                // from the new config.
                if (columnFilter && columnFilter.isGridFilter) {
                    columnFilter.deactivate();
                    columnFilter.destroy();
                    if (me.activeFilterMenuItem) {
                        me.activeFilterMenuItem.menu = null;
                    }
                }
                column.filter = filter;
            }
        }
        // Batch initialize all column filters.
        if (hasNewColumns) {
            store.suppressNextFilter = suppressNextFilter;
            me.initColumns();
            store.suppressNextFilter = false;
        }
    },
    /**
     * Adds filters to the collection.
     * @param {Array} filters An Array of filter configuration objects.
     */
    addFilters: function(filters) {
        if (filters) {
            this.addFilter(filters);
        }
    },
    /**
     * Turns all filters off. This does not clear the configuration information.
     */
    clearFilters: function() {
        var grid = this.grid,
            columns = grid.columnManager.getColumns(),
            store = grid.store,
            column, filter, i, len, filterCollection;
        // We start with filters defined on any columns.
        for (i = 0 , len = columns.length; i < len; i++) {
            column = columns[i];
            filter = column.filter;
            if (filter && filter.isGridFilter) {
                if (!filterCollection) {
                    filterCollection = store.getFilters();
                    filterCollection.beginUpdate();
                }
                filter.setActive(false);
            }
        }
        if (filterCollection) {
            filterCollection.endUpdate();
        }
    },
    onReconfigure: function(grid, store, columns, oldStore) {
        var me = this,
            filterMenuItem = me.filterMenuItem,
            changed = oldStore !== store,
            key;
        // The Filters item's menu should have already been destroyed by the time we get here but
        // we still need to null out the menu reference.
        if (columns) {
            for (key in filterMenuItem) {
                filterMenuItem[key].setMenu(null);
            }
        }
        if (store) {
            if (oldStore && !oldStore.destroyed && changed) {
                me.resetFilters(oldStore);
            }
            if (changed) {
                me.bindStore(store);
                me.applyFilters(store);
            }
        }
        me.initColumns();
    },
    privates: {
        applyFilters: function(store) {
            var columns = this.grid.columnManager.getColumns(),
                len = columns.length,
                i, column, filter, filterCollection;
            // We start with filters defined on any columns.
            for (i = 0; i < len; i++) {
                column = columns[i];
                filter = column.filter;
                if (filter && filter.isGridFilter) {
                    if (!filterCollection) {
                        filterCollection = store.getFilters();
                        filterCollection.beginUpdate();
                    }
                    if (filter.active) {
                        filter.activate();
                    }
                }
            }
            if (filterCollection) {
                filterCollection.endUpdate();
            }
        },
        resetFilters: function(store) {
            var filters = store.getFilters(),
                i, updating, filter;
            if (filters) {
                for (i = filters.getCount() - 1; i >= 0; --i) {
                    filter = filters.getAt(i);
                    if (filter.isGridFilter) {
                        if (!updating) {
                            filters.beginUpdate();
                        }
                        filters.remove(filter);
                        updating = true;
                    }
                }
                if (updating) {
                    filters.endUpdate();
                }
            }
        }
    }
});

/**
 * Private class which acts as a HeaderContainer for the Lockable which aggregates all columns
 * from both sides of the Lockable. It is never rendered, it's just used to interrogate the
 * column collection.
 * @private
 */
Ext.define('Ext.grid.locking.HeaderContainer', {
    extend: 'Ext.grid.header.Container',
    requires: [
        'Ext.grid.ColumnManager'
    ],
    headerCtRelayEvents: [
        "blur",
        "focus",
        "move",
        "resize",
        "destroy",
        "beforedestroy",
        "boxready",
        "afterrender",
        "render",
        "beforerender",
        "removed",
        "hide",
        "beforehide",
        "show",
        "beforeshow",
        "enable",
        "disable",
        "added",
        "deactivate",
        "beforedeactivate",
        "activate",
        "beforeactivate",
        "remove",
        "add",
        "beforeremove",
        "beforeadd",
        "afterlayout",
        "menucreate",
        "sortchange",
        "columnschanged",
        "columnshow",
        "columnhide",
        "columnmove",
        "headertriggerclick",
        "headercontextmenu",
        "headerclick",
        "columnresize",
        "statesave",
        "beforestatesave",
        "staterestore",
        "beforestaterestore"
    ],
    constructor: function(lockable) {
        var me = this,
            lockedGrid = lockable.lockedGrid,
            normalGrid = lockable.normalGrid;
        me.lockable = lockable;
        me.callParent();
        // Create the unified column manager for the lockable grid assembly
        lockedGrid.visibleColumnManager.rootColumns = normalGrid.visibleColumnManager.rootColumns = lockable.visibleColumnManager = me.visibleColumnManager = new Ext.grid.ColumnManager(true, lockedGrid.headerCt, normalGrid.headerCt);
        lockedGrid.columnManager.rootColumns = normalGrid.columnManager.rootColumns = lockable.columnManager = me.columnManager = new Ext.grid.ColumnManager(false, lockedGrid.headerCt, normalGrid.headerCt);
        // Relay *all* events from the two HeaderContainers
        me.lockedEventRelayers = me.relayEvents(lockedGrid.headerCt, me.headerCtRelayEvents);
        me.normalEventRelayers = me.relayEvents(normalGrid.headerCt, me.headerCtRelayEvents);
    },
    getRefItems: function() {
        return this.lockable.lockedGrid.headerCt.getRefItems().concat(this.lockable.normalGrid.headerCt.getRefItems());
    },
    // This is the function which all other column access methods are based upon
    // Return the full column set for the whole Lockable assembly
    getGridColumns: function() {
        return this.lockable.lockedGrid.headerCt.getGridColumns().concat(this.lockable.normalGrid.headerCt.getGridColumns());
    },
    // Lockable uses its headerCt to gather column state
    getColumnsState: function() {
        var me = this,
            locked = me.lockable.lockedGrid.headerCt.getColumnsState(),
            normal = me.lockable.normalGrid.headerCt.getColumnsState();
        return locked.concat(normal);
    },
    // Lockable uses its headerCt to apply column state
    applyColumnsState: function(columnsState, storeState) {
        var me = this,
            lockedGrid = me.lockable.lockedGrid,
            normalGrid = me.lockable.normalGrid,
            lockedHeaderCt = lockedGrid.headerCt,
            normalHeaderCt = me.lockable.normalGrid.headerCt,
            columns = lockedHeaderCt.items.items.concat(normalHeaderCt.items.items),
            length = columns.length,
            i, colState, column, lockedCount, switchSides;
        // Loop through the column set, applying state from the columnsState object.
        // Columns which have their "locked" property changed must be added to the appropriate
        // headerCt.
        for (i = 0; i < length; i++) {
            column = columns[i];
            colState = columnsState[column.getStateId()];
            if (colState) {
                // See if the state being applied needs to cause column movement
                // Coerce possibly absent locked config to boolean.
                switchSides = colState.locked != null && !!column.locked !== colState.locked;
                if (column.applyColumnState) {
                    column.applyColumnState(colState, storeState);
                }
                // If the column state means it has to change sides
                // move the column to the other side
                if (switchSides) {
                    (column.locked ? lockedHeaderCt : normalHeaderCt).add(column);
                }
            }
        }
        lockedCount = lockedHeaderCt.items.items.length;
        // We must now restore state in each side's HeaderContainer.
        // This means passing the state down into each side's applyColumnState
        // to get sortable, hidden and width states restored.
        // We must ensure that the index on the normal side is zero based.
        for (i = 0; i < length; i++) {
            column = columns[i];
            colState = columnsState[column.getStateId()];
            if (colState && !column.locked) {
                colState.index = Math.max(0, colState.index - lockedCount);
            }
        }
        // Each side must apply individual column's state
        lockedHeaderCt.applyColumnsState(columnsState, storeState);
        normalHeaderCt.applyColumnsState(columnsState, storeState);
        // Account for columns being hidden or moved by state application.
        if (!lockedGrid.getVisibleColumnManager().getColumns().length) {
            lockedGrid.hide();
        }
        if (!normalGrid.getVisibleColumnManager().getColumns().length) {
            normalGrid.hide();
        }
    },
    disable: function() {
        var topGrid = this.lockable;
        topGrid.lockedGrid.headerCt.disable();
        topGrid.normalGrid.headerCt.disable();
    },
    enable: function() {
        var topGrid = this.lockable;
        topGrid.lockedGrid.headerCt.enable();
        topGrid.normalGrid.headerCt.enable();
    }
});

/**
 * This class is used internally to provide a single interface when using
 * a locking grid. Internally, the locking grid creates two separate grids,
 * so this class is used to map calls appropriately.
 * @private
 */
Ext.define('Ext.grid.locking.View', {
    alternateClassName: 'Ext.grid.LockingView',
    requires: [
        'Ext.view.AbstractView',
        'Ext.view.Table'
    ],
    mixins: [
        'Ext.util.Observable',
        'Ext.util.StoreHolder',
        'Ext.mixin.Focusable'
    ],
    /**
     * @property {Boolean} isLockingView
     * `true` in this class to identify an object as an instantiated LockingView,
     * or subclass thereof.
     */
    isLockingView: true,
    loadMask: true,
    eventRelayRe: /^(beforeitem|beforecontainer|item|container|cell|refresh)/,
    constructor: function(config) {
        var ext = Ext,
            me = this,
            lockedView, normalView;
        me.ownerGrid = config.ownerGrid;
        me.ownerGrid.view = me;
        // A single NavigationModel is configured into both views.
        me.navigationModel = config.locked.xtype === 'treepanel' ? new ext.tree.NavigationModel(me) : new ext.grid.NavigationModel(me);
        // Disable store binding for the two child views.
        // The store is bound to the *this* locking View.
        // This avoids the store being bound to two views (with duplicated layouts
        // on each store mutation) and also avoids the store being bound
        // to the selection model twice.
        config.locked.viewConfig.bindStore = ext.emptyFn;
        config.normal.viewConfig.bindStore = me.subViewBindStore;
        config.normal.viewConfig.isNormalView = config.locked.viewConfig.isLockedView = true;
        // Share the same NavigationModel
        config.locked.viewConfig.navigationModel = config.normal.viewConfig.navigationModel = me.navigationModel;
        me.lockedGrid = me.ownerGrid.lockedGrid = ext.ComponentManager.create(config.locked);
        me.lockedView = lockedView = me.lockedGrid.getView();
        // The normal view uses the same selection model
        me.selModel = config.normal.viewConfig.selModel = lockedView.getSelectionModel();
        if (me.lockedGrid.isTree) {
            // Tree must not animate because the partner grid is unable to animate
            me.lockedView.animate = false;
            // When this is a locked tree, the normal side is just a gridpanel,
            // so needs the flat NodeStore
            config.normal.store = lockedView.store;
            // Match configs between sides
            config.normal.viewConfig.stripeRows = me.lockedView.stripeRows;
            config.normal.rowLines = me.lockedGrid.rowLines;
        }
        // Set up a bidirectional relationship between the two sides of the locked view.
        // Inject lockingGrid and normalGrid into owning panel.
        // This is because during constraction, it must be possible for descendant components
        // to navigate up to the owning lockable panel and then down into either side.
        me.normalGrid = me.ownerGrid.normalGrid = ext.ComponentManager.create(config.normal);
        lockedView.lockingPartner = normalView = me.normalView = me.normalGrid.getView();
        normalView.lockingPartner = lockedView;
        // We need to examine locked grid state at this time to sync the normal grid.
        Ext.override(me.normalGrid, {
            beforeRender: me.beforeNormalGridRender
        });
        me.loadMask = (config.loadMask !== undefined) ? config.loadMask : me.loadMask;
        me.mixins.observable.constructor.call(me);
        // Relay both view's events.
        me.lockedViewEventRelayers = me.relayEvents(lockedView, ext.view.Table.events);
        // Relay extra events from only the normal view.
        // These are events that both sides fire (selection events), so avoid firing them twice.
        me.normalViewEventRelayers = me.relayEvents(normalView, ext.view.Table.events.concat(ext.view.Table.normalSideEvents));
        normalView.on({
            scope: me,
            itemmouseleave: me.onItemMouseLeave,
            itemmouseenter: me.onItemMouseEnter
        });
        lockedView.on({
            scope: me,
            itemmouseleave: me.onItemMouseLeave,
            itemmouseenter: me.onItemMouseEnter
        });
        me.loadingText = normalView.loadingText;
        me.loadingCls = normalView.loadingCls;
        me.loadingUseMsg = normalView.loadingUseMsg;
        me.itemSelector = me.getItemSelector();
        // Share the items arrey with the normal view.
        // Certain methods need access to the start/end/count
        me.all = normalView.all;
        // Bind to the data source. Cache it by the property name "dataSource".
        // The store property is public and must reference the provided store.
        // We relay each call into both normal and locked views bracketed by a layout suspension.
        me.bindStore(normalView.dataSource, true, 'dataSource');
    },
    // This is injected into the two child views as the bindStore implementation.
    // Subviews in a lockable asseembly do not bind to stores.
    subViewBindStore: function(store, initial) {
        var me = this,
            grid = me.ownerGrid,
            selModel;
        if (me.destroying || me.destroyed || grid.destroying || grid.destroyed) {
            return;
        }
        selModel = me.getSelectionModel();
        selModel.bindStore(store, initial);
        selModel.bindComponent(me);
    },
    beforeNormalGridRender: function() {
        // This method is used in an Ext.override call, so the 'this' pointer will
        // not be the normal reference
        // If the locked side has a header (for example it's collapsible, or has tools)
        // and this has not been configured with a title, we need an &nbsp; title.
        if (this.ownerGrid.lockedGrid.getHeader() && !this.title) {
            this.title = '\xa0';
        }
        // @noOptimize.callParent
        this.callParent();
    },
    onPanelRender: function(el) {
        var me = this,
            mask = me.loadMask,
            cfg = {
                target: me.ownerGrid,
                msg: me.loadingText,
                msgCls: me.loadingCls,
                useMsg: me.loadingUseMsg,
                store: me.ownerGrid.store
            };
        // Because this is used as a View, it should have an el. Use the owning Lockable's
        // scrolling el. It also has to fire a render event so that Editing plugins
        // can attach listeners
        me.el = el;
        me.rendered = true;
        me.fireEvent('render', me);
        if (mask) {
            // either a config object 
            if (Ext.isObject(mask)) {
                cfg = Ext.apply(cfg, mask);
            }
            // Attach the LoadMask to a *Component* so that it can be sensitive to resizing
            // during long loads.
            // If this DataView is floating, then mask this DataView.
            // Otherwise, mask its owning Container (or this, if there *is* no owning Container).
            // LoadMask captures the element upon render.
            me.loadMask = new Ext.LoadMask(cfg);
        }
    },
    getRefOwner: function() {
        return this.ownerGrid;
    },
    // Implement the same API as Ext.view.Table.
    // This will return the topmost, unified visible column manager
    getVisibleColumnManager: function() {
        // ownerGrid refers to the topmost responsible Ext.panel.Grid.
        // This could be this view's ownerCt, or if part of a locking arrangement, the locking grid
        return this.ownerGrid.getVisibleColumnManager();
    },
    getTopLevelVisibleColumnManager: function() {
        // ownerGrid refers to the topmost responsible Ext.panel.Grid.
        // This could be this view's ownerCt, or if part of a locking arrangement, the locking grid
        return this.ownerGrid.getVisibleColumnManager();
    },
    getGridColumns: function() {
        return this.getVisibleColumnManager().getColumns();
    },
    getEl: function(column) {
        return column.getView().getEl();
    },
    getCellSelector: function() {
        return this.normalView.getCellSelector();
    },
    getItemSelector: function() {
        return this.normalView.getItemSelector();
    },
    onItemMouseEnter: function(view, record) {
        var me = this,
            locked = me.lockedView,
            other = me.normalView,
            item;
        if (view.trackOver) {
            if (view !== locked) {
                other = locked;
            }
            item = other.getNode(record);
            other.highlightItem(item);
        }
    },
    onItemMouseLeave: function(view, record) {
        var me = this,
            locked = me.lockedView,
            other = me.normalView;
        if (view.trackOver) {
            if (view !== locked) {
                other = locked;
            }
            other.clearHighlight();
        }
    },
    relayFn: function(name, args) {
        var me = this,
            view = me.lockedView;
        args = args || [];
        // Flag that we are already manipulating the view pair, so resulting excursions
        // back into this class can avoid breaking the sequence.
        me.relayingOperation = true;
        view[name].apply(view, args);
        view = me.normalView;
        view[name].apply(view, args);
        me.relayingOperation = false;
    },
    getSelectionModel: function() {
        return this.normalView.getSelectionModel();
    },
    getNavigationModel: function() {
        return this.navigationModel;
    },
    getStore: function() {
        return this.ownerGrid.store;
    },
    /**
     * Changes the data store bound to this view and refreshes it.
     * @param {Ext.data.Store} store The store to bind to this view
     * @since 3.4.0
     */
    onBindStore: function(store) {
        var me = this,
            lockedView = me.lockedView,
            normalView = me.normalView;
        // If we have already achieved our first layout, refresh immediately.
        // If we have bound to the Store before the first layout, then onBoxReady will
        // call doFirstRefresh
        if (normalView.componentLayoutCounter && !(lockedView.blockRefresh && normalView.blockRefresh)) {
            Ext.suspendLayouts();
            lockedView.doFirstRefresh(store);
            normalView.doFirstRefresh(store);
            Ext.resumeLayouts(true);
        }
    },
    getStoreListeners: function() {
        var me = this;
        return {
            // Give view listeners the highest priority, since they need to relay things to
            // children first
            priority: 1000,
            refresh: me.onDataRefresh,
            replace: me.onReplace,
            add: me.onAdd,
            remove: me.onRemove,
            update: me.onUpdate,
            clear: me.onDataRefresh,
            beginupdate: me.onBeginUpdate,
            endupdate: me.onEndUpdate
        };
    },
    onOwnerGridHide: function() {
        Ext.suspendLayouts();
        this.relayFn('onOwnerGridHide', arguments);
        Ext.resumeLayouts(true);
    },
    onOwnerGridShow: function() {
        Ext.suspendLayouts();
        this.relayFn('onOwnerGridShow', arguments);
        Ext.resumeLayouts(true);
    },
    onBeginUpdate: function() {
        Ext.suspendLayouts();
        this.relayFn('onBeginUpdate', arguments);
        Ext.resumeLayouts(true);
    },
    onEndUpdate: function() {
        Ext.suspendLayouts();
        this.relayFn('onEndUpdate', arguments);
        Ext.resumeLayouts(true);
    },
    onDataRefresh: function() {
        Ext.suspendLayouts();
        this.relayFn('onDataRefresh', arguments);
        Ext.resumeLayouts(true);
    },
    onReplace: function() {
        Ext.suspendLayouts();
        this.relayFn('onReplace', arguments);
        Ext.resumeLayouts(true);
    },
    onAdd: function() {
        Ext.suspendLayouts();
        this.relayFn('onAdd', arguments);
        Ext.resumeLayouts(true);
    },
    onRemove: function() {
        Ext.suspendLayouts();
        this.relayFn('onRemove', arguments);
        Ext.resumeLayouts(true);
    },
    /**
     * Toggles ARIA actionable mode on/off
     * @param {Boolean} enabled
     * @param {Ext.grid.CellContext} position
     * @return {Boolean} Returns `false` if the request failed.
     * @private
     */
    setActionableMode: function(enabled, position) {
        var result, targetView;
        if (enabled) {
            if (!position) {
                position = this.getNavigationModel().getPosition();
            }
            if (position) {
                position = position.clone();
                // Drill down to the side that we're actioning
                position.view = targetView = position.column.getView();
                // Attempt to switch the focused view to actionable.
                result = targetView.setActionableMode(enabled, position);
                // If successful, and the partner is visible, switch that too.
                if (result !== false && targetView.lockingPartner.grid.isVisible()) {
                    targetView.lockingPartner.setActionableMode(enabled, position);
                    // If the partner side refused to cooperate, the whole locking.View
                    // must not enter actionable mode
                    if (!targetView.lockingPartner.actionableMode) {
                        targetView.setActionableMode(false);
                        result = false;
                    }
                }
                return result;
            } else {
                return false;
            }
        } else {
            this.relayFn('setActionableMode', [
                false,
                position
            ]);
        }
    },
    onUpdate: function() {
        Ext.suspendLayouts();
        this.relayFn('onUpdate', arguments);
        Ext.resumeLayouts(true);
    },
    refresh: function() {
        var lockedView = this.lockedView,
            normalView = this.normalView;
        Ext.suspendLayouts();
        // Clear both views first so that any widgets are cached first.
        // Otherwise the second refresh's clear could remove widgets
        // that are in the first view who's column has been moved.
        lockedView.clearViewEl(true);
        normalView.clearViewEl(true);
        // Refresh locked view second, so that if it's refreshing from empty (can start
        // with no locked columns), the buffered renderer can look to its partner
        // to get the correct range to refresh.
        normalView.refresh();
        lockedView.refresh();
        Ext.resumeLayouts(true);
    },
    refreshView: function() {
        var lockedView = this.lockedView,
            normalView = this.normalView,
            startIndex = normalView.all.startIndex;
        Ext.suspendLayouts();
        // Clear both views first so that any widgets are cached first.
        // Otherwise the second refresh's clear could remove widgets
        // that are in the first view who's column has been moved.
        lockedView.clearViewEl(true);
        normalView.clearViewEl(true);
        // Refresh locked view second, so that if it's refreshing from empty (can start
        // with no locked columns), the buffered renderer can look to its partner
        // to get the correct range to refresh.
        normalView.refreshView(startIndex);
        lockedView.refreshView(startIndex);
        Ext.resumeLayouts(true);
    },
    setScrollable: function(scrollable) {
        Ext.suspendLayouts();
        this.lockedView.setScrollable(scrollable);
        if (scrollable.isScroller) {
            scrollable = new Ext.scroll.Scroller(scrollable.initialConfig);
        }
        this.normalView.setScrollable(scrollable);
        Ext.resumeLayouts(true);
    },
    getNode: function(nodeInfo) {
        // default to the normal view
        return this.normalView.getNode(nodeInfo);
    },
    getRow: function(nodeInfo) {
        // default to the normal view
        return this.normalView.getRow(nodeInfo);
    },
    getCell: function(record, column, returnElement) {
        var row = column.getView().getRow(record),
            cell = row.querySelector(column.getCellSelector());
        return returnElement ? Ext.get(cell) : cell;
    },
    indexOf: function(record) {
        var result = this.lockedView.indexOf(record);
        if (!result) {
            result = this.normalView.indexOf(record);
        }
        return result;
    },
    focus: function() {
        // Delegate to the view of first visible child tablepanel of the owning lockable assembly.
        var target = this.ownerGrid.down('>tablepanel:not(hidden)>tableview');
        if (target) {
            target.focus();
        }
    },
    focusRow: function(row) {
        var view,
            // Access lastFocused directly because getter nulls it if the record
            // is no longer in view and all we are interested in is the lastFocused View.
            lastFocused = this.getNavigationModel().lastFocused;
        view = lastFocused ? lastFocused.view : this.normalView;
        view.focusRow(row);
    },
    focusCell: function(position) {
        position.view.focusCell(position);
    },
    onRowFocus: function() {
        this.relayFn('onRowFocus', arguments);
    },
    cancelFocusTask: function() {
        this.lockedView.cancelFocusTask();
        this.normalView.cancelFocusTask();
    },
    isVisible: function(deep) {
        return this.ownerGrid.isVisible(deep);
    },
    // Old API. Used by tests now to test coercion of navigation from hidden column
    // to closest visible. Position.column includes all columns including hidden ones.
    getCellInclusive: function(pos, returnDom) {
        var col = pos.column,
            lockedSize = this.lockedGrid.getColumnManager().getColumns().length;
        // Normalize view
        if (col >= lockedSize) {
            // Make a copy so we don't mutate the passed object
            pos = Ext.apply({}, pos);
            pos.column -= lockedSize;
            return this.normalView.getCellInclusive(pos, returnDom);
        } else {
            return this.lockedView.getCellInclusive(pos, returnDom);
        }
    },
    getHeaderByCell: function(cell) {
        if (cell) {
            return this.getVisibleColumnManager().getHeaderById(cell.getAttribute('data-columnId'));
        }
        return false;
    },
    onRowSelect: function() {
        this.relayFn('onRowSelect', arguments);
    },
    onRowDeselect: function() {
        this.relayFn('onRowDeselect', arguments);
    },
    onCellSelect: function(cellContext) {
        // Pass a contextless cell descriptor to the child view
        cellContext.column.getView().onCellSelect({
            record: cellContext.record,
            column: cellContext.column
        });
    },
    onCellDeselect: function(cellContext) {
        // Pass a contextless cell descriptor to the child view
        cellContext.column.getView().onCellDeselect({
            record: cellContext.record,
            column: cellContext.column
        });
    },
    getCellByPosition: function(pos, returnDom) {
        var me = this,
            view = pos.view,
            col = pos.column;
        // Access the real Ext.view.Table for the specified Column
        if (view === me) {
            pos = new Ext.grid.CellContext(col.getView()).setPosition(pos.record, pos.column);
        }
        return view.getCellByPosition(pos, returnDom);
    },
    getRecord: function(node) {
        var result = this.lockedView.getRecord(node);
        if (!result) {
            result = this.normalView.getRecord(node);
        }
        return result;
    },
    scrollBy: function() {
        var scroller = this.ownerGrid.getScrollable();
        scroller.scrollBy.apply(scroller, arguments);
    },
    ensureVisible: function() {
        var normal = this.normalView;
        normal.ensureVisible.apply(normal, arguments);
    },
    disable: function() {
        this.relayFn('disable', arguments);
    },
    enable: function() {
        this.relayFn('enable', arguments);
    },
    addElListener: function() {
        this.relayFn('addElListener', arguments);
    },
    refreshNode: function() {
        this.relayFn('refreshNode', arguments);
    },
    addRowCls: function() {
        this.relayFn('addRowCls', arguments);
    },
    removeRowCls: function() {
        this.relayFn('removeRowCls', arguments);
    },
    destroy: function() {
        var me = this;
        me.rendered = false;
        // Unbind from the dataSource we bound to in constructor
        me.bindStore(null, false, 'dataSource');
        Ext.destroy(me.selModel, me.navigationModel, me.loadMask);
        me.lockedView.lockingPartner = me.normalView.lockingPartner = null;
        me.callParent();
    }
}, function() {
    this.borrow(Ext.Component, [
        'up'
    ]);
    this.borrow(Ext.view.AbstractView, [
        'doFirstRefresh',
        'applyFirstRefresh'
    ]);
    this.borrow(Ext.view.Table, [
        'cellSelector',
        'selectedCellCls',
        'selectedItemCls'
    ]);
});

Ext.define('Ext.scroll.LockingScroller', {
    extend: 'Ext.scroll.Scroller',
    alias: 'scroller.locking',
    config: {
        lockedScroller: null,
        normalScroller: null
    },
    scrollTo: function(x, y, animate) {
        var lockedX, lockedPromise, ret;
        if (Ext.isObject(x)) {
            lockedX = x.lockedX;
            if (lockedX) {
                lockedPromise = this.getLockedScroller().scrollTo(lockedX, null, animate);
            }
        }
        ret = this.callParent([
            x,
            y,
            animate
        ]);
        if (lockedPromise) {
            ret = Ext.Promise.all([
                ret,
                lockedPromise
            ]);
        }
        return ret;
    },
    updateLockedScroller: function(lockedScroller) {
        lockedScroller.on('scroll', 'onLockedScroll', this);
        lockedScroller.setLockingScroller(this);
    },
    updateNormalScroller: function(normalScroller) {
        normalScroller.on('scroll', 'onNormalScroll', this);
        normalScroller.setLockingScroller(this);
    },
    updateTouchAction: function(touchAction, oldTouchAction) {
        this.callParent([
            touchAction,
            oldTouchAction
        ]);
        this.getLockedScroller().setTouchAction(touchAction);
        this.getNormalScroller().setTouchAction(touchAction);
    },
    getPosition: function() {
        var position = this.callParent();
        position.x = this.getNormalScroller().getPosition().x;
        position.lockedX = this.getLockedScroller().getPosition().x;
        return position;
    },
    privates: {
        updateSpacerXY: function(pos) {
            var me = this,
                lockedScroller = me.getLockedScroller(),
                normalScroller = me.getNormalScroller(),
                lockedView = lockedScroller.component,
                normalView = normalScroller.component,
                height;
            height = pos.y + ((normalView.headerCt.tooNarrow || lockedView.headerCt.tooNarrow) ? Ext.scrollbar.height() : 0);
            normalView.stretchHeight(height);
            lockedView.stretchHeight(height);
            me.callParent([
                pos
            ]);
        },
        doScrollTo: function(x, y, animate) {
            var ret, normalPromise;
            if (x != null) {
                normalPromise = this.getNormalScroller().scrollTo(x, null, animate);
                x = null;
            }
            ret = this.callParent([
                x,
                y,
                animate
            ]);
            if (normalPromise) {
                ret = Ext.Promise.all([
                    ret,
                    normalPromise
                ]);
            }
            return ret;
        },
        onLockedScroll: function(lockedScroller, x, y) {
            this.position.lockedX = x;
        },
        onNormalScroll: function(normalScroller, x, y) {
            this.position.x = x;
        },
        readPosition: function(position) {
            var me = this;
            position = me.callParent([
                position
            ]);
            position = position || {};
            // read position should consider the normal view for the x axis
            position.x = me.getNormalScroller().getPosition().x;
            return position;
        }
    }
});

/**
 * @private
 *
 * Lockable is a private mixin which injects lockable behavior into any
 * TablePanel subclass such as GridPanel or TreePanel. TablePanel will
 * automatically inject the Ext.grid.locking.Lockable mixin in when one of the
 * these conditions are met:
 *
 *  - The TablePanel has the lockable configuration set to true
 *  - One of the columns in the TablePanel has locked set to true/false
 *
 * Each TablePanel subclass must register an alias. It should have an array
 * of configurations to copy to the 2 separate tablepanels that will be generated
 * to note what configurations should be copied. These are named normalCfgCopy and
 * lockedCfgCopy respectively.
 *
 * Configurations which are specified in this class will be available on any grid or
 * tree which is using the lockable functionality.
 *
 * By default the two grids, "locked" and "normal" will be arranged using an
 * {@link Ext.layout.container.HBox hbox} layout. If the lockable grid is configured with
 * `{@link #split split:true}`, a vertical splitter will be placed between the two grids
 * to resize them.
 *
 * It is possible to override the layout of the lockable grid, or example, you may wish to
 * use a border layout and have one of the grids collapsible.
 */
Ext.define('Ext.grid.locking.Lockable', {
    alternateClassName: 'Ext.grid.Lockable',
    requires: [
        'Ext.grid.locking.View',
        'Ext.grid.header.Container',
        'Ext.grid.locking.HeaderContainer',
        'Ext.view.Table',
        'Ext.scroll.LockingScroller'
    ],
    /**
     * @cfg {Boolean} syncRowHeight
     * Synchronize rowHeight between the normal and locked grid view. This is turned on
     * by default. If your grid is guaranteed to have rows of all the same height, you
     * should set this to false to optimize performance.
     */
    syncRowHeight: true,
    /**
     * @cfg {String} subGridXType
     * The xtype of the subgrid to specify. If this is not specified lockable will 
     * determine the subgrid xtype to create by the following rule. Use the superclasses
     * xtype if the superclass is NOT tablepanel, otherwise use the xtype itself.
     */
    /**
     * @cfg {Object} lockedViewConfig
     * A view configuration to be applied to the locked side of the grid. Any conflicting
     * configurations between lockedViewConfig and viewConfig will be overwritten by the
     * lockedViewConfig.
     */
    /**
     * @cfg {Object} normalViewConfig
     * A view configuration to be applied to the normal/unlocked side of the grid. Any
     * conflicting configurations between normalViewConfig and viewConfig will be
     * overwritten by the normalViewConfig.
     */
    headerCounter: 0,
    /**
     * @cfg {Object} lockedGridConfig
     * Any special configuration options for the locked part of the grid
     */
    /**
     * @cfg {Object} normalGridConfig
     * Any special configuration options for the normal part of the grid
     */
    /**
     * @cfg {Boolean/Object} [split=false]
     * Configure as `true` to place a resizing {@link Ext.resizer.Splitter splitter}
     * between the locked and unlocked columns. May also be a configuration object for the Splitter.
     */
    /**
     * @cfg {Object} layout
     * By default, a lockable grid uses an {@link Ext.layout.container.HBox HBox} layout to arrange
     * the two grids (possibly separated by a splitter).
     *
     * Using this config it is possible to specify a different layout to arrange the two grids.
     */
    /**
     * @cfg stateEvents
     * @inheritdoc Ext.state.Stateful#cfg-stateEvents
     * @localdoc Adds the following stateEvents:
     * 
     *  - {@link #event-lockcolumn}
     *  - {@link #event-unlockcolumn}
     */
    lockedGridCls: Ext.baseCSSPrefix + 'grid-inner-locked',
    normalGridCls: Ext.baseCSSPrefix + 'grid-inner-normal',
    lockingBodyCls: Ext.baseCSSPrefix + 'grid-locking-body',
    scrollContainerCls: Ext.baseCSSPrefix + 'grid-scroll-container',
    scrollBodyCls: Ext.baseCSSPrefix + 'grid-scroll-body',
    scrollbarClipperCls: Ext.baseCSSPrefix + 'grid-scrollbar-clipper',
    scrollbarCls: Ext.baseCSSPrefix + 'grid-scrollbar',
    scrollbarVisibleCls: Ext.baseCSSPrefix + 'grid-scrollbar-visible',
    /**
     * @cfg {String} lockText
     * The text to display on the column menu to lock a column.
     * @locale
     */
    lockText: 'Lock',
    /**
     * @cfg {String} unlockText
     * The text to display on the column menu to unlock a column.
     * @locale
     */
    unlockText: 'Unlock',
    // Required for the Lockable Mixin. These are the configurations which will be copied to the
    // normal and locked sub tablepanels
    bothCfgCopy: [
        'hideHeaders',
        'enableColumnHide',
        'enableColumnMove',
        'enableColumnResize',
        'sortableColumns',
        'multiColumnSort',
        'columnLines',
        'rowLines',
        'variableRowHeight',
        'numFromEdge',
        'trailingBufferZone',
        'leadingBufferZone',
        'scrollToLoadBuffer',
        'syncRowHeight'
    ],
    normalCfgCopy: [
        'scroll'
    ],
    lockedCfgCopy: [],
    /**
     * @event processcolumns
     * Fires when the configured (or **reconfigured**) column set is split into two
     * depending on the {@link Ext.grid.column.Column#locked locked} flag.
     * @param {Ext.grid.column.Column[]} lockedColumns The locked columns.
     * @param {Ext.grid.column.Column[]} normalColumns The normal columns.
     */
    /**
     * @event lockcolumn
     * Fires when a column is locked.
     * @param {Ext.grid.Panel} this The gridpanel.
     * @param {Ext.grid.column.Column} column The column being locked.
     */
    /**
     * @event unlockcolumn
     * Fires when a column is unlocked.
     * @param {Ext.grid.Panel} this The gridpanel.
     * @param {Ext.grid.column.Column} column The column being unlocked.
     */
    determineXTypeToCreate: function(lockedSide) {
        var me = this;
        if (me.subGridXType) {
            return me.subGridXType;
        } else if (!lockedSide) {
            // Tree columns only moves down into the locked side.
            // The normal side is always just a grid
            return 'gridpanel';
        }
        return me.isXType('treepanel') ? 'treepanel' : 'gridpanel';
    },
    // injectLockable will be invoked before initComponent's parent class implementation
    // is called, so throughout this method this. are configurations
    injectLockable: function() {
        // The child grids are focusable, not this one
        this.focusable = false;
        // ensure lockable is set to true in the TablePanel
        this.lockable = true;
        // Instruct the TablePanel it already has a view and not to create one.
        // We are going to aggregate 2 copies of whatever TablePanel we are using
        this.hasView = true;
        // eslint-disable-next-line vars-on-top
        var me = this,
            store = me.store = Ext.StoreManager.lookup(me.store),
            lockedViewConfig = me.lockedViewConfig,
            normalViewConfig = me.normalViewConfig,
            viewConfig = me.viewConfig,
            // When setting the loadMask value, the viewConfig wins if it is defined.
            loadMaskCfg = viewConfig && viewConfig.loadMask,
            loadMask = (loadMaskCfg !== undefined) ? loadMaskCfg : me.loadMask,
            bufferedRenderer = me.bufferedRenderer,
            Obj = Ext.Object,
            // Hash of {lockedFeatures:[],normalFeatures:[]}
            allFeatures, // Hash of {topPlugins:[],lockedPlugins:[],normalPlugins:[]}
            allPlugins, lockedGrid, normalGrid, columns, lockedHeaderCt, normalHeaderCt, setWidth, i;
        allFeatures = me.constructLockableFeatures();
        // Must be available early. The BufferedRenderer needs access to it to add
        // scroll listeners.
        me.scrollable = new Ext.scroll.LockingScroller({
            component: me,
            x: false,
            y: true
        });
        // This is just a "shell" Panel which acts as a Container for the two grids
        // and must not use the features
        me.features = null;
        // Distribute plugins to whichever Component needs them
        allPlugins = me.constructLockablePlugins();
        me.plugins = allPlugins.topPlugins;
        lockedGrid = {
            id: me.id + '-locked',
            $initParent: me,
            isLocked: true,
            bufferedRenderer: bufferedRenderer,
            ownerGrid: me,
            ownerLockable: me,
            xtype: me.determineXTypeToCreate(true),
            store: store,
            scrollerOwner: false,
            // Lockable does NOT support animations for Tree
            // Because the right side is just a grid, and the grid view doen't animate
            // bulk insertions/removals
            animate: false,
            border: false,
            cls: me.lockedGridCls,
            // Usually a layout in one side necessitates the laying out of the other side
            // even if each is fully managed in both dimensions, and is therefore a layout root.
            // The only situation that we do *not* want layouts to escape into the owning lockable
            // assembly is when using a border layout and any of the border regions is floated
            // from a collapsed state.
            isLayoutRoot: function() {
                return this.floatedFromCollapse || this.ownerGrid.normalGrid.floatedFromCollapse;
            },
            features: allFeatures.lockedFeatures,
            plugins: allPlugins.lockedPlugins
        };
        normalGrid = {
            id: me.id + '-normal',
            $initParent: me,
            isLocked: false,
            bufferedRenderer: bufferedRenderer,
            ownerGrid: me,
            ownerLockable: me,
            xtype: me.determineXTypeToCreate(),
            store: store,
            // Pass down our reserveScrollbar to the normal side:
            reserveScrollbar: me.reserveScrollbar,
            scrollerOwner: false,
            border: false,
            cls: me.normalGridCls,
            // As described above, isolate layouts when floated out from a collapsed border region.
            isLayoutRoot: function() {
                return this.floatedFromCollapse || this.ownerGrid.lockedGrid.floatedFromCollapse;
            },
            features: allFeatures.normalFeatures,
            plugins: allPlugins.normalPlugins
        };
        me.addCls(Ext.baseCSSPrefix + 'grid-locked');
        // Copy appropriate configurations to the respective aggregated tablepanel instances.
        // Pass 4th param true to NOT exclude those settings on our prototype.
        // Delete them from the master tablepanel.
        Ext.copy(normalGrid, me, me.bothCfgCopy, true);
        Ext.copy(lockedGrid, me, me.bothCfgCopy, true);
        Ext.copy(normalGrid, me, me.normalCfgCopy, true);
        Ext.copy(lockedGrid, me, me.lockedCfgCopy, true);
        Ext.apply(normalGrid, me.normalGridConfig);
        Ext.apply(lockedGrid, me.lockedGridConfig);
        for (i = 0; i < me.normalCfgCopy.length; i++) {
            delete me[me.normalCfgCopy[i]];
        }
        for (i = 0; i < me.lockedCfgCopy.length; i++) {
            delete me[me.lockedCfgCopy[i]];
        }
        me.addStateEvents([
            'lockcolumn',
            'unlockcolumn'
        ]);
        columns = me.processColumns(me.columns || [], lockedGrid);
        lockedGrid.columns = columns.locked;
        // If no locked columns, hide the locked grid
        if (!lockedGrid.columns.items.length) {
            lockedGrid.hidden = true;
        }
        normalGrid.columns = columns.normal;
        if (!normalGrid.columns.items.length) {
            normalGrid.hidden = true;
        }
        // normal grid should flex the rest of the width
        normalGrid.flex = 1;
        // Chain view configs to avoid mutating user's config
        lockedGrid.viewConfig = lockedViewConfig = (lockedViewConfig ? Obj.chain(lockedViewConfig) : {});
        normalGrid.viewConfig = normalViewConfig = (normalViewConfig ? Obj.chain(normalViewConfig) : {});
        lockedViewConfig.loadingUseMsg = false;
        lockedViewConfig.loadMask = false;
        normalViewConfig.loadMask = false;
        if (viewConfig && viewConfig.id) {
            Ext.log.warn('id specified on Lockable viewConfig, it will be shared ' + 'between both views: "' + viewConfig.id + '"');
        }
        Ext.applyIf(lockedViewConfig, viewConfig);
        Ext.applyIf(normalViewConfig, viewConfig);
        // Allow developer to configure the layout.
        // Instantiate the layout so its type can be ascertained.
        if (me.layout === Ext.panel.Table.prototype.layout) {
            me.layout = {
                type: 'hbox',
                align: 'stretch'
            };
        }
        me.getLayout();
        // Sanity check the split config.
        // Only allowed to insert a splitter between the two grids if it's a box layout
        if (me.layout.type === 'border') {
            if (me.split) {
                lockedGrid.split = me.split;
            }
            if (!lockedGrid.region) {
                lockedGrid.region = 'west';
            }
            if (!normalGrid.region) {
                normalGrid.region = 'center';
            }
            me.addCls(Ext.baseCSSPrefix + 'grid-locked-split');
        }
        if (!(me.layout instanceof Ext.layout.container.Box)) {
            me.split = false;
        }
        // The LockingView is a pseudo view which owns the two grids.
        // It listens for store events and relays the calls into each view bracketed
        // by a layout suspension.
        me.view = new Ext.grid.locking.View({
            loadMask: loadMask,
            locked: lockedGrid,
            normal: normalGrid,
            ownerGrid: me
        });
        me.view.relayEvents(me.scrollable, [
            'scroll'
        ]);
        // after creating the locking view we now have Grid instances for both locked and
        // unlocked sides
        lockedGrid = me.lockedGrid;
        normalGrid = me.normalGrid;
        // View has to be moved back into the panel during float
        lockedGrid.on({
            beginfloat: me.onBeginLockedFloat,
            endfloat: me.onEndLockedFloat,
            scope: me
        });
        setWidth = lockedGrid.setWidth;
        // Intercept setWidth here so we can tell the difference between
        // our own calls to setWidth vs user calls
        lockedGrid.setWidth = function() {
            lockedGrid.shrinkWrapColumns = false;
            setWidth.apply(lockedGrid, arguments);
        };
        // Account for initially hidden columns, or user hide of columns in handlers
        // called during grid construction
        if (!lockedGrid.getVisibleColumnManager().getColumns().length) {
            lockedGrid.hide();
        }
        if (!normalGrid.getVisibleColumnManager().getColumns().length) {
            normalGrid.hide();
        }
        // Extract the instantiated views from the locking View.
        // The locking View injects lockingGrid and normalGrid into this lockable panel.
        // This is because during constraction, it must be possible for descendant components
        // to navigate up to the owning lockable panel and then down into either side.
        lockedHeaderCt = lockedGrid.headerCt;
        normalHeaderCt = normalGrid.headerCt;
        // The top grid, and the LockingView both need to have a headerCt which is usable.
        // It is part of their private API that framework code uses when dealing with a grid
        // or grid view
        me.headerCt = me.view.headerCt = new Ext.grid.locking.HeaderContainer(me);
        lockedHeaderCt.lockedCt = true;
        lockedHeaderCt.lockableInjected = true;
        normalHeaderCt.lockableInjected = true;
        lockedHeaderCt.on({
            add: me.delaySyncLockedWidth,
            remove: me.delaySyncLockedWidth,
            columnshow: me.delaySyncLockedWidth,
            columnhide: me.delaySyncLockedWidth,
            sortchange: me.onLockedHeaderSortChange,
            columnresize: me.delaySyncLockedWidth,
            scope: me
        });
        normalHeaderCt.on({
            add: me.delaySyncLockedWidth,
            remove: me.delaySyncLockedWidth,
            columnshow: me.delaySyncLockedWidth,
            columnhide: me.delaySyncLockedWidth,
            sortchange: me.onNormalHeaderSortChange,
            scope: me
        });
        me.modifyHeaderCt();
        me.items = [
            lockedGrid
        ];
        if (me.split) {
            me.addCls(Ext.baseCSSPrefix + 'grid-locked-split');
            me.items[1] = Ext.apply({
                xtype: 'splitter'
            }, me.split);
        }
        me.items.push(normalGrid);
        me.relayHeaderCtEvents(lockedHeaderCt);
        me.relayHeaderCtEvents(normalHeaderCt);
        // The top level Lockable container does not get bound to the store, so we need
        // to programatically add the relayer so that The filterchange state event is fired.
        //
        // TreePanel also relays the beforeload and load events, so 
        me.storeRelayers = me.relayEvents(store, [
            /**
             * @event filterchange
             * @inheritdoc Ext.data.Store#filterchange
             */
            'filterchange',
            /**
             * @event groupchange
             * @inheritdoc Ext.data.Store#groupchange
             */
            'groupchange',
            /**
             * @event beforeload
             * @inheritdoc Ext.data.Store#beforeload
             */
            'beforeload',
            /**
             * @event load
             * @inheritdoc Ext.data.Store#load
             */
            'load'
        ]);
        // Only need to relay from the normalGrid. Since it's created after the lockedGrid,
        // we can be confident to only listen to it.
        me.gridRelayers = me.relayEvents(normalGrid, [
            /**
             * @event viewready
             * @inheritdoc Ext.panel.Table#viewready
             */
            'viewready'
        ]);
    },
    afterInjectLockable: function() {
        var me = this;
        // Here we should set the maskElement to scrollContainer so the loadMask cover both views
        // but not the headers and grid title bar.
        me.maskElement = 'scrollContainer';
        if (me.disableOnRender) {
            me.on('afterrender', function() {
                me.unmask();
            }, {
                single: true
            });
        }
        delete me.lockedGrid.$initParent;
        delete me.normalGrid.$initParent;
    },
    syncLockableHeaderVisibility: function() {
        var me = this,
            hideHeaders = me.hideHeaders,
            locked = me.lockedGrid,
            normal = me.normalGrid;
        if (hideHeaders === null) {
            hideHeaders = locked.shouldAutoHideHeaders() && normal.shouldAutoHideHeaders();
        }
        locked.hideHeaders = normal.hideHeaders = hideHeaders;
        locked.syncHeaderVisibility();
        normal.syncHeaderVisibility();
    },
    getLockingViewConfig: function() {
        return {
            xclass: 'Ext.grid.locking.View',
            locked: this.lockedGrid,
            normal: this.normalGrid,
            panel: this
        };
    },
    onBeginLockedFloat: function(locked) {
        var el = locked.getContentTarget().dom,
            lockedHeaderCt = this.lockedGrid.headerCt,
            normalHeaderCt = this.normalGrid.headerCt,
            headerCtHeight = Math.max(normalHeaderCt.getHeight(), lockedHeaderCt.getHeight());
        // The two layouts are seperated and no longer share stretchmax height data upon
        // layout, so for the duration of float, force them to be at least the current
        // matching height.
        lockedHeaderCt.minHeight = headerCtHeight;
        normalHeaderCt.minHeight = headerCtHeight;
        locked.el.addCls(Ext.panel.Panel.floatCls);
        // Move view into the grid unless it's already there.
        // We fire a beginfloat event when expanding or collapsing from 
        // floated out state.
        if (el.firstChild !== locked.view.el.dom) {
            el.appendChild(locked.view.el.dom);
        }
        locked.body.dom.scrollTop = this.getScrollable().getPosition().y;
    },
    onEndLockedFloat: function() {
        var locked = this.lockedGrid;
        // The two headerCts are connected now, allow them to stretchmax each other
        if (locked.collapsed) {
            locked.el.removeCls(Ext.panel.Panel.floatCls);
        } else {
            this.lockedGrid.headerCt.minHeight = this.normalGrid.headerCt.minHeight = null;
        }
        this.lockedScrollbarClipper.appendChild(locked.view.el.dom);
        this.doSyncLockableLayout();
    },
    beforeLayout: function() {
        var me = this,
            lockedGrid = me.lockedGrid,
            normalGrid = me.normalGrid,
            totalColumnWidth;
        if (lockedGrid && normalGrid) {
            // The locked side of a grid, if it is shrinkwrapping fixed size columns,
            // must take into account the column widths plus the border widths of the grid element
            // and the headerCt element.
            // This must happen at this late stage so that all relevant classes are added
            // which affect what borders are applied to what elements.
            if (lockedGrid.getSizeModel().width.shrinkWrap) {
                lockedGrid.gridPanelBorderWidth = lockedGrid.el.getBorderWidth('lr');
                lockedGrid.shrinkWrapColumns = true;
            }
            if (lockedGrid.shrinkWrapColumns) {
                totalColumnWidth = lockedGrid.headerCt.getTableWidth();
                if (isNaN(totalColumnWidth)) {
                    Ext.raise("Locked columns in an unsized locked side do NOT support " + "a flex width.");
                }
                lockedGrid.setWidth(totalColumnWidth + lockedGrid.gridPanelBorderWidth);
                // setWidth will clear shrinkWrapColumns, so force it again here
                lockedGrid.shrinkWrapColumns = true;
            }
            if (!me.scrollContainer) {
                me.initScrollContainer();
            }
            me.lastScrollPos = Ext.clone(me.getScrollable().getPosition());
            // Undo margin styles set by afterLayout
            lockedGrid.view.el.setStyle('margin-bottom', '');
            normalGrid.view.el.setStyle('margin-bottom', '');
        }
    },
    syncLockableLayout: function() {
        var me = this;
        // This is called directly from child TableView#afterComponentLayout
        // So we might get two calls if both are visible, and both lay out.
        // Schedule a single sync on the tail end of the current layout.
        if (!me.afterLayoutListener) {
            me.afterLayoutListener = Ext.on({
                afterlayout: me.doSyncLockableLayout,
                scope: me,
                single: true
            });
        }
    },
    doSyncLockableLayout: function() {
        var me = this,
            collapseExpand = me.isCollapsingOrExpanding,
            lockedGrid = me.lockedGrid,
            normalGrid = me.normalGrid,
            lockedViewEl, normalViewEl, lockedViewRegion, normalViewRegion, scrollbarSize, scrollbarWidth, scrollbarHeight, normalViewWidth, normalViewX, hasVerticalScrollbar, hasHorizontalScrollbar, scrollContainerHeight, scrollBodyHeight, lockedScrollbar, normalScrollbar, scrollbarVisibleCls, scrollHeight, lockedGridVisible, normalGridVisible, scrollBodyDom, viewRegion, scrollerElHeight, scrollable;
        me.afterLayoutListener = null;
        if (collapseExpand) {
            // Expand
            if (collapseExpand === 2) {
                me.on('expand', 'doSyncLockableLayout', me, {
                    single: true
                });
            }
            return;
        }
        /* eslint-disable max-len */
        if (lockedGrid && normalGrid) {
            lockedGridVisible = lockedGrid.isVisible(true) && !lockedGrid.collapsed;
            normalGridVisible = normalGrid.isVisible(true);
            lockedViewEl = lockedGrid.view.el;
            normalViewEl = normalGrid.view.el;
            scrollBodyDom = me.scrollBody.dom;
            lockedViewRegion = lockedGridVisible ? lockedGrid.body.getRegion(true) : new Ext.util.Region(0, 0, 0, 0);
            normalViewRegion = normalGridVisible ? normalGrid.body.getRegion(true) : new Ext.util.Region(0, 0, 0, 0);
            scrollbarSize = Ext.scrollbar.size();
            scrollbarWidth = scrollbarSize.width;
            scrollbarHeight = scrollerElHeight = scrollbarSize.height;
            normalViewWidth = normalGridVisible ? normalViewRegion.width : 0;
            normalViewX = lockedGridVisible ? (normalGridVisible ? normalViewRegion.x - lockedViewRegion.x : lockedViewRegion.width) : 0;
            hasHorizontalScrollbar = (normalGrid.headerCt.tooNarrow || lockedGrid.headerCt.tooNarrow) ? scrollbarHeight : 0;
            scrollContainerHeight = normalViewRegion.height || lockedViewRegion.height;
            scrollBodyHeight = scrollContainerHeight;
            lockedScrollbar = me.lockedScrollbar;
            normalScrollbar = me.normalScrollbar;
            scrollbarVisibleCls = me.scrollbarVisibleCls;
            scrollable = me.getScrollable();
            // EXTJS-23301 IE10/11 does not allow an overflowing element to scroll
            // if the element height is the same as the scrollbar height. This
            // affects the horizontal normal scrollbar only as the vertical
            // scrollbar container will always have a width larger due to content.
            if (Ext.supports.CannotScrollExactHeight) {
                scrollerElHeight += 1;
            }
            if (hasHorizontalScrollbar) {
                lockedViewEl.setStyle('margin-bottom', -scrollbarHeight + 'px');
                normalViewEl.setStyle('margin-bottom', -scrollbarHeight + 'px');
                scrollBodyHeight -= scrollbarHeight;
                if (lockedGridVisible && lockedGrid.view.body.dom) {
                    me.lockedScrollbarScroller.setSize({
                        x: lockedGrid.headerCt.getTableWidth()
                    });
                }
                if (normalGrid.view.body.dom) {
                    me.normalScrollbarScroller.setSize({
                        x: normalGrid.headerCt.getTableWidth()
                    });
                }
            }
            me.scrollBody.setHeight(scrollBodyHeight);
            lockedViewEl.dom.style.height = normalViewEl.dom.style.height = '';
            scrollHeight = (me.scrollable.getSize().y + hasHorizontalScrollbar);
            normalGrid.view.stretchHeight(scrollHeight);
            lockedGrid.view.stretchHeight(scrollHeight);
            hasVerticalScrollbar = scrollbarWidth && scrollBodyDom.scrollHeight > scrollBodyDom.clientHeight;
            if (hasVerticalScrollbar && normalViewWidth) {
                normalViewWidth -= scrollbarWidth;
                normalViewEl.setStyle('width', normalViewWidth + 'px');
            }
            lockedScrollbar.toggleCls(scrollbarVisibleCls, lockedGridVisible && !!hasHorizontalScrollbar);
            normalScrollbar.toggleCls(scrollbarVisibleCls, !!hasHorizontalScrollbar);
            // Floated from collapsed views must overlay. THis raises them up.
            me.normalScrollbarClipper.toggleCls(me.scrollbarClipperCls + '-floated', !!me.normalGrid.floatedFromCollapse);
            me.normalScrollbar.toggleCls(me.scrollbarCls + '-floated', !!me.normalGrid.floatedFromCollapse);
            me.lockedScrollbarClipper.toggleCls(me.scrollbarClipperCls + '-floated', !!me.lockedGrid.floatedFromCollapse);
            me.lockedScrollbar.toggleCls(me.scrollbarCls + '-floated', !!me.lockedGrid.floatedFromCollapse);
            lockedScrollbar.setSize(me.lockedScrollbarClipper.dom.offsetWidth, scrollerElHeight);
            normalScrollbar.setSize(normalViewWidth, scrollerElHeight);
            me.setNormalScrollerX(normalViewX);
            if (lockedGridVisible && normalGridVisible) {
                viewRegion = lockedViewRegion.union(normalViewRegion);
            } else if (lockedGridVisible) {
                viewRegion = lockedViewRegion;
            } else {
                viewRegion = normalViewRegion;
            }
            me.scrollContainer.setBox(viewRegion);
            me.onSyncLockableLayout(hasVerticalScrollbar, viewRegion.width);
            // We should only scroll if necessary 
            if (!Ext.Object.equals(scrollable.getPosition(), me.lastScrollPos)) {
                scrollable.scrollTo(me.lastScrollPos);
            }
        }
    },
    /* eslint-enable max-len */
    onSyncLockableLayout: Ext.emptyFn,
    setNormalScrollerX: function(x) {
        this.normalScrollbar.setLocalX(x);
        this.normalScrollbarClipper.setLocalX(x);
    },
    getScrollExtraCls: function() {
        return '';
    },
    initScrollContainer: function() {
        var me = this,
            extraCls = me.getScrollExtraCls(),
            scrollContainer = me.scrollContainer = me.body.insertFirst({
                cls: [
                    me.scrollContainerCls,
                    extraCls
                ]
            }),
            scrollBody = me.scrollBody = scrollContainer.appendChild({
                cls: me.scrollBodyCls
            }),
            lockedScrollbar = me.lockedScrollbar = scrollContainer.appendChild({
                cls: [
                    me.scrollbarCls,
                    me.scrollbarCls + '-locked',
                    extraCls
                ]
            }),
            normalScrollbar = me.normalScrollbar = scrollContainer.appendChild({
                cls: [
                    me.scrollbarCls,
                    extraCls
                ]
            }),
            lockedView = me.lockedGrid.view,
            normalView = me.normalGrid.view,
            lockedScroller = lockedView.getScrollable(),
            normalScroller = normalView.getScrollable(),
            Scroller = Ext.scroll.Scroller,
            lockedScrollbarScroller, normalScrollbarScroller, lockedScrollbarClipper, normalScrollbarClipper;
        lockedView.stretchHeight(0);
        normalView.stretchHeight(0);
        me.scrollable.setConfig({
            element: scrollBody,
            lockedScroller: lockedScroller,
            normalScroller: normalScroller
        });
        lockedScrollbarClipper = me.lockedScrollbarClipper = scrollBody.appendChild({
            cls: [
                me.scrollbarClipperCls,
                me.scrollbarClipperCls + '-locked',
                extraCls
            ]
        });
        normalScrollbarClipper = me.normalScrollbarClipper = scrollBody.appendChild({
            cls: [
                me.scrollbarClipperCls,
                extraCls
            ]
        });
        lockedScrollbarClipper.appendChild(lockedView.el);
        normalScrollbarClipper.appendChild(normalView.el);
        // We just moved the view elements into a containing element that is not the same
        // as their container's target element (grid body).  Setting the ignoreDomPosition
        // flag instructs the layout system not to move them back.
        lockedView.ignoreDomPosition = true;
        normalView.ignoreDomPosition = true;
        lockedScrollbarScroller = me.lockedScrollbarScroller = new Scroller({
            element: lockedScrollbar,
            x: 'scroll',
            y: false,
            rtl: lockedScroller.getRtl && lockedScroller.getRtl()
        });
        normalScrollbarScroller = me.normalScrollbarScroller = new Scroller({
            element: normalScrollbar,
            x: 'scroll',
            y: false,
            rtl: normalScroller.getRtl && normalScroller.getRtl()
        });
        me.initScrollers();
        lockedScrollbarScroller.addPartner(lockedScroller, 'x');
        normalScrollbarScroller.addPartner(normalScroller, 'x');
        // Tell the lockable.View that it has been rendered.
        me.view.onPanelRender(scrollBody);
    },
    initScrollers: Ext.emptyFn,
    processColumns: function(columns, lockedGrid) {
        // split apart normal and locked
        var me = this,
            i, len, column,
            cp = new Ext.grid.header.Container({
                "$initParent": me
            }),
            lockedHeaders = [],
            normalHeaders = [],
            lockedHeaderCt = {
                itemId: 'lockedHeaderCt',
                stretchMaxPartner: '^^>>#normalHeaderCt',
                items: lockedHeaders
            },
            normalHeaderCt = {
                itemId: 'normalHeaderCt',
                stretchMaxPartner: '^^>>#lockedHeaderCt',
                items: normalHeaders
            },
            result = {
                locked: lockedHeaderCt,
                normal: normalHeaderCt
            },
            copy;
        // In case they specified a config object with items...
        if (Ext.isObject(columns)) {
            Ext.applyIf(lockedHeaderCt, columns);
            Ext.applyIf(normalHeaderCt, columns);
            copy = Ext.apply({}, columns);
            delete copy.items;
            Ext.apply(cp, copy);
            columns = columns.items;
        }
        // Treat the column header as though we're just creating an instance, since this
        // doesn't follow the normal column creation pattern
        cp.constructing = true;
        for (i = 0 , len = columns.length; i < len; ++i) {
            column = columns[i];
            // Use the HeaderContainer object to correctly configure and create the column.
            // MUST instantiate now because the locked or autoLock config which we read here
            // might be in the prototype.
            // MUST use a Container instance so that defaults from an object columns config
            // get applied.
            if (!column.isComponent) {
                column = cp.applyDefaults(column);
                column.$initParent = cp;
                column = cp.lookupComponent(column);
                delete column.$initParent;
            }
            // mark the column as processed so that the locked attribute does not
            // trigger the locked subgrid to try to become a split lockable grid itself.
            column.processed = true;
            if (column.locked || column.autoLock) {
                lockedHeaders.push(column);
            } else {
                normalHeaders.push(column);
            }
        }
        me.fireEvent('processcolumns', me, lockedHeaders, normalHeaders);
        cp.destroy();
        return result;
    },
    ensureLockedVisible: function(record, options) {
        var column = options && options.column,
            lockedGrid = this.lockedGrid,
            // eslint-disable-next-line max-len
            grid = column ? column.getView().ownerCt : lockedGrid.isVisible() ? lockedGrid : this.normalGrid;
        // Just ask the appropriate grid to scroll. There is only one Y scroller.
        grid.ensureVisible.apply(grid, arguments);
    },
    /**
     * Synchronizes the row heights between the locked and non locked portion of the grid for each
     * row. If one row is smaller than the other, the height will be increased to match
     * the larger one.
     */
    syncRowHeights: function() {
        // This is now called on animationFrame. It may have been destroyed in the interval.
        if (!this.destroyed) {
            // eslint-disable-next-line vars-on-top
            var me = this,
                normalView = me.normalGrid.getView(),
                lockedView = me.lockedGrid.getView(),
                // These will reset any forced height styles from the last sync
                normalSync = normalView.syncRowHeightBegin(),
                lockedSync = lockedView.syncRowHeightBegin(),
                scrollTop;
            // Now bulk measure everything
            normalView.syncRowHeightMeasure(normalSync);
            lockedView.syncRowHeightMeasure(lockedSync);
            // Now write out all the explicit heights we need to sync up
            normalView.syncRowHeightFinish(normalSync, lockedSync);
            lockedView.syncRowHeightFinish(lockedSync, normalSync);
            // Synchronize the scrollTop positions of the two views
            scrollTop = normalView.getScrollY();
            lockedView.setScrollY(scrollTop);
            me.syncRowHeightOnNextLayout = false;
        }
    },
    // inject Lock and Unlock text
    // Hide/show Lock/Unlock options
    modifyHeaderCt: function() {
        var me = this;
        me.lockedGrid.headerCt.getMenuItems = me.getMenuItems(me.lockedGrid.headerCt.getMenuItems, true);
        me.normalGrid.headerCt.getMenuItems = me.getMenuItems(me.normalGrid.headerCt.getMenuItems, false);
        me.lockedGrid.headerCt.showMenuBy = Ext.Function.createInterceptor(me.lockedGrid.headerCt.showMenuBy, me.showMenuBy);
        me.normalGrid.headerCt.showMenuBy = Ext.Function.createInterceptor(me.normalGrid.headerCt.showMenuBy, me.showMenuBy);
    },
    onUnlockMenuClick: function() {
        this.unlock();
    },
    onLockMenuClick: function() {
        this.lock();
    },
    showMenuBy: function(clickEvent, t, header) {
        var menu = this.getMenu(),
            unlockItem = menu.down('#unlockItem'),
            lockItem = menu.down('#lockItem'),
            sep = unlockItem.prev();
        if (header.lockable === false) {
            sep.hide();
            unlockItem.hide();
            lockItem.hide();
        } else {
            sep.show();
            unlockItem.show();
            lockItem.show();
            if (!unlockItem.initialConfig.disabled) {
                unlockItem.setDisabled(header.lockable === false);
            }
            if (!lockItem.initialConfig.disabled) {
                lockItem.setDisabled(!header.isLockable());
            }
        }
    },
    getMenuItems: function(getMenuItems, locked) {
        var me = this,
            unlockText = me.unlockText,
            lockText = me.lockText,
            unlockCls = Ext.baseCSSPrefix + 'hmenu-unlock',
            lockCls = Ext.baseCSSPrefix + 'hmenu-lock',
            unlockHandler = me.onUnlockMenuClick.bind(me),
            lockHandler = me.onLockMenuClick.bind(me);
        // runs in the scope of headerCt
        return function() {
            // We cannot use the method from HeaderContainer's prototype here
            // because other plugins or features may already have injected an implementation
            var o = getMenuItems.call(this);
            o.push('-', {
                itemId: 'unlockItem',
                iconCls: unlockCls,
                text: unlockText,
                handler: unlockHandler,
                disabled: !locked
            });
            o.push({
                itemId: 'lockItem',
                iconCls: lockCls,
                text: lockText,
                handler: lockHandler,
                disabled: locked
            });
            return o;
        };
    },
    syncTaskDelay: 1,
    delaySyncLockedWidth: function() {
        var me = this,
            task = me.syncLockedWidthTask || (me.syncLockedWidthTask = new Ext.util.DelayedTask(me.syncLockedWidth, me));
        if (me.reconfiguring) {
            return;
        }
        // Do not delay if we are in suspension or configured to not delay
        if (!Ext.Component.layoutSuspendCount || me.syncTaskDelay === 0) {
            me.syncLockedWidth();
        } else {
            task.delay(1);
        }
    },
    /**
     * @private
     * Updates the overall view after columns have been resized, or moved from
     * the locked to unlocked side or vice-versa.
     *
     * If all columns are removed from either side, that side must be hidden, and the
     * sole remaining column owning grid then becomes *the* grid. It must flex to occupy the
     * whole of the locking view. And it must also allow scrolling.
     *
     * If columns are shared between the two sides, the *locked* grid shrinkwraps the
     * width of the visible locked columns while the normal grid flexes in what space remains.
     *
     * @return {Object} A pair of flags indicating which views need to be cleared then refreshed.
     * this contains two properties, `locked` and `normal` which are `true` if the view needs
     * to be cleared and refreshed.
     */
    syncLockedWidth: function() {
        var me = this,
            rendered = me.rendered,
            locked = me.lockedGrid,
            normal = me.normalGrid,
            lockedColCount = locked.getVisibleColumnManager().getColumns().length,
            normalColCount = normal.getVisibleColumnManager().getColumns().length,
            task = me.syncLockedWidthTask;
        // If we are called directly, veto any existing task.
        if (task) {
            task.cancel();
        }
        if (me.reconfiguring) {
            return;
        }
        Ext.suspendLayouts();
        // If there are still visible normal columns, then the normal grid will flex
        // while we effectively shrinkwrap the width of the locked columns
        if (normalColCount) {
            normal.show();
            if (lockedColCount) {
                // Revert locked grid to original region now it's not the only child grid.
                if (me.layout.type === 'border') {
                    locked.region = locked.initialConfig.region;
                }
                // The locked grid shrinkwraps the total column width while the normal grid
                // flexes in what remains UNLESS it has been set to forceFit
                if (rendered && locked.shrinkWrapColumns && !locked.headerCt.forceFit) {
                    delete locked.flex;
                    // Just set the property here and update the layout.
                    // Size settings assume it's NOT the layout root.
                    // If the locked has been floated, it might well be!
                    // Use gridPanelBorderWidth as measured in Ext.grid.ColumnLayout#beginLayout
                    // TODO: Use shrinkWrapDock on the locked grid when it works.
                    locked.width = locked.headerCt.getTableWidth() + locked.gridPanelBorderWidth;
                    locked.updateLayout();
                }
                locked.addCls(me.lockedGridCls);
                locked.show();
                if (locked.split) {
                    me.child('splitter').show();
                    me.addCls(Ext.baseCSSPrefix + 'grid-locked-split');
                }
            } else {
                // Hide before clearing to avoid DOM layout from clearing
                // the content and to avoid scroll syncing. TablePanel
                // disables scroll syncing on hide.
                locked.hide();
                // No visible locked columns: hide the locked grid
                // We also need to trigger a clearViewEl to clear out any
                // old dom nodes
                if (rendered) {
                    locked.getView().clearViewEl(true);
                }
                if (locked.split) {
                    me.child('splitter').hide();
                    me.removeCls(Ext.baseCSSPrefix + 'grid-locked-split');
                }
            }
        } else // There are no normal grid columns. The "locked" grid has to be *the*
        // grid, and cannot have a shrinkwrapped width, but must flex the entire width.
        {
            normal.hide();
            // The locked now becomes *the* grid and has to flex to occupy the full view width
            delete locked.width;
            if (me.layout.type === 'border') {
                locked.region = 'center';
                normal.region = 'west';
            } else {
                locked.flex = 1;
            }
            locked.removeCls(me.lockedGridCls);
            locked.show();
        }
        Ext.resumeLayouts(true);
        // Flag object indicating which views need to be cleared and refreshed.
        return {
            locked: !!lockedColCount,
            normal: !!normalColCount
        };
    },
    onLockedHeaderSortChange: Ext.emptyFn,
    onNormalHeaderSortChange: Ext.emptyFn,
    // going from unlocked section to locked
    /**
     * Locks the activeHeader as determined by which menu is open OR a header
     * as specified.
     * @param {Ext.grid.column.Column} [activeHd] Header to unlock from the locked section.
     * Defaults to the header which has the menu open currently.
     * @param {Number} [toIdx] The index to move the unlocked header to.
     * Defaults to appending as the last item.
     * @param toCt
     * @private
     */
    lock: function(activeHd, toIdx, toCt) {
        var me = this,
            normalGrid = me.normalGrid,
            lockedGrid = me.lockedGrid,
            normalView = normalGrid.view,
            lockedView = lockedGrid.view,
            normalScroller = normalView.getScrollable(),
            lockedScroller = lockedView.getScrollable(),
            normalHCt = normalGrid.headerCt,
            refreshFlags, ownerCt, lbr;
        activeHd = activeHd || normalHCt.getMenu().activeHeader;
        activeHd.unlockedWidth = activeHd.width;
        // If moving a flexed header back into a side where we can't know
        // whether the flex value will be invalid, revert it either to
        // its original width or actual width.
        if (activeHd.flex) {
            if (activeHd.lockedWidth) {
                activeHd.width = activeHd.lockedWidth;
                activeHd.lockedWidth = null;
            } else {
                activeHd.width = activeHd.lastBox.width;
            }
            activeHd.flex = null;
        }
        toCt = toCt || lockedGrid.headerCt;
        ownerCt = activeHd.ownerCt;
        // isLockable will test for making the locked side too wide.
        // The header we're locking may be to be added, and have no ownerCt.
        // For instance, a checkbox column being moved into the correct side
        if (ownerCt && !activeHd.isLockable()) {
            return;
        }
        Ext.suspendLayouts();
        if (normalScroller) {
            normalScroller.suspendPartnerSync();
            lockedScroller.suspendPartnerSync();
        }
        // If hidden, we need to show it now or the locked headerCt's VisibleColumnManager
        // may be out of sync as headers are only added to a visible manager if they are not
        // explicity hidden or hierarchically hidden.
        if (lockedGrid.hidden) {
            // The locked side's BufferedRenderer has never has a resize passed in,
            // so its viewSize will be the default viewSize, out of sync with the normal side.
            // Synchronize the viewSize before the two sides are refreshed.
            if (!lockedGrid.componentLayoutCounter) {
                lockedGrid.height = normalGrid.lastBox.height;
                lbr = lockedView.bufferedRenderer;
                if (lbr) {
                    lbr.rowHeight = normalView.bufferedRenderer.rowHeight;
                    lbr.onViewResize(lockedView, 0, normalGrid.body.lastBox.height);
                }
            }
            lockedGrid.show();
        }
        // TablePanel#onHeadersChanged does not respond if reconfiguring set.
        // We programatically refresh views which need it below.
        lockedGrid.reconfiguring = normalGrid.reconfiguring = true;
        // Keep the column in the hierarchy during the move.
        // So that grid.isAncestor(column) still returns true, and SpreadsheetModel
        // does not deselect
        activeHd.ownerCmp = activeHd.ownerCt;
        activeHd.locked = true;
        // Flag to the locked column add listener to do nothing
        if (Ext.isDefined(toIdx)) {
            toCt.insert(toIdx, activeHd);
        } else {
            toCt.add(activeHd);
        }
        lockedGrid.reconfiguring = normalGrid.reconfiguring = false;
        activeHd.ownerCmp = null;
        activeHd.rootHeaderCt = null;
        activeHd.view = lockedView;
        refreshFlags = me.syncLockedWidth();
        // Clear both views first so that any widgets are cached
        // before reuse. If we refresh the grid which just had a widget column added
        // first, the clear of the view which had the widget column in removes the widgets
        // from their new place.
        if (refreshFlags.locked) {
            lockedView.clearViewEl(true);
        }
        if (refreshFlags.normal) {
            normalView.clearViewEl(true);
        }
        // Refresh locked view second, so that if it's refreshing from empty (can start
        // with no locked columns), the buffered renderer can look to its partner
        // to get the correct range to refresh.
        normalGrid.getView().refreshNeeded = refreshFlags.normal;
        lockedGrid.getView().refreshNeeded = refreshFlags.locked;
        activeHd.onLock(activeHd);
        me.fireEvent('lockcolumn', me, activeHd);
        Ext.resumeLayouts(true);
        if (normalScroller) {
            normalScroller.resumePartnerSync(true);
            lockedScroller.resumePartnerSync();
        }
    },
    // going from locked section to unlocked
    /**
     * Unlocks the activeHeader as determined by which menu is open OR a header
     * as specified.
     * @param {Ext.grid.column.Column} [activeHd] Header to unlock from the locked section.
     * Defaults to the header which has the menu open currently.
     * @param {Number} [toIdx=0] The index to move the unlocked header to.
     * @param toCt
     * @private
     */
    unlock: function(activeHd, toIdx, toCt) {
        var me = this,
            normalGrid = me.normalGrid,
            lockedGrid = me.lockedGrid,
            normalView = normalGrid.view,
            lockedView = lockedGrid.view,
            startIndex = normalView.all.startIndex,
            lockedHCt = lockedGrid.headerCt,
            refreshFlags;
        // Unlocking; user expectation is that the unlocked column is inserted at the beginning.
        if (!Ext.isDefined(toIdx)) {
            toIdx = 0;
        }
        activeHd = activeHd || lockedHCt.getMenu().activeHeader;
        activeHd.lockedWidth = activeHd.width;
        // If moving a flexed header back into a side where we can't know
        // whether the flex value will be invalid, revert it either to
        // its original width or actual width.
        if (activeHd.flex) {
            if (activeHd.unlockedWidth) {
                activeHd.width = activeHd.unlockedWidth;
                activeHd.unlockedWidth = null;
            } else {
                activeHd.width = activeHd.lastBox.width;
            }
            activeHd.flex = null;
        }
        toCt = toCt || normalGrid.headerCt;
        Ext.suspendLayouts();
        // TablePanel#onHeadersChanged does not respond if reconfiguring set.
        // We programatically refresh views which need it below.
        lockedGrid.reconfiguring = normalGrid.reconfiguring = true;
        // Keep the column in the hierarchy during the move.
        // So that grid.isAncestor(column) still returns true, and SpreadsheetModel
        // does not deselect
        activeHd.ownerCmp = activeHd.ownerCt;
        if (activeHd.ownerCt) {
            activeHd.ownerCt.remove(activeHd, false);
        }
        activeHd.locked = false;
        toCt.insert(toIdx, activeHd);
        lockedGrid.reconfiguring = normalGrid.reconfiguring = false;
        activeHd.ownerCmp = null;
        activeHd.rootHeaderCt = null;
        activeHd.view = normalView;
        // syncLockedWidth returns visible column counts for both grids.
        // only refresh what needs refreshing
        refreshFlags = me.syncLockedWidth();
        // Clear both views first so that any widgets are cached
        // before reuse. If we refresh the grid which just had a widget column added
        // first, the clear of the view which had the widget column in removes the widgets
        // from their new place.
        if (refreshFlags.locked) {
            lockedView.clearViewEl(true);
        }
        if (refreshFlags.normal) {
            normalView.clearViewEl(true);
        }
        // Refresh locked view second, so that if it's refreshing from empty (can start
        // with no locked columns), the buffered renderer can look to its partner to get
        // the correct range to refresh.
        if (refreshFlags.normal) {
            normalGrid.getView().refreshView(startIndex);
        }
        if (refreshFlags.locked) {
            lockedGrid.getView().refreshView(startIndex);
        }
        activeHd.onUnlock(activeHd);
        me.fireEvent('unlockcolumn', me, activeHd);
        Ext.resumeLayouts(true);
    },
    /**
     * @private
     */
    reconfigureLockable: function(store, columns, allowUnbind) {
        // we want to totally override the reconfigure behaviour here,
        // since we're creating 2 sub-grids
        var me = this,
            oldStore = me.store,
            lockedGrid = me.lockedGrid,
            normalGrid = me.normalGrid,
            view, loadMask;
        if (!store && allowUnbind) {
            store = Ext.StoreManager.lookup('ext-empty-store');
        }
        // Note that we need to process the store first in case one or more passed columns
        // (if there are any) have active gridfilters with values which would filter
        // the currently-bound store.
        if (store && store !== oldStore) {
            store = Ext.data.StoreManager.lookup(store);
            me.store = store;
            lockedGrid.view.blockRefresh = normalGrid.view.blockRefresh = true;
            lockedGrid.bindStore(store);
            // Subsidiary views have their bindStore changed because they must not
            // bind listeners themselves. This view listens and relays calls to each view.
            // BUT the dataSource and store properties must be set
            view = lockedGrid.view;
            view.store = store;
            // If the dataSource being used by the View is *not* a FeatureStore
            // (a modified view of the base Store injected by a Feature)
            // Then we promote the store to be the dataSource.
            // If it was a FeatureStore, then it must not be changed. A FeatureStore is mutated
            // by the Feature to respond to changes in the underlying Store.
            if (!view.dataSource.isFeatureStore) {
                view.dataSource = store;
            }
            if (view.bufferedRenderer) {
                view.bufferedRenderer.bindStore(store);
            }
            normalGrid.bindStore(store);
            view = normalGrid.view;
            view.store = store;
            // If the dataSource being used by the View is *not* a FeatureStore
            // (a modified view of the base Store injected by a Feature)
            // Then we promote the store to be the dataSource.
            // If it was a FeatureStore, then it must not be changed. A FeatureStore is mutated
            // by the Feature to respond to changes in the underlying Store.
            if (!view.dataSource.isFeatureStore) {
                view.dataSource = store;
            }
            if (view.bufferedRenderer) {
                view.bufferedRenderer.bindStore(store);
            }
            me.view.store = store;
            // binding mask to new store
            loadMask = me.view.loadMask;
            if (loadMask && loadMask.isLoadMask) {
                loadMask.bindStore(store);
            }
            me.view.bindStore(normalGrid.view.dataSource, false, 'dataSource');
            lockedGrid.view.blockRefresh = normalGrid.view.blockRefresh = false;
        }
        if (columns) {
            // Both grids must not react to the headers being changed
            // (See panel/Table#onHeadersChanged)
            lockedGrid.reconfiguring = normalGrid.reconfiguring = true;
            lockedGrid.headerCt.removeAll();
            normalGrid.headerCt.removeAll();
            columns = me.processColumns(columns, lockedGrid);
            // Flag to the locked column add listener to do nothing
            lockedGrid.headerCt.add(columns.locked.items);
            normalGrid.headerCt.add(columns.normal.items);
            lockedGrid.reconfiguring = normalGrid.reconfiguring = false;
            // Ensure locked grid is set up correctly with correct width and bottom border,
            // and that both grids' visibility and scrollability status is correct
            me.syncLockedWidth();
        }
        me.refreshCounter = normalGrid.view.refreshCounter;
    },
    afterReconfigureLockable: function() {
        // Ensure width are set up, and visibility of sides are synced with whether
        // they have columns or not.
        this.syncLockedWidth();
        // If the counter hasn't changed since where we saved it previously, we haven't refreshed,
        // so kick it off now.
        if (this.refreshCounter === this.normalGrid.getView().refreshCounter) {
            this.view.refreshView();
        }
    },
    constructLockableFeatures: function() {
        var features = this.features,
            feature, featureClone, lockedFeatures, normalFeatures, i, len;
        if (features) {
            if (!Ext.isArray(features)) {
                features = [
                    features
                ];
            }
            lockedFeatures = [];
            normalFeatures = [];
            for (i = 0 , len = features.length; i < len; i++) {
                feature = features[i];
                if (!feature.isFeature) {
                    feature = Ext.create('feature.' + feature.ftype, feature);
                }
                switch (feature.lockableScope) {
                    case 'locked':
                        lockedFeatures.push(feature);
                        break;
                    case 'normal':
                        normalFeatures.push(feature);
                        break;
                    default:
                        feature.lockableScope = 'both';
                        lockedFeatures.push(feature);
                        normalFeatures.push(featureClone = feature.clone());
                        // When cloned to either side, each gets a "lockingPartner"
                        // reference to the other
                        featureClone.lockingPartner = feature;
                        feature.lockingPartner = featureClone;
                }
            }
        }
        return {
            normalFeatures: normalFeatures,
            lockedFeatures: lockedFeatures
        };
    },
    constructLockablePlugins: function() {
        var plugins = this.plugins,
            plugin, normalPlugin, lockedPlugin, topPlugins, lockedPlugins, normalPlugins, lockableScope, pluginCls, i, len;
        if (plugins) {
            if (!Ext.isArray(plugins)) {
                plugins = [
                    plugins
                ];
            }
            topPlugins = [];
            lockedPlugins = [];
            normalPlugins = [];
            for (i = 0 , len = plugins.length; i < len; i++) {
                plugin = plugins[i];
                // Plugin will most likely already have been instantiated by the Component
                // constructor
                if (plugin.init) {
                    lockableScope = plugin.lockableScope;
                } else // If not, it's because of late addition through a subclass's initComponent
                // implementation, so we must ascertain the lockableScope directly from the class.
                {
                    pluginCls = plugin.ptype ? Ext.ClassManager.getByAlias(('plugin.' + plugin.ptype)) : Ext.ClassManager.get(plugin.xclass);
                    lockableScope = pluginCls.prototype.lockableScope;
                }
                switch (lockableScope) {
                    case 'both':
                        lockedPlugins.push(lockedPlugin = plugin.clonePlugin());
                        normalPlugins.push(normalPlugin = plugin.clonePlugin());
                        // When cloned to both sides, each gets a "lockingPartner"
                        // reference to the other
                        lockedPlugin.lockingPartner = normalPlugin;
                        normalPlugin.lockingPartner = lockedPlugin;
                        // If the plugin has to be propagated down to both, a new plugin config
                        // object must be given to that side and this plugin must be destroyed.
                        Ext.destroy(plugin);
                        break;
                    case 'locked':
                        lockedPlugins.push(plugin);
                        break;
                    case 'normal':
                        normalPlugins.push(plugin);
                        break;
                    default:
                        topPlugins.push(plugin);
                }
            }
        }
        return {
            topPlugins: topPlugins,
            normalPlugins: normalPlugins,
            lockedPlugins: lockedPlugins
        };
    },
    destroyLockable: function() {
        // The locking view isn't a "real" view, so we need to destroy it manually
        var me = this,
            task = me.syncLockedWidthTask;
        if (task) {
            task.cancel();
            me.syncLockedWidthTask = null;
        }
        // Release interceptors created in modifyHeaderCt
        if (me.lockedGrid && me.lockedGrid.headerCt) {
            me.lockedGrid.headerCt.showMenuBy = null;
        }
        if (me.normalGrid && me.normalGrid.headerCt) {
            me.normalGrid.headerCt.showMenuBy = null;
        }
        Ext.destroy(me.normalScrollbarClipper, me.lockedScrollbarClipper, me.normalScrollbar, me.lockedScrollbar, me.scrollBody, me.scrollContainer, me.normalScrollbarScroller, me.lockedScrollbarScroller, me.view, me.headerCt);
    }
}, function() {
    this.borrow(Ext.Component, [
        'constructPlugin'
    ]);
});

/**
 * @private
 * Implements buffered rendering of a grid, allowing users to scroll
 * through thousands of records without the performance penalties of
 * rendering all the records into the DOM at once.
 *
 * The number of rows rendered outside the visible area can be controlled by configuring the plugin.
 *
 * Users should not instantiate this class. It is instantiated automatically
 * and applied to all grids.
 *
 * ## Implementation notes
 *
 * This class monitors scrolling of the {@link Ext.view.Table
 * TableView} within a {@link Ext.grid.Panel GridPanel} to render a small section of
 * the dataset.
 *
 */
Ext.define('Ext.grid.plugin.BufferedRenderer', {
    extend: 'Ext.plugin.Abstract',
    requires: [
        'Ext.grid.locking.RowSynchronizer'
    ],
    alias: 'plugin.bufferedrenderer',
    /**
     * @property {Boolean} isBufferedRenderer
     * `true` in this class to identify an object as an instantiated BufferedRenderer,
     * or subclass thereof.
     */
    isBufferedRenderer: true,
    lockableScope: 'both',
    /**
     * @cfg {Number}
     * The zone which causes new rows to be appended to the view. As soon as the edge
     * of the rendered grid is this number of rows from the edge of the viewport, the view is moved.
     */
    numFromEdge: 2,
    /**
     * @cfg {Number}
     * The number of extra rows to render on the trailing side of scrolling
     * **outside the {@link #numFromEdge}** buffer as scrolling proceeds.
     */
    trailingBufferZone: 10,
    /**
     * @cfg {Number}
     * The number of extra rows to render on the leading side of scrolling
     * **outside the {@link #numFromEdge}** buffer as scrolling proceeds.
     */
    leadingBufferZone: 20,
    /**
     * @cfg {Boolean} [synchronousRender=true]
     * By default, on detection of a scroll event which brings the end of the rendered table within
     * `{@link #numFromEdge}` rows of the grid viewport, if the required rows are available
     * in the Store, the BufferedRenderer will render rows from the Store *immediately* before
     * returning from the event handler.
     * This setting helps avoid the impression of whitespace appearing during scrolling.
     *
     * Set this to `false` to defer the render until the scroll event handler exits.
     * This allows for faster scrolling, but also allows whitespace to be more easily scrolled
     * into view.
     *
     */
    synchronousRender: true,
    /**
     * @cfg {Number}
     * This is the time in milliseconds to buffer load requests when the store is a
     * {@link Ext.data.BufferedStore buffered store} and a page required for rendering
     * is not present in the store's cache and needs loading.
     */
    scrollToLoadBuffer: 200,
    /**
     * @private
     */
    viewSize: 100,
    /**
     * @private
     */
    rowHeight: 21,
    /**
     * @property {Number} position
     * Current pixel scroll position of the associated {@link Ext.view.Table View}.
     */
    position: 0,
    scrollTop: 0,
    lastScrollDirection: 1,
    bodyTop: 0,
    scrollHeight: 0,
    loadId: 0,
    // Initialize this as a plugin
    init: function(grid) {
        var me = this,
            view = grid.view,
            viewListeners = {
                refresh: me.onViewRefresh,
                columnschanged: me.checkVariableRowHeight,
                scope: me,
                destroyable: true
            },
            scrollerListeners = {
                scroll: me.onViewScroll,
                scope: me
            },
            initialConfig = view.initialConfig;
        me.scroller = view.lockingPartner ? view.ownerGrid.scrollable : view.getScrollable();
        // If we are going to be handling a NodeStore then it's driven by node addition and removal,
        // *not* refreshing. The view overrides required above change the view's onAdd and onRemove
        // behaviour to call onDataRefresh when necessary.
        if (grid.isTree || (grid.ownerLockable && grid.ownerLockable.isTree)) {
            view.blockRefresh = false;
            // Set a load mask if undefined in the view config.
            if (initialConfig && initialConfig.loadMask === undefined) {
                view.loadMask = true;
            }
        }
        if (view.positionBody) {
            viewListeners.refresh = me.onViewRefresh;
        }
        me.grid = grid;
        me.view = view;
        me.isRTL = view.getInherited().rtl;
        view.bufferedRenderer = me;
        view.preserveScrollOnRefresh = true;
        view.animate = false;
        // It doesn't matter if it's a FeatureStore or a DataStore.
        // The important thing is to only bind the same Type of store in future operations!
        me.bindStore(view.dataSource);
        // Use a configured rowHeight in the view
        if (view.hasOwnProperty('rowHeight')) {
            me.rowHeight = view.rowHeight;
        }
        me.position = 0;
        me.viewListeners = view.on(viewListeners);
        // Listen to the correct scroller. Either the view's one, or of it is
        // in a lockable assembly, the y scroller which scrolls them both.
        // If the view is not scrollable, this will be falsy.
        if (me.scroller) {
            me.scrollListeners = me.scroller.on(scrollerListeners);
        }
    },
    // Keep the variableRowHeight property correct WRT variable row heights being possible.
    checkVariableRowHeight: function() {
        var hadVariableRowHeight = this.variableRowHeight;
        this.variableRowHeight = this.view.hasVariableRowHeight();
        // Next time we refresh size, row height will also be recalculated
        if (!!this.variableRowHeight !== !!hadVariableRowHeight) {
            delete this.rowHeight;
        }
    },
    bindStore: function(newStore) {
        var me = this,
            currentStore = me.store;
        // If the grid was configured with a feature such as Grouping that binds a FeatureStore
        // (GroupStore, in its case) as the view's dataSource, we must continue to use
        // the same Type of store.
        //
        // Note that reconfiguring the grid can call into here.
        if (currentStore && currentStore.isFeatureStore) {
            return;
        }
        if (currentStore) {
            me.unbindStore();
        }
        me.storeListeners = newStore.on({
            scope: me,
            groupchange: me.onStoreGroupChange,
            clear: me.onStoreClear,
            beforeload: me.onBeforeStoreLoad,
            load: me.onStoreLoad,
            destroyable: true
        });
        me.store = newStore;
        me.setBodyTop(me.position = me.scrollTop = 0);
        // Delete whatever our last viewSize might have been, and fall back
        // to the prototype's default.		
        delete me.viewSize;
        delete me.rowHeight;
        if (newStore.isBufferedStore) {
            newStore.setViewSize(me.viewSize);
        }
    },
    unbindStore: function() {
        this.storeListeners.destroy();
        this.storeListeners = this.store = null;
    },
    // Disable handling of scroll events until the load is finished
    onBeforeStoreLoad: function(store) {
        var me = this,
            view = me.view;
        if (view && view.refreshCounter) {
            // Unless we are loading tree nodes, or have preserveScrollOnReload,
            // set scroll position and row range back to zero.
            if (store.isTreeStore || view.preserveScrollOnReload) {
                me.nextRefreshStartIndex = view.all.startIndex;
            } else {
                if (me.scrollTop !== 0) {
                    // Zero position tracker so that next scroll event will not trigger any action
                    // eslint-disable-next-line max-len
                    me.setBodyTop(me.bodyTop = me.scrollTop = me.position = me.scrollHeight = me.nextRefreshStartIndex = 0);
                    me.scroller.scrollTo(null, 0);
                }
            }
            me.lastScrollDirection = me.scrollOffset = null;
        }
        me.disable();
    },
    // Re-enable scroll event handling on load.
    onStoreLoad: function() {
        this.isStoreLoading = true;
        this.enable();
    },
    onStoreClear: function() {
        var me = this,
            view = me.view;
        // Do not do anything if view is not rendered, or if the reason for cache clearing
        // is store destruction
        if (view.rendered && !me.store.destroyed) {
            if (me.scrollTop !== 0) {
                // Zero position tracker so that next scroll event will not trigger any action
                me.bodyTop = me.scrollTop = me.position = me.scrollHeight = 0;
                me.nextRefreshStartIndex = null;
                me.scroller.scrollTo(null, 0);
            }
            // TableView does not add a Store Clear listener if there's a BufferedRenderer
            // We handle that here.
            view.refresh();
            me.lastScrollDirection = me.scrollOffset = null;
        }
    },
    // If the store is not grouped, we can switch to fixed row height mode
    onStoreGroupChange: function(store) {
        this.refreshSize();
    },
    onViewRefresh: function(view, records) {
        var me = this,
            rows = view.all,
            height;
        // Recheck the variability of row height in the view.
        me.checkVariableRowHeight();
        // The first refresh on the leading edge of the initial layout will mean that the
        // View has not had the sizes of flexed columns calculated and flushed yet.
        // So measurement of DOM height for calculation of an approximation of the variableRowHeight
        // would be premature.
        // And measurement of the body width would be premature because of uncalculated flexes.
        // eslint-disable-next-line max-len
        if (!view.componentLayoutCounter && (view.headerCt.down('{flex}') || me.variableRowHeight)) {
            view.on({
                boxready: Ext.Function.pass(me.onViewRefresh, [
                    view,
                    records
                ], me),
                single: true
            });
            // AbstractView will call refreshSize() immediately after firing the 'refresh'
            // event; we need to skip that run for the reasons stated above.
            me.skipNextRefreshSize = true;
            return;
        }
        me.skipNextRefreshSize = false;
        // If we are instigating the refresh, we will have already called refreshSize
        // in doRefreshView
        if (me.refreshing) {
            return;
        }
        me.refreshSize();
        if (me.scroller) {
            if (me.scrollTop !== me.scroller.getPosition().y) {
                // The view may have refreshed and scrolled to the top, for example
                // on a sort. If so, it's as if we scrolled to the top, so we'll simulate
                // it here.
                me.onViewScroll();
            } else {
                if (!me.hasOwnProperty('bodyTop')) {
                    me.bodyTop = rows.startIndex * me.rowHeight;
                    me.scroller.scrollTo(null, me.bodyTop);
                }
                me.setBodyTop(me.bodyTop);
                // With new data, the height may have changed, so recalculate the rowHeight
                // and viewSize. This will either add or remove some rows.
                height = view.lastBox && view.lastBox.height;
                if (height && rows.getCount()) {
                    me.onViewResize(view, null, height);
                    // If we repaired the view by adding or removing records, then keep the records
                    // array consistent with what is there for subsequent listeners.
                    // For example the WidgetColumn listener which post-processes all rows:
                    // https://sencha.jira.com/browse/EXTJS-13942
                    if (records && (rows.getCount() !== records.length)) {
                        records.length = 0;
                        records.push.apply(records, me.store.getRange(rows.startIndex, rows.endIndex));
                    }
                }
            }
        }
    },
    /**
     * @private
     * @param {Ext.layout.ContextItem} ownerContext The view's layout context
     * Called before the start of a view's layout run
     */
    beforeTableLayout: function(ownerContext) {
        var dom = this.view.body.dom,
            size;
        if (dom) {
            size = this.grid.getElementSize(dom);
            ownerContext.bodyHeight = size.height;
            ownerContext.bodyWidth = size.width;
        }
    },
    /**
     * @private
     * @param {Ext.layout.ContextItem} ownerContext The view's layout context
     * Called when a view's layout run is complete.
     */
    afterTableLayout: function(ownerContext) {
        var me = this,
            view = me.view,
            renderedBlockHeight;
        // The rendered block has changed height.
        // This could happen if a cellWrap: true column has changed width.
        // We need to recalculate row height and scroll range
        if (ownerContext.bodyHeight && view.body.dom) {
            delete me.rowHeight;
            me.refreshSize();
            renderedBlockHeight = me.grid.getElementHeight(view.body.dom);
            if (renderedBlockHeight !== ownerContext.bodyHeight) {
                me.onViewResize(view, null, view.el.lastBox.height);
                // The view resize might have added or removed rows
                renderedBlockHeight = me.bodyHeight;
                // The layout has caused the rendered block to shrink in height.
                // This could happen if a cellWrap: true column has increased in width.
                // It could cause the bottom of the rendered view to zoom upwards
                // out of sight.
                if (renderedBlockHeight < ownerContext.bodyHeight) {
                    if (me.viewSize >= me.store.getCount()) {
                        me.setBodyTop(0);
                    }
                    // Column got wider causing scroll range to shrink, leaving the view
                    // stranded above the fold. Scroll up to bring it into view.
                    // eslint-disable-next-line max-len
                    else if (me.bodyTop > me.scrollTop || me.bodyTop + renderedBlockHeight < me.scrollTop + me.viewClientHeight) {
                        me.setBodyTop(me.scrollTop - me.trailingBufferZone * me.rowHeight);
                    }
                }
                // If the rendered block is the last lines in the dataset,
                // ensure the scroll range exactly encapsuates it.
                if (view.all.endIndex === (view.dataSource.getCount()) - 1) {
                    me.stretchView(view, me.scrollHeight = me.bodyTop + renderedBlockHeight - 1);
                }
            }
        }
    },
    refreshSize: function() {
        var me = this,
            view = me.view,
            // If we have been told to skip the next refresh, or there is going to be
            // an upcoming layout, skip this op.
            skipNextRefreshSize = me.skipNextRefreshSize || (Ext.Component.pendingLayouts && Ext.Component.layoutSuspendCount) || !view.body.dom;
        // We only want to skip ONE time.
        me.skipNextRefreshSize = false;
        if (skipNextRefreshSize) {
            return;
        }
        // Cache the rendered block height.
        me.bodyHeight = me.grid.getElementHeight(view.body.dom);
        // Calculates scroll range.
        // Also calculates rowHeight if we do not have an own rowHeight property.
        me.scrollHeight = me.getScrollHeight();
        me.stretchView(view, me.scrollHeight);
    },
    /**
     * Called directly from {@link Ext.view.Table#onResize}. Reacts to View changing height by
     * recalculating the size of the rendered block, and either trimming it or adding to it.
     * @param {Ext.view.Table} view The Table view.
     * @param {Number} width The new Width.
     * @param {Number} height The new height.
     * @param {Number} oldWidth The old width.
     * @param {Number} oldHeight The old height.
     * @private
     */
    onViewResize: function(view, width, height, oldWidth, oldHeight) {
        var me = this,
            newViewSize;
        me.refreshSize();
        // Only process first layout (the boxready event) or height resizes.
        if (!oldHeight || height !== oldHeight) {
            // Changing the content height may trigger multiple layouts for locked grids.
            // Ensure they are coalesced.
            Ext.suspendLayouts();
            // Recalculate the view size in rows now that the grid view has changed height
            me.viewClientHeight = height || view.el.dom.clientHeight;
            // Use the theme's default rowHeight unless the measured row height is smaller
            // when calculating the view size. If the rows are *larger*, that doesn't really matter.
            // We just need to cover the visible range with some scrolling range extra.
            newViewSize = Math.ceil(height / Math.min(me.getThemeRowHeight(), me.rowHeight)) + me.trailingBufferZone + me.leadingBufferZone;
            me.viewSize = me.setViewSize(newViewSize);
            Ext.resumeLayouts(true);
        }
    },
    stretchView: function(view, scrollRange) {
        var me = this,
            newY;
        // Ensure that both the scroll range AND the positioned view body are in the viewable area.
        if (me.scrollTop > scrollRange) {
            newY = me.nextRefreshStartIndex == null ? me.bodyHeight : scrollRange - me.bodyHeight;
            me.position = me.scrollTop = Math.max(newY, 0);
            me.scroller.scrollTo(null, me.scrollTop);
        }
        if (me.bodyTop > scrollRange) {
            view.body.translate(null, me.bodyTop = me.position);
        }
        // Tell the scroller what the scroll size is.
        if (view.getScrollable()) {
            me.refreshScroller(view, scrollRange);
        }
    },
    refreshScroller: function(view, scrollRange) {
        var scroller = view.getScrollable();
        if (scroller) {
            // Ensure the scroller viewport element size is up to date if it needs to be told
            // (touch scroller)
            if (scroller.setElementSize) {
                scroller.setElementSize();
            }
            // Ensure the scroller knows about content size
            scroller.setSize({
                x: view.headerCt.getTableWidth(),
                // No Y range in the view's scroller if we're in a locking assembly.
                // The LockingScroller stretches the views.
                y: view.lockingPartner ? null : scrollRange
            });
            // In a locking assembly, stretch the yScroller
            if (view.lockingPartner) {
                this.scroller.setSize({
                    x: 0,
                    y: scrollRange
                });
            }
        }
    },
    setViewSize: function(viewSize, fromLockingPartner) {
        var me = this,
            store = me.store,
            view = me.view,
            ownerGrid,
            rows = view.all,
            elCount = rows.getCount(),
            storeCount = store.getCount(),
            start, end,
            lockingPartner = me.view.lockingPartner && me.view.lockingPartner.bufferedRenderer,
            diff = elCount - viewSize,
            oldTop = 0,
            maxIndex = Math.max(0, storeCount - 1),
            // This is which end is closer to being visible therefore must be the first
            // to have rows added or the opposite end from which rows get removed
            // if shrinking the view.
            pointyEnd = Ext.Number.sign((me.getFirstVisibleRowIndex() - rows.startIndex) - (rows.endIndex - me.getLastVisibleRowIndex()));
        // Synchronize view sizes
        if (lockingPartner && !fromLockingPartner) {
            lockingPartner.setViewSize(viewSize, true);
        }
        diff = elCount - viewSize;
        if (diff) {
            // Must be set for getFirstVisibleRowIndex to work
            me.scrollTop = me.scroller ? me.scroller.getPosition().y : 0;
            me.viewSize = viewSize;
            if (store.isBufferedStore) {
                store.setViewSize(viewSize);
            }
            // If a store loads before we have calculated a viewSize, it loads me.defaultViewSize
            // records. This may be larger or smaller than the final viewSize so the store needs
            // adjusting when the view size is calculated.
            if (elCount) {
                // New start index should be current start index unless that's now too close
                // to the end of the store to yield a full view, in which case work back
                // from the end of the store. Ensure we don't go negative.
                start = Math.max(0, Math.min(rows.startIndex, storeCount - viewSize));
                // New end index works forward from the new start index ensuring
                // we don't walk off the end
                end = Math.min(start + viewSize - 1, maxIndex);
                // Only do expensive adding or removal if range is not already correct
                if (start === rows.startIndex && end === rows.endIndex) {
                    // Needs rows adding to or bottom depending on which end is closest
                    // to being visible (The pointy end)
                    if (diff < 0) {
                        me.handleViewScroll(pointyEnd);
                    }
                } else {
                    // While changing our visible range, the locking partner must not sync
                    if (lockingPartner) {
                        lockingPartner.disable();
                    }
                    // View must expand
                    if (diff < 0) {
                        // If it's *possible* to add rows...
                        if (storeCount > viewSize && storeCount > elCount) {
                            // Grab the render range with a view to appending and prepending
                            // nodes to the top and bottom as necessary.
                            // Store's getRange API always has been inclusive of endIndex.
                            store.getRange(start, end, {
                                callback: function(newRecords, start, end) {
                                    ownerGrid = view.ownerGrid;
                                    // Append if necessary
                                    if (end > rows.endIndex) {
                                        // eslint-disable-next-line max-len
                                        rows.scroll(Ext.Array.slice(newRecords, rows.endIndex + 1, Infinity), 1, 0);
                                    }
                                    // Prepend if necessary
                                    if (start < rows.startIndex) {
                                        oldTop = rows.first(true);
                                        // eslint-disable-next-line max-len
                                        rows.scroll(Ext.Array.slice(newRecords, 0, rows.startIndex - start), -1, 0);
                                        // We just added some rows to the top of the rendered block
                                        // We have to bump it up to keep the view stable.
                                        me.bodyTop -= oldTop.offsetTop;
                                    }
                                    me.setBodyTop(me.bodyTop);
                                    // The newly added rows must sync the row heights
                                    // eslint-disable-next-line max-len
                                    if (lockingPartner && !fromLockingPartner && (ownerGrid.syncRowHeight || ownerGrid.syncRowHeightOnNextLayout)) {
                                        lockingPartner.setViewSize(viewSize, true);
                                        ownerGrid.syncRowHeights();
                                    }
                                }
                            });
                        } else // If not possible just refresh
                        {
                            me.refreshView(0);
                        }
                    } else // View size is contracting
                    {
                        // If removing from top, we have to bump the rendered block downwards
                        // by the height of the removed rows.
                        if (pointyEnd === 1) {
                            oldTop = rows.item(rows.startIndex + diff, true).offsetTop;
                        }
                        // Clip the rows off the required end
                        rows.clip(pointyEnd, diff);
                        me.setBodyTop(me.bodyTop + oldTop);
                    }
                    if (lockingPartner) {
                        lockingPartner.enable();
                    }
                }
            }
            // Update scroll range
            me.refreshSize();
        }
        return viewSize;
    },
    /**
     * @private
     * TableView's getViewRange delegates the operation to this method
     * if buffered rendering is present.
     */
    getViewRange: function() {
        var me = this,
            view = me.view,
            rows = view.all,
            rowCount = rows.getCount(),
            lockingPartnerRows = view.lockingPartner && view.lockingPartner.all,
            store = me.store,
            startIndex = 0,
            endIndex;
        // Get a best guess at the number of rows to fill the view
        if (!me.hasOwnProperty('viewSize') && me.ownerCt && me.ownerCt.height) {
            me.viewSize = Math.ceil(me.ownerCt.height / me.rowHeight);
        }
        if (!store.data.getCount()) {
            return [];
        }
        // We're starting from nothing, but there's a locking partner with the range info,
        // so match that
        if (!rowCount && lockingPartnerRows && lockingPartnerRows.getCount()) {
            startIndex = lockingPartnerRows.startIndex;
            endIndex = Math.min(lockingPartnerRows.endIndex, startIndex + me.viewSize - 1, store.getCount() - 1);
        } else {
            // If there already is a view range, then the startIndex from that
            if (rowCount) {
                startIndex = rows.startIndex;
            }
            // Otherwise use start index of current page.
            // https://sencha.jira.com/browse/EXTJSIV-10724
            // Buffered store may be primed with loadPage(n) call rather than autoLoad
            // which starts at index 0.
            else if (store.isBufferedStore) {
                if (!store.currentPage) {
                    store.currentPage = 1;
                }
                startIndex = rows.startIndex = (store.currentPage - 1) * (store.pageSize || 1);
                // The RowNumberer uses the current page to offset the record index,
                // so when buffered, it must always be on page 1
                store.currentPage = 1;
            }
            endIndex = startIndex + (me.viewSize || store.defaultViewSize) - 1;
        }
        return store.getRange(startIndex, endIndex);
    },
    /**
     * @private
     * Handles the Store replace event, producing a correct buffered view
     * after the replace operation.
     */
    onReplace: function(store, startIndex, oldRecords, newRecords) {
        var me = this,
            scroller = me.scroller,
            view = me.view,
            rows = view.all,
            oldStartIndex,
            renderedSize = rows.getCount(),
            lastAffectedIndex = startIndex + oldRecords.length - 1,
            recordIncrement = newRecords.length - oldRecords.length,
            scrollIncrement = recordIncrement * me.rowHeight,
            preserveScrollOnRefresh;
        // All replacement activity is past the end of a full-sized rendered block;
        // do nothing except update scroll range
        if (startIndex >= rows.startIndex + me.viewSize) {
            me.refreshSize();
            return;
        }
        // If the change is all above the rendered block and the rendered block is its maximum size,
        // update the scroll range and ensure the buffer zone above is filled if possible.
        if (renderedSize && lastAffectedIndex < rows.startIndex && rows.getCount() >= me.viewSize) {
            // Move the index-based NodeCache up or down depending on whether it's a net
            // adding or removal above.
            rows.moveBlock(recordIncrement);
            me.refreshSize();
            // If the change above us was an addition, pretend that we just scrolled upwards
            // which will ensure that there is at least this.numFromEdge rows above the fold.
            oldStartIndex = rows.startIndex;
            if (recordIncrement > 0) {
                // Do not allow this operation to mirror to the partner side.
                me.doNotMirror = true;
                me.handleViewScroll(-1);
                me.doNotMirror = false;
            }
            // If the handleViewScroll did nothing, we just have to ensure the rendered block
            // is the correct amount down the scroll range, and then readjust the top
            // of the rendered block to keep the visuals the same.
            if (rows.startIndex === oldStartIndex) {
                // If inserting or removing invisible records above the start of the rendered block,
                // the visible block must then be moved up or down the scroll range.
                if (rows.startIndex) {
                    me.setBodyTop(me.bodyTop += scrollIncrement);
                    view.suspendEvent('scroll');
                    view.scrollBy(0, scrollIncrement);
                    view.resumeEvent('scroll');
                    me.position = me.scrollTop = me.scroller.getPosition().y;
                }
            } else // The handleViewScroll added rows, so we must scroll to keep the visuals the same;
            {
                view.suspendEvent('scroll');
                view.scrollBy(0, (oldStartIndex - rows.startIndex) * me.rowHeight);
                view.resumeEvent('scroll');
            }
            view.refreshSize(rows.getCount() !== renderedSize);
            return;
        }
        // If the change is all below the rendered block, update the scroll range
        // and ensure the buffer zone below us is filled if possible.
        if (renderedSize && startIndex > rows.endIndex) {
            me.refreshSize();
            // If the change below us was an addition, ask for <viewSize>
            // rows to be rendered starting from the current startIndex.
            // If more rows need to be scrolled onto the bottom of the rendered
            // block to achieve this, that will do it.
            if (recordIncrement > 0) {
                me.onRangeFetched(null, rows.startIndex, Math.min(store.getCount(), rows.startIndex + me.viewSize) - 1);
            }
            view.refreshSize(rows.getCount() !== renderedSize);
            return;
        }
        // Cut into rendered block from above
        if (startIndex < rows.startIndex && lastAffectedIndex <= rows.endIndex) {
            preserveScrollOnRefresh = view.preserveScrollOnRefresh;
            view.preserveScrollOnRefresh = false;
            me.refreshView(rows.startIndex - oldRecords.length + newRecords.length);
            view.preserveScrollOnRefresh = preserveScrollOnRefresh;
            return;
        }
        if (startIndex < rows.startIndex && lastAffectedIndex <= rows.endIndex && scrollIncrement) {
            me.doVerticalScroll(scroller, me.scrollTop += scrollIncrement, true);
        }
        // Only need to change display if the view is currently empty, or
        // change intersects the rendered view.
        me.refreshView(rows.startIndex, scrollIncrement);
    },
    doVerticalScroll: function(scroller, pos, supressEvents) {
        var me = this;
        if (!scroller) {
            return;
        }
        if (supressEvents) {
            scroller.suspendEvent('scroll');
        }
        scroller.scrollTo(null, me.position = pos);
        if (supressEvents) {
            scroller.resumeEvent('scroll');
        }
    },
    /**
     * @private
     * Scrolls to and optionally selects the specified row index **in the total dataset**.
     *
     * This is a private method for internal usage by the framework.
     *
     * Use the grid's {@link Ext.panel.Table#ensureVisible ensureVisible} method to scroll
     * a particular record or record index into view.
     *
     * @param {Number/Ext.data.Model} recordIdx The record, or the zero-based position
     * in the dataset to scroll to.
     * @param {Object} [options] An object containing options to modify the operation.
     * @param {Boolean} [options.animate] Pass `true` to animate the row into view.
     * @param {Boolean} [options.highlight] Pass `true` to highlight the row with a glow animation
     * when it is in view.
     * @param {Boolean} [options.select] Pass as `true` to select the specified row.
     * @param {Boolean} [options.focus] Pass as `true` to focus the specified row.
     * @param {Function} [options.callback] A function to call when the row has been scrolled to.
     * @param {Number} options.callback.recordIdx The resulting record index (may have changed
     * if the passed index was outside the valid range).
     * @param {Ext.data.Model} options.callback.record The resulting record from the store.
     * @param {HTMLElement} options.callback.node The resulting view row element.
     * @param {Object} [options.scope] The scope (`this` reference) in which to execute
     * the callback. Defaults to this BufferedRenderer.
     * @param {Ext.grid.column.Column/Number} [options.column] The column, or column index
     * to scroll into view.
     *
     */
    scrollTo: function(recordIdx, options) {
        var args = arguments,
            me = this,
            view = me.view,
            lockingPartner = view.lockingPartner && view.lockingPartner.grid.isVisible() && view.lockingPartner.bufferedRenderer,
            store = me.store,
            total = store.getCount(),
            startIdx, endIdx, targetRow, tableTop, groupingFeature, metaGroup, record, direction;
        // New option object API
        if (options !== undefined && !(options instanceof Object)) {
            options = {
                select: args[1],
                callback: args[2],
                scope: args[3]
            };
        }
        // If we have a grouping summary feature rendering the view in groups,
        // first, ensure that the record's group is expanded,
        // then work out which record in the groupStore the record is at.
        if ((groupingFeature = view.dataSource.groupingFeature) && (groupingFeature.collapsible)) {
            if (recordIdx.isEntity) {
                record = recordIdx;
            } else {
                record = view.store.getAt(Math.min(Math.max(recordIdx, 0), view.store.getCount() - 1));
            }
            metaGroup = groupingFeature.getMetaGroup(record);
            if (metaGroup && metaGroup.isCollapsed) {
                if (!groupingFeature.isExpandingOrCollapsing && record !== metaGroup.placeholder) {
                    groupingFeature.expand(groupingFeature.getGroup(record).getGroupKey());
                    total = store.getCount();
                    recordIdx = groupingFeature.indexOf(record);
                } else {
                    // If we've just been collapsed, then the only record we have is
                    // the wrapped placeholder
                    record = metaGroup.placeholder;
                    recordIdx = groupingFeature.indexOfPlaceholder(record);
                }
            } else {
                recordIdx = groupingFeature.indexOf(record);
            }
        } else {
            if (recordIdx.isEntity) {
                record = recordIdx;
                recordIdx = store.indexOf(record);
                // Currently loaded pages do not contain the passed record, we cannot proceed.
                if (recordIdx === -1) {
                    Ext.raise('Unknown record passed to BufferedRenderer#scrollTo');
                    return;
                }
            } else {
                // Sanitize the requested record index
                recordIdx = Math.min(Math.max(recordIdx, 0), total - 1);
                record = store.getAt(recordIdx);
            }
        }
        // See if the required row for that record happens to be within the rendered range.
        if (record && (targetRow = view.getNode(record))) {
            view.grid.ensureVisible(record, options);
            // Keep the view immediately replenished when we scroll an existing element into view.
            // DOM scroll events fire asynchronously, and we must not leave subsequent code
            // without a valid buffered row block.
            me.onViewScroll();
            return;
        }
        // Calculate view start index.
        // If the required record is above the fold...
        if (recordIdx < view.all.startIndex) {
            // The startIndex of the new rendered range is a little less
            // than the target record index.
            direction = -1;
            // eslint-disable-next-line max-len
            startIdx = Math.max(Math.min(recordIdx - (Math.floor((me.leadingBufferZone + me.trailingBufferZone) / 2)), total - me.viewSize + 1), 0);
            endIdx = Math.min(startIdx + me.viewSize - 1, total - 1);
        } else // If the required record is below the fold...
        {
            // The endIndex of the new rendered range is a little greater
            // than the target record index.
            direction = 1;
            // eslint-disable-next-line max-len
            endIdx = Math.min(recordIdx + (Math.floor((me.leadingBufferZone + me.trailingBufferZone) / 2)), total - 1);
            startIdx = Math.max(endIdx - (me.viewSize - 1), 0);
        }
        tableTop = Math.max(startIdx * me.rowHeight, 0);
        store.getRange(startIdx, endIdx, {
            callback: function(range, start, end) {
                // Render the range.
                // Pass synchronous flag so that it does it inline, not on a timer.
                // Pass fromLockingPartner flag so that it does not inform the lockingPartner.
                me.renderRange(start, end, true);
                record = store.data.getRange(recordIdx, recordIdx + 1)[0];
                targetRow = view.getNode(record);
                // bodyTop property must track the translated position of the body
                view.body.translate(null, me.bodyTop = tableTop);
                // Ensure the scroller knows about the range if we're going down
                if (direction === 1 && view.hasVariableRowHeight()) {
                    me.refreshSize();
                }
                // Locking partner must render the same range
                if (lockingPartner) {
                    lockingPartner.renderRange(start, end, true);
                    // Sync all row heights
                    me.syncRowHeights();
                    // bodyTop property must track the translated position of the body
                    lockingPartner.view.body.translate(null, lockingPartner.bodyTop = tableTop);
                    // Ensure the scroller knows about the range if we're going down
                    if (direction === 1) {
                        lockingPartner.refreshSize();
                    }
                }
                // The target does not map to a view node.
                // Cannot scroll to it.
                if (!targetRow) {
                    return;
                }
                view.grid.ensureVisible(record, options);
                me.scrollTop = me.position = me.scroller.getPosition().y;
                if (lockingPartner) {
                    lockingPartner.position = lockingPartner.scrollTop = me.scrollTop;
                }
            }
        });
    },
    onViewScroll: function(scroller, x, scrollTop) {
        var me = this,
            bodyDom = me.view.body.dom,
            store = me.store,
            totalCount = (store.getCount()),
            vscrollDistance, scrollDirection;
        // May be directly called with no args, as well as from the Scroller's scroll event
        me.scrollTop = scrollTop == null ? (scrollTop = me.scroller.getPosition().y) : scrollTop;
        // Because lockable assemblies now only have one Y scroller,
        // initially hidden grids (one side may begin with all the columns)
        // still get the scroll notification, but may not have any DOM
        // to scroll.
        if (bodyDom) {
            // Only check for nearing the edge if we are enabled, and if there is overflow
            // beyond our view bounds. If there is no paging to be done
            // (Store's dataset is all in memory) we will be disabled.
            if (!(me.disabled || totalCount < me.viewSize)) {
                vscrollDistance = scrollTop - me.position;
                scrollDirection = vscrollDistance > 0 ? 1 : -1;
                // Moved at least 20 pixels, or changed direction, so test whether the numFromEdge
                // is triggered
                if (Math.abs(vscrollDistance) >= 20 || (scrollDirection !== me.lastScrollDirection)) {
                    me.lastScrollDirection = scrollDirection;
                    me.handleViewScroll(me.lastScrollDirection, vscrollDistance);
                }
            }
        }
    },
    handleViewScroll: function(direction, vscrollDistance) {
        var me = this,
            rows = me.view.all,
            store = me.store,
            storeCount = store.getCount(),
            viewSize = me.viewSize,
            lastItemIndex = storeCount - 1,
            maxRequestStart = Math.max(0, storeCount - viewSize),
            requestStart, requestEnd;
        // We're scrolling up
        if (direction === -1) {
            // If table starts at record zero, we have nothing to do
            if (rows.startIndex) {
                if (me.topOfViewCloseToEdge()) {
                    requestStart = Math.max(0, me.getLastVisibleRowIndex() + me.trailingBufferZone - viewSize);
                    // If, having scrolled up, a variableRowHeight calculation based
                    // upon scrolTop/rowHeight yields an obviously wrong value,
                    // then constrain it to a calculated value.
                    // We CANNOT just Math.min it with maxRequestStart, because we may already
                    // be at maxRequestStart, and asking to render the same block
                    // will have no effect.
                    // We calculate a start value a few rows above the current startIndex.
                    if (requestStart > rows.startIndex) {
                        requestStart = Math.max(0, rows.startIndex + Math.floor(vscrollDistance / me.rowHeight));
                    }
                }
            }
        } else // We're scrolling down
        {
            // If table ends at last record, we have nothing to do
            if (rows.endIndex < lastItemIndex) {
                if (me.bottomOfViewCloseToEdge()) {
                    // eslint-disable-next-line max-len
                    requestStart = Math.max(0, Math.min(me.getFirstVisibleRowIndex() - me.trailingBufferZone, maxRequestStart));
                }
            }
        }
        // View is OK at this scroll. Advance loadId so that any load requests in flight do not
        // result in rendering upon their return.
        if (requestStart == null) {
            // View is still valid at this scroll position.
            // Do not trigger a handleViewScroll call until *ANOTHER* 20 pixels have scrolled by.
            me.position = me.scrollTop;
            me.loadId++;
        } else // We scrolled close to the edge and the Store needs reloading
        {
            requestEnd = Math.min(requestStart + viewSize - 1, lastItemIndex);
            // viewSize was calculated too small due to small sample row count with some skewed
            // item height in there such as a tall group header item. Bump range
            // down by one in this case.
            if (me.variableRowHeight && requestEnd === rows.endIndex && requestEnd < lastItemIndex) {
                requestEnd++;
                requestStart++;
            }
            // If calculated view range has moved, then render it and return the fact
            // that the scroll was handled.
            if (requestStart !== rows.startIndex || requestEnd !== rows.endIndex) {
                me.scroller.trackingScrollTop = me.scrollTop;
                me.renderRange(requestStart, requestEnd);
                return true;
            }
        }
    },
    bottomOfViewCloseToEdge: function() {
        var me = this;
        if (me.variableRowHeight) {
            return me.bodyTop + me.bodyHeight < me.scrollTop + me.view.lastBox.height + (me.numFromEdge * me.rowHeight);
        } else {
            return (me.view.all.endIndex - me.getLastVisibleRowIndex()) < me.numFromEdge;
        }
    },
    topOfViewCloseToEdge: function() {
        var me = this;
        if (me.variableRowHeight) {
            // The body top position is within the numFromEdge zone
            return me.bodyTop > me.scrollTop - (me.numFromEdge * me.rowHeight);
        } else {
            return (me.getFirstVisibleRowIndex() - me.view.all.startIndex) < me.numFromEdge;
        }
    },
    /**
     * @private
     * Refreshes the current rendered range if possible.
     * Optionally refreshes starting at the specified index.
     */
    refreshView: function(startIndex, scrollIncrement) {
        var me = this,
            viewSize = me.viewSize,
            view = me.view,
            rows = view.all,
            store = me.store,
            storeCount = store.getCount(),
            maxIndex = Math.max(0, storeCount - 1),
            lockingPartnerRows = view.lockingPartner && view.lockingPartner.all,
            preserveScroll = me.bodyTop && view.preserveScrollOnRefresh || scrollIncrement,
            endIndex;
        // Empty Store is simple, don't even ask the store
        if (!storeCount) {
            return me.doRefreshView([], 0, 0);
        }
        // Store doesn't fill the required view size. Simple start/end calcs.
        else if (storeCount < viewSize) {
            startIndex = 0;
            endIndex = maxIndex;
            me.nextRefreshStartIndex = preserveScroll ? null : 0;
        }
        // We're starting from nothing, but there's a locking partner with the range info,
        // so match that
        else if (startIndex == null && !rows.getCount() && lockingPartnerRows && lockingPartnerRows.getCount()) {
            startIndex = lockingPartnerRows.startIndex;
            endIndex = Math.min(lockingPartnerRows.endIndex, startIndex + viewSize - 1, maxIndex);
        } else // Work out range to refresh
        {
            if (startIndex == null) {
                // Use a nextRefreshStartIndex as set by a load operation
                // in which we are maintaining scroll position
                if (me.nextRefreshStartIndex != null && !preserveScroll) {
                    startIndex = me.nextRefreshStartIndex;
                } else {
                    startIndex = rows.startIndex;
                }
                me.nextRefreshStartIndex = null;
            }
            // New start index should be current start index unless that's now too close
            // to the end of the store to yield a full view, in which case work back
            // from the end of the store. Ensure we don't go negative.
            startIndex = Math.max(0, Math.min(startIndex, maxIndex - viewSize + 1));
            // New end index works forward from the new start index ensuring
            // we don't walk off the end    
            endIndex = Math.min(startIndex + viewSize - 1, maxIndex);
            if (endIndex - startIndex + 1 > viewSize) {
                startIndex = endIndex - viewSize + 1;
            }
        }
        if (startIndex === 0 && endIndex === -1) {
            me.doRefreshView([], 0, 0);
        } else {
            store.getRange(startIndex, endIndex, {
                callback: me.doRefreshView,
                scope: me
            });
        }
    },
    doRefreshView: function(range, startIndex, endIndex) {
        var me = this,
            view = me.view,
            scroller = me.scroller,
            rows = view.all,
            previousStartIndex = rows.startIndex,
            previousEndIndex = rows.endIndex,
            prevRowCount = rows.getCount(),
            viewMoved = startIndex !== rows.startIndex && !me.isStoreLoading,
            calculatedTop = -1,
            previousFirstItem, previousLastItem, scrollIncrement, restoreFocus;
        me.isStoreLoading = false;
        // So that listeners to the itemremove events know that its because of a refresh.
        // And so that this class's refresh listener knows to ignore it.
        view.refreshing = me.refreshing = true;
        if (view.refreshCounter) {
            // Give CellEditors or other transient in-cell items a chance to get out of the way.
            if (view.hasListeners.beforerefresh && view.fireEvent('beforerefresh', view) === false) {
                return view.refreshNeeded = view.refreshing = me.refreshing = false;
            }
            // If focus was in any way in the view, whether actionable or navigable,
            // this will return a function which will restore that state.
            restoreFocus = view.saveFocusState();
            view.clearViewEl(true);
            view.refreshCounter++;
            if (range.length) {
                view.doAdd(range, startIndex);
                if (viewMoved) {
                    // Try to find overlap between newly rendered block and old block
                    previousFirstItem = rows.item(previousStartIndex, true);
                    previousLastItem = rows.item(previousEndIndex, true);
                    // Work out where to move the view top if there is overlap
                    if (previousFirstItem) {
                        scrollIncrement = -previousFirstItem.offsetTop;
                    } else if (previousLastItem) {
                        scrollIncrement = rows.last(true).offsetTop - previousLastItem.offsetTop;
                    }
                    // If there was an overlap, we know exactly where to move the view
                    if (scrollIncrement) {
                        calculatedTop = Math.max(me.bodyTop + scrollIncrement, 0);
                        me.scrollTop = calculatedTop ? me.scrollTop + scrollIncrement : 0;
                    } else // No overlap: calculate the a new body top and scrollTop.
                    {
                        calculatedTop = startIndex * me.rowHeight;
                        // eslint-disable-next-line max-len
                        me.scrollTop = Math.max(calculatedTop + me.rowHeight * (calculatedTop < me.bodyTop ? me.leadingBufferZone : me.trailingBufferZone), 0);
                    }
                }
            } else // Clearing the view.
            // Ensure we jump to top.
            // Apply empty text.
            {
                me.scrollTop = calculatedTop = me.position = 0;
                view.addEmptyText();
            }
            // Keep scroll and rendered block positions synched if there is scrolling.
            if (calculatedTop !== -1) {
                me.setBodyTop(calculatedTop);
                me.doVerticalScroll(scroller, me.scrollTop, true);
            }
            // Correct scroll range
            me.refreshSize();
            view.refreshSize(rows.getCount() !== prevRowCount);
            view.fireItemMutationEvent('refresh', view, range);
            // If focus was in any way in this view, this will restore it
            restoreFocus();
            if (view.preserveScrollOnRefresh && restoreFocus !== Ext.emptyFn) {
                me.doVerticalScroll(scroller, me.scrollTop, true);
            }
            view.headerCt.setSortState();
        } else {
            view.refresh();
        }
        // If there are columns to trigger rendering, and the rendered block is not
        // either the view size or, if store count less than view size, the store count,
        // then there's a bug.
        if (view.getVisibleColumnManager().getColumns().length && rows.getCount() !== Math.min(me.store.getCount(), me.viewSize)) {
            Ext.raise('rendered block refreshed at ' + rows.getCount() + ' rows while BufferedRenderer view size is ' + me.viewSize);
        }
        view.refreshNeeded = view.refreshing = me.refreshing = false;
    },
    renderRange: function(start, end, forceSynchronous) {
        var me = this,
            rows = me.view.all,
            store = me.store;
        // We're being told to render what we already have rendered.
        if (rows.startIndex === start && rows.endIndex === end) {
            return;
        }
        // Skip if we are being asked to render exactly the rows that we already have.
        // This can happen if the viewSize has to be recalculated
        // (due to either a data refresh or a view resize event) but the calculated size
        // ends up the same.
        if (!(start === rows.startIndex && end === rows.endIndex)) {
            // If range is available synchronously, process it now.
            if (store.rangeCached(start, end)) {
                me.cancelLoad();
                if (me.synchronousRender || forceSynchronous) {
                    me.onRangeFetched(null, start, end);
                } else {
                    if (!me.renderTask) {
                        me.renderTask = new Ext.util.DelayedTask(me.onRangeFetched, me);
                    }
                    // Render the new range very soon after this scroll event handler exits.
                    // If scrolling very quickly, a few more scroll events may fire before
                    // the render takes place. Each one will just *update* the arguments with which
                    // the pending invocation is called.
                    me.renderTask.delay(-1, null, null, [
                        null,
                        start,
                        end
                    ]);
                }
            } else // Required range is not in the prefetch buffer. Ask the store to prefetch it.
            {
                me.attemptLoad(start, end, me.scrollTop);
            }
        }
    },
    onRangeFetched: function(range, start, end) {
        var me = this,
            view = me.view,
            scroller = me.scroller,
            viewEl = view.el,
            rows = view.all,
            increment = 0,
            calculatedTop,
            partnerView = !me.doNotMirror && view.lockingPartner,
            partnerColManger = partnerView && partnerView.getVisibleColumnManager(),
            partnerViewConfigured = partnerColManger && partnerColManger.getColumns().length,
            lockingPartner = partnerViewConfigured && partnerView.bufferedRenderer,
            partnerRows = partnerViewConfigured && partnerView.all,
            variableRowHeight = me.variableRowHeight,
            doSyncRowHeight = partnerViewConfigured && partnerView.ownerCt.isVisible() && (view.ownerGrid.syncRowHeight || view.ownerGrid.syncRowHeightOnNextLayout || (lockingPartner.variableRowHeight !== variableRowHeight)),
            activeEl, focusedView, i, newRows, newTop, noOverlap, oldStart, partnerNewRows, pos, removeCount, topAdditionSize, topBufferZone, records;
        // View may have been destroyed since the DelayedTask was kicked off.
        if (view.destroyed) {
            return;
        }
        // If called as a callback from the Store, the range will be passed,
        // if called from renderRange, it won't
        if (range) {
            // Re-cache the scrollTop if there has been an asynchronous call to the server.
            me.scrollTop = scroller.getPosition().y;
        } else {
            range = me.store.getRange(start, end);
            // Store may have been cleared since the DelayedTask was kicked off.
            if (!range) {
                return;
            }
        }
        // If we contain focus now, but do not when we have rendered the new rows,
        // we must focus the view el.
        activeEl = Ext.fly(Ext.Element.getActiveElement());
        if (viewEl.contains(activeEl)) {
            focusedView = view;
        } else if (partnerView && partnerView.el.contains(activeEl)) {
            focusedView = partnerView;
        }
        // In case the browser does fire synchronous focus events when a focused element
        // is derendered...
        if (focusedView) {
            activeEl.suspendFocusEvents();
        }
        // Best guess rendered block position is start row index * row height.
        // We can use this as bodyTop if the row heights are all standard.
        // We MUST use this as bodyTop if the scroll is a teleporting scroll.
        // If we are incrementally scrolling, we add the rows to the bottom, and
        // remove a block of rows from the top.
        // The bodyTop is then incremented by the height of the removed block to keep
        // the visuals the same.
        //
        // We cannot always use the calculated top, and compensate by adjusting the scroll position
        // because that would break momentum scrolling on DOM scrolling platforms, and would be
        // immediately undone in the next frame update of a momentum scroll on touch scroll
        // platforms.
        calculatedTop = start * me.rowHeight;
        // The new range encompasses the current range. Refresh and keep the scroll position stable
        if (start < rows.startIndex && end > rows.endIndex) {
            // How many rows will be added at top. So that we can reposition the table
            // to maintain scroll position
            topAdditionSize = rows.startIndex - start;
            // MUST use View method so that itemremove events are fired so widgets can be recycled.
            view.clearViewEl(true);
            newRows = view.doAdd(range, start);
            view.fireItemMutationEvent('itemadd', range, start, newRows, view);
            // Keep other side's rendered block the same
            if (lockingPartner) {
                partnerView.clearViewEl(true);
                partnerNewRows = partnerView.doAdd(range, start);
                partnerView.fireItemMutationEvent('itemadd', range, start, partnerNewRows, partnerView);
                // We're going to be doing measurement of newRows
                // Ensure heights are synced first
                if (doSyncRowHeight) {
                    me.syncRowHeights(newRows, partnerNewRows);
                    doSyncRowHeight = false;
                }
            }
            for (i = 0; i < topAdditionSize; i++) {
                increment -= me.grid.getElementHeight(newRows[i]);
            }
            // We've just added a bunch of rows to the top of our range,
            // so move upwards to keep the row appearance stable
            newTop = me.bodyTop + increment;
        } else {
            // No overlapping nodes; we'll need to render the whole range.
            // teleported flag is set in getFirstVisibleRowIndex/getLastVisibleRowIndex if
            // the table body has moved outside the viewport bounds
            noOverlap = me.teleported || start > rows.endIndex || end < rows.startIndex;
            if (noOverlap) {
                view.clearViewEl(true);
                me.teleported = false;
            }
            if (!rows.getCount()) {
                newRows = view.doAdd(range, start);
                view.fireItemMutationEvent('itemadd', range, start, newRows, view);
                // Keep other side's rendered block the same
                if (lockingPartner) {
                    partnerView.clearViewEl(true);
                    partnerNewRows = lockingPartner.view.doAdd(range, start);
                    partnerView.fireItemMutationEvent('itemadd', range, start, partnerNewRows, partnerView);
                }
                newTop = calculatedTop;
                // Adjust the bodyTop to place the data correctly around the scroll vieport
                if (noOverlap && variableRowHeight) {
                    topBufferZone = me.scrollTop < me.position ? me.leadingBufferZone : me.trailingBufferZone;
                    // Can't calculate a new top if there are fewer than topBufferZone rows above us
                    if (start > topBufferZone) {
                        // eslint-disable-next-line max-len
                        newTop = Math.max(me.scrollTop - rows.item(rows.startIndex + topBufferZone - 1, true).offsetTop, 0);
                    }
                }
            }
            // Moved down the dataset (content moved up): remove rows from top, add to end
            else if (end > rows.endIndex) {
                removeCount = Math.max(start - rows.startIndex, 0);
                // We only have to bump the table down by the height of removed rows
                // if rows are not a standard size
                if (variableRowHeight) {
                    increment = rows.item(rows.startIndex + removeCount, true).offsetTop;
                }
                records = Ext.Array.slice(range, rows.endIndex + 1 - start);
                newRows = rows.scroll(records, 1, removeCount);
                if (lockingPartner) {
                    partnerNewRows = partnerRows.scroll(records, 1, removeCount);
                }
                // We only have to bump the table down by the height of removed rows
                // if rows are not a standard size
                if (variableRowHeight) {
                    // Bump the table downwards by the height scraped off the top
                    newTop = me.bodyTop + increment;
                } else // If the rows are standard size, then the calculated top will be correct
                {
                    newTop = calculatedTop;
                }
            } else // Moved up the dataset: remove rows from end, add to top
            {
                removeCount = Math.max(rows.endIndex - end, 0);
                oldStart = rows.startIndex;
                records = Ext.Array.slice(range, 0, rows.startIndex - start);
                newRows = rows.scroll(records, -1, removeCount);
                if (lockingPartner) {
                    partnerNewRows = partnerRows.scroll(records, -1, removeCount);
                }
                // We only have to bump the table up by the height of top-added rows if
                // rows are not a standard size. If they are standard, calculatedTop is correct.
                // Sync the row heights *before* calculating the newTop and increment
                if (doSyncRowHeight) {
                    me.syncRowHeights(newRows, partnerNewRows);
                    doSyncRowHeight = false;
                    // Bump the table upwards by the height added to the top
                    newTop = me.bodyTop - rows.item(oldStart, true).offsetTop;
                    // We've arrived at row zero...
                    if (!rows.startIndex) {
                        // But the calculated top position is out. It must be zero at this point
                        // We adjust the scroll position to keep visual position of table the same.
                        if (newTop) {
                            me.doVerticalScroll(scroller, me.scrollTop -= newTop);
                            newTop = 0;
                        }
                    }
                    // Not at zero yet, but the position has moved into negative range
                    else if (newTop < 0) {
                        increment = rows.startIndex * me.rowHeight;
                        me.doVerticalScroll(scroller, me.scrollTop += increment);
                        newTop = me.bodyTop + increment;
                    }
                } else // If the rows are standard size, then the calculated top will be correct
                {
                    newTop = calculatedTop;
                }
            }
            // The position property is the scrollTop value *at which the table was last correct*
            // MUST be set at table render/adjustment time
            me.position = me.scrollTop;
        }
        // A view contained focus at the start, check whether activeEl has been derendered.
        // Focus the cell's column header if so.
        if (focusedView) {
            // Restore active element's focus processing.
            activeEl.resumeFocusEvents();
            if (!focusedView.el.contains(activeEl)) {
                pos = focusedView.actionableMode ? focusedView.actionPosition : focusedView.lastFocused;
                if (pos && pos.column) {
                    // we set the rendering rows to true here so the actionables know
                    // that view is forcing the onFocusLeave method here
                    focusedView.renderingRows = true;
                    focusedView.onFocusLeave({});
                    focusedView.renderingRows = false;
                    me.getNewFocusTarget(pos).focus();
                }
            }
        }
        // Calculate position of item container.
        newTop = Math.max(Math.floor(newTop), 0);
        if (view.positionBody) {
            me.setBodyTop(newTop, true);
        }
        // Sync the other side to exactly the same range from the dataset.
        // Then ensure that we are still at exactly the same scroll position.
        if (lockingPartner) {
            // Locking partner BufferedRenderer must not react to the scroll.
            lockingPartner.scrollTop = me.scrollTop;
            if (lockingPartner.bodyTop !== newTop) {
                lockingPartner.setBodyTop(newTop, true);
            }
            if (doSyncRowHeight) {
                me.syncRowHeights(newRows, partnerNewRows);
            }
        } else if (variableRowHeight) {
            delete me.rowHeight;
            me.refreshSize();
        }
        // If there are columns to trigger rendering, and the rendered block
        // is not either the view size or, if store count less than view size,
        // the store count, then there's a bug.
        if (view.getVisibleColumnManager().getColumns().length && rows.getCount() !== Math.min(me.store.getCount(), me.viewSize)) {
            Ext.raise('rendered block refreshed at ' + rows.getCount() + ' rows while BufferedRenderer view size is ' + me.viewSize);
        }
        return newRows;
    },
    /**
     * Gets the next focus target based on the position
     * @param {Ext.grid.CellContext} pos
     * @returns {Ext.Component}
     * @since 6.2.2
     */
    getNewFocusTarget: function(pos) {
        var view = pos.view,
            grid = view.grid,
            column = pos.column,
            hiddenHeaders = column.isHidden() || grid.hideHeaders,
            tabbableItems;
        // Focus MUST NOT silently die due to DOM removal. Focus will be moved
        // in the following order as available:
        // Try focusing the contextual column header
        if (column.focusable && !hiddenHeaders) {
            return column;
        }
        tabbableItems = column.el.findTabbableElements();
        // Failing that, look inside it for a tabbable element
        if (tabbableItems && tabbableItems.length) {
            return tabbableItems[0];
        }
        // Failing that, find the available focus target of the grid or focus the view
        return grid.findFocusTarget() || view.el;
    },
    syncRowHeights: function(itemEls, partnerItemEls) {
        var me = this,
            ln = 0,
            otherLn = 1,
            // Different initial values so that all items are synched
            mySynchronizer = [],
            otherSynchronizer = [],
            RowSynchronizer = Ext.grid.locking.RowSynchronizer,
            i, rowSync;
        if (itemEls && partnerItemEls) {
            ln = itemEls.length;
            otherLn = partnerItemEls.length;
        }
        // The other side might not quite by in scroll sync with us, in which case
        // it may have gone a different path way and rolled some rows into
        // the rendered block where we may have re-rendered the whole thing.
        // If this has happened, fall back to syncing all rows.
        if (ln !== otherLn) {
            itemEls = me.view.all.slice();
            partnerItemEls = me.view.lockingPartner.all.slice();
            ln = otherLn = itemEls.length;
        }
        for (i = 0; i < ln; i++) {
            mySynchronizer[i] = rowSync = new RowSynchronizer(me.view, itemEls[i]);
            rowSync.measure();
        }
        for (i = 0; i < otherLn; i++) {
            otherSynchronizer[i] = rowSync = new RowSynchronizer(me.view.lockingPartner, partnerItemEls[i]);
            rowSync.measure();
        }
        for (i = 0; i < ln; i++) {
            mySynchronizer[i].finish(otherSynchronizer[i]);
            otherSynchronizer[i].finish(mySynchronizer[i]);
        }
        // Ensure that both BufferedRenderers have the same idea about scroll range and row height
        me.syncRowHeightsFinish();
    },
    syncRowHeightsFinish: function() {
        var me = this,
            view = me.view,
            lockingPartner = view.lockingPartner.bufferedRenderer,
            ownerGrid = view.ownerGrid,
            scrollable = view.getScrollable();
        ownerGrid.syncRowHeightOnNextLayout = false;
        // Now that row heights have potentially changed, both BufferedRenderers
        // have to re-evaluate what they think the average rowHeight is
        // based on the synchronized-height rows.
        //
        // If the view has not been layed out, then the upcoming first resize event
        // will trigger the needed refreshSize call; See onViewRefresh -
        // If control arrives there and the componentLayoutCounter is zero and
        // there is variableRowHeight, it schedules itself to be run on boxready
        // so refreshSize will be called there for the first time.
        if (view.componentLayoutCounter) {
            delete me.rowHeight;
            me.refreshSize();
            delete lockingPartner.rowHeight;
            lockingPartner.refreshSize();
        }
        // Component layout only restores the scroller's state for managed layouts
        // here we need to make sure the scroller is restores after the rows sync
        if (scrollable) {
            scrollable.restoreState();
        }
    },
    setBodyTop: function(bodyTop, skipStretchView) {
        var me = this,
            view = me.view,
            rows = view.all,
            store = me.store,
            body = view.body;
        if (!body.dom) {
            // The view may be rendered, but the body element not attached.
            return;
        }
        me.translateBody(body, bodyTop);
        // If this is the last page, correct the scroll range to be just enough to fit.
        if (me.variableRowHeight) {
            me.bodyHeight = me.grid.getElementHeight(body.dom);
            // We are displaying the last row, so ensure the scroll range
            // finishes exactly at the bottom of the view body
            if (rows.endIndex === store.getCount() - 1) {
                me.scrollHeight = bodyTop + me.bodyHeight - 1;
            } else // Not last row - recalculate scroll range
            {
                me.scrollHeight = me.getScrollHeight();
            }
            if (!skipStretchView) {
                me.stretchView(view, me.scrollHeight);
            }
        } else {
            // If we have fixed row heights, calculate rendered block height
            // without forcing a layout
            me.bodyHeight = rows.getCount() * me.rowHeight;
        }
    },
    translateBody: function(body, bodyTop) {
        body.translate(null, this.bodyTop = bodyTop);
    },
    getFirstVisibleRowIndex: function(startRow, endRow, viewportTop, viewportBottom) {
        var me = this,
            view = me.view,
            rows = view.all,
            elements = rows.elements,
            clientHeight = me.viewClientHeight,
            target, targetTop,
            bodyTop = me.bodyTop;
        // If variableRowHeight, we have to search for the first row who's bottom edge
        // is within the viewport
        if (rows.getCount() && me.variableRowHeight) {
            if (!arguments.length) {
                startRow = rows.startIndex;
                endRow = rows.endIndex;
                viewportTop = me.scrollTop;
                viewportBottom = viewportTop + clientHeight;
                // Teleported so that body is outside viewport: Use rowHeight calculation
                if (bodyTop > viewportBottom || bodyTop + me.bodyHeight < viewportTop) {
                    me.teleported = true;
                    return Math.floor(me.scrollTop / me.rowHeight);
                }
                // In first, non-recursive call, begin targeting the most likely first row
                target = startRow + Math.min(me.numFromEdge + ((me.lastScrollDirection === -1) ? me.leadingBufferZone : me.trailingBufferZone), Math.floor((endRow - startRow) / 2));
            } else {
                if (startRow === endRow) {
                    return endRow;
                }
                target = startRow + Math.floor((endRow - startRow) / 2);
            }
            targetTop = bodyTop + elements[target].offsetTop;
            // If target is entirely above the viewport, chop downwards
            if (targetTop + me.grid.getElementHeight(elements[target]) <= viewportTop) {
                return me.getFirstVisibleRowIndex(target + 1, endRow, viewportTop, viewportBottom);
            }
            // Target is first
            if (targetTop <= viewportTop) {
                return target;
            }
            // Not narrowed down to 1 yet; chop upwards
            else if (target !== startRow) {
                return me.getFirstVisibleRowIndex(startRow, target - 1, viewportTop, viewportBottom);
            }
        }
        return Math.floor(me.scrollTop / me.rowHeight);
    },
    /**
     * Returns the index of the last row in your table view deemed to be visible.
     * @return {Number}
     * @private
     */
    getLastVisibleRowIndex: function(startRow, endRow, viewportTop, viewportBottom) {
        var me = this,
            view = me.view,
            rows = view.all,
            elements = rows.elements,
            clientHeight = me.viewClientHeight,
            target, targetTop, targetBottom,
            bodyTop = me.bodyTop;
        // If variableRowHeight, we have to search for the first row who's bottom edge
        // is below the bottom of the viewport
        if (rows.getCount() && me.variableRowHeight) {
            if (!arguments.length) {
                startRow = rows.startIndex;
                endRow = rows.endIndex;
                viewportTop = me.scrollTop;
                viewportBottom = viewportTop + clientHeight;
                // Teleported so that body is outside viewport: Use rowHeight calculation
                if (bodyTop > viewportBottom || bodyTop + me.bodyHeight < viewportTop) {
                    me.teleported = true;
                    return Math.floor(me.scrollTop / me.rowHeight) + Math.ceil(clientHeight / me.rowHeight);
                }
                // In first, non-recursive call, begin targeting the most likely last row
                target = endRow - Math.min(me.numFromEdge + ((me.lastScrollDirection === 1) ? me.leadingBufferZone : me.trailingBufferZone), Math.floor((endRow - startRow) / 2));
            } else {
                if (startRow === endRow) {
                    return endRow;
                }
                target = startRow + Math.floor((endRow - startRow) / 2);
            }
            targetTop = bodyTop + elements[target].offsetTop;
            // If target is entirely below the viewport, chop upwards
            if (targetTop > viewportBottom) {
                return me.getLastVisibleRowIndex(startRow, target - 1, viewportTop, viewportBottom);
            }
            targetBottom = targetTop + me.grid.getElementHeight(elements[target]);
            // Target is last
            if (targetBottom >= viewportBottom) {
                return target;
            }
            // Not narrowed down to 1 yet; chop downwards
            else if (target !== endRow) {
                return me.getLastVisibleRowIndex(target + 1, endRow, viewportTop, viewportBottom);
            }
        }
        return Math.min(me.getFirstVisibleRowIndex() + Math.ceil(clientHeight / me.rowHeight), rows.endIndex);
    },
    getScrollHeight: function() {
        var me = this,
            view = me.view,
            rows = view.all,
            store = me.store,
            recCount = store.getCount(),
            rowCount = rows.getCount(),
            row, rowHeight, borderWidth, scrollHeight;
        if (!recCount) {
            return 0;
        }
        if (!me.hasOwnProperty('rowHeight')) {
            if (rowCount) {
                if (me.variableRowHeight) {
                    me.rowHeight = Math.floor(me.bodyHeight / rowCount);
                } else {
                    row = rows.first();
                    rowHeight = row.getHeight();
                    // In IE8 we're adding bottom border on all the rows to work around
                    // the lack of :last-child selector, and we compensate that by setting
                    // a negative top margin that equals the border width, so that top and
                    // bottom borders overlap on adjacent rows. Negative margin does not
                    // affect the row's reported height though so we have to compensate
                    // for that effectively invisible additional border width here.
                    if (Ext.isIE8) {
                        borderWidth = row.getBorderWidth('b');
                        if (borderWidth > 0) {
                            rowHeight -= borderWidth;
                        }
                    }
                    me.rowHeight = rowHeight;
                }
            } else {
                delete me.rowHeight;
            }
        }
        if (me.variableRowHeight) {
            // If this is the last page, ensure the scroll range is exactly enough
            // to scroll to the end of the rendered block.
            if (rows.endIndex === recCount - 1) {
                scrollHeight = me.bodyTop + me.bodyHeight - 1;
            } else // Calculate the scroll range based upon measured row height and our scrollPosition.
            {
                scrollHeight = Math.floor((recCount - rowCount) * me.rowHeight) + me.bodyHeight;
                // If there's a discrepancy between the boy position we have scrolled to,
                // and the calculated position, account for that in the scroll range
                // so that we have enough range to scroll all the data into view.
                scrollHeight += me.bodyTop - rows.startIndex * me.rowHeight;
            }
        } else {
            scrollHeight = Math.floor(recCount * me.rowHeight);
        }
        return (me.scrollHeight = scrollHeight);
    },
    getThemeRowHeight: function() {
        var me = this,
            testEl;
        if (!me.themeRowHeight) {
            testEl = Ext.getBody().createChild({
                cls: Ext.baseCSSPrefix + 'theme-row-height-el'
            });
            me.self.prototype.themeRowHeight = testEl.dom.offsetHeight;
            testEl.destroy();
        }
        return me.themeRowHeight;
    },
    attemptLoad: function(start, end, loadScrollPosition) {
        var me = this;
        if (me.scrollToLoadBuffer) {
            if (!me.loadTask) {
                me.loadTask = new Ext.util.DelayedTask();
            }
            me.loadTask.delay(me.scrollToLoadBuffer, me.doAttemptLoad, me, [
                start,
                end,
                loadScrollPosition
            ]);
        } else {
            me.doAttemptLoad(start, end, loadScrollPosition);
        }
    },
    cancelLoad: function() {
        if (this.loadTask) {
            this.loadTask.cancel();
        }
    },
    doAttemptLoad: function(start, end, loadScrollPosition) {
        var me = this;
        // If we were called on a delay, check for destruction
        if (!me.destroyed) {
            me.store.getRange(start, end, {
                loadId: ++me.loadId,
                callback: function(range, start, end, options) {
                    // If our loadId position has not changed since the getRange request started,
                    // we can continue to render.
                    // If the scroll position is different to the scroll position which triggered
                    // the load, ignore it - we don't need the data any more.
                    if (options.loadId === me.loadId && me.scrollTop === loadScrollPosition) {
                        me.onRangeFetched(range, start, end);
                    }
                },
                fireEvent: false
            });
        }
    },
    destroy: function() {
        var me = this;
        me.cancelLoad();
        if (me.store) {
            me.unbindStore();
        }
        // Remove listeners from old grid, view and store
        Ext.destroy(me.viewListeners, me.stretcher, me.gridListeners, me.scrollListeners);
        me.callParent();
    }
});

/**
 * This class provides an abstract grid editing plugin on selected
 * {@link Ext.grid.column.Column columns}. The editable columns are specified by providing an
 * {@link Ext.grid.column.Column#editor editor} in the
 * {@link Ext.grid.column.Column column configuration}.
 *
 * **Note:** This class should not be used directly. See {@link Ext.grid.plugin.CellEditing} and
 * {@link Ext.grid.plugin.RowEditing}.
 */
Ext.define('Ext.grid.plugin.Editing', {
    extend: 'Ext.plugin.Abstract',
    alias: 'editing.editing',
    requires: [
        'Ext.grid.column.Column',
        'Ext.util.KeyNav',
        // Requiring Ext.form.field.Base and Ext.view.Table ensures that grid editor sass
        // variables can derive from both form field vars and grid vars in the neutral theme
        'Ext.form.field.Base',
        'Ext.view.Table'
    ],
    mixins: [
        'Ext.mixin.Observable'
    ],
    /**
     * @cfg {Number} clicksToEdit
     * The number of clicks on a grid required to display the editor.
     * The only accepted values are **1** and **2**.
     */
    clicksToEdit: 2,
    /**
     * @cfg {String} triggerEvent
     * The event which triggers editing. Supersedes the {@link #clicksToEdit} configuration.
     * May be one of:
     *
     *  * cellclick
     *  * celldblclick
     *  * cellfocus
     *  * rowfocus
     */
    triggerEvent: undefined,
    /**
     * @property {Boolean} editing
     * Set to `true` while the editing plugin is active and an Editor is visible.
     */
    relayedEvents: [
        'beforeedit',
        'edit',
        'validateedit',
        'canceledit'
    ],
    /**
     * @cfg {String} default UI for editor fields
     */
    defaultFieldUI: 'default',
    defaultFieldXType: 'textfield',
    // cell, row, form
    editStyle: '',
    /**
     * @event beforeedit
     * Fires before editing is triggered. Return false from event handler to stop the editing.
     *
     * @param {Ext.grid.plugin.Editing} editor
     * @param {Object} context The editing context with the following properties:
     * @param {Ext.grid.Panel} context.grid The owning grid Panel.
     * @param {Ext.data.Model} context.record The record being edited.
     * @param {String} context.field The name of the field being edited.
     * @param {Mixed} context.value The field's current value.
     * @param {HTMLElement} context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The Column being edited.
     * @param {Number} context.rowIdx The index of the row being edited.
     * @param {Number} context.colIdx The index of the column being edited.
     * @param {Boolean} context.cancel Set this to `true` to cancel the edit or return false
     * from your handler.
     * @param {Mixed} context.originalValue Alias for value (only when using
     * {@link Ext.grid.plugin.CellEditing CellEditing}).
     */
    /**
     * @event edit
     * Fires after editing. Usage example:
     *
     *     grid.on('edit', function(editor, e) {
     *         // commit the changes right after editing finished
     *         e.record.commit();
     *     });
     *
     * @param {Ext.grid.plugin.Editing} editor
     * @param {Object} context The editing context with the following properties:
     * @param {Ext.grid.Panel} context.grid The owning grid Panel.
     * @param {Ext.data.Model} context.record The record being edited.
     * @param {String} context.field The name of the field being edited.
     * @param {Mixed} context.value The field's current value.
     * @param {HTMLElement} context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The Column being edited.
     * @param {Number} context.rowIdx The index of the row being edited.
     * @param {Number} context.colIdx The index of the column being edited.
     */
    /**
     * @event validateedit
     * Fires after editing, but before the value is set in the record. Return false
     * from event handler to cancel the change.
     *
     * Usage example showing how to remove the red triangle (dirty record indicator)
     * from some records (not all). By observing the grid's validateedit event, it can be cancelled
     * if the edit occurs on a targeted row (for example) and then setting the field's new value
     * in the Record directly:
     *
     *     grid.on('validateedit', function (editor, context) {
     *         var myTargetRow = 6;
     *
     *         if (context.rowIdx === myTargetRow) {
     *             context.record.data[context.field] = context.value;
     *         }
     *     });
     *
     * @param {Ext.grid.plugin.Editing} editor
     * @param {Object} context The editing context with the following properties:
     * @param {Ext.grid.Panel} context.grid The owning grid Panel.
     * @param {Ext.data.Model} context.record The record being edited.
     * @param {String} context.field The name of the field being edited.
     * @param {Mixed} context.value The field's current value.
     * @param {HTMLElement} context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The Column being edited.
     * @param {Number} context.rowIdx The index of the row being edited.
     * @param {Number} context.colIdx The index of the column being edited.
     */
    /**
     * @event canceledit
     * Fires when the user started editing but then cancelled the edit.
     * @param {Ext.grid.plugin.Editing} editor
     * @param {Object} context The editing context with the following properties:
     * @param {Ext.grid.Panel} context.grid The owning grid Panel.
     * @param {Ext.data.Model} context.record The record being edited.
     * @param {String} context.field The name of the field being edited.
     * @param {Mixed} context.value The field's current value.
     * @param {HTMLElement} context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The Column being edited.
     * @param {Number} context.rowIdx The index of the row being edited.
     * @param {Number} context.colIdx The index of the column being edited.
     */
    constructor: function(config) {
        var me = this;
        me.callParent([
            config
        ]);
        me.mixins.observable.constructor.call(me);
        // TODO: Deprecated, remove in 5.0
        me.on("edit", function(editor, e) {
            me.fireEvent("afteredit", editor, e);
        });
    },
    init: function(grid) {
        var me = this,
            ownerLockable = grid.ownerLockable;
        me.grid = grid;
        me.view = grid.view;
        me.initEvents();
        // Set up fields at render and reconfigure time
        if (grid.rendered) {
            me.setup();
        } else {
            me.mon(grid, {
                beforereconfigure: me.onBeforeReconfigure,
                reconfigure: me.onReconfigure,
                scope: me,
                beforerender: {
                    fn: me.onBeforeRender,
                    single: true,
                    scope: me
                }
            });
        }
        grid.editorEventRelayers = grid.relayEvents(me, me.relayedEvents);
        // If the editable grid is owned by a lockable, relay up another level.
        if (ownerLockable) {
            ownerLockable.editorEventRelayers = ownerLockable.relayEvents(me, me.relayedEvents);
        }
        // Marks the grid as editable, so that the SelectionModel
        // can make appropriate decisions during navigation
        grid.isEditable = true;
        grid.editingPlugin = grid.view.editingPlugin = me;
    },
    onBeforeReconfigure: function() {
        this.reconfiguring = true;
    },
    /**
     * Fires after the grid is reconfigured
     * @protected
     */
    onReconfigure: function() {
        this.setup();
        delete this.reconfiguring;
    },
    onBeforeRender: function() {
        this.setup();
    },
    setup: function() {
        // In a Lockable assembly, the owner's view aggregates all grid columns across both sides.
        // We grab all columns here.
        this.initFieldAccessors(this.grid.getTopLevelColumnManager().getColumns());
    },
    destroy: function() {
        var me = this,
            grid = me.grid;
        Ext.destroy(me.keyNav);
        // Clear all listeners from all our events, clear all managed listeners we added
        // to other Observables
        me.clearListeners();
        if (grid) {
            if (grid.ownerLockable) {
                Ext.destroy(grid.ownerLockable.editorEventRelayers);
                grid.ownerLockable.editorEventRelayers = null;
            }
            Ext.destroy(grid.editorEventRelayers);
            grid.editorEventRelayers = null;
            grid.editingPlugin = grid.view.editingPlugin = null;
        }
        me.callParent();
    },
    getEditStyle: function() {
        return this.editStyle;
    },
    initFieldAccessors: function(columns) {
        // If we have been passed a group header, process its leaf headers
        if (columns.isGroupHeader) {
            columns = columns.getGridColumns();
        }
        // Ensure we are processing an array
        else if (!Ext.isArray(columns)) {
            columns = [
                columns
            ];
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            c,
            cLen = columns.length,
            getEditor = function(record, defaultField) {
                return me.getColumnField(this, defaultField);
            },
            hasEditor = function() {
                return me.hasColumnField(this);
            },
            setEditor = function(field) {
                me.setColumnField(this, field);
            },
            column;
        for (c = 0; c < cLen; c++) {
            column = columns[c];
            if (!column.getEditor) {
                column.getEditor = getEditor;
            }
            if (!column.hasEditor) {
                column.hasEditor = hasEditor;
            }
            if (!column.setEditor) {
                column.setEditor = setEditor;
            }
        }
    },
    removeFieldAccessors: function(columns) {
        // If we have been passed a group header, process its leaf headers
        if (columns.isGroupHeader) {
            columns = columns.getGridColumns();
        }
        // Ensure we are processing an array
        else if (!Ext.isArray(columns)) {
            columns = [
                columns
            ];
        }
        // eslint-disable-next-line vars-on-top
        var c,
            cLen = columns.length,
            column;
        for (c = 0; c < cLen; c++) {
            column = columns[c];
            column.getEditor = column.hasEditor = column.setEditor = column.field = column.editor = null;
        }
    },
    getColumnField: function(columnHeader, defaultField) {
        // remaps to the public API of Ext.grid.column.Column.getEditor
        var me = this,
            field = columnHeader.field;
        if (!(field && field.isFormField)) {
            field = columnHeader.field = me.createColumnField(columnHeader, defaultField);
        }
        if (field && field.ui === 'default' && !field.hasOwnProperty('ui')) {
            field.ui = me.defaultFieldUI;
        }
        return field;
    },
    hasColumnField: function(columnHeader) {
        // remaps to the public API of Ext.grid.column.Column.hasEditor
        return !!(columnHeader.field && columnHeader.field.isComponent);
    },
    setColumnField: function(columnHeader, field) {
        // remaps to the public API of Ext.grid.column.Column.setEditor
        columnHeader.field = field;
        columnHeader.field = this.createColumnField(columnHeader);
    },
    createColumnField: function(column, defaultField) {
        var field = column.field,
            dataIndex;
        if (!field && column.editor) {
            // Protect the column's editor propwerty from the mutation we are going
            // to be doing here.
            field = column.editor = Ext.clone(column.editor);
            // Allow for this kind of setup when CellEditing is being used, and the field
            // is wrapped in a CellEditor. They might need to configure the CellEditor.
            //    editor: {
            //        completeOnEnter: false,
            //        field: {
            //            xtype: 'combobox'
            //        }
            //    }
            if (field.field) {
                field = field.field;
                field.editorCfg = column.editor;
                delete field.editorCfg.field;
            }
            column.editor = null;
        }
        if (!field && defaultField) {
            field = defaultField;
        }
        if (field) {
            dataIndex = column.dataIndex;
            if (field.isComponent) {
                field.column = column;
            } else {
                if (Ext.isString(field)) {
                    field = {
                        name: dataIndex,
                        xtype: field,
                        column: column
                    };
                } else {
                    field = Ext.apply({
                        name: dataIndex,
                        column: column
                    }, field);
                }
                field = Ext.ComponentManager.create(field, this.defaultFieldXType);
            }
            // Stamp on the dataIndex which will serve as a reliable lookup regardless
            // of how the editor was defined (as a config or as an existing component).
            // See EXTJSIV-11650.
            field.dataIndex = dataIndex;
            field.isEditorComponent = true;
            column.field = field;
        }
        return field;
    },
    initEvents: function() {
        var me = this;
        me.initEditTriggers();
        me.initCancelTriggers();
    },
    initCancelTriggers: Ext.emptyFn,
    initEditTriggers: function() {
        var me = this,
            view = me.view;
        // Listen for the edit trigger event.
        if (me.triggerEvent === 'cellfocus') {
            me.mon(view, 'cellfocus', me.onCellFocus, me);
        } else if (me.triggerEvent === 'rowfocus') {
            me.mon(view, 'rowfocus', me.onRowFocus, me);
        } else {
            // Prevent the View from processing when the SelectionModel focuses.
            // This is because the SelectionModel processes the mousedown event, and
            // focusing causes a scroll which means that the subsequent mouseup might
            // take place at a different document XY position, and will therefore
            // not trigger a click.
            // This Editor must call the View's focusCell method directly when we
            // receive a request to edit
            if (view.getSelectionModel().isCellModel) {
                view.onCellFocus = me.beforeViewCellFocus.bind(me);
            }
            // Listen for whichever click event we are configured to use
            me.mon(view, me.triggerEvent || ('cell' + (me.clicksToEdit === 1 ? 'click' : 'dblclick')), me.onCellClick, me);
        }
        // add/remove header event listeners need to be added immediately because
        // columns can be added/removed before render
        me.initAddRemoveHeaderEvents();
        // Attach new bindings to the View's NavigationModel which processes cellkeydown events.
        me.view.getNavigationModel().addKeyBindings({
            esc: me.onEscKey,
            defaultEventAction: false,
            scope: me
        });
    },
    // Override of View's method so that we can pre-empt the View's processing if the view
    // is being triggered by a mousedown
    beforeViewCellFocus: function(position) {
        // Pass call on to view if the navigation is from the keyboard,
        // or we are not going to edit this cell.
        if (this.view.selModel.keyNavigation || !this.editing || !this.isCellEditable || !this.isCellEditable(position.row, position.columnHeader)) {
            this.view.focusCell.apply(this.view, arguments);
        }
    },
    onRowFocus: function(record, row, rowIdx) {
        // Used if we are triggered by the rowfocus event
        this.startEdit(row, 0);
    },
    onCellFocus: function(record, cell, position) {
        // Used if we are triggered by the cellfocus event
        this.startEdit(position.row, position.column);
    },
    onCellClick: function(view, cell, colIdx, record, row, rowIdx, e) {
        // Used if we are triggered by a cellclick event
        // *IMPORTANT* Due to V4.0.0 history, the colIdx here is the index within ALL columns,
        // including hidden.
        //
        // Make sure that the column has an editor.  In the case of CheckboxModel,
        // calling startEdit doesn't make sense when the checkbox is clicked.
        // Also, cancel editing if the element that was clicked was a tree expander.
        var ownerGrid = view.ownerGrid,
            expanderSelector = view.expanderSelector,
            // Use getColumnManager() in this context because colIdx includes hidden columns.
            columnHeader = view.ownerCt.getColumnManager().getHeaderAtIndex(colIdx),
            editor = columnHeader.getEditor(record),
            targetCmp;
        if (this.shouldStartEdit(editor) && (!expanderSelector || !e.getTarget(expanderSelector))) {
            ownerGrid.setActionableMode(true, e.position);
        }
        // Clicking on a component in a widget column
        else if (ownerGrid.actionableMode && view.owns(e.target) && (targetCmp = Ext.Component.from(e, cell)) && targetCmp.focusable) {
            return;
        }
        // The cell is not actionable, we we must exit actionable mode
        else if (ownerGrid.actionableMode) {
            ownerGrid.setActionableMode(false);
        }
    },
    initAddRemoveHeaderEvents: function() {
        var me = this,
            headerCt = me.grid.headerCt;
        me.mon(headerCt, {
            scope: me,
            add: me.onColumnAdd,
            columnmove: me.onColumnMove,
            beforedestroy: me.beforeGridHeaderDestroy
        });
    },
    onColumnAdd: function(ct, column) {
        this.initFieldAccessors(column);
    },
    // Template method which may be implemented in subclasses (RowEditing and CellEditing)
    onColumnMove: Ext.emptyFn,
    onEscKey: function(e) {
        var targetComponent;
        if (this.editing) {
            targetComponent = Ext.getCmp(e.getTarget().getAttribute('componentId'));
            // ESCAPE when a picker is expanded does not cancel the edit
            if (!(targetComponent && targetComponent.isPickerField && targetComponent.isExpanded)) {
                e.stopEvent();
                return this.cancelEdit();
            }
        }
    },
    /**
     * @method
     * @private
     * @template
     * Template method called before editing begins.
     * @param {Object} context The current editing context
     * @return {Boolean} Return false to cancel the editing process
     */
    beforeEdit: Ext.emptyFn,
    shouldStartEdit: function(editor) {
        return !!editor;
    },
    /**
     * @private
     * Collects all information necessary for any subclasses to perform their editing functions.
     * @param {Ext.data.Model/Number} record The record or record index to edit.
     * @param {Ext.grid.column.Column/Number} columnHeader The column of column index to edit.
     * @param {Boolean} horizontalScroll True to scroll horizontally and display the Cell
     * in the editing context
     * @param {Ext.view.Table} view The view to get the context from (only useful
     * with lockable grids).
     * @return {Ext.grid.CellContext/undefined} The editing context based upon the passed record
     * and column
     */
    getEditingContext: function(record, columnHeader, horizontalScroll, view) {
        var me = this,
            grid = me.grid,
            colMgr = ((view && view.grid) || grid).visibleColumnManager,
            layoutView = me.grid.lockable ? me.grid : me.view,
            gridRow, rowIdx, colIdx, result;
        // The view must have had a layout to show the editor correctly, defer until that time.
        // In case a grid's startup code invokes editing immediately.
        if (!layoutView.componentLayoutCounter) {
            layoutView.on({
                boxready: Ext.Function.bind(me.startEdit, me, [
                    record,
                    columnHeader
                ]),
                single: true
            });
            return;
        }
        // If disabled or grid collapsed, or view not truly visible, don't calculate a context -
        // we cannot edit
        if (me.disabled || me.grid.collapsed || !me.grid.view.isVisible(true)) {
            return;
        }
        // They've asked to edit by column number.
        // Note that in a locked grid, the columns are enumerated in a unified set for this purpose.
        if (Ext.isNumber(columnHeader)) {
            columnHeader = colMgr.getHeaderAtIndex(Math.min(columnHeader, colMgr.getColumns().length));
        }
        // No corresponding column. Possible if all columns have been moved to the other side
        // of a lockable grid pair
        if (!columnHeader) {
            return;
        }
        // Coerce the column to the closest visible column
        if (columnHeader.hidden) {
            columnHeader = columnHeader.next(':not([hidden])') || columnHeader.prev(':not([hidden])');
        }
        // Navigate to the view and grid which the column header relates to.
        if (!view) {
            view = columnHeader.getView();
        }
        grid = view.ownerCt;
        if (Ext.isNumber(record)) {
            rowIdx = Math.min(record, view.dataSource.getCount() - 1);
            record = view.dataSource.getAt(rowIdx);
        } else {
            rowIdx = view.dataSource.indexOf(record);
        }
        // Ensure the row we want to edit is in the rendered range if the view is buffer rendered
        grid.ensureVisible(record, {
            column: horizontalScroll ? columnHeader : null
        });
        gridRow = view.getRow(record);
        // An intervening listener may have deleted the Record.
        if (!gridRow) {
            return;
        }
        // Column index must be relative to the View the Context is using.
        // It must be the real owning View, NOT the lockable pseudo view.
        colIdx = view.getVisibleColumnManager().indexOf(columnHeader);
        // The record may be removed from the store but the view
        // not yet updated, so check it exists
        if (!record) {
            return;
        }
        // Create a new CellContext
        result = new Ext.grid.CellContext(view).setAll(view, rowIdx, colIdx, record, columnHeader);
        // Add extra Editing information
        result.grid = grid;
        result.store = view.dataSource;
        result.field = columnHeader.dataIndex;
        result.value = result.originalValue = record.get(columnHeader.dataIndex);
        result.row = gridRow;
        result.node = view.getNode(record);
        result.cell = result.getCell(true);
        return result;
    },
    /**
     * Cancels any active edit that is in progress.
     */
    cancelEdit: function() {
        var me = this;
        me.editing = false;
        me.fireEvent('canceledit', me, me.context);
    },
    /**
     * Completes the edit if there is an active edit in progress.
     */
    completeEdit: function() {
        var me = this;
        if (me.editing && me.validateEdit()) {
            me.fireEvent('edit', me, me.context);
        }
        me.context = null;
        me.editing = false;
    },
    validateEdit: function(context) {
        var me = this;
        return me.fireEvent('validateedit', me, context) !== false && !context.cancel;
    }
});

/**
 * The Ext.grid.plugin.CellEditing plugin injects editing at a cell level for a Grid. Only a single
 * cell will be editable at a time. The field that will be used for the editor is defined at the
 * {@link Ext.grid.column.Column#editor editor}. The editor can be a field instance or a field
 * configuration.
 *
 * If an editor is not specified for a particular column then that cell will not be editable
 * and it will be skipped when activated via the mouse or the keyboard.
 *
 * The editor may be shared for each column in the grid, or a different one may be specified
 * for each column. An appropriate field type should be chosen to match the data structure
 * that it will be editing. For example, to edit a date, it would be useful to specify
 * {@link Ext.form.field.Date} as the editor.
 *
 * If the `editor` config on a column contains a `field` property, then the `editor` config
 * is used to create the wrapping {@link Ext.grid.CellEditor CellEditor}, and the `field` property
 * is used to create the editing  input field.
 *
 * ## Example
 *
 * A grid with editor for the name and the email columns:
 *
 *     @example
 *     Ext.create('Ext.data.Store', {
 *         storeId: 'simpsonsStore',
 *         fields:[ 'name', 'email', 'phone'],
 *         data: [
 *             { name: 'Lisa', email: 'lisa@simpsons.com', phone: '555-111-1224' },
 *             { name: 'Bart', email: 'bart@simpsons.com', phone: '555-222-1234' },
 *             { name: 'Homer', email: 'homer@simpsons.com', phone: '555-222-1244' },
 *             { name: 'Marge', email: 'marge@simpsons.com', phone: '555-222-1254' }
 *         ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Simpsons',
 *         store: Ext.data.StoreManager.lookup('simpsonsStore'),
 *         columns: [
 *             {header: 'Name', dataIndex: 'name', editor: 'textfield'},
 *             {header: 'Email', dataIndex: 'email', flex:1,
 *                 editor: {
 *                     completeOnEnter: false,
 *
 *                     // If the editor config contains a field property, then
 *                     // the editor config is used to create the CellEditor
 *                     // and the field property is used to create the editing input field.
 *                     field: {
 *                         xtype: 'textfield',
 *                         allowBlank: false
 *                     }
 *                 }
 *             },
 *             {header: 'Phone', dataIndex: 'phone'}
 *         ],
 *         selModel: 'cellmodel',
 *         plugins: {
 *             cellediting: {
 *                 clicksToEdit: 1
 *             }
 *         },
 *         height: 200,
 *         width: 400,
 *         renderTo: Ext.getBody()
 *     });
 *
 * This requires a little explanation. We're passing in `store` and `columns` as normal, but
 * we also specify a {@link Ext.grid.column.Column#field field} on two of our columns. For the
 * Name column we just want a default textfield to edit the value, so we specify 'textfield'.
 * For the Email column we customized the editor slightly by passing allowBlank: false, which
 * will provide inline validation.
 *
 * To support cell editing, we also specified that the grid should use the 'cellmodel'
 * {@link Ext.grid.Panel#selModel selModel}, and created an instance of the CellEditing plugin,
 * which we configured to activate each editor after a single click.
 *
 */
Ext.define('Ext.grid.plugin.CellEditing', {
    alias: 'plugin.cellediting',
    extend: 'Ext.grid.plugin.Editing',
    requires: [
        'Ext.grid.CellEditor',
        'Ext.util.DelayedTask'
    ],
    /**
     * @event beforeedit
     * Fires before cell editing is triggered. Return false from event handler to stop the editing.
     *
     * @param {Ext.grid.plugin.CellEditing} editor
     * @param {Object} context An editing context event with the following properties:
     *  @param {Ext.grid.Panel} context.grid The owning grid Panel.
     *  @param {Ext.data.Model} context.record The record being edited.
     *  @param {String} context.field The name of the field being edited.
     *  @param {Mixed} context.value The field's current value.
     *  @param {HTMLElement} context.row The grid row element.
     *  @param {Ext.grid.column.Column} context.column The {@link Ext.grid.column.Column} Column}
     * being edited.
     *  @param {Number} context.rowIdx The index of the row being edited.
     *  @param {Number} context.colIdx The index of the column being edited.
     *  @param {Boolean} context.cancel Set this to `true` to cancel the edit or return false
     * from your handler.
     */
    /**
     * @event edit
     * Fires after a cell is edited. Usage example:
     *
     *     grid.on('edit', function(editor, e) {
     *         // commit the changes right after editing finished
     *         e.record.commit();
     *     });
     *
     * @param {Ext.grid.plugin.CellEditing} editor
     * @param {Object} context An editing context with the following properties:
     *  @param {Ext.grid.Panel} context.grid The owning grid Panel.
     *  @param {Ext.data.Model} context.record The record being edited.
     *  @param {String} context.field The name of the field being edited.
     *  @param {Mixed} context.value The field's current value.
     *  @param {HTMLElement} context.row The grid row element.
     *  @param {Ext.grid.column.Column} context.column The {@link Ext.grid.column.Column} Column}
     * being edited.
     *  @param {Number} context.rowIdx The index of the row being edited.
     *  @param {Number} context.colIdx The index of the column being edited.
     *  @param {Mixed} context.originalValue The original value before being edited.
     */
    /**
     * @event validateedit
     * Fires after a cell is edited, but before the value is set in the record.
     * There are three possible outcomes when handling the validateedit event:
     *
     *  - Return `true` - Return true to commit the change to the underlying record and
     *    hide the editor
     *  - Return 'false' - Return false to prevent 1) the edit from being committed to
     *    the underlying record and 2) the editor from hiding / blurring.
     *  - Set context.cancel: true and return `false` - Set the context param's cancel property
     *    to true and returning false will 1) prevent the edit from being committed to
     *    the underlying record but _will_ allow the edit to hide once blurred.
     *
     * In the following example, entering 10 in the editor field and tabbing out /
     * blurring the editor field will result in the the editor remaining focused as the
     * required validation criteria has not been met.
     *
     *     grid.on('validateedit', function(editor, context) {
     *         if (context.value < 10) {
     *             return false;
     *         }
     *     });
     *
     * If we modify the previous example by setting context.cancel to true then changing
     * the editor value from 2 to 10 and tabbing out of the field will result in the
     * editor hiding and the grid cell retaining the initial value of 2.
     *
     *     grid.on('validateedit', function(editor, context) {
     *         if (context.value < 10) {
     *             context.cancel = true;
     *             return false;
     *         }
     *     });
     *
     * Below is a usage example showing how to remove the red triangle (dirty-record
     * indicator) from some records (not all). By observing the grid's validateedit
     * event, it can be cancelled if the edit occurs on a targeted row (for example) and
     * then setting the field's new value in the Record directly:
     *
     *     grid.on('validateedit', function(editor, e) {
     *       var myTargetRow = 6;
     *
     *       if (e.row == myTargetRow) {
     *         e.cancel = true;
     *         e.record.data[e.field] = e.value;
     *       }
     *     });
     *
     * @param {Ext.grid.plugin.CellEditing} editor
     * @param {Object} context An editing context with the following properties:
     * @param {Ext.grid.Panel} context.grid The owning grid Panel.
     * @param {Ext.data.Model} context.record The record being edited.
     * @param {String} context.field The name of the field being edited.
     * @param {Mixed} context.value The field's current value.
     * @param {HTMLElement} context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The {@link Ext.grid.column.Column} Column}
     * being edited.
     * @param {Number} context.rowIdx The index of the row being edited.
     * @param {Number} context.colIdx The index of the column being edited.
     * @param {Mixed} context.originalValue The original value before being edited.
     * @param {Boolean} context.cancel Set this to `true` to cancel the edit or return false
     * from your handler (see the method description for additional details).
     */
    /**
     * @event canceledit
     * Fires when the user started editing a cell but then cancelled the edit.
     * @param {Ext.grid.plugin.CellEditing} editor
     * @param {Object} context An edit event with the following properties:
     * @param {Ext.grid.Panel} context.grid The owning grid Panel.
     * @param {Ext.data.Model} context.record The record being edited.
     * @param {String} context.field The name of the field being edited.
     * @param {Mixed} context.value The field's current value.
     * @param {HTMLElement} context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The {@link Ext.grid.column.Column} Column}
     * being edited.
     * @param {Number} context.rowIdx The index of the row being edited.
     * @param {Number} context.colIdx The index of the column being edited.
     * @param {Mixed} context.originalValue The original value before being edited.
     */
    restartEvent: null,
    cachedEditorValue: null,
    init: function(grid) {
        var me = this;
        // This plugin has an interest in entering actionable mode.
        // It places the cell editors into the tabbable flow.
        grid.registerActionable(me);
        me.callParent(arguments);
        me.editors = new Ext.util.MixedCollection(false, function(editor) {
            return editor.editorId;
        });
    },
    // Ensure editors are cleaned up.
    beforeGridHeaderDestroy: function(headerCt) {
        var me = this,
            columns = me.grid.getColumnManager().getColumns(),
            len = columns.length,
            i, column, editor;
        for (i = 0; i < len; i++) {
            column = columns[i];
            // Try to get the CellEditor which contains the field to destroy the whole assembly
            editor = me.editors.getByKey(column.getItemId());
            // Failing that, the field has not yet been accessed to add to the CellEditor,
            // but must still be destroyed
            if (!editor) {
                // If we have an editor, it will wrap the field which will be destroyed.
                editor = column.editor || column.field;
            }
            // Destroy the CellEditor or field
            Ext.destroy(editor);
            me.removeFieldAccessors(column);
        }
    },
    onReconfigure: function(grid, store, columns) {
        // Only reconfigure editors if passed a new set of columns
        if (columns) {
            this.destroyEditors();
        }
        this.callParent();
    },
    destroy: function() {
        var me = this;
        me.destroyEditors();
        if (me.restartEvent) {
            me.restartEvent = me.cachedEditorValue = Ext.destroy(me.restartEvent);
        }
        me.callParent();
    },
    /**
     * @private
     * Template method called from the base class's initEvents
     */
    initCancelTriggers: function() {
        var me = this,
            grid = me.grid;
        me.mon(grid, {
            columnresize: me.cancelEdit,
            columnmove: me.cancelEdit,
            scope: me
        });
    },
    isCellEditable: function(record, columnHeader) {
        var me = this,
            context = me.getEditingContext(record, columnHeader);
        if (context.view.isVisible(true) && context) {
            columnHeader = context.column;
            record = context.record;
            if (columnHeader && me.getEditor(record, columnHeader)) {
                return true;
            }
        }
    },
    /**
     * This method is called when actionable mode is requested for a cell. 
     * @param {Ext.grid.CellContext} position The position at which actionable mode was requested.
     * @param {Boolean} skipBeforeCheck Pass `true` to skip the possible vetoing conditions
     * like event firing.
     * @param {Boolean} doFocus Pass `true` to immediately focus the active editor.
     * @return {Boolean} `true` if this cell is actionable (editable)
     * @protected
     */
    activateCell: function(position, skipBeforeCheck, doFocus) {
        var me = this,
            record = position.record,
            column = position.column,
            prevEditor = me.getActiveEditor(),
            view = me.view,
            isResuming = me.restartEvent != null,
            context, contextGeneration, cell, editor, p, editValue, abortEdit;
        if (isResuming) {
            me.restartEvent = Ext.destroy(me.restartEvent);
        }
        context = me.getEditingContext(record, column);
        if (!context || !column.getEditor(record)) {
            return;
        }
        // Activating a new cell while editing.
        // Complete the edit, and cache the editor in the detached body.
        if (prevEditor && prevEditor.editing) {
            // Silently drop actionPosition in case completion of edit causes
            // and view refreshing which would attempt to restore actionable mode
            view.actionPosition = null;
            contextGeneration = context.generation;
            if (prevEditor.completeEdit() === false) {
                return;
            }
            // Complete edit could cause a sort or column movement.
            // Reposition context unless user code has modified it for its own purposes.
            if (context.generation === contextGeneration) {
                context.refresh();
            }
        }
        if (!skipBeforeCheck) {
            // Allow vetoing, or setting a new editor *before* we call getEditor
            contextGeneration = context.generation;
            // Disable focus restoration in any of the before edit handling.
            // We are going to be doing that below
            if (view.actionableMode) {
                view.skipSaveFocusState = true;
            }
            abortEdit = me.beforeEdit(context) === false || me.fireEvent('beforeedit', me, context) === false || context.cancel;
            // Clear temporary flag
            view.skipSaveFocusState = false;
            if (abortEdit) {
                return;
            }
            // beforeedit edit could cause sort or column movement
            // Reposition context unless user code has modified it for its own purposes.
            if (context.generation === contextGeneration) {
                context.refresh();
            }
        }
        // Recapture the editor. The beforeedit listener is allowed to replace the field.
        editor = me.getEditor(record, column);
        // If the events fired above ('beforeedit' and potentially 'edit') triggered
        // any destructive operations regather the context using the ordinal position.
        if (context.cell !== context.getCell(true)) {
            context = me.getEditingContext(context.rowIdx, context.colIdx, null, context.view);
            position.setPosition(context);
        }
        if (editor) {
            cell = Ext.get(context.cell);
            // Ensure editor is there in the cell.
            // And will then be found in the tabbable children of the activating cell
            if (!editor.rendered) {
                editor.hidden = true;
                editor.render(cell);
            } else {
                p = editor.el.dom.parentNode;
                if (p !== cell.dom) {
                    // This can sometimes throw an error
                    // https://code.google.com/p/chromium/issues/detail?id=432392
                    try {
                        p.removeChild(editor.el.dom);
                    } catch (e) {}
                    // ignore
                    if (editor.container && editor.container.dom !== cell.dom) {
                        editor.container.collect();
                    }
                    editor.container = cell;
                    cell.dom.appendChild(editor.el.dom, cell.dom.firstChild);
                }
            }
            // Refresh the contextual value in case any event handlers (either the 'beforeedit'
            // of this edit, or the 'edit' of any just terminated previous editor) mutated
            // the record
            // https://sencha.jira.com/browse/EXTJS-19899
            editValue = context.record.get(context.column.dataIndex);
            if (editValue !== context.originalValue) {
                context.value = context.originalValue = editValue;
            }
            me.setEditingContext(context);
            // Request that the editor start.
            // Ensure that the focusing defaults to false.
            // It may veto, and return with the editing flag false.
            editor.startEdit(cell, context.value, (doFocus && !isResuming) || false, isResuming);
            // Set contextual information if we began editing (can be vetoed by events)
            if (editor.editing) {
                me.setActiveEditor(editor);
                me.setActiveRecord(context.record);
                me.setActiveColumn(context.column);
                me.editing = true;
                me.scroll = position.view.el.getScroll();
                if (isResuming) {
                    editor.setValue(me.cachedEditorValue);
                    me.cachedEditorValue = null;
                }
            }
            // Return true if the cell is actionable according to us
            return editor.editing;
        }
    },
    // CellEditing only activates individual cells.
    activateRow: Ext.emptyFn,
    /**
     * Cancels the currently focused operation. In this case CellEditing.
     * the view is being changed.
     * @protected
     */
    deactivate: function() {
        var me = this,
            context = me.context,
            editors = me.editors.items,
            len = editors.length,
            editor, i, callback;
        for (i = 0; i < len; i++) {
            editor = editors[i];
            // if we are deactivating the editor because it was de-rendered by a bufferedRenderer
            // cycle (scroll while editing), we should retain the editor's info before caching
            // also only run if we don't have a suspendedEditor to make sure we don't add two 
            // listeners on locked grids.
            if (context.view.renderingRows && !me.suspendedEditor) {
                if (editor.editing) {
                    me.suspendedEditor = editor;
                    me.cachedEditorValue = editor.getValue();
                    callback = function() {
                        var ctx;
                        if (me.suspendedEditor) {
                            ctx = me.suspendedEditor.context;
                            if (me.view.getNode(ctx.record)) {
                                me.view.ownerGrid.setActionableMode(true, ctx);
                                me.suspendedEditor = null;
                            }
                        } else {
                            me.restartEvent = Ext.destroy(me.restartEvent);
                            me.cachedEditorValue = null;
                        }
                    };
                    me.cancelEdit(editor);
                    me.restartEvent = me.view.on({
                        itemadd: callback,
                        destroyable: true
                    });
                }
                editor.cacheElement(true);
            }
        }
    },
    /**
     * Called by TableView#suspendActionableMode to suspend actionable processing while
     * the view is being changed.
     * @protected
     */
    suspend: function() {
        var me = this,
            editor = me.activeEditor;
        if (editor && editor.editing) {
            me.suspendedEditor = editor;
            me.suspendEvents();
            editor.suspendEvents();
            editor.cancelEdit(true);
            editor.resumeEvents();
            me.resumeEvents();
        }
    },
    /**
     * Called by TableView#resumeActionableMode to resume actionable processing after
     * the view has been changed.
     * @param {Ext.grid.CellContext} position The position at which to resume actionable processing.
     * @return {Boolean} `true` if this Actionable has successfully resumed.
     * @protected
     */
    resume: function(position) {
        var me = this,
            editor = me.activeEditor = me.suspendedEditor,
            result;
        if (editor) {
            me.suspendEvents();
            editor.suspendEvents();
            result = me.activateCell(position, true, true);
            editor.resumeEvents();
            me.resumeEvents();
            me.suspendedEditor = null;
        }
        return result;
    },
    /**
     * @deprecated 5.5.0 Use the grid's {@link Ext.panel.Table#setActionableMode actionable mode}
     * to activate cell contents. Starts editing the specified record, using the specified Column
     * definition to define which field is being edited.
     * @param {Ext.data.Model/Number} record The Store data record which backs the row to be edited,
     * or index of the record.
     * @param {Ext.grid.column.Column/Number} columnHeader The Column object defining the column
     * to be edited, or index of the column.
     */
    startEdit: function(record, columnHeader) {
        this.startEditByPosition(new Ext.grid.CellContext(this.view).setPosition(record, columnHeader));
    },
    completeEdit: function(remainVisible) {
        var activeEd = this.getActiveEditor();
        if (activeEd) {
            activeEd.completeEdit(remainVisible);
        }
    },
    // internal getters/setters
    setEditingContext: function(context) {
        this.context = context;
    },
    setActiveEditor: function(ed) {
        this.activeEditor = ed;
    },
    getActiveEditor: function() {
        return this.activeEditor;
    },
    setActiveColumn: function(column) {
        this.activeColumn = column;
    },
    getActiveColumn: function() {
        return this.activeColumn;
    },
    setActiveRecord: function(record) {
        this.activeRecord = record;
    },
    getActiveRecord: function() {
        return this.activeRecord;
    },
    getEditor: function(record, column) {
        return this.getCachedEditor(column.getItemId(), record, column);
    },
    getCachedEditor: function(editorId, record, column) {
        var me = this,
            editors = me.editors,
            editor = editors.getByKey(editorId);
        if (!editor) {
            editor = column.getEditor(record);
            if (!editor) {
                return false;
            }
            // Allow them to specify a CellEditor in the Column
            if (!(editor instanceof Ext.grid.CellEditor)) {
                // Apply the field's editorCfg to the CellEditor config.
                // See Editor#createColumnField. A Column's editor config may
                // be used to specify the CellEditor config if it contains a field property.
                editor = Ext.widget(Ext.apply({
                    xtype: 'celleditor',
                    floating: true,
                    editorId: editorId,
                    field: editor
                }, editor.editorCfg));
            }
            // Add the Editor as a floating child of the grid
            // Prevent this field from being included in an Ext.form.Basic
            // collection, if the grid happens to be used inside a form
            editor.field.excludeForm = true;
            // If the editor is new to this grid, then add it to the grid, and ensure
            // it tells us about its life cycle.
            if (editor.column !== column) {
                editor.column = column;
                column.on('removed', me.onColumnRemoved, me);
            }
            editors.add(editor);
        }
        // Inject an upward link to its owning grid even though it is not an added child.
        editor.ownerCmp = me.grid.ownerGrid;
        if (column.isTreeColumn) {
            editor.isForTree = column.isTreeColumn;
            editor.addCls(Ext.baseCSSPrefix + 'tree-cell-editor');
        }
        // Set the owning grid.
        // This needs to be kept up to date because in a Lockable assembly, an editor
        // needs to swap sides if the column is moved across.
        editor.setGrid(me.grid);
        // Keep upward pointer correct for each use - editors are shared between locking sides
        editor.editingPlugin = me;
        editor.collectContainerElement = true;
        return editor;
    },
    onColumnRemoved: function(column) {
        var me = this,
            context = me.context;
        // If the column was being edited, when plucked out of the grid, cancel the edit.
        if (context && context.column === column) {
            me.cancelEdit();
        }
        // Remove the CellEditor of that column from the grid, and no longer listen
        // for events from it.
        column.un('removed', me.onColumnRemoved, me);
    },
    setColumnField: function(column, field) {
        var ed = this.editors.getByKey(column.getItemId());
        Ext.destroy(ed, column.field);
        this.editors.removeAtKey(column.getItemId());
        this.callParent(arguments);
    },
    /**
     * Gets the cell (td) for a particular record and column.
     * @param {Ext.data.Model} record
     * @param {Ext.grid.column.Column} column
     * @param {Boolean} [returnElement=false] `true` to return an Ext.Element,
     * else a raw `<td>` is returned.
     * @private
     */
    getCell: function(record, column, returnElement) {
        return this.grid.getView().getCell(record, column, returnElement);
    },
    onEditComplete: function(ed, value, startValue) {
        var me = this,
            context = ed.context,
            view, record;
        view = context.view;
        record = context.record;
        context.value = value;
        // Only update the record if the new value is different than the
        // startValue. When the view refreshes its el will gain focus
        if (!record.isEqual(value, startValue)) {
            view.skipSaveFocusState = true;
            record.set(context.column.dataIndex, value);
            view.skipSaveFocusState = false;
            // Changing the record may impact the position
            context.rowIdx = view.indexOf(record);
        }
        // We clear down our context here in response to the CellEditor completing.
        // We only do this if we have not already started editing a new context.
        if (me.context === context) {
            me.setActiveEditor(null);
            me.setActiveColumn(null);
            me.setActiveRecord(null);
            me.editing = false;
        }
        me.fireEvent('edit', me, context);
    },
    /**
     * Cancels any active editing.
     */
    cancelEdit: function(activeEd) {
        var me = this,
            context = me.context;
        // Called from CellEditor#onEditComplete when canceling.
        if (activeEd && activeEd.isCellEditor) {
            me.context.value = ('editedValue' in activeEd) ? activeEd.editedValue : activeEd.getValue();
            // Editing flag cleared in superclass.
            // canceledit event fired in superclass.
            me.callParent(arguments);
            // Clear our current editing context.
            // We only do this if we have not already started editing a new context.
            if (activeEd.context === context) {
                me.setActiveEditor(null);
                me.setActiveColumn(null);
                me.setActiveRecord(null);
            } else // Re-instate editing flag after callParent
            {
                me.editing = true;
            }
        } else // This is a programmatic call to cancel any active edit
        {
            activeEd = me.getActiveEditor();
            if (activeEd && activeEd.field) {
                activeEd.cancelEdit();
            }
        }
    },
    /**
     * Starts editing by position (row/column)
     * @param {Object} position A position with keys of row and column.
     * Example usage:
     * 
     *     cellEditing.startEditByPosition({
     *         row: 3,
     *         column: 2
     *     });
     */
    startEditByPosition: function(position) {
        var me = this,
            cm = me.grid.getColumnManager(),
            index,
            activeEditor = me.getActiveEditor();
        // If a raw {row:0, column:0} object passed.
        // The historic API is that column indices INCLUDE hidden columns, so use getColumnManager.
        if (!position.isCellContext) {
            position = new Ext.grid.CellContext(me.view).setPosition(position.row, me.grid.getColumnManager().getColumns()[position.column]);
        }
        // Coerce the edit column to the closest visible column. This typically
        // only needs to be done when called programatically, since the position
        // is handled by walkCells, which is called before this is invoked.
        index = cm.getHeaderIndex(position.column);
        position.column = cm.getVisibleHeaderClosestToIndex(index);
        // Already in actionable mode.
        if (me.grid.actionableMode) {
            // We are being asked to edit right where we are (click in an active editor
            // will get here)
            if (me.editing && position.isEqual(me.context)) {
                return;
            }
            // Finish any current edit.
            if (activeEditor) {
                activeEditor.completeEdit();
            }
        }
        // If we are STILL in actionable mode - synchronous blurring has not tipped us
        // out of actionable mode...
        if (me.grid.actionableMode) {
            // Get the editor for the position, and if there is one, focus it
            if (me.activateCell(position)) {
                // Ensure the row is activated.
                me.activateRow(me.view.all.item(position.rowIdx, true));
                activeEditor = me.getEditor(position.record, position.column);
                if (activeEditor) {
                    activeEditor.field.focus();
                }
            }
        } else {
            // Enter actionable mode at the requested position
            return me.grid.setActionableMode(true, position);
        }
    },
    destroyEditors: function() {
        var me = this,
            editors = me.editors;
        if (editors) {
            editors.each(Ext.destroy, Ext);
            editors.clear();
        }
    }
});

/**
 * This {@link Ext.grid.Panel grid} plugin adds clipboard support to a grid.
 *
 * *Note that the grid must use the
 * {@link Ext.grid.selection.SpreadsheetModel spreadsheet selection model}
 * to utilize this plugin.*
 *
 * This class supports the following `{@link Ext.plugin.AbstractClipboard#formats formats}`
 * for grid data:
 *
 *  * `cell` - Complete field data that can be matched to other grids using the same
 *    {@link Ext.data.Model model} regardless of column order.
 *  * `text` - Cell content stripped of HTML tags.
 *  * `html` - Complete cell content, including any rendered HTML tags.
 *  * `raw` - Underlying field values based on `dataIndex`.
 *
 * The `cell` format is not valid for the `{@link Ext.plugin.AbstractClipboard#system system}`
 * clipboard format.
 */
Ext.define('Ext.grid.plugin.Clipboard', {
    extend: 'Ext.plugin.AbstractClipboard',
    alias: 'plugin.clipboard',
    requires: [
        'Ext.util.Format',
        'Ext.util.TSV'
    ],
    formats: {
        cell: {
            get: 'getCells'
        },
        html: {
            get: 'getCellData'
        },
        raw: {
            get: 'getCellData',
            put: 'putCellData'
        }
    },
    gridListeners: {
        render: 'onCmpReady'
    },
    getCellData: function(format, erase) {
        var cmp = this.getCmp(),
            selection = cmp.getSelectionModel().getSelected(),
            ret = [],
            isRaw = format === 'raw',
            isText = format === 'text',
            viewNode, cell, data, dataIndex, lastRecord, column, record, row, view;
        if (selection) {
            selection.eachCell(function(cellContext) {
                column = cellContext.column;
                view = cellContext.column.getView();
                record = cellContext.record;
                // Do not copy the check column or row numberer column
                if (column.ignoreExport) {
                    return;
                }
                if (lastRecord !== record) {
                    lastRecord = record;
                    ret.push(row = []);
                }
                dataIndex = column.dataIndex;
                if (isRaw) {
                    data = record.data[dataIndex];
                } else {
                    // Try to access the view node.
                    viewNode = view.all.item(cellContext.rowIdx);
                    // If we could not, it's because it's outside of the rendered block -
                    // recreate it.
                    if (!viewNode) {
                        viewNode = Ext.fly(view.createRowElement(record, cellContext.rowIdx));
                    }
                    cell = viewNode.dom.querySelector(column.getCellInnerSelector());
                    data = cell.innerHTML;
                    if (isText) {
                        data = Ext.util.Format.stripTags(data);
                    }
                }
                row.push(data);
                if (erase && dataIndex) {
                    record.set(dataIndex, null);
                }
            });
        }
        // See decode() comment below
        return Ext.util.TSV.encode(ret, undefined, null);
    },
    getCells: function(format, erase) {
        var cmp = this.getCmp(),
            selection = cmp.getSelectionModel().getSelected(),
            ret = [],
            dataIndex, lastRecord, record, row;
        if (selection) {
            selection.eachCell(function(cellContext) {
                record = cellContext.record;
                if (lastRecord !== record) {
                    lastRecord = record;
                    ret.push(row = {
                        model: record.self,
                        fields: []
                    });
                }
                dataIndex = cellContext.column.dataIndex;
                row.fields.push({
                    name: dataIndex,
                    value: record.data[dataIndex]
                });
                if (erase && dataIndex) {
                    record.set(dataIndex, null);
                }
            });
        }
        return ret;
    },
    getTextData: function(format, erase) {
        return this.getCellData(format, erase);
    },
    putCellData: function(data, format) {
        // We pass null as field quote here to override default TSV decoding behavior
        // that will try to unquote fields and break if double quote character is
        // encountered in the data. TSV format does not support any kind of field quoting
        // but Ext.util.TSV mistakenly assumed otherwise pre-6.5.3
        var values = Ext.util.TSV.decode(data, undefined, null),
            row,
            recCount = values.length,
            colCount = recCount ? values[0].length : 0,
            sourceRowIdx, sourceColIdx,
            view = this.getCmp().getView(),
            maxRowIdx = view.dataSource.getCount() - 1,
            maxColIdx = view.getVisibleColumnManager().getColumns().length - 1,
            selModel = view.getSelectionModel(),
            selected = selModel.getSelected(),
            navModel = view.getNavigationModel(),
            destination = selected.startCell || navModel.getPosition(),
            dataIndex, destinationStartColumn,
            dataObject = {};
        // If the view is not focused, use the first cell of the selection as the destination.
        if (!destination && selected) {
            selected.eachCell(function(c) {
                destination = c;
                return false;
            });
        }
        if (destination) {
            // Create a new Context based upon the outermost View.
            // NavigationModel works on local views.
            // TODO: remove this step when NavModel is fixed to use outermost view in locked grid.
            // At that point, we can use navModel.getPosition()
            destination = new Ext.grid.CellContext(view).setPosition(destination.record, destination.column);
        } else {
            destination = new Ext.grid.CellContext(view).setPosition(0, 0);
        }
        destinationStartColumn = destination.colIdx;
        for (sourceRowIdx = 0; sourceRowIdx < recCount; sourceRowIdx++) {
            row = values[sourceRowIdx];
            // Collect new values in dataObject
            for (sourceColIdx = 0; sourceColIdx < colCount; sourceColIdx++) {
                dataIndex = destination.column.dataIndex;
                if (dataIndex) {
                    switch (format) {
                        // Raw field values
                        case 'raw':
                            dataObject[dataIndex] = row[sourceColIdx];
                            break;
                        // Textual data with HTML tags stripped    
                        case 'text':
                            dataObject[dataIndex] = row[sourceColIdx];
                            break;
                        // innerHTML from the cell inner
                        case 'html':
                            break;
                    }
                }
                // If we are at the end of the destination row, break the column loop.
                if (destination.colIdx === maxColIdx) {
                    break;
                }
                destination.setColumn(destination.colIdx + 1);
            }
            // Update the record in one go.
            destination.record.set(dataObject);
            // If we are at the end of the destination store, break the row loop.
            if (destination.rowIdx === maxRowIdx) {
                break;
            }
            // Jump to next row in destination
            destination.setPosition(destination.rowIdx + 1, destinationStartColumn);
        }
    },
    putTextData: function(data, format) {
        this.putCellData(data, format);
    },
    getTarget: function(comp) {
        return comp.body;
    },
    privates: {
        validateAction: function(event) {
            var view = this.getCmp().getView();
            if (view.actionableMode) {
                return false;
            }
        }
    }
});

/**
 * This plugin provides drag and drop functionality for a {@link Ext.grid.View GridView}.
 *
 * A specialized instance of {@link Ext.dd.DragZone DragZone} and {@link Ext.dd.DropZone 
 * DropZone} are attached to the grid view.  The DropZone will participate in drops 
 * from DragZones having the same {@link #ddGroup} including drops from within the same 
 * grid.
 *
 * Note that where touch gestures are available, the `longpress` gesture will initiate
 * the drag in order that the `touchstart` may still be used to initiate a scroll.
 *
 * On platforms which implement the [Pointer Events standard](https://www.w3.org/TR/pointerevents/)
 * (IE), the `touchstart` event is usually claimed by the platform, however, this plugin
 * uses the `longpress` event to trigger drags, so `touchstart` will not initiate a scroll.
 * On these platforms, a two finger drag gesture will scroll the content, or a single
 * finger drag on an empty area of the view will scroll the content.
 *
 * During the drop operation a data object is passed to a participating DropZone's drop 
 * handlers.  The drag data object has the following properties:
 *
 * - **copy:** {@link Boolean} <br> The value of {@link #copy}.  Or `true` if 
 * {@link #allowCopy} is true **and** the control key was pressed as the drag operation 
 * began.
 * 
 * - **view:** {@link Ext.grid.View GridView} <br> The source grid view from which the 
 * drag originated
 * 
 * - **ddel:** HTMLElement <br> The drag proxy element which moves with the cursor
 * 
 * - **item:** HTMLElement <br> The grid view node upon which the mousedown event was 
 * registered
 * 
 * - **records:** {@link Array} <br> An Array of {@link Ext.data.Model Model}s 
 * representing the selected data being dragged from the source grid view
 *
 * By adding this plugin to a view, two new events will be fired from the client 
 * grid view as well as its owning Grid: `{@link #beforedrop}` and `{@link #drop}`.
 *
 *     @example
 *     var store = Ext.create('Ext.data.Store', {
 *         fields: ['name'],
 *         data: [
 *             ["Lisa"],
 *             ["Bart"],
 *             ["Homer"],
 *             ["Marge"]
 *         ],
 *         proxy: {
 *             type: 'memory',
 *             reader: 'array'
 *         }
 *     });
 *     
 *     Ext.create('Ext.grid.Panel', {
 *         store: store,
 *         enableLocking: true,
 *         columns: [{
 *             header: 'Name',
 *             dataIndex: 'name',
 *             flex: true
 *         }],
 *         viewConfig: {
 *             plugins: {
 *                 gridviewdragdrop: {
 *                     dragText: 'Drag and drop to reorganize'
 *                 }
 *             }
 *         },
 *         height: 200,
 *         width: 400,
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.grid.plugin.DragDrop', {
    extend: 'Ext.plugin.Abstract',
    alias: 'plugin.gridviewdragdrop',
    uses: [
        'Ext.view.DragZone',
        'Ext.grid.ViewDropZone'
    ],
    /**
     * @event beforedrop
     * **This event is fired through the {@link Ext.grid.View GridView} and its owning 
     * {@link Ext.grid.Panel Grid}. You can add listeners to the grid or grid {@link 
     * Ext.grid.Panel#viewConfig view config} object**
     *
     * Fired when a drop gesture has been triggered by a mouseup event in a valid drop 
     * position in the grid view.
     * 
     * Returning `false` to this event signals that the drop gesture was invalid and 
     * animates the drag proxy back to the point from which the drag began.
     * 
     * The dropHandlers parameter can be used to defer the processing of this event. For 
     * example, you can force the handler to wait for the result of a message box 
     * confirmation or an asynchronous server call (_see the details of the dropHandlers 
     * property for more information_).
     *  
     *     grid.on('beforedrop', function(node, data, overModel, dropPosition, dropHandlers) {
     *         // Defer the handling
     *         dropHandlers.wait = true;
     *         Ext.MessageBox.confirm('Drop', 'Are you sure', function(btn){
     *             if (btn === 'yes') {
     *                 dropHandlers.processDrop();
     *             } else {
     *                 dropHandlers.cancelDrop();
     *             }
     *         });
     *     });
     * 
     * Any other return value continues with the data transfer operation unless the wait 
     * property is set.
     *
     * @param {HTMLElement} node The {@link Ext.grid.View grid view} node **if any** over 
     * which the cursor was positioned.
     *
     * @param {Object} data The data object gathered at mousedown time by the 
     * cooperating {@link Ext.dd.DragZone DragZone}'s {@link Ext.dd.DragZone#getDragData 
     * getDragData} method.  It contains the following properties:
     * @param {Boolean} data.copy The value of {@link #copy}.  Or `true` if 
     * {@link #allowCopy} is true **and** the control key was pressed as the drag 
     * operation began.
     * @param {Ext.grid.View} data.view The source grid view from which the drag 
     * originated
     * @param {HTMLElement} data.ddel The drag proxy element which moves with the cursor
     * @param {HTMLElement} data.item The grid view node upon which the mousedown event 
     * was registered
     * @param {Ext.data.Model[]} data.records An Array of Models representing the 
     * selected data being dragged from the source grid view
     *
     * @param {Ext.data.Model} overModel The Model over which the drop gesture took place
     *
     * @param {String} dropPosition `"before"` or `"after"` depending on whether the 
     * cursor is above or below the mid-line of the node.
     *
     * @param {Object} dropHandlers
     * This parameter allows the developer to control when the drop action takes place. 
     * It is useful if any asynchronous processing needs to be completed before 
     * performing the drop. This object has the following properties:
     * 
     * @param {Boolean} dropHandlers.wait Indicates whether the drop should be deferred. 
     * Set this property to true to defer the drop.
     * @param {Function} dropHandlers.processDrop A function to be called to complete 
     * the drop operation.
     * @param {Function} dropHandlers.cancelDrop A function to be called to cancel the 
     * drop operation.
     */
    /**
     * @event drop
     * **This event is fired through the {@link Ext.grid.View GridView} and its owning 
     * {@link Ext.grid.Panel Grid}. You can add listeners to the grid or grid {@link 
     * Ext.grid.Panel#viewConfig view config} object**
     * 
     * Fired when a drop operation has been completed and the data has been moved or 
     * copied.
     *
     * @param {HTMLElement} node The {@link Ext.grid.View GridView} node **if any** over 
     * which the cursor was positioned.
     *
     * @param {Object} data The data object gathered at mousedown time by the 
     * cooperating {@link Ext.dd.DragZone DragZone}'s {@link Ext.dd.DragZone#getDragData 
     * getDragData} method.  It contains the following properties:
     * @param {Boolean} data.copy The value of {@link #copy}.  Or `true` if 
     * {@link #allowCopy} is true **and** the control key was pressed as the drag 
     * operation began.
     * @param {Ext.grid.View} data.view The source grid view from which the drag 
     * originated
     * @param {HTMLElement} data.ddel The drag proxy element which moves with the cursor
     * @param {HTMLElement} data.item The grid view node upon which the mousedown event 
     * was registered
     * @param {Ext.data.Model[]} data.records An Array of Models representing the 
     * selected data being dragged from the source grid view
     *
     * @param {Ext.data.Model} overModel The Model over which the drop gesture took 
     * place.
     *
     * @param {String} dropPosition `"before"` or `"after"` depending on whether the 
     * cursor is above or below the mid-line of the node.
     */
    /**
     * @cfg {Boolean} [copy=false]
     * Set as `true` to copy the records from the source grid to the destination drop 
     * grid.  Otherwise, dragged records will be moved.
     * 
     * **Note:** This only applies to records dragged between two different grids with 
     * unique stores.
     * 
     * See {@link #allowCopy} to allow only control-drag operations to copy records.
     */
    /**
     * @cfg {Boolean} [allowCopy=false]
     * Set as `true` to allow the user to hold down the control key at the start of the 
     * drag operation and copy the dragged records between grids.  Otherwise, dragged 
     * records will be moved.
     * 
     * **Note:** This only applies to records dragged between two different grids with 
     * unique stores.
     * 
     * See {@link #copy} to enable the copying of all dragged records.
     */
    /**
     * @cfg {String} dragText
     * The text to show while dragging.
     *
     * Two placeholders can be used in the text:
     *
     * - `{0}` The number of selected items.
     * - `{1}` 's' when more than 1 items (only useful for English).
     * @locale
     */
    dragText: '{0} selected row{1}',
    /**
     * @cfg {String} [ddGroup=gridDD]
     * A named drag drop group to which this object belongs. If a group is specified, then both
     * the DragZones and DropZone used by this plugin will only interact with other drag drop
     * objects in the same group.
     */
    ddGroup: "GridDD",
    /**
     * @cfg {String} [dragGroup]
     * The {@link #ddGroup} to which the DragZone will belong.
     *
     * This defines which other DropZones the DragZone will interact with. Drag/DropZones
     * only interact with other Drag/DropZones which are members of the same {@link #ddGroup}.
     */
    /**
     * @cfg {String} [dropGroup]
     * The {@link #ddGroup} to which the DropZone will belong.
     *
     * This defines which other DragZones the DropZone will interact with. Drag/DropZones
     * only interact with other Drag/DropZones which are members of the same {@link #ddGroup}.
     */
    /**
     * @cfg {Boolean} enableDrop
     * `false` to disallow the View from accepting drop gestures.
     */
    enableDrop: true,
    /**
     * @cfg {Boolean} enableDrag
     * `false` to disallow dragging items from the View.
     */
    enableDrag: true,
    /**
     * `true` to register this container with the Scrollmanager for auto scrolling during drag
     * operations. A {@link Ext.dd.ScrollManager} configuration may also be passed.
     * @cfg {Object/Boolean} containerScroll
     */
    containerScroll: false,
    /**
     * @cfg {Object} [dragZone]
     * A config object to apply to the creation of the {@link #property-dragZone DragZone}
     * which handles for drag start gestures.
     *
     * Template methods of the DragZone may be overridden using this config.
     */
    /**
     * @cfg {Object} [dropZone]
     * A config object to apply to the creation of the {@link #property-dropZone DropZone}
     * which handles mouseover and drop gestures.
     *
     * Template methods of the DropZone may be overridden using this config.
     */
    /**
     * @property {Ext.view.DragZone} dragZone
     * An {@link Ext.view.DragZone DragZone} which handles mousedown and dragging of records
     * from the grid.
     */
    /**
     * @property {Ext.grid.ViewDropZone} dropZone
     * An {@link Ext.grid.ViewDropZone DropZone} which handles mouseover and dropping records
     * in any grid which shares the same {@link #dropGroup}.
     */
    init: function(view) {
        Ext.applyIf(view, {
            copy: this.copy,
            allowCopy: this.allowCopy
        });
        view.on('render', this.onViewRender, this, {
            single: true
        });
    },
    /**
     * @private
     * Component calls destroy on all its plugins at destroy time.
     */
    destroy: function() {
        var me = this;
        me.dragZone = me.dropZone = Ext.destroy(me.dragZone, me.dropZone);
        me.callParent();
    },
    enable: function() {
        var me = this;
        if (me.dragZone) {
            me.dragZone.unlock();
        }
        if (me.dropZone) {
            me.dropZone.unlock();
        }
        me.callParent();
    },
    disable: function() {
        var me = this;
        if (me.dragZone) {
            me.dragZone.lock();
        }
        if (me.dropZone) {
            me.dropZone.lock();
        }
        me.callParent();
    },
    onViewRender: function(view) {
        var me = this,
            ownerGrid = view.ownerCt.ownerGrid || view.ownerCt,
            dragZone = me.dragZone || {};
        ownerGrid.relayEvents(view, [
            'beforedrop',
            'drop'
        ]);
        if (me.enableDrag) {
            if (me.containerScroll) {
                dragZone.scrollEl = view.getEl();
                dragZone.containerScroll = true;
            }
            me.dragZone = new Ext.view.DragZone(Ext.apply({
                view: view,
                ddGroup: me.dragGroup || me.ddGroup,
                dragText: me.dragText
            }, dragZone));
        }
        if (me.enableDrop) {
            me.dropZone = new Ext.grid.ViewDropZone(Ext.apply({
                view: view,
                ddGroup: me.dropGroup || me.ddGroup
            }, me.dropZone));
        }
    }
});

/**
 * The Ext.grid.plugin.RowEditing plugin injects editing at a row level for a Grid. When editing
 * begins, a small floating dialog will be shown for the appropriate row. Each editable column
 * will show a field for editing. There is a button to save or cancel all changes for the edit.
 *
 * The field that will be used for the editor is defined at the
 * {@link Ext.grid.column.Column#editor editor}. The editor can be a field instance or a field
 * configuration. If an editor is not specified for a particular column then that column
 * won't be editable and the value of the column will be displayed. To provide a custom renderer
 * for non-editable values, use the 
 * {@link Ext.grid.column.Column#editRenderer editRenderer} configuration on the column.
 *
 * The editor may be shared for each column in the grid, or a different one may be specified
 * for each column. An appropriate field type should be chosen to match the data structure
 * that it will be editing. For example, to edit a date, it would be useful to specify
 * {@link Ext.form.field.Date} as the editor.
 *
 *     @example
 *     Ext.create('Ext.data.Store', {
 *         storeId: 'simpsonsStore',
 *         fields:[ 'name', 'email', 'phone'],
 *         data: [
 *             { name: 'Lisa', email: 'lisa@simpsons.com', phone: '555-111-1224' },
 *             { name: 'Bart', email: 'bart@simpsons.com', phone: '555-222-1234' },
 *             { name: 'Homer', email: 'homer@simpsons.com', phone: '555-222-1244' },
 *             { name: 'Marge', email: 'marge@simpsons.com', phone: '555-222-1254' }
 *         ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Simpsons',
 *         store: Ext.data.StoreManager.lookup('simpsonsStore'),
 *         columns: [
 *             {header: 'Name', dataIndex: 'name', editor: 'textfield'},
 *             {header: 'Email', dataIndex: 'email', flex:1,
 *                 editor: {
 *                     xtype: 'textfield',
 *                     allowBlank: false
 *                 }
 *             },
 *             {header: 'Phone', dataIndex: 'phone'}
 *         ],
 *         selModel: 'rowmodel',
 *         plugins: {
 *             rowediting: {
 *                 clicksToEdit: 1
 *             }
 *         },
 *         height: 200,
 *         width: 400,
 *         renderTo: Ext.getBody()
 *     });
 *
 */
Ext.define('Ext.grid.plugin.RowEditing', {
    extend: 'Ext.grid.plugin.Editing',
    alias: 'plugin.rowediting',
    requires: [
        'Ext.grid.RowEditor'
    ],
    lockableScope: 'top',
    editStyle: 'row',
    /**
     * @cfg {Boolean} [autoCancel=true]
     * `true` to automatically cancel any pending changes when the row editor begins editing
     * a new row. `false` to force the user to explicitly cancel the pending changes.
     * Note that this option is mutually exclusive with {@link #autoUpdate}.
     */
    autoCancel: true,
    /**
     * @cfg {Boolean} [autoUpdate=false]
     * Set this to `true` to automatically confirm any pending changes when the row editor
     * begins editing a new row. When `false`, the user will need to explicitly confirm
     * the pending changes.
     * Note that if this is set to `true`, {@link #autoCancel} will be set to `false`.
     */
    autoUpdate: false,
    /**
     * @cfg {Boolean} [removeUnmodified=false]
     * If configured as `true`, then canceling an edit on a newly inserted
     * record which has not been modified will delete that record from the store.
     */
    /**
     * @cfg {Number} clicksToMoveEditor
     * The number of clicks to move the row editor to a new row while it is visible and actively
     * editing another row. This will default to the same value as
     * {@link Ext.grid.plugin.Editing#clicksToEdit clicksToEdit}.
     */
    /**
     * @cfg {Boolean} errorSummary
     * True to show a {@link Ext.tip.ToolTip tooltip} that summarizes all validation errors present
     * in the row editor. Set to false to prevent the tooltip from showing.
     */
    errorSummary: true,
    /**
     * @cfg {String} [formAriaLabel="'Editing row {0}'"]
     * The ARIA label template for screen readers to announce when row editing starts.
     * This label can be a {@link Ext.String#format} template, with the only parameter
     * being the row number. Note that row numbers start at base {@link #formAriaLabelRowBase}.
     * @locale
     */
    formAriaLabel: 'Editing row {0}',
    /**
     * @cfg {Number} [formAriaLabelRowBase=2]
     * Screen readers will announce grid column header as first row of the ARIA table,
     * so the first actual data row is #2 for screen reader users. If your grid has
     * more than one column header row, you might want to increase this number.
     * If the column header is not visible, the base will be decreased automatically.
     * @locale
     */
    formAriaLabelRowBase: 2,
    constructor: function() {
        var me = this;
        me.callParent(arguments);
        if (!me.clicksToMoveEditor) {
            me.clicksToMoveEditor = me.clicksToEdit;
        }
        me.autoCancel = !!me.autoCancel;
        me.autoUpdate = !!me.autoUpdate;
        if (me.autoUpdate) {
            me.autoCancel = false;
        }
    },
    init: function(grid) {
        this.callParent([
            grid
        ]);
        // This plugin has an interest in processing a request for actionable mode.
        // It does not actually enter actionable mode, it just calls startEdit
        if (grid.lockedGrid) {
            grid.lockedGrid.registerActionable(this);
            grid.normalGrid.registerActionable(this);
        } else {
            grid.registerActionable(this);
        }
    },
    destroy: function() {
        Ext.destroy(this.editor);
        this.callParent();
    },
    onBeforeReconfigure: function() {
        this.callParent(arguments);
        this.cancelEdit();
    },
    onReconfigure: function(grid, store, columns) {
        var ed = this.editor;
        this.callParent(arguments);
        // Only need to adjust column widths if we have new columns 
        if (columns && ed && ed.rendered) {
            ed.needsSyncFieldWidths = true;
        }
    },
    shouldStartEdit: function(editor) {
        return true;
    },
    /**
     * Starts editing the specified record, using the specified Column definition to define
     * which field is being edited.
     * @param {Ext.data.Model} record The Store data record which backs the row to be edited.
     * @param {Ext.grid.column.Column/Number} [columnHeader] The Column object defining
     * the column field to be focused, or index of the column. If not specified, it will default
     * to the first visible column.
     * @return {Boolean} `true` if editing was started, `false` otherwise.
     */
    startEdit: function(record, columnHeader) {
        var me = this,
            editor = me.getEditor(),
            context;
        if (Ext.isEmpty(columnHeader)) {
            columnHeader = me.grid.getTopLevelVisibleColumnManager().getHeaderAtIndex(0);
        }
        if (editor.beforeEdit() !== false) {
            context = me.getEditingContext(record, columnHeader, true);
            if (context && me.beforeEdit(context) !== false && me.fireEvent('beforeedit', me, context) !== false && !context.cancel) {
                me.context = context;
                // If editing one side of a lockable grid, cancel any edit on the other side.
                if (me.lockingPartner) {
                    me.lockingPartner.cancelEdit();
                }
                editor.startEdit(context.record, context.column, context);
                me.editing = true;
                return true;
            }
        }
        return false;
    },
    /**
     * This method is called when actionable mode is requested for a cell. 
     * @param {Ext.grid.CellContext} pos The position at which actionable mode was requested.
     * @return {Boolean} `false` Actionable mode is *not* entered for RowEditing.
     * @protected
     */
    activateCell: function(pos) {
        // Only activate editing if there are no readily activatable elements
        // in the activate position.
        // We defer to those focusables. Editing may be started on other columns.
        if (!pos.getCell(true).querySelector('[tabIndex="-1"]')) {
            this.startEdit(pos.record, pos.column);
            return true;
        }
    },
    /**
     * @method
     * Called by TableView#suspendActionableMode to suspend actionable processing while
     * the view is being changed.
     * @protected
     */
    suspend: Ext.emptyFn,
    /**
     * @method
     * Called by TableView#resumeActionableMode to resume actionable processing after
     * the view has been changed.
     * @param {Ext.grid.CellContext} position The position at which to resume actionable processing.
     * @return {Boolean} `true` if this Actionable has successfully resumed.
     * @protected
     */
    resume: Ext.emptyFn,
    /**
     * @private
     * The {@link Ext.grid.RowEditor RowEditor} hooks up a KeyNav to call this method
     * to complete the edit.
     */
    onEnterKey: function(e) {
        var me = this,
            targetComponent;
        // KeyMap entry for EnterKey added after the entry that sets actionable mode,
        // so this will get called after that handler. We must ignore ENTER key in actionable mode.
        if (!me.grid.ownerGrid.actionableMode && me.editing) {
            targetComponent = Ext.getCmp(e.getTarget().getAttribute('componentId'));
            // ENTER when a picker is expanded does not complete the edit
            if (!(targetComponent && targetComponent.isPickerField && targetComponent.isExpanded)) {
                me.completeEdit();
            }
        }
    },
    cancelEdit: function() {
        var me = this;
        if (me.editing) {
            me.getContextFieldValues();
            me.getEditor().cancelEdit();
            me.callParent(arguments);
            return;
        }
        // If we aren't editing, return true to allow the event to bubble
        return true;
    },
    completeEdit: function() {
        var me = this,
            context = me.context;
        if (me.editing && me.validateEdit(context)) {
            me.editing = false;
            me.fireEvent('edit', me, context);
        }
    },
    validateEdit: function() {
        this.getContextFieldValues();
        return this.callParent(arguments) && this.getEditor().completeEdit();
    },
    getEditor: function() {
        var me = this;
        if (!me.editor) {
            me.editor = me.initEditor();
        }
        return me.editor;
    },
    getContextFieldValues: function() {
        var editor = this.editor,
            context = this.context,
            record = context.record,
            newValues = {},
            originalValues = {},
            editors = editor.query('>[isFormField]'),
            len = editors.length,
            i, name, item;
        for (i = 0; i < len; i++) {
            item = editors[i];
            name = item.dataIndex;
            newValues[name] = item.getValue();
            originalValues[name] = record.get(name);
        }
        Ext.apply(context, {
            newValues: newValues,
            originalValues: originalValues
        });
    },
    /**
     * @private
     */
    initEditor: function() {
        return new Ext.grid.RowEditor(this.initEditorConfig());
    },
    initEditorConfig: function() {
        var me = this,
            grid = me.grid,
            view = me.view,
            headerCt = grid.headerCt,
            btns = [
                'saveBtnText',
                'cancelBtnText',
                'errorsText',
                'dirtyText'
            ],
            b,
            bLen = btns.length,
            cfg = {
                autoCancel: me.autoCancel,
                autoUpdate: me.autoUpdate,
                removeUnmodified: me.removeUnmodified,
                errorSummary: me.errorSummary,
                formAriaLabel: me.formAriaLabel,
                formAriaLabelRowBase: me.formAriaLabelRowBase + (grid.hideHeaders ? -1 : 0),
                fields: headerCt.getGridColumns(),
                hidden: true,
                view: view,
                // keep a reference..
                editingPlugin: me
            },
            item;
        for (b = 0; b < bLen; b++) {
            item = btns[b];
            if (Ext.isDefined(me[item])) {
                cfg[item] = me[item];
            }
        }
        return cfg;
    },
    /**
     * @private
     */
    initEditTriggers: function() {
        var me = this,
            view = me.view,
            moveEditorEvent = me.clicksToMoveEditor === 1 ? 'click' : 'dblclick';
        me.callParent(arguments);
        if (me.clicksToMoveEditor !== me.clicksToEdit) {
            me.mon(view, 'cell' + moveEditorEvent, me.moveEditorByClick, me);
        }
        view.on({
            render: function() {
                me.mon(me.grid.headerCt, {
                    scope: me,
                    columnresize: me.onColumnResize,
                    columnhide: me.onColumnHide,
                    columnshow: me.onColumnShow
                });
            },
            single: true
        });
    },
    moveEditorByClick: function() {
        var me = this;
        if (me.editing) {
            me.superclass.onCellClick.apply(me, arguments);
        }
    },
    /**
     * @private
     */
    onColumnAdd: function(ct, column, pos) {
        var me = this,
            editor;
        if (column.isHeader) {
            me.initFieldAccessors(column);
            // Only inform the editor about a new column if the editor has already been
            // instantiated, so do not use getEditor which instantiates the editor if not present.
            editor = me.editor;
            if (editor) {
                editor.onColumnAdd(column, pos);
            }
        }
    },
    // Ensure editors are cleaned up.
    beforeGridHeaderDestroy: function(headerCt) {
        var columns = this.grid.getColumnManager().getColumns(),
            len = columns.length,
            i, column, field;
        for (i = 0; i < len; i++) {
            column = columns[i];
            // If it has a field accessor, then destroy any field, and remove the accessors.
            if (column.hasEditor) {
                if (column.hasEditor() && (field = column.getEditor())) {
                    field.destroy();
                }
                this.removeFieldAccessors(column);
            }
        }
    },
    /**
     * @private
     */
    onColumnResize: function(ct, column, width) {
        var me = this,
            editor;
        if (column.isHeader) {
            editor = me.getEditor();
            if (editor) {
                editor.onColumnResize(column, width);
            }
        }
    },
    /**
     * @private
     */
    onColumnHide: function(ct, column) {
        // no isHeader check here since its already a columnhide event.
        var me = this,
            editor = me.getEditor();
        if (editor) {
            editor.onColumnHide(column);
        }
    },
    /**
     * @private
     */
    onColumnShow: function(ct, column) {
        // no isHeader check here since its already a columnshow event.
        var me = this,
            editor = me.getEditor();
        if (editor) {
            editor.onColumnShow(column);
        }
    },
    /**
     * @private
     */
    onColumnMove: function(ct, column, fromIdx, toIdx) {
        // no isHeader check here since its already a columnmove event.
        var me = this,
            editor = me.getEditor();
        // Inject field accessors on move because if the move FROM the main headerCt
        // and INTO a grouped header, the accessors will have been deleted but not added.
        // They are added conditionally.
        me.initFieldAccessors(column);
        if (editor) {
            // Must adjust the toIdx to account for removal if moving rightwards
            // because RowEditor.onColumnMove just calls Container.move which does not do this.
            editor.onColumnMove(column, fromIdx, toIdx);
        }
    },
    /**
     * @private
     */
    setColumnField: function(column, field) {
        var me = this,
            editor = me.getEditor();
        if (editor) {
            // Remove the old editor and destroy it.
            editor.destroyColumnEditor(column);
        }
        me.callParent(arguments);
        if (editor) {
            editor.insertColumnEditor(column);
        }
    },
    createColumnField: function(column, defaultField) {
        var editor = this.editor,
            def, field;
        if (editor) {
            def = editor.getDefaultFieldCfg();
        }
        field = this.callParent([
            column,
            defaultField || def
        ]);
        if (field) {
            field.skipLabelForAttribute = true;
            field.ariaAttributes = field.ariaAttributes || {};
            if (this.grid.hideHeaders) {
                field.ariaAttributes['aria-label'] = column.text;
            } else {
                field.ariaAttributes['aria-labelledby'] = column.id;
            }
        }
        return field;
    }
});

/**
 * Plugin (ptype = 'rowexpander') that adds the ability to have a Column in a grid which enables
 * a second row body which expands/contracts.  The expand/contract behavior is configurable to react
 * on clicking of the column, double click of the row, and/or hitting enter while a row is selected.
 *
 * **Note:** The {@link Ext.grid.plugin.RowExpander rowexpander} plugin and the rowbody
 * feature are exclusive and cannot both be set on the same grid / tree.
 */
Ext.define('Ext.grid.plugin.RowExpander', {
    extend: 'Ext.plugin.Abstract',
    lockableScope: 'top',
    requires: [
        'Ext.grid.feature.RowBody'
    ],
    alias: 'plugin.rowexpander',
    /**
     * @cfg {Number} [columnWidth=24]
     * The width of the row expander column which contains the [+]/[-] icons to toggle
     * row expansion.
     */
    columnWidth: 24,
    /**
     * @cfg {Ext.XTemplate} rowBodyTpl
     * An XTemplate which, when passed a record data object, produces HTML for the expanded
     * row content.
     *
     * Note that if this plugin is applied to a lockable grid, the rowBodyTpl applies to the normal
     * (unlocked) side. See {@link #lockedTpl}
     *
     */
    rowBodyTpl: null,
    /**
     * @cfg {Ext.XTemplate} [lockedTpl]
     * An XTemplate which, when passed a record data object, produces HTML for the expanded
     * row content *on the locked side of a lockable grid*.
     */
    lockedTpl: null,
    /**
     * @cfg {Boolean} expandOnEnter
     * This config is no longer supported. The Enter key initiated the grid's actinoable mode.
     */
    /**
     * @cfg {Boolean} expandOnDblClick
     * `true` to toggle a row between expanded/collapsed when double clicked
     * (defaults to `true`).
     */
    expandOnDblClick: true,
    /**
     * @cfg {Boolean} selectRowOnExpand
     * `true` to select a row when clicking on the expander icon
     * (defaults to `false`).
     */
    selectRowOnExpand: false,
    /**
     * @cfg {Boolean} scrollIntoViewOnExpand
     * @since 6.2.0
     * `true` to ensure that the full row expander body is visible when clicking on the expander
     * icon (defaults to `true`)
     */
    scrollIntoViewOnExpand: true,
    /**
     * @cfg {Number}
     * The width of the Row Expander column header
     */
    headerWidth: 24,
    /**
     * @cfg {Boolean} [bodyBefore=false]
     * Configure as `true` to put the row expander body *before* the data row.
     * 
     */
    bodyBefore: false,
    rowBodyTrSelector: '.' + Ext.baseCSSPrefix + 'grid-rowbody-tr',
    rowBodyHiddenCls: Ext.baseCSSPrefix + 'grid-row-body-hidden',
    rowCollapsedCls: Ext.baseCSSPrefix + 'grid-row-collapsed',
    addCollapsedCls: {
        fn: function(out, values, parent) {
            var me = this.rowExpander;
            if (!me.recordsExpanded[values.record.internalId]) {
                values.itemClasses.push(me.rowCollapsedCls);
            }
            this.nextTpl.applyOut(values, out, parent);
        },
        syncRowHeights: function(lockedItem, normalItem) {
            this.rowExpander.syncRowHeights(lockedItem, normalItem);
        },
        // We need a high priority to get in ahead of the outerRowTpl
        // so we can setup row data
        priority: 20000
    },
    /**
     * @event expandbody
     * **Fired through the grid's View**
     * @param {HTMLElement} rowNode The &lt;tr> element which owns the expanded row.
     * @param {Ext.data.Model} record The record providing the data.
     * @param {HTMLElement} expandRow The &lt;tr> element containing the expanded data.
     */
    /**
     * @event collapsebody
     * **Fired through the grid's View.**
     * @param {HTMLElement} rowNode The &lt;tr> element which owns the expanded row.
     * @param {Ext.data.Model} record The record providing the data.
     * @param {HTMLElement} expandRow The &lt;tr> element containing the expanded data.
     */
    setCmp: function(grid) {
        var me = this,
            features;
        me.callParent([
            grid
        ]);
        // Keep track of which record internalIds are expanded.
        me.recordsExpanded = {};
        if (!me.rowBodyTpl) {
            Ext.raise("The 'rowBodyTpl' config is required and is not defined.");
        }
        me.rowBodyTpl = Ext.XTemplate.getTpl(me, 'rowBodyTpl');
        features = me.getFeatureConfig(grid);
        if (grid.features) {
            grid.features = Ext.Array.push(features, grid.features);
        } else {
            grid.features = features;
        }
    },
    // NOTE: features have to be added before init (before Table.initComponent)
    /**
     * @protected
     * @return {Array} And array of Features or Feature config objects.
     * Returns the array of Feature configurations needed to make the RowExpander work.
     * May be overridden in a subclass to modify the returned array.
     */
    getFeatureConfig: function(grid) {
        var me = this,
            features = [],
            featuresCfg = {
                ftype: 'rowbody',
                rowExpander: me,
                rowIdCls: me.rowIdCls,
                bodyBefore: me.bodyBefore,
                recordsExpanded: me.recordsExpanded,
                rowBodyHiddenCls: me.rowBodyHiddenCls,
                rowCollapsedCls: me.rowCollapsedCls,
                setupRowData: me.getRowBodyFeatureData,
                setup: me.setup
            };
        features.push(Ext.apply({
            lockableScope: 'normal',
            getRowBodyContents: me.getRowBodyContentsFn(me.rowBodyTpl)
        }, featuresCfg));
        // Locked side will need a copy to keep the two DOM structures symmetrical.
        // A lockedTpl config is available to create content in locked side.
        // The enableLocking flag is set early in Ext.panel.Table#initComponent if any columns
        // are locked.
        if (grid.enableLocking) {
            features.push(Ext.apply({
                lockableScope: 'locked',
                getRowBodyContents: me.lockedTpl ? me.getRowBodyContentsFn(me.lockedTpl) : function() {
                    return '';
                }
            }, featuresCfg));
        }
        return features;
    },
    getRowBodyContentsFn: function(rowBodyTpl) {
        var me = this;
        return function(rowValues) {
            rowBodyTpl.owner = me;
            return rowBodyTpl.applyTemplate(rowValues.record.getData());
        };
    },
    init: function(grid) {
        var me = this,
            // Plugin attaches to topmost grid if lockable
            ownerLockable = grid.lockable && grid,
            view, lockedView, normalView;
        if (ownerLockable) {
            me.lockedGrid = ownerLockable.lockedGrid;
            me.normalGrid = ownerLockable.normalGrid;
            lockedView = me.lockedView = me.lockedGrid.getView();
            normalView = me.normalView = me.normalGrid.getView();
        }
        me.callParent([
            grid
        ]);
        me.grid = grid;
        view = me.view = grid.getView();
        // If the owning grid is lockable, ensure the collapsed class is applied to the locked side
        // by adding a row processor to both views.
        if (ownerLockable) {
            me.bindView(lockedView);
            me.bindView(normalView);
            me.addExpander(me.lockedGrid.headerCt.items.getCount() ? me.lockedGrid : me.normalGrid);
            // Add row processor which adds collapsed class.
            // Ensure tpl and view can access this plugin via a "rowExpander" property.
            lockedView.addRowTpl(me.addCollapsedCls).rowExpander = normalView.addRowTpl(me.addCollapsedCls).rowExpander = lockedView.rowExpander = normalView.rowExpander = me;
            // If our client grid part of a lockable grid, we listen to its ownerLockable's
            // processcolumns
            ownerLockable.mon(ownerLockable, {
                processcolumns: me.onLockableProcessColumns,
                lockcolumn: me.onColumnLock,
                unlockcolumn: me.onColumnUnlock,
                scope: me
            });
        } else // Add row processor which adds collapsed class
        {
            me.bindView(view);
            // Ensure tpl and view can access this plugin
            view.addRowTpl(me.addCollapsedCls).rowExpander = view.rowExpander = me;
            me.addExpander(grid);
            grid.on('beforereconfigure', me.beforeReconfigure, me);
        }
    },
    onItemAdd: function(newRecords, startIndex, newItems) {
        var me = this,
            ownerLockable = me.grid.lockable,
            len = newItems.length,
            record, i;
        // If any added items are expanded, we will need a syncRowHeights call on next layout
        for (i = 0; i < len; i++) {
            record = newRecords[i];
            if (!record.isNonData && me.recordsExpanded[record.internalId]) {
                if (ownerLockable) {
                    me.grid.syncRowHeightOnNextLayout = true;
                }
                return;
            }
        }
    },
    beforeReconfigure: function(grid, store, columns, oldStore, oldColumns) {
        var me = this;
        if (columns) {
            me.expanderColumn = new Ext.grid.column.Column(me.getHeaderConfig());
            columns.unshift(me.expanderColumn);
        }
    },
    onLockableProcessColumns: function(lockable, lockedHeaders, normalHeaders) {
        this.addExpander(lockedHeaders.length ? lockable.lockedGrid : lockable.normalGrid);
    },
    /**
     * @private
     * Inject the expander column into the correct grid.
     *
     * If we are expanding the normal side of a lockable grid, poke the column
     * into the locked side if the locked side has columns
     */
    addExpander: function(expanderGrid) {
        var me = this,
            selModel = expanderGrid.getSelectionModel(),
            checkBoxPosition = selModel.injectCheckbox;
        me.expanderColumn = expanderGrid.headerCt.insert(0, me.getHeaderConfig());
        // If a CheckboxModel, and it's position is 0, it must now go at position one because this
        // cell always gets in at position zero, and spans 2 columns.
        if (checkBoxPosition === 0 || checkBoxPosition === 'first') {
            checkBoxPosition = 1;
        }
        selModel.injectCheckbox = checkBoxPosition;
    },
    getRowBodyFeatureData: function(record, idx, rowValues) {
        var me = this;
        me.self.prototype.setupRowData.apply(me, arguments);
        rowValues.rowBody = me.getRowBodyContents(rowValues);
        rowValues.rowBodyCls = me.recordsExpanded[record.internalId] ? '' : me.rowBodyHiddenCls;
    },
    bindView: function(view) {
        var me = this,
            listeners = {
                itemkeydown: me.onKeyDown,
                scope: me
            };
        if (me.expandOnDblClick) {
            listeners.itemdblclick = me.onDblClick;
        }
        if (me.grid.lockable) {
            listeners.itemadd = me.onItemAdd;
        }
        view.on(listeners);
    },
    onKeyDown: function(view, record, row, rowIdx, e) {
        var me = this,
            key = e.getKey(),
            pos = view.getNavigationModel().getPosition(),
            isCollapsed;
        if (pos) {
            row = Ext.fly(row);
            isCollapsed = row.hasCls(me.rowCollapsedCls);
            // + key on collapsed or - key on expanded
            if (((key === 107 || (key === 187 && e.shiftKey)) && isCollapsed) || ((key === 109 || key === 189) && !isCollapsed)) {
                me.toggleRow(rowIdx, record);
            }
        }
    },
    onDblClick: function(view, record, row, rowIdx, e) {
        this.toggleRow(rowIdx, record);
    },
    toggleRow: function(rowIdx, record) {
        var me = this,
            // If we are handling a lockable assembly,
            // handle the normal view first
            view = me.normalView || me.view,
            fireView = view,
            rowNode = view.getNode(rowIdx),
            normalRow = Ext.fly(rowNode),
            lockedRow,
            nextBd = normalRow.down(me.rowBodyTrSelector, true),
            wasCollapsed = normalRow.hasCls(me.rowCollapsedCls),
            addOrRemoveCls = wasCollapsed ? 'removeCls' : 'addCls',
            ownerLockable = me.grid.lockable && me.grid;
        normalRow[addOrRemoveCls](me.rowCollapsedCls);
        Ext.fly(nextBd)[addOrRemoveCls](me.rowBodyHiddenCls);
        me.recordsExpanded[record.internalId] = wasCollapsed;
        Ext.suspendLayouts();
        // Sync the collapsed/hidden classes on the locked side
        if (ownerLockable) {
            // It's the top level grid's LockingView that does the firing
            // when there's a lockable assembly involved.
            fireView = ownerLockable.getView();
            // Only attempt to toggle lockable side if it is visible.
            if (me.lockedGrid.isVisible()) {
                view = me.lockedView;
                // The other side must be thrown into the layout matrix so that
                // row height syncing can be done. If it is collapsed but floated,
                // it will not automatically be added to the layout when the top 
                // level grid layout calculates its layout children.
                view.lockingPartner.updateLayout();
                // Process the locked side.
                lockedRow = Ext.fly(view.getNode(rowIdx));
                // Just because the grid is locked, doesn't mean we'll necessarily have
                // a locked row.
                if (lockedRow) {
                    lockedRow[addOrRemoveCls](me.rowCollapsedCls);
                    // If there is a template for expander content in the locked side,
                    // toggle that side too
                    nextBd = lockedRow.down(me.rowBodyTrSelector, true);
                    Ext.fly(nextBd)[addOrRemoveCls](me.rowBodyHiddenCls);
                }
            }
            // We're going to need a layout run to synchronize row heights
            ownerLockable.syncRowHeightOnNextLayout = true;
        }
        fireView.fireEvent(wasCollapsed ? 'expandbody' : 'collapsebody', rowNode, record, nextBd);
        view.refreshSize(true);
        Ext.resumeLayouts(true);
        if (me.scrollIntoViewOnExpand && wasCollapsed) {
            me.grid.ensureVisible(rowIdx);
        }
    },
    // Called from TableLayout.finishedLayout
    syncRowHeights: function(lockedItem, normalItem) {
        var me = this,
            lockedBd = Ext.fly(lockedItem).down(me.rowBodyTrSelector),
            normalBd = Ext.fly(normalItem).down(me.rowBodyTrSelector),
            lockedHeight, normalHeight;
        // If expanded, we have to ensure expander row heights are synched
        if (normalBd.isVisible()) {
            // If heights are different, expand the smallest one
            if ((lockedHeight = lockedBd.getHeight()) !== (normalHeight = normalBd.getHeight())) {
                if (lockedHeight > normalHeight) {
                    normalBd.setHeight(lockedHeight);
                } else {
                    lockedBd.setHeight(normalHeight);
                }
            }
        } else // When not expanded we do not control the heights
        {
            lockedBd.dom.style.height = normalBd.dom.style.height = '';
        }
    },
    onColumnUnlock: function(lockable, column) {
        var me = this,
            lockedColumns;
        lockable = lockable || me.grid;
        lockedColumns = lockable.lockedGrid.visibleColumnManager.getColumns();
        // User has unlocked all columns and left only the expander column in the locked side.
        if (lockedColumns.length === 1) {
            lockable.normalGrid.removeCls(Ext.baseCSSPrefix + 'grid-hide-row-expander-spacer');
            lockable.lockedGrid.addCls(Ext.baseCSSPrefix + 'grid-hide-row-expander-spacer');
            if (lockedColumns[0] === me.expanderColumn) {
                lockable.unlock(me.expanderColumn);
            } else {
                lockable.lock(me.expanderColumn, 0);
            }
        }
    },
    onColumnLock: function(lockable, column) {
        var me = this,
            lockedColumns;
        lockable = lockable || me.grid;
        lockedColumns = me.lockedGrid.visibleColumnManager.getColumns();
        // This is the first column to move into the locked side.
        // The expander column must follow it.
        if (lockedColumns.length === 1) {
            me.lockedGrid.headerCt.insert(0, me.expanderColumn);
            lockable.normalGrid.addCls(Ext.baseCSSPrefix + 'grid-hide-row-expander-spacer');
            lockable.lockedGrid.removeCls(Ext.baseCSSPrefix + 'grid-hide-row-expander-spacer');
        }
    },
    getHeaderConfig: function() {
        var me = this,
            lockable = me.grid.lockable && me.grid;
        return {
            width: me.headerWidth,
            ignoreExport: true,
            lockable: false,
            autoLock: true,
            sortable: false,
            resizable: false,
            draggable: false,
            hideable: false,
            menuDisabled: true,
            tdCls: Ext.baseCSSPrefix + 'grid-cell-special',
            innerCls: Ext.baseCSSPrefix + 'grid-cell-inner-row-expander',
            renderer: function() {
                return '<div class="' + Ext.baseCSSPrefix + 'grid-row-expander" role="presentation" tabIndex="0"></div>';
            },
            processEvent: function(type, view, cell, rowIndex, cellIndex, e, record) {
                var isTouch = e.pointerType === 'touch',
                    isExpanderClick = !!e.getTarget('.' + Ext.baseCSSPrefix + 'grid-row-expander');
                if ((type === "click" && isExpanderClick) || (type === 'keydown' && e.getKey() === e.SPACE)) {
                    // Focus the cell on real touch tap.
                    // This is because the toggleRow saves and restores focus
                    // which may be elsewhere than clicked on causing a scroll jump.
                    if (isTouch) {
                        cell.focus();
                    }
                    me.toggleRow(rowIndex, record, e);
                    e.stopSelection = !me.selectRowOnExpand;
                } else if (e.type === 'mousedown' && !isTouch && isExpanderClick) {
                    e.preventDefault();
                }
            },
            // This column always migrates to the locked side if the locked side is visible.
            // It has to report this correctly so that editors can position things correctly
            isLocked: function() {
                return lockable && (lockable.lockedGrid.isVisible() || this.locked);
            },
            // In an editor, this shows nothing.
            editRenderer: function() {
                return '&#160;';
            }
        };
    }
});

/**
 * Plugin (ptype = 'rowwidget') that adds the ability to second row body in a grid
 * which expands/contracts.
 *
 * The expand/contract behavior is configurable to react on clicking of the column,
 * double click of the row, and/or hitting enter while a row is selected.
 *
 * The expansion row may contain a {@link #cfg-widget} which is primed with the record
 * of the corresponding grid row. The widget's
 * {@link Ext.Component#cfg-defaultBindProperty defaultBindProperty} property is set to the record.
 */
Ext.define('Ext.grid.plugin.RowWidget', {
    extend: 'Ext.grid.plugin.RowExpander',
    alias: 'plugin.rowwidget',
    mixins: [
        'Ext.mixin.Identifiable',
        'Ext.mixin.StyleCacher'
    ],
    lockableScope: 'top',
    config: {
        /**
         * @cfg {Object} defaultWidgetUI
         * A map of xtype to {@link Ext.Component#ui} names to use when using Components
         * in the expansion row.
         */
        defaultWidgetUI: {}
    },
    /**
     * @cfg {Object} widget
     * A config object containing an {@link Ext.Component#cfg-xtype xtype}.
     *
     * This is used to create the widgets or components which are rendered into the expansion row.
     *
     * The associated grid row's record is used to update the widget/component's
     * {@link Ext.Component#defaultBindProperty defaultBindProperty}.
     *
     * Note that if this plugin is applied to a lockable grid, the widget applies to the normal
     * (unlocked) side.
     * See {@link #lockedWidget}
     *
     */
    widget: null,
    /**
     * @cfg {Object} [lockedWidget]
     * A config object containing an {@link Ext.Component#cfg-xtype xtype}.
     *
     * This is used to create the widgets or components which are rendered into the expansion row
     * *on the locked side of a lockable grid*.
     */
    lockedWidget: null,
    addCollapsedCls: {
        fn: function(out, values, parent) {
            var me = this.rowExpander;
            if (!me.recordsExpanded[values.record.internalId]) {
                values.itemClasses.push(me.rowCollapsedCls);
            }
            this.nextTpl.applyOut(values, out, parent);
        },
        // We need a high priority to get in ahead of the outerRowTpl
        // so we can setup row data
        priority: 20000
    },
    setCmp: function(grid) {
        var me = this,
            features, widget;
        // Generate a unique class name so we can identify our row element.
        me.rowIdCls = Ext.id(null, Ext.baseCSSPrefix + 'rowwidget-');
        // Keep track of which record internalIds are expanded.
        me.recordsExpanded = {};
        Ext.plugin.Abstract.prototype.setCmp.apply(me, arguments);
        widget = me.widget;
        if (!widget || widget.isComponent) {
            Ext.raise('RowWidget requires a widget configuration.');
        }
        me.widget = widget = Ext.apply({}, widget);
        // Apply the default UI for the xtype which is going to feature
        // in the normal side's expansion row.
        if (!widget.ui) {
            widget.ui = me.getDefaultWidgetUI()[widget.xtype] || 'default';
        }
        // If the grid is a lockable assembly, we have to track locked widgets.
        if (grid.enableLocking && me.lockedWidget) {
            me.lockedWidget = widget = Ext.apply({}, me.lockedWidget);
            // Apply the default UI for the xtype which is going to feature
            // in the locked side's expansion row.
            if (!widget.ui) {
                widget.ui = me.getDefaultWidgetUI()[widget.xtype] || 'default';
            }
        }
        features = me.getFeatureConfig(grid);
        if (grid.features) {
            grid.features = Ext.Array.push(features, grid.features);
        } else {
            grid.features = features;
        }
    },
    // NOTE: features have to be added before init (before Table.initComponent)
    /**
     * @protected
     * @return {Array} And array of Features or Feature config objects.
     * Returns the array of Feature configurations needed to make the RowWidget work.
     * May be overridden in a subclass to modify the returned array.
     */
    getFeatureConfig: function(grid) {
        var me = this,
            features = [],
            featuresCfg = {
                ftype: 'rowbody',
                rowExpander: me,
                doSync: false,
                rowIdCls: me.rowIdCls,
                bodyBefore: me.bodyBefore,
                recordsExpanded: me.recordsExpanded,
                rowBodyHiddenCls: me.rowBodyHiddenCls,
                rowCollapsedCls: me.rowCollapsedCls,
                setupRowData: me.setupRowData,
                setup: me.setup,
                // Do not relay click events into the client grid's row
                onClick: Ext.emptyFn
            };
        features.push(Ext.apply({
            lockableScope: 'normal'
        }, featuresCfg));
        // Locked side will need a copy to keep the two DOM structures symmetrical.
        // A lockedWidget config is available to create content in locked side.
        // The enableLocking flag is set early in Ext.panel.Table#initComponent
        // if any columns are locked.
        if (grid.enableLocking) {
            features.push(Ext.apply({
                lockableScope: 'locked'
            }, featuresCfg));
        }
        return features;
    },
    setupRowData: function(record, rowIndex, rowValues) {
        var me = this.rowExpander;
        me.rowBodyFeature = this;
        rowValues.rowBodyCls = me.recordsExpanded[record.internalId] ? '' : me.rowBodyHiddenCls;
    },
    bindView: function(view) {
        var me = this;
        me.viewListeners = view.on({
            refresh: me.onViewRefresh,
            itemadd: me.onItemAdd,
            scope: me,
            destroyable: true
        });
        Ext.override(view, me.viewOverrides);
    },
    destroy: function() {
        var me = this,
            id = me.getId();
        me.viewListeners.destroy();
        if (me.grid.lockable) {
            me.grid.destroyManagedWidgets(id + '-' + me.lockedView.getId());
            me.grid.destroyManagedWidgets(id + '-' + me.normalView.getId());
        } else {
            me.grid.destroyManagedWidgets(id + '-' + me.view.getId());
        }
        me.callParent();
    },
    privates: {
        viewOverrides: {
            handleEvent: function(e) {
                // An override applied to the client view so that it ignores events
                // from within the expander row
                // Ignore all events from within our rowwidget
                if (e.getTarget('.' + this.rowExpander.rowIdCls, this.body)) {
                    return;
                }
                this.callParent([
                    e
                ]);
            },
            onFocusEnter: function(e) {
                // An override applied to the client view so that it ignores
                // focus moving into the expander row
                if (e.event.getTarget('.' + this.rowExpander.rowIdCls, this.body)) {
                    return;
                }
                this.callParent([
                    e
                ]);
            },
            toggleChildrenTabbability: function(enableTabbing) {
                // An override applied to the client view so that it does not interfere
                // with tabbability of elements within the expander rows.
                var focusEl = this.getTargetEl(),
                    rows = this.all,
                    restoreOptions = {
                        skipSelf: true
                    },
                    saveOptions = {
                        skipSelf: true,
                        includeSaved: false
                    },
                    i;
                for (i = rows.startIndex; i <= rows.endIndex; i++) {
                    // Extract the data row from each row.
                    // We do not interfere with tabbing in the the expander row.
                    focusEl = Ext.fly(this.getRow(rows.item(i)));
                    if (!focusEl) {
                        
                        continue;
                    }
                    if (enableTabbing) {
                        focusEl.restoreTabbableState(restoreOptions);
                    } else {
                        // Do NOT includeSaved
                        // Once an item has had tabbability saved, do not increment its save level
                        focusEl.saveTabbableState(saveOptions);
                    }
                }
            }
        },
        destroyLiveWidget: function(recId, widget) {
            widget.destroy();
        },
        destroyFreeWidget: function(widget) {
            widget.destroy();
        },
        onItemAdd: function(newRecords, startIndex, newItems, view) {
            var me = this,
                len = newItems.length,
                i, record,
                ownerLockable = me.grid.lockable;
            // May be multiple widgets being layed out here
            Ext.suspendLayouts();
            for (i = 0; i < len; i++) {
                record = newRecords[i];
                if (!record.isNonData && me.recordsExpanded[record.internalId]) {
                    // If any added items are expanded, we will need a syncRowHeights
                    // call on next layout
                    if (ownerLockable) {
                        me.grid.syncRowHeightOnNextLayout = true;
                    }
                    me.addWidget(view, record);
                }
            }
            Ext.resumeLayouts(true);
        },
        onViewRefresh: function(view, records) {
            var me = this,
                rows = view.all,
                itemIndex, recordIndex;
            Ext.suspendLayouts();
            // eslint-disable-next-line max-len
            for (itemIndex = rows.startIndex , recordIndex = 0; itemIndex <= rows.endIndex; itemIndex++ , recordIndex++) {
                if (me.recordsExpanded[records[recordIndex].internalId]) {
                    me.addWidget(view, records[recordIndex]);
                }
            }
            Ext.resumeLayouts(true);
        },
        returnFalse: function() {
            return false;
        },
        /**
         * Returns if possible the widget currently associated with the passed record
         * within the passed view.
         *
         * Note that if the record is not currently in the rendered block, *or*,
         * it has never been expanded then there will not be a widget associated
         * with that `record/view` context.
         * @param {type} view The view for which to return the widget
         * @param {type} record The record for which to return the widget
         * @return {me.lockedLiveWidgets/me.liveWidgets}
         */
        getWidget: function(view, record) {
            var me = this,
                result, widget;
            if (record) {
                widget = me.grid.lockable && view === me.lockedView ? me.lockedWidget : me.widget;
                if (widget) {
                    result = me.grid.createManagedWidget(view, me.getId() + '-' + view.getId(), widget, record);
                    result.measurer = me;
                    result.ownerLayout = view.componentLayout;
                }
            }
            return result;
        },
        addWidget: function(view, record) {
            var me = this,
                target, width, widget,
                hasAttach = !!me.onWidgetAttach,
                isFixedSize = me.isFixedSize,
                el;
            // If the record is non data (placeholder), or not expanded, return
            if (record.isNonData || !me.recordsExpanded[record.internalId]) {
                return;
            }
            target = Ext.fly(view.getNode(record).querySelector(me.rowBodyFeature.innerSelector));
            width = target.getWidth(true) - target.getPadding('lr');
            widget = me.getWidget(view, record);
            // Might be no widget if we are handling a lockable grid
            // and only one side has a widget definition.
            if (widget) {
                if (hasAttach) {
                    Ext.callback(me.onWidgetAttach, me.scope, [
                        me,
                        widget,
                        record
                    ], 0, me);
                }
                el = widget.el || widget.element;
                if (el) {
                    target.dom.appendChild(el.dom);
                    if (!isFixedSize && widget.width !== width) {
                        widget.setWidth(width);
                    } else {
                        widget.updateLayout();
                    }
                    widget.reattachToBody();
                } else {
                    if (!isFixedSize) {
                        widget.width = width;
                    }
                    widget.render(target);
                }
                widget.updateLayout();
            }
            return widget;
        },
        toggleRow: function(rowIdx, record) {
            var me = this,
                // If we are handling a lockable assembly,
                // handle the normal view first
                view = me.normalView || me.view,
                rowNode = view.getNode(rowIdx),
                normalRow = Ext.fly(rowNode),
                lockedRow,
                nextBd = normalRow.down(me.rowBodyTrSelector, true),
                wasCollapsed = normalRow.hasCls(me.rowCollapsedCls),
                addOrRemoveCls = wasCollapsed ? 'removeCls' : 'addCls',
                ownerLockable = me.grid.lockable && me.grid,
                widget, vm;
            normalRow[addOrRemoveCls](me.rowCollapsedCls);
            Ext.fly(nextBd)[addOrRemoveCls](me.rowBodyHiddenCls);
            // All layouts must be coalesced.
            // Particularly important for locking assemblies which need
            // to sync row height on the next layout.
            Ext.suspendLayouts();
            // We're expanding
            if (wasCollapsed) {
                me.recordsExpanded[record.internalId] = true;
                widget = me.addWidget(view, record);
                vm = widget.lookupViewModel();
            } else {
                delete me.recordsExpanded[record.internalId];
                widget = me.getWidget(view, record);
            }
            // Sync the collapsed/hidden classes on the locked side
            if (ownerLockable) {
                // Only attempt to toggle lockable side if it is visible.
                if (ownerLockable.lockedGrid.isVisible()) {
                    view = me.lockedView;
                    // Process the locked side.
                    lockedRow = Ext.fly(view.getNode(rowIdx));
                    // Just because the grid is locked, doesn't mean we'll necessarily
                    // have a locked row.
                    if (lockedRow) {
                        lockedRow[addOrRemoveCls](me.rowCollapsedCls);
                        // If there is a template for expander content in the locked side,
                        // toggle that side too
                        nextBd = lockedRow.down(me.rowBodyTrSelector, true);
                        Ext.fly(nextBd)[addOrRemoveCls](me.rowBodyHiddenCls);
                        // Pass an array if we're in a lockable assembly.
                        if (wasCollapsed && me.lockedWidget) {
                            widget = [
                                widget,
                                me.addWidget(view, record)
                            ];
                        } else {
                            widget = [
                                widget,
                                me.getWidget(view, record)
                            ];
                        }
                    }
                    // We're going to need a layout run to synchronize row heights
                    ownerLockable.syncRowHeightOnNextLayout = true;
                }
            }
            me.view.fireEvent(wasCollapsed ? 'expandbody' : 'collapsebody', rowNode, record, nextBd, widget);
            view.updateLayout();
            // Before layouts are resumed, if we have *expanded* the widget row,
            // then ensure bound data is flushed into the widget so that it assumes its final size.
            if (vm) {
                vm.notify();
            }
            Ext.resumeLayouts(true);
            if (me.scrollIntoViewOnExpand && wasCollapsed) {
                me.grid.ensureVisible(rowIdx);
            }
        }
    }
});

/**
 * A specialized grid implementation intended to mimic the traditional property grid as
 * typically seen in development IDEs.  Each row in the grid represents a property of some object,
 * and the data is stored as a set of name/value pairs in
 * {@link Ext.grid.property.Property Properties}. By default, the editors shown are inferred
 * from the data in the cell. More control over this can be specified by using the
 * {@link #sourceConfig} option. Example usage:
 *
 *     @example
 *     Ext.create('Ext.grid.property.Grid', {
 *         title: 'Properties Grid',
 *         width: 300,
 *         renderTo: Ext.getBody(),
 *         source: {
 *             "(name)": "My Object",
 *             "Created": Ext.Date.parse('10/15/2006', 'm/d/Y'),
 *             "Available": false,
 *             "Version": 0.01,
 *             "Description": "A test object"
 *         }
 *     });
 */
Ext.define('Ext.grid.property.Grid', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.propertygrid',
    alternateClassName: 'Ext.grid.PropertyGrid',
    uses: [
        'Ext.grid.plugin.CellEditing',
        'Ext.grid.property.Store',
        'Ext.grid.property.HeaderContainer',
        'Ext.XTemplate',
        'Ext.grid.CellEditor',
        'Ext.form.field.Date',
        'Ext.form.field.Text',
        'Ext.form.field.Number',
        'Ext.form.field.ComboBox'
    ],
    /**
     * @cfg {Object} sourceConfig
     * This option allows various configurations to be set for each field in the property grid.
     * None of these configurations are required
     *
     * ####displayName
     * A custom name to appear as label for this field. If specified, the display name will be shown
     * in the name column instead of the property name. Example usage:
     *
     *     new Ext.grid.property.Grid({
     *         source: {
     *             clientIsAvailable: true
     *         },
     *         sourceConfig: {
     *             clientIsAvailable: {
     *                 // Custom name different to the field
     *                 displayName: 'Available'
     *             }
     *         }
     *     });
     *
     * ####renderer
     * A function used to transform the underlying value before it is displayed in the grid.
     * By default, the grid supports strongly-typed rendering of strings, dates, numbers and
     * booleans using built-in form editors, but any custom type can be supported and associated
     * with the type of the value. Example usage:
     *
     *     new Ext.grid.property.Grid({
     *         source: {
     *             clientIsAvailable: true
     *         },
     *         sourceConfig: {
     *             clientIsAvailable: {
     *                 // Custom renderer to change the color based on the value
     *                 renderer: function(v){
     *                     var color = v ? 'green' : 'red';
     *                     return '<span style="color: ' + color + ';">' + v + '</span>';
     *                 }
     *             }
     *         }
     *     });
     *
     * ####type
     * Used to explicitly specify the editor type for a particular value. By default, the type is
     * automatically inferred from the value. See {@link #inferTypes}. Accepted values are:
     *
     * - 'date'
     * - 'boolean'
     * - 'number'
     * - 'string'
     *
     * For more advanced control an editor configuration can be passed (see the next section).
     * Example usage:
     *
     *     new Ext.grid.property.Grid({
     *         source: {
     *             attending: null
     *         },
     *         sourceConfig: {
     *             attending: {
     *                 // Force the type to be a numberfield, a null value would otherwise default
     *                 // to a textfield
     *                 type: 'number'
     *             }
     *         }
     *     });
     *
     * ####editor
     * Allows the grid to support additional types of editable fields.  By default, the grid
     * supports strongly-typed editing of strings, dates, numbers and booleans using built-in
     * form editors, but any custom type can be supported and associated with a custom input control
     * by specifying a custom editor. Example usage
     *
     *     new Ext.grid.property.Grid({
     *         // Data object containing properties to edit
     *         source: {
     *             evtStart: '10:00 AM'
     *         },
     *
     *         sourceConfig: {
     *             evtStart: {
     *                 editor: Ext.create('Ext.form.field.Time', {selectOnFocus: true}),
     *                 displayName: 'Start Time'
     *             }
     *         }
     *     });
     */
    /**
    * @cfg {Object} propertyNames
    * An object containing custom property name/display name pairs.
    * If specified, the display name will be shown in the name column instead of the property name.
    * @deprecated 6.5.0 See {@link #sourceConfig} displayName
    */
    /**
     * @cfg {Object} source
     * A data object to use as the data source of the grid (see {@link #setSource} for details).
     */
    /**
     * @cfg {Object} customEditors
     * An object containing name/value pairs of custom editor type definitions that allow
     * the grid to support additional types of editable fields.  By default, the grid supports
     * strongly-typed editing of strings, dates, numbers and booleans using built-in form editors,
     * but any custom type can be supported and associated with a custom input control
     * by specifying a custom editor.  The name of the editor type should correspond
     * with the name of the property that will use the editor.  Example usage:
     *
     *     var grid = new Ext.grid.property.Grid({
     *
     *         // Custom editors for certain property names
     *         customEditors: {
     *             evtStart: Ext.create('Ext.form.TimeField', {selectOnFocus: true})
     *         },
     *
     *         // Displayed name for property names in the source
     *         propertyNames: {
     *             evtStart: 'Start Time'
     *         },
     *
     *         // Data object containing properties to edit
     *         source: {
     *             evtStart: '10:00 AM'
     *         }
     *     });
     * @deprecated 6.5.0 See {@link #sourceConfig} editor
     */
    /**
     * @cfg {Object} customRenderers
     * An object containing name/value pairs of custom renderer type definitions that allow
     * the grid to support custom rendering of fields. By default, the grid supports strongly-typed
     * rendering of strings, dates, numbers and booleans using built-in form editors,
     * but any custom type can be supported and associated with the type of the value.
     * The name of the renderer type should correspond with the name of the property
     * that it will render.  Example usage:
     *
     *     var grid = Ext.create('Ext.grid.property.Grid', {
     *         customRenderers: {
     *             Available: function(v){
     *                 if (v) {
     *                     return '<span style="color: green;">Yes</span>';
     *                 } else {
     *                     return '<span style="color: red;">No</span>';
     *                 }
     *             }
     *         },
     *         source: {
     *             Available: true
     *         }
     *     });
     * @deprecated 6.5.0 See {@link #sourceConfig} renderer
     */
    /**
     * @cfg {String} valueField
     * The name of the field from the property store to use as the value field name.
     * This may be useful if you do not configure the property Grid from an object,
     * but use your own store configuration.
     */
    valueField: 'value',
    /**
     * @cfg {String} nameField
     * The name of the field from the property store to use as the property field name.
     * This may be useful if you do not configure the property Grid from an object,
     * but use your own store configuration.
     */
    nameField: 'name',
    /**
     * @cfg {Boolean} inferTypes
     * True to automatically infer the {@link #sourceConfig type} based on the initial value passed
     * for each field. This ensures the editor remains the correct type even if the value is blanked
     * and becomes empty.
     */
    inferTypes: true,
    /**
     * @cfg {Number/String} [nameColumnWidth=115]
     * Specify the width for the name column. The value column will take any remaining space.
     */
    // private config overrides
    enableColumnMove: false,
    columnLines: true,
    stripeRows: false,
    trackMouseOver: false,
    clicksToEdit: 1,
    enableHdMenu: false,
    gridCls: Ext.baseCSSPrefix + 'property-grid',
    /**
     * @event beforepropertychange
     * Fires before a property value changes.  Handlers can return false to cancel the property
     * change (this will internally call {@link Ext.data.Model#reject} on the property's record).
     * @param {Object} source The source data object for the grid (corresponds to the same object
     * passed in as the {@link #source} config property).
     * @param {String} recordId The record's id in the data store
     * @param {Object} value The current edited property value
     * @param {Object} oldValue The original property value prior to editing
     */
    /**
     * @event propertychange
     * Fires after a property value has changed.
     * @param {Object} source The source data object for the grid (corresponds to the same object
     * passed in as the {@link #source} config property).
     * @param {String} recordId The record's id in the data store
     * @param {Object} value The current edited property value
     * @param {Object} oldValue The original property value prior to editing
     */
    initComponent: function() {
        var me = this,
            // selectOnFocus: true results in weird exceptions thrown when tabbing
            // between cell editors in IE and there's no known cure at the moment
            selectOnFocus = !Ext.isIE,
            view;
        me.source = me.source || {};
        me.addCls(me.gridCls);
        me.plugins = me.plugins || [];
        // Enable cell editing. Inject a custom startEdit which always edits column 1
        // regardless of which column was clicked.
        me.plugins.push(new Ext.grid.plugin.CellEditing({
            clicksToEdit: me.clicksToEdit,
            // Inject a startEdit which always edits the value column
            startEdit: function(record, column) {
                // Maintainer: Do not change this 'this' to 'me'! It is the CellEditing object's
                // own scope.
                return this.self.prototype.startEdit.call(this, record, me.valueColumn);
            },
            // Gets an editor based on the property name and not column itemId
            getEditor: function(record, column) {
                return this.getCachedEditor(record.get(me.nameField), record, column);
            }
        }));
        me.selModel = {
            type: 'cellmodel',
            onCellSelect: function(position) {
                // We are only allowed to select the value column.
                position.column = me.valueColumn;
                position.colIdx = me.valueColumn.getVisibleIndex();
                return this.self.prototype.onCellSelect.call(this, position);
            }
        };
        me.sourceConfig = Ext.apply({}, me.sourceConfig);
        // Create a property.Store from the source object unless configured with a store
        if (!me.store) {
            me.propStore = me.store = new Ext.grid.property.Store(me, me.source);
        }
        me.configure(me.sourceConfig);
        if (me.sortableColumns) {
            me.store.sort('name', 'ASC');
        }
        me.columns = new Ext.grid.property.HeaderContainer(me, me.store);
        me.callParent();
        view = me.getView();
        // Inject a custom implementation of walkCells which only goes up or down
        view.walkCells = me.walkCells;
        // Inject a custom implementation that only allows focusing value column
        view.getDefaultFocusPosition = me.getDefaultFocusPosition;
        // Set up our default editor set for the 4 atomic data types
        me.editors = {
            'date': new Ext.grid.CellEditor({
                field: new Ext.form.field.Date({
                    selectOnFocus: selectOnFocus
                })
            }),
            'string': new Ext.grid.CellEditor({
                field: new Ext.form.field.Text({
                    selectOnFocus: selectOnFocus
                })
            }),
            'number': new Ext.grid.CellEditor({
                field: new Ext.form.field.Number({
                    selectOnFocus: selectOnFocus
                })
            }),
            'boolean': new Ext.grid.CellEditor({
                field: new Ext.form.field.ComboBox({
                    editable: false,
                    store: [
                        [
                            true,
                            me.headerCt.trueText
                        ],
                        [
                            false,
                            me.headerCt.falseText
                        ]
                    ]
                })
            })
        };
        // Track changes to the data so we can fire our events.
        me.store.on('update', me.onUpdate, me);
    },
    configure: function(config) {
        var me = this,
            store = me.store,
            i = 0,
            len = me.store.getCount(),
            nameField = me.nameField,
            valueField = me.valueField,
            name, value, rec, type;
        me.configureLegacy(config);
        if (me.inferTypes) {
            for (; i < len; ++i) {
                rec = store.getAt(i);
                name = rec.get(nameField);
                if (!me.getConfigProp(name, 'type')) {
                    value = rec.get(valueField);
                    if (Ext.isDate(value)) {
                        type = 'date';
                    } else if (Ext.isNumber(value)) {
                        type = 'number';
                    } else if (Ext.isBoolean(value)) {
                        type = 'boolean';
                    } else {
                        type = 'string';
                    }
                    me.setConfigProp(name, 'type', type);
                }
            }
        }
    },
    getConfigProp: function(fieldName, key, defaultValue) {
        var config = this.sourceConfig[fieldName],
            out;
        if (config) {
            out = config[key];
        }
        return out || defaultValue;
    },
    setConfigProp: function(fieldName, key, value) {
        var sourceCfg = this.sourceConfig,
            o = sourceCfg[fieldName];
        if (!o) {
            o = sourceCfg[fieldName] = {
                __copied: true
            };
        } else if (!o.__copied) {
            o = Ext.apply({
                __copied: true
            }, o);
            sourceCfg[fieldName] = o;
        }
        o[key] = value;
        return value;
    },
    // to be deprecated in 4.2
    configureLegacy: function(config) {
        var me = this;
        me.copyLegacyObject(config, me.customRenderers, 'renderer');
        me.copyLegacyObject(config, me.customEditors, 'editor');
        me.copyLegacyObject(config, me.propertyNames, 'displayName');
        // exclude types since it's new
        if (me.customRenderers || me.customEditors || me.propertyNames) {
            if (Ext.global.console && Ext.global.console.warn) {
                Ext.global.console.warn(this.$className, 'customRenderers, customEditors & propertyNames have been consolidated ' + 'into a new config, see "sourceConfig". ' + 'These configurations will be deprecated.');
            }
        }
    },
    copyLegacyObject: function(config, o, keyName) {
        var key;
        for (key in o) {
            if (o.hasOwnProperty(key)) {
                if (!config[key]) {
                    config[key] = {};
                }
                config[key][keyName] = o[key];
            }
        }
    },
    /**
     * @private
     */
    onUpdate: function(store, record, operation) {
        var me = this,
            v, oldValue;
        if (me.rendered && operation === Ext.data.Model.EDIT) {
            v = record.get(me.valueField);
            oldValue = record.modified.value;
            // eslint-disable-next-line max-len
            if (me.fireEvent('beforepropertychange', me.source, record.getId(), v, oldValue) !== false) {
                if (me.source) {
                    me.source[record.getId()] = v;
                }
                record.commit();
                me.fireEvent('propertychange', me.source, record.getId(), v, oldValue);
            } else {
                record.reject();
            }
        }
    },
    // Custom implementation of walkCells which only goes up and down.
    // Runs in the scope of the TableView
    walkCells: function(pos, direction, e, preventWrap, verifierFn, scope) {
        var me = this,
            valueColumn = me.ownerCt.valueColumn;
        if (direction === 'left') {
            direction = 'up';
        } else if (direction === 'right') {
            direction = 'down';
        }
        pos = Ext.view.Table.prototype.walkCells.call(me, pos, direction, e, preventWrap, verifierFn, scope);
        // We are only allowed to navigate to the value column.
        pos.column = valueColumn;
        pos.colIdx = valueColumn.getVisibleIndex();
        return pos;
    },
    getDefaultFocusPosition: function() {
        var view = this,
            // NOT grid!
            focusPosition;
        focusPosition = new Ext.grid.CellContext(view).setColumn(1);
        return focusPosition;
    },
    /**
     * @private
     * Returns the correct editor type for the property type, or a custom one keyed
     * by the property name
     */
    getCellEditor: function(record, column) {
        var me = this,
            propName = record.get(me.nameField),
            val = record.get(me.valueField),
            editor = me.getConfigProp(propName, 'editor'),
            type = me.getConfigProp(propName, 'type'),
            editors = me.editors,
            field;
        // A custom editor was found. If not already wrapped with a CellEditor, wrap it,
        // and stash it back
        // If it's not even a Field, just a config object, instantiate it before wrapping it.
        if (editor) {
            if (!(editor instanceof Ext.grid.CellEditor)) {
                if (!(editor instanceof Ext.form.field.Base)) {
                    editor = Ext.ComponentManager.create(editor, 'textfield');
                }
                editor = me.setConfigProp(propName, 'editor', new Ext.grid.CellEditor({
                    field: editor
                }));
            }
        } else if (type) {
            switch (type) {
                case 'date':
                    editor = editors.date;
                    break;
                case 'number':
                    editor = editors.number;
                    break;
                case 'boolean':
                    editor = editors[type];
                    break;
                default:
                    editor = editors.string;
            }
        } else if (Ext.isDate(val)) {
            editor = editors.date;
        } else if (Ext.isNumber(val)) {
            editor = editors.number;
        } else if (Ext.isBoolean(val)) {
            editor = editors.boolean;
        } else {
            editor = editors.string;
        }
        field = editor.field;
        if (field && field.ui === 'default' && !field.hasOwnProperty('ui')) {
            field.ui = me.editingPlugin.defaultFieldUI;
        }
        // Give the editor a unique ID because the CellEditing plugin caches them
        editor.editorId = propName;
        editor.field.column = me.valueColumn;
        if (propName) {
            propName = Ext.String.htmlEncode(propName);
            if (field.rendered) {
                field.inputEl.dom.setAttribute('aria-label', propName);
            } else {
                field.ariaLabel = propName;
            }
        }
        return editor;
    },
    doDestroy: function() {
        var me = this;
        me.destroyEditors(me.editors);
        me.destroyEditors(me.customEditors);
        me.callParent();
    },
    destroyEditors: function(editors) {
        var ed;
        for (ed in editors) {
            if (editors.hasOwnProperty(ed)) {
                Ext.destroy(editors[ed]);
            }
        }
    },
    /**
     * Sets the source data object containing the property data.  The data object can contain
     * one or more name/value pairs representing all of the properties of an object to display
     * in the grid, and this data will automatically be loaded into the grid's {@link #store}.
     * The values should be supplied in the proper data type if needed, otherwise string type
     * will be assumed.  If the grid already contains data, this method will replace any
     * existing data.  See also the {@link #source} config value.  Example usage:
     *
     *     grid.setSource({
     *         "(name)": "My Object",
     *         "Created": Ext.Date.parse('10/15/2006', 'm/d/Y'),  // date type
     *         "Available": false,  // boolean type
     *         "Version": .01,      // decimal type
     *         "Description": "A test object"
     *     });
     *
     * @param {Object} source The data object.
     * @param {Object} [sourceConfig] A new {@link #sourceConfig object}. If this argument
     * is not passed the current configuration will be re-used. To reset the config,
     * pass `null` or an empty object literal.
     */
    setSource: function(source, sourceConfig) {
        var me = this;
        me.source = source;
        if (sourceConfig !== undefined) {
            me.sourceConfig = Ext.apply({}, sourceConfig);
            me.configure(me.sourceConfig);
        }
        me.propStore.setSource(source);
    },
    /**
     * Gets the source data object containing the property data.  See {@link #setSource}
     * for details regarding the format of the data object.
     * @return {Object} The data object.
     */
    getSource: function() {
        return this.propStore.getSource();
    },
    /**
     * Gets the value of a property.
     * @param {String} prop The name of the property.
     * @return {Object} The property value. `null` if there is no value.
     *
     * @since 5.1.1
     */
    getProperty: function(prop) {
        return this.propStore.getProperty(prop);
    },
    /**
     * Sets the value of a property.
     * @param {String} prop The name of the property to set.
     * @param {Object} value The value to test.
     * @param {Boolean} [create=false] `true` to create the property if it doesn't already exist.
     */
    setProperty: function(prop, value, create) {
        this.propStore.setValue(prop, value, create);
    },
    /**
     * Removes a property from the grid.
     * @param {String} prop The name of the property to remove.
     */
    removeProperty: function(prop) {
        this.propStore.remove(prop);
    }
});
/**
     * @cfg {Object} store
     * @private
     */
/**
     * @cfg {Object} columns
     * @private
     */

/**
 * A custom HeaderContainer for the {@link Ext.grid.property.Grid}.
 * Generally it should not need to be used directly.
 */
Ext.define('Ext.grid.property.HeaderContainer', {
    extend: 'Ext.grid.header.Container',
    alternateClassName: 'Ext.grid.PropertyColumnModel',
    nameWidth: 115,
    /**
     * @cfg {String} nameText
     * The column header text for the name column.
     * @locale
     */
    nameText: 'Name',
    /**
     * @cfg {String} valueText
     * The column header text for the value column.
     * @locale
     */
    valueText: 'Value',
    /**
     * @cfg {String} dateFormat
     * The {@link Ext.Date date format} to use for date values.
     * @locale
     */
    dateFormat: 'm/j/Y',
    /**
     * @cfg {String} trueText
     * The text to display for boolean `true` values.
     * @locale
     */
    trueText: 'true',
    /**
     * @cfg {String} falseText
     * The text to display for boolean `false` values.
     * @locale
     */
    falseText: 'false',
    /**
     * @private
     */
    nameColumnCls: Ext.baseCSSPrefix + 'grid-property-name',
    nameColumnInnerCls: Ext.baseCSSPrefix + 'grid-cell-inner-property-name',
    /**
     * Creates new HeaderContainer.
     * @param {Ext.grid.property.Grid} grid The grid this store will be bound to
     * @param {Object} source The source data config object
     */
    constructor: function(grid, source) {
        var me = this;
        me.grid = grid;
        me.store = source;
        me.callParent([
            {
                isRootHeader: true,
                enableColumnResize: Ext.isDefined(grid.enableColumnResize) ? grid.enableColumnResize : me.enableColumnResize,
                enableColumnMove: Ext.isDefined(grid.enableColumnMove) ? grid.enableColumnMove : me.enableColumnMove,
                items: [
                    {
                        header: me.nameText,
                        width: grid.nameColumnWidth || me.nameWidth,
                        sortable: grid.sortableColumns,
                        dataIndex: grid.nameField,
                        scope: me,
                        renderer: me.renderProp,
                        itemId: grid.nameField,
                        menuDisabled: true,
                        tdCls: me.nameColumnCls,
                        innerCls: me.nameColumnInnerCls
                    },
                    {
                        header: me.valueText,
                        scope: me,
                        renderer: me.renderCell,
                        getEditor: me.getCellEditor.bind(me),
                        sortable: grid.sortableColumns,
                        flex: 1,
                        fixed: true,
                        dataIndex: grid.valueField,
                        itemId: grid.valueField,
                        menuDisabled: true
                    }
                ]
            }
        ]);
        // PropertyGrid needs to know which column is the editable "value" column.
        me.grid.valueColumn = me.items.getAt(1);
    },
    getCellEditor: function(record) {
        return this.grid.getCellEditor(record, this);
    },
    /**
     * @private
     * Render a property name cell
     */
    renderProp: function(v) {
        return this.getPropertyName(v);
    },
    /**
     * @private
     * Render a property value cell
     */
    renderCell: function(val, meta, rec) {
        var me = this,
            grid = me.grid,
            renderer = grid.getConfigProp(rec.get(grid.nameField), 'renderer'),
            result = val;
        if (renderer) {
            return Ext.callback(renderer, null, arguments, 0, me);
        }
        if (Ext.isDate(val)) {
            result = me.renderDate(val);
        } else if (Ext.isBoolean(val)) {
            result = me.renderBool(val);
        }
        return Ext.util.Format.htmlEncode(result);
    },
    /**
     * @private
     */
    renderDate: Ext.util.Format.date,
    /**
     * @private
     */
    renderBool: function(bVal) {
        return this[bVal ? 'trueText' : 'falseText'];
    },
    /**
     * @private
     * Renders custom property names instead of raw names if defined in the Grid
     */
    getPropertyName: function(name) {
        return this.grid.getConfigProp(name, 'displayName', name);
    }
});

/**
 * A specific {@link Ext.data.Model} type that represents a name/value pair and is made to work
 * with the {@link Ext.grid.property.Grid}. Typically, Properties do not need to be created
 * directly as they can be created implicitly by simply using the appropriate data configs
 * either via the {@link Ext.grid.property.Grid#source} config property or by calling
 * {@link Ext.grid.property.Grid#setSource}. However, if the need arises, these records
 * can also be created explicitly as shown below. Example usage:
 *
 *     var rec = new Ext.grid.property.Property({
 *         name: 'birthday',
 *         value: Ext.Date.parse('17/06/1962', 'd/m/Y')
 *     });
 *     // Add record to an already populated grid
 *     grid.store.addSorted(rec);
 *
 * @constructor
 * Creates new property.
 * @param {Object} config A data object in the format:
 * @param {String/String[]} config.name A name or names for the property.
 * @param {Mixed/Mixed[]} config.value A value or values for the property.
 * The specified value's type will be read automatically by the grid to determine
 * the type of editor to use when displaying it.
 * @return {Object}
 */
Ext.define('Ext.grid.property.Property', {
    extend: 'Ext.data.Model',
    alternateClassName: 'Ext.PropGridProperty',
    fields: [
        {
            name: 'name',
            type: 'string'
        },
        {
            name: 'value'
        }
    ],
    idProperty: 'name',
    constructor: function(data, value) {
        if (!Ext.isObject(data)) {
            data = {
                name: data,
                value: value
            };
        }
        this.callParent([
            data
        ]);
    }
});

/**
 * @private
 * Custom reader for property grid data
 */
Ext.define('Ext.grid.property.Reader', {
    extend: 'Ext.data.reader.Reader',
    successProperty: null,
    totalProperty: null,
    messageProperty: null,
    read: function(dataObject) {
        return this.readRecords(dataObject);
    },
    readRecords: function(dataObject) {
        var Model = this.getModel(),
            result = {
                records: [],
                success: true
            },
            val, propName;
        for (propName in dataObject) {
            if (dataObject.hasOwnProperty(propName)) {
                val = dataObject[propName];
                if (this.isEditableValue(val)) {
                    result.records.push(new Model({
                        name: propName,
                        value: val
                    }));
                }
            }
        }
        result.total = result.count = result.records.length;
        return new Ext.data.ResultSet(result);
    },
    /**
     * @private
     */
    isEditableValue: function(val) {
        return Ext.isPrimitive(val) || Ext.isDate(val) || val === null;
    }
});

/**
 * A custom {@link Ext.data.Store} for the {@link Ext.grid.property.Grid}. This class handles
 * the mapping between the custom data source objects supported by the grid and the
 * {@link Ext.grid.property.Property} format used by the {@link Ext.data.Store} base class.
 */
Ext.define('Ext.grid.property.Store', {
    extend: 'Ext.data.Store',
    alternateClassName: 'Ext.grid.PropertyStore',
    requires: [
        'Ext.grid.property.Reader',
        'Ext.data.proxy.Memory',
        'Ext.grid.property.Property'
    ],
    remoteSort: true,
    /**
     * Creates new property store.
     * @param {Ext.grid.Panel} grid The grid this store will be bound to
     * @param {Object} source The source data config object
     */
    constructor: function(grid, source) {
        var me = this;
        me.grid = grid;
        me.source = source;
        me.callParent([
            {
                data: source,
                model: Ext.grid.property.Property,
                proxy: me.getProxy()
            }
        ]);
    },
    // Return a singleton, customized Proxy object which configures itself with a custom Reader
    getProxy: function() {
        var proxy = this.proxy;
        if (!proxy) {
            proxy = this.proxy = new Ext.data.proxy.Memory({
                model: Ext.grid.property.Property,
                reader: this.getReader()
            });
        }
        return proxy;
    },
    // Return a singleton, customized Reader object which reads Ext.grid.property.Property
    // records from an object.
    getReader: function() {
        var reader = this.reader;
        if (!reader) {
            reader = this.reader = new Ext.grid.property.Reader({
                model: Ext.grid.property.Property
            });
        }
        return reader;
    },
    // @protected
    // Should only be called by the grid.  Use grid.setSource instead.
    setSource: function(dataObject) {
        var me = this;
        me.source = dataObject;
        me.suspendEvents();
        me.removeAll();
        me.getProxy().setData(dataObject);
        me.load();
        me.resumeEvents();
        me.fireEvent('datachanged', me);
        me.fireEvent('refresh', me);
    },
    /**
     * @private
     */
    getProperty: function(row) {
        var rec = Ext.isNumber(row) ? this.getAt(row) : this.getById(row),
            ret = null;
        if (rec) {
            ret = rec.get('value');
        }
        return ret;
    },
    /**
     * @private
     */
    setValue: function(prop, value, create) {
        var me = this,
            rec = me.getRec(prop);
        if (rec) {
            rec.set('value', value);
            me.source[prop] = value;
        } else if (create) {
            // only create if specified.
            me.source[prop] = value;
            rec = new Ext.grid.property.Property({
                name: prop,
                value: value
            }, prop);
            me.add(rec);
        }
    },
    /**
     * @private
     */
    remove: function(prop) {
        var rec = this.getRec(prop);
        if (rec) {
            this.callParent([
                rec
            ]);
            delete this.source[prop];
        }
    },
    /**
     * @private
     */
    getRec: function(prop) {
        return this.getById(prop);
    },
    /**
     * @protected
     * Should only be called by the grid.  Use grid.getSource instead.
     */
    getSource: function() {
        return this.source;
    },
    doDestroy: function() {
        Ext.destroy(this.reader, this.proxy);
        this.callParent();
    }
});

/**
 * Base class for selections which may be of three subtypes:
 *
 * - {@link Ext.grid.selection.Cells Cells} A rectangular range of cells defined by a start
 *   record/column and an end record/column.
 * - {@link Ext.grid.selection.Rows Rows} An array of records.
 * - {@link Ext.grid.selection.Columns Columns} An array of columns in which all records
 *   are included.
 *
 * @since 5.1.0
 */
Ext.define('Ext.grid.selection.Selection', {
    constructor: function(view) {
        if (!view || !(view.isTableView || view.isLockingView)) {
            Ext.raise('Selection must be created for a given TableView or LockingView');
        }
        // We use the topmost (possible Ext.locking.View) view
        this.view = view.ownerGrid.view;
    }
});
/**
     * Clones this selection object.
     * @return {Ext.grid.selection.Selection} A clone of this instance.
     * @method clone
     */
/**
     * Clears the selection represented by this selection object.
     * @private
     * @method clear
     */
/**
     * Calls the passed function for each selected {@link Ext.data.Model record}.
     *
     * @param {Function} fn The function to call. If this returns `false`, the iteration is
     * halted with no further calls.
     * @param {Ext.data.Model} fn.record The current record.
     * @param {Object} [scope] The context (`this` reference) in which the function is executed.
     * Defaults to this Selection object.
     * @method eachRow
     */
/**
     * Calls the passed function for each selected cell from top left to bottom right
     * iterating over columns within each row.
     *
     * @param {Function} fn The function to call. If this returns `false`, the iteration is
     * halted with no further calls.
     * @param {Ext.grid.CellContext} fn.context The CellContext representing the current cell.
     * @param {Number} fn.columnIndex The column index of the current cell.
     * @param {Number} fn.rowIndex The row index of the current cell.
     * @param {Object} [scope] The context (`this` reference) in which `fn` is executed.
     * Defaults to this Selection object.
     * @method eachCell
     */
/**
     * Calls the passed function for each selected column from left to right.
     *
     * @param {Function} fn The function to call. If this returns false, the iteration is
     * halted with no further calls.
     * @param {Ext.grid.column.Column} fn.column The current column.
     * @param {Number} fn.columnIndex The index of the current column. *Note that in a
     * locked grid, this is relative to the outermost grid encompassing both sides*.
     * @param {Object} [scope] The context (`this` reference) in which `fn` is executed.
     * Defaults to this Selection object.
     * @method eachColumn
     */
/**
     * Called when selection is completed.
     * @method onSelectionFinish
     * @private
     */

/**
 * A class which encapsulates a range of cells defining a selection in a grid.
 *
 * Note that when range start and end points are represented by an array, the
 * order is traditional `x, y` order, that is column index followed by row index.
 * @since 5.1.0
 */
Ext.define('Ext.grid.selection.Cells', {
    extend: 'Ext.grid.selection.Selection',
    type: 'cells',
    /**
     * @property {Boolean} isCells
     * This property indicates the this selection represents selected cells.
     * @readonly
     */
    isCells: true,
    //-------------------------------------------------------------------------
    // Base Selection API
    clone: function() {
        var me = this,
            result = new me.self(me.view);
        if (me.startCell) {
            result.startCell = me.startCell.clone();
            result.endCell = me.endCell.clone();
        }
        return result;
    },
    /**
     * Returns `true` if the passed {@link Ext.grid.CellContext cell context} is selected.
     * @param {Ext.grid.CellContext} cellContext The cell context to test.
     * @return {Boolean} `true` if the passed {@link Ext.grid.CellContext cell context} is selected.
     */
    contains: function(cellContext) {
        var range;
        if (!cellContext || !cellContext.isCellContext) {
            return false;
        }
        if (this.startCell) {
            // get start and end rows in the range
            range = this.getRowRange();
            if (cellContext.rowIdx >= range[0] && cellContext.rowIdx <= range[1]) {
                // get start and end columns in the range
                range = this.getColumnRange();
                return (cellContext.colIdx >= range[0] && cellContext.colIdx <= range[1]);
            }
        }
        return false;
    },
    eachRow: function(fn, scope) {
        var me = this,
            rowRange = me.getRowRange(),
            context = new Ext.grid.CellContext(me.view),
            rowIdx;
        for (rowIdx = rowRange[0]; rowIdx <= rowRange[1]; rowIdx++) {
            context.setRow(rowIdx);
            if (fn.call(scope || me, context.record) === false) {
                return;
            }
        }
    },
    eachColumn: function(fn, scope) {
        var me = this,
            colRange = me.getColumnRange(),
            context = new Ext.grid.CellContext(me.view),
            colIdx;
        for (colIdx = colRange[0]; colIdx <= colRange[1]; colIdx++) {
            context.setColumn(colIdx);
            if (fn.call(scope || me, context.column, colIdx) === false) {
                return;
            }
        }
    },
    eachCell: function(fn, scope) {
        var me = this,
            rowRange = me.getRowRange(),
            colRange = me.getColumnRange(),
            context = new Ext.grid.CellContext(me.view),
            rowIdx, colIdx;
        for (rowIdx = rowRange[0]; rowIdx <= rowRange[1]; rowIdx++) {
            context.setRow(rowIdx);
            for (colIdx = colRange[0]; colIdx <= colRange[1]; colIdx++) {
                context.setColumn(colIdx);
                if (fn.call(scope || me, context, colIdx, rowIdx) === false) {
                    return;
                }
            }
        }
    },
    /**
     * @return {Number} The row index of the first row in the range or zero if no range.
     */
    getFirstRowIndex: function() {
        return this.startCell ? Math.min(this.startCell.rowIdx, this.endCell.rowIdx) : 0;
    },
    /**
     * @return {Number} The row index of the last row in the range or -1 if no range.
     */
    getLastRowIndex: function() {
        return this.startCell ? Math.max(this.startCell.rowIdx, this.endCell.rowIdx) : -1;
    },
    /**
     * @return {Number} The column index of the first column in the range or zero if no range.
     */
    getFirstColumnIndex: function() {
        return this.startCell ? Math.min(this.startCell.colIdx, this.endCell.colIdx) : 0;
    },
    /**
     * @return {Number} The column index of the last column in the range or -1 if no range.
     */
    getLastColumnIndex: function() {
        return this.startCell ? Math.max(this.startCell.colIdx, this.endCell.colIdx) : -1;
    },
    //-------------------------------------------------------------------------
    privates: {
        /**
         * @private
         */
        clear: function() {
            var me = this,
                view = me.view;
            if (view.getVisibleColumnManager().getColumns().length) {
                me.eachCell(function(cellContext) {
                    view.onCellDeselect(cellContext);
                });
            }
            me.startCell = me.endCell = null;
        },
        /**
         * Used during drag/shift+downarrow range selection on start.
         * @param {Ext.grid.CellContext} startCell The start cell of the cell drag selection.
         * @param {Ext.grid.CellContext} endCell The end cell of the cell drag selection.
         * @private
         */
        setRangeStart: function(startCell, endCell) {
            // Must clone them. Users might use one instance and reconfigure it to navigate.
            this.startCell = (this.endCell = startCell.clone()).clone();
            this.view.onCellSelect(startCell);
        },
        /**
         * Used during drag/shift+downarrow range selection on drag.
         * @param {Ext.grid.CellContext} endCell The end cell of the cell drag selection.
         * @private
         */
        setRangeEnd: function(endCell) {
            var me = this,
                view = me.view,
                rows = view.all,
                cell = new Ext.grid.CellContext(view),
                maxColIdx = view.getVisibleColumnManager().getColumns().length - 1,
                range, lastRange, rowStart, rowEnd, colStart, colEnd, rowIdx, colIdx;
            me.endCell = endCell.clone();
            range = me.getRange();
            lastRange = me.lastRange || range;
            rowStart = Math.max(Math.min(range[0][1], lastRange[0][1]), rows.startIndex);
            rowEnd = Math.min(Math.max(range[1][1], lastRange[1][1]), rows.endIndex);
            colStart = Math.min(range[0][0], lastRange[0][0]);
            colEnd = Math.min(Math.max(range[1][0], lastRange[1][0]), maxColIdx);
            // Loop through the union of last range and current range
            for (rowIdx = rowStart; rowIdx <= rowEnd; rowIdx++) {
                for (colIdx = colStart; colIdx <= colEnd; colIdx++) {
                    cell.setPosition(rowIdx, colIdx);
                    // If we are outside the current range, deselect
                    if (rowIdx < range[0][1] || rowIdx > range[1][1] || colIdx < range[0][0] || colIdx > range[1][0]) {
                        view.onCellDeselect(cell);
                    } else {
                        view.onCellSelect(cell);
                    }
                }
            }
            me.lastRange = range;
        },
        extendRange: function(extensionVector) {
            var me = this,
                newEndCell;
            if (extensionVector[extensionVector.type] < 0) {
                newEndCell = me.endCell.clone().setPosition(me.getLastRowIndex(), me.getLastColumnIndex());
                me.startCell = extensionVector.start.clone();
                me.setRangeEnd(newEndCell);
                me.view.getNavigationModel().setPosition(extensionVector.start);
            } else {
                me.startCell = me.startCell.setPosition(me.getFirstRowIndex(), me.getFirstColumnIndex());
                me.setRangeEnd(extensionVector.end);
                me.view.getNavigationModel().setPosition(extensionVector.end);
            }
        },
        reduceRange: function(extensionVector) {
            var me = this,
                newEndCell;
            if (extensionVector[extensionVector.type] < 0) {
                newEndCell = extensionVector.end.clone();
                me.startCell = extensionVector.start.clone();
                me.setRangeEnd(newEndCell);
                me.view.getNavigationModel().setPosition(extensionVector.start);
            }
        },
        /**
         * Returns the `[[x, y],[x,y]]` coordinates in top-left to bottom-right order
         * of the current selection.
         *
         * If no selection, returns [[0, 0],[-1, -1]] so that an incrementing iteration
         * will not execute.
         *
         * @return {Number[][]}
         * @private
         */
        getRange: function() {
            return [
                [
                    this.getFirstColumnIndex(),
                    this.getFirstRowIndex()
                ],
                [
                    this.getLastColumnIndex(),
                    this.getLastRowIndex()
                ]
            ];
        },
        /**
         * Returns the size of the selection rectangle.
         * @return {Number}
         * @private
         */
        getRangeSize: function() {
            return this.getCount();
        },
        /**
         * Returns the number of cells selected.
         * @return {Number} The nuimber of cells selected
         * @private
         */
        getCount: function() {
            var range = this.getRange();
            return (range[1][0] - range[0][0] + 1) * (range[1][1] - range[0][1] + 1);
        },
        /**
         * @private
         */
        selectAll: function() {
            var me = this,
                view = me.view;
            me.clear();
            me.setRangeStart(new Ext.grid.CellContext(view).setPosition(0, 0));
            me.setRangeEnd(new Ext.grid.CellContext(view).setPosition(view.dataSource.getCount() - 1, view.getVisibleColumnManager().getColumns().length - 1));
        },
        /**
         * @return {Boolean}
         * @private
         */
        isAllSelected: function() {
            var start = this.rangeStart,
                end = this.rangeEnd;
            // All selected only if we encompass the entire store and every visible column
            if (start) {
                if (!start.colIdx && !start.rowIdx) {
                    // eslint-disable-next-line max-len
                    return end.colIdx === end.view.getVisibleColumnManager().getColumns().length - 1 && end.rowIdx === end.view.dataSource.getCount - 1;
                }
            }
            return false;
        },
        /**
         * @return {Number[]} The column range which encapsulates the range.
         * @private
         */
        getColumnRange: function() {
            return [
                this.getFirstColumnIndex(),
                this.getLastColumnIndex()
            ];
        },
        /**
         * @private
         * Called through {@link Ext.grid.selection.SpreadsheetModel#getLastSelected}
         * by {@link Ext.panel.Table#updateBindSelection} when publishing the `selection` property.
         * It should yield the last record selected.
         */
        getLastSelected: function() {
            return this.view.dataSource.getAt(this.endCell.rowIdx);
        },
        /**
         * Returns the row range which encapsulates the range - the view range that needs
         * updating.
         * @return {Number[]}
         * @private
         */
        getRowRange: function() {
            return [
                this.getFirstRowIndex(),
                this.getLastRowIndex()
            ];
        },
        onSelectionFinish: function() {
            var me = this;
            if (me.getCount()) {
                me.view.getSelectionModel().onSelectionFinish(me, new Ext.grid.CellContext(me.view).setPosition(me.getFirstRowIndex(), me.getFirstColumnIndex()), new Ext.grid.CellContext(me.view).setPosition(me.getLastRowIndex(), me.getLastColumnIndex()));
            } else {
                me.view.getSelectionModel().onSelectionFinish(me);
            }
        }
    }
});

/**
 * A class which encapsulates a range of columns defining a selection in a grid.
 * @since 5.1.0
 */
Ext.define('Ext.grid.selection.Columns', {
    extend: 'Ext.grid.selection.Selection',
    type: 'columns',
    /**
     * @property {Boolean} isColumns
     * This property indicates the this selection represents selected columns.
     * @readonly
     */
    isColumns: true,
    //-------------------------------------------------------------------------
    // Base Selection API
    clone: function() {
        var me = this,
            result = new me.self(me.view),
            columns = me.selectedColumns;
        if (columns) {
            result.selectedColumns = Ext.Array.slice(columns);
        }
        return result;
    },
    eachRow: function(fn, scope) {
        var columns = this.selectedColumns;
        if (columns && columns.length) {
            this.view.dataSource.each(fn, scope || this);
        }
    },
    eachColumn: function(fn, scope) {
        var me = this,
            view = me.view,
            columns = me.selectedColumns,
            len, i,
            context = new Ext.grid.CellContext(view);
        if (columns) {
            len = columns.length;
            for (i = 0; i < len; i++) {
                context.setColumn(columns[i]);
                if (fn.call(scope || me, context.column, context.colIdx) === false) {
                    return false;
                }
            }
        }
    },
    eachCell: function(fn, scope) {
        var me = this,
            view = me.view,
            columns = me.selectedColumns,
            len, i,
            context = new Ext.grid.CellContext(view);
        if (columns) {
            len = columns.length;
            // Use Store#each instead of copying the entire dataset into an array
            // and iterating that.
            view.dataSource.each(function(record) {
                context.setRow(record);
                for (i = 0; i < len; i++) {
                    context.setColumn(columns[i]);
                    if (fn.call(scope || me, context, context.colIdx, context.rowIdx) === false) {
                        return false;
                    }
                }
            });
        }
    },
    //-------------------------------------------------------------------------
    // Methods unique to this type of Selection
    /**
     * Returns `true` if the passed {@link Ext.grid.column.Column column} is selected.
     * @param {Ext.grid.column.Column} column The column to test.
     * @return {Boolean} `true` if the passed {@link Ext.grid.column.Column column} is selected.
     */
    contains: function(column) {
        var selectedColumns = this.selectedColumns;
        if (column && column.isColumn && selectedColumns && selectedColumns.length) {
            return Ext.Array.contains(selectedColumns, column);
        }
        return false;
    },
    /**
     * Returns the number of columns selected.
     * @return {Number} The number of columns selected.
     */
    getCount: function() {
        var selectedColumns = this.selectedColumns;
        return selectedColumns ? selectedColumns.length : 0;
    },
    /**
     * Returns the columns selected.
     * @return {Ext.grid.column.Column[]} The columns selected.
     */
    getColumns: function() {
        return this.selectedColumns || [];
    },
    //-------------------------------------------------------------------------
    privates: {
        /**
         * Adds the passed Column to the selection.
         * @param {Ext.grid.column.Column} column
         * @private
         */
        add: function(column) {
            if (!column.isColumn) {
                Ext.raise('Column selection must be passed a grid Column header object');
            }
            Ext.Array.include((this.selectedColumns || (this.selectedColumns = [])), column);
            this.refreshColumns(column);
        },
        /**
         * @private
         */
        clear: function() {
            var me = this,
                prevSelection = me.selectedColumns;
            if (prevSelection && prevSelection.length) {
                me.selectedColumns = [];
                me.refreshColumns.apply(me, prevSelection);
            }
            me.setRangeStart(null);
        },
        setRangeStart: function(startColumn) {
            var me = this,
                prevSelection = me.getColumns();
            me.startColumn = startColumn;
            if (startColumn != null) {
                me.selectedColumns = [
                    startColumn
                ];
                prevSelection.push(startColumn);
                me.refreshColumns.apply(me, prevSelection);
            }
        },
        setRangeEnd: function(endColumn) {
            var me = this,
                prevSelection = me.getColumns(),
                colManager = this.view.ownerGrid.getVisibleColumnManager(),
                columns = colManager.getColumns(),
                start = colManager.indexOf(me.startColumn),
                end = colManager.indexOf(endColumn),
                i;
            // Allow looping through columns
            if (end < start) {
                i = start;
                start = end;
                end = i;
            }
            me.selectedColumns = [];
            for (i = start; i <= end; i++) {
                me.selectedColumns.push(columns[i]);
                prevSelection.push(columns[i]);
            }
            me.refreshColumns.apply(me, prevSelection);
        },
        /**
         * @return {Boolean}
         * @private
         */
        isAllSelected: function() {
            var selectedColumns = this.selectedColumns;
            // All selected means all columns, across both views if we are in a locking assembly.
            // eslint-disable-next-line max-len
            return selectedColumns && selectedColumns.length === this.view.ownerGrid.getVisibleColumnManager().getColumns().length;
        },
        /**
         * @private
         */
        refreshColumns: function(column) {
            var me = this,
                view = me.view,
                rows = view.all,
                rowIdx,
                columns = arguments,
                len = columns.length,
                colIdx,
                cellContext = new Ext.grid.CellContext(view),
                selected = [];
            if (view.rendered) {
                for (colIdx = 0; colIdx < len; colIdx++) {
                    selected[colIdx] = me.contains(columns[colIdx]);
                }
                for (rowIdx = rows.startIndex; rowIdx <= rows.endIndex; rowIdx++) {
                    cellContext.setRow(rowIdx);
                    for (colIdx = 0; colIdx < len; colIdx++) {
                        // Note colIdx is not the column's visible index.
                        // setColumn must be passed the column object
                        cellContext.setColumn(columns[colIdx]);
                        if (selected[colIdx]) {
                            view.onCellSelect(cellContext);
                        } else {
                            view.onCellDeselect(cellContext);
                        }
                    }
                }
            }
        },
        /**
         * Removes the passed Column from the selection.
         * @param {Ext.grid.column.Column} column
         * @private
         */
        remove: function(column) {
            if (!column.isColumn) {
                Ext.raise('Column selection must be passed a grid Column header object');
            }
            if (this.selectedColumns) {
                Ext.Array.remove(this.selectedColumns, column);
                // Might be being called because of column removal/hiding.
                // In which case the view will have selected cells removed, so no refresh needed.
                if (column.getView() && column.isVisible()) {
                    this.refreshColumns(column);
                }
            }
        },
        /**
         * @private
         */
        selectAll: function() {
            var me = this;
            me.clear();
            me.selectedColumns = me.view.getSelectionModel().lastContiguousColumnRange = me.view.getVisibleColumnManager().getColumns();
            me.refreshColumns.apply(me, me.selectedColumns);
        },
        extendRange: function(extensionVector) {
            var me = this,
                columns = me.view.getVisibleColumnManager().getColumns(),
                i;
            for (i = extensionVector.start.colIdx; i <= extensionVector.end.colIdx; i++) {
                me.add(columns[i]);
            }
        },
        reduceRange: function(extensionVector) {
            var me = this,
                columns = me.view.getVisibleColumnManager().getColumns(),
                startIdx = extensionVector.start.colIdx,
                endIdx = extensionVector.end.colIdx,
                reduceTo = Math.abs(startIdx - endIdx) + 1,
                diff = me.selectedColumns.length - reduceTo,
                i;
            for (i = diff; i > 0; i--) {
                me.remove(columns[endIdx + i]);
            }
        },
        onSelectionFinish: function() {
            var me = this,
                range = me.getContiguousSelection();
            if (range) {
                me.view.getSelectionModel().onSelectionFinish(me, new Ext.grid.CellContext(me.view).setPosition(0, range[0]), new Ext.grid.CellContext(me.view).setPosition(me.view.dataSource.getCount() - 1, range[1]));
            } else {
                me.view.getSelectionModel().onSelectionFinish(me);
            }
        },
        /**
         * @return {Array} `[startColumn, endColumn]` if the selection represents
         * a visually contiguous set of columns.
         * The SelectionReplicator is only enabled if there is a contiguous block.
         * @private
         */
        getContiguousSelection: function() {
            var selection = Ext.Array.sort(this.getColumns(), function(c1, c2) {
                    // Use index *in ownerGrid* so that a locking assembly
                    // can order columns correctly
                    return c1.getView().ownerGrid.getVisibleColumnManager().indexOf(c1) - c2.getView().ownerGrid.getVisibleColumnManager().indexOf(c2);
                }),
                len = selection.length,
                i;
            if (len) {
                for (i = 1; i < len; i++) {
                    if (selection[i].getVisibleIndex() !== selection[i - 1].getVisibleIndex() + 1) {
                        return false;
                    }
                }
                return [
                    selection[0],
                    selection[len - 1]
                ];
            }
        }
    }
});

/**
 * A plugin for use in grids which use the {@link Ext.grid.selection.SpreadsheetModel spreadsheet}
 * selection model, with {@link Ext.grid.selection.SpreadsheetModel#extensible extensible}
 * configured as `true` or `"y"`, meaning that the selection may be extended up or down
 * using a draggable extension handle.
 *
 * This plugin propagates values from the selection into the extension area.
 *
 * If just *one* row is selected, the values in that row are replicated unchanged into the extension
 * area.
 *
 * If more than one row is selected, the two rows closest to the selected block are taken to provide
 * a numeric difference, and that difference is used to calculate the sequence of values all the way
 * into the extension area.
 *
 */
Ext.define('Ext.grid.selection.Replicator', {
    extend: 'Ext.plugin.Abstract',
    alias: 'plugin.selectionreplicator',
    /**
     * 
     * @property {Ext.grid.column.Column[]} columns
     * An array of the columns encompassed by the selection block. This is gathered before
     * {@link #replicateSelection} is called, so is available to subclasses which implement
     * their own {@link #replicateSelection} method.
     */
    init: function(grid) {
        this.gridListeners = grid.on({
            beforeselectionextend: this.onBeforeSelectionExtend,
            scope: this,
            destroyable: true
        });
    },
    onBeforeSelectionExtend: function(ownerGrid, sel, extension) {
        var columns = this.columns = [];
        sel.eachColumn(function(column) {
            columns.push(column);
        });
        return this.replicateSelection(ownerGrid, sel, extension);
    },
    /**
     * This is the method which is called when the
     * {@link Ext.grid.selection.SpreadsheetModel spreadsheet} selection model's extender handle
     * is dragged and released.
     *
     * It is passed contextual information about the selection and the extension area.
     *
     * Subclass authors may override it to gain access to the event and perform their own
     * data replication.
     *
     * By default, the selection is extended to encompass the selection area.
     * Returning `false` from this method vetoes that.
     *
     * @param {Ext.panel.Table} ownerGrid The owning grid.
     * @param {Ext.grid.selection.Selection} sel An object describing the contiguous selected area.
     * @param {Object} extension An object describing the type and size of extension.
     * @param {String} extension.type `"rows"` or `"columns"`
     * @param {Ext.grid.CellContext} extension.start The start (top left) cell of the extension
     * area.
     * @param {Ext.grid.CellContext} extension.end The end (bottom right) cell of the extension
     * area.
     * @param {number} [extension.columns] The number of columns extended (-ve means
     * on the left side).
     * @param {number} [extension.rows] The number of rows extended (-ve means on the top side).
     */
    replicateSelection: function(ownerGrid, sel, extension) {
        // This can only handle extending rows
        if (extension.columns || sel.isColumns) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            columns = me.columns,
            selFirstRowIdx = sel.getFirstRowIndex(),
            selLastRowIdx = sel.getLastRowIndex(),
            selectedRowCount = selLastRowIdx - selFirstRowIdx + 1,
            lastTwoRecords = [],
            colCount, i, j, column, values, startIdx, endIdx, increment, store, record, prevValues, prevValue, x, y;
        colCount = columns.length;
        store = columns[0].getView().dataSource;
        // Single row, just duplicate values into extension
        if (selectedRowCount === 1) {
            values = me.getColumnValues(sel.view.dataSource.getAt(selFirstRowIdx));
        } else // Multiple rows, take the numeric values from the closest two rows,
        // calculate an array of differences and propagate it
        {
            values = new Array(colCount);
            if (extension.rows < 0) {
                lastTwoRecords = [
                    store.getAt(selFirstRowIdx + 1),
                    store.getAt(selFirstRowIdx)
                ];
            } else {
                lastTwoRecords = [
                    store.getAt(selLastRowIdx - 1),
                    store.getAt(selLastRowIdx)
                ];
            }
            lastTwoRecords[0] = me.getColumnValues(lastTwoRecords[0]);
            lastTwoRecords[1] = me.getColumnValues(lastTwoRecords[1]);
            // The values array will be the differences between all numeric columns
            // in the selection of the closet two records.
            for (j = 0; j < colCount; j++) {
                x = lastTwoRecords[1][j];
                y = lastTwoRecords[0][j];
                if (!isNaN(x) && !isNaN(y)) {
                    values[j] = Number(x) - Number(y);
                }
            }
        }
        // Loop from end to start of extension area
        if (extension.rows < 0) {
            startIdx = extension.end.rowIdx;
            endIdx = extension.start.rowIdx - 1;
            increment = -1;
        } else {
            startIdx = extension.start.rowIdx;
            endIdx = extension.end.rowIdx + 1;
            increment = 1;
        }
        // Replicate single selected row
        if (selectedRowCount === 1) {
            for (i = startIdx; i !== endIdx; i += increment) {
                record = store.getAt(i);
                for (j = 0; j < colCount; j++) {
                    column = columns[j];
                    if (column.dataIndex) {
                        record.set(column.dataIndex, values[j]);
                    }
                }
            }
        } else // Add differences from closest two rows
        {
            for (i = startIdx; i !== endIdx; i += increment) {
                record = store.getAt(i);
                prevValues = me.getColumnValues(store.getAt(i - increment));
                for (j = 0; j < colCount; j++) {
                    column = columns[j];
                    if (column.dataIndex) {
                        prevValue = prevValues[j];
                        if (!isNaN(prevValue)) {
                            record.set(column.dataIndex, Ext.coerce(Number(prevValue) + values[j], prevValue));
                        }
                    }
                }
            }
        }
    },
    /**
     * A utility method, which, when passed a record, uses the {@link #columns} property
     * to extract the values of that record which are encompassed by the selection.
     *
     * Note that columns with no {@link Ext.grid.column.Column#dataIndex dataIndex}
     * cannot yield a value.
     * @param {Ext.data.Model} record The record from which to read values.
     * @return {Mixed[]} The values of the fields used by the selected column range
     * for the passed record.
     */
    getColumnValues: function(record) {
        var columns = this.columns,
            len = columns.length,
            result = new Array(columns.length),
            i, column;
        for (i = 0; i < len; i++) {
            column = columns[i];
            // If there's a dataIndex, get the value
            if (column.dataIndex) {
                result[i] = record.get(column.dataIndex);
            }
        }
        return result;
    },
    destroy: function() {
        this.gridListeners = Ext.destroy(this.gridListeners);
        this.callParent();
    }
});

/**
 * A class which encapsulates a range of rows defining a selection in a grid.
 * @since 5.1.0
 */
Ext.define('Ext.grid.selection.Rows', {
    extend: 'Ext.grid.selection.Selection',
    requires: [
        'Ext.util.Collection'
    ],
    type: 'rows',
    /**
     * @property {Boolean} isRows
     * This property indicates the this selection represents selected rows.
     * @readonly
     */
    isRows: true,
    //-------------------------------------------------------------------------
    // Base Selection API
    clone: function() {
        var me = this,
            result = new me.self(me.view);
        // Clone our record collection
        if (me.selectedRecords) {
            result.selectedRecords = me.selectedRecords.clone();
        }
        // Clone the current drag range
        if (me.rangeStart) {
            result.setRangeStart(me.rangeStart);
            result.setRangeEnd(me.rangeEnd);
        }
        return result;
    },
    //-------------------------------------------------------------------------
    // Methods unique to this type of Selection
    addOne: function(record) {
        var me = this,
            selection;
        if (!(record.isModel)) {
            Ext.raise('Row selection must be passed a record');
        }
        selection = me.selectedRecords || (me.selectedRecords = me.createRecordCollection());
        if (!selection.byInternalId.get(record.internalId)) {
            selection.add(record);
            me.view.onRowSelect(record);
        }
    },
    add: function(record) {
        var me = this,
            i, len;
        if (record.isModel) {
            me.addOne(record);
        } else if (Ext.isArray(record)) {
            for (i = 0 , len = record.length; i < len; i++) {
                me.addOne(record[i]);
            }
        } else {
            Ext.raise('add must be called with a record or array of records');
        }
    },
    removeOne: function(record) {
        var me = this;
        if (!(record.isModel)) {
            Ext.raise('Row selection must be passed a record');
        }
        if (me.selectedRecords && me.selectedRecords.byInternalId.get(record.internalId)) {
            me.selectedRecords.remove(record);
            me.view.onRowDeselect(record);
            // Flag when selectAll called.
            // While this is set, a call to contains will add the record to the collection
            // and return true
            me.allSelected = false;
            return true;
        }
        return false;
    },
    remove: function(record) {
        var me = this,
            ret = true,
            i, len;
        if (record.isModel) {
            return me.removeOne(record);
        } else if (Ext.isArray(record)) {
            for (i = 0 , len = record.length; i < len; i++) {
                ret &= me.removeOne(record[i]);
            }
        } else {
            Ext.raise('remove must be called with a record or array of records');
        }
        return ret;
    },
    /**
     * Returns `true` if the passed {@link Ext.data.Model record} is selected.
     * @param {Ext.data.Model} record The record to test.
     * @return {Boolean} `true` if the passed {@link Ext.data.Model record} is selected.
     */
    contains: function(record) {
        if (!record || !record.isModel) {
            return false;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            result = false,
            selectedRecords = me.selectedRecords;
        // Flag set when selectAll is called in the selModel.
        // This allows buffered stores to treat all *rendered* records
        // as selected, so that the selection model will always encompass
        // What the user *sees* as selected
        if (me.allSelected) {
            me.add(record);
            return true;
        }
        // First check if the record is in our collection
        if (selectedRecords) {
            result = !!selectedRecords.byInternalId.get(record.internalId);
        }
        return result;
    },
    /**
     * Returns the number of records selected
     * @return {Number} The number of records selected.
     */
    getCount: function() {
        var selectedRecords = this.selectedRecords;
        return (selectedRecords && selectedRecords.length) || 0;
    },
    /**
     * Returns the records selected.
     * @return {Ext.data.Model[]} The records selected.
     */
    getRecords: function() {
        var selectedRecords = this.selectedRecords;
        return selectedRecords ? selectedRecords.getRange() : [];
    },
    selectAll: function() {
        var me = this,
            ds = me.view.dataSource,
            rangeSize = ds.isBufferedStore ? ds.getData().getCount() : ds.getCount();
        me.clear();
        me.setRangeStart(0);
        me.setRangeEnd(rangeSize - 1);
        // Adds the records to the collection
        me.addRange();
        // While this is set, a call to contains will add the record to the collection
        // and return true.
        // This is so that buffer rendered stores can utulize row based selectAll
        me.allSelected = true;
    },
    /**
     * @return {Number} The row index of the first row in the range or zero if no range.
     */
    getFirstRowIndex: function() {
        return this.getCount() ? this.view.dataSource.indexOf(this.selectedRecords.first()) : 0;
    },
    /**
     * @return {Number} The row index of the last row in the range or -1 if no range.
     */
    getLastRowIndex: function() {
        return this.getCount() ? this.view.dataSource.indexOf(this.selectedRecords.last()) : -1;
    },
    eachRow: function(fn, scope) {
        var selectedRecords = this.selectedRecords;
        if (selectedRecords) {
            selectedRecords.each(fn, scope || this);
        }
    },
    eachColumn: function(fn, scope) {
        var columns = this.view.getVisibleColumnManager().getColumns(),
            len = columns.length,
            i;
        // If we have any records selected, then all visible columns are selected.
        if (this.selectedRecords) {
            for (i = 0; i < len; i++) {
                if (fn.call(this || scope, columns[i], i) === false) {
                    return;
                }
            }
        }
    },
    eachCell: function(fn, scope) {
        var me = this,
            selection = me.selectedRecords,
            view = me.view,
            columns = view.ownerGrid.getVisibleColumnManager().getColumns(),
            abort = false,
            colCount, i, j, context, range, recCount;
        if (columns) {
            colCount = columns.length;
            context = new Ext.grid.CellContext(view);
            // Use Collection#each instead of copying the entire dataset into an array
            // and iterating that.
            if (selection) {
                selection.each(function(record) {
                    context.setRow(record);
                    for (i = 0; i < colCount; i++) {
                        context.setColumn(columns[i]);
                        // eslint-disable-next-line max-len
                        if (fn.call(scope || me, context, context.colIdx, context.rowIdx) === false) {
                            abort = true;
                            return false;
                        }
                    }
                });
            }
            // If called during a drag select, or SHIFT+arrow select, include the drag range
            if (!abort) {
                range = me.getRange();
                if (range[0] === range[1]) {
                    return;
                }
                me.view.dataSource.getRange(range[0], range[1], {
                    forRender: false,
                    callback: function(records) {
                        recCount = records.length;
                        for (i = 0; !abort && i < recCount; i++) {
                            context.setRow(records[i]);
                            for (j = 0; !abort && j < colCount; j++) {
                                context.setColumn(columns[j]);
                                // eslint-disable-next-line max-len
                                if (fn.call(scope || me, context, context.colIdx, context.rowIdx) === false) {
                                    abort = true;
                                }
                            }
                        }
                    }
                });
            }
        }
    },
    /**
     * This method is called to indicate the start of multiple changes to the selected row set.
     *
     * Internally this method increments a counter that is decremented by `{@link #endUpdate}`. It
     * is important, therefore, that if you call `beginUpdate` directly you match that
     * call with a call to `endUpdate` or you will prevent the collection from updating
     * properly.
     */
    beginUpdate: function() {
        var selectedRecords = this.selectedRecords;
        if (selectedRecords) {
            selectedRecords.beginUpdate();
        }
    },
    /**
     * This method is called after modifications are complete on a selected row set. For details
     * see `{@link #beginUpdate}`.
     */
    endUpdate: function() {
        var selectedRecords = this.selectedRecords;
        if (selectedRecords) {
            selectedRecords.endUpdate();
        }
    },
    destroy: function() {
        this.selectedRecords = Ext.destroy(this.selectedRecords);
        this.callParent();
    },
    //-------------------------------------------------------------------------
    privates: {
        /**
         * @private
         */
        clear: function() {
            var me = this,
                view = me.view;
            // Flag when selectAll called.
            // While this is set, a call to contains will add the record to the collection
            // and return true
            me.allSelected = false;
            if (me.selectedRecords) {
                me.eachRow(function(record) {
                    view.onRowDeselect(record);
                });
                me.selectedRecords.clear();
            }
            me.setRangeStart(null);
        },
        /**
         * @return {Boolean}
         * @private
         */
        isAllSelected: function() {
            // This branch has a flag because it encompasses a possibly buffered store,
            // where the full dataset might not be present, so a flag indicates that all
            // records are selected even as they flow into or out of the buffered page cache.
            return !!this.allSelected;
        },
        /**
         * Used during drag/shift+downarrow range selection on start.
         * @param {Number} start The start row index of the row drag selection.
         * @param {Boolean} suppressEvent True to prevent onRowSelect from being fired.
         * @private
         */
        setRangeStart: function(start, suppressEvent) {
            // Flag when selectAll called.
            // While this is set, a call to contains will add the record to the collection
            // and return true
            this.allSelected = false;
            this.rangeStart = this.rangeEnd = start;
            if (!suppressEvent) {
                this.view.onRowSelect(start);
            }
        },
        /**
         * Used during drag/shift+downarrow range selection on change of row.
         * @param {Number} end The end row index of the row drag selection.
         * @param {Boolean} append True if are appending to an existing selection.
         * @private
         */
        setRangeEnd: function(end, append) {
            var me = this,
                view = me.view,
                store = view.dataSource,
                selected = me.selectedRecords,
                withinRange, range, lastRange, rowIdx, rec;
            // Update the range as requested, then calculate the
            // range in lowest index first form
            me.rangeEnd = end;
            range = me.getRange();
            lastRange = me.lastRange || range;
            rowIdx = Math.min(range[0], lastRange[0]);
            end = Math.max(range[1], lastRange[1]);
            // Loop through the union of last range and current range
            for (; rowIdx <= end; rowIdx++) {
                rec = store.getAt(rowIdx);
                withinRange = (rowIdx < range[0] || rowIdx > range[1]);
                // If we are outside the current range, deselect
                if (!append && withinRange) {
                    // If we are deselecting, also remove from collection
                    if (selected && me.contains(rec)) {
                        selected.remove(rec);
                    }
                    view.onRowDeselect(rowIdx);
                } else {
                    if (append && withinRange) {
                        
                        continue;
                    }
                    view.onRowSelect(rowIdx);
                }
            }
            me.lastRange = range;
        },
        extendRange: function(extensionVector) {
            var me = this,
                store = me.view.dataSource,
                i;
            for (i = extensionVector.start.rowIdx; i <= extensionVector.end.rowIdx; i++) {
                me.add(store.getAt(i));
            }
        },
        /**
         * @private
         * Called through {@link Ext.grid.selection.SpreadsheetModel#getLastSelected} by
         * {@link Ext.panel.Table#updateBindSelection} when publishing the `selection` property.
         * It should yield the last record selected.
         */
        getLastSelected: function() {
            var sel = this.selectedRecords;
            return sel && sel.last();
        },
        /**
         * @return {Number[]}
         * @private
         */
        getRange: function() {
            var start = this.rangeStart,
                end = this.rangeEnd;
            if (start == null) {
                return [
                    0,
                    -1
                ];
            } else if (start <= end) {
                return [
                    start,
                    end
                ];
            }
            return [
                end,
                start
            ];
        },
        /**
         * Returns the size of the mousedown+drag, or SHIFT+arrow selection range.
         * @return {Number}
         * @private
         */
        getRangeSize: function() {
            var range = this.getRange();
            return range[1] - range[0] + 1;
        },
        /**
         * @return {Ext.util.Collection}
         * @private
         */
        createRecordCollection: function() {
            var store = this.view.dataSource,
                result = new Ext.util.Collection({
                    rootProperty: 'data',
                    extraKeys: {
                        byInternalId: {
                            rootProperty: false,
                            property: 'internalId'
                        }
                    },
                    sorters: [
                        function(r1, r2) {
                            return store.indexOf(r1) - store.indexOf(r2);
                        }
                    ]
                });
            return result;
        },
        /**
         * Called at the end of a drag, or shift+downarrow row range select.
         * The record range delineated by the start and end row indices is added to the selected
         * Collection.
         * @private
         */
        addRange: function() {
            var me = this,
                range, selection;
            if (me.rangeStart != null) {
                range = me.getRange();
                selection = me.selectedRecords || (me.selectedRecords = me.createRecordCollection());
                me.view.dataSource.getRange(range[0], range[1], {
                    forRender: false,
                    callback: function(range) {
                        selection.add.apply(selection, range);
                    }
                });
            }
        },
        onSelectionFinish: function() {
            var me = this,
                range = me.getContiguousSelection();
            if (range) {
                me.view.getSelectionModel().onSelectionFinish(me, new Ext.grid.CellContext(me.view).setPosition(range[0], 0), new Ext.grid.CellContext(me.view).setPosition(range[1], me.view.getVisibleColumnManager().getColumns().length - 1));
            } else {
                me.view.getSelectionModel().onSelectionFinish(me);
            }
        },
        /**
         * @return {Array} `[startRowIndex, endRowIndex]` if the selection represents
         * a visually contiguous set of rows.
         * The SelectionReplicator is only enabled if there is a contiguous block.
         * @private
         */
        getContiguousSelection: function() {
            var store = this.view.dataSource,
                selection, len, i;
            if (this.selectedRecords) {
                selection = Ext.Array.sort(this.selectedRecords.getRange(), function(r1, r2) {
                    return store.indexOf(r1) - store.indexOf(r2);
                });
                len = selection.length;
                if (len) {
                    for (i = 1; i < len; i++) {
                        if (store.indexOf(selection[i]) !== store.indexOf(selection[i - 1]) + 1) {
                            return false;
                        }
                    }
                    return [
                        store.indexOf(selection[0]),
                        store.indexOf(selection[len - 1])
                    ];
                }
            }
        }
    }
});

Ext.define('Ext.grid.selection.SelectionExtender', {
    extend: 'Ext.dd.DragTracker',
    maskBox: {},
    constructor: function(config) {
        var me = this;
        // We can only initialize properly if there are elements to work with
        if (config.view.rendered) {
            me.initSelectionExtender(config);
        } else {
            me.view = config.view;
            config.view.on({
                render: me.initSelectionExtender,
                args: [
                    config
                ],
                scope: me
            });
        }
    },
    initSelectionExtender: function(config) {
        var me = this,
            displayMode = Ext.dom.Element.DISPLAY;
        me.el = config.view.el;
        me.handle = config.view.ownerGrid.body.createChild({
            cls: Ext.baseCSSPrefix + 'ssm-extender-drag-handle',
            style: 'display:none'
        }).setVisibilityMode(displayMode);
        me.handle.on({
            contextmenu: function(e) {
                e.stopEvent();
            }
        });
        me.mask = me.el.createChild({
            cls: Ext.baseCSSPrefix + 'ssm-extender-mask',
            style: 'display:none'
        }).setVisibilityMode(displayMode);
        me.superclass.constructor.call(me, config);
        // Mask and andle must survive being orphaned
        me.mask.skipGarbageCollection = me.handle.skipGarbageCollection = true;
        me.viewListeners = me.view.on({
            scroll: me.onViewScroll,
            scope: me,
            destroyable: true
        });
        me.gridListeners = me.view.ownerGrid.on({
            columnResize: me.alignHandle,
            scope: me,
            destroyable: true
        });
        me.extendX = !!(me.axes & 1);
        me.extendY = !!(me.axes & 2);
    },
    setHandle: function(firstPos, lastPos) {
        var me = this;
        if (!me.view.rendered) {
            me.view.on({
                render: me.initSelectionExtender,
                args: [
                    firstPos,
                    lastPos
                ],
                scope: me
            });
            return;
        }
        me.firstPos = firstPos;
        me.lastPos = lastPos;
        // If we've done a "select all rows" and there is buffered rendering, then
        // the cells might not be rendered, so we can't activate the replicator.
        if (firstPos && lastPos && firstPos.getCell(true) && lastPos.getCell(true)) {
            if (me.curPos) {
                me.curPos.setPosition(lastPos);
            } else {
                me.curPos = lastPos.clone();
            }
            // Align centre of handle with bottom-right corner of last cell if possible.
            me.alignHandle();
        } else {
            me.disable();
        }
    },
    alignHandle: function() {
        var me = this,
            firstCell = me.firstPos && me.firstPos.getCell(true),
            lastCell = me.lastPos && me.lastPos.getCell(true),
            handle = me.handle,
            shouldDisplay;
        // Cell corresponding to the position might not be rendered.
        // This will be called upon scroll
        if (firstCell && lastCell) {
            me.enable();
            handle.alignTo(lastCell, 'c-br');
            shouldDisplay = me.isHandleWithinView(Ext.fly(lastCell).up('.x-grid-view'));
            handle.setVisible(shouldDisplay);
        } else {
            me.disable();
        }
    },
    isHandleWithinView: function(view) {
        var me = this,
            viewBox = view.getBox(),
            handleBox = me.handle.getBox(),
            withinX;
        withinX = viewBox.left <= handleBox.left && viewBox.right >= (handleBox.right - handleBox.width);
        return withinX;
    },
    enable: function() {
        this.handle.show();
        this.callParent();
    },
    disable: function() {
        this.handle.hide();
        this.mask.hide();
        this.callParent();
    },
    onDrag: function(e) {
        // pointer-events-none is not supported on IE10m.
        // So if shrinking the extension zone, the mousemove target may be the mask.
        // We have to retarget on the cell *below* that.
        if (e.target === this.mask.dom) {
            this.mask.hide();
            e.target = document.elementFromPoint.apply(document, e.getXY());
            this.mask.show();
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            view = me.view,
            viewTop = view.el.getY(),
            viewLeft = view.el.getX(),
            overCell = e.getTarget(me.view.getCellSelector()),
            scrollTask = me.scrollTask || (me.scrollTask = Ext.util.TaskManager.newTask({
                run: me.doAutoScroll,
                scope: me,
                interval: 10
            })),
            scrollBy = me.scrollBy || (me.scrollBy = []);
        // Dragged outside the view; stop scrolling.
        if (!me.el.contains(e.target)) {
            scrollBy[0] = scrollBy[1] = 0;
            return scrollTask.stop();
        }
        // Neart bottom of view
        if (me.lastXY[1] > viewTop + view.el.getHeight(true) - 15) {
            if (me.extendY) {
                scrollBy[1] = 3;
                scrollTask.start();
            }
        }
        // Near top of view
        else if (me.lastXY[1] < viewTop + 10) {
            if (me.extendY) {
                scrollBy[1] = -3;
                scrollTask.start();
            }
        }
        // Near right edge of view
        else if (me.lastXY[0] > viewLeft + view.el.getWidth(true) - 15) {
            if (me.extendX) {
                scrollBy[0] = 3;
                scrollTask.start();
            }
        }
        // Near left edge of view
        else if (me.lastXY[0] < viewLeft + 10) {
            if (me.extendX) {
                scrollBy[0] = -3;
                scrollTask.start();
            }
        } else // Not near an edge, cancel autoscrolling
        {
            scrollBy[0] = scrollBy[1] = 0;
            scrollTask.stop();
        }
        if (overCell && overCell !== me.lastOverCell) {
            me.lastOverCell = overCell;
            me.syncMaskOnCell(overCell);
        }
    },
    doAutoScroll: function() {
        var me = this,
            view = me.view,
            scrollOverCell;
        // Bump the view in whatever direction was decided in the onDrag method.
        view.scrollBy.apply(view, me.scrollBy);
        // Mouseover does not fire on autoscroll so see where the mouse is over on each scroll
        scrollOverCell = document.elementFromPoint.apply(document, me.lastXY);
        if (scrollOverCell) {
            scrollOverCell = Ext.fly(scrollOverCell).up(view.cellSelector);
            if (scrollOverCell && scrollOverCell !== me.lastOverCell) {
                me.lastOverCell = scrollOverCell;
                me.syncMaskOnCell(scrollOverCell);
            }
        }
    },
    onEnd: function(e) {
        var me = this;
        if (me.scrollTask) {
            me.scrollTask.stop();
        }
        if (me.extensionDescriptor) {
            me.disable();
            me.view.getSelectionModel().extendSelection(me.extensionDescriptor);
        }
    },
    onViewScroll: function() {
        var me = this;
        // If being dragged
        if (me.active && me.lastOverCell) {
            me.syncMaskOnCell(me.lastOverCell);
        }
        // We have been applied to a selection block
        if (me.firstPos) {
            // Align centre of handle with bottom-right corner of last cell if possible.
            me.alignHandle();
        }
    },
    syncMaskOnCell: function(overCell) {
        var me = this,
            view = me.view,
            rows = view.all,
            curPos = me.curPos,
            maskBox = me.maskBox,
            selRegion,
            firstPos = me.firstPos.clone(),
            lastPos = me.lastPos.clone(),
            extensionStart = me.firstPos.clone(),
            extensionEnd = me.lastPos.clone(),
            preventReduce = !me.allowReduceSelection;
        // Constrain cell positions to be within rendered range.
        firstPos.setRow(Math.min(Math.max(firstPos.rowIdx, rows.startIndex), rows.endIndex));
        lastPos.setRow(Math.min(Math.max(lastPos.rowIdx, rows.startIndex), rows.endIndex));
        me.selectionRegion = selRegion = firstPos.getCell().getRegion().union(lastPos.getCell().getRegion());
        curPos.setPosition(view.getRecord(overCell), view.getHeaderByCell(overCell));
        // The above calls require the cell to be a DOM reference
        overCell = Ext.fly(overCell);
        // Reset border to default, which is the overall border setting from SASS
        // We disable the border which is contiguous to the selection.
        me.mask.dom.style.borderTopWidth = me.mask.dom.style.borderRightWidth = me.mask.dom.style.borderBottomWidth = me.mask.dom.style.borderLeftWidth = '';
        // Dragged above the selection
        if (curPos.rowIdx < me.firstPos.rowIdx && me.extendY) {
            me.extensionDescriptor = {
                type: 'rows',
                start: extensionStart.setRow(curPos.rowIdx),
                end: extensionEnd.setRow(me.firstPos.rowIdx - 1),
                rows: curPos.rowIdx - me.firstPos.rowIdx,
                mousePosition: me.lastXY
            };
            me.mask.dom.style.borderBottomWidth = '0';
            maskBox.x = selRegion.x;
            maskBox.y = overCell.getY();
            maskBox.width = selRegion.right - selRegion.left;
            maskBox.height = selRegion.top - overCell.getY();
        }
        // Dragged below selection
        else if (curPos.rowIdx > me.lastPos.rowIdx && me.extendY) {
            me.extensionDescriptor = {
                type: 'rows',
                start: extensionStart.setRow(me.lastPos.rowIdx + 1),
                end: extensionEnd.setRow(curPos.rowIdx),
                rows: curPos.rowIdx - me.lastPos.rowIdx,
                mousePosition: me.lastXY
            };
            me.mask.dom.style.borderTopWidth = '0';
            maskBox.x = selRegion.x;
            maskBox.y = selRegion.bottom;
            maskBox.width = selRegion.right - selRegion.left;
            maskBox.height = overCell.getRegion().bottom - selRegion.bottom;
        }
        // reducing Y selection dragged from the bottom
        else if (!preventReduce && curPos.rowIdx < me.lastPos.rowIdx && me.extendY && curPos.colIdx === me.lastPos.colIdx) {
            me.extensionDescriptor = {
                type: 'rows',
                start: extensionStart.setRow(me.firstPos.rowIdx),
                end: extensionEnd.setRow(curPos.rowIdx),
                rows: -1,
                mousePosition: me.lastXY,
                reduce: true
            };
            me.mask.dom.style.borderTopWidth = '0';
            maskBox.x = selRegion.x;
            maskBox.y = selRegion.top;
            maskBox.width = selRegion.right - selRegion.left;
            maskBox.height = overCell.getRegion().bottom - selRegion.top;
        } else // row position is within selected row range
        {
            // Dragged to left of selection
            if (curPos.colIdx < me.firstPos.colIdx && me.extendX) {
                me.extensionDescriptor = {
                    type: 'columns',
                    start: extensionStart.setColumn(curPos.colIdx),
                    end: extensionEnd.setColumn(me.firstPos.colIdx - 1),
                    columns: curPos.colIdx - me.firstPos.colIdx,
                    mousePosition: me.lastXY
                };
                me.mask.dom.style.borderRightWidth = '0';
                maskBox.x = overCell.getX();
                maskBox.y = selRegion.top;
                maskBox.width = selRegion.left - overCell.getRegion().left;
                maskBox.height = selRegion.bottom - selRegion.top;
            }
            // Dragged to right of selection
            else if (curPos.colIdx > me.lastPos.colIdx && me.extendX) {
                me.extensionDescriptor = {
                    type: 'columns',
                    start: extensionStart.setColumn(me.lastPos.colIdx + 1),
                    end: extensionEnd.setColumn(curPos.colIdx),
                    columns: curPos.colIdx - me.lastPos.colIdx,
                    mousePosition: me.lastXY
                };
                me.mask.dom.style.borderLeftWidth = '0';
                maskBox.x = selRegion.right;
                maskBox.y = selRegion.top;
                maskBox.width = overCell.getRegion().right - selRegion.right;
                maskBox.height = selRegion.bottom - selRegion.top;
            }
            // reducing X selection dragged from the right
            else if (!preventReduce && curPos.colIdx < me.lastPos.colIdx && me.extendX) {
                me.extensionDescriptor = {
                    type: 'columns',
                    start: extensionStart.setColumn(me.firstPos.colIdx),
                    end: extensionEnd.setColumn(curPos.colIdx),
                    columns: -1,
                    mousePosition: me.lastXY,
                    reduce: true
                };
                me.mask.dom.style.borderLeftWidth = '0';
                maskBox.x = selRegion.left;
                maskBox.y = selRegion.top;
                maskBox.width = overCell.getRegion().right - selRegion.left;
                maskBox.height = selRegion.bottom - selRegion.top;
            } else {
                me.extensionDescriptor = null;
            }
        }
        if (view.ownerGrid.hasListeners.selectionextenderdrag) {
            view.ownerGrid.fireEvent('selectionextenderdrag', view.ownerGrid, view.getSelectionModel().getSelected(), me.extensionDescriptor);
        }
        if (me.extensionDescriptor) {
            me.mask.show();
            me.mask.setBox(maskBox);
        } else {
            me.mask.hide();
        }
    },
    destroy: function() {
        var me = this;
        Ext.destroy(me.gridListeners, me.viewListeners, me.mask, me.handle);
        me.callParent();
    }
});

/**
 * A selection model for {@link Ext.grid.Panel grids} which allows you to select data in
 * a spreadsheet-like manner.
 *
 * Supported features:
 *
 *  - Single / Range / Multiple individual row selection.
 *  - Single / Range cell selection.
 *  - Column selection by click selecting column headers.
 *  - Select / deselect all by clicking in the top-left, header.
 *  - Adds row number column to enable row selection.
 *  - Optionally you can enable row selection using checkboxes
 *
 * # Example usage
 *
 *     @example
 *     var store = Ext.create('Ext.data.Store', {
 *         fields: ['name', 'email', 'phone'],
 *         data: [
 *             { name: 'Lisa', email: 'lisa@simpsons.com', phone: '555-111-1224' },
 *             { name: 'Bart', email: 'bart@simpsons.com', phone: '555-222-1234' },
 *             { name: 'Homer', email: 'homer@simpsons.com', phone: '555-222-1244' },
 *             { name: 'Marge', email: 'marge@simpsons.com', phone: '555-222-1254' }
 *         ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Simpsons',
 *         store: store,
 *         width: 400,
 *         renderTo: Ext.getBody(),
 *         columns: [
 *             { text: 'Name', dataIndex: 'name' },
 *             { text: 'Email', dataIndex: 'email', flex: 1 },
 *             { text: 'Phone', dataIndex: 'phone' }
 *         ],
 *         selModel: {
 *            type: 'spreadsheet'
 *         }
 *     });
 *
 * # Using {@link Ext.data.BufferedStore}s
 * It is very important to remember that a {@link Ext.data.BufferedStore} does *not* contain the
 * full dataset. The purpose of a BufferedStore is to only hold in the client, a range of
 * pages from the dataset that corresponds with what is currently visible in the grid
 * (plus a few pages above and below the visible range to allow fast scrolling).
 *
 * When using "select all" rows and a BufferedStore, an `allSelected` flag is set, and so all
 * records which are read into the client side cache will thenceforth be selected, and will
 * be rendered as selected in the grid.
 *
 * *But records which have not been read into the cache will obviously not be available
 * when interrogating selected records. As you scroll through the dataset, and more
 * pages are read from the server, they will become available to add to the selection.*
 *
 * @since 5.1.0
 */
Ext.define('Ext.grid.selection.SpreadsheetModel', {
    extend: 'Ext.selection.Model',
    requires: [
        'Ext.grid.selection.Selection',
        'Ext.grid.selection.Cells',
        'Ext.grid.selection.Rows',
        'Ext.grid.selection.Columns',
        'Ext.grid.selection.SelectionExtender'
    ],
    // TODO: cmd-auto-dependency
    alias: 'selection.spreadsheet',
    isSpreadsheetModel: true,
    config: {
        /**
         * @cfg {Boolean} [columnSelect=false]
         * Set to `true` to enable selection of columns.
         *
         * **NOTE**: This will remove sorting on header click and instead provide column
         * selection and deselection. Sorting is still available via column header menu.
         */
        columnSelect: {
            $value: false,
            lazy: true
        },
        /**
         * @cfg {Boolean} [cellSelect=true]
         * Set to `true` to enable selection of individual cells or a single rectangular
         * range of cells. This will provide cell range selection using click, and
         * potentially drag to select a rectangular range. You can also use "SHIFT + arrow"
         * key navigation to select a range of cells.
         */
        cellSelect: {
            $value: true,
            lazy: true
        },
        /**
         * @cfg {Boolean} [rowSelect=true]
         * Set to `true` to enable selection of rows by clicking on a row number column.
         *
         * *Note*: This feature will add the row number as the first column.
         */
        rowSelect: {
            $value: true,
            lazy: true
        },
        /**
        * @cfg {Boolean} [dragSelect=true]
        * Set to `true` to enables cell range selection by cell dragging.
        */
        dragSelect: {
            $value: true,
            lazy: true
        },
        /**
        * @cfg {Ext.grid.selection.Selection} [selected]
        * Pass an instance of one of the subclasses of {@link Ext.grid.selection.Selection}.
        */
        selected: null,
        /**
         * @cfg {String} extensible
         * This configures whether this selection model is to implement a mouse based dragging
         * gesture to extend a *contiguous* selection.
         *
         * Note that if there are multiple, discontiguous selected rows or columns, selection
         * extension is not available.
         *
         * If set, then the bottom right corner of the contiguous selection will display
         * a drag handle. By dragging this, an extension area may be defined into which
         * the selection is extended.
         *
         * Upon the end of the drag, the
         * {@link Ext.panel.Table#beforeselectionextend beforeselectionextend} event will be fired
         * though the encapsulating grid. Event handlers may manipulate the store data in any way.
         *
         * Possible values for this configuration are
         *
         *    - `"x"` Only allow extending the block to the left or right.
         *    - `"y"` Only allow extending the block above or below.
         *    - `"xy"` Allow extending the block in both dimensions.
         *    - `"both"` Allow extending the block in both dimensions.
         *    - `true` Allow extending the block in both dimensions.
         *    - `false` Disable the extensible feature
         *    - `null` Disable the extensible feature
         *
         * It's important to notice that setting this to `"both"`, `"xy"` or `true` will allow you
         * to extend the selection in both directions, but only one direction at a time.
         * It will NOT be possible to drag it diagonally. 
         */
        extensible: {
            $value: true,
            lazy: true
        },
        /**
         * @cfg {Boolean} reducible
         * @since 6.6.0
         * This configures if the extensible config is also allowed to reduce its selection
         *
         * Note: This is only relevant if `extensible` is not `false` or `null`
         */
        reducible: true
    },
    /**
     * @event selectionchange
     * Fired *by the grid* after the selection changes. Return `false` to veto the selection
     * extension.
     *
     * Note that the behavior of selectionchange is different in Ext 6.x vs. Ext 5.  In Ext 6.x,
     * if rows are being selected, a block of records is passed as the second parameter.
     * In Ext 5, the selection object was passed.  
     * 
     *
     * @param {Ext.grid.Panel} grid The grid whose selection has changed.
     * @param {Ext.grid.selection.Selection} selection A subclass of
     * {@link Ext.grid.selection.Selection} describing the new selection.
     */
    /**
     * @cfg {Boolean} checkboxSelect [checkboxSelect=false]
     * Enables selection of the row via clicking on checkbox. Note: this feature will add
     * new column at position specified by {@link #checkboxColumnIndex}.
     */
    checkboxSelect: false,
    /**
     * @cfg {Number/String} [checkboxColumnIndex=0]
     * The index at which to insert the checkbox column.
     * Supported values are a numeric index, and the strings 'first' and 'last'. Only valid when set
     * *before* render.
     */
    checkboxColumnIndex: 0,
    /**
     * @cfg {Boolean} [showHeaderCheckbox=true]
     * Configure as `false` to not display the header checkbox at the top of the checkbox column
     * when {@link #checkboxSelect} is set.
     */
    showHeaderCheckbox: true,
    /**
     * @cfg {String} [checkColumnHeaderText]
     * Displays the configured text in the check column's header.
     *
     * if {@link #cfg-showHeaderCheckbox} is `true`, the text is shown *above* the checkbox.
     * @since 6.0.1
     */
    checkColumnHeaderText: null,
    /**
     * @cfg {Number/String} [checkboxHeaderWidth=24]
     * Width of checkbox column.
     */
    checkboxHeaderWidth: 24,
    /**
     * @cfg {Number/String} [rowNumbererHeaderWidth=46]
     * Width of row numbering column.
     */
    rowNumbererHeaderWidth: 46,
    columnSelectCls: Ext.baseCSSPrefix + 'ssm-column-select',
    rowNumbererHeaderCls: Ext.baseCSSPrefix + 'ssm-row-numberer-hd',
    tdCls: Ext.baseCSSPrefix + 'grid-cell-special ' + Ext.baseCSSPrefix + 'selmodel-column',
    /**
     * @method getCount
     * This method is not supported by SpreadsheetModel.
     *
     * To interrogate the selection use {@link #cfg!selected}'s getter, which will return
     * an instance of one of the three selection types, or `null` if no selection.
     *
     * The three selection types are:
     *
     *    * {@link Ext.grid.selection.Rows}
     *    * {@link Ext.grid.selection.Columns}
     *    * {@link Ext.grid.selection.Cells}
     */
    /**
     * @method getSelectionMode
     * This method is not supported by SpreadsheetModel.
     */
    /**
     * @method setSelectionMode
     * This method is not supported by SpreadsheetModel.
     */
    /**
     * @method setLocked
     * This method is not currently supported by SpreadsheetModel.
     */
    /**
     * @method isLocked
     * This method is not currently supported by SpreadsheetModel.
     */
    /**
     * @method isRangeSelected
     * This method is not supported by SpreadsheetModel.
     *
     * To interrogate the selection use {@link #cfg!selected}'s getter, which will return
     * an instance of one of the three selection types, or `null` if no selection.
     *
     * The three selection types are:
     *
     *    * {@link Ext.grid.selection.Rows}
     *    * {@link Ext.grid.selection.Columns}
     *    * {@link Ext.grid.selection.Cells}
     */
    /**
     * @member Ext.panel.Table
     * @event beforeselectionextend An event fired when an extension block is extended 
     * using a drag gesture.  Only fired when the SpreadsheetSelectionModel is used and 
     * configured with the 
     * {@link Ext.grid.selection.SpreadsheetModel#extensible extensible} config.
     * @param {Ext.panel.Table} grid The owning grid.
     * @param {Ext.grid.selection.Selection} An object which encapsulates a contiguous
     * selection block.
     * @param {Object} extension An object describing the type and size of extension.
     * @param {String} extension.type `"rows"` or `"columns"`
     * @param {Ext.grid.CellContext} extension.start The start (top left) cell of the
     * extension area.
     * @param {Ext.grid.CellContext} extension.end The end (bottom right) cell of the
     * extension area.
     * @param {number} [extension.columns] The number of columns extended (-ve means
     * on the left side).
     * @param {number} [extension.rows] The number of rows extended (-ve means on the top side).
     */
    /**
     * @member Ext.panel.Table
     * @event selectionextenderdrag An event fired when an extension block is dragged to 
     * encompass a new range.  Only fired when the SpreadsheetSelectionModel is used and 
     * configured with the 
     * {@link Ext.grid.selection.SpreadsheetModel#extensible extensible} config.
     * @param {Ext.panel.Table} grid The owning grid.
     * @param {Ext.grid.selection.Selection} An object which encapsulates a contiguous
     * selection block.
     * @param {Object} extension An object describing the type and size of extension.
     * @param {String} extension.type `"rows"` or `"columns"`
     * @param {HTMLElement} extension.overCell The grid cell over which the mouse is being dragged.
     * @param {Ext.grid.CellContext} extension.start The start (top left) cell of the
     * extension area.
     * @param {Ext.grid.CellContext} extension.end The end (bottom right) cell of the
     * extension area.
     * @param {number} [extension.columns] The number of columns extended (-ve means
     * on the left side).
     * @param {number} [extension.rows] The number of rows extended (-ve means on the top side).
     */
    /**
     * @private
     */
    bindComponent: function(view) {
        var me = this,
            viewListeners, storeListeners, lockedGrid;
        if (me.view !== view) {
            if (me.view) {
                me.navigationModel = null;
                Ext.destroy(me.viewListeners, me.navigationListeners);
            }
            me.view = view;
            if (view) {
                // We need to realize our lazy configs now that we have the view...
                me.getCellSelect();
                lockedGrid = view.ownerGrid.lockedGrid;
                // If there is a locked grid, process it now
                if (lockedGrid) {
                    me.hasLockedHeader = true;
                    me.onViewCreated(lockedGrid, lockedGrid.getView());
                } else // Otherwise, get back to us when the view is fully created
                // so that we can tweak its headerCt
                {
                    view.grid.on({
                        viewcreated: me.onViewCreated,
                        scope: me,
                        single: true
                    });
                }
                me.gridListeners = view.ownerGrid.on({
                    columnschanged: me.onColumnsChanged,
                    columnmove: me.onColumnMove,
                    scope: me,
                    destroyable: true
                });
                storeListeners = me.getStoreListeners();
                storeListeners.scope = me;
                storeListeners.destroyable = true;
                me.storeListeners = me.store.on(storeListeners);
                viewListeners = me.getViewListeners();
                viewListeners.scope = me;
                viewListeners.destroyable = true;
                me.viewListeners = view.on(viewListeners);
                me.navigationModel = view.getNavigationModel();
                me.navigationListeners = me.navigationModel.on({
                    navigate: me.onNavigate,
                    scope: me,
                    destroyable: true
                });
                // Add class to add special cursor pointer to column headers
                if (me.getColumnSelect()) {
                    view.ownerGrid.addCls(me.columnSelectCls);
                }
                me.updateHeaderState();
            }
        }
    },
    /**
     * Retrieve a configuration to be used in a HeaderContainer.
     * This should be used when checkboxSelect is set to false.
     * @protected
     */
    getCheckboxHeaderConfig: function() {
        var me = this,
            showCheck = me.showHeaderCheckbox !== false;
        return {
            xtype: 'checkcolumn',
            // historically used as a discriminator property before isCheckColumn
            isCheckerHd: showCheck,
            headerCheckbox: showCheck,
            ignoreExport: true,
            text: me.checkColumnHeaderText,
            clickTargetName: 'el',
            width: me.checkboxHeaderWidth,
            sortable: false,
            draggable: false,
            resizable: false,
            hideable: false,
            menuDisabled: true,
            tdCls: me.tdCls,
            cls: Ext.baseCSSPrefix + 'selmodel-column',
            stopSelection: false,
            editRenderer: me.editRenderer || me.renderEmpty,
            locked: me.hasLockedHeader,
            updateHeaderState: me.updateHeaderState.bind(me),
            // It must not attempt to set anything in the records on toggle.
            // We handle that in onHeaderClick.
            toggleAll: Ext.emptyFn,
            // The selection model listens to the navigation model to select/deselect
            setRecordCheck: Ext.emptyFn,
            // It uses our isRowSelected to test whether a row is checked
            isRecordChecked: Ext.emptyFn
        };
    },
    renderEmpty: function() {
        return '\xa0';
    },
    /**
     * @private
     */
    getStoreListeners: function() {
        var me = this,
            r = me.callParent();
        r.priority = 2000;
        r.refresh = me.onStoreChanged;
        r.clear = me.onStoreChanged;
        return r;
    },
    /**
     * @private
     */
    onHeaderClick: function(headerCt, header, e) {
        // Template method. See base class
        var me = this,
            sel = me.selected,
            isSelected = false;
        if (header === me.numbererColumn || header === me.checkColumn) {
            e.stopEvent();
            // Not all selected, select all
            if (!sel || !sel.isAllSelected()) {
                me.selectAll();
            } else {
                me.deselectAll();
            }
            me.updateHeaderState();
            me.lastColumnSelected = null;
        } else if (me.columnSelect) {
            if (e.shiftKey && sel && sel.lastColumnSelected) {
                sel.setRangeEnd(header);
                me.fireSelectionChange();
            } else {
                // keeping track of the column selection status before we go through the clear block
                isSelected = me.isColumnSelected(header);
                if (sel) {
                    if (!e.ctrlKey) {
                        sel.clear();
                        me.updateSelectionExtender();
                    } else if (isSelected) {
                        me.deselectColumn(header);
                        me.selected.lastColumnSelected = null;
                    }
                }
                if (!isSelected || (!e.ctrlKey && e.pointerType !== 'touch')) {
                    me.selectColumn(header, e.ctrlKey);
                    sel = me.selected;
                    sel.lastColumnSelected = header;
                    if (!sel.startColumn) {
                        sel.startColumn = header;
                    }
                }
            }
            me.lastOverColumn = header;
        }
    },
    selectByPosition: function(position) {
        var me = this;
        position = new Ext.grid.CellContext(me.view).setPosition(position.row, position.column);
        if (me.getCellSelect()) {
            me.selectCells(position, position);
        } else if (me.getRowSelect()) {
            this.select(position.record);
        } else if (me.getColumnSelect()) {
            me.selectColumn(position.column);
        }
    },
    /**
     * @private
     */
    updateHeaderState: function() {
        // check to see if all records are selected
        var me = this,
            store = me.view.dataSource,
            views = me.views,
            sel = me.selected,
            isChecked = false,
            checkHd = me.checkColumn,
            storeCount;
        if (store && sel && sel.isRows) {
            storeCount = store.getCount();
            if (store.isBufferedStore) {
                isChecked = sel.allSelected;
            } else {
                isChecked = storeCount > 0 && (storeCount === sel.getCount());
            }
        }
        if (views && views.length) {
            if (checkHd) {
                checkHd.setHeaderStatus(isChecked);
            }
        }
    },
    onBindStore: function(store, oldStore, initial) {
        if (!initial) {
            this.onStoreRefresh();
        }
    },
    /**
     * Handles the grid's beforereconfigure event.
     * Adds the checkbox header if the columns have been reconfigured.
     * Also adds the row numberer.
     * @param {Ext.panel.Table} grid
     * @param {Ext.data.Store} store
     * @param {Object[]} columns
     * @param {Ext.data.Store} oldStore
     * @param {Object[]} oldColumns
     * @private
     */
    onBeforeReconfigure: function(grid, store, columns, oldStore, oldColumns) {
        var me = this,
            checkboxColumnIndex = me.checkboxColumnIndex;
        if (columns) {
            Ext.suspendLayouts();
            if (me.numbererColumn) {
                me.numbererColumn.ownerCt.remove(me.numbererColumn, false);
                columns.unshift(me.numbererColumn);
            }
            if (me.checkColumn) {
                if (checkboxColumnIndex === 'first') {
                    checkboxColumnIndex = 0;
                } else if (checkboxColumnIndex === 'last') {
                    checkboxColumnIndex = columns.length;
                }
                me.checkColumn.ownerCt.remove(me.checkColumn, false);
                Ext.Array.insert(columns, checkboxColumnIndex, [
                    me.checkColumn
                ]);
            }
            Ext.resumeLayouts();
        }
    },
    /**
     * This is a helper method to create a cell context which encapsulates one cell in a grid view.
     *
     * It will contain the following properties:
     *  colIdx - column index
     *  rowIdx - row index
     *  column - {@link Ext.grid.column.Column Column} under which the cell is located.
     *  record - {@link Ext.data.Model} Record from which the cell derives its data.
     *  view - The view. If this selection model is for a locking grid, this will be the 
     *  outermost view, the {@link Ext.grid.locking.View} which encapsulates the sub 
     *  grids. Column indices are relative to the outermost view's visible column set.
     *
     * @param {Number} record Record for which to select the cell, or row index.
     * @param {Number} column Grid column header, or column index.
     * @return {Ext.grid.CellContext} A context object describing the cell. Note that the
     * `rowidx` and `colIdx` properties are only valid
     * at the time the context object is created. Column movement, sorting or filtering
     * might changed where the cell is.
     * @private
     */
    getCellContext: function(record, column) {
        return new Ext.grid.CellContext(this.view.ownerGrid.getView()).setPosition(record, column);
    },
    select: function(records, keepExisting, suppressEvent) {
        // API docs are inherited
        var me = this,
            sel = me.selected,
            view = me.view,
            store = view.dataSource,
            len, i, record,
            changed = false;
        // Ensure selection object is of the correct type
        if (!sel || !sel.isRows) {
            me.resetSelection(true);
            sel = me.selected = new Ext.grid.selection.Rows(view);
        } else if (!keepExisting) {
            sel.clear();
        }
        if (!Ext.isArray(records)) {
            records = [
                records
            ];
        }
        len = records.length;
        for (i = 0; i < len; i++) {
            record = records[i];
            if (typeof record === 'number') {
                record = store.getAt(record);
            }
            if (!sel.contains(record)) {
                sel.add(record);
                changed = true;
            }
        }
        if (changed) {
            me.updateHeaderState();
            if (!suppressEvent) {
                me.fireSelectionChange();
            }
        }
    },
    deselect: function(records, suppressEvent) {
        // API docs are inherited
        var me = this,
            sel = me.selected,
            store = me.view.dataSource,
            len, i, record,
            changed = false;
        if (sel && sel.isRows) {
            if (!Ext.isArray(records)) {
                records = [
                    records
                ];
            }
            len = records.length;
            for (i = 0; i < len; i++) {
                record = records[i];
                if (typeof record === 'number') {
                    record = store.getAt(record);
                }
                sel.remove(record);
                if (!changed) {
                    changed = true;
                }
            }
        }
        if (changed) {
            me.updateHeaderState();
            if (!suppressEvent) {
                me.fireSelectionChange();
            }
        }
    },
    /* eslint-disable max-len */
    /**
     * This method allows programmatic selection of the cell range.
     *
     *     @example
     *     var store = Ext.create('Ext.data.Store', {
     *         fields  : ['name', 'email', 'phone'],
     *         data    : {
     *             items : [
     *                 { name : 'Lisa',  email : 'lisa@simpsons.com',  phone : '555-111-1224' },
     *                 { name : 'Bart',  email : 'bart@simpsons.com',  phone : '555-222-1234' },
     *                 { name : 'Homer', email : 'homer@simpsons.com', phone : '555-222-1244' },
     *                 { name : 'Marge', email : 'marge@simpsons.com', phone : '555-222-1254' }
     *             ]
     *         },
     *         proxy   : {
     *             type   : 'memory',
     *             reader : {
     *                 type : 'json',
     *                 root : 'items'
     *             }
     *         }
     *     });
     *
     *     var grid = Ext.create('Ext.grid.Panel', {
     *         title    : 'Simpsons',
     *         store    : store,
     *         width    : 400,
     *         renderTo : Ext.getBody(),
     *         columns  : [
     *            columns: [
     *               { text: 'Name',  dataIndex: 'name' },
     *               { text: 'Email', dataIndex: 'email', flex: 1 },
     *               { text: 'Phone', dataIndex: 'phone', width:120 },
     *               {
     *                   text:'Combined', dataIndex: 'name', width : 300,
     *                   renderer: function(value, metaData, record, rowIndex, colIndex, store, view) {
     *                       console.log(arguments);
     *                       return value + ' has email: ' + record.get('email');
     *                   }
     *               }
     *           ],
     *         ],
     *         selType: 'spreadsheet'
     *     });
     *
     *     var model = grid.getSelectionModel();  // get selection model
     *
     *     // We will create range of 4 cells.
     *
     *     // Now set the range  and prevent rangeselect event from being fired.
     *     // We can use a simple array when we have no locked columns.
     *     model.selectCells([0, 0], [1, 1], true);
     *
     * @param rangeStart {Ext.grid.CellContext/Number[]} Range starting position. Can be either Cell
     * context or a `[rowIndex, columnIndex]` numeric array.
     *
     * Note that when a numeric array is used in a locking grid, the column indices are relative
     * to the outermost grid, encompassing locked *and* normal sides.
     * @param rangeEnd {Ext.grid.CellContext/Number[]} Range end position. Can be either
     * Cell context or a `[rowIndex, columnIndex]` numeric array.
     *
     * Note that when a numeric array is used in a locking grid, the column indices are relative
     * to the outermost grid, encompassing locked *and* normal sides.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    selectCells: function(rangeStart, rangeEnd, suppressEvent) {
        var me = this,
            view = me.view.ownerGrid.view,
            sel;
        rangeStart = rangeStart.isCellContext ? rangeStart.clone() : new Ext.grid.CellContext(view).setPosition(rangeStart);
        rangeEnd = rangeEnd.isCellContext ? rangeEnd.clone() : new Ext.grid.CellContext(view).setPosition(rangeEnd);
        me.resetSelection(true);
        me.selected = sel = new Ext.grid.selection.Cells(rangeStart.view);
        sel.setRangeStart(rangeStart);
        sel.setRangeEnd(rangeEnd);
        if (!suppressEvent) {
            me.fireSelectionChange();
        }
    },
    /* eslint-enable max-len */
    /**
     * Select all the data if possible.
     *
     * If {@link #rowSelect} is `true`, then all *records* will be selected.
     *
     * If {@link #cellSelect} is `true`, then all *rendered cells* will be selected.
     *
     * If {@link #columnSelect} is `true`, then all *columns* will be selected.
     *
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    selectAll: function(suppressEvent) {
        var me = this,
            sel = me.selected,
            doSelect,
            view = me.view;
        if (me.rowSelect) {
            if (!sel || !sel.isRows) {
                me.resetSelection(true);
                me.selected = sel = new Ext.grid.selection.Rows(view);
            }
            doSelect = true;
        } else if (me.cellSelect) {
            if (!sel || !sel.isCells) {
                me.resetSelection(true);
                me.selected = sel = new Ext.grid.selection.Cells(view);
            }
            doSelect = true;
        } else if (me.columnSelect) {
            if (!sel || !sel.isColumns) {
                me.resetSelection(true);
                me.selected = sel = new Ext.grid.selection.Columns(view);
            }
            doSelect = true;
        }
        if (sel) {
            sel.allSelected = true;
        }
        if (doSelect) {
            me.updateHeaderState();
            sel.selectAll();
            // this populates the selection with the records
            if (!suppressEvent) {
                me.fireSelectionChange();
            }
        }
    },
    /**
     * Clears the selection.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    deselectAll: function(suppressEvent) {
        var me = this,
            sel = me.selected;
        if (sel && sel.getCount()) {
            sel.clear();
            sel.allSelected = false;
            me.updateHeaderState();
            if (!suppressEvent) {
                me.fireSelectionChange();
            }
        }
    },
    /**
     * Select one or more rows.
     * @param rows {Ext.data.Model[]} Records to select.
     * @param {Boolean} [keepSelection=false] Pass `true` to keep previous selection.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    selectRows: function(rows, keepSelection, suppressEvent) {
        var me = this,
            sel = me.selected,
            isSelectingRows = sel && sel.isRows,
            len = rows.length,
            i;
        if (!keepSelection || !isSelectingRows) {
            me.resetSelection(true);
        }
        if (!isSelectingRows) {
            me.selected = sel = new Ext.grid.selection.Rows(me.view);
        }
        if (rows.isEntity) {
            sel.add(rows);
        } else {
            for (i = 0; i < len; i++) {
                sel.add(rows[i]);
            }
        }
        if (!suppressEvent) {
            me.fireSelectionChange();
        }
    },
    isSelected: function(record) {
        // API docs are inherited.
        return this.isRowSelected(record);
    },
    /**
     * Selects a column.
     * @param {Ext.grid.column.Column} column Column to select.
     * @param {Boolean} [keepSelection=false] Pass `true` to keep previous selection.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    selectColumn: function(column, keepSelection, suppressEvent) {
        var me = this,
            selData = me.selected,
            view = column.getView();
        // Clear other selection types
        if (!selData || !selData.isColumns || selData.view !== view.ownerGrid.view) {
            me.resetSelection(true);
            me.selected = selData = new Ext.grid.selection.Columns(view);
        }
        if (!selData.contains(column)) {
            if (!keepSelection) {
                selData.clear();
            }
            selData.add(column);
            me.updateHeaderState();
            if (!suppressEvent) {
                me.fireSelectionChange();
            }
        }
    },
    /**
     * Deselects a column.
     * @param {Ext.grid.column.Column} column Column to deselect.
     * @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
     * `{@link #selectionchange}` event.
     */
    deselectColumn: function(column, suppressEvent) {
        var me = this,
            selData = me.getSelected();
        if (selData && selData.isColumns && selData.contains(column)) {
            selData.remove(column);
            me.updateHeaderState();
            if (!suppressEvent) {
                me.fireSelectionChange();
            }
        }
    },
    getSelection: function() {
        // API docs are inherited.
        // Superclass returns array of selected records
        var selData = this.selected;
        if (selData && selData.isRows) {
            return selData.getRecords();
        }
        return [];
    },
    destroy: function() {
        var me = this,
            scrollEls = me.scrollEls;
        Ext.destroy(me.gridListeners, me.viewListeners, me.selected, me.navigationListeners, me.extensible);
        if (scrollEls) {
            Ext.dd.ScrollManager.unregister(scrollEls);
        }
        if (me._onMouseUp && !me._onMouseUp.destroyed) {
            me.stopAutoScroller();
            me._onMouseUp.destroy();
        }
        me.selected = me.gridListeners = me.viewListeners = me.selectionData = me.navigationListeners = me.scrollEls = null;
        me.callParent();
    },
    //-------------------------------------------------------------------------
    privates: {
        /**
         * @property {Object} axesConfigs
         * Use when converting the extensible config into a SelectionExtender
         * to create its `axes` config to specify which axes it may extend.
         * @private
         */
        axesConfigs: {
            x: 1,
            y: 2,
            xy: 3,
            both: 3,
            "true": 3
        },
        // reserved word MUST be quoted when used an a property name
        getNumbererColumnConfig: function() {
            var me = this;
            return {
                xtype: 'rownumberer',
                width: me.rowNumbererHeaderWidth,
                editRenderer: me.renderEmpty,
                tdCls: me.rowNumbererTdCls,
                cls: me.rowNumbererHeaderCls,
                locked: me.hasLockedHeader
            };
        },
        /**
         * @return {Object}
         * @private
         */
        getViewListeners: function() {
            return {
                refresh: this.onViewRefresh,
                keyup: {
                    element: 'el',
                    fn: this.onViewKeyUp,
                    scope: this
                }
            };
        },
        /**
         * @private
         */
        onViewKeyUp: function(e) {
            var sel = this.selected;
            // Released the shift key, terminate a keyboard based range selection
            if (e.keyCode === e.SHIFT && sel && sel.isRows && sel.getRangeSize()) {
                // Copy the drag range into the selected records collection
                sel.addRange();
            }
        },
        /**
         * @private
         */
        onStoreChanged: function() {
            var me = this,
                selData = me.selected;
            if (selData) {
                if (selData.isCells) {
                    me.resetSelection();
                } else if (selData.isRows) {
                    if (me.pruneRemoved === false && selData.selectedRecords.length) {
                        me.refresh();
                    } else {
                        me.resetSelection();
                    }
                }
            }
        },
        /**
         * @private
         */
        onColumnsChanged: function() {
            var me = this,
                selectionChanged = me.onViewChanged(me.view, true);
            // This event is fired directly from the HeaderContainer before the view updates.
            // So we have to wait until idle to update the selection UI.
            // NB: fireSelectionChange calls updateSelectionExtender after firing its event.
            Ext.on('idle', selectionChanged ? me.fireSelectionChange : me.updateSelectionExtender, me, {
                single: true
            });
        },
        // The selection may have acquired or lost contiguity, so the replicator may need
        // enabling or disabling
        onColumnMove: function() {
            this.updateSelectionExtender();
        },
        /**
         * @private
         */
        onViewRefresh: function(view) {
            var me = this,
                selectionChanged = me.onViewChanged(view);
            // The selection may have acquired or lost contiguity, so the replicator may need
            // enabling or disabling
            // NB: fireSelectionChange calls updateSelectionExtender after firing its event.
            me[selectionChanged ? 'fireSelectionChange' : 'updateSelectionExtender']();
        },
        /**
         * @private
         */
        resetSelection: function(suppressEvent) {
            var sel = this.selected;
            if (sel) {
                sel.clear();
                if (!suppressEvent) {
                    this.fireSelectionChange();
                }
            }
        },
        /**
         * When the view has changed, whether it be to a refresh or a column change, we need
         * to check the current selection and deselect anything that may no longer be valid.
         * @param {Ext.view.Table} view
         * @param {Boolean} isColumnChange `true` if this change is based on a column change
         * @returns {Boolean} `true` if a change to the selection was made
         * @private
         * @since 6.2.2
         */
        onViewChanged: function(view, isColumnChange) {
            var me = this,
                selData = me.selected,
                store = view.store,
                selectionChanged = false,
                rowRange, colCount, colIdx, rowIdx, context;
            // When columns have changed, we have to deselect *every* cell in the row range
            // because we do not know where the columns have gone to.
            if (selData) {
                view = selData.view;
                if (isColumnChange) {
                    if (selData.isCells) {
                        context = new Ext.grid.CellContext(view);
                        rowRange = selData.getRowRange();
                        colCount = view.ownerGrid.getColumnManager().getColumns().length;
                        if (colCount) {
                            for (rowIdx = rowRange[0]; rowIdx <= rowRange[1]; rowIdx++) {
                                context.setRow(rowIdx);
                                for (colIdx = 0; colIdx < colCount; colIdx++) {
                                    // CellContext only works with visible columns and this index is
                                    // potentially a hidden column. Ensure the column is available
                                    // before deselecting the cell.
                                    context.setColumn(colIdx);
                                    if (context.column) {
                                        view.onCellDeselect(context);
                                    }
                                    // Selection may still reference a hidden column and may need
                                    // to be cleared
                                    if (me.maybeClearSelection(context)) {
                                        selectionChanged = true;
                                    }
                                }
                            }
                        } else {
                            me.clearSelections();
                            selectionChanged = true;
                        }
                    }
                    // We have to deselect columns which have been hidden/removed
                    else if (selData.isColumns) {
                        selectionChanged = false;
                        selData.eachColumn(function(column, columnIdx) {
                            if (!column.isVisible() || !view.ownerGrid.isAncestor(column)) {
                                me.remove(column);
                                if (me.maybeClearSelection({
                                    column: column
                                })) {
                                    selectionChanged = true;
                                }
                            }
                        });
                    }
                }
                // View has refreshed; deselect filtered out records
                else if (selData.isRows && store.isFiltered()) {
                    selData.eachRow(function(rec) {
                        if (!store.contains(rec)) {
                            // Maintainer: `this` is the Rows selection object, *NOT* me.
                            this.remove(rec);
                            if (me.maybeClearSelection({
                                rowIdx: view.indexOf(rec)
                            })) {
                                selectionChanged = true;
                            }
                        }
                    });
                }
            }
            return selectionChanged;
        },
        onViewCreated: function(grid, view) {
            var me = this,
                ownerGrid = view.ownerGrid,
                headerCt = view.headerCt;
            // Only add columns to the locked view, or only view if there is no twin
            if (!ownerGrid.lockable || view.isLockedView) {
                // if there is no row number column and we ask for it, then it should be added here
                if (me.getRowSelect()) {
                    // Ensure we have a rownumber column
                    me.getNumbererColumn();
                }
                if (me.checkboxSelect) {
                    me.addCheckbox(view, true);
                }
                me.mon(view.ownerGrid, 'beforereconfigure', me.onBeforeReconfigure, me);
            }
            // Disable sortOnClick if we're columnSelecting
            headerCt.sortOnClick = !me.getColumnSelect();
            if (me.getDragSelect()) {
                view.on('render', me.onViewRender, me, {
                    single: true
                });
            }
        },
        /**
         * Initialize drag selection support
         * @private
         */
        onViewRender: function(view) {
            var me = this,
                el = view.getEl(),
                views = me.views,
                len = views.length,
                i;
            // If we receive the render event after the columnSelect config has been set,
            // ensure that the view's headerCts know not to sort on click
            // if we're selecting columns.
            for (i = 0; i < len; i++) {
                views[i].headerCt.sortOnClick = !me.columnSelect;
            }
            el.ddScrollConfig = {
                vthresh: 50,
                hthresh: 50,
                frequency: 300,
                increment: 100
            };
            Ext.dd.ScrollManager.register(el);
            // Possible two child views to register as scrollable on drag
            (me.scrollEls || (me.scrollEls = [])).push(el);
            view.on('cellmousedown', me.handleMouseDown, me);
            // In a locking situation, we need a mousedown listener on both sides.
            if (view.lockingPartner) {
                view.lockingPartner.on('cellmousedown', me.handleMouseDown, me);
            }
        },
        /**
         * Plumbing for drag selection of cell range
         * @private
         */
        handleMouseDown: function(view, td, cellIndex, record, tr, rowIdx, e) {
            var me = this,
                sel = me.selected,
                header = e.position.column,
                resumingSelection = false,
                isCheckClick, startDragSelect, containsSelection;
            // Ignore right click and alt modifiers.
            // Also ignore touchstart because e cannot drag select using touches and
            // ignore when actionableMode is true so we can select the text inside an editor
            if (e.button || e.altKey || e.pointerType === 'touch' || !header) {
                return;
            }
            me.mousedownPosition = e.position.clone();
            isCheckClick = header === me.checkColumn;
            if (isCheckClick) {
                me.checkCellClicked = e.position.getCell(true);
            } else if (view.actionableMode) {
                return;
            }
            // Differentiate between row and cell selections.
            if (header === me.numbererColumn || isCheckClick || !me.cellSelect) {
                // Enforce rowSelect setting
                if (me.rowSelect) {
                    if (sel) {
                        containsSelection = sel.contains(record);
                        if (e.shiftKey && containsSelection) {
                            resumingSelection = true;
                        } else if (!e.shiftKey && !e.ctrlKey && !isCheckClick) {
                            sel.clear();
                        }
                    }
                    if (!sel || !sel.isRows) {
                        if (sel) {
                            sel.clear();
                        }
                        sel = me.selected = new Ext.grid.selection.Rows(view);
                    }
                } else if (me.columnSelect) {
                    if (sel) {
                        containsSelection = sel.contains(me.mousedownPosition.column);
                        if (e.shiftKey && containsSelection) {
                            resumingSelection = true;
                        } else if (!e.shiftKey && !e.ctrlKey && !isCheckClick) {
                            sel.clear();
                        }
                    }
                    if (!sel || !sel.isColumns) {
                        if (sel) {
                            sel.clear();
                        }
                        sel = me.selected = new Ext.grid.selection.Columns(view);
                    }
                } else {
                    return false;
                }
            } else {
                if (sel) {
                    containsSelection = sel.contains(me.getCellContext(record, cellIndex));
                    if (e.shiftKey && containsSelection) {
                        resumingSelection = true;
                    } else if (!e.shiftKey) {
                        sel.clear();
                    }
                }
                if (!sel || !sel.isCells) {
                    if (sel) {
                        sel.clear();
                    }
                    sel = me.selected = new Ext.grid.selection.Cells(view);
                }
            }
            startDragSelect = resumingSelection || !e.shiftKey;
            if (!resumingSelection) {
                if (e.shiftKey) {
                    return;
                }
                me.lastOverRecord = me.lastOverColumn = null;
            }
            // Add the listener after the view has potentially been corrected
            me._onMouseUp = Ext.getBody().on('mouseup', me.onMouseUp, me, {
                single: true,
                view: sel.view,
                destroyable: true
            });
            // Only begin the drag process if configured to select what they asked for
            if (startDragSelect) {
                sel.view.el.on('mousemove', me.onMouseMove, me, {
                    view: sel.view
                });
            }
        },
        /**
         * Selects range based on mouse movements
         * @param e
         * @param target
         * @param opts
         * @private
         */
        onMouseMove: function(e, target, opts) {
            var me = this,
                view = opts.view,
                cell = e.getTarget(view.cellSelector),
                header = opts.view.getHeaderByCell(cell),
                selData = me.selected;
            if (view.isLockingView) {
                view = e.within(view.lockedView.el) ? view.lockedView : view.normalView;
            }
            // when the mousedown happens in a checkcolumn, we need to verify is the mouse pointer
            // has moved out of the initial clicked cell.
            // if it has, then we select the initial row and mark it as the range start,
            // otherwise passing the lastOverRecord and return as we don't want
            // to select the record while moving the pointer around the initial cell.
            if (me.checkCellClicked) {
                // We are dragging within the check cell...
                if (cell === me.checkCellClicked) {
                    if (!me.lastOverRecord) {
                        me.lastOverRecord = view.getRecord(cell.parentNode);
                    }
                    return;
                } else {
                    me.checkCellClicked = null;
                    if (me.lastOverRecord) {
                        me.select(me.lastOverRecord);
                        selData.setRangeStart(me.store.indexOf(me.lastOverRecord));
                    }
                }
            }
            me.isDragging = true;
            // Disable until a valid new selection is announced in fireSelectionChange
            if (me.extensible) {
                me.extensible.disable();
            }
            if (header) {
                me.changeSelectionRange(view, cell, header, e);
            } else if (!e.within(view.body.el)) {
                me.scrollTowardsPointer(e, view.ownerGrid.view);
            }
        },
        changeSelectionRange: function(view, cell, header, e) {
            var me = this,
                selData = me.selected,
                record, rowIdx, recChange, colChange, pos;
            me.stopAutoScroller();
            record = view.getRecord(cell.parentNode);
            rowIdx = me.store.indexOf(record);
            recChange = record !== me.lastOverRecord;
            colChange = header !== me.lastOverColumn;
            if (recChange || colChange) {
                pos = me.getCellContext(record, header);
            }
            // Initial mousedown was in rownumberer or checkbox column
            if (selData.isRows) {
                // Only react if we've changed row
                if (recChange) {
                    if (me.lastOverRecord) {
                        selData.setRangeEnd(rowIdx, e.ctrlKey);
                    } else {
                        selData.setRangeStart(rowIdx);
                    }
                }
            }
            // Selecting cells
            else if (selData.isCells) {
                // Only react if we've changed row or column
                if (recChange || colChange) {
                    if (me.lastOverRecord) {
                        selData.setRangeEnd(pos);
                    } else {
                        selData.setRangeStart(pos);
                    }
                }
            }
            // Selecting columns
            else if (selData.isColumns) {
                // Only react if we've changed column
                if (colChange) {
                    if (me.lastOverColumn) {
                        selData.setRangeEnd(pos.column);
                    } else {
                        selData.setRangeStart(pos.column);
                    }
                }
            }
            // Focus MUST follow the mouse.
            // Otherwise the focus may scroll out of the rendered range and revert to document
            if (recChange || colChange) {
                // We MUST pass local view into NavigationModel, not the potentially outermost
                // locking view.
                // TODO: When that's fixed, use setPosition(pos).
                view.getNavigationModel().setPosition(new Ext.grid.CellContext(header.getView()).setPosition(record, header));
            }
            me.lastOverColumn = header;
            me.lastOverRecord = record;
        },
        scrollTowardsPointer: function(e, view) {
            var me = this,
                viewRegion = view.el.getConstrainRegion(),
                point = e.getXY(),
                scrollTask, scrollBy;
            scrollTask = me.scrollTask || (me.scrollTask = Ext.util.TaskManager.newTask({
                run: me.doAutoScroll,
                args: [
                    e,
                    view
                ],
                scope: me,
                interval: 10
            }));
            scrollBy = me.scrollBy || (me.scrollBy = []);
            // Neart bottom of view
            if (point[1] > viewRegion.bottom) {
                scrollBy[0] = 0;
                scrollBy[1] = 3;
                scrollTask.start();
            } else if (point[1] < viewRegion.top) {
                scrollBy[0] = 0;
                scrollBy[1] = -3;
                scrollTask.start();
            }
            // Near right edge of view
            else if (point[0] > viewRegion.right) {
                scrollBy[0] = 3;
                scrollBy[1] = 0;
                scrollTask.start();
            } else if (point[0] < viewRegion.left) {
                scrollBy[0] = -3;
                scrollBy[1] = 0;
                scrollTask.start();
            }
        },
        doAutoScroll: function(e, view) {
            var me = this,
                viewRegion = view.el.getConstrainRegion(),
                xy = [],
                cell, record, header;
            if (me.destroyed) {
                return;
            }
            // Bump the view in whatever direction was decided in the onDrag method.
            if (view.scrollBy) {
                view.scrollBy.apply(view, me.scrollBy);
            }
            if (me.scrollBy[0]) {
                xy[0] = me.scrollBy[0] > 0 ? viewRegion.right - 5 : viewRegion.left + 5;
            } else {
                xy[0] = e.getX();
            }
            if (me.scrollBy[1]) {
                xy[1] = me.scrollBy[1] > 0 ? viewRegion.bottom - 5 : viewRegion.top + 5;
            } else {
                xy[1] = e.getY();
            }
            cell = document.elementFromPoint.apply(document, xy);
            if (cell) {
                cell = Ext.fly(cell).up(view.cellSelector);
                if (!cell) {
                    me.stopAutoScroller();
                    return;
                }
                record = view.getRecord(cell.dom.parentNode);
                header = view.getHeaderByCell(cell.dom);
                if (cell && (record !== me.lastOverRecord || header !== me.lastOverColumn)) {
                    me.changeSelectionRange(view, cell.dom, header, e);
                }
            }
        },
        stopAutoScroller: function() {
            var me = this;
            if (me.scrollTask) {
                me.scrollBy[0] = me.scrollBy[1] = 0;
                me.scrollTask.stop();
                me.scrollTask = null;
            }
        },
        /**
         * Clean up mousemove event
         * @param e
         * @param target
         * @param opts
         * @private
         */
        onMouseUp: function(e, target, opts) {
            var me = this,
                view = opts.view,
                lastPos = me.lastOverRecord && new Ext.grid.CellContext(view).setPosition(me.lastOverRecord, me.lastOverColumn),
                changedCell = lastPos && !lastPos.isEqual(me.mousedownPosition),
                cell, record;
            me.checkCellClicked = null;
            me.stopAutoScroller();
            if (view && !view.destroyed) {
                // If we catch the event before the View sees it and stamps a position in,
                // we need to know where they mouseupped.
                if (!e.position) {
                    cell = e.getTarget(view.cellSelector);
                    if (cell) {
                        record = view.getRecord(cell);
                        if (record) {
                            e.position = new Ext.grid.CellContext(view).setPosition(record, view.getHeaderByCell(cell));
                        }
                    }
                }
                if (e.position) {
                    changedCell = !e.position.isEqual(me.mousedownPosition);
                }
                // Disable until a valid new selection is announced in fireSelectionChange
                // unless it's a click
                if (me.extensible && changedCell) {
                    me.extensible.disable();
                }
                view.el.un('mousemove', me.onMouseMove, me);
                // Copy the records encompassed by the drag range into the record collection
                // if we are not dragging, the range will be added by onNavigate
                if (me.selected.isRows && me.isDragging) {
                    me.selected.addRange();
                }
                // Fire selection change only if we have dragged - if the mouseup position
                // is different from the mousedown position.
                // If there has been no drag, the click handler will select the single row
                if (changedCell) {
                    me.fireSelectionChange();
                }
            }
            me.isDragging = false;
        },
        /**
         * Add the header checkbox to the header row
         * @param view
         * @param {Boolean} initial True if we're binding for the first time.
         * @private
         */
        addCheckbox: function(view, initial) {
            var me = this,
                checkbox = me.checkboxColumnIndex,
                headerCt = view.headerCt;
            // Preserve behaviour of false, but not clear why that would ever be done.
            if (checkbox !== false) {
                if (checkbox === 'first') {
                    checkbox = 0;
                } else if (checkbox === 'last') {
                    checkbox = headerCt.getColumnCount();
                }
                me.checkColumn = headerCt.add(checkbox, me.getCheckboxHeaderConfig());
            }
            if (initial !== true) {
                view.refresh();
            }
        },
        /**
         * Called when the grid's Navigation model detects navigation events (`mousedown`,
         * `click` and certain `keydown` events).
         * @param {Ext.event.Event} navigateEvent The event which caused navigation.
         * @private
         */
        onNavigate: function(navigateEvent) {
            var me = this,
                // Use outermost view. May be lockable
                view = navigateEvent.view && navigateEvent.view.ownerGrid.view,
                record = navigateEvent.record,
                sel = me.selected,
                // Create a new Context based upon the outermost View.
                // NavigationModel works on local views.
                // TODO: remove this step when NavModel is fixed to use outermost view
                // in locked grid. At that point, we can use navigateEvent.position
                pos = view && new Ext.grid.CellContext(view).setPosition(record, navigateEvent.column),
                keyEvent = navigateEvent.keyEvent,
                ctrlKey = keyEvent.ctrlKey,
                shiftKey = keyEvent.shiftKey,
                keyCode = keyEvent.getKey(),
                selectionChanged, rowRangeStart, lastRecord;
            // if there's no position then the user might have clicked outside a cell
            if (!pos) {
                return;
            }
            // A Column's processEvent method may set this flag if configured to do so.
            if (keyEvent.stopSelection) {
                return;
            }
            // CTRL/Arrow just navigates, does not select
            if (ctrlKey && (keyCode === keyEvent.UP || keyCode === keyEvent.LEFT || keyCode === keyEvent.RIGHT || keyCode === keyEvent.DOWN)) {
                return;
            }
            // Click is the mouseup at the end of a multi-cell/multi-column select swipe; reject.
            if (sel && (sel.isCells || (sel.isColumns && !me.getRowSelect() && !ctrlKey)) && sel.getCount() > 1) {
                if (shiftKey && keyEvent.type === 'click' && !keyEvent.position.isEqual(me.mousedownPosition)) {
                    return;
                }
            }
            // If all selection types are disabled, or it's not a selecting event, return
            if (!(me.cellSelect || me.columnSelect || me.rowSelect) || !navigateEvent.record || keyEvent.type === 'mousedown') {
                return;
            }
            // Ctrl/A key - Deselect current selection, or select all if no selection
            if (ctrlKey && keyEvent.keyCode === keyEvent.A) {
                // No selection, or only one, select all
                if (!sel || sel.getCount() < 2) {
                    me.selectAll();
                } else {
                    me.deselectAll();
                }
                me.updateHeaderState();
                return;
            }
            if (shiftKey) {
                // If the event is in one of the row selecting cells,
                // or cell selecting is turned off
                if (pos.column === me.numbererColumn || pos.column === me.checkColumn || !(me.cellSelect || me.columnSelect) || (sel && sel.isRows)) {
                    if (me.rowSelect) {
                        // Ensure selection object is of the correct type
                        if (!sel || !sel.isRows || sel.view !== view) {
                            me.resetSelection(true);
                            sel = me.selected = new Ext.grid.selection.Rows(view);
                        }
                        // First shift
                        if (!sel.getRangeSize()) {
                            rowRangeStart = navigateEvent.previousRecordIndex;
                            if (rowRangeStart == null) {
                                // previousRecordIndex could be empty due to BufferedRenderer
                                // de-rendering the last selected row.
                                // In that case we need to select the last selected record
                                // or start from 0.
                                lastRecord = me.getLastSelected();
                                rowRangeStart = lastRecord ? me.store.indexOf(lastRecord) : 0;
                            }
                            sel.setRangeStart(rowRangeStart);
                        }
                        sel.setRangeEnd(navigateEvent.recordIndex);
                        sel.addRange();
                        selectionChanged = true;
                    }
                } else // Navigate event in a normal cell
                {
                    if (me.cellSelect) {
                        // Ensure selection object is of the correct type
                        if (!sel || !sel.isCells || sel.view !== view) {
                            me.resetSelection(true);
                            sel = me.selected = new Ext.grid.selection.Cells(view);
                        }
                        // First shift
                        if (!sel.getRangeSize()) {
                            sel.setRangeStart(navigateEvent.previousPosition || me.getCellContext(0, 0));
                        }
                        sel.setRangeEnd(pos);
                        selectionChanged = true;
                    } else if (me.columnSelect) {
                        // Ensure selection object is of the correct type
                        if (!sel || !sel.isColumns || sel.view !== view) {
                            me.resetSelection(true);
                            sel = me.selected = new Ext.grid.selection.Columns(view);
                        }
                        if (!sel.getCount()) {
                            sel.setRangeStart(pos.column);
                        }
                        sel.setRangeEnd(navigateEvent.position.column);
                        selectionChanged = true;
                    }
                }
            } else {
                // If the event is in one of the row selecting cells, or we have enabled
                // row selection but not column selection so prioritize selecting rows
                if (pos.column === me.numbererColumn || pos.column === me.checkColumn || (me.rowSelect && !me.cellSelect)) {
                    // Ensure selection object is of the correct type
                    if (!sel || !sel.isRows || sel.view !== view) {
                        me.resetSelection(true);
                        sel = me.selected = new Ext.grid.selection.Rows(view);
                    }
                    if (ctrlKey || pos.column === me.checkColumn) {
                        if (sel.contains(record)) {
                            sel.remove(record);
                        } else {
                            sel.add(record);
                        }
                    } else {
                        sel.clear();
                        sel.add(record);
                        sel.setRangeStart(pos.rowIdx, true);
                    }
                    selectionChanged = true;
                }
                // Navigate event in a normal cell
                else if (keyEvent.getTarget(me.view.getCellSelector())) {
                    // Prioritize cell selection over column selection, also we have to make sure
                    // we only handle events that were fired by a cellClick.
                    // If an itemclick (row selection) was fired due to dragging,
                    // it will be handled by the selection#setRangeEnd method.
                    if (me.cellSelect) {
                        // Ensure selection object is of the correct type
                        if (!sel || !sel.isCells || sel.view !== view) {
                            me.resetSelection(true);
                            me.selected = sel = new Ext.grid.selection.Cells(view);
                        } else {
                            sel.clear();
                        }
                        sel.setRangeStart(pos);
                        selectionChanged = true;
                    } else if (me.columnSelect) {
                        // Ensure selection object is of the correct type
                        if (!sel || !sel.isColumns || sel.view !== view) {
                            me.resetSelection(true);
                            me.selected = sel = new Ext.grid.selection.Columns(view);
                        }
                        if (ctrlKey) {
                            if (sel.contains(pos.column)) {
                                sel.remove(pos.column);
                            } else {
                                sel.add(pos.column);
                            }
                        } else {
                            sel.setRangeStart(pos.column);
                        }
                        selectionChanged = true;
                    }
                }
            }
            // If our configuration allowed selection changes, update check header and fire event
            if (selectionChanged) {
                if (sel.isRows) {
                    me.updateHeaderState();
                }
                // this will give continuity between keyboard selection and mouse selection
                me.lastOverRecord = record;
                me.lastOverColumn = pos.column;
                me.fireSelectionChange();
            }
        },
        /**
         * Checks the current selection (if available) against the context being removed.
         * If the context was selected, the selection is cleared since it's no longer valid.
         * @param {Object} removedContext
         * @return {Boolean} `true` if part or all of the selection was cleared
         * @since 6.2.2
         */
        maybeClearSelection: function(removedContext) {
            var me = this,
                selData = me.selected,
                startCell = selData.startCell,
                endCell = selData.endCell,
                column = removedContext.column,
                colIdx = removedContext.colIdx,
                rowIdx = removedContext.rowIdx,
                changed;
            if (startCell && (startCell.column === column || startCell.colIdx === colIdx) && startCell.rowIdx === rowIdx) {
                selData.startCell = changed = null;
            }
            if (endCell && (endCell.column === column || endCell.colIdx === colIdx) && endCell.rowIdx === rowIdx) {
                selData.endCell = changed = null;
            }
            return changed === null;
        },
        /**
         * Check if given record is currently selected.
         *
         * Used in {@link Ext.view.Table view} rendering to decide upon cell UI treatment.
         * @param {Ext.data.Model} record
         * @return {Boolean}
         * @private
         */
        isRowSelected: function(record) {
            var me = this,
                sel = me.selected;
            if (sel && sel.isRows) {
                record = Ext.isNumber(record) ? me.store.getAt(record) : record;
                return sel.contains(record);
            } else {
                return false;
            }
        },
        /**
         * Check if given column is currently selected.
         *
         * @param {Ext.grid.column.Column} column
         * @return {Boolean}
         * @private
         */
        isColumnSelected: function(column) {
            var me = this,
                sel = me.selected;
            if (sel && sel.isColumns) {
                return sel.contains(column);
            } else {
                return false;
            }
        },
        /**
         * Returns true if specified cell within specified view is selected
         *
         * Used in {@link Ext.view.Table view} rendering to decide upon row UI treatment.
         * @param {Ext.grid.View} view - impactful when locked columns are used
         * @param {Number} row - row index
         * @param {Number} column - column index, within the current view
         *
         * @return {Boolean}
         * @private
         */
        isCellSelected: function(view, row, column) {
            var me = this,
                testPos,
                sel = me.selected;
            // view MUST be outermost (possible locking) view
            view = view.ownerGrid.view;
            if (sel) {
                if (sel.isColumns) {
                    if (typeof column === 'number') {
                        column = view.getVisibleColumnManager().getColumns()[column];
                    }
                    return sel.contains(column);
                }
                if (sel.isCells) {
                    testPos = new Ext.grid.CellContext(view).setPosition({
                        row: row,
                        // IMPORTANT: The historic API for columns has been to include
                        // hidden columns in the index.
                        // So we must index into the "all" ColumnManager.
                        column: column
                    });
                    return sel.contains(testPos);
                }
            }
            return false;
        },
        /**
         * @private
         */
        applySelected: function(selected) {
            // Must override base class's applier which creates a Collection
            if (selected && !(selected.isRows || selected.isCells || selected.isColumns)) {
                Ext.raise('SpreadsheelModel#setSelected must be passed an instance ' + 'of Ext.grid.selection.Selection');
            }
            return selected;
        },
        /**
         * @private
         */
        updateSelected: function(selected, oldSelected) {
            var view, columns, len, i, cell;
            // Clear old selection.
            if (oldSelected) {
                oldSelected.clear();
            }
            // Update the UI to match the new selection
            if (selected && selected.getCount()) {
                view = selected.view;
                // Rows; update each selected row
                if (selected.isRows) {
                    selected.eachRow(view.onRowSelect, view);
                }
                // Columns; update the selected columns for all rows
                else if (selected.isColumns) {
                    columns = selected.getColumns();
                    len = columns.length;
                    if (len) {
                        cell = new Ext.grid.CelContext(view);
                        view.store.each(function(rec) {
                            cell.setRow(rec);
                            for (i = 0; i < len; i++) {
                                cell.setColumn(columns[i]);
                                view.onCellSelect(cell);
                            }
                        });
                    }
                }
                // Cells; update each selected cell
                else if (selected.isCells) {
                    selected.eachCell(view.onCellSelect, view);
                }
            }
        },
        getNumbererColumn: function(col) {
            var me = this,
                result = me.numbererColumn,
                view = me.view;
            if (!result) {
                // Always put row selection columns in the locked side if there is one.
                if (view.isNormalView) {
                    view = view.ownerGrid.lockedGrid;
                }
                result = me.numbererColumn = view.headerCt.down('rownumberer') || view.headerCt.add(0, me.getNumbererColumnConfig());
            }
            return result;
        },
        /**
         * Show/hide the extra column headers depending upon rowSelection.
         * @private
         */
        updateRowSelect: function(rowSelect) {
            var me = this,
                sel = me.selected,
                view = me.view;
            if (view && view.rendered) {
                if (rowSelect) {
                    if (me.checkColumn) {
                        me.checkColumn.show();
                    }
                    me.getNumbererColumn().show();
                } else {
                    if (me.checkColumn) {
                        me.checkColumn.hide();
                    }
                    if (me.numbererColumn) {
                        me.numbererColumn.hide();
                    }
                }
                if (!rowSelect && sel && sel.isRows) {
                    sel.clear();
                    me.fireSelectionChange();
                }
            }
        },
        /**
         * Enable/disable the HeaderContainer's sortOnClick in line with column select on
         * column click.
         * @private
         */
        updateColumnSelect: function(columnSelect) {
            var me = this,
                sel = me.selected,
                views = me.views,
                len = views ? views.length : 0,
                i;
            for (i = 0; i < len; i++) {
                views[i].headerCt.sortOnClick = !columnSelect;
            }
            if (!columnSelect && sel && sel.isColumns) {
                sel.clear();
                me.fireSelectionChange();
            }
            if (columnSelect) {
                me.view.ownerGrid.addCls(me.columnSelectCls);
            } else {
                me.view.ownerGrid.removeCls(me.columnSelectCls);
            }
        },
        /**
         * @private
         */
        updateCellSelect: function(cellSelect) {
            var me = this,
                sel = me.selected;
            if (!cellSelect && sel && sel.isCells) {
                sel.clear();
                me.fireSelectionChange();
            }
        },
        /**
         * @private
         */
        fireSelectionChange: function() {
            var me = this,
                sel = me.selected,
                view = sel.view,
                grid = view.ownerGrid,
                store = view.dataSource,
                records, count;
            // Inform selection object that we're done
            me.updateSelectionExtender();
            // We must still fire a selectionchange event through the SelectionModel
            // because Ext.panel.Table listens for this event to update its bound selection.
            if (sel.isRows) {
                records = sel.getRecords();
                count = store.getTotalCount() || store.getCount();
                // When there is a BufferedStore the allSelected flag cannot be set
                // in a manual selection
                // eslint-disable-next-line max-len 
                me.selected.allSelected = !!(store.isBufferedStore ? me.selected.allSelected : count && records.length && (count === records.length));
                me.fireEvent('selectionchange', me, records);
            } else if (sel.isCells) {
                me.selected.allSelected = false;
                // eslint-disable-next-line max-len
                me.fireEvent('selectionchange', me, sel.getCount() ? me.store.getRange.apply(sel.view.dataSource, sel.getRowRange()) : []);
            }
            grid.fireEvent('selectionchange', grid, sel);
        },
        /**
         * @private
         * Called by {@link Ext.panel.Table#updateBindSelection} when publishing the `selection`
         * property. It should yield the last record selected.
         * @return {Ext.data.Model} The last record selected. This is only available
         * if the current selection type is cells or rows.
         * In the case of multiple selection, the *last* record added to the selection is returned.
         */
        getLastSelected: function() {
            var sel = this.selected;
            if (sel.getLastSelected) {
                return sel.getLastSelected();
            }
        },
        updateSelectionExtender: function() {
            var sel = this.selected;
            if (sel) {
                sel.onSelectionFinish();
            }
        },
        /**
         * Called when a selection has been made. The selection object's onSelectionFinish
         * calls back into this.
         * @param {Ext.grid.selection.Selection} sel The selection object specific to 
         * the selection performed.
         * @param {Ext.grid.CellContext} [firstCell] The left/top most selected cell.
         * Will be undefined if the selection is clear.
         * @param {Ext.grid.CellContext} [lastCell] The bottom/right most selected cell.
         * Will be undefined if the selection is clear.
         * @private
         */
        onSelectionFinish: function(sel, firstCell, lastCell) {
            var extensible = this.getExtensible();
            if (extensible) {
                extensible.setHandle(firstCell, lastCell);
            }
        },
        applyExtensible: function(extensible) {
            var me = this;
            // if extensible is false/null we should return undefined so the value
            // does not get set and we don't call updateExtensible
            if (!extensible) {
                return undefined;
            }
            if (extensible === true || typeof extensible === 'string') {
                extensible = {
                    axes: me.axesConfigs[extensible]
                };
            } else {
                extensible = Ext.Object.chain(extensible);
            }
            // don't mutate the user's config
            extensible.allowReduceSelection = me.getReducible();
            extensible.view = me.selected.view;
            return new Ext.grid.selection.SelectionExtender(extensible);
        },
        /*
         * @private
         */
        applyReducible: function(reducible) {
            return !!reducible;
        },
        updateReducible: function(reducible) {
            // do not call getExtensible() here to avoid creation
            var extensible = this.extensible;
            if (extensible) {
                extensible.allowReduceSelection = reducible;
            }
        },
        /**
         * Called when the SelectionExtender has the mouse released.
         * @param {Object} extension An object describing the type and size of extension.
         * @param {String} extension.type `"rows"` or `"columns"`
         * @param {Ext.grid.CellContext} extension.start The start (top left) cell of the
         * extension area.
         * @param {Ext.grid.CellContext} extension.end The end (bottom right) cell of the
         * extension area.
         * @param {number} [extension.columns] The number of columns extended (-ve means
         * on the left side).
         * @param {number} [extension.rows] The number of rows extended (-ve means on the top side).
         * @private
         */
        extendSelection: function(extension) {
            var me = this,
                sel = me.selected,
                action = extension.reduce ? 'reduce' : 'extend';
            // Announce that the selection is to be extended, and if no objections, extend it
            // eslint-disable-next-line max-len
            if (me.view.ownerGrid.fireEvent('beforeselectionextend', me.view.ownerGrid, sel, extension) !== false) {
                sel[action + 'Range'](extension);
                me.fireSelectionChange();
            }
        },
        /**
         * @private
         */
        onIdChanged: function(store, rec, oldId, newId) {
            var sel = this.selected;
            if (sel && sel.isRows && sel.selectedRecords) {
                sel.selectedRecords.updateKey(rec, oldId);
            }
        },
        /**
         * Called when a page is added to BufferedStore.
         * @private
         */
        onPageAdd: function(pageMap, pageNumber, records) {
            var sel = this.selected,
                len = records.length,
                i, record,
                selected = sel && sel.selectedRecords;
            // Check for return of already selected records.
            // Maintainer: To only use one conditional expression, the value of assignment of
            // (selected = sel.selectedRecords) is part of the single conditional expression.
            if (selected && sel.isRows) {
                for (i = 0; i < len; i++) {
                    record = records[i];
                    if (selected.get(record.id)) {
                        selected.replace(record);
                    } else if (sel.allSelected) {
                        selected.add(record);
                    }
                }
            }
        },
        /**
         * @private
         */
        refresh: function() {
            var sel = this.getSelected();
            // Refreshing the selected record Collection based upon a possible
            // store mutation is only valid if we are selecting records.
            if (sel && sel.isRows) {
                this.callParent();
            }
        },
        /**
         * @private
         */
        onStoreAdd: function() {
            var sel = this.getSelected();
            // Updating on store mutation is only valid if we are selecting records.
            if (sel && sel.isRows) {
                this.callParent(arguments);
                this.updateHeaderState();
            }
        },
        /**
         * @private
         */
        onStoreClear: function() {
            this.resetSelection();
        },
        /**
         * @private
         */
        onStoreLoad: function() {
            var sel = this.getSelected();
            // Updating on store mutation is only valid if we are selecting records.
            if (sel && sel.isRows) {
                this.callParent(arguments);
                this.updateHeaderState();
            }
        },
        /**
         * @private
         */
        onStoreRefresh: function() {
            var sel = this.selected;
            // Ensure that records which are no longer in the new store are pruned
            // if configured to do so.
            // Ensure that selected records in the collection are the correct instance.
            if (sel && sel.isRows && sel.selectedRecords) {
                this.updateSelectedInstances(sel.selectedRecords);
            }
            if (this.view) {
                this.updateHeaderState();
            }
        },
        /**
         * @private
         */
        onPageRemove: function(pageMap, pageNumber, records) {
            var sel = this.selected;
            // On page purge from a buffered store, do not react if
            // we have selected all. All are still selected!
            if (!(sel && sel.allSelected)) {
                this.onStoreRemove(this.store, records);
            }
        },
        /**
         * @private
         */
        onStoreRemove: function() {
            var sel = this.getSelected();
            // Updating on store mutation is only valid if we are selecting records.
            if (sel && sel.isRows) {
                this.callParent(arguments);
            }
        }
    }
}, function(SpreadsheetModel) {
    var RowNumberer = Ext.ClassManager.get('Ext.grid.column.RowNumberer');
    if (RowNumberer) {
        SpreadsheetModel.prototype.rowNumbererTdCls = Ext.grid.column.RowNumberer.prototype.tdCls + ' ' + Ext.baseCSSPrefix + 'ssm-row-numberer-cell';
    }
});

/**
 * An internal Queue class.
 * @private
 */
Ext.define('Ext.util.Queue', {
    constructor: function() {
        this.clear();
    },
    add: function(obj, replace) {
        var me = this,
            key = me.getKey(obj),
            prevEntry;
        if (!(prevEntry = me.map[key])) {
            ++me.length;
            me.items.push(obj);
            me.map[key] = obj;
        } else if (replace) {
            me.map[key] = obj;
            me.items[Ext.Array.indexOf(me.items, prevEntry)] = obj;
        }
        return obj;
    },
    /**
     * Removes all items from the collection.
     */
    clear: function() {
        var me = this,
            items = me.items;
        me.items = [];
        me.map = {};
        me.length = 0;
        return items;
    },
    contains: function(obj) {
        var key = this.getKey(obj);
        return this.map.hasOwnProperty(key);
    },
    /**
     * Returns the number of items in the collection.
     * @return {Number} the number of items in the collection.
     */
    getCount: function() {
        return this.length;
    },
    getKey: function(obj) {
        return obj.id;
    },
    /**
     * Remove an item from the collection.
     * @param {Object} obj The item to remove.
     * @return {Object} The item removed or false if no item was removed.
     */
    remove: function(obj) {
        var me = this,
            key = me.getKey(obj),
            items = me.items,
            index;
        if (me.map[key]) {
            index = Ext.Array.indexOf(items, obj);
            Ext.Array.erase(items, index, 1);
            delete me.map[key];
            --me.length;
        }
        return obj;
    }
});

/**
 * This class manages state information for a component or element during a layout.
 * 
 * # Blocks
 *
 * A "block" is a required value that is preventing further calculation. When a layout has
 * encountered a situation where it cannot possibly calculate results, it can associate
 * itself with the context item and missing property so that it will not be rescheduled
 * until that property is set.
 * 
 * Blocks are a one-shot registration. Once the property changes, the block is removed.
 * 
 * Be careful with blocks. If *any* further calculations can be made, a block is not the
 * right choice.
 * 
 * # Triggers
 *
 * Whenever any call to {@link #getProp}, {@link #getDomProp}, {@link #hasProp} or
 * {@link #hasDomProp} is made, the current layout is automatically registered as being
 * dependent on that property in the appropriate state. Any changes to the property will
 * trigger the layout and it will be queued in the {@link Ext.layout.Context}.
 *
 * Triggers, once added, remain for the entire layout. Any changes to the property will
 * reschedule all unfinished layouts in their trigger set.
 *
 * @private
 */
Ext.define('Ext.layout.ContextItem', {
    heightModel: null,
    widthModel: null,
    sizeModel: null,
    /**
     * There are several cases that allow us to skip (opt out) of laying out a component
     * and its children as long as its `lastBox` is not marked as `invalid`. If anything
     * happens to change things, the `lastBox` is marked as `invalid` by `updateLayout`
     * as it ascends the component hierarchy.
     * 
     * @property {Boolean} optOut
     * @private
     * @readonly
     */
    optOut: false,
    ownerSizePolicy: null,
    // plaed here by Component.getSizeModel
    boxChildren: null,
    boxParent: null,
    children: [],
    dirty: null,
    // The number of dirty properties
    dirtyCount: 0,
    hasRawContent: true,
    isContextItem: true,
    isTopLevel: false,
    consumersContentHeight: 0,
    consumersContentWidth: 0,
    consumersContainerHeight: 0,
    consumersContainerWidth: 0,
    consumersHeight: 0,
    consumersWidth: 0,
    ownerCtContext: null,
    remainingChildDimensions: 0,
    // the current set of property values:
    props: null,
    /**
     * @property {Object} state
     * State variables that are cleared when invalidated. Only applies to component items.
     */
    state: null,
    /**
     * @property {Boolean} wrapsComponent
     * True if this item wraps a Component (rather than an Element).
     * @readonly
     */
    wrapsComponent: false,
    constructor: function(config) {
        var me = this,
            sizeModels = Ext.layout.SizeModel.sizeModels,
            configured = sizeModels.configured,
            shrinkWrap = sizeModels.shrinkWrap,
            el, lastBox, ownerCt, ownerCtContext, props, sizeModel, target, lastWidth, lastHeight, sameWidth, sameHeight, widthModel, heightModel, optOut;
        Ext.apply(me, config);
        target = me.target;
        el = me.el;
        me.id = target.id;
        // These hold collections of layouts that are either blocked or triggered by sets
        // to our properties (either ASAP or after flushing to the DOM). All of them have
        // the same structure:
        //
        //      me.blocks = {
        //          width: {
        //              'layout-1001': layout1001
        //          }
        //      }
        //
        // The property name is the primary key which yields an object keyed by layout id
        // with the layout instance as the value. This prevents duplicate entries for one
        // layout and gives O(1) access to the layout instance when we need to iterate and
        // process them.
        // 
        // me.blocks = {};
        // me.domBlocks = {};
        // me.domTriggers = {};
        // me.triggers = {};
        me.flushedProps = {};
        me.props = props = {};
        // the set of cached styles for the element:
        me.styles = {};
        if (!target.isComponent) {
            lastBox = el.lastBox;
        } else {
            me.wrapsComponent = true;
            me.framing = target.frameSize || null;
            me.isComponentChild = target.ownerLayout && target.ownerLayout.isComponentLayout;
            lastBox = target.lastBox;
            // These items are created top-down, so the ContextItem of our ownerCt should
            // be available (if it is part of this layout run).
            ownerCt = target.ownerCt;
            if (ownerCt && (ownerCtContext = ownerCt.el && me.context.items[ownerCt.el.id])) {
                me.ownerCtContext = ownerCtContext;
            }
            // If our ownerCtContext is in the run, it will have a SizeModel that we use to
            // optimize the determination of our sizeModel. Also see recalculateSizeModel, similar
            // logic exists there.
            me.sizeModel = sizeModel = target.getSizeModel(ownerCtContext && ownerCtContext.widthModel.pairsByHeightOrdinal[ownerCtContext.heightModel.ordinal]);
            // NOTE: The initial determination of sizeModel is valid (thankfully) and is
            // needed to cope with adding components to a layout run on-the-fly (e.g., in
            // the menu overflow handler of a box layout). Since this is the case, we do
            // not need to recompute the sizeModel in init unless it is a "full" init (as
            // our ownerCt's sizeModel could have changed in that case).
            me.widthModel = widthModel = sizeModel.width;
            me.heightModel = heightModel = sizeModel.height;
            // The lastBox is populated early but does not get an "invalid" property
            // until layout has occurred. The "false" value is placed in the lastBox
            // by Component.finishedLayout.
            if (lastBox && lastBox.invalid === false) {
                sameWidth = (target.width === (lastWidth = lastBox.width));
                sameHeight = (target.height === (lastHeight = lastBox.height));
                if (widthModel === shrinkWrap && heightModel === shrinkWrap) {
                    optOut = true;
                } else if (widthModel === configured && sameWidth) {
                    optOut = heightModel === shrinkWrap || (heightModel === configured && sameHeight);
                }
                if (optOut) {
                    // Flag this component and capture its last size...
                    me.optOut = true;
                    props.width = lastWidth;
                    props.height = lastHeight;
                }
            }
        }
        me.lastBox = lastBox;
    },
    /**
     * Clears all properties on this object except (perhaps) those not calculated by this
     * component. This is more complex than it would seem because a layout can decide to
     * invalidate its results and run the component's layouts again, but since some of the
     * values may be calculated by the container, care must be taken to preserve those
     * values.
     *
     * @param {Boolean} full True if all properties are to be invalidated, false to keep
     * those calculated by the ownerCt.
     * @param {Object} options
     * @return {Mixed} A value to pass as the first argument to {@link #initContinue}.
     * @private
     */
    init: function(full, options) {
        var me = this,
            oldProps = me.props,
            oldDirty = me.dirty,
            ownerCtContext = me.ownerCtContext,
            ownerLayout = me.target.ownerLayout,
            firstTime = !me.state,
            ret = full || firstTime,
            children, i, n, ownerCt, sizeModel, target,
            oldHeightModel = me.heightModel,
            oldWidthModel = me.widthModel,
            newHeightModel, newWidthModel,
            remainingCount = 0;
        me.dirty = me.invalid = false;
        me.props = {};
        // Reset the number of child dimensions since the children will add their part:
        me.remainingChildDimensions = 0;
        if (me.boxChildren) {
            me.boxChildren.length = 0;
        }
        // keep array (more GC friendly)
        if (!firstTime) {
            me.clearAllBlocks('blocks');
            me.clearAllBlocks('domBlocks');
        }
        // For Element wrappers, we are done...
        if (!me.wrapsComponent) {
            return ret;
        }
        // From here on, we are only concerned with Component wrappers...
        target = me.target;
        me.state = {};
        // only Component wrappers need a "state"
        if (firstTime) {
            // This must occur before we proceed since it can do many things (like add
            // child items perhaps):
            if (target.beforeLayout && target.beforeLayout !== Ext.emptyFn) {
                target.beforeLayout();
            }
            // Determine the ownerCtContext if we aren't given one. Normally the firstTime
            // we meet a component is before the context is run, but it is possible for
            // components to be added to a run that is already in progress. If so, we have
            // to lookup the ownerCtContext since the odds are very high that the new
            // component is a child of something already in the run. It is currently
            // unsupported to drag in the owner of a running component (needs testing).
            if (!ownerCtContext && (ownerCt = target.ownerCt)) {
                ownerCtContext = me.context.items[ownerCt.el.id];
            }
            if (ownerCtContext) {
                me.ownerCtContext = ownerCtContext;
                me.isBoxParent = ownerLayout && ownerLayout.isItemBoxParent(me);
            } else {
                me.isTopLevel = true;
            }
            // this is used by initAnimation...
            me.frameBodyContext = me.getEl('frameBody');
        } else {
            ownerCtContext = me.ownerCtContext;
            // In theory (though untested), this flag can change on-the-fly...
            me.isTopLevel = !ownerCtContext;
            // Init the children element items since they may have dirty state (no need to
            // do this the firstTime).
            children = me.children;
            for (i = 0 , n = children.length; i < n; ++i) {
                children[i].init(true);
            }
        }
        // We need to know how we will determine content size: containers can look at the
        // results of their items but non-containers or item-less containers with just raw
        // markup need to be measured in the DOM:
        me.hasRawContent = !(target.isContainer && target.items.items.length > 0);
        if (full) {
            // We must null these out or getSizeModel will assume they are the correct,
            // dynamic size model and return them (the previous dynamic sizeModel).
            me.widthModel = me.heightModel = null;
            sizeModel = target.getSizeModel(ownerCtContext && ownerCtContext.widthModel.pairsByHeightOrdinal[ownerCtContext.heightModel.ordinal]);
            if (firstTime) {
                me.sizeModel = sizeModel;
            }
            me.widthModel = sizeModel.width;
            me.heightModel = sizeModel.height;
            // if we are a container child (e.g., not a docked item), and this is a full
            // init, that means our parent was invalidated, and therefore we must initialize
            // our remainingChildDimensions to ensure that containerChildrenSizeDone
            // gets set properly once all dimensions have had their sizes determined.
            // There are 3 possible scenarios here:
            //
            // 1. Layouts that both read and set sizes of their items (e.g. box).  These
            // layouts must always add both dimensions to remainingChildDimensions.
            //
            // 2. Layouts that neither read nor set the size of their items (e.g.
            // autocontainer, form).  These layouts will not create context items for their
            // children, and so we will never end up here.
            //
            // 3. Layouts that may set the size of their items, but will never read them
            // because they measure an outer containing element in the shrink-wrapping
            // dimension(s) (e.g. anchor, column).  There are 2 possible outcomes:
            //   a. The child item uses liquid CSS layout.  In this case, the only dimensions
            //   that affect containerChildrenSizeDone are the dimensions that the owner
            //   layout is responsible for calculating, and so these are the dimensions
            //   that are added to remainingChildDimensions. Non-calculated dimensions will
            //   never be published because the child's component layout does not run.
            //
            //   b. The child item does not use liquid CSS layout.  In this case, the
            //   component layout will run like normal, and any non-calculated dimensions
            //   will be published, therefore, we need to add both dimensions to
            //   remainingChildDimensions
            if (ownerCtContext && !me.isComponentChild) {
                if (ownerLayout.needsItemSize || !target.liquidLayout) {
                    ownerCtContext.remainingChildDimensions += 2;
                } else {
                    if (me.widthModel.calculated) {
                        ++ownerCtContext.remainingChildDimensions;
                    }
                    if (me.heightModel.calculated) {
                        ++ownerCtContext.remainingChildDimensions;
                    }
                }
            }
        } else if (oldProps) {
            // these are almost always calculated by the ownerCt (we might need to track
            // this at some point more carefully):
            me.recoverProp('x', oldProps, oldDirty);
            me.recoverProp('y', oldProps, oldDirty);
            // if these are calculated by the ownerCt, don't trash them:
            if (me.widthModel.calculated) {
                me.recoverProp('width', oldProps, oldDirty);
            } else if ('width' in oldProps) {
                ++remainingCount;
            }
            if (me.heightModel.calculated) {
                me.recoverProp('height', oldProps, oldDirty);
            } else if ('height' in oldProps) {
                ++remainingCount;
            }
            // if we are a container child and this is not a full init, that means our
            // parent was not invalidated and therefore only the dimensions that were
            // set last time and removed from remainingChildDimensions last time, need to
            // be added back to remainingChildDimensions. This only needs to happen for
            // properties that we don't recover above (model=calculated)
            if (ownerCtContext && !me.isComponentChild) {
                ownerCtContext.remainingChildDimensions += remainingCount;
            }
        }
        if (oldProps && ownerLayout && ownerLayout.manageMargins) {
            me.recoverProp('margin-top', oldProps, oldDirty);
            me.recoverProp('margin-right', oldProps, oldDirty);
            me.recoverProp('margin-bottom', oldProps, oldDirty);
            me.recoverProp('margin-left', oldProps, oldDirty);
        }
        // Process any invalidate options present. These can only come from explicit calls
        // to the invalidate() method.
        if (options) {
            // Consider a container box with wrapping text. If the box is made wider, the
            // text will take up less height (until there is no more wrapping). Conversely,
            // if the box is made narrower, the height starts to increase due to wrapping.
            //
            // Imposing a minWidth constraint would increase the width. This may decrease
            // the height. If the box is shrinkWrap, however, the width will already be
            // such that there is no wrapping, so the height will not further decrease.
            // Since the height will also not increase if we widen the box, there is no
            // problem simultaneously imposing a minHeight or maxHeight constraint.
            //
            // When we impose as maxWidth constraint, however, we are shrinking the box
            // which may increase the height. If we are imposing a maxHeight constraint,
            // that is fine because a further increased height will still need to be
            // constrained. But if we are imposing a minHeight constraint, we cannot know
            // whether the increase in height due to wrapping will be greater than the
            // minHeight. If we impose a minHeight constraint at the same time, then, we
            // could easily be locking in the wrong height.
            //
            // It is important to note that this logic applies to simultaneously *adding*
            // both a maxWidth and a minHeight constraint. It is perfectly fine to have
            // a state with both constraints, but we cannot add them both at once.
            newHeightModel = options.heightModel;
            newWidthModel = options.widthModel;
            if (newWidthModel && newHeightModel && oldWidthModel && oldHeightModel) {
                if (oldWidthModel.shrinkWrap && oldHeightModel.shrinkWrap) {
                    if (newWidthModel.constrainedMax && newHeightModel.constrainedMin) {
                        newHeightModel = null;
                    }
                }
            }
            // Apply size model updates (if any) and state updates (if any).
            if (newWidthModel) {
                me.widthModel = newWidthModel;
            }
            if (newHeightModel) {
                me.heightModel = newHeightModel;
            }
            if (options.state) {
                Ext.apply(me.state, options.state);
            }
        }
        return ret;
    },
    /**
     * @private
     */
    initContinue: function(full) {
        var me = this,
            ownerCtContext = me.ownerCtContext,
            comp = me.target,
            widthModel = me.widthModel,
            inheritedState = comp.getInherited(),
            boxParent;
        if (widthModel.fixed) {
            // calculated or configured
            inheritedState.inShrinkWrapTable = false;
        } else {
            delete inheritedState.inShrinkWrapTable;
        }
        if (full) {
            if (ownerCtContext && widthModel.shrinkWrap) {
                boxParent = ownerCtContext.isBoxParent ? ownerCtContext : ownerCtContext.boxParent;
                if (boxParent) {
                    boxParent.addBoxChild(me);
                }
            } else if (widthModel.natural) {
                me.boxParent = ownerCtContext;
            }
        }
        return full;
    },
    /**
     * @private
     */
    initDone: function(containerLayoutDone) {
        var me = this,
            props = me.props,
            state = me.state;
        // These properties are only set when they are true:
        if (me.remainingChildDimensions === 0) {
            props.containerChildrenSizeDone = true;
        }
        if (containerLayoutDone) {
            props.containerLayoutDone = true;
        }
        if (me.boxChildren && me.boxChildren.length && me.widthModel.shrinkWrap) {
            // set a very large width to allow the children to measure their natural
            // widths (this is cleared once all children have been measured):
            me.el.setWidth(10000);
            // don't run layouts for this component until we clear this width...
            state.blocks = (state.blocks || 0) + 1;
        }
    },
    /**
     * @private
     */
    initAnimation: function() {
        var me = this,
            target = me.target,
            ownerCtContext = me.ownerCtContext;
        if (ownerCtContext && ownerCtContext.isTopLevel) {
            // See which properties we are supposed to animate to their new state.
            // If there are any, queue ourself to be animated by the owning Context
            me.animatePolicy = target.ownerLayout.getAnimatePolicy(me);
        } else if (!ownerCtContext && target.isCollapsingOrExpanding && target.animCollapse) {
            // Collapsing/expnding a top level Panel with animation. We need to fabricate
            // an animatePolicy depending on which dimension the collapse is using,
            // isCollapsingOrExpanding is set during the collapse/expand process.
            me.animatePolicy = target.componentLayout.getAnimatePolicy(me);
        }
        if (me.animatePolicy) {
            me.context.queueAnimation(me);
        }
    },
    /**
     * Adds a block.
     * 
     * @param {String} name The name of the block list ('blocks' or 'domBlocks').
     * @param {Ext.layout.Layout} layout The layout that is blocked.
     * @param {String} propName The property name that blocked the layout (e.g., 'width').
     * @private
     */
    addBlock: function(name, layout, propName) {
        var me = this,
            collection = me[name] || (me[name] = {}),
            blockedLayouts = collection[propName] || (collection[propName] = {});
        if (!blockedLayouts[layout.id]) {
            blockedLayouts[layout.id] = layout;
            ++layout.blockCount;
            ++me.context.blockCount;
        }
    },
    addBoxChild: function(boxChildItem) {
        var me = this,
            children,
            widthModel = boxChildItem.widthModel;
        boxChildItem.boxParent = this;
        // Children that are widthModel.auto (regardless of heightModel) that measure the
        // DOM (by virtue of hasRawContent), need to wait for their "box parent" to be sized.
        // If they measure too early, they will be wrong results. In the widthModel.shrinkWrap
        // case, the boxParent "crushes" the child. In the case of widthModel.natural, the
        // boxParent's width is likely a key part of the child's width (e.g., "50%" or just
        // normal block-level behavior of 100% width)
        boxChildItem.measuresBox = widthModel.shrinkWrap ? boxChildItem.hasRawContent : widthModel.natural;
        if (boxChildItem.measuresBox) {
            children = me.boxChildren;
            if (children) {
                children.push(boxChildItem);
            } else {
                me.boxChildren = [
                    boxChildItem
                ];
            }
        }
    },
    /**
     * Adds x and y values from a props object to a styles object as "left" and "top" values.
     * Overridden to add the x property as "right" in rtl mode.
     * @param {Object} styles A styles object for an Element
     * @param {Object} props A ContextItem props object
     * @return {Number} count The number of styles that were set.
     * @private
     */
    addPositionStyles: function(styles, props) {
        var x = props.x,
            y = props.y,
            count = 0;
        if (x !== undefined) {
            styles[this.translateProps.x] = x + 'px';
            ++count;
        }
        if (y !== undefined) {
            styles.top = y + 'px';
            ++count;
        }
        return count;
    },
    /**
     * Adds a trigger.
     * 
     * @param {String} propName The property name that triggers the layout (e.g., 'width').
     * @param {Boolean} inDom True if the trigger list is `domTriggers`, false if `triggers`.
     * @private
     */
    addTrigger: function(propName, inDom) {
        var me = this,
            name = inDom ? 'domTriggers' : 'triggers',
            collection = me[name] || (me[name] = {}),
            context = me.context,
            layout = context.currentLayout,
            triggers = collection[propName] || (collection[propName] = {});
        if (!triggers[layout.id]) {
            triggers[layout.id] = layout;
            ++layout.triggerCount;
            triggers = context.triggers[inDom ? 'dom' : 'data'];
            (triggers[layout.id] || (triggers[layout.id] = [])).push({
                item: this,
                prop: propName
            });
            if (me.props[propName] !== undefined) {
                if (!inDom || !(me.dirty && (propName in me.dirty))) {
                    ++layout.firedTriggers;
                }
            }
        }
    },
    boxChildMeasured: function() {
        var me = this,
            state = me.state,
            count = (state.boxesMeasured = (state.boxesMeasured || 0) + 1);
        if (count === me.boxChildren.length) {
            // all of our children have measured themselves, so we can clear the width
            // and resume layouts for this component...
            state.clearBoxWidth = 1;
            ++me.context.progressCount;
            me.markDirty();
        }
    },
    borderNames: [
        'border-top-width',
        'border-right-width',
        'border-bottom-width',
        'border-left-width'
    ],
    marginNames: [
        'margin-top',
        'margin-right',
        'margin-bottom',
        'margin-left'
    ],
    paddingNames: [
        'padding-top',
        'padding-right',
        'padding-bottom',
        'padding-left'
    ],
    trblNames: [
        'top',
        'right',
        'bottom',
        'left'
    ],
    cacheMissHandlers: {
        borderInfo: function(me) {
            var info = me.getStyles(me.borderNames, me.trblNames);
            info.width = info.left + info.right;
            info.height = info.top + info.bottom;
            return info;
        },
        marginInfo: function(me) {
            var info = me.getStyles(me.marginNames, me.trblNames);
            info.width = info.left + info.right;
            info.height = info.top + info.bottom;
            return info;
        },
        paddingInfo: function(me) {
            // if this context item's target is a framed component the padding is on the frameBody,
            // not on the main el
            var item = me.frameBodyContext || me,
                info = item.getStyles(me.paddingNames, me.trblNames);
            info.width = info.left + info.right;
            info.height = info.top + info.bottom;
            return info;
        }
    },
    checkCache: function(entry) {
        return this.cacheMissHandlers[entry](this);
    },
    clearAllBlocks: function(name) {
        var collection = this[name],
            propName;
        if (collection) {
            for (propName in collection) {
                this.clearBlocks(name, propName);
            }
        }
    },
    /**
     * Removes any blocks on a property in the specified set. Any layouts that were blocked
     * by this property and are not still blocked (by other properties) will be rescheduled.
     * 
     * @param {String} name The name of the block list ('blocks' or 'domBlocks').
     * @param {String} propName The property name that blocked the layout (e.g., 'width').
     * @private
     */
    clearBlocks: function(name, propName) {
        var collection = this[name],
            blockedLayouts = collection && collection[propName],
            context, layout, layoutId;
        if (blockedLayouts) {
            delete collection[propName];
            context = this.context;
            for (layoutId in blockedLayouts) {
                layout = blockedLayouts[layoutId];
                --context.blockCount;
                if (!--layout.blockCount && !layout.pending && !layout.done) {
                    context.queueLayout(layout);
                }
            }
        }
    },
    /**
     * Registers a layout in the block list for the given property. Once the property is
     * set in the {@link Ext.layout.Context}, the layout is unblocked.
     * 
     * @param {Ext.layout.Layout} layout
     * @param {String} propName The property name that blocked the layout (e.g., 'width').
     */
    block: function(layout, propName) {
        this.addBlock('blocks', layout, propName);
    },
    /**
     * Registers a layout in the DOM block list for the given property. Once the property
     * flushed to the DOM by the {@link Ext.layout.Context}, the layout is unblocked.
     * 
     * @param {Ext.layout.Layout} layout
     * @param {String} propName The property name that blocked the layout (e.g., 'width').
     */
    domBlock: function(layout, propName) {
        this.addBlock('domBlocks', layout, propName);
    },
    /**
     * Reschedules any layouts associated with a given trigger.
     * 
     * @param {String} name The name of the trigger list ('triggers' or 'domTriggers').
     * @param {String} propName The property name that triggers the layout (e.g., 'width').
     * @private
     */
    fireTriggers: function(name, propName) {
        var collection = this[name],
            triggers = collection && collection[propName],
            context = this.context,
            layout, layoutId;
        if (triggers) {
            for (layoutId in triggers) {
                layout = triggers[layoutId];
                ++layout.firedTriggers;
                if (!layout.done && !layout.blockCount && !layout.pending) {
                    context.queueLayout(layout);
                }
            }
        }
    },
    /**
     * Flushes any updates in the dirty collection to the DOM. This is only called if there
     * are dirty entries because this object is only added to the flushQueue of the
     * {@link Ext.layout.Context} when entries become dirty.
     */
    flush: function() {
        var me = this,
            dirty = me.dirty,
            state = me.state,
            targetEl = me.el;
        me.dirtyCount = 0;
        // Set any queued DOM attributes
        if ('attributes' in me) {
            targetEl.set(me.attributes);
            delete me.attributes;
        }
        // Set any queued DOM HTML content
        if ('innerHTML' in me) {
            targetEl.innerHTML = me.innerHTML;
            delete me.innerHTML;
        }
        if (state && state.clearBoxWidth) {
            state.clearBoxWidth = 0;
            me.el.setStyle('width', null);
            if (!--state.blocks) {
                me.context.queueItemLayouts(me);
            }
        }
        if (dirty) {
            delete me.dirty;
            me.writeProps(dirty, true);
        }
    },
    /**
     * @private
     */
    flushAnimations: function() {
        var me = this,
            animateFrom = me.previousSize,
            target, animQueue, targetAnim, duration, animateProps, anim, changeCount, j, propsLen, propName, oldValue, newValue, flag;
        // Only animate if the Component has been previously layed out:
        // first layout should not animate
        if (animateFrom) {
            target = me.target;
            targetAnim = target.getAnimationProps();
            duration = targetAnim.duration;
            animateProps = Ext.Object.getKeys(me.animatePolicy);
            // Create an animation block using the targetAnim configuration to provide defaults.
            // They may want custom duration, or easing, or listeners.
            anim = Ext.apply({}, {
                from: {},
                to: {},
                duration: duration || Ext.fx.Anim.prototype.duration
            }, targetAnim);
            for (changeCount = 0 , j = 0 , propsLen = animateProps.length; j < propsLen; j++) {
                propName = animateProps[j];
                oldValue = animateFrom[propName];
                newValue = me.peek(propName);
                if (oldValue !== newValue && newValue != null) {
                    propName = me.translateProps[propName] || propName;
                    anim.from[propName] = oldValue;
                    anim.to[propName] = newValue;
                    ++changeCount;
                }
            }
            // If any values have changed, kick off animation from the cached old values
            // to the new values
            if (changeCount) {
                // It'a Panel being collapsed. rollback, and then fix the class name string
                if (me.isCollapsingOrExpanding === 1) {
                    target.componentLayout.undoLayout(me);
                } else // Otherwise, undo just the animated properties so the animation can proceed
                // from the old layout.
                {
                    me.writeProps(anim.from);
                }
                me.el.animate(anim);
                animQueue = Ext.fx.Manager.getFxQueue(me.el.id);
                anim = animQueue[animQueue.length - 1];
                target.$layoutAnim = anim;
                anim.on({
                    afteranimate: function() {
                        var flag;
                        delete target.$layoutAnim;
                        // afteranimate can fire when the target is being destroyed
                        // and the animation queue is being stopped.
                        if (target.destroying || target.destroyed) {
                            return;
                        }
                        flag = me.isCollapsingOrExpanding;
                        if (flag === 1) {
                            target.componentLayout.redoLayout(me);
                            target.afterCollapse(true);
                        } else if (flag === 2) {
                            target.afterExpand(true);
                        }
                        if (target.hasListeners.afterlayoutanimation) {
                            target.fireEvent('afterlayoutanimation', target);
                        }
                    }
                });
            } else // If no values were changed that could mean that the component
            // started its lifecycle already collapsed and we simply don't have
            // the proper expanded size. In such case we can't run the animation
            // but still have to finish the expand sequence.
            {
                flag = me.isCollapsingOrExpanding;
                if (flag === 1) {
                    target.afterCollapse(true);
                } else if (flag === 2) {
                    target.afterExpand(true);
                }
            }
        }
    },
    /**
     * Gets the border information for the element as an object with left, top, right and
     * bottom properties holding border size in pixels. This object is only read from the
     * DOM on first request and is cached.
     * @return {Object}
     */
    getBorderInfo: function() {
        var me = this,
            info = me.borderInfo;
        if (!info) {
            me.borderInfo = info = me.checkCache('borderInfo');
        }
        return info;
    },
    /**
     * @member Ext.layout.ContextItem
     * Returns the context item for an owned element. This should only be called on a
     * component's item. The list of child items is used to manage invalidating calculated
     * results.
     * @param {String/Ext.dom.Element} nameOrEl The element or the name of an owned element
     * @param {Ext.layout.container.Container/Ext.Component} [owner] The owner of the
     * named element if the passed "nameOrEl" parameter is a String. Defaults to this
     * ContextItem's "target" property.  For more details on owned elements see
     * {@link Ext.Component#cfg-childEls childEls} and
     * {@link Ext.Component#renderSelectors renderSelectors}
     * @return {Ext.layout.ContextItem}
     */
    getEl: function(nameOrEl, owner) {
        var me = this,
            src, el, elContext;
        if (nameOrEl) {
            if (nameOrEl.dom) {
                el = nameOrEl;
            } else {
                src = me.target;
                if (owner) {
                    src = owner;
                }
                el = src[nameOrEl];
                if (typeof el === 'function') {
                    // ex 'getTarget'
                    el = el.call(src);
                    if (el === me.el) {
                        return this;
                    }
                }
            }
            // comp.getTarget() often returns comp.el
            if (el) {
                elContext = me.context.getEl(me, el);
            }
        }
        return elContext || null;
    },
    /**
     * Gets the "frame" information for the element as an object with left, top, right and
     * bottom properties holding border+framing size in pixels. This object is calculated
     * on first request and is cached.
     * @return {Object}
     */
    getFrameInfo: function() {
        var me = this,
            info = me.frameInfo,
            framing, border;
        if (!info) {
            framing = me.framing;
            border = me.getBorderInfo();
            me.frameInfo = info = framing ? {
                top: framing.top + border.top,
                right: framing.right + border.right,
                bottom: framing.bottom + border.bottom,
                left: framing.left + border.left,
                width: framing.width + border.width,
                height: framing.height + border.height
            } : border;
        }
        return info;
    },
    /**
     * Gets the margin information for the element as an object with left, top, right and
     * bottom properties holding margin size in pixels. This object is only read from the
     * DOM on first request and is cached.
     * @return {Object}
     */
    getMarginInfo: function() {
        var me = this,
            info = me.marginInfo,
            comp, manageMargins, ownerLayout, ownerLayoutId;
        if (!info) {
            if (!me.wrapsComponent) {
                info = me.checkCache('marginInfo');
            } else {
                comp = me.target;
                ownerLayout = comp.ownerLayout;
                ownerLayoutId = ownerLayout ? ownerLayout.id : null;
                manageMargins = ownerLayout && ownerLayout.manageMargins;
                // TODO: stop caching margin$ on the component EXTJS-13359
                info = comp.margin$;
                if (info && info.ownerId !== ownerLayoutId) {
                    // got one but from the wrong owner
                    info = null;
                }
                if (!info) {
                    // if (no cache)
                    // CSS margins are only checked if there isn't a margin property
                    // on the component
                    info = me.parseMargins(comp, comp.margin) || me.checkCache('marginInfo');
                    if (manageMargins) {
                        // TODO: Stop zeroing out the margins EXTJS-13359
                        me.setProp('margin-top', 0);
                        me.setProp('margin-right', 0);
                        me.setProp('margin-bottom', 0);
                        me.setProp('margin-left', 0);
                    }
                    // cache the layout margins and tag them with the layout id:
                    info.ownerId = ownerLayoutId;
                    comp.margin$ = info;
                }
                info.width = info.left + info.right;
                info.height = info.top + info.bottom;
            }
            me.marginInfo = info;
        }
        return info;
    },
    /**
     * Clears the margin cache so that marginInfo get re-read from the dom on the next call
     * to getMarginInfo(). This is needed in some special cases where the margins have changed
     * since the last layout, making the cached values invalid.
     * For example collapsed window headers have different margin than expanded ones.
     */
    clearMarginCache: function() {
        delete this.marginInfo;
        delete this.target.margin$;
    },
    /**
     * Gets the padding information for the element as an object with left, top, right and
     * bottom properties holding padding size in pixels. This object is only read from the
     * DOM on first request and is cached.
     * @return {Object}
     */
    getPaddingInfo: function() {
        var me = this,
            info = me.paddingInfo;
        if (!info) {
            me.paddingInfo = info = me.checkCache('paddingInfo');
        }
        return info;
    },
    /**
     * Gets a property of this object. Also tracks the current layout as dependent on this
     * property so that changes to it will trigger the layout to be recalculated.
     * @param {String} propName The property name that blocked the layout (e.g., 'width').
     * @return {Object} The property value or undefined if not yet set.
     */
    getProp: function(propName) {
        var me = this,
            result = me.props[propName];
        me.addTrigger(propName);
        return result;
    },
    /**
     * Gets a property of this object if it is correct in the DOM. Also tracks the current
     * layout as dependent on this property so that DOM writes of it will trigger the
     * layout to be recalculated.
     * @param {String} propName The property name (e.g., 'width').
     * @return {Object} The property value or undefined if not yet set or is dirty.
     */
    getDomProp: function(propName) {
        var me = this,
            result = (me.dirty && (propName in me.dirty)) ? undefined : me.props[propName];
        me.addTrigger(propName, true);
        return result;
    },
    /**
     * Returns a style for this item. Each style is read from the DOM only once on first
     * request and is then cached. If the value is an integer, it is parsed automatically
     * (so '5px' is not returned, but rather 5).
     *
     * @param {String} styleName The CSS style name.
     * @return {Object} The value of the DOM style (parsed as necessary).
     */
    getStyle: function(styleName) {
        var me = this,
            styles = me.styles,
            info, value;
        if (styleName in styles) {
            value = styles[styleName];
        } else {
            info = me.styleInfo[styleName];
            value = me.el.getStyle(styleName);
            if (info && info.parseInt) {
                value = parseInt(value, 10) || 0;
            }
            styles[styleName] = value;
        }
        return value;
    },
    /**
     * Returns styles for this item. Each style is read from the DOM only once on first
     * request and is then cached. If the value is an integer, it is parsed automatically
     * (so '5px' is not returned, but rather 5).
     *
     * @param {String[]} styleNames The CSS style names.
     * @param {String[]} [altNames] The alternate names for the returned styles. If given,
     * these names must correspond one-for-one to the `styleNames`.
     * @return {Object} The values of the DOM styles (parsed as necessary).
     */
    getStyles: function(styleNames, altNames) {
        var me = this,
            styleCache = me.styles,
            values = {},
            hits = 0,
            n = styleNames.length,
            i, missing, missingAltNames, name, info, styleInfo, styles, value;
        altNames = altNames || styleNames;
        // We are optimizing this for all hits or all misses. If we hit on all styles, we
        // don't create a missing[]. If we miss on all styles, we also don't create one.
        for (i = 0; i < n; ++i) {
            name = styleNames[i];
            if (name in styleCache) {
                values[altNames[i]] = styleCache[name];
                ++hits;
                if (i && hits === 1) {
                    // if (first hit was after some misses)
                    missing = styleNames.slice(0, i);
                    missingAltNames = altNames.slice(0, i);
                }
            } else if (hits) {
                (missing || (missing = [])).push(name);
                (missingAltNames || (missingAltNames = [])).push(altNames[i]);
            }
        }
        if (hits < n) {
            missing = missing || styleNames;
            missingAltNames = missingAltNames || altNames;
            styleInfo = me.styleInfo;
            styles = me.el.getStyle(missing);
            for (i = missing.length; i--; ) {
                name = missing[i];
                info = styleInfo[name];
                value = styles[name];
                if (info && info.parseInt) {
                    value = parseInt(value, 10) || 0;
                }
                values[missingAltNames[i]] = value;
                styleCache[name] = value;
            }
        }
        return values;
    },
    /**
     * Returns true if the given property has been set. This is equivalent to calling
     * {@link #getProp} and not getting an undefined result. In particular, this call
     * registers the current layout to be triggered by changes to this property.
     * 
     * @param {String} propName The property name (e.g., 'width').
     * @return {Boolean}
     */
    hasProp: function(propName) {
        return this.getProp(propName) != null;
    },
    /**
     * Returns true if the given property is correct in the DOM. This is equivalent to
     * calling {@link #getDomProp} and not getting an undefined result. In particular,
     * this call registers the current layout to be triggered by flushes of this property.
     * 
     * @param {String} propName The property name (e.g., 'width').
     * @return {Boolean}
     */
    hasDomProp: function(propName) {
        return this.getDomProp(propName) != null;
    },
    /**
     * Invalidates the component associated with this item. The layouts for this component
     * and all of its contained items will be re-run after first clearing any computed
     * values.
     * 
     * If state needs to be carried forward beyond the invalidation, the `options` parameter
     * can be used.
     *
     * @param {Object} options An object describing how to handle the invalidation.
     * @param {Object} options.state An object to {@link Ext#apply} to the {@link #state}
     *  of this item after invalidation clears all other properties.
     * @param {Function} options.before A function to call after the context data is cleared
     * and before the {@link Ext.layout.Layout#beginLayoutCycle} methods are called.
     * @param {Ext.layout.ContextItem} options.before.item This ContextItem.
     * @param {Object} options.before.options The options object passed to {@link #invalidate}.
     * @param {Function} options.after A function to call after the context data is cleared
     * and after the {@link Ext.layout.Layout#beginLayoutCycle} methods are called.
     * @param {Ext.layout.ContextItem} options.after.item This ContextItem.
     * @param {Object} options.after.options The options object passed to {@link #invalidate}.
     * @param {Object} options.scope The scope to use when calling the callback functions.
     */
    invalidate: function(options) {
        this.context.queueInvalidate(this, options);
    },
    markDirty: function() {
        if (++this.dirtyCount === 1) {
            // our first dirty property... queue us for flush
            this.context.queueFlush(this);
        }
    },
    onBoxMeasured: function() {
        var boxParent = this.boxParent,
            state = this.state;
        if (boxParent && boxParent.widthModel.shrinkWrap && !state.boxMeasured && this.measuresBox) {
            // since an autoWidth boxParent is holding a width on itself to allow each
            // child to measure
            state.boxMeasured = 1;
            // best to only call once per child
            boxParent.boxChildMeasured();
        }
    },
    parseMargins: function(comp, margins) {
        var type, ret;
        if (margins === true) {
            margins = 5;
        }
        type = typeof margins;
        if (type === 'string' || type === 'number') {
            ret = comp.parseBox(margins);
        } else if (margins) {
            ret = {
                top: 0,
                right: 0,
                bottom: 0,
                left: 0
            };
            // base defaults
            if (margins) {
                margins = Ext.apply(ret, comp.parseBox(margins));
            }
        }
        // + config
        return ret;
    },
    peek: function(propName) {
        return this.props[propName];
    },
    recalculateSizeModel: function() {
        // See the constructor, this logic is very similar. Not broken out into
        // a separate method for performance reasons
        var me = this,
            target = me.target,
            componentLayout = target.componentLayout,
            ownerCtContext = me.ownerCtContext,
            oldContext = componentLayout.ownerContext,
            sizeModel;
        // If the componentLayout has an ownerContext, it will just use the sizeModel that
        // exists on the context. Instead, force it to recalculate
        componentLayout.ownerContext = null;
        me.sizeModel = sizeModel = target.getSizeModel(ownerCtContext && ownerCtContext.widthModel.pairsByHeightOrdinal[ownerCtContext.heightModel.ordinal]);
        me.widthModel = sizeModel.width;
        me.heightModel = sizeModel.height;
        if (oldContext) {
            componentLayout.ownerContext = me;
        }
    },
    /**
     * Recovers a property value from the last computation and restores its value and
     * dirty state.
     * 
     * @param {String} propName The name of the property to recover.
     * @param {Object} oldProps The old "props" object from which to recover values.
     * @param {Object} oldDirty The old "dirty" object from which to recover state.
     */
    recoverProp: function(propName, oldProps, oldDirty) {
        var me = this,
            props = me.props,
            dirty;
        if (propName in oldProps) {
            props[propName] = oldProps[propName];
            if (oldDirty && propName in oldDirty) {
                dirty = me.dirty || (me.dirty = {});
                dirty[propName] = oldDirty[propName];
            }
        }
    },
    redo: function(deep) {
        var me = this,
            items, len, i;
        me.revertProps(me.props);
        if (deep && me.wrapsComponent) {
            // Rollback the state of child Components
            if (me.childItems) {
                for (i = 0 , items = me.childItems , len = items.length; i < len; i++) {
                    items[i].redo(deep);
                }
            }
            // Rollback the state of child Elements
            for (i = 0 , items = me.children , len = items.length; i < len; i++) {
                items[i].redo();
            }
        }
    },
    /**
     * Removes a cached ContextItem that was created using {@link #getEl}.  It may be
     * necessary to call this method if the dom reference for owned element changes so 
     * that {@link #getEl} can be called again to reinitialize the ContextItem with the
     * new element.
     * @param {String/Ext.dom.Element} nameOrEl The element or the name of an owned element
     * @param {Ext.layout.container.Container/Ext.Component} [owner] The owner of the
     * named element if the passed "nameOrEl" parameter is a String. Defaults to this
     * ContextItem's "target" property.
     */
    removeEl: function(nameOrEl, owner) {
        var me = this,
            src, el;
        if (nameOrEl) {
            if (nameOrEl.dom) {
                el = nameOrEl;
            } else {
                src = me.target;
                if (owner) {
                    src = owner;
                }
                el = src[nameOrEl];
                if (typeof el === 'function') {
                    // ex 'getTarget'
                    el = el.call(src);
                    if (el === me.el) {
                        return this;
                    }
                }
            }
            // comp.getTarget() often returns comp.el
            if (el) {
                me.context.removeEl(el, me);
            }
        }
    },
    revertProps: function(props) {
        var name,
            flushed = this.flushedProps,
            reverted = {};
        for (name in props) {
            if (flushed.hasOwnProperty(name)) {
                reverted[name] = props[name];
            }
        }
        this.writeProps(reverted);
    },
    /**
     * Queue the setting of a DOM attribute on this ContextItem's target when next flushed.
     */
    setAttribute: function(name, value) {
        var me = this;
        if (!me.attributes) {
            me.attributes = {};
        }
        me.attributes[name] = value;
        me.markDirty();
    },
    setBox: function(box) {
        var me = this;
        if ('left' in box) {
            me.setProp('x', box.left);
        }
        if ('top' in box) {
            me.setProp('y', box.top);
        }
        // if sizeModel says we should not be setting these, the appropriate calls will be
        // null operations... otherwise, we must set these values, so what we have in box
        // is what we go with (undefined, NaN and no change are handled at a lower level):
        me.setSize(box.width, box.height);
    },
    /**
     * Sets the contentHeight property. If the component uses raw content, then only the
     * measured height is acceptable.
     *
     * Calculated values can sometimes be NaN or undefined, which generally mean the
     * calculation is not done. To indicate that such as value was passed, 0 is returned.
     * Otherwise, 1 is returned.
     *
     * If the caller is not measuring (i.e., they are calculating) and the component has raw
     * content, 1 is returned indicating that the caller is done.
     */
    setContentHeight: function(height, measured) {
        if (!measured && this.hasRawContent) {
            return 1;
        }
        return this.setProp('contentHeight', height);
    },
    /**
     * Sets the contentWidth property. If the component uses raw content, then only the
     * measured width is acceptable.
     * 
     * Calculated values can sometimes be NaN or undefined, which generally means that the
     * calculation is not done. To indicate that such as value was passed, 0 is returned.
     * Otherwise, 1 is returned.
     *
     * If the caller is not measuring (i.e., they are calculating) and the component has raw
     * content, 1 is returned indicating that the caller is done.
     */
    setContentWidth: function(width, measured) {
        if (!measured && this.hasRawContent) {
            return 1;
        }
        return this.setProp('contentWidth', width);
    },
    /**
     * Sets the contentWidth and contentHeight properties. If the component uses raw content,
     * then only the measured values are acceptable.
     * 
     * Calculated values can sometimes be NaN or undefined, which generally means that the
     * calculation is not done. To indicate that either passed value was such a value, false
     * returned. Otherwise, true is returned.
     *
     * If the caller is not measuring (i.e., they are calculating) and the component has raw
     * content, true is returned indicating that the caller is done.
     */
    setContentSize: function(width, height, measured) {
        return this.setContentWidth(width, measured) + this.setContentHeight(height, measured) === 2;
    },
    /**
     * Sets a property value. This will unblock and/or trigger dependent layouts if the
     * property value is being changed. Values of NaN and undefined are not accepted by
     * this method.
     * 
     * @param {String} propName The property name (e.g., 'width').
     * @param {Object} value The new value of the property.
     * @param {Boolean} dirty Optionally specifies if the value is currently in the DOM
     *  (default is `true` which indicates the value is not in the DOM and must be flushed
     *  at some point).
     * @return {Number} 1 if this call specified the property value, 0 if not.
     */
    setProp: function(propName, value, dirty) {
        var me = this,
            valueType = typeof value,
            info;
        if (valueType === 'undefined' || (valueType === 'number' && isNaN(value))) {
            return 0;
        }
        if (me.props[propName] === value) {
            return 1;
        }
        me.props[propName] = value;
        ++me.context.progressCount;
        if (dirty === false) {
            // if the prop is equivalent to what is in the DOM (we won't be writing it),
            // we need to clear hard blocks (domBlocks) on that property.
            me.fireTriggers('domTriggers', propName);
            me.clearBlocks('domBlocks', propName);
        } else {
            info = me.styleInfo[propName];
            if (info) {
                if (!me.dirty) {
                    me.dirty = {};
                }
                me.dirty[propName] = value;
                me.markDirty();
            }
        }
        // we always clear soft blocks on set
        me.fireTriggers('triggers', propName);
        me.clearBlocks('blocks', propName);
        return 1;
    },
    /**
     * Sets the height and constrains the height to min/maxHeight range.
     * 
     * @param {Number} height The height.
     * @param {Boolean} [dirty=true] Specifies if the value is currently in the DOM. A
     * value of `false` indicates that the value is already in the DOM.
     * @return {Number} The actual height after constraining.
     */
    setHeight: function(height, dirty) {
        var me = this,
            comp = me.target,
            ownerCtContext = me.ownerCtContext,
            frameBody, frameInfo, min, oldHeight, rem;
        if (height < 0) {
            height = 0;
        }
        if (!me.wrapsComponent) {
            if (!me.setProp('height', height, dirty)) {
                return NaN;
            }
        } else {
            min = me.collapsedVert ? 0 : (comp.minHeight || 0);
            height = Ext.Number.constrain(height, min, comp.maxHeight);
            oldHeight = me.props.height;
            if (!me.setProp('height', height, dirty)) {
                return NaN;
            }
            // if we are a container child, since the height is now known we can decrement
            // the number of remainingChildDimensions that the ownerCtContext is waiting on.
            if (ownerCtContext && !me.isComponentChild && isNaN(oldHeight)) {
                rem = --ownerCtContext.remainingChildDimensions;
                if (!rem) {
                    // if there are 0 remainingChildDimensions set containerChildrenSizeDone
                    // on the ownerCtContext to indicate that all of its children's dimensions
                    // are known
                    ownerCtContext.setProp('containerChildrenSizeDone', true);
                }
            }
            frameBody = me.frameBodyContext;
            if (frameBody) {
                frameInfo = me.getFrameInfo();
                frameBody[me.el.vertical ? 'setWidth' : 'setHeight'](height - frameInfo.height, dirty);
            }
        }
        return height;
    },
    /**
     * Sets the height and constrains the width to min/maxWidth range.
     * 
     * @param {Number} width The width.
     * @param {Boolean} [dirty=true] Specifies if the value is currently in the DOM. A
     * value of `false` indicates that the value is already in the DOM.
     * @return {Number} The actual width after constraining.
     */
    setWidth: function(width, dirty) {
        var me = this,
            comp = me.target,
            ownerCtContext = me.ownerCtContext,
            frameBody, frameInfo, min, oldWidth, rem;
        if (width < 0) {
            width = 0;
        }
        if (!me.wrapsComponent) {
            if (!me.setProp('width', width, dirty)) {
                return NaN;
            }
        } else {
            min = me.collapsedHorz ? 0 : (comp.minWidth || 0);
            width = Ext.Number.constrain(width, min, comp.maxWidth);
            oldWidth = me.props.width;
            if (!me.setProp('width', width, dirty)) {
                return NaN;
            }
            // if we are a container child, since the width is now known we can decrement
            // the number of remainingChildDimensions that the ownerCtContext is waiting on.
            if (ownerCtContext && !me.isComponentChild && isNaN(oldWidth)) {
                rem = --ownerCtContext.remainingChildDimensions;
                if (!rem) {
                    // if there are 0 remainingChildDimensions set containerChildrenSizeDone
                    // on the ownerCtContext to indicate that all of its children's dimensions
                    // are known
                    ownerCtContext.setProp('containerChildrenSizeDone', true);
                }
            }
            // if ((frameBody = me.target.frameBody) && (frameBody = me.getEl(frameBody))){
            frameBody = me.frameBodyContext;
            if (frameBody) {
                frameInfo = me.getFrameInfo();
                frameBody.setWidth(width - frameInfo.width, dirty);
            }
        }
        /* if (owner.frameBody) {
                frameContext = ownerContext.frameContext ||
                        (ownerContext.frameContext = ownerContext.getEl('frameBody'));
                width += (frameContext.paddingInfo || frameContext.getPaddingInfo()).width;
            } */
        return width;
    },
    setSize: function(width, height, dirty) {
        this.setWidth(width, dirty);
        this.setHeight(height, dirty);
    },
    translateProps: {
        x: 'left',
        y: 'top'
    },
    undo: function(deep) {
        var me = this,
            items, len, i;
        me.revertProps(me.lastBox);
        if (deep && me.wrapsComponent) {
            // Rollback the state of child Components
            if (me.childItems) {
                for (i = 0 , items = me.childItems , len = items.length; i < len; i++) {
                    items[i].undo(deep);
                }
            }
            // Rollback the state of child Elements
            for (i = 0 , items = me.children , len = items.length; i < len; i++) {
                items[i].undo();
            }
        }
    },
    unsetProp: function(propName) {
        var dirty = this.dirty;
        delete this.props[propName];
        if (dirty) {
            delete dirty[propName];
        }
    },
    writeProps: function(dirtyProps, flushing) {
        if (!(dirtyProps && typeof dirtyProps === 'object')) {
            Ext.Logger.warn('writeProps expected dirtyProps to be an object');
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            el = me.el,
            target = me.target,
            styleInfo = me.styleInfo,
            styles = {},
            width = dirtyProps.width,
            height = dirtyProps.height,
            styleCount = 0,
            // used as a boolean, the exact count doesn't matter
            info, propName, numericValue, hasWidth, hasHeight, isAbsolute, scrollbarSize, style, targetEl, scroller;
        // Process non-style properties:
        if ('displayed' in dirtyProps) {
            el.setDisplayed(dirtyProps.displayed);
        }
        // Unblock any hard blocks (domBlocks) and copy dom styles into 'styles'
        for (propName in dirtyProps) {
            if (flushing) {
                me.fireTriggers('domTriggers', propName);
                me.clearBlocks('domBlocks', propName);
                me.flushedProps[propName] = 1;
            }
            info = styleInfo[propName];
            if (info && info.dom) {
                // Numeric dirty values should have their associated suffix added
                if (info.suffix && (numericValue = parseInt(dirtyProps[propName], 10))) {
                    styles[propName] = numericValue + info.suffix;
                } else // Non-numeric (eg "auto") go in unchanged.
                {
                    styles[propName] = dirtyProps[propName];
                }
                ++styleCount;
            }
        }
        // convert x/y into setPosition (for a component) or left/top styles (for an el)
        if ('x' in dirtyProps || 'y' in dirtyProps) {
            if (target.isComponent) {
                target.setPosition(dirtyProps.x, dirtyProps.y);
            } else {
                // we wrap an element, so convert x/y to styles:
                styleCount += me.addPositionStyles(styles, dirtyProps);
            }
        }
        // Handle overflow settings updated by layout
        if ('overflowX' in dirtyProps) {
            scroller = target.getScrollable();
            if (scroller) {
                scroller.setX(dirtyProps.overflowX);
            }
        }
        if ('overflowY' in dirtyProps) {
            if (scroller || (scroller = target.getScrollable())) {
                scroller.setY(dirtyProps.overflowY);
            }
        }
        // IE9 subtracts the scrollbar size from the element size when the element
        // is absolutely positioned and uses box-sizing: border-box. To workaround this
        // issue we have to add the the scrollbar size.
        // 
        // See http://social.msdn.microsoft.com/Forums/da-DK/iewebdevelopment/thread/47c5148f-a142-4a99-9542-5f230c78cb3b
        //
        if (me.wrapsComponent && Ext.isIE9) {
            // when we set a width and we have a vertical scrollbar (overflowY), we need
            // to add the scrollbar width... conversely for the height and overflowX
            if ((hasWidth = width !== undefined && me.hasOverflowY) || (hasHeight = height !== undefined && me.hasOverflowX)) {
                // check that the component is absolute positioned.
                isAbsolute = me.isAbsolute;
                if (isAbsolute === undefined) {
                    isAbsolute = false;
                    targetEl = me.target.getTargetEl();
                    style = targetEl.getStyle('position');
                    me.isAbsolute = isAbsolute = (style === 'absolute');
                }
                // cache it
                if (isAbsolute) {
                    scrollbarSize = Ext.scrollbar.size();
                    if (hasWidth) {
                        width = parseInt(width, 10) + scrollbarSize.width;
                        styles.width = width + 'px';
                        ++styleCount;
                    }
                    if (hasHeight) {
                        height = parseInt(height, 10) + scrollbarSize.height;
                        styles.height = height + 'px';
                        ++styleCount;
                    }
                }
            }
        }
        // we make only one call to setStyle to allow it to optimize itself:
        if (styleCount) {
            el.setStyle(styles);
        }
    },
    //-------------------------------------------------------------------------
    // Diagnostics
    debugHooks: {
        $enabled: false,
        // Disable by default
        addBlock: function(name, layout, propName) {
            var blockedBy = (layout.blockedBy || (layout.blockedBy = {}));
            // Ext.log(this.id,'.',propName,' ',name,': ',this.context.getLayoutName(layout));
            blockedBy[this.id + '.' + propName + (name.substring(0, 3) === 'dom' ? ':dom' : '')] = 1;
            return this.callParent(arguments);
        },
        addBoxChild: function(boxChildItem) {
            var ret = this.callParent(arguments),
                boxChildren = this.boxChildren,
                boxParents;
            if (boxChildren && boxChildren.length === 1) {
                // the boxParent collection is created by the run override found in
                // Ext.diag.layout.Context, but IE sometimes does not load that override, so
                // we work around it for now
                boxParents = this.context.boxParents || (this.context.boxParents = new Ext.util.MixedCollection());
                boxParents.add(this);
            }
            return ret;
        },
        addTrigger: function(propName, inDom) {
            var layout = this.context.currentLayout,
                triggers;
            // Ext.log(this.id,'.',propName,' ',inDom ? ':dom' : '',' ',
            //         this.context.getLayoutName(layout));
            this.callParent(arguments);
            triggers = this.context.triggersByLayoutId;
            (triggers[layout.id] || (triggers[layout.id] = {}))[this.id + '.' + propName + (inDom ? ':dom' : '')] = {
                item: this,
                name: propName
            };
        },
        checkAuthority: function(prop) {
            var me = this,
                model = me[prop + 'Model'],
                // not me.sizeModel[prop] since it is immutable
                layout = me.context.currentLayout,
                ok, setBy;
            if (layout === me.target.ownerLayout) {
                // the ownerLayout is only allowed to set calculated dimensions
                ok = model.calculated;
            } else if (layout.isComponentLayout) {
                // the component's componentLayout (normally) is only allowed to set auto or
                // configured dimensions. The exception is when a component is run w/o its
                // ownerLayout in the picture (isTopLevel), someone must publish the lastBox
                // values and that lucky layout is the componentLayout (kinda had to be since
                // the ownerLayout is not running)
                ok = me.isTopLevel || model.auto || model.configured;
            }
            if (!ok) {
                setBy = me.context.getLayoutName(layout);
                Ext.log(setBy + ' cannot set ' + prop);
            }
        },
        clearBlocks: function(name, propName) {
            var collection = this[name],
                blockedLayouts = collection && collection[propName],
                key = this.id + '.' + propName + (name.substring(0, 3) === 'dom' ? ':dom' : ''),
                layout, layoutId;
            if (blockedLayouts) {
                for (layoutId in blockedLayouts) {
                    layout = blockedLayouts[layoutId];
                    delete layout.blockedBy[key];
                }
            }
            return this.callParent(arguments);
        },
        getEl: function(el) {
            var child = this.callParent(arguments);
            if (child && child !== this && child.parent !== this) {
                Ext.raise({
                    msg: 'Got element from wrong component'
                });
            }
            return child;
        },
        init: function() {
            var me = this,
                ret;
            ret = me.callParent(arguments);
            if (me.context.logOn.initItem) {
                Ext.log(me.id, ' consumers: content=', me.consumersContentWidth, '/', me.consumersContentHeight, ', container=', me.consumersContainerWidth, '/', me.consumersContainerHeight, ', size=', me.consumersWidth, '/', me.consumersHeight);
            }
            return ret;
        },
        invalidate: function() {
            if (this.wrapsComponent) {
                if (this.context.logOn.invalidate) {
                    Ext.log('invalidate: ', this.id);
                }
            } else {
                Ext.raise({
                    msg: 'Cannot invalidate an element contextItem'
                });
            }
            return this.callParent(arguments);
        },
        setProp: function(propName, value, dirty) {
            var me = this,
                layout = me.context.currentLayout,
                setBy = me.context.getLayoutName(layout),
                fullName = me.id + '.' + propName,
                setByProps;
            if (value !== null) {
                setByProps = me.setBy || (me.setBy = {});
                if (!setByProps[propName]) {
                    setByProps[propName] = setBy;
                } else if (setByProps[propName] !== setBy) {
                    Ext.log({
                        level: 'warn'
                    }, 'BAD! ', fullName, ' set by ', setByProps[propName], ' and ', setBy);
                }
            }
            if (me.context.logOn.setProp) {
                if (typeof value !== 'undefined' && !isNaN(value) && me.props[propName] !== value) {
                    Ext.log('set ', fullName, ' = ', value, ' (', dirty, ')');
                }
            }
            return this.callParent(arguments);
        },
        setHeight: function(height, dirty, force) {
            if (!force && this.wrapsComponent) {
                this.checkAuthority('height');
            }
            return this.callParent(arguments);
        },
        setWidth: function(width, dirty, force) {
            if (!force && this.wrapsComponent) {
                this.checkAuthority('width');
            }
            return this.callParent(arguments);
        }
    }
}, // End Diagnostics
//-------------------------------------------------------------------------
function() {
    var px = {
            dom: true,
            parseInt: true,
            suffix: 'px'
        },
        isDom = {
            dom: true
        },
        faux = {
            dom: false
        };
    // If a property exists in styleInfo, it participates in some way with the DOM. It may
    // be virtualized (like 'x' and y') and be indirect, but still requires a flush cycle
    // to reach the DOM. Properties (like 'contentWidth' and 'contentHeight') have no real
    // presence in the DOM and hence have no flush intanglements.
    // 
    // For simple styles, the object value on the right contains properties that help in
    // decoding values read by getStyle and preparing values to pass to setStyle.
    //
    this.prototype.styleInfo = {
        containerChildrenSizeDone: faux,
        containerLayoutDone: faux,
        displayed: faux,
        done: faux,
        x: faux,
        y: faux,
        // For Ext.grid.ColumnLayout
        columnsChanged: faux,
        rowHeights: faux,
        viewOverflowY: faux,
        // Scroller state set by layouts
        overflowX: faux,
        overflowY: faux,
        left: px,
        top: px,
        right: px,
        bottom: px,
        width: px,
        height: px,
        'border-top-width': px,
        'border-right-width': px,
        'border-bottom-width': px,
        'border-left-width': px,
        'margin-top': px,
        'margin-right': px,
        'margin-bottom': px,
        'margin-left': px,
        'padding-top': px,
        'padding-right': px,
        'padding-bottom': px,
        'padding-left': px,
        'line-height': isDom,
        display: isDom,
        clear: isDom
    };
});

/**
 * Manages context information during a layout.
 *
 * # Algorithm
 *
 * This class performs the following jobs:
 *
 *  - Cache DOM reads to avoid reading the same values repeatedly.
 *  - Buffer DOM writes and flush them as a block to avoid read/write interleaving.
 *  - Track layout dependencies so each layout does not have to figure out the source of
 *    its dependent values.
 *  - Intelligently run layouts when the values on which they depend change (a trigger).
 *  - Allow layouts to avoid processing when required values are unavailable (a block).
 *
 * Work done during layout falls into either a "read phase" or a "write phase" and it is
 * essential to always be aware of the current phase. Most methods in
 * {@link Ext.layout.Layout Layout} are called during a read phase:
 * {@link Ext.layout.Layout#calculate calculate},
 * {@link Ext.layout.Layout#completeLayout completeLayout} and
 * {@link Ext.layout.Layout#finalizeLayout finalizeLayout}. The exceptions to this are
 * {@link Ext.layout.Layout#beginLayout beginLayout},
 * {@link Ext.layout.Layout#beginLayoutCycle beginLayoutCycle} and
 * {@link Ext.layout.Layout#finishedLayout finishedLayout} which are called during
 * a write phase. While {@link Ext.layout.Layout#finishedLayout finishedLayout} is called
 * a write phase, it is really intended to be a catch-all for post-processing after a
 * layout run.
 * 
 * In a read phase, it is OK to read the DOM but this should be done using the appropriate
 * {@link Ext.layout.ContextItem ContextItem} where possible since that provides a cache
 * to avoid redundant reads. No writes should be made to the DOM in a read phase! Instead,
 * the values should be written to the proper ContextItem for later write-back.
 * 
 * The rules flip-flop in a write phase. The only difference is that ContextItem methods
 * like {@link Ext.layout.ContextItem#getStyle getStyle} will still read the DOM unless the
 * value was previously read. This detail is unknowable from the outside of ContextItem, so
 * read calls to ContextItem should also be avoided in a write phase.
 *
 * Calculating interdependent layouts requires a certain amount of iteration. In a given
 * cycle, some layouts will contribute results that allow other layouts to proceed. The
 * general flow then is to gather all of the layouts (both component and container) in a
 * component tree and queue them all for processing. The initial queue order is bottom-up
 * and component layout first, then container layout (if applicable) for each component.
 *
 * This initial step also calls the beginLayout method on all layouts to clear any values
 * from the DOM that might interfere with calculations and measurements. In other words,
 * this is a "write phase" and reads from the DOM should be strictly avoided.
 * 
 * Next the layout enters into its iterations or "cycles". Each cycle consists of calling
 * the {@link Ext.layout.Layout#calculate calculate} method on all layouts in the
 * {@link #layoutQueue}. These calls are part of a "read phase" and writes to the DOM should
 * be strictly avoided.
 *
 * # Considerations
 *
 * **RULE 1**: Respect the read/write cycles. Always use the
 * {@link Ext.layout.ContextItem#getProp getProp}
 * or {@link Ext.layout.ContextItem#getDomProp getDomProp} methods to get calculated values;
 * only use the {@link Ext.layout.ContextItem#getStyle getStyle} method to read styles; use
 * {@link Ext.layout.ContextItem#setProp setProp} to set DOM values. Some reads will, of
 * course, still go directly to the DOM, but if there is a method in
 * {@link Ext.layout.ContextItem ContextItem} to do a certain job, it should be used instead
 * of a lower-level equivalent.
 *
 * The basic logic flow in {@link Ext.layout.Layout#calculate calculate} consists of gathering
 * values by calling {@link Ext.layout.ContextItem#getProp getProp} or
 * {@link Ext.layout.ContextItem#getDomProp getDomProp}, calculating results and publishing
 * them by calling {@link Ext.layout.ContextItem#setProp setProp}. It is important to realize
 * that {@link Ext.layout.ContextItem#getProp getProp} will return `undefined` if the value
 * is not yet known. But the act of calling the method is enough to track the fact that the
 * calling layout depends (in some way) on this value. In other words, the calling layout is
 * "triggered" by the properties it requests.
 *
 * **RULE 2**: Avoid calling {@link Ext.layout.ContextItem#getProp getProp} unless the value
 * is needed. Gratuitous calls cause inefficiency because the layout will appear to depend on
 * values that it never actually uses. This applies equally to
 * {@link Ext.layout.ContextItem#getDomProp getDomProp} and the test-only methods
 * {@link Ext.layout.ContextItem#hasProp hasProp} and
 * {@link Ext.layout.ContextItem#hasDomProp hasDomProp}.
 *
 * Because {@link Ext.layout.ContextItem#getProp getProp} can return `undefined`, it is often
 * the case that subsequent math will produce NaN's. This is usually not a problem as the
 * NaN's simply propagate along and result in final results that are NaN. Both `undefined`
 * and NaN are ignored by {@link Ext.layout.ContextItem#setProp}, so it is often not necessary
 * to even know that this is happening. It does become important for determining if a layout
 * is not done or if it might lead to publishing an incorrect (but not NaN or `undefined`)
 * value.
 * 
 * **RULE 3**: If a layout has not calculated all the values it is required to calculate, it
 * must set {@link Ext.layout.Layout#done done} to `false` before returning from
 * {@link Ext.layout.Layout#calculate calculate}. This value is always `true` on entry because
 * it is simpler to detect the incomplete state rather than the complete state (especially up
 * and down a class hierarchy).
 * 
 * **RULE 4**: A layout must never publish an incomplete (wrong) result. Doing so would cause
 * dependent layouts to run their calculations on those wrong values, producing more wrong
 * values and some layouts may even incorrectly flag themselves as
 * {@link Ext.layout.Layout#done done} before the correct values are determined and republished.
 * Doing this will poison the calculations.
 *
 * **RULE 5**: Each value should only be published by one layout. If multiple layouts attempt
 * to publish the same values, it would be nearly impossible to avoid breaking **RULE 4**. To
 * help detect this problem, the layout diagnostics will trap on an attempt to set a value
 * from different layouts.
 *
 * Complex layouts can produce many results as part of their calculations. These values are
 * important for other layouts to proceed and need to be published by the earliest possible
 * call to {@link Ext.layout.Layout#calculate} to avoid unnecessary cycles and poor performance.
 * It is also possible, however, for some results to be related in a way such that publishing them
 * may be an all-or-none proposition (typically to avoid breaking *RULE 4*).
 * 
 * **RULE 6**: Publish results as soon as they are known to be correct rather than wait for
 * all values to be calculated. Waiting for everything to be complete can lead to deadlock.
 * The key here is not to forget **RULE 4** in the process.
 *
 * Some layouts depend on certain critical values as part of their calculations. For example,
 * HBox depends on width and cannot do anything until the width is known. In these cases, it
 * is best to use {@link Ext.layout.ContextItem#block block} or
 * {@link Ext.layout.ContextItem#domBlock domBlock} and thereby avoid processing the layout
 * until the needed value is available.
 *
 * **RULE 7**: Use {@link Ext.layout.ContextItem#block block} or
 * {@link Ext.layout.ContextItem#domBlock domBlock} when values are required to make progress.
 * This will mimize wasted recalculations.
 *
 * **RULE 8**: Blocks should only be used when no forward progress can be made. If even one
 * value could still be calculated, a block could result in a deadlock.
 *
 * Historically, layouts have been invoked directly by component code, sometimes in places
 * like an `afterLayout` method for a child component. With the flexibility now available
 * to solve complex, iterative issues, such things should be done in a responsible layout
 * (be it component or container).
 *
 * **RULE 9**: Use layouts to solve layout issues and don't wait for the layout to finish to
 * perform further layouts. This is especially important now that layouts process entire
 * component trees and not each layout in isolation.
 *
 * # Sequence Diagram
 *
 * The simplest sequence diagram for a layout run looks roughly like this:
 *
 *       Context         Layout 1     Item 1     Layout 2     Item 2
 *          |               |           |           |           |
 *     ---->X-------------->X           |           |           |
 *     run  X---------------|-----------|---------->X           |
 *          X beginLayout   |           |           |           |
 *          X               |           |           |           |
 *        A X-------------->X           |           |           |
 *          X  calculate    X---------->X           |           |
 *          X             C X  getProp  |           |           |
 *        B X               X---------->X           |           |
 *          X               |  setProp  |           |           |
 *          X               |           |           |           |
 *        D X---------------|-----------|---------->X           |
 *          X  calculate    |           |           X---------->X
 *          X               |           |           |  setProp  |
 *        E X               |           |           |           |
 *          X---------------|-----------|---------->X           |
 *          X completeLayout|           |         F |           |
 *          X               |           |           |           |
 *        G X               |           |           |           |
 *        H X-------------->X           |           |           |
 *          X  calculate    X---------->X           |           |
 *          X             I X  getProp  |           |           |
 *          X               X---------->X           |           |
 *          X               |  setProp  |           |           |
 *        J X-------------->X           |           |           |
 *          X completeLayout|           |           |           |
 *          X               |           |           |           |
 *        K X-------------->X           |           |           |
 *          X---------------|-----------|---------->X           |
 *          X finalizeLayout|           |           |           |
 *          X               |           |           |           |
 *        L X-------------->X           |           |           |
 *          X---------------|-----------|---------->X           |
 *          X finishedLayout|           |           |           |
 *          X               |           |           |           |
 *        M X-------------->X           |           |           |
 *          X---------------|-----------|---------->X           |
 *          X notifyOwner   |           |           |           |
 *        N |               |           |           |           |
 *          -               -           -           -           -
 *
 *
 * Notes:
 *
 * **A.** This is a call from the {@link #run} method to the {@link #run} method.
 * Each layout in the queue will have its {@link Ext.layout.Layout#calculate calculate}
 * method called.
 *
 * **B.** After each {@link Ext.layout.Layout#calculate calculate} method is called the
 * {@link Ext.layout.Layout#done done} flag is checked to see if the Layout has completed.
 * If it has completed and that layout object implements a
 * {@link Ext.layout.Layout#completeLayout completeLayout} method, this layout is queued to
 * receive its call. Otherwise, the layout will be queued again unless there are blocks or
 * triggers that govern its requeueing.
 * 
 * **C.** The call to {@link Ext.layout.ContextItem#getProp getProp} is made to the Item
 * and that will be tracked as a trigger (keyed by the name of the property being requested).
 * Changes to this property will cause this layout to be requeued. The call to
 * {@link Ext.layout.ContextItem#setProp setProp} will place a value in the item and not
 * directly into the DOM.
 * 
 * **D.** Call the other layouts now in the first cycle (repeat **B** and **C** for each
 * layout).
 * 
 * **E.** After completing a cycle, if progress was made (new properties were written to
 * the context) and if the {@link #layoutQueue} is not empty, the next cycle is run. If no
 * progress was made or no layouts are ready to run, all buffered values are written to
 * the DOM (a flush).
 *
 * **F.** After flushing, any layouts that were marked as {@link Ext.layout.Layout#done done}
 * that also have a {@link Ext.layout.Layout#completeLayout completeLayout} method are called.
 * This can cause them to become no longer done (see {@link #invalidate}). As with
 * {@link Ext.layout.Layout#calculate calculate}, this is considered a "read phase" and
 * direct DOM writes should be avoided.
 * 
 * **G.** Flushing and calling any pending {@link Ext.layout.Layout#completeLayout completeLayout}
 * methods will likely trigger layouts that called
 * {@link Ext.layout.ContextItem#getDomProp getDomProp} and unblock layouts that have called
 * {@link Ext.layout.ContextItem#domBlock domBlock}. These variants are used when a layout
 * needs the value to be correct in the DOM and not simply known. If this does not cause
 * at least one layout to enter the queue, we have a layout FAILURE. Otherwise, we continue
 * with the next cycle.
 * 
 * **H.** Call {@link Ext.layout.Layout#calculate calculate} on any layouts in the queue
 * at the start of this cycle. Just a repeat of **B** through **G**.
 * 
 * **I.** Once the layout has calculated all that it is resposible for, it can leave itself
 * in the {@link Ext.layout.Layout#done done} state. This is the value on entry to
 * {@link Ext.layout.Layout#calculate calculate} and must be cleared in that call if the
 * layout has more work to do.
 * 
 * **J.** Now that all layouts are done, flush any DOM values and
 * {@link Ext.layout.Layout#completeLayout completeLayout} calls. This can again cause
 * layouts to become not done, and so we will be back on another cycle if that happens.
 * 
 * **K.** After all layouts are done, call the
 * {@link Ext.layout.Layout#finalizeLayout finalizeLayout} method on any layouts that have one.
 * As with {@link Ext.layout.Layout#completeLayout completeLayout}, this can cause layouts
 * to become no longer done. This is less desirable than using
 * {@link Ext.layout.Layout#completeLayout completeLayout} because it will cause all
 * {@link Ext.layout.Layout#finalizeLayout finalizeLayout} methods to be called again
 * when we think things are all wrapped up.
 *
 * **L.** After finishing the last iteration, layouts that have a
 * {@link Ext.layout.Layout#finishedLayout finishedLayout} method will be called. This
 * call will only happen once per run and cannot cause layouts to be run further.
 *
 * **M.** After calling finahedLayout, layouts that have a
 * {@link Ext.layout.Layout#notifyOwner notifyOwner} method will be called. This
 * call will only happen once per run and cannot cause layouts to be run further.
 *
 * **N.** One last flush to make sure everything has been written to the DOM.
 *
 * # Inter-Layout Collaboration
 * 
 * Many layout problems require collaboration between multiple layouts. In some cases, this
 * is as simple as a component's container layout providing results used by its component
 * layout or vise-versa. A slightly more distant collaboration occurs in a box layout when
 * stretchmax is used: the child item's component layout provides results that are consumed
 * by the ownerCt's box layout to determine the size of the children.
 *
 * The various forms of interdependence between a container and its children are described by
 * each components' {@link Ext.Component#getSizeModel size model}.
 *
 * To facilitate this collaboration, the following pairs of properties are published to the
 * component's {@link Ext.layout.ContextItem ContextItem}:
 *
 *  - width/height: These hold the final size of the component. The layout indicated by the
 *    {@link Ext.Component#getSizeModel size model} is responsible for setting these.
 *  - contentWidth/contentHeight: These hold size information published by the container
 *    layout or from DOM measurement. These describe the content only. These values are
 *    used by the component layout to determine the outer width/height when that component
 *    is {@link Ext.Component#shrinkWrap shrink-wrapped}. They are also used to
 *    determine overflow. All container layouts must publish these values for dimensions
 *    that are shrink-wrapped. If a component has raw content (not container items), the
 *    componentLayout must publish these values instead.
 * 
 * @private
 */
Ext.define('Ext.layout.Context', {
    requires: [
        'Ext.perf.Monitor',
        'Ext.util.Queue',
        'Ext.layout.ContextItem',
        'Ext.layout.Layout',
        'Ext.fx.Anim',
        'Ext.fx.Manager'
    ],
    remainingLayouts: 0,
    /**
     * @property {Number} state One of these values:
     *
     *  - 0 - Before run
     *  - 1 - Running
     *  - 2 - Run complete
     */
    state: 0,
    /**
     * @property {Number} cycleWatchDog
     * This value is used to detect layouts that cannot progress by checking the amount of
     * cycles processed. The value should be large enough to satisfy all but exceptionally large
     * layout structures. When the amount of cycles is reached, the layout will fail. This should
     * only be used for debugging, layout failures should be considered as an exceptional
     * occurrence.
     * @private
     * @since 5.1.1
     */
    cycleWatchDog: 200,
    constructor: function(config) {
        var me = this;
        Ext.apply(me, config);
        // holds the ContextItem collection, keyed by element id
        me.items = {};
        // a collection of layouts keyed by layout id
        me.layouts = {};
        // the number of blocks of any kind:
        me.blockCount = 0;
        // the number of cycles that have been run:
        me.cycleCount = 0;
        // the number of flushes to the DOM:
        me.flushCount = 0;
        // the number of layout calculate calls:
        me.calcCount = 0;
        me.animateQueue = me.newQueue();
        me.completionQueue = me.newQueue();
        me.finalizeQueue = me.newQueue();
        me.finishQueue = me.newQueue();
        me.flushQueue = me.newQueue();
        me.invalidateData = {};
        /**
         * @property {Ext.util.Queue} layoutQueue
         * List of layouts to perform.
         */
        me.layoutQueue = me.newQueue();
        // this collection is special because we ensure that there are no parent/child pairs
        // present, only distinct top-level components
        me.invalidQueue = [];
        me.triggers = {
            data: {},
            /*
                layoutId: [
                    { item: contextItem, prop: propertyName }
                ]
                */
            dom: {}
        };
    },
    callLayout: function(layout, methodName) {
        this.currentLayout = layout;
        layout[methodName](this.getCmp(layout.owner));
    },
    cancelComponent: function(comp, isChild, isDestroying) {
        var me = this,
            components = comp,
            isArray = !comp.isComponent,
            length = isArray ? components.length : 1,
            i, k, klen, items, layout, newQueue, oldQueue, entry, temp, ownerCtContext;
        for (i = 0; i < length; ++i) {
            if (isArray) {
                comp = components[i];
            }
            if (isDestroying) {
                if (comp.ownerCt) {
                    // If the component is being destroyed, remove the component's ContextItem
                    // from its parent's contextItem.childItems array
                    ownerCtContext = this.items[comp.ownerCt.el.id];
                    if (ownerCtContext) {
                        Ext.Array.remove(ownerCtContext.childItems, me.getCmp(comp));
                    }
                } else if (comp.rendered) {
                    me.removeEl(comp.el);
                }
            }
            if (!isChild) {
                oldQueue = me.invalidQueue;
                klen = oldQueue.length;
                if (klen) {
                    me.invalidQueue = newQueue = [];
                    for (k = 0; k < klen; ++k) {
                        entry = oldQueue[k];
                        temp = entry.item.target;
                        if (temp !== comp && !temp.up(comp)) {
                            newQueue.push(entry);
                        }
                    }
                }
            }
            layout = comp.componentLayout;
            me.cancelLayout(layout);
            if (!comp.destroying) {
                if (layout.getLayoutItems) {
                    items = layout.getLayoutItems();
                    if (items.length) {
                        me.cancelComponent(items, true);
                    }
                }
                if (comp.isContainer && !comp.collapsed) {
                    layout = comp.layout;
                    me.cancelLayout(layout);
                    items = layout.getVisibleItems();
                    if (items.length) {
                        me.cancelComponent(items, true);
                    }
                }
            }
        }
    },
    cancelLayout: function(layout) {
        var me = this;
        me.completionQueue.remove(layout);
        me.finalizeQueue.remove(layout);
        me.finishQueue.remove(layout);
        me.layoutQueue.remove(layout);
        if (layout.running) {
            me.layoutDone(layout);
        }
        layout.ownerContext = null;
    },
    clearTriggers: function(layout, inDom) {
        var id = layout.id,
            collection = this.triggers[inDom ? 'dom' : 'data'],
            triggers = collection && collection[id],
            length = (triggers && triggers.length) || 0,
            i, item, trigger;
        for (i = 0; i < length; ++i) {
            trigger = triggers[i];
            item = trigger.item;
            collection = inDom ? item.domTriggers : item.triggers;
            delete collection[trigger.prop][id];
        }
    },
    /**
     * Flushes any pending writes to the DOM by calling each ContextItem in the flushQueue.
     */
    flush: function() {
        var me = this,
            items = me.flushQueue.clear(),
            length = items.length,
            i;
        if (length) {
            ++me.flushCount;
            for (i = 0; i < length; ++i) {
                items[i].flush();
            }
        }
    },
    flushAnimations: function() {
        var me = this,
            items = me.animateQueue.clear(),
            len = items.length,
            i;
        if (len) {
            for (i = 0; i < len; i++) {
                // Each Component may refuse to participate in animations.
                // This is used by the BoxReorder plugin which drags a Component,
                // during which that Component must be exempted from layout positioning.
                if (items[i].target.animate !== false) {
                    items[i].flushAnimations();
                }
            }
            // Ensure the first frame fires now to avoid a browser repaint with the elements
            // in the "to" state before they are returned to their "from" state by the animation.
            Ext.fx.Manager.runner();
        }
    },
    flushInvalidates: function() {
        var me = this,
            queue = me.invalidQueue,
            length = queue && queue.length,
            comp, components, entry, i;
        me.invalidQueue = [];
        if (length) {
            components = [];
            for (i = 0; i < length; ++i) {
                comp = (entry = queue[i]).item.target;
                // we filter out-of-body components here but allow them into the queue to
                // ensure that their child components are coalesced out (w/no additional
                // cost beyond our normal effort to avoid parent/child components in the
                // queue)
                if (!comp.container.isDetachedBody) {
                    components.push(comp);
                    if (entry.options) {
                        me.invalidateData[comp.id] = entry.options;
                    }
                }
            }
            me.invalidate(components, null);
        }
    },
    flushLayouts: function(queueName, methodName, dontClear) {
        var me = this,
            layouts = dontClear ? me[queueName].items : me[queueName].clear(),
            length = layouts.length,
            i, layout;
        if (length) {
            for (i = 0; i < length; ++i) {
                layout = layouts[i];
                if (!layout.running) {
                    me.callLayout(layout, methodName);
                }
            }
            me.currentLayout = null;
        }
    },
    /**
     * Returns the ContextItem for a component.
     * @param {Ext.Component} cmp
     */
    getCmp: function(cmp) {
        return this.getItem(cmp, cmp.el);
    },
    /**
     * Returns the ContextItem for an element.
     * @param {Ext.layout.ContextItem} parent
     * @param {Ext.dom.Element} el
     */
    getEl: function(parent, el) {
        var item = this.getItem(el, el, parent);
        if (!item.parent) {
            item.parent = parent;
            // all items share an empty children array (to avoid null checks), so we can
            // only push on to the children array if there is already something there (we
            // copy-on-write):
            if (parent.children.length) {
                parent.children.push(item);
            } else {
                parent.children = [
                    item
                ];
            }
        }
        // now parent has its own children[] (length=1)
        return item;
    },
    /**
     * Get a context item, if a cached item already exists it will
     * be returned.
     * @param {Ext.Component/Ext.dom.Element} target The target.
     * @param {Ext.dom.Element} el The element for the context item. If `target`,
     * is an element, these should be the same.
     * @param {Ext.layout.ContextItem} [componentContext] The owning component
     * context. This is used for element contexts.
     * @return {Ext.layout.ContextItem} The context item.
     */
    getItem: function(target, el, componentContext) {
        var id = el.id,
            items = this.items,
            item;
        item = items[id] || (items[id] = new Ext.layout.ContextItem({
            context: this,
            target: target,
            el: el,
            componentContext: componentContext
        }));
        return item;
    },
    handleFailure: function() {
        // This method should never be called, but is need when layouts fail (hence the
        // "should never"). We just disconnect any of the layouts from the run and return
        // them to the state they would be in had the layout completed properly.
        var layouts = this.layouts,
            layout, key;
        Ext.failedLayouts = (Ext.failedLayouts || 0) + 1;
        for (key in layouts) {
            layout = layouts[key];
            if (layouts.hasOwnProperty(key)) {
                layout.running = false;
                layout.ownerContext = null;
            }
        }
        if (Ext.devMode === 2 && !this.pageAnalyzerMode) {
            Ext.raise('Layout run failed');
        } else {
            Ext.log.error('Layout run failed');
        }
    },
    /**
     * Invalidates one or more components' layouts (component and container). This can be
     * called before run to identify the components that need layout or during the run to
     * restart the layout of a component. This is called internally to flush any queued
     * invalidations at the start of a cycle. If called during a run, it is not expected
     * that new components will be introduced to the layout.
     * 
     * @param {Ext.Component/Array} components An array of Components or a single Component.
     * @param {Boolean} full True if all properties should be invalidated, otherwise only
     *  those calculated by the component should be invalidated.
     */
    invalidate: function(components, full) {
        var me = this,
            isArray = !components.isComponent,
            containerLayoutDone, ownerLayout, firstTime, i, comp, item, items, length, componentLayout, layout, invalidateOptions, token, skipLayout;
        for (i = 0 , length = isArray ? components.length : 1; i < length; ++i) {
            comp = isArray ? components[i] : components;
            if (comp.rendered && !comp.hidden) {
                ownerLayout = comp.ownerLayout;
                componentLayout = comp.componentLayout;
                skipLayout = false;
                if ((!ownerLayout || !ownerLayout.needsItemSize) && comp.liquidLayout) {
                    // our owning layout doesn't need us to run, and our componentLayout
                    // wants to opt out because it uses liquid CSS layout.
                    // We can skip invalidation for this component.
                    skipLayout = true;
                }
                // if we are skipping layout, we can also skip creation of the context
                // item, unless our owner layout needs it to set our size.
                if (!skipLayout || (ownerLayout && ownerLayout.setsItemSize)) {
                    item = me.getCmp(comp);
                    firstTime = !item.state;
                    // If the component has had no changes which necessitate a layout,
                    // do not lay it out.
                    // Temporarily disabled because this breaks dock layout (see EXTJSIV-10251)
                    // if (item.optOut) {
                    //     skipLayout = true;
                    // }
                    layout = (comp.isContainer && !comp.collapsed) ? comp.layout : null;
                    // Extract any invalidate() options for this item.
                    invalidateOptions = me.invalidateData[item.id];
                    delete me.invalidateData[item.id];
                    // We invalidate the contextItem's in a top-down manner so that SizeModel
                    // info for containers is available to their children. This is a critical
                    // optimization since sizeModel determination often requires knowing the
                    // sizeModel of the ownerCt. If this weren't cached as we descend, this
                    // would be an O(N^2) operation! (where N=number of components, or 300+/-
                    // in Themes)
                    token = item.init(full, invalidateOptions);
                }
                if (skipLayout) {
                    
                    continue;
                }
                if (invalidateOptions) {
                    me.processInvalidate(invalidateOptions, item, 'before');
                }
                // Allow the component layout a chance to effect its size model before we
                // recurse down the component hierarchy (since children need to know the
                // size model of their ownerCt).
                if (componentLayout.beforeLayoutCycle) {
                    componentLayout.beforeLayoutCycle(item);
                }
                if (layout && layout.beforeLayoutCycle) {
                    // allow the container layout take a peek as well. Table layout can
                    // influence its children's styling due to the interaction of nesting
                    // table-layout:fixed and auto inside each other without intervening
                    // elements of known size.
                    layout.beforeLayoutCycle(item);
                }
                // Finish up the item-level processing that is based on the size model of
                // the component.
                token = item.initContinue(token);
                // Start this state variable at true, since that is the value we want if
                // they do not apply (i.e., no work of this kind on which to wait).
                containerLayoutDone = true;
                // A ComponentLayout MUST implement getLayoutItems to allow its children
                // to be collected. Ext.container.Container does this, but non-Container
                // Components which manage Components as part of their structure (e.g.,
                // HtmlEditor) must still return child Components via getLayoutItems.
                if (componentLayout.getLayoutItems) {
                    componentLayout.renderChildren();
                    items = componentLayout.getLayoutItems();
                    if (items.length) {
                        me.invalidate(items, true);
                    }
                }
                if (layout) {
                    containerLayoutDone = false;
                    layout.renderChildren();
                    if (layout.needsItemSize || layout.activeItemCount) {
                        // if the layout specified that it needs the layouts of its children
                        // to run, or if the number of "liquid" child layouts is greater
                        // than 0, we need to recurse into the children, since some or
                        // all of them may need their layouts to run.
                        items = layout.getVisibleItems();
                        if (items.length) {
                            me.invalidate(items, true);
                        }
                    }
                }
                // Finish the processing that requires the size models of child items to
                // be determined (and some misc other stuff).
                item.initDone(containerLayoutDone);
                // Inform the layouts that we are about to begin (or begin again) now that
                // the size models of the component and its children are setup.
                me.resetLayout(componentLayout, item, firstTime);
                if (layout) {
                    me.resetLayout(layout, item, firstTime);
                }
                // This has to occur after the component layout has had a chance to begin
                // so that we can determine what kind of animation might be needed. TODO-
                // move this determination into the layout itself.
                item.initAnimation();
                if (invalidateOptions) {
                    me.processInvalidate(invalidateOptions, item, 'after');
                }
            }
        }
        me.currentLayout = null;
    },
    // Returns true is descendant is a descendant of ancestor
    isDescendant: function(ancestor, descendant) {
        var c;
        if (ancestor.isContainer) {
            for (c = descendant.ownerCt; c; c = c.ownerCt) {
                if (c === ancestor) {
                    return true;
                }
            }
        }
        return false;
    },
    layoutDone: function(layout) {
        var ownerContext = layout.ownerContext;
        layout.running = false;
        // Once a component layout completes, we can mark it as "done".
        if (layout.isComponentLayout) {
            if (ownerContext.measuresBox) {
                ownerContext.onBoxMeasured();
            }
            // be sure to release our boxParent
            ownerContext.setProp('done', true);
        } else {
            ownerContext.setProp('containerLayoutDone', true);
        }
        --this.remainingLayouts;
        ++this.progressCount;
    },
    // a layout completion is progress
    newQueue: function() {
        return new Ext.util.Queue();
    },
    processInvalidate: function(options, item, name) {
        var me = this,
            currentLayout;
        // When calling a callback, the currentLayout needs to be adjusted so
        // that whichever layout caused the invalidate is the currentLayout...
        if (options[name]) {
            currentLayout = me.currentLayout;
            me.currentLayout = options.layout || null;
            options[name](item, options);
            me.currentLayout = currentLayout;
        }
    },
    /**
     * Queues a ContextItem to have its {@link Ext.layout.ContextItem#flushAnimations}
     * method called.
     *
     * @param {Ext.layout.ContextItem} item
     * @private
     */
    queueAnimation: function(item) {
        this.animateQueue.add(item);
    },
    /**
     * Queues a layout to have its {@link Ext.layout.Layout#completeLayout} method called.
     *
     * @param {Ext.layout.Layout} layout
     * @private
     */
    queueCompletion: function(layout) {
        this.completionQueue.add(layout);
    },
    /**
     * Queues a layout to have its {@link Ext.layout.Layout#finalizeLayout} method called.
     *
     * @param {Ext.layout.Layout} layout
     * @private
     */
    queueFinalize: function(layout) {
        this.finalizeQueue.add(layout);
    },
    /**
     * Queues a ContextItem for the next flush to the DOM. This should only be called by
     * the {@link Ext.layout.ContextItem} class.
     *
     * @param {Ext.layout.ContextItem} item
     * @param {Boolean} [replace=false] If an item by that ID is already queued, replace it.
     * @private
     */
    queueFlush: function(item, replace) {
        this.flushQueue.add(item, replace);
    },
    chainFns: function(oldOptions, newOptions, funcName) {
        var me = this,
            oldLayout = oldOptions.layout,
            newLayout = newOptions.layout,
            oldFn = oldOptions[funcName],
            newFn = newOptions[funcName];
        // Call newFn last so it can get the final word on things... also, the "this"
        // pointer will be passed correctly by createSequence with oldFn first.
        return function(contextItem) {
            var prev = me.currentLayout;
            if (oldFn) {
                me.currentLayout = oldLayout;
                oldFn.call(oldOptions.scope || oldOptions, contextItem, oldOptions);
            }
            me.currentLayout = newLayout;
            newFn.call(newOptions.scope || newOptions, contextItem, newOptions);
            me.currentLayout = prev;
        };
    },
    purgeInvalidates: function() {
        var me = this,
            newQueue = [],
            oldQueue = me.invalidQueue,
            oldLength = oldQueue.length,
            oldIndex, newIndex, newEntry, newComp, oldEntry, oldComp, keep;
        for (oldIndex = 0; oldIndex < oldLength; ++oldIndex) {
            oldEntry = oldQueue[oldIndex];
            oldComp = oldEntry.item.target;
            keep = true;
            for (newIndex = newQueue.length; newIndex--; ) {
                newEntry = newQueue[newIndex];
                newComp = newEntry.item.target;
                if (oldComp.isLayoutChild(newComp)) {
                    keep = false;
                    break;
                }
                if (newComp.isLayoutChild(oldComp)) {
                    Ext.Array.erase(newQueue, newIndex, 1);
                }
            }
            if (keep) {
                newQueue.push(oldEntry);
            }
        }
        me.invalidQueue = newQueue;
    },
    /**
     * Queue a component (and its tree) to be invalidated on the next cycle.
     *
     * @param {Ext.Component/Ext.layout.ContextItem} item The component or ContextItem
     * to invalidate.
     * @param {Object} options An object describing how to handle the invalidation (see
     *  {@link Ext.layout.ContextItem#invalidate} for details).
     * @private
     */
    queueInvalidate: function(item, options) {
        var me = this,
            newQueue = [],
            oldQueue = me.invalidQueue,
            index = oldQueue.length,
            comp, old, oldComp, oldOptions, oldState;
        if (item.isComponent) {
            comp = item;
            item = me.items[comp.el.id];
            if (item) {
                item.recalculateSizeModel();
            } else {
                item = me.getCmp(comp);
            }
        } else {
            comp = item.target;
        }
        item.invalid = true;
        // See if comp is contained by any component already in the queue (ignore comp if
        // that is the case). Eliminate any components in the queue that are contained by
        // comp (by not adding them to newQueue).
        while (index--) {
            old = oldQueue[index];
            oldComp = old.item.target;
            if (!comp.isFloating && comp.up(oldComp)) {
                return;
            }
            // oldComp contains comp, so this invalidate is redundant
            if (oldComp === comp) {
                // if already in the queue, update the options...
                if (!(oldOptions = old.options)) {
                    old.options = options;
                } else if (options) {
                    if (options.widthModel) {
                        oldOptions.widthModel = options.widthModel;
                    }
                    if (options.heightModel) {
                        oldOptions.heightModel = options.heightModel;
                    }
                    if (!(oldState = oldOptions.state)) {
                        oldOptions.state = options.state;
                    } else if (options.state) {
                        Ext.apply(oldState, options.state);
                    }
                    if (options.before) {
                        oldOptions.before = me.chainFns(oldOptions, options, 'before');
                    }
                    if (options.after) {
                        oldOptions.after = me.chainFns(oldOptions, options, 'after');
                    }
                }
                // leave the old queue alone now that we've update this comp's entry...
                return;
            }
            if (!oldComp.isLayoutChild(comp)) {
                newQueue.push(old);
            }
        }
        // comp does not contain oldComp
        // else if (oldComp isDescendant of comp) skip
        // newQueue contains only those components not a descendant of comp
        // to get here, comp must not be a child of anything already in the queue, so it
        // needs to be added along with its "options":
        newQueue.push({
            item: item,
            options: options
        });
        me.invalidQueue = newQueue;
    },
    queueItemLayouts: function(item) {
        var comp = item.isComponent ? item : item.target,
            layout = comp.componentLayout;
        if (!layout.pending && !layout.invalid && !layout.done) {
            this.queueLayout(layout);
        }
        layout = comp.layout;
        if (layout && !layout.pending && !layout.invalid && !layout.done && !comp.collapsed) {
            this.queueLayout(layout);
        }
    },
    /**
     * Queues a layout for the next calculation cycle. This should not be called if the
     * layout is done, blocked or already in the queue. The only classes that should call
     * this method are this class and {@link Ext.layout.ContextItem}.
     *
     * @param {Ext.layout.Layout} layout The layout to add to the queue.
     * @private
     */
    queueLayout: function(layout) {
        this.layoutQueue.add(layout);
        layout.pending = true;
    },
    /**
     * Removes the ContextItem for an element from the cache and from the parent's
     * "children" array.
     * @param {Ext.dom.Element} el
     * @param {Ext.layout.ContextItem} parent
     */
    removeEl: function(el, parent) {
        var id = el.id,
            children = parent ? parent.children : null,
            items = this.items;
        if (children) {
            Ext.Array.remove(children, items[id]);
        }
        delete items[id];
    },
    /**
     * Resets the given layout object. This is called at the start of the run and can also
     * be called during the run by calling {@link #invalidate}.
     */
    resetLayout: function(layout, ownerContext, firstTime) {
        var me = this;
        me.currentLayout = layout;
        layout.done = false;
        layout.pending = true;
        layout.firedTriggers = 0;
        me.layoutQueue.add(layout);
        if (firstTime) {
            me.layouts[layout.id] = layout;
            // track the layout for this run by its id
            layout.running = true;
            if (layout.finishedLayout) {
                me.finishQueue.add(layout);
            }
            // reset or update per-run counters:
            ++me.remainingLayouts;
            ++layout.layoutCount;
            // the number of whole layouts run for the layout
            layout.ownerContext = ownerContext;
            layout.beginCount = 0;
            // the number of beginLayout calls
            layout.blockCount = 0;
            // the number of blocks set for the layout
            layout.calcCount = 0;
            // the number of times calculate is called
            layout.triggerCount = 0;
            // the number of triggers set for the layout
            if (!layout.initialized) {
                layout.initLayout();
            }
            layout.beginLayout(ownerContext);
        } else {
            ++layout.beginCount;
            if (!layout.running) {
                // back into the mahem with this one:
                ++me.remainingLayouts;
                layout.running = true;
                layout.ownerContext = ownerContext;
                if (layout.isComponentLayout) {
                    // this one is fun... if we call setProp('done', false) that would still
                    // trigger/unblock layouts, but what layouts are really looking for with
                    // this property is for it to go to true, not just be set to a value...
                    ownerContext.unsetProp('done');
                }
                // and it needs to be removed from the completion and/or finalize queues...
                me.completionQueue.remove(layout);
                me.finalizeQueue.remove(layout);
            }
        }
        layout.beginLayoutCycle(ownerContext, firstTime);
    },
    /**
     * Runs the layout calculations. This can be called only once on this object.
     * @return {Boolean} True if all layouts were completed, false if not.
     */
    run: function() {
        var me = this,
            flushed = false,
            watchDog = me.cycleWatchDog;
        me.purgeInvalidates();
        me.flushInvalidates();
        me.state = 1;
        me.totalCount = me.layoutQueue.getCount();
        // We may start with unflushed data placed by beginLayout calls. Since layouts may
        // use setProp as a convenience, even in a write phase, we don't want to transition
        // to a read phase with unflushed data since we can write it now "cheaply". Also,
        // these value could easily be needed in the DOM in order to really get going with
        // the calculations. In particular, fixed (configured) dimensions fall into this
        // category.
        me.flush();
        // While we have layouts that have not completed...
        while ((me.remainingLayouts || me.invalidQueue.length) && watchDog--) {
            if (me.invalidQueue.length) {
                me.flushInvalidates();
            }
            // if any of them can run right now, run them
            if (me.runCycle()) {
                flushed = false;
            }
            // progress means we probably need to flush something
            // but not all progress appears in the flushQueue (e.g. 'contentHeight')
            else if (!flushed) {
                // as long as we are making progress, flush updates to the DOM and see if
                // that triggers or unblocks any layouts...
                me.flush();
                flushed = true;
                // all flushed now, so more progress is required
                me.flushLayouts('completionQueue', 'completeLayout');
            } else if (!me.invalidQueue.length) {
                // after a flush, we must make progress or something is WRONG
                me.state = 2;
                break;
            }
            if (!(me.remainingLayouts || me.invalidQueue.length)) {
                me.flush();
                me.flushLayouts('completionQueue', 'completeLayout');
                me.flushLayouts('finalizeQueue', 'finalizeLayout');
            }
        }
        return me.runComplete();
    },
    runComplete: function() {
        var me = this;
        me.state = 2;
        if (me.remainingLayouts) {
            me.handleFailure();
            return false;
        }
        me.flush();
        // Call finishedLayout on all layouts, but do not clear the queue.
        me.flushLayouts('finishQueue', 'finishedLayout', true);
        // Call notifyOwner on all layouts and then clear the queue.
        me.flushLayouts('finishQueue', 'notifyOwner');
        me.flush();
        // in case any setProp calls were made
        me.flushAnimations();
        return true;
    },
    /**
     * Performs one layout cycle by calling each layout in the layout queue.
     * @return {Boolean} True if some progress was made, false if not.
     * @protected
     */
    runCycle: function() {
        var me = this,
            layouts = me.layoutQueue.clear(),
            length = layouts.length,
            i;
        ++me.cycleCount;
        // This value is incremented by ContextItem#setProp whenever new values are set
        // (thereby detecting forward progress):
        me.progressCount = 0;
        for (i = 0; i < length; ++i) {
            me.runLayout(me.currentLayout = layouts[i]);
        }
        me.currentLayout = null;
        return me.progressCount > 0;
    },
    /**
     * Runs one layout as part of a cycle.
     * @private
     */
    runLayout: function(layout) {
        var me = this,
            ownerContext = me.getCmp(layout.owner);
        layout.pending = false;
        if (ownerContext.state.blocks) {
            return;
        }
        // We start with the assumption that the layout will finish and if it does not, it
        // must clear this flag. It turns out this is much simpler than knowing when a layout
        // is done (100% correctly) when base classes and derived classes are collaborating.
        // Knowing that some part of the layout is not done is much more obvious.
        layout.done = true;
        ++layout.calcCount;
        ++me.calcCount;
        layout.calculate(ownerContext);
        if (layout.done) {
            me.layoutDone(layout);
            if (layout.completeLayout) {
                me.queueCompletion(layout);
            }
            if (layout.finalizeLayout) {
                me.queueFinalize(layout);
            }
        } else if (!layout.pending && !layout.invalid && !(layout.blockCount + layout.triggerCount - layout.firedTriggers)) {
            // A layout that is not done and has no blocks or triggers that will queue it
            // automatically, must be queued now:
            me.queueLayout(layout);
        }
    },
    /* eslint-disable max-len */
    /**
     * Set the size of a component, element or composite or an array of components or elements.
     * @param {Ext.Component/Ext.Component[]/Ext.dom.Element/Ext.dom.Element[]/Ext.dom.CompositeElement} item
     * The item(s) to size.
     * @param {Number} width The new width to set (ignored if undefined or NaN).
     * @param {Number} height The new height to set (ignored if undefined or NaN).
     */
    setItemSize: function(item, width, height) {
        var items = item,
            len = 1,
            contextItem, i;
        // NOTE: we don't pre-check for validity because:
        //  - maybe only one dimension is valid
        //  - the diagnostics layer will track the setProp call to help find who is trying
        //      (but failing) to set a property
        //  - setProp already checks this anyway
        if (item.isComposite) {
            items = item.elements;
            len = items.length;
            item = items[0];
        } else if (!item.dom && !item.el) {
            // array by process of elimination
            len = items.length;
            item = items[0];
        }
        // else len = 1 and items = item (to avoid error on "items[++i]")
        for (i = 0; i < len; ) {
            contextItem = this.get(item);
            contextItem.setSize(width, height);
            item = items[++i];
        }
    },
    // this accomodation avoids making an array of 1
    /* eslint-enable max-len */
    //-------------------------------------------------------------------------
    // Diagnostics
    debugHooks: {
        $enabled: false,
        // off by default
        pageAnalyzerMode: true,
        logOn: {
            // boxParent: true,
            // calculate: true,
            // cancelComponent: true,
            // cancelLayout: true,
            // doInvalidate: true,
            // flush: true,
            // flushInvalidate: true,
            // invalidate: true,
            // initItem: true,
            // layoutDone: true,
            // queueLayout: true,
            // resetLayout: true,
            // runCycle: true,
            // setProp: true,
            0: 0
        },
        // profileLayoutsByType: true,
        // reportOnSuccess: true,
        cancelComponent: function(comp) {
            if (this.logOn.cancelComponent) {
                Ext.log('cancelCmp: ', comp.id);
            }
            this.callParent(arguments);
        },
        cancelLayout: function(layout) {
            if (this.logOn.cancelLayout) {
                Ext.log('cancelLayout: ', this.getLayoutName(layout));
            }
            this.callParent(arguments);
        },
        callLayout: function(layout, methodName) {
            var accum = this.accumByType[layout.type],
                frame = accum && accum.enter();
            this.callParent(arguments);
            if (accum) {
                frame.leave();
            }
        },
        checkRemainingLayouts: function() {
            var me = this,
                expected = 0,
                key, layout;
            for (key in me.layouts) {
                layout = me.layouts[key];
                if (me.layouts.hasOwnProperty(key) && layout.running) {
                    ++expected;
                }
            }
            if (me.remainingLayouts !== expected) {
                Ext.raise({
                    msg: 'Bookkeeping error me.remainingLayouts'
                });
            }
        },
        flush: function() {
            var items;
            if (this.logOn.flush) {
                items = this.flushQueue;
                Ext.log('--- Flush ', items && items.getCount());
            }
            return this.callParent(arguments);
        },
        flushInvalidates: function() {
            var ret;
            if (this.logOn.flushInvalidate) {
                Ext.log('>> flushInvalidates');
            }
            ret = this.callParent(arguments);
            if (this.logOn.flushInvalidate) {
                Ext.log('<< flushInvalidates');
            }
            return ret;
        },
        getCmp: function(target) {
            var ret = this.callParent(arguments);
            if (!ret.wrapsComponent) {
                Ext.raise({
                    msg: target.id + ' is not a component'
                });
            }
            return ret;
        },
        getEl: function(parent, target) {
            var ret = this.callParent(arguments);
            if (ret && ret.wrapsComponent) {
                Ext.raise({
                    msg: parent.id + '/' + target.id + ' is a component (expected element)'
                });
            }
            return ret;
        },
        getLayoutName: function(layout) {
            return layout.owner.id + '<' + layout.type + '>';
        },
        layoutDone: function(layout) {
            var me = this,
                name = me.getLayoutName(layout);
            if (me.logOn.layoutDone) {
                Ext.log('layoutDone: ', name, ' ( ', me.remainingLayouts, ' running)');
            }
            if (!layout.running) {
                Ext.raise({
                    msg: name + ' is already done'
                });
            }
            if (!me.remainingLayouts) {
                Ext.raise({
                    msg: name + ' finished but no layouts are running'
                });
            }
            me.callParent(arguments);
        },
        layoutTreeHasFailures: function(layout, reported) {
            var me = this;
            function hasFailure(lo) {
                var failure = !lo.done,
                    key, childLayout;
                if (lo.done) {
                    for (key in me.layouts) {
                        if (me.layouts.hasOwnProperty(key)) {
                            childLayout = me.layouts[key];
                            if (childLayout.owner.ownerLayout === lo) {
                                if (hasFailure(childLayout)) {
                                    failure = true;
                                }
                            }
                        }
                    }
                }
                return failure;
            }
            if (hasFailure(layout)) {
                return true;
            }
            function markReported(lo) {
                var key, childLayout;
                reported[lo.id] = 1;
                for (key in me.layouts) {
                    if (me.layouts.hasOwnProperty(key)) {
                        childLayout = me.layouts[key];
                        if (childLayout.owner.ownerLayout === lo) {
                            markReported(childLayout);
                        }
                    }
                }
            }
            markReported(layout);
            return false;
        },
        queueLayout: function(layout) {
            if (layout.done || layout.blockCount || layout.pending) {
                Ext.raise({
                    msg: this.getLayoutName(layout) + ' should not be queued for layout'
                });
            }
            if (this.logOn.queueLayout) {
                Ext.log('Queue ', this.getLayoutName(layout));
            }
            return this.callParent(arguments);
        },
        reportLayoutResult: function(layout, reported) {
            var me = this,
                owner = layout.owner,
                ownerContext = me.getCmp(owner),
                blockedBy = [],
                triggeredBy = [],
                key, value, i, length, childLayout, item, setBy, info;
            reported[layout.id] = 1;
            for (key in layout.blockedBy) {
                if (layout.blockedBy.hasOwnProperty(key)) {
                    blockedBy.push(layout.blockedBy[key]);
                }
            }
            blockedBy.sort();
            for (key in me.triggersByLayoutId[layout.id]) {
                if (me.triggersByLayoutId[layout.id].hasOwnProperty(key)) {
                    value = me.triggersByLayoutId[layout.id][key];
                    triggeredBy.push({
                        name: key,
                        info: value
                    });
                }
            }
            triggeredBy.sort(function(a, b) {
                return a.name < b.name ? -1 : (b.name < a.name ? 1 : 0);
            });
            Ext.log({
                indent: 1
            }, (layout.done ? '++' : '--'), me.getLayoutName(layout), (ownerContext.isBoxParent ? ' [isBoxParent]' : ''), (// eslint-disable-next-line max-len
            ownerContext.boxChildren ? ' - boxChildren: ' + ownerContext.state.boxesMeasured + '/' + ownerContext.boxChildren.length : ''), ownerContext.boxParent ? (' - boxParent: ' + ownerContext.boxParent.id) : '', ' - size: ', ownerContext.widthModel.name, '/', ownerContext.heightModel.name);
            if (!layout.done || me.reportOnSuccess) {
                if (blockedBy.length) {
                    ++Ext.log.indent;
                    Ext.log({
                        indent: 1
                    }, 'blockedBy:  count=', layout.blockCount);
                    length = blockedBy.length;
                    for (i = 0; i < length; i++) {
                        Ext.log(blockedBy[i]);
                    }
                    Ext.log.indent -= 2;
                }
                if (triggeredBy.length) {
                    ++Ext.log.indent;
                    Ext.log({
                        indent: 1
                    }, 'triggeredBy: count=' + layout.triggerCount);
                    length = triggeredBy.length;
                    for (i = 0; i < length; i++) {
                        info = value.info || value;
                        item = info.item;
                        setBy = (item.setBy && item.setBy[info.name]) || '?';
                        value = triggeredBy[i];
                        Ext.log(value.name, ' (', item.props[info.name], ') dirty: ', (item.dirty ? !!item.dirty[info.name] : false), ', setBy: ', setBy);
                    }
                    Ext.log.indent -= 2;
                }
            }
            for (key in me.layouts) {
                if (me.layouts.hasOwnProperty(key)) {
                    childLayout = me.layouts[key];
                    if (!childLayout.done && childLayout.owner.ownerLayout === layout) {
                        me.reportLayoutResult(childLayout, reported);
                    }
                }
            }
            for (key in me.layouts) {
                if (me.layouts.hasOwnProperty(key)) {
                    childLayout = me.layouts[key];
                    if (childLayout.done && childLayout.owner.ownerLayout === layout) {
                        me.reportLayoutResult(childLayout, reported);
                    }
                }
            }
            --Ext.log.indent;
        },
        resetLayout: function(layout) {
            var me = this,
                type = layout.type,
                name = me.getLayoutName(layout),
                accum = me.accumByType[type],
                frame;
            if (me.logOn.resetLayout) {
                Ext.log('resetLayout: ', name, ' ( ', me.remainingLayouts, ' running)');
            }
            if (!me.state) {
                // if (first time ... before run)
                if (!accum && me.profileLayoutsByType) {
                    me.accumByType[type] = accum = Ext.Perf.get('layout_' + layout.type);
                }
                me.numByType[type] = (me.numByType[type] || 0) + 1;
            }
            frame = accum && accum.enter();
            me.callParent(arguments);
            if (accum) {
                frame.leave();
            }
            me.checkRemainingLayouts();
        },
        round: function(t) {
            return Math.round(t * 1000) / 1000;
        },
        run: function() {
            var me = this,
                ret, time, key, i, layout, boxParent, children, n, reported, unreported, calcs, total, calcsLength, calc;
            me.accumByType = {};
            me.calcsByType = {};
            me.numByType = {};
            me.timesByType = {};
            me.triggersByLayoutId = {};
            Ext.log.indentSize = 3;
            Ext.log('==================== LAYOUT ====================');
            time = Ext.perf.getTimestamp();
            ret = me.callParent(arguments);
            time = Ext.perf.getTimestamp() - time;
            if (me.logOn.boxParent && me.boxParents) {
                for (key in me.boxParents) {
                    if (me.boxParents.hasOwnProperty(key)) {
                        boxParent = me.boxParents[key];
                        children = boxParent.boxChildren;
                        n = children.length;
                        Ext.log('boxParent: ', boxParent.id);
                        for (i = 0; i < n; ++i) {
                            Ext.log(' --> ', children[i].id);
                        }
                    }
                }
            }
            if (ret) {
                Ext.log('----------------- SUCCESS -----------------');
            } else {
                Ext.log({
                    level: 'error'
                }, '----------------- FAILURE -----------------');
            }
            for (key in me.layouts) {
                if (me.layouts.hasOwnProperty(key)) {
                    layout = me.layouts[key];
                    if (layout.running) {
                        Ext.log.error('Layout left running: ', me.getLayoutName(layout));
                    }
                    if (layout.ownerContext) {
                        Ext.log.error('Layout left connected: ', me.getLayoutName(layout));
                    }
                }
            }
            if (!ret || me.reportOnSuccess) {
                reported = {};
                unreported = 0;
                for (key in me.layouts) {
                    if (me.layouts.hasOwnProperty(key)) {
                        layout = me.layouts[key];
                        if (me.items[layout.owner.el.id].isTopLevel) {
                            if (me.reportOnSuccess || me.layoutTreeHasFailures(layout, reported)) {
                                me.reportLayoutResult(layout, reported);
                            }
                        }
                    }
                }
                // Just in case we missed any layouts...
                for (key in me.layouts) {
                    if (me.layouts.hasOwnProperty(key)) {
                        layout = me.layouts[key];
                        if (!reported[layout.id]) {
                            if (!unreported) {
                                Ext.log('----- Unreported!! -----');
                            }
                            ++unreported;
                            me.reportLayoutResult(layout, reported);
                        }
                    }
                }
            }
            Ext.log('Cycles: ', me.cycleCount, ', Flushes: ', me.flushCount, ', Calculates: ', me.calcCount, ' in ', me.round(time), ' msec');
            Ext.log('Calculates by type:');
            /* Ext.Object.each(me.numByType, function(type, total) {
                Ext.log(type, ': ', total, ' in ', me.calcsByType[type], ' tries (',
                    Math.round(me.calcsByType[type] / total * 10) / 10, 'x) at ',
                    me.round(me.timesByType[type]), ' msec (avg ',
                    me.round(me.timesByType[type] / me.calcsByType[type]), ' msec)');
            }); */
            calcs = [];
            for (key in me.numByType) {
                if (me.numByType.hasOwnProperty(key)) {
                    total = me.numByType[key];
                    calcs.push({
                        type: key,
                        total: total,
                        calcs: me.calcsByType[key],
                        multiple: Math.round(me.calcsByType[key] / total * 10) / 10,
                        calcTime: me.round(me.timesByType[key]),
                        avgCalcTime: me.round(me.timesByType[key] / me.calcsByType[key])
                    });
                }
            }
            calcs.sort(function(a, b) {
                return b.calcTime - a.calcTime;
            });
            calcsLength = calcs.length;
            for (i = 0; i < calcsLength; i++) {
                calc = calcs[i];
                Ext.log(calc.type, ': ', calc.total, ' in ', calc.calcs, ' tries (', calc.multiple, 'x) at ', calc.calcTime, ' msec (avg ', calc.avgCalcTime, ' msec)');
            }
            return ret;
        },
        runCycle: function() {
            if (this.logOn.runCycle) {
                Ext.log('>>> Cycle ', this.cycleCount, ' (queue length: ', this.layoutQueue.length, ')');
            }
            return this.callParent(arguments);
        },
        runLayout: function(layout) {
            var me = this,
                type = layout.type,
                accum = me.accumByType[type],
                frame, ret, time;
            if (me.logOn.calculate) {
                Ext.log('-- calculate ', this.getLayoutName(layout));
            }
            frame = accum && accum.enter();
            time = Ext.perf.getTimestamp();
            ret = me.callParent(arguments);
            time = Ext.perf.getTimestamp() - time;
            if (accum) {
                frame.leave();
            }
            me.calcsByType[type] = (me.calcsByType[type] || 0) + 1;
            me.timesByType[type] = (me.timesByType[type] || 0) + time;
            /* add a / to the front of this line to enable layout completion logging
            if (layout.done) {
                var ownerContext = me.getCmp(layout.owner),
                    props = ownerContext.props;

                if (layout.isComponentLayout) {
                    Ext.log('complete ', layout.owner.id, ':', type, ' w=',props.width,
                            ' h=', props.height);
                } else {
                    Ext.log('complete ', layout.owner.id, ':', type, ' cw=',props.contentWidth,
                            ' ch=', props.contentHeight);
                }
            } */
            return ret;
        }
    }
});
// End Diagnostics

// @define Ext.layout.SizePolicy
/**
 * This class describes how a layout will interact with a component it manages.
 * 
 * There are special instances of this class stored as static properties to avoid object
 * instantiation. All instances of this class should be treated as readonly objects.
 *
 * @class Ext.layout.SizePolicy
 * @protected
 */
/**
 * @property {Boolean} readsWidth
 * Indicates that the `width` of the component is consumed.
 * @readonly
 */
/**
 * @property {Boolean} readsHeight
 * Indicates that the `height` of the component is consumed.
 * @readonly
 */
/**
 * @property {Boolean} setsWidth
 * Indicates that the `width` of the component will be set (i.e., calculated).
 * @readonly
 */
/**
 * @property {Boolean} setsHeight
 * Indicates that the `height` of the component will be set (i.e., calculated).
 * @readonly
 */

/**
 * Component layout for components which maintain an inner body element which must be resized
 * to synchronize with the Component size.
 * @private
 */
Ext.define('Ext.layout.component.Body', {
    extend: 'Ext.layout.component.Auto',
    alias: 'layout.body',
    type: 'body',
    beginLayout: function(ownerContext) {
        this.callParent(arguments);
        ownerContext.bodyContext = ownerContext.getEl('body');
    },
    beginLayoutCycle: function(ownerContext, firstCycle) {
        var me = this,
            lastWidthModel = me.lastWidthModel,
            lastHeightModel = me.lastHeightModel,
            body = me.owner.body;
        me.callParent(arguments);
        if (lastWidthModel && lastWidthModel.fixed && ownerContext.widthModel.shrinkWrap) {
            body.setWidth(null);
        }
        if (lastHeightModel && lastHeightModel.fixed && ownerContext.heightModel.shrinkWrap) {
            body.setHeight(null);
        }
    },
    // Padding is exciting here because we have 2 el's: owner.el and owner.body. Content
    // size always includes the padding of the targetEl, which should be owner.body. But
    // it is common to have padding on owner.el also (such as a panel header), so we need
    // to do some more padding work if targetContext is not owner.el. The base class has
    // already handled the ownerContext's frameInfo (border+framing) so all that is left
    // is padding.
    calculateOwnerHeightFromContentHeight: function(ownerContext, contentHeight) {
        var height = this.callParent(arguments);
        if (ownerContext.targetContext !== ownerContext) {
            height += ownerContext.getPaddingInfo().height;
        }
        return height;
    },
    calculateOwnerWidthFromContentWidth: function(ownerContext, contentWidth) {
        var width = this.callParent(arguments);
        if (ownerContext.targetContext !== ownerContext) {
            width += ownerContext.getPaddingInfo().width;
        }
        return width;
    },
    measureContentWidth: function(ownerContext) {
        return ownerContext.bodyContext.setWidth(ownerContext.bodyContext.el.dom.offsetWidth, false);
    },
    measureContentHeight: function(ownerContext) {
        return ownerContext.bodyContext.setHeight(ownerContext.bodyContext.el.dom.offsetHeight, false);
    },
    publishInnerHeight: function(ownerContext, height) {
        var innerHeight = height - ownerContext.getFrameInfo().height,
            targetContext = ownerContext.targetContext;
        if (targetContext !== ownerContext) {
            innerHeight -= ownerContext.getPaddingInfo().height;
        }
        // return the value here, it may get used in a subclass
        return ownerContext.bodyContext.setHeight(innerHeight, !ownerContext.heightModel.natural);
    },
    publishInnerWidth: function(ownerContext, width) {
        var innerWidth = width - ownerContext.getFrameInfo().width,
            targetContext = ownerContext.targetContext;
        if (targetContext !== ownerContext) {
            innerWidth -= ownerContext.getPaddingInfo().width;
        }
        ownerContext.bodyContext.setWidth(innerWidth, !ownerContext.widthModel.natural);
    }
});

/**
 * Component layout for Ext.form.FieldSet components
 * @private
 */
Ext.define('Ext.layout.component.FieldSet', {
    extend: 'Ext.layout.component.Body',
    alias: [
        'layout.fieldset'
    ],
    type: 'fieldset',
    defaultCollapsedWidth: 100,
    beforeLayoutCycle: function(ownerContext) {
        if (ownerContext.target.collapsed) {
            ownerContext.heightModel = this.sizeModels.shrinkWrap;
        }
    },
    beginLayout: function(ownerContext) {
        var legend = this.owner.legend;
        this.callParent([
            ownerContext
        ]);
        if (legend) {
            ownerContext.legendContext = ownerContext.context.getCmp(legend);
        }
    },
    beginLayoutCycle: function(ownerContext) {
        var target = ownerContext.target,
            lastSize;
        this.callParent(arguments);
        // Each time we begin (2nd+ would be due to invalidate) we need to publish the
        // known contentHeight if we are collapsed:
        //
        if (target.collapsed) {
            ownerContext.setContentHeight(0);
            // if we're collapsed, ignore a minHeight because it's likely going to
            // be greater than the collapsed height
            ownerContext.restoreMinHeight = target.minHeight;
            delete target.minHeight;
            // If we are also shrinkWrap width, we must provide a contentWidth (since the
            // container layout is not going to run).
            //
            if (ownerContext.widthModel.shrinkWrap) {
                lastSize = this.lastComponentSize;
                ownerContext.setContentWidth((lastSize && lastSize.contentWidth) || this.defaultCollapsedWidth);
            }
        }
    },
    finishedLayout: function(ownerContext) {
        var owner = this.owner,
            restore = ownerContext.restoreMinHeight;
        this.callParent(arguments);
        if (restore) {
            owner.minHeight = restore;
        }
    },
    calculateOwnerWidthFromContentWidth: function(ownerContext, contentWidth) {
        var legendContext = ownerContext.legendContext;
        if (legendContext) {
            contentWidth = Math.max(contentWidth, legendContext.getProp('width'));
        }
        return this.callParent([
            ownerContext,
            contentWidth
        ]);
    },
    calculateOwnerHeightFromContentHeight: function(ownerContext, contentHeight) {
        var border = ownerContext.getBorderInfo(),
            legendContext = ownerContext.legendContext;
        // Height of fieldset is content height plus top border width (which is either the
        // legend height or top border width) plus bottom border width
        return ownerContext.getProp('contentHeight') + ownerContext.getPaddingInfo().height + (// In IE8m the top padding is on the body el
        Ext.isIE8 ? ownerContext.bodyContext.getPaddingInfo().top : 0) + (legendContext ? legendContext.getProp('height') : border.top) + border.bottom;
    },
    publishInnerHeight: function(ownerContext, height) {
        // Subtract the legend off here and pass it up to the body
        // We do this because we don't want to set an incorrect body height
        // and then setting it again with the correct value
        var legendContext = ownerContext.legendContext,
            legendHeight = 0;
        if (legendContext) {
            legendHeight = legendContext.getProp('height');
        }
        if (legendHeight === undefined) {
            this.done = false;
        } else {
            this.callParent([
                ownerContext,
                height - legendHeight
            ]);
        }
    },
    getLayoutItems: function() {
        var legend = this.owner.legend;
        return legend ? [
            legend
        ] : [];
    }
});

/**
 * This is a layout that inherits the anchoring of {@link Ext.layout.container.Anchor} and adds the
 * ability for x/y positioning using the standard x and y component config options.
 *
 * This class is intended to be extended or created via the
 * {@link Ext.container.Container#layout layout} configuration property.
 * See {@link Ext.container.Container#layout} for additional details.
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Absolute Layout',
 *         width: 300,
 *         height: 275,
 *         layout: {
 *             type: 'absolute'
 *             // layout-specific configs go here
 *             //itemCls: 'x-abs-layout-item',
 *         },
 *         url:'save-form.php',
 *         defaultType: 'textfield',
 *         items: [{
 *             x: 10,
 *             y: 10,
 *             xtype:'label',
 *             text: 'Send To:'
 *         },{
 *             x: 80,
 *             y: 10,
 *             name: 'to',
 *             anchor:'90%'  // anchor width by percentage
 *         },{
 *             x: 10,
 *             y: 40,
 *             xtype:'label',
 *             text: 'Subject:'
 *         },{
 *             x: 80,
 *             y: 40,
 *             name: 'subject',
 *             anchor: '90%'  // anchor width by percentage
 *         },{
 *             x:0,
 *             y: 80,
 *             xtype: 'textareafield',
 *             name: 'msg',
 *             anchor: '100% 100%'  // anchor width and height
 *         }],
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.layout.container.Absolute', {
    /* Begin Definitions */
    alias: 'layout.absolute',
    extend: 'Ext.layout.container.Anchor',
    alternateClassName: 'Ext.layout.AbsoluteLayout',
    /* End Definitions */
    targetCls: Ext.baseCSSPrefix + 'abs-layout-ct',
    itemCls: Ext.baseCSSPrefix + 'abs-layout-item',
    type: 'absolute',
    /**
     * @private
     */
    adjustWidthAnchor: function(width, childContext) {
        var padding = this.targetPadding,
            x = childContext.getStyle('left');
        return width - x + padding.left;
    },
    /**
     * @private
     */
    adjustHeightAnchor: function(height, childContext) {
        var padding = this.targetPadding,
            y = childContext.getStyle('top');
        return height - y + padding.top;
    },
    isItemShrinkWrap: function(item) {
        return true;
    },
    onContentChange: function(comp, context) {
        var ret = false;
        // In a vast majority of cases we don't need to run the layout
        // when the content changes.
        if (comp.anchor && context && context.show) {
            ret = this.callParent([
                comp,
                context
            ]);
        }
        return ret;
    },
    beginLayout: function(ownerContext) {
        var me = this,
            target = me.getTarget();
        me.callParent([
            ownerContext
        ]);
        // Do not set position: relative; when the absolute layout target is the body
        if (target.dom !== document.body) {
            target.position();
        }
        me.targetPadding = ownerContext.targetContext.getPaddingInfo();
    },
    isItemBoxParent: function(itemContext) {
        return true;
    },
    calculateContentSize: function(ownerContext, dimensions) {
        var me = this,
            containerDimensions = (dimensions || 0) | ((ownerContext.widthModel.shrinkWrap ? 1 : 0) | (ownerContext.heightModel.shrinkWrap ? 2 : 0)),
            calcWidth = (containerDimensions & 1) || undefined,
            calcHeight = (containerDimensions & 2) || undefined,
            childItems = ownerContext.childItems,
            length = childItems.length,
            contentHeight = 0,
            contentWidth = 0,
            needed = 0,
            props = ownerContext.props,
            targetPadding, child, childContext, height, i, margins, width;
        if (calcWidth) {
            if (isNaN(props.contentWidth)) {
                ++needed;
            } else {
                calcWidth = undefined;
            }
        }
        if (calcHeight) {
            if (isNaN(props.contentHeight)) {
                ++needed;
            } else {
                calcHeight = undefined;
            }
        }
        if (needed) {
            for (i = 0; i < length; ++i) {
                childContext = childItems[i];
                child = childContext.target;
                height = calcHeight && childContext.getProp('height');
                width = calcWidth && childContext.getProp('width');
                margins = childContext.getMarginInfo();
                height += margins.bottom;
                width += margins.right;
                contentHeight = Math.max(contentHeight, (child.y || 0) + height);
                contentWidth = Math.max(contentWidth, (child.x || 0) + width);
                if (isNaN(contentHeight) && isNaN(contentWidth)) {
                    me.done = false;
                    return;
                }
            }
            if (calcWidth || calcHeight) {
                targetPadding = ownerContext.targetContext.getPaddingInfo();
            }
            if (calcWidth && !ownerContext.setContentWidth(contentWidth + targetPadding.width)) {
                me.done = false;
            }
            // eslint-disable-next-line max-len
            if (calcHeight && !ownerContext.setContentHeight(contentHeight + targetPadding.height)) {
                me.done = false;
            }
        }
    }
});

/**
 * This is a layout that manages multiple Panels in an expandable accordion style such that
 * by default only one Panel can be expanded at any given time (set {@link #multi} config
 * to have more open). Each Panel has built-in support for expanding and collapsing.
 *
 * Note: Only Ext Panels and all subclasses of Ext.panel.Panel may be used in an accordion layout
 * Container.
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *         title: 'Accordion Layout',
 *         width: 300,
 *         height: 300,
 *         defaults: {
 *             // applied to each contained panel
 *             bodyStyle: 'padding:15px'
 *         },
 *         layout: {
 *             // layout-specific configs go here
 *             type: 'accordion',
 *             titleCollapse: false,
 *             animate: true,
 *             activeOnTop: true
 *         },
 *         items: [{
 *             title: 'Panel 1',
 *             html: 'Panel content!'
 *         },{
 *             title: 'Panel 2',
 *             html: 'Panel content!'
 *         },{
 *             title: 'Panel 3',
 *             html: 'Panel content!'
 *         }],
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.layout.container.Accordion', {
    extend: 'Ext.layout.container.VBox',
    alias: 'layout.accordion',
    type: 'accordion',
    alternateClassName: 'Ext.layout.AccordionLayout',
    targetCls: Ext.baseCSSPrefix + 'accordion-layout-ct',
    itemCls: [
        Ext.baseCSSPrefix + 'box-item',
        Ext.baseCSSPrefix + 'accordion-item'
    ],
    align: 'stretch',
    enableSplitters: false,
    /**
     * @cfg {Boolean} fill
     * True to adjust the active item's height to fill the available space in the container,
     * false to use the item's current height, or auto height if not explicitly set.
     */
    fill: true,
    /**
     * @cfg {Boolean} autoWidth
     * Child Panels have their width actively managed to fit within the accordion's width.
     * @removed This config is ignored in ExtJS 4
     */
    /**
     * @cfg {Boolean} titleCollapse
     * True to allow expand/collapse of each contained panel by clicking anywhere on the title bar,
     * false to allow
     * expand/collapse only when the toggle tool button is clicked.  When set to false,
     * {@link #hideCollapseTool} should be false also. An explicit
     * {@link Ext.panel.Panel#titleCollapse} declared on the panel will override this setting.
     */
    titleCollapse: true,
    /**
     * @cfg {Boolean} hideCollapseTool
     * True to hide the contained Panels' collapse/expand toggle buttons, false to display them.
     * When set to true, {@link #titleCollapse} is automatically set to true.
     */
    hideCollapseTool: false,
    /**
     * @cfg {Boolean} collapseFirst
     * True to make sure the collapse/expand toggle button always renders first (to the left of)
     * any other tools in the contained Panels' title bars, false to render it last. By default,
     * this will use the {@link Ext.panel.Panel#collapseFirst} setting on the panel.
     * If the config option is specified on the layout, it will override the panel value.
     */
    collapseFirst: undefined,
    /**
     * @cfg {Boolean} animate
     * True to slide the contained panels open and closed during expand/collapse using animation,
     * false to open and close directly with no animation. Note: The layout performs animated
     * collapsing and expanding, *not* the child Panels.
     */
    animate: true,
    /**
     * @cfg {Boolean} activeOnTop
     * Only valid when {@link #multi} is `false` and {@link #animate} is `false`.
     *
     * True to swap the position of each panel as it is expanded so that it becomes the first item
     * in the container, false to keep the panels in the rendered order.
     */
    activeOnTop: false,
    /**
     * @cfg {Boolean} multi
     * Set to true to enable multiple accordion items to be open at once.
     */
    multi: false,
    /**
     * @cfg {Boolean} [wrapOver=true] When `true`, pressing Down or Right arrow key on the
     * focused last accordion panel header will navigate to the first panel; pressing Up
     * or Left arrow key on the focused first accordion panel header will navigate to the
     * last panel.
     * Set this to `false` to prevent keyboard navigation from wrapping over the edges.
     */
    wrapOver: true,
    panelCollapseMode: 'header',
    defaultAnimatePolicy: {
        y: true,
        height: true
    },
    constructor: function() {
        var me = this;
        me.callParent(arguments);
        if (me.animate) {
            me.animatePolicy = {};
            /* Animate our parallel dimension and position.
               So in the default vertical accordion, this will be
                {
                    y: true,
                    height: true
                }
            */
            me.animatePolicy[me.names.x] = true;
            me.animatePolicy[me.names.width] = true;
        } else {
            me.animatePolicy = null;
        }
    },
    beforeRenderItems: function(items) {
        var me = this,
            ln = items.length,
            owner = me.owner,
            collapseFirst = me.collapseFirst,
            hasCollapseFirst = Ext.isDefined(collapseFirst),
            expandedItem = me.getExpanded(true)[0],
            multi = me.multi,
            comp, i;
        for (i = 0; i < ln; i++) {
            comp = items[i];
            if (!comp.rendered) {
                // Set up initial properties for Panels in an accordion.
                comp.isAccordionPanel = true;
                comp.bodyAriaRole = 'tabpanel';
                comp.accordionWrapOver = me.wrapOver;
                if (!multi || comp.collapsible !== false) {
                    comp.collapsible = true;
                }
                if (comp.collapsible) {
                    if (hasCollapseFirst) {
                        comp.collapseFirst = collapseFirst;
                    }
                    if (me.hideCollapseTool) {
                        comp.hideCollapseTool = me.hideCollapseTool;
                        comp.titleCollapse = true;
                    } else if (me.titleCollapse && comp.titleCollapse === undefined) {
                        // Only force titleCollapse if we don't explicitly
                        // set one on the child panel
                        comp.titleCollapse = me.titleCollapse;
                    }
                }
                comp.hideHeader = comp.width = null;
                comp.title = comp.title || '&#160;';
                comp.addBodyCls(Ext.baseCSSPrefix + 'accordion-body');
                // If only one child Panel is allowed to be expanded
                // then collapse all except the first one found with collapsed:false
                // If we have hasExpanded set, we've already done this
                if (!multi) {
                    if (expandedItem) {
                        comp.collapsed = expandedItem !== comp;
                    } else if (comp.hasOwnProperty('collapsed') && comp.collapsed === false) {
                        expandedItem = comp;
                    } else {
                        comp.collapsed = true;
                    }
                    // If only one child Panel may be expanded, then intercept expand/show requests.
                    owner.mon(comp, 'show', me.onComponentShow, me);
                }
                // Need to still check this outside multi because we don't want
                // a single item to be able to collapse
                comp.headerOverCls = Ext.baseCSSPrefix + 'accordion-hd-over';
            }
        }
        // If no collapsed:false Panels found, make the first one expanded, only if we're
        // not during an expand/collapse
        if (!me.processing && !multi) {
            if (!expandedItem) {
                if (ln) {
                    items[0].collapsed = false;
                }
            } else if (me.activeOnTop) {
                expandedItem.collapsed = false;
                me.configureItem(expandedItem);
                if (owner.items.indexOf(expandedItem) > 0) {
                    owner.insert(0, expandedItem);
                }
            }
        }
    },
    getItemsRenderTree: function(items) {
        this.beforeRenderItems(items);
        return this.callParent(arguments);
    },
    renderItems: function(items, target) {
        this.beforeRenderItems(items);
        this.callParent(arguments);
    },
    configureItem: function(item) {
        this.callParent(arguments);
        // Accordion headers are immune to dock layout's border-management rules
        item.ignoreHeaderBorderManagement = true;
        // We handle animations for the expand/collapse of items.
        // Items do not have individual borders
        item.animCollapse = false;
        // If filling available space, all Panels flex.
        if (this.fill) {
            item.flex = 1;
        }
    },
    beginLayout: function(ownerContext) {
        this.callParent(arguments);
        // Accordion widgets have the role of tablist along with the attribute
        // aria-multiselectable="true" to indicate that it's an accordion
        // and not just a simple tab panel.
        // We can't set this role on the panel's main el as this panel may be
        // a region in a border layout which yields its own set of ARIA attributes.
        // We also can't set this role on panel's body el, because the panel could be
        // a FormPanel that would have role="form" on the body el, and the tablist
        // needs to be contained within it.
        // innerCt seems to be the most logical choice here.
        this.innerCt.dom.setAttribute('role', 'tablist');
        this.innerCt.dom.setAttribute('aria-multiselectable', true);
        this.updatePanelClasses(ownerContext);
    },
    updatePanelClasses: function(ownerContext) {
        var children = ownerContext.visibleItems,
            ln = children.length,
            siblingCollapsed = true,
            i, child, header;
        for (i = 0; i < ln; i++) {
            child = children[i];
            header = child.header;
            header.addCls(Ext.baseCSSPrefix + 'accordion-hd');
            if (siblingCollapsed) {
                header.removeCls(Ext.baseCSSPrefix + 'accordion-hd-sibling-expanded');
            } else {
                header.addCls(Ext.baseCSSPrefix + 'accordion-hd-sibling-expanded');
            }
            if (i + 1 === ln && child.collapsed) {
                header.addCls(Ext.baseCSSPrefix + 'accordion-hd-last-collapsed');
            } else {
                header.removeCls(Ext.baseCSSPrefix + 'accordion-hd-last-collapsed');
            }
            siblingCollapsed = child.collapsed;
        }
    },
    // When a Component expands, adjust the heights of the other Components
    // to be just enough to accommodate their headers.
    // The expanded Component receives the only flex value, and so gets all remaining space.
    onBeforeComponentExpand: function(toExpand) {
        var me = this,
            owner = me.owner,
            multi = me.multi,
            moveToTop = !multi && !me.animate && me.activeOnTop,
            expanded, previousValue, anim;
        if (!me.processing) {
            me.processing = true;
            previousValue = owner.deferLayouts;
            owner.deferLayouts = true;
            if (!multi) {
                expanded = me.getExpanded()[0];
                if (expanded && expanded !== toExpand) {
                    anim = expanded.$layoutAnim;
                    // If the item is animating, finish it.
                    if (anim) {
                        anim.jumpToEnd();
                    }
                    expanded.collapse();
                }
            }
            if (moveToTop) {
                // Prevent extra layout when moving the item
                Ext.suspendLayouts();
                owner.insert(0, toExpand);
                Ext.resumeLayouts();
            }
            owner.deferLayouts = previousValue;
            me.processing = false;
        }
    },
    onBeforeComponentCollapse: function(comp) {
        var me = this,
            owner = me.owner,
            toExpand, expanded, previousValue;
        if (me.owner.items.getCount() === 1) {
            // do not allow collapse if there is only one item
            return false;
        }
        if (!me.processing) {
            me.processing = true;
            previousValue = owner.deferLayouts;
            owner.deferLayouts = true;
            toExpand = comp.next() || comp.prev();
            // If we are allowing multi, and the "toCollapse" component
            // is NOT the only expanded Component, then ask the box layout
            // to collapse it to its header.
            if (me.multi) {
                expanded = me.getExpanded();
                // If the collapsing Panel is the only expanded one, expand the following Component.
                // All this is handling fill: true, so there must be at least one expanded,
                if (expanded.length === 1) {
                    toExpand.expand();
                }
            } else if (toExpand) {
                toExpand.expand();
            }
            owner.deferLayouts = previousValue;
            me.processing = false;
        }
    },
    onComponentShow: function(comp) {
        this.onBeforeComponentExpand(comp);
    },
    onAdd: function(item) {
        var me = this;
        me.callParent(arguments);
        if (item.collapseMode === 'placeholder') {
            item.collapseMode = me.panelCollapseMode;
        }
        item.collapseDirection = item.headerPosition;
        // If we add to an accordion after its is has run once we need to make sure
        // new items are collapsed on entry. The item is also in the collection now,
        // so only collapse it if we have more than 1.
        if (me.layoutCount && !me.multi && me.owner.items.getCount() > 1) {
            // If we get here, we must already have something expanded, so we don't
            // want to react here.
            me.processing = true;
            item.collapse();
            me.processing = false;
        }
    },
    onRemove: function(panel, destroying) {
        var me = this,
            item;
        me.callParent(arguments);
        if (!me.owner.destroying && !me.multi && !panel.collapsed) {
            item = me.owner.items.first();
            if (item) {
                item.expand();
            }
        }
    },
    getExpanded: function(explicitCheck) {
        var items = this.owner.items.items,
            len = items.length,
            i = 0,
            out = [],
            add, item;
        for (; i < len; ++i) {
            item = items[i];
            if (!item.hidden) {
                if (explicitCheck) {
                    add = item.hasOwnProperty('collapsed') && item.collapsed === false;
                } else {
                    add = !item.collapsed;
                }
                if (add) {
                    out.push(item);
                }
            }
        }
        return out;
    },
    // No need to run an extra layout since everything has already achieved the
    // desired size when using an accordion.
    afterCollapse: Ext.emptyFn,
    afterExpand: Ext.emptyFn
});

/**
 * Private utility class for Ext.layout.container.Border.
 * @private
 */
Ext.define('Ext.resizer.BorderSplitter', {
    extend: 'Ext.resizer.Splitter',
    alias: 'widget.bordersplitter',
    uses: [
        'Ext.resizer.BorderSplitterTracker'
    ],
    // must be configured in by the border layout:
    collapseTarget: null,
    getTrackerConfig: function() {
        var trackerConfig = this.callParent();
        trackerConfig.xclass = 'Ext.resizer.BorderSplitterTracker';
        return trackerConfig;
    },
    onTargetCollapse: function(target) {
        this.callParent([
            target
        ]);
        if (this.performCollapse !== false && target.collapseMode === 'mini') {
            target.addCls(target.baseCls + '-' + target.collapsedCls + '-mini');
        }
    },
    onTargetExpand: function(target) {
        this.callParent([
            target
        ]);
        if (this.performCollapse !== false && target.collapseMode === 'mini') {
            target.removeCls(target.baseCls + '-' + target.collapsedCls + '-mini');
        }
    }
});

/**
 * This is a multi-pane, application-oriented UI layout style that supports multiple nested panels,
 * automatic bars between regions and built-in
 * {@link Ext.panel.Panel#collapsible expanding and collapsing} of regions.
 *
 * This class is intended to be extended or created via the `layout: 'border'`
 * {@link Ext.container.Container#layout} config, and should generally not need to be created
 * directly via the new keyword.
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *         width: 500,
 *         height: 300,
 *         title: 'Border Layout',
 *         layout: 'border',
 *         items: [{
 *             title: 'South Region is resizable',
 *             region: 'south',     // position for region
 *             xtype: 'panel',
 *             height: 100,
 *             split: true,         // enable resizing
 *             margin: '0 5 5 5'
 *         },{
 *             // xtype: 'panel' implied by default
 *             title: 'West Region is collapsible',
 *             region:'west',
 *             xtype: 'panel',
 *             margin: '5 0 0 5',
 *             width: 200,
 *             collapsible: true,   // make collapsible
 *             id: 'west-region-container',
 *             layout: 'fit'
 *         },{
 *             title: 'Center Region',
 *             region: 'center',     // center region is required, no width/height specified
 *             xtype: 'panel',
 *             layout: 'fit',
 *             margin: '5 5 0 0'
 *         }],
 *         renderTo: Ext.getBody()
 *     });
 *
 * # Notes
 * 
 *   - When using the split option, the layout will automatically insert a
 *     {@link Ext.resizer.Splitter} into the appropriate place. This will modify the underlying
 *     {@link Ext.container.Container#property-items items} collection in the container.
 *
 *   - Any Container using the Border layout **must** have a child item with `region:'center'`.
 *     The child item in the center region will always be resized to fill the remaining space
 *     not used by the other regions in the layout.
 *
 *   - Any child items with a region of `west` or `east` may be configured with either an initial
 *     `width`, or a {@link Ext.layout.container.Box#flex} value, or an initial percentage width
 *     **string** (Which is simply divided by 100 and used as a flex value).
 *     The 'center' region has a flex value of `1`.
 *
 *   - Any child items with a region of `north` or `south` may be configured with either an initial
 *     `height`, or a {@link Ext.layout.container.Box#flex} value, or an initial percentage height
 *     **string** (Which is simply divided by 100 and used as a flex value).
 *     The 'center' region has a flex value of `1`.
 *
 *   - **There is no BorderLayout.Region class in ExtJS 4.0+**
 */
Ext.define('Ext.layout.container.Border', {
    extend: 'Ext.layout.container.Container',
    alias: 'layout.border',
    alternateClassName: 'Ext.layout.BorderLayout',
    requires: [
        'Ext.resizer.BorderSplitter',
        'Ext.fx.Anim',
        // Overrides for Panel that provide border layout features
        'Ext.layout.container.border.Region'
    ],
    targetCls: Ext.baseCSSPrefix + 'border-layout-ct',
    itemCls: [
        Ext.baseCSSPrefix + 'border-item',
        Ext.baseCSSPrefix + 'box-item'
    ],
    type: 'border',
    isBorderLayout: true,
    /**
     * @cfg {Boolean/Ext.resizer.BorderSplitter} split
     * This configuration option is to be applied to the **child `items`** managed by this layout.
     * Each region with `split:true` will get a {@link Ext.resizer.BorderSplitter Splitter} that
     * allows for manual resizing of the container. Except for the `center` region.
     *
     * This option can also accept an object of configurations from the
     * {@link Ext.resizer.BorderSplitter}. An example of this would be:
     *
     *     {
     *         title: 'North',
     *         region: 'north',
     *         height: 100,
     *         collapsible: true,
     *         split: {
     *             size: 20
     *         }
     *     }
     */
    /**
     * @cfg {Boolean} [splitterResize=true]
     * This configuration option is to be applied to the **child `items`** managed by this layout
     * and is used in conjunction with {@link #split}. By default, when specifying {@link #split},
     * the region can be dragged to be resized. Set this option to false to show the split bar
     * but prevent resizing.
     */
    /**
     * @cfg {Number/String/Object} padding
     * Sets the padding to be applied to all child items managed by this layout.
     * 
     * This property can be specified as a string containing space-separated, numeric
     * padding values. The order of the sides associated with each value matches the way
     * CSS processes padding values:
     *
     *  - If there is only one value, it applies to all sides.
     *  - If there are two values, the top and bottom borders are set to the first value
     *    and the right and left are set to the second.
     *  - If there are three values, the top is set to the first value, the left and right
     *    are set to the second, and the bottom is set to the third.
     *  - If there are four values, they apply to the top, right, bottom, and left,
     *    respectively.
     *
     */
    padding: undefined,
    percentageRe: /(\d+)%/,
    horzPositionProp: 'left',
    padOnContainerProp: 'left',
    padNotOnContainerProp: 'right',
    /**
     * Reused meta-data objects that describe axis properties.
     * @private
     */
    axisProps: {
        horz: {
            borderBegin: 'west',
            borderEnd: 'east',
            horizontal: true,
            posProp: 'x',
            sizeProp: 'width',
            sizePropCap: 'Width'
        },
        vert: {
            borderBegin: 'north',
            borderEnd: 'south',
            horizontal: false,
            posProp: 'y',
            sizeProp: 'height',
            sizePropCap: 'Height'
        }
    },
    /**
     * @private
     */
    centerRegion: null,
    manageMargins: true,
    panelCollapseAnimate: true,
    panelCollapseMode: 'placeholder',
    /**
     * @cfg {Object} regionWeights
     * The default weights to assign to regions in the border layout. These values are
     * used when a region does not contain a `weight` property. This object must have
     * properties for all regions ("north", "south", "east" and "west").
     * 
     * **IMPORTANT:** Since this is an object, changing its properties will impact ALL
     * instances of Border layout. If this is not desired, provide a replacement object as
     * a config option instead:
     * 
     *      layout: {
     *          type: 'border',
     *          regionWeights: {
     *              west: 20,
     *              north: 10,
     *              south: -10,
     *              east: -20
     *          }
     *      }
     *
     * The region with the highest weight is assigned space from the border before other
     * regions. Regions of equal weight are assigned space based on their position in the
     * owner's items list (first come, first served).
     */
    regionWeights: {
        north: 20,
        south: 10,
        center: 0,
        west: -10,
        east: -20
    },
    //----------------------------------
    // Layout processing
    /**
     * Creates the axis objects for the layout. These are only missing size information
     * which is added during {@link #calculate}.
     * @private
     */
    beginAxis: function(ownerContext, regions, name) {
        var me = this,
            props = me.axisProps[name],
            isVert = !props.horizontal,
            sizeProp = props.sizeProp,
            totalFlex = 0,
            childItems = ownerContext.childItems,
            length = childItems.length,
            center, i, childContext, centerFlex, comp, region, match, size, type, target, placeholder;
        for (i = 0; i < length; ++i) {
            childContext = childItems[i];
            comp = childContext.target;
            childContext.layoutPos = {};
            if (comp.region) {
                childContext.region = region = comp.region;
                childContext.isCenter = comp.isCenter;
                childContext.isHorz = comp.isHorz;
                childContext.isVert = comp.isVert;
                childContext.weight = comp.weight || me.regionWeights[region] || 0;
                comp.weight = childContext.weight;
                regions[comp.id] = childContext;
                if (comp.isCenter) {
                    center = childContext;
                    centerFlex = comp.flex;
                    ownerContext.centerRegion = center;
                    
                    continue;
                }
                if (isVert !== childContext.isVert) {
                    
                    continue;
                }
                // process regions "isVert ? north||south : east||center||west"
                childContext.reverseWeighting = (region === props.borderEnd);
                size = comp[sizeProp];
                type = typeof size;
                if (!comp.collapsed) {
                    if (type === 'string' && (match = me.percentageRe.exec(size))) {
                        childContext.percentage = parseInt(match[1], 10);
                    } else if (comp.flex) {
                        totalFlex += childContext.flex = comp.flex;
                    }
                }
            }
        }
        // Special cases for a collapsed center region
        if (center) {
            target = center.target;
            if ((placeholder = target.placeholderFor)) {
                if (!centerFlex && isVert === placeholder.collapsedVertical()) {
                    // The center region is a placeholder, collapsed in this axis
                    centerFlex = 0;
                    center.collapseAxis = name;
                }
            } else if (target.collapsed && (isVert === target.collapsedVertical())) {
                // The center region is a collapsed header, collapsed in this axis
                centerFlex = 0;
                center.collapseAxis = name;
            }
        }
        if (centerFlex == null) {
            // If we still don't have a center flex, default to 1
            centerFlex = 1;
        }
        totalFlex += centerFlex;
        return Ext.apply({
            before: isVert ? 'top' : 'left',
            totalFlex: totalFlex
        }, props);
    },
    beginLayout: function(ownerContext) {
        var me = this,
            items = me.getLayoutItems(),
            pad = me.padding,
            type = typeof pad,
            padOnContainer = false,
            childContext, item, length, i, regions, collapseTarget, doShow, hidden, region;
        // TODO: EXTJSIV-13015
        if (ownerContext.heightModel.shrinkWrap) {
            Ext.raise("Border layout does not currently support shrinkWrap height. " + "Please specify a height on component: " + me.owner.id + ", or use a container layout that sets the component's height.");
        }
        if (ownerContext.widthModel.shrinkWrap) {
            Ext.raise("Border layout does not currently support shrinkWrap width. " + "Please specify a width on component: " + me.owner.id + ", or use a container layout that sets the component's width.");
        }
        // We sync the visibility state of splitters with their region:
        if (pad) {
            if (type === 'string' || type === 'number') {
                pad = Ext.util.Format.parseBox(pad);
            }
        } else {
            pad = ownerContext.getEl('getTargetEl').getPaddingInfo();
            padOnContainer = true;
        }
        ownerContext.outerPad = pad;
        ownerContext.padOnContainer = padOnContainer;
        for (i = 0 , length = items.length; i < length; ++i) {
            item = items[i];
            collapseTarget = me.getSplitterTarget(item);
            if (collapseTarget) {
                // if (splitter)
                doShow = undefined;
                hidden = !!item.hidden;
                if (!collapseTarget.split) {
                    if (collapseTarget.isCollapsingOrExpanding) {
                        doShow = !!collapseTarget.collapsed;
                    }
                } else if (hidden !== collapseTarget.hidden) {
                    doShow = !collapseTarget.hidden;
                }
                if (doShow) {
                    item.show();
                } else if (doShow === false) {
                    item.hide();
                }
            }
        }
        // The above synchronized visibility of splitters with their regions, so we need
        // to make this call after that so that childItems and visibleItems are correct:
        //
        me.callParent(arguments);
        items = ownerContext.childItems;
        length = items.length;
        regions = {};
        ownerContext.borderAxisHorz = me.beginAxis(ownerContext, regions, 'horz');
        ownerContext.borderAxisVert = me.beginAxis(ownerContext, regions, 'vert');
        // Now that weights are assigned to the region's contextItems, we assign those
        // same weights to the contextItem for the splitters. We also cross link the
        // contextItems for the collapseTarget and its splitter.
        for (i = 0; i < length; ++i) {
            childContext = items[i];
            collapseTarget = me.getSplitterTarget(childContext.target);
            if (collapseTarget) {
                // if (splitter)
                region = regions[collapseTarget.id];
                if (!region) {
                    // if the region was hidden it will not be part of childItems, and
                    // so beginAxis() won't add it to the regions object, so we have
                    // to create the context item here.
                    region = ownerContext.getEl(collapseTarget.el, me);
                    region.region = collapseTarget.region;
                }
                childContext.collapseTarget = collapseTarget = region;
                childContext.weight = collapseTarget.weight;
                childContext.reverseWeighting = collapseTarget.reverseWeighting;
                collapseTarget.splitter = childContext;
                childContext.isHorz = collapseTarget.isHorz;
                childContext.isVert = collapseTarget.isVert;
            }
        }
        // Now we want to sort the childItems by their weight.
        me.sortWeightedItems(items, 'reverseWeighting');
        me.setupSplitterNeighbors(items);
    },
    calculate: function(ownerContext) {
        var me = this,
            containerSize = me.getContainerSize(ownerContext),
            childItems = ownerContext.childItems,
            length = childItems.length,
            horz = ownerContext.borderAxisHorz,
            vert = ownerContext.borderAxisVert,
            pad = ownerContext.outerPad,
            padOnContainer = ownerContext.padOnContainer,
            i, childContext, childMargins, size, horzPercentTotal, vertPercentTotal;
        horz.begin = pad[me.padOnContainerProp];
        vert.begin = pad.top;
        // If the padding is already on the container we need to add it to the space
        // If not on the container, it's "virtual" padding.
        horzPercentTotal = horz.end = horz.flexSpace = containerSize.width + (padOnContainer ? pad[me.padOnContainerProp] : -pad[me.padNotOnContainerProp]);
        vertPercentTotal = vert.end = vert.flexSpace = containerSize.height + (padOnContainer ? pad.top : -pad.bottom);
        // Reduce flexSpace on each axis by the fixed/auto sized dimensions of items that
        // aren't flexed along that axis.
        for (i = 0; i < length; ++i) {
            childContext = childItems[i];
            childMargins = childContext.getMarginInfo();
            // Margins are always fixed size and must be removed from the space
            // used for percentages and flexes
            if (childContext.isHorz || childContext.isCenter) {
                horz.addUnflexed(childMargins.width);
                horzPercentTotal -= childMargins.width;
            }
            if (childContext.isVert || childContext.isCenter) {
                vert.addUnflexed(childMargins.height);
                vertPercentTotal -= childMargins.height;
            }
            // Fixed size components must have their sizes removed from the space used for flex
            if (!childContext.flex && !childContext.percentage) {
                if (childContext.isHorz || (childContext.isCenter && childContext.collapseAxis === 'horz')) {
                    size = childContext.getProp('width');
                    horz.addUnflexed(size);
                    // splitters should not count towards percentages
                    if (childContext.collapseTarget) {
                        horzPercentTotal -= size;
                    }
                } else if (childContext.isVert || (childContext.isCenter && childContext.collapseAxis === 'vert')) {
                    size = childContext.getProp('height');
                    vert.addUnflexed(size);
                    // splitters should not count towards percentages
                    if (childContext.collapseTarget) {
                        vertPercentTotal -= size;
                    }
                }
            }
        }
        // else ignore center since it is fully flexed
        for (i = 0; i < length; ++i) {
            childContext = childItems[i];
            childMargins = childContext.getMarginInfo();
            // Calculate the percentage sizes. After this calculation percentages
            // are very similar to fixed sizes
            if (childContext.percentage) {
                if (childContext.isHorz) {
                    size = Math.ceil(horzPercentTotal * childContext.percentage / 100);
                    size = childContext.setWidth(size);
                    horz.addUnflexed(size);
                } else if (childContext.isVert) {
                    size = Math.ceil(vertPercentTotal * childContext.percentage / 100);
                    size = childContext.setHeight(size);
                    vert.addUnflexed(size);
                }
            }
        }
        // center shouldn't have a percentage but if it does it should be ignored
        // If we haven't gotten sizes for all unflexed dimensions on an axis, the flexSpace
        // will be NaN so we won't be calculating flexed dimensions until that is resolved.
        for (i = 0; i < length; ++i) {
            childContext = childItems[i];
            if (!childContext.isCenter) {
                me.calculateChildAxis(childContext, horz);
                me.calculateChildAxis(childContext, vert);
            }
        }
        // Once all items are placed, the final size of the center can be determined. If we
        // can determine both width and height, we are done. We use '+' instead of '&&' to
        // avoid short-circuiting (we want to call both):
        if (me.finishAxis(ownerContext, vert) + me.finishAxis(ownerContext, horz) < 2) {
            me.done = false;
        } else {
            // Size information is published as we place regions but position is hard to do
            // that way (while avoiding published multiple times) so we publish all the
            // positions at the end.
            me.finishPositions(childItems);
        }
    },
    /**
     * Performs the calculations for a region on a specified axis.
     * @private
     */
    calculateChildAxis: function(childContext, axis) {
        var collapseTarget = childContext.collapseTarget,
            setSizeMethod = 'set' + axis.sizePropCap,
            sizeProp = axis.sizeProp,
            childMarginSize = childContext.getMarginInfo()[sizeProp],
            region, isBegin, flex, pos, size;
        if (collapseTarget) {
            // if (splitter)
            region = collapseTarget.region;
        } else {
            region = childContext.region;
            flex = childContext.flex;
        }
        isBegin = region === axis.borderBegin;
        if (!isBegin && region !== axis.borderEnd) {
            // a north/south region on the horizontal axis or an east/west region on the
            // vertical axis: stretch to fill remaining space:
            childContext[setSizeMethod](axis.end - axis.begin - childMarginSize);
            pos = axis.begin;
        } else {
            if (flex) {
                size = Math.ceil(axis.flexSpace * (flex / axis.totalFlex));
                size = childContext[setSizeMethod](size);
            } else if (childContext.percentage) {
                // Like getProp but without registering a dependency - we calculated the size,
                // we don't depend on it
                size = childContext.peek(sizeProp);
            } else {
                size = childContext.getProp(sizeProp);
            }
            size += childMarginSize;
            if (isBegin) {
                pos = axis.begin;
                axis.begin += size;
            } else {
                axis.end = pos = axis.end - size;
            }
        }
        childContext.layoutPos[axis.posProp] = pos;
    },
    eachItem: function(region, fn, scope) {
        var me = this,
            items = me.getLayoutItems(),
            i = 0,
            item;
        if (Ext.isFunction(region)) {
            fn = region;
            scope = fn;
        }
        for (i; i < items.length; i++) {
            item = items[i];
            if (!region || item.region === region) {
                if (fn.call(scope, item) === false) {
                    break;
                }
            }
        }
    },
    /**
     * Finishes the calculations on an axis. This basically just assigns the remaining
     * space to the center region.
     * @private
     */
    finishAxis: function(ownerContext, axis) {
        var size = axis.end - axis.begin,
            center = ownerContext.centerRegion;
        if (center) {
            center['set' + axis.sizePropCap](size - center.getMarginInfo()[axis.sizeProp]);
            center.layoutPos[axis.posProp] = axis.begin;
        }
        return Ext.isNumber(size) ? 1 : 0;
    },
    /**
     * Finishes by setting the positions on the child items.
     * @private
     */
    finishPositions: function(childItems) {
        var length = childItems.length,
            index, childContext,
            marginProp = this.horzPositionProp;
        for (index = 0; index < length; ++index) {
            childContext = childItems[index];
            childContext.setProp('x', childContext.layoutPos.x + childContext.marginInfo[marginProp]);
            childContext.setProp('y', childContext.layoutPos.y + childContext.marginInfo.top);
        }
    },
    getLayoutItems: function() {
        var owner = this.owner,
            ownerItems = (owner && owner.items && owner.items.items) || [],
            length = ownerItems.length,
            items = [],
            i = 0,
            ownerItem, placeholderFor;
        for (; i < length; i++) {
            ownerItem = ownerItems[i];
            placeholderFor = ownerItem.placeholderFor;
            // There are a couple of scenarios where we do NOT want an item to
            // be included in the layout items:
            //
            // 1. If the item is floated. This can happen when a region's header
            // is clicked to "float" the item, then another region's header or
            // is clicked quickly before the first floated region has had a
            // chance to slide out. When this happens, the second click triggers
            // a layout, the purpose of which is to determine what the size of the 
            // second region will be after it is floated, so it can be animated
            // to that size. In this case the existing floated item should not be
            // included in the layout items because it will not be visible once
            // it's slideout animation has completed.
            //
            // 2. If the item is a placeholder for a panel that is currently
            // being expanded. Similar to scenario 1, a second layout can be
            // triggered by another panel being expanded/collapsed/floated before
            // the first panel has finished it's expand animation. If this is the
            // case we do not want the placeholder to be included in the layout
            // items because it will not be once the panel has finished expanding.
            //
            // If the component is hidden, we need none of these shenanigans
            if (ownerItem.hidden || ((!ownerItem.floated || ownerItem.isCollapsingOrExpanding === 2) && !(placeholderFor && placeholderFor.isCollapsingOrExpanding === 2))) {
                items.push(ownerItem);
            }
        }
        return items;
    },
    getPlaceholder: function(comp) {
        return comp.getPlaceholder && comp.getPlaceholder();
    },
    getMaxWeight: function(region) {
        return this.getMinMaxWeight(region);
    },
    getMinWeight: function(region) {
        return this.getMinMaxWeight(region, true);
    },
    getMinMaxWeight: function(region, min) {
        var me = this,
            weight = null;
        me.eachItem(region, function(item) {
            if (item.hasOwnProperty('weight')) {
                if (weight === null) {
                    weight = item.weight;
                    return;
                }
                if ((min && item.weight < weight) || item.weight > weight) {
                    weight = item.weight;
                }
            }
        }, this);
        return weight;
    },
    getSplitterTarget: function(splitter) {
        var collapseTarget = splitter.collapseTarget;
        if (collapseTarget && collapseTarget.collapsed) {
            return collapseTarget.placeholder || collapseTarget;
        }
        return collapseTarget;
    },
    isItemBoxParent: function(itemContext) {
        return true;
    },
    isItemShrinkWrap: function(item) {
        return true;
    },
    //----------------------------------
    // Event handlers
    /**
     * Inserts the splitter for a given region. A reference to the splitter is also stored
     * on the component as "splitter".
     * @private
     */
    insertSplitter: function(item, index, hidden, splitterCfg) {
        var region = item.region,
            splitter = Ext.apply({
                xtype: 'bordersplitter',
                collapseTarget: item,
                id: item.id + '-splitter',
                hidden: hidden,
                canResize: item.splitterResize !== false,
                splitterFor: item,
                synthetic: true
            }, // not user-defined
            splitterCfg),
            at = index + ((region === 'south' || region === 'east') ? 0 : 1);
        if (item.collapseMode === 'mini') {
            splitter.collapsedCls = item.collapsedCls;
        }
        item.splitter = this.owner.add(at, splitter);
    },
    getMoveAfterIndex: function(after) {
        var index = this.callParent(arguments);
        if (after.splitter) {
            index++;
        }
        return index;
    },
    moveItemBefore: function(item, before) {
        var beforeRegion;
        if (before && before.splitter) {
            beforeRegion = before.region;
            if (beforeRegion === 'south' || beforeRegion === 'east') {
                before = before.splitter;
            }
        }
        return this.callParent([
            item,
            before
        ]);
    },
    /**
     * Called when a region (actually when any component) is added to the container. The
     * region is decorated with some helpful properties (isCenter, isHorz, isVert) and its
     * splitter is added if its "split" property is true.
     * @private
     */
    onAdd: function(item, index) {
        var me = this,
            placeholderFor = item.placeholderFor,
            region = item.region,
            isCenter, split, hidden, cfg;
        me.callParent(arguments);
        if (region) {
            Ext.apply(item, me.regionFlags[region]);
            if (me.owner.isViewport) {
                item.isViewportBorderChild = true;
            }
            if (item.initBorderRegion) {
                // This method should always be present but perhaps the override is being
                // excluded.
                item.initBorderRegion();
            }
            isCenter = region === 'center';
            if (isCenter) {
                if (me.centerRegion) {
                    Ext.raise("Cannot have multiple center regions in a BorderLayout.");
                }
                me.centerRegion = item;
            } else {
                split = item.split;
                hidden = !!item.hidden;
                if (typeof split === 'object') {
                    cfg = split;
                    split = true;
                }
                if ((item.isHorz || item.isVert) && (split || item.collapseMode === 'mini')) {
                    if (item.collapseMode === 'mini' && item.collapsed) {
                        hidden = false;
                    } else if (!split) {
                        hidden = true;
                    }
                    me.insertSplitter(item, index, hidden, cfg);
                }
            }
            if (!isCenter && !item.hasOwnProperty('collapseMode')) {
                item.collapseMode = me.panelCollapseMode;
            }
            if (!item.hasOwnProperty('animCollapse')) {
                if (item.collapseMode !== 'placeholder') {
                    // other collapse modes do not animate nicely in a border layout, so
                    // default them to off:
                    item.animCollapse = false;
                } else {
                    item.animCollapse = me.panelCollapseAnimate;
                }
            }
            // Item can be collapsed when added
            if (hidden && item.placeholder && item.placeholder.isVisible()) {
                me.owner.insert(index, item.placeholder);
            }
        } else if (placeholderFor) {
            Ext.apply(item, me.regionFlags[placeholderFor.region]);
            item.region = placeholderFor.region;
            item.weight = placeholderFor.weight;
        }
    },
    onDestroy: function() {
        this.centerRegion = null;
        this.callParent();
    },
    onRemove: function(comp, isDestroying) {
        var me = this,
            region = comp.region,
            splitter = comp.splitter,
            owner = me.owner,
            destroying = owner.destroying,
            el;
        if (region) {
            if (comp.isCenter) {
                me.centerRegion = null;
            }
            delete comp.isCenter;
            delete comp.isHorz;
            delete comp.isVert;
            // If the owner is destroying, the splitter will be cleared anyway
            if (splitter && !owner.destroying) {
                owner.doRemove(splitter, true);
            }
            // avoid another layout
            delete comp.splitter;
        }
        me.callParent(arguments);
        if (!destroying && !isDestroying && comp.rendered) {
            // Clear top/left styles
            el = comp.getEl();
            if (el) {
                el.setStyle('top', '');
                el.setStyle(me.horzPositionProp, '');
            }
        }
    },
    //----------------------------------
    // Misc
    regionMeta: {
        center: {
            splitterDelta: 0
        },
        north: {
            splitterDelta: 1
        },
        south: {
            splitterDelta: -1
        },
        west: {
            splitterDelta: 1
        },
        east: {
            splitterDelta: -1
        }
    },
    /**
     * Flags and configs that get set of regions based on their `region` property.
     * @private
     */
    regionFlags: {
        center: {
            isCenter: true,
            isHorz: false,
            isVert: false
        },
        north: {
            isCenter: false,
            isHorz: false,
            isVert: true,
            collapseDirection: 'top'
        },
        south: {
            isCenter: false,
            isHorz: false,
            isVert: true,
            collapseDirection: 'bottom'
        },
        west: {
            isCenter: false,
            isHorz: true,
            isVert: false,
            collapseDirection: 'left'
        },
        east: {
            isCenter: false,
            isHorz: true,
            isVert: false,
            collapseDirection: 'right'
        }
    },
    setupSplitterNeighbors: function(items) {
        var edgeRegions = {},
            // north: null,
            // south: null,
            // east: null,
            // west: null
            length = items.length,
            touchedRegions = this.touchedRegions,
            i, j, center, count, edge, comp, region, splitter, touched;
        for (i = 0; i < length; ++i) {
            comp = items[i].target;
            region = comp.region;
            if (comp.isCenter) {
                center = comp;
            } else if (region) {
                touched = touchedRegions[region];
                for (j = 0 , count = touched.length; j < count; ++j) {
                    edge = edgeRegions[touched[j]];
                    if (edge) {
                        edge.neighbors.push(comp);
                    }
                }
                if (comp.placeholderFor) {
                    // placeholder, so grab the splitter for the actual panel
                    splitter = comp.placeholderFor.splitter;
                } else {
                    splitter = comp.splitter;
                }
                if (splitter) {
                    splitter.neighbors = [];
                }
                edgeRegions[region] = splitter;
            }
        }
        if (center) {
            touched = touchedRegions.center;
            for (j = 0 , count = touched.length; j < count; ++j) {
                edge = edgeRegions[touched[j]];
                if (edge) {
                    edge.neighbors.push(center);
                }
            }
        }
    },
    /**
     * Lists the regions that would consider an interior region a neighbor. For example,
     * a north region would consider an east or west region its neighbords (as well as
     * an inner north region).
     * @private
     */
    touchedRegions: {
        center: [
            'north',
            'south',
            'east',
            'west'
        ],
        north: [
            'north',
            'east',
            'west'
        ],
        south: [
            'south',
            'east',
            'west'
        ],
        east: [
            'east',
            'north',
            'south'
        ],
        west: [
            'west',
            'north',
            'south'
        ]
    },
    sizePolicies: {
        vert: {
            readsWidth: 0,
            readsHeight: 1,
            setsWidth: 1,
            setsHeight: 0
        },
        horz: {
            readsWidth: 1,
            readsHeight: 0,
            setsWidth: 0,
            setsHeight: 1
        },
        flexAll: {
            readsWidth: 0,
            readsHeight: 0,
            setsWidth: 1,
            setsHeight: 1
        }
    },
    getItemSizePolicy: function(item) {
        var me = this,
            policies = this.sizePolicies,
            collapseTarget, size, policy, placeholderFor;
        if (item.isCenter) {
            placeholderFor = item.placeholderFor;
            if (placeholderFor) {
                if (placeholderFor.collapsedVertical()) {
                    return policies.vert;
                }
                return policies.horz;
            }
            if (item.collapsed) {
                if (item.collapsedVertical()) {
                    return policies.vert;
                }
                return policies.horz;
            }
            return policies.flexAll;
        }
        collapseTarget = item.collapseTarget;
        if (collapseTarget) {
            return collapseTarget.isVert ? policies.vert : policies.horz;
        }
        if (item.region) {
            if (item.isVert) {
                size = item.height;
                policy = policies.vert;
            } else {
                size = item.width;
                policy = policies.horz;
            }
            if (item.flex || (typeof size === 'string' && me.percentageRe.test(size))) {
                return policies.flexAll;
            }
            return policy;
        }
        return me.autoSizePolicy;
    }
}, function() {
    var methods = {
            addUnflexed: function(px) {
                this.flexSpace = Math.max(this.flexSpace - px, 0);
            }
        },
        props = this.prototype.axisProps;
    Ext.apply(props.horz, methods);
    Ext.apply(props.vert, methods);
});

/**
 * This layout manages multiple child Components, each fitted to the Container, where only
 * a single child Component can be visible at any given time.  This layout style is most commonly
 * used for wizards, tab implementations, etc. This class is intended to be extended or created
 * via the `layout: 'card'` {@link Ext.container.Container#layout} config, and should generally
 * not need to be created directly via the new keyword.
 *
 * The CardLayout's focal method is {@link #setActiveItem}.  Since only one panel is displayed
 * at a time, the only way to move from one Component to the next is by calling setActiveItem,
 * passing the next panel to display (or its id or index).  The layout itself does not provide
 * a user interface for handling this navigation, so that functionality must be provided
 * by the developer.
 *
 * To change the active card of a container, call the setActiveItem method of its layout:
 *
 *     @example
 *     var p = Ext.create('Ext.panel.Panel', {
 *         layout: 'card',
 *         items: [
 *             { html: 'Card 1' },
 *             { html: 'Card 2' }
 *         ],
 *         renderTo: Ext.getBody()
 *     });
 *
 *     p.getLayout().setActiveItem(1);
 * 
 * The {@link Ext.Component#beforedeactivate beforedeactivate} and
 * {@link Ext.Component#beforeactivate beforeactivate} events can be used to prevent a card
 * from activating or deactivating by returning `false`.
 * 
 *     @example   
 *     var active = 0;
 *     var main = Ext.create('Ext.panel.Panel', {
 *         renderTo: Ext.getBody(),
 *         width: 200,
 *         height: 200,
 *         layout: 'card',
 *         tbar: [{
 *             text: 'Next',
 *             handler: function(){
 *                 var layout = main.getLayout();
 *                 ++active;
 *                 layout.setActiveItem(active);
 *                 active = main.items.indexOf(layout.getActiveItem());
 *             }
 *         }],
 *         items: [{
 *             title: 'P1'
 *         }, {
 *             title: 'P2'
 *         }, {
 *             title: 'P3',
 *             listeners: {
 *                 beforeactivate: function(){
 *                     return false;
 *                 }
 *             }
 *         }]
 *     });
 *
 * In the following example, a simplistic wizard setup is demonstrated.  A button bar is added
 * to the footer of the containing panel to provide navigation buttons.  The buttons will be handled
 * by a common navigation routine.  Note that other uses of a CardLayout (like a tab control)
 * would require a completely different implementation.  For serious implementations,
 * a better approach would be to extend CardLayout to provide the custom functionality needed.
 *
 *     @example
 *     var navigate = function(panel, direction){
 *         // This routine could contain business logic required to manage the navigation steps.
 *         // It would call setActiveItem as needed, manage navigation button state, handle any
 *         // branching logic that might be required, handle alternate actions like cancellation
 *         // or finalization, etc.  A complete wizard implementation could get pretty
 *         // sophisticated depending on the complexity required, and should probably be
 *         // done as a subclass of CardLayout in a real-world implementation.
 *         var layout = panel.getLayout();
 *         layout[direction]();
 *         Ext.getCmp('move-prev').setDisabled(!layout.getPrev());
 *         Ext.getCmp('move-next').setDisabled(!layout.getNext());
 *     };
 *
 *     Ext.create('Ext.panel.Panel', {
 *         title: 'Example Wizard',
 *         width: 300,
 *         height: 200,
 *         layout: 'card',
 *         bodyStyle: 'padding:15px',
 *         defaults: {
 *             // applied to each contained panel
 *             border: false
 *         },
 *         // just an example of one possible navigation scheme, using buttons
 *         bbar: [
 *             {
 *                 id: 'move-prev',
 *                 text: 'Back',
 *                 handler: function(btn) {
 *                     navigate(btn.up("panel"), "prev");
 *                 },
 *                 disabled: true
 *             },
 *             '->', // greedy spacer so that the buttons are aligned to each side
 *             {
 *                 id: 'move-next',
 *                 text: 'Next',
 *                 handler: function(btn) {
 *                     navigate(btn.up("panel"), "next");
 *                 }
 *             }
 *         ],
 *         // the panels (or "cards") within the layout
 *         items: [{
 *             id: 'card-0',
 *             html: '<h1>Welcome to the Wizard!</h1><p>Step 1 of 3</p>'
 *         },{
 *             id: 'card-1',
 *             html: '<p>Step 2 of 3</p>'
 *         },{
 *             id: 'card-2',
 *             html: '<h1>Congratulations!</h1><p>Step 3 of 3 - Complete</p>'
 *         }],
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.layout.container.Card', {
    extend: 'Ext.layout.container.Fit',
    alternateClassName: 'Ext.layout.CardLayout',
    alias: 'layout.card',
    type: 'card',
    hideInactive: true,
    /**
     * @cfg {Boolean} deferredRender
     * `true` to render each contained item at the time it becomes active, `false` to render
     * all contained items as soon as the layout is rendered (defaults to false). If there is
     * a significant amount of content or a lot of heavy controls being rendered into panels
     * that are not displayed by default, setting this to `true` might improve performance.
     */
    deferredRender: false,
    getRenderTree: function() {
        var me = this,
            activeItem = me.getActiveItem();
        if (activeItem) {
            // If they veto the activate, we have no active item
            if (activeItem.hasListeners.beforeactivate && activeItem.fireEvent('beforeactivate', activeItem) === false) {
                // We must null our activeItem reference, AND the one in our owning Container.
                // Because upon layout invalidation, renderChildren will use this.getActiveItem
                // which uses this.activeItem || this.owner.activeItem
                activeItem = me.activeItem = me.owner.activeItem = null;
            }
            // Item is to be the active one. Fire event after it is first layed out
            else if (activeItem.hasListeners.activate) {
                activeItem.on({
                    boxready: function() {
                        activeItem.fireEvent('activate', activeItem);
                    },
                    single: true
                });
            }
            if (me.deferredRender) {
                if (activeItem) {
                    return me.getItemsRenderTree([
                        activeItem
                    ]);
                }
            } else {
                return me.callParent(arguments);
            }
        }
    },
    renderChildren: function() {
        var me = this,
            active = me.getActiveItem();
        if (!me.deferredRender) {
            me.callParent();
        } else if (active) {
            // ensure the active item is configured for the layout
            me.renderItems([
                active
            ], me.getRenderTarget());
        }
    },
    isValidParent: function(item, target, position) {
        // Note: Card layout does not care about order within the target because only one
        // is ever visible.
        // We only care whether the item is a direct child of the target.
        var itemEl = item.el ? item.el.dom : Ext.getDom(item);
        return (itemEl && itemEl.parentNode === (target.dom || target)) || false;
    },
    /**
     * Return the active (visible) component in the layout.
     * @return {Ext.Component}
     */
    getActiveItem: function() {
        var me = this,
            // It's necessary to check that me.activeItem is not undefined
            // as it could be 0 (falsey). We're more interested in checking
            // the layout's activeItem property, since that is the source of truth
            // for an activeItem. If it's determined to be empty, check the owner.
            // Note that a default item is returned if activeItem is `undefined` but not `null`.
            // Also, note that `null` is legitimate value and completely different from `undefined`.
            item = me.activeItem === undefined ? (me.owner && me.owner.activeItem) : me.activeItem,
            result = me.parseActiveItem(item);
        // Sanitize the result in case the active item is no longer there.
        if (result && me.owner.items.indexOf(result) !== -1) {
            me.activeItem = result;
        }
        // Note that in every use case me.activeItem will have a truthy value except for
        // when a container or tabpanel is explicity configured with activeItem/Tab === null
        // or when an out-of-range index is given for an active tab (as it will be undefined).
        // In those cases, it is meaningful to return the null value, so do so.
        return result == null ? null : (me.activeItem || me.owner.activeItem);
    },
    /**
     * @private
     */
    parseActiveItem: function(item) {
        var activeItem;
        if (item && item.isComponent) {
            activeItem = item;
        } else if (typeof item === 'number' || item === undefined) {
            activeItem = this.getLayoutItems()[item || 0];
        } else if (item === null) {
            activeItem = null;
        } else {
            activeItem = this.owner.getComponent(item);
        }
        return activeItem;
    },
    /**
     * @private
     * Called before both dynamic render, and bulk render.
     * Ensure that the active item starts visible, and inactive ones start invisible.
     */
    configureItem: function(item) {
        item.setHiddenState(item !== this.getActiveItem());
        this.callParent(arguments);
    },
    onAdd: function(item, pos) {
        this.callParent([
            item,
            pos
        ]);
        this.setItemHideMode(item);
    },
    onRemove: function(component) {
        var me = this;
        me.callParent([
            component
        ]);
        me.resetItemHideMode(component);
        if (component === me.activeItem) {
            // Note setting to `undefined` is intentional. Don't null it out since null
            // now has a specific meaning in tab management (it specifies not setting
            // an active item).
            me.activeItem = undefined;
        }
    },
    /**
     * @private
     */
    getAnimation: function(newCard, owner) {
        var newAnim = (newCard || {}).cardSwitchAnimation;
        if (newAnim === false) {
            return false;
        }
        return newAnim || owner.cardSwitchAnimation;
    },
    /**
     * Return the active (visible) component in the layout to the next card
     * @return {Ext.Component} The next component or false.
     */
    getNext: function() {
        var wrap = arguments[0],
            items = this.getLayoutItems(),
            index = Ext.Array.indexOf(items, this.activeItem);
        return items[index + 1] || (wrap ? items[0] : false);
    },
    /**
     * Sets the active (visible) component in the layout to the next card
     * @return {Ext.Component} the activated component or false when nothing activated.
     */
    next: function() {
        var anim = arguments[0],
            wrap = arguments[1];
        return this.setActiveItem(this.getNext(wrap), anim);
    },
    /**
     * Return the active (visible) component in the layout to the previous card
     * @return {Ext.Component} The previous component or false.
     */
    getPrev: function() {
        var wrap = arguments[0],
            items = this.getLayoutItems(),
            index = Ext.Array.indexOf(items, this.activeItem);
        return items[index - 1] || (wrap ? items[items.length - 1] : false);
    },
    /**
     * Sets the active (visible) component in the layout to the previous card
     * @return {Ext.Component} the activated component or false when nothing activated.
     */
    prev: function() {
        var anim = arguments[0],
            wrap = arguments[1];
        return this.setActiveItem(this.getPrev(wrap), anim);
    },
    /**
     * Makes the given card active.
     *
     *     var card1 = Ext.create('Ext.panel.Panel', {itemId: 'card-1'});
     *     var card2 = Ext.create('Ext.panel.Panel', {itemId: 'card-2'});
     *     var panel = Ext.create('Ext.panel.Panel', {
     *         layout: 'card',
     *         activeItem: 0,
     *         items: [card1, card2]
     *     });
     *     // These are all equivalent
     *     panel.getLayout().setActiveItem(card2);
     *     panel.getLayout().setActiveItem('card-2');
     *     panel.getLayout().setActiveItem(1);
     *
     * @param {Ext.Component/Number/String} newCard  The component, component 
     * {@link Ext.Component#id id}, {@link Ext.Component#itemId itemId}, or index of component.
     * @return {Ext.Component} the activated component or false when nothing activated.
     * False is returned also when trying to activate an already active card.
     */
    setActiveItem: function(newCard) {
        var me = this,
            owner = me.owner,
            oldCard = me.activeItem,
            rendered = owner.rendered,
            newIndex, focusNewCard;
        newCard = me.parseActiveItem(newCard);
        newIndex = owner.items.indexOf(newCard);
        // If the card is not a child of the owner, then add it.
        // Without doing a layout!
        if (newIndex === -1) {
            newIndex = owner.items.items.length;
            Ext.suspendLayouts();
            newCard = owner.add(newCard);
            Ext.resumeLayouts();
        }
        // Is this a valid, different card?
        if (newCard && oldCard !== newCard) {
            // Fire the beforeactivate and beforedeactivate events on the cards
            if (newCard.fireEvent('beforeactivate', newCard, oldCard) === false) {
                return false;
            }
            if (oldCard && oldCard.fireEvent('beforedeactivate', oldCard, newCard) === false) {
                return false;
            }
            if (rendered) {
                Ext.suspendLayouts();
                // If the card has not been rendered yet, now is the time to do so.
                if (!newCard.rendered) {
                    me.renderItem(newCard, me.getRenderTarget(), owner.items.length);
                }
                if (oldCard) {
                    if (me.hideInactive) {
                        focusNewCard = oldCard.el.contains(Ext.Element.getActiveElement());
                        oldCard.hide();
                        if (oldCard.hidden) {
                            oldCard.hiddenByLayout = true;
                            oldCard.fireEvent('deactivate', oldCard, newCard);
                        } else // Hide was vetoed, we cannot change cards.
                        {
                            return false;
                        }
                    }
                }
                // Make sure the new card is shown
                if (newCard.hidden) {
                    newCard.show();
                }
                // Layout needs activeItem to be correct, so clear it if the show has been vetoed,
                // set it if the show has *not* been vetoed.
                if (newCard.hidden) {
                    me.activeItem = newCard = null;
                } else {
                    me.activeItem = newCard;
                    // If the card being hidden contained focus, attempt to focus the new card
                    // So as not to leave focus undefined.
                    // The focus() call will focus the defaultFocus if it is a container
                    // so ensure there is a defaultFocus.
                    if (focusNewCard) {
                        if (!newCard.defaultFocus) {
                            newCard.defaultFocus = ':focusable';
                        }
                        newCard.focus();
                    }
                }
                Ext.resumeLayouts(true);
            } else {
                me.activeItem = newCard;
            }
            newCard.fireEvent('activate', newCard, oldCard);
            return me.activeItem;
        }
        return false;
    },
    /**
     * @private
     * Reset back to initial config when item is removed from the panel.
     */
    resetItemHideMode: function(item) {
        item.hideMode = item.originalHideMode;
        delete item.originalHideMode;
    },
    /**
     * @private
     * A card layout items must have its visibility mode set to OFFSETS so its scroll
     * positions isn't reset when hidden.
     *
     * Do this automatically when an item is added to the panel.
     */
    setItemHideMode: function(item) {
        item.originalHideMode = item.hideMode;
        item.hideMode = 'offsets';
    }
});

/**
 * This layout manager is used to center contents within a container. As a subclass of
 * {@link Ext.layout.container.Fit fit layout}, CenterLayout expects to have one child
 * item; multiple items will be placed overlapping. The layout does not require any config
 * options. Items in the container can use percentage width or height rather than be fit
 * to the full size of the container.
 *
 * Example usage:
 *
 *      // The content panel is centered in the container
 *
 *      var p = Ext.create('Ext.Panel', {
 *          title: 'Center Layout',
 *          layout: 'center',
 *          items: [{
 *              title: 'Centered Content',
 *              width: '75%',  // assign 75% of the container width to the item
 *              html: 'Some content'
 *          }]
 *      });
 *
 * If you leave the title blank and specify no border you can create a non-visual, structural
 * container just for centering the contents.
 *
 *      var p = Ext.create('Ext.Container', {
 *          layout: 'center',
 *          items: [{
 *              title: 'Centered Content',
 *              width: 300,
 *              height: '90%', // assign 90% of the container height to the item
 *              html: 'Some content'
 *          }]
 *      });
 */
Ext.define('Ext.layout.container.Center', {
    extend: 'Ext.layout.container.Fit',
    alternateClassName: 'Ext.ux.layout.Center',
    alias: [
        'layout.center',
        'layout.ux.center'
    ],
    type: 'center',
    percentRe: /^\d+(?:\.\d+)?%$/,
    itemCls: Ext.baseCSSPrefix + 'center-layout-item',
    childEls: [
        'targetEl'
    ],
    /* eslint-disable indent, max-len */
    renderTpl: [
        '<div id="{ownerId}-targetEl" data-ref="targetEl" class="{targetElCls}" role="presentation">' + '{%this.renderBody(out, values)%}' + '</div>'
    ],
    /* eslint-enable indent, max-len */
    targetElCls: Ext.baseCSSPrefix + 'center-target',
    beginLayout: function(ownerContext) {
        var me = this,
            percentRe = me.percentRe,
            childItems, len, i, itemContext, item;
        me.callParent([
            ownerContext
        ]);
        childItems = ownerContext.childItems;
        for (i = 0 , len = childItems.length; i < len; ++i) {
            itemContext = childItems[i];
            item = itemContext.target;
            if (percentRe.test(item.width)) {
                item.getEl().setStyle('width', '');
            }
            if (percentRe.test(item.height)) {
                item.getEl().setStyle('height', '');
            }
        }
        ownerContext.targetElContext = ownerContext.getEl('targetEl', me);
    },
    beginLayoutCycle: function(ownerContext, firstCycle) {
        var targetEl = this.targetEl;
        this.callParent([
            ownerContext,
            firstCycle
        ]);
        targetEl.setStyle('width', '');
        targetEl.setStyle('height', '');
    },
    getRenderData: function() {
        var data = this.callParent();
        data.targetElCls = this.targetElCls;
        return data;
    },
    getRenderTarget: function() {
        return this.targetEl;
    },
    getItemSizePolicy: function(item, ownerSizeModel) {
        var me = this,
            sizeModel = ownerSizeModel || me.owner.getSizeModel(),
            percentRe = me.percentRe,
            mode = ((sizeModel.width.shrinkWrap || !percentRe.test(item.width)) ? 0 : 1) | ((sizeModel.height.shrinkWrap || !percentRe.test(item.height)) ? 0 : 2);
        return me.sizePolicies[mode];
    },
    isItemShrinkWrap: function(item) {
        return true;
    },
    calculate: function(ownerContext) {
        var targetElContext = ownerContext.targetElContext,
            info;
        this.callParent([
            ownerContext
        ]);
        info = ownerContext.state.info;
        if (ownerContext.widthModel.shrinkWrap) {
            targetElContext.setWidth(info.contentWidth);
        }
        if (ownerContext.heightModel.shrinkWrap) {
            targetElContext.setHeight(info.contentHeight);
        }
    },
    getPos: function(itemContext, info, dimension) {
        var modelName = dimension + 'Model',
            size = itemContext.props[dimension],
            pos = 0;
        if (!itemContext[modelName].calculated) {
            size += info.margins[dimension];
        }
        if (!info.ownerContext[modelName].shrinkWrap) {
            pos = Math.round((info.targetSize[dimension] - size) / 2);
            if (isNaN(pos)) {
                this.done = false;
            }
        }
        return Math.max(pos, 0);
    },
    positionItemX: function(itemContext, info) {
        var left = this.getPos(itemContext, info, 'width');
        itemContext.setProp('x', left);
    },
    positionItemY: function(itemContext, info) {
        var top = this.getPos(itemContext, info, 'height');
        itemContext.setProp('y', top);
    },
    setItemHeight: function(itemContext, info) {
        var ratio = parseFloat(itemContext.target.height) / 100;
        itemContext.setHeight(Math.round((info.targetSize.height - info.margins.height) * ratio));
    },
    setItemWidth: function(itemContext, info) {
        var ratio = parseFloat(itemContext.target.width) / 100;
        itemContext.setWidth(Math.round((info.targetSize.width - info.margins.width) * ratio));
    }
});

/**
 * This is a layout that will render form Fields, one under the other all stretched to the Container
 * width.
 *
 *     @example
 *     Ext.create('Ext.Panel', {
 *         width: 500,
 *         height: 300,
 *         title: "FormLayout Panel",
 *         layout: 'form',
 *         renderTo: Ext.getBody(),
 *         bodyPadding: 5,
 *         defaultType: 'textfield',
 *         items: [{
 *            fieldLabel: 'First Name',
 *             name: 'first',
 *             allowBlank:false
 *         },{
 *             fieldLabel: 'Last Name',
 *             name: 'last'
 *         },{
 *             fieldLabel: 'Company',
 *             name: 'company'
 *         }, {
 *             fieldLabel: 'Email',
 *             name: 'email',
 *             vtype:'email'
 *         }, {
 *             fieldLabel: 'DOB',
 *             name: 'dob',
 *             xtype: 'datefield'
 *         }, {
 *             fieldLabel: 'Age',
 *             name: 'age',
 *             xtype: 'numberfield',
 *             minValue: 0,
 *             maxValue: 100
 *         }, {
 *             xtype: 'timefield',
 *             fieldLabel: 'Time',
 *             name: 'time',
 *             minValue: '8:00am',
 *             maxValue: '6:00pm'
 *         }]
 *     });
 */
Ext.define('Ext.layout.container.Form', {
    extend: 'Ext.layout.container.Auto',
    alternateClassName: 'Ext.layout.FormLayout',
    alias: 'layout.form',
    type: 'form',
    formWrapCls: Ext.baseCSSPrefix + 'form-layout-wrap',
    formWrapAutoLabelCls: Ext.baseCSSPrefix + 'form-layout-auto-label',
    formWrapSizedLabelCls: Ext.baseCSSPrefix + 'form-layout-sized-label',
    formColGroupCls: Ext.baseCSSPrefix + 'form-layout-colgroup',
    formColumnCls: Ext.baseCSSPrefix + 'form-layout-column',
    formLabelColumnCls: Ext.baseCSSPrefix + 'form-layout-label-column',
    /**
     * @cfg {Number} itemSpacing
     * The amount of space, in pixels, to use between the items. Defaults to the value
     * inherited from the theme's stylesheet as configured by
     * {@link Ext.form.Labelable#$form-item-margin-bottom $form-item-margin-bottom}.
     */
    /**
     * @cfg {Number/String} labelWidth
     * The width of the labels. This can be either a number in pixels, or a valid CSS
     * "width" style, e.g. `'100px'`, or `'30%'`.  When configured, all labels will assume
     * this width, and any {@link Ext.form.Labelable#labelWidth labelWidth} specified
     * on the items will be ignored.
     *
     * The default behavior of this layout when no no labelWidth is specified is to size
     * the labels to the text-width of the label with the longest text.
     */
    childEls: [
        'formWrap',
        'labelColumn'
    ],
    /* eslint-disable max-len */
    beforeBodyTpl: '<div id="{ownerId}-formWrap" data-ref="formWrap" class="{formWrapCls}"' + '<tpl if="itemSpacing"> style="border-spacing:{itemSpacing}px"</tpl>>' + '<div class="{formColGroupCls}">' + '<div id="{ownerId}-labelColumn" data-ref="labelColumn" class="{formColumnCls} {formLabelColumnCls}"' + '<tpl if="labelWidth"> style="width:{labelWidth}"</tpl>>' + '</div>' + '<div class="{formColumnCls}"></div>' + '</div>',
    /* eslint-enable max-len */
    afterBodyTpl: '</div>',
    getRenderData: function() {
        var me = this,
            labelWidth = me.labelWidth,
            formWrapCls = me.formWrapCls,
            data = me.callParent();
        if (labelWidth) {
            if (typeof labelWidth === 'number') {
                labelWidth += 'px';
            }
            data.labelWidth = labelWidth;
            formWrapCls += ' ' + me.formWrapSizedLabelCls;
        } else {
            formWrapCls += ' ' + me.formWrapAutoLabelCls;
        }
        data.formWrapCls = formWrapCls;
        data.formColGroupCls = me.formColGroupCls;
        data.formColumnCls = me.formColumnCls;
        data.formLabelColumnCls = me.formLabelColumnCls;
        return data;
    },
    getRenderTarget: function() {
        return this.formWrap;
    }
});

/**
 * Horizontal Menu bar widget, a specialized version of the {@link Ext.menu.Menu}.
 *
 *      @example
 *      new Ext.menu.Bar({
 *          renderTo: Ext.getBody(),
 *          width: 200,
 *          items: [{
 *              text: 'File',
 *              menu: [
 *                  { text: 'Open...' },
 *                  '-',
 *                  { text: 'Close' }
 *              ]
 *          }, {
 *              text: 'Edit',
 *              menu: [
 *                  { text: 'Cut' },
 *                  { text: 'Copy' }
 *                  { text: 'Paste' }
 *              ]
 *          }]
 *      });
 */
Ext.define('Ext.menu.Bar', {
    extend: 'Ext.menu.Menu',
    xtype: 'menubar',
    isMenuBar: true,
    /**
     * @config {String} defaultMenuAlign The default {@link Ext.menu.Item#menuAlign} config
     * for direct child items of this Menu bar.
     */
    defaultMenuAlign: 'tl-bl?',
    floating: false,
    constrain: false,
    showSeparator: false,
    allowOtherMenus: true,
    ariaRole: 'menubar',
    ui: 'default-menubar',
    layout: {
        type: 'hbox',
        align: 'stretchmax',
        pack: 'start',
        overflowHandler: 'menu'
    },
    lookupComponent: function(comp) {
        comp = this.callParent([
            comp
        ]);
        if (comp.isMenuItem) {
            comp.menuAlign = this.defaultMenuAlign;
        }
        return comp;
    },
    privates: {
        onFocusableContainerLeftKey: function(e) {
            // The default action is to scroll the nearest horizontally scrollable container
            e.preventDefault();
            this.mixins.focusablecontainer.onFocusableContainerLeftKey.call(this, e);
        },
        onFocusableContainerRightKey: function(e) {
            // Ditto
            e.preventDefault();
            this.mixins.focusablecontainer.onFocusableContainerRightKey.call(this, e);
        },
        onFocusableContainerUpKey: function(e) {
            var focusItem = this.lastFocusedChild;
            e.preventDefault();
            // As per WAI-ARIA, both Up and Down arrow keys open submenu
            if (focusItem && focusItem.expandMenu) {
                focusItem.expandMenu(e, 0);
            }
        },
        onFocusableContainerDownKey: function(e) {
            var focusItem = this.lastFocusedChild;
            e.preventDefault();
            if (focusItem && focusItem.expandMenu) {
                focusItem.expandMenu(e, 0);
            }
        }
    }
});

/**
 * A menu containing a Ext.picker.Color Component.
 *
 * Notes:
 *
 *   - Although not listed here, the **constructor** for this class accepts all of the
 *     configuration options of {@link Ext.picker.Color}.
 *   - If subclassing ColorMenu, any configuration options for the ColorPicker must be
 *     applied to the **initialConfig** property of the ColorMenu. Applying
 *     {@link Ext.picker.Color ColorPicker} configuration settings to `this` will **not**
 *     affect the ColorPicker's configuration.
 *
 * Example:
 *
 *     @example
 *     var colorPicker = Ext.create('Ext.menu.ColorPicker', {
 *         value: '000000'
 *     });
 *
 *     Ext.create('Ext.menu.Menu', {
 *         items: [{
 *             text: 'Choose a color',
 *             menu: colorPicker
 *         }, {
 *             iconCls: 'add16',
 *             text: 'Icon item'
 *         }, {
 *             text: 'Regular item'
 *         }]
 *     }).showAt([5, 5]);
 */
Ext.define('Ext.menu.ColorPicker', {
    extend: 'Ext.menu.Menu',
    alias: 'widget.colormenu',
    requires: [
        'Ext.picker.Color'
    ],
    /**
     * @cfg {Boolean} hideOnClick
     * False to continue showing the menu after a color is selected.
     */
    hideOnClick: true,
    /**
     * @cfg {String} pickerId
     * An id to assign to the underlying color picker.
     */
    pickerId: null,
    /**
     * @cfg {Number} maxHeight
     * @private
     */
    /**
     * @property {Ext.picker.Color} picker
     * The {@link Ext.picker.Color} instance for this ColorMenu
     */
    /**
     * @event click
     * @private
     */
    initComponent: function() {
        var me = this,
            cfg = Ext.apply({}, me.initialConfig),
            pickerConfig;
        // Ensure we don't get duplicate listeners
        delete cfg.listeners;
        pickerConfig = Ext.applyIf({
            cls: Ext.baseCSSPrefix + 'menu-color-item',
            margin: 0,
            id: me.pickerId,
            xtype: 'colorpicker'
        }, cfg);
        // This is a Menu and it will have an ownerCmp pointing to its owning MenuItem.
        // This MUST not be propagated down into the picker. The picker's getRefOwner
        // must follow the ownerCt and find this Menu.
        delete pickerConfig.ownerCmp;
        Ext.apply(me, {
            plain: true,
            showSeparator: false,
            bodyPadding: 0,
            items: [
                pickerConfig
            ]
        });
        me.callParent();
        me.picker = me.down('colorpicker');
        /**
         * @event select
         * @inheritdoc Ext.picker.Color#select
         */
        me.relayEvents(me.picker, [
            'select'
        ]);
        if (me.hideOnClick) {
            me.on('select', me.hidePickerOnSelect, me);
        }
    },
    /**
     * Hides picker on select if hideOnClick is true
     * @private
     */
    hidePickerOnSelect: function() {
        Ext.menu.Manager.hideAll();
    }
});

/**
 * A menu containing an Ext.picker.Date Component.
 *
 * Notes:
 *
 * - Although not listed here, the **constructor** for this class accepts all of the
 *   configuration options of **{@link Ext.picker.Date}**.
 * - If subclassing DateMenu, any configuration options for the DatePicker must be applied
 *   to the **initialConfig** property of the DateMenu. Applying {@link Ext.picker.Date Date Picker}
 *   configuration settings to **this** will **not** affect the Date Picker's configuration.
 *
 * Example:
 *
 *     @example
 *     var dateMenu = Ext.create('Ext.menu.DatePicker', {
 *         handler: function(dp, date){
 *             Ext.Msg.alert('Date Selected', 'You selected ' + Ext.Date.format(date, 'M j, Y'));
 *         }
 *     });
 *
 *     Ext.create('Ext.menu.Menu', {
 *         items: [{
 *             text: 'Choose a date',
 *             menu: dateMenu
 *         },{
 *             iconCls: 'add16',
 *             text: 'Icon item'
 *         },{
 *             text: 'Regular item'
 *         }]
 *     }).showAt([5, 5]);
 */
Ext.define('Ext.menu.DatePicker', {
    extend: 'Ext.menu.Menu',
    alias: 'widget.datemenu',
    requires: [
        'Ext.picker.Date'
    ],
    ariaRole: 'dialog',
    /**
     * @cfg {String} ariaLabel
     * The ARIA label for the Date Picker menu.
     * @locale
     */
    ariaLabel: 'Date picker',
    /**
     * @cfg {Boolean} hideOnClick
     * False to continue showing the menu after a date is selected.
     */
    hideOnClick: true,
    /**
     * @cfg {String} pickerId
     * An id to assign to the underlying date picker.
     */
    pickerId: null,
    /**
     * @cfg {Object} [pickerCfg] Date picker configuration. This config
     * takes priority over {@link #pickerId}.
     */
    /**
     * @cfg {Number} maxHeight
     * @private
     */
    /**
     * @property {Ext.picker.Date} picker
     * The {@link Ext.picker.Date} instance for this DateMenu
     */
    // DatePicker menu is a special case; Date picker does all key handling
    // except the Esc key which is also handled unlike the ordinary menu
    focusableContainer: false,
    initComponent: function() {
        var me = this,
            cfg, pickerConfig;
        if (me.pickerCfg) {
            pickerConfig = Ext.apply({
                cls: Ext.baseCSSPrefix + 'menu-date-item',
                margin: 0,
                border: false,
                xtype: 'datepicker'
            }, me.pickerCfg);
        } else {
            // Need to keep this insanity for backwards compat :(
            cfg = Ext.apply({}, me.initialConfig);
            // Ensure we clear any listeners so they aren't duplicated
            delete cfg.listeners;
            pickerConfig = Ext.applyIf({
                cls: Ext.baseCSSPrefix + 'menu-date-item',
                margin: 0,
                border: false,
                xtype: 'datepicker'
            }, cfg);
        }
        if (me.pickerId != null && pickerConfig.id == null) {
            pickerConfig.id = me.pickerId;
        }
        // This is a Menu and it will have an ownerCmp pointing to its owning MenuItem.
        // This MUST not be propagated down into the picker. The picker's getRefOwner
        // must follow the ownerCt and find this Menu.
        delete pickerConfig.ownerCmp;
        Ext.apply(me, {
            showSeparator: false,
            plain: true,
            // remove the body padding from the datepicker menu item so it looks like 3.3
            bodyPadding: 0,
            items: [
                pickerConfig
            ]
        });
        me.callParent();
        me.picker = me.down('datepicker');
        /**
         * @event select
         * @inheritdoc Ext.picker.Date#select
         */
        me.relayEvents(me.picker, [
            'select'
        ]);
        if (me.hideOnClick) {
            me.on('select', me.hidePickerOnSelect, me);
        }
    },
    onEscapeKey: function(e) {
        var me = this;
        // Unlike the other menus, DatePicker menu should not close completely on Esc key.
        // This is because ordinary menu items will allow using Left arrow key to return
        // to the parent menu; however in the Date picker left arrow is used to navigate
        // in the calendar. So we use Esc key to return to the parent menu instead.
        if (me.floating && me.ownerCmp && me.ownerCmp.focus) {
            me.ownerCmp.focus();
            me.hide();
        }
    },
    hidePickerOnSelect: function() {
        Ext.menu.Manager.hideAll();
    }
});

/**
 * This mixin is applied to panels that want to manage a Pin state and corresponding tool.
 */
Ext.define('Ext.panel.Pinnable', {
    extend: 'Ext.Mixin',
    mixinId: 'pinnable',
    pinnable: true,
    pinnedTip: 'Unpin this item',
    unpinnedTip: 'Pin this item',
    initPinnable: function() {
        var me = this,
            pinned = me.isPinned();
        me.addTool(me.pinTool = Ext.widget({
            xtype: 'tool',
            type: pinned ? 'unpin' : 'pin',
            callback: 'togglePin',
            scope: me,
            tooltip: pinned ? me.pinnedTip : me.unpinnedTip
        }));
    },
    isPinned: function() {
        return !this.floating;
    },
    setPinned: function(pinned) {
        var me = this,
            args;
        if (pinned !== me.isPinned()) {
            args = [
                me,
                pinned
            ];
            if (me.fireEventArgs('beforepinchange', args) !== false) {
                me.updatePinned(pinned);
                me.fireEventArgs('pinchange', args);
            }
        }
    },
    togglePin: function() {
        this.setPinned(!this.isPinned());
    },
    updatePinned: function(pinned) {
        var me = this,
            tool = me.pinTool;
        tool.setTooltip(pinned ? me.pinnedTip : me.unpinnedTip);
        tool.setType(pinned ? 'unpin' : 'pin');
    }
});

/**
 * This plugin defers the execution cost of the instantiation and initialization of child components
 * of un-rendered items.
 *
 * For example, in a {@link Ext.tab.Panel#deferredRender deferredRender}
 * {@link Ext.tab.Panel TabPanel}, the un-rendered tabs do not have to incur the cost
 * of instantiating and initializing their descendant components until render.
 *
 * This plugin allows that.
 *
 * Add the items to the plugin:
 *
 *     {
 *         xtype: 'tabpanel',
 *         items: [{
 *             title: 'Tab One',
 *             plugins: {
 *                 lazyitems: {
 *                     items: [... tab child items...]
 *                 }
 *             }
 *         }, {
 *             title: 'Tab One',
 *             plugins: {
 *                 lazyitems: {
 *                     items: [... tab child items...]
 *                 }
 *             }
 *         }]
 *     }
 *
 */
Ext.define('Ext.plugin.LazyItems', {
    extend: 'Ext.plugin.Abstract',
    alias: 'plugin.lazyitems',
    init: function(comp) {
        this.callParent(arguments);
        if (this.items) {
            // Eager instantiation means create the child items now
            if (this.eagerInstantiation) {
                this.items = comp.prepareItems(this.items);
            }
        }
        // We need to jump in right before the beforeRender call
        comp.beforeRender = Ext.Function.createInterceptor(comp.beforeRender, this.beforeComponentRender, this);
    },
    // Add the child items at the last possible moment.
    beforeComponentRender: function() {
        this.cmp.add(this.items);
        // Remove the interceptor
        this.cmp.beforeComponentRender = null;
    }
});

/**
 * @deprecated 6.6.0 Require `Ext.Responsive` instead to enable `responsiveConfig`.
 */
Ext.define('Ext.plugin.Responsive', {
    extend: 'Ext.plugin.Abstract',
    alias: 'plugin.responsive',
    requires: [
        'Ext.Responsive'
    ],
    constructor: function() {
        this.callParent(arguments);
        Ext.log.warn('responsive plugin is deprecated; require "Ext.Responsive" instead');
    },
    id: 'responsive'
});

/**
 * Private utility class for Ext.BorderSplitter.
 * @private
 */
Ext.define('Ext.resizer.BorderSplitterTracker', {
    extend: 'Ext.resizer.SplitterTracker',
    requires: [
        'Ext.util.Region'
    ],
    getPrevCmp: null,
    getNextCmp: null,
    // calculate the constrain Region in which the splitter el may be moved.
    calculateConstrainRegion: function() {
        var me = this,
            splitter = me.splitter,
            collapseTarget = splitter.collapseTarget,
            defaultSplitMin = splitter.defaultSplitMin,
            sizePropCap = splitter.vertical ? 'Width' : 'Height',
            minSizeProp = 'min' + sizePropCap,
            maxSizeProp = 'max' + sizePropCap,
            getSizeMethod = 'get' + sizePropCap,
            neighbors = splitter.neighbors,
            length = neighbors.length,
            box = collapseTarget.el.getBox(),
            left = box.x,
            top = box.y,
            right = box.right,
            bottom = box.bottom,
            size = splitter.vertical ? (right - left) : (bottom - top),
            // neighborSizes = [],
            i, neighbor, neighborMaxSize, minRange, maxRange, maxGrowth, maxShrink, targetSize;
        // if size=100 and minSize=80, we can reduce by 20 so minRange = minSize-size = -20
        minRange = (collapseTarget[minSizeProp] || Math.min(size, defaultSplitMin)) - size;
        // if maxSize=150, maxRange = maxSize - size = 50
        maxRange = collapseTarget[maxSizeProp];
        if (!maxRange) {
            maxRange = 1000000000;
        } else {
            maxRange -= size;
        }
        targetSize = size;
        for (i = 0; i < length; ++i) {
            neighbor = neighbors[i];
            size = neighbor[getSizeMethod]();
            neighborMaxSize = neighbor[maxSizeProp];
            if (neighborMaxSize === null) {
                // calculations that follow expect NaN as a result if max size is undefined
                // e.g. (10 - undefined) returns NaN
                // Unfortunately the same is not true for null - (10 - null === 10) so
                // we convert null into undefined to make sure they both behave the same
                neighborMaxSize = undefined;
            }
            maxGrowth = size - neighborMaxSize;
            // NaN if no maxSize or negative
            maxShrink = size - (neighbor[minSizeProp] || Math.min(size, defaultSplitMin));
            if (!isNaN(maxGrowth)) {
                // if neighbor can only grow by 10 (maxGrowth = -10), minRange cannot be
                // -20 anymore, but now only -10:
                if (minRange < maxGrowth) {
                    minRange = maxGrowth;
                }
            }
            // if neighbor can shrink by 20 (maxShrink=20), maxRange cannot be 50 anymore,
            // but now only 20:
            if (maxRange > maxShrink) {
                maxRange = maxShrink;
            }
        }
        if (maxRange - minRange < 2) {
            return null;
        }
        box = new Ext.util.Region(top, right, bottom, left);
        me.constraintAdjusters[me.getCollapseDirection()](box, minRange, maxRange, splitter);
        me.dragInfo = {
            minRange: minRange,
            maxRange: maxRange,
            // neighborSizes: neighborSizes,
            targetSize: targetSize
        };
        return box;
    },
    constraintAdjusters: {
        // splitter is to the right of the box
        left: function(box, minRange, maxRange, splitter) {
            box[0] = box.x = box.left = box.right + minRange;
            box.right += maxRange + splitter.getWidth();
        },
        // splitter is below the box
        top: function(box, minRange, maxRange, splitter) {
            box[1] = box.y = box.top = box.bottom + minRange;
            box.bottom += maxRange + splitter.getHeight();
        },
        // splitter is above the box
        bottom: function(box, minRange, maxRange, splitter) {
            box.bottom = box.top - minRange;
            box.top -= maxRange + splitter.getHeight();
        },
        // splitter is to the left of the box
        right: function(box, minRange, maxRange, splitter) {
            box.right = box.left - minRange;
            box[0] = box.x = box.left = box.x - maxRange + splitter.getWidth();
        }
    },
    onBeforeStart: function(e) {
        var me = this,
            splitter = me.splitter,
            collapseTarget = splitter.collapseTarget,
            neighbors = splitter.neighbors,
            length = neighbors.length,
            i, neighbor;
        if (collapseTarget.collapsed) {
            return false;
        }
        // disabled if any neighbors are collapsed in parallel direction.
        for (i = 0; i < length; ++i) {
            neighbor = neighbors[i];
            if (neighbor.collapsed && neighbor.isHorz === collapseTarget.isHorz) {
                return false;
            }
        }
        if (!(me.constrainTo = me.calculateConstrainRegion())) {
            return false;
        }
        return true;
    },
    performResize: function(e, offset) {
        var me = this,
            splitter = me.splitter,
            collapseDirection = splitter.getCollapseDirection(),
            collapseTarget = splitter.collapseTarget,
            // a vertical splitter adjusts horizontal dimensions
            adjusters = me.splitAdjusters[splitter.vertical ? 'horz' : 'vert'],
            delta = offset[adjusters.index],
            dragInfo = me.dragInfo,
            // neighbors = splitter.neighbors,
            // length = neighbors.length,
            // neighborSizes = dragInfo.neighborSizes,
            // isVert = collapseTarget.isVert,
            // i, neighbor,
            owner;
        if (collapseDirection === 'right' || collapseDirection === 'bottom') {
            // these splitters grow by moving left/up, so flip the sign of delta...
            delta = -delta;
        }
        // now constrain delta to our computed range:
        delta = Math.min(Math.max(dragInfo.minRange, delta), dragInfo.maxRange);
        if (delta) {
            (owner = splitter.ownerCt).suspendLayouts();
            adjusters.adjustTarget(collapseTarget, dragInfo.targetSize, delta);
            // for (i = 0; i < length; ++i) {
            //    neighbor = neighbors[i];
            //    if (!neighbor.isCenter && !neighbor.maintainFlex && neighbor.isVert == isVert) {
            //        delete neighbor.flex;
            //        adjusters.adjustNeighbor(neighbor, neighborSizes[i], delta);
            //     }
            // }
            owner.resumeLayouts(true);
        }
    },
    splitAdjusters: {
        horz: {
            index: 0,
            // adjustNeighbor: function(neighbor, size, delta) {
            //    neighbor.setSize(size - delta);
            // },
            adjustTarget: function(target, size, delta) {
                target.flex = null;
                target.setSize(size + delta);
            }
        },
        vert: {
            index: 1,
            // adjustNeighbor: function(neighbor, size, delta) {
            //    neighbor.setSize(undefined, size - delta);
            // },
            adjustTarget: function(target, targetSize, delta) {
                target.flex = null;
                target.setSize(undefined, targetSize + delta);
            }
        }
    },
    getCollapseDirection: function() {
        return this.splitter.getCollapseDirection();
    }
});

/**
 * Provides a handle for 9-point resizing of Elements or Components.
 */
Ext.define('Ext.resizer.Handle', {
    extend: 'Ext.Component',
    handleCls: '',
    baseHandleCls: Ext.baseCSSPrefix + 'resizable-handle',
    // Ext.resizer.Resizer.prototype.possiblePositions define the regions
    // which will be passed in as a region configuration.
    region: '',
    ariaRole: 'presentation',
    beforeRender: function() {
        var me = this;
        me.callParent();
        me.protoEl.unselectable();
        me.addCls(me.baseHandleCls, me.baseHandleCls + '-' + me.region, me.handleCls);
    }
});

/**
 * Private utility class for Ext.resizer.Resizer.
 * @private
 */
Ext.define('Ext.resizer.ResizeTracker', {
    extend: 'Ext.dd.DragTracker',
    dynamic: true,
    preserveRatio: false,
    // Resizer mousedown must blur active element
    preventDefault: false,
    // Default to no constraint
    constrainTo: null,
    proxyCls: Ext.baseCSSPrefix + 'resizable-proxy',
    constructor: function(config) {
        var me = this,
            widthRatio, heightRatio, throttledResizeFn;
        if (!config.el) {
            if (config.target.isComponent) {
                me.el = config.target.getEl();
            } else {
                me.el = config.target;
            }
        }
        this.callParent(arguments);
        // Ensure that if we are preserving aspect ratio, the largest minimum is honoured
        if (me.preserveRatio && me.minWidth && me.minHeight) {
            widthRatio = me.minWidth / me.el.getWidth();
            heightRatio = me.minHeight / me.el.getHeight();
            // largest ratio of minimum:size must be preserved.
            // So if a 400x200 pixel image has
            // minWidth: 50, maxWidth: 50, the maxWidth will be 400 * (50/200)... that is 100
            if (heightRatio > widthRatio) {
                me.minWidth = me.el.getWidth() * heightRatio;
            } else {
                me.minHeight = me.el.getHeight() * widthRatio;
            }
        }
        // If configured as throttled, create an instance version of resize which calls
        // a throttled function to perform the resize operation.
        if (me.throttle) {
            throttledResizeFn = Ext.Function.createThrottled(function() {
                Ext.resizer.ResizeTracker.prototype.resize.apply(me, arguments);
            }, me.throttle);
            me.resize = function(box, direction, atEnd) {
                if (atEnd) {
                    Ext.resizer.ResizeTracker.prototype.resize.apply(me, arguments);
                } else {
                    throttledResizeFn.apply(null, arguments);
                }
            };
        }
    },
    onBeforeStart: function(e) {
        // record the startBox
        this.startBox = this.target.getBox();
    },
    /**
     * @private
     * Returns the object that will be resized instead of the true target on every mousemove event.
     * If dynamic is false, this will be a proxy, otherwise it will be null target.
     */
    getProxy: function() {
        var me = this;
        if (!me.dynamic && !me.proxy) {
            me.proxy = me.createProxy(me.target || me.el);
            // Only hide proxy at end if we create one dynamically
            // When a wrapped resizer is used it passes the wrapping el in as the proxy.
            me.hideProxy = true;
        }
        if (me.proxy) {
            me.proxy.show();
            return me.proxy;
        }
    },
    /**
     * Create a proxy for this resizer
     * @param {Ext.Component/Ext.dom.Element} target The target
     * @return {Ext.dom.Element} A proxy element
     */
    createProxy: function(target) {
        var proxy,
            cls = this.proxyCls;
        if (target.isComponent) {
            proxy = target.getProxy().addCls(cls);
        } else {
            proxy = target.createProxy({
                tag: 'div',
                role: 'presentation',
                cls: cls,
                id: target.id + '-rzproxy'
            }, Ext.getBody());
        }
        proxy.removeCls(Ext.baseCSSPrefix + 'proxy-el');
        return proxy;
    },
    onStart: function(e) {
        // returns the Ext.ResizeHandle that the user started dragging
        this.activeResizeHandle = Ext.get(this.getDragTarget().id);
        // If we are using a proxy, ensure it is sized.
        if (!this.dynamic) {
            this.resize(this.startBox);
        }
    },
    onMouseDown: function(e, target) {
        // Logic to resize components on top of iframes or handle resizers bound to iframes.
        // To properly handle iframes "below" the resizable component, we cannot wait for 
        // triggerStart or onStart because if the cursor moves out of the component and over the 
        // iframe we won't detect that the drag has started. We cannot do this in
        // general because other draggable things cannot assume that mouseDown is safe
        // for this purpose. In particular ComponentDragger on a maximizable window will
        // get tricked by the maximize button onMouseDown and mask everything but will
        // never get the onMouseUp to unmask.
        this.callParent([
            e,
            target
        ]);
        Ext.dom.Element.maskIframes();
    },
    onMouseUp: function(e) {
        this.callParent([
            e
        ]);
        Ext.dom.Element.unmaskIframes();
    },
    onDrag: function(e) {
        // dynamic resizing, update dimensions during resize
        if (this.dynamic || this.proxy) {
            this.updateDimensions(e);
        }
    },
    updateDimensions: function(e, atEnd) {
        var me = this,
            region = me.activeResizeHandle.region,
            offset = me.getOffset(me.constrainTo ? 'dragTarget' : null),
            box = me.startBox,
            ratio,
            widthAdjust = 0,
            heightAdjust = 0,
            snappedWidth, snappedHeight,
            adjustX = 0,
            adjustY = 0,
            dragRatio, oppositeCorner, axis, // 1 = x, 2 = y, 3 = x and y.
            newBox, newHeight, newWidth;
        region = me.convertRegionName(region);
        switch (region) {
            case 'south':
                heightAdjust = offset[1];
                axis = 2;
                break;
            case 'north':
                heightAdjust = -offset[1];
                adjustY = -heightAdjust;
                axis = 2;
                break;
            case 'east':
                widthAdjust = offset[0];
                axis = 1;
                break;
            case 'west':
                widthAdjust = -offset[0];
                adjustX = -widthAdjust;
                axis = 1;
                break;
            case 'northeast':
                heightAdjust = -offset[1];
                adjustY = -heightAdjust;
                widthAdjust = offset[0];
                oppositeCorner = [
                    box.x,
                    box.y + box.height
                ];
                axis = 3;
                break;
            case 'southeast':
                heightAdjust = offset[1];
                widthAdjust = offset[0];
                oppositeCorner = [
                    box.x,
                    box.y
                ];
                axis = 3;
                break;
            case 'southwest':
                widthAdjust = -offset[0];
                adjustX = -widthAdjust;
                heightAdjust = offset[1];
                oppositeCorner = [
                    box.x + box.width,
                    box.y
                ];
                axis = 3;
                break;
            case 'northwest':
                heightAdjust = -offset[1];
                adjustY = -heightAdjust;
                widthAdjust = -offset[0];
                adjustX = -widthAdjust;
                oppositeCorner = [
                    box.x + box.width,
                    box.y + box.height
                ];
                axis = 3;
                break;
        }
        newBox = {
            width: box.width + widthAdjust,
            height: box.height + heightAdjust,
            x: box.x + adjustX,
            y: box.y + adjustY
        };
        // Snap value between stops according to configured increments
        snappedWidth = Ext.Number.snap(newBox.width, me.widthIncrement);
        snappedHeight = Ext.Number.snap(newBox.height, me.heightIncrement);
        if (snappedWidth !== newBox.width || snappedHeight !== newBox.height) {
            switch (region) {
                case 'northeast':
                    newBox.y -= snappedHeight - newBox.height;
                    break;
                case 'north':
                    newBox.y -= snappedHeight - newBox.height;
                    break;
                case 'southwest':
                    newBox.x -= snappedWidth - newBox.width;
                    break;
                case 'west':
                    newBox.x -= snappedWidth - newBox.width;
                    break;
                case 'northwest':
                    newBox.x -= snappedWidth - newBox.width;
                    newBox.y -= snappedHeight - newBox.height;
            }
            newBox.width = snappedWidth;
            newBox.height = snappedHeight;
        }
        // out of bounds
        if (newBox.width < me.minWidth || newBox.width > me.maxWidth) {
            newBox.width = Ext.Number.constrain(newBox.width, me.minWidth, me.maxWidth);
            // Re-adjust the X position if we were dragging the west side
            if (adjustX) {
                newBox.x = box.x + (box.width - newBox.width);
            }
        } else {
            me.lastX = newBox.x;
        }
        if (newBox.height < me.minHeight || newBox.height > me.maxHeight) {
            newBox.height = Ext.Number.constrain(newBox.height, me.minHeight, me.maxHeight);
            // Re-adjust the Y position if we were dragging the north side
            if (adjustY) {
                newBox.y = box.y + (box.height - newBox.height);
            }
        } else {
            me.lastY = newBox.y;
        }
        // If this is configured to preserve the aspect ratio,
        // or they are dragging using the shift key
        if (me.preserveRatio || e.shiftKey) {
            ratio = me.startBox.width / me.startBox.height;
            // Calculate aspect ratio constrained values.
            newHeight = Math.min(Math.max(me.minHeight, newBox.width / ratio), me.maxHeight);
            newWidth = Math.min(Math.max(me.minWidth, newBox.height * ratio), me.maxWidth);
            // X axis: width-only change, height must obey
            if (axis === 1) {
                newBox.height = newHeight;
            }
            // Y axis: height-only change, width must obey
            else if (axis === 2) {
                newBox.width = newWidth;
            } else // Corner drag.
            {
                // Drag ratio is the ratio of the mouse point from the opposite corner.
                // Basically what edge we are dragging, a horizontal edge or a vertical edge.
                dragRatio = Math.abs(oppositeCorner[0] - this.lastXY[0]) / Math.abs(oppositeCorner[1] - this.lastXY[1]);
                // If drag ratio > aspect ratio then width is dominant and height must obey
                if (dragRatio > ratio) {
                    newBox.height = newHeight;
                } else {
                    newBox.width = newWidth;
                }
                // Handle dragging start coordinates
                if (region === 'northeast') {
                    newBox.y = box.y - (newBox.height - box.height);
                } else if (region === 'northwest') {
                    newBox.y = box.y - (newBox.height - box.height);
                    newBox.x = box.x - (newBox.width - box.width);
                } else if (region === 'southwest') {
                    newBox.x = box.x - (newBox.width - box.width);
                }
            }
        }
        // Keep track of whether position needs changing
        me.setPosition = newBox.x !== me.startBox.x || newBox.y !== me.startBox.y;
        me.resize(newBox, atEnd);
    },
    resize: function(box, atEnd) {
        var me = this,
            target,
            setPosition = me.setPosition;
        // We are live resizing the target, or at the end: Size the target
        if (me.dynamic || (!me.dynamic && atEnd)) {
            // Resize the target
            if (setPosition) {
                me.target.setBox(box);
            } else {
                me.target.setSize(box.width, box.height);
            }
        }
        // In the middle of a resize - just resize the proxy
        if (!atEnd) {
            target = me.getProxy();
            if (target && target !== me.target) {
                if (setPosition || me.hideProxy) {
                    target.setBox(box);
                } else {
                    target.setSize(box.width, box.height);
                }
            }
        }
    },
    onEnd: function(e) {
        this.updateDimensions(e, true);
        if (this.proxy && this.hideProxy) {
            this.proxy.hide();
        }
    },
    convertRegionName: function(name) {
        return name;
    }
});

/**
 * Applies drag handles to an element or component to make it resizable. The drag handles
 * are inserted into the element (or component's element) and positioned absolute.
 *
 * Textarea and img elements will be wrapped with an additional div because these elements
 * do not support child nodes. The original element can be accessed through the originalTarget
 * property.
 *
 * Here is the list of valid resize handles:
 *
 *     Value   Description
 *     ------  -------------------
 *      'n'     north
 *      's'     south
 *      'e'     east
 *      'w'     west
 *      'nw'    northwest
 *      'sw'    southwest
 *      'se'    southeast
 *      'ne'    northeast
 *      'all'   all
 *
 * {@img Ext.resizer.Resizer/Ext.resizer.Resizer.png Ext.resizer.Resizer component}
 *
 * Here's an example showing the creation of a typical Resizer:
 *
 *     Ext.create('Ext.resizer.Resizer', {
 *         target: 'elToResize',
 *         handles: 'all',
 *         minWidth: 200,
 *         minHeight: 100,
 *         maxWidth: 500,
 *         maxHeight: 400,
 *         pinned: true
 *     });
 */
Ext.define('Ext.resizer.Resizer', {
    alternateClassName: 'Ext.Resizable',
    mixins: {
        observable: 'Ext.util.Observable'
    },
    uses: [
        'Ext.resizer.ResizeTracker',
        'Ext.Component'
    ],
    handleCls: Ext.baseCSSPrefix + 'resizable-handle',
    overCls: Ext.baseCSSPrefix + 'resizable-handle-over',
    pinnedCls: Ext.baseCSSPrefix + 'resizable-pinned',
    wrapCls: Ext.baseCSSPrefix + 'resizable-wrap',
    wrappedCls: Ext.baseCSSPrefix + 'resizable-wrapped',
    delimiterRe: /(?:\s*[,;]\s*)|\s+/,
    /**
     * @cfg {Boolean} dynamic
     * Specify as true to update the {@link #target} (Element or {@link Ext.Component Component})
     * dynamically during dragging. This is `true` by default, but the
     * {@link Ext.Component Component} class passes `false` when it is configured as
     * {@link Ext.Component#resizable}.
     *
     * If specified as `false`, a proxy element is displayed during the resize operation,
     * and the {@link #target} is updated on mouseup.
     */
    dynamic: true,
    /**
     * @cfg {String} handles
     * String consisting of the resize handles to display. Defaults to 's e se' for Elements
     * and fixed position Components. Defaults to 8 point resizing for floating Components
     * (such as Windows). Specify either `'all'` or any of `'n s e w ne nw se sw'`.
     */
    handles: 's e se',
    /**
     * @cfg {Number} height
     * Optional. The height to set target to in pixels
     */
    height: null,
    /**
     * @cfg {Number} width
     * Optional. The width to set the target to in pixels
     */
    width: null,
    /**
     * @cfg {Number} heightIncrement
     * The increment to snap the height resize in pixels.
     */
    heightIncrement: 0,
    /**
     * @cfg {Number} widthIncrement
     * The increment to snap the width resize in pixels.
     */
    widthIncrement: 0,
    /**
     * @cfg {Number} minHeight
     * The minimum height for the element
     */
    minHeight: 20,
    /**
     * @cfg {Number} minWidth
     * The minimum width for the element
     */
    minWidth: 20,
    /**
     * @cfg {Number} maxHeight
     * The maximum height for the element
     */
    maxHeight: 10000,
    /**
     * @cfg {Number} maxWidth
     * The maximum width for the element
     */
    maxWidth: 10000,
    /**
     * @cfg {Boolean} pinned
     * True to ensure that the resize handles are always visible, false indicates resizing
     * by cursor changes only
     */
    pinned: false,
    /**
     * @cfg {Boolean} preserveRatio
     * True to preserve the original ratio between height and width during resize
     */
    preserveRatio: false,
    /**
     * @cfg {Boolean} transparent
     * True for transparent handles. This is only applied at config time.
     */
    transparent: false,
    /**
     * @cfg {Ext.dom.Element/Ext.util.Region} constrainTo
     * An element, or a {@link Ext.util.Region Region} into which the resize operation
     * must be constrained.
     */
    possiblePositions: {
        n: 'north',
        s: 'south',
        e: 'east',
        w: 'west',
        se: 'southeast',
        sw: 'southwest',
        nw: 'northwest',
        ne: 'northeast'
    },
    /**
     * @private
     */
    touchActionMap: {
        n: {
            panY: false
        },
        s: {
            panY: false
        },
        e: {
            panX: false
        },
        w: {
            panX: false
        },
        se: {
            panX: false,
            panY: false
        },
        sw: {
            panX: false,
            panY: false
        },
        nw: {
            panX: false,
            panY: false
        },
        ne: {
            panX: false,
            panY: false
        }
    },
    /**
     * @cfg {Ext.dom.Element/Ext.Component} target
     * The Element or Component to resize.
     */
    /**
     * @property {Ext.dom.Element} el
     * Outer element for resizing behavior.
     */
    ariaRole: 'presentation',
    /**
     * @event beforeresize
     * Fired before resize is allowed. Return false to cancel resize.
     * @param {Ext.resizer.Resizer} this
     * @param {Number} width The start width
     * @param {Number} height The start height
     * @param {Ext.event.Event} e The mousedown event
     */
    /**
     * @event resizedrag
     * Fires during resizing.
     * @param {Ext.resizer.Resizer} this
     * @param {Number} width The new width
     * @param {Number} height The new height
     * @param {Ext.event.Event} e The mousedown event
     */
    /**
     * @event resize
     * Fired after a resize.
     * @param {Ext.resizer.Resizer} this
     * @param {Number} width The new width
     * @param {Number} height The new height
     * @param {Ext.event.Event} e The mouseup event
     */
    constructor: function(config) {
        var me = this,
            unselectableCls = Ext.dom.Element.unselectableCls,
            handleEls = [],
            resizeTarget, handleCls, possibles, tag, len, i, pos, box, handle, handles, handleEl, wrapTarget, positioning, targetBaseCls;
        if (Ext.isString(config) || Ext.isElement(config) || config.dom) {
            resizeTarget = config;
            config = arguments[1] || {};
            config.target = resizeTarget;
        }
        // will apply config to this
        me.mixins.observable.constructor.call(me, config);
        // If target is a Component, ensure that we pull the element out.
        // Resizer must examine the underlying Element.
        resizeTarget = me.target;
        if (resizeTarget) {
            if (resizeTarget.isComponent) {
                // Resizable Components get a new UI class on them which makes them overflow:visible
                // if the border width is non-zero and therefore the SASS has embedded the handles
                // in the borders using -ve position.
                resizeTarget.addClsWithUI('resizable');
                if (resizeTarget.minWidth) {
                    me.minWidth = resizeTarget.minWidth;
                }
                if (resizeTarget.minHeight) {
                    me.minHeight = resizeTarget.minHeight;
                }
                if (resizeTarget.maxWidth) {
                    me.maxWidth = resizeTarget.maxWidth;
                }
                if (resizeTarget.maxHeight) {
                    me.maxHeight = resizeTarget.maxHeight;
                }
                if (resizeTarget.floating) {
                    if (!me.hasOwnProperty('handles')) {
                        me.handles = 'n ne e se s sw w nw';
                    }
                }
                me.el = resizeTarget.getEl();
            } else {
                resizeTarget = me.el = me.target = Ext.get(resizeTarget);
            }
        } else // Backwards compatibility with Ext3.x's Resizable which used el as a config.
        {
            resizeTarget = me.target = me.el = Ext.get(me.el);
        }
        // Locally enforce border box model.
        // https://sencha.jira.com/browse/EXTJSIV-11511
        me.el.addCls(Ext.Component.prototype.borderBoxCls);
        // Constrain within configured maxima
        if (Ext.isNumber(me.width)) {
            me.width = Ext.Number.constrain(me.width, me.minWidth, me.maxWidth);
        }
        if (Ext.isNumber(me.height)) {
            me.height = Ext.Number.constrain(me.height, me.minHeight, me.maxHeight);
        }
        // Size the target.
        if (me.width !== null || me.height !== null) {
            me.target.setSize(me.width, me.height);
        }
        // Tags like textarea and img cannot
        // have children and therefore must
        // be wrapped
        tag = me.el.dom.tagName.toUpperCase();
        if (tag === 'TEXTAREA' || tag === 'IMG' || tag === 'TABLE') {
            /**
             * @property {Ext.dom.Element/Ext.Component} originalTarget
             * Reference to the original resize target if the element of the original
             * resize target was a {@link Ext.form.field.Field Field}, or an IMG or a TEXTAREA
             * which must be wrapped in a DIV.
             */
            me.originalTarget = me.target;
            wrapTarget = resizeTarget.isComponent ? resizeTarget.getEl() : resizeTarget;
            // Tag the wrapped element with a class so thaht we can force it
            // to use border box sizing model
            me.el.addCls(me.wrappedCls);
            me.target = me.el = me.el.wrap({
                role: 'presentation',
                cls: me.wrapCls,
                id: me.el.id + '-rzwrap',
                style: wrapTarget.getStyle([
                    'margin-top',
                    'margin-bottom'
                ])
            });
            positioning = wrapTarget.getPositioning();
            // Transfer originalTarget's positioning+sizing+margins
            me.el.setPositioning(positioning);
            wrapTarget.clearPositioning();
            box = wrapTarget.getBox();
            if (positioning.position !== 'absolute') {
                // reset coordinates
                box.x = 0;
                box.y = 0;
            }
            me.el.setBox(box);
            // Position the wrapped element absolute so that it does not stretch the wrapper
            wrapTarget.setStyle('position', 'absolute');
            me.isTargetWrapped = true;
        }
        // Position the element, this enables us to absolute position
        // the handles within this.el
        me.el.position();
        if (me.pinned) {
            me.el.addCls(me.pinnedCls);
        }
        /**
         * @property {Ext.resizer.ResizeTracker} resizeTracker
         */
        me.resizeTracker = new Ext.resizer.ResizeTracker({
            disabled: me.disabled,
            target: resizeTarget,
            el: me.el,
            constrainTo: me.constrainTo,
            handleCls: me.handleCls,
            overCls: me.overCls,
            throttle: me.throttle,
            // If we have wrapped something, instruct the ResizerTracker to use that wrapper
            // as a proxy and we should resize the wrapped target dynamically.
            proxy: me.originalTarget ? me.el : null,
            dynamic: me.originalTarget ? true : me.dynamic,
            originalTarget: me.originalTarget,
            delegate: '.' + me.handleCls,
            preserveRatio: me.preserveRatio,
            heightIncrement: me.heightIncrement,
            widthIncrement: me.widthIncrement,
            minHeight: me.minHeight,
            maxHeight: me.maxHeight,
            minWidth: me.minWidth,
            maxWidth: me.maxWidth
        });
        // Relay the ResizeTracker's superclass events as our own resize events
        me.resizeTracker.on({
            mousedown: me.onBeforeResize,
            drag: me.onResize,
            dragend: me.onResizeEnd,
            scope: me
        });
        if (me.handles === 'all') {
            me.handles = 'n s e w ne nw se sw';
        }
        handles = me.handles = me.handles.split(me.delimiterRe);
        possibles = me.possiblePositions;
        len = handles.length;
        handleCls = me.handleCls + ' ' + me.handleCls + '-{0}';
        if (me.target.isComponent) {
            targetBaseCls = me.target.baseCls;
            handleCls += ' ' + targetBaseCls + '-handle ' + targetBaseCls + '-handle-{0}';
            if (Ext.supports.CSS3BorderRadius) {
                handleCls += ' ' + targetBaseCls + '-handle-{0}-br';
            }
        }
        for (i = 0; i < len; i++) {
            // if specified and possible, create
            handle = handles[i];
            if (handle && possibles[handle]) {
                pos = possibles[handle];
                handleEl = me[pos] = me.el.createChild({
                    id: me.el.id + '-' + pos + '-handle',
                    cls: Ext.String.format(handleCls, pos) + ' ' + unselectableCls,
                    unselectable: 'on',
                    role: 'presentation'
                });
                handleEl.region = pos;
                if (me.transparent) {
                    handleEl.setOpacity(0);
                }
                handleEl.setTouchAction(me.touchActionMap[handle]);
                handleEls.push(handleEl);
            }
        }
        me.resizeTracker.handleEls = handleEls;
    },
    disable: function() {
        this.disabled = true;
        this.resizeTracker.disable();
    },
    enable: function() {
        this.disabled = false;
        this.resizeTracker.enable();
    },
    /**
     * @private
     * Relay the Tracker's mousedown event as beforeresize
     * @param {Ext.resizer.ResizeTracker} tracker
     * @param {Ext.event.Event} e The event
     */
    onBeforeResize: function(tracker, e) {
        return this.fireResizeEvent('beforeresize', tracker, e);
    },
    /**
     * @private
     * Relay the Tracker's drag event as resizedrag
     * @param {Ext.resizer.ResizeTracker} tracker
     * @param {Ext.event.Event} e The event
     */
    onResize: function(tracker, e) {
        return this.fireResizeEvent('resizedrag', tracker, e);
    },
    /**
     * @private
     * Relay the Tracker's dragend event as resize
     * @param {Ext.resizer.ResizeTracker} tracker
     * @param {Ext.event.Event} e The event
     */
    onResizeEnd: function(tracker, e) {
        return this.fireResizeEvent('resize', tracker, e);
    },
    /**
     * @private
     * Fire a resize event, checking if we have listeners before firing.
     * @param {String} name The name of the event
     * @param {Ext.resizer.ResizeTracker} tracker
     * @param {Ext.event.Event} e The event
     */
    fireResizeEvent: function(name, tracker, e) {
        var me = this,
            box;
        if (me.hasListeners[name]) {
            box = me.el.getBox();
            return me.fireEvent(name, me, box.width, box.height, e);
        }
    },
    /**
     * Perform a manual resize and fires the 'resize' event.
     * @param {Number} width
     * @param {Number} height
     */
    resizeTo: function(width, height) {
        var me = this;
        me.target.setSize(width, height);
        me.fireEvent('resize', me, width, height, null);
    },
    /**
     * Returns the element that was configured with the el or target config property.
     * If a component was configured with the target property then this will return the element
     * of this component.
     *
     * Textarea and img elements will be wrapped with an additional div because these elements
     * do not support child nodes. The original element can be accessed through the originalTarget
     * property.
     * @return {Ext.dom.Element} element
     */
    getEl: function() {
        return this.el;
    },
    /**
     * Returns the element or component that was configured with the target config property.
     *
     * Textarea and img elements will be wrapped with an additional div because these elements
     * do not support child nodes. The original element can be accessed through the originalTarget
     * property.
     * @return {Ext.dom.Element/Ext.Component}
     */
    getTarget: function() {
        return this.target;
    },
    destroy: function() {
        var me = this,
            handles = me.handles,
            len = handles.length,
            positions = me.possiblePositions,
            handle, pos, i;
        me.resizeTracker.destroy();
        // The target is redefined as an element when it's wrapped so we must destroy it.
        if (me.isTargetWrapped) {
            me.target.destroy();
        }
        for (i = 0; i < len; i++) {
            pos = positions[handles[i]];
            if ((handle = me[pos])) {
                handle.destroy();
                me[pos] = null;
            }
        }
        me.callParent();
    }
});

/**
 * A selection model for {@link Ext.grid.Panel grid panels} which allows selection of a single cell
 * at a time.
 *
 * Implements cell based navigation via keyboard.
 *
 *     @example
 *     var store = Ext.create('Ext.data.Store', {
 *         fields: ['name', 'email', 'phone'],
 *         data: [
 *             { name: 'Lisa', email: 'lisa@simpsons.com',  phone: '555-111-1224' },
 *             { name: 'Bart', email: 'bart@simpsons.com',  phone: '555-222-1234' },
 *             { name: 'Homer', email: 'homer@simpsons.com', phone: '555-222-1244' },
 *             { name: 'Marge', email: 'marge@simpsons.com', phone: '555-222-1254' }
 *         ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Simpsons',
 *         store: store,
 *         width: 400,
 *         renderTo: Ext.getBody(),
 *         columns: [
 *             { text: 'Name',  dataIndex: 'name' },
 *             { text: 'Email', dataIndex: 'email', flex: 1 },
 *             { text: 'Phone', dataIndex: 'phone' }
 *         ],
 *         selModel: 'cellmodel'
 *     });
 */
Ext.define('Ext.selection.CellModel', {
    extend: 'Ext.selection.DataViewModel',
    alias: 'selection.cellmodel',
    requires: [
        'Ext.grid.CellContext'
    ],
    isCellModel: true,
    /**
     * @cfg {"SINGLE"} mode
     * Mode of selection.  Valid values are:
     *
     * - **"SINGLE"** - Only allows selecting one item at a time. This is the default.
     */
    /**
     * @cfg deselectOnContainerClick
     * @inheritdoc Ext.mixin.Selectable#cfg!deselectOnContainerClick
     */
    deselectOnContainerClick: false,
    /**
     * @cfg {Boolean} enableKeyNav
     * Turns on/off keyboard navigation within the grid.
     */
    enableKeyNav: true,
    /**
     * @cfg {Boolean} preventWrap
     * Set this configuration to true to prevent wrapping around of selection as
     * a user navigates to the first or last column.
     */
    preventWrap: false,
    /**
     * @event deselect
     * Fired after a cell is deselected
     * @param {Ext.selection.CellModel} this
     * @param {Ext.data.Model} record The record of the deselected cell
     * @param {Number} row The row index deselected
     * @param {Number} column The column index deselected
     */
    /**
     * @event select
     * Fired after a cell is selected
     * @param {Ext.selection.CellModel} this
     * @param {Ext.data.Model} record The record of the selected cell
     * @param {Number} row The row index selected
     * @param {Number} column The column index selected
     */
    bindComponent: function(view) {
        var me = this,
            grid;
        // Unbind from a view
        if (me.view && me.gridListeners) {
            me.gridListeners.destroy();
        }
        // DataViewModel's bindComponent
        me.callParent([
            view
        ]);
        if (view) {
            // view.grid is present during View construction, before the view has been
            // added as a child of the Panel, and an upward link it still needed.
            grid = view.grid || view.ownerCt;
            if (grid.optimizedColumnMove !== false) {
                me.gridListeners = grid.on({
                    columnmove: me.onColumnMove,
                    scope: me,
                    destroyable: true
                });
            }
        }
    },
    getViewListeners: function() {
        var result = this.callParent();
        result.refresh = this.onViewRefresh;
        return result;
    },
    getHeaderCt: function() {
        var selection = this.navigationModel.getPosition(),
            view = selection ? selection.view : this.primaryView;
        return view.headerCt;
    },
    // Selection blindly follows focus. For now.
    onNavigate: function(e) {
        // It was a navigate out event.
        // Or stopSelection was stamped into the event by an upstream handler.
        // This is used by ActionColumn and CheckColumn to implement their stopSelection config
        if (!e.record || e.keyEvent.stopSelection) {
            return;
        }
        this.setPosition(e.position);
    },
    selectWithEvent: function(record, e) {
        this.select(record);
    },
    /** 
     * Selects a cell by row / column.
     *
     *     var grid = Ext.create('Ext.grid.Panel', {
     *         title: 'Simpsons',
     *         store: {
     *             fields: ['name', 'email', 'phone'],
     *             data: [{
     *                 name: "Lisa",
     *                 email: "lisa@simpsons.com",
     *                 phone: "555-111-1224"
     *             }]
     *         },
     *         columns: [{
     *             text: 'Name',
     *             dataIndex: 'name'
     *         }, {
     *             text: 'Email',
     *             dataIndex: 'email',
     *             hidden: true
     *         }, {
     *             text: 'Phone',
     *             dataIndex: 'phone',
     *             flex: 1
     *         }],
     *         height: 200,
     *         width: 400,
     *         renderTo: Ext.getBody(),
     *         selType: 'cellmodel',
     *         tbar: [{
     *             text: 'Select position Object',
     *             handler: function() {
     *                 grid.getSelectionModel().select({
     *                     row: grid.getStore().getAt(0),
     *                     column: grid.down('gridcolumn[dataIndex=name]')
     *                 });
     *             }
     *         }, {
     *             text: 'Select position by Number',
     *             handler: function() {
     *                 grid.getSelectionModel().select({
     *                     row: 0,
     *                     column: 1
     *                 });
     *             }
     *         }]
     *     });
     *
     * @param {Object} pos An object with row and column properties
     * @param {Ext.data.Model/Number} pos.row
     *   A record or index of the record (starting at 0)
     * @param {Ext.grid.column.Column/Number} pos.column
     *   A column or index of the column (starting at 0).  Includes visible columns only.
     * @param keepExisting (private)
     * @param suppressEvent (private)
     */
    select: function(pos, keepExisting, suppressEvent) {
        var me = this,
            row,
            oldPos = me.getPosition(),
            store = me.view.store;
        if (pos || pos === 0) {
            if (pos.isModel) {
                row = store.indexOf(pos);
                if (row !== -1) {
                    pos = {
                        row: row,
                        column: oldPos ? oldPos.column : 0
                    };
                } else {
                    pos = null;
                }
            } else if (typeof pos === 'number') {
                pos = {
                    row: pos,
                    column: 0
                };
            }
        }
        if (pos) {
            me.selectByPosition(pos, suppressEvent);
        } else {
            me.deselect();
        }
    },
    /**
     * Returns the current position in the format {row: row, column: column}
     * @deprecated 5.0.1 This API uses column indices which include hidden columns in the count.
     * Use {@link #getPosition} instead.
     */
    getCurrentPosition: function() {
        // If it's during a select, return nextSelection since we buffer
        // the real selection until after the event fires
        var position = this.selecting ? this.nextSelection : this.selection;
        // This is the previous Format of the private CellContext class which was used here.
        // Do not return a CellContext so that if this object is passed into setCurrentPosition,
        // it will be read in the legacy (including hidden columns) way.
        return position ? {
            view: position.view,
            record: position.record,
            row: position.rowIdx,
            columnHeader: position.column,
            // IMPORTANT: The historic API for columns has been to include hidden columns
            // in the index. So we must report the index of the column
            // in the "all" ColumnManager.
            column: position.view.getColumnManager().indexOf(position.column)
        } : position;
    },
    /**
     * Returns the current position in the format {row: row, column: column}
     * @return {Ext.grid.CellContext} A CellContext object describing the current cell.
     */
    getPosition: function() {
        return (this.selecting ? this.nextSelection : this.selection) || null;
    },
    /**
     * Sets the current position.
     * @deprecated 5.0.1 This API uses column indices which include hidden columns in the
     * count. Use {@link #setPosition} instead.
     * @param {Ext.grid.CellContext/Object} pos The position to set. May be an object of
     * the form `{row:1, column:2}`
     * @param {Boolean} suppressEvent True to suppress selection events
     * @param {Boolean} preventCheck (private)
     */
    setCurrentPosition: function(pos, suppressEvent, preventCheck) {
        if (pos && !pos.isCellContext) {
            pos = new Ext.grid.CellContext(this.view).setPosition({
                row: pos.row,
                // IMPORTANT: The historic API for columns has been to include hidden columns
                // in the index. So we must index into the "all" ColumnManager.
                column: typeof pos.column === 'number' ? this.view.getColumnManager().getColumns()[pos.column] : pos.column
            });
        }
        return this.setPosition(pos, suppressEvent, preventCheck);
    },
    /**
     * Sets the current position.
     *
     * Note that if passing a column index, it is the index within the *visible* column set.
     *
     * @param {Ext.grid.CellContext/Object} pos The position to set. May be an object of the form
     * `{ row: 1, column: 2 }`
     * @param {Boolean} suppressEvent True to suppress selection events
     * @param {Boolean} preventCheck (private)
     */
    setPosition: function(pos, suppressEvent, preventCheck) {
        var me = this,
            last = me.selection;
        // Normalize it into an Ext.grid.CellContext if necessary
        if (pos) {
            pos = pos.isCellContext ? pos.clone() : new Ext.grid.CellContext(me.view).setPosition(pos);
        }
        if (!preventCheck && last) {
            // If the position is the same, jump out & don't fire the event
            if (pos && (pos.record === last.record && pos.column === last.column && pos.view === last.view)) {
                pos = null;
            } else {
                me.onCellDeselect(me.selection, suppressEvent);
            }
        }
        if (pos) {
            me.nextSelection = pos;
            // set this flag here so we know to use nextSelection
            // if the node is updated during a select
            me.selecting = true;
            me.onCellSelect(me.nextSelection, suppressEvent);
            me.selecting = false;
            // Deselect triggered by new selection will kill the selection property,
            // so restore it here.
            return (me.selection = pos);
        }
        // Enforce code correctness in unbuilt source.
        return null;
    },
    isCellSelected: function(view, row, column) {
        var me = this,
            testPos,
            pos = me.getPosition();
        if (pos && pos.view === view) {
            testPos = new Ext.grid.CellContext(view).setPosition({
                row: row,
                // IMPORTANT: The historic API for columns has been to include hidden columns
                // in the index. So we must index into the "all" ColumnManager.
                column: typeof column === 'number' ? view.getColumnManager().getColumns()[column] : column
            });
            return (testPos.record === pos.record) && (testPos.column === pos.column);
        }
    },
    // Keep selection model in consistent state upon record deletion.
    onStoreRemove: function(store, records, indices) {
        var me = this,
            pos = me.getPosition();
        me.callParent(arguments);
        if (pos && store.isMoving(pos.record)) {
            return;
        }
        if (pos && store.getCount() && store.indexOf(pos.record) !== -1) {
            pos.setRow(pos.record);
        } else {
            me.selection = null;
        }
    },
    onStoreClear: function() {
        this.callParent(arguments);
        this.selection = null;
    },
    onStoreAdd: function() {
        var me = this,
            pos = me.getPosition();
        me.callParent(arguments);
        if (pos) {
            pos.setRow(pos.record);
        } else {
            me.selection = null;
        }
    },
    /**
     * @private
     * Called when the store is refreshed.
     * Refresh the current position.
     * @param {Ext.util.Bag} selected A Collection representing the currently selected records.
     */
    updateSelectedInstances: function(selected) {
        var pos = this.getPosition(),
            selRec = selected.getAt(0);
        // Keep row/record pointer synchronized.
        // This handler is scheduled before the refresh op
        // So UI will appear in the correct state.
        if (selRec && pos && pos.record.id === selRec.id) {
            pos.setRow(selRec);
        }
        this.callParent([
            selected
        ]);
        // If the record has been removed, fix up the position.
        if (selected.length === 0 && selRec) {
            this.selection = null;
        }
    },
    /**
     * Set the current position based on where the user clicks.
     * @private
     * IMPORTANT* Due to V4.0.0 history, the cellIndex here is the index within ALL columns,
     * including hidden.
     */
    onCellClick: function(view, cell, cellIndex, record, row, recordIndex, e) {
        // Record index will be -1 if the clicked record is a metadata record and not selectable
        if (recordIndex !== -1) {
            this.setPosition(e.position);
        }
    },
    // notify the view that the cell has been selected to update the ui
    // appropriately and bring the cell into focus
    onCellSelect: function(position, supressEvent) {
        if (position && position.rowIdx !== undefined && position.rowIdx > -1) {
            this.doSelect(position.record, /* keepExisting */
            false, supressEvent);
        }
    },
    // notify view that the cell has been deselected to update the ui
    // appropriately
    onCellDeselect: function(position, supressEvent) {
        if (position && position.rowIdx !== undefined) {
            this.doDeselect(position.record, supressEvent);
        }
    },
    onSelectChange: function(record, isSelected, suppressEvent, commitFn) {
        var me = this,
            pos, eventName, view;
        if (isSelected) {
            pos = me.nextSelection;
            eventName = 'select';
        } else {
            pos = me.selection;
            eventName = 'deselect';
        }
        // CellModel may be shared between two sides of a Lockable.
        // The position must include a reference to the view in which the selection is current.
        // Ensure we use the view specified by the position.
        view = pos.view || me.primaryView;
        if ((suppressEvent || me.fireEvent('before' + eventName, me, record, pos.rowIdx, pos.colIdx)) !== false && commitFn() !== false) {
            if (isSelected) {
                view.onCellSelect(pos);
            } else {
                view.onCellDeselect(pos);
                delete me.selection;
            }
            if (!suppressEvent) {
                me.fireEvent(eventName, me, record, pos.rowIdx, pos.colIdx);
            }
        }
    },
    refresh: function() {
        var pos = this.getPosition(),
            selRowIdx;
        // Synchronize the current position's row with the row of the last selected record.
        if (pos && (selRowIdx = this.store.indexOf(this.selected.last())) !== -1) {
            pos.rowIdx = selRowIdx;
        }
    },
    /**
     * @private
     * When grid uses {@link Ext.panel.Table#optimizedColumnMove optimizedColumnMove}
     * (the default), this is added as a
     * {@link Ext.panel.Table#columnmove columnmove} handler to correctly maintain the
     * selected column using the same column header.
     * 
     * If optimizedColumnMove === false, (which some grid Features set) then the view is refreshed,
     * so this is not added as a handler because the selected column.
     */
    onColumnMove: function(headerCt, header, fromIdx, toIdx) {
        var grid = headerCt.up('tablepanel');
        if (grid) {
            this.onViewRefresh(grid.view);
        }
    },
    onUpdate: function(record) {
        var me = this,
            pos;
        if (me.isSelected(record)) {
            pos = me.selecting ? me.nextSelection : me.selection;
            me.view.onCellSelect(pos);
        }
    },
    onViewRefresh: function(view) {
        var me = this,
            pos = me.getPosition(),
            newPos,
            headerCt = view.headerCt,
            record, column;
        // Re-establish selection of the same cell coordinate.
        // DO NOT fire events because the selected 
        if (pos && pos.view === view) {
            record = pos.record;
            column = view.getColumnByPosition(pos);
            // After a refresh, recreate the selection using the same record
            // and grid column as before
            if (column && !column.isDescendantOf(headerCt)) {
                // column header is not a child of the header container
                // this happens when the grid is reconfigured with new columns
                // make a best effort to select something by matching on id,
                // then text, then dataIndex
                column = headerCt.queryById(column.id) || headerCt.down('[text="' + column.text + '"]') || headerCt.down('[dataIndex="' + column.dataIndex + '"]');
            }
            // If we have a columnHeader (either the column header that already exists in
            // the headerCt, or a suitable match that was found after reconfiguration)
            // AND the record still exists in the store (or a record matching the id of
            // the previously selected record) We are ok to go ahead and set the selection
            if (column && record) {
                if (view.store.indexOfId(record.getId()) !== -1) {
                    newPos = new Ext.grid.CellContext(view).setPosition({
                        row: record,
                        column: column
                    });
                    me.setPosition(newPos);
                }
            } else {
                me.selection = null;
            }
        }
    },
    /**
     * @private
     * Used internally by CellEditing
     */
    selectByPosition: function(position, suppressEvent) {
        this.setPosition(position, suppressEvent);
    }
});

/**
 * RowModel Selection Model implements row based navigation for
 * {@link Ext.grid.Panel grid panels} via user input. RowModel is the default grid selection
 * model and, generally, will not need to be specified.
 *
 * By utilizing the selModel config as an object, you may also set configurations for:
 *
 * + {@link #mode} - Specifies whether user may select multiple rows or single rows
 * + {@link #allowDeselect} - Specifies whether user may deselect records when in SINGLE mode
 * + {@link #ignoreRightMouseSelection} - Specifies whether user may ignore right clicks
 * for selection purposes
 *
 * In the example below, we've enabled MULTI mode. This means that multiple rows can be selected.
 *
 *     @example
 *     var store = Ext.create('Ext.data.Store', {
 *         fields: ['name', 'email', 'phone'],
 *         data: [
 *             { name: 'Lisa', email: 'lisa@simpsons.com', phone: '555-111-1224' },
 *             { name: 'Bart', email: 'bart@simpsons.com', phone: '555-222-1234' },
 *             { name: 'Homer', email: 'homer@simpsons.com', phone: '555-222-1244' },
 *             { name: 'Marge', email: 'marge@simpsons.com', phone: '555-222-1254' }
 *         ]
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Simpsons',
 *         store: store,
 *         width: 400,
 *         renderTo: Ext.getBody(),
 *         selModel: {
 *            selType: 'rowmodel', // rowmodel is the default selection model
 *            mode: 'MULTI' // Allows selection of multiple rows
 *         },
 *         columns: [
 *             { text: 'Name',  dataIndex: 'name'  },
 *             { text: 'Email', dataIndex: 'email', flex: 1 },
 *             { text: 'Phone', dataIndex: 'phone' }
 *         ]
 *     });
 */
Ext.define('Ext.selection.RowModel', {
    extend: 'Ext.selection.DataViewModel',
    alias: 'selection.rowmodel',
    requires: [
        'Ext.grid.CellContext'
    ],
    /**
     * @cfg {Boolean} enableKeyNav
     *
     * Turns on/off keyboard navigation within the grid.
     */
    enableKeyNav: true,
    /**
     * @event beforedeselect
     * Fired before a record is deselected. If any listener returns false, the
     * deselection is cancelled.
     * @param {Ext.selection.RowModel} this
     * @param {Ext.data.Model} record The deselected record
     * @param {Number} index The row index deselected
     */
    /**
     * @event beforeselect
     * Fired before a record is selected. If any listener returns false, the
     * selection is cancelled.
     * @param {Ext.selection.RowModel} this
     * @param {Ext.data.Model} record The selected record
     * @param {Number} index The row index selected
     */
    /**
     * @event deselect
     * Fired after a record is deselected
     * @param {Ext.selection.RowModel} this
     * @param {Ext.data.Model} record The deselected record
     * @param {Number} index The row index deselected
     */
    /**
     * @event select
     * Fired after a record is selected
     * @param {Ext.selection.RowModel} this
     * @param {Ext.data.Model} record The selected record
     * @param {Number} index The row index selected
     */
    isRowModel: true,
    /**
     * @cfg deselectOnContainerClick
     * @inheritdoc Ext.mixin.Selectable#cfg!deselectOnContainerClick
     */
    deselectOnContainerClick: false,
    onUpdate: function(record) {
        var me = this,
            view = me.view,
            index;
        if (view && me.isSelected(record)) {
            index = view.indexOf(record);
            view.onRowSelect(index);
            if (record === me.lastFocused) {
                view.onRowFocus(index, true);
            }
        }
    },
    // Allow the GridView to update the UI by
    // adding/removing a CSS class from the row.
    onSelectChange: function(record, isSelected, suppressEvent, commitFn) {
        var me = this,
            views = me.views || [
                me.view
            ],
            viewsLn = views.length,
            recordIndex = me.store.indexOf(record),
            eventName = isSelected ? 'select' : 'deselect',
            i, view;
        if ((suppressEvent || me.fireEvent('before' + eventName, me, record, recordIndex)) !== false && commitFn() !== false) {
            // Selection models can handle more than one view
            for (i = 0; i < viewsLn; i++) {
                view = views[i];
                recordIndex = view.indexOf(record);
                // The record might not be rendered due to either buffered rendering,
                // or removal/hiding of all columns (eg empty locked side).
                if (view.indexOf(record) !== -1) {
                    if (isSelected) {
                        view.onRowSelect(recordIndex, suppressEvent);
                    } else {
                        view.onRowDeselect(recordIndex, suppressEvent);
                    }
                }
            }
            if (!suppressEvent) {
                me.fireEvent(eventName, me, record, recordIndex);
            }
        }
    },
    /**
     * Returns position of the first selected cell in the selection in the format
     * {row: row, column: column}
     * @deprecated 5.0.1 Use the {@link Ext.view.Table#getNavigationModel NavigationModel} instead.
     */
    getCurrentPosition: function() {
        var firstSelection = this.selected.getAt(0);
        if (firstSelection) {
            return new Ext.grid.CellContext(this.view).setPosition(this.store.indexOf(firstSelection), 0);
        }
    },
    selectByPosition: function(position, keepExisting) {
        if (!position.isCellContext) {
            position = new Ext.grid.CellContext(this.view).setPosition(position.row, position.column);
        }
        this.select(position.record, keepExisting);
    },
    /**
     * Selects the record immediately following the currently selected record.
     * @param {Boolean} [keepExisting] True to retain existing selections
     * @param {Boolean} [suppressEvent] Set to false to not fire a select event
     * @return {Boolean} `true` if there is a next record, else `false`
     */
    selectNext: function(keepExisting, suppressEvent) {
        var me = this,
            store = me.store,
            selection = me.getSelection(),
            record = selection[selection.length - 1],
            index = me.view.indexOf(record) + 1,
            success;
        if (index === store.getCount() || index === 0) {
            success = false;
        } else {
            me.doSelect(index, keepExisting, suppressEvent);
            success = true;
        }
        return success;
    },
    /**
     * Selects the record that precedes the currently selected record.
     * @param {Boolean} [keepExisting] True to retain existing selections
     * @param {Boolean} [suppressEvent] Set to false to not fire a select event
     * @return {Boolean} `true` if there is a previous record, else `false`
     */
    selectPrevious: function(keepExisting, suppressEvent) {
        var me = this,
            selection = me.getSelection(),
            record = selection[0],
            index = me.view.indexOf(record) - 1,
            success;
        if (index < 0) {
            success = false;
        } else {
            me.doSelect(index, keepExisting, suppressEvent);
            success = true;
        }
        return success;
    },
    isRowSelected: function(record) {
        return this.isSelected(record);
    },
    isCellSelected: function(view, record, columnHeader) {
        return this.isSelected(record);
    },
    vetoSelection: function(e) {
        var navModel = this.view.getNavigationModel(),
            key = e.getKey(),
            isLeftRight = key === e.RIGHT || key === e.LEFT;
        // Veto row selection upon key-based, in-row left/right navigation.
        // Else pass to superclass to veto.
        return (isLeftRight && navModel.previousRecord === navModel.record) || this.callParent([
            e
        ]);
    }
});

/**
 * A selection model that renders a column of checkboxes that can be toggled to
 * select or deselect rows. The default mode for this selection model is MULTI.
 *
 *       @example
 *       var store = Ext.create('Ext.data.Store', {
 *           fields: ['name', 'email', 'phone'],
 *           data: [{
 *               name: 'Lisa',
 *               email: 'lisa@simpsons.com',
 *               phone: '555-111-1224'
 *           }, {
 *               name: 'Bart',
 *               email: 'bart@simpsons.com',
 *               phone: '555-222-1234'
 *           }, {
 *               name: 'Homer',
 *               email: 'homer@simpsons.com',
 *               phone: '555-222-1244'
 *           }, {
 *               name: 'Marge',
 *               email: 'marge@simpsons.com',
 *               phone: '555-222-1254'
 *           }]
 *       });
 *
 *       Ext.create('Ext.grid.Panel', {
 *           title: 'Simpsons',
 *           store: store,
 *           columns: [{
 *               text: 'Name',
 *               dataIndex: 'name'
 *           }, {
 *               text: 'Email',
 *               dataIndex: 'email',
 *               flex: 1
 *           }, {
 *               text: 'Phone',
 *               dataIndex: 'phone'
 *           }],
 *           height: 200,
 *           width: 400,
 *           renderTo: Ext.getBody(),
 *           selModel: {
 *               selType: 'checkboxmodel'
 *           }
 *       });
 *
 * The selection model will inject a header for the checkboxes in the first view
 * and according to the {@link #injectCheckbox} configuration.
 */
Ext.define('Ext.selection.CheckboxModel', {
    alias: 'selection.checkboxmodel',
    extend: 'Ext.selection.RowModel',
    requires: [
        'Ext.grid.column.Check'
    ],
    /**
     * @cfg {"SINGLE"/"SIMPLE"/"MULTI"} mode
     * Modes of selection.
     * Valid values are `"SINGLE"`, `"SIMPLE"`, and `"MULTI"`.
     */
    mode: 'MULTI',
    /**
     * @cfg {Number/String} [injectCheckbox=0]
     * The index at which to insert the checkbox column.
     * Supported values are a numeric index, and the strings 'first' and 'last'.
     */
    injectCheckbox: 0,
    /**
     * @cfg {Boolean} checkOnly
     * True if rows can only be selected by clicking on the checkbox column, not by clicking
     * on the row itself. Note that this only refers to selection via the UI, programmatic
     * selection will still occur regardless.
     */
    checkOnly: false,
    /**
     * @cfg {Function/String} renderer
     * @inheritDoc Ext.grid.column.Column#cfg-renderer
     */
    /**
     * @cfg {Function/String} editRenderer
     * @inheritDoc Ext.grid.column.Column#cfg-editRenderer
     */
    /* @cfg {Boolean} [locked=false]
     * If set to true, the checkbox column will be locked.
     * Note: For this config to work, it is necessary to configure the grid with
     * `enableLocking: true`, if no other columns are initially locked.
     */
    locked: false,
    /**
     * @cfg {Boolean} [showHeaderCheckbox=false]
     * Configure as `false` to not display the header checkbox at the top of the column.
     * When the store is a {@link Ext.data.BufferedStore BufferedStore}, this configuration will
     * not be available because the buffered data set does not always contain all data.
     */
    showHeaderCheckbox: undefined,
    /**
     * @cfg {String} headerText
     * Displays the configured text in the check column's header.
     *
     * if {@link #cfg-showHeaderCheckbox} is `true`, the text is shown *above* the checkbox.
     * @since 6.0.1
     */
    headerText: undefined,
    /**
     * @cfg {String} headerAriaLabel
     * ARIA label for screen readers to announce for the check column's header when it is focused.
     * Note that this label will not be visible on screen.
     *
     * @since 6.2.0
     * @locale
     */
    headerAriaLabel: 'Row selector',
    /**
     * @cfg {String} headerSelectText
     * ARIA description text to announce for the check column's header when it is focused,
     * {@link #showHeaderCheckbox} is shown, and not all rows are selected.
     *
     * @since 6.2.0
     * @locale
     */
    headerSelectText: 'Press Space to select all rows',
    /**
     * @cfg {String} headerDeselectText
     * ARIA description text to announce for the check column's header when it is focused,
     * {@link #showHeaderCheckbox} is shown, and all rows are selected.
     * @locale
     */
    headerDeselectText: 'Press Space to deselect all rows',
    /**
     * @cfg {String} rowSelectText
     * ARIA description text to announce when check column cell is focused and the row
     * is not selected.
     * @locale
     */
    rowSelectText: 'Press Space to select this row',
    /**
     * @cfg {String} rowDeselectText
     * ARIA description text to announce when check column cell is focused and the row
     * is selected.
     * @locale
     */
    rowDeselectText: 'Press Space to deselect this row',
    allowDeselect: true,
    headerWidth: 24,
    /**
     * @private
     */
    checkerOnCls: Ext.baseCSSPrefix + 'grid-hd-checker-on',
    tdCls: Ext.baseCSSPrefix + 'grid-cell-special ' + Ext.baseCSSPrefix + 'selmodel-column',
    constructor: function() {
        var me = this;
        me.callParent(arguments);
        // If mode is single and showHeaderCheck isn't explicity set to
        // true, hide it.
        if (me.mode === 'SINGLE') {
            if (me.showHeaderCheckbox) {
                Ext.Error.raise('The header checkbox is not supported for SINGLE mode ' + 'selection models.');
            }
            me.showHeaderCheckbox = false;
        }
    },
    beforeViewRender: function(view) {
        var me = this,
            ownerLockable = view.grid.ownerLockable,
            isLocked = me.locked || me.config && me.config.locked;
        me.callParent(arguments);
        // Preserve behaviour of false, but not clear why that would ever be done.
        if (me.injectCheckbox !== false) {
            // The check column gravitates to the locked side unless
            // the locked side is emptied, in which case it migrates to the normal side.
            if (ownerLockable && !me.lockListeners) {
                me.lockListeners = ownerLockable.mon(ownerLockable, {
                    lockcolumn: me.onColumnLock,
                    unlockcolumn: me.onColumnUnlock,
                    scope: me,
                    destroyable: true
                });
            }
            // If the controlling grid is NOT lockable, there's only one chance to add the column,
            // so add it.
            // If the view is the locked one and there are locked headers, add the column.
            // If the view is the normal one and we have not already added the column, add it.
            if (!ownerLockable || (view.isLockedView && (me.hasLockedHeader() || isLocked)) || (view.isNormalView && !me.column)) {
                me.addCheckbox(view);
                // Listen for reconfigure of outermost grid panel.
                me.mon(view.ownerGrid, {
                    beforereconfigure: me.onBeforeReconfigure,
                    reconfigure: me.onReconfigure,
                    scope: me
                });
            }
        }
    },
    onColumnUnlock: function(lockable, column) {
        var me = this,
            checkbox = me.injectCheckbox,
            lockedColumns = lockable.lockedGrid.visibleColumnManager.getColumns();
        // User has unlocked all columns and left only the expander column in the locked side.
        if (lockedColumns.length === 1 && lockedColumns[0] === me.column) {
            if (checkbox === 'first') {
                checkbox = 0;
            } else if (checkbox === 'last') {
                checkbox = lockable.normalGrid.visibleColumnManager.getColumns().length;
            }
            lockable.unlock(me.column, checkbox);
        }
    },
    onColumnLock: function(lockable, column) {
        var me = this,
            checkbox = me.injectCheckbox,
            lockedColumns = lockable.lockedGrid.visibleColumnManager.getColumns();
        // User has begun filling the empty locked side - migrate to the locked side..
        if (lockedColumns.length === 1) {
            if (checkbox === 'first') {
                checkbox = 0;
            } else if (checkbox === 'last') {
                checkbox = lockable.lockedGrid.visibleColumnManager.getColumns().length;
            }
            lockable.lock(me.column, checkbox);
        }
    },
    bindComponent: function(view) {
        this.sortable = false;
        this.callParent(arguments);
    },
    hasLockedHeader: function() {
        var columns = this.view.ownerGrid.getVisibleColumnManager().getColumns(),
            len = columns.length,
            i;
        for (i = 0; i < len; i++) {
            if (columns[i].locked) {
                return true;
            }
        }
        return false;
    },
    /**
     * Add the header checkbox to the header row
     * @private
     */
    addCheckbox: function(view) {
        var me = this,
            checkboxIndex = me.injectCheckbox,
            headerCt = view.headerCt;
        // Preserve behaviour of false, but not clear why that would ever be done.
        if (checkboxIndex !== false) {
            if (checkboxIndex === 'first') {
                checkboxIndex = 0;
            } else if (checkboxIndex === 'last') {
                checkboxIndex = headerCt.getColumnCount();
            }
            Ext.suspendLayouts();
            // Cannot select all in a buffered store.
            // We do not have all the records
            if (view.getStore().isBufferedStore) {
                me.showHeaderCheckbox = false;
            }
            me.column = headerCt.add(checkboxIndex, me.column || me.getHeaderConfig());
            Ext.resumeLayouts();
        }
    },
    /**
     * Handles the grid's beforereconfigure event. Removes the checkbox header
     * if the columns are being reconfigured.
     * @private
     */
    onBeforeReconfigure: function(grid, store, columns, oldStore, oldColumns) {
        var column = this.column,
            headerCt = column.ownerCt;
        // Save out check column from destruction.
        // addCheckbox will reuse it instead of creation a new one.
        if (columns && headerCt) {
            headerCt.remove(column, false);
        }
    },
    /**
     * Handles the grid's reconfigure event. Adds the checkbox header if the columns
     * have been reconfigured.
     * @private
     * @param {Ext.panel.Table} grid
     * @param {Ext.data.Store} store
     * @param {Object[]} columns
     */
    onReconfigure: function(grid, store, columns) {
        var me = this;
        if (columns) {
            // If it's a lockable assembly, add the column to the correct side
            if (grid.lockable) {
                if (grid.lockedGrid.isVisible()) {
                    grid.lock(me.column, 0);
                } else {
                    grid.unlock(me.column, 0);
                }
            } else {
                me.addCheckbox(me.view);
            }
            grid.view.refreshView();
        }
    },
    /**
     * Toggle between selecting all and deselecting all when clicking on
     * a checkbox header.
     * @private
     */
    onHeaderClick: function(headerCt, header, e) {
        var me = this,
            store = me.store,
            isChecked, records, i, len, selections, selection;
        if (me.showHeaderCheckbox !== false && header === me.column && me.mode !== 'SINGLE') {
            e.stopEvent();
            isChecked = header.el.hasCls(Ext.baseCSSPrefix + 'grid-hd-checker-on');
            // selectAll will only select the contents of the store, whereas deselectAll
            // will remove all the current selections. In this case we only want to
            // deselect whatever is available in the view.
            if (isChecked) {
                records = [];
                selections = this.getSelection();
                for (i = 0 , len = selections.length; i < len; ++i) {
                    selection = selections[i];
                    if (store.indexOf(selection) > -1) {
                        records.push(selection);
                    }
                }
                if (records.length > 0) {
                    me.deselect(records);
                }
            } else {
                me.selectAll();
            }
        }
    },
    /**
     * Retrieve a configuration to be used in a HeaderContainer.
     * This is called when injectCheckbox is not `false`.
     */
    getHeaderConfig: function() {
        var me = this,
            showCheck = me.showHeaderCheckbox !== false,
            htmlEncode = Ext.String.htmlEncode,
            config;
        config = {
            xtype: 'checkcolumn',
            headerCheckbox: showCheck,
            // historically used as a dicriminator property before isCheckColumn
            isCheckerHd: showCheck,
            ignoreExport: true,
            text: me.headerText,
            width: me.headerWidth,
            sortable: false,
            draggable: false,
            resizable: false,
            hideable: false,
            menuDisabled: true,
            checkOnly: me.checkOnly,
            checkboxAriaRole: 'presentation',
            // Firefox needs pointer-events: none on the checkbox span to work around
            // focusing issues
            tdCls: Ext.baseCSSPrefix + 'selmodel-checkbox ' + me.tdCls,
            cls: Ext.baseCSSPrefix + 'selmodel-column',
            editRenderer: me.editRenderer || me.renderEmpty,
            locked: me.hasLockedHeader(),
            processEvent: Ext.emptyFn,
            // if a custom renderer is provided in selModel, use that else use default renderer
            renderer: me.renderer || me.defaultRenderer,
            // It must not attempt to set anything in the records on toggle.
            // We handle that in onHeaderClick.
            toggleAll: Ext.emptyFn,
            // The selection model listens to the navigation model to select/deselect
            setRecordCheck: Ext.emptyFn,
            // It uses our isRowSelected to test whether a row is checked
            isRecordChecked: me.isRowSelected.bind(me)
        };
        if (!me.checkOnly) {
            // tabIndex and focusable properties should not be removed as
            // they must depend on actual column configuration
            config.ariaRole = 'presentation';
        } else {
            config.useAriaElements = true;
            config.ariaLabel = htmlEncode(me.headerAriaLabel);
            config.headerSelectText = htmlEncode(me.headerSelectText);
            config.headerDeselectText = htmlEncode(me.headerDeselectText);
            config.rowSelectText = htmlEncode(me.rowSelectText);
            config.rowDeselectText = htmlEncode(me.rowDeselectText);
        }
        return config;
    },
    toggleRecord: function(record, recordIndex, checked, cell) {
        this[checked ? 'select' : 'deselect']([
            record
        ], this.mode !== 'SINGLE');
    },
    renderEmpty: function() {
        return '&#160;';
    },
    // After refresh, ensure that the header checkbox state matches
    refresh: function() {
        this.callParent(arguments);
        this.updateHeaderState();
    },
    selectByPosition: function(position, keepExisting) {
        if (!position.isCellContext) {
            position = new Ext.grid.CellContext(this.view).setPosition(position.row, position.column);
        }
        // Do not select if checkOnly, and the requested position is not the check column
        if (!this.checkOnly || position.column === this.column) {
            this.callParent([
                position,
                keepExisting
            ]);
        }
    },
    /**
     * Synchronize header checker value as selection changes.
     * @private
     */
    onSelectChange: function(record, isSelected) {
        var me = this;
        me.callParent(arguments);
        if (me.column) {
            me.column.updateCellAriaDescription(record, isSelected);
        }
        if (!me.suspendChange) {
            me.updateHeaderState();
        }
    },
    /**
     * @private
     */
    onStoreLoad: function() {
        this.callParent(arguments);
        this.updateHeaderState();
    },
    onStoreAdd: function() {
        this.callParent(arguments);
        this.updateHeaderState();
    },
    onStoreRemove: function() {
        this.callParent(arguments);
        this.updateHeaderState();
    },
    onStoreRefresh: function() {
        this.callParent(arguments);
        this.updateHeaderState();
    },
    maybeFireSelectionChange: function(fireEvent) {
        if (fireEvent && !this.suspendChange) {
            this.updateHeaderState();
        }
        this.callParent(arguments);
    },
    resumeChanges: function() {
        this.callParent();
        if (!this.suspendChange) {
            this.updateHeaderState();
        }
    },
    /**
     * @private
     */
    updateHeaderState: function() {
        // check to see if all records are selected
        var me = this,
            store = me.store,
            storeCount = store.getCount(),
            views = me.views,
            hdSelectStatus = false,
            selectedCount = 0,
            selected, len, i;
        if (!store.isBufferedStore && storeCount > 0) {
            selected = me.selected;
            hdSelectStatus = true;
            for (i = 0 , len = selected.getCount(); i < len; ++i) {
                if (store.indexOfId(selected.getAt(i).id) > -1) {
                    ++selectedCount;
                }
            }
            hdSelectStatus = storeCount === selectedCount;
        }
        if (views && views.length) {
            me.column.setHeaderStatus(hdSelectStatus);
        }
    },
    vetoSelection: function(e) {
        var me = this,
            column = me.column,
            veto, isClick, isSpace;
        if (me.checkOnly) {
            isClick = e.type === column.triggerEvent && e.getTarget(me.column.getCellSelector());
            isSpace = e.getKey() === e.SPACE && e.position.column === column;
            veto = !(isClick || isSpace);
        }
        return veto || me.callParent([
            e
        ]);
    },
    privates: {
        onBeforeNavigate: function(metaEvent) {
            var e = metaEvent.keyEvent;
            if (this.selectionMode !== 'SINGLE') {
                metaEvent.ctrlKey = metaEvent.ctrlKey || e.ctrlKey || (e.type === this.column.triggerEvent && !e.shiftKey) || e.getKey() === e.SPACE;
            }
        },
        selectWithEventMulti: function(record, e, isSelected) {
            var me = this;
            if (!e.shiftKey && !e.ctrlKey && e.getTarget(me.column.getCellSelector())) {
                if (isSelected) {
                    me.doDeselect(record);
                } else {
                    me.doSelect(record, true);
                }
            } else {
                me.callParent([
                    record,
                    e,
                    isSelected
                ]);
            }
        }
    }
}, function(CheckboxModel) {
    CheckboxModel.prototype.checkSelector = '.' + Ext.grid.column.Check.prototype.checkboxCls;
});

/**
 * This selection model is created by default for {@link Ext.tree.Panel}.
 *
 * It implements a row selection model.
 */
Ext.define('Ext.selection.TreeModel', {
    extend: 'Ext.selection.RowModel',
    alias: 'selection.treemodel',
    /**
     * @cfg {Boolean} pruneRemoved
     * @hide
     */
    /**
     * @cfg {Boolean} selectOnExpanderClick
     * `true` to select the row when clicking on the icon to collapse or expand
     * a tree node.
     *
     * @since 5.1.0
     */
    selectOnExpanderClick: false,
    constructor: function(config) {
        var me = this;
        me.callParent([
            config
        ]);
        // If pruneRemoved is required, we must listen to the the Store's bubbled noderemove
        // event to know when nodes are added and removed from parentNodes.
        // The Store's remove event will be fired during collapses.
        if (me.pruneRemoved) {
            me.pruneRemoved = false;
            me.pruneRemovedNodes = true;
        }
    },
    getStoreListeners: function() {
        var me = this,
            result = me.callParent();
        result.noderemove = me.onNodeRemove;
        return result;
    },
    onNodeRemove: function(parent, node, isMove) {
        // deselection of deleted records done in base Model class
        if (!isMove) {
            // eslint-disable-next-line vars-on-top
            var toDeselect = [];
            this.gatherSelected(node, toDeselect);
            if (toDeselect.length) {
                this.deselect(toDeselect);
            }
        }
    },
    // onStoreRefresh asks if it should remove from the selection any selected records which are no
    // longer findable in the store after the refresh.
    // TreeModel does not use the pruneRemoved flag because records are being added and removed
    // from TreeStores on exand and collapse. It uses the pruneRemovedNodes flag.
    pruneRemovedOnRefresh: function() {
        return this.pruneRemovedNodes;
    },
    vetoSelection: function(e) {
        var view = this.view,
            select = this.selectOnExpanderClick,
            veto;
        veto = !select && e.type === 'click' && e.getTarget(view.expanderSelector || (view.lockingPartner && view.lockingPartner.expanderSelector));
        return veto || this.callParent([
            e
        ]);
    },
    privates: {
        gatherSelected: function(node, toDeselect) {
            var childNodes = node.childNodes,
                i, len, child;
            if (this.selected.containsKey(node.id)) {
                toDeselect.push(node);
            }
            if (childNodes) {
                for (i = 0 , len = childNodes.length; i < len; ++i) {
                    child = childNodes[i];
                    this.gatherSelected(child, toDeselect);
                }
            }
        }
    }
});

/**
 * @class Ext.slider.Thumb
 * @private
 * Represents a single thumb element on a Slider. This would not usually be created manually
 * and would instead be created internally by an {@link Ext.slider.Multi Multi slider}.
 */
Ext.define('Ext.slider.Thumb', {
    requires: [
        'Ext.dd.DragTracker',
        'Ext.util.Format'
    ],
    overCls: Ext.baseCSSPrefix + 'slider-thumb-over',
    /**
     * @cfg {Ext.slider.MultiSlider} slider (required)
     * The Slider to render to.
     */
    /**
     * Creates new slider thumb.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        var me = this;
        /**
         * @property {Ext.slider.MultiSlider} slider
         * The slider this thumb is contained within
         */
        Ext.apply(me, config || {}, {
            cls: Ext.baseCSSPrefix + 'slider-thumb',
            /**
             * @cfg {Boolean} constrain True to constrain the thumb so that it cannot overlap
             * its siblings
             */
            constrain: false
        });
        if (me.id == null) {
            Ext.id(me, 'ext-slider-thumb-');
        }
        me.callParent([
            config
        ]);
    },
    /**
     * Renders the thumb into a slider
     */
    render: function() {
        var me = this;
        me.el = me.slider.innerEl.insertFirst(me.getElConfig());
        me.onRender();
    },
    onRender: function() {
        var me = this,
            panDisable = me.slider.vertical ? 'panY' : 'panX',
            touchAction = {};
        touchAction[panDisable] = false;
        me.el.setTouchAction(touchAction);
        if (me.disabled) {
            me.disable();
        }
        me.initEvents();
    },
    getElConfig: function() {
        var me = this,
            slider = me.slider,
            style = {};
        style[slider.vertical ? 'bottom' : slider.horizontalProp] = slider.calculateThumbPosition(slider.normalizeValue(me.value)) + '%';
        return {
            style: style,
            id: me.id,
            cls: me.cls,
            role: 'presentation'
        };
    },
    /**
     * @private
     * move the thumb
     */
    move: function(v, animate) {
        var me = this,
            el = me.el,
            slider = me.slider,
            styleProp = slider.vertical ? 'bottom' : slider.horizontalProp,
            to, from, animCfg;
        v += '%';
        if (!animate) {
            el.dom.style[styleProp] = v;
        } else {
            to = {};
            to[styleProp] = v;
            if (!Ext.supports.GetPositionPercentage) {
                from = {};
                from[styleProp] = el.dom.style[styleProp];
            }
            // Animation config
            animCfg = {
                target: el,
                duration: 350,
                from: from,
                to: to,
                scope: me,
                callback: me.onAnimComplete
            };
            if (animate !== true) {
                Ext.apply(animCfg, animate);
            }
            me.anim = new Ext.fx.Anim(animCfg);
        }
    },
    onAnimComplete: function() {
        this.anim = null;
    },
    /**
     * Enables the thumb if it is currently disabled
     */
    enable: function() {
        var el = this.el;
        this.disabled = false;
        if (el) {
            el.removeCls(this.slider.disabledCls);
        }
    },
    /**
     * Disables the thumb if it is currently enabled
     */
    disable: function() {
        var el = this.el;
        this.disabled = true;
        if (el) {
            el.addCls(this.slider.disabledCls);
        }
    },
    /**
     * Sets up an Ext.dd.DragTracker for this thumb
     */
    initEvents: function() {
        var me = this;
        me.tracker = new Ext.dd.DragTracker({
            el: me.el,
            onBeforeStart: me.onBeforeDragStart.bind(me),
            onStart: me.onDragStart.bind(me),
            onDrag: me.onDrag.bind(me),
            onEnd: me.onDragEnd.bind(me),
            tolerance: 3,
            autoStart: 300
        });
        me.el.hover(me.addOverCls, me.removeOverCls, me);
    },
    addOverCls: function() {
        var me = this;
        if (!me.disabled) {
            me.el.addCls(me.overCls);
        }
    },
    removeOverCls: function() {
        this.el.removeCls(this.overCls);
    },
    /**
     * @private
     * This is tied into the internal Ext.dd.DragTracker. If the slider is currently disabled,
     * this returns false to disable the DragTracker too.
     * @return {Boolean} False if the slider is currently disabled
     */
    onBeforeDragStart: function(e) {
        var me = this,
            el = me.el,
            trackerXY = me.tracker.getXY(),
            delta = me.pointerOffset = el.getXY();
        if (me.disabled) {
            return false;
        } else {
            // Work out the delta of the pointer from the dead centre of the thumb.
            // Slider.getTrackPoint positions the centre of the slider at the reported
            // pointer position, so we have to correct for that in getValueFromTracker.
            delta[0] += Math.floor(el.getWidth() / 2) - trackerXY[0];
            delta[1] += Math.floor(el.getHeight() / 2) - trackerXY[1];
            me.slider.promoteThumb(me);
            return true;
        }
    },
    /**
     * @private
     * This is tied into the internal Ext.dd.DragTracker's onStart template method.
     * Adds the drag CSS class to the thumb and fires the 'dragstart' event
     */
    onDragStart: function(e) {
        var me = this,
            slider = me.slider;
        slider.onDragStart(me, e);
        me.el.addCls(Ext.baseCSSPrefix + 'slider-thumb-drag');
        me.dragging = me.slider.dragging = true;
        me.dragStartValue = me.value;
        slider.fireEvent('dragstart', slider, e, me);
    },
    /**
     * @private
     * This is tied into the internal Ext.dd.DragTracker's onDrag template method.
     * This is called every time the DragTracker detects a drag movement.
     * It updates the Slider's value using the position of the drag
     */
    onDrag: function(e) {
        var me = this,
            slider = me.slider,
            index = me.index,
            newValue = me.getValueFromTracker(),
            above, below;
        // If dragged out of range, value will be undefined
        if (newValue !== undefined) {
            if (me.constrain) {
                above = slider.thumbs[index + 1];
                below = slider.thumbs[index - 1];
                if (below !== undefined && newValue <= below.value) {
                    newValue = below.value;
                }
                if (above !== undefined && newValue >= above.value) {
                    newValue = above.value;
                }
            }
            slider.setValue(index, newValue, false);
            slider.fireEvent('drag', slider, e, me);
        }
    },
    getValueFromTracker: function() {
        var slider = this.slider,
            trackerXY = this.tracker.getXY(),
            trackPoint;
        trackerXY[0] += this.pointerOffset[0];
        trackerXY[1] += this.pointerOffset[1];
        trackPoint = slider.getTrackpoint(trackerXY);
        // If dragged out of range, value will be undefined
        if (trackPoint != null) {
            return slider.reversePixelValue(trackPoint);
        }
    },
    /**
     * @private
     * This is tied to the internal Ext.dd.DragTracker's onEnd template method.
     * Removes the drag CSS class and fires the 'changecomplete' event with the new value
     */
    onDragEnd: function(e) {
        var me = this,
            slider = me.slider,
            value = me.value;
        slider.onDragEnd(me, e);
        me.el.removeCls(Ext.baseCSSPrefix + 'slider-thumb-drag');
        me.dragging = slider.dragging = false;
        slider.fireEvent('dragend', slider, e);
        if (me.dragStartValue !== value) {
            slider.fireEvent('changecomplete', slider, value, me);
        }
    },
    destroy: function() {
        var me = this,
            anim = this.anim;
        if (anim) {
            anim.end();
        }
        me.el = me.tracker = me.anim = Ext.destroy(me.el, me.tracker);
        me.callParent();
    }
});

/**
 * Simple plugin for using an Ext.tip.Tip with a slider to show the slider value.
 * In general this class is not created directly, instead pass the {@link Ext.slider.Multi#useTips}
 * and {@link Ext.slider.Multi#tipText} configuration options to the slider directly.
 *
 *     @example
 *     Ext.create('Ext.slider.Single', {
 *         width: 214,
 *         minValue: 0,
 *         maxValue: 100,
 *         useTips: true,
 *         renderTo: Ext.getBody()
 *     });
 *
 * Optionally provide your own tip text by passing tipText:
 *
 *     @example
 *     Ext.create('Ext.slider.Single', {
 *         width: 214,
 *         minValue: 0,
 *         maxValue: 100,
 *         useTips: true,
 *         tipText: function(thumb){
 *             return Ext.String.format('**{0}% complete**', thumb.value);
 *         },
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.slider.Tip', {
    extend: 'Ext.tip.Tip',
    alias: 'widget.slidertip',
    /**
     * @cfg {Array} [offsets=null]
     * Offsets for aligning the tip to the slider. See {@link Ext.util.Positionable#alignTo}.
     * Default values for offsets are provided by specifying the {@link #position} config.
     */
    offsets: null,
    /**
     * @cfg {String} [align=null]
     * Alignment configuration for the tip to the slider. See {@link Ext.util.Positionable#alignTo}.
     * Default values for alignment are provided by specifying the {@link #position} config.
     */
    align: null,
    /**
     * @cfg {String} [position=For horizontal sliders, "top", for vertical sliders, "left"] 
     * Sets the position for where the tip will be displayed related to the thumb. This sets
     * defaults for {@link #align} and {@link #offsets} configurations. If {@link #align} or 
     * {@link #offsets} configurations are specified, they will override the defaults defined
     * by position.
     */
    position: '',
    minWidth: 10,
    defaultVerticalPosition: 'left',
    defaultHorizontalPosition: 'top',
    isSliderTip: true,
    init: function(slider) {
        var me = this,
            align, offsets;
        if (!me.position) {
            me.position = slider.vertical ? me.defaultVerticalPosition : me.defaultHorizontalPosition;
        }
        switch (me.position) {
            case 'top':
                offsets = [
                    0,
                    -10
                ];
                align = 'b-t?';
                break;
            case 'bottom':
                offsets = [
                    0,
                    10
                ];
                align = 't-b?';
                break;
            case 'left':
                offsets = [
                    -10,
                    0
                ];
                align = 'r-l?';
                break;
            case 'right':
                offsets = [
                    10,
                    0
                ];
                align = 'l-r?';
                break;
        }
        if (!me.align) {
            me.align = align;
        }
        if (!me.offsets) {
            me.offsets = offsets;
        }
        slider.on({
            scope: me,
            dragstart: me.onSlide,
            drag: me.onSlide,
            dragend: me.hide,
            destroy: me.destroy
        });
    },
    /**
     * @private
     * Called whenever a dragstart or drag event is received on the associated Thumb.
     * Aligns the Tip with the Thumb's new position.
     * @param {Ext.slider.MultiSlider} slider The slider
     * @param {Ext.event.Event} e The Event object
     * @param {Ext.slider.Thumb} thumb The thumb that the Tip is attached to
     */
    onSlide: function(slider, e, thumb) {
        var me = this;
        me.update(me.getText(thumb));
        me.show();
        me.el.alignTo(thumb.el, me.align, me.offsets);
    },
    /**
     * Used to create the text that appears in the Tip's body. By default this just returns
     * the value of the Slider Thumb that the Tip is attached to. Override to customize.
     * @param {Ext.slider.Thumb} thumb The Thumb that the Tip is attached to
     * @return {String} The text to display in the tip
     * @protected
     * @template
     */
    getText: function(thumb) {
        return String(thumb.value);
    }
});

/**
 * Slider which supports vertical or horizontal orientation, keyboard adjustments,
 * configurable snapping, axis clicking and animation. Can be added as an item to any container.
 *
 * Sliders can be created with more than one thumb handle by passing an array of values
 * instead of a single one:
 *
 *     @example
 *     Ext.create('Ext.slider.Multi', {
 *         width: 200,
 *         values: [25, 50, 75],
 *         increment: 5,
 *         minValue: 0,
 *         maxValue: 100,
 *
 *         // this defaults to true, setting to false allows the thumbs to pass each other
 *         constrainThumbs: false,
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.slider.Multi', {
    extend: 'Ext.form.field.Base',
    alias: 'widget.multislider',
    alternateClassName: 'Ext.slider.MultiSlider',
    requires: [
        'Ext.slider.Thumb',
        'Ext.slider.Tip',
        'Ext.Number',
        'Ext.util.Format',
        'Ext.Template'
    ],
    /**
     * @cfg {Number} value
     * A value with which to initialize the slider. Setting this will only result in the creation
     * of a single slider thumb; if you want multiple thumbs then use the {@link #values}
     * config instead.
     *
     * Defaults to #minValue.
     */
    /**
     * @cfg {Number[]} values
     * Array of Number values with which to initalize the slider. A separate slider thumb
     * will be created for each value in this array. This will take precedence over the single
     * {@link #value} config.
     */
    /**
     * @cfg {Boolean} vertical
     * Orient the Slider vertically rather than horizontally.
     */
    vertical: false,
    /**
     * @cfg {Number} minValue
     * The minimum value for the Slider.
     */
    minValue: 0,
    /**
     * @cfg {Number} maxValue
     * The maximum value for the Slider.
     */
    maxValue: 100,
    /**
     * @cfg {Number/Boolean} decimalPrecision
     * The number of decimal places to which to round the Slider's value.
     *
     * To disable rounding, configure as **false**.
     */
    decimalPrecision: 0,
    /**
     * @cfg {Number} keyIncrement
     * How many units to change the Slider when adjusting with keyboard navigation. If the increment
     * config is larger, it will be used instead.
     */
    keyIncrement: 1,
    /**
     * @cfg {Number} pageSize
     * How many units to change the Slider when using PageUp and PageDown keys.
     */
    pageSize: 10,
    /**
     * @cfg {Number} increment
     * How many units to change the slider when adjusting by drag and drop. Use this option
     * to enable 'snapping'.
     */
    increment: 0,
    /**
     * @cfg {Boolean} [zeroBasedSnapping=false]
     * Set to `true` to calculate snap points based on {@link #increment}s from zero as opposed to
     * from this Slider's {@link #minValue}.
     *
     * By Default, valid snap points are calculated starting {@link #increment}s from the
     * {@link #minValue}
     */
    /**
     * @private
     * @property {Number[]} clickRange
     * Determines whether or not a click to the slider component is considered to be a user request
     * to change the value. Specified as an array of [top, bottom], the click event's
     * 'top' property is compared to these numbers and the click only considered a change request
     * if it falls within them. e.g. if the 'top' value of the click event is 4 or 16,
     * the click is not considered a change request as it falls outside of the [5, 15] range
     */
    clickRange: [
        5,
        15
    ],
    /**
     * @cfg {Boolean} clickToChange
     * Determines whether or not clicking on the Slider axis will change the slider.
     */
    clickToChange: true,
    /**
     * @cfg {Object/Boolean} animate
     * Turn on or off animation. May be an animation configuration object:
     *
     *     animate: {
     *         duration: 3000,
     *         easing: 'easeIn'
     *     }
     */
    animate: true,
    /**
     * @property {Boolean} dragging
     * True while the thumb is in a drag operation
     */
    dragging: false,
    /**
     * @cfg {Boolean} constrainThumbs
     * True to disallow thumbs from overlapping one another.
     */
    constrainThumbs: true,
    /**
     * @cfg {Object/Boolean} useTips
     * True to use an {@link Ext.slider.Tip} to display tips for the value. This option may also
     * provide a configuration object for an {@link Ext.slider.Tip}.
     */
    useTips: true,
    /**
     * @cfg {Function/String} tipText
     * A function used to display custom text for the slider tip or the name of the
     * method on the corresponding `{@link Ext.app.ViewController controller}`.
     *
     * Defaults to null, which will use the default on the plugin.
     *
     * @cfg {Ext.slider.Thumb} tipText.thumb The Thumb that the Tip is attached to
     * @cfg {String} tipText.return The text to display in the tip
     *
     * @controllable
     */
    tipText: null,
    /**
     * @property defaultBindProperty
     * @inheritdoc
     */
    defaultBindProperty: 'value',
    /**
     * @cfg publishes
     * @inheritdoc
     */
    publishes: [
        'value'
    ],
    /**
     * @cfg {Boolean} thumbPerValue
     * Configure as true to allow thumbs to be added and removed dynamically from the slider 
     * when calling {@link #method-setValue} with an an array of values differing from the 
     * current number of thumbs.  
     * 
     * You will need to set {@link #cfg-values} explicitly when configuring the slider if 
     * 1) thumbPerValue is false and 2) the slider has multiple thumbs (values).
     * @since 6.5.0
     */
    thumbPerValue: false,
    /**
     * @event beforechange
     * Fires before the slider value is changed. By returning false from an event handler,
     * you can cancel the event and prevent the slider from changing.
     * @param {Ext.slider.Multi} slider The slider
     * @param {Number/null} newValue The new value which the slider is being changed to,
     * null if the change is a removal of a thumb.
     * @param {Number/null} oldValue The old value which the slider was previously,
     * null if the change is an addition of a thumb.
     * @param {Ext.slider.Thumb/null} thumb The thumb that was will be changed, null if the change
     * is an addition of a thumb.
     * @param {String} type The type of change that is going to occur (add/update/remove)
     */
    /**
     * @event change
     * Fires when the slider value is changed.
     * @param {Ext.slider.Multi} slider The slider
     * @param {Number/null} newValue The new value which the slider has been changed to,
     * null if the change is the removal of a thumb.
     * @param {Ext.slider.Thumb/null} thumb The thumb that was changed, null if the change
     * is the removal of a thumb.
     * @param {String} type The type of change that occurred (add/update/remove)
     */
    /**
     * @event changecomplete
     * Fires when the slider value is changed by the user and any drag operations have completed.
     * @param {Ext.slider.Multi} slider The slider
     * @param {Number} newValue The new value which the slider has been changed to.
     * @param {Ext.slider.Thumb} thumb The thumb that was changed
     */
    /**
     * @event dragstart
     * Fires after a drag operation has started.
     * @param {Ext.slider.Multi} slider The slider
     * @param {Ext.event.Event} e The event fired from Ext.dd.DragTracker
     */
    /**
     * @event drag
     * Fires continuously during the drag operation while the mouse is moving.
     * @param {Ext.slider.Multi} slider The slider
     * @param {Ext.event.Event} e The event fired from Ext.dd.DragTracker
     */
    /**
     * @event dragend
     * Fires after the drag operation has completed.
     * @param {Ext.slider.Multi} slider The slider
     * @param {Ext.event.Event} e The event fired from Ext.dd.DragTracker
     */
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'slider',
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: true,
    needArrowKeys: true,
    /**
     * @cfg tabIndex
     * @inheritdoc
     */
    tabIndex: 0,
    skipLabelForAttribute: true,
    /**
     * @cfg focusCls
     * @inheritdoc
     */
    focusCls: 'slider-focus',
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        'endEl',
        'innerEl'
    ],
    /* eslint-disable indent, max-len */
    // note: {id} here is really {inputId}, but {cmpId} is available
    /**
     * @cfg fieldSubTpl
     * @inheritdoc
     */
    fieldSubTpl: [
        '<div id="{id}" data-ref="inputEl" {inputAttrTpl}',
        ' class="',
        Ext.baseCSSPrefix,
        'slider {fieldCls} {vertical}',
        '{childElCls}"',
        '<tpl if="tabIdx != null"> tabindex="{tabIdx}"</tpl>',
        '<tpl foreach="ariaElAttributes"> {$}="{.}"</tpl>',
        '<tpl foreach="inputElAriaAttributes"> {$}="{.}"</tpl>',
        '>',
        '<div id="{cmpId}-endEl" data-ref="endEl" class="' + Ext.baseCSSPrefix + 'slider-end" role="presentation">',
        '<div id="{cmpId}-innerEl" data-ref="innerEl" class="' + Ext.baseCSSPrefix + 'slider-inner" role="presentation">',
        '{%this.renderThumbs(out, values)%}',
        '</div>',
        '</div>',
        '</div>',
        {
            renderThumbs: function(out, values) {
                var me = values.$comp,
                    i = 0,
                    thumbs = me.thumbs,
                    len = thumbs.length,
                    thumb, thumbConfig;
                for (; i < len; i++) {
                    thumb = thumbs[i];
                    thumbConfig = thumb.getElConfig();
                    thumbConfig.id = me.id + '-thumb-' + i;
                    Ext.DomHelper.generateMarkup(thumbConfig, out);
                }
            },
            disableFormats: true
        }
    ],
    /* eslint-enable indent, max-len */
    horizontalProp: 'left',
    initValue: function() {
        var me = this,
            extValueFrom = Ext.valueFrom,
            thumbPerValue = me.thumbPerValue,
            values;
        // Fallback for initial values: values config -> value config -> minValue config -> 0
        values = extValueFrom(me.values, [
            extValueFrom(me.value, extValueFrom(me.minValue, 0))
        ]);
        // Store for use in dirty check
        me.originalValue = values;
        // ensure that thumbPerValue is set to add initial thumbs
        me.initializingValues = true;
        me.updateValues(values);
        me.initializingValues = false;
        // restore config
        me.thumbPerValue = thumbPerValue;
    },
    initComponent: function() {
        var me = this,
            tipText = me.tipText,
            tipPlug, hasTip, p, pLen, plugins;
        /**
         * @property {Array} thumbs
         * Array containing references to each thumb
         */
        me.thumbs = [];
        me.keyIncrement = Math.max(me.increment, me.keyIncrement);
        me.extraFieldBodyCls = Ext.baseCSSPrefix + 'slider-ct-' + (me.vertical ? 'vert' : 'horz');
        me.callParent();
        // only can use it if it exists.
        if (me.useTips) {
            tipPlug = {};
            if (Ext.isObject(me.useTips)) {
                Ext.apply(tipPlug, me.useTips);
            } else if (tipText) {
                tipPlug.getText = tipText;
            }
            if (typeof (tipText = tipPlug.getText) === 'string') {
                tipPlug.getText = function(thumb) {
                    return Ext.callback(tipText, null, [
                        thumb
                    ], 0, me, me);
                };
            }
            plugins = me.plugins = me.plugins || [];
            pLen = plugins.length;
            for (p = 0; p < pLen; p++) {
                if (plugins[p].isSliderTip) {
                    hasTip = true;
                    break;
                }
            }
            if (!hasTip) {
                me.plugins.push(new Ext.slider.Tip(tipPlug));
            }
        }
    },
    /**
     * Creates a new thumb and adds it to the slider
     * @param {Number} [value=0] The initial value to set on the thumb.
     * @return {Ext.slider.Thumb} The thumb
     */
    addThumb: function(value) {
        var me = this,
            thumb = new Ext.slider.Thumb({
                ownerCt: me,
                value: value,
                slider: me,
                index: me.thumbs.length,
                constrain: me.constrainThumbs,
                disabled: !!me.readOnly
            });
        me.thumbs.push(thumb);
        // render the thumb now if needed
        if (me.rendered) {
            thumb.render();
        }
        return thumb;
    },
    /**
     * @private
     * Moves the given thumb above all other by increasing its z-index. This is called when as drag
     * any thumb, so that the thumb that was just dragged is always at the highest z-index. This is
     * required when the thumbs are stacked on top of each other at one of the ends of the slider's
     * range, which can result in the user not being able to move any of them.
     * @param {Ext.slider.Thumb} topThumb The thumb to move to the top
     */
    promoteThumb: function(topThumb) {
        var thumbs = this.thumbStack || (this.thumbStack = Ext.Array.slice(this.thumbs)),
            ln = thumbs.length,
            zIndex = 10000,
            i;
        // Move topthumb to position zero
        if (thumbs[0] !== topThumb) {
            Ext.Array.remove(thumbs, topThumb);
            thumbs.unshift(topThumb);
        }
        // Then shuffle the zIndices
        for (i = 0; i < ln; i++) {
            thumbs[i].el.setStyle('zIndex', zIndex);
            zIndex -= 1000;
        }
    },
    getSubTplData: function(fieldData) {
        var me = this,
            data, ariaAttr;
        data = Ext.apply(me.callParent([
            fieldData
        ]), {
            $comp: me,
            vertical: me.vertical ? Ext.baseCSSPrefix + 'slider-vert' : Ext.baseCSSPrefix + 'slider-horz',
            minValue: me.minValue,
            maxValue: me.maxValue,
            value: me.value,
            tabIdx: me.tabIndex,
            childElCls: ''
        });
        ariaAttr = data.inputElAriaAttributes;
        if (ariaAttr) {
            if (!ariaAttr['aria-labelledby']) {
                ariaAttr['aria-labelledby'] = me.id + '-labelEl';
            }
            ariaAttr['aria-orientation'] = me.vertical ? 'vertical' : 'horizontal';
            ariaAttr['aria-valuemin'] = me.minValue;
            ariaAttr['aria-valuemax'] = me.maxValue;
            ariaAttr['aria-valuenow'] = me.value;
        }
        return data;
    },
    onRender: function() {
        var me = this,
            thumbs = me.thumbs,
            len = thumbs.length,
            i = 0,
            thumb;
        me.callParent(arguments);
        for (i = 0; i < len; i++) {
            thumb = thumbs[i];
            thumb.el = me.el.getById(me.id + '-thumb-' + i);
            thumb.onRender();
        }
    },
    /**
     * @private
     * Adds keyboard and mouse listeners on this.el. Ignores click events
     * on the internal focus element.
     */
    initEvents: function() {
        var me = this;
        me.callParent();
        me.mon(me.el, {
            scope: me,
            mousedown: me.onMouseDown,
            keydown: me.onKeyDown
        });
    },
    onDragStart: Ext.emptyFn,
    onDragEnd: Ext.emptyFn,
    /**
     * @private
     * Given an `[x, y]` position within the slider's track (Points outside the slider's track
     * are coerced to either the minimum or maximum value), calculate how many pixels
     * **from the slider origin** (left for horizontal Sliders and bottom for vertical Sliders)
     * that point is.
     *
     * If the point is outside the range of the Slider's track, the return value is `undefined`
     * @param {Number[]} xy The point to calculate the track point for
     */
    getTrackpoint: function(xy) {
        var me = this,
            vertical = me.vertical,
            sliderTrack = me.innerEl,
            trackLength, result, positionProperty;
        if (vertical) {
            positionProperty = 'top';
            trackLength = sliderTrack.getHeight();
        } else {
            positionProperty = me.horizontalProp;
            trackLength = sliderTrack.getWidth();
        }
        xy = me.transformTrackPoints(sliderTrack.translatePoints(xy));
        result = Ext.Number.constrain(xy[positionProperty], 0, trackLength);
        return vertical ? trackLength - result : result;
    },
    transformTrackPoints: Ext.identityFn,
    // Base field checkChange method will fire 'change' event with signature common to all fields,
    // but Slider fires the same event with different signature. Hence we disable checkChange here
    // to avoid breakage.
    checkChange: Ext.emptyFn,
    /**
     * @private
     * Mousedown handler for the slider. If the clickToChange is enabled and the click was not
     * on the draggable 'thumb', this calculates the new value of the slider and tells
     * the implementation (Horizontal or Vertical) to move the thumb
     * @param {Ext.event.Event} e The click event
     */
    onMouseDown: function(e) {
        var me = this,
            thumbClicked = false,
            i = 0,
            thumbs = me.thumbs,
            len = thumbs.length,
            trackPoint;
        if (me.disabled) {
            return;
        }
        // see if the click was on any of the thumbs
        for (; !thumbClicked && i < len; i++) {
            thumbClicked = thumbClicked || e.target === thumbs[i].el.dom;
        }
        // Focus ourselves before setting the value. This allows other
        // fields that have blur handlers (for example, date/number field)
        // to take care of themselves first. This is important for
        // databinding.
        me.focus();
        if (me.clickToChange && !thumbClicked) {
            trackPoint = me.getTrackpoint(e.getXY());
            if (trackPoint !== undefined) {
                me.onClickChange(trackPoint);
            }
        }
    },
    /**
     * @private
     * Moves the thumb to the indicated position.
     * Only changes the value if the click was within this.clickRange.
     * @param {Number} trackPoint local pixel offset **from the origin** (left for horizontal
     * and bottom for vertical) along the Slider's axis at which the click event occured.
     */
    onClickChange: function(trackPoint) {
        var me = this,
            thumb, index;
        // How far along the track *from the origin* was the click.
        // If vertical, the origin is the bottom of the slider track.
        // find the nearest thumb to the click event
        thumb = me.getNearest(trackPoint);
        if (!thumb.disabled) {
            index = thumb.index;
            me.setValue(index, Ext.util.Format.round(me.reversePixelValue(trackPoint), me.decimalPrecision), undefined, true);
        }
    },
    /**
     * @private
     * Returns the nearest thumb to a click event, along with its distance
     * @param {Number} trackPoint local pixel position along the Slider's axis to find the Thumb for
     * @return {Object} The closest thumb object and its distance from the click event
     */
    getNearest: function(trackPoint) {
        var me = this,
            clickValue = me.reversePixelValue(trackPoint),
            nearestDistance = me.getRange() + 5,
            // add a small fudge for the end of the slider
            nearest = null,
            thumbs = me.thumbs,
            i = 0,
            len = thumbs.length,
            thumb, value, dist;
        for (; i < len; i++) {
            thumb = me.thumbs[i];
            value = thumb.value;
            dist = Math.abs(value - clickValue);
            if (Math.abs(dist) <= nearestDistance) {
                // this makes sure that thumbs will stay in order
                // eslint-disable-next-line eqeqeq
                if (nearest && nearest.value == value && value > clickValue && thumb.index > nearest.index) {
                    
                    continue;
                }
                nearest = thumb;
                nearestDistance = dist;
            }
        }
        return nearest;
    },
    /**
     * @private
     * Handler for any keypresses captured by the slider. If the key is UP or RIGHT,
     * the thumb is moved along to the right by this.keyIncrement. If DOWN or LEFT it is moved left.
     * Pressing CTRL moves the slider to the end in either direction
     * @param {Ext.event.Event} e The Event object
     */
    onKeyDown: function(e) {
        var me = this,
            ariaDom = me.ariaEl.dom,
            k, val;
        k = e.getKey();
        /*
         * The behaviour for keyboard handling with multiple thumbs is currently undefined.
         * There's no real sane default for it, so leave it like this until we come up
         * with a better way of doing it.
         */
        if (me.disabled || me.thumbs.length !== 1) {
            // Must not mingle with the Tab key!
            if (k !== e.TAB) {
                e.preventDefault();
            }
            return;
        }
        switch (k) {
            case e.UP:
            case e.RIGHT:
                val = e.ctrlKey ? me.maxValue : me.getValue(0) + me.keyIncrement;
                break;
            case e.DOWN:
            case e.LEFT:
                val = e.ctrlKey ? me.minValue : me.getValue(0) - me.keyIncrement;
                break;
            case e.HOME:
                val = me.minValue;
                break;
            case e.END:
                val = me.maxValue;
                break;
            case e.PAGE_UP:
                val = me.getValue(0) + me.pageSize;
                break;
            case e.PAGE_DOWN:
                val = me.getValue(0) - me.pageSize;
                break;
        }
        if (val !== undefined) {
            e.stopEvent();
            val = me.normalizeValue(val);
            me.setValue(0, val, undefined, true);
            if (ariaDom) {
                ariaDom.setAttribute('aria-valuenow', val);
            }
        }
    },
    /**
     * @private
     * Returns a snapped, constrained value when given a desired value
     * @param {Number} value Raw number value
     * @return {Number} The raw value rounded to the correct d.p. and constrained within
     * the set max and min values
     */
    normalizeValue: function(value) {
        var me = this,
            snapFn = me.zeroBasedSnapping ? 'snap' : 'snapInRange';
        value = Ext.Number[snapFn](value, me.increment, me.minValue, me.maxValue);
        value = Ext.util.Format.round(value, me.decimalPrecision);
        value = Ext.Number.constrain(value, me.minValue, me.maxValue);
        return value;
    },
    /**
     * Sets the minimum value for the slider instance. If the current value is less than
     * the minimum value, the current value will be changed.
     * @param {Number} val The new minimum value
     */
    setMinValue: function(val) {
        var me = this,
            thumbs = me.thumbs,
            len = thumbs.length,
            ariaDom = me.ariaEl.dom,
            thumb, i;
        me.minValue = val;
        for (i = 0; i < len; ++i) {
            thumb = thumbs[i];
            if (thumb.value < val) {
                me.setValue(i, val, false);
            }
        }
        if (ariaDom) {
            ariaDom.setAttribute('aria-valuemin', val);
        }
        me.syncThumbs();
    },
    /**
     * Sets the maximum value for the slider instance. If the current value is more than
     * the maximum value, the current value will be changed.
     * @param {Number} val The new maximum value
     */
    setMaxValue: function(val) {
        var me = this,
            thumbs = me.thumbs,
            len = thumbs.length,
            ariaDom = me.ariaEl.dom,
            thumb, i;
        me.maxValue = val;
        for (i = 0; i < len; ++i) {
            thumb = thumbs[i];
            if (thumb.value > val) {
                me.setValue(i, val, false);
            }
        }
        if (ariaDom) {
            ariaDom.setAttribute('aria-valuemax', val);
        }
        me.syncThumbs();
    },
    /**
     * Programmatically sets the value of the Slider. Ensures that the value is constrained within
     * the minValue and maxValue.
     *
     * Setting the second slider's value without animation:
     *
     *     mySlider.setValue(1, 50, false);
     *
     * Setting multiple values with animation:
     *
     *     mySlider.setValue([20, 40, 60], true);
     *
     * @param {Number/Number[]} index Index of the thumb to move. Alternatively, it can be an array
     * of values to set for each thumb in the slider.
     * @param {Number} value The value to set the slider to. (This will be constrained within
     * minValue and maxValue)
     * @param {Object/Boolean} [animate] `false` to not animate. `true` to use the default
     * animation. This may also be an animate configuration object, see {@link #cfg-animate}.
     * If this configuration is omitted, the {@link #cfg-animate} configuration will be used.
     * @param {Boolean} changeComplete
     * @return {Ext.slider.Multi} this
     */
    setValue: function(index, value, animate, changeComplete) {
        var me = this,
            ariaDom = me.ariaEl.dom,
            thumb;
        if (Ext.isArray(index)) {
            me.updateValues(index, value);
            return me;
        }
        thumb = me.thumbs[index];
        // ensures value is contstrained and snapped
        value = me.normalizeValue(value);
        if (value !== thumb.value && me.fireEvent('beforechange', me, value, thumb.value, thumb, 'update') !== false) {
            thumb.value = value;
            if (me.rendered) {
                if (Ext.isDefined(animate)) {
                    animate = animate === false ? false : animate;
                } else {
                    animate = me.animate;
                }
                thumb.move(me.calculateThumbPosition(value), animate);
                // At this moment we can only handle one thumb wrt ARIA
                if (index === 0 && ariaDom) {
                    ariaDom.setAttribute('aria-valuenow', value);
                }
                me.fireEvent('change', me, value, thumb, 'update');
                me.checkDirty();
                if (changeComplete) {
                    me.fireEvent('changecomplete', me, value, thumb);
                }
            }
        }
        return me;
    },
    /**
     * @private
     * Given a value within this Slider's range, calculates a Thumb's percentage CSS position
     * to map that value.
     */
    calculateThumbPosition: function(v) {
        var me = this,
            minValue = me.minValue,
            pos = (v - minValue) / me.getRange() * 100;
        if (isNaN(pos)) {
            pos = 0;
        }
        return pos;
    },
    /**
     * @private
     * Returns the ratio of pixels to mapped values. e.g. if the slider is 200px wide
     * and maxValue - minValue is 100, the ratio is 2
     * @return {Number} The ratio of pixels to mapped values
     */
    getRatio: function() {
        var me = this,
            innerEl = me.innerEl,
            trackLength = me.vertical ? innerEl.getHeight() : innerEl.getWidth(),
            valueRange = me.getRange();
        return valueRange === 0 ? trackLength : (trackLength / valueRange);
    },
    getRange: function() {
        return this.maxValue - this.minValue;
    },
    /**
     * @private
     * Given a pixel location along the slider, returns the mapped slider value for that pixel.
     * E.g. if we have a slider 200px wide with minValue = 100 and maxValue = 500,
     * reversePixelValue(50) returns 200
     * @param {Number} pos The position along the slider to return a mapped value for
     * @return {Number} The mapped value for the given position
     */
    reversePixelValue: function(pos) {
        return this.minValue + (pos / this.getRatio());
    },
    /**
     * @private
     * Given a Thumb's percentage position along the slider, returns the mapped slider value
     * for that pixel. E.g. if we have a slider 200px wide with minValue = 100 and maxValue = 500,
     * reversePercentageValue(25) returns 200
     * @param {Number} pos The percentage along the slider track to return a mapped value for
     * @return {Number} The mapped value for the given position
     */
    reversePercentageValue: function(pos) {
        return this.minValue + this.getRange() * (pos / 100);
    },
    onDisable: function() {
        var me = this,
            i = 0,
            thumbs = me.thumbs,
            len = thumbs.length,
            thumb, el, xy;
        me.callParent();
        for (; i < len; i++) {
            thumb = thumbs[i];
            el = thumb.el;
            thumb.disable();
            if (Ext.isIE) {
                // IE breaks when using overflow visible and opacity other than 1.
                // Create a place holder for the thumb and display it.
                xy = el.getXY();
                el.hide();
                me.innerEl.addCls(me.disabledCls).dom.disabled = true;
                if (!me.thumbHolder) {
                    me.thumbHolder = me.endEl.createChild({
                        role: 'presentation',
                        cls: Ext.baseCSSPrefix + 'slider-thumb ' + me.disabledCls
                    });
                }
                me.thumbHolder.show().setXY(xy);
            }
        }
    },
    onEnable: function() {
        var me = this,
            i = 0,
            thumbs = me.thumbs,
            len = thumbs.length,
            thumb, el;
        this.callParent();
        for (; i < len; i++) {
            thumb = thumbs[i];
            el = thumb.el;
            thumb.enable();
            if (Ext.isIE) {
                me.innerEl.removeCls(me.disabledCls).dom.disabled = false;
                if (me.thumbHolder) {
                    me.thumbHolder.hide();
                }
                el.show();
                me.syncThumbs();
            }
        }
    },
    /**
     * Synchronizes thumbs position to the proper proportion of the total component width
     * based on the current slider {@link #value}. This will be called automatically
     * when the Slider is resized by a layout, but if it is rendered auto width,
     * this method can be called from another resize handler to sync the Slider if necessary.
     */
    syncThumbs: function() {
        if (this.rendered) {
            // eslint-disable-next-line vars-on-top
            var thumbs = this.thumbs,
                length = thumbs.length,
                i = 0;
            for (; i < length; i++) {
                thumbs[i].move(this.calculateThumbPosition(thumbs[i].value));
            }
        }
    },
    /**
     * Returns the current value of the slider
     * @param {Number} index The index of the thumb to return a value for
     * @return {Number/Number[]} The current value of the slider at the given index,
     * or an array of all thumb values if no index is given.
     */
    getValue: function(index) {
        return Ext.isNumber(index) ? this.thumbs[index].value : this.getValues();
    },
    /**
     * Returns an array of values - one for the location of each thumb
     * @return {Number[]} The set of thumb values
     */
    getValues: function() {
        var values = [],
            i = 0,
            thumbs = this.thumbs,
            len = thumbs.length;
        for (; i < len; i++) {
            values.push(thumbs[i].value);
        }
        return values;
    },
    getSubmitValue: function() {
        var me = this;
        return (me.disabled || !me.submitValue) ? null : me.getValue();
    },
    reset: function() {
        var me = this,
            arr = [].concat(me.originalValue);
        me.updateValues(arr);
        me.clearInvalid();
        // delete here so we reset back to the original state
        delete me.wasValid;
    },
    /**
     * Programmatically sets the values of the slider while ensuring that the value is constrained 
     * within the minValue and maxValue.
     * @private
     *
     * @param {Number[]} values Array of values that will be used to set the sliders
     * @param {Object/Boolean} [animate] `false` to not animate. `true` to use the default
     * animation. This may also be an animate configuration object, see {@link #cfg-animate}.
     * If this configuration is omitted, the {@link #cfg-animate} configuration will be used.
     * @param {Boolean} supressEvents
     * @return {Ext.slider.Multi} this
     */
    updateValues: function(values, animate, supressEvents) {
        var me = this,
            len = values.length,
            thumbs = me.thumbs,
            thumbLen = thumbs.length,
            newValues = [],
            skipEvents = me.initializingValues || supressEvents,
            i, thumb, value, addLen, removeLen;
        for (i = 0; i < len; i++) {
            thumb = thumbs[i];
            value = values[i];
            if (thumb) {
                me.setValue(i, value, animate);
            } else {
                newValues.push(value);
            }
        }
        if (me.thumbPerValue || me.initializingValues) {
            addLen = newValues.length;
            removeLen = thumbLen - len;
            for (i = 0; i < addLen; i++) {
                value = newValues[i];
                if (skipEvents || me.fireEvent('beforechange', me, value, null, null, 'add') !== false) {
                    thumb = me.addThumb(me.normalizeValue(value));
                    if (!skipEvents) {
                        me.fireEvent('change', me, value, thumb, 'add');
                    }
                    me.checkDirty();
                }
            }
            for (i = 0; i < removeLen; i++) {
                thumb = thumbs[thumbs.length - 1];
                // eslint-disable-next-line max-len
                if (skipEvents || me.fireEvent('beforechange', me, null, thumb.value, thumb, 'remove') !== false) {
                    me.removeThumb(thumb);
                    if (!skipEvents) {
                        me.fireEvent('change', me, null, null, 'remove');
                    }
                    me.checkDirty();
                }
            }
        }
        return me;
    },
    /**
     * Removes a thumb from the slider
     * @param {Number/Ext.slider.Thumb} thumb The index of the thumb within the slider to remove,
     * or the thumb itself
     */
    removeThumb: function(thumb) {
        var me = this,
            thumbs = me.thumbs,
            index;
        if (Ext.isNumber(thumb)) {
            index = thumb;
            thumb = thumbs[index];
        } else {
            index = me.findThumbIndex(thumb);
        }
        if (thumb && Ext.isNumber(index)) {
            // remove from tracking array
            thumbs.splice(index, 1);
            // reset the thumb stack
            me.thumbStack = Ext.Array.slice(me.thumbs);
            // now destroy the thumb
            Ext.destroy(thumb);
        }
    },
    /**
     * Returns the index of thumb within the thumbs array
     * @private
     * @param {Ext.slider.Thumb} thumb The instance of the thumb to find
     * @return {Number/null} Returns the index of the thumb, or null if thumb is not found
     */
    findThumbIndex: function(thumb) {
        var thumbs = this.thumbs,
            len = thumbs.length,
            index = null,
            i;
        for (i = 0; i < len; i++) {
            if (thumbs[i] === thumb) {
                index = i;
                break;
            }
        }
        return index;
    },
    setReadOnly: function(readOnly) {
        var me = this,
            thumbs = me.thumbs,
            len = thumbs.length,
            i = 0;
        me.callParent(arguments);
        readOnly = me.readOnly;
        for (; i < len; ++i) {
            if (readOnly) {
                thumbs[i].disable();
            } else {
                thumbs[i].enable();
            }
        }
    },
    doDestroy: function() {
        var me = this;
        if (me.rendered) {
            Ext.destroy(me.thumbs);
        }
        if (me.thumbHolder) {
            me.thumbHolder.destroy();
            me.thumbHolder = null;
        }
        me.callParent();
    }
});

/**
 * Slider which supports vertical or horizontal orientation, keyboard adjustments,
 * configurable snapping, axis clicking and animation. Can be added as an item to any container.
 *
 *     @example
 *     Ext.create('Ext.slider.Single', {
 *         width: 200,
 *         value: 50,
 *         increment: 10,
 *         minValue: 0,
 *         maxValue: 100,
 *         renderTo: Ext.getBody()
 *     });
 *
 * The class Ext.slider.Single is aliased to Ext.Slider for backwards compatibility.
 */
Ext.define('Ext.slider.Single', {
    extend: 'Ext.slider.Multi',
    alias: [
        'widget.slider',
        'widget.sliderfield'
    ],
    alternateClassName: [
        'Ext.Slider',
        'Ext.form.SliderField',
        'Ext.slider.SingleSlider',
        'Ext.slider.Slider'
    ],
    initComponent: function() {
        if (this.publishOnComplete) {
            this.valuePublishEvent = 'changecomplete';
        }
        this.callParent();
    },
    /**
     * @cfg {Boolean} [publishOnComplete=true]
     * This controls when the value of the slider is published to the `ViewModel`. By
     * default this is done only when the thumb is released (the change is complete). To
     * cause this to happen on every change of the thumb position, specify `false`. This
     * setting is `true` by default for improved performance on slower devices (such as
     * older browsers or tablets).
     */
    publishOnComplete: true,
    /**
     * Returns the current value of the slider
     * @return {Number} The current value of the slider
     */
    getValue: function() {
        // just returns the value of the first thumb,
        // which should be the only one in a single slider
        return this.callParent([
            0
        ]);
    },
    /**
     * Programmatically sets the value of the Slider. Ensures that the value is constrained within
     * the minValue and maxValue.
     * @param {Number} value The value to set the slider to. (This will be constrained within
     * minValue and maxValue)
     * @param {Object/Boolean} [animate] `false` to not animate. `true` to use the default
     * animation. This may also be an animate configuration object, see {@link #cfg-animate}.
     * If this configuration is omitted, the {@link #cfg-animate} configuration will be used.
     */
    setValue: function(value, animate) {
        var args = arguments,
            len = args.length;
        // this is to maintain backwards compatibility for sliders with only one thumb.
        // Usually you must pass the thumb index to setValue, but if we only have one thumb
        // we inject the index here first if given the multi-slider signature
        // without the required index. The index will always be 0 for a single slider
        if (len === 1 || (len <= 3 && typeof args[1] !== 'number')) {
            args = Ext.toArray(args);
            args.unshift(0);
        }
        return this.callParent(args);
    },
    /**
     * @private
     */
    getNearest: function() {
        // Since there's only 1 thumb, it's always the nearest
        return this.thumbs[0];
    }
});

/**
 * A Widget-based implementation of a slider.
 * @since 5.0.0
 */
Ext.define('Ext.slider.Widget', {
    extend: 'Ext.Widget',
    alias: 'widget.sliderwidget',
    // Required to pull in the styles
    requires: [
        'Ext.slider.Multi'
    ],
    cachedConfig: {
        /**
        * @cfg {Boolean} vertical
        * Orients the slider vertically rather than horizontally.
        */
        vertical: false
    },
    config: {
        /**
         * @cfg {Boolean} clickToChange
         * Determines whether or not clicking on the Slider axis will change the slider.
         */
        clickToChange: true,
        ui: 'widget',
        /**
         * @cfg {Number/Number[]} value
         * One more values for the position of the slider's thumb(s).
         */
        value: 0,
        /**
         * @cfg {Number} minValue
         * The minimum value for any slider thumb.
         */
        minValue: 0,
        /**
         * @cfg {Number} maxValue
         * The maximum value for any slider thumb.
         */
        maxValue: 100,
        /**
         * @cfg {Boolean} [publishOnComplete=true]
         * This controls when the value of the slider is published to the `ViewModel`. By
         * default this is done only when the thumb is released (the change is complete). To
         * cause this to happen on every change of the thumb position, specify `false`. This
         * setting is `true` by default for improved performance on slower devices (such as
         * older browsers or tablets).
         */
        publishOnComplete: true,
        /**
         * @cfg {Object} twoWayBindable
         * This object is a map of config property names holding a `true` if changes to
         * that config should written back to its binding. Most commonly this is used to
         * indicate that the `value` config should be monitored and changes written back
         * to the bound value.
         */
        twoWayBindable: {
            value: 1
        }
    },
    decimalPrecision: 0,
    defaultBindProperty: 'value',
    element: {
        reference: 'element',
        cls: Ext.baseCSSPrefix + 'slider',
        listeners: {
            mousedown: 'onMouseDown',
            dragstart: 'cancelDrag',
            drag: 'cancelDrag',
            dragend: 'cancelDrag'
        },
        children: [
            {
                reference: 'endEl',
                cls: Ext.baseCSSPrefix + 'slider-end',
                children: [
                    {
                        reference: 'innerEl',
                        cls: Ext.baseCSSPrefix + 'slider-inner'
                    }
                ]
            }
        ]
    },
    thumbCls: Ext.baseCSSPrefix + 'slider-thumb',
    horizontalProp: 'left',
    // This property is set to false onMouseDown and deleted onMouseUp. It is used only
    // by applyValue when it passes the animate parameter to setThumbValue.
    animateOnSetValue: undefined,
    applyValue: function(value) {
        var me = this,
            animate = me.animateOnSetValue,
            i, len;
        if (Ext.isArray(value)) {
            value = Ext.Array.from(value);
            for (i = 0 , len = value.length; i < len; ++i) {
                me.setThumbValue(i, value[i] = me.normalizeValue(value[i]), animate, true);
            }
        } else {
            value = me.normalizeValue(value);
            me.setThumbValue(0, value, animate, true);
        }
        return value;
    },
    updateVertical: function(vertical, oldVertical) {
        this.element.removeCls(Ext.baseCSSPrefix + 'slider-' + (oldVertical ? 'vert' : 'horz'));
        this.element.addCls(Ext.baseCSSPrefix + 'slider-' + (vertical ? 'vert' : 'horz'));
    },
    updateHeight: function(height, oldHeight) {
        this.callParent([
            height,
            oldHeight
        ]);
        this.endEl.dom.style.height = this.innerEl.dom.style.height = '100%';
    },
    cancelDrag: function(e) {
        // prevent the touch scroller from scrolling when the slider is being dragged
        e.stopPropagation();
    },
    getThumb: function(ordinal) {
        var me = this,
            thumbConfig,
            result = (me.thumbs || (me.thumbs = []))[ordinal],
            panDisable = me.getVertical() ? 'panY' : 'panX',
            touchAction = {};
        if (!result) {
            thumbConfig = {
                cls: me.thumbCls,
                style: {}
            };
            thumbConfig['data-thumbIndex'] = ordinal;
            result = me.thumbs[ordinal] = me.innerEl.createChild(thumbConfig);
            touchAction[panDisable] = false;
            result.setTouchAction(touchAction);
        }
        return result;
    },
    getThumbPositionStyle: function() {
        return this.getVertical() ? 'bottom' : this.horizontalProp;
    },
    //    // TODO: RTL
    //    getRenderTree: function() {
    //        var me = this,
    //            rtl = me.rtl;
    //
    //        if (rtl && Ext.rtl) {
    //            me.baseCls += ' ' + (Ext.rtl.util.Renderable.prototype._rtlCls);
    //            me.horizontalProp = 'right';
    //        } else if (rtl === false) {
    //            me.addCls(Ext.rtl.util.Renderable.prototype._ltrCls);
    //        }
    //
    //        return me.callParent();
    //    },
    update: function() {
        var me = this,
            values = me.getValues(),
            len = values.length,
            i;
        for (i = 0; i < len; i++) {
            this.thumbs[i].dom.style[me.getThumbPositionStyle()] = me.calculateThumbPosition(values[i]) + '%';
        }
    },
    updateMaxValue: function(maxValue) {
        this.onRangeAdjustment(maxValue, 'min');
    },
    updateMinValue: function(minValue) {
        this.onRangeAdjustment(minValue, 'max');
    },
    /**
     * @private
     * Conditionally updates value of slider when minValue or maxValue are updated
     * @param {Number} rangeValue The new min or max value
     * @param {String} compareType The comparison type (e.g., min/max)
     */
    onRangeAdjustment: function(rangeValue, compareType) {
        var value = this._value,
            newValue;
        if (!isNaN(value)) {
            newValue = Math[compareType](value, rangeValue);
        }
        if (newValue !== undefined) {
            this.setValue(newValue);
        }
        this.update();
    },
    onMouseDown: function(e) {
        var me = this,
            thumb,
            trackPoint = e.getXY(),
            delta;
        if (!me.disabled && e.button === 0) {
            // Stop any selection caused by mousedown + mousemove
            Ext.getDoc().on({
                scope: me,
                capture: true,
                selectstart: me.stopSelect
            });
            thumb = e.getTarget('.' + me.thumbCls, null, true);
            if (thumb) {
                me.animateOnSetValue = false;
                me.promoteThumb(thumb);
                me.captureMouse(me.onMouseMove, me.onMouseUp, [
                    thumb
                ], 1);
                delta = me.pointerOffset = thumb.getXY();
                // Work out the delta of the pointer from the dead centre of the thumb.
                // Slider.getTrackPoint positions the centre of the slider at the reported
                // pointer position, so we have to correct for that in getValueFromTracker.
                delta[0] += Math.floor(thumb.getWidth() / 2) - trackPoint[0];
                delta[1] += Math.floor(thumb.getHeight() / 2) - trackPoint[1];
            } else {
                if (me.getClickToChange()) {
                    trackPoint = me.getTrackpoint(trackPoint);
                    if (trackPoint != null) {
                        me.onClickChange(trackPoint);
                    }
                }
            }
        }
    },
    /**
     * @private
     * Moves the thumb to the indicated position.
     * Only changes the value if the click was within this.clickRange.
     * @param {Number} trackPoint local pixel offset **from the origin**
     * (left for horizontal and bottom for vertical) along the Slider's axis
     * at which the click event occured.
     */
    onClickChange: function(trackPoint) {
        var me = this,
            thumb, index, value;
        // How far along the track *from the origin* was the click.
        // If vertical, the origin is the bottom of the slider track.
        // find the nearest thumb to the click event
        thumb = me.getNearest(trackPoint);
        index = parseInt(thumb.getAttribute('data-thumbIndex'), 10);
        value = Ext.util.Format.round(me.reversePixelValue(trackPoint), me.decimalPrecision);
        if (index) {
            me.setThumbValue(index, value, undefined, true);
        } else {
            me.setValue(value);
        }
    },
    /**
     * @private
     * Returns the nearest thumb to a click event, along with its distance
     * @param {Number} trackPoint local pixel position along the Slider's axis to find the Thumb for
     * @return {Object} The closest thumb object and its distance from the click event
     */
    getNearest: function(trackPoint) {
        var me = this,
            clickValue = me.reversePixelValue(trackPoint),
            nearestDistance = me.getRange() + 5,
            // add a small fudge for the end of the slider
            nearest = null,
            thumbs = me.thumbs,
            i = 0,
            len = thumbs.length,
            thumb, value, dist;
        for (; i < len; i++) {
            thumb = thumbs[i];
            value = thumb.value;
            dist = Math.abs(value - clickValue);
            if (Math.abs(dist) <= nearestDistance) {
                nearest = thumb;
                nearestDistance = dist;
            }
        }
        return nearest;
    },
    /**
     * @private
     * Moves the given thumb above all other by increasing its z-index. This is called when as drag
     * any thumb, so that the thumb that was just dragged is always at the highest z-index. This is
     * required when the thumbs are stacked on top of each other at one of the ends of the slider's
     * range, which can result in the user not being able to move any of them.
     * @param {Ext.slider.Thumb} topThumb The thumb to move to the top
     */
    promoteThumb: function(topThumb) {
        var thumbs = this.thumbStack || (this.thumbStack = Ext.Array.slice(this.thumbs)),
            ln = thumbs.length,
            zIndex = 10000,
            i;
        // Move topthumb to position zero
        if (thumbs[0] !== topThumb) {
            Ext.Array.remove(thumbs, topThumb);
            thumbs.unshift(topThumb);
        }
        // Then shuffle the zIndices
        for (i = 0; i < ln; i++) {
            thumbs[i].el.setStyle('zIndex', zIndex);
            zIndex -= 1000;
        }
    },
    doMouseMove: function(e, thumb, changeComplete) {
        var me = this,
            trackerXY = e.getXY(),
            newValue, thumbIndex, trackPoint;
        trackerXY[0] += me.pointerOffset[0];
        trackerXY[1] += me.pointerOffset[1];
        trackPoint = me.getTrackpoint(trackerXY);
        // If dragged out of range, value will be undefined
        if (trackPoint != null) {
            newValue = Ext.util.Format.round(me.reversePixelValue(trackPoint), me.decimalPrecision);
            thumbIndex = parseInt(thumb.getAttribute('data-thumbIndex'), 10);
            if (thumbIndex || (!changeComplete && me.getPublishOnComplete())) {
                me.setThumbValue(thumbIndex, newValue, false, changeComplete);
            } else {
                me.setValue(newValue);
            }
        }
    },
    onMouseMove: function(e, thumb) {
        this.doMouseMove(e, thumb, false);
    },
    onMouseUp: function(e, thumb) {
        var me = this;
        me.doMouseMove(e, thumb, true);
        Ext.getDoc().un({
            scope: me,
            capture: true,
            selectstart: me.stopSelect
        });
        delete me.animateOnSetValue;
    },
    // expose "undefined" on prototype
    stopSelect: function(e) {
        e.stopEvent();
        return false;
    },
    /**
     * Programmatically sets the value of the Slider. Ensures that the value is constrained within
     * the minValue and maxValue.
     *
     * Setting a single value:
     *     // Set the second slider value, don't animate
     *     mySlider.setThumbValue(1, 50, false);
     *
     * Setting multiple values at once
     *     // Set 3 thumb values, animate
     *     mySlider.setThumbValue([20, 40, 60], true);
     *
     * @param {Number/Number[]} index Index of the thumb to move. Alternatively, it can be an array
     * of values to set for each thumb in the slider.
     * @param {Number} value The value to set the slider to. (This will be constrained within
     * minValue and maxValue)
     * @param {Boolean} [animate=true] Turn on or off animation
     * @param {Boolean} changeComplete
     * @return {Ext.slider.Multi} this
     */
    setThumbValue: function(index, value, animate, changeComplete) {
        var me = this,
            thumb, len, i, values;
        if (Ext.isArray(index)) {
            values = index;
            animate = value;
            for (i = 0 , len = values.length; i < len; ++i) {
                me.setThumbValue(i, values[i], animate, changeComplete);
            }
            return me;
        }
        thumb = me.getThumb(index);
        // ensures value is contstrained and snapped
        value = me.normalizeValue(value);
        if (value !== thumb.value && me.fireEvent('beforechange', me, value, thumb.value, thumb) !== false) {
            thumb.value = value;
            if (me.element.dom) {
                // TODO this only handles a single value; need a solution for exposing
                // multiple values to aria.
                // Perhaps this should go on each thumb element rather than the outer element.
                me.element.set({
                    'aria-valuenow': value,
                    'aria-valuetext': value
                });
                me.moveThumb(thumb, me.calculateThumbPosition(value), Ext.isDefined(animate) ? animate !== false : me.animate);
                me.fireEvent('change', me, value, thumb);
            }
        }
        return me;
    },
    /**
     * Returns the current value of the slider
     * @param {Number} index The index of the thumb to return a value for
     * @return {Number/Number[]} The current value of the slider at the given index,
     * or an array of all thumb values if no index is given.
     */
    getValue: function(index) {
        var me = this,
            value;
        if (Ext.isNumber(index)) {
            value = me.thumbs[index].value;
        } else {
            value = me.getValues();
            if (value.length === 1) {
                value = value[0];
            }
        }
        return value;
    },
    /**
     * Returns an array of values - one for the location of each thumb
     * @return {Number[]} The set of thumb values
     */
    getValues: function() {
        var me = this,
            values = [],
            i = 0,
            thumbs = me.thumbs,
            len = thumbs && thumbs.length;
        for (; i < len; i++) {
            values.push(me.thumbs[i].value);
        }
        return values;
    },
    /**
     * @private
     * move the thumb
     */
    moveThumb: function(thumb, v, animate) {
        var me = this,
            styleProp = me.getThumbPositionStyle(),
            to, from;
        v += '%';
        if (!animate) {
            thumb.dom.style[styleProp] = v;
        } else {
            to = {};
            to[styleProp] = v;
            if (!Ext.supports.GetPositionPercentage) {
                from = {};
                from[styleProp] = thumb.dom.style[styleProp];
            }
            new Ext.fx.Anim({
                target: thumb,
                duration: 350,
                from: from,
                to: to
            });
        }
    },
    /**
     * @private
     * Returns a snapped, constrained value when given a desired value
     * @param {Number} v Raw number value
     * @return {Number} The raw value rounded to the correct d.p. and constrained within
     * the set max and min values
     */
    normalizeValue: function(v) {
        var me = this,
            snapFn = me.zeroBasedSnapping ? 'snap' : 'snapInRange';
        v = Ext.Number[snapFn](v, me.increment, me.minValue, me.maxValue);
        v = Ext.util.Format.round(v, me.decimalPrecision);
        v = Ext.Number.constrain(v, me.minValue, me.maxValue);
        return v;
    },
    /**
     * @private
     * Given an `[x, y]` position within the slider's track (Points outside the slider's track
     * are coerced to either the minimum or maximum value), calculate how many pixels
     * **from the slider origin** (left for horizontal Sliders and bottom for vertical Sliders)
     * that point is.
     *
     * If the point is outside the range of the Slider's track, the return value is `undefined`
     * @param {Number[]} xy The point to calculate the track point for
     */
    getTrackpoint: function(xy) {
        var me = this,
            vertical = me.getVertical(),
            sliderTrack = me.innerEl,
            trackLength, result, positionProperty;
        if (vertical) {
            positionProperty = 'top';
            trackLength = sliderTrack.getHeight();
        } else {
            positionProperty = me.horizontalProp;
            trackLength = sliderTrack.getWidth();
        }
        xy = me.transformTrackPoints(sliderTrack.translatePoints(xy));
        result = Ext.Number.constrain(xy[positionProperty], 0, trackLength);
        return vertical ? trackLength - result : result;
    },
    transformTrackPoints: Ext.identityFn,
    /**
     * @private
     * Given a value within this Slider's range, calculates a Thumb's percentage CSS position
     * to map that value.
     */
    calculateThumbPosition: function(v) {
        var me = this,
            pos = (v - me.getMinValue()) / me.getRange() * 100;
        if (isNaN(pos)) {
            pos = 0;
        }
        return pos;
    },
    /**
     * @private
     * Returns the ratio of pixels to mapped values. e.g. if the slider is 200px wide
     * and maxValue - minValue is 100, the ratio is 2
     * @return {Number} The ratio of pixels to mapped values
     */
    getRatio: function() {
        var me = this,
            innerEl = me.innerEl,
            trackLength = me.getVertical() ? innerEl.getHeight() : innerEl.getWidth(),
            valueRange = me.getRange();
        return valueRange === 0 ? trackLength : (trackLength / valueRange);
    },
    getRange: function() {
        return this.getMaxValue() - this.getMinValue();
    },
    /**
     * @private
     * Given a pixel location along the slider, returns the mapped slider value for that pixel.
     * E.g. if we have a slider 200px wide with minValue = 100 and maxValue = 500,
     * reversePixelValue(50) returns 200
     * @param {Number} pos The position along the slider to return a mapped value for
     * @return {Number} The mapped value for the given position
     */
    reversePixelValue: function(pos) {
        return this.getMinValue() + (pos / this.getRatio());
    },
    captureMouse: function(onMouseMove, onMouseUp, args, appendArgs) {
        var me = this,
            onMouseupWrap, listeners;
        onMouseMove = onMouseMove && Ext.Function.bind(onMouseMove, me, args, appendArgs);
        onMouseUp = onMouseUp && Ext.Function.bind(onMouseUp, me, args, appendArgs);
        onMouseupWrap = function() {
            Ext.getDoc().un(listeners);
            if (onMouseUp) {
                onMouseUp.apply(me, arguments);
            }
        };
        listeners = {
            mousemove: onMouseMove,
            mouseup: onMouseupWrap
        };
        // Funnel mousemove events and the final mouseup event back into the gadget
        Ext.getDoc().on(listeners);
    },
    doDestroy: function() {
        Ext.destroy(this.thumbs);
        this.callParent();
    }
});

/**
 * A Provider implementation which saves and retrieves state via cookies. The CookieProvider
 * supports the usual cookie options, such as:
 *
 * - {@link #path}
 * - {@link #expires}
 * - {@link #domain}
 * - {@link #secure}
 *
 * Example:
 *
 *     var cp = Ext.create('Ext.state.CookieProvider', {
 *         path: "/cgi-bin/",
 *         expires: new Date(new Date().getTime()+(1000*60*60*24*30)), //30 days
 *         domain: "sencha.com"
 *     });
 *
 *     Ext.state.Manager.setProvider(cp);
 *
 */
Ext.define('Ext.state.CookieProvider', {
    extend: 'Ext.state.Provider',
    /**
     * @cfg {String} path
     * The path for which the cookie is active. Defaults to root '/' which makes it active
     * for all pages in the site.
     */
    /**
     * @cfg {Date} expires
     * The cookie expiration date. Defaults to 7 days from now.
     */
    /**
     * @cfg {String} domain
     * The domain to save the cookie for. Note that you cannot specify a different domain
     * than your page is on, but you can specify a sub-domain, or simply the domain itself
     * like 'sencha.com' to include all sub-domains if you need to access cookies across different
     * sub-domains. Defaults to null which uses the same domain the page is running on including
     * the 'www' like 'www.sencha.com'.
     */
    /**
     * @cfg {Boolean} [secure=false]
     * True if the site is using SSL
     */
    /**
     * Creates a new CookieProvider.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        var me = this;
        me.path = "/";
        me.expires = new Date(Ext.Date.now() + (1000 * 60 * 60 * 24 * 7));
        // 7 days
        me.domain = null;
        me.secure = false;
        me.callParent(arguments);
        me.state = me.readCookies();
    },
    /**
     * @private
     */
    set: function(name, value) {
        var me = this;
        if (typeof value === "undefined" || value === null) {
            me.clear(name);
            return;
        }
        me.setCookie(name, value);
        me.callParent(arguments);
    },
    /**
     * @private
     */
    clear: function(name) {
        this.clearCookie(name);
        this.callParent(arguments);
    },
    /**
     * @private
     */
    readCookies: function() {
        var cookies = {},
            c = document.cookie + ";",
            re = /\s?(.*?)=(.*?);/g,
            prefix = this.prefix,
            len = prefix.length,
            matches, name, value;
        while ((matches = re.exec(c)) != null) {
            name = matches[1];
            value = matches[2];
            if (name && name.substring(0, len) === prefix) {
                cookies[name.substr(len)] = this.decodeValue(value);
            }
        }
        return cookies;
    },
    /**
     * @private
     */
    setCookie: function(name, value) {
        var me = this;
        document.cookie = me.prefix + name + "=" + me.encodeValue(value) + ((me.expires == null) ? "" : ("; expires=" + me.expires.toUTCString())) + ((me.path == null) ? "" : ("; path=" + me.path)) + ((me.domain == null) ? "" : ("; domain=" + me.domain)) + (me.secure ? "; secure" : "");
    },
    /**
     * @private
     */
    clearCookie: function(name) {
        var me = this;
        document.cookie = me.prefix + name + "=null; expires=Thu, 01-Jan-1970 00:00:01 GMT" + ((me.path == null) ? "" : ("; path=" + me.path)) + ((me.domain == null) ? "" : ("; domain=" + me.domain)) + (me.secure ? "; secure" : "");
    }
});

/**
 * A Provider implementation which saves and retrieves state via the HTML5 localStorage API
 * or IE `userData` storage. For details see `Ext.util.LocalStorage`.
 * 
 * If the browser does not support local storage, there will be no attempt to read the state.
 * Before creating this class, check {@link Ext.util.LocalStorage#supported}.
 */
Ext.define('Ext.state.LocalStorageProvider', {
    extend: 'Ext.state.Provider',
    requires: [
        'Ext.util.LocalStorage'
    ],
    alias: 'state.localstorage',
    constructor: function() {
        var me = this;
        me.callParent(arguments);
        me.store = me.getStorageObject();
        if (me.store) {
            me.state = me.readLocalStorage();
        } else {
            me.state = {};
        }
    },
    readLocalStorage: function() {
        var store = this.store,
            data = {},
            keys = store.getKeys(),
            i = keys.length,
            key;
        while (i--) {
            key = keys[i];
            data[key] = this.decodeValue(store.getItem(key));
        }
        return data;
    },
    set: function(name, value) {
        var me = this;
        me.clear(name);
        if (value != null) {
            // !== undefined && !== null
            me.store.setItem(name, me.encodeValue(value));
            me.callParent(arguments);
        }
    },
    /**
     * @private
     */
    clear: function(name) {
        this.store.removeItem(name);
        this.callParent(arguments);
    },
    getStorageObject: function() {
        var prefix = this.prefix,
            id = prefix,
            n = id.length - 1;
        if (id.charAt(n) === '-') {
            id = id.substring(0, n);
        }
        return new Ext.util.LocalStorage({
            id: id,
            prefix: prefix
        });
    }
});

/**
 * Represents a single Tab in a {@link Ext.tab.Panel TabPanel}. A Tab is simply a slightly
 * customized {@link Ext.button.Button Button}, styled to look like a tab. Tabs are optionally
 * closable, and can also be disabled. 99% of the time you will not need to create Tabs manually
 * as the framework does so automatically when you use a {@link Ext.tab.Panel TabPanel}
 */
Ext.define('Ext.tab.Tab', {
    extend: 'Ext.button.Button',
    alias: 'widget.tab',
    /**
     * @property {Boolean} isTab
     * `true` in this class to identify an object as an instantiated Tab, or subclass thereof.
     */
    isTab: true,
    baseCls: Ext.baseCSSPrefix + 'tab',
    closeElOverCls: Ext.baseCSSPrefix + 'tab-close-btn-over',
    closeElPressedCls: Ext.baseCSSPrefix + 'tab-close-btn-pressed',
    config: {
        /**
         * @cfg {'default'/0/1/2} rotation
         * The rotation of the tab.  Can be one of the following values:
         *
         * - `null` - use the default rotation, depending on the dock position of the tabbar
         * - `0` - no rotation
         * - `1` - rotate 90deg clockwise
         * - `2` - rotate 90deg counter-clockwise
         *
         * The default behavior of this config depends on the dock position of the tabbar:
         *
         * - `'top'` or `'bottom'` - `0`
         * - `'right'` - `1`
         * - `'left'` - `2`
         */
        rotation: 'default',
        /**
         * @cfg {'top'/'right'/'bottom'/'left'} tabPosition
         * The tab's position.  Users should not typically need to set this, as it is
         * configured automatically by the tab bar
         */
        tabPosition: 'top'
    },
    /**
     * @cfg {Boolean} closable
     * True to make the Tab start closable (the close icon will be visible).
     */
    closable: true,
    // The wording is chosen to be less confusing to blind users.
    /**
     * @cfg {String} [closeText="removable"]
     * The accessible text label for the close button link to be announced by screen readers
     * when the tab is focused. This text does not appear visually and is only used when
     * {@link #cfg-closable} is `true`.
     * @locale
     */
    closeText: 'removable',
    /**
     * @property {Boolean} active
     * Indicates that this tab is currently active. This is NOT a public configuration.
     * @readonly
     */
    active: false,
    /**
     * @property {Boolean} closable
     * True if the tab is currently closable
     */
    childEls: [
        'closeEl'
    ],
    scale: false,
    /**
     * @event activate
     * Fired when the tab is activated.
     * @param {Ext.tab.Tab} this
     */
    /**
     * @event deactivate
     * Fired when the tab is deactivated.
     * @param {Ext.tab.Tab} this
     */
    /**
     * @event beforeclose
     * Fires if the user clicks on the Tab's close button, but before the {@link #close}
     * event is fired. Return false from any listener to stop the close event being fired
     * @param {Ext.tab.Tab} tab The Tab object
     */
    /**
     * @event close
     * Fires to indicate that the tab is to be closed, usually because the user has clicked
     * the close button.
     * @param {Ext.tab.Tab} tab The Tab object
     */
    ariaRole: 'tab',
    tabIndex: -1,
    keyMap: {
        scope: 'this',
        DELETE: 'onDeleteKey'
    },
    _btnWrapCls: Ext.baseCSSPrefix + 'tab-wrap',
    _btnCls: Ext.baseCSSPrefix + 'tab-button',
    _baseIconCls: Ext.baseCSSPrefix + 'tab-icon-el',
    _glyphCls: Ext.baseCSSPrefix + 'tab-glyph',
    _innerCls: Ext.baseCSSPrefix + 'tab-inner',
    _textCls: Ext.baseCSSPrefix + 'tab-text',
    _noTextCls: Ext.baseCSSPrefix + 'tab-no-text',
    _hasIconCls: Ext.baseCSSPrefix + 'tab-icon',
    _activeCls: Ext.baseCSSPrefix + 'tab-active',
    _closableCls: Ext.baseCSSPrefix + 'tab-closable',
    overCls: Ext.baseCSSPrefix + 'tab-over',
    _pressedCls: Ext.baseCSSPrefix + 'tab-pressed',
    _disabledCls: Ext.baseCSSPrefix + 'tab-disabled',
    _rotateClasses: {
        1: Ext.baseCSSPrefix + 'tab-rotate-right',
        2: Ext.baseCSSPrefix + 'tab-rotate-left'
    },
    // a mapping of the "ui" positions.  When "rotation" is anything other than 0, a ui
    // position other than the docked side must be used.
    _positions: {
        top: {
            'default': 'top',
            0: 'top',
            1: 'left',
            2: 'right'
        },
        right: {
            'default': 'top',
            0: 'right',
            1: 'top',
            2: 'bottom'
        },
        bottom: {
            'default': 'bottom',
            0: 'bottom',
            1: 'right',
            2: 'left'
        },
        left: {
            'default': 'top',
            0: 'left',
            1: 'bottom',
            2: 'top'
        }
    },
    _defaultRotations: {
        top: 0,
        right: 1,
        bottom: 0,
        left: 2
    },
    initComponent: function() {
        var me = this;
        // Although WAI-ARIA spec has a provision for deleting tab panels,
        // according to accessibility experts at University of Washington
        // closable tab panels can be very confusing to vision impaired users.
        // On top of that there are some technical issues with screen readers
        // not recognizing the changed number of open tabs, so it is better
        // to avoid closable tabs in accessible applications.
        if (me.closable) {
            Ext.ariaWarn(me, "Closable tabs can be confusing to users relying on Assistive Technologies " + "such as Screen Readers, and are not recommended in accessible applications. " + "Please consider setting " + me.title + " tab (" + me.id + ") to closable: false.");
        }
        if (me.card) {
            me.setCard(me.card);
        }
        me.callParent(arguments);
    },
    getActualRotation: function() {
        var rotation = this.getRotation();
        return (rotation !== 'default') ? rotation : this._defaultRotations[this.getTabPosition()];
    },
    updateRotation: function() {
        this.syncRotationAndPosition();
    },
    updateTabPosition: function() {
        this.syncRotationAndPosition();
    },
    syncRotationAndPosition: function() {
        var me = this,
            rotateClasses = me._rotateClasses,
            position = me.getTabPosition(),
            rotation = me.getActualRotation(),
            oldRotateCls = me._rotateCls,
            rotateCls = me._rotateCls = rotateClasses[rotation],
            oldPositionCls = me._positionCls,
            positionCls = me._positionCls = me._positions[position][rotation];
        if (oldRotateCls !== rotateCls) {
            if (oldRotateCls) {
                me.removeCls(oldRotateCls);
            }
            if (rotateCls) {
                me.addCls(rotateCls);
            }
        }
        if (oldPositionCls !== positionCls) {
            if (oldPositionCls) {
                me.removeClsWithUI(oldPositionCls);
            }
            if (positionCls) {
                me.addClsWithUI(positionCls);
            }
            if (me.rendered) {
                me.updateFrame();
            }
        }
        if (me.rendered) {
            me.setElOrientation();
        }
    },
    onAdded: function(container, pos, instanced) {
        this.callParent([
            container,
            pos,
            instanced
        ]);
        this.syncRotationAndPosition();
    },
    getTemplateArgs: function() {
        var me = this,
            result = me.callParent();
        result.closable = me.closable;
        result.closeText = me.closeText;
        return result;
    },
    beforeRender: function() {
        var me = this,
            tabBar = me.up('tabbar'),
            tabPanel = me.up('tabpanel');
        me.callParent();
        me.ariaRenderAttributes = me.ariaRenderAttributes || {};
        if (me.active) {
            me.ariaRenderAttributes['aria-selected'] = true;
            me.addCls(me._activeCls);
        } else {
            me.ariaRenderAttributes['aria-selected'] = false;
        }
        me.syncClosableCls();
        // Propagate minTabWidth and maxTabWidth settings from the owning TabBar then TabPanel
        if (!me.minWidth) {
            me.minWidth = (tabBar) ? tabBar.minTabWidth : me.minWidth;
            if (!me.minWidth && tabPanel) {
                me.minWidth = tabPanel.minTabWidth;
            }
            if (me.minWidth && me.iconCls) {
                me.minWidth += 25;
            }
        }
        if (!me.maxWidth) {
            me.maxWidth = (tabBar) ? tabBar.maxTabWidth : me.maxWidth;
            if (!me.maxWidth && tabPanel) {
                me.maxWidth = tabPanel.maxTabWidth;
            }
        }
    },
    onRender: function() {
        var me = this;
        me.setElOrientation();
        me.callParent(arguments);
        if (me.closable) {
            me.closeEl.addClsOnOver(me.closeElOverCls);
            me.closeEl.addClsOnClick(me.closeElPressedCls);
        }
    },
    setElOrientation: function() {
        var me = this,
            rotation = me.getActualRotation(),
            el = me.el;
        if (rotation) {
            el.setVertical(rotation === 1 ? 90 : 270);
        } else {
            el.setHorizontal();
        }
    },
    enable: function(silent) {
        var me = this;
        me.callParent(arguments);
        me.removeCls(me._disabledCls);
        return me;
    },
    disable: function(silent) {
        var me = this;
        me.callParent(arguments);
        me.addCls(me._disabledCls);
        return me;
    },
    /**
     * Sets the tab as either closable or not.
     * @param {Boolean} closable Pass false to make the tab not closable. Otherwise the tab
     * will be made closable (eg a close button will appear on the tab)
     */
    setClosable: function(closable) {
        var me = this;
        // Closable must be true if no args
        closable = (!arguments.length || !!closable);
        if (me.closable !== closable) {
            me.closable = closable;
            // set property on the user-facing item ('card'):
            if (me.card) {
                me.card.closable = closable;
            }
            me.syncClosableCls();
            if (me.rendered) {
                me.syncClosableElements();
                // Tab will change width to accommodate close icon
                me.updateLayout();
            }
        }
    },
    /**
     * This method ensures that the closeBtn element exists or not based on 'closable'.
     * @private
     */
    syncClosableElements: function() {
        var me = this,
            closeEl = me.closeEl;
        if (me.closable) {
            if (!closeEl) {
                closeEl = me.closeEl = me.btnWrap.insertSibling({
                    tag: 'span',
                    id: me.id + '-closeEl',
                    cls: me.baseCls + '-close-btn',
                    html: me.closeText
                }, 'after');
            }
            closeEl.addClsOnOver(me.closeElOverCls);
            closeEl.addClsOnClick(me.closeElPressedCls);
        } else if (closeEl) {
            closeEl.destroy();
            delete me.closeEl;
        }
    },
    /**
     * This method ensures that the closable cls are added or removed based on 'closable'.
     * @private
     */
    syncClosableCls: function() {
        var me = this,
            closableCls = me._closableCls;
        if (me.closable) {
            me.addCls(closableCls);
        } else {
            me.removeCls(closableCls);
        }
    },
    /**
     * Sets this tab's attached card. Usually this is handled automatically by the
     * {@link Ext.tab.Panel} that this Tab belongs to and would not need to be done by the developer
     * @param {Ext.Component} card The card to set
     */
    setCard: function(card) {
        var me = this;
        me.card = card;
        if (card.iconAlign) {
            me.setIconAlign(card.iconAlign);
        }
        if (card.textAlign) {
            me.setTextAlign(card.textAlign);
        }
        me.setText(me.title || card.title);
        me.setIconCls(me.iconCls || card.iconCls);
        me.setIcon(me.icon || card.icon);
        me.setGlyph(me.glyph || card.glyph);
    },
    /**
     * @private
     * Listener attached to click events on the Tab's close button
     */
    onCloseClick: function() {
        var me = this;
        if (me.fireEvent('beforeclose', me) !== false) {
            if (me.tabBar) {
                if (me.tabBar.closeTab(me) === false) {
                    // beforeclose on the panel vetoed the event, stop here
                    return;
                }
            } else {
                // if there's no tabbar, fire the close event
                me.fireClose();
            }
        }
    },
    /**
     * Fires the close event on the tab.
     * @private
     */
    fireClose: function() {
        this.fireEvent('close', this);
    },
    /**
     * @private
     */
    onEnterKey: function(e) {
        var me = this;
        if (me.tabBar) {
            me.tabBar.onClick(e, me.el);
            e.stopEvent();
            return false;
        }
    },
    /**
     * @private
     */
    onDeleteKey: function(e) {
        if (this.closable) {
            this.onCloseClick();
            e.stopEvent();
            return false;
        }
    },
    /**
     * @private
     */
    beforeClick: function(isCloseClick) {
        if (!isCloseClick) {
            this.focus();
        }
    },
    /**
     * @private
     */
    activate: function(supressEvent) {
        var me = this,
            card = me.card,
            ariaDom = me.ariaEl.dom;
        me.active = true;
        me.addCls(me._activeCls);
        if (ariaDom) {
            ariaDom.setAttribute('aria-selected', true);
        } else {
            me.ariaRenderAttributes = me.ariaRenderAttributes || {};
            me.ariaRenderAttributes['aria-selected'] = true;
        }
        if (card) {
            if (card.ariaEl.dom) {
                card.ariaEl.dom.setAttribute('aria-expanded', true);
            } else {
                card.ariaRenderAttributes = card.ariaRenderAttributes || {};
                card.ariaRenderAttributes['aria-expanded'] = true;
            }
        }
        if (supressEvent !== true) {
            me.fireEvent('activate', me);
        }
    },
    /**
     * @private
     */
    deactivate: function(supressEvent) {
        var me = this,
            card = me.card,
            ariaDom = me.ariaEl.dom;
        me.active = false;
        me.removeCls(me._activeCls);
        if (ariaDom) {
            ariaDom.setAttribute('aria-selected', false);
        } else {
            me.ariaRenderAttributes = me.ariaRenderAttributes || {};
            me.ariaRenderAttributes['aria-selected'] = false;
        }
        if (card) {
            if (card.ariaEl.dom) {
                card.ariaEl.dom.setAttribute('aria-expanded', false);
            } else {
                card.ariaRenderAttributes = card.ariaRenderAttributes || {};
                card.ariaRenderAttributes['aria-expanded'] = false;
            }
        }
        if (supressEvent !== true) {
            me.fireEvent('deactivate', me);
        }
    },
    privates: {
        getFramingInfoCls: function() {
            return this.baseCls + '-' + this.ui + '-' + this._positionCls;
        },
        wrapPrimaryEl: function(dom) {
            // Tabs don't need the hacks in Ext.dom.ButtonElement
            Ext.Button.superclass.wrapPrimaryEl.call(this, dom);
        }
    }
});

/**
 * TabBar is used internally by a {@link Ext.tab.Panel TabPanel} and typically should not
 * need to be created manually.
 */
Ext.define('Ext.tab.Bar', {
    extend: 'Ext.panel.Bar',
    xtype: 'tabbar',
    requires: [
        'Ext.tab.Tab',
        'Ext.util.Point',
        'Ext.layout.component.Body'
    ],
    baseCls: Ext.baseCSSPrefix + 'tab-bar',
    componentLayout: 'body',
    /**
     * @property {Boolean} isTabBar
     * `true` in this class to identify an object as an instantiated Tab Bar, or subclass thereof.
     */
    isTabBar: true,
    config: {
        /**
         * @cfg {'default'/0/1/2} tabRotation
         * The rotation of the tabs.  Can be one of the following values:
         *
         * - `default` - use the default rotation, depending on the dock position (see below)
         * - `0` - no rotation
         * - `1` - rotate 90deg clockwise
         * - `2` - rotate 90deg counter-clockwise
         *
         * The default behavior of this config depends on the dock position:
         *
         * - `'top'` or `'bottom'` - `0`
         * - `'right'` - `1`
         * - `'left'` - `2`
         */
        tabRotation: 'default',
        /**
         * @cfg {Boolean} tabStretchMax
         * `true` to stretch all tabs to the height of the tallest tab when the tabBar
         * is docked horizontally, or the width of the widest tab when the tabBar is
         * docked vertically.
         */
        tabStretchMax: true,
        // NB: This option is named this way for the intent, but in fact activation
        // happens in arrow key handler, not in focus handler. In IE focus events are
        // asynchronous, so activation happens before the tab's focus handler is fired.
        /**
         * @cfg {Boolean} [activateOnFocus=true]
         * `true` to follow WAI-ARIA requirement and activate tab when it is navigated to
         * with arrow keys, or `false` to disable that behavior. When activation on focus
         * is disabled, users will have to use arrow keys to focus a tab, and then press
         * Space key to activate it.
         */
        activateOnFocus: true
    },
    /**
     * @private
     */
    defaultType: 'tab',
    /**
     * @cfg {Boolean} plain
     * True to not show the full background on the tabbar
     */
    plain: false,
    /**
     * @cfg {Boolean} ensureActiveVisibleOnChange
     * `true` to ensure the active tab is scrolled into view when the tab changes, the text, the
     * icon or the glyph. This is only applicable if using an overflow scroller.
     *
     * @since 5.1.1
     */
    ensureActiveVisibleOnChange: true,
    ariaRole: 'tablist',
    focusableContainer: true,
    childEls: [
        'body',
        'strip'
    ],
    _stripCls: Ext.baseCSSPrefix + 'tab-bar-strip',
    _baseBodyCls: Ext.baseCSSPrefix + 'tab-bar-body',
    /* eslint-disable max-len */
    renderTpl: '<tpl if="hasTabGuard">{% this.renderTabGuard(out, values, \'before\'); %}</tpl>' + '<div id="{id}-body" data-ref="body" role="presentation" class="{baseBodyCls} {baseBodyCls}-{ui} ' + '{bodyCls} {bodyTargetCls}{childElCls}"<tpl if="bodyStyle"> style="{bodyStyle}"</tpl>>' + '{%this.renderContainer(out,values)%}' + '</div>' + '<tpl if="hasTabGuard">{% this.renderTabGuard(out, values, \'after\'); %}</tpl>' + '<div id="{id}-strip" data-ref="strip" role="presentation" class="{stripCls} {stripCls}-{ui}{childElCls}"></div>',
    /* eslint-enable max-len */
    /**
     * @cfg {Number} minTabWidth
     * The minimum width for a tab in this tab Bar. Defaults to the tab Panel's
     * {@link Ext.tab.Panel#minTabWidth minTabWidth} value.
     * @deprecated 6.5.0 This config is deprecated. Please use the
     * {@link Ext.tab.Panel#minTabWidth minTabWidth} config on the TabPanel.
     */
    /**
     * @cfg {Number} maxTabWidth
     * The maximum width for a tab in this tab Bar. Defaults to the tab Panel's
     * {@link Ext.tab.Panel#maxTabWidth maxTabWidth} value.
     * @deprecated 6.5.0 This config is deprecated. Please use the
     * {@link Ext.tab.Panel#maxTabWidth maxTabWidth} config on the TabPanel.
     */
    _reverseDockNames: {
        left: 'right',
        right: 'left'
    },
    _layoutAlign: {
        top: 'end',
        right: 'begin',
        bottom: 'begin',
        left: 'end'
    },
    /**
     * @event change
     * Fired when the currently-active tab has changed
     * @param {Ext.tab.Bar} tabBar The TabBar
     * @param {Ext.tab.Tab} tab The new Tab
     * @param {Ext.Component} card The card that was just shown in the TabPanel
     */
    /**
     * @private
     */
    initComponent: function() {
        var me = this,
            initialLayout = me.initialConfig.layout,
            initialAlign = initialLayout && initialLayout.align,
            initialOverflowHandler = initialLayout && initialLayout.overflowHandler;
        if (me.plain) {
            me.addCls(me.baseCls + '-plain');
        }
        // Will be applied to Ext.panel.Bar's layout config in parent initComponent
        me.layout = Ext.apply({
            align: initialAlign || (me.getTabStretchMax() ? 'stretchmax' : me._layoutAlign[me.dock]),
            overflowHandler: initialOverflowHandler || 'scroller'
        }, me.layout);
        me.callParent();
        me.on({
            click: me.onClick,
            element: 'el',
            scope: me
        });
    },
    /**
     * Ensure the passed tab is visible if using overflow scrolling 
     * @param {Ext.tab.Tab/Ext.Component/Number} [tab] The tab, item in the owning
     * {@link Ext.tab.Panel} or the index of the item to scroll to. Defaults to the active tab.
     */
    ensureTabVisible: function(tab) {
        var me = this,
            tabPanel = me.tabPanel,
            overflowHandler = me.layout.overflowHandler;
        if (me.rendered && overflowHandler && me.tooNarrow && overflowHandler.scrollToItem) {
            if (tab || tab === 0) {
                if (!tab.isTab) {
                    if (Ext.isNumber(tab)) {
                        tab = this.items.getAt(tab);
                    } else if (tab.isComponent && tabPanel && tabPanel.items.contains(tab)) {
                        tab = tab.tab;
                    }
                }
            }
            if (!tab) {
                tab = me.activeTab;
            }
            if (tab) {
                overflowHandler.scrollToItem(tab);
            }
        }
    },
    initRenderData: function() {
        var me = this;
        return Ext.apply(me.callParent(), {
            bodyCls: me.bodyCls,
            baseBodyCls: me._baseBodyCls,
            bodyTargetCls: me.bodyTargetCls,
            stripCls: me._stripCls,
            dock: me.dock
        });
    },
    setDock: function(dock) {
        var me = this,
            items = me.items,
            ownerCt = me.ownerCt,
            item, i, ln;
        items = items && items.items;
        if (items) {
            for (i = 0 , ln = items.length; i < ln; i++) {
                item = items[i];
                if (item.isTab) {
                    item.setTabPosition(dock);
                }
            }
        }
        if (me.rendered) {
            // TODO: remove resetItemMargins once EXTJS-13359 is fixed
            me.resetItemMargins();
            if (ownerCt && ownerCt.isHeader) {
                ownerCt.resetItemMargins();
            }
            me.needsScroll = true;
        }
        me.callParent([
            dock
        ]);
    },
    updateTabRotation: function(tabRotation) {
        var me = this,
            items = me.items,
            i, ln, item;
        items = items && items.items;
        if (items) {
            for (i = 0 , ln = items.length; i < ln; i++) {
                item = items[i];
                if (item.isTab) {
                    item.setRotation(tabRotation);
                }
            }
        }
        if (me.rendered) {
            // TODO: remove resetItemMargins once EXTJS-13359 is fixed
            me.resetItemMargins();
            me.needsScroll = true;
            me.updateLayout();
        }
    },
    onRender: function() {
        var me = this,
            overflowHandler = this.layout.overflowHandler;
        me.callParent();
        if (Ext.isIE8 && me.vertical) {
            me.el.on({
                mousemove: me.onMouseMove,
                scope: me
            });
        }
        if (overflowHandler && overflowHandler.type === 'menu') {
            overflowHandler.menu.on('click', 'onOverflowMenuItemClick', me);
        }
    },
    afterLayout: function() {
        this.adjustTabPositions();
        this.callParent(arguments);
    },
    onAdd: function(tab, pos) {
        var fn = this.onTabContentChange;
        if (this.ensureActiveVisibleOnChange) {
            tab.barListeners = tab.on({
                scope: this,
                destroyable: true,
                glyphchange: fn,
                iconchange: fn,
                textchange: fn
            });
        }
        this.callParent([
            tab,
            pos
        ]);
    },
    onAdded: function(container, pos, instanced) {
        if (container.isHeader) {
            this.addCls(container.baseCls + '-' + container.ui + '-tab-bar');
        }
        this.callParent([
            container,
            pos,
            instanced
        ]);
    },
    onRemove: function(tab, destroying) {
        var me = this;
        // If we're not destroying, no need to do this here since they will
        // be cleaned up
        if (me.ensureActiveVisibleOnChange) {
            if (!destroying) {
                tab.barListeners.destroy();
            }
            tab.barListeners = null;
        }
        if (tab === me.previousTab) {
            me.previousTab = null;
        }
        me.callParent([
            tab,
            destroying
        ]);
    },
    onRemoved: function(destroying) {
        var ownerCt = this.ownerCt;
        if (ownerCt.isHeader) {
            this.removeCls(ownerCt.baseCls + '-' + ownerCt.ui + '-tab-bar');
        }
        this.callParent([
            destroying
        ]);
    },
    onTabContentChange: function(tab) {
        if (tab === this.activeTab) {
            this.ensureTabVisible(tab);
        }
    },
    afterComponentLayout: function(width) {
        var me = this,
            needsScroll = me.needsScroll,
            overflowHandler = me.layout.overflowHandler;
        me.callParent(arguments);
        if (overflowHandler && needsScroll && me.tooNarrow && overflowHandler.scrollToItem) {
            overflowHandler.scrollToItem(me.activeTab);
        }
        delete me.needsScroll;
    },
    /**
     * @private
     */
    onMouseMove: function(e) {
        var me = this,
            overTab = me._overTab,
            tabInfo, tab;
        if (e.getTarget('.' + Ext.baseCSSPrefix + 'box-scroller')) {
            return;
        }
        tabInfo = me.getTabInfoFromPoint(e.getXY());
        tab = tabInfo.tab;
        if (tab !== overTab) {
            if (overTab && overTab.rendered) {
                overTab.onMouseLeave(e);
                me._overTab = null;
            }
            if (tab) {
                tab.onMouseEnter(e);
                me._overTab = tab;
                if (!tab.disabled) {
                    me.el.setStyle('cursor', 'pointer');
                }
            } else {
                me.el.setStyle('cursor', 'default');
            }
        }
    },
    onMouseLeave: function(e) {
        var overTab = this._overTab;
        if (overTab && overTab.rendered) {
            overTab.onMouseLeave(e);
        }
    },
    /**
     * @private
     * in IE8 and IE9 the clickable region of a rotated element is not its new rotated
     * position, but it's original unrotated position.  The result is that rotated tabs do
     * not capture click and mousenter/mosueleave events correctly.  This method accepts
     * an xy position and calculates if the coordinates are within a tab and if they
     * are within the tab's close icon (if any)
     */
    getTabInfoFromPoint: function(xy) {
        var me = this,
            tabs = me.items.items,
            length = tabs.length,
            innerCt = me.layout.innerCt,
            innerCtXY = innerCt.getXY(),
            point = new Ext.util.Point(xy[0], xy[1]),
            i = 0,
            lastBox, tabRegion, closeEl, close, closeXY, closeX, closeY, closeWidth, closeHeight, tabX, tabY, tabWidth, tabHeight, closeRegion, isTabReversed, direction, tab;
        for (; i < length; i++) {
            tab = tabs[i];
            lastBox = tab.lastBox;
            if (!lastBox || !tab.isTab) {
                // avoid looping hidden or not laid out, or if the item
                // is not a tab
                
                continue;
            }
            tabX = innerCtXY[0] + lastBox.x;
            tabY = innerCtXY[1] - innerCt.dom.scrollTop + lastBox.y;
            tabWidth = lastBox.width;
            tabHeight = lastBox.height;
            tabRegion = new Ext.util.Region(tabY, tabX + tabWidth, tabY + tabHeight, tabX);
            if (tabRegion.contains(point)) {
                closeEl = tab.closeEl;
                if (closeEl) {
                    // Read the dom to determine if the contents of the tab are reversed
                    // (rotated 180 degrees).  If so, we can cache the result becuase
                    // it's safe to assume all tabs in the tabbar will be the same
                    if (me._isTabReversed === undefined) {
                        me._isTabReversed = isTabReversed = (// use currentStyle because getComputedStyle won't get the
                        // filter property in IE9
                        tab.btnWrap.dom.currentStyle.filter.indexOf('rotation=2') !== -1);
                    }
                    direction = isTabReversed ? this._reverseDockNames[me.dock] : me.dock;
                    closeWidth = closeEl.getWidth();
                    closeHeight = closeEl.getHeight();
                    closeXY = me.getCloseXY(closeEl, tabX, tabY, tabWidth, tabHeight, closeWidth, closeHeight, direction);
                    closeX = closeXY[0];
                    closeY = closeXY[1];
                    closeRegion = new Ext.util.Region(closeY, closeX + closeWidth, closeY + closeHeight, closeX);
                    close = closeRegion.contains(point);
                }
                break;
            }
        }
        return {
            tab: tab,
            close: close
        };
    },
    /**
     * @private
     */
    getCloseXY: function(closeEl, tabX, tabY, tabWidth, tabHeight, closeWidth, closeHeight, direction) {
        // eslint-disable-line max-len
        var closeXY = closeEl.getXY(),
            closeX, closeY;
        if (direction === 'right') {
            closeX = tabX + tabWidth - ((closeXY[1] - tabY) + closeHeight);
            closeY = tabY + (closeXY[0] - tabX);
        } else {
            closeX = tabX + (closeXY[1] - tabY);
            closeY = tabY + tabX + tabHeight - closeXY[0] - closeWidth;
        }
        return [
            closeX,
            closeY
        ];
    },
    /**
     * @private
     * Closes the given tab by removing it from the TabBar and removing the corresponding card
     * from the TabPanel
     * @param {Ext.tab.Tab} toClose The tab to close
     */
    closeTab: function(toClose) {
        var me = this,
            card = toClose.card,
            tabPanel = me.tabPanel,
            toActivate;
        if (card && card.fireEvent('beforeclose', card) === false) {
            return false;
        }
        // If we are closing the active tab, revert to the previously active tab (or the previous
        // or next enabled sibling if there *is* no previously active tab, or the previously
        // active tab is the one that's being closed or the previously active tab has since
        // been disabled)
        toActivate = me.findNextActivatable(toClose);
        // We are going to remove the associated card, and then, if that was sucessful,
        // remove the Tab, and then potentially activate another Tab.
        // We should not layout for each of these operations.
        Ext.suspendLayouts();
        // If we are closing the active tab, revert to the previously active tab
        // (or the previous sibling or the next sibling)
        if (toActivate) {
            // Our owning TabPanel calls our setActiveTab method, so only call that
            // if this Bar is being used
            // in some other context (unlikely)
            if (tabPanel) {
                tabPanel.setActiveTab(toActivate.card);
            } else {
                me.setActiveTab(toActivate);
            }
            toActivate.focus();
        }
        if (tabPanel && card) {
            // Remove the ownerCt so the tab doesn't get destroyed if the remove is successful
            // We need this so we can have the tab fire it's own close event.
            delete toClose.ownerCt;
            // we must fire 'close' before removing the card from panel, otherwise
            // the event will no loger have any listener
            card.fireEvent('close', card);
            tabPanel.remove(card);
            // Remove succeeded
            if (card.ownerCt !== tabPanel) {
                /*
                 * Force the close event to fire. By the time this function returns,
                 * the tab is already destroyed and all listeners have been purged
                 * so the tab can't fire itself.
                 */
                toClose.fireClose();
                me.remove(toClose);
            } else {
                // Restore the ownerCt from above
                toClose.ownerCt = me;
                Ext.resumeLayouts(true);
                return false;
            }
        }
        Ext.resumeLayouts(true);
    },
    /**
     * @private
     * Determines the next tab to activate when one tab is closed.
     */
    findNextActivatable: function(toClose) {
        var me = this,
            previousTab = me.previousTab,
            nextTab;
        if (toClose.active && me.items.getCount() > 1) {
            if (previousTab && previousTab !== toClose && !previousTab.disabled) {
                nextTab = previousTab;
            } else {
                nextTab = toClose.next('tab[disabled=false]') || toClose.prev('tab[disabled=false]');
            }
        }
        // If we couldn't find the next tab to activate, fall back
        // to the currently active one. We need to have a focused tab
        // at all times.
        return nextTab || me.activeTab;
    },
    /**
     * @private
     * Marks the given tab as active
     * @param {Ext.tab.Tab} tab The tab to mark active
     * @param {Boolean} initial True if we're setting the tab during setup
     */
    setActiveTab: function(tab, initial) {
        var me = this;
        if (!tab.disabled && tab !== me.activeTab) {
            // Deactivate the previous tab, and ensure this FocusableContainer knows about it
            if (me.activeTab) {
                if (me.activeTab.destroyed) {
                    me.previousTab = null;
                } else {
                    me.previousTab = me.activeTab;
                    me.activeTab.deactivate();
                    me.deactivateFocusable(me.activeTab);
                }
            }
            // Activate the new tab, and ensure this FocusableContainer knows about it
            tab.activate();
            me.activateFocusable(tab);
            me.activeTab = tab;
            me.needsScroll = true;
            // We don't fire the change event when setting the first tab.
            // Also no need to run a layout
            if (!initial) {
                me.fireEvent('change', me, tab, tab.card);
                // Ensure that after the currently in progress layout,
                // the active tab is scrolled into view
                me.updateLayout();
            }
        }
    },
    privates: {
        adjustTabPositions: function() {
            var me = this,
                items = me.items.items,
                i = items.length,
                tab, lastBox, el, rotation, prop;
            // When tabs are rotated vertically we don't have a reliable way to position
            // them using CSS in modern browsers.  This is because of the way transform-orign
            // works - it requires the width to be known, and the width is not known in css.
            // Consequently we have to make an adjustment to the tab's position in these browsers.
            // This is similar to what we do in Ext.panel.Header#adjustTitlePosition
            if (!Ext.isIE8) {
                // 'left' in normal mode, 'right' in rtl
                prop = me._getTabAdjustProp();
                while (i--) {
                    tab = items[i];
                    el = tab.el;
                    lastBox = tab.lastBox;
                    rotation = tab.isTab ? tab.getActualRotation() : 0;
                    if (rotation === 1 && tab.isVisible()) {
                        // rotated 90 degrees using the top left corner as the axis.
                        // tabs need to be shifted to the right by their width
                        el.setStyle(prop, (lastBox.x + lastBox.width) + 'px');
                    } else if (rotation === 2 && tab.isVisible()) {
                        // rotated 270 degrees using the bottom right corner as the axis.
                        // tabs need to be shifted to the left by their height
                        el.setStyle(prop, (lastBox.x - lastBox.height) + 'px');
                    }
                }
            }
        },
        applyTargetCls: function(targetCls) {
            this.bodyTargetCls = targetCls;
        },
        // rtl hook
        _getTabAdjustProp: function() {
            return 'left';
        },
        getTargetEl: function() {
            return this.body || this.frameBody || this.el;
        },
        onClick: function(e, target) {
            var me = this,
                tabEl, tab, isCloseClick, tabInfo;
            if (e.getTarget('.' + Ext.baseCSSPrefix + 'box-scroller')) {
                return;
            }
            if (Ext.isIE8 && me.vertical) {
                tabInfo = me.getTabInfoFromPoint(e.getXY());
                tab = tabInfo.tab;
                isCloseClick = tabInfo.close;
            } else {
                // The target might not be a valid tab el.
                tabEl = e.getTarget('.' + Ext.tab.Tab.prototype.baseCls);
                tab = tabEl && Ext.getCmp(tabEl.id);
                isCloseClick = tab && tab.closeEl && (target === tab.closeEl.dom);
            }
            if (isCloseClick) {
                e.preventDefault();
            }
            if (tab && tab.isDisabled && !tab.isDisabled()) {
                // This will focus the tab; we do it before activating the card
                // because the card may attempt to focus itself or a child item.
                // We need to focus the tab explicitly because click target is
                // the Bar, not the Tab.
                tab.beforeClick(isCloseClick);
                if (tab.closable && isCloseClick) {
                    tab.onCloseClick();
                } else {
                    me.doActivateTab(tab);
                }
            }
        },
        onOverflowMenuItemClick: function(menu, item, e, eOpts) {
            var tab = item && item.masterComponent,
                overflowHandler = this.layout.overflowHandler;
            if (tab && !tab.isDisabled()) {
                this.doActivateTab(tab);
                // set focus to menuTrigger so that it doesn't revert to previous activeTab
                if (overflowHandler.menuTrigger) {
                    overflowHandler.menuTrigger.focus();
                }
            }
        },
        doActivateTab: function(tab) {
            var tabPanel = this.tabPanel;
            if (tabPanel) {
                // TabPanel will call setActiveTab of the TabBar
                if (!tab.disabled) {
                    tabPanel.setActiveTab(tab.card);
                }
            } else {
                this.setActiveTab(tab);
            }
        },
        onFocusableContainerFocus: function(e) {
            var me = this,
                mixin = me.mixins.focusablecontainer,
                child;
            child = mixin.onFocusableContainerFocus.call(me, e);
            if (child && child.isTab) {
                me.doActivateTab(child);
            }
        },
        onFocusableContainerFocusEnter: function(e) {
            var me = this,
                mixin = me.mixins.focusablecontainer,
                child;
            child = mixin.onFocusableContainerFocusEnter.call(me, e);
            if (child && child.isTab) {
                me.doActivateTab(child);
            }
        },
        focusChild: function(child, forward) {
            var me = this,
                mixin = me.mixins.focusablecontainer,
                nextChild;
            nextChild = mixin.focusChild.call(me, child, forward);
            if (me.activateOnFocus && nextChild && nextChild.isTab) {
                me.doActivateTab(nextChild);
            }
        }
    }
});

/**
 * A basic tab container. TabPanels can be used exactly like a standard {@link Ext.panel.Panel} for
 * layout purposes, but also have special support for containing child Components
 * (`{@link Ext.container.Container#cfg-items items}`) that are managed using a
 * {@link Ext.layout.container.Card CardLayout layout manager}, and displayed as separate tabs.
 *
 * **Note:** By default, a tab's close tool _destroys_ the child tab Component and all its
 * descendants. This makes the child tab Component, and all its descendants **unusable**.
 * To enable re-use of a tab, configure the TabPanel with `{@link #autoDestroy autoDestroy: false}`.
 *
 * ## TabPanel's layout
 *
 * TabPanels use a Dock layout to position the {@link Ext.tab.Bar TabBar} at the top of the widget.
 * Panels added to the TabPanel will have their header hidden by default because the Tab will
 * automatically take the Panel's configured title and icon.
 *
 * TabPanels use their {@link Ext.panel.Header header} or {@link Ext.panel.Panel#fbar footer}
 * element (depending on the {@link #tabPosition} configuration) to accommodate the tab selector
 * buttons. This means that a TabPanel will not display any configured title, and will not display
 * any configured header {@link Ext.panel.Panel#tools tools}.
 *
 * To display a header, embed the TabPanel in a {@link Ext.panel.Panel Panel} which uses
 * `{@link Ext.container.Container#layout layout: 'fit'}`.
 *
 * ## Controlling tabs
 *
 * Configuration options for the {@link Ext.tab.Tab} that represents the component can be passed in
 * by specifying the tabConfig option:
 *
 *     @example
 *     Ext.tip.QuickTipManager.init();
 *     Ext.create('Ext.tab.Panel', {
 *         width: 400,
 *         height: 400,
 *         renderTo: document.body,
 *         items: [{
 *             title: 'Foo'
 *         }, {
 *             title: 'Bar',
 *             tabConfig: {
 *                 title: 'Custom Title',
 *                 tooltip: 'A button tooltip'
 *             }
 *         }]
 *     });
 * 
 * ## Vetoing Changes
 * 
 * User interaction when changing the tabs can be vetoed by listening to the
 * {@link #beforetabchange} event. By returning `false`, the tab change will not occur.
 * 
 *     @example
 *     Ext.create('Ext.tab.Panel', {
 *         renderTo: Ext.getBody(),
 *         width: 200,
 *         height: 200,
 *         listeners: {
 *             beforetabchange: function(tabs, newTab, oldTab) {
 *                 return newTab.title != 'P2';
 *             }
 *         },
 *         items: [{
 *             title: 'P1'
 *         }, {
 *             title: 'P2'
 *         }, {
 *             title: 'P3'
 *         }]
 *     }); 
 *
 * # Examples
 *
 * Here is a basic TabPanel rendered to the body. This also shows the useful configuration
 * {@link #activeTab}, which allows you to set the active tab on render.
 *
 *     @example
 *     Ext.create('Ext.tab.Panel', {
 *         width: 300,
 *         height: 200,
 *         activeTab: 0,
 *         items: [
 *             {
 *                 title: 'Tab 1',
 *                 bodyPadding: 10,
 *                 html : 'A simple tab'
 *             },
 *             {
 *                 title: 'Tab 2',
 *                 html : 'Another one'
 *             }
 *         ],
 *         renderTo : Ext.getBody()
 *     });
 *
 * It is easy to control the visibility of items in the tab bar. Specify hidden: true to have the
 * tab button hidden initially. Items can be subsequently hidden and show by accessing the
 * tab property on the child item.
 *
 *     @example
 *     var tabs = Ext.create('Ext.tab.Panel', {
 *         width: 400,
 *         height: 400,
 *         renderTo: document.body,
 *         items: [{
 *             title: 'Home',
 *             html: 'Home',
 *             itemId: 'home'
 *         }, {
 *             title: 'Users',
 *             html: 'Users',
 *             itemId: 'users',
 *             hidden: true
 *         }, {
 *             title: 'Tickets',
 *             html: 'Tickets',
 *             itemId: 'tickets'
 *         }]
 *     });
 *
 *     Ext.defer(function(){
 *         tabs.child('#home').tab.hide();
 *         var users = tabs.child('#users');
 *         users.tab.show();
 *         tabs.setActiveTab(users);
 *     }, 1000);
 *
 * You can remove the background of the TabBar by setting the {@link #plain} property to `true`.
 *
 *     @example
 *     Ext.create('Ext.tab.Panel', {
 *         width: 300,
 *         height: 200,
 *         activeTab: 0,
 *         plain: true,
 *         items: [
 *             {
 *                 title: 'Tab 1',
 *                 bodyPadding: 10,
 *                 html : 'A simple tab'
 *             },
 *             {
 *                 title: 'Tab 2',
 *                 html : 'Another one'
 *             }
 *         ],
 *         renderTo : Ext.getBody()
 *     });
 *
 * Another useful configuration of TabPanel is {@link #tabPosition}. This allows you to change the
 * position where the tabs are displayed. The available options for this are `'top'` (default) and
 * `'bottom'`.
 *
 *     @example
 *     Ext.create('Ext.tab.Panel', {
 *         width: 300,
 *         height: 200,
 *         activeTab: 0,
 *         bodyPadding: 10,
 *         tabPosition: 'bottom',
 *         items: [
 *             {
 *                 title: 'Tab 1',
 *                 html : 'A simple tab'
 *             },
 *             {
 *                 title: 'Tab 2',
 *                 html : 'Another one'
 *             }
 *         ],
 *         renderTo : Ext.getBody()
 *     });
 *
 * The {@link #setActiveTab} is a very useful method in TabPanel which will allow you to change the
 * current active tab. You can either give it an index or an instance of a tab. For example:
 *
 *     @example
 *     var tabs = Ext.create('Ext.tab.Panel', {
 *         items: [
 *             {
 *                 id   : 'my-tab',
 *                 title: 'Tab 1',
 *                 html : 'A simple tab'
 *             },
 *             {
 *                 title: 'Tab 2',
 *                 html : 'Another one'
 *             }
 *         ],
 *         renderTo : Ext.getBody()
 *     });
 *
 *     var tab = Ext.getCmp('my-tab');
 *
 *     Ext.create('Ext.button.Button', {
 *         renderTo: Ext.getBody(),
 *         text    : 'Select the first tab',
 *         scope   : this,
 *         handler : function() {
 *             tabs.setActiveTab(tab);
 *         }
 *     });
 *
 *     Ext.create('Ext.button.Button', {
 *         text    : 'Select the second tab',
 *         scope   : this,
 *         handler : function() {
 *             tabs.setActiveTab(1);
 *         },
 *         renderTo : Ext.getBody()
 *     });
 *
 * The {@link #getActiveTab} is a another useful method in TabPanel which will return the current
 * active tab.
 *
 *     @example
 *     var tabs = Ext.create('Ext.tab.Panel', {
 *         items: [
 *             {
 *                 title: 'Tab 1',
 *                 html : 'A simple tab'
 *             },
 *             {
 *                 title: 'Tab 2',
 *                 html : 'Another one'
 *             }
 *         ],
 *         renderTo : Ext.getBody()
 *     });
 *
 *     Ext.create('Ext.button.Button', {
 *         text    : 'Get active tab',
 *         scope   : this,
 *         handler : function() {
 *             var tab = tabs.getActiveTab();
 *             alert('Current tab: ' + tab.title);
 *         },
 *         renderTo : Ext.getBody()
 *     });
 *
 * Adding a new tab is very simple with a TabPanel. You simple call the {@link #method-add} method
 * with an config object for a panel.
 *
 *     @example
 *     var tabs = Ext.create('Ext.tab.Panel', {
 *         items: [
 *             {
 *                 title: 'Tab 1',
 *                 html : 'A simple tab'
 *             },
 *             {
 *                 title: 'Tab 2',
 *                 html : 'Another one'
 *             }
 *         ],
 *         renderTo : Ext.getBody()
 *     });
 *
 *     Ext.create('Ext.button.Button', {
 *         text    : 'New tab',
 *         scope   : this,
 *         handler : function() {
 *             var tab = tabs.add({
 *                 // we use the tabs.items property to get the length of current items/tabs
 *                 title: 'Tab ' + (tabs.items.length + 1),
 *                 html : 'Another one'
 *             });
 *
 *             tabs.setActiveTab(tab);
 *         },
 *         renderTo : Ext.getBody()
 *     });
 *
 * Additionally, removing a tab is very also simple with a TabPanel. You simple call the
 * {@link #method-remove} method with an config object for a panel.
 *
 *     @example
 *     var tabs = Ext.create('Ext.tab.Panel', {
 *         items: [
 *             {
 *                 title: 'Tab 1',
 *                 html : 'A simple tab'
 *             },
 *             {
 *                 id   : 'remove-this-tab',
 *                 title: 'Tab 2',
 *                 html : 'Another one'
 *             }
 *         ],
 *         renderTo : Ext.getBody()
 *     });
 *
 *     Ext.create('Ext.button.Button', {
 *         text    : 'Remove tab',
 *         scope   : this,
 *         handler : function() {
 *             var tab = Ext.getCmp('remove-this-tab');
 *             tabs.remove(tab);
 *         },
 *         renderTo : Ext.getBody()
 *     });
 */
Ext.define('Ext.tab.Panel', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.tabpanel',
    alternateClassName: [
        'Ext.TabPanel'
    ],
    requires: [
        'Ext.layout.container.Card',
        'Ext.tab.Bar'
    ],
    config: {
        // @cmd-auto-dependency { directRef: 'Ext.tab.Bar' }
        /**
         * @cfg {Object} tabBar
         * Optional configuration object for the internal {@link Ext.tab.Bar}.
         * If present, this is passed straight through to the TabBar's constructor
         */
        tabBar: undefined,
        /**
         * @cfg {"top"/"bottom"/"left"/"right"} tabPosition
         * The position where the tab strip should be rendered. Possible values are: 
         * 
         *  - top
         *  - bottom
         *  - left
         *  - right
         */
        tabPosition: 'top',
        /**
         * @cfg {'default'/0/1/2} tabRotation
         * The rotation of the tabs.  Can be one of the following values:
         *
         * - `'default'` use the default rotation, depending on {@link #tabPosition} (see below)
         * - `0` - no rotation
         * - `1` - rotate 90deg clockwise
         * - `2` - rotate 90deg counter-clockwise
         *
         * The default behavior of this config depends on the {@link #tabPosition}:
         *
         * - `'top'` or `'bottom'` - `0`
         * - `'right'` - `1`
         * - `'left'` - `2`
         */
        tabRotation: 'default',
        /**
         * @cfg {Boolean} tabStretchMax
         * `true` to stretch all tabs to the height of the tallest tab when the tabBar
         * is docked horizontally, or the width of the widest tab when the tabBar is
         * docked vertically.
         */
        tabStretchMax: true
    },
    /**
     * @cfg {Number} tabBarHeaderPosition
     * If specified, the {@link #tabBar} will be rendered as an item of the TabPanel's
     * Header and the specified `tabBarHeaderPosition` will be used as the Panel header's
     * {@link Ext.panel.Header#itemPosition}.  If not specified, the {@link #tabBar}
     * will be rendered as a docked item at {@link #tabPosition}.
     */
    /**
     * @cfg {String/Number} activeItem
     * Doesn't apply for {@link Ext.tab.Panel TabPanel}, use {@link #activeTab} instead.
     */
    /**
     * @cfg {String/Number/Ext.Component} activeTab
     * The tab to activate initially. Either an ID, index or the tab component itself.
     * If null, no tab will be set as active.
     */
    /**
     * @cfg {Ext.enums.Layout/Object} layout
     * Optional configuration object for the internal {@link Ext.layout.container.Card card layout}.
     * If present, this is passed straight through to the layout's constructor
     */
    /**
     * @cfg {Boolean} removePanelHeader
     * True to instruct each Panel added to the TabContainer to not render its header element.
     * This is to ensure that the title of the panel does not appear twice.
     */
    removePanelHeader: true,
    /**
     * @cfg {Boolean} plain
     * True to not show the full background on the TabBar.
     */
    plain: false,
    /**
     * @cfg {String} [itemCls='x-tabpanel-child']
     * The class added to each child item of this TabPanel.
     */
    itemCls: Ext.baseCSSPrefix + 'tabpanel-child',
    /**
     * @cfg {Number} minTabWidth
     * The minimum width for a tab in the {@link #cfg-tabBar}.
     */
    minTabWidth: undefined,
    /**
     * @cfg {Number} maxTabWidth The maximum width for each tab.
     */
    maxTabWidth: undefined,
    /**
     * @cfg {Boolean} deferredRender
     *
     * True by default to defer the rendering of child
     * {@link Ext.container.Container#cfg-items items} to the browsers DOM until a tab is activated.
     * False will render all contained {@link Ext.container.Container#cfg-items items} as soon as
     * the {@link Ext.layout.container.Card layout} is rendered. If there is a significant amount
     * of content or a lot of heavy controls being rendered into panels that are not displayed
     * by default, setting this to true might improve performance.
     *
     * The deferredRender property is internally passed to the layout manager for TabPanels
     * ({@link Ext.layout.container.Card}) as its {@link Ext.layout.container.Card#deferredRender}
     * configuration value.
     *
     * **Note**: leaving deferredRender as true means that the content within an unactivated tab
     * will not be available
     */
    deferredRender: true,
    _defaultTabRotation: {
        top: 0,
        right: 1,
        bottom: 0,
        left: 2
    },
    /**
     * @event beforetabchange
     * Fires before a tab change (activated by {@link #setActiveTab}). Return false in any listener
     * to cancel the tabchange
     * @param {Ext.tab.Panel} tabPanel The TabPanel
     * @param {Ext.Component} newCard The card that is about to be activated
     * @param {Ext.Component} oldCard The card that is currently active
     */
    /**
     * @event tabchange
     * Fires when a new tab has been activated (activated by {@link #setActiveTab}).
     * @param {Ext.tab.Panel} tabPanel The TabPanel
     * @param {Ext.Component} newCard The newly activated item
     * @param {Ext.Component} oldCard The previously active item
     */
    initComponent: function() {
        var me = this,
            // Default to 0 if undefined and not null!
            activeTab = me.activeTab !== null ? (me.activeTab || 0) : null,
            dockedItems = me.dockedItems,
            header = me.header,
            tabBarHeaderPosition = me.tabBarHeaderPosition,
            tabBar = me.getTabBar(),
            headerItems;
        // Configure the layout with our deferredRender, and with our activeTeb
        me.layout = Ext.apply({
            type: 'card',
            deferredRender: me.deferredRender,
            itemCls: me.itemCls,
            activeItem: activeTab
        }, me.layout);
        if (tabBarHeaderPosition != null) {
            header = me.header = Ext.apply({}, header);
            headerItems = header.items = (header.items ? header.items.slice() : []);
            header.itemPosition = tabBarHeaderPosition;
            headerItems.push(tabBar);
            header.hasTabBar = true;
        } else {
            dockedItems = [].concat(me.dockedItems || []);
            dockedItems.push(tabBar);
            me.dockedItems = dockedItems;
        }
        me.callParent(arguments);
        // We have to convert the numeric index/string ID config into its component reference
        activeTab = me.activeTab = me.getComponent(activeTab);
        // Ensure that the active child's tab is rendered in the active UI state
        if (activeTab) {
            tabBar.setActiveTab(activeTab.tab, true);
        }
    },
    /**
     * @method getTabBar
     * Returns the {@link Ext.tab.Bar} associated with this tabPanel.
     * @return {Ext.tab.Bar} The tabBar for this tabPanel
     */
    /**
     * @method setTabBar
     * @ignore
     */
    onRender: function() {
        var items = this.items.items,
            len = items.length,
            i;
        this.callParent(arguments);
        // We want to force any view model for the panel to be created.
        // This is because we capture various parts about the panel itself (title, icon, etc)
        // So we need to be able to use that to populate the tabs.
        // In the future, this could be optimized to be a bit smarter to prevent creation when
        // not required.
        for (i = 0; i < len; ++i) {
            items[i].getBind();
        }
    },
    /**
     * Makes the given card active. Makes it the visible card in the TabPanel's CardLayout
     * and highlights the Tab.
     * @param {String/Number/Ext.Component} card The card to make active. Either an ID,
     * index or the component itself.
     * @return {Ext.Component} The resulting active child Component. The call may have been vetoed,
     * or otherwise modified by an event listener.
     */
    setActiveTab: function(card) {
        var me = this,
            previous;
        // Check for a config object
        if (!Ext.isObject(card) || card.isComponent) {
            card = me.getComponent(card);
        }
        previous = me.getActiveTab();
        if (card) {
            Ext.suspendLayouts();
            // We may be passed a config object, so add it.
            // Without doing a layout!
            if (!card.isComponent) {
                card = me.add(card);
            }
            if (previous === card || me.fireEvent('beforetabchange', me, card, previous) === false) {
                Ext.resumeLayouts(true);
                return previous;
            }
            // MUST set the activeTab first so that the machinery which listens for show doesn't
            // think that the show is "driving" the activation and attempt to recurse into here.
            me.activeTab = card;
            // Attempt to switch to the requested card. Suspend layouts because
            // if that was successful we have to also update the active tab in the tab bar
            // which is another layout operation  and we must coalesce them.
            me.layout.setActiveItem(card);
            // Read the result of the card layout. Events dear boy, events!
            card = me.activeTab = me.layout.getActiveItem();
            // Card switch was not vetoed by an event listener
            if (card && card !== previous) {
                // Update the active tab in the tab bar and resume layouts.
                me.tabBar.setActiveTab(card.tab);
                Ext.resumeLayouts(true);
                // previous will be undefined or this.activeTab at instantiation
                if (previous !== card) {
                    me.fireEvent('tabchange', me, card, previous);
                }
            } else // Card switch was vetoed by an event listener.
            // Resume layouts (Nothing should have changed on a veto).
            {
                Ext.resumeLayouts(true);
            }
            return card;
        }
        return previous;
    },
    setActiveItem: function(item) {
        return this.setActiveTab(item);
    },
    /**
     * Returns the item that is currently active inside this TabPanel.
     * @return {Ext.Component} The currently active item.
     */
    getActiveTab: function() {
        var me = this,
            // Ensure the calculated result references a Component
            result = me.getComponent(me.activeTab);
        // Sanitize the result in case the active tab is no longer there.
        if (result && me.items.indexOf(result) !== -1) {
            me.activeTab = result;
        } else {
            me.activeTab = undefined;
        }
        return me.activeTab;
    },
    applyTabBar: function(tabBar) {
        var me = this,
            // if we are rendering the tabbar into the panel header, use same alignment
            // as header position, and ignore tabPosition.
            dock = (me.tabBarHeaderPosition != null) ? me.getHeaderPosition() : me.getTabPosition();
        return new Ext.tab.Bar(Ext.apply({
            ui: me.ui,
            dock: dock,
            tabRotation: me.getTabRotation(),
            vertical: (dock === 'left' || dock === 'right'),
            plain: me.plain,
            tabStretchMax: me.getTabStretchMax(),
            tabPanel: me
        }, tabBar));
    },
    updateHeaderPosition: function(headerPosition, oldHeaderPosition) {
        var tabBar = this.getTabBar();
        if (tabBar && (this.tabBarHeaderPosition != null)) {
            tabBar.setDock(headerPosition);
        }
        this.callParent([
            headerPosition,
            oldHeaderPosition
        ]);
    },
    updateTabPosition: function(tabPosition) {
        var tabBar = this.getTabBar();
        if (tabBar && (this.tabBarHeaderPosition == null)) {
            tabBar.setDock(tabPosition);
        }
    },
    updateTabRotation: function(tabRotation) {
        var tabBar = this.getTabBar();
        if (tabBar) {
            tabBar.setTabRotation(tabRotation);
        }
    },
    /**
     * @protected
     * Makes sure we have a Tab for each item added to the TabPanel
     */
    onAdd: function(item, index) {
        var me = this,
            cfg = Ext.apply({}, item.tabConfig),
            tabBar = me.getTabBar(),
            ariaDom,
            defaultConfig = {
                xtype: 'tab',
                title: item.title,
                icon: item.icon,
                iconCls: item.iconCls,
                glyph: item.glyph,
                ui: tabBar.ui,
                card: item,
                disabled: item.disabled,
                closable: item.closable,
                // only hide if it wasn't hidden by the layout itself
                hidden: item.hidden && !item.hiddenByLayout,
                tooltip: item.tooltip,
                tabBar: tabBar,
                tabPosition: tabBar.dock,
                rotation: tabBar.getTabRotation()
            };
        if (item.closeText !== undefined) {
            defaultConfig.closeText = item.closeText;
        }
        cfg = Ext.applyIf(cfg, defaultConfig);
        // Create the correspondiong tab in the tab bar
        item.tab = me.tabBar.insert(index, cfg);
        // We want to force the relationship of the tabpanel to the tab
        item.ariaRole = 'tabpanel';
        // Item might be already rendered and then added to the TabPanel
        ariaDom = item.ariaEl.dom;
        if (ariaDom) {
            ariaDom.setAttribute('aria-labelledby', item.tab.id);
        } else {
            item.ariaRenderAttributes = item.ariaRenderAttributes || {};
            item.ariaRenderAttributes['aria-labelledby'] = item.tab.id;
        }
        item.on({
            scope: me,
            enable: me.onItemEnable,
            disable: me.onItemDisable,
            beforeshow: me.onItemBeforeShow,
            iconchange: me.onItemIconChange,
            iconclschange: me.onItemIconClsChange,
            glyphchange: me.onItemGlyphChange,
            titlechange: me.onItemTitleChange
        });
        if (item.isPanel) {
            if (me.removePanelHeader) {
                if (item.rendered) {
                    if (item.header) {
                        item.header.hide();
                    }
                } else {
                    item.header = false;
                }
            }
            if (item.isPanel && me.border) {
                item.setBorder(false);
            }
        }
        // Force the view model to be created, see onRender
        if (me.rendered) {
            item.getBind();
        }
        // Ensure that there is at least one active tab. This is only needed when adding tabs
        // via a loader config, i.e., there may be no pre-existing tabs. Note that we need to check
        // if activeTab was explicitly set to `null` in the tabpanel config (which tells the layout
        // not to set an active item), as this is a valid value to mean 'do not set an active tab'.
        if (me.rendered && me.loader && me.activeTab === undefined && me.layout.activeItem !== null) {
            me.setActiveTab(0);
        }
    },
    onMove: function(item, fromIdx, toIdx) {
        var tabBar = this.getTabBar();
        this.callParent([
            item,
            fromIdx,
            toIdx
        ]);
        // If the move of the item.tab triggered the movement of the child Panel,
        // then we're done.
        if (tabBar.items.indexOf(item.tab) !== toIdx) {
            tabBar.move(item.tab, toIdx);
        }
    },
    /**
     * @private
     * Enable corresponding tab when item is enabled.
     */
    onItemEnable: function(item) {
        item.tab.enable();
    },
    /**
     * @private
     * Disable corresponding tab when item is enabled.
     */
    onItemDisable: function(item) {
        item.tab.disable();
    },
    /**
     * @private
     * Sets activeTab before item is shown.
     */
    onItemBeforeShow: function(item) {
        if (item !== this.activeTab) {
            this.setActiveTab(item);
            return false;
        }
    },
    /**
     * @private
     * Update the tab icon when panel glyph has been set or changed.
     */
    onItemGlyphChange: function(item, newGlyph) {
        item.tab.setGlyph(newGlyph);
    },
    /**
     * @private
     * Update the tab icon when panel icon has been set or changed.
     */
    onItemIconChange: function(item, newIcon) {
        item.tab.setIcon(newIcon);
    },
    /**
     * @private
     * Update the tab iconCls when panel iconCls has been set or changed.
     */
    onItemIconClsChange: function(item, newIconCls) {
        item.tab.setIconCls(newIconCls);
    },
    /**
     * @private
     * Update the tab title when panel title has been set or changed.
     */
    onItemTitleChange: function(item, newTitle) {
        item.tab.setText(newTitle);
    },
    /**
     * @private
     * Makes sure we remove the corresponding Tab when an item is removed
     */
    onRemove: function(item, destroying) {
        var me = this;
        item.un({
            scope: me,
            enable: me.onItemEnable,
            disable: me.onItemDisable,
            beforeshow: me.onItemBeforeShow,
            iconchange: me.onItemIconChange,
            iconclschange: me.onItemIconClsChange,
            glyphchange: me.onItemGlyphChange,
            titlechange: me.onItemTitleChange
        });
        if (item.tab && !me.destroying && item.tab.ownerCt === me.tabBar) {
            me.tabBar.remove(item.tab);
        }
    },
    enable: function() {
        var me = this,
            activeTab = me.activeTab !== null ? (me.activeTab || 0) : null,
            wasDisabled = me.disabled;
        me.callParent(arguments);
        if (wasDisabled) {
            activeTab = activeTab.isComponent ? activeTab : me.getComponent(activeTab);
            if (activeTab) {
                me.getTabBar().setActiveTab(activeTab.tab);
            }
        }
        return me;
    },
    privates: {
        /**
         * @private
         * Unlink the removed child item from its (@link Ext.tab.Tab Tab}.
         *
         * If we're removing the currently active tab, activate the nearest one.
         * The item is removed when we call super, so we can do preprocessing before then
         * to find the card's index
         */
        doRemove: function(item, autoDestroy) {
            var me = this,
                toActivate;
            // The component is not removed from the items collection until callParent, so
            // don't trigger a layout until that happens
            Ext.suspendLayouts();
            // Destroying, or removing the last item, nothing to activate
            if (me.removingAll || me.destroying || me.items.getCount() === 1) {
                me.activeTab = null;
            }
            // Ask the TabBar which tab to activate next.
            // Set the active child panel using the index of that tab
            // eslint-disable-next-line no-cond-assign, max-len
            else if (item.tab && (toActivate = me.tabBar.items.indexOf(me.tabBar.findNextActivatable(item.tab))) !== -1) {
                me.setActiveTab(toActivate);
            }
            me.callParent([
                item,
                autoDestroy
            ]);
            Ext.resumeLayouts();
            // Don't want to flush here, this will happen in remove()
            if (item.tab) {
                // Remove the two references
                delete item.tab.card;
                delete item.tab;
            }
        }
    }
});

/**
 * A toolbar that displays hierarchical data from a {@link Ext.data.TreeStore TreeStore}
 * as a trail of breadcrumb buttons.  Each button represents a node in the store.  A click
 * on a button will "select" that node in the tree.  Non-leaf nodes will display their
 * child nodes on a dropdown menu of the corresponding button in the breadcrumb trail,
 * and a click on an item in the menu will trigger selection of the corresponding child
 * node.
 *
 * The selection can be set programmatically  using {@link #setSelection}, or retrieved
 * using {@link #getSelection}.
 */
Ext.define('Ext.toolbar.Breadcrumb', {
    extend: 'Ext.container.Container',
    xtype: 'breadcrumb',
    requires: [
        'Ext.data.TreeStore',
        'Ext.button.Split'
    ],
    isBreadcrumb: true,
    /**
     * @cfg baseCls
     * @inheritdoc
     */
    baseCls: Ext.baseCSSPrefix + 'breadcrumb',
    /**
     * @cfg layout
     * @inheritdoc
     */
    layout: {
        type: 'hbox',
        align: 'middle'
    },
    config: {
        /**
         * @cfg {String} buttonUI
         * Button UI to use for breadcrumb items.  Use {@link #extjs-breadcrumb-ui} to
         * add special styling to the breadcrumb arrows
         */
        buttonUI: 'plain-toolbar',
        /**
         * @cfg {String} displayField
         * The name of the field in the data model to display in the navigation items of
         * this breadcrumb toolbar
         */
        displayField: 'text',
        /**
         * @cfg {String} overflowHandler
         * The overflowHandler for this Breadcrumb:
         *
         * - `null` - hidden overflow
         * - `'scroller'` to render left/right scroller buttons on either side of the breadcrumb
         * - `'menu'` to render the overflowing buttons as items of an overflow menu.
         */
        overflowHandler: null,
        /**
         * @cfg {Boolean} showIcons
         *
         * Controls whether or not icons of tree nodes are displayed in the breadcrumb
         * buttons.  There are 3 possible values for this config:
         *
         * 1. unspecified (`null`) - the default value. In this mode only icons that are
         * specified in the tree data using ({@link Ext.data.NodeInterface#icon icon}
         * or {@link Ext.data.NodeInterface#iconCls iconCls} will be displayed, but the
         * default "folder" and "leaf" icons will not be displayed.
         *
         * 2. `true` - Icons specified in the tree data will be displayed, and the default
         * "folder" and "leaf" icons will be displayed for nodes that do not specify
         * an `icon` or `iconCls`.
         *
         * 3. `false` - No icons will be displayed in the breadcrumb buttons, only text.
         */
        showIcons: null,
        /**
         * @cfg {Boolean} showMenuIcons
         *
         * Controls whether or not icons of tree nodes are displayed in the breadcrumb
         * menu items. There are 3 possible values for this config:
         *
         * 1. unspecified (`null`) - the default value. In this mode only icons that are
         * specified in the tree data using ({@link Ext.data.NodeInterface#icon icon}
         * or {@link Ext.data.NodeInterface#iconCls iconCls} will be displayed, but the
         * default "folder" and "leaf" icons will not be displayed.
         *
         * 2. `true` - Icons specified in the tree data will be displayed, and the default
         * "folder" and "leaf" icons will be displayed for nodes that do not specify
         * an `icon` or `iconCls`.
         *
         * 3. `false` - No icons will be displayed on the breadcrumb menu items, only text.
         */
        showMenuIcons: null,
        /**
         * @cfg {Ext.data.TreeStore} store
         * The TreeStore that this breadcrumb toolbar should use as its data source
         */
        store: null,
        /**
         * @cfg {Boolean} useSplitButtons
         * `false` to use regular {@link Ext.button.Button Button}s instead of {@link
         * Ext.button.Split Split Buttons}.  When `true`, a click on the body of a button
         * will navigate to the specified node, and a click on the arrow will show a menu
         * containing the the child nodes.  When `false`, the only mode of navigation is
         * the menu, since a click anywhere on the button will show the menu.
         */
        useSplitButtons: true
    },
    renderConfig: {
        /**
         * @cfg {Ext.data.TreeModel/String} selection
         * The selected node, or `"root"` to select the root node
         * @accessor
         */
        selection: 'root'
    },
    /**
     * @cfg publishes
     * @inheritdoc
     */
    publishes: [
        'selection'
    ],
    /**
     * @cfg twoWayBindable
     * @inheritdoc
     */
    twoWayBindable: [
        'selection'
    ],
    _breadcrumbCls: Ext.baseCSSPrefix + 'breadcrumb',
    _btnCls: Ext.baseCSSPrefix + 'breadcrumb-btn',
    _folderIconCls: Ext.baseCSSPrefix + 'breadcrumb-icon-folder',
    _leafIconCls: Ext.baseCSSPrefix + 'breadcrumb-icon-leaf',
    /**
     * @cfg focusableContainer
     * @inheritdoc
     */
    focusableContainer: true,
    initComponent: function() {
        var me = this,
            layout = me.layout,
            overflowHandler = me.getOverflowHandler();
        if (typeof layout === 'string') {
            layout = {
                type: layout
            };
        }
        if (overflowHandler) {
            layout.overflowHandler = overflowHandler;
        }
        me.layout = layout;
        // set defaultButtonUI for possible menu overflow handler.
        me.defaultButtonUI = me.getButtonUI();
        /**
         * Internal cache of buttons that are re-purposed as the items of this container
         * as navigation occurs.
         * @property {Ext.button.Split[]} buttons
         * @private
         */
        me._buttons = [];
        me.addCls([
            me._breadcrumbCls,
            me._breadcrumbCls + '-' + me.ui
        ]);
        me.callParent();
    },
    doDestroy: function() {
        Ext.destroy(this._buttons);
        this.setStore(null);
        this.callParent();
    },
    onRemove: function(component, destroying) {
        this.callParent([
            component,
            destroying
        ]);
        delete component._breadcrumbNodeId;
    },
    afterComponentLayout: function() {
        var me = this,
            overflowHandler = me.layout.overflowHandler;
        me.callParent(arguments);
        if (overflowHandler && me.tooNarrow && overflowHandler.scrollToItem) {
            overflowHandler.scrollToItem(me.getSelection().get('depth'));
        }
    },
    /**
     * @method getSelection
     * Returns the currently selected {@link Ext.data.TreeModel node}.
     * @return {Ext.data.TreeModel} node The selected node (or null if there is no
     * selection).
     */
    /**
     * @method setSelection
     * Selects the passed {@link Ext.data.TreeModel node} in the breadcrumb component.
     * @param {Ext.data.TreeModel} node The node in the breadcrumb {@link #store} to
     * select as the active node.
     * @return {Ext.toolbar.Breadcrumb} this The breadcrumb component
     */
    applySelection: function(node) {
        var store = this.getStore();
        if (store) {
            node = (node === 'root') ? this.getStore().getRoot() : node;
        } else {
            node = null;
        }
        return node;
    },
    updateSelection: function(node, prevNode) {
        var me = this,
            buttons = me._buttons,
            items = [],
            itemCount = Ext.ComponentQuery.query('[isCrumb]', me.getRefItems()).length,
            needsSync = me._needsSync,
            displayField = me.getDisplayField(),
            showIcons, glyph, iconCls, icon, newItemCount, currentNode, text, button, id, depth, i;
        Ext.suspendLayouts();
        if (node) {
            currentNode = node;
            depth = node.get('depth');
            newItemCount = depth + 1;
            i = depth;
            while (currentNode) {
                id = currentNode.getId();
                button = buttons[i];
                if (!needsSync && button && button._breadcrumbNodeId === id) {
                    // reached a level in the hierarchy where we are already in sync.
                    break;
                }
                text = currentNode.get(displayField);
                if (button) {
                    // If we already have a button for this depth in the button cache reuse it
                    button.setText(text);
                } else {
                    // no button in the cache - make one and add it to the cache
                    button = buttons[i] = Ext.create({
                        isCrumb: true,
                        xtype: me.getUseSplitButtons() ? 'splitbutton' : 'button',
                        ui: me.getButtonUI(),
                        componentCls: me._btnCls + ' ' + me._btnCls + '-' + me.ui,
                        separateArrowStyling: false,
                        text: text,
                        showEmptyMenu: true,
                        // begin with an empty menu - items are populated on beforeshow
                        menu: {
                            listeners: {
                                click: '_onMenuClick',
                                beforeshow: '_onMenuBeforeShow',
                                scope: this
                            }
                        },
                        handler: '_onButtonClick',
                        scope: me
                    });
                }
                showIcons = this.getShowIcons();
                if (showIcons !== false) {
                    glyph = currentNode.get('glyph');
                    icon = currentNode.get('icon');
                    iconCls = currentNode.get('iconCls');
                    if (glyph) {
                        button.setGlyph(glyph);
                        button.setIcon(null);
                        button.setIconCls(iconCls);
                    }
                    // may need css to get glyph
                    else if (icon) {
                        button.setGlyph(null);
                        button.setIconCls(null);
                        button.setIcon(icon);
                    } else if (iconCls) {
                        button.setGlyph(null);
                        button.setIcon(null);
                        button.setIconCls(iconCls);
                    } else if (showIcons) {
                        // only show default icons if showIcons === true
                        button.setGlyph(null);
                        button.setIcon(null);
                        button.setIconCls((currentNode.isLeaf() ? me._leafIconCls : me._folderIconCls) + '-' + me.ui);
                    } else {
                        // if showIcons is null do not show default icons
                        button.setGlyph(null);
                        button.setIcon(null);
                        button.setIconCls(null);
                    }
                }
                button.setArrowVisible(currentNode.hasChildNodes());
                button._breadcrumbNodeId = currentNode.getId();
                currentNode = currentNode.parentNode;
                i--;
            }
            if (newItemCount > itemCount) {
                // new selection has more buttons than existing selection, add the new buttons
                items = buttons.slice(itemCount, depth + 1);
                me.add(items);
            } else {
                // new selection has fewer buttons, remove the extra ones from the items, but
                // do not destroy them, as they are returned to the cache and recycled.
                for (i = itemCount - 1; i >= newItemCount; i--) {
                    me.remove(buttons[i], false);
                }
            }
        } else {
            // null selection
            for (i = 0; i < buttons.length; i++) {
                me.remove(buttons[i], false);
            }
        }
        Ext.resumeLayouts(true);
        /**
         * @event selectionchange
         * Fires when the selected node changes. At render time, this event will fire
         * indicating that the configured {@link #selection} has been selected.
         * @param {Ext.toolbar.Breadcrumb} this
         * @param {Ext.data.TreeModel} node The selected node.
         * @param {Ext.data.TreeModel} prevNode The previously selected node.
         */
        me.fireEvent('selectionchange', me, node, prevNode);
        if (me._shouldFireChangeEvent) {
            /**
             * @event change
             * Fires when the user changes the selected record. In contrast to the
             * {@link #selectionchange} event, this does *not* fire at render time,
             * only in response to user activity.
             * @param {Ext.toolbar.Breadcrumb} this
             * @param {Ext.data.TreeModel} node The selected node.
             * @param {Ext.data.TreeModel} prevNode The previously selected node.
             */
            me.fireEvent('change', me, node, prevNode);
        }
        me._shouldFireChangeEvent = true;
        me._needsSync = false;
    },
    applyUseSplitButtons: function(useSplitButtons, oldUseSplitButtons) {
        if (this.rendered && useSplitButtons !== oldUseSplitButtons) {
            Ext.raise("Cannot reconfigure 'useSplitButtons' config of Ext.toolbar.Breadcrumb " + "after initial render");
        }
        return useSplitButtons;
    },
    applyStore: function(store) {
        if (store) {
            store = Ext.data.StoreManager.lookup(store);
        }
        return store;
    },
    updateStore: function(store, oldStore) {
        this._needsSync = true;
        if (store && !this.isConfiguring) {
            this.setSelection(store.getRoot());
        }
    },
    updateOverflowHandler: function(overflowHandler) {
        if (overflowHandler === 'menu') {
            Ext.raise("Using Menu overflow with breadcrumb is not currently supported.");
        }
    },
    privates: {
        /**
         * Handles a click on a breadcrumb button
         * @private
         * @param {Ext.button.Split} button
         * @param {Ext.event.Event} e
         */
        _onButtonClick: function(button, e) {
            if (this.getUseSplitButtons()) {
                this.setSelection(this.getStore().getNodeById(button._breadcrumbNodeId));
            }
        },
        /**
         * Handles a click on a button menu
         * @private
         * @param {Ext.menu.Menu} menu
         * @param {Ext.menu.Item} item
         * @param {Ext.event.Event} e
         */
        _onMenuClick: function(menu, item, e) {
            if (item) {
                // Find the TreeStore node corresponding to the menu item
                item = this.getStore().getNodeById(item._breadcrumbNodeId);
                this.setSelection(item);
                // Find the button that has just been shown and focus it.
                item = this._buttons[item.getDepth()];
                if (item) {
                    item.focus();
                }
            }
        },
        /**
         * Handles the `beforeshow` event of a button menu
         * @private
         * @param {Ext.menu.Menu} menu
         */
        _onMenuBeforeShow: function(menu) {
            var me = this,
                node = me.getStore().getNodeById(menu.ownerCmp._breadcrumbNodeId),
                displayField = me.getDisplayField(),
                showMenuIcons = me.getShowMenuIcons(),
                childNodes, child, glyph, items, i, icon, iconCls, ln, item;
            if (node.hasChildNodes()) {
                childNodes = node.childNodes;
                items = [];
                for (i = 0 , ln = childNodes.length; i < ln; i++) {
                    child = childNodes[i];
                    item = {
                        text: child.get(displayField),
                        _breadcrumbNodeId: child.getId()
                    };
                    if (showMenuIcons !== false) {
                        glyph = child.get('glyph');
                        icon = child.get('icon');
                        iconCls = child.get('iconCls');
                        if (glyph) {
                            item.glyph = glyph;
                            item.iconCls = iconCls;
                        }
                        // may need css to get glyph
                        else if (icon) {
                            item.icon = icon;
                        } else if (iconCls) {
                            item.iconCls = iconCls;
                        } else if (showMenuIcons) {
                            // only show default icons if showIcons === true
                            item.iconCls = (child.isLeaf() ? me._leafIconCls : me._folderIconCls) + '-' + me.ui;
                        }
                    }
                    items.push(item);
                }
                menu.removeAll();
                menu.add(items);
            } else {
                // prevent menu from being shown for nodes with no children
                return false;
            }
        }
    }
});

/**
 * A non-rendering placeholder item which instructs the Toolbar's Layout to begin using
 * the right-justified button container.
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *          title: 'Toolbar Fill Example',
 *          width: 300,
 *          height: 200,
 *          tbar : [
 *              'Item 1',
 *              { xtype: 'tbfill' },
 *              'Item 2'
 *          ],
 *          renderTo: Ext.getBody()
 *      });
 */
Ext.define('Ext.toolbar.Fill', {
    extend: 'Ext.Component',
    alias: 'widget.tbfill',
    alternateClassName: 'Ext.Toolbar.Fill',
    // Toolbar required here because we'll try to decorate it's alternateClassName
    // with this class' alternate name
    requires: [
        'Ext.toolbar.Toolbar'
    ],
    ariaRole: 'presentation',
    /**
     * @property {Boolean} isFill
     * `true` in this class to identify an object as an instantiated Fill, or subclass thereof.
     */
    isFill: true,
    flex: 1
});

/**
 * A simple element that adds extra horizontal space between items in a toolbar.
 * By default a 2px wide space is added via CSS specification:
 *
 *     .x-toolbar .x-toolbar-spacer {
 *         width: 2px;
 *     }
 *
 * Example:
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *         title: 'Toolbar Spacer Example',
 *         width: 300,
 *         height: 200,
 *         tbar : [
 *             'Item 1',
 *             { xtype: 'tbspacer' }, // or ' '
 *             'Item 2',
 *             // space width is also configurable via javascript
 *             { xtype: 'tbspacer', width: 50 }, // add a 50px space
 *             'Item 3'
 *         ],
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.toolbar.Spacer', {
    extend: 'Ext.Component',
    alias: 'widget.tbspacer',
    alternateClassName: 'Ext.Toolbar.Spacer',
    // Toolbar required here because we'll try to decorate it's alternateClassName
    // with this class' alternate name
    requires: [
        'Ext.toolbar.Toolbar'
    ],
    baseCls: Ext.baseCSSPrefix + 'toolbar-spacer',
    ariaRole: 'presentation'
});

/**
 * Provides indentation and folder structure markup for a Tree taking into account
 * depth and position within the tree hierarchy.
 */
Ext.define('Ext.tree.Column', {
    extend: 'Ext.grid.column.Column',
    alias: 'widget.treecolumn',
    tdCls: Ext.baseCSSPrefix + 'grid-cell-treecolumn',
    autoLock: true,
    lockable: false,
    draggable: false,
    hideable: false,
    iconCls: Ext.baseCSSPrefix + 'tree-icon',
    checkboxCls: Ext.baseCSSPrefix + 'tree-checkbox',
    elbowCls: Ext.baseCSSPrefix + 'tree-elbow',
    expanderCls: Ext.baseCSSPrefix + 'tree-expander',
    textCls: Ext.baseCSSPrefix + 'tree-node-text',
    innerCls: Ext.baseCSSPrefix + 'grid-cell-inner-treecolumn',
    customIconCls: Ext.baseCSSPrefix + 'tree-icon-custom',
    isTreeColumn: true,
    /**
     * @cfg {Function/String} renderer
     * A renderer is an 'interceptor' method which can be used to transform data (value, 
     * appearance, etc.) before it is rendered. Note that a *tree column* renderer yields
     * the *text* of the node. The lines and icons are produced by configurations. See
     * below for more information.
     * 
     * **NOTE:** In previous releases, a string was treated as a method on 
     * `Ext.util.Format` but that is now handled by the {@link #formatter} config.
     *
     * @param {Object} value The data value for the current cell
     * 
     *     renderer: function(value){
     *         // evaluates `value` to append either `person' or `people`
     *         return Ext.util.Format.plural(value, 'person', 'people');
     *     }
     * 
     * @param {Object} metaData A collection of metadata about the current cell; can be 
     * used or modified by the renderer. Recognized properties are: `tdCls`, `tdAttr`, 
     * `tdStyle`, `icon`, `iconCls` and `glyph`.
     *
     * For the standard grid column metaData propertyes see
     * {@link Ext.grid.column.Column#cfg-renderer column renderer}
     *
     * To change the icon used in the node, you can use the glyph metaData property as below. 
     *
     * You can see an example of using the metaData parameter below.
     *
     *      renderer: function(v, metaData, record) {
     *          metaData.glyph = record.glyph;
     *          return v;
     *      }
     *
     * or
     *
     *      renderer: function(v, metaData, record) {
     *          metaData.icon = record.icon;
     *          return v;
     *      }
     *
     * @param {Ext.data.Model} record The record for the current row
     *
     *     renderer: function (value, metaData, record) {
     *         // evaluate the record's `updated` field and if truthy return the value 
     *         // from the `newVal` field, else return value
     *         var updated = record.get('updated');
     *         return updated ? record.get('newVal') : value;
     *     }
     * 
     * @param {Number} rowIndex The index of the current row
     * 
     *     renderer: function (value, metaData, record, rowIndex) {
     *         // style the cell differently for even / odd values
     *         var odd = (rowIndex % 2 === 0);
     *         metaData.tdStyle = 'color:' + (odd ? 'gray' : 'red');
     *     }
     * 
     * @param {Number} colIndex The index of the current column
     * 
     *     var myRenderer = function(value, metaData, record, rowIndex, colIndex) {
     *         if (colIndex === 0) {
     *             metaData.tdAttr = 'data-qtip=' + value;
     *         }
     *         // additional logic to apply to values in all columns
     *         return value;
     *     }
     *     
     *     // using the same renderer on all columns you can process the value for
     *     // each column with the same logic and only set a tooltip on the first column
     *     renderer: myRenderer
     * 
     * _See also {@link Ext.tip.QuickTipManager}_
     * 
     * @param {Ext.data.Store} store The data store
     * 
     *     renderer: function (value, metaData, record, rowIndex, colIndex, store) {
     *         // style the cell differently depending on how the value relates to the 
     *         // average of all values
     *         var average = store.average('grades');
     *         metaData.tdCls = (value < average) ? 'needsImprovement' : 'satisfactory';
     *         return value;
     *     }
     * 
     * @param {Ext.view.View} view The data view
     * 
     *     renderer: function (value, metaData, record, rowIndex, colIndex, store, view) {
     *         // style the cell using the dataIndex of the column
     *         var headerCt = this.getHeaderContainer(),
     *             column = headerCt.getHeaderAtIndex(colIndex);
     * 
     *         metaData.tdCls = 'app-' + column.dataIndex;
     *         return value;
     *     }
     * 
     * @return {String}
     * The HTML string to be rendered into the text portion of the tree node.
     */
    /* eslint-disable indent, max-len */
    cellTpl: [
        '<tpl for="lines">',
        '<div class="{parent.childCls} {parent.elbowCls}-img ',
        '{parent.elbowCls}-<tpl if=".">line<tpl else>empty</tpl>" role="presentation"></div>',
        '</tpl>',
        '<div class="{childCls} {elbowCls}-img {elbowCls}',
        '<tpl if="isLast">-end</tpl><tpl if="expandable">-plus {expanderCls}</tpl>" role="presentation"></div>',
        '<tpl if="checked !== null">',
        '<div role="button" {ariaCellCheckboxAttr}',
        ' class="{childCls} {checkboxCls}<tpl if="checked"> {checkboxCls}-checked</tpl>"></div>',
        '</tpl>',
        '<tpl if="glyph">',
        '<span class="{baseIconCls}" ',
        '<tpl if="glyphFontFamily">',
        'style="font-family:{glyphFontFamily}"',
        '</tpl>',
        '>{glyph}</span>',
        '<tpl else>',
        '<tpl if="icon">',
        '<img src="{blankUrl}"',
        '<tpl else>',
        '<div',
        '</tpl>',
        ' role="presentation" class="{childCls} {baseIconCls} {customIconCls} ',
        '{baseIconCls}-<tpl if="leaf">leaf<tpl else><tpl if="expanded">parent-expanded<tpl else>parent</tpl></tpl> {iconCls}" ',
        '<tpl if="icon">style="background-image:url({icon})"/><tpl else>></div></tpl>',
        '</tpl>',
        '<tpl if="href">',
        '<a href="{href}" role="link" target="{hrefTarget}" class="{textCls} {childCls}">{value}</a>',
        '<tpl else>',
        '<span class="{textCls} {childCls}">{value}</span>',
        '</tpl>'
    ],
    /* eslint-enable indent, max-len */
    // fields that will trigger a change in the ui that aren't likely to be bound to a column
    uiFields: {
        checked: 1,
        icon: 1,
        iconCls: 1
    },
    // fields that requires a full row render
    rowFields: {
        expanded: 1,
        loaded: 1,
        expandable: 1,
        leaf: 1,
        loading: 1,
        qtip: 1,
        qtitle: 1,
        cls: 1
    },
    initComponent: function() {
        var me = this;
        me.rendererScope = me.scope;
        me.setupRenderer();
        // This always uses its own renderer.
        // Any custom renderer is used as an inner renderer to produce the text node of a tree cell.
        me.innerRenderer = me.renderer;
        me.renderer = me.treeRenderer;
        me.callParent();
        me.scope = me;
    },
    treeRenderer: function(value, metaData, record, rowIdx, colIdx, store, view) {
        var me = this,
            cls = record.get('cls'),
            rendererData;
        // The initial render will inject the cls into the TD's attributes.
        // If cls is ever *changed*, then the full rendering path is followed.
        if (metaData && cls) {
            metaData.tdCls += ' ' + cls;
        }
        rendererData = me.initTemplateRendererData(value, metaData, record, rowIdx, colIdx, store, view);
        return me.lookupTpl('cellTpl').apply(rendererData);
    },
    initTemplateRendererData: function(value, metaData, record, rowIdx, colIdx, store, view) {
        var me = this,
            innerRenderer = me.innerRenderer,
            data = record.data,
            parent = record.parentNode,
            rootVisible = view.rootVisible,
            lines = [],
            parentData, glyph, glyphFontFamily;
        while (parent && (rootVisible || parent.data.depth > 0)) {
            parentData = parent.data;
            lines[rootVisible ? parentData.depth : parentData.depth - 1] = parent.isLastVisible() ? 0 : 1;
            parent = parent.parentNode;
        }
        // Clear down metadata properties which may be set by the user renderer
        // because we apply them if we find them set, and the metedata property
        // is a static object owned by the View.
        // metaData may not be present if the column is being rendered through a
        // default renderer with no extra params.
        if (metaData) {
            metaData.iconCls = metaData.icon = metaData.glyph = null;
        } else {
            metaData = {};
        }
        // Call renderer now so that we can use metaData properties that it may set.
        value = innerRenderer ? innerRenderer.apply(me.rendererScope, arguments) : value;
        // If a glyph was specified, then
        // transform glyph to the useful parts
        glyph = metaData.glyph || data.glyph;
        if (glyph) {
            glyph = Ext.Glyph.fly(glyph);
            glyphFontFamily = glyph.fontFamily;
            glyph = glyph.character;
        }
        return {
            record: record,
            baseIconCls: me.iconCls,
            customIconCls: (data.icon || data.iconCls) ? me.customIconCls : '',
            glyph: glyph,
            glyphFontFamily: glyphFontFamily,
            iconCls: metaData.iconCls || data.iconCls,
            icon: metaData.icon || data.icon,
            checkboxCls: me.checkboxCls,
            checked: data.checked,
            elbowCls: me.elbowCls,
            expanderCls: me.expanderCls,
            textCls: me.textCls,
            leaf: data.leaf,
            expandable: record.isExpandable(),
            expanded: data.expanded,
            isLast: record.isLastVisible(),
            blankUrl: Ext.BLANK_IMAGE_URL,
            href: data.href,
            hrefTarget: data.hrefTarget,
            lines: lines,
            metaData: metaData,
            // subclasses or overrides can implement a getChildCls() method, which can
            // return an extra class to add to all of the cell's child elements (icon,
            // expander, elbow, checkbox).  This is used by the rtl override to add the
            // "x-rtl" class to these elements.
            childCls: me.getChildCls ? me.getChildCls() + ' ' : '',
            value: value || store.defaultRootText
        };
    },
    shouldUpdateCell: function(record, changedFieldNames) {
        // For the TreeColumn, if any of the known tree column UI affecting fields are updated
        // the cell should be updated in whatever way.
        // 1 if a custom renderer (not our default tree cell renderer), else 2.
        var me = this,
            i = 0,
            len, field;
        // If the column has a renderer which peeks and pokes at other data,
        // return 1 which means that a whole new TableView item must be rendered.
        if (me.hasCustomRenderer) {
            return 1;
        }
        if (changedFieldNames) {
            len = changedFieldNames.length;
            for (; i < len; ++i) {
                field = changedFieldNames[i];
                // Check for fields which always require a full row update.
                if (me.rowFields[field]) {
                    return 1;
                }
                // Check for fields which require this column to be updated.
                // The TreeColumn's treeRenderer is not a custom renderer.
                if (me.uiFields[field]) {
                    return 2;
                }
            }
        }
        return me.callParent([
            record,
            changedFieldNames
        ]);
    },
    privates: {
        shouldFlagCustomRenderer: function() {
            return this.hasCustomRenderer || (this.innerRenderer && this.innerRenderer.length > 1);
        }
    }
});

/**
 * @class Ext.tree.NavigationModel
 * @private
 * This class listens for key events fired from a {@link Ext.tree.Panel TreePanel},
 * and moves the currently focused item by adding the class {@link #focusCls}.
 *
 * Navigation and interactions are defined by http://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#TreeView
 * or, if there are multiple visible columns, by http://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#treegrid
 */
Ext.define('Ext.tree.NavigationModel', {
    extend: 'Ext.grid.NavigationModel',
    alias: 'view.navigation.tree',
    initKeyNav: function(view) {
        var me = this,
            columns = me.view.ownerGrid.columns;
        // Must go up to any possible locking assembly to find total number of columns
        me.isTreeGrid = columns && columns.length > 1;
        me.callParent([
            view
        ]);
        me.view.grid.on({
            columnschanged: me.onColumnsChanged,
            scope: me
        });
    },
    onKeyNavCreate: function(keyNav) {
        var fn = this.onAsterisk;
        keyNav.map.addBinding([
            {
                key: '8',
                shift: true,
                handler: fn,
                scope: this
            },
            {
                key: Ext.event.Event.NUM_MULTIPLY,
                handler: fn,
                scope: this
            }
        ]);
    },
    onColumnsChanged: function() {
        // Must go up to any possible locking assembly to find total number of columns
        this.isTreeGrid = this.view.ownerGrid.getVisibleColumnManager().getColumns().length > 1;
    },
    onCellClick: function(view, cell, cellIndex, record, row, recordIndex, clickEvent) {
        this.callParent([
            view,
            cell,
            cellIndex,
            record,
            row,
            recordIndex,
            clickEvent
        ]);
        // Return false if node toggled.
        // Do not process the cell click further when we do an expand/collapse
        return !clickEvent.nodeToggled;
    },
    onKeyLeft: function(keyEvent) {
        var me = this,
            view = keyEvent.view,
            record = me.record;
        // Left when a TreeGrid navigates between columns
        if (me.isTreeGrid && !keyEvent.ctrlKey) {
            return me.callParent([
                keyEvent
            ]);
        }
        // Left arrow key on an expanded node closes the node.
        if (keyEvent.position.column.isTreeColumn && record.isExpanded()) {
            view.collapse(record);
        } else // Left arrow key on a closed or end node moves focus to the node's parent
        // (don't attempt to focus hidden root).
        {
            record = record.parentNode;
            if (record && !(record.isRoot() && !view.rootVisible)) {
                me.setPosition(record, null, keyEvent);
            }
        }
    },
    onKeyRight: function(keyEvent) {
        var me = this,
            record = me.record;
        // Right when a TreeGrid navigates between columns
        if (me.isTreeGrid && !keyEvent.ctrlKey) {
            return me.callParent([
                keyEvent
            ]);
        }
        // Right arrow key expands a closed node, moves to the first child of an open node,
        // or does nothing on an end node.
        if (!record.isLeaf()) {
            if (keyEvent.position.column.isTreeColumn && !record.isExpanded()) {
                keyEvent.view.expand(record);
            } else if (record.isExpanded()) {
                record = record.childNodes[0];
                if (record) {
                    me.setPosition(record);
                }
            }
        }
    },
    onKeyEnter: function(keyEvent) {
        if (this.record.data.checked != null) {
            this.toggleCheck(keyEvent);
        } else {
            this.callParent([
                keyEvent
            ]);
        }
    },
    onKeySpace: function(keyEvent) {
        if (this.record.data.checked != null) {
            this.toggleCheck(keyEvent);
        } else {
            this.callParent([
                keyEvent
            ]);
        }
    },
    toggleCheck: function(keyEvent) {
        this.view.onCheckChange(keyEvent);
    },
    // (asterisk) on keypad expands all nodes.
    onAsterisk: function(keyEvent) {
        this.view.ownerGrid.expandAll();
    }
});

/**
 * Used as a view by {@link Ext.tree.Panel TreePanel}.
 */
Ext.define('Ext.tree.View', {
    extend: 'Ext.view.Table',
    alias: 'widget.treeview',
    config: {
        /**
         * @cfg selectionModel
         * @inheritdoc
         */
        selectionModel: {
            type: 'treemodel'
        }
    },
    /**
     * @property {Boolean} isTreeView
     * `true` in this class to identify an object as an instantiated TreeView, or subclass thereof.
     */
    isTreeView: true,
    /**
     * @cfg loadingCls
     * @inheritdoc
     */
    loadingCls: Ext.baseCSSPrefix + 'grid-tree-loading',
    expandedCls: Ext.baseCSSPrefix + 'grid-tree-node-expanded',
    leafCls: Ext.baseCSSPrefix + 'grid-tree-node-leaf',
    expanderSelector: '.' + Ext.baseCSSPrefix + 'tree-expander',
    checkboxSelector: '.' + Ext.baseCSSPrefix + 'tree-checkbox',
    expanderIconOverCls: Ext.baseCSSPrefix + 'tree-expander-over',
    // Class to add to the node wrap element used to hold nodes when a parent is being
    // collapsed or expanded. During the animation, UI interaction is forbidden by testing
    // for an ancestor node with this class.
    nodeAnimWrapCls: Ext.baseCSSPrefix + 'tree-animator-wrap',
    /**
     * @cfg loadMask
     * @inheritdoc
     */
    loadMask: false,
    /**
     * @cfg {Boolean} rootVisible
     * False to hide the root node.
     */
    rootVisible: true,
    /**
     * @cfg {Boolean} animate
     * True to enable animated expand/collapse (defaults to the value of
     * {@link Ext#enableFx Ext.enableFx})
     */
    expandDuration: 250,
    collapseDuration: 250,
    /**
     * @cfg {Boolean} toggleOnDblClick
     * True to toggle expand or collapse with a double click.
     */
    toggleOnDblClick: true,
    /**
     * @cfg stripeRows
     * @inheritdoc
     */
    stripeRows: false,
    /* eslint-disable indent */
    // treeRowTpl which is inserted into the rowTpl chain before the base rowTpl.
    // Sets tree-specific classes and attributes
    treeRowTpl: [
        '{%',
        'this.processRowValues(values);',
        'this.nextTpl.applyOut(values, out, parent);',
        '%}',
        {
            priority: 10,
            processRowValues: function(rowValues) {
                var record = rowValues.record,
                    view = rowValues.view;
                // We always need to set the qtip/qtitle, because they may have been
                // emptied, which means we still need to flush that change to the DOM
                // so the old values are overwritten
                rowValues.rowAttr['data-qtip'] = record.get('qtip') || '';
                rowValues.rowAttr['data-qtitle'] = record.get('qtitle') || '';
                // aria-level is 1-based
                rowValues.rowAttr['aria-level'] = record.getDepth() + 1;
                if (record.isLeaf()) {
                    rowValues.rowClasses.push(view.leafCls);
                } else {
                    if (record.isExpanded()) {
                        rowValues.rowClasses.push(view.expandedCls);
                        rowValues.rowAttr['aria-expanded'] = true;
                    } else {
                        rowValues.rowAttr['aria-expanded'] = false;
                    }
                }
                if (record.isLoading()) {
                    rowValues.rowClasses.push(view.loadingCls);
                }
            }
        }
    ],
    /* eslint-enable indent */
    /**
     * @event afteritemexpand
     * Fires after an item has been visually expanded and is visible in the tree.
     * @param {Ext.data.NodeInterface} node The node that was expanded
     * @param {Number} index The index of the node
     * @param {HTMLElement} item The HTML element for the node that was expanded
     */
    /**
     * @event afteritemcollapse
     * Fires after an item has been visually collapsed and is no longer visible in the tree.
     * @param {Ext.data.NodeInterface} node The node that was collapsed
     * @param {Number} index The index of the node
     * @param {HTMLElement} item The HTML element for the node that was collapsed
     */
    /**
     * @event nodedragover
     * Fires when a tree node is being targeted for a drag drop, return false to signal
     * drop not allowed.
     * @param {Ext.data.NodeInterface} targetNode The target node
     * @param {String} position The drop position, "before", "after" or "append",
     * @param {Object} dragData Data relating to the drag operation
     * @param {Ext.event.Event} e The event object for the drag
     */
    initComponent: function() {
        var me = this;
        if (me.bufferedRenderer) {
            me.animate = false;
        } else if (me.initialConfig.animate === undefined) {
            me.animate = Ext.enableFx;
        }
        me.store = me.panel.getStore();
        me.onRootChange(me.store.getRoot());
        me.animQueue = {};
        me.animWraps = {};
        me.callParent();
        me.store.setRootVisible(me.rootVisible);
        me.addRowTpl(me.lookupTpl('treeRowTpl'));
    },
    onFillComplete: function(treeStore, fillRoot, newNodes) {
        var me = this,
            store = me.store,
            start = store.indexOf(newNodes[0]);
        // Always update the current node, since the load may be triggered
        // by .load() directly instead of .expand() on the node
        fillRoot.triggerUIUpdate();
        // In the cases of expand, the records might not be in the store yet,
        // so jump out early and expand will handle it later
        if (!newNodes.length || start === -1) {
            return;
        }
        // Insert new nodes into the view
        me.onAdd(me.store, newNodes, start);
        me.refreshPartner();
    },
    refreshPartner: function() {
        var partner = this.lockingPartner;
        if (partner) {
            partner.refresh();
        }
    },
    afterRender: function() {
        var me = this;
        me.callParent();
        me.el.on({
            scope: me,
            delegate: me.expanderSelector,
            mouseover: me.onExpanderMouseOver,
            mouseout: me.onExpanderMouseOut
        });
    },
    processUIEvent: function(e) {
        // If the clicked node is part of an animation, ignore the click.
        // This is because during a collapse animation, the associated Records
        // will already have been removed from the Store, and the event is not processable.
        if (e.getTarget('.' + this.nodeAnimWrapCls, this.el)) {
            return false;
        }
        return this.callParent([
            e
        ]);
    },
    setRootNode: function(node) {
        this.node = node;
    },
    getChecked: function() {
        var checked = [];
        this.node.cascade(function(rec) {
            if (rec.get('checked')) {
                checked.push(rec);
            }
        });
        return checked;
    },
    isItemChecked: function(rec) {
        return rec.get('checked');
    },
    /**
     * @private
     */
    createAnimWrap: function(record, index) {
        var me = this,
            node = me.getNode(record),
            tmpEl;
        tmpEl = Ext.fly(node).insertSibling({
            role: 'presentation',
            tag: 'div',
            cls: me.nodeAnimWrapCls
        }, 'after');
        return {
            record: record,
            node: node,
            el: tmpEl,
            expanding: false,
            collapsing: false,
            animateEl: tmpEl,
            targetEl: tmpEl
        };
    },
    /**
     * @private
     * Returns the animation wrapper element for the specified parent node, used to wrap
     * the child nodes as they slide up or down during expand/collapse.
     *
     * @param parent The parent node to be expanded or collapsed
     *
     * @param [bubble=true] If the passed parent node does not already have a wrap element created,
     * by default this function will bubble up to each parent node looking for a valid wrap element
     * to reuse, returning the first one it finds. This is the appropriate behavior, e.g.,
     * for the collapse direction, so that the entire expanded set of branch nodes can collapse
     * as a single unit.
     *
     * However for expanding each parent node should instead always create its own animation wrap
     * if one doesn't exist, so that its children can expand independently of any other nodes --
     * this is crucial when executing the "expand all" behavior. If multiple nodes attempt to reuse
     * the same ancestor wrap element concurrently during expansion it will lead to problems
     * as the first animation to complete will delete the wrap el out from under other running
     * animations. For that reason, when expanding you should always pass `bubble: false`
     * to be on the safe side.
     *
     * If the passed parent has no wrap (or there is no valid ancestor wrap after bubbling),
     * this function will return null and the calling code should then call {@link #createAnimWrap}
     * if needed.
     *
     * @return {Ext.dom.Element} The wrapping element as created in {@link #createAnimWrap}, or null
     */
    getAnimWrap: function(parent, bubble) {
        if (!this.animate) {
            return null;
        }
        // eslint-disable-next-line vars-on-top
        var wraps = this.animWraps,
            wrap = wraps[parent.internalId];
        if (bubble !== false) {
            while (!wrap && parent) {
                parent = parent.parentNode;
                if (parent) {
                    wrap = wraps[parent.internalId];
                }
            }
        }
        return wrap;
    },
    doAdd: function(records, index) {
        var me = this,
            record = records[0],
            parent = record.parentNode,
            all = me.all,
            relativeIndex,
            animWrap = me.getAnimWrap(parent),
            targetEl, childNodes, len, result, children;
        if (!animWrap || !animWrap.expanding) {
            return me.callParent([
                records,
                index
            ]);
        }
        // If we are adding records which have a parent that is currently expanding
        // lets add them to the animation wrap
        result = me.bufferRender(records, index, true);
        children = result.children;
        // We need the parent that has the animWrap, not the node's parent
        parent = animWrap.record;
        // If there is an anim wrap we do our special magic logic
        targetEl = animWrap.targetEl;
        childNodes = targetEl.dom.childNodes;
        len = childNodes.length;
        // The relative index is the index in the full flat collection
        // minus the index of the wraps parent
        relativeIndex = index - me.indexInStore(parent) - 1;
        // If we are adding records to the wrap that have a higher relative index
        // then there are currently children
        // it means we have to append the nodes to the wrap
        if (!len || relativeIndex >= len) {
            targetEl.appendChild(result.fragment, true);
        } else // If there are already more children then the relative index it means we are adding
        // child nodes of some expanded node in the anim wrap. In this case we have to insert
        // the nodes in the right location
        {
            Ext.fly(childNodes[relativeIndex]).insertSibling(children, 'before', true);
        }
        // We also have to update the node cache of the DataView
        all.insert(index, children);
        return children;
    },
    onRemove: function(ds, records, index) {
        var me = this,
            empty, i, oldItems;
        if (me.viewReady) {
            empty = me.store.getCount() === 0;
            // If buffered rendering is being used, call the parent class.
            if (me.bufferedRenderer) {
                return me.callParent([
                    ds,
                    records,
                    index
                ]);
            }
            oldItems = this.all.slice(index, index + records.length);
            // Nothing left, just refresh the view.
            if (empty) {
                me.refresh();
            } else {
                // Remove in reverse order so that indices remain correct
                for (i = records.length - 1 , index += i; i >= 0; --i , --index) {
                    me.doRemove(records[i], index);
                }
                me.refreshSizePending = true;
            }
            me.fireItemMutationEvent('itemremove', records, index, oldItems, me);
        }
    },
    doRemove: function(record, index) {
        // If we are adding records which have a parent that is currently expanding
        // lets add them to the animation wrap
        var me = this,
            all = me.all,
            animWrap = me.getAnimWrap(record),
            item = all.item(index),
            node = item ? item.dom : null;
        if (!node || !animWrap || !animWrap.collapsing) {
            return me.callParent([
                record,
                index
            ]);
        }
        // Insert the item at the beginning of the animate el - child nodes are removed
        // in reverse order so that the index can be used.
        animWrap.targetEl.dom.insertBefore(node, animWrap.targetEl.dom.firstChild);
        all.removeElement(index);
    },
    onBeforeExpand: function(parent, records, index) {
        var me = this,
            animWrap;
        if (me.rendered && me.all.getCount() && me.animate) {
            if (me.getNode(parent)) {
                animWrap = me.getAnimWrap(parent, false);
                if (!animWrap) {
                    animWrap = me.animWraps[parent.internalId] = me.createAnimWrap(parent);
                    animWrap.animateEl.setHeight(0);
                } else if (animWrap.collapsing) {
                    // If we expand this node while it is still expanding then we
                    // have to remove the nodes from the animWrap.
                    animWrap.targetEl.select(me.itemSelector).destroy();
                }
                animWrap.expanding = true;
                animWrap.collapsing = false;
            }
        }
    },
    onExpand: function(parent) {
        var me = this,
            queue = me.animQueue,
            id = parent.getId(),
            node = me.getNode(parent),
            index = node ? me.indexOf(node) : -1,
            animWrap, animateEl, targetEl;
        if (me.singleExpand) {
            me.ensureSingleExpand(parent);
        }
        // The item is not visible yet
        if (index === -1) {
            return;
        }
        animWrap = me.getAnimWrap(parent, false);
        if (!animWrap) {
            parent.isExpandingOrCollapsing = false;
            me.fireEvent('afteritemexpand', parent, index, node);
            return;
        }
        animateEl = animWrap.animateEl;
        targetEl = animWrap.targetEl;
        animateEl.stopAnimation();
        queue[id] = true;
        // Must set element height before this event finishes because animation does not set
        // initial condition until first tick has elapsed.
        // Which is good because the upcoming layout resumption must read the content height
        // BEFORE it gets squished.
        Ext.on('idle', function() {
            animateEl.dom.style.height = '0px';
        }, null, {
            single: true
        });
        animateEl.animate({
            from: {
                height: 0
            },
            to: {
                height: targetEl.dom.scrollHeight
            },
            duration: me.expandDuration,
            listeners: {
                afteranimate: function() {
                    // Move all the nodes out of the anim wrap to their proper location
                    // Must do this in afteranimate because lastframe does not fire if the
                    // animation is stopped.
                    var items = targetEl.dom.childNodes,
                        activeEl = Ext.Element.getActiveElement();
                    if (items.length) {
                        if (!targetEl.contains(activeEl)) {
                            activeEl = null;
                        }
                        animWrap.el.insertSibling(items, 'before', true);
                        if (activeEl) {
                            Ext.fly(activeEl).focus();
                        }
                    }
                    animWrap.el.destroy();
                    queue[id] = null;
                    if (!me.destroyed) {
                        me.animWraps[animWrap.record.internalId] = null;
                    }
                }
            },
            callback: function() {
                parent.isExpandingOrCollapsing = false;
                if (!me.destroyed) {
                    me.refreshSize(true);
                }
                me.fireEvent('afteritemexpand', parent, index, node);
            }
        });
    },
    // Triggered by the TreeStore's beforecollapse event.
    onBeforeCollapse: function(parent, records, index, callback, scope) {
        var me = this,
            animWrap;
        if (me.rendered && me.all.getCount()) {
            if (me.animate) {
                // Only process if the collapsing node is in the UI.
                // A node may be collapsed as part of a recursive ancestor collapse, and if it
                // has already been removed from the UI by virtue of an ancestor being collapsed,
                // we should not do anything.
                if (parent.getTreeStore().isVisible(parent)) {
                    animWrap = me.getAnimWrap(parent);
                    if (!animWrap) {
                        animWrap = me.animWraps[parent.internalId] = me.createAnimWrap(parent, index);
                    } else if (animWrap.expanding) {
                        // If we collapse this node while it is still expanding then we
                        // have to remove the nodes from the animWrap.
                        animWrap.targetEl.select(this.itemSelector).destroy();
                    }
                    animWrap.expanding = false;
                    animWrap.collapsing = true;
                    animWrap.callback = callback;
                    animWrap.scope = scope;
                }
            } else {
                // Cache any passed callback for use in the onCollapse post collapse handler
                // non-animated codepath
                me.onCollapseCallback = callback;
                me.onCollapseScope = scope;
            }
        }
    },
    onCollapse: function(parent) {
        var me = this,
            queue = me.animQueue,
            id = parent.getId(),
            node = me.getNode(parent),
            index = node ? me.indexOf(node) : -1,
            animWrap = me.getAnimWrap(parent),
            animateEl;
        // If the collapsed node is already removed from the UI
        // by virtue of being a descendant of a collapsed node, then
        // we have nothing to do here.
        if (!me.all.getCount() || !parent.isVisible()) {
            return;
        }
        // Not animating, all items will have been added, so updateLayout and resume layouts
        if (!animWrap) {
            parent.isExpandingOrCollapsing = false;
            me.fireEvent('afteritemcollapse', parent, index, node);
            // Call any collapse callback cached in the onBeforeCollapse handler
            Ext.callback(me.onCollapseCallback, me.onCollapseScope);
            me.onCollapseCallback = me.onCollapseScope = null;
            return;
        }
        animateEl = animWrap.animateEl;
        queue[id] = true;
        animateEl.stopAnimation();
        animateEl.animate({
            to: {
                height: 0
            },
            duration: me.collapseDuration,
            listeners: {
                afteranimate: function() {
                    // In case lastframe did not fire because the animation was stopped.
                    animWrap.el.destroy();
                    queue[id] = null;
                    if (!me.destroyed) {
                        me.animWraps[animWrap.record.internalId] = null;
                    }
                }
            },
            callback: function() {
                parent.isExpandingOrCollapsing = false;
                if (!me.destroyed) {
                    me.refreshSize(true);
                }
                me.fireEvent('afteritemcollapse', parent, index, node);
                // Call any collapse callback cached in the onBeforeCollapse handler
                Ext.callback(animWrap.callback, animWrap.scope);
                animWrap.callback = animWrap.scope = null;
            }
        });
    },
    /**
     * Checks if a node is currently undergoing animation
     * @private
     * @param {Ext.data.Model} node The node
     * @return {Boolean} True if the node is animating
     */
    isAnimating: function(node) {
        return !!this.animQueue[node.getId()];
    },
    /**
     * Expands a record that is loaded in the view.
     *
     * If an animated collapse or expand of the record is in progress, this call will be ignored.
     * @param {Ext.data.Model} record The record to expand
     * @param {Boolean} [deep] True to expand nodes all the way down the tree hierarchy.
     * @param {Function} [callback] The function to run after the expand is completed
     * @param {Object} [scope] The scope of the callback function.
     */
    expand: function(record, deep, callback, scope) {
        var me = this,
            doAnimate = !!me.animate,
            result;
        // Block toggling if we are already animating an expand or collapse operation.
        if (!doAnimate || !record.isExpandingOrCollapsing) {
            if (!record.isLeaf()) {
                record.isExpandingOrCollapsing = doAnimate;
            }
            // Need to suspend layouts because the expand process makes multiple changes to the UI
            // in addition to inserting new nodes. Folder and elbow images have to change, so we
            // need to coalesce all resulting layouts.
            Ext.suspendLayouts();
            result = record.expand(deep, callback, scope);
            Ext.resumeLayouts(true);
            return result;
        }
    },
    /**
     * Collapses a record that is loaded in the view.
     *
     * If an animated collapse or expand of the record is in progress, this call will be ignored.
     * @param {Ext.data.Model} record The record to collapse
     * @param {Boolean} [deep] True to collapse nodes all the way up the tree hierarchy.
     * @param {Function} [callback] The function to run after the collapse is completed
     * @param {Object} [scope] The scope of the callback function.
     */
    collapse: function(record, deep, callback, scope) {
        var me = this,
            doAnimate = !!me.animate;
        // Block toggling if we are already animating an expand or collapse operation.
        if (!doAnimate || !record.isExpandingOrCollapsing) {
            if (!record.isLeaf()) {
                record.isExpandingOrCollapsing = doAnimate;
            }
            return record.collapse(deep, callback, scope);
        }
    },
    /**
     * Toggles a record between expanded and collapsed.
     *
     * If an animated collapse or expand of the record is in progress, this call will be ignored.
     * @param {Ext.data.Model} record
     * @param {Boolean} [deep] True to collapse nodes all the way up the tree hierarchy.
     * @param {Function} [callback] The function to run after the expand/collapse is completed
     * @param {Object} [scope] The scope of the callback function.
     */
    toggle: function(record, deep, callback, scope) {
        if (record.isExpanded()) {
            this.collapse(record, deep, callback, scope);
        } else {
            this.expand(record, deep, callback, scope);
        }
    },
    onItemDblClick: function(record, item, index, e) {
        var me = this,
            editingPlugin = me.editingPlugin;
        me.callParent([
            record,
            item,
            index,
            e
        ]);
        if (me.toggleOnDblClick && record.isExpandable() && !(editingPlugin && editingPlugin.clicksToEdit === 2)) {
            me.toggle(record);
        }
    },
    onCellClick: function(cell, cellIndex, record, row, rowIndex, e) {
        var me = this,
            column = e.position.column;
        // We're only interested in clicks in the tree column
        if (column.isTreeColumn) {
            // Click on the checkbox and there is a defined data value; toggle it.
            if (e.getTarget(me.checkboxSelector, cell) && record.get('checked') != null) {
                me.onCheckChange(e);
                // Allow the stopSelection config on checkable tree columns to prevent selection
                if (column.stopSelection) {
                    e.stopSelection = true;
                }
            }
            // Click on the expander
            else if (e.getTarget(me.expanderSelector, cell) && record.isExpandable()) {
                // Ensure focus is on the clicked cell so that if this causes a refresh,
                // focus restoration does not scroll back to the previouslty focused position.
                // onCellClick is called *befor* cellclick is fired which is what changes
                // focus position.
                // TODO: connect directly from View's event processing to NavigationModel
                // without relying on events.
                me.getNavigationModel().setPosition(e.position);
                me.toggle(record, e.ctrlKey);
                // So that we know later to stop event propagation by returning false
                // from the NavigationModel
                // TODO: when NavigationModel is directly hooked up to be called *before*
                // the event sequence
                // This flag will not be necessary.
                e.nodeToggled = true;
            }
            return me.callParent([
                cell,
                cellIndex,
                record,
                row,
                rowIndex,
                e
            ]);
        }
    },
    onCheckChange: function(e) {
        var me = this,
            record = e.record,
            wasChecked = record.get('checked'),
            checked;
        // 1 means semi-checked.
        // Toggle of that state checks.
        if (wasChecked === 1) {
            checked = true;
        } else {
            checked = !wasChecked;
        }
        me.setChecked(record, checked, e);
    },
    setChecked: function(record, meChecked, e, options) {
        var me = this,
            checkPropagation = me.checkPropagationFlags[me.ownerGrid.checkPropagation.toLowerCase()],
            wasChecked = record.data.checked,
            halfCheckedValue = me.ownerGrid.triStateCheckbox ? 1 : false,
            progagateCheck = (!options || options.propagateCheck !== false) && (checkPropagation & 1),
            checkParent = (!options || options.checkParent !== false) && (checkPropagation & 2),
            matchedChildCount = 0,
            parentNode, parentChecked, foundCheck, foundClear, childNodes, len, i;
        if (me.fireEvent('beforecheckchange', record, wasChecked, e) === false) {
            return;
        }
        // Propagate full ->true and ->false changes to child nodes
        // unless we're being called from a setChecked on a child node.
        if (meChecked !== 1 && progagateCheck) {
            childNodes = record.childNodes;
            len = childNodes.length;
            for (i = 0; i < len; i++) {
                // We are setting child nodes, so pass the
                // checkParent flag as false to avoid reentry back into this node.
                me.setChecked(childNodes[i], meChecked, e, {
                    checkParent: false
                });
                if (childNodes[i].get('checked') === meChecked) {
                    matchedChildCount++;
                }
            }
            // If one or more of the child nodes refused
            if (matchedChildCount !== len) {
                meChecked = matchedChildCount ? halfCheckedValue : false;
            }
        }
        // If the new valud was not reset due to vetoing from
        // changes propagated to child nodes, then go ahead with the change.
        if (record.get('checked') !== meChecked) {
            record.set('checked', meChecked, options);
            // Fire checkchange now we know the valus has changed.
            me.fireEvent('checkchange', record, meChecked, e);
            // If there's a parent node, and the parent node has a checked data property
            // keep parent up to date with checkedness of its child nodes.
            if (checkParent && (parentNode = record.parentNode) && (parentChecked = parentNode.data.checked) != null) {
                childNodes = parentNode.childNodes;
                len = childNodes.length;
                // If we're semi checked, the parent is semi checked.
                if (meChecked === halfCheckedValue) {
                    parentChecked = halfCheckedValue;
                }
                // If we're the sole child, the parent is our state.
                else if (len === 1) {
                    parentChecked = meChecked;
                } else {
                    foundCheck = foundClear = false;
                    for (i = 0; !(foundCheck && foundClear) & i < len; i++) {
                        if (childNodes[i].data.checked === 1) {
                            foundCheck = foundClear = true;
                        } else if (!childNodes[i].data.checked) {
                            foundClear = true;
                        } else {
                            foundCheck = true;
                        }
                    }
                    parentChecked = foundCheck && foundClear ? halfCheckedValue : (foundCheck ? true : false);
                }
                if (parentNode.get('checked') !== parentChecked) {
                    // We are setting the parent node, so pass the
                    // progagateCheck flag as false to avoid reentry back into this node.
                    me.setChecked(parentNode, parentChecked, e, {
                        propagateCheck: false
                    });
                }
            }
        }
    },
    onExpanderMouseOver: function(e) {
        Ext.fly(e.getTarget(this.cellSelector, 10)).addCls(this.expanderIconOverCls);
    },
    onExpanderMouseOut: function(e) {
        Ext.fly(e.getTarget(this.cellSelector, 10)).removeCls(this.expanderIconOverCls);
    },
    getStoreListeners: function() {
        return Ext.apply(this.callParent(), {
            rootchange: this.onRootChange,
            fillcomplete: this.onFillComplete
        });
    },
    onBindStore: function(store, initial, propName, oldStore) {
        var oldRoot = oldStore && oldStore.getRootNode(),
            newRoot = store && store.getRootNode();
        this.callParent([
            store,
            initial,
            propName,
            oldStore
        ]);
        // The root implicitly changes when reconfigured with a new store.
        // The store's own rootChange event when it initially sets its own rootNode
        // will not have reached us because it was not ourt store during its initialization.
        if (newRoot !== oldRoot) {
            this.onRootChange(newRoot, oldRoot);
        }
    },
    onRootChange: function(newRoot, oldRoot) {
        var me = this,
            grid = me.grid;
        if (oldRoot) {
            me.rootListeners.destroy();
            me.rootListeners = null;
        }
        if (newRoot) {
            me.rootListeners = newRoot.on({
                beforeexpand: me.onBeforeExpand,
                expand: me.onExpand,
                beforecollapse: me.onBeforeCollapse,
                collapse: me.onCollapse,
                destroyable: true,
                scope: me
            });
            grid.addRelayers(newRoot);
        }
    },
    ensureSingleExpand: function(node) {
        var parent = node.parentNode;
        if (parent) {
            parent.eachChild(function(child) {
                if (child !== node && child.isExpanded()) {
                    child.collapse();
                }
            });
        }
    },
    privates: {
        checkPropagationFlags: {
            none: 0,
            down: 1,
            up: 2,
            both: 3
        },
        deferRefreshForLoad: function(store) {
            var ret = this.callParent([
                    store
                ]),
                options, node;
            if (ret) {
                options = store.lastOptions;
                node = options && options.node;
                // If the root isn't loading, then proceed with the refresh, we'll
                // add the other nodes as they come in
                if (node && node !== store.getRoot()) {
                    ret = false;
                }
            }
            return ret;
        }
    }
});

/**
 * The TreePanel provides tree-structured UI representation of tree-structured data.
 * A TreePanel must be bound to a {@link Ext.data.TreeStore}.
 *
 * TreePanels support multiple columns through the {@link #columns} configuration.
 *
 * By default a TreePanel contains a single column which uses the `text` Field of
 * the store's nodes.
 *
 * Simple TreePanel using inline data:
 *
 *     @example
 *     var store = Ext.create('Ext.data.TreeStore', {
 *         root: {
 *             expanded: true,
 *             children: [
 *                 { text: 'detention', leaf: true },
 *                 { text: 'homework', expanded: true, children: [
 *                     { text: 'book report', leaf: true },
 *                     { text: 'algebra', leaf: true}
 *                 ] },
 *                 { text: 'buy lottery tickets', leaf: true }
 *             ]
 *         }
 *     });
 *
 *     Ext.create('Ext.tree.Panel', {
 *         title: 'Simple Tree',
 *         width: 200,
 *         height: 200,
 *         store: store,
 *         rootVisible: false,
 *         renderTo: Ext.getBody()
 *     });
 *
 * For the tree node config options (like `text`, `leaf`, `expanded`), see the documentation of
 * {@link Ext.data.NodeInterface NodeInterface} config options.
 *
 * Unless the TreeStore is configured with a {@link Ext.data.Model model} of your choosing,
 * nodes in the {@link Ext.data.TreeStore} are by default, instances of {@link Ext.data.TreeModel}.
 *
 * # Heterogeneous node types.
 *
 * If the tree needs to use different data model classes at different levels there is much
 * flexibility in how to specify this.
 *
 * ### Configuring the Reader.
 * If you configure the proxy's reader with a
 * {@link Ext.data.reader.Reader#typeProperty typeProperty}, then the server is in control of which
 * data model types are created. A discriminator field is used in the raw data to decide which class
 * to instantiate. **If this is configured, then the data from the server is prioritized over other
 * ways of determining node class**.
 *
 *     @example
 *     Ext.define('myApp.Territory', {
 *         extend: 'Ext.data.TreeModel',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.define('myApp.Country', {
 *         extend: 'Ext.data.TreeModel',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.define('myApp.City', {
 *         extend: 'Ext.data.TreeModel',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.create('Ext.tree.Panel', {
 *         renderTo: document.body,
 *         height: 200,
 *         width: 400,
 *         title: 'Sales Areas - using typeProperty',
 *         rootVisible: false,
 *         store: {
 *             // Child types use namespace of store's model by default
 *             model: 'myApp.Territory',
 *             proxy: {
 *                 type: 'memory',
 *                 reader: {
 *                     typeProperty: 'mtype'
 *                 }
 *             },
 *             root: {
 *                 children: [{
 *                     name: 'Europe, ME, Africa',
 *                     mtype: 'Territory',
 *                     children: [{
 *                         name: 'UK of GB & NI',
 *                         mtype: 'Country',
 *                         children: [{
 *                             name: 'London',
 *                             mtype: 'City',
 *                             leaf: true
 *                         }]
 *                     }]
 *                 }, {
 *                     name: 'North America',
 *                     mtype: 'Territory',
 *                     children: [{
 *                         name: 'USA',
 *                         mtype: 'Country',
 *                         children: [{
 *                             name: 'Redwood City',
 *                             mtype: 'City',
 *                             leaf: true
 *                         }]
 *                     }]
 *                 }]
 *             }
 *         }
 *     });
 *
 * ### Node being loaded decides.
 * You can declare your TreeModel subclasses with a {@link Ext.data.TreeModel#childType childType}
 * which means that the node being loaded decides the class to instantiate for all of its
 * child nodes.
 *
 * It is important to note that if the root node is {@link Ext.tree.Panel#rootVisible hidden},
 * its type will default to the store's model type, and if left as the default
 * (`{@link Ext.data.TreeModel}`) this will have no knowledge of creation of special child node
 * types. So be sure to specify a store model in this case:
 *
 *     @example
 *     Ext.define('myApp.TerritoryRoot', {
 *         extend: 'Ext.data.TreeModel',
 *         childType: 'myApp.Territory',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.define('myApp.Territory', {
 *         extend: 'Ext.data.TreeModel',
 *         childType: 'myApp.Country',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.define('myApp.Country', {
 *         extend: 'Ext.data.TreeModel',
 *         childType: 'myApp.City',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.define('myApp.City', {
 *         extend: 'Ext.data.TreeModel',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.create('Ext.tree.Panel', {
 *         renderTo: document.body,
 *         height: 200,
 *         width: 400,
 *         title: 'Sales Areas',
 *         rootVisible: false,
 *         store: {
 *             // Needs to be this so it knows to create 'Country' child nodes
 *             model: 'myApp.TerritoryRoot',
 *             root: {
 *                 children: [{
 *                     name: 'Europe, ME, Africa',
 *                     children: [{
 *                         name: 'UK of GB & NI',
 *                         children: [{
 *                             name: 'London',
 *                             leaf: true
 *                         }]
 *                     }]
 *                 }, {
 *                     name: 'North America',
 *                     children: [{
 *                         name: 'USA',
 *                         children: [{
 *                             name: 'Redwood City',
 *                             leaf: true
 *                         }]
 *                     }]
 *                 }]
 *             }
 *         }
 *     });
 *
 * # Data structure
 *
 * The {@link Ext.data.TreeStore TreeStore} maintains a {@link Ext.data.TreeStore#getRoot root node}
 * and a hierarchical structure of {@link Ext.data.TreeModel node}s.
 *
 * The {@link Ext.tree.View UI} of the tree is driven by a {Ext.data.NodeStore NodeStore} which is
 * a flattened view of *visible* nodes. The NodeStore is dynamically updated to reflect
 * the visibility state of nodes as nodes are added, removed or expanded. The UI responds
 * to mutation events fire by the NodeStore.
 * 
 * Note that nodes have several more {@link Ext.data.Model#cfg-fields fields} in order to describe
 * their state within the hierarchy.
 *
 * If you add store listeners to the {@link Ext.data.Store#event-update update} event, then you will
 * receive notification when any of this state changes. You should check the array of modified field
 * names passed to the listener to decide whether the listener should take action
 * or ignore the event.
 * 
 * # Tree Grid
 * Trees may be configured using the {@link #cfg-columns} config including a 
 * {@link Ext.tree.Column treecolumn} to give the tree panel a hybrid tree / 
 * {@link Ext.grid.Panel grid} structure.
 * 
 *     @example
 *     Ext.create({
 *         xtype: 'treepanel',
 *         renderTo: Ext.getBody(),
 *         height: 200,
 *         width: 300,
 *         rootVisible: false,
 *         store: Ext.create('Ext.data.TreeStore', {
 *             fields: ['text', 'duration', 'isLayover'],
 *             root: {
 *                 expanded: true,
 *                 children: [{
 *                     text: 'SFO  &nbsp;✈&nbsp; DFW',
 *                     duration: '6h 55m',
 *                     expanded: true,
 *                     children: [{
 *                         text: 'SFO &nbsp;✈&nbsp; PHX',
 *                         duration: '2h 04m',
 *                         leaf: true
 *                     }, {
 *                         text: 'PHX layover',
 *                         duration: '2h 36m',
 *                         isLayover: true,
 *                         leaf: true
 *                     }, {
 *                         text: 'PHX &nbsp;✈&nbsp; DFW',
 *                         duration: '2h 15m',
 *                         leaf: true
 *                     }]
 *                 }]
 *             }
 *         }),
 *         columns: [{
 *             xtype: 'treecolumn',
 *             text: 'Flight Endpoints',
 *             dataIndex: 'text',
 *             flex: 1,
 *             renderer: function (val, meta, rec) {
 *                 if (rec.get('isLayover')) {
 *                     meta.tdStyle = 'color: gray; font-style: italic;';
 *                 }
 *                 return val;
 *             }
 *         }, {
 *             text: 'Duration',
 *             dataIndex: 'duration',
 *             width: 100
 *         }]
 *     });
 */
Ext.define('Ext.tree.Panel', {
    extend: 'Ext.panel.Table',
    alias: 'widget.treepanel',
    alternateClassName: [
        'Ext.tree.TreePanel',
        'Ext.TreePanel'
    ],
    requires: [
        'Ext.tree.View',
        'Ext.selection.TreeModel',
        'Ext.tree.Column',
        'Ext.data.TreeStore',
        'Ext.tree.NavigationModel'
    ],
    viewType: 'treeview',
    treeCls: Ext.baseCSSPrefix + 'tree-panel',
    /**
     * @cfg {Boolean} [rowLines=false]
     * Configure as true to separate rows with visible horizontal lines (depends on theme).
     */
    rowLines: false,
    /**
     * @cfg {Boolean} [lines=true]
     * False to disable tree lines.
     */
    lines: true,
    /**
     * @cfg {Boolean} [useArrows=false]
     * True to use Vista-style arrows in the tree.
     */
    useArrows: false,
    /**
     * @cfg {Boolean} [singleExpand=false]
     * True if only 1 node per branch may be expanded.
     */
    singleExpand: false,
    ddConfig: {
        enableDrag: true,
        enableDrop: true
    },
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'treegrid',
    /**
     * @cfg {Boolean} animate
     * True to enable animated expand/collapse. Defaults to the value of {@link Ext#enableFx}.
     */
    /**
     * @cfg {Boolean} [rootVisible=true]
     * False to hide the root node.
     *
     * Note that trees *always* have a root node. If you do not specify a {@link #cfg-root} node,
     * one will be created.
     *
     * If the root node is not visible, then in order for a tree to appear to the end user,
     * the root node is autoloaded with its child nodes.
     */
    rootVisible: true,
    /**
     * @cfg {String} [displayField=text]
     * The field inside the model that will be used as the node's text.
     */
    displayField: 'text',
    /**
     * @cfg {Ext.data.Model/Ext.data.TreeModel/Object} root
     * Allows you to not specify a store on this TreePanel. This is useful for creating a simple
     * tree with preloaded data without having to specify a TreeStore and Model. A store and model
     * will be created and root will be passed
     * to that store. For example:
     *
     *     Ext.create('Ext.tree.Panel', {
     *         title: 'Simple Tree',
     *         root: {
     *             text: "Root node",
     *             expanded: true,
     *             children: [
     *                 { text: "Child 1", leaf: true },
     *                 { text: "Child 2", leaf: true }
     *             ]
     *         },
     *         renderTo: Ext.getBody()
     *     });
     */
    root: null,
    /**
     * @cfg {String} [checkPropagation=none]
     * This configuration controls whether, and how checkbox click gestures are propagated to
     * child nodes, or to a parent node.
     *
     * Valid values are
     *
     *      - `'none'` Checking a check node does not affect any other nodes.
     *      - `'up'` Checking a check node synchronizes the value of its parent node with the state
     *         of its children.
     *      - `'down'` Checking a check node propagates the value to its child nodes.
     *      - `'both'` Checking a check node updates its child nodes, and syncs its parent node.
     */
    checkPropagation: 'none',
    // Required for the Lockable Mixin. These are the configurations which will be copied to the
    // normal and locked sub tablepanels
    normalCfgCopy: [
        'displayField',
        'root',
        'singleExpand',
        'useArrows',
        'lines',
        'rootVisible',
        'scroll'
    ],
    lockedCfgCopy: [
        'displayField',
        'root',
        'singleExpand',
        'useArrows',
        'lines',
        'rootVisible'
    ],
    isTree: true,
    /**
     * @cfg {Boolean} folderSort
     * True to automatically prepend a leaf sorter to the store.
     */
    /**
     * @cfg {Ext.data.TreeStore} store (required)
     * The {@link Ext.data.TreeStore Store} the tree should use as its data source.
     */
    arrowCls: Ext.baseCSSPrefix + 'tree-arrows',
    linesCls: Ext.baseCSSPrefix + 'tree-lines',
    noLinesCls: Ext.baseCSSPrefix + 'tree-no-lines',
    autoWidthCls: Ext.baseCSSPrefix + 'autowidth-table',
    constructor: function(config) {
        config = config || {};
        if (config.animate === undefined) {
            config.animate = Ext.isBoolean(this.animate) ? this.animate : Ext.enableFx;
        }
        this.enableAnimations = config.animate;
        delete config.animate;
        this.callParent([
            config
        ]);
    },
    initComponent: function() {
        var me = this,
            cls = [
                me.treeCls
            ],
            store, autoTree, view;
        if (me.useArrows) {
            cls.push(me.arrowCls);
            me.lines = false;
        }
        if (me.lines) {
            cls.push(me.linesCls);
        } else if (!me.useArrows) {
            cls.push(me.noLinesCls);
        }
        store = me.applyStore(me.store);
        // If there is no root node defined, then create one.
        if (!store.getRoot()) {
            store.setRoot({});
        }
        // Store must have the same idea about root visibility as us BEFORE callParent binds it.
        store.setRootVisible(me.rootVisible);
        // If the user specifies the headers collection manually then don't inject
        // our own
        if (!me.columns) {
            me.isAutoTree = autoTree = true;
        }
        me.viewConfig = Ext.apply({
            rootVisible: me.rootVisible,
            animate: me.enableAnimations,
            singleExpand: me.singleExpand,
            node: store.getRoot(),
            navigationModel: 'tree',
            isAutoTree: autoTree
        }, me.viewConfig);
        if (autoTree) {
            me.addCls(me.autoWidthCls);
            me.columns = [
                {
                    xtype: 'treecolumn',
                    text: me.hideHeaders === true ? 'Name' : null,
                    flex: 1,
                    dataIndex: me.displayField
                }
            ];
        }
        if (me.cls) {
            cls.push(me.cls);
        }
        me.cls = cls.join(' ');
        me.callParent();
        view = me.getView();
        // Relay events from the TreeView.
        // An injected LockingView relays events from its locked side's View
        me.relayEvents(view, [
            /**
            * @event beforecheckchange
            * Fires when a node with a checkbox's checked property changes.
            * @param {Ext.data.TreeModel} node The node who's checked property is to be changed.
            * @param {Boolean} checked The node's current checked state.
            * @param {Ext.event.Event} e The click event.
            */
            'beforecheckchange',
            /**
            * @event checkchange
            * Fires when a node with a checkbox's checked property changes.
            * @param {Ext.data.TreeModel} node The node who's checked property was changed.
            * @param {Boolean} checked The node's new checked state.
            * @param {Ext.event.Event} e The click event.
            */
            'checkchange',
            /**
            * @event afteritemexpand
            * @inheritdoc Ext.tree.View#afteritemexpand
            */
            'afteritemexpand',
            /**
            * @event afteritemcollapse
            * @inheritdoc Ext.tree.View#afteritemcollapse
            */
            'afteritemcollapse'
        ]);
    },
    applyStore: function(store) {
        // private
        // Note that this is not a config system applier. store is not yet a config.
        // It just does the job of an applier and converts a config object to the true value
        // for the setter to use.
        var me = this;
        if (Ext.isString(store)) {
            store = me.store = Ext.StoreMgr.lookup(store);
        } else if (!store || !store.isStore) {
            store = Ext.apply({
                type: 'tree',
                proxy: 'memory'
            }, store);
            if (me.root) {
                store.root = me.root;
            }
            if (me.fields) {
                store.fields = me.fields;
            } else if (me.model) {
                store.model = me.model;
            }
            if (me.folderSort) {
                store.folderSort = me.folderSort;
            }
            store = me.store = Ext.StoreMgr.lookup(store);
        } else if (me.root) {
            store = me.store = Ext.data.StoreManager.lookup(store);
            store.setRoot(me.root);
            if (me.folderSort !== undefined) {
                store.folderSort = me.folderSort;
                store.sort();
            }
        }
        return store;
    },
    setRoot: function(root) {
        this.store.setRoot(root);
    },
    setStore: function(store) {
        var me = this;
        store = me.applyStore(store);
        // If there is no rootnode defined, then create one.
        if (!store.getRoot()) {
            store.setRoot({});
        }
        // Store must have the same idea about root visibility as us BEFORE callParent binds it.
        store.setRootVisible(me.rootVisible);
        if (me.enableLocking) {
            me.reconfigure(store);
        } else {
            if (me.view) {
                me.view.setRootNode(store.getRootNode());
            }
            me.bindStore(store);
        }
    },
    /**
     * @private
     * Hook into the TreeStore.
     */
    bindStore: function(store, initial) {
        var me = this,
            root = store.getRoot();
        // Bind to store, and autocreate the BufferedRenderer.
        me.callParent(arguments);
        // The TreeStore needs to know about this TreePanel's singleExpand constraint so that
        // it can ensure the compliance of NodeInterface.expandAll.
        store.singleExpand = me.singleExpand;
        // Monitor the TreeStore for the root node being changed. Return a Destroyable object
        me.storeListeners = me.mon(store, {
            destroyable: true,
            rootchange: me.onRootChange,
            scope: me
        });
        // Relay store events. relayEvents always returns a Destroyable object.
        me.storeRelayers = me.relayEvents(store, [
            /**
             * @event beforeload
             * @inheritdoc Ext.data.TreeStore#beforeload
             */
            'beforeload',
            /**
             * @event load
             * @inheritdoc Ext.data.TreeStore#load
             */
            'load'
        ]);
        // If rootVisible is false, we *might* need to expand the node.
        // If store is autoLoad, that will already have been kicked off.
        // If its already expanded, or in the process of loading, the TreeStore
        // has started that at the end of updateRoot 
        if (!me.rootVisible && !store.autoLoad && !(root.isExpanded() || root.isLoading())) {
            // A hidden root must be expanded, unless it's overridden with autoLoad: false.
            // If it's loaded, set its expanded field (silently), and skip ahead
            // to the onNodeExpand callback.
            if (root.isLoaded()) {
                root.data.expanded = true;
                store.onNodeExpand(root, root.childNodes);
            }
            // Root is not loaded; go through the expand mechanism to force a load
            // unless we were told explicitly not to load the store by setting
            // autoLoad: false. This is useful with Direct proxy in cases when
            // Direct API is loaded dynamically and may not be available at the time
            // when TreePanel is created.
            else if (store.autoLoad !== false && !store.hasPendingLoad()) {
                root.data.expanded = false;
                root.expand();
            }
        }
        // TreeStore must have an upward link to the TreePanel so that nodes can find their owning
        // tree in NodeInterface.getOwnerTree
        // TODO: NodeInterface.getOwnerTree is deprecated. Data class must not be coupled to UI.
        // Remove this link when that method is removed.
        store.ownerTree = me;
        if (!initial) {
            me.view.setRootNode(root);
        }
    },
    /**
     * @private
     */
    addRelayers: function(newRoot) {
        var me = this;
        if (me.rootRelayers) {
            me.rootRelayers.destroy();
            me.rootRelayers = null;
        }
        // Relay store events with prefix. Return a Destroyable object
        me.rootRelayers = me.mon(newRoot, {
            destroyable: true,
            /**
             * @event itemappend
             * @inheritdoc Ext.data.TreeStore#nodeappend
             */
            append: me.createRelayer('itemappend'),
            /**
             * @event itemremove
             * @inheritdoc Ext.data.TreeStore#noderemove
             */
            remove: me.createRelayer('itemremove'),
            /**
             * @event itemmove
             * @inheritdoc Ext.data.TreeStore#nodemove
             */
            move: me.createRelayer('itemmove', [
                0,
                4
            ]),
            /**
             * @event iteminsert
             * @inheritdoc Ext.data.TreeStore#nodeinsert
             */
            insert: me.createRelayer('iteminsert'),
            /**
             * @event beforeitemappend
             * @inheritdoc Ext.data.TreeStore#nodebeforeappend
             */
            beforeappend: me.createRelayer('beforeitemappend'),
            /**
             * @event beforeitemremove
             * @inheritdoc Ext.data.TreeStore#nodebeforeremove
             */
            beforeremove: me.createRelayer('beforeitemremove'),
            /**
             * @event beforeitemmove
             * @inheritdoc Ext.data.TreeStore#nodebeforemove
             */
            beforemove: me.createRelayer('beforeitemmove'),
            /**
             * @event beforeiteminsert
             * @inheritdoc Ext.data.TreeStore#nodebeforeinsert
             */
            beforeinsert: me.createRelayer('beforeiteminsert'),
            /**
             * @event itemexpand
             * @inheritdoc Ext.data.TreeStore#nodeexpand
             */
            expand: me.createRelayer('itemexpand', [
                0,
                1
            ]),
            /**
             * @event itemcollapse
             * @inheritdoc Ext.data.TreeStore#nodecollapse
             */
            collapse: me.createRelayer('itemcollapse', [
                0,
                1
            ]),
            /**
             * @event beforeitemexpand
             * @inheritdoc Ext.data.TreeStore#nodebeforeexpand
             */
            beforeexpand: me.createRelayer('beforeitemexpand', [
                0,
                1
            ]),
            /**
             * @event beforeitemcollapse
             * @inheritdoc Ext.data.TreeStore#nodebeforecollapse
             */
            beforecollapse: me.createRelayer('beforeitemcollapse', [
                0,
                1
            ]),
            scope: me
        });
    },
    /**
     * @private
     */
    unbindStore: function() {
        var me = this,
            store = me.store;
        if (store) {
            me.callParent();
            Ext.destroy(me.storeListeners, me.storeRelayers, me.rootRelayers);
            delete store.ownerTree;
            store.singleExpand = null;
        }
    },
    /**
     * Sets root node of this tree. All trees *always* have a root node. It may be
     * {@link #rootVisible hidden}.
     *
     * If the passed node has not already been loaded with child nodes, and has its expanded
     * field set, this triggers the {@link #cfg-store} to load the child nodes of the root.
     * @param {Ext.data.TreeModel/Object} root
     * @return {Ext.data.TreeModel} The new root
     */
    setRootNode: function() {
        return this.store.setRoot.apply(this.store, arguments);
    },
    /**
     * Returns the root node for this tree.
     * @return {Ext.data.TreeModel}
     */
    getRootNode: function() {
        return this.store.getRoot();
    },
    onRootChange: function(root) {
        this.view.setRootNode(root);
    },
    /**
     * Retrieve an array of checked records.
     * @return {Ext.data.TreeModel[]} An array containing the checked records
     */
    getChecked: function() {
        return this.getView().getChecked();
    },
    isItemChecked: function(rec) {
        return rec.get('checked');
    },
    /**
     * Expands a record that is loaded in the tree.
     * @param {Ext.data.Model} record The record to expand
     * @param {Boolean} [deep] True to expand nodes all the way down the tree hierarchy.
     * @param {Function} [callback] The function to run after the expand is completed
     * @param {Object} [scope] The scope of the callback function.
     */
    expandNode: function(record, deep, callback, scope) {
        return this.getView().expand(record, deep, callback, scope || this);
    },
    /**
     * Collapses a record that is loaded in the tree.
     * @param {Ext.data.Model} record The record to collapse
     * @param {Boolean} [deep] True to collapse nodes all the way up the tree hierarchy.
     * @param {Function} [callback] The function to run after the collapse is completed
     * @param {Object} [scope] The scope of the callback function.
     */
    collapseNode: function(record, deep, callback, scope) {
        return this.getView().collapse(record, deep, callback, scope || this);
    },
    /**
     * Expand all nodes
     * @param {Function} [callback] A function to execute when the expand finishes.
     * @param {Object} [scope] The scope of the callback function
     */
    expandAll: function(callback, scope) {
        var me = this,
            root = me.getRootNode();
        if (root) {
            Ext.suspendLayouts();
            root.expand(true, callback, scope || me);
            Ext.resumeLayouts(true);
        }
    },
    /**
     * Collapse all nodes
     * @param {Function} [callback] A function to execute when the collapse finishes.
     * @param {Object} [scope] The scope of the callback function
     */
    collapseAll: function(callback, scope) {
        var me = this,
            root = me.getRootNode(),
            view = me.getView();
        if (root) {
            Ext.suspendLayouts();
            scope = scope || me;
            if (view.rootVisible) {
                root.collapse(true, callback, scope);
            } else {
                root.collapseChildren(true, callback, scope);
            }
            Ext.resumeLayouts(true);
        }
    },
    /**
     * Expand the tree to the path of a particular node. This is the way to expand a known path
     * when the intervening nodes are not yet loaded.
     *
     * The path may be an absolute path (beginning with a `'/'` character) from the root, eg:
     *
     *     '/rootId/nodeA/nodeB/nodeC'
     *
     * Or, the path may be relative, starting from an **existing** node in the tree:
     *
     *     'nodeC/nodeD'
     *
     * @param {String} path The path to expand. The path may be absolute, including a leading
     * separator and starting from the root node id, or relative with no leading separator,
     * starting from an *existing* node in the tree.
     * @param {Object} [options] An object containing options to modify the operation.
     * @param {String} [options.field] The field to get the data from. Defaults to the model
     * idProperty.
     * @param {String} [options.separator='/'] A separator to use.
     * @param {Boolean} [options.select] Pass as `true` to select the specified row.
     * @param {Boolean} [options.focus] Pass as `true` to focus the specified row.
     * @param {Function} [options.callback] A function to execute when the expand finishes.
     * @param {Boolean} options.callback.success `true` if the node expansion was successful.
     * @param {Ext.data.Model} options.callback.record If successful, the target record.
     * @param {HTMLElement} options.callback.node If successful, the record's view node.
     * If unsuccessful, the last view node encountered while expanding the path.
     * @param {Object} [options.scope] The scope (`this` reference) in which the callback function
     * is executed.
     */
    expandPath: function(path, options) {
        var args = arguments,
            me = this,
            view = me.view,
            field = (options && options.field) || me.store.model.idProperty,
            separator = (options && options.separator) || '/',
            select, doFocus, callback, scope, current, index, keys, rooted, expander;
        // New option object API
        if (options && typeof options === 'object') {
            field = options.field || me.store.model.idProperty;
            separator = options.separator || '/';
            callback = options.callback;
            scope = options.scope;
            select = options.select;
            doFocus = options.focus;
        } else // Old multi argument API
        {
            field = args[1] || me.store.model.idProperty;
            separator = args[2] || '/';
            callback = args[3];
            scope = args[4];
        }
        if (Ext.isEmpty(path)) {
            return Ext.callback(callback, scope || me, [
                false,
                null
            ]);
        }
        keys = path.split(separator);
        // If they began the path with '/', this indicates starting from the root ID.
        // otherwise, then can start at any *existing* node id.
        rooted = !keys[0];
        if (rooted) {
            current = me.getRootNode();
            index = 1;
        } else // Not rooted, gather the first node in the path which MUST already exist.
        {
            current = me.store.findNode(field, keys[0]);
            index = 0;
        }
        // Invalid root. Relative start could not be found, absolute start was not the rootNode.
        // The ids paths may be numeric, so cast the value to a string for comparison.
        if (!current || (rooted && (current.get(field) + '') !== keys[1])) {
            return Ext.callback(callback, scope || me, [
                false,
                current
            ]);
        }
        // The expand success callback passed to every expand call down the path.
        // Called in the scope of the node being expanded.
        expander = function(newChildren) {
            var node = this,
                len, i, value;
            // We've arrived at the end of the path.
            if (++index === keys.length) {
                if (select) {
                    view.getSelectionModel().select(node);
                }
                if (doFocus) {
                    view.getNavigationModel().setPosition(node, 0);
                }
                return Ext.callback(callback, scope || me, [
                    true,
                    node,
                    view.getNode(node)
                ]);
            }
            // Find the next child in the path if it's there and expand it.
            for (i = 0 , len = newChildren ? newChildren.length : 0; i < len; i++) {
                // The ids paths may be numeric, so cast the value to a string for comparison
                node = newChildren[i];
                value = node.get(field);
                if (value || value === 0) {
                    value = value.toString();
                }
                if (value === keys[index]) {
                    return node.expand(false, expander);
                }
            }
            // If we get here, there's been a miss along the path, and the operation is a fail.
            node = this;
            Ext.callback(callback, scope || me, [
                false,
                node,
                view.getNode(node)
            ]);
        };
        current.expand(false, expander);
    },
    /**
     * Expand the tree to the path of a particular node, then scroll it into view.
     * @param {String} path The path to bring into view. The path may be absolute, including
     * a leading separator and starting from the root node id, or relative with no leading
     * separator, starting from an *existing* node in the tree.
     * @param {Object} [options] An object containing options to modify the operation.
     * @param {String} [options.field] The field to get the data from. Defaults to the model
     * idProperty.
     * @param {String} [options.separator='/'] A separator to use.
     * @param {Boolean} [options.animate] Pass `true` to animate the row into view.
     * @param {Boolean} [options.highlight] Pass `true` to highlight the row with a glow animation
     * when it is in view.
     * @param {Boolean} [options.select] Pass as `true` to select the specified row.
     * @param {Boolean} [options.focus] Pass as `true` to focus the specified row.
     * @param {Function} [options.callback] A function to execute when the expand finishes.
     * @param {Boolean} options.callback.success `true` if the node expansion was successful.
     * @param {Ext.data.Model} options.callback.record If successful, the target record.
     * @param {HTMLElement} options.callback.node If successful, the record's view node.
     * If unsuccessful, the last view node encountered while expanding the path.
     * @param {Object} [options.scope] The scope (`this` reference) in which the callback function
     * is executed.
     */
    ensureVisible: function(path, options) {
        // They passed a record instance or row index. Use the TablePanel's method.
        if (path.isEntity || typeof path === 'number') {
            return this.callParent([
                path,
                options
            ]);
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            field = (options && options.field) || me.store.model.idProperty,
            separator = (options && options.separator) || '/',
            callback, scope, keys, rooted, last, node, parentNode, onLastExpanded;
        onLastExpanded = function(success, lastExpanded, lastExpandedHtmlNode, targetNode) {
            if (!targetNode && success && lastExpanded) {
                targetNode = lastExpanded.findChild(field, last);
            }
            // Once we have the node, we can use the TablePanel's ensureVisible method
            if (targetNode) {
                me.doEnsureVisible(targetNode, options);
            } else {
                Ext.callback(callback, scope || me, [
                    false,
                    lastExpanded
                ]);
            }
        };
        if (options) {
            callback = options.callback;
            scope = options.scope;
        }
        keys = path.split(separator);
        rooted = !keys[0];
        last = keys.pop();
        // If the path was "foo/bar" or "/foo/Bar"
        if (keys.length && !(rooted && keys.length === 1)) {
            me.expandPath(keys.join(separator), field, separator, onLastExpanded);
        } else // If the path was "foo" or "/foo"
        {
            node = me.store.findNode(field, last);
            if (node) {
                parentNode = node.parentNode;
                if (parentNode && !parentNode.isExpanded()) {
                    parentNode.expand();
                }
                // Pass the target node as the 4th parameter
                // so the callback doesn't have to look it up
                onLastExpanded(true, null, null, node);
            } else {
                Ext.callback(callback, scope || me, [
                    false,
                    null
                ]);
            }
        }
    },
    /**
     * Expand the tree to the path of a particular node, then select it.
     * @param {String} path The path to expand. The path may be absolute, including a leading
     * separator and starting from the root node id, or relative with no leading separator,
     * starting from an *existing* node in the tree.
     * @param {String} [field] The field to get the data from. Defaults to the model idProperty.
     * @param {String} [separator='/'] A separator to use.
     * @param {Function} [callback] A function to execute when the select finishes.
     * @param {Boolean} callback.success `true` if the node expansion was successful.
     * @param {Ext.data.NodeInterface} callback.lastNode If successful, the target node.
     * If unsuccessful, the last tree node encountered while expanding the path.
     * @param {HTMLElement} callback.node If successful, the record's view node.
     * @param {Object} [scope] The scope of the callback function
     */
    selectPath: function(path, field, separator, callback, scope) {
        this.ensureVisible(path, {
            field: field,
            separator: separator,
            select: true,
            callback: callback,
            scope: scope
        });
    }
});

/**
 * @private
 */
Ext.define('Ext.view.DragZone', {
    extend: 'Ext.dd.DragZone',
    containerScroll: false,
    constructor: function(config) {
        var me = this,
            view, ownerCt, el;
        Ext.apply(me, config);
        // Create a ddGroup unless one has been configured.
        // User configuration of ddGroups allows users to specify which
        // DD instances can interact with each other. Using one
        // based on the id of the View would isolate it and mean it can only
        // interact with a DropZone on the same View also using a generated ID.
        if (!me.ddGroup) {
            me.ddGroup = 'view-dd-zone-' + me.view.id;
        }
        // Ext.dd.DragDrop instances are keyed by the ID of their encapsulating element.
        // So a View's DragZone cannot use the View's main element because the DropZone
        // must use that because the DropZone may need to scroll on hover at a scrolling boundary,
        // and it is the View's main element which handles scrolling.
        // We use the View's parent element to drag from. Ideally, we would use the internal
        // structure, but that is transient; DataViews recreate the internal structure dynamically
        // as data changes.
        // TODO: Ext 5.0 DragDrop must allow multiple DD objects to share the same element.
        view = me.view;
        // This is for https://www.w3.org/TR/pointerevents/ platforms.
        // On these platforms, the pointerdown event (single touchstart) is reserved for
        // initiating a scroll gesture. Setting the items draggable defeats that and
        // enables the touchstart event to trigger a drag.
        //
        // Two finger dragging will still scroll on these platforms.
        view.setItemsDraggable(true);
        ownerCt = view.ownerCt;
        // We don't just grab the parent el, since the parent el may be
        // some el injected by the layout
        if (ownerCt) {
            el = ownerCt.getTargetEl().dom;
        } else {
            el = view.el.dom.parentNode;
        }
        me.callParent([
            el
        ]);
        me.ddel = document.createElement('div');
        me.ddel.className = Ext.baseCSSPrefix + 'grid-dd-wrap';
    },
    init: function(id, sGroup, config) {
        var me = this,
            eventSpec = {
                itemmousedown: me.onItemMouseDown,
                scope: me
            };
        // If there may be ambiguity with touch/swipe to scroll and a drag gesture
        // trigger drag start on longpress and a *real* mousedown.
        if (Ext.supports.Touch) {
            eventSpec.itemlongpress = me.onItemLongPress;
            // Longpress fires contextmenu in some touch platforms, so if we are using longpress
            // inhibit the contextmenu on this element
            eventSpec.contextmenu = {
                element: 'el',
                fn: me.onViewContextMenu
            };
        }
        me.initTarget(id, sGroup, config);
        me.view.mon(me.view, eventSpec);
    },
    onValidDrop: function(target, e, id) {
        this.callParent([
            target,
            e,
            id
        ]);
        // focus the view that the node was dropped onto so that keynav will be enabled.
        if (!target.el.contains(Ext.Element.getActiveElement())) {
            target.el.focus();
        }
    },
    onViewContextMenu: function(e) {
        if (e.pointerType !== 'mouse') {
            e.preventDefault();
        }
    },
    onItemMouseDown: function(view, record, item, index, e) {
        // Ignore touchstart.
        // For touch events, we use longpress.
        if (e.pointerType === 'mouse') {
            this.onTriggerGesture(view, record, item, index, e);
        }
    },
    onItemLongPress: function(view, record, item, index, e) {
        // Ignore long mousedowns.
        // The initial mousedown started the drag.
        // For touch events, we use longpress.
        if (e.pointerType !== 'mouse') {
            this.onTriggerGesture(view, record, item, index, e);
        }
    },
    onTriggerGesture: function(view, record, item, index, e) {
        var navModel;
        // Only respond to longpress for touch dragging.
        // Reject drag start if mousedown is on the actionable cell of a grid view
        if ((e.pointerType === 'touch' && e.type !== 'longpress') || (e.position && e.position.isEqual(e.view.actionPosition))) {
            return;
        }
        if (!this.isPreventDrag(e, record, item, index)) {
            navModel = view.getNavigationModel();
            // Since handleMouseDown prevents the default behavior of the event, which
            // is to focus the view, we focus the view now.  This ensures that the view
            // remains focused if the drag is cancelled, or if no drag occurs.
            //
            // A Table event will have a position property which is a CellContext
            if (e.position) {
                navModel.setPosition(e.position);
            } else // Otherwise, just use the item index
            {
                navModel.setPosition(index);
            }
            this.handleMouseDown(e);
        }
    },
    /**
     * @protected
     * Template method called upon mousedown. May be overridden in subclasses, or configured
     * into an instance.
     *
     * Return `true` to prevent drag start.
     * @param {Ext.event.Event} e The mousedown event.
     * @param {Ext.data.Model} record The record mousedowned upon.
     * @param {HTMLElement} item The grid row mousedowned upon.
     * @param {Number} index The row number mousedowned upon.
     */
    isPreventDrag: function(e, record, item, index) {
        return !!e.isInputFieldEvent;
    },
    getDragData: function(e) {
        var view = this.view,
            item = e.getTarget(view.getItemSelector());
        if (item) {
            return {
                copy: view.copy || (view.allowCopy && e.ctrlKey),
                event: e,
                view: view,
                ddel: this.ddel,
                item: item,
                records: view.getSelectionModel().getSelection(),
                fromPosition: Ext.fly(item).getXY()
            };
        }
    },
    onInitDrag: function(x, y) {
        var me = this,
            data = me.dragData,
            view = data.view,
            selectionModel = view.getSelectionModel(),
            record = view.getRecord(data.item);
        // Update the selection to match what would have been selected if the user had
        // done a full click on the target node rather than starting a drag from it
        if (!selectionModel.isSelected(record)) {
            selectionModel.selectWithEvent(record, me.DDMInstance.mousedownEvent);
        }
        data.records = selectionModel.getSelection();
        Ext.fly(me.ddel).setHtml(me.getDragText());
        me.proxy.update(me.ddel);
        me.onStartDrag(x, y);
        return true;
    },
    getDragText: function() {
        var count = this.dragData.records.length;
        return Ext.String.format(this.dragText, count, count === 1 ? '' : 's');
    },
    getRepairXY: function(e, data) {
        return data ? data.fromPosition : false;
    }
});

/**
 * @private
 */
Ext.define('Ext.tree.ViewDragZone', {
    extend: 'Ext.view.DragZone',
    isPreventDrag: function(e, record) {
        return (record.get('allowDrag') === false) || !!e.getTarget(this.view.expanderSelector);
    },
    getDragText: function() {
        var records = this.dragData.records,
            count = records.length,
            text = records[0].get(this.displayField),
            suffix = 's',
            formatRe = /\{\d+\}/,
            dragText = this.dragText;
        if (formatRe.test(dragText) && count === 1 && text) {
            return text;
        } else if (!text) {
            suffix = '';
        }
        return Ext.String.format(dragText, count, suffix);
    },
    afterRepair: function() {
        var me = this,
            view = me.view,
            selectedRowCls = view.selectedItemCls,
            records = me.dragData.records,
            r,
            rLen = records.length,
            fly = Ext.fly,
            item;
        if (Ext.enableFx && me.repairHighlight) {
            // Roll through all records and highlight all the ones we attempted to drag.
            for (r = 0; r < rLen; r++) {
                // anonymous fns below, don't hoist up unless below is wrapped in
                // a self-executing function passing in item.
                item = view.getNode(records[r]);
                // We must remove the selected row class before animating, because
                // the selected row class declares !important on its background-color.
                fly(item.firstChild).highlight(me.repairHighlightColor, {
                    listeners: {
                        beforeanimate: function() {
                            if (view.isSelected(item)) {
                                fly(item).removeCls(selectedRowCls);
                            }
                        },
                        afteranimate: function() {
                            if (view.isSelected(item)) {
                                fly(item).addCls(selectedRowCls);
                            }
                        }
                    }
                });
            }
        }
        me.dragging = false;
    }
});

/**
 * @private
 */
Ext.define('Ext.tree.ViewDropZone', {
    extend: 'Ext.view.DropZone',
    /**
     * @cfg {Boolean} allowParentInserts
     * Allow inserting a dragged node between an expanded parent node and its first child
     * that will become a sibling of the parent when dropped.
     */
    allowParentInserts: false,
    /**
     * @cfg {Boolean} allowContainerDrops
     * True if drops on the tree container (outside of a specific tree node) are allowed.
     *
     * These are treated as appends to the root node.
     */
    allowContainerDrops: false,
    /**
     * @cfg {Boolean} appendOnly
     * True if the tree should only allow append drops (use for trees which are sorted).
     */
    appendOnly: false,
    /**
     * @cfg {Number} expandDelay
     * The delay in milliseconds to wait before expanding a target tree node while dragging
     * a droppable node over the target.
     */
    expandDelay: 500,
    indicatorCls: Ext.baseCSSPrefix + 'tree-ddindicator',
    /**
     * @private
     */
    expandNode: function(node) {
        var view = this.view;
        this.expandProcId = false;
        if (!node.isLeaf() && !node.isExpanded()) {
            view.expand(node);
            this.expandProcId = false;
        }
    },
    /**
     * @private
     */
    queueExpand: function(node) {
        this.expandProcId = Ext.defer(this.expandNode, this.expandDelay, this, [
            node
        ]);
    },
    /**
     * @private
     */
    cancelExpand: function() {
        this.expandProcId = Ext.undefer(this.expandProcId);
    },
    getPosition: function(e, node) {
        var view = this.view,
            record = view.getRecord(node),
            y = e.getY(),
            noAppend = record.isLeaf(),
            noBelow = false,
            region = Ext.fly(node).getRegion(),
            fragment;
        // If we are dragging on top of the root node of the tree, we always want to append.
        if (record.isRoot()) {
            return 'append';
        }
        // Return 'append' if the node we are dragging on top of is not a leaf else return false.
        if (this.appendOnly) {
            return noAppend ? false : 'append';
        }
        if (!this.allowParentInserts) {
            noBelow = record.hasChildNodes() && record.isExpanded();
        }
        fragment = (region.bottom - region.top) / (noAppend ? 2 : 3);
        if (y >= region.top && y < (region.top + fragment)) {
            return 'before';
        } else if (!noBelow && (noAppend || (y >= (region.bottom - fragment) && y <= region.bottom))) {
            return 'after';
        } else {
            return 'append';
        }
    },
    isValidDropPoint: function(node, position, dragZone, e, data) {
        if (!node || !data.item) {
            return false;
        }
        // eslint-disable-next-line vars-on-top
        var view = this.view,
            targetNode = view.getRecord(node),
            draggedRecords = data.records,
            dataLength = draggedRecords.length,
            ln = draggedRecords.length,
            i, record;
        // No drop position, or dragged records: invalid drop point
        if (!(targetNode && position && dataLength)) {
            return false;
        }
        // If the targetNode is within the folder we are dragging
        for (i = 0; i < ln; i++) {
            record = draggedRecords[i];
            if (record.isNode && record.contains(targetNode)) {
                return false;
            }
        }
        // Respect the allowDrop field on Tree nodes
        if (position === 'append' && targetNode.get('allowDrop') === false) {
            return false;
        } else if (position !== 'append' && targetNode.parentNode.get('allowDrop') === false) {
            return false;
        }
        // If the target record is in the dragged dataset, then invalid drop
        if (Ext.Array.contains(draggedRecords, targetNode)) {
            return false;
        }
        return view.fireEvent('nodedragover', targetNode, position, data, e) !== false;
    },
    onNodeOver: function(node, dragZone, e, data) {
        var position = this.getPosition(e, node),
            returnCls = this.dropNotAllowed,
            view = this.view,
            targetNode = view.getRecord(node),
            indicator = this.getIndicator(),
            indicatorY = 0;
        // auto node expand check
        this.cancelExpand();
        if (position === 'append' && !this.expandProcId && !Ext.Array.contains(data.records, targetNode) && !targetNode.isLeaf() && !targetNode.isExpanded()) {
            this.queueExpand(targetNode);
        }
        if (this.isValidDropPoint(node, position, dragZone, e, data)) {
            this.valid = true;
            this.currentPosition = position;
            this.overRecord = targetNode;
            indicator.setWidth(Ext.fly(node).getWidth());
            indicatorY = Ext.fly(node).getY() - Ext.fly(view.el).getY() - 1;
            /*
             * In the code below we show the proxy again. The reason for doing this is showing
             * the indicator will call toFront, causing it to get a new z-index which can sometimes
             * push the proxy behind it. We always  want the proxy to be above, so calling show
             * on the proxy will call toFront and bring it forward.
             */
            if (position === 'before') {
                returnCls = targetNode.isFirst() ? Ext.baseCSSPrefix + 'tree-drop-ok-above' : Ext.baseCSSPrefix + 'tree-drop-ok-between';
                indicator.showAt(0, indicatorY);
                dragZone.proxy.show();
            } else if (position === 'after') {
                returnCls = targetNode.isLast() ? Ext.baseCSSPrefix + 'tree-drop-ok-below' : Ext.baseCSSPrefix + 'tree-drop-ok-between';
                indicatorY += Ext.fly(node).getHeight();
                indicator.showAt(0, indicatorY);
                dragZone.proxy.show();
            } else {
                returnCls = Ext.baseCSSPrefix + 'tree-drop-ok-append';
                // @TODO: set a class on the parent folder node to be able to style it
                indicator.hide();
            }
        } else {
            this.valid = false;
        }
        this.currentCls = returnCls;
        return returnCls;
    },
    // The mouse is no longer over a tree node, so dropping is not valid
    onNodeOut: function(n, dd, e, data) {
        this.valid = false;
        this.getIndicator().hide();
    },
    onContainerOver: function(dd, e, data) {
        return this.allowContainerDrops ? this.dropAllowed : e.getTarget('.' + this.indicatorCls) ? this.currentCls : this.dropNotAllowed;
    },
    // This will be called is allowContainerDrops is set.
    // The target node is the root
    onContainerDrop: function(dragZone, e, data) {
        if (this.allowContainerDrops) {
            this.valid = true;
            this.currentPosition = 'append';
            this.overRecord = this.view.store.getRoot();
            this.onNodeDrop(this.overRecord, dragZone, e, data);
        }
    },
    notifyOut: function() {
        this.callParent(arguments);
        this.cancelExpand();
    },
    handleNodeDrop: function(data, targetNode, position) {
        var me = this,
            targetView = me.view,
            parentNode = targetNode ? targetNode.parentNode : targetView.panel.getRootNode(),
            Model = targetView.store.getModel(),
            records, i, len, record, insertionMethod, argList, needTargetExpand, transferData;
        // If the copy flag is set, create a copy of the models
        if (data.copy) {
            records = data.records;
            data.records = [];
            for (i = 0 , len = records.length; i < len; i++) {
                record = records[i];
                if (record.isNode) {
                    data.records.push(record.copy(undefined, true));
                } else {
                    // If it's not a node, make a node copy
                    data.records.push(new Model(Ext.apply({}, record.data)));
                }
            }
        }
        // Cancel any pending expand operation
        me.cancelExpand();
        // Grab a reference to the correct node insertion method.
        // Create an arg list array intended for the apply method of the
        // chosen node insertion method.
        // Ensure the target object for the method is referenced by 'targetNode'
        if (position === 'before') {
            insertionMethod = parentNode.insertBefore;
            argList = [
                null,
                targetNode
            ];
            targetNode = parentNode;
        } else if (position === 'after') {
            if (targetNode.nextSibling) {
                insertionMethod = parentNode.insertBefore;
                argList = [
                    null,
                    targetNode.nextSibling
                ];
            } else {
                insertionMethod = parentNode.appendChild;
                argList = [
                    null
                ];
            }
            targetNode = parentNode;
        } else {
            if (!(targetNode.isExpanded() || targetNode.isLoading())) {
                needTargetExpand = true;
            }
            insertionMethod = targetNode.appendChild;
            argList = [
                null
            ];
        }
        // A function to transfer the data into the destination tree
        transferData = function() {
            var color, n;
            // Coalesce layouts caused by node removal, appending and sorting
            Ext.suspendLayouts();
            // Insert the records into the target node
            for (i = 0 , len = data.records.length; i < len; i++) {
                record = data.records[i];
                if (!record.isNode) {
                    if (record.isModel) {
                        record = new Model(record.data, record.getId());
                    } else {
                        record = new Model(record);
                    }
                    data.records[i] = record;
                }
                argList[0] = record;
                insertionMethod.apply(targetNode, argList);
            }
            // If configured to sort on drop, do it according to the TreeStore's comparator
            if (me.sortOnDrop) {
                targetNode.sort(targetNode.getTreeStore().getSorters().sortFn);
            }
            Ext.resumeLayouts(true);
            // Focus the dropped node.
            record = data.records[0];
            targetView.ownerGrid.ensureVisible(record);
            targetView.getNavigationModel().setPosition(record);
            // Kick off highlights after everything's been inserted, so they are
            // more in sync without insertion/render overhead.
            // Element.highlight can handle highlighting table nodes.
            if (Ext.enableFx && me.dropHighlight) {
                color = me.dropHighlightColor;
                for (i = 0; i < len; i++) {
                    n = targetView.getNode(data.records[i]);
                    if (n) {
                        Ext.fly(n).highlight(color);
                    }
                }
            }
        };
        // If dropping right on an unexpanded node, transfer the data after it is expanded.
        if (needTargetExpand) {
            targetNode.expand(false, transferData);
        }
        // If the node is waiting for its children, we must transfer the data after the expansion.
        // The expand event does NOT signal UI expansion, it is the SIGNAL for UI expansion.
        // It's listened for by the NodeStore on the root node. Which means that listeners
        // on the target node get notified BEFORE UI expansion. So we need a delay.
        // TODO: Refactor NodeInterface.expand/collapse to notify its owning tree directly
        // when it needs to expand/collapse.
        else if (targetNode.isLoading()) {
            targetNode.on({
                expand: transferData,
                delay: 1,
                single: true
            });
        } else // Otherwise, call the data transfer function immediately
        {
            transferData();
        }
    }
});

/**
 * This plugin provides drag and drop functionality for a {@link Ext.tree.View TreeView}.
 * 
 * A specialized instance of {@link Ext.dd.DragZone DragZone} and {@link Ext.dd.DropZone 
 * DropZone} are attached to the tree view.  The DropZone will participate in drops 
 * from DragZones having the same {@link #ddGroup} including drops from within the same 
 * tree.
 * 
 * During the drop operation a data object is passed to a participating DropZone's drop 
 * handlers.  The drag data object has the following properties:
 *
 * - **copy:** {@link Boolean} <br> The value of {@link #copy}.  Or `true` if {@link #allowCopy}
 * is true **and** the control key was pressed as the drag operation began.
 * 
 * - **view:** {@link Ext.tree.View TreeView} <br> The source tree view from which the 
 * drag originated
 * 
 * - **ddel:** HTMLElement <br> The drag proxy element which moves with the cursor
 * 
 * - **item:** HTMLElement <br> The tree view node upon which the mousedown event was 
 * registered
 * 
 * - **records:** {@link Array} <br> An Array of {@link Ext.data.Model Model}s 
 * representing the selected data being dragged from the source tree view.
 *
 * By adding this plugin to a view, two new events will be fired from the client 
 * tree view as well as its owning Tree: `{@link #beforedrop}` and `{@link #drop}`.
 *
 *     var store = Ext.create('Ext.data.TreeStore', {
 *         root: {
 *             expanded: true,
 *             children: [{
 *                 text: "detention",
 *                 leaf: true
 *             }, {
 *                 text: "homework",
 *                 expanded: true,
 *                 children: [{
 *                     text: "book report",
 *                     leaf: true
 *                 }, {
 *                     text: "algebra",
 *                     leaf: true
 *                 }]
 *             }, {
 *                 text: "buy lottery tickets",
 *                 leaf: true
 *             }]
 *         }
 *     });
 *     
 *     Ext.create('Ext.tree.Panel', {
 *         title: 'Simple Tree',
 *         width: 200,
 *         height: 200,
 *         store: store,
 *         rootVisible: false,
 *         renderTo: document.body,
 *         viewConfig: {
 *             plugins: {
 *                 treeviewdragdrop: {
 *                     dragText: 'Drag and drop to reorganize'
 *                 }
 *             }
 *         }
 *     });
 */
Ext.define('Ext.tree.plugin.TreeViewDragDrop', {
    extend: 'Ext.plugin.Abstract',
    alias: 'plugin.treeviewdragdrop',
    uses: [
        'Ext.tree.ViewDragZone',
        'Ext.tree.ViewDropZone'
    ],
    /**
     * @event beforedrop
     * **This event is fired through the {@link Ext.tree.View TreeView} and its owning 
     * {@link Ext.tree.Panel Tree}. You can add listeners to the tree or tree {@link 
     * Ext.tree.Panel#viewConfig view config} object**
     *
     * Fired when a drop gesture has been triggered by a mouseup event in a valid drop 
     * position in the tree view.
     * 
     * Returning `false` to this event signals that the drop gesture was invalid and 
     * animates the drag proxy back to the point from which the drag began.
     * 
     * The dropHandlers parameter can be used to defer the processing of this event. For 
     * example, you can force the handler to wait for the result of a message box 
     * confirmation or an asynchronous server call (_see the details of the dropHandlers 
     * property for more information_).
     *  
     *     tree.on('beforedrop', function(node, data, overModel, dropPosition, dropHandlers) {
     *         // Defer the handling
     *         dropHandlers.wait = true;
     *         Ext.MessageBox.confirm('Drop', 'Are you sure', function(btn){
     *             if (btn === 'yes') {
     *                 dropHandlers.processDrop();
     *             } else {
     *                 dropHandlers.cancelDrop();
     *             }
     *         });
     *     });
     * 
     * Any other return value continues with the data transfer operation unless the wait 
     * property is set.
     *
     * @param {HTMLElement} node The {@link Ext.tree.View tree view} node **if any** over 
     * which the cursor was positioned.
     *
     * @param {Object} data The data object gathered at mousedown time by the 
     * cooperating {@link Ext.dd.DragZone DragZone}'s {@link Ext.dd.DragZone#getDragData 
     * getDragData} method.  It contains the following properties:
     * @param {Boolean} data.copy The value of {@link #copy}.  Or `true` if 
     * {@link #allowCopy} is true **and** the control key was pressed as the drag 
     * operation began.
     * @param {Ext.tree.View} data.view The source tree view from which the drag 
     * originated
     * @param {HTMLElement} data.ddel The drag proxy element which moves with the cursor
     * @param {HTMLElement} data.item The tree view node upon which the mousedown event 
     * was registered
     * @param {Ext.data.TreeModel[]} data.records An Array of Models representing the 
     * selected data being dragged from the source tree view
     *
     * @param {Ext.data.TreeModel} overModel The Model over which the drop gesture took place
     *
     * @param {String} dropPosition `"before"` or `"after"` depending on whether the 
     * cursor is above or below the mid-line of the node.
     *
     * @param {Object} dropHandlers
     * This parameter allows the developer to control when the drop action takes place. 
     * It is useful if any asynchronous processing needs to be completed before 
     * performing the drop. This object has the following properties:
     * 
     * @param {Boolean} dropHandlers.wait Indicates whether the drop should be deferred. 
     * Set this property to true to defer the drop.
     * @param {Function} dropHandlers.processDrop A function to be called to complete 
     * the drop operation.
     * @param {Function} dropHandlers.cancelDrop A function to be called to cancel the 
     * drop operation.
     */
    /**
     * @event drop
     * **This event is fired through the {@link Ext.tree.View TreeView} and its owning 
     * {@link Ext.tree.Panel Tree}. You can add listeners to the tree or tree {@link 
     * Ext.tree.Panel#viewConfig view config} object**
     * 
     * Fired when a drop operation has been completed and the data has been moved or 
     * copied.
     *
     * @param {HTMLElement} node The {@link Ext.tree.View tree view} node **if any** over 
     * which the cursor was positioned.
     *
     * @param {Object} data The data object gathered at mousedown time by the 
     * cooperating {@link Ext.dd.DragZone DragZone}'s {@link Ext.dd.DragZone#getDragData 
     * getDragData} method.  It contains the following properties:
     * @param {Boolean} data.copy The value of {@link #copy}.  Or `true` if 
     * {@link #allowCopy} is true **and** the control key was pressed as the drag 
     * operation began.
     * @param {Ext.tree.View} data.view The source tree view from which the drag 
     * originated
     * @param {HTMLElement} data.ddel The drag proxy element which moves with the cursor
     * @param {HTMLElement} data.item The tree view node upon which the mousedown event 
     * was registered
     * @param {Ext.data.TreeModel[]} data.records An Array of Models representing the 
     * selected data being dragged from the source tree view
     *
     * @param {Ext.data.TreeModel} overModel The Model over which the drop gesture took 
     * place.
     *
     * @param {String} dropPosition `"before"` or `"after"` depending on whether the 
     * cursor is above or below the mid-line of the node.
     */
    /**
     * @cfg {Boolean} [copy=false]
     * Set as `true` to copy the records from the source grid to the destination drop 
     * grid.  Otherwise, dragged records will be moved.
     * 
     * **Note:** This only applies to records dragged between two different grids with 
     * unique stores.
     * 
     * See {@link #allowCopy} to allow only control-drag operations to copy records.
     */
    /**
     * @cfg {Boolean} [allowCopy=false]
     * Set as `true` to allow the user to hold down the control key at the start of the 
     * drag operation and copy the dragged records between grids.  Otherwise, dragged 
     * records will be moved.
     * 
     * **Note:** This only applies to records dragged between two different grids with 
     * unique stores.
     * 
     * See {@link #copy} to enable the copying of all dragged records.
     */
    /**
     * @cfg {String} dragText
     * The text to show while dragging.
     *
     * Two placeholders can be used in the text:
     *
     * - `{0}` The number of selected items.
     * - `{1}` 's' when more than 1 items (only useful for English).
     * 
     * **NOTE:** The node's {@link Ext.tree.Panel#cfg-displayField text} will be shown 
     * when a single node is dragged unless `dragText` is a simple text string.
     * @locale
     */
    dragText: '{0} selected node{1}',
    /**
     * @cfg {Boolean} allowParentInserts
     * Allow inserting a dragged node between an expanded parent node and its first child
     * that will become a sibling of the parent when dropped.
     */
    allowParentInserts: false,
    /**
     * @cfg {Boolean} allowContainerDrops
     * True if drops on the tree container (outside of a specific tree node) are allowed.
     */
    allowContainerDrops: false,
    /**
     * @cfg {Boolean} appendOnly
     * True if the tree should only allow append drops (use for trees which are sorted).
     */
    appendOnly: false,
    /**
     * @cfg {String} [ddGroup=TreeDD]
     * A named drag drop group to which this object belongs. If a group is specified, then both
     * the DragZones and DropZone used by this plugin will only interact with other drag drop
     * objects in the same group.
     */
    ddGroup: "TreeDD",
    /**
     * True to register this container with the Scrollmanager for auto scrolling during
     * drag operations. A {@link Ext.dd.ScrollManager} configuration may also be passed.
     * @cfg {Object/Boolean} containerScroll
     */
    containerScroll: false,
    /**
     * @cfg {String} [dragGroup]
     * The ddGroup to which the {@link #property-dragZone DragZone} will belong.
     *
     * This defines which other DropZones the DragZone will interact with. Drag/DropZones
     * only interact with other Drag/DropZones which are members of the same ddGroup.
     */
    /**
     * @cfg {String} [dropGroup]
     * The ddGroup to which the {@link #property-dropZone DropZone} will belong.
     *
     * This defines which other DragZones the DropZone will interact with. Drag/DropZones
     * only interact with other Drag/DropZones which are members of the same {@link #ddGroup}.
     */
    /**
     * @cfg {Boolean} [sortOnDrop=false]
     * Configure as `true` to sort the target node into the current tree sort order after
     * the dropped node is added.
     */
    /**
     * @cfg {Number} [expandDelay=1000]
     * The delay in milliseconds to wait before expanding a target tree node while dragging
     * a droppable node over the target.
     */
    expandDelay: 1000,
    /**
     * @cfg {Boolean} enableDrop
     * Set to `false` to disallow the View from accepting drop gestures.
     */
    enableDrop: true,
    /**
     * @cfg {Boolean} enableDrag
     * Set to `false` to disallow dragging items from the View.
     */
    enableDrag: true,
    /**
     * @cfg {String} [nodeHighlightColor=c3daf9]
     * The color to use when visually highlighting the dragged or dropped node (default value
     * is light blue). The color must be a 6 digit hex value, without a preceding '#'.
     * See also {@link #nodeHighlightOnDrop} and
     * {@link #nodeHighlightOnRepair}.
     */
    nodeHighlightColor: 'c3daf9',
    /**
     * @cfg {Boolean} nodeHighlightOnDrop
     * Whether or not to highlight any nodes after they are
     * successfully dropped on their target. Defaults to the value of `Ext.enableFx`.
     * See also {@link #nodeHighlightColor} and {@link #nodeHighlightOnRepair}.
     */
    nodeHighlightOnDrop: Ext.enableFx,
    /**
     * @cfg {Boolean} nodeHighlightOnRepair
     * Whether or not to highlight any nodes after they are
     * repaired from an unsuccessful drag/drop. Defaults to the value of `Ext.enableFx`.
     * See also {@link #nodeHighlightColor} and {@link #nodeHighlightOnDrop}.
     */
    /**
     * @cfg {String} [displayField=text]
     * The name of the model field that is used to display the text for the nodes
     */
    displayField: 'text',
    /**
     * @cfg {Object} [dragZone]
     * A config object to apply to the creation of the {@link #property-dragZone DragZone}
     * which handles for drag start gestures.
     *
     * Template methods of the DragZone may be overridden using this config.
     */
    /**
     * @cfg {Object} [dropZone]
     * A config object to apply to the creation of the {@link #property-dropZone DropZone}
     * which handles mouseover and drop gestures.
     *
     * Template methods of the DropZone may be overridden using this config.
     */
    /**
     * @property {Ext.view.DragZone} dragZone
     * An {@link Ext.view.DragZone DragZone} which handles mousedown and dragging of records
     * from the grid.
     */
    /**
     * @property {Ext.grid.ViewDropZone} dropZone
     * An {@link Ext.grid.ViewDropZone DropZone} which handles mouseover and dropping records
     * in any grid which shares the same {@link #dropGroup}.
     */
    init: function(view) {
        Ext.applyIf(view, {
            copy: this.copy,
            allowCopy: this.allowCopy
        });
        view.on('render', this.onViewRender, this, {
            single: true
        });
    },
    destroy: function() {
        var me = this;
        me.dragZone = me.dropZone = Ext.destroy(me.dragZone, me.dropZone);
        me.callParent();
    },
    onViewRender: function(view) {
        var me = this,
            ownerGrid = view.ownerCt.ownerGrid || view.ownerCt,
            scrollEl;
        ownerGrid.relayEvents(view, [
            'beforedrop',
            'drop'
        ]);
        if (me.enableDrag) {
            if (me.containerScroll) {
                scrollEl = view.getEl();
            }
            me.dragZone = new Ext.tree.ViewDragZone(Ext.apply({
                view: view,
                ddGroup: me.dragGroup || me.ddGroup,
                dragText: me.dragText,
                displayField: me.displayField,
                repairHighlightColor: me.nodeHighlightColor,
                repairHighlight: me.nodeHighlightOnRepair,
                scrollEl: scrollEl
            }, me.dragZone));
        }
        if (me.enableDrop) {
            me.dropZone = new Ext.tree.ViewDropZone(Ext.apply({
                view: view,
                ddGroup: me.dropGroup || me.ddGroup,
                allowContainerDrops: me.allowContainerDrops,
                appendOnly: me.appendOnly,
                allowParentInserts: me.allowParentInserts,
                expandDelay: me.expandDelay,
                dropHighlightColor: me.nodeHighlightColor,
                dropHighlight: me.nodeHighlightOnDrop,
                sortOnDrop: me.sortOnDrop,
                containerScroll: me.containerScroll
            }, me.dropZone));
        }
    }
}, function() {
    var proto = this.prototype;
    proto.nodeHighlightOnDrop = proto.nodeHighlightOnRepair = Ext.enableFx;
});

/**
 * This component provides a grid holding selected items from a second store of potential
 * members. The `store` of this component represents the selected items. The `searchStore`
 * represents the potentially selected items.
 *
 * The default view defined by this class is intended to be easily replaced by deriving a
 * new class and overriding the appropriate methods. For example, the following is a very
 * different view that uses a date range and a data view:
 *
 *      Ext.define('App.view.DateBoundSearch', {
 *          extend: 'Ext.view.MultiSelectorSearch',
 *
 *          makeDockedItems: function () {
 *              return {
 *                  xtype: 'toolbar',
 *                  items: [{
 *                      xtype: 'datefield',
 *                      emptyText: 'Start date...',
 *                      flex: 1
 *                  },{
 *                      xtype: 'datefield',
 *                      emptyText: 'End date...',
 *                      flex: 1
 *                  }]
 *              };
 *          },
 *
 *          makeItems: function () {
 *              return [{
 *                  xtype: 'dataview',
 *                  itemSelector: '.search-item',
 *                  selModel: 'rowselection',
 *                  store: this.store,
 *                  scrollable: true,
 *                  tpl:
 *                      '<tpl for=".">' +
 *                          '<div class="search-item">' +
 *                              '<img src="{icon}">' +
 *                              '<div>{name}</div>' +
 *                          '</div>' +
 *                      '</tpl>'
 *              }];
 *          },
 *
 *          getSearchStore: function () {
 *              return this.items.getAt(0).getStore();
 *          },
 *
 *          selectRecords: function (records) {
 *              var view = this.items.getAt(0);
 *              return view.getSelectionModel().select(records);
 *          }
 *      });
 *
 * **Important**: This class assumes there are two components with specific `reference`
 * names assigned to them. These are `"searchField"` and `"searchGrid"`. These components
 * are produced by the `makeDockedItems` and `makeItems` method, respectively. When
 * overriding these it is important to remember to place these `reference` values on the
 * appropriate components.
 */
Ext.define('Ext.view.MultiSelectorSearch', {
    extend: 'Ext.panel.Panel',
    xtype: 'multiselector-search',
    /**
     * @cfg layout
     * @inheritdoc
     */
    layout: 'fit',
    /**
     * @cfg floating
     * @inheritdoc
     */
    floating: true,
    /**
     * @cfg alignOnScroll
     * @inheritdoc
     */
    alignOnScroll: false,
    /**
     * @cfg minWidth
     * @inheritdoc
     */
    minWidth: 200,
    /**
     * @cfg minHeight
     * @inheritdoc
     */
    minHeight: 200,
    /**
     * @cfg border
     * @inheritdoc
     */
    border: true,
    /**
     * @cfg keyMap
     * @inheritdoc
     */
    keyMap: {
        scope: 'this',
        ESC: 'hide'
    },
    platformConfig: {
        desktop: {
            resizable: true
        },
        'tablet && rtl': {
            resizable: {
                handles: 'sw'
            }
        },
        'tablet && !rtl': {
            resizable: {
                handles: 'se'
            }
        }
    },
    /**
     * @cfg defaultListenerScope
     * @inheritdoc
     */
    defaultListenerScope: true,
    /**
     * @cfg referenceHolder
     * @inheritdoc
     */
    referenceHolder: true,
    /**
     * @cfg {String} field
     * A field from your grid's store that will be used for filtering your search results.
     */
    /**
     * @cfg store
     * @inheritdoc Ext.panel.Table#cfg-store
     */
    /**
     * @cfg {String} searchText
     * This text is displayed as the "emptyText" of the search `textfield`.
     */
    searchText: 'Search...',
    initComponent: function() {
        var me = this,
            owner = me.owner,
            items = me.makeItems(),
            i, item, records, store;
        me.dockedItems = me.makeDockedItems();
        me.items = items;
        store = Ext.data.StoreManager.lookup(me.store);
        for (i = items.length; i--; ) {
            if ((item = items[i]).xtype === 'grid') {
                item.store = store;
                item.isSearchGrid = true;
                item.selModel = item.selModel || {
                    type: 'checkboxmodel',
                    pruneRemoved: false,
                    listeners: {
                        selectionchange: 'onSelectionChange'
                    }
                };
                Ext.merge(item, me.grid);
                if (!item.columns) {
                    item.hideHeaders = true;
                    item.columns = [
                        {
                            flex: 1,
                            dataIndex: me.field
                        }
                    ];
                }
                break;
            }
        }
        me.callParent();
        records = me.getOwnerStore().getRange();
        if (!owner.convertSelectionRecord.$nullFn) {
            for (i = records.length; i--; ) {
                records[i] = owner.convertSelectionRecord(records[i]);
            }
        }
        if (store.isLoading() || (store.loadCount === 0 && !store.getCount())) {
            // If it is NOT a preloaded store, then unless a Session is being used,
            // The newly loaded records will NOT match any in the ownerStore.
            // So we must match them by ID in order to select the same dataset.
            store.on('load', function() {
                if (!me.destroyed) {
                    me.selectRecords(records);
                }
            }, null, {
                single: true
            });
        } else {
            me.selectRecords(records);
        }
    },
    getOwnerStore: function() {
        return this.owner.getStore();
    },
    afterShow: function() {
        var searchField;
        this.callParent(arguments);
        // Do not focus if this was invoked by a touch gesture
        if (!this.invocationEvent || this.invocationEvent.pointerType !== 'touch') {
            searchField = this.lookupReference('searchField');
            if (searchField) {
                searchField.focus();
            }
        }
        this.invocationEvent = null;
    },
    /**
     * Returns the store that holds search results. By default this comes from the
     * "search grid". If this aspect of the view is changed sufficiently so that the
     * search grid cannot be found, this method should be overridden to return the proper
     * store.
     * @return {Ext.data.Store}
     */
    getSearchStore: function() {
        var searchGrid = this.lookupReference('searchGrid');
        return searchGrid.getStore();
    },
    makeDockedItems: function() {
        return [
            {
                xtype: 'textfield',
                reference: 'searchField',
                dock: 'top',
                hideFieldLabel: true,
                emptyText: this.searchText,
                cls: Ext.baseCSSPrefix + 'multiselector-search-input',
                triggers: {
                    clear: {
                        cls: Ext.baseCSSPrefix + 'form-clear-trigger',
                        handler: 'onClearSearch',
                        hidden: true
                    }
                },
                listeners: {
                    specialKey: 'onSpecialKey',
                    change: {
                        fn: 'onSearchChange',
                        buffer: 300
                    }
                }
            }
        ];
    },
    onSpecialKey: function(field, event) {
        if (event.getKey() === event.TAB && event.shiftKey) {
            event.preventDefault();
            this.owner.searchTool.focus();
        }
    },
    makeItems: function() {
        return [
            {
                xtype: 'grid',
                reference: 'searchGrid',
                trailingBufferZone: 2,
                leadingBufferZone: 2,
                viewConfig: {
                    deferEmptyText: false,
                    emptyText: 'No results.'
                }
            }
        ];
    },
    getMatchingRecords: function(records) {
        var searchGrid = this.lookupReference('searchGrid'),
            store = searchGrid.getStore(),
            selections = [],
            i, record, len;
        records = Ext.isArray(records) ? records : [
            records
        ];
        for (i = 0 , len = records.length; i < len; i++) {
            record = store.getById(records[i].getId());
            if (record) {
                selections.push(record);
            }
        }
        return selections;
    },
    selectRecords: function(records) {
        var searchGrid = this.lookupReference('searchGrid');
        // match up passed records to the records in the search store so that the right
        // internal ids are used
        records = this.getMatchingRecords(records);
        return searchGrid.getSelectionModel().select(records);
    },
    deselectRecords: function(records) {
        var searchGrid = this.lookupReference('searchGrid');
        // match up passed records to the records in the search store so that the right
        // internal ids are used
        records = this.getMatchingRecords(records);
        return searchGrid.getSelectionModel().deselect(records);
    },
    search: function(text) {
        var me = this,
            filter = me.searchFilter,
            filters = me.getSearchStore().getFilters();
        if (text) {
            filters.beginUpdate();
            if (filter) {
                filter.setValue(text);
            } else {
                me.searchFilter = filter = new Ext.util.Filter({
                    id: 'search',
                    property: me.field,
                    value: text
                });
            }
            filters.add(filter);
            filters.endUpdate();
        } else if (filter) {
            filters.remove(filter);
        }
    },
    privates: {
        onClearSearch: function() {
            var searchField = this.lookupReference('searchField');
            searchField.setValue(null);
            searchField.focus();
        },
        onSearchChange: function(searchField) {
            var value = searchField.getValue(),
                trigger = searchField.getTrigger('clear');
            trigger.setHidden(!value);
            this.search(value);
        },
        onSelectionChange: function(selModel, selection) {
            var owner = this.owner,
                store = owner.getStore(),
                data = store.data,
                remove = 0,
                map = {},
                add, i, id, record;
            for (i = selection.length; i--; ) {
                record = selection[i];
                id = record.id;
                map[id] = record;
                if (!data.containsKey(id)) {
                    (add || (add = [])).push(owner.convertSearchRecord(record));
                }
            }
            for (i = data.length; i--; ) {
                record = data.getAt(i);
                if (!map[record.id]) {
                    (remove || (remove = [])).push(record);
                }
            }
            if (add || remove) {
                data.splice(data.length, remove, add);
            }
        }
    }
});

/**
 * This component provides a grid holding selected items from a second store of potential
 * members. The `store` of this component represents the selected items. The "search store"
 * represents the potentially selected items.
 *
 * While this component is a grid and so you can configure `columns`, it is best to leave
 * that to this class in its `initComponent` method. That allows this class to create the
 * extra column that allows the user to remove rows. Instead use `{@link #fieldName}` and
 * `{@link #fieldTitle}` to configure the primary column's `dataIndex` and column `text`,
 * respectively.
 *
 * @since 5.0.0
 */
Ext.define('Ext.view.MultiSelector', {
    extend: 'Ext.grid.Panel',
    xtype: 'multiselector',
    config: {
        /**
         * @cfg {Object} search
         * This object configures the search popup component. By default this contains the
         * `xtype` for a `Ext.view.MultiSelectorSearch` component and specifies `autoLoad`
         * for its `store`.
         */
        search: {
            xtype: 'multiselector-search',
            width: 200,
            height: 200,
            store: {
                autoLoad: true
            }
        }
    },
    /**
     * @cfg {String} [fieldName="name"]
     * The name of the data field to display in the primary column of the grid.
     * @since 5.0.0
     */
    fieldName: 'name',
    /**
     * @cfg {String} [fieldTitle]
     * The text to display in the column header for the primary column of the grid.
     * @since 5.0.0
     */
    fieldTitle: null,
    /**
     * @cfg {String} removeRowText
     * The text to display in the "remove this row" column. By default this is a Unicode
     * "X" looking glyph.
     * @since 5.0.0
     */
    removeRowText: '✖',
    /**
     * @cfg {String} removeRowTip
     * The tooltip to display when the user hovers over the remove cell.
     * @since 5.0.0
     */
    removeRowTip: 'Remove this item',
    emptyText: 'Nothing selected',
    /**
     * @cfg {String} addToolText
     * The tooltip to display when the user hovers over the "+" tool in the panel header.
     * @since 5.0.0
     */
    addToolText: 'Search for items to add',
    initComponent: function() {
        var me = this,
            emptyText = me.emptyText,
            store = me.getStore(),
            search = me.getSearch(),
            fieldTitle = me.fieldTitle,
            searchStore, model;
        if (!search) {
            Ext.raise('The search configuration is required for the multi selector');
        }
        searchStore = search.store;
        if (searchStore.isStore) {
            model = searchStore.getModel();
        } else {
            model = searchStore.model;
        }
        if (!store) {
            me.store = {
                model: model
            };
        }
        if (emptyText && !me.viewConfig) {
            me.viewConfig = {
                deferEmptyText: false,
                emptyText: emptyText
            };
        }
        if (!me.columns) {
            me.hideHeaders = !fieldTitle;
            me.columns = [
                {
                    text: fieldTitle,
                    dataIndex: me.fieldName,
                    flex: 1
                },
                me.makeRemoveRowColumn()
            ];
        }
        me.callParent();
    },
    addTools: function() {
        var me = this;
        me.addTool({
            type: 'plus',
            tooltip: me.addToolText,
            callback: 'onShowSearch',
            scope: me
        });
        me.searchTool = me.tools[me.tools.length - 1];
    },
    convertSearchRecord: Ext.identityFn,
    convertSelectionRecord: Ext.identityFn,
    makeRemoveRowColumn: function() {
        var me = this;
        return {
            width: 32,
            align: 'center',
            menuDisabled: true,
            tdCls: Ext.baseCSSPrefix + 'multiselector-remove',
            processEvent: me.processRowEvent.bind(me),
            renderer: me.renderRemoveRow,
            updater: Ext.emptyFn,
            scope: me
        };
    },
    processRowEvent: function(type, view, cell, recordIndex, cellIndex, e, record, row) {
        var body = Ext.getBody();
        if (e.type === 'click' || (e.type === 'keydown' && (e.keyCode === e.SPACE || e.keyCode === e.ENTER))) {
            // Deleting the focused row will momentarily focusLeave
            // That would dismiss the popup, so disable that.
            body.suspendFocusEvents();
            this.store.remove(record);
            body.resumeFocusEvents();
            if (this.searchPopup) {
                this.searchPopup.deselectRecords(record);
            }
        }
    },
    renderRemoveRow: function() {
        return '<span data-qtip="' + this.removeRowTip + '" role="button" tabIndex="0">' + this.removeRowText + '</span>';
    },
    onFocusLeave: function(e) {
        this.onDismissSearch();
        this.callParent([
            e
        ]);
    },
    afterComponentLayout: function(width, height, prevWidth, prevHeight) {
        var me = this,
            popup = me.searchPopup;
        me.callParent([
            width,
            height,
            prevWidth,
            prevHeight
        ]);
        if (popup && popup.isVisible()) {
            popup.showBy(me, me.popupAlign);
        }
    },
    privates: {
        popupAlign: 'tl-tr?',
        onGlobalScroll: function(scroller) {
            // Collapse if the scroll is anywhere but inside this selector or the popup
            if (!this.owns(scroller.getElement())) {
                this.onDismissSearch();
            }
        },
        onDismissSearch: function(e) {
            var searchPopup = this.searchPopup;
            if (searchPopup && (!e || !(searchPopup.owns(e.getTarget()) || this.owns(e.getTarget())))) {
                this.scrollListeners.destroy();
                this.touchListeners.destroy();
                searchPopup.hide();
            }
        },
        onShowSearch: function(panel, tool, event) {
            var me = this,
                searchPopup = me.searchPopup,
                store = me.getStore();
            if (!searchPopup) {
                searchPopup = Ext.merge({
                    owner: me,
                    field: me.fieldName,
                    floating: true,
                    alignOnScroll: false
                }, me.getSearch());
                me.searchPopup = searchPopup = me.add(searchPopup);
                // If we were configured with records prior to the UI requesting the popup,
                // ensure that the records are selected in the popup.
                if (store.getCount()) {
                    searchPopup.selectRecords(store.getRange());
                }
            }
            searchPopup.invocationEvent = event;
            searchPopup.showBy(me, me.popupAlign);
            // It only autofocuses its defaultFocus target if it was hidden.
            // If they're reactivating the show tool, they'll expect to focus the search.
            if (!event || event.pointerType !== 'touch') {
                searchPopup.lookupReference('searchField').focus();
            }
            me.scrollListeners = Ext.on({
                scroll: 'onGlobalScroll',
                scope: me,
                destroyable: true
            });
            // Dismiss on touch outside this component tree.
            // Because touch platforms do not focus document.body on touch
            // so no focusleave would occur to trigger a collapse.
            me.touchListeners = Ext.getDoc().on({
                // Do not translate on non-touch platforms.
                // mousedown will blur the field.
                translate: false,
                touchstart: me.onDismissSearch,
                scope: me,
                delegated: false,
                destroyable: true
            });
        }
    }
});

/*
 * This class is a derived work from:
 *
 *	Notification extension for Ext JS 4.0.2+
 *	Version: 2.1.3
 *
 *	Copyright (c) 2011 Eirik Lorentsen (http://www.eirik.net/)
 *
 *	Follow project on GitHub: https://github.com/EirikLorentsen/Ext.ux.window.Notification
 *
 *	Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 *	and GPL (http://opensource.org/licenses/GPL-3.0) licenses.
 */
/**
 * This class provides for lightweight, auto-dismissing pop-up notifications called "toasts".
 * At the base level, you can display a toast message by calling `Ext.toast` like so:
 *
 *      Ext.toast('Data saved');
 *
 * This will result in a toast message, which displays in the default location at the top
 * of your viewport.
 *
 * You may expand upon this simple example with the following parameters: 
 *
 *      Ext.toast(message, title, align, iconCls);
 *
 * For example, the following toast will appear top-middle in your viewport.  It will display 
 * the 'Data Saved' message with a title of 'Title'  
 *
 *      Ext.toast('Data Saved', 'Title', 't')
 *
 * It should be noted that the toast's width is determined by the message's width. 
 * If you need to set a specific width, or any of the other available configurations for your toast,
 * you can create the toast object as seen below:
 *
 *      Ext.toast({
 *          html: 'Data Saved',
 *          title: 'My Title',
 *          width: 200,
 *          align: 't'
 *      });
 *
 * This component is derived from the excellent work of a Sencha community member, Eirik
 * Lorentsen.
 */
Ext.define('Ext.window.Toast', {
    extend: 'Ext.window.Window',
    xtype: 'toast',
    isToast: true,
    /**
     * @cfg cls
     * @inheritdoc
     */
    cls: Ext.baseCSSPrefix + 'toast',
    /**
     * @cfg bodyPadding
     * @inheritdoc
     */
    bodyPadding: 10,
    /**
     * @cfg {Boolean} autoClose
     * This config ensures that the Toast is closed automatically after a certain amount of time.
     * If this is set to `false`, closing the Toast will have to be handled some other way
     * (e.g., Setting `closable: true`).
     */
    autoClose: true,
    /**
     * @cfg plain
     * @inheritdoc
     */
    plain: false,
    /**
     * @cfg draggable
     * @inheritdoc
     */
    draggable: false,
    /**
     * @cfg resizable
     * @inheritdoc
     */
    resizable: false,
    /**
     * @cfg shadow
     * @inheritdoc
     */
    shadow: false,
    focus: Ext.emptyFn,
    /**
     * @cfg {String/Ext.Component} anchor
     * The component or the `id` of the component to which the `toast` will be anchored.
     * The default behavior is to anchor a `toast` to the document body (no component).
     */
    anchor: null,
    /**
     * @cfg {Boolean} useXAxis
     * Directs the toast message to animate on the x-axis (if `true`) or y-axis (if `false`).
     * This value defaults to a value based on the `align` config.
     */
    useXAxis: false,
    /**
     * @cfg {"br"/"bl"/"tr"/"tl"/"t"/"l"/"b"/"r"} align
     * Specifies the basic alignment of the toast message with its {@link #anchor}. This 
     * controls many aspects of the toast animation as well. For fine grain control of 
     * the final placement of the toast and its `anchor` you may set 
     * {@link #anchorAlign} as well.
     * 
     * Possible values:
     * 
     *  - br - bottom-right
     *  - bl - bottom-left
     *  - tr - top-right
     *  - tl - top-left
     *  - t  - top
     *  - l  - left
     *  - b  - bottom
     *  - r  - right
     */
    align: 't',
    /**
     * @cfg alwaysOnTop
     * @inheritdoc
     */
    alwaysOnTop: true,
    /**
     * @cfg {String} anchorAlign
     * This string is a full specification of how to position the toast with respect to
     * its `anchor`. This is set to a reasonable value based on `align` but the `align`
     * also sets defaults for various other properties. This config controls only the
     * final position of the toast.
     */
    /**
     * @cfg {Boolean} [animate=true]
     * Set this to `false` to make toasts appear and disappear without animation.
     * This is helpful with applications' unit and integration testing.
     */
    // Pixels between each notification
    /**
     * @cfg {Number} spacing
     * The number of pixels between each Toast notification.
     */
    spacing: 6,
    // TODO There should be a way to control from and to positions for the introduction.
    // TODO The align/anchorAlign configs don't actually work as expected.
    // Pixels from the anchor's borders to start the first notification
    paddingX: 30,
    paddingY: 10,
    /**
     * @cfg {String} slideInAnimation
     * The animation used for the Toast to slide in.
     */
    slideInAnimation: 'easeIn',
    /**
     * @cfg {String} slideBackAnimation
     * The animation used for the Toast to slide back.
     */
    slideBackAnimation: 'bounceOut',
    /**
     * @cfg {Number} slideInDuration
     * The number of milliseconds it takes for a Toast to slide in.
     */
    slideInDuration: 500,
    /**
     * @cfg {Number} slideBackDuration
     * The number of milliseconds it takes for a Toast to slide back.
     */
    slideBackDuration: 500,
    /**
     * @cfg {Number} hideDuration
     * The number of milliseconds it takes for a Toast to hide.
     */
    hideDuration: 500,
    /**
     * @cfg {Number} autoCloseDelay
     * The number of milliseconds a Toast waits before automatically closing.
     */
    autoCloseDelay: 3000,
    /**
     * @cfg {Boolean} stickOnClick
     * This config will prevent the Toast from closing when you click on it. If this is set
     * to `true`, closing the Toast will have to be handled some other way
     * (e.g., Setting `closable: true`).
     */
    stickOnClick: false,
    /**
     * @cfg {Boolean} stickWhileHover
     * This config will prevent the Toast from closing while you're hovered over it.
     */
    stickWhileHover: true,
    /**
     * @cfg {Boolean} closeOnMouseDown
     * This config will prevent the Toast from closing when a user produces a mousedown event.
     */
    closeOnMouseDown: false,
    /**
     * @cfg closable
     * @inheritdoc
     */
    closable: false,
    /**
     * @cfg minHeight
     * @inheritdoc
     */
    minHeight: 1,
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: false,
    // Private. Do not override!
    isHiding: false,
    isFading: false,
    destroyAfterHide: false,
    closeOnMouseOut: false,
    // Caching coordinates to be able to align to final position of siblings being animated
    xPos: 0,
    yPos: 0,
    constructor: function(config) {
        config = config || {};
        if (config.animate === undefined) {
            config.animate = Ext.isBoolean(this.animate) ? this.animate : Ext.enableFx;
        }
        this.enableAnimations = config.animate;
        delete config.animate;
        this.callParent([
            config
        ]);
    },
    initComponent: function() {
        var me = this;
        // Close tool is not really helpful to sight impaired users
        // when Toast window is set to auto-close on timeout; however
        // if it's forced, respect that.
        if (me.autoClose && me.closable == null) {
            me.closable = false;
        }
        me.updateAlignment(me.align);
        me.setAnchor(me.anchor);
        me.callParent();
    },
    onRender: function() {
        var me = this;
        me.callParent(arguments);
        me.el.hover(me.onMouseEnter, me.onMouseLeave, me);
        // Mousedown outside of this, when visible, hides it immediately
        if (me.closeOnMouseDown) {
            Ext.getDoc().on('mousedown', me.onDocumentMousedown, me);
        }
    },
    /*
     * These properties are keyed by "align" and set defaults for various configs.
     */
    alignmentProps: {
        br: {
            paddingFactorX: -1,
            paddingFactorY: -1,
            siblingAlignment: "br-br",
            anchorAlign: "tr-br"
        },
        bl: {
            paddingFactorX: 1,
            paddingFactorY: -1,
            siblingAlignment: "bl-bl",
            anchorAlign: "tl-bl"
        },
        tr: {
            paddingFactorX: -1,
            paddingFactorY: 1,
            siblingAlignment: "tr-tr",
            anchorAlign: "br-tr"
        },
        tl: {
            paddingFactorX: 1,
            paddingFactorY: 1,
            siblingAlignment: "tl-tl",
            anchorAlign: "bl-tl"
        },
        b: {
            paddingFactorX: 0,
            paddingFactorY: -1,
            siblingAlignment: "b-b",
            useXAxis: 0,
            anchorAlign: "t-b"
        },
        t: {
            paddingFactorX: 0,
            paddingFactorY: 1,
            siblingAlignment: "t-t",
            useXAxis: 0,
            anchorAlign: "b-t"
        },
        l: {
            paddingFactorX: 1,
            paddingFactorY: 0,
            siblingAlignment: "l-l",
            useXAxis: 1,
            anchorAlign: "r-l"
        },
        r: {
            paddingFactorX: -1,
            paddingFactorY: 0,
            siblingAlignment: "r-r",
            useXAxis: 1,
            anchorAlign: "l-r"
        },
        /*
         * These properties take priority over the above and applied only when useXAxis
         * is set to true. Again these are keyed by "align".
         */
        x: {
            br: {
                anchorAlign: "bl-br"
            },
            bl: {
                anchorAlign: "br-bl"
            },
            tr: {
                anchorAlign: "tl-tr"
            },
            tl: {
                anchorAlign: "tr-tl"
            }
        }
    },
    updateAlignment: function(align) {
        var me = this,
            alignmentProps = me.alignmentProps,
            props = alignmentProps[align],
            xprops = alignmentProps.x[align];
        if (xprops && me.useXAxis) {
            Ext.applyIf(me, xprops);
        }
        Ext.applyIf(me, props);
    },
    getXposAlignedToAnchor: function() {
        var me = this,
            align = me.align,
            anchor = me.anchor,
            anchorEl = anchor && anchor.el,
            el = me.el,
            xPos = 0;
        // Avoid error messages if the anchor does not have a dom element
        if (anchorEl && anchorEl.dom) {
            if (!me.useXAxis) {
                // Element should already be aligned vertically
                xPos = el.getLeft();
            }
            // Using getAnchorXY instead of getTop/getBottom should give a correct placement
            // when document is used as the anchor but is still 0 px high.
            // Before rendering the viewport.
            else if (align === 'br' || align === 'tr' || align === 'r') {
                xPos += anchorEl.getAnchorXY('r')[0];
                xPos -= (el.getWidth() + me.paddingX);
            } else {
                xPos += anchorEl.getAnchorXY('l')[0];
                xPos += me.paddingX;
            }
        }
        return xPos;
    },
    getYposAlignedToAnchor: function() {
        var me = this,
            align = me.align,
            anchor = me.anchor,
            anchorEl = anchor && anchor.el,
            el = me.el,
            yPos = 0;
        // Avoid error messages if the anchor does not have a dom element
        if (anchorEl && anchorEl.dom) {
            if (me.useXAxis) {
                // Element should already be aligned horizontally
                yPos = el.getTop();
            }
            // Using getAnchorXY instead of getTop/getBottom should give a correct placement
            // when document is used as the anchor but is still 0 px high.
            // Before rendering the viewport.
            else if (align === 'br' || align === 'bl' || align === 'b') {
                yPos += anchorEl.getAnchorXY('b')[1];
                yPos -= (el.getHeight() + me.paddingY);
            } else {
                yPos += anchorEl.getAnchorXY('t')[1];
                yPos += me.paddingY;
            }
        }
        return yPos;
    },
    getXposAlignedToSibling: function(sibling) {
        var me = this,
            align = me.align,
            el = me.el,
            xPos;
        if (!me.useXAxis) {
            xPos = el.getLeft();
        } else if (align === 'tl' || align === 'bl' || align === 'l') {
            // Using sibling's width when adding
            xPos = (sibling.xPos + sibling.el.getWidth() + sibling.spacing);
        } else {
            // Using own width when subtracting
            xPos = (sibling.xPos - el.getWidth() - me.spacing);
        }
        return xPos;
    },
    getYposAlignedToSibling: function(sibling) {
        var me = this,
            align = me.align,
            el = me.el,
            yPos;
        if (me.useXAxis) {
            yPos = el.getTop();
        } else if (align === 'tr' || align === 'tl' || align === 't') {
            // Using sibling's width when adding
            yPos = (sibling.yPos + sibling.el.getHeight() + sibling.spacing);
        } else {
            // Using own width when subtracting
            yPos = (sibling.yPos - el.getHeight() - sibling.spacing);
        }
        return yPos;
    },
    getToasts: function() {
        var anchor = this.anchor,
            alignment = this.anchorAlign,
            activeToasts = anchor.activeToasts || (anchor.activeToasts = {});
        return activeToasts[alignment] || (activeToasts[alignment] = []);
    },
    setAnchor: function(anchor) {
        var me = this,
            Toast;
        me.anchor = anchor = ((typeof anchor === 'string') ? Ext.getCmp(anchor) : anchor);
        // If no anchor is provided or found, then the static object is used and the el
        // property pointed to the body document.
        if (!anchor) {
            Toast = Ext.window.Toast;
            me.anchor = Toast.bodyAnchor || (Toast.bodyAnchor = {
                el: Ext.getBody()
            });
        }
    },
    beforeShow: function() {
        var me = this;
        if (me.stickOnClick) {
            me.body.on('click', function() {
                me.cancelAutoClose();
            });
        }
        if (me.autoClose) {
            if (!me.closeTask) {
                me.closeTask = new Ext.util.DelayedTask(me.doAutoClose, me);
            }
        }
        // Shunting offscreen to avoid flicker
        me.el.setX(-10000);
        me.el.setOpacity(1);
    },
    afterShow: function() {
        var me = this,
            el = me.el,
            activeToasts, sibling, length, xy;
        me.callParent(arguments);
        activeToasts = me.getToasts();
        length = activeToasts.length;
        sibling = length && activeToasts[length - 1];
        if (sibling) {
            el.alignTo(sibling.el, me.siblingAlignment, [
                0,
                0
            ]);
            me.xPos = me.getXposAlignedToSibling(sibling);
            me.yPos = me.getYposAlignedToSibling(sibling);
        } else {
            el.alignTo(me.anchor.el, me.anchorAlign, [
                (me.paddingX * me.paddingFactorX),
                (me.paddingY * me.paddingFactorY)
            ], false);
            me.xPos = me.getXposAlignedToAnchor();
            me.yPos = me.getYposAlignedToAnchor();
        }
        Ext.Array.include(activeToasts, me);
        if (me.enableAnimations) {
            // Repeating from coordinates makes sure the windows does not flicker
            // into the center of the viewport during animation
            xy = el.getXY();
            el.animate({
                from: {
                    x: xy[0],
                    y: xy[1]
                },
                to: {
                    x: me.xPos,
                    y: me.yPos,
                    opacity: 1
                },
                easing: me.slideInAnimation,
                duration: me.slideInDuration,
                dynamic: true,
                callback: me.afterPositioned,
                scope: me
            });
        } else {
            me.setLocalXY(me.xPos, me.yPos);
            me.afterPositioned();
        }
    },
    afterPositioned: function() {
        var me = this;
        // This method can be called from afteranimation event being fired
        // during destruction sequence.
        if (!me.destroying && !me.destroyed && me.autoClose) {
            me.closeTask.delay(me.autoCloseDelay);
        }
    },
    onDocumentMousedown: function(e) {
        if (this.isVisible() && !this.owns(e.getTarget())) {
            this.hide();
        }
    },
    slideBack: function() {
        var me = this,
            anchor = me.anchor,
            anchorEl = anchor && anchor.el,
            el = me.el,
            activeToasts = me.getToasts(),
            index = Ext.Array.indexOf(activeToasts, me);
        // Not animating the element if it already started to hide itself
        // or if the anchor is not present in the dom
        if (!me.isHiding && el && el.dom && anchorEl && anchorEl.isVisible()) {
            if (index) {
                me.xPos = me.getXposAlignedToSibling(activeToasts[index - 1]);
                me.yPos = me.getYposAlignedToSibling(activeToasts[index - 1]);
            } else {
                me.xPos = me.getXposAlignedToAnchor();
                me.yPos = me.getYposAlignedToAnchor();
            }
            me.stopAnimation();
            if (me.enableAnimations) {
                el.animate({
                    to: {
                        x: me.xPos,
                        y: me.yPos
                    },
                    easing: me.slideBackAnimation,
                    duration: me.slideBackDuration,
                    dynamic: true
                });
            }
        }
    },
    update: function() {
        var me = this;
        if (me.isVisible()) {
            me.isHiding = true;
            me.hide();
        }
        // TODO offer a way to just update and reposition after layout
        me.callParent(arguments);
        me.show();
    },
    cancelAutoClose: function() {
        var closeTask = this.closeTask;
        if (closeTask) {
            closeTask.cancel();
        }
    },
    doAutoClose: function() {
        var me = this;
        if (!(me.stickWhileHover && me.mouseIsOver)) {
            // Close immediately
            me.close();
        } else {
            // Delayed closing when mouse leaves the component.
            me.closeOnMouseOut = true;
        }
    },
    doDestroy: function() {
        this.removeFromAnchor();
        this.cancelAutoClose();
        this.callParent();
    },
    onMouseEnter: function() {
        this.mouseIsOver = true;
    },
    onMouseLeave: function() {
        var me = this;
        me.mouseIsOver = false;
        if (me.closeOnMouseOut) {
            me.closeOnMouseOut = false;
            me.close();
        }
    },
    removeFromAnchor: function() {
        var me = this,
            activeToasts, index;
        if (me.anchor) {
            activeToasts = me.getToasts();
            index = Ext.Array.indexOf(activeToasts, me);
            if (index !== -1) {
                Ext.Array.erase(activeToasts, index, 1);
                // Slide "down" all activeToasts "above" the hidden one
                for (; index < activeToasts.length; index++) {
                    activeToasts[index].slideBack();
                }
            }
        }
    },
    getFocusEl: Ext.emptyFn,
    hide: function() {
        var me = this,
            el = me.el;
        me.cancelAutoClose();
        if (me.isHiding) {
            if (!me.isFading) {
                me.callParent(arguments);
                me.isHiding = false;
            }
        } else {
            // Must be set right away in case of double clicks on the close button
            me.isHiding = true;
            me.isFading = true;
            me.cancelAutoClose();
            if (el) {
                if (me.enableAnimations && !me.destroying && !me.destroyed) {
                    el.fadeOut({
                        opacity: 0,
                        easing: 'easeIn',
                        duration: me.hideDuration,
                        listeners: {
                            scope: me,
                            afteranimate: function() {
                                var me = this;
                                me.isFading = false;
                                if (!me.destroying && !me.destroyed) {
                                    me.hide(me.animateTarget, me.doClose, me);
                                }
                            }
                        }
                    });
                } else {
                    me.isFading = false;
                    me.hide(me.animateTarget, me.doClose, me);
                }
            }
        }
        return me;
    }
}, function(Toast) {
    Ext.toast = function(message, title, align, iconCls) {
        var config = message,
            toast;
        if (Ext.isString(message)) {
            config = {
                title: title,
                html: message,
                iconCls: iconCls
            };
            if (align) {
                config.align = align;
            }
        }
        toast = new Toast(config);
        toast.show();
        return toast;
    };
});

